mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	refactorings
This commit is contained in:
		| @@ -6,6 +6,10 @@ const becca = require('../becca'); | ||||
| const AbstractBeccaEntity = require("./abstract_becca_entity"); | ||||
| const sql = require("../../services/sql"); | ||||
|  | ||||
| const attachmentRoleToNoteTypeMapping = { | ||||
|     'image': 'image' | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * 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. | ||||
| @@ -92,6 +96,55 @@ class BAttachment extends AbstractBeccaEntity { | ||||
|         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() { | ||||
|         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} = {}) { | ||||
|         return this.getSubtree({includeArchived, includeHidden, resolveSearch}) | ||||
|             .notes | ||||
| @@ -1033,6 +1033,11 @@ class BNote extends AbstractBeccaEntity { | ||||
|         return this.ancestorCache; | ||||
|     } | ||||
|  | ||||
|     /** @returns {string[]} */ | ||||
|     getAncestorNoteIds() { | ||||
|         return this.getAncestors().map(note => note.noteId); | ||||
|     } | ||||
|  | ||||
|     /** @returns {boolean} */ | ||||
|     hasAncestor(ancestorNoteId) { | ||||
|         for (const ancestorNote of this.getAncestors()) { | ||||
| @@ -1408,61 +1413,6 @@ class BNote extends AbstractBeccaEntity { | ||||
|         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. | ||||
|      * | ||||
|   | ||||
| @@ -63,6 +63,12 @@ class BNoteRevision extends AbstractBeccaEntity { | ||||
|         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 | ||||
|      * 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) => { | ||||
|                 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})); | ||||
|  | ||||
|                 cb(result); | ||||
| @@ -52,7 +52,7 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const attributeValues = (await server.get(`attributes/values/${encodeURIComponent(attributeName)}`)) | ||||
|     const attributeValues = (await server.get(`attribute-values/${encodeURIComponent(attributeName)}`)) | ||||
|         .map(attribute => ({ value: attribute })); | ||||
|  | ||||
|     if (attributeValues.length === 0) { | ||||
|   | ||||
| @@ -48,7 +48,7 @@ async function openNoteExternally(noteId, mime) { | ||||
| } | ||||
|  | ||||
| function downloadNoteRevision(noteId, noteRevisionId) { | ||||
|     const url = getUrlForDownload(`api/notes/${noteId}/revisions/${noteRevisionId}/download`); | ||||
|     const url = getUrlForDownload(`api/revisions/${noteRevisionId}/download`); | ||||
|  | ||||
|     download(url); | ||||
| } | ||||
|   | ||||
| @@ -95,7 +95,7 @@ const mentionSetup = { | ||||
|         { | ||||
|             marker: '#', | ||||
|             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 { | ||||
| @@ -110,7 +110,7 @@ const mentionSetup = { | ||||
|         { | ||||
|             marker: '~', | ||||
|             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 { | ||||
|   | ||||
| @@ -219,7 +219,7 @@ export default class ExportDialog extends BasicWidget { | ||||
|     exportBranch(branchId, type, format, version) { | ||||
|         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); | ||||
|     } | ||||
|   | ||||
| @@ -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.'; | ||||
|  | ||||
|             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'); | ||||
|  | ||||
| @@ -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.'; | ||||
|  | ||||
|             if (await dialogService.confirm(text)) { | ||||
|                 await server.remove(`notes/${revisionItem.noteId}/revisions/${revisionItem.noteRevisionId}`); | ||||
|                 await server.remove(`revisions/${revisionItem.noteRevisionId}`); | ||||
|  | ||||
|                 this.loadNoteRevisions(revisionItem.noteId); | ||||
|  | ||||
| @@ -232,7 +232,7 @@ export default class NoteRevisionsDialog extends BasicWidget { | ||||
|     async renderContent(revisionItem) { | ||||
|         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') { | ||||
|             this.$content.html(fullNoteRevision.content); | ||||
|   | ||||
| @@ -142,7 +142,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget { | ||||
|                 $input.prop("type", "text"); | ||||
|  | ||||
|                 // 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) { | ||||
|                         return; | ||||
|                     } | ||||
|   | ||||
| @@ -237,7 +237,7 @@ export default class RelationMapTypeWidget extends TypeWidget { | ||||
|  | ||||
|     async loadNotesAndRelations() { | ||||
|         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 = []; | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| const becca = require("../../becca/becca"); | ||||
| const NotFoundError = require("../../errors/not_found_error"); | ||||
| const utils = require("../../services/utils"); | ||||
| const noteService = require("../../services/notes"); | ||||
|  | ||||
| function getAttachments(req) { | ||||
|     const includeContent = req.query.includeContent === 'true'; | ||||
| @@ -19,18 +18,12 @@ function getAttachments(req) { | ||||
|  | ||||
| function getAttachment(req) { | ||||
|     const includeContent = req.query.includeContent === 'true'; | ||||
|     const {noteId, attachmentId} = req.params; | ||||
|     const {attachmentId} = req.params; | ||||
|  | ||||
|     const note = becca.getNote(noteId); | ||||
|  | ||||
|     if (!note) { | ||||
|         throw new NotFoundError(`Note '${noteId}' doesn't exist.`); | ||||
|     } | ||||
|  | ||||
|     const attachment = note.getAttachmentById(attachmentId); | ||||
|     const attachment = becca.getAttachment(attachmentId); | ||||
|  | ||||
|     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); | ||||
| @@ -72,15 +65,9 @@ function saveAttachment(req) { | ||||
| } | ||||
|  | ||||
| function deleteAttachment(req) { | ||||
|     const {noteId, attachmentId} = req.params; | ||||
|     const {attachmentId} = req.params; | ||||
|  | ||||
|     const note = becca.getNote(noteId); | ||||
|  | ||||
|     if (!note) { | ||||
|         throw new NotFoundError(`Note '${noteId}' doesn't exist.`); | ||||
|     } | ||||
|  | ||||
|     const attachment = note.getAttachmentById(attachmentId); | ||||
|     const attachment = becca.getAttachment(attachmentId); | ||||
|  | ||||
|     if (attachment) { | ||||
|         attachment.markAsDeleted(); | ||||
| @@ -88,15 +75,15 @@ function deleteAttachment(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) { | ||||
|         throw new NotFoundError(`Note '${noteId}' doesn't exist.`); | ||||
|     if (!attachment) { | ||||
|         throw new NotFoundError(`Attachment '${attachmentId}' doesn't exist.`); | ||||
|     } | ||||
|  | ||||
|     return note.convertAttachmentToChildNote(attachmentId); | ||||
|     return attachment.convertToNote(); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   | ||||
| @@ -64,13 +64,7 @@ function getRevisionFilename(noteRevision) { | ||||
| function downloadNoteRevision(req, res) { | ||||
|     const noteRevision = becca.getNoteRevision(req.params.noteRevisionId); | ||||
|  | ||||
|     if (noteRevision.noteId !== req.params.noteId) { | ||||
|         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()) { | ||||
|     if (!noteRevision.isContentAvailable()) { | ||||
|         return res.setHeader("Content-Type", "text/plain") | ||||
|             .status(401) | ||||
|             .send("Protected session not available"); | ||||
|   | ||||
| @@ -127,69 +127,6 @@ function setNoteTypeMime(req) { | ||||
|     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) { | ||||
|     const noteId = req.params.noteId; | ||||
|     const title = req.body.title; | ||||
| @@ -273,6 +210,7 @@ function getDeleteNotesPreview(req) { | ||||
|     if (noteIdsToBeDeleted.size > 0) { | ||||
|         sql.fillParamList(noteIdsToBeDeleted); | ||||
|  | ||||
|         // FIXME: No need to do this in database, can be done with becca data | ||||
|         brokenRelations = sql.getRows(` | ||||
|             SELECT attr.noteId, attr.name, attr.value | ||||
|             FROM attributes attr | ||||
| @@ -334,7 +272,6 @@ module.exports = { | ||||
|     sortChildNotes, | ||||
|     protectNote, | ||||
|     setNoteTypeMime, | ||||
|     getRelationMap, | ||||
|     changeTitle, | ||||
|     duplicateSubtree, | ||||
|     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 fontsRoute = require('./api/fonts'); | ||||
| const etapiTokensApiRoutes = require('./api/etapi_tokens'); | ||||
| const relationMapApiRoute = require('./api/relation-map'); | ||||
| const otherRoute = require('./api/other'); | ||||
| const shareRoutes = require('../share/routes'); | ||||
|  | ||||
| const etapiAuthRoutes = require('../etapi/auth'); | ||||
| const etapiAppInfoRoutes = require('../etapi/app_info'); | ||||
| const etapiAttributeRoutes = require('../etapi/attributes'); | ||||
| @@ -73,7 +75,7 @@ const csrfMiddleware = csurf({ | ||||
| }); | ||||
|  | ||||
| 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(); | ||||
|  | ||||
| @@ -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! | ||||
|     }); | ||||
|  | ||||
|     route(POST, '/login', [loginRateLimiter], loginRoute.login); | ||||
|     route(POST, '/logout', [csrfMiddleware, auth.checkAuth], loginRoute.logout); | ||||
|     route(POST, '/set-password', [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPassword); | ||||
|     route(PST, '/login', [loginRateLimiter], loginRoute.login); | ||||
|     route(PST, '/logout', [csrfMiddleware, auth.checkAuth], loginRoute.logout); | ||||
|     route(PST, '/set-password', [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPassword); | ||||
|     route(GET, '/setup', [], setupRoute.setupPage); | ||||
|  | ||||
|     apiRoute(GET, '/api/tree', treeApiRoute.getTree); | ||||
|     apiRoute(POST, '/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(PST, '/api/tree/load', treeApiRoute.load); | ||||
|  | ||||
|     apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote); | ||||
|     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(POST, '/api/notes/:noteId/revision', notesApiRoute.forceSaveNoteRevision); | ||||
|     apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote); | ||||
|     apiRoute(PST, '/api/notes/:noteId/revision', notesApiRoute.forceSaveNoteRevision); | ||||
|     apiRoute(PST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/sort-children', notesApiRoute.sortChildNotes); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote); | ||||
|     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(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree); | ||||
|     apiRoute(POST, '/api/notes/:noteId/upload-modified-file', notesApiRoute.uploadModifiedFile); | ||||
|  | ||||
|     apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate); | ||||
|  | ||||
|     apiRoute(PST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree); | ||||
|     apiRoute(PST, '/api/notes/:noteId/upload-modified-file', notesApiRoute.uploadModifiedFile); | ||||
|     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/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToNote); | ||||
|     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], | ||||
|         filesRoute.updateFile, apiResultHandler); | ||||
|  | ||||
|     route(GET, '/api/notes/:noteId/open', [auth.checkApiAuthOrElectron], filesRoute.openFile); | ||||
|     route(GET, '/api/notes/:noteId/open-partial', [auth.checkApiAuthOrElectron], | ||||
|         createPartialContentHandler(filesRoute.fileContentProvider, { | ||||
| @@ -166,69 +137,73 @@ function register(app) { | ||||
|     route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); | ||||
|     // this "hacky" path is used for easier referencing of CSS resources | ||||
|     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(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/attribute', attributesRoute.updateNoteAttribute); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/set-attribute', attributesRoute.setNoteAttribute); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/relations/:name/to/:targetNoteId', attributesRoute.createRelation); | ||||
|     apiRoute(DELETE, '/api/notes/:noteId/relations/:name/to/:targetNoteId', attributesRoute.deleteRelation); | ||||
|     apiRoute(DELETE, '/api/notes/:noteId/attributes/:attributeId', attributesRoute.deleteNoteAttribute); | ||||
|     apiRoute(GET, '/api/attributes/names', attributesRoute.getAttributeNames); | ||||
|     apiRoute(GET, '/api/attributes/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); | ||||
|     apiRoute(DEL, '/api/notes/:noteId/relations/:name/to/:targetNoteId', attributesRoute.deleteRelation); | ||||
|     apiRoute(DEL, '/api/notes/:noteId/attributes/:attributeId', attributesRoute.deleteNoteAttribute); | ||||
|     apiRoute(GET, '/api/attribute-names', attributesRoute.getAttributeNames); | ||||
|     apiRoute(GET, '/api/attribute-values/:attributeName', attributesRoute.getValuesForAttribute); | ||||
|  | ||||
|     // :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/notes/:noteId/images/:attachmentId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage); | ||||
|     route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.uploadImage, apiResultHandler); | ||||
|     route(PST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.uploadImage, 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); | ||||
|     // 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', optionsApiRoute.updateOptions); | ||||
|     apiRoute(GET, '/api/options/user-themes', optionsApiRoute.getUserThemes); | ||||
|  | ||||
|     apiRoute(POST, '/api/password/change', passwordApiRoute.changePassword); | ||||
|     apiRoute(POST, '/api/password/reset', passwordApiRoute.resetPassword); | ||||
|     apiRoute(PST, '/api/password/change', passwordApiRoute.changePassword); | ||||
|     apiRoute(PST, '/api/password/reset', passwordApiRoute.resetPassword); | ||||
|  | ||||
|     apiRoute(POST, '/api/sync/test', syncApiRoute.testSync); | ||||
|     apiRoute(POST, '/api/sync/now', syncApiRoute.syncNow); | ||||
|     apiRoute(POST, '/api/sync/fill-entity-changes', syncApiRoute.fillEntityChanges); | ||||
|     apiRoute(POST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync); | ||||
|     apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync); | ||||
|     apiRoute(PST, '/api/sync/test', syncApiRoute.testSync); | ||||
|     apiRoute(PST, '/api/sync/now', syncApiRoute.syncNow); | ||||
|     apiRoute(PST, '/api/sync/fill-entity-changes', syncApiRoute.fillEntityChanges); | ||||
|     apiRoute(PST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync); | ||||
|     apiRoute(PST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync); | ||||
|     route(GET, '/api/sync/check', [auth.checkApiAuth], syncApiRoute.checkSync, apiResultHandler); | ||||
|     route(GET, '/api/sync/changed', [auth.checkApiAuth], syncApiRoute.getChanged, apiResultHandler); | ||||
|     route(PUT, '/api/sync/update', [auth.checkApiAuth], syncApiRoute.update, apiResultHandler); | ||||
|     route(POST, '/api/sync/finished', [auth.checkApiAuth], syncApiRoute.syncFinished, apiResultHandler); | ||||
|     route(POST, '/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/finished', [auth.checkApiAuth], syncApiRoute.syncFinished, apiResultHandler); | ||||
|     route(PST, '/api/sync/check-entity-changes', [auth.checkApiAuth], syncApiRoute.checkEntityChanges, apiResultHandler); | ||||
|     route(PST, '/api/sync/queue-sector/:entityName/:sector', [auth.checkApiAuth], syncApiRoute.queueSector, 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); | ||||
|  | ||||
|     // docker health check | ||||
| @@ -236,82 +211,102 @@ function register(app) { | ||||
|  | ||||
|     // group of services below are meant to be executed from outside | ||||
|     route(GET, '/api/setup/status', [], setupApiRoute.getStatus, apiResultHandler); | ||||
|     route(POST, '/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/new-document', [auth.checkAppNotInitialized], setupApiRoute.setupNewDocument, 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(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(POST, '/api/sql/execute/:noteId', sqlRoute.execute); | ||||
|     route(POST, '/api/database/anonymize/:type', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false); | ||||
|     apiRoute(PST, '/api/sql/execute/:noteId', sqlRoute.execute); | ||||
|     route(PST, '/api/database/anonymize/:type', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false); | ||||
|  | ||||
|     // 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 | ||||
|     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(POST, '/api/script/exec', scriptRoute.exec); | ||||
|     apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run); | ||||
|     apiRoute(PST, '/api/script/exec', scriptRoute.exec); | ||||
|     apiRoute(PST, '/api/script/run/:noteId', scriptRoute.run); | ||||
|     apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles); | ||||
|     apiRoute(GET, '/api/script/widgets', scriptRoute.getWidgetBundles); | ||||
|     apiRoute(GET, '/api/script/bundle/:noteId', scriptRoute.getBundle); | ||||
|     apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles); | ||||
|  | ||||
|     // no CSRF since this is called from android app | ||||
|     route(POST, '/api/sender/login', [loginRateLimiter], loginApiRoute.token, apiResultHandler); | ||||
|     route(POST, '/api/sender/image', [auth.checkEtapiToken, uploadMiddlewareWithErrorHandling], senderRoute.uploadImage, apiResultHandler); | ||||
|     route(POST, '/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); | ||||
|     route(PST, '/api/sender/login', [loginRateLimiter], loginApiRoute.token, apiResultHandler); | ||||
|     route(PST, '/api/sender/image', [auth.checkEtapiToken, uploadMiddlewareWithErrorHandling], senderRoute.uploadImage, apiResultHandler); | ||||
|     route(PST, '/api/sender/note', [auth.checkEtapiToken], senderRoute.saveNote, apiResultHandler); | ||||
|  | ||||
|     apiRoute(GET, '/api/keyboard-actions', keysRoute.getKeyboardActions); | ||||
|     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/stats/note-size/:noteId', statsRoute.getNoteSize); | ||||
|     apiRoute(GET, '/api/stats/subtree-size/:noteId', statsRoute.getSubtreeSize); | ||||
|  | ||||
|     apiRoute(POST, '/api/delete-notes-preview', notesApiRoute.getDeleteNotesPreview); | ||||
|  | ||||
|     apiRoute(PST, '/api/delete-notes-preview', notesApiRoute.getDeleteNotesPreview); | ||||
|     route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss); | ||||
|     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(POST, '/api/etapi-tokens', etapiTokensApiRoutes.createToken); | ||||
|     apiRoute(PATCH, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.patchToken); | ||||
|     apiRoute(DELETE, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.deleteToken); | ||||
|     apiRoute(PST, '/api/note-map/:noteId/tree', noteMapRoute.getTreeMap); | ||||
|     apiRoute(PST, '/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); | ||||
|  | ||||
|     shareRoutes.register(router); | ||||
|  | ||||
|   | ||||
| @@ -23,12 +23,8 @@ function isHoistedInHiddenSubtree() { | ||||
|     return hoistedNote.isHiddenCompletely(); | ||||
| } | ||||
|  | ||||
| function getHoistedNote() { | ||||
|     return becca.getNote(cls.getHoistedNoteId()); | ||||
| } | ||||
|  | ||||
| function getWorkspaceNote() { | ||||
|     const hoistedNote = getHoistedNote(); | ||||
|     const hoistedNote = becca.getNote(cls.getHoistedNoteId()); | ||||
|  | ||||
|     if (hoistedNote.isRoot() || hoistedNote.hasLabel('workspace')) { | ||||
|         return hoistedNote; | ||||
| @@ -39,7 +35,6 @@ function getWorkspaceNote() { | ||||
|  | ||||
| module.exports = { | ||||
|     getHoistedNoteId, | ||||
|     getHoistedNote, | ||||
|     getWorkspaceNote, | ||||
|     isHoistedInHiddenSubtree | ||||
| }; | ||||
|   | ||||
| @@ -2,35 +2,32 @@ | ||||
|  | ||||
| const log = require('./log'); | ||||
| const sql = require('./sql'); | ||||
| const protectedSession = require("./protected_session"); | ||||
| const protectedSessionService = require("./protected_session"); | ||||
|  | ||||
| /** | ||||
|  * @param {BNote} 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()) { | ||||
|         if (note.isProtected !== revision.isProtected) { | ||||
|             if (!protectedSession.isProtectedSessionAvailable()) { | ||||
|                 log.error("Protected session is not available to fix note revisions."); | ||||
|         if (note.isProtected === revision.isProtected) { | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|         try { | ||||
|             const content = revision.getContent(); | ||||
|  | ||||
|             try { | ||||
|                 const content = revision.getContent(); | ||||
|             revision.isProtected = note.isProtected; | ||||
|  | ||||
|                 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 | ||||
|                 revision.setContent(content); | ||||
|  | ||||
|                 revision.save(); | ||||
|             } | ||||
|             catch (e) { | ||||
|                 log.error(`Could not un/protect note revision ID = ${revision.noteRevisionId}`); | ||||
|  | ||||
|                 throw e; | ||||
|             } | ||||
|             throw e; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -9,7 +9,6 @@ const protectedSessionService = require('../services/protected_session'); | ||||
| const log = require('../services/log'); | ||||
| const utils = require('../services/utils'); | ||||
| const noteRevisionService = require('../services/note_revisions'); | ||||
| const attributeService = require('../services/attributes'); | ||||
| const request = require('./request'); | ||||
| const path = require('path'); | ||||
| const url = require('url'); | ||||
| @@ -21,8 +20,8 @@ const dayjs = require("dayjs"); | ||||
| const htmlSanitizer = require("./html_sanitizer"); | ||||
| const ValidationError = require("../errors/validation_error"); | ||||
| const noteTypesService = require("./note_types"); | ||||
| const {attach} = require("jsdom/lib/jsdom/living/helpers/svg/basic-types.js"); | ||||
|  | ||||
| /** @param {BNote} parentNote */ | ||||
| function getNewNotePosition(parentNote) { | ||||
|     if (parentNote.isLabelTruthy('newNotesOnTop')) { | ||||
|         const minNotePos = parentNote.getChildBranches() | ||||
| @@ -37,6 +36,7 @@ function getNewNotePosition(parentNote) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** @param {BNote} note */ | ||||
| function triggerNoteTitleChanged(note) { | ||||
|     eventService.emit(eventService.NOTE_TITLE_CHANGED, note); | ||||
| } | ||||
| @@ -53,6 +53,10 @@ function deriveMime(type, mime) { | ||||
|     return noteTypesService.getDefaultMimeForNoteType(type); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {BNote} parentNote | ||||
|  * @param {BNote} childNote | ||||
|  */ | ||||
| function copyChildAttributes(parentNote, childNote) { | ||||
|     const hasAlreadyTemplate = childNote.hasRelation('template'); | ||||
|  | ||||
| @@ -78,6 +82,7 @@ function copyChildAttributes(parentNote, childNote) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** @param {BNote} parentNote */ | ||||
| function getNewNoteTitle(parentNote) { | ||||
|     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) { | ||||
|     protectNote(note, protect); | ||||
|  | ||||
| @@ -290,7 +301,15 @@ function protectNoteRecursively(note, protect, includingSubTree, taskContext) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {BNote} note | ||||
|  * @param {boolean} 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 { | ||||
|         if (protect !== note.isProtected) { | ||||
|             const content = note.getContent(); | ||||
| @@ -310,7 +329,7 @@ function protectNote(note, protect) { | ||||
|         noteRevisionService.protectNoteRevisions(note); | ||||
|     } | ||||
|     catch (e) { | ||||
|         log.error(`Could not un/protect note ID = ${note.noteId}`); | ||||
|         log.error(`Could not un/protect note '${note.noteId}'`); | ||||
|  | ||||
|         throw e; | ||||
|     } | ||||
| @@ -565,6 +584,7 @@ function saveLinks(note, content) { | ||||
|     return content; | ||||
| } | ||||
|  | ||||
| /** @param {BNote} note */ | ||||
| function saveNoteRevisionIfNeeded(note) { | ||||
|     // files and images are versioned separately | ||||
|     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) | ||||
|  */ | ||||
| async function asyncPostProcessContent(note, content) { | ||||
|   | ||||
| @@ -4,7 +4,6 @@ const becca = require("../becca/becca"); | ||||
| const noteService = require("./notes"); | ||||
| const dateUtils = require("./date_utils"); | ||||
| const log = require("./log"); | ||||
| const hiddenSubtreeService = require("./hidden_subtree"); | ||||
| const hoistedNoteService = require("./hoisted_note"); | ||||
| const searchService = require("./search/services/search"); | ||||
| const SearchContext = require("./search/search_context"); | ||||
|   | ||||
| @@ -4,31 +4,8 @@ const sql = require('./sql'); | ||||
| const log = require('./log'); | ||||
| const BBranch = require('../becca/entities/bbranch'); | ||||
| const entityChangesService = require('./entity_changes'); | ||||
| const protectedSessionService = require('./protected_session'); | ||||
| 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) { | ||||
|     if (['root', '_hidden', '_share', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(childNoteId)) { | ||||
|         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.` }; | ||||
|     } | ||||
|  | ||||
|     const existing = getExistingBranch(parentNoteId, childNoteId); | ||||
|     const existing = becca.getBranchFromChildAndParent(childNoteId, parentNoteId); | ||||
|  | ||||
|     if (existing && (branchId === null || existing.branchId !== branchId)) { | ||||
|         const parentNote = becca.getNote(parentNoteId); | ||||
| @@ -51,7 +28,7 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) { | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     if (!checkTreeCycle(parentNoteId, childNoteId)) { | ||||
|     if (wouldAddingBranchCreateCycle(parentNoteId, childNoteId)) { | ||||
|         return { | ||||
|             success: false, | ||||
|             message: 'Moving/cloning note here would create cycle.' | ||||
| @@ -68,59 +45,22 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) { | ||||
|     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. | ||||
|  */ | ||||
| function checkTreeCycle(parentNoteId, childNoteId) { | ||||
|     const subtreeNoteIds = []; | ||||
| function wouldAddingBranchCreateCycle(parentNoteId, childNoteId) { | ||||
|     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 | ||||
|     loadSubtreeNoteIds(childNoteId, subtreeNoteIds); | ||||
|     const childSubtreeNoteIds = new Set(childNote.getSubtreeNoteIds()); | ||||
|     const parentAncestorNoteIds = parentNote.getAncestorNoteIds(); | ||||
|  | ||||
|     function checkTreeCycleInner(parentNoteId) { | ||||
|         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); | ||||
|     } | ||||
|     return parentAncestorNoteIds.some(parentAncestorNoteId => childSubtreeNoteIds.has(parentAncestorNoteId)); | ||||
| } | ||||
|  | ||||
| function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, foldersFirst = false, sortNatural = false, sortLocale) { | ||||
| @@ -295,7 +235,6 @@ function setNoteToParent(noteId, prefix, parentNoteId) { | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     getNotes, | ||||
|     validateParentChild, | ||||
|     sortNotes, | ||||
|     sortNotesIfNeeded, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user