From 291b791b675d9f99b84b00dd5e52d3ea9608c35e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 21:21:22 +0300 Subject: [PATCH 01/25] server-ts: Convert routes/api/image --- src/becca/entities/bnote.ts | 2 +- src/routes/api/{image.js => image.ts} | 34 +++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) rename src/routes/api/{image.js => image.ts} (75%) diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index ec094cde6..6b9118a67 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -222,7 +222,7 @@ class BNote extends AbstractBeccaEntity { /** * @throws Error in case of invalid JSON */ - getJsonContent(): {} | null { + getJsonContent(): any | null { const content = this.getContent(); if (typeof content !== "string" || !content || !content.trim()) { diff --git a/src/routes/api/image.js b/src/routes/api/image.ts similarity index 75% rename from src/routes/api/image.js rename to src/routes/api/image.ts index 32bce601b..1a2750195 100644 --- a/src/routes/api/image.js +++ b/src/routes/api/image.ts @@ -1,27 +1,26 @@ "use strict"; -const imageService = require('../../services/image'); -const becca = require('../../becca/becca'); +import imageService = require('../../services/image'); +import becca = require('../../becca/becca'); const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR; -const fs = require('fs'); +import fs = require('fs'); +import { Request, Response } from 'express'; +import BNote = require('../../becca/entities/bnote'); +import BRevision = require('../../becca/entities/brevision'); -function returnImageFromNote(req, res) { +function returnImageFromNote(req: Request, res: Response) { const image = becca.getNote(req.params.noteId); return returnImageInt(image, res); } -function returnImageFromRevision(req, res) { +function returnImageFromRevision(req: Request, res: Response) { const image = becca.getRevision(req.params.revisionId); return returnImageInt(image, res); } -/** - * @param {BNote|BRevision} image - * @param res - */ -function returnImageInt(image, res) { +function returnImageInt(image: BNote | BRevision | null, res: Response) { if (!image) { res.set('Content-Type', 'image/png'); return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`)); @@ -40,12 +39,13 @@ function returnImageInt(image, res) { } } -function renderSvgAttachment(image, res, attachmentName) { +function renderSvgAttachment(image: BNote | BRevision, res: Response, attachmentName: string) { let svgString = '' const attachment = image.getAttachmentByTitle(attachmentName); - if (attachment) { - svgString = attachment.getContent(); + const content = attachment.getContent(); + if (attachment && typeof content === "string") { + svgString = content; } else { // backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key const contentSvg = image.getJsonContentSafely()?.svg; @@ -62,7 +62,7 @@ function renderSvgAttachment(image, res, attachmentName) { } -function returnAttachedImage(req, res) { +function returnAttachedImage(req: Request, res: Response) { const attachment = becca.getAttachment(req.params.attachmentId); if (!attachment) { @@ -81,9 +81,9 @@ function returnAttachedImage(req, res) { res.send(attachment.getContent()); } -function updateImage(req) { +function updateImage(req: Request) { const {noteId} = req.params; - const {file} = req; + const {file} = (req as any); const note = becca.getNoteOrThrow(noteId); @@ -99,7 +99,7 @@ function updateImage(req) { return { uploaded: true }; } -module.exports = { +export = { returnImageFromNote, returnImageFromRevision, returnAttachedImage, From 39027190087f320258ab5c4b1a2e08461b3d3270 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 21:30:27 +0300 Subject: [PATCH 02/25] server-ts: Convert routes/api/import --- src/routes/api/{import.js => import.ts} | 60 ++++++++++++++++--------- src/routes/routes.js | 2 +- src/services/import/enex.ts | 2 +- src/services/task_context.ts | 2 +- src/services/ws.ts | 2 +- 5 files changed, 42 insertions(+), 26 deletions(-) rename src/routes/api/{import.js => import.ts} (65%) diff --git a/src/routes/api/import.js b/src/routes/api/import.ts similarity index 65% rename from src/routes/api/import.js rename to src/routes/api/import.ts index 978fe9d7c..6062ae37f 100644 --- a/src/routes/api/import.js +++ b/src/routes/api/import.ts @@ -1,18 +1,20 @@ "use strict"; -const enexImportService = require('../../services/import/enex'); -const opmlImportService = require('../../services/import/opml'); -const zipImportService = require('../../services/import/zip'); -const singleImportService = require('../../services/import/single'); -const cls = require('../../services/cls'); -const path = require('path'); -const becca = require('../../becca/becca'); -const beccaLoader = require('../../becca/becca_loader'); -const log = require('../../services/log'); -const TaskContext = require('../../services/task_context'); -const ValidationError = require('../../errors/validation_error'); +import enexImportService = require('../../services/import/enex'); +import opmlImportService = require('../../services/import/opml'); +import zipImportService = require('../../services/import/zip'); +import singleImportService = require('../../services/import/single'); +import cls = require('../../services/cls'); +import path = require('path'); +import becca = require('../../becca/becca'); +import beccaLoader = require('../../becca/becca_loader'); +import log = require('../../services/log'); +import TaskContext = require('../../services/task_context'); +import ValidationError = require('../../errors/validation_error'); +import { Request } from 'express'; +import BNote = require('../../becca/entities/bnote'); -async function importNotesToBranch(req) { +async function importNotesToBranch(req: Request) { const { parentNoteId } = req.params; const { taskId, last } = req.body; @@ -25,7 +27,7 @@ async function importNotesToBranch(req) { replaceUnderscoresWithSpaces: req.body.replaceUnderscoresWithSpaces !== 'false' }; - const file = req.file; + const file = (req as any).file; if (!file) { throw new ValidationError("No file has been uploaded"); @@ -42,7 +44,7 @@ async function importNotesToBranch(req) { // eliminate flickering during import cls.ignoreEntityChangeIds(); - let note; // typically root of the import - client can show it after finishing the import + let note: BNote | null; // typically root of the import - client can show it after finishing the import const taskContext = TaskContext.getInstance(taskId, 'importNotes', options); @@ -50,14 +52,24 @@ async function importNotesToBranch(req) { if (extension === '.zip' && options.explodeArchives) { note = await zipImportService.importZip(taskContext, file.buffer, parentNote); } else if (extension === '.opml' && options.explodeArchives) { - note = await opmlImportService.importOpml(taskContext, file.buffer, parentNote); + const importResult = await opmlImportService.importOpml(taskContext, file.buffer, parentNote); + if (!Array.isArray(importResult)) { + note = importResult; + } else { + return importResult; + } } else if (extension === '.enex' && options.explodeArchives) { - note = await enexImportService.importEnex(taskContext, file, parentNote); + const importResult = await enexImportService.importEnex(taskContext, file, parentNote); + if (!Array.isArray(importResult)) { + note = importResult; + } else { + return importResult; + } } else { note = await singleImportService.importSingleFile(taskContext, file, parentNote); } } - catch (e) { + catch (e: any) { const message = `Import failed with following error: '${e.message}'. More details might be in the logs.`; taskContext.reportError(message); @@ -66,11 +78,15 @@ async function importNotesToBranch(req) { return [500, message]; } + if (!note) { + return [500, "No note was generated as a result of the import."]; + } + if (last === "true") { // small timeout to avoid race condition (the message is received before the transaction is committed) setTimeout(() => taskContext.taskSucceeded({ parentNoteId: parentNoteId, - importedNoteId: note.noteId + importedNoteId: note?.noteId }), 1000); } @@ -80,7 +96,7 @@ async function importNotesToBranch(req) { return note.getPojo(); } -async function importAttachmentsToNote(req) { +async function importAttachmentsToNote(req: Request) { const { parentNoteId } = req.params; const { taskId, last } = req.body; @@ -88,7 +104,7 @@ async function importAttachmentsToNote(req) { shrinkImages: req.body.shrinkImages !== 'false', }; - const file = req.file; + const file = (req as any).file; if (!file) { throw new ValidationError("No file has been uploaded"); @@ -102,7 +118,7 @@ async function importAttachmentsToNote(req) { try { await singleImportService.importAttachment(taskContext, file, parentNote); } - catch (e) { + catch (e: any) { const message = `Import failed with following error: '${e.message}'. More details might be in the logs.`; taskContext.reportError(message); @@ -119,7 +135,7 @@ async function importAttachmentsToNote(req) { } } -module.exports = { +export = { importNotesToBranch, importAttachmentsToNote }; diff --git a/src/routes/routes.js b/src/routes/routes.js index 21dbe6166..384c2c737 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -37,7 +37,7 @@ const loginApiRoute = require('./api/login.js'); const recentNotesRoute = require('./api/recent_notes.js'); const appInfoRoute = require('./api/app_info'); const exportRoute = require('./api/export'); -const importRoute = require('./api/import.js'); +const importRoute = require('./api/import'); const setupApiRoute = require('./api/setup.js'); const sqlRoute = require('./api/sql'); const databaseRoute = require('./api/database'); diff --git a/src/services/import/enex.ts b/src/services/import/enex.ts index 8277ae1bd..5e715a0ca 100644 --- a/src/services/import/enex.ts +++ b/src/services/import/enex.ts @@ -55,7 +55,7 @@ interface Note { let note: Partial = {}; let resource: Resource; -function importEnex(taskContext: TaskContext, file: File, parentNote: BNote) { +function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Promise { const saxStream = sax.createStream(true); const rootNoteTitle = file.originalname.toLowerCase().endsWith(".enex") diff --git a/src/services/task_context.ts b/src/services/task_context.ts index bacf3e8f8..27f7d1358 100644 --- a/src/services/task_context.ts +++ b/src/services/task_context.ts @@ -66,7 +66,7 @@ class TaskContext { }); } - taskSucceeded(result?: string) { + taskSucceeded(result?: string | Record) { ws.sendMessageToAllClients({ type: 'taskSucceeded', taskId: this.taskId, diff --git a/src/services/ws.ts b/src/services/ws.ts index 1b581d358..f3217bfd2 100644 --- a/src/services/ws.ts +++ b/src/services/ws.ts @@ -42,7 +42,7 @@ interface Message { taskType?: string | null; message?: string; reason?: string; - result?: string; + result?: string | Record; script?: string; params?: any[]; From b0d6035a674ae4c2d6827c4fac04aa641a21bc9a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 21:31:09 +0300 Subject: [PATCH 03/25] server-ts: Convert routes/api/keys --- src/routes/api/{keys.js => keys.ts} | 6 +++--- src/routes/routes.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/routes/api/{keys.js => keys.ts} (74%) diff --git a/src/routes/api/keys.js b/src/routes/api/keys.ts similarity index 74% rename from src/routes/api/keys.js rename to src/routes/api/keys.ts index bc1b97d4a..fe4f208f9 100644 --- a/src/routes/api/keys.js +++ b/src/routes/api/keys.ts @@ -1,7 +1,7 @@ "use strict"; -const keyboardActions = require('../../services/keyboard_actions'); -const becca = require('../../becca/becca'); +import keyboardActions = require('../../services/keyboard_actions'); +import becca = require('../../becca/becca'); function getKeyboardActions() { return keyboardActions.getKeyboardActions(); @@ -14,7 +14,7 @@ function getShortcutsForNotes() { return labels.filter(attr => becca.getNote(attr.noteId)?.type !== 'launcher'); } -module.exports = { +export = { getKeyboardActions, getShortcutsForNotes }; diff --git a/src/routes/routes.js b/src/routes/routes.js index 384c2c737..76c27672d 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -52,7 +52,7 @@ const specialNotesRoute = require('./api/special_notes'); const noteMapRoute = require('./api/note_map.js'); const clipperRoute = require('./api/clipper'); const similarNotesRoute = require('./api/similar_notes.js'); -const keysRoute = require('./api/keys.js'); +const keysRoute = require('./api/keys'); const backendLogRoute = require('./api/backend_log'); const statsRoute = require('./api/stats.js'); const fontsRoute = require('./api/fonts'); From b1744c3867ef67bcb843960cb114368032f4557e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 21:34:34 +0300 Subject: [PATCH 04/25] server-ts: Convert routes/api/login --- src/routes/api/{login.js => login.ts} | 41 ++++++++++++++++----------- src/routes/routes.js | 2 +- src/services/events.ts | 2 +- 3 files changed, 26 insertions(+), 19 deletions(-) rename src/routes/api/{login.js => login.ts} (73%) diff --git a/src/routes/api/login.js b/src/routes/api/login.ts similarity index 73% rename from src/routes/api/login.js rename to src/routes/api/login.ts index 9cb0ec8bc..a4c41c460 100644 --- a/src/routes/api/login.js +++ b/src/routes/api/login.ts @@ -1,19 +1,20 @@ "use strict"; -const options = require('../../services/options'); -const utils = require('../../services/utils'); -const dateUtils = require('../../services/date_utils'); -const instanceId = require('../../services/instance_id'); -const passwordEncryptionService = require('../../services/encryption/password_encryption'); -const protectedSessionService = require('../../services/protected_session'); -const appInfo = require('../../services/app_info'); -const eventService = require('../../services/events'); -const sqlInit = require('../../services/sql_init'); -const sql = require('../../services/sql'); -const ws = require('../../services/ws'); -const etapiTokenService = require('../../services/etapi_tokens'); +import options = require('../../services/options'); +import utils = require('../../services/utils'); +import dateUtils = require('../../services/date_utils'); +import instanceId = require('../../services/instance_id'); +import passwordEncryptionService = require('../../services/encryption/password_encryption'); +import protectedSessionService = require('../../services/protected_session'); +import appInfo = require('../../services/app_info'); +import eventService = require('../../services/events'); +import sqlInit = require('../../services/sql_init'); +import sql = require('../../services/sql'); +import ws = require('../../services/ws'); +import etapiTokenService = require('../../services/etapi_tokens'); +import { Request } from 'express'; -function loginSync(req) { +function loginSync(req: Request) { if (!sqlInit.schemaExists()) { return [500, { message: "DB schema does not exist, can't sync." }]; } @@ -44,7 +45,7 @@ function loginSync(req) { return [400, { message: "Sync login credentials are incorrect. It looks like you're trying to sync two different initialized documents which is not possible." }]; } - req.session.loggedIn = true; + (req as any).session.loggedIn = true; return { instanceId: instanceId, @@ -52,7 +53,7 @@ function loginSync(req) { }; } -function loginToProtectedSession(req) { +function loginToProtectedSession(req: Request) { const password = req.body.password; if (!passwordEncryptionService.verifyPassword(password)) { @@ -63,6 +64,12 @@ function loginToProtectedSession(req) { } const decryptedDataKey = passwordEncryptionService.getDataKey(password); + if (!decryptedDataKey) { + return { + success: false, + message: "Unable to obtain data key." + } + } protectedSessionService.setDataKey(decryptedDataKey); @@ -87,7 +94,7 @@ function touchProtectedSession() { protectedSessionService.touchProtectedSession(); } -function token(req) { +function token(req: Request) { const password = req.body.password; if (!passwordEncryptionService.verifyPassword(password)) { @@ -102,7 +109,7 @@ function token(req) { return { token: authToken }; } -module.exports = { +export = { loginSync, loginToProtectedSession, logoutFromProtectedSession, diff --git a/src/routes/routes.js b/src/routes/routes.js index 76c27672d..7799cb415 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -33,7 +33,7 @@ const recentChangesApiRoute = require('./api/recent_changes.js'); const optionsApiRoute = require('./api/options.js'); const passwordApiRoute = require('./api/password'); const syncApiRoute = require('./api/sync'); -const loginApiRoute = require('./api/login.js'); +const loginApiRoute = require('./api/login'); const recentNotesRoute = require('./api/recent_notes.js'); const appInfoRoute = require('./api/app_info'); const exportRoute = require('./api/export'); diff --git a/src/services/events.ts b/src/services/events.ts index fffcc3982..327443a95 100644 --- a/src/services/events.ts +++ b/src/services/events.ts @@ -44,7 +44,7 @@ function subscribeBeccaLoader(eventTypes: EventType, listener: EventListener) { } } -function emit(eventType: string, data: any) { +function emit(eventType: string, data?: any) { const listeners = eventListeners[eventType]; if (listeners) { From 37697c7db72502ae221fbf8a072483d6fa4c0c93 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 21:45:58 +0300 Subject: [PATCH 05/25] server-ts: Convert routes/api/note_map --- src/routes/api/{note_map.js => note_map.ts} | 84 +++++++++++---------- src/routes/routes.js | 2 +- 2 files changed, 46 insertions(+), 40 deletions(-) rename src/routes/api/{note_map.js => note_map.ts} (81%) diff --git a/src/routes/api/note_map.js b/src/routes/api/note_map.ts similarity index 81% rename from src/routes/api/note_map.js rename to src/routes/api/note_map.ts index 2e13f6167..97a0bf6f6 100644 --- a/src/routes/api/note_map.js +++ b/src/routes/api/note_map.ts @@ -1,18 +1,25 @@ "use strict"; -const becca = require('../../becca/becca'); -const { JSDOM } = require("jsdom"); +import becca = require('../../becca/becca'); +import { JSDOM } from "jsdom"; +import BNote = require('../../becca/entities/bnote'); +import BAttribute = require('../../becca/entities/battribute'); +import { Request } from 'express'; +import ValidationError = require('../../errors/validation_error'); -function buildDescendantCountMap(noteIdsToCount) { +function buildDescendantCountMap(noteIdsToCount: string[]) { if (!Array.isArray(noteIdsToCount)) { throw new Error('noteIdsToCount: type error'); } const noteIdToCountMap = Object.create(null); - function getCount(noteId) { + function getCount(noteId: string) { if (!(noteId in noteIdToCountMap)) { const note = becca.getNote(noteId); + if (!note) { + return; + } const hiddenImageNoteIds = note.getRelations('imageLink').map(rel => rel.value); const childNoteIds = note.children.map(child => child.noteId); @@ -33,19 +40,14 @@ function buildDescendantCountMap(noteIdsToCount) { return noteIdToCountMap; } -/** - * @param {BNote} note - * @param {int} depth - * @returns {string[]} noteIds - */ -function getNeighbors(note, depth) { +function getNeighbors(note: BNote, depth: number): string[] { if (depth === 0) { return []; } const retNoteIds = []; - function isIgnoredRelation(relation) { + function isIgnoredRelation(relation: BAttribute) { return ['relationMapLink', 'template', 'inherit', 'image', 'ancestor'].includes(relation.name); } @@ -90,8 +92,9 @@ function getNeighbors(note, depth) { return retNoteIds; } -function getLinkMap(req) { - const mapRootNote = becca.getNote(req.params.noteId); +function getLinkMap(req: Request) { + const mapRootNote = becca.getNoteOrThrow(req.params.noteId); + // if the map root itself has "excludeFromNoteMap" attribute (journal typically) then there wouldn't be anything // to display, so we'll just ignore it const ignoreExcludeFromNoteMap = mapRootNote.isLabelTruthy('excludeFromNoteMap'); @@ -125,7 +128,7 @@ function getLinkMap(req) { const noteIdsArray = Array.from(noteIds) const notes = noteIdsArray.map(noteId => { - const note = becca.getNote(noteId); + const note = becca.getNoteOrThrow(noteId); return [ note.noteId, @@ -144,6 +147,9 @@ function getLinkMap(req) { } else if (rel.name === 'imageLink') { const parentNote = becca.getNote(rel.noteId); + if (!parentNote) { + return false; + } return !parentNote.getChildNotes().find(childNote => childNote.noteId === rel.value); } @@ -165,8 +171,8 @@ function getLinkMap(req) { }; } -function getTreeMap(req) { - const mapRootNote = becca.getNote(req.params.noteId); +function getTreeMap(req: Request) { + const mapRootNote = becca.getNoteOrThrow(req.params.noteId); // if the map root itself has "excludeFromNoteMap" (journal typically) then there wouldn't be anything to display, // so we'll just ignore it const ignoreExcludeFromNoteMap = mapRootNote.isLabelTruthy('excludeFromNoteMap'); @@ -198,8 +204,8 @@ function getTreeMap(req) { note.getLabelValue('color') ]); - const noteIds = new Set(); - notes.forEach(([noteId]) => noteIds.add(noteId)); + const noteIds = new Set(); + notes.forEach(([noteId]) => noteId && noteIds.add(noteId)); const links = []; @@ -225,7 +231,7 @@ function getTreeMap(req) { }; } -function updateDescendantCountMapForSearch(noteIdToDescendantCountMap, relationships) { +function updateDescendantCountMapForSearch(noteIdToDescendantCountMap: Record, relationships: { parentNoteId: string, childNoteId: string }[]) { for (const {parentNoteId, childNoteId} of relationships) { const parentNote = becca.notes[parentNoteId]; if (!parentNote || parentNote.type !== 'search') { @@ -237,16 +243,17 @@ function updateDescendantCountMapForSearch(noteIdToDescendantCountMap, relations } } -function removeImages(document) { +function removeImages(document: Document) { const images = document.getElementsByTagName('img'); - while (images.length > 0) { - images[0].parentNode.removeChild(images[0]); + while (images && images.length > 0) { + images[0]?.parentNode?.removeChild(images[0]); } } const EXCERPT_CHAR_LIMIT = 200; +type ElementOrText = (Element | Text); -function findExcerpts(sourceNote, referencedNoteId) { +function findExcerpts(sourceNote: BNote, referencedNoteId: string) { const html = sourceNote.getContent(); const document = new JSDOM(html).window.document; @@ -263,25 +270,24 @@ function findExcerpts(sourceNote, referencedNoteId) { linkEl.classList.add("backlink-link"); - let centerEl = linkEl; + let centerEl: HTMLElement = linkEl; - while (centerEl.tagName !== 'BODY' && centerEl.parentElement?.textContent?.length <= EXCERPT_CHAR_LIMIT) { + while (centerEl.tagName !== 'BODY' && centerEl.parentElement && (centerEl.parentElement?.textContent?.length || 0) <= EXCERPT_CHAR_LIMIT) { centerEl = centerEl.parentElement; } - /** @var {HTMLElement[]} */ - const excerptEls = [centerEl]; - let excerptLength = centerEl.textContent.length; - let left = centerEl; - let right = centerEl; + const excerptEls: ElementOrText[] = [centerEl]; + let excerptLength = centerEl.textContent?.length || 0; + let left: ElementOrText = centerEl; + let right: ElementOrText = centerEl; while (excerptLength < EXCERPT_CHAR_LIMIT) { let added = false; - const prev = left.previousElementSibling; + const prev: Element | null = left.previousElementSibling; if (prev) { - const prevText = prev.textContent; + const prevText = prev.textContent || ""; if (prevText.length + excerptLength > EXCERPT_CHAR_LIMIT) { const prefix = prevText.substr(prevText.length - (EXCERPT_CHAR_LIMIT - excerptLength)); @@ -298,12 +304,12 @@ function findExcerpts(sourceNote, referencedNoteId) { added = true; } - const next = right.nextElementSibling; + const next: Element | null = right.nextElementSibling; if (next) { const nextText = next.textContent; - if (nextText.length + excerptLength > EXCERPT_CHAR_LIMIT) { + if (nextText && nextText.length + excerptLength > EXCERPT_CHAR_LIMIT) { const suffix = nextText.substr(nextText.length - (EXCERPT_CHAR_LIMIT - excerptLength)); const textNode = document.createTextNode(`${suffix}…`); @@ -314,7 +320,7 @@ function findExcerpts(sourceNote, referencedNoteId) { right = next; excerptEls.push(right); - excerptLength += nextText.length; + excerptLength += nextText?.length || 0; added = true; } @@ -336,13 +342,13 @@ function findExcerpts(sourceNote, referencedNoteId) { return excerpts; } -function getFilteredBacklinks(note) { +function getFilteredBacklinks(note: BNote) { return note.getTargetRelations() // search notes have "ancestor" relations which are not interesting .filter(relation => !!relation.getNote() && relation.getNote().type !== 'search'); } -function getBacklinkCount(req) { +function getBacklinkCount(req: Request) { const {noteId} = req.params; const note = becca.getNoteOrThrow(noteId); @@ -352,7 +358,7 @@ function getBacklinkCount(req) { }; } -function getBacklinks(req) { +function getBacklinks(req: Request) { const {noteId} = req.params; const note = becca.getNoteOrThrow(noteId); @@ -379,7 +385,7 @@ function getBacklinks(req) { }); } -module.exports = { +export = { getLinkMap, getTreeMap, getBacklinkCount, diff --git a/src/routes/routes.js b/src/routes/routes.js index 7799cb415..ed48edb50 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -49,7 +49,7 @@ const filesRoute = require('./api/files'); const searchRoute = require('./api/search'); const bulkActionRoute = require('./api/bulk_action'); const specialNotesRoute = require('./api/special_notes'); -const noteMapRoute = require('./api/note_map.js'); +const noteMapRoute = require('./api/note_map'); const clipperRoute = require('./api/clipper'); const similarNotesRoute = require('./api/similar_notes.js'); const keysRoute = require('./api/keys'); From c63c7d518c09f3ebb5ca23d01b157a6271fffc5c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 21:55:27 +0300 Subject: [PATCH 06/25] server-ts: Convert routes/api/notes --- src/becca/entities/abstract_becca_entity.ts | 4 +- src/routes/api/{notes.js => notes.ts} | 86 ++++++++++++--------- src/routes/routes.js | 2 +- src/services/sql.ts | 4 +- 4 files changed, 55 insertions(+), 41 deletions(-) rename src/routes/api/{notes.js => notes.ts} (71%) diff --git a/src/becca/entities/abstract_becca_entity.ts b/src/becca/entities/abstract_becca_entity.ts index ca7446368..ad4eeae03 100644 --- a/src/becca/entities/abstract_becca_entity.ts +++ b/src/becca/entities/abstract_becca_entity.ts @@ -26,8 +26,8 @@ interface ContentOpts { abstract class AbstractBeccaEntity> { utcDateModified?: string; - protected dateCreated?: string; - protected dateModified?: string; + dateCreated?: string; + dateModified?: string; utcDateCreated!: string; diff --git a/src/routes/api/notes.js b/src/routes/api/notes.ts similarity index 71% rename from src/routes/api/notes.js rename to src/routes/api/notes.ts index 8787acbf8..ec50c848a 100644 --- a/src/routes/api/notes.js +++ b/src/routes/api/notes.ts @@ -1,25 +1,28 @@ "use strict"; -const noteService = require('../../services/notes'); -const eraseService = require('../../services/erase'); -const treeService = require('../../services/tree'); -const sql = require('../../services/sql'); -const utils = require('../../services/utils'); -const log = require('../../services/log'); -const TaskContext = require('../../services/task_context'); -const becca = require('../../becca/becca'); -const ValidationError = require('../../errors/validation_error'); -const blobService = require('../../services/blob'); +import noteService = require('../../services/notes'); +import eraseService = require('../../services/erase'); +import treeService = require('../../services/tree'); +import sql = require('../../services/sql'); +import utils = require('../../services/utils'); +import log = require('../../services/log'); +import TaskContext = require('../../services/task_context'); +import becca = require('../../becca/becca'); +import ValidationError = require('../../errors/validation_error'); +import blobService = require('../../services/blob'); +import { Request } from 'express'; +import BBranch = require('../../becca/entities/bbranch'); +import { AttributeRow } from '../../becca/entities/rows'; -function getNote(req) { +function getNote(req: Request) { return becca.getNoteOrThrow(req.params.noteId); } -function getNoteBlob(req) { +function getNoteBlob(req: Request) { return blobService.getBlobPojo('notes', req.params.noteId); } -function getNoteMetadata(req) { +function getNoteMetadata(req: Request) { const note = becca.getNoteOrThrow(req.params.noteId); return { @@ -30,12 +33,20 @@ function getNoteMetadata(req) { }; } -function createNote(req) { +function createNote(req: Request) { const params = Object.assign({}, req.body); // clone params.parentNoteId = req.params.parentNoteId; const { target, targetBranchId } = req.query; + if (target !== "into" && target !== "after") { + throw new ValidationError("Invalid target type."); + } + + if (typeof targetBranchId !== "string") { + throw new ValidationError("Missing or incorrect type for target branch ID."); + } + const { note, branch } = noteService.createNewNoteWithTarget(target, targetBranchId, params); return { @@ -44,14 +55,14 @@ function createNote(req) { }; } -function updateNoteData(req) { +function updateNoteData(req: Request) { const {content, attachments} = req.body; const {noteId} = req.params; return noteService.updateNoteData(noteId, content, attachments); } -function deleteNote(req) { +function deleteNote(req: Request) { const noteId = req.params.noteId; const taskId = req.query.taskId; const eraseNotes = req.query.eraseNotes === 'true'; @@ -60,8 +71,11 @@ function deleteNote(req) { // note how deleteId is separate from taskId - single taskId produces separate deleteId for each "top level" deleted note const deleteId = utils.randomString(10); - const note = becca.getNote(noteId); + const note = becca.getNoteOrThrow(noteId); + if (typeof taskId !== "string") { + throw new ValidationError("Missing or incorrect type for task ID."); + } const taskContext = TaskContext.getInstance(taskId, 'deleteNotes'); note.deleteNote(deleteId, taskContext); @@ -75,7 +89,7 @@ function deleteNote(req) { } } -function undeleteNote(req) { +function undeleteNote(req: Request) { const taskContext = TaskContext.getInstance(utils.randomString(10), 'undeleteNotes'); noteService.undeleteNote(req.params.noteId, taskContext); @@ -83,7 +97,7 @@ function undeleteNote(req) { taskContext.taskSucceeded(); } -function sortChildNotes(req) { +function sortChildNotes(req: Request) { const noteId = req.params.noteId; const {sortBy, sortDirection, foldersFirst, sortNatural, sortLocale} = req.body; @@ -94,11 +108,11 @@ function sortChildNotes(req) { treeService.sortNotes(noteId, sortBy, reverse, foldersFirst, sortNatural, sortLocale); } -function protectNote(req) { +function protectNote(req: Request) { const noteId = req.params.noteId; const note = becca.notes[noteId]; const protect = !!parseInt(req.params.isProtected); - const includingSubTree = !!parseInt(req.query.subtree); + const includingSubTree = !!parseInt(req.query?.subtree as string); const taskContext = new TaskContext(utils.randomString(10), 'protectNotes', {protect}); @@ -107,18 +121,18 @@ function protectNote(req) { taskContext.taskSucceeded(); } -function setNoteTypeMime(req) { +function setNoteTypeMime(req: Request) { // can't use [] destructuring because req.params is not iterable const {noteId} = req.params; const {type, mime} = req.body; - const note = becca.getNote(noteId); + const note = becca.getNoteOrThrow(noteId); note.type = type; note.mime = mime; note.save(); } -function changeTitle(req) { +function changeTitle(req: Request) { const noteId = req.params.noteId; const title = req.body.title; @@ -145,7 +159,7 @@ function changeTitle(req) { return note; } -function duplicateSubtree(req) { +function duplicateSubtree(req: Request) { const {noteId, parentNoteId} = req.params; return noteService.duplicateSubtree(noteId, parentNoteId); @@ -159,14 +173,14 @@ function eraseUnusedAttachmentsNow() { eraseService.eraseUnusedAttachmentsNow(); } -function getDeleteNotesPreview(req) { +function getDeleteNotesPreview(req: Request) { const {branchIdsToDelete, deleteAllClones} = req.body; - const noteIdsToBeDeleted = new Set(); - const strongBranchCountToDelete = {}; // noteId => count (integer) + const noteIdsToBeDeleted = new Set(); + const strongBranchCountToDelete: Record = {}; // noteId => count - function branchPreviewDeletion(branch) { - if (branch.isWeak) { + function branchPreviewDeletion(branch: BBranch) { + if (branch.isWeak || !branch.branchId) { return; } @@ -196,18 +210,18 @@ function getDeleteNotesPreview(req) { branchPreviewDeletion(branch); } - let brokenRelations = []; + let brokenRelations: AttributeRow[] = []; if (noteIdsToBeDeleted.size > 0) { 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 FROM attributes attr JOIN param_list ON param_list.paramId = attr.value WHERE attr.isDeleted = 0 - AND attr.type = 'relation'`).filter(attr => !noteIdsToBeDeleted.has(attr.noteId)); + AND attr.type = 'relation'`).filter(attr => attr.noteId && !noteIdsToBeDeleted.has(attr.noteId)); } return { @@ -216,7 +230,7 @@ function getDeleteNotesPreview(req) { }; } -function forceSaveRevision(req) { +function forceSaveRevision(req: Request) { const {noteId} = req.params; const note = becca.getNoteOrThrow(noteId); @@ -227,7 +241,7 @@ function forceSaveRevision(req) { note.saveRevision(); } -function convertNoteToAttachment(req) { +function convertNoteToAttachment(req: Request) { const {noteId} = req.params; const note = becca.getNoteOrThrow(noteId); @@ -236,7 +250,7 @@ function convertNoteToAttachment(req) { }; } -module.exports = { +export = { getNote, getNoteBlob, getNoteMetadata, diff --git a/src/routes/routes.js b/src/routes/routes.js index ed48edb50..d028698a9 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -23,7 +23,7 @@ const indexRoute = require('./index.js'); // API routes const treeApiRoute = require('./api/tree.js'); -const notesApiRoute = require('./api/notes.js'); +const notesApiRoute = require('./api/notes'); const branchesApiRoute = require('./api/branches'); const attachmentsApiRoute = require('./api/attachments'); const autocompleteApiRoute = require('./api/autocomplete'); diff --git a/src/services/sql.ts b/src/services/sql.ts index 4aa367fbe..18d72912f 100644 --- a/src/services/sql.ts +++ b/src/services/sql.ts @@ -269,8 +269,8 @@ function transactional(func: (statement: Statement) => T) { } } -function fillParamList(paramIds: string[], truncate = true) { - if (paramIds.length === 0) { +function fillParamList(paramIds: string[] | Set, truncate = true) { + if ("length" in paramIds && paramIds.length === 0) { return; } From 3d75366f02152afc342b033415e1d1301884f24f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 21:57:09 +0300 Subject: [PATCH 07/25] server-ts: Convert routes/api/options --- src/routes/api/{options.js => options.ts} | 21 +++++++++++---------- src/routes/routes.js | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) rename src/routes/api/{options.js => options.ts} (86%) diff --git a/src/routes/api/options.js b/src/routes/api/options.ts similarity index 86% rename from src/routes/api/options.js rename to src/routes/api/options.ts index 88f72ae71..54630e825 100644 --- a/src/routes/api/options.js +++ b/src/routes/api/options.ts @@ -1,9 +1,10 @@ "use strict"; -const optionService = require('../../services/options'); -const log = require('../../services/log'); -const searchService = require('../../services/search/services/search'); -const ValidationError = require('../../errors/validation_error'); +import optionService = require('../../services/options'); +import log = require('../../services/log'); +import searchService = require('../../services/search/services/search'); +import ValidationError = require('../../errors/validation_error'); +import { Request } from 'express'; // options allowed to be updated directly in the Options dialog const ALLOWED_OPTIONS = new Set([ @@ -62,7 +63,7 @@ const ALLOWED_OPTIONS = new Set([ function getOptions() { const optionMap = optionService.getOptionMap(); - const resultMap = {}; + const resultMap: Record = {}; for (const optionName in optionMap) { if (isAllowed(optionName)) { @@ -75,7 +76,7 @@ function getOptions() { return resultMap; } -function updateOption(req) { +function updateOption(req: Request) { const {name, value} = req.params; if (!update(name, value)) { @@ -83,7 +84,7 @@ function updateOption(req) { } } -function updateOptions(req) { +function updateOptions(req: Request) { for (const optionName in req.body) { if (!update(optionName, req.body[optionName])) { // this should be improved @@ -93,7 +94,7 @@ function updateOptions(req) { } } -function update(name, value) { +function update(name: string, value: string) { if (!isAllowed(name)) { return false; } @@ -128,14 +129,14 @@ function getUserThemes() { return ret; } -function isAllowed(name) { +function isAllowed(name: string) { return ALLOWED_OPTIONS.has(name) || name.startsWith("keyboardShortcuts") || name.endsWith("Collapsed") || name.startsWith("hideArchivedNotes"); } -module.exports = { +export = { getOptions, updateOption, updateOptions, diff --git a/src/routes/routes.js b/src/routes/routes.js index d028698a9..b5886e8f3 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -30,7 +30,7 @@ const autocompleteApiRoute = require('./api/autocomplete'); const cloningApiRoute = require('./api/cloning'); const revisionsApiRoute = require('./api/revisions'); const recentChangesApiRoute = require('./api/recent_changes.js'); -const optionsApiRoute = require('./api/options.js'); +const optionsApiRoute = require('./api/options'); const passwordApiRoute = require('./api/password'); const syncApiRoute = require('./api/sync'); const loginApiRoute = require('./api/login'); From eb7a7e49885154a8b897a3fc31db14d9ef570591 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 21:58:32 +0300 Subject: [PATCH 08/25] server-ts: Convert routes/api/other --- src/routes/api/{other.js => other.ts} | 12 +++++++----- src/routes/routes.js | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) rename src/routes/api/{other.js => other.ts} (74%) diff --git a/src/routes/api/other.js b/src/routes/api/other.ts similarity index 74% rename from src/routes/api/other.js rename to src/routes/api/other.ts index 437636e8e..1d157f291 100644 --- a/src/routes/api/other.js +++ b/src/routes/api/other.ts @@ -1,8 +1,10 @@ -const becca = require('../../becca/becca'); -const markdownService = require('../../services/import/markdown'); +import { Request } from "express"; + +import becca = require('../../becca/becca'); +import markdownService = require('../../services/import/markdown'); function getIconUsage() { - const iconClassToCountMap = {}; + const iconClassToCountMap: Record = {}; for (const {value: iconClass, noteId} of becca.findAttributes('label', 'iconClass')) { if (noteId.startsWith("_")) { @@ -25,7 +27,7 @@ function getIconUsage() { return { iconClassToCountMap }; } -function renderMarkdown(req) { +function renderMarkdown(req: Request) { const { markdownContent } = req.body; return { @@ -33,7 +35,7 @@ function renderMarkdown(req) { }; } -module.exports = { +export = { getIconUsage, renderMarkdown }; diff --git a/src/routes/routes.js b/src/routes/routes.js index b5886e8f3..d918c0476 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -58,7 +58,7 @@ const statsRoute = require('./api/stats.js'); const fontsRoute = require('./api/fonts'); const etapiTokensApiRoutes = require('./api/etapi_tokens'); const relationMapApiRoute = require('./api/relation-map'); -const otherRoute = require('./api/other.js'); +const otherRoute = require('./api/other'); const shareRoutes = require('../share/routes.js'); const etapiAuthRoutes = require('../etapi/auth.js'); From 4b1c35119566792c36ec98c13783c422ff2a4582 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 22:00:03 +0300 Subject: [PATCH 09/25] server-ts: Convert routes/api/password --- src/routes/api/{password.js => password.ts} | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) rename src/routes/api/{password.js => password.ts} (68%) diff --git a/src/routes/api/password.js b/src/routes/api/password.ts similarity index 68% rename from src/routes/api/password.js rename to src/routes/api/password.ts index 42bd8b0e0..5fb4c89d6 100644 --- a/src/routes/api/password.js +++ b/src/routes/api/password.ts @@ -1,9 +1,10 @@ "use strict"; -const passwordService = require('../../services/encryption/password'); -const ValidationError = require('../../errors/validation_error'); +import passwordService = require('../../services/encryption/password'); +import ValidationError = require('../../errors/validation_error'); +import { Request } from 'express'; -function changePassword(req) { +function changePassword(req: Request) { if (passwordService.isPasswordSet()) { return passwordService.changePassword(req.body.current_password, req.body.new_password); } @@ -12,7 +13,7 @@ function changePassword(req) { } } -function resetPassword(req) { +function resetPassword(req: Request) { // protection against accidental call (not a security measure) if (req.query.really !== "yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes") { throw new ValidationError("Incorrect password reset confirmation"); @@ -21,7 +22,7 @@ function resetPassword(req) { return passwordService.resetPassword(); } -module.exports = { +export = { changePassword, resetPassword }; From 66d7548046dd17b467a2cf81722b67e555f4a5d8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 22:07:03 +0300 Subject: [PATCH 10/25] server-ts: Convert routes/api/recent_changes --- .../{recent_changes.js => recent_changes.ts} | 34 +++++++++++++------ src/routes/routes.js | 2 +- 2 files changed, 25 insertions(+), 11 deletions(-) rename src/routes/api/{recent_changes.js => recent_changes.ts} (80%) diff --git a/src/routes/api/recent_changes.js b/src/routes/api/recent_changes.ts similarity index 80% rename from src/routes/api/recent_changes.js rename to src/routes/api/recent_changes.ts index 44e964ecf..bf662a784 100644 --- a/src/routes/api/recent_changes.js +++ b/src/routes/api/recent_changes.ts @@ -1,16 +1,30 @@ "use strict"; -const sql = require('../../services/sql'); -const protectedSessionService = require('../../services/protected_session'); -const noteService = require('../../services/notes'); -const becca = require('../../becca/becca'); +import sql = require('../../services/sql'); +import protectedSessionService = require('../../services/protected_session'); +import noteService = require('../../services/notes'); +import becca = require('../../becca/becca'); +import { Request } from 'express'; +import { RevisionRow } from '../../becca/entities/rows'; -function getRecentChanges(req) { +interface RecentChangeRow { + noteId: string; + current_isDeleted: boolean; + current_deleteId: string; + current_title: string; + current_isProtected: boolean, + title: string; + utcDate: string; + date: string; + canBeUndeleted?: boolean; +} + +function getRecentChanges(req: Request) { const {ancestorNoteId} = req.params; let recentChanges = []; - const revisionRows = sql.getRows(` + const revisionRows = sql.getRows(` SELECT notes.noteId, notes.isDeleted AS current_isDeleted, @@ -36,7 +50,7 @@ function getRecentChanges(req) { // now we need to also collect date points not represented in note revisions: // 1. creation for all notes (dateCreated) // 2. deletion for deleted notes (dateModified) - const noteRows = sql.getRows(` + const noteRows = sql.getRows(` SELECT notes.noteId, notes.isDeleted AS current_isDeleted, @@ -76,8 +90,8 @@ function getRecentChanges(req) { for (const change of recentChanges) { if (change.current_isProtected) { if (protectedSessionService.isProtectedSessionAvailable()) { - change.title = protectedSessionService.decryptString(change.title); - change.current_title = protectedSessionService.decryptString(change.current_title); + change.title = protectedSessionService.decryptString(change.title) || "[protected]"; + change.current_title = protectedSessionService.decryptString(change.current_title) || "[protected]"; } else { change.title = change.current_title = "[protected]"; @@ -97,6 +111,6 @@ function getRecentChanges(req) { return recentChanges; } -module.exports = { +export = { getRecentChanges }; diff --git a/src/routes/routes.js b/src/routes/routes.js index d918c0476..d8bda744a 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -29,7 +29,7 @@ const attachmentsApiRoute = require('./api/attachments'); const autocompleteApiRoute = require('./api/autocomplete'); const cloningApiRoute = require('./api/cloning'); const revisionsApiRoute = require('./api/revisions'); -const recentChangesApiRoute = require('./api/recent_changes.js'); +const recentChangesApiRoute = require('./api/recent_changes'); const optionsApiRoute = require('./api/options'); const passwordApiRoute = require('./api/password'); const syncApiRoute = require('./api/sync'); From 96c8c9080d511756e86957d6d9cf46d1a1263e52 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 22:07:58 +0300 Subject: [PATCH 11/25] server-ts: Convert routes/api/recent_notes --- src/routes/api/{recent_notes.js => recent_notes.ts} | 11 ++++++----- src/routes/routes.js | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) rename src/routes/api/{recent_notes.js => recent_notes.ts} (62%) diff --git a/src/routes/api/recent_notes.js b/src/routes/api/recent_notes.ts similarity index 62% rename from src/routes/api/recent_notes.js rename to src/routes/api/recent_notes.ts index 40139477a..627bdd357 100644 --- a/src/routes/api/recent_notes.js +++ b/src/routes/api/recent_notes.ts @@ -1,10 +1,11 @@ "use strict"; -const BRecentNote = require('../../becca/entities/brecent_note'); -const sql = require('../../services/sql'); -const dateUtils = require('../../services/date_utils'); +import BRecentNote = require('../../becca/entities/brecent_note'); +import sql = require('../../services/sql'); +import dateUtils = require('../../services/date_utils'); +import { Request } from 'express'; -function addRecentNote(req) { +function addRecentNote(req: Request) { new BRecentNote({ noteId: req.body.noteId, notePath: req.body.notePath @@ -18,6 +19,6 @@ function addRecentNote(req) { } } -module.exports = { +export = { addRecentNote }; diff --git a/src/routes/routes.js b/src/routes/routes.js index d8bda744a..17d0ab3d5 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -34,7 +34,7 @@ const optionsApiRoute = require('./api/options'); const passwordApiRoute = require('./api/password'); const syncApiRoute = require('./api/sync'); const loginApiRoute = require('./api/login'); -const recentNotesRoute = require('./api/recent_notes.js'); +const recentNotesRoute = require('./api/recent_notes'); const appInfoRoute = require('./api/app_info'); const exportRoute = require('./api/export'); const importRoute = require('./api/import'); From b50ceaf299dbae15928d8b804ffee191d67ed0e8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 22:13:47 +0300 Subject: [PATCH 12/25] server-ts: Convert routes/api/relation-map --- .../api/{relation-map.js => relation-map.ts} | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) rename src/routes/api/{relation-map.js => relation-map.ts} (73%) diff --git a/src/routes/api/relation-map.js b/src/routes/api/relation-map.ts similarity index 73% rename from src/routes/api/relation-map.js rename to src/routes/api/relation-map.ts index 280ed7f67..c81496349 100644 --- a/src/routes/api/relation-map.js +++ b/src/routes/api/relation-map.ts @@ -1,10 +1,22 @@ -const becca = require('../../becca/becca'); -const sql = require('../../services/sql'); +import { Request } from 'express'; +import becca = require('../../becca/becca'); +import sql = require('../../services/sql'); -function getRelationMap(req) { +interface ResponseData { + noteTitles: Record; + relations: { + attributeId: string, + sourceNoteId: string, + targetNoteId: string, + name: string + }[]; + inverseRelations: Record; +} + +function getRelationMap(req: Request) { const {relationMapNoteId, noteIds} = req.body; - const resp = { + const resp: ResponseData = { // noteId => title noteTitles: {}, relations: [], @@ -14,13 +26,13 @@ function getRelationMap(req) { } }; - if (noteIds.length === 0) { + if (!Array.isArray(noteIds) || noteIds.length === 0) { return resp; } const questionMarks = noteIds.map(noteId => '?').join(','); - const relationMapNote = becca.getNote(relationMapNoteId); + const relationMapNote = becca.getNoteOrThrow(relationMapNoteId); const displayRelationsVal = relationMapNote.getLabelValue('displayRelations'); const displayRelations = !displayRelationsVal ? [] : displayRelationsVal @@ -32,7 +44,7 @@ function getRelationMap(req) { .split(",") .map(token => token.trim()); - const foundNoteIds = sql.getColumn(`SELECT noteId FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds); + 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) { @@ -64,6 +76,6 @@ function getRelationMap(req) { return resp; } -module.exports = { +export = { getRelationMap }; From 4ab6f159e5e73cb6c0e43bff986ee8e8ef144267 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 22:17:47 +0300 Subject: [PATCH 13/25] server-ts: Fix "Missing or incorrect type for target branch ID" When attempting to add a new note from the relation map --- src/routes/api/notes.ts | 2 +- src/services/notes.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/api/notes.ts b/src/routes/api/notes.ts index ec50c848a..c5e3e1c37 100644 --- a/src/routes/api/notes.ts +++ b/src/routes/api/notes.ts @@ -43,7 +43,7 @@ function createNote(req: Request) { throw new ValidationError("Invalid target type."); } - if (typeof targetBranchId !== "string") { + if (targetBranchId && typeof targetBranchId !== "string") { throw new ValidationError("Missing or incorrect type for target branch ID."); } diff --git a/src/services/notes.ts b/src/services/notes.ts index 6dd68dc73..a60799e80 100644 --- a/src/services/notes.ts +++ b/src/services/notes.ts @@ -251,7 +251,7 @@ function createNewNote(params: NoteParams): { }); } -function createNewNoteWithTarget(target: ("into" | "after"), targetBranchId: string, params: NoteParams) { +function createNewNoteWithTarget(target: ("into" | "after"), targetBranchId: string | undefined, params: NoteParams) { if (!params.type) { const parentNote = becca.notes[params.parentNoteId]; @@ -263,7 +263,7 @@ function createNewNoteWithTarget(target: ("into" | "after"), targetBranchId: str if (target === 'into') { return createNewNote(params); } - else if (target === 'after') { + else if (target === 'after' && targetBranchId) { const afterBranch = becca.branches[targetBranchId]; // not updating utcDateModified to avoid having to sync whole rows From 6265aa99d37d1af1ed7e1839557fca77c7a0f5d7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 22:32:03 +0300 Subject: [PATCH 14/25] server-ts: Convert routes/api/revisions --- src/becca/becca-interface.ts | 24 +++++- src/becca/entities/bnote.ts | 3 +- src/becca/entities/brevision.ts | 9 +-- src/routes/api/{revisions.js => revisions.ts} | 78 +++++++++++-------- 4 files changed, 75 insertions(+), 39 deletions(-) rename src/routes/api/{revisions.js => revisions.ts} (68%) diff --git a/src/becca/becca-interface.ts b/src/becca/becca-interface.ts index 7fa3ca60f..e7298fff5 100644 --- a/src/becca/becca-interface.ts +++ b/src/becca/becca-interface.ts @@ -164,6 +164,14 @@ export default class Becca { return row ? new BRevision(row) : null; } + getRevisionOrThrow(revisionId: string): BRevision { + const revision = this.getRevision(revisionId); + if (!revision) { + throw new NotFoundError(`Revision '${revisionId}' has not been found.`); + } + return revision; + } + getAttachment(attachmentId: string, opts: AttachmentOpts = {}): BAttachment | null { opts.includeContentLength = !!opts.includeContentLength; @@ -249,7 +257,7 @@ export default class Becca { return rows.map(row => new BRecentNote(row)); } - getRevisionsFromQuery(query: string, params = []): BRevision[] { + getRevisionsFromQuery(query: string, params: string[] = []): BRevision[] { const rows = sql.getRows(query, params); const BRevision = require('./entities/brevision'); // avoiding circular dependency problems @@ -291,4 +299,18 @@ export interface ConstructorData> { primaryKeyName: string; entityName: string; hashedProperties: (keyof T)[]; +} + +export interface NotePojo { + noteId: string; + title?: string; + isProtected?: boolean; + type: string; + mime: string; + blobId?: string; + isDeleted: boolean; + dateCreated?: string; + dateModified?: string; + utcDateCreated: string; + utcDateModified?: string; } \ No newline at end of file diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index 6b9118a67..c29945a93 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -15,6 +15,7 @@ import eventService = require('../../services/events'); import { AttachmentRow, NoteRow, NoteType, RevisionRow } from './rows'; import BBranch = require('./bbranch'); import BAttribute = require('./battribute'); +import { NotePojo } from '../becca-interface'; dayjs.extend(utc); const LABEL = 'label'; @@ -1679,7 +1680,7 @@ class BNote extends AbstractBeccaEntity { this.utcDateModified = dateUtils.utcNowDateTime(); } - getPojo() { + getPojo(): NotePojo { return { noteId: this.noteId, title: this.title || undefined, diff --git a/src/becca/entities/brevision.ts b/src/becca/entities/brevision.ts index 101506858..18a7a8df5 100644 --- a/src/becca/entities/brevision.ts +++ b/src/becca/entities/brevision.ts @@ -40,7 +40,7 @@ class BRevision extends AbstractBeccaEntity { utcDateLastEdited?: string; utcDateCreated!: string; contentLength?: number; - content?: string; + content?: string | Buffer; constructor(row: RevisionRow, titleDecrypted = false) { super(); @@ -91,9 +91,8 @@ class BRevision extends AbstractBeccaEntity { * * This is the same approach as is used for Note's content. */ - // TODO: initial declaration included Buffer, but everywhere it's treated as a string. - getContent(): string { - return this._getContent() as string; + getContent(): string | Buffer { + return this._getContent(); } /** @@ -101,7 +100,7 @@ class BRevision extends AbstractBeccaEntity { getJsonContent(): {} | null { const content = this.getContent(); - if (!content || !content.trim()) { + if (!content || typeof content !== "string" || !content.trim()) { return null; } diff --git a/src/routes/api/revisions.js b/src/routes/api/revisions.ts similarity index 68% rename from src/routes/api/revisions.js rename to src/routes/api/revisions.ts index e317fec95..475b5b9b1 100644 --- a/src/routes/api/revisions.js +++ b/src/routes/api/revisions.ts @@ -1,22 +1,38 @@ "use strict"; -const beccaService = require('../../becca/becca_service'); -const revisionService = require('../../services/revisions'); -const utils = require('../../services/utils'); -const sql = require('../../services/sql'); -const cls = require('../../services/cls'); -const path = require('path'); -const becca = require('../../becca/becca'); -const blobService = require('../../services/blob'); -const eraseService = require("../../services/erase"); +import beccaService = require('../../becca/becca_service'); +import revisionService = require('../../services/revisions'); +import utils = require('../../services/utils'); +import sql = require('../../services/sql'); +import cls = require('../../services/cls'); +import path = require('path'); +import becca = require('../../becca/becca'); +import blobService = require('../../services/blob'); +import eraseService = require("../../services/erase"); +import { Request, Response } from 'express'; +import BRevision = require('../../becca/entities/brevision'); +import BNote = require('../../becca/entities/bnote'); +import { NotePojo } from '../../becca/becca-interface'; -function getRevisionBlob(req) { +interface NotePath { + noteId: string; + branchId?: string; + title: string; + notePath: string[]; + path: string; +} + +interface NotePojoWithNotePath extends NotePojo { + notePath?: string[] | null; +} + +function getRevisionBlob(req: Request) { const preview = req.query.preview === 'true'; return blobService.getBlobPojo('revisions', req.params.revisionId, { preview }); } -function getRevisions(req) { +function getRevisions(req: Request) { return becca.getRevisionsFromQuery(` SELECT revisions.*, LENGTH(blobs.content) AS contentLength @@ -26,12 +42,12 @@ function getRevisions(req) { ORDER BY revisions.utcDateCreated DESC`, [req.params.noteId]); } -function getRevision(req) { - const revision = becca.getRevision(req.params.revisionId); +function getRevision(req: Request) { + const revision = becca.getRevisionOrThrow(req.params.revisionId); if (revision.type === 'file') { if (revision.hasStringContent()) { - revision.content = revision.getContent().substr(0, 10000); + revision.content = (revision.getContent() as string).substr(0, 10000); } } else { @@ -45,11 +61,7 @@ function getRevision(req) { return revision; } -/** - * @param {BRevision} revision - * @returns {string} - */ -function getRevisionFilename(revision) { +function getRevisionFilename(revision: BRevision) { let filename = utils.formatDownloadTitle(revision.title, revision.type, revision.mime); const extension = path.extname(filename); @@ -68,8 +80,8 @@ function getRevisionFilename(revision) { return filename; } -function downloadRevision(req, res) { - const revision = becca.getRevision(req.params.revisionId); +function downloadRevision(req: Request, res: Response) { + const revision = becca.getRevisionOrThrow(req.params.revisionId); if (!revision.isContentAvailable()) { return res.setHeader("Content-Type", "text/plain") @@ -85,18 +97,18 @@ function downloadRevision(req, res) { res.send(revision.getContent()); } -function eraseAllRevisions(req) { - const revisionIdsToErase = sql.getColumn('SELECT revisionId FROM revisions WHERE noteId = ?', +function eraseAllRevisions(req: Request) { + const revisionIdsToErase = sql.getColumn('SELECT revisionId FROM revisions WHERE noteId = ?', [req.params.noteId]); eraseService.eraseRevisions(revisionIdsToErase); } -function eraseRevision(req) { +function eraseRevision(req: Request) { eraseService.eraseRevisions([req.params.revisionId]); } -function restoreRevision(req) { +function restoreRevision(req: Request) { const revision = becca.getRevision(req.params.revisionId); if (revision) { @@ -117,7 +129,9 @@ function restoreRevision(req) { noteAttachment.setContent(revisionAttachment.getContent(), { forceSave: true }); // content is rewritten to point to the restored revision attachments - revisionContent = revisionContent.replaceAll(`attachments/${revisionAttachment.attachmentId}`, `attachments/${noteAttachment.attachmentId}`); + if (typeof revisionContent === "string") { + revisionContent = revisionContent.replaceAll(`attachments/${revisionAttachment.attachmentId}`, `attachments/${noteAttachment.attachmentId}`); + } } note.title = revision.title; @@ -126,8 +140,8 @@ function restoreRevision(req) { } } -function getEditedNotesOnDate(req) { - const noteIds = sql.getColumn(` +function getEditedNotesOnDate(req: Request) { + const noteIds = sql.getColumn(` SELECT notes.* FROM notes WHERE noteId IN ( @@ -152,7 +166,7 @@ function getEditedNotesOnDate(req) { return notes.map(note => { const notePath = getNotePathData(note); - const notePojo = note.getPojo(); + const notePojo: NotePojoWithNotePath = note.getPojo(); notePojo.notePath = notePath ? notePath.notePath : null; return notePojo; @@ -160,7 +174,7 @@ function getEditedNotesOnDate(req) { } -function getNotePathData(note) { +function getNotePathData(note: BNote): NotePath | undefined { const retPath = note.getBestNotePath(); if (retPath) { @@ -173,7 +187,7 @@ function getNotePathData(note) { } else { const parentNote = note.parents[0]; - branchId = becca.getBranchFromChildAndParent(note.noteId, parentNote.noteId).branchId; + branchId = becca.getBranchFromChildAndParent(note.noteId, parentNote.noteId)?.branchId; } return { @@ -186,7 +200,7 @@ function getNotePathData(note) { } } -module.exports = { +export = { getRevisionBlob, getRevisions, getRevision, From de42df40bb825ce22bfda08b1b4057a4f446ff90 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 22:38:17 +0300 Subject: [PATCH 15/25] server-ts: Convert routes/api/script --- src/routes/api/{script.js => script.ts} | 49 +++++++++++++++---------- src/routes/routes.js | 2 +- src/services/script.ts | 4 +- 3 files changed, 33 insertions(+), 22 deletions(-) rename src/routes/api/{script.js => script.ts} (70%) diff --git a/src/routes/api/script.js b/src/routes/api/script.ts similarity index 70% rename from src/routes/api/script.js rename to src/routes/api/script.ts index 8bd7ba712..8a1b3d072 100644 --- a/src/routes/api/script.js +++ b/src/routes/api/script.ts @@ -1,19 +1,30 @@ "use strict"; -const scriptService = require('../../services/script'); -const attributeService = require('../../services/attributes'); -const becca = require('../../becca/becca'); -const syncService = require('../../services/sync'); -const sql = require('../../services/sql'); +import scriptService = require('../../services/script'); +import attributeService = require('../../services/attributes'); +import becca = require('../../becca/becca'); +import syncService = require('../../services/sync'); +import sql = require('../../services/sql'); +import { Request } from 'express'; + +interface ScriptBody { + script: string; + params: any[]; + startNoteId: string; + currentNoteId: string; + originEntityName: string; + originEntityId: string; + transactional: boolean; +} // The async/await here is very confusing, because the body.script may, but may not be async. If it is async, then we // need to await it and make the complete response including metadata available in a Promise, so that the route detects // this and does result.then(). -async function exec(req) { +async function exec(req: Request) { try { - const { body } = req; + const body = (req.body as ScriptBody); - const execute = body => scriptService.executeScript( + const execute = (body: ScriptBody) => scriptService.executeScript( body.script, body.params, body.startNoteId, @@ -32,20 +43,20 @@ async function exec(req) { maxEntityChangeId: syncService.getMaxEntityChangeId() }; } - catch (e) { + catch (e: any) { return { success: false, error: e.message }; } } -function run(req) { - const note = becca.getNote(req.params.noteId); +function run(req: Request) { + const note = becca.getNoteOrThrow(req.params.noteId); const result = scriptService.executeNote(note, { originEntity: note }); return { executionResult: result }; } -function getBundlesWithLabel(label, value) { +function getBundlesWithLabel(label: string, value?: string) { const notes = attributeService.getNotesWithLabel(label, value); const bundles = []; @@ -61,7 +72,7 @@ function getBundlesWithLabel(label, value) { return bundles; } -function getStartupBundles(req) { +function getStartupBundles(req: Request) { if (!process.env.TRILIUM_SAFE_MODE) { if (req.query.mobile === "true") { return getBundlesWithLabel("run", "mobileStartup"); @@ -84,9 +95,9 @@ function getWidgetBundles() { } } -function getRelationBundles(req) { +function getRelationBundles(req: Request) { const noteId = req.params.noteId; - const note = becca.getNote(noteId); + const note = becca.getNoteOrThrow(noteId); const relationName = req.params.relationName; const attributes = note.getAttributes(); @@ -97,7 +108,7 @@ function getRelationBundles(req) { const bundles = []; for (const noteId of uniqueNoteIds) { - const note = becca.getNote(noteId); + const note = becca.getNoteOrThrow(noteId); if (!note.isJavaScript() || note.getScriptEnv() !== 'frontend') { continue; @@ -113,14 +124,14 @@ function getRelationBundles(req) { return bundles; } -function getBundle(req) { - const note = becca.getNote(req.params.noteId); +function getBundle(req: Request) { + const note = becca.getNoteOrThrow(req.params.noteId); const { script, params } = req.body; return scriptService.getScriptBundleForFrontend(note, script, params); } -module.exports = { +export = { exec, run, getStartupBundles, diff --git a/src/routes/routes.js b/src/routes/routes.js index 17d0ab3d5..3285b5b97 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -43,7 +43,7 @@ const sqlRoute = require('./api/sql'); const databaseRoute = require('./api/database'); const imageRoute = require('./api/image'); const attributesRoute = require('./api/attributes'); -const scriptRoute = require('./api/script.js'); +const scriptRoute = require('./api/script'); const senderRoute = require('./api/sender.js'); const filesRoute = require('./api/files'); const searchRoute = require('./api/search'); diff --git a/src/services/script.ts b/src/services/script.ts index 1df79290e..e798584d9 100644 --- a/src/services/script.ts +++ b/src/services/script.ts @@ -106,7 +106,7 @@ function execute(ctx: any, script: string) { return function () { return eval(`const apiContext = this;\r\n(${script}\r\n)()`); }.call(ctx); } -function getParams(params: ScriptParams) { +function getParams(params?: ScriptParams) { if (!params) { return params; } @@ -121,7 +121,7 @@ function getParams(params: ScriptParams) { }).join(","); } -function getScriptBundleForFrontend(note: BNote, script: string, params: ScriptParams) { +function getScriptBundleForFrontend(note: BNote, script?: string, params?: ScriptParams) { let overrideContent = null; if (script) { From fa82158e30c62507f9a68f1cea7b39eede98462d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 22:48:20 +0300 Subject: [PATCH 16/25] server-ts: Convert routes/api/search --- src/routes/api/{search.js => search.ts} | 35 ++++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) rename src/routes/api/{search.js => search.ts} (73%) diff --git a/src/routes/api/search.js b/src/routes/api/search.ts similarity index 73% rename from src/routes/api/search.js rename to src/routes/api/search.ts index 96428c6af..358c0d1d2 100644 --- a/src/routes/api/search.js +++ b/src/routes/api/search.ts @@ -1,14 +1,17 @@ "use strict"; -const becca = require('../../becca/becca'); -const SearchContext = require('../../services/search/search_context'); -const searchService = require('../../services/search/services/search'); -const bulkActionService = require('../../services/bulk_actions'); -const cls = require('../../services/cls'); -const {formatAttrForSearch} = require('../../services/attribute_formatter'); -const ValidationError = require('../../errors/validation_error'); +import { Request } from "express"; -function searchFromNote(req) { +import becca = require('../../becca/becca'); +import SearchContext = require('../../services/search/search_context'); +import searchService = require('../../services/search/services/search'); +import bulkActionService = require('../../services/bulk_actions'); +import cls = require('../../services/cls'); +import attributeFormatter = require('../../services/attribute_formatter'); +import ValidationError = require('../../errors/validation_error'); +import SearchResult = require("../../services/search/search_result"); + +function searchFromNote(req: Request) { const note = becca.getNoteOrThrow(req.params.noteId); if (!note) { @@ -23,7 +26,7 @@ function searchFromNote(req) { return searchService.searchFromNote(note); } -function searchAndExecute(req) { +function searchAndExecute(req: Request) { const note = becca.getNoteOrThrow(req.params.noteId); if (!note) { @@ -40,7 +43,7 @@ function searchAndExecute(req) { bulkActionService.executeActions(note, searchResultNoteIds); } -function quickSearch(req) { +function quickSearch(req: Request) { const {searchString} = req.params; const searchContext = new SearchContext({ @@ -58,7 +61,7 @@ function quickSearch(req) { }; } -function search(req) { +function search(req: Request) { const {searchString} = req.params; const searchContext = new SearchContext({ @@ -72,7 +75,7 @@ function search(req) { .map(sr => sr.noteId); } -function getRelatedNotes(req) { +function getRelatedNotes(req: Request) { const attr = req.body; const searchSettings = { @@ -81,10 +84,10 @@ function getRelatedNotes(req) { fuzzyAttributeSearch: false }; - const matchingNameAndValue = searchService.findResultsWithQuery(formatAttrForSearch(attr, true), new SearchContext(searchSettings)); - const matchingName = searchService.findResultsWithQuery(formatAttrForSearch(attr, false), new SearchContext(searchSettings)); + const matchingNameAndValue = searchService.findResultsWithQuery(attributeFormatter.formatAttrForSearch(attr, true), new SearchContext(searchSettings)); + const matchingName = searchService.findResultsWithQuery(attributeFormatter.formatAttrForSearch(attr, false), new SearchContext(searchSettings)); - const results = []; + const results: SearchResult[] = []; const allResults = matchingNameAndValue.concat(matchingName); @@ -123,7 +126,7 @@ function searchTemplates() { }).map(note => note.noteId); } -module.exports = { +export = { searchFromNote, searchAndExecute, getRelatedNotes, From 90cf9130832845bcd5dcfb818f0b0fac9c374606 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 23:01:08 +0300 Subject: [PATCH 17/25] server-ts: Convert routes/api/sender --- src/routes/api/sender.js | 66 -------------------------------- src/routes/api/sender.ts | 83 ++++++++++++++++++++++++++++++++++++++++ src/routes/routes.js | 2 +- 3 files changed, 84 insertions(+), 67 deletions(-) delete mode 100644 src/routes/api/sender.js create mode 100644 src/routes/api/sender.ts diff --git a/src/routes/api/sender.js b/src/routes/api/sender.js deleted file mode 100644 index b2309d3c7..000000000 --- a/src/routes/api/sender.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; - -const imageType = require('image-type'); -const imageService = require('../../services/image'); -const noteService = require('../../services/notes'); -const { sanitizeAttributeName } = require('../../services/sanitize_attribute_name'); -const specialNotesService = require('../../services/special_notes'); - -function uploadImage(req) { - const file = req.file; - - if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) { - return [400, `Unknown image type: ${file.mimetype}`]; - } - - const originalName = `Sender image.${imageType(file.buffer).ext}`; - - const parentNote = specialNotesService.getInboxNote(req.headers['x-local-date']); - - const { note, noteId } = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true); - - const labelsStr = req.headers['x-labels']; - - if (labelsStr?.trim()) { - const labels = JSON.parse(labelsStr); - - for (const { name, value } of labels) { - note.setLabel(sanitizeAttributeName(name), value); - } - } - - note.setLabel("sentFromSender"); - - return { - noteId: noteId - }; -} - -function saveNote(req) { - const parentNote = specialNotesService.getInboxNote(req.headers['x-local-date']); - - const { note, branch } = noteService.createNewNote({ - parentNoteId: parentNote.noteId, - title: req.body.title, - content: req.body.content, - isProtected: false, - type: 'text', - mime: 'text/html' - }); - - if (req.body.labels) { - for (const { name, value } of req.body.labels) { - note.setLabel(sanitizeAttributeName(name), value); - } - } - - return { - noteId: note.noteId, - branchId: branch.branchId - }; -} - -module.exports = { - uploadImage, - saveNote -}; diff --git a/src/routes/api/sender.ts b/src/routes/api/sender.ts new file mode 100644 index 000000000..dc31c42f6 --- /dev/null +++ b/src/routes/api/sender.ts @@ -0,0 +1,83 @@ +"use strict"; + +import imageType = require('image-type'); +import imageService = require('../../services/image'); +import noteService = require('../../services/notes'); +import sanitize_attribute_name = require('../../services/sanitize_attribute_name'); +import specialNotesService = require('../../services/special_notes'); +import { Request } from 'express'; + +function uploadImage(req: Request) { + const file = (req as any).file; + + if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) { + return [400, `Unknown image type: ${file.mimetype}`]; + } + + const uploadedImageType = imageType(file.buffer); + if (!uploadedImageType) { + return [400, "Unable to determine image type."]; + } + const originalName = `Sender image.${uploadedImageType.ext}`; + + if (!req.headers["x-local-date"] || Array.isArray(req.headers["x-local-date"])) { + return [400, "Invalid local date"]; + } + + if (Array.isArray(req.headers["x-labels"])) { + return [400, "Invalid value type."]; + } + + const parentNote = specialNotesService.getInboxNote(req.headers['x-local-date']); + + const { note, noteId } = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true); + + const labelsStr = req.headers['x-labels']; + + if (labelsStr?.trim()) { + const labels = JSON.parse(labelsStr); + + for (const { name, value } of labels) { + note.setLabel(sanitize_attribute_name.sanitizeAttributeName(name), value); + } + } + + note.setLabel("sentFromSender"); + + return { + noteId: noteId + }; +} + +function saveNote(req: Request) { + if (!req.headers["x-local-date"] || Array.isArray(req.headers["x-local-date"])) { + return [400, "Invalid local date"]; + } + + const parentNote = specialNotesService.getInboxNote(req.headers['x-local-date']); + + const { note, branch } = noteService.createNewNote({ + parentNoteId: parentNote.noteId, + title: req.body.title, + content: req.body.content, + isProtected: false, + type: 'text', + mime: 'text/html' + }); + + if (req.body.labels) { + for (const { name, value } of req.body.labels) { + note.setLabel(sanitize_attribute_name.sanitizeAttributeName(name), value); + } + } + + return { + noteId: note.noteId, + branchId: branch.branchId + }; +} + +export = { + uploadImage, + saveNote +}; diff --git a/src/routes/routes.js b/src/routes/routes.js index 3285b5b97..a88dccd80 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -44,7 +44,7 @@ const databaseRoute = require('./api/database'); const imageRoute = require('./api/image'); const attributesRoute = require('./api/attributes'); const scriptRoute = require('./api/script'); -const senderRoute = require('./api/sender.js'); +const senderRoute = require('./api/sender'); const filesRoute = require('./api/files'); const searchRoute = require('./api/search'); const bulkActionRoute = require('./api/bulk_action'); From 5804dc52bc5ea5da6912276aa8cdf7f36eaae98e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 23:08:41 +0300 Subject: [PATCH 18/25] server-ts: Convert routes/api/setup --- src/routes/api/{setup.js => setup.ts} | 15 ++++++++------- src/routes/routes.js | 4 ++-- src/services/setup.ts | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) rename src/routes/api/{setup.js => setup.ts} (78%) diff --git a/src/routes/api/setup.js b/src/routes/api/setup.ts similarity index 78% rename from src/routes/api/setup.js rename to src/routes/api/setup.ts index 84f310725..9e63a2331 100644 --- a/src/routes/api/setup.js +++ b/src/routes/api/setup.ts @@ -1,9 +1,10 @@ "use strict"; -const sqlInit = require('../../services/sql_init'); -const setupService = require('../../services/setup'); -const log = require('../../services/log'); -const appInfo = require('../../services/app_info'); +import sqlInit = require('../../services/sql_init'); +import setupService = require('../../services/setup'); +import log = require('../../services/log'); +import appInfo = require('../../services/app_info'); +import { Request } from 'express'; function getStatus() { return { @@ -17,13 +18,13 @@ async function setupNewDocument() { await sqlInit.createInitialDatabase(); } -function setupSyncFromServer(req) { +function setupSyncFromServer(req: Request) { const { syncServerHost, syncProxy, password } = req.body; return setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, password); } -function saveSyncSeed(req) { +function saveSyncSeed(req: Request) { const { options, syncVersion } = req.body; if (appInfo.syncVersion !== syncVersion) { @@ -50,7 +51,7 @@ function getSyncSeed() { }; } -module.exports = { +export = { getStatus, setupNewDocument, setupSyncFromServer, diff --git a/src/routes/routes.js b/src/routes/routes.js index a88dccd80..9e4f0c152 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -17,7 +17,7 @@ const NotFoundError = require('../errors/not_found_error'); const ValidationError = require('../errors/validation_error'); // page routes -const setupRoute = require('./setup.js'); +const setupRoute = require('./setup'); const loginRoute = require('./login.js'); const indexRoute = require('./index.js'); @@ -38,7 +38,7 @@ const recentNotesRoute = require('./api/recent_notes'); const appInfoRoute = require('./api/app_info'); const exportRoute = require('./api/export'); const importRoute = require('./api/import'); -const setupApiRoute = require('./api/setup.js'); +const setupApiRoute = require('./api/setup'); const sqlRoute = require('./api/sql'); const databaseRoute = require('./api/database'); const imageRoute = require('./api/image'); diff --git a/src/services/setup.ts b/src/services/setup.ts index 3d7706dba..08b1ee806 100644 --- a/src/services/setup.ts +++ b/src/services/setup.ts @@ -110,7 +110,7 @@ function getSyncSeedOptions() { ]; } -module.exports = { +export = { hasSyncServerSchemaAndSeed, triggerSync, sendSeedToSyncServer, From 249e81c9eb61157872563e734be87d86d466611b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 23:09:32 +0300 Subject: [PATCH 19/25] server-ts: Convert routes/api/similar_notes --- src/routes/api/similar_notes.js | 16 ---------------- src/routes/api/similar_notes.ts | 18 ++++++++++++++++++ src/routes/routes.js | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) delete mode 100644 src/routes/api/similar_notes.js create mode 100644 src/routes/api/similar_notes.ts diff --git a/src/routes/api/similar_notes.js b/src/routes/api/similar_notes.js deleted file mode 100644 index 555efd1b5..000000000 --- a/src/routes/api/similar_notes.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; - -const similarityService = require('../../becca/similarity'); -const becca = require('../../becca/becca'); - -async function getSimilarNotes(req) { - const noteId = req.params.noteId; - - const note = becca.getNoteOrThrow(noteId); - - return await similarityService.findSimilarNotes(noteId); -} - -module.exports = { - getSimilarNotes -}; diff --git a/src/routes/api/similar_notes.ts b/src/routes/api/similar_notes.ts new file mode 100644 index 000000000..f60d7f7c6 --- /dev/null +++ b/src/routes/api/similar_notes.ts @@ -0,0 +1,18 @@ +"use strict"; + +import { Request } from "express"; + +import similarityService = require('../../becca/similarity'); +import becca = require('../../becca/becca'); + +async function getSimilarNotes(req: Request) { + const noteId = req.params.noteId; + + const note = becca.getNoteOrThrow(noteId); + + return await similarityService.findSimilarNotes(noteId); +} + +export = { + getSimilarNotes +}; diff --git a/src/routes/routes.js b/src/routes/routes.js index 9e4f0c152..98120062e 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -51,7 +51,7 @@ const bulkActionRoute = require('./api/bulk_action'); const specialNotesRoute = require('./api/special_notes'); const noteMapRoute = require('./api/note_map'); const clipperRoute = require('./api/clipper'); -const similarNotesRoute = require('./api/similar_notes.js'); +const similarNotesRoute = require('./api/similar_notes'); const keysRoute = require('./api/keys'); const backendLogRoute = require('./api/backend_log'); const statsRoute = require('./api/stats.js'); From c1875a8c8f7ec0aa5affb69e7c88d5f5ef4c85e0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 23:11:02 +0300 Subject: [PATCH 20/25] server-ts: Convert routes/api/special_notes --- .../{special_notes.js => special_notes.ts} | 39 ++++++++++--------- src/services/special_notes.ts | 2 +- 2 files changed, 21 insertions(+), 20 deletions(-) rename src/routes/api/{special_notes.js => special_notes.ts} (69%) diff --git a/src/routes/api/special_notes.js b/src/routes/api/special_notes.ts similarity index 69% rename from src/routes/api/special_notes.js rename to src/routes/api/special_notes.ts index 5ed6e0656..445cd7315 100644 --- a/src/routes/api/special_notes.js +++ b/src/routes/api/special_notes.ts @@ -1,32 +1,33 @@ "use strict"; -const dateNoteService = require('../../services/date_notes'); -const sql = require('../../services/sql'); -const cls = require('../../services/cls'); -const specialNotesService = require('../../services/special_notes'); -const becca = require('../../becca/becca'); +import dateNoteService = require('../../services/date_notes'); +import sql = require('../../services/sql'); +import cls = require('../../services/cls'); +import specialNotesService = require('../../services/special_notes'); +import becca = require('../../becca/becca'); +import { Request } from 'express'; -function getInboxNote(req) { +function getInboxNote(req: Request) { return specialNotesService.getInboxNote(req.params.date); } -function getDayNote(req) { +function getDayNote(req: Request) { return dateNoteService.getDayNote(req.params.date); } -function getWeekNote(req) { +function getWeekNote(req: Request) { return dateNoteService.getWeekNote(req.params.date); } -function getMonthNote(req) { +function getMonthNote(req: Request) { return dateNoteService.getMonthNote(req.params.month); } -function getYearNote(req) { +function getYearNote(req: Request) { return dateNoteService.getYearNote(req.params.year); } -function getDayNotesForMonth(req) { +function getDayNotesForMonth(req: Request) { const month = req.params.month; return sql.getMap(` @@ -42,7 +43,7 @@ function getDayNotesForMonth(req) { AND attr.value LIKE '${month}%'`); } -function saveSqlConsole(req) { +function saveSqlConsole(req: Request) { return specialNotesService.saveSqlConsole(req.body.sqlConsoleNoteId); } @@ -50,14 +51,14 @@ function createSqlConsole() { return specialNotesService.createSqlConsole(); } -function saveSearchNote(req) { +function saveSearchNote(req: Request) { return specialNotesService.saveSearchNote(req.body.searchNoteId); } -function createSearchNote(req) { +function createSearchNote(req: Request) { const hoistedNote = getHoistedNote(); const searchString = req.body.searchString || ""; - const ancestorNoteId = req.body.ancestorNoteId || hoistedNote.noteId; + const ancestorNoteId = req.body.ancestorNoteId || hoistedNote?.noteId; return specialNotesService.createSearchNote(searchString, ancestorNoteId); } @@ -66,22 +67,22 @@ function getHoistedNote() { return becca.getNote(cls.getHoistedNoteId()); } -function createLauncher(req) { +function createLauncher(req: Request) { return specialNotesService.createLauncher({ parentNoteId: req.params.parentNoteId, launcherType: req.params.launcherType }); } -function resetLauncher(req) { +function resetLauncher(req: Request) { return specialNotesService.resetLauncher(req.params.noteId); } -function createOrUpdateScriptLauncherFromApi(req) { +function createOrUpdateScriptLauncherFromApi(req: Request) { return specialNotesService.createOrUpdateScriptLauncherFromApi(req.body); } -module.exports = { +export = { getInboxNote, getDayNote, getWeekNote, diff --git a/src/services/special_notes.ts b/src/services/special_notes.ts index 8a75873ec..65cc14c31 100644 --- a/src/services/special_notes.ts +++ b/src/services/special_notes.ts @@ -166,7 +166,7 @@ function createScriptLauncher(parentNoteId: string, forceNoteId?: string) { interface LauncherConfig { parentNoteId: string; launcherType: string; - noteId: string; + noteId?: string; } function createLauncher({ parentNoteId, launcherType, noteId }: LauncherConfig) { From cecfc4cd34d5ed92866ca8b04db9f66f2316e1ba Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 23:12:22 +0300 Subject: [PATCH 21/25] server-ts: Convert routes/api/sql --- src/routes/api/{sql.js => sql.ts} | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) rename src/routes/api/{sql.js => sql.ts} (75%) diff --git a/src/routes/api/sql.js b/src/routes/api/sql.ts similarity index 75% rename from src/routes/api/sql.js rename to src/routes/api/sql.ts index 4e06ed78e..3bd1d3eca 100644 --- a/src/routes/api/sql.js +++ b/src/routes/api/sql.ts @@ -1,7 +1,9 @@ "use strict"; -const sql = require('../../services/sql'); -const becca = require('../../becca/becca'); +import sql = require('../../services/sql'); +import becca = require('../../becca/becca'); +import { Request } from 'express'; +import ValidationError = require('../../errors/validation_error'); function getSchema() { const tableNames = sql.getColumn(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`); @@ -17,10 +19,15 @@ function getSchema() { return tables; } -function execute(req) { +function execute(req: Request) { const note = becca.getNoteOrThrow(req.params.noteId); - const queries = note.getContent().split("\n---"); + const content = note.getContent(); + if (typeof content !== "string") { + throw new ValidationError("Invalid note type."); + } + + const queries = content.split("\n---"); try { const results = []; @@ -51,7 +58,7 @@ function execute(req) { results }; } - catch (e) { + catch (e: any) { return { success: false, error: e.message @@ -59,7 +66,7 @@ function execute(req) { } } -module.exports = { +export = { getSchema, execute }; From 6bbb1f8404f2156e340736a035db6516292c1d73 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 23:15:00 +0300 Subject: [PATCH 22/25] server-ts: Convert routes/api/stats --- src/routes/api/{stats.js => stats.ts} | 15 ++++++++------- src/routes/routes.js | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) rename src/routes/api/{stats.js => stats.ts} (83%) diff --git a/src/routes/api/stats.js b/src/routes/api/stats.ts similarity index 83% rename from src/routes/api/stats.js rename to src/routes/api/stats.ts index 05d05d25c..053647daa 100644 --- a/src/routes/api/stats.js +++ b/src/routes/api/stats.ts @@ -1,10 +1,11 @@ -const sql = require('../../services/sql'); -const becca = require('../../becca/becca'); +import sql = require('../../services/sql'); +import becca = require('../../becca/becca'); +import { Request } from 'express'; -function getNoteSize(req) { +function getNoteSize(req: Request) { const {noteId} = req.params; - const blobSizes = sql.getMap(` + const blobSizes = sql.getMap(` SELECT blobs.blobId, LENGTH(content) FROM blobs LEFT JOIN notes ON notes.blobId = blobs.blobId AND notes.noteId = ? AND notes.isDeleted = 0 @@ -21,14 +22,14 @@ function getNoteSize(req) { }; } -function getSubtreeSize(req) { +function getSubtreeSize(req: Request) { const note = becca.getNoteOrThrow(req.params.noteId); const subTreeNoteIds = note.getSubtreeNoteIds(); sql.fillParamList(subTreeNoteIds); - const blobSizes = sql.getMap(` + const blobSizes = sql.getMap(` SELECT blobs.blobId, LENGTH(content) FROM param_list JOIN notes ON notes.noteId = param_list.paramId AND notes.isDeleted = 0 @@ -44,7 +45,7 @@ function getSubtreeSize(req) { }; } -module.exports = { +export = { getNoteSize, getSubtreeSize }; diff --git a/src/routes/routes.js b/src/routes/routes.js index 98120062e..47bd7fe1a 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -54,7 +54,7 @@ const clipperRoute = require('./api/clipper'); const similarNotesRoute = require('./api/similar_notes'); const keysRoute = require('./api/keys'); const backendLogRoute = require('./api/backend_log'); -const statsRoute = require('./api/stats.js'); +const statsRoute = require('./api/stats'); const fontsRoute = require('./api/fonts'); const etapiTokensApiRoutes = require('./api/etapi_tokens'); const relationMapApiRoute = require('./api/relation-map'); From 7a98718e64495a5ae7a43ae5b30ad1cb8edcb1cf Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 23:28:51 +0300 Subject: [PATCH 23/25] server-ts: Convert routes/api/sync --- src/routes/api/{sync.js => sync.ts} | 57 ++++++++++++++++++----------- 1 file changed, 35 insertions(+), 22 deletions(-) rename src/routes/api/{sync.js => sync.ts} (76%) diff --git a/src/routes/api/sync.js b/src/routes/api/sync.ts similarity index 76% rename from src/routes/api/sync.js rename to src/routes/api/sync.ts index bd38e2905..728065336 100644 --- a/src/routes/api/sync.js +++ b/src/routes/api/sync.ts @@ -1,16 +1,19 @@ "use strict"; -const syncService = require('../../services/sync'); -const syncUpdateService = require('../../services/sync_update'); -const entityChangesService = require('../../services/entity_changes'); -const sql = require('../../services/sql'); -const sqlInit = require('../../services/sql_init'); -const optionService = require('../../services/options'); -const contentHashService = require('../../services/content_hash'); -const log = require('../../services/log'); -const syncOptions = require('../../services/sync_options'); -const utils = require('../../services/utils'); -const ws = require('../../services/ws'); +import syncService = require('../../services/sync'); +import syncUpdateService = require('../../services/sync_update'); +import entityChangesService = require('../../services/entity_changes'); +import sql = require('../../services/sql'); +import sqlInit = require('../../services/sql_init'); +import optionService = require('../../services/options'); +import contentHashService = require('../../services/content_hash'); +import log = require('../../services/log'); +import syncOptions = require('../../services/sync_options'); +import utils = require('../../services/utils'); +import ws = require('../../services/ws'); +import { Request } from 'express'; +import { EntityChange, EntityChangeRecord } from '../../services/entity_changes_interface'; +import ValidationError = require('../../errors/validation_error'); async function testSync() { try { @@ -26,7 +29,7 @@ async function testSync() { return { success: true, message: "Sync server handshake has been successful, sync has been started." }; } - catch (e) { + catch (e: any) { return { success: false, message: e.message @@ -82,15 +85,19 @@ function forceFullSync() { syncService.sync(); } -function getChanged(req) { +function getChanged(req: Request) { const startTime = Date.now(); - let lastEntityChangeId = parseInt(req.query.lastEntityChangeId); + if (typeof req.query.lastEntityChangeId !== "string") { + throw new ValidationError("Missing or invalid last entity change ID."); + } + + let lastEntityChangeId: number | null | undefined = parseInt(req.query.lastEntityChangeId); const clientInstanceId = req.query.instanceId; - let filteredEntityChanges = []; + let filteredEntityChanges: EntityChange[] = []; do { - const entityChanges = sql.getRows(` + const entityChanges: EntityChange[] = sql.getRows(` SELECT * FROM entity_changes WHERE isSynced = 1 @@ -129,16 +136,22 @@ function getChanged(req) { }; } -const partialRequests = {}; +const partialRequests: Record = {}; -function update(req) { +function update(req: Request) { let { body } = req; - const pageCount = parseInt(req.get('pageCount')); - const pageIndex = parseInt(req.get('pageIndex')); + const pageCount = parseInt(req.get('pageCount') as string); + const pageIndex = parseInt(req.get('pageIndex') as string); if (pageCount !== 1) { const requestId = req.get('requestId'); + if (!requestId) { + throw new Error("Missing request ID."); + } if (pageIndex === 0) { partialRequests[requestId] = { @@ -185,7 +198,7 @@ function syncFinished() { sqlInit.setDbAsInitialized(); } -function queueSector(req) { +function queueSector(req: Request) { const entityName = utils.sanitizeSqlIdentifier(req.params.entityName); const sector = utils.sanitizeSqlIdentifier(req.params.sector); @@ -196,7 +209,7 @@ function checkEntityChanges() { require('../../services/consistency_checks').runEntityChangesChecks(); } -module.exports = { +export = { testSync, checkSync, syncNow, From 6e042c20e9d7e88eed8a3a84f9e20813f373f092 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 6 Apr 2024 23:34:47 +0300 Subject: [PATCH 24/25] server-ts: Convert routes/api/tree --- src/routes/api/{tree.js => tree.ts} | 43 ++++++++++++++++------------- src/routes/routes.js | 2 +- 2 files changed, 25 insertions(+), 20 deletions(-) rename src/routes/api/{tree.js => tree.ts} (75%) diff --git a/src/routes/api/tree.js b/src/routes/api/tree.ts similarity index 75% rename from src/routes/api/tree.js rename to src/routes/api/tree.ts index c8188068b..0dcb42e67 100644 --- a/src/routes/api/tree.js +++ b/src/routes/api/tree.ts @@ -1,16 +1,18 @@ "use strict"; -const becca = require('../../becca/becca'); -const log = require('../../services/log'); -const NotFoundError = require('../../errors/not_found_error'); +import becca = require('../../becca/becca'); +import log = require('../../services/log'); +import NotFoundError = require('../../errors/not_found_error'); +import { Request } from 'express'; +import BNote = require('../../becca/entities/bnote'); -function getNotesAndBranchesAndAttributes(noteIds) { - noteIds = new Set(noteIds); - const collectedNoteIds = new Set(); - const collectedAttributeIds = new Set(); - const collectedBranchIds = new Set(); +function getNotesAndBranchesAndAttributes(_noteIds: string[] | Set) { + const noteIds = new Set(_noteIds); + const collectedNoteIds = new Set(); + const collectedAttributeIds = new Set(); + const collectedBranchIds = new Set(); - function collectEntityIds(note) { + function collectEntityIds(note?: BNote) { if (!note || collectedNoteIds.has(note.noteId)) { return; } @@ -18,15 +20,18 @@ function getNotesAndBranchesAndAttributes(noteIds) { collectedNoteIds.add(note.noteId); for (const branch of note.getParentBranches()) { - collectedBranchIds.add(branch.branchId); + if (branch.branchId) { + collectedBranchIds.add(branch.branchId); + } collectEntityIds(branch.parentNote); } for (const childNote of note.children) { const childBranch = becca.getBranchFromChildAndParent(childNote.noteId, note.noteId); - - collectedBranchIds.add(childBranch.branchId); + if (childBranch && childBranch.branchId) { + collectedBranchIds.add(childBranch.branchId); + } } for (const attr of note.ownedAttributes) { @@ -122,11 +127,11 @@ function getNotesAndBranchesAndAttributes(noteIds) { }; } -function getTree(req) { - const subTreeNoteId = req.query.subTreeNoteId || 'root'; - const collectedNoteIds = new Set([subTreeNoteId]); +function getTree(req: Request) { + const subTreeNoteId = req.query.subTreeNoteId === "string" ? req.query.subTreeNoteId : "" || 'root'; + const collectedNoteIds = new Set([subTreeNoteId]); - function collect(parentNote) { + function collect(parentNote: BNote) { if (!parentNote) { console.trace(parentNote); } @@ -136,7 +141,7 @@ function getTree(req) { const childBranch = becca.getBranchFromChildAndParent(childNote.noteId, parentNote.noteId); - if (childBranch.isExpanded) { + if (childBranch?.isExpanded) { collect(childBranch.childNote); } } @@ -151,11 +156,11 @@ function getTree(req) { return getNotesAndBranchesAndAttributes(collectedNoteIds); } -function load(req) { +function load(req: Request) { return getNotesAndBranchesAndAttributes(req.body.noteIds); } -module.exports = { +export = { getTree, load }; diff --git a/src/routes/routes.js b/src/routes/routes.js index 47bd7fe1a..2e53e4546 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -22,7 +22,7 @@ const loginRoute = require('./login.js'); const indexRoute = require('./index.js'); // API routes -const treeApiRoute = require('./api/tree.js'); +const treeApiRoute = require('./api/tree'); const notesApiRoute = require('./api/notes'); const branchesApiRoute = require('./api/branches'); const attachmentsApiRoute = require('./api/attachments'); From 1372cc1cb9e87200d64b0920c27d0276a37a7708 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 15 Apr 2024 21:20:59 +0300 Subject: [PATCH 25/25] server-ts: Fix regression --- src/routes/api/tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/api/tree.ts b/src/routes/api/tree.ts index 0dcb42e67..1b172f10b 100644 --- a/src/routes/api/tree.ts +++ b/src/routes/api/tree.ts @@ -128,7 +128,7 @@ function getNotesAndBranchesAndAttributes(_noteIds: string[] | Set) { } function getTree(req: Request) { - const subTreeNoteId = req.query.subTreeNoteId === "string" ? req.query.subTreeNoteId : "" || 'root'; + const subTreeNoteId = typeof req.query.subTreeNoteId === "string" ? req.query.subTreeNoteId : 'root'; const collectedNoteIds = new Set([subTreeNoteId]); function collect(parentNote: BNote) {