mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +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"> | ||||
|     <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); | ||||
|   | ||||
							
								
								
									
										11451
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11451
									
								
								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"); | ||||
| %} | ||||
		Reference in New Issue
	
	Block a user