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) { | ||||
|         console.log(`Reading note '${noteId}'`); | ||||
|  | ||||
|         let childTargetPath, note, fileNameWithPath; | ||||
|         let childTargetPath, noteRow, fileNameWithPath; | ||||
|  | ||||
|         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++; | ||||
|  | ||||
|                 if (!options.includeDeleted) { | ||||
| @@ -44,13 +44,13 @@ function dumpDocument(documentPath, targetPath, options) { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (note.isProtected) { | ||||
|             if (noteRow.isProtected) { | ||||
|                 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) { | ||||
|                 safeTitle = safeTitle.substring(0, 20); | ||||
| @@ -64,8 +64,8 @@ function dumpDocument(documentPath, targetPath, options) { | ||||
|  | ||||
|             existingPaths[childTargetPath] = true; | ||||
|  | ||||
|             if (note.noteId in noteIdToPath) { | ||||
|                 const message = `Note '${noteId}' has been already dumped to ${noteIdToPath[note.noteId]}`; | ||||
|             if (noteRow.noteId in noteIdToPath) { | ||||
|                 const message = `Note '${noteId}' has been already dumped to ${noteIdToPath[noteRow.noteId]}`; | ||||
|  | ||||
|                 console.log(message); | ||||
|  | ||||
| @@ -74,16 +74,16 @@ function dumpDocument(documentPath, targetPath, options) { | ||||
|                 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); | ||||
|             } | ||||
|  | ||||
|             if (isContentEmpty(content)) { | ||||
|                 console.log(`Note '${noteId}' is empty, skipping.`); | ||||
|             } else { | ||||
|                 fileNameWithPath = extensionService.getFileName(note, childTargetPath, safeTitle); | ||||
|                 fileNameWithPath = extensionService.getFileName(noteRow, childTargetPath, safeTitle); | ||||
|  | ||||
|                 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 | ||||
|  | ||||
|             log.info(`Deleting note ${note.noteId}`); | ||||
|             log.info(`Deleting note '${note.noteId}'`); | ||||
|  | ||||
|             this.becca.notes[note.noteId].isBeingDeleted = true; | ||||
|  | ||||
|   | ||||
| @@ -1549,6 +1549,8 @@ class BNote extends AbstractBeccaEntity { | ||||
|     } | ||||
|  | ||||
|     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; | ||||
|     } | ||||
|  | ||||
| @@ -1602,7 +1604,7 @@ class BNote extends AbstractBeccaEntity { | ||||
|     /** | ||||
|      * @returns {BAttachment} | ||||
|      */ | ||||
|     saveAttachment({attachmentId, role, mime, title, content}) { | ||||
|     saveAttachment({attachmentId, role, mime, title, content, position}) { | ||||
|         let attachment; | ||||
|  | ||||
|         if (attachmentId) { | ||||
| @@ -1613,15 +1615,13 @@ class BNote extends AbstractBeccaEntity { | ||||
|                 title, | ||||
|                 role, | ||||
|                 mime, | ||||
|                 isProtected: this.isProtected | ||||
|                 isProtected: this.isProtected, | ||||
|                 position | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (content !== undefined && content !== null) { | ||||
|             attachment.setContent(content, {forceSave: true}); | ||||
|         } else { | ||||
|             attachment.save(); | ||||
|         } | ||||
|         content = content || ""; | ||||
|         attachment.setContent(content, {forceSave: true}); | ||||
|  | ||||
|         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) => { | ||||
|         const attribute = becca.getAttribute(req.params.attributeId); | ||||
|  | ||||
|         if (!attribute || attribute.isDeleted) { | ||||
|         if (!attribute) { | ||||
|             return res.sendStatus(204); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -64,7 +64,7 @@ function register(router) { | ||||
|     eu.route(router, 'delete' ,'/etapi/branches/:branchId', (req, res, next) => { | ||||
|         const branch = becca.getBranch(req.params.branchId); | ||||
|  | ||||
|         if (!branch || branch.isDeleted) { | ||||
|         if (!branch) { | ||||
|             return res.sendStatus(204); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -77,7 +77,18 @@ function getAndCheckNote(noteId) { | ||||
|         return note; | ||||
|     } | ||||
|     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; | ||||
|     } | ||||
|     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; | ||||
|     } | ||||
|     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]); | ||||
|  | ||||
|                 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, | ||||
|     getAndCheckNote, | ||||
|     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 = { | ||||
|     mapNoteToPojo, | ||||
|     mapBranchToPojo, | ||||
|     mapAttributeToPojo | ||||
|     mapAttributeToPojo, | ||||
|     mapAttachmentToPojo | ||||
| }; | ||||
|   | ||||
| @@ -14,7 +14,7 @@ function register(router) { | ||||
|         const {search} = req.query; | ||||
|  | ||||
|         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); | ||||
| @@ -78,10 +78,10 @@ function register(router) { | ||||
|     }; | ||||
|  | ||||
|     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) { | ||||
|             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); | ||||
| @@ -95,7 +95,7 @@ function register(router) { | ||||
|  | ||||
|         const note = becca.getNote(noteId); | ||||
|  | ||||
|         if (!note || note.isDeleted) { | ||||
|         if (!note) { | ||||
|             return res.sendStatus(204); | ||||
|         } | ||||
|  | ||||
| @@ -107,6 +107,10 @@ function register(router) { | ||||
|     eu.route(router, 'get', '/etapi/notes/:noteId/content', (req, res, next) => { | ||||
|         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); | ||||
|  | ||||
|         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) => { | ||||
|         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); | ||||
|  | ||||
|         noteService.asyncPostProcessContent(note, req.body); | ||||
| @@ -132,7 +140,7 @@ function register(router) { | ||||
|         const format = req.query.format || "html"; | ||||
|  | ||||
|         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'); | ||||
| @@ -153,6 +161,15 @@ function register(router) { | ||||
|  | ||||
|         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) { | ||||
| @@ -186,7 +203,7 @@ function parseBoolean(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'; | ||||
| @@ -200,7 +217,7 @@ function parseOrderDirection(obj, name) { | ||||
|     const integer = parseInt(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; | ||||
| @@ -214,7 +231,7 @@ function parseInteger(obj, name) { | ||||
|     const integer = parseInt(obj[name]); | ||||
|  | ||||
|     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; | ||||
|   | ||||
| @@ -149,7 +149,7 @@ function getEditedNotesOnDate(req) { | ||||
|     } | ||||
|  | ||||
|     return notes.map(note => { | ||||
|         const notePath = note.isDeleted ? null : getNotePathData(note); | ||||
|         const notePath = getNotePathData(note); | ||||
|  | ||||
|         const notePojo = note.getPojo(); | ||||
|         notePojo.notePath = notePath ? notePath.notePath : null; | ||||
|   | ||||
| @@ -11,8 +11,8 @@ const ValidationError = require("../../errors/validation_error"); | ||||
| function searchFromNote(req) { | ||||
|     const note = becca.getNoteOrThrow(req.params.noteId); | ||||
|  | ||||
|     if (note.isDeleted) { | ||||
|         // this can be triggered from recent changes, and it's harmless to return empty list rather than fail | ||||
|     if (!note) { | ||||
|         // this can be triggered from recent changes, and it's harmless to return an empty list rather than fail | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
| @@ -26,8 +26,8 @@ function searchFromNote(req) { | ||||
| function searchAndExecute(req) { | ||||
|     const note = becca.getNoteOrThrow(req.params.noteId); | ||||
|  | ||||
|     if (note.isDeleted) { | ||||
|         // this can be triggered from recent changes, and it's harmless to return empty list rather than fail | ||||
|     if (!note) { | ||||
|         // this can be triggered from recent changes, and it's harmless to return an empty list rather than fail | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -63,6 +63,7 @@ const shareRoutes = require('../share/routes'); | ||||
|  | ||||
| const etapiAuthRoutes = require('../etapi/auth'); | ||||
| const etapiAppInfoRoutes = require('../etapi/app_info'); | ||||
| const etapiAttachmentRoutes = require('../etapi/attachments'); | ||||
| const etapiAttributeRoutes = require('../etapi/attributes'); | ||||
| const etapiBranchRoutes = require('../etapi/branches'); | ||||
| const etapiNoteRoutes = require('../etapi/notes'); | ||||
| @@ -332,6 +333,7 @@ function register(app) { | ||||
|  | ||||
|     etapiAuthRoutes.register(router, [loginRateLimiter]); | ||||
|     etapiAppInfoRoutes.register(router); | ||||
|     etapiAttachmentRoutes.register(router); | ||||
|     etapiAttributeRoutes.register(router); | ||||
|     etapiBranchRoutes.register(router); | ||||
|     etapiNoteRoutes.register(router); | ||||
|   | ||||
| @@ -134,7 +134,7 @@ function executeActions(note, searchResultNoteIds) { | ||||
|     for (const resultNoteId of searchResultNoteIds) { | ||||
|         const resultNote = becca.getNote(resultNoteId); | ||||
|  | ||||
|         if (!resultNote || resultNote.isDeleted) { | ||||
|         if (!resultNote) { | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,10 @@ const beccaService = require("../becca/becca_service"); | ||||
| const log = require("./log"); | ||||
|  | ||||
| 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); | ||||
|  | ||||
|     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); | ||||
|  | ||||
|     if (!validationResult.success) { | ||||
| @@ -174,12 +174,6 @@ function cloneNoteAfter(noteId, afterBranchId) { | ||||
|     return { success: true, branchId: branch.branchId }; | ||||
| } | ||||
|  | ||||
| function isNoteDeleted(noteId) { | ||||
|     const note = becca.getNote(noteId); | ||||
|  | ||||
|     return !note || note.isDeleted; | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     cloneNoteToBranch, | ||||
|     cloneNoteToParentNote, | ||||
|   | ||||
| @@ -607,16 +607,16 @@ class ConsistencyChecks { | ||||
|             WHERE  | ||||
|               entity_changes.id IS NULL`, | ||||
|             ({entityId}) => { | ||||
|                 const entity = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]); | ||||
|                 const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]); | ||||
|  | ||||
|                 if (this.autoFix) { | ||||
|                     entityChangesService.addEntityChange({ | ||||
|                         entityName, | ||||
|                         entityId, | ||||
|                         hash: utils.randomString(10), // doesn't matter, will force sync, but that's OK | ||||
|                         isErased: !!entity.isErased, | ||||
|                         utcDateChanged: entity.utcDateModified || entity.utcDateCreated, | ||||
|                         isSynced: entityName !== 'options' || entity.isSynced | ||||
|                         isErased: !!entityRow.isErased, | ||||
|                         utcDateChanged: entityRow.utcDateModified || entityRow.utcDateCreated, | ||||
|                         isSynced: entityName !== 'options' || entityRow.isSynced | ||||
|                     }); | ||||
|  | ||||
|                     logFix(`Created missing entity change for entityName '${entityName}', entityId '${entityId}'`); | ||||
|   | ||||
| @@ -570,7 +570,7 @@ function downloadImages(noteId, content) { | ||||
|                 for (const url in imageUrlToAttachmentIdMapping) { | ||||
|                     const imageNote = imageNotes.find(note => note.noteId === imageUrlToAttachmentIdMapping[url]); | ||||
|  | ||||
|                     if (imageNote && !imageNote.isDeleted) { | ||||
|                     if (imageNote) { | ||||
|                         updatedContent = replaceUrl(updatedContent, url, imageNote); | ||||
|                     } | ||||
|                 } | ||||
| @@ -697,14 +697,14 @@ function updateNoteData(noteId, content) { | ||||
|  * @param {TaskContext} 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.`); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const undeletedParentBranchIds = getUndeletedParentBranchIds(noteId, note.deleteId); | ||||
|     const undeletedParentBranchIds = getUndeletedParentBranchIds(noteId, noteRow.deleteId); | ||||
|  | ||||
|     if (undeletedParentBranchIds.length === 0) { | ||||
|         // cannot undelete if there's no undeleted parent | ||||
| @@ -712,7 +712,7 @@ function undeleteNote(noteId, taskContext) { | ||||
|     } | ||||
|  | ||||
|     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 | ||||
|  */ | ||||
| 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; | ||||
|     } | ||||
|  | ||||
|     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; | ||||
|     } | ||||
|  | ||||
|     new BBranch(branch).save(); | ||||
|     new BBranch(branchRow).save(); | ||||
|  | ||||
|     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 | ||||
|         const noteEntity = becca.getNote(note.noteId); | ||||
|         noteEntity.updateFromRow(note); | ||||
|         const noteEntity = becca.getNote(noteRow.noteId); | ||||
|         noteEntity.updateFromRow(noteRow); | ||||
|         noteEntity.save(); | ||||
|  | ||||
|         const attributes = sql.getRows(` | ||||
|         const attributeRows = sql.getRows(` | ||||
|                 SELECT * FROM attributes  | ||||
|                 WHERE isDeleted = 1  | ||||
|                   AND deleteId = ?  | ||||
|                   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 | ||||
|             new BAttribute(attribute).save({skipValidation: true}); | ||||
|             new BAttribute(attributeRow).save({skipValidation: true}); | ||||
|         } | ||||
|  | ||||
|         const childBranchIds = sql.getColumn(` | ||||
| @@ -761,7 +761,7 @@ function undeleteBranch(branchId, deleteId, taskContext) { | ||||
|             FROM branches | ||||
|             WHERE branches.isDeleted = 1 | ||||
|               AND branches.deleteId = ? | ||||
|               AND branches.parentNoteId = ?`, [deleteId, note.noteId]); | ||||
|               AND branches.parentNoteId = ?`, [deleteId, noteRow.noteId]); | ||||
|  | ||||
|         for (const childBranchId of childBranchIds) { | ||||
|             undeleteBranch(childBranchId, deleteId, taskContext); | ||||
|   | ||||
| @@ -155,7 +155,6 @@ function findResultsWithExpression(expression, searchContext) { | ||||
|     const noteSet = expression.execute(allNoteSet, executionContext, searchContext); | ||||
|  | ||||
|     const searchResults = noteSet.notes | ||||
|         .filter(note => !note.isDeleted) | ||||
|         .map(note => { | ||||
|             const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath(); | ||||
|  | ||||
|   | ||||
| @@ -315,21 +315,21 @@ function getEntityChangeRow(entityName, entityId) { | ||||
|             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.`); | ||||
|         } | ||||
|  | ||||
|         if (entityName === 'blobs' && entity.content !== null) { | ||||
|             if (typeof entity.content === 'string') { | ||||
|                 entity.content = Buffer.from(entity.content, 'utf-8'); | ||||
|         if (entityName === 'blobs' && entityRow.content !== null) { | ||||
|             if (typeof entityRow.content === 'string') { | ||||
|                 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) { | ||||
|     const parentNote = becca.getNote(parentNoteId); | ||||
|  | ||||
|     if (parentNote && parentNote.isDeleted) { | ||||
|     if (!parentNote) { | ||||
|         throw new Error(`Cannot move note to deleted parent note '${parentNoteId}'`); | ||||
|     } | ||||
|  | ||||
| @@ -209,7 +209,7 @@ function setNoteToParent(noteId, prefix, parentNoteId) { | ||||
|  | ||||
|     if (branch) { | ||||
|         if (!parentNoteId) { | ||||
|             log.info(`Removing note ${noteId} from parent ${parentNoteId}`); | ||||
|             log.info(`Removing note '${noteId}' from parent '${parentNoteId}'`); | ||||
|  | ||||
|             branch.markAsDeleted(); | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user