mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	attachment ETAPI support WIP
This commit is contained in:
		| @@ -29,12 +29,12 @@ function dumpDocument(documentPath, targetPath, options) { | |||||||
|     function dumpNote(targetPath, noteId) { |     function dumpNote(targetPath, noteId) { | ||||||
|         console.log(`Reading note '${noteId}'`); |         console.log(`Reading note '${noteId}'`); | ||||||
|  |  | ||||||
|         let childTargetPath, note, fileNameWithPath; |         let childTargetPath, noteRow, fileNameWithPath; | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             note = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); |             noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||||
|  |  | ||||||
|             if (note.isDeleted) { |             if (noteRow.isDeleted) { | ||||||
|                 stats.deleted++; |                 stats.deleted++; | ||||||
|  |  | ||||||
|                 if (!options.includeDeleted) { |                 if (!options.includeDeleted) { | ||||||
| @@ -44,13 +44,13 @@ function dumpDocument(documentPath, targetPath, options) { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (note.isProtected) { |             if (noteRow.isProtected) { | ||||||
|                 stats.protected++; |                 stats.protected++; | ||||||
|  |  | ||||||
|                 note.title = decryptService.decryptString(dataKey, note.title); |                 noteRow.title = decryptService.decryptString(dataKey, noteRow.title); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             let safeTitle = sanitize(note.title); |             let safeTitle = sanitize(noteRow.title); | ||||||
|  |  | ||||||
|             if (safeTitle.length > 20) { |             if (safeTitle.length > 20) { | ||||||
|                 safeTitle = safeTitle.substring(0, 20); |                 safeTitle = safeTitle.substring(0, 20); | ||||||
| @@ -64,8 +64,8 @@ function dumpDocument(documentPath, targetPath, options) { | |||||||
|  |  | ||||||
|             existingPaths[childTargetPath] = true; |             existingPaths[childTargetPath] = true; | ||||||
|  |  | ||||||
|             if (note.noteId in noteIdToPath) { |             if (noteRow.noteId in noteIdToPath) { | ||||||
|                 const message = `Note '${noteId}' has been already dumped to ${noteIdToPath[note.noteId]}`; |                 const message = `Note '${noteId}' has been already dumped to ${noteIdToPath[noteRow.noteId]}`; | ||||||
|  |  | ||||||
|                 console.log(message); |                 console.log(message); | ||||||
|  |  | ||||||
| @@ -74,16 +74,16 @@ function dumpDocument(documentPath, targetPath, options) { | |||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             let {content} = sql.getRow("SELECT content FROM blobs WHERE blobId = ?", [note.blobId]); |             let {content} = sql.getRow("SELECT content FROM blobs WHERE blobId = ?", [noteRow.blobId]); | ||||||
|  |  | ||||||
|             if (content !== null && note.isProtected && dataKey) { |             if (content !== null && noteRow.isProtected && dataKey) { | ||||||
|                 content = decryptService.decrypt(dataKey, content); |                 content = decryptService.decrypt(dataKey, content); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (isContentEmpty(content)) { |             if (isContentEmpty(content)) { | ||||||
|                 console.log(`Note '${noteId}' is empty, skipping.`); |                 console.log(`Note '${noteId}' is empty, skipping.`); | ||||||
|             } else { |             } else { | ||||||
|                 fileNameWithPath = extensionService.getFileName(note, childTargetPath, safeTitle); |                 fileNameWithPath = extensionService.getFileName(noteRow, childTargetPath, safeTitle); | ||||||
|  |  | ||||||
|                 fs.writeFileSync(fileNameWithPath, content); |                 fs.writeFileSync(fileNameWithPath, content); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -187,7 +187,7 @@ class BBranch extends AbstractBeccaEntity { | |||||||
|  |  | ||||||
|             // first delete children and then parent - this will show up better in recent changes |             // first delete children and then parent - this will show up better in recent changes | ||||||
|  |  | ||||||
|             log.info(`Deleting note ${note.noteId}`); |             log.info(`Deleting note '${note.noteId}'`); | ||||||
|  |  | ||||||
|             this.becca.notes[note.noteId].isBeingDeleted = true; |             this.becca.notes[note.noteId].isBeingDeleted = true; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1549,6 +1549,8 @@ class BNote extends AbstractBeccaEntity { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     get isDeleted() { |     get isDeleted() { | ||||||
|  |         // isBeingDeleted is relevant only in the transition period when the deletion process have begun, but not yet | ||||||
|  |         // finished (note is still in becca) | ||||||
|         return !(this.noteId in this.becca.notes) || this.isBeingDeleted; |         return !(this.noteId in this.becca.notes) || this.isBeingDeleted; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -1602,7 +1604,7 @@ class BNote extends AbstractBeccaEntity { | |||||||
|     /** |     /** | ||||||
|      * @returns {BAttachment} |      * @returns {BAttachment} | ||||||
|      */ |      */ | ||||||
|     saveAttachment({attachmentId, role, mime, title, content}) { |     saveAttachment({attachmentId, role, mime, title, content, position}) { | ||||||
|         let attachment; |         let attachment; | ||||||
|  |  | ||||||
|         if (attachmentId) { |         if (attachmentId) { | ||||||
| @@ -1613,15 +1615,13 @@ class BNote extends AbstractBeccaEntity { | |||||||
|                 title, |                 title, | ||||||
|                 role, |                 role, | ||||||
|                 mime, |                 mime, | ||||||
|                 isProtected: this.isProtected |                 isProtected: this.isProtected, | ||||||
|  |                 position | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (content !== undefined && content !== null) { |         content = content || ""; | ||||||
|             attachment.setContent(content, {forceSave: true}); |         attachment.setContent(content, {forceSave: true}); | ||||||
|         } else { |  | ||||||
|             attachment.save(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return attachment; |         return attachment; | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										104
									
								
								src/etapi/attachments.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/etapi/attachments.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | |||||||
|  | const becca = require("../becca/becca"); | ||||||
|  | const eu = require("./etapi_utils"); | ||||||
|  | const mappers = require("./mappers"); | ||||||
|  | const v = require("./validators"); | ||||||
|  | const utils = require("../services/utils.js"); | ||||||
|  | const noteService = require("../services/notes.js"); | ||||||
|  |  | ||||||
|  | function register(router) { | ||||||
|  |     const ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT = { | ||||||
|  |         'parentId': [v.notNull, v.isNoteId], | ||||||
|  |         'role': [v.notNull, v.isString], | ||||||
|  |         'mime': [v.notNull, v.isString], | ||||||
|  |         'title': [v.notNull, v.isString], | ||||||
|  |         'position': [v.notNull, v.isInteger], | ||||||
|  |         'content': [v.isString], | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     eu.route(router, 'post' ,'/etapi/attachments', (req, res, next) => { | ||||||
|  |         const params = {}; | ||||||
|  |  | ||||||
|  |         eu.validateAndPatch(params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT); | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             const note = becca.getNoteOrThrow(params.parentId); | ||||||
|  |             const attachment = note.saveAttachment(params); | ||||||
|  |  | ||||||
|  |             res.status(201).json(mappers.mapAttachmentToPojo(attachment)); | ||||||
|  |         } | ||||||
|  |         catch (e) { | ||||||
|  |             throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     eu.route(router, 'get', '/etapi/attachments/:attachmentId', (req, res, next) => { | ||||||
|  |         const attachment = eu.getAndCheckAttachment(req.params.attachmentId); | ||||||
|  |  | ||||||
|  |         res.json(mappers.mapAttachmentToPojo(attachment)); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const ALLOWED_PROPERTIES_FOR_PATCH = { | ||||||
|  |         'role': [v.notNull, v.isString], | ||||||
|  |         'mime': [v.notNull, v.isString], | ||||||
|  |         'title': [v.notNull, v.isString], | ||||||
|  |         'position': [v.notNull, v.isInteger], | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     eu.route(router, 'patch' ,'/etapi/attachments/:attachmentId', (req, res, next) => { | ||||||
|  |         const attachment = eu.getAndCheckAttachment(req.params.attachmentId); | ||||||
|  |  | ||||||
|  |         if (attachment.isProtected) { | ||||||
|  |             throw new eu.EtapiError(400, "ATTACHMENT_IS_PROTECTED", `Attachment '${req.params.attachmentId}' is protected and cannot be modified through ETAPI.`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         eu.validateAndPatch(attachment, req.body, ALLOWED_PROPERTIES_FOR_PATCH); | ||||||
|  |         attachment.save(); | ||||||
|  |  | ||||||
|  |         res.json(mappers.mapAttachmentToPojo(attachment)); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     eu.route(router, 'get', '/etapi/attachments/:attachmentId/content', (req, res, next) => { | ||||||
|  |         const attachment = eu.getAndCheckAttachment(req.params.attachmentId); | ||||||
|  |  | ||||||
|  |         if (attachment.isProtected) { | ||||||
|  |             throw new eu.EtapiError(400, "ATTACHMENT_IS_PROTECTED", `Attachment '${req.params.attachmentId}' is protected and content cannot be read through ETAPI.`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const filename = utils.formatDownloadTitle(attachment.title, attachment.type, attachment.mime); | ||||||
|  |  | ||||||
|  |         res.setHeader('Content-Disposition', utils.getContentDisposition(filename)); | ||||||
|  |  | ||||||
|  |         res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); | ||||||
|  |         res.setHeader('Content-Type', attachment.mime); | ||||||
|  |  | ||||||
|  |         res.send(attachment.getContent()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     eu.route(router, 'put', '/etapi/attachments/:attachmentId/content', (req, res, next) => { | ||||||
|  |         const attachment = eu.getAndCheckAttachment(req.params.attachmentId); | ||||||
|  |  | ||||||
|  |         if (attachment.isProtected) { | ||||||
|  |             throw new eu.EtapiError(400, "ATTACHMENT_IS_PROTECTED", `Attachment '${req.params.attachmentId}' is protected and cannot be modified through ETAPI.`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         attachment.setContent(req.body); | ||||||
|  |  | ||||||
|  |         return res.sendStatus(204); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     eu.route(router, 'delete' ,'/etapi/attachments/:attachmentId', (req, res, next) => { | ||||||
|  |         const attachment = becca.getAttachment(req.params.attachmentId); | ||||||
|  |  | ||||||
|  |         if (!attachment) { | ||||||
|  |             return res.sendStatus(204); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         attachment.markAsDeleted(); | ||||||
|  |  | ||||||
|  |         res.sendStatus(204); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     register | ||||||
|  | }; | ||||||
| @@ -68,7 +68,7 @@ function register(router) { | |||||||
|     eu.route(router, 'delete' ,'/etapi/attributes/:attributeId', (req, res, next) => { |     eu.route(router, 'delete' ,'/etapi/attributes/:attributeId', (req, res, next) => { | ||||||
|         const attribute = becca.getAttribute(req.params.attributeId); |         const attribute = becca.getAttribute(req.params.attributeId); | ||||||
|  |  | ||||||
|         if (!attribute || attribute.isDeleted) { |         if (!attribute) { | ||||||
|             return res.sendStatus(204); |             return res.sendStatus(204); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ function register(router) { | |||||||
|     eu.route(router, 'delete' ,'/etapi/branches/:branchId', (req, res, next) => { |     eu.route(router, 'delete' ,'/etapi/branches/:branchId', (req, res, next) => { | ||||||
|         const branch = becca.getBranch(req.params.branchId); |         const branch = becca.getBranch(req.params.branchId); | ||||||
|  |  | ||||||
|         if (!branch || branch.isDeleted) { |         if (!branch) { | ||||||
|             return res.sendStatus(204); |             return res.sendStatus(204); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -77,7 +77,18 @@ function getAndCheckNote(noteId) { | |||||||
|         return note; |         return note; | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|         throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found`); |         throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getAndCheckAttachment(attachmentId) { | ||||||
|  |     const attachment = becca.getAttachment(attachmentId, {includeContentLength: true}); | ||||||
|  |  | ||||||
|  |     if (attachment) { | ||||||
|  |         return attachment; | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -88,7 +99,7 @@ function getAndCheckBranch(branchId) { | |||||||
|         return branch; |         return branch; | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|         throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found`); |         throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -99,7 +110,7 @@ function getAndCheckAttribute(attributeId) { | |||||||
|         return attribute; |         return attribute; | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|         throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found`); |         throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -113,7 +124,7 @@ function validateAndPatch(target, source, allowedProperties) { | |||||||
|                 const validationResult = validator(source[key]); |                 const validationResult = validator(source[key]); | ||||||
|  |  | ||||||
|                 if (validationResult) { |                 if (validationResult) { | ||||||
|                     throw new EtapiError(400, "PROPERTY_VALIDATION_ERROR", `Validation failed on property '${key}': ${validationResult}`); |                     throw new EtapiError(400, "PROPERTY_VALIDATION_ERROR", `Validation failed on property '${key}': ${validationResult}.`); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -134,5 +145,6 @@ module.exports = { | |||||||
|     validateAndPatch, |     validateAndPatch, | ||||||
|     getAndCheckNote, |     getAndCheckNote, | ||||||
|     getAndCheckBranch, |     getAndCheckBranch, | ||||||
|     getAndCheckAttribute |     getAndCheckAttribute, | ||||||
|  |     getAndCheckAttachment | ||||||
| } | } | ||||||
|   | |||||||
| @@ -46,8 +46,26 @@ function mapAttributeToPojo(attr) { | |||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** @param {BAttachment} attachment */ | ||||||
|  | function mapAttachmentToPojo(attachment) { | ||||||
|  |     return { | ||||||
|  |         attachmentId: attachment.attachmentId, | ||||||
|  |         parentId: attachment.parentId, | ||||||
|  |         role: attachment.role, | ||||||
|  |         mime: attachment.mime, | ||||||
|  |         title: attachment.title, | ||||||
|  |         position: attachment.position, | ||||||
|  |         blobId: attachment.blobId, | ||||||
|  |         dateModified: attachment.dateModified, | ||||||
|  |         utcDateModified: attachment.utcDateModified, | ||||||
|  |         utcDateScheduledForErasureSince: attachment.utcDateScheduledForErasureSince, | ||||||
|  |         contentLength: attachment.contentLength | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     mapNoteToPojo, |     mapNoteToPojo, | ||||||
|     mapBranchToPojo, |     mapBranchToPojo, | ||||||
|     mapAttributeToPojo |     mapAttributeToPojo, | ||||||
|  |     mapAttachmentToPojo | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ function register(router) { | |||||||
|         const {search} = req.query; |         const {search} = req.query; | ||||||
|  |  | ||||||
|         if (!search?.trim()) { |         if (!search?.trim()) { | ||||||
|             throw new eu.EtapiError(400, 'SEARCH_QUERY_PARAM_MANDATORY', "'search' query parameter is mandatory"); |             throw new eu.EtapiError(400, 'SEARCH_QUERY_PARAM_MANDATORY', "'search' query parameter is mandatory."); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const searchParams = parseSearchParams(req); |         const searchParams = parseSearchParams(req); | ||||||
| @@ -78,10 +78,10 @@ function register(router) { | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     eu.route(router, 'patch' ,'/etapi/notes/:noteId', (req, res, next) => { |     eu.route(router, 'patch' ,'/etapi/notes/:noteId', (req, res, next) => { | ||||||
|         const note = eu.getAndCheckNote(req.params.noteId) |         const note = eu.getAndCheckNote(req.params.noteId); | ||||||
|  |  | ||||||
|         if (note.isProtected) { |         if (note.isProtected) { | ||||||
|             throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI`); |             throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI.`); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         eu.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH); |         eu.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH); | ||||||
| @@ -95,7 +95,7 @@ function register(router) { | |||||||
|  |  | ||||||
|         const note = becca.getNote(noteId); |         const note = becca.getNote(noteId); | ||||||
|  |  | ||||||
|         if (!note || note.isDeleted) { |         if (!note) { | ||||||
|             return res.sendStatus(204); |             return res.sendStatus(204); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -107,6 +107,10 @@ function register(router) { | |||||||
|     eu.route(router, 'get', '/etapi/notes/:noteId/content', (req, res, next) => { |     eu.route(router, 'get', '/etapi/notes/:noteId/content', (req, res, next) => { | ||||||
|         const note = eu.getAndCheckNote(req.params.noteId); |         const note = eu.getAndCheckNote(req.params.noteId); | ||||||
|  |  | ||||||
|  |         if (note.isProtected) { | ||||||
|  |             throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and content cannot be read through ETAPI.`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         const filename = utils.formatDownloadTitle(note.title, note.type, note.mime); |         const filename = utils.formatDownloadTitle(note.title, note.type, note.mime); | ||||||
|  |  | ||||||
|         res.setHeader('Content-Disposition', utils.getContentDisposition(filename)); |         res.setHeader('Content-Disposition', utils.getContentDisposition(filename)); | ||||||
| @@ -120,6 +124,10 @@ function register(router) { | |||||||
|     eu.route(router, 'put', '/etapi/notes/:noteId/content', (req, res, next) => { |     eu.route(router, 'put', '/etapi/notes/:noteId/content', (req, res, next) => { | ||||||
|         const note = eu.getAndCheckNote(req.params.noteId); |         const note = eu.getAndCheckNote(req.params.noteId); | ||||||
|  |  | ||||||
|  |         if (note.isProtected) { | ||||||
|  |             throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI.`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         note.setContent(req.body); |         note.setContent(req.body); | ||||||
|  |  | ||||||
|         noteService.asyncPostProcessContent(note, req.body); |         noteService.asyncPostProcessContent(note, req.body); | ||||||
| @@ -132,7 +140,7 @@ function register(router) { | |||||||
|         const format = req.query.format || "html"; |         const format = req.query.format || "html"; | ||||||
|  |  | ||||||
|         if (!["html", "markdown"].includes(format)) { |         if (!["html", "markdown"].includes(format)) { | ||||||
|             throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'`); |             throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'.`); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const taskContext = new TaskContext('no-progress-reporting'); |         const taskContext = new TaskContext('no-progress-reporting'); | ||||||
| @@ -153,6 +161,15 @@ function register(router) { | |||||||
|  |  | ||||||
|         return res.sendStatus(204); |         return res.sendStatus(204); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     eu.route(router, 'get', '/etapi/notes/:noteId/attachments', (req, res, next) => { | ||||||
|  |         const note = eu.getAndCheckNote(req.params.noteId); | ||||||
|  |         const attachments = note.getAttachments({includeContentLength: true}) | ||||||
|  |  | ||||||
|  |         res.json( | ||||||
|  |             attachments.map(attachment => mappers.mapAttachmentToPojo(attachment)) | ||||||
|  |         ); | ||||||
|  |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function parseSearchParams(req) { | function parseSearchParams(req) { | ||||||
| @@ -186,7 +203,7 @@ function parseBoolean(obj, name) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!['true', 'false'].includes(obj[name])) { |     if (!['true', 'false'].includes(obj[name])) { | ||||||
|         throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse boolean '${name}' value '${obj[name]}, allowed values are 'true' and 'false'`); |         throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse boolean '${name}' value '${obj[name]}, allowed values are 'true' and 'false'.`); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return obj[name] === 'true'; |     return obj[name] === 'true'; | ||||||
| @@ -200,7 +217,7 @@ function parseOrderDirection(obj, name) { | |||||||
|     const integer = parseInt(obj[name]); |     const integer = parseInt(obj[name]); | ||||||
|  |  | ||||||
|     if (!['asc', 'desc'].includes(obj[name])) { |     if (!['asc', 'desc'].includes(obj[name])) { | ||||||
|         throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse order direction value '${obj[name]}, allowed values are 'asc' and 'desc'`); |         throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse order direction value '${obj[name]}, allowed values are 'asc' and 'desc'.`); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return integer; |     return integer; | ||||||
| @@ -214,7 +231,7 @@ function parseInteger(obj, name) { | |||||||
|     const integer = parseInt(obj[name]); |     const integer = parseInt(obj[name]); | ||||||
|  |  | ||||||
|     if (Number.isNaN(integer)) { |     if (Number.isNaN(integer)) { | ||||||
|         throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse integer '${name}' value '${obj[name]}`); |         throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse integer '${name}' value '${obj[name]}'.`); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return integer; |     return integer; | ||||||
|   | |||||||
| @@ -149,7 +149,7 @@ function getEditedNotesOnDate(req) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     return notes.map(note => { |     return notes.map(note => { | ||||||
|         const notePath = note.isDeleted ? null : getNotePathData(note); |         const notePath = getNotePathData(note); | ||||||
|  |  | ||||||
|         const notePojo = note.getPojo(); |         const notePojo = note.getPojo(); | ||||||
|         notePojo.notePath = notePath ? notePath.notePath : null; |         notePojo.notePath = notePath ? notePath.notePath : null; | ||||||
|   | |||||||
| @@ -11,8 +11,8 @@ const ValidationError = require("../../errors/validation_error"); | |||||||
| function searchFromNote(req) { | function searchFromNote(req) { | ||||||
|     const note = becca.getNoteOrThrow(req.params.noteId); |     const note = becca.getNoteOrThrow(req.params.noteId); | ||||||
|  |  | ||||||
|     if (note.isDeleted) { |     if (!note) { | ||||||
|         // this can be triggered from recent changes, and it's harmless to return empty list rather than fail |         // this can be triggered from recent changes, and it's harmless to return an empty list rather than fail | ||||||
|         return []; |         return []; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -26,8 +26,8 @@ function searchFromNote(req) { | |||||||
| function searchAndExecute(req) { | function searchAndExecute(req) { | ||||||
|     const note = becca.getNoteOrThrow(req.params.noteId); |     const note = becca.getNoteOrThrow(req.params.noteId); | ||||||
|  |  | ||||||
|     if (note.isDeleted) { |     if (!note) { | ||||||
|         // this can be triggered from recent changes, and it's harmless to return empty list rather than fail |         // this can be triggered from recent changes, and it's harmless to return an empty list rather than fail | ||||||
|         return []; |         return []; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -63,6 +63,7 @@ const shareRoutes = require('../share/routes'); | |||||||
|  |  | ||||||
| const etapiAuthRoutes = require('../etapi/auth'); | const etapiAuthRoutes = require('../etapi/auth'); | ||||||
| const etapiAppInfoRoutes = require('../etapi/app_info'); | const etapiAppInfoRoutes = require('../etapi/app_info'); | ||||||
|  | const etapiAttachmentRoutes = require('../etapi/attachments'); | ||||||
| const etapiAttributeRoutes = require('../etapi/attributes'); | const etapiAttributeRoutes = require('../etapi/attributes'); | ||||||
| const etapiBranchRoutes = require('../etapi/branches'); | const etapiBranchRoutes = require('../etapi/branches'); | ||||||
| const etapiNoteRoutes = require('../etapi/notes'); | const etapiNoteRoutes = require('../etapi/notes'); | ||||||
| @@ -332,6 +333,7 @@ function register(app) { | |||||||
|  |  | ||||||
|     etapiAuthRoutes.register(router, [loginRateLimiter]); |     etapiAuthRoutes.register(router, [loginRateLimiter]); | ||||||
|     etapiAppInfoRoutes.register(router); |     etapiAppInfoRoutes.register(router); | ||||||
|  |     etapiAttachmentRoutes.register(router); | ||||||
|     etapiAttributeRoutes.register(router); |     etapiAttributeRoutes.register(router); | ||||||
|     etapiBranchRoutes.register(router); |     etapiBranchRoutes.register(router); | ||||||
|     etapiNoteRoutes.register(router); |     etapiNoteRoutes.register(router); | ||||||
|   | |||||||
| @@ -134,7 +134,7 @@ function executeActions(note, searchResultNoteIds) { | |||||||
|     for (const resultNoteId of searchResultNoteIds) { |     for (const resultNoteId of searchResultNoteIds) { | ||||||
|         const resultNote = becca.getNote(resultNoteId); |         const resultNote = becca.getNote(resultNoteId); | ||||||
|  |  | ||||||
|         if (!resultNote || resultNote.isDeleted) { |         if (!resultNote) { | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,6 +9,10 @@ const beccaService = require("../becca/becca_service"); | |||||||
| const log = require("./log"); | const log = require("./log"); | ||||||
|  |  | ||||||
| function cloneNoteToParentNote(noteId, parentNoteId, prefix) { | function cloneNoteToParentNote(noteId, parentNoteId, prefix) { | ||||||
|  |     if (!(noteId in becca.notes) || !(parentNoteId in becca.notes)) { | ||||||
|  |         return { success: false, message: 'Note cannot be cloned because either the cloned note or the intended parent is deleted.' }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const parentNote = becca.getNote(parentNoteId); |     const parentNote = becca.getNote(parentNoteId); | ||||||
|  |  | ||||||
|     if (parentNote.type === 'search') { |     if (parentNote.type === 'search') { | ||||||
| @@ -18,10 +22,6 @@ function cloneNoteToParentNote(noteId, parentNoteId, prefix) { | |||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) { |  | ||||||
|         return { success: false, message: 'Note cannot be cloned because either the cloned note or the intended parent is deleted.' }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const validationResult = treeService.validateParentChild(parentNoteId, noteId); |     const validationResult = treeService.validateParentChild(parentNoteId, noteId); | ||||||
|  |  | ||||||
|     if (!validationResult.success) { |     if (!validationResult.success) { | ||||||
| @@ -174,12 +174,6 @@ function cloneNoteAfter(noteId, afterBranchId) { | |||||||
|     return { success: true, branchId: branch.branchId }; |     return { success: true, branchId: branch.branchId }; | ||||||
| } | } | ||||||
|  |  | ||||||
| function isNoteDeleted(noteId) { |  | ||||||
|     const note = becca.getNote(noteId); |  | ||||||
|  |  | ||||||
|     return !note || note.isDeleted; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     cloneNoteToBranch, |     cloneNoteToBranch, | ||||||
|     cloneNoteToParentNote, |     cloneNoteToParentNote, | ||||||
|   | |||||||
| @@ -607,16 +607,16 @@ class ConsistencyChecks { | |||||||
|             WHERE  |             WHERE  | ||||||
|               entity_changes.id IS NULL`, |               entity_changes.id IS NULL`, | ||||||
|             ({entityId}) => { |             ({entityId}) => { | ||||||
|                 const entity = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]); |                 const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]); | ||||||
|  |  | ||||||
|                 if (this.autoFix) { |                 if (this.autoFix) { | ||||||
|                     entityChangesService.addEntityChange({ |                     entityChangesService.addEntityChange({ | ||||||
|                         entityName, |                         entityName, | ||||||
|                         entityId, |                         entityId, | ||||||
|                         hash: utils.randomString(10), // doesn't matter, will force sync, but that's OK |                         hash: utils.randomString(10), // doesn't matter, will force sync, but that's OK | ||||||
|                         isErased: !!entity.isErased, |                         isErased: !!entityRow.isErased, | ||||||
|                         utcDateChanged: entity.utcDateModified || entity.utcDateCreated, |                         utcDateChanged: entityRow.utcDateModified || entityRow.utcDateCreated, | ||||||
|                         isSynced: entityName !== 'options' || entity.isSynced |                         isSynced: entityName !== 'options' || entityRow.isSynced | ||||||
|                     }); |                     }); | ||||||
|  |  | ||||||
|                     logFix(`Created missing entity change for entityName '${entityName}', entityId '${entityId}'`); |                     logFix(`Created missing entity change for entityName '${entityName}', entityId '${entityId}'`); | ||||||
|   | |||||||
| @@ -570,7 +570,7 @@ function downloadImages(noteId, content) { | |||||||
|                 for (const url in imageUrlToAttachmentIdMapping) { |                 for (const url in imageUrlToAttachmentIdMapping) { | ||||||
|                     const imageNote = imageNotes.find(note => note.noteId === imageUrlToAttachmentIdMapping[url]); |                     const imageNote = imageNotes.find(note => note.noteId === imageUrlToAttachmentIdMapping[url]); | ||||||
|  |  | ||||||
|                     if (imageNote && !imageNote.isDeleted) { |                     if (imageNote) { | ||||||
|                         updatedContent = replaceUrl(updatedContent, url, imageNote); |                         updatedContent = replaceUrl(updatedContent, url, imageNote); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| @@ -697,14 +697,14 @@ function updateNoteData(noteId, content) { | |||||||
|  * @param {TaskContext} taskContext |  * @param {TaskContext} taskContext | ||||||
|  */ |  */ | ||||||
| function undeleteNote(noteId, taskContext) { | function undeleteNote(noteId, taskContext) { | ||||||
|     const note = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); |     const noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||||
|  |  | ||||||
|     if (!note.isDeleted) { |     if (!noteRow.isDeleted) { | ||||||
|         log.error(`Note '${noteId}' is not deleted and thus cannot be undeleted.`); |         log.error(`Note '${noteId}' is not deleted and thus cannot be undeleted.`); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const undeletedParentBranchIds = getUndeletedParentBranchIds(noteId, note.deleteId); |     const undeletedParentBranchIds = getUndeletedParentBranchIds(noteId, noteRow.deleteId); | ||||||
|  |  | ||||||
|     if (undeletedParentBranchIds.length === 0) { |     if (undeletedParentBranchIds.length === 0) { | ||||||
|         // cannot undelete if there's no undeleted parent |         // cannot undelete if there's no undeleted parent | ||||||
| @@ -712,7 +712,7 @@ function undeleteNote(noteId, taskContext) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     for (const parentBranchId of undeletedParentBranchIds) { |     for (const parentBranchId of undeletedParentBranchIds) { | ||||||
|         undeleteBranch(parentBranchId, note.deleteId, taskContext); |         undeleteBranch(parentBranchId, noteRow.deleteId, taskContext); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -722,38 +722,38 @@ function undeleteNote(noteId, taskContext) { | |||||||
|  * @param {TaskContext} taskContext |  * @param {TaskContext} taskContext | ||||||
|  */ |  */ | ||||||
| function undeleteBranch(branchId, deleteId, taskContext) { | function undeleteBranch(branchId, deleteId, taskContext) { | ||||||
|     const branch = sql.getRow("SELECT * FROM branches WHERE branchId = ?", [branchId]) |     const branchRow = sql.getRow("SELECT * FROM branches WHERE branchId = ?", [branchId]) | ||||||
|  |  | ||||||
|     if (!branch.isDeleted) { |     if (!branchRow.isDeleted) { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const note = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [branch.noteId]); |     const noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [branchRow.noteId]); | ||||||
|  |  | ||||||
|     if (note.isDeleted && note.deleteId !== deleteId) { |     if (noteRow.isDeleted && noteRow.deleteId !== deleteId) { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     new BBranch(branch).save(); |     new BBranch(branchRow).save(); | ||||||
|  |  | ||||||
|     taskContext.increaseProgressCount(); |     taskContext.increaseProgressCount(); | ||||||
|  |  | ||||||
|     if (note.isDeleted && note.deleteId === deleteId) { |     if (noteRow.isDeleted && noteRow.deleteId === deleteId) { | ||||||
|         // becca entity was already created as skeleton in "new Branch()" above |         // becca entity was already created as skeleton in "new Branch()" above | ||||||
|         const noteEntity = becca.getNote(note.noteId); |         const noteEntity = becca.getNote(noteRow.noteId); | ||||||
|         noteEntity.updateFromRow(note); |         noteEntity.updateFromRow(noteRow); | ||||||
|         noteEntity.save(); |         noteEntity.save(); | ||||||
|  |  | ||||||
|         const attributes = sql.getRows(` |         const attributeRows = sql.getRows(` | ||||||
|                 SELECT * FROM attributes  |                 SELECT * FROM attributes  | ||||||
|                 WHERE isDeleted = 1  |                 WHERE isDeleted = 1  | ||||||
|                   AND deleteId = ?  |                   AND deleteId = ?  | ||||||
|                   AND (noteId = ?  |                   AND (noteId = ?  | ||||||
|                            OR (type = 'relation' AND value = ?))`, [deleteId, note.noteId, note.noteId]); |                            OR (type = 'relation' AND value = ?))`, [deleteId, noteRow.noteId, noteRow.noteId]); | ||||||
|  |  | ||||||
|         for (const attribute of attributes) { |         for (const attributeRow of attributeRows) { | ||||||
|             // relation might point to a note which hasn't been undeleted yet and would thus throw up |             // relation might point to a note which hasn't been undeleted yet and would thus throw up | ||||||
|             new BAttribute(attribute).save({skipValidation: true}); |             new BAttribute(attributeRow).save({skipValidation: true}); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const childBranchIds = sql.getColumn(` |         const childBranchIds = sql.getColumn(` | ||||||
| @@ -761,7 +761,7 @@ function undeleteBranch(branchId, deleteId, taskContext) { | |||||||
|             FROM branches |             FROM branches | ||||||
|             WHERE branches.isDeleted = 1 |             WHERE branches.isDeleted = 1 | ||||||
|               AND branches.deleteId = ? |               AND branches.deleteId = ? | ||||||
|               AND branches.parentNoteId = ?`, [deleteId, note.noteId]); |               AND branches.parentNoteId = ?`, [deleteId, noteRow.noteId]); | ||||||
|  |  | ||||||
|         for (const childBranchId of childBranchIds) { |         for (const childBranchId of childBranchIds) { | ||||||
|             undeleteBranch(childBranchId, deleteId, taskContext); |             undeleteBranch(childBranchId, deleteId, taskContext); | ||||||
|   | |||||||
| @@ -155,7 +155,6 @@ function findResultsWithExpression(expression, searchContext) { | |||||||
|     const noteSet = expression.execute(allNoteSet, executionContext, searchContext); |     const noteSet = expression.execute(allNoteSet, executionContext, searchContext); | ||||||
|  |  | ||||||
|     const searchResults = noteSet.notes |     const searchResults = noteSet.notes | ||||||
|         .filter(note => !note.isDeleted) |  | ||||||
|         .map(note => { |         .map(note => { | ||||||
|             const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath(); |             const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -315,21 +315,21 @@ function getEntityChangeRow(entityName, entityId) { | |||||||
|             throw new Error(`Unknown entity '${entityName}'`); |             throw new Error(`Unknown entity '${entityName}'`); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const entity = sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]); |         const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]); | ||||||
|  |  | ||||||
|         if (!entity) { |         if (!entityRow) { | ||||||
|             throw new Error(`Entity ${entityName} '${entityId}' not found.`); |             throw new Error(`Entity ${entityName} '${entityId}' not found.`); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (entityName === 'blobs' && entity.content !== null) { |         if (entityName === 'blobs' && entityRow.content !== null) { | ||||||
|             if (typeof entity.content === 'string') { |             if (typeof entityRow.content === 'string') { | ||||||
|                 entity.content = Buffer.from(entity.content, 'utf-8'); |                 entityRow.content = Buffer.from(entityRow.content, 'utf-8'); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             entity.content = entity.content.toString("base64"); |             entityRow.content = entityRow.content.toString("base64"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return entity; |         return entityRow; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -199,7 +199,7 @@ function sortNotesIfNeeded(parentNoteId) { | |||||||
| function setNoteToParent(noteId, prefix, parentNoteId) { | function setNoteToParent(noteId, prefix, parentNoteId) { | ||||||
|     const parentNote = becca.getNote(parentNoteId); |     const parentNote = becca.getNote(parentNoteId); | ||||||
|  |  | ||||||
|     if (parentNote && parentNote.isDeleted) { |     if (!parentNote) { | ||||||
|         throw new Error(`Cannot move note to deleted parent note '${parentNoteId}'`); |         throw new Error(`Cannot move note to deleted parent note '${parentNoteId}'`); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -209,7 +209,7 @@ function setNoteToParent(noteId, prefix, parentNoteId) { | |||||||
|  |  | ||||||
|     if (branch) { |     if (branch) { | ||||||
|         if (!parentNoteId) { |         if (!parentNoteId) { | ||||||
|             log.info(`Removing note ${noteId} from parent ${parentNoteId}`); |             log.info(`Removing note '${noteId}' from parent '${parentNoteId}'`); | ||||||
|  |  | ||||||
|             branch.markAsDeleted(); |             branch.markAsDeleted(); | ||||||
|         } |         } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user