ETAPI delete/patch, refactoring

This commit is contained in:
zadam 2022-01-07 19:33:59 +01:00
parent 82b2871a08
commit 9ee1c9f3da
36 changed files with 1304 additions and 11678 deletions

2
.idea/misc.xml generated
View File

@ -3,7 +3,7 @@
<component name="JavaScriptSettings"> <component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" /> <option name="languageLevel" value="ES6" />
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_16" project-jdk-name="openjdk-16" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

View File

@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS "mig_api_tokens"
(
apiTokenId TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
token TEXT NOT NULL,
utcDateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0);
INSERT INTO mig_api_tokens (apiTokenId, name, token, utcDateCreated, isDeleted)
SELECT apiTokenId, 'Trilium Sender', token, utcDateCreated, isDeleted FROM api_tokens;
DROP TABLE api_tokens;
ALTER TABLE mig_api_tokens RENAME TO api_tokens;

View File

@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS "entity_changes" (
CREATE TABLE IF NOT EXISTS "api_tokens" CREATE TABLE IF NOT EXISTS "api_tokens"
( (
apiTokenId TEXT PRIMARY KEY NOT NULL, apiTokenId TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
token TEXT NOT NULL, token TEXT NOT NULL,
utcDateCreated TEXT NOT NULL, utcDateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0); isDeleted INT NOT NULL DEFAULT 0);

11451
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,15 @@ const dateUtils = require('../../services/date_utils.js');
const AbstractEntity = require("./abstract_entity.js"); const AbstractEntity = require("./abstract_entity.js");
/** /**
* ApiToken is an entity representing token used to authenticate against Trilium API from client applications. Currently used only by Trilium Sender. * ApiToken is an entity representing token used to authenticate against Trilium API from client applications.
* Used by:
* - Trilium Sender
* - ETAPI clients
*/ */
class ApiToken extends AbstractEntity { class ApiToken extends AbstractEntity {
static get entityName() { return "api_tokens"; } static get entityName() { return "api_tokens"; }
static get primaryKeyName() { return "apiTokenId"; } static get primaryKeyName() { return "apiTokenId"; }
static get hashedProperties() { return ["apiTokenId", "token", "utcDateCreated"]; } static get hashedProperties() { return ["apiTokenId", "name", "token", "utcDateCreated"]; }
constructor(row) { constructor(row) {
super(); super();
@ -17,6 +20,8 @@ class ApiToken extends AbstractEntity {
/** @type {string} */ /** @type {string} */
this.apiTokenId = row.apiTokenId; this.apiTokenId = row.apiTokenId;
/** @type {string} */ /** @type {string} */
this.name = row.name;
/** @type {string} */
this.token = row.token; this.token = row.token;
/** @type {string} */ /** @type {string} */
this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime(); this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
@ -25,6 +30,7 @@ class ApiToken extends AbstractEntity {
getPojo() { getPojo() {
return { return {
apiTokenId: this.apiTokenId, apiTokenId: this.apiTokenId,
name: this.name,
token: this.token, token: this.token,
utcDateCreated: this.utcDateCreated utcDateCreated: this.utcDateCreated
} }

64
src/etapi/attributes.js Normal file
View File

@ -0,0 +1,64 @@
const becca = require("../becca/becca");
const ru = require("./route_utils");
const mappers = require("./mappers");
const attributeService = require("../services/attributes");
const validators = require("./validators.js");
function register(router) {
ru.route(router, 'get', '/etapi/attributes/:attributeId', (req, res, next) => {
const attribute = ru.getAndCheckAttribute(req.params.attributeId);
res.json(mappers.mapAttributeToPojo(attribute));
});
ru.route(router, 'post' ,'/etapi/attributes', (req, res, next) => {
const params = req.body;
ru.getAndCheckNote(params.noteId);
if (params.type === 'relation') {
ru.getAndCheckNote(params.value);
}
if (params.type !== 'relation' && params.type !== 'label') {
throw new ru.EtapiError(400, ru.GENERIC_CODE, `Only "relation" and "label" are supported attribute types, "${params.type}" given.`);
}
try {
const attr = attributeService.createAttribute(params);
res.json(mappers.mapAttributeToPojo(attr));
}
catch (e) {
throw new ru.EtapiError(400, ru.GENERIC_CODE, e.message);
}
});
const ALLOWED_PROPERTIES_FOR_PATCH = {
'value': validators.isString
};
ru.route(router, 'patch' ,'/etapi/attributes/:attributeId', (req, res, next) => {
const attribute = ru.getAndCheckAttribute(req.params.attributeId);
ru.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
res.json(mappers.mapAttributeToPojo(attribute));
});
ru.route(router, 'delete' ,'/etapi/attributes/:attributeId', (req, res, next) => {
const attribute = becca.getAttribute(req.params.attributeId);
if (!attribute) {
return res.sendStatus(204);
}
attribute.markAsDeleted();
res.sendStatus(204);
});
}
module.exports = {
register
};

78
src/etapi/branches.js Normal file
View File

@ -0,0 +1,78 @@
const becca = require("../becca/becca.js");
const ru = require("./route_utils");
const mappers = require("./mappers");
const Branch = require("../becca/entities/branch");
const noteService = require("../services/notes");
const TaskContext = require("../services/task_context");
const entityChangesService = require("../services/entity_changes");
const validators = require("./validators.js");
function register(router) {
ru.route(router, 'get', '/etapi/branches/:branchId', (req, res, next) => {
const branch = ru.getAndCheckBranch(req.params.branchId);
res.json(mappers.mapBranchToPojo(branch));
});
ru.route(router, 'post' ,'/etapi/branches', (req, res, next) => {
const params = req.body;
ru.getAndCheckNote(params.noteId);
ru.getAndCheckNote(params.parentNoteId);
const existing = becca.getBranchFromChildAndParent(params.noteId, params.parentNoteId);
if (existing) {
existing.notePosition = params.notePosition;
existing.prefix = params.prefix;
existing.save();
return res.json(mappers.mapBranchToPojo(existing));
}
try {
const branch = new Branch(params).save();
res.json(mappers.mapBranchToPojo(branch));
}
catch (e) {
throw new ru.EtapiError(400, ru.GENERIC_CODE, e.message);
}
});
const ALLOWED_PROPERTIES_FOR_PATCH = {
'notePosition': validators.isInteger,
'prefix': validators.isStringOrNull,
'isExpanded': validators.isBoolean
};
ru.route(router, 'patch' ,'/etapi/branches/:branchId', (req, res, next) => {
const branch = ru.getAndCheckBranch(req.params.branchId);
ru.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
res.json(mappers.mapBranchToPojo(branch));
});
ru.route(router, 'delete' ,'/etapi/branches/:branchId', (req, res, next) => {
const branch = becca.getBranch(req.params.branchId);
if (!branch) {
return res.sendStatus(204);
}
noteService.deleteBranch(branch, null, new TaskContext('no-progress-reporting'));
res.sendStatus(204);
});
ru.route(router, 'post' ,'/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => {
ru.getAndCheckNote(req.params.parentNoteId);
entityChangesService.addNoteReorderingEntityChange(req.params.parentNoteId, "etapi");
});
}
module.exports = {
register
};

49
src/etapi/mappers.js Normal file
View File

@ -0,0 +1,49 @@
function mapNoteToPojo(note) {
return {
noteId: note.noteId,
isProtected: note.isProtected,
title: note.title,
type: note.type,
mime: note.mime,
dateCreated: note.dateCreated,
dateModified: note.dateModified,
utcDateCreated: note.utcDateCreated,
utcDateModified: note.utcDateModified,
parentNoteIds: note.getParentNotes().map(p => p.noteId),
childNoteIds: note.getChildNotes().map(ch => ch.noteId),
parentBranchIds: note.getParentBranches().map(p => p.branchId),
childBranchIds: note.getChildBranches().map(ch => ch.branchId),
attributes: note.getAttributes().map(attr => mapAttributeToPojo(attr))
};
}
function mapBranchToPojo(branch) {
return {
branchId: branch.branchId,
noteId: branch.noteId,
parentNoteId: branch.parentNoteId,
prefix: branch.prefix,
notePosition: branch.notePosition,
isExpanded: branch.isExpanded,
utcDateModified: branch.utcDateModified
};
}
function mapAttributeToPojo(attr) {
return {
attributeId: attr.attributeId,
noteId: attr.noteId,
type: attr.type,
name: attr.name,
value: attr.value,
position: attr.position,
isInheritable: attr.isInheritable,
utcDateModified: attr.utcDateModified
};
}
module.exports = {
mapNoteToPojo,
mapBranchToPojo,
mapAttributeToPojo
};

82
src/etapi/notes.js Normal file
View File

@ -0,0 +1,82 @@
const becca = require("../becca/becca");
const utils = require("../services/utils");
const ru = require("./route_utils");
const mappers = require("./mappers");
const noteService = require("../services/notes");
const TaskContext = require("../services/task_context");
const validators = require("./validators");
function register(router) {
ru.route(router, 'get', '/etapi/notes/:noteId', (req, res, next) => {
const note = ru.getAndCheckNote(req.params.noteId);
res.json(mappers.mapNoteToPojo(note));
});
ru.route(router, 'get', '/etapi/notes/:noteId/content', (req, res, next) => {
const note = ru.getAndCheckNote(req.params.noteId);
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', note.mime);
res.send(note.getContent());
});
ru.route(router, 'post' ,'/etapi/create-note', (req, res, next) => {
const params = req.body;
ru.getAndCheckNote(params.parentNoteId);
try {
const resp = noteService.createNewNote(params);
res.json({
note: mappers.mapNoteToPojo(resp.note),
branch: mappers.mapBranchToPojo(resp.branch)
});
}
catch (e) {
return ru.sendError(res, 400, ru.GENERIC_CODE, e.message);
}
});
const ALLOWED_PROPERTIES_FOR_PATCH = {
'title': validators.isString,
'type': validators.isString,
'mime': validators.isString
};
ru.route(router, 'patch' ,'/etapi/notes/:noteId', (req, res, next) => {
const note = ru.getAndCheckNote(req.params.noteId)
if (note.isProtected) {
throw new ru.EtapiError(404, "NOTE_IS_PROTECTED", `Note ${req.params.noteId} is protected and cannot be modified through ETAPI`);
}
ru.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
res.json(mappers.mapNoteToPojo(note));
});
ru.route(router, 'delete' ,'/etapi/notes/:noteId', (req, res, next) => {
const {noteId} = req.params;
const note = becca.getNote(noteId);
if (!note) {
return res.sendStatus(204);
}
noteService.deleteNote(note, null, new TaskContext('no-progress-reporting'));
res.sendStatus(204);
});
}
module.exports = {
register
};

132
src/etapi/route_utils.js Normal file
View File

@ -0,0 +1,132 @@
const cls = require("../services/cls.js");
const sql = require("../services/sql.js");
const log = require("../services/log.js");
const becca = require("../becca/becca.js");
const GENERIC_CODE = "GENERIC";
class EtapiError extends Error {
constructor(statusCode, code, message) {
super();
this.statusCode = statusCode;
this.code = code;
this.message = message;
}
}
function sendError(res, statusCode, code, message) {
return res
.set('Content-Type', 'application/json')
.status(statusCode)
.send(JSON.stringify({
"status": statusCode,
"code": code,
"message": message
}));
}
function checkEtapiAuth(req, res, next) {
if (false) {
sendError(res, 401, "NOT_AUTHENTICATED", "Not authenticated");
}
else {
next();
}
}
function route(router, method, path, routeHandler) {
router[method](path, checkEtapiAuth, (req, res, next) => {
try {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
cls.init(() => {
cls.set('sourceId', "etapi");
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
const cb = () => routeHandler(req, res, next);
return sql.transactional(cb);
});
}
catch (e) {
log.error(`${method} ${path} threw exception ${e.message} with stacktrace: ${e.stack}`);
if (e instanceof EtapiError) {
sendError(res, e.statusCode, e.code, e.message);
}
else {
sendError(res, 500, GENERIC_CODE, e.message);
}
}
});
}
function getAndCheckNote(noteId) {
const note = becca.getNote(noteId);
if (note) {
return note;
}
else {
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found`);
}
}
function getAndCheckBranch(branchId) {
const branch = becca.getBranch(branchId);
if (branch) {
return branch;
}
else {
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found`);
}
}
function getAndCheckAttribute(attributeId) {
const attribute = becca.getAttribute(attributeId);
if (attribute) {
return attribute;
}
else {
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found`);
}
}
function validateAndPatch(entity, props, allowedProperties) {
for (const key of Object.keys(props)) {
if (!(key in allowedProperties)) {
throw new EtapiError(400, "PROPERTY_NOT_ALLOWED_FOR_PATCH", `Property '${key}' is not allowed for PATCH.`);
}
else {
const validator = allowedProperties[key];
const validationResult = validator(props[key]);
if (validationResult) {
throw new EtapiError(400, "PROPERTY_VALIDATION_ERROR", `Validation failed on property '${key}': ${validationResult}`);
}
}
}
// validation passed, let's patch
for (const propName of Object.keys(props)) {
entity[propName] = props[propName];
}
entity.save();
}
module.exports = {
EtapiError,
sendError,
checkEtapiAuth,
route,
GENERIC_CODE,
validateAndPatch,
getAndCheckNote,
getAndCheckBranch,
getAndCheckAttribute,
getNotAllowedPatchPropertyError: (propertyName, allowedProperties) => new EtapiError(400, "PROPERTY_NOT_ALLOWED_FOR_PATCH", `Property '${propertyName}' is not allowed to be patched, allowed properties are ${allowedProperties}.`),
}

View File

@ -0,0 +1,95 @@
openapi: "3.1.0"
info:
version: 1.0.0
title: ETAPI
description: External Trilium API
contact:
name: zadam
email: zadam.apps@gmail.com
url: https://github.com/zadam/trilium
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: http://localhost:37740/etapi
- url: http://localhost:8080/etapi
paths:
/pets/{id}:
get:
description: Returns a user based on a single ID, if the user does not have access to the pet
operationId: find pet by id
parameters:
- name: id
in: path
description: ID of pet to fetch
required: true
schema:
type: integer
format: int64
responses:
'200':
description: pet response
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
description: deletes a single pet based on the ID supplied
operationId: deletePet
parameters:
- name: id
in: path
description: ID of pet to delete
required: true
schema:
type: integer
format: int64
responses:
'204':
description: pet deleted
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Pet:
allOf:
- $ref: '#/components/schemas/NewPet'
- type: object
required:
- id
properties:
id:
type: integer
format: int64
NewPet:
type: object
required:
- name
properties:
name:
type: string
tag:
type: string
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string

View File

@ -0,0 +1,77 @@
const specialNotesService = require("../services/special_notes");
const dateNotesService = require("../services/date_notes");
const ru = require("./route_utils");
const mappers = require("./mappers");
const getDateInvalidError = date => new ru.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`);
const getMonthInvalidError = month => new ru.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`);
const getYearInvalidError = year => new ru.EtapiError(400, "YEAR_INVALID", `Year "${year}" is not valid.`);
function isValidDate(date) {
if (!/[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(date)) {
return false;
}
return !!Date.parse(date);
}
function register(router) {
ru.route(router, 'get', '/etapi/inbox/:date', (req, res, next) => {
const {date} = req.params;
if (!isValidDate(date)) {
throw getDateInvalidError(res, date);
}
const note = specialNotesService.getInboxNote(date);
res.json(mappers.mapNoteToPojo(note));
});
ru.route(router, 'get', '/etapi/date/:date', (req, res, next) => {
const {date} = req.params;
if (!isValidDate(date)) {
throw getDateInvalidError(res, date);
}
const note = dateNotesService.getDateNote(date);
res.json(mappers.mapNoteToPojo(note));
});
ru.route(router, 'get', '/etapi/week/:date', (req, res, next) => {
const {date} = req.params;
if (!isValidDate(date)) {
throw getDateInvalidError(res, date);
}
const note = dateNotesService.getWeekNote(date);
res.json(mappers.mapNoteToPojo(note));
});
ru.route(router, 'get', '/etapi/month/:month', (req, res, next) => {
const {month} = req.params;
if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
throw getMonthInvalidError(res, month);
}
const note = dateNotesService.getMonthNote(month);
res.json(mappers.mapNoteToPojo(note));
});
ru.route(router, 'get', '/etapi/year/:year', (req, res, next) => {
const {year} = req.params;
if (!/[0-9]{4}/.test(year)) {
throw getYearInvalidError(res, year);
}
const note = dateNotesService.getYearNote(year);
res.json(mappers.mapNoteToPojo(note));
});
}
module.exports = {
register
};

30
src/etapi/validators.js Normal file
View File

@ -0,0 +1,30 @@
function isString(obj) {
if (typeof obj !== 'string') {
return `'${obj}' is not a string`;
}
}
function isStringOrNull(obj) {
if (obj) {
return isString(obj);
}
}
function isBoolean(obj) {
if (typeof obj !== 'boolean') {
return `'${obj}' is not a boolean`;
}
}
function isInteger(obj) {
if (!Number.isInteger(obj)) {
return `'${obj}' is not an integer`;
}
}
module.exports = {
isString,
isStringOrNull,
isBoolean,
isInteger
};

View File

@ -81,6 +81,7 @@ async function renderAttributes(attributes, renderIsInheritable) {
const HIDDEN_ATTRIBUTES = [ const HIDDEN_ATTRIBUTES = [
'originalFileName', 'originalFileName',
'fileSize',
'template', 'template',
'cssClass', 'cssClass',
'iconClass', 'iconClass',

View File

@ -3,16 +3,17 @@ const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
const CODE_MIRROR = { const CODE_MIRROR = {
js: [ js: [
"libraries/codemirror/codemirror.js", "libraries/codemirror/codemirror.js",
"libraries/codemirror/addon/mode/loadmode.js", "libraries/codemirror/addon/display/placeholder.js",
"libraries/codemirror/addon/mode/simple.js",
"libraries/codemirror/addon/fold/xml-fold.js",
"libraries/codemirror/addon/edit/matchbrackets.js", "libraries/codemirror/addon/edit/matchbrackets.js",
"libraries/codemirror/addon/edit/matchtags.js", "libraries/codemirror/addon/edit/matchtags.js",
"libraries/codemirror/addon/fold/xml-fold.js",
"libraries/codemirror/addon/lint/lint.js",
"libraries/codemirror/addon/lint/eslint.js",
"libraries/codemirror/addon/mode/loadmode.js",
"libraries/codemirror/addon/mode/simple.js",
"libraries/codemirror/addon/search/match-highlighter.js", "libraries/codemirror/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js", "libraries/codemirror/mode/meta.js",
"libraries/codemirror/keymap/vim.js", "libraries/codemirror/keymap/vim.js"
"libraries/codemirror/addon/lint/lint.js",
"libraries/codemirror/addon/lint/eslint.js"
], ],
css: [ css: [
"libraries/codemirror/codemirror.css", "libraries/codemirror/codemirror.css",

View File

@ -218,6 +218,13 @@ class NoteContext extends Component {
} }
} }
} }
hasNoteList() {
return this.note.hasChildren()
&& ['book', 'text', 'code'].includes(this.note.type)
&& this.note.mime !== 'text/x-sqlite;schema=trilium'
&& !this.note.hasLabel('hideChildrenOverview');
}
} }
export default NoteContext; export default NoteContext;

View File

@ -29,6 +29,10 @@ const TPL = `
font-family: var(--detail-font-family); font-family: var(--detail-font-family);
font-size: var(--detail-font-size); font-size: var(--detail-font-size);
} }
.note-detail.full-height {
height: 100%;
}
</style> </style>
</div> </div>
`; `;
@ -128,7 +132,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
await typeWidget.handleEvent('setNoteContext', {noteContext: this.noteContext}); await typeWidget.handleEvent('setNoteContext', {noteContext: this.noteContext});
// this is happening in update() so note has been already set and we need to reflect this // this is happening in update() so note has been already set, and we need to reflect this
await typeWidget.handleEvent('noteSwitched', { await typeWidget.handleEvent('noteSwitched', {
noteContext: this.noteContext, noteContext: this.noteContext,
notePath: this.noteContext.notePath notePath: this.noteContext.notePath
@ -136,6 +140,15 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
this.child(typeWidget); this.child(typeWidget);
} }
this.checkFullHeight();
}
checkFullHeight() {
// https://github.com/zadam/trilium/issues/2522
this.$widget.toggleClass("full-height",
!this.noteContext.hasNoteList()
&& ['editable-text', 'editable-code'].includes(this.type));
} }
getTypeWidget() { getTypeWidget() {

View File

@ -5,8 +5,6 @@ const TPL = `
<div class="note-list-widget"> <div class="note-list-widget">
<style> <style>
.note-list-widget { .note-list-widget {
flex-grow: 100000;
flex-shrink: 100000;
min-height: 0; min-height: 0;
overflow: auto; overflow: auto;
} }
@ -22,11 +20,7 @@ const TPL = `
export default class NoteListWidget extends NoteContextAwareWidget { export default class NoteListWidget extends NoteContextAwareWidget {
isEnabled() { isEnabled() {
return super.isEnabled() return super.isEnabled() && this.noteContext.hasNoteList();
&& ['book', 'text', 'code'].includes(this.note.type)
&& this.note.mime !== 'text/x-sqlite;schema=trilium'
&& this.note.hasChildren()
&& !this.note.hasLabel('hideChildrenOverview');
} }
doRender() { doRender() {

View File

@ -13,10 +13,12 @@ const TPL = `
<style> <style>
.note-detail-code { .note-detail-code {
position: relative; position: relative;
height: 100%;
} }
.note-detail-code-editor { .note-detail-code-editor {
min-height: 50px; min-height: 50px;
height: 100%;
} }
</style> </style>
@ -105,7 +107,8 @@ export default class EditableCodeTypeWidget extends TypeWidget {
// we linewrap partly also because without it horizontal scrollbar displays only when you scroll // we linewrap partly also because without it horizontal scrollbar displays only when you scroll
// all the way to the bottom of the note. With line wrap there's no horizontal scrollbar so no problem // all the way to the bottom of the note. With line wrap there's no horizontal scrollbar so no problem
lineWrapping: true, lineWrapping: true,
dragDrop: false // with true the editor inlines dropped files which is not what we expect dragDrop: false, // with true the editor inlines dropped files which is not what we expect
placeholder: "Type the content of your code note here..."
}); });
this.codeEditor.on('change', () => this.spacedUpdate.scheduleUpdate()); this.codeEditor.on('change', () => this.spacedUpdate.scheduleUpdate());

View File

@ -36,6 +36,7 @@ const TPL = `
font-family: var(--detail-font-family); font-family: var(--detail-font-family);
padding-left: 14px; padding-left: 14px;
padding-top: 10px; padding-top: 10px;
height: 100%;
} }
.note-detail-editable-text a:hover { .note-detail-editable-text a:hover {
@ -73,6 +74,7 @@ const TPL = `
border: 0 !important; border: 0 !important;
box-shadow: none !important; box-shadow: none !important;
min-height: 50px; min-height: 50px;
height: 100%;
} }
</style> </style>

View File

@ -241,6 +241,10 @@ body .CodeMirror {
background-color: #eeeeee background-color: #eeeeee
} }
.CodeMirror pre.CodeMirror-placeholder {
color: #999 !important;
}
#sql-console-query { #sql-console-query {
height: 150px; height: 150px;
width: 100%; width: 100%;

View File

@ -3,320 +3,17 @@ const utils = require("../../services/utils");
const noteService = require("../../services/notes"); const noteService = require("../../services/notes");
const attributeService = require("../../services/attributes"); const attributeService = require("../../services/attributes");
const Branch = require("../../becca/entities/branch"); const Branch = require("../../becca/entities/branch");
const cls = require("../../services/cls");
const sql = require("../../services/sql");
const log = require("../../services/log");
const specialNotesService = require("../../services/special_notes"); const specialNotesService = require("../../services/special_notes");
const dateNotesService = require("../../services/date_notes"); const dateNotesService = require("../../services/date_notes");
const entityChangesService = require("../../services/entity_changes.js"); const entityChangesService = require("../../services/entity_changes.js");
const TaskContext = require("../../services/task_context.js");
const GENERIC_CODE = "GENERIC";
function sendError(res, statusCode, code, message) {
return res
.set('Content-Type', 'application/json')
.status(statusCode)
.send(JSON.stringify({
"status": statusCode,
"code": code,
"message": message
}));
}
const sendNoteNotFoundError = (res, noteId) => sendError(res, 404, "NOTE_NOT_FOUND", `Note ${noteId} not found`);
const sendBranchNotFoundError = (res, branchId) => sendError(res, 404, "BRANCH_NOT_FOUND", `Branch ${branchId} not found`);
const sendAttributeNotFoundError = (res, attributeId) => sendError(res, 404, "ATTRIBUTE_NOT_FOUND", `Attribute ${attributeId} not found`);
const sendDateInvalidError = (res, date) => sendError(res, 400, "DATE_INVALID", `Date "${date}" is not valid.`);
const sendMonthInvalidError = (res, month) => sendError(res, 400, "MONTH_INVALID", `Month "${month}" is not valid.`);
const sendYearInvalidError = (res, year) => sendError(res, 400, "YEAR_INVALID", `Year "${year}" is not valid.`);
function isValidDate(date) {
if (!/[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(date)) {
return false;
}
return !!Date.parse(date);
}
function checkEtapiAuth(req, res, next) {
if (false) {
sendError(res, 401, "NOT_AUTHENTICATED", "Not authenticated");
}
else {
next();
}
}
function register(router) { function register(router) {
function route(method, path, routeHandler) {
router[method](path, checkEtapiAuth, (req, res, next) => {
try {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
cls.init(() => {
cls.set('sourceId', "etapi");
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
const cb = () => routeHandler(req, res, next);
return sql.transactional(cb);
});
}
catch (e) {
log.error(`${method} ${path} threw exception: ` + e.stack);
res.status(500).send(e.message);
}
});
}
route('get', '/etapi/inbox/:date', (req, res, next) => {
const {date} = req.params;
if (!isValidDate(date)) {
return sendDateInvalidError(res, date);
}
const note = specialNotesService.getInboxNote(date);
res.json(mapNoteToPojo(note));
});
route('get', '/etapi/date/:date', (req, res, next) => {
const {date} = req.params;
if (!isValidDate(date)) {
return sendDateInvalidError(res, date);
}
const note = dateNotesService.getDateNote(date);
res.json(mapNoteToPojo(note));
});
route('get', '/etapi/week/:date', (req, res, next) => {
const {date} = req.params;
if (!isValidDate(date)) {
return sendDateInvalidError(res, date);
}
const note = dateNotesService.getWeekNote(date);
res.json(mapNoteToPojo(note));
});
route('get', '/etapi/month/:month', (req, res, next) => {
const {month} = req.params;
if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
return sendMonthInvalidError(res, month);
}
const note = dateNotesService.getMonthNote(month);
res.json(mapNoteToPojo(note));
});
route('get', '/etapi/year/:year', (req, res, next) => {
const {year} = req.params;
if (!/[0-9]{4}/.test(year)) {
return sendYearInvalidError(res, year);
}
const note = dateNotesService.getYearNote(year);
res.json(mapNoteToPojo(note));
});
route('get', '/etapi/notes/:noteId', (req, res, next) => {
const {noteId} = req.params;
const note = becca.getNote(noteId);
if (!note) {
return sendNoteNotFoundError(res, noteId);
}
res.json(mapNoteToPojo(note));
});
route('get', '/etapi/notes/:noteId', (req, res, next) => {
const {noteId} = req.params;
const note = becca.getNote(noteId);
if (!note) {
return sendNoteNotFoundError(res, noteId);
}
res.json(mapNoteToPojo(note));
});
route('get', '/etapi/notes/:noteId/content', (req, res, next) => {
const {noteId} = req.params;
const note = becca.getNote(noteId);
if (!note) {
return sendNoteNotFoundError(res, noteId);
}
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', note.mime);
res.send(note.getContent());
});
route('get', '/etapi/branches/:branchId', (req, res, next) => {
const {branchId} = req.params;
const branch = becca.getBranch(branchId);
if (!branch) {
return sendBranchNotFoundError(res, branchId);
}
res.json(mapBranchToPojo(branch));
});
route('get', '/etapi/attributes/:attributeId', (req, res, next) => {
const {attributeId} = req.params;
const attribute = becca.getAttribute(attributeId);
if (!attribute) {
return sendAttributeNotFoundError(res, attributeId);
}
res.json(mapAttributeToPojo(attribute));
});
route('post' ,'/etapi/notes', (req, res, next) => {
const params = req.body;
if (!becca.getNote(params.parentNoteId)) {
return sendNoteNotFoundError(res, params.parentNoteId);
}
try {
const resp = noteService.createNewNote(params);
res.json({
note: mapNoteToPojo(resp.note),
branch: mapBranchToPojo(resp.branch)
});
}
catch (e) {
return sendError(res, 400, GENERIC_CODE, e.message);
}
});
route('post' ,'/etapi/branches', (req, res, next) => {
const params = req.body;
if (!becca.getNote(params.noteId)) {
return sendNoteNotFoundError(res, params.noteId);
}
if (!becca.getNote(params.parentNoteId)) {
return sendNoteNotFoundError(res, params.parentNoteId);
}
const existing = becca.getBranchFromChildAndParent(params.noteId, params.parentNoteId);
if (existing) {
existing.notePosition = params.notePosition;
existing.prefix = params.prefix;
existing.save();
return res.json(mapBranchToPojo(existing));
}
try {
const branch = new Branch(params).save();
res.json(mapBranchToPojo(branch));
}
catch (e) {
return sendError(res, 400, GENERIC_CODE, e.message);
}
});
route('post' ,'/etapi/attributes', (req, res, next) => {
const params = req.body;
if (!becca.getNote(params.noteId)) {
return sendNoteNotFoundError(res, params.noteId);
}
if (params.type === 'relation' && !becca.getNote(params.value)) {
return sendNoteNotFoundError(res, params.value);
}
if (params.type !== 'relation' && params.type !== 'label') {
return sendError(res, 400, GENERIC_CODE, `Only "relation" and "label" are supported attribute types, "${params.type}" given.`);
}
try {
const attr = attributeService.createAttribute(params);
res.json(mapAttributeToPojo(attr));
}
catch (e) {
return sendError(res, 400, GENERIC_CODE, e.message);
}
});
route('post' ,'/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => {
const {parentNoteId} = req.params;
if (!becca.getNote(parentNoteId)) {
return sendNoteNotFoundError(res, parentNoteId);
}
entityChangesService.addNoteReorderingEntityChange(parentNoteId, "etapi");
});
}
function mapNoteToPojo(note) {
return {
noteId: note.noteId,
isProtected: note.isProtected,
title: note.title,
type: note.type,
mime: note.mime,
dateCreated: note.dateCreated,
dateModified: note.dateModified,
utcDateCreated: note.utcDateCreated,
utcDateModified: note.utcDateModified,
parentNoteIds: note.getParentNotes().map(p => p.noteId),
childNoteIds: note.getChildNotes().map(ch => ch.noteId),
parentBranchIds: note.getParentBranches().map(p => p.branchId),
childBranchIds: note.getChildBranches().map(ch => ch.branchId),
attributes: note.getAttributes().map(attr => mapAttributeToPojo(attr))
};
}
function mapBranchToPojo(branch) {
return {
branchId: branch.branchId,
noteId: branch.noteId,
parentNoteId: branch.parentNoteId,
prefix: branch.prefix,
notePosition: branch.notePosition,
isExpanded: branch.isExpanded,
utcDateModified: branch.utcDateModified
};
}
function mapAttributeToPojo(attr) {
return {
attributeId: attr.attributeId,
noteId: attr.noteId,
type: attr.type,
name: attr.name,
value: attr.value,
position: attr.position,
isInheritable: attr.isInheritable,
utcDateModified: attr.utcDateModified
};
} }
module.exports = { module.exports = {

View File

@ -10,7 +10,6 @@ const appInfo = require('../../services/app_info');
const eventService = require('../../services/events'); const eventService = require('../../services/events');
const sqlInit = require('../../services/sql_init'); const sqlInit = require('../../services/sql_init');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const optionService = require('../../services/options');
const ApiToken = require('../../becca/entities/api_token'); const ApiToken = require('../../becca/entities/api_token');
const ws = require("../../services/ws"); const ws = require("../../services/ws");
@ -92,6 +91,8 @@ function token(req) {
} }
const apiToken = new ApiToken({ const apiToken = new ApiToken({
// for backwards compatibility with Sender which does not send the name
name: req.body.tokenName || "Trilium Sender",
token: utils.randomSecureToken() token: utils.randomSecureToken()
}).save(); }).save();

View File

@ -73,9 +73,7 @@ function deleteNote(req) {
const taskContext = TaskContext.getInstance(taskId, 'delete-notes'); const taskContext = TaskContext.getInstance(taskId, 'delete-notes');
for (const branch of note.getParentBranches()) { noteService.deleteNote(note, deleteId, taskContext);
noteService.deleteBranch(branch, deleteId, taskContext);
}
if (eraseNotes) { if (eraseNotes) {
noteService.eraseNotesWithDeleteId(deleteId); noteService.eraseNotesWithDeleteId(deleteId);

View File

@ -40,7 +40,10 @@ const backendLogRoute = require('./api/backend_log');
const statsRoute = require('./api/stats'); const statsRoute = require('./api/stats');
const fontsRoute = require('./api/fonts'); const fontsRoute = require('./api/fonts');
const shareRoutes = require('../share/routes'); const shareRoutes = require('../share/routes');
const etapiRoutes = require('./api/etapi'); const etapiAttributeRoutes = require('../etapi/attributes');
const etapiBranchRoutes = require('../etapi/branches');
const etapiNoteRoutes = require('../etapi/notes');
const etapiSpecialNoteRoutes = require('../etapi/special_notes');
const log = require('../services/log'); const log = require('../services/log');
const express = require('express'); const express = require('express');
@ -376,7 +379,10 @@ function register(app) {
route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss); route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);
shareRoutes.register(router); shareRoutes.register(router);
etapiRoutes.register(router); etapiAttributeRoutes.register(router);
etapiBranchRoutes.register(router);
etapiNoteRoutes.register(router);
etapiSpecialNoteRoutes.register(router);
app.use('', router); app.use('', router);
} }

View File

@ -4,8 +4,8 @@ const build = require('./build');
const packageJson = require('../../package'); const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir'); const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 189; const APP_DB_VERSION = 190;
const SYNC_VERSION = 24; const SYNC_VERSION = 25;
const CLIPPER_PROTOCOL_VERSION = "1.0"; const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = { module.exports = {

View File

@ -88,7 +88,7 @@ function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
} }
/** @returns {Note} */ /** @returns {Note} */
function getMonthNote(dateStr, rootNote) { function getMonthNote(dateStr, rootNote = null) {
if (!rootNote) { if (!rootNote) {
rootNote = getRootCalendarNote(); rootNote = getRootCalendarNote();
} }

View File

@ -106,6 +106,10 @@ function createNewNote(params) {
throw new Error(`Note title must not be empty`); throw new Error(`Note title must not be empty`);
} }
if (params.content === null || params.content === undefined) {
throw new Error(`Note content must be set`);
}
return sql.transactional(() => { return sql.transactional(() => {
const note = new Note({ const note = new Note({
noteId: params.noteId, // optionally can force specific noteId noteId: params.noteId, // optionally can force specific noteId
@ -519,7 +523,7 @@ function updateNote(noteId, noteUpdates) {
/** /**
* @param {Branch} branch * @param {Branch} branch
* @param {string} deleteId * @param {string|null} deleteId
* @param {TaskContext} taskContext * @param {TaskContext} taskContext
* *
* @return {boolean} - true if note has been deleted, false otherwise * @return {boolean} - true if note has been deleted, false otherwise
@ -569,6 +573,17 @@ function deleteBranch(branch, deleteId, taskContext) {
} }
} }
/**
* @param {Note} note
* @param {string|null} deleteId
* @param {TaskContext} taskContext
*/
function deleteNote(note, deleteId, taskContext) {
for (const branch of note.getParentBranches()) {
deleteBranch(branch, deleteId, taskContext);
}
}
/** /**
* @param {string} noteId * @param {string} noteId
* @param {TaskContext} taskContext * @param {TaskContext} taskContext
@ -914,6 +929,7 @@ module.exports = {
createNewNoteWithTarget, createNewNoteWithTarget,
updateNote, updateNote,
deleteBranch, deleteBranch,
deleteNote,
undeleteNote, undeleteNote,
protectNoteRecursively, protectNoteRecursively,
scanForLinks, scanForLinks,

View File

@ -28,7 +28,15 @@ function initNotSyncedOptions(initialized, opts = {}) {
optionService.createOption('lastSyncedPull', '0', false); optionService.createOption('lastSyncedPull', '0', false);
optionService.createOption('lastSyncedPush', '0', false); optionService.createOption('lastSyncedPush', '0', false);
optionService.createOption('theme', opts.theme || 'white', false); let theme = 'dark'; // default based on the poll in https://github.com/zadam/trilium/issues/2516
if (utils.isElectron()) {
const {nativeTheme} = require('electron');
theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
}
optionService.createOption('theme', theme, false);
optionService.createOption('syncServerHost', opts.syncServerHost || '', false); optionService.createOption('syncServerHost', opts.syncServerHost || '', false);
optionService.createOption('syncServerTimeout', '120000', false); optionService.createOption('syncServerTimeout', '120000', false);

View File

@ -1,4 +1,4 @@
POST {{triliumHost}}/etapi/notes POST {{triliumHost}}/etapi/create-note
Content-Type: application/json Content-Type: application/json
{ {
@ -15,12 +15,33 @@ Content-Type: application/json
client.assert(response.body.branch.parentNoteId == "root"); client.assert(response.body.branch.parentNoteId == "root");
}); });
client.log(`Created note "${createdNoteId}" and branch ${createdBranchId}`); client.log(`Created note ` + response.body.note.noteId + ` and branch ` + response.body.branch.branchId);
client.global.set("createdNoteId", response.body.note.noteId); client.global.set("createdNoteId", response.body.note.noteId);
client.global.set("createdBranchId", response.body.branch.branchId); client.global.set("createdBranchId", response.body.branch.branchId);
%} %}
### Clone to another location
POST {{triliumHost}}/etapi/branches
Content-Type: application/json
{
"noteId": "{{createdNoteId}}",
"parentNoteId": "hidden"
}
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.body.parentNoteId == "hidden");
});
client.global.set("clonedBranchId", response.body.branchId);
client.log(`Created cloned branch ` + response.body.branchId);
%}
### ###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}} GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
@ -30,6 +51,9 @@ GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
client.assert(response.status === 200, "Response status is not 200"); client.assert(response.status === 200, "Response status is not 200");
client.assert(response.body.noteId == client.global.get("createdNoteId")); client.assert(response.body.noteId == client.global.get("createdNoteId"));
client.assert(response.body.title == "Hello"); client.assert(response.body.title == "Hello");
// order is not defined and may fail in the future
client.assert(response.body.parentBranchIds[0] == client.global.get("clonedBranchId"))
client.assert(response.body.parentBranchIds[1] == client.global.get("createdBranchId"));
}); });
%} %}
@ -58,6 +82,18 @@ GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
### ###
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.body.branchId == client.global.get("clonedBranchId"));
client.assert(response.body.parentNoteId == "hidden");
});
%}
###
POST {{triliumHost}}/etapi/attributes POST {{triliumHost}}/etapi/attributes
Content-Type: application/json Content-Type: application/json
@ -74,7 +110,7 @@ Content-Type: application/json
client.assert(response.status === 200, "Response status is not 200"); client.assert(response.status === 200, "Response status is not 200");
}); });
client.log(`Created attribute ${response.body.attributeId}`); client.log(`Created attribute ` + response.body.attributeId);
client.global.set("createdAttributeId", response.body.attributeId); client.global.set("createdAttributeId", response.body.attributeId);
%} %}

View File

@ -0,0 +1,56 @@
POST {{triliumHost}}/etapi/create-note
Content-Type: application/json
{
"parentNoteId": "root",
"title": "Hello",
"type": "text",
"content": "Hi there!"
}
> {%
client.global.set("createdNoteId", response.body.note.noteId);
client.global.set("createdBranchId", response.body.branch.branchId);
%}
###
POST {{triliumHost}}/etapi/attributes
Content-Type: application/json
{
"noteId": "{{createdNoteId}}",
"type": "label",
"name": "mylabel",
"value": "val",
"isInheritable": "true"
}
> {% client.global.set("createdAttributeId", response.body.attributeId); %}
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
DELETE {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
> {% client.assert(response.status === 204, "Response status is not 204"); %}
###
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
> {%
client.assert(response.status === 404, "Response status is not 404");
client.assert(response.body.code == "ATTRIBUTE_NOT_FOUND");
%}

View File

@ -0,0 +1,71 @@
POST {{triliumHost}}/etapi/create-note
Content-Type: application/json
{
"parentNoteId": "root",
"title": "Hello",
"type": "text",
"content": "Hi there!"
}
> {%
client.global.set("createdNoteId", response.body.note.noteId);
client.global.set("createdBranchId", response.body.branch.branchId);
%}
### Clone to another location
POST {{triliumHost}}/etapi/branches
Content-Type: application/json
{
"noteId": "{{createdNoteId}}",
"parentNoteId": "hidden"
}
> {% client.global.set("clonedBranchId", response.body.branchId); %}
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
DELETE {{triliumHost}}/etapi/branches/{{createdBranchId}}
> {% client.assert(response.status === 204, "Response status is not 204"); %}
###
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
> {%
client.assert(response.status === 404, "Response status is not 404");
client.assert(response.body.code == "BRANCH_NOT_FOUND");
%}
###
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}

View File

@ -0,0 +1,107 @@
POST {{triliumHost}}/etapi/create-note
Content-Type: application/json
{
"parentNoteId": "root",
"title": "Hello",
"type": "text",
"content": "Hi there!"
}
> {%
client.global.set("createdNoteId", response.body.note.noteId);
client.global.set("createdBranchId", response.body.branch.branchId);
%}
###
POST {{triliumHost}}/etapi/attributes
Content-Type: application/json
{
"noteId": "{{createdNoteId}}",
"type": "label",
"name": "mylabel",
"value": "val",
"isInheritable": "true"
}
> {% client.global.set("createdAttributeId", response.body.attributeId); %}
### Clone to another location
POST {{triliumHost}}/etapi/branches
Content-Type: application/json
{
"noteId": "{{createdNoteId}}",
"parentNoteId": "hidden"
}
> {% client.global.set("clonedBranchId", response.body.branchId); %}
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
> {% client.assert(response.status === 200, "Response status is not 200"); %}
###
DELETE {{triliumHost}}/etapi/notes/{{createdNoteId}}
> {% client.assert(response.status === 204, "Response status is not 204"); %}
###
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
> {%
client.assert(response.status === 404, "Response status is not 404");
client.assert(response.body.code == "BRANCH_NOT_FOUND");
%}
###
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
> {%
client.assert(response.status === 404, "Response status is not 404");
client.assert(response.body.code == "BRANCH_NOT_FOUND");
%}
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
> {%
client.assert(response.status === 404, "Response status is not 404");
client.assert(response.body.code == "NOTE_NOT_FOUND");
%}
###
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
> {%
client.assert(response.status === 404, "Response status is not 404");
client.assert(response.body.code == "ATTRIBUTE_NOT_FOUND");
%}

View File

@ -0,0 +1,74 @@
POST {{triliumHost}}/etapi/create-note
Content-Type: application/json
{
"parentNoteId": "root",
"title": "Hello",
"type": "text",
"content": "Hi there!"
}
> {%
client.global.set("createdNoteId", response.body.note.noteId);
client.global.set("createdBranchId", response.body.branch.branchId);
%}
###
POST {{triliumHost}}/etapi/attributes
Content-Type: application/json
{
"noteId": "{{createdNoteId}}",
"type": "label",
"name": "mylabel",
"value": "val",
"isInheritable": "true"
}
> {% client.global.set("createdAttributeId", response.body.attributeId); %}
###
PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
Content-Type: application/json
{
"value": "CHANGED"
}
###
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
> {%
client.assert(response.body.value === "CHANGED");
%}
###
PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
Content-Type: application/json
{
"noteId": "root"
}
> {%
client.assert(response.status === 400);
client.assert(response.body.code == "PROPERTY_NOT_ALLOWED_FOR_PATCH");
%}
###
PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
Content-Type: application/json
{
"value": null
}
> {%
client.assert(response.status === 400);
client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR");
%}

View File

@ -0,0 +1,61 @@
POST {{triliumHost}}/etapi/create-note
Content-Type: application/json
{
"parentNoteId": "root",
"type": "text",
"title": "Hello",
"content": ""
}
> {% client.global.set("createdBranchId", response.body.branch.branchId); %}
###
PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}}
Content-Type: application/json
{
"prefix": "pref",
"notePosition": 666,
"isExpanded": true
}
###
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
> {%
client.assert(response.status === 200);
client.assert(response.body.prefix === 'pref');
client.assert(response.body.notePosition === 666);
client.assert(response.body.isExpanded === true);
%}
###
PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}}
Content-Type: application/json
{
"parentNoteId": "root"
}
> {%
client.assert(response.status === 400);
client.assert(response.body.code == "PROPERTY_NOT_ALLOWED_FOR_PATCH");
%}
###
PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}}
Content-Type: application/json
{
"prefix": 123
}
> {%
client.assert(response.status === 400);
client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR");
%}

View File

@ -0,0 +1,73 @@
POST {{triliumHost}}/etapi/create-note
Content-Type: application/json
{
"parentNoteId": "root",
"title": "Hello",
"type": "code",
"mime": "application/json",
"content": "{}"
}
> {% client.global.set("createdNoteId", response.body.note.noteId); %}
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
> {%
client.assert(response.status === 200);
client.assert(response.body.title === 'Hello');
client.assert(response.body.type === 'code');
client.assert(response.body.mime === 'application/json');
%}
###
PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}}
Content-Type: application/json
{
"title": "Wassup",
"type": "html",
"mime": "text/html"
}
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
> {%
client.assert(response.status === 200);
client.assert(response.body.title === 'Wassup');
client.assert(response.body.type === 'html');
client.assert(response.body.mime === 'text/html');
%}
###
PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}}
Content-Type: application/json
{
"isProtected": true
}
> {%
client.assert(response.status === 400);
client.assert(response.body.code == "PROPERTY_NOT_ALLOWED_FOR_PATCH");
%}
###
PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}}
Content-Type: application/json
{
"title": true
}
> {%
client.assert(response.status === 400);
client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR");
%}