mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-11-04 15:11:31 +08:00 
			
		
		
		
	ETAPI delete/patch, refactoring
This commit is contained in:
		
							parent
							
								
									82b2871a08
								
							
						
					
					
						commit
						9ee1c9f3da
					
				
							
								
								
									
										2
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							@ -3,7 +3,7 @@
 | 
			
		||||
  <component name="JavaScriptSettings">
 | 
			
		||||
    <option name="languageLevel" value="ES6" />
 | 
			
		||||
  </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" />
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										14
									
								
								db/migrations/0190__add_token_name.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								db/migrations/0190__add_token_name.sql
									
									
									
									
									
										Normal 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;
 | 
			
		||||
@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS "entity_changes" (
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "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);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										11453
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11453
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -4,12 +4,15 @@ const dateUtils = require('../../services/date_utils.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 {
 | 
			
		||||
    static get entityName() { return "api_tokens"; }
 | 
			
		||||
    static get primaryKeyName() { return "apiTokenId"; }
 | 
			
		||||
    static get hashedProperties() { return ["apiTokenId", "token", "utcDateCreated"]; }
 | 
			
		||||
    static get hashedProperties() { return ["apiTokenId", "name", "token", "utcDateCreated"]; }
 | 
			
		||||
 | 
			
		||||
    constructor(row) {
 | 
			
		||||
        super();
 | 
			
		||||
@ -17,6 +20,8 @@ class ApiToken extends AbstractEntity {
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.apiTokenId = row.apiTokenId;
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.name = row.name;
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.token = row.token;
 | 
			
		||||
        /** @type {string} */
 | 
			
		||||
        this.utcDateCreated = row.utcDateCreated || dateUtils.utcNowDateTime();
 | 
			
		||||
@ -25,6 +30,7 @@ class ApiToken extends AbstractEntity {
 | 
			
		||||
    getPojo() {
 | 
			
		||||
        return {
 | 
			
		||||
            apiTokenId: this.apiTokenId,
 | 
			
		||||
            name: this.name,
 | 
			
		||||
            token: this.token,
 | 
			
		||||
            utcDateCreated: this.utcDateCreated
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										64
									
								
								src/etapi/attributes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/etapi/attributes.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										78
									
								
								src/etapi/branches.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										49
									
								
								src/etapi/mappers.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										82
									
								
								src/etapi/notes.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										132
									
								
								src/etapi/route_utils.js
									
									
									
									
									
										Normal 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}.`),
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										95
									
								
								src/etapi/spec.openapi.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/etapi/spec.openapi.yaml
									
									
									
									
									
										Normal 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
 | 
			
		||||
							
								
								
									
										77
									
								
								src/etapi/special_notes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/etapi/special_notes.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										30
									
								
								src/etapi/validators.js
									
									
									
									
									
										Normal 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
 | 
			
		||||
};
 | 
			
		||||
@ -81,6 +81,7 @@ async function renderAttributes(attributes, renderIsInheritable) {
 | 
			
		||||
 | 
			
		||||
const HIDDEN_ATTRIBUTES = [
 | 
			
		||||
    'originalFileName',
 | 
			
		||||
    'fileSize',
 | 
			
		||||
    'template',
 | 
			
		||||
    'cssClass',
 | 
			
		||||
    'iconClass',
 | 
			
		||||
 | 
			
		||||
@ -3,16 +3,17 @@ const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
 | 
			
		||||
const CODE_MIRROR = {
 | 
			
		||||
    js: [
 | 
			
		||||
        "libraries/codemirror/codemirror.js",
 | 
			
		||||
        "libraries/codemirror/addon/mode/loadmode.js",
 | 
			
		||||
        "libraries/codemirror/addon/mode/simple.js",
 | 
			
		||||
        "libraries/codemirror/addon/fold/xml-fold.js",
 | 
			
		||||
        "libraries/codemirror/addon/display/placeholder.js",
 | 
			
		||||
        "libraries/codemirror/addon/edit/matchbrackets.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/mode/meta.js",
 | 
			
		||||
        "libraries/codemirror/keymap/vim.js",
 | 
			
		||||
        "libraries/codemirror/addon/lint/lint.js",
 | 
			
		||||
        "libraries/codemirror/addon/lint/eslint.js"
 | 
			
		||||
        "libraries/codemirror/keymap/vim.js"
 | 
			
		||||
    ],
 | 
			
		||||
    css: [
 | 
			
		||||
        "libraries/codemirror/codemirror.css",
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,10 @@ const TPL = `
 | 
			
		||||
        font-family: var(--detail-font-family);
 | 
			
		||||
        font-size: var(--detail-font-size);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .note-detail.full-height {
 | 
			
		||||
        height: 100%;
 | 
			
		||||
    }
 | 
			
		||||
    </style>
 | 
			
		||||
</div>
 | 
			
		||||
`;
 | 
			
		||||
@ -128,7 +132,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
            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', {
 | 
			
		||||
                noteContext: this.noteContext,
 | 
			
		||||
                notePath: this.noteContext.notePath
 | 
			
		||||
@ -136,6 +140,15 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
            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() {
 | 
			
		||||
 | 
			
		||||
@ -5,8 +5,6 @@ const TPL = `
 | 
			
		||||
<div class="note-list-widget">
 | 
			
		||||
    <style>
 | 
			
		||||
    .note-list-widget {
 | 
			
		||||
        flex-grow: 100000;
 | 
			
		||||
        flex-shrink: 100000;
 | 
			
		||||
        min-height: 0;
 | 
			
		||||
        overflow: auto;
 | 
			
		||||
    }
 | 
			
		||||
@ -22,11 +20,7 @@ const TPL = `
 | 
			
		||||
 | 
			
		||||
export default class NoteListWidget extends NoteContextAwareWidget {
 | 
			
		||||
    isEnabled() {
 | 
			
		||||
        return super.isEnabled()
 | 
			
		||||
            && ['book', 'text', 'code'].includes(this.note.type)
 | 
			
		||||
            && this.note.mime !== 'text/x-sqlite;schema=trilium'
 | 
			
		||||
            && this.note.hasChildren()
 | 
			
		||||
            && !this.note.hasLabel('hideChildrenOverview');
 | 
			
		||||
        return super.isEnabled() && this.noteContext.hasNoteList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    doRender() {
 | 
			
		||||
 | 
			
		||||
@ -13,10 +13,12 @@ const TPL = `
 | 
			
		||||
    <style>
 | 
			
		||||
    .note-detail-code {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .note-detail-code-editor {
 | 
			
		||||
        min-height: 50px;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
    }
 | 
			
		||||
    </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
 | 
			
		||||
            // all the way to the bottom of the note. With line wrap there's no horizontal scrollbar so no problem
 | 
			
		||||
            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());
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,7 @@ const TPL = `
 | 
			
		||||
        font-family: var(--detail-font-family);
 | 
			
		||||
        padding-left: 14px;
 | 
			
		||||
        padding-top: 10px;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .note-detail-editable-text a:hover {
 | 
			
		||||
@ -73,6 +74,7 @@ const TPL = `
 | 
			
		||||
        border: 0 !important;
 | 
			
		||||
        box-shadow: none !important;
 | 
			
		||||
        min-height: 50px;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
    }
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -241,6 +241,10 @@ body .CodeMirror {
 | 
			
		||||
    background-color: #eeeeee
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.CodeMirror pre.CodeMirror-placeholder { 
 | 
			
		||||
    color: #999 !important; 
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#sql-console-query {
 | 
			
		||||
    height: 150px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 | 
			
		||||
@ -3,320 +3,17 @@ const utils = require("../../services/utils");
 | 
			
		||||
const noteService = require("../../services/notes");
 | 
			
		||||
const attributeService = require("../../services/attributes");
 | 
			
		||||
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 dateNotesService = require("../../services/date_notes");
 | 
			
		||||
const entityChangesService = require("../../services/entity_changes.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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
const TaskContext = require("../../services/task_context.js");
 | 
			
		||||
 | 
			
		||||
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 = {
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,6 @@ const appInfo = require('../../services/app_info');
 | 
			
		||||
const eventService = require('../../services/events');
 | 
			
		||||
const sqlInit = require('../../services/sql_init');
 | 
			
		||||
const sql = require('../../services/sql');
 | 
			
		||||
const optionService = require('../../services/options');
 | 
			
		||||
const ApiToken = require('../../becca/entities/api_token');
 | 
			
		||||
const ws = require("../../services/ws");
 | 
			
		||||
 | 
			
		||||
@ -92,6 +91,8 @@ function token(req) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const apiToken = new ApiToken({
 | 
			
		||||
        // for backwards compatibility with Sender which does not send the name
 | 
			
		||||
        name: req.body.tokenName || "Trilium Sender",
 | 
			
		||||
        token: utils.randomSecureToken()
 | 
			
		||||
    }).save();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -73,9 +73,7 @@ function deleteNote(req) {
 | 
			
		||||
 | 
			
		||||
    const taskContext = TaskContext.getInstance(taskId, 'delete-notes');
 | 
			
		||||
 | 
			
		||||
    for (const branch of note.getParentBranches()) {
 | 
			
		||||
        noteService.deleteBranch(branch, deleteId, taskContext);
 | 
			
		||||
    }
 | 
			
		||||
    noteService.deleteNote(note, deleteId, taskContext);
 | 
			
		||||
 | 
			
		||||
    if (eraseNotes) {
 | 
			
		||||
        noteService.eraseNotesWithDeleteId(deleteId);
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,10 @@ const backendLogRoute = require('./api/backend_log');
 | 
			
		||||
const statsRoute = require('./api/stats');
 | 
			
		||||
const fontsRoute = require('./api/fonts');
 | 
			
		||||
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 express = require('express');
 | 
			
		||||
@ -376,7 +379,10 @@ function register(app) {
 | 
			
		||||
    route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);
 | 
			
		||||
 | 
			
		||||
    shareRoutes.register(router);
 | 
			
		||||
    etapiRoutes.register(router);
 | 
			
		||||
    etapiAttributeRoutes.register(router);
 | 
			
		||||
    etapiBranchRoutes.register(router);
 | 
			
		||||
    etapiNoteRoutes.register(router);
 | 
			
		||||
    etapiSpecialNoteRoutes.register(router);
 | 
			
		||||
 | 
			
		||||
    app.use('', router);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,8 +4,8 @@ const build = require('./build');
 | 
			
		||||
const packageJson = require('../../package');
 | 
			
		||||
const {TRILIUM_DATA_DIR} = require('./data_dir');
 | 
			
		||||
 | 
			
		||||
const APP_DB_VERSION = 189;
 | 
			
		||||
const SYNC_VERSION = 24;
 | 
			
		||||
const APP_DB_VERSION = 190;
 | 
			
		||||
const SYNC_VERSION = 25;
 | 
			
		||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
 | 
			
		||||
@ -88,7 +88,7 @@ function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @returns {Note} */
 | 
			
		||||
function getMonthNote(dateStr, rootNote) {
 | 
			
		||||
function getMonthNote(dateStr, rootNote = null) {
 | 
			
		||||
    if (!rootNote) {
 | 
			
		||||
        rootNote = getRootCalendarNote();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -106,6 +106,10 @@ function createNewNote(params) {
 | 
			
		||||
        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(() => {
 | 
			
		||||
        const note = new Note({
 | 
			
		||||
            noteId: params.noteId, // optionally can force specific noteId
 | 
			
		||||
@ -519,7 +523,7 @@ function updateNote(noteId, noteUpdates) {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {Branch} branch
 | 
			
		||||
 * @param {string} deleteId
 | 
			
		||||
 * @param {string|null} deleteId
 | 
			
		||||
 * @param {TaskContext} taskContext
 | 
			
		||||
 *
 | 
			
		||||
 * @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 {TaskContext} taskContext
 | 
			
		||||
@ -914,6 +929,7 @@ module.exports = {
 | 
			
		||||
    createNewNoteWithTarget,
 | 
			
		||||
    updateNote,
 | 
			
		||||
    deleteBranch,
 | 
			
		||||
    deleteNote,
 | 
			
		||||
    undeleteNote,
 | 
			
		||||
    protectNoteRecursively,
 | 
			
		||||
    scanForLinks,
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,15 @@ function initNotSyncedOptions(initialized, opts = {}) {
 | 
			
		||||
    optionService.createOption('lastSyncedPull', '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('syncServerTimeout', '120000', false);
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
POST {{triliumHost}}/etapi/notes
 | 
			
		||||
POST {{triliumHost}}/etapi/create-note
 | 
			
		||||
Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
@ -15,12 +15,33 @@ Content-Type: application/json
 | 
			
		||||
        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("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}}
 | 
			
		||||
@ -30,6 +51,9 @@ GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
 | 
			
		||||
        client.assert(response.status === 200, "Response status is not 200");
 | 
			
		||||
        client.assert(response.body.noteId == client.global.get("createdNoteId"));
 | 
			
		||||
        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
 | 
			
		||||
Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
@ -74,7 +110,7 @@ Content-Type: application/json
 | 
			
		||||
        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);
 | 
			
		||||
%}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										56
									
								
								test-etapi/delete-attribute.http
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								test-etapi/delete-attribute.http
									
									
									
									
									
										Normal 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");
 | 
			
		||||
%}
 | 
			
		||||
							
								
								
									
										71
									
								
								test-etapi/delete-cloned-branch.http
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								test-etapi/delete-cloned-branch.http
									
									
									
									
									
										Normal 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"); %}
 | 
			
		||||
							
								
								
									
										107
									
								
								test-etapi/delete-note-with-all-branches.http
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								test-etapi/delete-note-with-all-branches.http
									
									
									
									
									
										Normal 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");
 | 
			
		||||
%}
 | 
			
		||||
							
								
								
									
										74
									
								
								test-etapi/patch-attribute.http
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								test-etapi/patch-attribute.http
									
									
									
									
									
										Normal 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");
 | 
			
		||||
%}
 | 
			
		||||
							
								
								
									
										61
									
								
								test-etapi/patch-branch.http
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								test-etapi/patch-branch.http
									
									
									
									
									
										Normal 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");
 | 
			
		||||
%}
 | 
			
		||||
							
								
								
									
										73
									
								
								test-etapi/patch-note.http
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								test-etapi/patch-note.http
									
									
									
									
									
										Normal 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");
 | 
			
		||||
%}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user