mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	refactorings
This commit is contained in:
		| @@ -6,6 +6,10 @@ const becca = require('../becca'); | |||||||
| const AbstractBeccaEntity = require("./abstract_becca_entity"); | const AbstractBeccaEntity = require("./abstract_becca_entity"); | ||||||
| const sql = require("../../services/sql"); | const sql = require("../../services/sql"); | ||||||
|  |  | ||||||
|  | const attachmentRoleToNoteTypeMapping = { | ||||||
|  |     'image': 'image' | ||||||
|  | }; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Attachment represent data related/attached to the note. Conceptually similar to attributes, but intended for |  * Attachment represent data related/attached to the note. Conceptually similar to attributes, but intended for | ||||||
|  * larger amounts of data and generally not accessible to the user. |  * larger amounts of data and generally not accessible to the user. | ||||||
| @@ -92,6 +96,55 @@ class BAttachment extends AbstractBeccaEntity { | |||||||
|         this._setContent(content, opts); |         this._setContent(content, opts); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @returns {{note: BNote, branch: BBranch}} | ||||||
|  |      */ | ||||||
|  |     convertToNote() { | ||||||
|  |         if (this.type === 'search') { | ||||||
|  |             throw new Error(`Note of type search cannot have child notes`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!this.getNote()) { | ||||||
|  |             throw new Error("Cannot find note of this attachment. It is possible that this is note revision's attachment. " + | ||||||
|  |                 "Converting note revision's attachments to note is not (yet) supported."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!(this.role in attachmentRoleToNoteTypeMapping)) { | ||||||
|  |             throw new Error(`Mapping from attachment role '${this.role}' to note's type is not defined`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!this.isContentAvailable()) { // isProtected is same for attachment | ||||||
|  |             throw new Error(`Cannot convert protected attachment outside of protected session`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const noteService = require('../../services/notes'); | ||||||
|  |  | ||||||
|  |         const { note, branch } = noteService.createNewNote({ | ||||||
|  |             parentNoteId: this.parentId, | ||||||
|  |             title: this.title, | ||||||
|  |             type: attachmentRoleToNoteTypeMapping[this.role], | ||||||
|  |             mime: this.mime, | ||||||
|  |             content: this.getContent(), | ||||||
|  |             isProtected: this.isProtected | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         this.markAsDeleted(); | ||||||
|  |  | ||||||
|  |         if (this.role === 'image' && this.type === 'text') { | ||||||
|  |             const origContent = this.getContent(); | ||||||
|  |             const oldAttachmentUrl = `api/attachment/${this.attachmentId}/image/`; | ||||||
|  |             const newNoteUrl = `api/images/${note.noteId}/`; | ||||||
|  |  | ||||||
|  |             const fixedContent = utils.replaceAll(origContent, oldAttachmentUrl, newNoteUrl); | ||||||
|  |  | ||||||
|  |             if (origContent !== fixedContent) { | ||||||
|  |                 this.setContent(fixedContent); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return { note, branch }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     beforeSaving() { |     beforeSaving() { | ||||||
|         super.beforeSaving(); |         super.beforeSaving(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -947,7 +947,7 @@ class BNote extends AbstractBeccaEntity { | |||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** @returns {String[]} - includes the subtree node as well */ |     /** @returns {String[]} - includes the subtree root note as well */ | ||||||
|     getSubtreeNoteIds({includeArchived = true, includeHidden = false, resolveSearch = false} = {}) { |     getSubtreeNoteIds({includeArchived = true, includeHidden = false, resolveSearch = false} = {}) { | ||||||
|         return this.getSubtree({includeArchived, includeHidden, resolveSearch}) |         return this.getSubtree({includeArchived, includeHidden, resolveSearch}) | ||||||
|             .notes |             .notes | ||||||
| @@ -1033,6 +1033,11 @@ class BNote extends AbstractBeccaEntity { | |||||||
|         return this.ancestorCache; |         return this.ancestorCache; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** @returns {string[]} */ | ||||||
|  |     getAncestorNoteIds() { | ||||||
|  |         return this.getAncestors().map(note => note.noteId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** @returns {boolean} */ |     /** @returns {boolean} */ | ||||||
|     hasAncestor(ancestorNoteId) { |     hasAncestor(ancestorNoteId) { | ||||||
|         for (const ancestorNote of this.getAncestors()) { |         for (const ancestorNote of this.getAncestors()) { | ||||||
| @@ -1408,61 +1413,6 @@ class BNote extends AbstractBeccaEntity { | |||||||
|         return attachment; |         return attachment; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @param attachmentId |  | ||||||
|      * @returns {{note: BNote, branch: BBranch}} |  | ||||||
|      */ |  | ||||||
|     convertAttachmentToChildNote(attachmentId) { |  | ||||||
|         if (this.type === 'search') { |  | ||||||
|             throw new Error(`Note of type search cannot have child notes`); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const attachment = this.getAttachmentById(attachmentId); |  | ||||||
|  |  | ||||||
|         if (!attachment) { |  | ||||||
|             throw new NotFoundError(`Attachment '${attachmentId} of note '${this.noteId}' doesn't exist.`); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const attachmentRoleToNoteTypeMapping = { |  | ||||||
|             'image': 'image' |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         if (!(attachment.role in attachmentRoleToNoteTypeMapping)) { |  | ||||||
|             throw new Error(`Mapping from attachment role '${attachment.role}' to note's type is not defined`); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!this.isContentAvailable()) { // isProtected is same for attachment |  | ||||||
|             throw new Error(`Cannot convert protected attachment outside of protected session`); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const noteService = require('../../services/notes'); |  | ||||||
|  |  | ||||||
|         const {note, branch} = noteService.createNewNote({ |  | ||||||
|             parentNoteId: this.noteId, |  | ||||||
|             title: attachment.title, |  | ||||||
|             type: attachmentRoleToNoteTypeMapping[attachment.role], |  | ||||||
|             mime: attachment.mime, |  | ||||||
|             content: attachment.getContent(), |  | ||||||
|             isProtected: this.isProtected |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         attachment.markAsDeleted(); |  | ||||||
|  |  | ||||||
|         if (attachment.role === 'image' && this.type === 'text') { |  | ||||||
|             const origContent = this.getContent(); |  | ||||||
|             const oldAttachmentUrl = `api/notes/${this.noteId}/images/${attachment.attachmentId}/`; |  | ||||||
|             const newNoteUrl = `api/images/${note.noteId}/`; |  | ||||||
|  |  | ||||||
|             const fixedContent = utils.replaceAll(origContent, oldAttachmentUrl, newNoteUrl); |  | ||||||
|  |  | ||||||
|             if (origContent !== fixedContent) { |  | ||||||
|                 this.setContent(fixedContent); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return { note, branch }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * (Soft) delete a note and all its descendants. |      * (Soft) delete a note and all its descendants. | ||||||
|      * |      * | ||||||
|   | |||||||
| @@ -63,6 +63,12 @@ class BNoteRevision extends AbstractBeccaEntity { | |||||||
|         return utils.isStringNote(this.type, this.mime); |         return utils.isStringNote(this.type, this.mime); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     isContentAvailable() { | ||||||
|  |         return !this.noteRevisionId // new note which was not encrypted yet | ||||||
|  |             || !this.isProtected | ||||||
|  |             || protectedSessionService.isProtectedSessionAvailable() | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /* |     /* | ||||||
|      * Note revision content has quite special handling - it's not a separate entity, but a lazily loaded |      * Note revision content has quite special handling - it's not a separate entity, but a lazily loaded | ||||||
|      * part of NoteRevision entity with its own sync. Reason behind this hybrid design is that |      * part of NoteRevision entity with its own sync. Reason behind this hybrid design is that | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) { | |||||||
|             source: async (term, cb) => { |             source: async (term, cb) => { | ||||||
|                 const type = typeof attributeType === "function" ? attributeType() : attributeType; |                 const type = typeof attributeType === "function" ? attributeType() : attributeType; | ||||||
|  |  | ||||||
|                 const names = await server.get(`attributes/names/?type=${type}&query=${encodeURIComponent(term)}`); |                 const names = await server.get(`attribute-names/?type=${type}&query=${encodeURIComponent(term)}`); | ||||||
|                 const result = names.map(name => ({name})); |                 const result = names.map(name => ({name})); | ||||||
|  |  | ||||||
|                 cb(result); |                 cb(result); | ||||||
| @@ -52,7 +52,7 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }) { | |||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const attributeValues = (await server.get(`attributes/values/${encodeURIComponent(attributeName)}`)) |     const attributeValues = (await server.get(`attribute-values/${encodeURIComponent(attributeName)}`)) | ||||||
|         .map(attribute => ({ value: attribute })); |         .map(attribute => ({ value: attribute })); | ||||||
|  |  | ||||||
|     if (attributeValues.length === 0) { |     if (attributeValues.length === 0) { | ||||||
|   | |||||||
| @@ -48,7 +48,7 @@ async function openNoteExternally(noteId, mime) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function downloadNoteRevision(noteId, noteRevisionId) { | function downloadNoteRevision(noteId, noteRevisionId) { | ||||||
|     const url = getUrlForDownload(`api/notes/${noteId}/revisions/${noteRevisionId}/download`); |     const url = getUrlForDownload(`api/revisions/${noteRevisionId}/download`); | ||||||
|  |  | ||||||
|     download(url); |     download(url); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -95,7 +95,7 @@ const mentionSetup = { | |||||||
|         { |         { | ||||||
|             marker: '#', |             marker: '#', | ||||||
|             feed: async queryText => { |             feed: async queryText => { | ||||||
|                 const names = await server.get(`attributes/names/?type=label&query=${encodeURIComponent(queryText)}`); |                 const names = await server.get(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`); | ||||||
|  |  | ||||||
|                 return names.map(name => { |                 return names.map(name => { | ||||||
|                     return { |                     return { | ||||||
| @@ -110,7 +110,7 @@ const mentionSetup = { | |||||||
|         { |         { | ||||||
|             marker: '~', |             marker: '~', | ||||||
|             feed: async queryText => { |             feed: async queryText => { | ||||||
|                 const names = await server.get(`attributes/names/?type=relation&query=${encodeURIComponent(queryText)}`); |                 const names = await server.get(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`); | ||||||
|  |  | ||||||
|                 return names.map(name => { |                 return names.map(name => { | ||||||
|                     return { |                     return { | ||||||
|   | |||||||
| @@ -219,7 +219,7 @@ export default class ExportDialog extends BasicWidget { | |||||||
|     exportBranch(branchId, type, format, version) { |     exportBranch(branchId, type, format, version) { | ||||||
|         this.taskId = utils.randomString(10); |         this.taskId = utils.randomString(10); | ||||||
|  |  | ||||||
|         const url = openService.getUrlForDownload(`api/notes/${branchId}/export/${type}/${format}/${version}/${this.taskId}`); |         const url = openService.getUrlForDownload(`api/branches/${branchId}/export/${type}/${format}/${version}/${this.taskId}`); | ||||||
|  |  | ||||||
|         openService.download(url); |         openService.download(url); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -188,7 +188,7 @@ export default class NoteRevisionsDialog extends BasicWidget { | |||||||
|             const text = 'Do you want to restore this revision? This will overwrite current title/content of the note with this revision.'; |             const text = 'Do you want to restore this revision? This will overwrite current title/content of the note with this revision.'; | ||||||
|  |  | ||||||
|             if (await dialogService.confirm(text)) { |             if (await dialogService.confirm(text)) { | ||||||
|                 await server.put(`notes/${revisionItem.noteId}/restore-revision/${revisionItem.noteRevisionId}`); |                 await server.post(`revisions/${revisionItem.noteRevisionId}/restore`); | ||||||
|  |  | ||||||
|                 this.$widget.modal('hide'); |                 this.$widget.modal('hide'); | ||||||
|  |  | ||||||
| @@ -202,7 +202,7 @@ export default class NoteRevisionsDialog extends BasicWidget { | |||||||
|             const text = 'Do you want to delete this revision? This action will delete revision title and content, but still preserve revision metadata.'; |             const text = 'Do you want to delete this revision? This action will delete revision title and content, but still preserve revision metadata.'; | ||||||
|  |  | ||||||
|             if (await dialogService.confirm(text)) { |             if (await dialogService.confirm(text)) { | ||||||
|                 await server.remove(`notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}`); |                 await server.remove(`revisions/${revisionItem.noteRevisionId}`); | ||||||
|  |  | ||||||
|                 this.loadNoteRevisions(revisionItem.noteId); |                 this.loadNoteRevisions(revisionItem.noteId); | ||||||
|  |  | ||||||
| @@ -232,7 +232,7 @@ export default class NoteRevisionsDialog extends BasicWidget { | |||||||
|     async renderContent(revisionItem) { |     async renderContent(revisionItem) { | ||||||
|         this.$content.empty(); |         this.$content.empty(); | ||||||
|  |  | ||||||
|         const fullNoteRevision = await server.get(`notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}`); |         const fullNoteRevision = await server.get(`revisions/${revisionItem.noteRevisionId}`); | ||||||
|  |  | ||||||
|         if (revisionItem.type === 'text') { |         if (revisionItem.type === 'text') { | ||||||
|             this.$content.html(fullNoteRevision.content); |             this.$content.html(fullNoteRevision.content); | ||||||
|   | |||||||
| @@ -142,7 +142,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget { | |||||||
|                 $input.prop("type", "text"); |                 $input.prop("type", "text"); | ||||||
|  |  | ||||||
|                 // no need to await for this, can be done asynchronously |                 // no need to await for this, can be done asynchronously | ||||||
|                 server.get(`attributes/values/${encodeURIComponent(valueAttr.name)}`).then(attributeValues => { |                 server.get(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then(attributeValues => { | ||||||
|                     if (attributeValues.length === 0) { |                     if (attributeValues.length === 0) { | ||||||
|                         return; |                         return; | ||||||
|                     } |                     } | ||||||
|   | |||||||
| @@ -237,7 +237,7 @@ export default class RelationMapTypeWidget extends TypeWidget { | |||||||
|  |  | ||||||
|     async loadNotesAndRelations() { |     async loadNotesAndRelations() { | ||||||
|         const noteIds = this.mapData.notes.map(note => note.noteId); |         const noteIds = this.mapData.notes.map(note => note.noteId); | ||||||
|         const data = await server.post("notes/relation-map", {noteIds, relationMapNoteId: this.noteId}); |         const data = await server.post("relation-map", {noteIds, relationMapNoteId: this.noteId}); | ||||||
|  |  | ||||||
|         this.relations = []; |         this.relations = []; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| const becca = require("../../becca/becca"); | const becca = require("../../becca/becca"); | ||||||
| const NotFoundError = require("../../errors/not_found_error"); | const NotFoundError = require("../../errors/not_found_error"); | ||||||
| const utils = require("../../services/utils"); | const utils = require("../../services/utils"); | ||||||
| const noteService = require("../../services/notes"); |  | ||||||
|  |  | ||||||
| function getAttachments(req) { | function getAttachments(req) { | ||||||
|     const includeContent = req.query.includeContent === 'true'; |     const includeContent = req.query.includeContent === 'true'; | ||||||
| @@ -19,18 +18,12 @@ function getAttachments(req) { | |||||||
|  |  | ||||||
| function getAttachment(req) { | function getAttachment(req) { | ||||||
|     const includeContent = req.query.includeContent === 'true'; |     const includeContent = req.query.includeContent === 'true'; | ||||||
|     const {noteId, attachmentId} = req.params; |     const {attachmentId} = req.params; | ||||||
|  |  | ||||||
|     const note = becca.getNote(noteId); |     const attachment = becca.getAttachment(attachmentId); | ||||||
|  |  | ||||||
|     if (!note) { |  | ||||||
|         throw new NotFoundError(`Note '${noteId}' doesn't exist.`); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const attachment = note.getAttachmentById(attachmentId); |  | ||||||
|  |  | ||||||
|     if (!attachment) { |     if (!attachment) { | ||||||
|         throw new NotFoundError(`Attachment '${attachmentId} of note '${noteId}' doesn't exist.`); |         throw new NotFoundError(`Attachment '${attachmentId}' doesn't exist.`); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return processAttachment(attachment, includeContent); |     return processAttachment(attachment, includeContent); | ||||||
| @@ -72,15 +65,9 @@ function saveAttachment(req) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function deleteAttachment(req) { | function deleteAttachment(req) { | ||||||
|     const {noteId, attachmentId} = req.params; |     const {attachmentId} = req.params; | ||||||
|  |  | ||||||
|     const note = becca.getNote(noteId); |     const attachment = becca.getAttachment(attachmentId); | ||||||
|  |  | ||||||
|     if (!note) { |  | ||||||
|         throw new NotFoundError(`Note '${noteId}' doesn't exist.`); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const attachment = note.getAttachmentById(attachmentId); |  | ||||||
|  |  | ||||||
|     if (attachment) { |     if (attachment) { | ||||||
|         attachment.markAsDeleted(); |         attachment.markAsDeleted(); | ||||||
| @@ -88,15 +75,15 @@ function deleteAttachment(req) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function convertAttachmentToNote(req) { | function convertAttachmentToNote(req) { | ||||||
|     const {noteId, attachmentId} = req.params; |     const {attachmentId} = req.params; | ||||||
|  |  | ||||||
|     const note = becca.getNote(noteId); |     const attachment = becca.getAttachment(attachmentId); | ||||||
|  |  | ||||||
|     if (!note) { |     if (!attachment) { | ||||||
|         throw new NotFoundError(`Note '${noteId}' doesn't exist.`); |         throw new NotFoundError(`Attachment '${attachmentId}' doesn't exist.`); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return note.convertAttachmentToChildNote(attachmentId); |     return attachment.convertToNote(); | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   | |||||||
| @@ -64,13 +64,7 @@ function getRevisionFilename(noteRevision) { | |||||||
| function downloadNoteRevision(req, res) { | function downloadNoteRevision(req, res) { | ||||||
|     const noteRevision = becca.getNoteRevision(req.params.noteRevisionId); |     const noteRevision = becca.getNoteRevision(req.params.noteRevisionId); | ||||||
|  |  | ||||||
|     if (noteRevision.noteId !== req.params.noteId) { |     if (!noteRevision.isContentAvailable()) { | ||||||
|         return res.setHeader("Content-Type", "text/plain") |  | ||||||
|             .status(400) |  | ||||||
|             .send(`Note revision ${req.params.noteRevisionId} does not belong to note ${req.params.noteId}`); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (noteRevision.isProtected && !protectedSessionService.isProtectedSessionAvailable()) { |  | ||||||
|         return res.setHeader("Content-Type", "text/plain") |         return res.setHeader("Content-Type", "text/plain") | ||||||
|             .status(401) |             .status(401) | ||||||
|             .send("Protected session not available"); |             .send("Protected session not available"); | ||||||
|   | |||||||
| @@ -127,69 +127,6 @@ function setNoteTypeMime(req) { | |||||||
|     note.save(); |     note.save(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function getRelationMap(req) { |  | ||||||
|     const {relationMapNoteId, noteIds} = req.body; |  | ||||||
|  |  | ||||||
|     const resp = { |  | ||||||
|         // noteId => title |  | ||||||
|         noteTitles: {}, |  | ||||||
|         relations: [], |  | ||||||
|         // relation name => inverse relation name |  | ||||||
|         inverseRelations: { |  | ||||||
|             'internalLink': 'internalLink' |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     if (noteIds.length === 0) { |  | ||||||
|         return resp; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const questionMarks = noteIds.map(noteId => '?').join(','); |  | ||||||
|  |  | ||||||
|     const relationMapNote = becca.getNote(relationMapNoteId); |  | ||||||
|  |  | ||||||
|     const displayRelationsVal = relationMapNote.getLabelValue('displayRelations'); |  | ||||||
|     const displayRelations = !displayRelationsVal ? [] : displayRelationsVal |  | ||||||
|         .split(",") |  | ||||||
|         .map(token => token.trim()); |  | ||||||
|  |  | ||||||
|     const hideRelationsVal = relationMapNote.getLabelValue('hideRelations'); |  | ||||||
|     const hideRelations = !hideRelationsVal ? [] : hideRelationsVal |  | ||||||
|         .split(",") |  | ||||||
|         .map(token => token.trim()); |  | ||||||
|  |  | ||||||
|     const foundNoteIds = sql.getColumn(`SELECT noteId FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds); |  | ||||||
|     const notes = becca.getNotes(foundNoteIds); |  | ||||||
|  |  | ||||||
|     for (const note of notes) { |  | ||||||
|         resp.noteTitles[note.noteId] = note.title; |  | ||||||
|  |  | ||||||
|         resp.relations = resp.relations.concat(note.getRelations() |  | ||||||
|             .filter(relation => !relation.isAutoLink() || displayRelations.includes(relation.name)) |  | ||||||
|             .filter(relation => displayRelations.length > 0 |  | ||||||
|                 ? displayRelations.includes(relation.name) |  | ||||||
|                 : !hideRelations.includes(relation.name)) |  | ||||||
|             .filter(relation => noteIds.includes(relation.value)) |  | ||||||
|             .map(relation => ({ |  | ||||||
|                 attributeId: relation.attributeId, |  | ||||||
|                 sourceNoteId: relation.noteId, |  | ||||||
|                 targetNoteId: relation.value, |  | ||||||
|                 name: relation.name |  | ||||||
|             }))); |  | ||||||
|  |  | ||||||
|         for (const relationDefinition of note.getRelationDefinitions()) { |  | ||||||
|             const def = relationDefinition.getDefinition(); |  | ||||||
|  |  | ||||||
|             if (def.inverseRelation) { |  | ||||||
|                 resp.inverseRelations[relationDefinition.getDefinedName()] = def.inverseRelation; |  | ||||||
|                 resp.inverseRelations[def.inverseRelation] = relationDefinition.getDefinedName(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return resp; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function changeTitle(req) { | function changeTitle(req) { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|     const title = req.body.title; |     const title = req.body.title; | ||||||
| @@ -273,6 +210,7 @@ function getDeleteNotesPreview(req) { | |||||||
|     if (noteIdsToBeDeleted.size > 0) { |     if (noteIdsToBeDeleted.size > 0) { | ||||||
|         sql.fillParamList(noteIdsToBeDeleted); |         sql.fillParamList(noteIdsToBeDeleted); | ||||||
|  |  | ||||||
|  |         // FIXME: No need to do this in database, can be done with becca data | ||||||
|         brokenRelations = sql.getRows(` |         brokenRelations = sql.getRows(` | ||||||
|             SELECT attr.noteId, attr.name, attr.value |             SELECT attr.noteId, attr.name, attr.value | ||||||
|             FROM attributes attr |             FROM attributes attr | ||||||
| @@ -334,7 +272,6 @@ module.exports = { | |||||||
|     sortChildNotes, |     sortChildNotes, | ||||||
|     protectNote, |     protectNote, | ||||||
|     setNoteTypeMime, |     setNoteTypeMime, | ||||||
|     getRelationMap, |  | ||||||
|     changeTitle, |     changeTitle, | ||||||
|     duplicateSubtree, |     duplicateSubtree, | ||||||
|     eraseDeletedNotesNow, |     eraseDeletedNotesNow, | ||||||
|   | |||||||
							
								
								
									
										69
									
								
								src/routes/api/relation-map.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/routes/api/relation-map.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | const becca = require("../../becca/becca.js"); | ||||||
|  | const sql = require("../../services/sql.js"); | ||||||
|  |  | ||||||
|  | function getRelationMap(req) { | ||||||
|  |     const {relationMapNoteId, noteIds} = req.body; | ||||||
|  |  | ||||||
|  |     const resp = { | ||||||
|  |         // noteId => title | ||||||
|  |         noteTitles: {}, | ||||||
|  |         relations: [], | ||||||
|  |         // relation name => inverse relation name | ||||||
|  |         inverseRelations: { | ||||||
|  |             'internalLink': 'internalLink' | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (noteIds.length === 0) { | ||||||
|  |         return resp; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const questionMarks = noteIds.map(noteId => '?').join(','); | ||||||
|  |  | ||||||
|  |     const relationMapNote = becca.getNote(relationMapNoteId); | ||||||
|  |  | ||||||
|  |     const displayRelationsVal = relationMapNote.getLabelValue('displayRelations'); | ||||||
|  |     const displayRelations = !displayRelationsVal ? [] : displayRelationsVal | ||||||
|  |         .split(",") | ||||||
|  |         .map(token => token.trim()); | ||||||
|  |  | ||||||
|  |     const hideRelationsVal = relationMapNote.getLabelValue('hideRelations'); | ||||||
|  |     const hideRelations = !hideRelationsVal ? [] : hideRelationsVal | ||||||
|  |         .split(",") | ||||||
|  |         .map(token => token.trim()); | ||||||
|  |  | ||||||
|  |     const foundNoteIds = sql.getColumn(`SELECT noteId FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds); | ||||||
|  |     const notes = becca.getNotes(foundNoteIds); | ||||||
|  |  | ||||||
|  |     for (const note of notes) { | ||||||
|  |         resp.noteTitles[note.noteId] = note.title; | ||||||
|  |  | ||||||
|  |         resp.relations = resp.relations.concat(note.getRelations() | ||||||
|  |             .filter(relation => !relation.isAutoLink() || displayRelations.includes(relation.name)) | ||||||
|  |             .filter(relation => displayRelations.length > 0 | ||||||
|  |                 ? displayRelations.includes(relation.name) | ||||||
|  |                 : !hideRelations.includes(relation.name)) | ||||||
|  |             .filter(relation => noteIds.includes(relation.value)) | ||||||
|  |             .map(relation => ({ | ||||||
|  |                 attributeId: relation.attributeId, | ||||||
|  |                 sourceNoteId: relation.noteId, | ||||||
|  |                 targetNoteId: relation.value, | ||||||
|  |                 name: relation.name | ||||||
|  |             }))); | ||||||
|  |  | ||||||
|  |         for (const relationDefinition of note.getRelationDefinitions()) { | ||||||
|  |             const def = relationDefinition.getDefinition(); | ||||||
|  |  | ||||||
|  |             if (def.inverseRelation) { | ||||||
|  |                 resp.inverseRelations[relationDefinition.getDefinedName()] = def.inverseRelation; | ||||||
|  |                 resp.inverseRelations[def.inverseRelation] = relationDefinition.getDefinedName(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return resp; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     getRelationMap | ||||||
|  | }; | ||||||
| @@ -57,8 +57,10 @@ const backendLogRoute = require('./api/backend_log'); | |||||||
| const statsRoute = require('./api/stats'); | const statsRoute = require('./api/stats'); | ||||||
| const fontsRoute = require('./api/fonts'); | const fontsRoute = require('./api/fonts'); | ||||||
| const etapiTokensApiRoutes = require('./api/etapi_tokens'); | const etapiTokensApiRoutes = require('./api/etapi_tokens'); | ||||||
|  | const relationMapApiRoute = require('./api/relation-map'); | ||||||
| const otherRoute = require('./api/other'); | const otherRoute = require('./api/other'); | ||||||
| const shareRoutes = require('../share/routes'); | 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 etapiAttributeRoutes = require('../etapi/attributes'); | const etapiAttributeRoutes = require('../etapi/attributes'); | ||||||
| @@ -73,7 +75,7 @@ const csrfMiddleware = csurf({ | |||||||
| }); | }); | ||||||
|  |  | ||||||
| const MAX_ALLOWED_FILE_SIZE_MB = 250; | const MAX_ALLOWED_FILE_SIZE_MB = 250; | ||||||
| const GET = 'get', POST = 'post', PUT = 'put', PATCH = 'patch', DELETE = 'delete'; | const GET = 'get', PST = 'post', PUT = 'put', PATCH = 'patch', DEL = 'delete'; | ||||||
|  |  | ||||||
| const uploadMiddleware = createUploadMiddleware(); | const uploadMiddleware = createUploadMiddleware(); | ||||||
|  |  | ||||||
| @@ -101,63 +103,32 @@ function register(app) { | |||||||
|         skipSuccessfulRequests: true // successful auth to rate-limited ETAPI routes isn't counted. However, successful auth to /login is still counted! |         skipSuccessfulRequests: true // successful auth to rate-limited ETAPI routes isn't counted. However, successful auth to /login is still counted! | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     route(POST, '/login', [loginRateLimiter], loginRoute.login); |     route(PST, '/login', [loginRateLimiter], loginRoute.login); | ||||||
|     route(POST, '/logout', [csrfMiddleware, auth.checkAuth], loginRoute.logout); |     route(PST, '/logout', [csrfMiddleware, auth.checkAuth], loginRoute.logout); | ||||||
|     route(POST, '/set-password', [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPassword); |     route(PST, '/set-password', [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPassword); | ||||||
|     route(GET, '/setup', [], setupRoute.setupPage); |     route(GET, '/setup', [], setupRoute.setupPage); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/tree', treeApiRoute.getTree); |     apiRoute(GET, '/api/tree', treeApiRoute.getTree); | ||||||
|     apiRoute(POST, '/api/tree/load', treeApiRoute.load); |     apiRoute(PST, '/api/tree/load', treeApiRoute.load); | ||||||
|     apiRoute(PUT, '/api/branches/:branchId/set-prefix', branchesApiRoute.setPrefix); |  | ||||||
|  |  | ||||||
|     apiRoute(PUT, '/api/branches/:branchId/move-to/:parentBranchId', branchesApiRoute.moveBranchToParent); |  | ||||||
|     apiRoute(PUT, '/api/branches/:branchId/move-before/:beforeBranchId', branchesApiRoute.moveBranchBeforeNote); |  | ||||||
|     apiRoute(PUT, '/api/branches/:branchId/move-after/:afterBranchId', branchesApiRoute.moveBranchAfterNote); |  | ||||||
|     apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded); |  | ||||||
|     apiRoute(PUT, '/api/branches/:branchId/expanded-subtree/:expanded', branchesApiRoute.setExpandedForSubtree); |  | ||||||
|     apiRoute(DELETE, '/api/branches/:branchId', branchesApiRoute.deleteBranch); |  | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/autocomplete', autocompleteApiRoute.getAutocomplete); |  | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote); |     apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/data', notesApiRoute.updateNoteData); |     apiRoute(PUT, '/api/notes/:noteId/data', notesApiRoute.updateNoteData); | ||||||
|     apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote); |     apiRoute(DEL, '/api/notes/:noteId', notesApiRoute.deleteNote); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote); |     apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote); | ||||||
|     apiRoute(POST, '/api/notes/:noteId/revision', notesApiRoute.forceSaveNoteRevision); |     apiRoute(PST, '/api/notes/:noteId/revision', notesApiRoute.forceSaveNoteRevision); | ||||||
|     apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote); |     apiRoute(PST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/sort-children', notesApiRoute.sortChildNotes); |     apiRoute(PUT, '/api/notes/:noteId/sort-children', notesApiRoute.sortChildNotes); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote); |     apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/type', notesApiRoute.setNoteTypeMime); |     apiRoute(PUT, '/api/notes/:noteId/type', notesApiRoute.setNoteTypeMime); | ||||||
|     apiRoute(GET, '/api/notes/:noteId/attachments', attachmentsApiRoute.getAttachments); |  | ||||||
|     apiRoute(GET, '/api/notes/:noteId/attachments/:attachmentId', attachmentsApiRoute.getAttachment); |  | ||||||
|     apiRoute(POST, '/api/notes/:noteId/attachments', attachmentsApiRoute.saveAttachment); |  | ||||||
|     apiRoute(POST, '/api/notes/:noteId/attachments/:attachmentId/convert-to-note', attachmentsApiRoute.convertAttachmentToNote); |  | ||||||
|     apiRoute(DELETE, '/api/notes/:noteId/attachments/:attachmentId', attachmentsApiRoute.deleteAttachment); |  | ||||||
|     apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions); |  | ||||||
|     apiRoute(DELETE, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions); |  | ||||||
|     apiRoute(GET, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.getNoteRevision); |  | ||||||
|     apiRoute(DELETE, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.eraseNoteRevision); |  | ||||||
|     route(GET, '/api/notes/:noteId/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision); |  | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/restore-revision/:noteRevisionId', noteRevisionsApiRoute.restoreNoteRevision); |  | ||||||
|     apiRoute(POST, '/api/notes/relation-map', notesApiRoute.getRelationMap); |  | ||||||
|     apiRoute(POST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow); |  | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/title', notesApiRoute.changeTitle); |     apiRoute(PUT, '/api/notes/:noteId/title', notesApiRoute.changeTitle); | ||||||
|     apiRoute(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree); |     apiRoute(PST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree); | ||||||
|     apiRoute(POST, '/api/notes/:noteId/upload-modified-file', notesApiRoute.uploadModifiedFile); |     apiRoute(PST, '/api/notes/:noteId/upload-modified-file', notesApiRoute.uploadModifiedFile); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate); |  | ||||||
|  |  | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch); |     apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/toggle-in-parent/:parentNoteId/:present', cloningApiRoute.toggleNoteInParent); |     apiRoute(PUT, '/api/notes/:noteId/toggle-in-parent/:parentNoteId/:present', cloningApiRoute.toggleNoteInParent); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToNote); |     apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToNote); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter); |     apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter); | ||||||
|  |  | ||||||
|     route(GET, '/api/notes/:branchId/export/:type/:format/:version/:taskId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch); |  | ||||||
|     route(POST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importToBranch, apiResultHandler); |  | ||||||
|  |  | ||||||
|     route(PUT, '/api/notes/:noteId/file', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], |     route(PUT, '/api/notes/:noteId/file', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], | ||||||
|         filesRoute.updateFile, apiResultHandler); |         filesRoute.updateFile, apiResultHandler); | ||||||
|  |  | ||||||
|     route(GET, '/api/notes/:noteId/open', [auth.checkApiAuthOrElectron], filesRoute.openFile); |     route(GET, '/api/notes/:noteId/open', [auth.checkApiAuthOrElectron], filesRoute.openFile); | ||||||
|     route(GET, '/api/notes/:noteId/open-partial', [auth.checkApiAuthOrElectron], |     route(GET, '/api/notes/:noteId/open-partial', [auth.checkApiAuthOrElectron], | ||||||
|         createPartialContentHandler(filesRoute.fileContentProvider, { |         createPartialContentHandler(filesRoute.fileContentProvider, { | ||||||
| @@ -166,69 +137,73 @@ function register(app) { | |||||||
|     route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); |     route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); | ||||||
|     // this "hacky" path is used for easier referencing of CSS resources |     // this "hacky" path is used for easier referencing of CSS resources | ||||||
|     route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); |     route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); | ||||||
|     apiRoute(POST, '/api/notes/:noteId/save-to-tmp-dir', filesRoute.saveToTmpDir); |     apiRoute(PST, '/api/notes/:noteId/save-to-tmp-dir', filesRoute.saveToTmpDir); | ||||||
|  |  | ||||||
|  |     apiRoute(PUT, '/api/branches/:branchId/move-to/:parentBranchId', branchesApiRoute.moveBranchToParent); | ||||||
|  |     apiRoute(PUT, '/api/branches/:branchId/move-before/:beforeBranchId', branchesApiRoute.moveBranchBeforeNote); | ||||||
|  |     apiRoute(PUT, '/api/branches/:branchId/move-after/:afterBranchId', branchesApiRoute.moveBranchAfterNote); | ||||||
|  |     apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded); | ||||||
|  |     apiRoute(PUT, '/api/branches/:branchId/expanded-subtree/:expanded', branchesApiRoute.setExpandedForSubtree); | ||||||
|  |     apiRoute(DEL, '/api/branches/:branchId', branchesApiRoute.deleteBranch); | ||||||
|  |     apiRoute(PUT, '/api/branches/:branchId/set-prefix', branchesApiRoute.setPrefix); | ||||||
|  |  | ||||||
|  |     apiRoute(GET, '/api/notes/:noteId/attachments', attachmentsApiRoute.getAttachments); | ||||||
|  |     apiRoute(PST, '/api/notes/:noteId/attachments', attachmentsApiRoute.saveAttachment); | ||||||
|  |     apiRoute(GET, '/api/attachments/:attachmentId', attachmentsApiRoute.getAttachment); | ||||||
|  |     apiRoute(PST, '/api/attachments/:attachmentId/convert-to-note', attachmentsApiRoute.convertAttachmentToNote); | ||||||
|  |     apiRoute(DEL, '/api/attachments/:attachmentId', attachmentsApiRoute.deleteAttachment); | ||||||
|  |     route(GET, '/api/attachments/:attachmentId/image/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage); | ||||||
|  |  | ||||||
|  |     apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions); | ||||||
|  |     apiRoute(DEL, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions); | ||||||
|  |     apiRoute(GET, '/api/revisions/:noteRevisionId', noteRevisionsApiRoute.getNoteRevision); | ||||||
|  |     apiRoute(DEL, '/api/revisions/:noteRevisionId', noteRevisionsApiRoute.eraseNoteRevision); | ||||||
|  |     apiRoute(PST, '/api/revisions/:noteRevisionId/restore', noteRevisionsApiRoute.restoreNoteRevision); | ||||||
|  |     route(GET, '/api/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     route(GET, '/api/branches/:branchId/export/:type/:format/:version/:taskId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch); | ||||||
|  |     route(PST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importToBranch, apiResultHandler); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getEffectiveNoteAttributes); |     apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getEffectiveNoteAttributes); | ||||||
|     apiRoute(POST, '/api/notes/:noteId/attributes', attributesRoute.addNoteAttribute); |     apiRoute(PST, '/api/notes/:noteId/attributes', attributesRoute.addNoteAttribute); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/attributes', attributesRoute.updateNoteAttributes); |     apiRoute(PUT, '/api/notes/:noteId/attributes', attributesRoute.updateNoteAttributes); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/attribute', attributesRoute.updateNoteAttribute); |     apiRoute(PUT, '/api/notes/:noteId/attribute', attributesRoute.updateNoteAttribute); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/set-attribute', attributesRoute.setNoteAttribute); |     apiRoute(PUT, '/api/notes/:noteId/set-attribute', attributesRoute.setNoteAttribute); | ||||||
|     apiRoute(PUT, '/api/notes/:noteId/relations/:name/to/:targetNoteId', attributesRoute.createRelation); |     apiRoute(PUT, '/api/notes/:noteId/relations/:name/to/:targetNoteId', attributesRoute.createRelation); | ||||||
|     apiRoute(DELETE, '/api/notes/:noteId/relations/:name/to/:targetNoteId', attributesRoute.deleteRelation); |     apiRoute(DEL, '/api/notes/:noteId/relations/:name/to/:targetNoteId', attributesRoute.deleteRelation); | ||||||
|     apiRoute(DELETE, '/api/notes/:noteId/attributes/:attributeId', attributesRoute.deleteNoteAttribute); |     apiRoute(DEL, '/api/notes/:noteId/attributes/:attributeId', attributesRoute.deleteNoteAttribute); | ||||||
|     apiRoute(GET, '/api/attributes/names', attributesRoute.getAttributeNames); |     apiRoute(GET, '/api/attribute-names', attributesRoute.getAttributeNames); | ||||||
|     apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute); |     apiRoute(GET, '/api/attribute-values/:attributeName', attributesRoute.getValuesForAttribute); | ||||||
|  |  | ||||||
|     apiRoute(POST, '/api/note-map/:noteId/tree', noteMapRoute.getTreeMap); |  | ||||||
|     apiRoute(POST, '/api/note-map/:noteId/link', noteMapRoute.getLinkMap); |  | ||||||
|     apiRoute(GET, '/api/note-map/:noteId/backlink-count', noteMapRoute.getBacklinkCount); |  | ||||||
|     apiRoute(GET, '/api/note-map/:noteId/backlinks', noteMapRoute.getBacklinks); |  | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote); |  | ||||||
|     apiRoute(GET, '/api/special-notes/days/:date', specialNotesRoute.getDayNote); |  | ||||||
|     apiRoute(GET, '/api/special-notes/weeks/:date', specialNotesRoute.getWeekNote); |  | ||||||
|     apiRoute(GET, '/api/special-notes/months/:month', specialNotesRoute.getMonthNote); |  | ||||||
|     apiRoute(GET, '/api/special-notes/years/:year', specialNotesRoute.getYearNote); |  | ||||||
|     apiRoute(GET, '/api/special-notes/notes-for-month/:month', specialNotesRoute.getDayNotesForMonth); |  | ||||||
|     apiRoute(POST, '/api/special-notes/sql-console', specialNotesRoute.createSqlConsole); |  | ||||||
|     apiRoute(POST, '/api/special-notes/save-sql-console', specialNotesRoute.saveSqlConsole); |  | ||||||
|     apiRoute(POST, '/api/special-notes/search-note', specialNotesRoute.createSearchNote); |  | ||||||
|     apiRoute(POST, '/api/special-notes/save-search-note', specialNotesRoute.saveSearchNote); |  | ||||||
|     apiRoute(POST, '/api/special-notes/launchers/:noteId/reset', specialNotesRoute.resetLauncher); |  | ||||||
|     apiRoute(POST, '/api/special-notes/launchers/:parentNoteId/:launcherType', specialNotesRoute.createLauncher); |  | ||||||
|     apiRoute(PUT, '/api/special-notes/api-script-launcher', specialNotesRoute.createOrUpdateScriptLauncherFromApi); |  | ||||||
|  |  | ||||||
|     // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename |     // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename | ||||||
|     route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage); |     route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage); | ||||||
|     route(GET, '/api/notes/:noteId/images/:attachmentId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage); |     route(PST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.uploadImage, apiResultHandler); | ||||||
|     route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.uploadImage, apiResultHandler); |  | ||||||
|     route(PUT, '/api/images/:noteId', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.updateImage, apiResultHandler); |     route(PUT, '/api/images/:noteId', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.updateImage, apiResultHandler); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/recent-changes/:ancestorNoteId', recentChangesApiRoute.getRecentChanges); |  | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/options', optionsApiRoute.getOptions); |     apiRoute(GET, '/api/options', optionsApiRoute.getOptions); | ||||||
|     // FIXME: possibly change to sending value in the body to avoid host of HTTP server issues with slashes |     // FIXME: possibly change to sending value in the body to avoid host of HTTP server issues with slashes | ||||||
|     apiRoute(PUT, '/api/options/:name/:value*', optionsApiRoute.updateOption); |     apiRoute(PUT, '/api/options/:name/:value*', optionsApiRoute.updateOption); | ||||||
|     apiRoute(PUT, '/api/options', optionsApiRoute.updateOptions); |     apiRoute(PUT, '/api/options', optionsApiRoute.updateOptions); | ||||||
|     apiRoute(GET, '/api/options/user-themes', optionsApiRoute.getUserThemes); |     apiRoute(GET, '/api/options/user-themes', optionsApiRoute.getUserThemes); | ||||||
|  |  | ||||||
|     apiRoute(POST, '/api/password/change', passwordApiRoute.changePassword); |     apiRoute(PST, '/api/password/change', passwordApiRoute.changePassword); | ||||||
|     apiRoute(POST, '/api/password/reset', passwordApiRoute.resetPassword); |     apiRoute(PST, '/api/password/reset', passwordApiRoute.resetPassword); | ||||||
|  |  | ||||||
|     apiRoute(POST, '/api/sync/test', syncApiRoute.testSync); |     apiRoute(PST, '/api/sync/test', syncApiRoute.testSync); | ||||||
|     apiRoute(POST, '/api/sync/now', syncApiRoute.syncNow); |     apiRoute(PST, '/api/sync/now', syncApiRoute.syncNow); | ||||||
|     apiRoute(POST, '/api/sync/fill-entity-changes', syncApiRoute.fillEntityChanges); |     apiRoute(PST, '/api/sync/fill-entity-changes', syncApiRoute.fillEntityChanges); | ||||||
|     apiRoute(POST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync); |     apiRoute(PST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync); | ||||||
|     apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync); |     apiRoute(PST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync); | ||||||
|     route(GET, '/api/sync/check', [auth.checkApiAuth], syncApiRoute.checkSync, apiResultHandler); |     route(GET, '/api/sync/check', [auth.checkApiAuth], syncApiRoute.checkSync, apiResultHandler); | ||||||
|     route(GET, '/api/sync/changed', [auth.checkApiAuth], syncApiRoute.getChanged, apiResultHandler); |     route(GET, '/api/sync/changed', [auth.checkApiAuth], syncApiRoute.getChanged, apiResultHandler); | ||||||
|     route(PUT, '/api/sync/update', [auth.checkApiAuth], syncApiRoute.update, apiResultHandler); |     route(PUT, '/api/sync/update', [auth.checkApiAuth], syncApiRoute.update, apiResultHandler); | ||||||
|     route(POST, '/api/sync/finished', [auth.checkApiAuth], syncApiRoute.syncFinished, apiResultHandler); |     route(PST, '/api/sync/finished', [auth.checkApiAuth], syncApiRoute.syncFinished, apiResultHandler); | ||||||
|     route(POST, '/api/sync/check-entity-changes', [auth.checkApiAuth], syncApiRoute.checkEntityChanges, apiResultHandler); |     route(PST, '/api/sync/check-entity-changes', [auth.checkApiAuth], syncApiRoute.checkEntityChanges, apiResultHandler); | ||||||
|     route(POST, '/api/sync/queue-sector/:entityName/:sector', [auth.checkApiAuth], syncApiRoute.queueSector, apiResultHandler); |     route(PST, '/api/sync/queue-sector/:entityName/:sector', [auth.checkApiAuth], syncApiRoute.queueSector, apiResultHandler); | ||||||
|     route(GET, '/api/sync/stats', [], syncApiRoute.getStats, apiResultHandler); |     route(GET, '/api/sync/stats', [], syncApiRoute.getStats, apiResultHandler); | ||||||
|  |  | ||||||
|     apiRoute(POST, '/api/recent-notes', recentNotesRoute.addRecentNote); |     apiRoute(PST, '/api/recent-notes', recentNotesRoute.addRecentNote); | ||||||
|     apiRoute(GET, '/api/app-info', appInfoRoute.getAppInfo); |     apiRoute(GET, '/api/app-info', appInfoRoute.getAppInfo); | ||||||
|  |  | ||||||
|     // docker health check |     // docker health check | ||||||
| @@ -236,82 +211,102 @@ function register(app) { | |||||||
|  |  | ||||||
|     // group of services below are meant to be executed from outside |     // group of services below are meant to be executed from outside | ||||||
|     route(GET, '/api/setup/status', [], setupApiRoute.getStatus, apiResultHandler); |     route(GET, '/api/setup/status', [], setupApiRoute.getStatus, apiResultHandler); | ||||||
|     route(POST, '/api/setup/new-document', [auth.checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler, false); |     route(PST, '/api/setup/new-document', [auth.checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler, false); | ||||||
|     route(POST, '/api/setup/sync-from-server', [auth.checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler, false); |     route(PST, '/api/setup/sync-from-server', [auth.checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler, false); | ||||||
|     route(GET, '/api/setup/sync-seed', [auth.checkCredentials], setupApiRoute.getSyncSeed, apiResultHandler); |     route(GET, '/api/setup/sync-seed', [auth.checkCredentials], setupApiRoute.getSyncSeed, apiResultHandler); | ||||||
|     route(POST, '/api/setup/sync-seed', [auth.checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler, false); |     route(PST, '/api/setup/sync-seed', [auth.checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler, false); | ||||||
|  |  | ||||||
|  |     apiRoute(GET, '/api/autocomplete', autocompleteApiRoute.getAutocomplete); | ||||||
|  |     apiRoute(GET, '/api/quick-search/:searchString', searchRoute.quickSearch); | ||||||
|  |     apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote); | ||||||
|  |     apiRoute(PST, '/api/search-and-execute-note/:noteId', searchRoute.searchAndExecute); | ||||||
|  |     apiRoute(PST, '/api/search-related', searchRoute.getRelatedNotes); | ||||||
|  |     apiRoute(GET, '/api/search/:searchString', searchRoute.search); | ||||||
|  |     apiRoute(GET, '/api/search-templates', searchRoute.searchTemplates); | ||||||
|  |  | ||||||
|  |     apiRoute(PST, '/api/bulk-action/execute', bulkActionRoute.execute); | ||||||
|  |     apiRoute(PST, '/api/bulk-action/affected-notes', bulkActionRoute.getAffectedNoteCount); | ||||||
|  |  | ||||||
|  |     route(PST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); | ||||||
|  |     // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username) | ||||||
|  |     apiRoute(PST, '/api/login/protected', loginApiRoute.loginToProtectedSession); | ||||||
|  |     apiRoute(PST, '/api/login/protected/touch', loginApiRoute.touchProtectedSession); | ||||||
|  |     apiRoute(PST, '/api/logout/protected', loginApiRoute.logoutFromProtectedSession); | ||||||
|  |  | ||||||
|  |     route(PST, '/api/login/token', [loginRateLimiter], loginApiRoute.token, apiResultHandler); | ||||||
|  |  | ||||||
|  |     apiRoute(GET, '/api/etapi-tokens', etapiTokensApiRoutes.getTokens); | ||||||
|  |     apiRoute(PST, '/api/etapi-tokens', etapiTokensApiRoutes.createToken); | ||||||
|  |     apiRoute(PATCH, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.patchToken); | ||||||
|  |     apiRoute(DEL, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.deleteToken); | ||||||
|  |  | ||||||
|  |     // in case of local electron, local calls are allowed unauthenticated, for server they need auth | ||||||
|  |     const clipperMiddleware = utils.isElectron() ? [] : [auth.checkEtapiToken]; | ||||||
|  |  | ||||||
|  |     route(GET, '/api/clipper/handshake', clipperMiddleware, clipperRoute.handshake, apiResultHandler); | ||||||
|  |     route(PST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler); | ||||||
|  |     route(PST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler); | ||||||
|  |     route(PST, '/api/clipper/open/:noteId', clipperMiddleware, clipperRoute.openNote, apiResultHandler); | ||||||
|  |  | ||||||
|  |     apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote); | ||||||
|  |     apiRoute(GET, '/api/special-notes/days/:date', specialNotesRoute.getDayNote); | ||||||
|  |     apiRoute(GET, '/api/special-notes/weeks/:date', specialNotesRoute.getWeekNote); | ||||||
|  |     apiRoute(GET, '/api/special-notes/months/:month', specialNotesRoute.getMonthNote); | ||||||
|  |     apiRoute(GET, '/api/special-notes/years/:year', specialNotesRoute.getYearNote); | ||||||
|  |     apiRoute(GET, '/api/special-notes/notes-for-month/:month', specialNotesRoute.getDayNotesForMonth); | ||||||
|  |     apiRoute(PST, '/api/special-notes/sql-console', specialNotesRoute.createSqlConsole); | ||||||
|  |     apiRoute(PST, '/api/special-notes/save-sql-console', specialNotesRoute.saveSqlConsole); | ||||||
|  |     apiRoute(PST, '/api/special-notes/search-note', specialNotesRoute.createSearchNote); | ||||||
|  |     apiRoute(PST, '/api/special-notes/save-search-note', specialNotesRoute.saveSearchNote); | ||||||
|  |     apiRoute(PST, '/api/special-notes/launchers/:noteId/reset', specialNotesRoute.resetLauncher); | ||||||
|  |     apiRoute(PST, '/api/special-notes/launchers/:parentNoteId/:launcherType', specialNotesRoute.createLauncher); | ||||||
|  |     apiRoute(PUT, '/api/special-notes/api-script-launcher', specialNotesRoute.createOrUpdateScriptLauncherFromApi); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/sql/schema', sqlRoute.getSchema); |     apiRoute(GET, '/api/sql/schema', sqlRoute.getSchema); | ||||||
|     apiRoute(POST, '/api/sql/execute/:noteId', sqlRoute.execute); |     apiRoute(PST, '/api/sql/execute/:noteId', sqlRoute.execute); | ||||||
|     route(POST, '/api/database/anonymize/:type', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false); |     route(PST, '/api/database/anonymize/:type', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false); | ||||||
|  |  | ||||||
|     // backup requires execution outside of transaction |     // backup requires execution outside of transaction | ||||||
|     route(POST, '/api/database/backup-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false); |     route(PST, '/api/database/backup-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false); | ||||||
|  |  | ||||||
|     // VACUUM requires execution outside of transaction |     // VACUUM requires execution outside of transaction | ||||||
|     route(POST, '/api/database/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.vacuumDatabase, apiResultHandler, false); |     route(PST, '/api/database/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.vacuumDatabase, apiResultHandler, false); | ||||||
|  |  | ||||||
|     route(POST, '/api/database/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.findAndFixConsistencyIssues, apiResultHandler, false); |     route(PST, '/api/database/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.findAndFixConsistencyIssues, apiResultHandler, false); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/database/check-integrity', databaseRoute.checkIntegrity); |     apiRoute(GET, '/api/database/check-integrity', databaseRoute.checkIntegrity); | ||||||
|  |  | ||||||
|     apiRoute(POST, '/api/script/exec', scriptRoute.exec); |     apiRoute(PST, '/api/script/exec', scriptRoute.exec); | ||||||
|     apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run); |     apiRoute(PST, '/api/script/run/:noteId', scriptRoute.run); | ||||||
|     apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles); |     apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles); | ||||||
|     apiRoute(GET, '/api/script/widgets', scriptRoute.getWidgetBundles); |     apiRoute(GET, '/api/script/widgets', scriptRoute.getWidgetBundles); | ||||||
|     apiRoute(GET, '/api/script/bundle/:noteId', scriptRoute.getBundle); |     apiRoute(GET, '/api/script/bundle/:noteId', scriptRoute.getBundle); | ||||||
|     apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles); |     apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles); | ||||||
|  |  | ||||||
|     // no CSRF since this is called from android app |     // no CSRF since this is called from android app | ||||||
|     route(POST, '/api/sender/login', [loginRateLimiter], loginApiRoute.token, apiResultHandler); |     route(PST, '/api/sender/login', [loginRateLimiter], loginApiRoute.token, apiResultHandler); | ||||||
|     route(POST, '/api/sender/image', [auth.checkEtapiToken, uploadMiddlewareWithErrorHandling], senderRoute.uploadImage, apiResultHandler); |     route(PST, '/api/sender/image', [auth.checkEtapiToken, uploadMiddlewareWithErrorHandling], senderRoute.uploadImage, apiResultHandler); | ||||||
|     route(POST, '/api/sender/note', [auth.checkEtapiToken], senderRoute.saveNote, apiResultHandler); |     route(PST, '/api/sender/note', [auth.checkEtapiToken], senderRoute.saveNote, apiResultHandler); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/quick-search/:searchString', searchRoute.quickSearch); |  | ||||||
|     apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote); |  | ||||||
|     apiRoute(POST, '/api/search-and-execute-note/:noteId', searchRoute.searchAndExecute); |  | ||||||
|     apiRoute(POST, '/api/search-related', searchRoute.getRelatedNotes); |  | ||||||
|     apiRoute(GET, '/api/search/:searchString', searchRoute.search); |  | ||||||
|     apiRoute(GET, '/api/search-templates', searchRoute.searchTemplates); |  | ||||||
|  |  | ||||||
|     apiRoute(POST, '/api/bulk-action/execute', bulkActionRoute.execute); |  | ||||||
|     apiRoute(POST, '/api/bulk-action/affected-notes', bulkActionRoute.getAffectedNoteCount); |  | ||||||
|  |  | ||||||
|     route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); |  | ||||||
|     // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username) |  | ||||||
|     apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession); |  | ||||||
|     apiRoute(POST, '/api/login/protected/touch', loginApiRoute.touchProtectedSession); |  | ||||||
|     apiRoute(POST, '/api/logout/protected', loginApiRoute.logoutFromProtectedSession); |  | ||||||
|  |  | ||||||
|     route(POST, '/api/login/token', [loginRateLimiter], loginApiRoute.token, apiResultHandler); |  | ||||||
|  |  | ||||||
|     // in case of local electron, local calls are allowed unauthenticated, for server they need auth |  | ||||||
|     const clipperMiddleware = utils.isElectron() ? [] : [auth.checkEtapiToken]; |  | ||||||
|  |  | ||||||
|     route(GET, '/api/clipper/handshake', clipperMiddleware, clipperRoute.handshake, apiResultHandler); |  | ||||||
|     route(POST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler); |  | ||||||
|     route(POST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler); |  | ||||||
|     route(POST, '/api/clipper/open/:noteId', clipperMiddleware, clipperRoute.openNote, apiResultHandler); |  | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/similar-notes/:noteId', similarNotesRoute.getSimilarNotes); |  | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/keyboard-actions', keysRoute.getKeyboardActions); |     apiRoute(GET, '/api/keyboard-actions', keysRoute.getKeyboardActions); | ||||||
|     apiRoute(GET, '/api/keyboard-shortcuts-for-notes', keysRoute.getShortcutsForNotes); |     apiRoute(GET, '/api/keyboard-shortcuts-for-notes', keysRoute.getShortcutsForNotes); | ||||||
|  |  | ||||||
|  |     apiRoute(PST, '/api/relation-map', relationMapApiRoute.getRelationMap); | ||||||
|  |     apiRoute(PST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow); | ||||||
|  |     apiRoute(GET, '/api/similar-notes/:noteId', similarNotesRoute.getSimilarNotes); | ||||||
|     apiRoute(GET, '/api/backend-log', backendLogRoute.getBackendLog); |     apiRoute(GET, '/api/backend-log', backendLogRoute.getBackendLog); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/stats/note-size/:noteId', statsRoute.getNoteSize); |     apiRoute(GET, '/api/stats/note-size/:noteId', statsRoute.getNoteSize); | ||||||
|     apiRoute(GET, '/api/stats/subtree-size/:noteId', statsRoute.getSubtreeSize); |     apiRoute(GET, '/api/stats/subtree-size/:noteId', statsRoute.getSubtreeSize); | ||||||
|  |     apiRoute(PST, '/api/delete-notes-preview', notesApiRoute.getDeleteNotesPreview); | ||||||
|     apiRoute(POST, '/api/delete-notes-preview', notesApiRoute.getDeleteNotesPreview); |  | ||||||
|  |  | ||||||
|     route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss); |     route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss); | ||||||
|     apiRoute(GET, '/api/other/icon-usage', otherRoute.getIconUsage); |     apiRoute(GET, '/api/other/icon-usage', otherRoute.getIconUsage); | ||||||
|  |     apiRoute(GET, '/api/recent-changes/:ancestorNoteId', recentChangesApiRoute.getRecentChanges); | ||||||
|  |     apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/etapi-tokens', etapiTokensApiRoutes.getTokens); |     apiRoute(PST, '/api/note-map/:noteId/tree', noteMapRoute.getTreeMap); | ||||||
|     apiRoute(POST, '/api/etapi-tokens', etapiTokensApiRoutes.createToken); |     apiRoute(PST, '/api/note-map/:noteId/link', noteMapRoute.getLinkMap); | ||||||
|     apiRoute(PATCH, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.patchToken); |     apiRoute(GET, '/api/note-map/:noteId/backlink-count', noteMapRoute.getBacklinkCount); | ||||||
|     apiRoute(DELETE, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.deleteToken); |     apiRoute(GET, '/api/note-map/:noteId/backlinks', noteMapRoute.getBacklinks); | ||||||
|  |  | ||||||
|     shareRoutes.register(router); |     shareRoutes.register(router); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,12 +23,8 @@ function isHoistedInHiddenSubtree() { | |||||||
|     return hoistedNote.isHiddenCompletely(); |     return hoistedNote.isHiddenCompletely(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function getHoistedNote() { |  | ||||||
|     return becca.getNote(cls.getHoistedNoteId()); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function getWorkspaceNote() { | function getWorkspaceNote() { | ||||||
|     const hoistedNote = getHoistedNote(); |     const hoistedNote = becca.getNote(cls.getHoistedNoteId()); | ||||||
|  |  | ||||||
|     if (hoistedNote.isRoot() || hoistedNote.hasLabel('workspace')) { |     if (hoistedNote.isRoot() || hoistedNote.hasLabel('workspace')) { | ||||||
|         return hoistedNote; |         return hoistedNote; | ||||||
| @@ -39,7 +35,6 @@ function getWorkspaceNote() { | |||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     getHoistedNoteId, |     getHoistedNoteId, | ||||||
|     getHoistedNote, |  | ||||||
|     getWorkspaceNote, |     getWorkspaceNote, | ||||||
|     isHoistedInHiddenSubtree |     isHoistedInHiddenSubtree | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -2,35 +2,32 @@ | |||||||
|  |  | ||||||
| const log = require('./log'); | const log = require('./log'); | ||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const protectedSession = require("./protected_session"); | const protectedSessionService = require("./protected_session"); | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @param {BNote} note |  * @param {BNote} note | ||||||
|  */ |  */ | ||||||
| function protectNoteRevisions(note) { | function protectNoteRevisions(note) { | ||||||
|  |     if (!protectedSessionService.isProtectedSessionAvailable()) { | ||||||
|  |         throw new Error(`Cannot (un)protect revisions of note '${note.noteId}' without active protected session`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     for (const revision of note.getNoteRevisions()) { |     for (const revision of note.getNoteRevisions()) { | ||||||
|         if (note.isProtected !== revision.isProtected) { |         if (note.isProtected === revision.isProtected) { | ||||||
|             if (!protectedSession.isProtectedSessionAvailable()) { |             continue; | ||||||
|                 log.error("Protected session is not available to fix note revisions."); |         } | ||||||
|  |  | ||||||
|                 return; |         try { | ||||||
|             } |             const content = revision.getContent(); | ||||||
|  |  | ||||||
|             try { |             revision.isProtected = note.isProtected; | ||||||
|                 const content = revision.getContent(); |  | ||||||
|  |  | ||||||
|                 revision.isProtected = note.isProtected; |             // this will force de/encryption | ||||||
|  |             revision.setContent(content, {forceSave: true}); | ||||||
|  |         } catch (e) { | ||||||
|  |             log.error(`Could not un/protect note revision '${revision.noteRevisionId}'`); | ||||||
|  |  | ||||||
|                 // this will force de/encryption |             throw e; | ||||||
|                 revision.setContent(content); |  | ||||||
|  |  | ||||||
|                 revision.save(); |  | ||||||
|             } |  | ||||||
|             catch (e) { |  | ||||||
|                 log.error(`Could not un/protect note revision ID = ${revision.noteRevisionId}`); |  | ||||||
|  |  | ||||||
|                 throw e; |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,7 +9,6 @@ const protectedSessionService = require('../services/protected_session'); | |||||||
| const log = require('../services/log'); | const log = require('../services/log'); | ||||||
| const utils = require('../services/utils'); | const utils = require('../services/utils'); | ||||||
| const noteRevisionService = require('../services/note_revisions'); | const noteRevisionService = require('../services/note_revisions'); | ||||||
| const attributeService = require('../services/attributes'); |  | ||||||
| const request = require('./request'); | const request = require('./request'); | ||||||
| const path = require('path'); | const path = require('path'); | ||||||
| const url = require('url'); | const url = require('url'); | ||||||
| @@ -21,8 +20,8 @@ const dayjs = require("dayjs"); | |||||||
| const htmlSanitizer = require("./html_sanitizer"); | const htmlSanitizer = require("./html_sanitizer"); | ||||||
| const ValidationError = require("../errors/validation_error"); | const ValidationError = require("../errors/validation_error"); | ||||||
| const noteTypesService = require("./note_types"); | const noteTypesService = require("./note_types"); | ||||||
| const {attach} = require("jsdom/lib/jsdom/living/helpers/svg/basic-types.js"); |  | ||||||
|  |  | ||||||
|  | /** @param {BNote} parentNote */ | ||||||
| function getNewNotePosition(parentNote) { | function getNewNotePosition(parentNote) { | ||||||
|     if (parentNote.isLabelTruthy('newNotesOnTop')) { |     if (parentNote.isLabelTruthy('newNotesOnTop')) { | ||||||
|         const minNotePos = parentNote.getChildBranches() |         const minNotePos = parentNote.getChildBranches() | ||||||
| @@ -37,6 +36,7 @@ function getNewNotePosition(parentNote) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** @param {BNote} note */ | ||||||
| function triggerNoteTitleChanged(note) { | function triggerNoteTitleChanged(note) { | ||||||
|     eventService.emit(eventService.NOTE_TITLE_CHANGED, note); |     eventService.emit(eventService.NOTE_TITLE_CHANGED, note); | ||||||
| } | } | ||||||
| @@ -53,6 +53,10 @@ function deriveMime(type, mime) { | |||||||
|     return noteTypesService.getDefaultMimeForNoteType(type); |     return noteTypesService.getDefaultMimeForNoteType(type); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param {BNote} parentNote | ||||||
|  |  * @param {BNote} childNote | ||||||
|  |  */ | ||||||
| function copyChildAttributes(parentNote, childNote) { | function copyChildAttributes(parentNote, childNote) { | ||||||
|     const hasAlreadyTemplate = childNote.hasRelation('template'); |     const hasAlreadyTemplate = childNote.hasRelation('template'); | ||||||
|  |  | ||||||
| @@ -78,6 +82,7 @@ function copyChildAttributes(parentNote, childNote) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** @param {BNote} parentNote */ | ||||||
| function getNewNoteTitle(parentNote) { | function getNewNoteTitle(parentNote) { | ||||||
|     let title = "new note"; |     let title = "new note"; | ||||||
|  |  | ||||||
| @@ -278,6 +283,12 @@ function createNewNoteWithTarget(target, targetBranchId, params) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param {BNote} note | ||||||
|  |  * @param {boolean} protect | ||||||
|  |  * @param {boolean} includingSubTree | ||||||
|  |  * @param {TaskContext} taskContext | ||||||
|  |  */ | ||||||
| function protectNoteRecursively(note, protect, includingSubTree, taskContext) { | function protectNoteRecursively(note, protect, includingSubTree, taskContext) { | ||||||
|     protectNote(note, protect); |     protectNote(note, protect); | ||||||
|  |  | ||||||
| @@ -290,7 +301,15 @@ function protectNoteRecursively(note, protect, includingSubTree, taskContext) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param {BNote} note | ||||||
|  |  * @param {boolean} protect | ||||||
|  |  */ | ||||||
| function protectNote(note, protect) { | function protectNote(note, protect) { | ||||||
|  |     if (!protectedSessionService.isProtectedSessionAvailable()) { | ||||||
|  |         throw new Error(`Cannot (un)protect note '${note.noteId}' with protect flag '${protect}' without active protected session`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|         if (protect !== note.isProtected) { |         if (protect !== note.isProtected) { | ||||||
|             const content = note.getContent(); |             const content = note.getContent(); | ||||||
| @@ -310,7 +329,7 @@ function protectNote(note, protect) { | |||||||
|         noteRevisionService.protectNoteRevisions(note); |         noteRevisionService.protectNoteRevisions(note); | ||||||
|     } |     } | ||||||
|     catch (e) { |     catch (e) { | ||||||
|         log.error(`Could not un/protect note ID = ${note.noteId}`); |         log.error(`Could not un/protect note '${note.noteId}'`); | ||||||
|  |  | ||||||
|         throw e; |         throw e; | ||||||
|     } |     } | ||||||
| @@ -565,6 +584,7 @@ function saveLinks(note, content) { | |||||||
|     return content; |     return content; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** @param {BNote} note */ | ||||||
| function saveNoteRevisionIfNeeded(note) { | function saveNoteRevisionIfNeeded(note) { | ||||||
|     // files and images are versioned separately |     // files and images are versioned separately | ||||||
|     if (note.type === 'file' || note.type === 'image' || note.hasLabel('disableVersioning')) { |     if (note.type === 'file' || note.type === 'image' || note.hasLabel('disableVersioning')) { | ||||||
| @@ -709,6 +729,8 @@ function scanForLinks(note, content) { | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  |  * @param {BNote} note | ||||||
|  |  * @param {string} content | ||||||
|  * Things which have to be executed after updating content, but asynchronously (separate transaction) |  * Things which have to be executed after updating content, but asynchronously (separate transaction) | ||||||
|  */ |  */ | ||||||
| async function asyncPostProcessContent(note, content) { | async function asyncPostProcessContent(note, content) { | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ const becca = require("../becca/becca"); | |||||||
| const noteService = require("./notes"); | const noteService = require("./notes"); | ||||||
| const dateUtils = require("./date_utils"); | const dateUtils = require("./date_utils"); | ||||||
| const log = require("./log"); | const log = require("./log"); | ||||||
| const hiddenSubtreeService = require("./hidden_subtree"); |  | ||||||
| const hoistedNoteService = require("./hoisted_note"); | const hoistedNoteService = require("./hoisted_note"); | ||||||
| const searchService = require("./search/services/search"); | const searchService = require("./search/services/search"); | ||||||
| const SearchContext = require("./search/search_context"); | const SearchContext = require("./search/search_context"); | ||||||
|   | |||||||
| @@ -4,31 +4,8 @@ const sql = require('./sql'); | |||||||
| const log = require('./log'); | const log = require('./log'); | ||||||
| const BBranch = require('../becca/entities/bbranch'); | const BBranch = require('../becca/entities/bbranch'); | ||||||
| const entityChangesService = require('./entity_changes'); | const entityChangesService = require('./entity_changes'); | ||||||
| const protectedSessionService = require('./protected_session'); |  | ||||||
| const becca = require('../becca/becca'); | const becca = require('../becca/becca'); | ||||||
|  |  | ||||||
| function getNotes(noteIds) { |  | ||||||
|     // we return also deleted notes which have been specifically asked for |  | ||||||
|     const notes = sql.getManyRows(` |  | ||||||
|         SELECT  |  | ||||||
|           noteId, |  | ||||||
|           title, |  | ||||||
|           isProtected, |  | ||||||
|           type, |  | ||||||
|           mime, |  | ||||||
|           isDeleted |  | ||||||
|         FROM notes |  | ||||||
|         WHERE noteId IN (???)`, noteIds); |  | ||||||
|  |  | ||||||
|     protectedSessionService.decryptNotes(notes); |  | ||||||
|  |  | ||||||
|     notes.forEach(note => { |  | ||||||
|         note.isProtected = !!note.isProtected |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return notes; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function validateParentChild(parentNoteId, childNoteId, branchId = null) { | function validateParentChild(parentNoteId, childNoteId, branchId = null) { | ||||||
|     if (['root', '_hidden', '_share', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(childNoteId)) { |     if (['root', '_hidden', '_share', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(childNoteId)) { | ||||||
|         return { success: false, message: `Cannot change this note's location.`}; |         return { success: false, message: `Cannot change this note's location.`}; | ||||||
| @@ -39,7 +16,7 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) { | |||||||
|         return { success: false, message: `Cannot move anything into 'none' parent.` }; |         return { success: false, message: `Cannot move anything into 'none' parent.` }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const existing = getExistingBranch(parentNoteId, childNoteId); |     const existing = becca.getBranchFromChildAndParent(childNoteId, parentNoteId); | ||||||
|  |  | ||||||
|     if (existing && (branchId === null || existing.branchId !== branchId)) { |     if (existing && (branchId === null || existing.branchId !== branchId)) { | ||||||
|         const parentNote = becca.getNote(parentNoteId); |         const parentNote = becca.getNote(parentNoteId); | ||||||
| @@ -51,7 +28,7 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) { | |||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!checkTreeCycle(parentNoteId, childNoteId)) { |     if (wouldAddingBranchCreateCycle(parentNoteId, childNoteId)) { | ||||||
|         return { |         return { | ||||||
|             success: false, |             success: false, | ||||||
|             message: 'Moving/cloning note here would create cycle.' |             message: 'Moving/cloning note here would create cycle.' | ||||||
| @@ -68,59 +45,22 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) { | |||||||
|     return { success: true }; |     return { success: true }; | ||||||
| } | } | ||||||
|  |  | ||||||
| function getExistingBranch(parentNoteId, childNoteId) { |  | ||||||
|     const branchId = sql.getValue(` |  | ||||||
|         SELECT branchId  |  | ||||||
|         FROM branches  |  | ||||||
|         WHERE noteId = ?  |  | ||||||
|           AND parentNoteId = ?  |  | ||||||
|           AND isDeleted = 0`, [childNoteId, parentNoteId]); |  | ||||||
|  |  | ||||||
|     return becca.getBranch(branchId); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Tree cycle can be created when cloning or when moving existing clone. This method should detect both cases. |  * Tree cycle can be created when cloning or when moving existing clone. This method should detect both cases. | ||||||
|  */ |  */ | ||||||
| function checkTreeCycle(parentNoteId, childNoteId) { | function wouldAddingBranchCreateCycle(parentNoteId, childNoteId) { | ||||||
|     const subtreeNoteIds = []; |     const childNote = becca.getNote(childNoteId); | ||||||
|  |     const parentNote = becca.getNote(parentNoteId); | ||||||
|  |  | ||||||
|  |     if (!childNote || !parentNote) { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // we'll load the whole subtree - because the cycle can start in one of the notes in the subtree |     // we'll load the whole subtree - because the cycle can start in one of the notes in the subtree | ||||||
|     loadSubtreeNoteIds(childNoteId, subtreeNoteIds); |     const childSubtreeNoteIds = new Set(childNote.getSubtreeNoteIds()); | ||||||
|  |     const parentAncestorNoteIds = parentNote.getAncestorNoteIds(); | ||||||
|  |  | ||||||
|     function checkTreeCycleInner(parentNoteId) { |     return parentAncestorNoteIds.some(parentAncestorNoteId => childSubtreeNoteIds.has(parentAncestorNoteId)); | ||||||
|         if (parentNoteId === 'root') { |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (subtreeNoteIds.includes(parentNoteId)) { |  | ||||||
|             // while towards the root of the tree we encountered noteId which is already present in the subtree |  | ||||||
|             // joining parentNoteId with childNoteId would then clearly create a cycle |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const parentNoteIds = sql.getColumn("SELECT DISTINCT parentNoteId FROM branches WHERE noteId = ? AND isDeleted = 0", [parentNoteId]); |  | ||||||
|  |  | ||||||
|         for (const pid of parentNoteIds) { |  | ||||||
|             if (!checkTreeCycleInner(pid)) { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return checkTreeCycleInner(parentNoteId); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function loadSubtreeNoteIds(parentNoteId, subtreeNoteIds) { |  | ||||||
|     subtreeNoteIds.push(parentNoteId); |  | ||||||
|  |  | ||||||
|     const children = sql.getColumn("SELECT noteId FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]); |  | ||||||
|  |  | ||||||
|     for (const childNoteId of children) { |  | ||||||
|         loadSubtreeNoteIds(childNoteId, subtreeNoteIds); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, foldersFirst = false, sortNatural = false, sortLocale) { | function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, foldersFirst = false, sortNatural = false, sortLocale) { | ||||||
| @@ -295,7 +235,6 @@ function setNoteToParent(noteId, prefix, parentNoteId) { | |||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     getNotes, |  | ||||||
|     validateParentChild, |     validateParentChild, | ||||||
|     sortNotes, |     sortNotes, | ||||||
|     sortNotesIfNeeded, |     sortNotesIfNeeded, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user