mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	ETAPI delete/patch, refactoring
This commit is contained in:
		
							
								
								
									
										2
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							| @@ -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> | ||||||
							
								
								
									
										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" | 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); | ||||||
|   | |||||||
							
								
								
									
										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"); | 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
									
								
							
							
						
						
									
										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 = [ | const HIDDEN_ATTRIBUTES = [ | ||||||
|     'originalFileName', |     'originalFileName', | ||||||
|  |     'fileSize', | ||||||
|     'template', |     'template', | ||||||
|     'cssClass', |     'cssClass', | ||||||
|     'iconClass', |     'iconClass', | ||||||
|   | |||||||
| @@ -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", | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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() { | ||||||
|   | |||||||
| @@ -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() { | ||||||
|   | |||||||
| @@ -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()); | ||||||
|   | |||||||
| @@ -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> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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%; | ||||||
|   | |||||||
| @@ -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 = { | ||||||
|   | |||||||
| @@ -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(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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); | ||||||
|   | |||||||
| @@ -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); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 = { | ||||||
|   | |||||||
| @@ -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(); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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); | ||||||
|   | |||||||
| @@ -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); | ||||||
| %} | %} | ||||||
|   | |||||||
							
								
								
									
										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"); | ||||||
|  | %} | ||||||
		Reference in New Issue
	
	Block a user