diff --git a/package-lock.json b/package-lock.json index e80b41f85..a0fdabf82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,6 +92,7 @@ "@types/better-sqlite3": "^7.6.9", "@types/cls-hooked": "^4.3.8", "@types/csurf": "^1.11.5", + "@types/ejs": "^3.1.5", "@types/escape-html": "^1.0.4", "@types/express": "^4.17.21", "@types/express-session": "^1.18.0", @@ -101,6 +102,7 @@ "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.11", "@types/node": "^20.11.19", + "@types/safe-compare": "^1.1.2", "@types/sanitize-html": "^2.11.0", "@types/sax": "^1.2.7", "@types/stream-throttle": "^0.1.4", @@ -1271,6 +1273,12 @@ "@types/ms": "*" } }, + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true + }, "node_modules/@types/escape-html": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz", @@ -1537,6 +1545,12 @@ "@types/node": "*" } }, + "node_modules/@types/safe-compare": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/safe-compare/-/safe-compare-1.1.2.tgz", + "integrity": "sha512-kK/IM1+pvwCMom+Kezt/UlP8LMEwm8rP6UgGbRc6zUnhU/csoBQ5rWgmD2CJuHxiMiX+H1VqPGpo0kDluJGXYA==", + "dev": true + }, "node_modules/@types/sanitize-html": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz", @@ -14276,6 +14290,12 @@ "@types/ms": "*" } }, + "@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true + }, "@types/escape-html": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz", @@ -14535,6 +14555,12 @@ "@types/node": "*" } }, + "@types/safe-compare": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/safe-compare/-/safe-compare-1.1.2.tgz", + "integrity": "sha512-kK/IM1+pvwCMom+Kezt/UlP8LMEwm8rP6UgGbRc6zUnhU/csoBQ5rWgmD2CJuHxiMiX+H1VqPGpo0kDluJGXYA==", + "dev": true + }, "@types/sanitize-html": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz", diff --git a/package.json b/package.json index 89ccc993c..8cc9cf947 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "@types/better-sqlite3": "^7.6.9", "@types/cls-hooked": "^4.3.8", "@types/csurf": "^1.11.5", + "@types/ejs": "^3.1.5", "@types/escape-html": "^1.0.4", "@types/express": "^4.17.21", "@types/express-session": "^1.18.0", @@ -122,6 +123,7 @@ "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.11", "@types/node": "^20.11.19", + "@types/safe-compare": "^1.1.2", "@types/sanitize-html": "^2.11.0", "@types/sax": "^1.2.7", "@types/stream-throttle": "^0.1.4", diff --git a/src/becca/entities/battribute.ts b/src/becca/entities/battribute.ts index ebd9e74cf..6ad1965ed 100644 --- a/src/becca/entities/battribute.ts +++ b/src/becca/entities/battribute.ts @@ -125,9 +125,6 @@ class BAttribute extends AbstractBeccaEntity { } } - /** - * @returns {BNote|null} - */ getNote() { const note = this.becca.getNote(this.noteId); @@ -138,9 +135,6 @@ class BAttribute extends AbstractBeccaEntity { return note; } - /** - * @returns {BNote|null} - */ getTargetNote() { if (this.type !== 'relation') { throw new Error(`Attribute '${this.attributeId}' is not a relation.`); @@ -153,9 +147,6 @@ class BAttribute extends AbstractBeccaEntity { return this.becca.getNote(this.value); } - /** - * @returns {boolean} - */ isDefinition() { return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:')); } diff --git a/src/becca/entities/bbranch.ts b/src/becca/entities/bbranch.ts index 2b6dc8657..0f904de92 100644 --- a/src/becca/entities/bbranch.ts +++ b/src/becca/entities/bbranch.ts @@ -127,8 +127,6 @@ class BBranch extends AbstractBeccaEntity { * An example is shared or bookmarked clones - they are created automatically and exist for technical reasons, * not as user-intended actions. From user perspective, they don't count as real clones and for the purpose * of deletion should not act as a clone. - * - * @returns {boolean} */ get isWeak() { return ['_share', '_lbBookmarks'].includes(this.parentNoteId); diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index c29945a93..a007f5278 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -167,39 +167,32 @@ class BNote extends AbstractBeccaEntity { return this.isContentAvailable() ? this.title : '[protected]'; } - /** @returns {BBranch[]} */ getParentBranches() { return this.parentBranches; } /** * Returns strong (as opposed to weak) parent branches. See isWeak for details. - * - * @returns {BBranch[]} */ getStrongParentBranches() { return this.getParentBranches().filter(branch => !branch.isWeak); } /** - * @returns {BBranch[]} * @deprecated use getParentBranches() instead */ getBranches() { return this.parentBranches; } - /** @returns {BNote[]} */ getParentNotes() { return this.parents; } - /** @returns {BNote[]} */ getChildNotes() { return this.children; } - /** @returns {boolean} */ hasChildren() { return this.children && this.children.length > 0; } @@ -209,7 +202,7 @@ class BNote extends AbstractBeccaEntity { .map(childNote => this.becca.getBranchFromChildAndParent(childNote.noteId, this.noteId)) as BBranch[]; } - /* + /** * Note content has quite special handling - it's not a separate entity, but a lazily loaded * part of Note entity with its own sync. Reasons behind this hybrid design has been: * @@ -222,7 +215,8 @@ class BNote extends AbstractBeccaEntity { } /** - * @throws Error in case of invalid JSON */ + * @throws Error in case of invalid JSON + */ getJsonContent(): any | null { const content = this.getContent(); @@ -233,7 +227,7 @@ class BNote extends AbstractBeccaEntity { return JSON.parse(content); } - /** @returns {*|null} valid object or null if the content cannot be parsed as JSON */ + /** @returns valid object or null if the content cannot be parsed as JSON */ getJsonContentSafely() { try { return this.getJsonContent(); @@ -269,17 +263,17 @@ class BNote extends AbstractBeccaEntity { return this.utcDateModified === null ? null : dayjs.utc(this.utcDateModified); } - /** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */ + /** @returns true if this note is the root of the note tree. Root note has "root" noteId */ isRoot() { return this.noteId === 'root'; } - /** @returns {boolean} true if this note is of application/json content type */ + /** @returns true if this note is of application/json content type */ isJson() { return this.mime === "application/json"; } - /** @returns {boolean} true if this note is JavaScript (code or attachment) */ + /** @returns true if this note is JavaScript (code or attachment) */ isJavaScript() { return (this.type === "code" || this.type === "file" || this.type === 'launcher') && (this.mime.startsWith("application/javascript") @@ -287,13 +281,13 @@ class BNote extends AbstractBeccaEntity { || this.mime === "text/javascript"); } - /** @returns {boolean} true if this note is HTML */ + /** @returns true if this note is HTML */ isHtml() { return ["code", "file", "render"].includes(this.type) && this.mime === "text/html"; } - /** @returns {boolean} true if this note is an image */ + /** @returns true if this note is an image */ isImage() { return this.type === 'image' || (this.type === 'file' && this.mime?.startsWith('image/')); @@ -304,12 +298,12 @@ class BNote extends AbstractBeccaEntity { return this.hasStringContent(); } - /** @returns {boolean} true if the note has string content (not binary) */ + /** @returns true if the note has string content (not binary) */ hasStringContent() { return utils.isStringNote(this.type, this.mime); } - /** @returns {string|null} JS script environment - either "frontend" or "backend" */ + /** @returns JS script environment - either "frontend" or "backend" */ getScriptEnv() { if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) { return "frontend"; @@ -518,8 +512,8 @@ class BNote extends AbstractBeccaEntity { } /** - * @param {string} name - label name - * @returns {BAttribute|null} label if it exists, null otherwise + * @param name - label name + * @returns label if it exists, null otherwise */ getLabel(name: string): BAttribute | null { return this.getAttribute(LABEL, name); @@ -680,7 +674,7 @@ class BNote extends AbstractBeccaEntity { * @param type - (optional) attribute type to filter * @param name - (optional) attribute name to filter * @param value - (optional) attribute value to filter - * @returns {BAttribute[]} note's "owned" attributes - excluding inherited ones + * @returns note's "owned" attributes - excluding inherited ones */ getOwnedAttributes(type: string | null = null, name: string | null = null, value: string | null = null) { this.__validateTypeName(type, name); @@ -703,7 +697,7 @@ class BNote extends AbstractBeccaEntity { } /** - * @returns {BAttribute} attribute belonging to this specific note (excludes inherited attributes) + * @returns attribute belonging to this specific note (excludes inherited attributes) * * This method can be significantly faster than the getAttribute() */ @@ -780,7 +774,7 @@ class BNote extends AbstractBeccaEntity { * - fast searching * - note similarity evaluation * - * @returns {string} - returns flattened textual representation of note, prefixes and attributes + * @returns - returns flattened textual representation of note, prefixes and attributes */ getFlatText() { if (!this.__flatTextCache) { @@ -971,7 +965,7 @@ class BNote extends AbstractBeccaEntity { }; } - /** @returns {string[]} - includes the subtree root note as well */ + /** @returns includes the subtree root note as well */ getSubtreeNoteIds({includeArchived = true, includeHidden = false, resolveSearch = false} = {}) { return this.getSubtree({includeArchived, includeHidden, resolveSearch}) .notes @@ -1031,7 +1025,6 @@ class BNote extends AbstractBeccaEntity { return this.getOwnedAttributes().length; } - /** @returns {BNote[]} */ getAncestors() { if (!this.__ancestorCache) { const noteIds = new Set(); @@ -1075,7 +1068,6 @@ class BNote extends AbstractBeccaEntity { return this.noteId === '_hidden' || this.hasAncestor('_hidden'); } - /** @returns {BAttribute[]} */ getTargetRelations() { return this.targetRelations; } @@ -1117,7 +1109,6 @@ class BNote extends AbstractBeccaEntity { .map(row => new BRevision(row)); } - /** @returns {BAttachment[]} */ getAttachments(opts: AttachmentOpts = {}) { opts.includeContentLength = !!opts.includeContentLength; // from testing, it looks like calculating length does not make a difference in performance even on large-ish DB @@ -1135,7 +1126,6 @@ class BNote extends AbstractBeccaEntity { .map(row => new BAttachment(row)); } - /** @returns {BAttachment|null} */ getAttachmentById(attachmentId: string, opts: AttachmentOpts = {}) { opts.includeContentLength = !!opts.includeContentLength; @@ -1582,10 +1572,7 @@ class BNote extends AbstractBeccaEntity { return !(this.noteId in this.becca.notes) || this.isBeingDeleted; } - /** - * @returns {BRevision|null} - */ - saveRevision() { + saveRevision(): BRevision { return sql.transactional(() => { let noteContent = this.getContent(); @@ -1632,9 +1619,8 @@ class BNote extends AbstractBeccaEntity { } /** - * @param {string} matchBy - choose by which property we detect if to update an existing attachment. - * Supported values are either 'attachmentId' (default) or 'title' - * @returns {BAttachment} + * @param matchBy - choose by which property we detect if to update an existing attachment. + * Supported values are either 'attachmentId' (default) or 'title' */ saveAttachment({attachmentId, role, mime, title, content, position}: AttachmentRow, matchBy = 'attachmentId') { if (!['attachmentId', 'title'].includes(matchBy)) { diff --git a/src/routes/routes.js b/src/routes/routes.js index 00c803cb0..7aa6a64ef 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -59,7 +59,7 @@ const fontsRoute = require('./api/fonts'); const etapiTokensApiRoutes = require('./api/etapi_tokens'); const relationMapApiRoute = require('./api/relation-map'); const otherRoute = require('./api/other'); -const shareRoutes = require('../share/routes.js'); +const shareRoutes = require('../share/routes'); const etapiAuthRoutes = require('../etapi/auth'); const etapiAppInfoRoutes = require('../etapi/app_info'); diff --git a/src/services/backend_script_api.ts b/src/services/backend_script_api.ts index 627cd3c49..9b72929a4 100644 --- a/src/services/backend_script_api.ts +++ b/src/services/backend_script_api.ts @@ -127,8 +127,8 @@ interface Api { /** * Retrieves notes with given label name & value * - * @param name - attribute name - * @param value - attribute value + * @param name - attribute name + * @param value - attribute value */ getNotesWithLabel(name: string, value?: string): BNote[]; diff --git a/src/services/consistency_checks.ts b/src/services/consistency_checks.ts index a8f51c024..1ec432883 100644 --- a/src/services/consistency_checks.ts +++ b/src/services/consistency_checks.ts @@ -68,7 +68,7 @@ class ConsistencyChecks { childToParents[childNoteId].push(parentNoteId); } - /** @returns {boolean} true if cycle was found and we should try again */ + /** @returns true if cycle was found and we should try again */ const checkTreeCycle = (noteId: string, path: string[]) => { if (noteId === 'root') { return false; diff --git a/src/services/events.ts b/src/services/events.ts index 327443a95..0eda326b9 100644 --- a/src/services/events.ts +++ b/src/services/events.ts @@ -17,8 +17,7 @@ type EventListener = (data: any) => void; const eventListeners: Record = {}; /** - * @param {string|string[]}eventTypes - can be either single event or an array of events - * @param listener + * @param eventTypes - can be either single event or an array of events */ function subscribe(eventTypes: EventType, listener: EventListener) { if (!Array.isArray(eventTypes)) { diff --git a/src/services/sql.ts b/src/services/sql.ts index 18d72912f..cb4a75df5 100644 --- a/src/services/sql.ts +++ b/src/services/sql.ts @@ -323,9 +323,9 @@ export = { * Get single value from the given query - first column from first returned row. * * @method - * @param {string} query - SQL query with ? used as parameter placeholder - * @param {object[]} [params] - array of params if needed - * @returns [object] - single value + * @param query - SQL query with ? used as parameter placeholder + * @param params - array of params if needed + * @returns single value */ getValue, @@ -333,9 +333,9 @@ export = { * Get first returned row. * * @method - * @param {string} query - SQL query with ? used as parameter placeholder - * @param {object[]} [params] - array of params if needed - * @returns {object} - map of column name to column value + * @param query - SQL query with ? used as parameter placeholder + * @param params - array of params if needed + * @returns - map of column name to column value */ getRow, getRowOrNull, @@ -344,9 +344,9 @@ export = { * Get all returned rows. * * @method - * @param {string} query - SQL query with ? used as parameter placeholder - * @param {object[]} [params] - array of params if needed - * @returns {object[]} - array of all rows, each row is a map of column name to column value + * @param query - SQL query with ? used as parameter placeholder + * @param params - array of params if needed + * @returns - array of all rows, each row is a map of column name to column value */ getRows, getRawRows, @@ -357,9 +357,9 @@ export = { * Get a map of first column mapping to second column. * * @method - * @param {string} query - SQL query with ? used as parameter placeholder - * @param {object[]} [params] - array of params if needed - * @returns {object} - map of first column to second column + * @param query - SQL query with ? used as parameter placeholder + * @param params - array of params if needed + * @returns - map of first column to second column */ getMap, @@ -367,9 +367,9 @@ export = { * Get a first column in an array. * * @method - * @param {string} query - SQL query with ? used as parameter placeholder - * @param {object[]} [params] - array of params if needed - * @returns {object[]} - array of first column of all returned rows + * @param query - SQL query with ? used as parameter placeholder + * @param params - array of params if needed + * @returns array of first column of all returned rows */ getColumn, @@ -377,8 +377,8 @@ export = { * Execute SQL * * @method - * @param {string} query - SQL query with ? used as parameter placeholder - * @param {object[]} [params] - array of params if needed + * @param query - SQL query with ? used as parameter placeholder + * @param params - array of params if needed */ execute, executeMany, diff --git a/src/services/utils.ts b/src/services/utils.ts index 6b2974df0..aef261a6a 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -156,9 +156,9 @@ const STRING_MIME_TYPES = [ "image/svg+xml" ]; -function isStringNote(type: string, mime: string) { +function isStringNote(type: string | null, mime: string) { // render and book are string note in the sense that they are expected to contain empty string - return ["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"].includes(type) + return (type && ["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"].includes(type)) || mime.startsWith('text/') || STRING_MIME_TYPES.includes(mime); } diff --git a/src/share/content_renderer.js b/src/share/content_renderer.ts similarity index 82% rename from src/share/content_renderer.js rename to src/share/content_renderer.ts index 9065fec08..cb111008a 100644 --- a/src/share/content_renderer.js +++ b/src/share/content_renderer.ts @@ -1,10 +1,17 @@ -const {JSDOM} = require("jsdom"); -const shaca = require('./shaca/shaca.js'); -const assetPath = require('../services/asset_path'); -const shareRoot = require('./share_root.js'); -const escapeHtml = require('escape-html'); +import { JSDOM } from "jsdom"; +import shaca = require('./shaca/shaca'); +import assetPath = require('../services/asset_path'); +import shareRoot = require('./share_root'); +import escapeHtml = require('escape-html'); +import SNote = require("./shaca/entities/snote"); -function getContent(note) { +interface Result { + header: string; + content: string | Buffer | undefined; + isEmpty: boolean; +} + +function getContent(note: SNote) { if (note.isProtected) { return { header: '', @@ -13,7 +20,7 @@ function getContent(note) { }; } - const result = { + const result: Result = { content: note.getContent(), header: '', isEmpty: false @@ -38,7 +45,7 @@ function getContent(note) { return result; } -function renderIndex(result) { +function renderIndex(result: Result) { result.content += '
    '; const rootNote = shaca.getNote(shareRoot.SHARE_ROOT_NOTE_ID); @@ -53,10 +60,10 @@ function renderIndex(result) { result.content += '
'; } -function renderText(result, note) { +function renderText(result: Result, note: SNote) { const document = new JSDOM(result.content || "").window.document; - result.isEmpty = document.body.textContent.trim().length === 0 + result.isEmpty = document.body.textContent?.trim().length === 0 && document.querySelectorAll("img").length === 0; if (!result.isEmpty) { @@ -89,7 +96,9 @@ function renderText(result, note) { if (linkedNote) { const isExternalLink = linkedNote.hasLabel("shareExternalLink"); const href = isExternalLink ? linkedNote.getLabelValue("shareExternalLink") : `./${linkedNote.shareId}`; - linkEl.setAttribute("href", href); + if (href) { + linkEl.setAttribute("href", href); + } if (isExternalLink) { linkEl.setAttribute("target", "_blank"); linkEl.setAttribute("rel", "noopener noreferrer"); @@ -122,8 +131,8 @@ document.addEventListener("DOMContentLoaded", function() { } } -function renderCode(result) { - if (!result.content?.trim()) { +function renderCode(result: Result) { + if (typeof result.content !== "string" || !result.content?.trim()) { result.isEmpty = true; } else { const document = new JSDOM().window.document; @@ -135,7 +144,11 @@ function renderCode(result) { } } -function renderMermaid(result, note) { +function renderMermaid(result: Result, note: SNote) { + if (typeof result.content !== "string") { + return; + } + result.content = `
@@ -145,11 +158,11 @@ function renderMermaid(result, note) { ` } -function renderImage(result, note) { +function renderImage(result: Result, note: SNote) { result.content = ``; } -function renderFile(note, result) { +function renderFile(note: SNote, result: Result) { if (note.mime === 'application/pdf') { result.content = `` } else { @@ -157,6 +170,6 @@ function renderFile(note, result) { } } -module.exports = { +export = { getContent }; diff --git a/src/share/routes.js b/src/share/routes.ts similarity index 73% rename from src/share/routes.js rename to src/share/routes.ts index 4ad5a15c6..97a0dd8e1 100644 --- a/src/share/routes.js +++ b/src/share/routes.ts @@ -1,23 +1,22 @@ -const express = require('express'); -const path = require('path'); -const safeCompare = require('safe-compare'); -const ejs = require("ejs"); +import safeCompare = require('safe-compare'); +import ejs = require("ejs"); -const shaca = require('./shaca/shaca.js'); -const shacaLoader = require('./shaca/shaca_loader.js'); -const shareRoot = require('./share_root.js'); -const contentRenderer = require('./content_renderer.js'); -const assetPath = require('../services/asset_path'); -const appPath = require('../services/app_path'); -const searchService = require('../services/search/services/search'); -const SearchContext = require('../services/search/search_context'); -const log = require('../services/log'); +import type { Request, Response, Router } from "express"; -/** - * @param {SNote} note - * @return {{note: SNote, branch: SBranch}|{}} - */ -function getSharedSubTreeRoot(note) { +import shaca = require('./shaca/shaca'); +import shacaLoader = require('./shaca/shaca_loader'); +import shareRoot = require('./share_root'); +import contentRenderer = require('./content_renderer'); +import assetPath = require('../services/asset_path'); +import appPath = require('../services/app_path'); +import searchService = require('../services/search/services/search'); +import SearchContext = require('../services/search/search_context'); +import log = require('../services/log'); +import SNote = require('./shaca/entities/snote'); +import SBranch = require('./shaca/entities/sbranch'); +import SAttachment = require('./shaca/entities/sattachment'); + +function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } { if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) { // share root itself is not shared return {}; @@ -37,19 +36,18 @@ function getSharedSubTreeRoot(note) { return getSharedSubTreeRoot(parentBranch.getParentNote()); } -function addNoIndexHeader(note, res) { +function addNoIndexHeader(note: SNote, res: Response) { if (note.isLabelTruthy('shareDisallowRobotIndexing')) { res.setHeader('X-Robots-Tag', 'noindex'); } } -function requestCredentials(res) { +function requestCredentials(res: Response) { res.setHeader('WWW-Authenticate', 'Basic realm="User Visible Realm", charset="UTF-8"') .sendStatus(401); } -/** @returns {SAttachment|boolean} */ -function checkAttachmentAccess(attachmentId, req, res) { +function checkAttachmentAccess(attachmentId: string, req: Request, res: Response) { const attachment = shaca.getAttachment(attachmentId); if (!attachment) { @@ -65,8 +63,7 @@ function checkAttachmentAccess(attachmentId, req, res) { return note ? attachment : false; } -/** @returns {SNote|boolean} */ -function checkNoteAccess(noteId, req, res) { +function checkNoteAccess(noteId: string, req: Request, res: Response) { const note = shaca.getNote(noteId); if (!note) { @@ -109,12 +106,16 @@ function checkNoteAccess(noteId, req, res) { return false; } -function renderImageAttachment(image, res, attachmentName) { +function renderImageAttachment(image: SNote, res: Response, attachmentName: string) { let svgString = '' const attachment = image.getAttachmentByTitle(attachmentName); - - if (attachment) { - svgString = attachment.getContent(); + if (!attachment) { + res.status(404).render("share/404"); + return; + } + const content = attachment.getContent(); + if (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; @@ -130,8 +131,8 @@ function renderImageAttachment(image, res, attachmentName) { res.send(svg); } -function register(router) { - function renderNote(note, req, res) { +function register(router: Router) { + function renderNote(note: SNote, req: Request, res: Response) { if (!note) { res.status(404).render("share/404"); return; @@ -152,35 +153,42 @@ function register(router) { return; } - const {header, content, isEmpty} = contentRenderer.getContent(note); + const { header, content, isEmpty } = contentRenderer.getContent(note); const subRoot = getSharedSubTreeRoot(note); - const opts = {note, header, content, isEmpty, subRoot, assetPath, appPath}; + const opts = { note, header, content, isEmpty, subRoot, assetPath, appPath }; let useDefaultView = true; // Check if the user has their own template if (note.hasRelation('shareTemplate')) { // Get the template note and content - const templateId = note.getRelation('shareTemplate').value; - const templateNote = shaca.getNote(templateId); + const templateId = note.getRelation('shareTemplate')?.value; + const templateNote = templateId && shaca.getNote(templateId); // Make sure the note type is correct - if (templateNote.type === 'code' && templateNote.mime === 'application/x-ejs') { + if (templateNote && templateNote.type === 'code' && templateNote.mime === 'application/x-ejs') { // EJS caches the result of this so we don't need to pre-cache - const includer = (path) => { + const includer = (path: string) => { const childNote = templateNote.children.find(n => path === n.title); - if (!childNote) return null; - if (childNote.type !== 'code' || childNote.mime !== 'application/x-ejs') return null; - return { template: childNote.getContent() }; + if (!childNote) throw new Error("Unable to find child note."); + if (childNote.type !== 'code' || childNote.mime !== 'application/x-ejs') throw new Error("Incorrect child note type."); + + const template = childNote.getContent(); + if (typeof template !== "string") throw new Error("Invalid template content type."); + + return { template }; }; // Try to render user's template, w/ fallback to default view try { - const ejsResult = ejs.render(templateNote.getContent(), opts, {includer}); - res.send(ejsResult); - useDefaultView = false; // Rendering went okay, don't use default view + const content = templateNote.getContent(); + if (typeof content === "string") { + const ejsResult = ejs.render(content, opts, { includer }); + res.send(ejsResult); + useDefaultView = false; // Rendering went okay, don't use default view + } } - catch (e) { + catch (e: any) { log.error(`Rendering user provided share template (${templateId}) threw exception ${e.message} with stacktrace: ${e.stack}`); } } @@ -199,13 +207,18 @@ function register(router) { shacaLoader.ensureLoad(); + if (!shaca.shareRootNote) { + return res.status(404) + .json({ message: "Share root note not found" }); + } + renderNote(shaca.shareRootNote, req, res); }); router.get('/share/:shareId', (req, res, next) => { shacaLoader.ensureLoad(); - const {shareId} = req.params; + const { shareId } = req.params; const note = shaca.aliasToNote[shareId] || shaca.notes[shareId]; @@ -214,7 +227,7 @@ function register(router) { router.get('/share/api/notes/:noteId', (req, res, next) => { shacaLoader.ensureLoad(); - let note; + let note: SNote | boolean; if (!(note = checkNoteAccess(req.params.noteId, req, res))) { return; @@ -228,7 +241,7 @@ function register(router) { router.get('/share/api/notes/:noteId/download', (req, res, next) => { shacaLoader.ensureLoad(); - let note; + let note: SNote | boolean; if (!(note = checkNoteAccess(req.params.noteId, req, res))) { return; @@ -252,7 +265,7 @@ function register(router) { router.get('/share/api/images/:noteId/:filename', (req, res, next) => { shacaLoader.ensureLoad(); - let image; + let image: SNote | boolean; if (!(image = checkNoteAccess(req.params.noteId, req, res))) { return; @@ -277,7 +290,7 @@ function register(router) { router.get('/share/api/attachments/:attachmentId/image/:filename', (req, res, next) => { shacaLoader.ensureLoad(); - let attachment; + let attachment: SAttachment | boolean; if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) { return; @@ -296,7 +309,7 @@ function register(router) { router.get('/share/api/attachments/:attachmentId/download', (req, res, next) => { shacaLoader.ensureLoad(); - let attachment; + let attachment: SAttachment | boolean; if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) { return; @@ -320,7 +333,7 @@ function register(router) { router.get('/share/api/notes/:noteId/view', (req, res, next) => { shacaLoader.ensureLoad(); - let note; + let note: SNote | boolean; if (!(note = checkNoteAccess(req.params.noteId, req, res))) { return; @@ -341,18 +354,22 @@ function register(router) { const ancestorNoteId = req.query.ancestorNoteId ?? "_share"; let note; + if (typeof ancestorNoteId !== "string") { + return res.status(400).json({ message: "'ancestorNoteId' parameter is mandatory." }); + } + // This will automatically return if no ancestorNoteId is provided and there is no shareIndex if (!(note = checkNoteAccess(ancestorNoteId, req, res))) { return; } - const {search} = req.query; + const { search } = req.query; - if (!search?.trim()) { + if (typeof search !== "string" || !search?.trim()) { return res.status(400).json({ message: "'search' parameter is mandatory." }); } - const searchContext = new SearchContext({ancestorNoteId: ancestorNoteId}); + const searchContext = new SearchContext({ ancestorNoteId: ancestorNoteId }); const searchResults = searchService.findResultsWithQuery(search, searchContext); const filteredResults = searchResults.map(sr => { const fullNote = shaca.notes[sr.noteId]; @@ -366,6 +383,6 @@ function register(router) { }); } -module.exports = { +export = { register } diff --git a/src/share/shaca/entities/abstract_shaca_entity.js b/src/share/shaca/entities/abstract_shaca_entity.js deleted file mode 100644 index 772eb4d6f..000000000 --- a/src/share/shaca/entities/abstract_shaca_entity.js +++ /dev/null @@ -1,14 +0,0 @@ -let shaca; - -class AbstractShacaEntity { - /** @return {Shaca} */ - get shaca() { - if (!shaca) { - shaca = require('../shaca.js'); - } - - return shaca; - } -} - -module.exports = AbstractShacaEntity; diff --git a/src/share/shaca/entities/abstract_shaca_entity.ts b/src/share/shaca/entities/abstract_shaca_entity.ts new file mode 100644 index 000000000..20dba31ff --- /dev/null +++ b/src/share/shaca/entities/abstract_shaca_entity.ts @@ -0,0 +1,15 @@ +import Shaca from "../shaca-interface"; + +let shaca: Shaca; + +class AbstractShacaEntity { + get shaca(): Shaca { + if (!shaca) { + shaca = require('../shaca'); + } + + return shaca; + } +} + +export = AbstractShacaEntity; diff --git a/src/share/shaca/entities/rows.ts b/src/share/shaca/entities/rows.ts new file mode 100644 index 000000000..d8b8ec84e --- /dev/null +++ b/src/share/shaca/entities/rows.ts @@ -0,0 +1,4 @@ +type SNoteRow = [ string, string, string, string, string, string, boolean ]; +type SBranchRow = [ string, string, string, string, string, boolean ]; +type SAttributeRow = [ string, string, string, string, string, boolean, number ]; +type SAttachmentRow = [ string, string, string, string, string, string, string ]; diff --git a/src/share/shaca/entities/sattachment.js b/src/share/shaca/entities/sattachment.ts similarity index 64% rename from src/share/shaca/entities/sattachment.js rename to src/share/shaca/entities/sattachment.ts index 4b76fea2c..1e9565ce3 100644 --- a/src/share/shaca/entities/sattachment.js +++ b/src/share/shaca/entities/sattachment.ts @@ -1,39 +1,42 @@ "use strict"; -const sql = require('../../sql'); -const utils = require('../../../services/utils'); -const AbstractShacaEntity = require('./abstract_shaca_entity.js'); +import sql = require('../../sql'); +import utils = require('../../../services/utils'); +import AbstractShacaEntity = require('./abstract_shaca_entity'); +import SNote = require('./snote'); +import { Blob } from '../../../services/blob-interface'; class SAttachment extends AbstractShacaEntity { - constructor([attachmentId, ownerId, role, mime, title, blobId, utcDateModified]) { + private attachmentId: string; + ownerId: string; + title: string; + role: string; + mime: string; + private blobId: string; + /** used for caching of images */ + private utcDateModified: string; + + constructor([attachmentId, ownerId, role, mime, title, blobId, utcDateModified]: SAttachmentRow) { super(); - /** @param {string} */ this.attachmentId = attachmentId; - /** @param {string} */ this.ownerId = ownerId; - /** @param {string} */ this.title = title; - /** @param {string} */ this.role = role; - /** @param {string} */ this.mime = mime; - /** @param {string} */ this.blobId = blobId; - /** @param {string} */ - this.utcDateModified = utcDateModified; // used for caching of images + this.utcDateModified = utcDateModified; this.shaca.attachments[this.attachmentId] = this; this.shaca.notes[this.ownerId].attachments.push(this); } - /** @returns {SNote} */ - get note() { + get note(): SNote { return this.shaca.notes[this.ownerId]; } getContent(silentNotFoundError = false) { - const row = sql.getRow(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]); + const row = sql.getRow>(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]); if (!row) { if (silentNotFoundError) { @@ -56,7 +59,7 @@ class SAttachment extends AbstractShacaEntity { } } - /** @returns {boolean} true if the attachment has string content (not binary) */ + /** @returns true if the attachment has string content (not binary) */ hasStringContent() { return utils.isStringNote(null, this.mime); } @@ -67,11 +70,10 @@ class SAttachment extends AbstractShacaEntity { role: this.role, mime: this.mime, title: this.title, - position: this.position, blobId: this.blobId, utcDateModified: this.utcDateModified }; } } -module.exports = SAttachment; +export = SAttachment; diff --git a/src/share/shaca/entities/sattribute.js b/src/share/shaca/entities/sattribute.ts similarity index 80% rename from src/share/shaca/entities/sattribute.js rename to src/share/shaca/entities/sattribute.ts index bc3b5437a..390a85b8c 100644 --- a/src/share/shaca/entities/sattribute.js +++ b/src/share/shaca/entities/sattribute.ts @@ -1,24 +1,28 @@ "use strict"; -const AbstractShacaEntity = require('./abstract_shaca_entity.js'); +import SNote = require("./snote"); + +const AbstractShacaEntity = require('./abstract_shaca_entity'); class SAttribute extends AbstractShacaEntity { - constructor([attributeId, noteId, type, name, value, isInheritable, position]) { + + attributeId: string; + private noteId: string; + type: string; + name: string; + private position: number; + value: string; + isInheritable: boolean; + + constructor([attributeId, noteId, type, name, value, isInheritable, position]: SAttributeRow) { super(); - /** @param {string} */ this.attributeId = attributeId; - /** @param {string} */ this.noteId = noteId; - /** @param {string} */ this.type = type; - /** @param {string} */ this.name = name; - /** @param {int} */ this.position = position; - /** @param {string} */ this.value = value; - /** @param {boolean} */ this.isInheritable = !!isInheritable; this.shaca.attributes[this.attributeId] = this; @@ -53,41 +57,34 @@ class SAttribute extends AbstractShacaEntity { } } - /** @returns {boolean} */ get isAffectingSubtree() { return this.isInheritable || (this.type === 'relation' && ['template', 'inherit'].includes(this.name)); } - /** @returns {string} */ get targetNoteId() { // alias return this.type === 'relation' ? this.value : undefined; } - /** @returns {boolean} */ isAutoLink() { return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name); } - /** @returns {SNote} */ - get note() { + get note(): SNote { return this.shaca.notes[this.noteId]; } - /** @returns {SNote|null} */ - get targetNote() { + get targetNote(): SNote | null | undefined { if (this.type === 'relation') { return this.shaca.notes[this.value]; } } - /** @returns {SNote|null} */ - getNote() { + getNote(): SNote | null { return this.shaca.getNote(this.noteId); } - /** @returns {SNote|null} */ - getTargetNote() { + getTargetNote(): SNote | null { if (this.type !== 'relation') { throw new Error(`Attribute '${this.attributeId}' is not relation`); } @@ -112,4 +109,4 @@ class SAttribute extends AbstractShacaEntity { } } -module.exports = SAttribute; +export = SAttribute; diff --git a/src/share/shaca/entities/sbranch.js b/src/share/shaca/entities/sbranch.ts similarity index 72% rename from src/share/shaca/entities/sbranch.js rename to src/share/shaca/entities/sbranch.ts index 1be0f424a..0ff356922 100644 --- a/src/share/shaca/entities/sbranch.js +++ b/src/share/shaca/entities/sbranch.ts @@ -1,22 +1,25 @@ "use strict"; -const AbstractShacaEntity = require('./abstract_shaca_entity.js'); +import AbstractShacaEntity = require('./abstract_shaca_entity'); +import SNote = require('./snote'); class SBranch extends AbstractShacaEntity { - constructor([branchId, noteId, parentNoteId, prefix, isExpanded]) { + + private branchId: string; + private noteId: string; + parentNoteId: string; + private prefix: string; + private isExpanded: boolean; + isHidden: boolean; + + constructor([branchId, noteId, parentNoteId, prefix, isExpanded]: SBranchRow) { super(); - /** @param {string} */ this.branchId = branchId; - /** @param {string} */ this.noteId = noteId; - /** @param {string} */ this.parentNoteId = parentNoteId; - /** @param {string} */ this.prefix = prefix; - /** @param {boolean} */ this.isExpanded = !!isExpanded; - /** @param {boolean} */ this.isHidden = false; const childNote = this.childNote; @@ -38,25 +41,21 @@ class SBranch extends AbstractShacaEntity { this.shaca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this; } - /** @returns {SNote} */ - get childNote() { + get childNote(): SNote { return this.shaca.notes[this.noteId]; } - /** @returns {SNote} */ getNote() { return this.childNote; } - /** @returns {SNote} */ - get parentNote() { + get parentNote(): SNote { return this.shaca.notes[this.parentNoteId]; } - /** @returns {SNote} */ getParentNote() { return this.parentNote; } } -module.exports = SBranch; +export = SBranch; diff --git a/src/share/shaca/entities/snote.js b/src/share/shaca/entities/snote.ts similarity index 51% rename from src/share/shaca/entities/snote.js rename to src/share/shaca/entities/snote.ts index fd889c23f..bae610886 100644 --- a/src/share/shaca/entities/snote.js +++ b/src/share/shaca/entities/snote.ts @@ -1,108 +1,103 @@ "use strict"; -const sql = require('../../sql'); -const utils = require('../../../services/utils'); -const AbstractShacaEntity = require('./abstract_shaca_entity.js'); -const escape = require('escape-html'); +import sql = require('../../sql'); +import utils = require('../../../services/utils'); +import AbstractShacaEntity = require('./abstract_shaca_entity'); +import escape = require('escape-html'); +import { Blob } from '../../../services/blob-interface'; +import SAttachment = require('./sattachment'); +import SAttribute = require('./sattribute'); +import SBranch = require('./sbranch'); const LABEL = 'label'; const RELATION = 'relation'; const CREDENTIALS = 'shareCredentials'; -const isCredentials = attr => attr.type === 'label' && attr.name === CREDENTIALS; +const isCredentials = (attr: SAttribute) => attr.type === 'label' && attr.name === CREDENTIALS; class SNote extends AbstractShacaEntity { - constructor([noteId, title, type, mime, blobId, utcDateModified, isProtected]) { + noteId: string; + title: string; + type: string; + mime: string; + private blobId: string; + utcDateModified: string; + isProtected: boolean; + parentBranches: SBranch[]; + parents: SNote[]; + children: SNote[]; + private ownedAttributes: SAttribute[]; + private __attributeCache: SAttribute[] | null; + private __inheritableAttributeCache: SAttribute[] | null; + targetRelations: SAttribute[]; + attachments: SAttachment[]; + + constructor([noteId, title, type, mime, blobId, utcDateModified, isProtected]: SNoteRow) { super(); - /** @param {string} */ this.noteId = noteId; - /** @param {string} */ this.title = isProtected ? "[protected]" : title; - /** @param {string} */ this.type = type; - /** @param {string} */ this.mime = mime; - /** @param {string} */ this.blobId = blobId; - /** @param {string} */ this.utcDateModified = utcDateModified; // used for caching of images - /** @param {boolean} */ this.isProtected = isProtected; - /** @param {SBranch[]} */ this.parentBranches = []; - /** @param {SNote[]} */ this.parents = []; - /** @param {SNote[]} */ this.children = []; - /** @param {SAttribute[]} */ this.ownedAttributes = []; - /** @param {SAttribute[]|null} */ this.__attributeCache = null; - /** @param {SAttribute[]|null} */ this.__inheritableAttributeCache = null; - /** @param {SAttribute[]} */ this.targetRelations = []; - - /** @param {SAttachment[]} */ this.attachments = []; this.shaca.notes[this.noteId] = this; } - /** @returns {SBranch[]} */ getParentBranches() { return this.parentBranches; } - /** @returns {SBranch[]} */ getBranches() { return this.parentBranches; } - /** @returns {SBranch[]} */ - getChildBranches() { + getChildBranches(): SBranch[] { return this.children.map(childNote => this.shaca.getBranchFromChildAndParent(childNote.noteId, this.noteId)); } - /** @returns {SBranch[]} */ getVisibleChildBranches() { return this.getChildBranches() .filter(branch => !branch.isHidden && !branch.getNote().isLabelTruthy('shareHiddenFromTree')); } - /** @returns {SNote[]} */ getParentNotes() { return this.parents; } - /** @returns {SNote[]} */ getChildNotes() { return this.children; } - /** @returns {SNote[]} */ getVisibleChildNotes() { return this.getVisibleChildBranches() .map(branch => branch.getNote()); } - /** @returns {boolean} */ hasChildren() { return this.children && this.children.length > 0; } - /** @returns {boolean} */ hasVisibleChildren() { return this.getVisibleChildNotes().length > 0; } getContent(silentNotFoundError = false) { - const row = sql.getRow(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]); + const row = sql.getRow>(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]); if (!row) { if (silentNotFoundError) { @@ -125,43 +120,41 @@ class SNote extends AbstractShacaEntity { } } - /** @returns {boolean} true if the note has string content (not binary) */ + /** @returns true if the note has string content (not binary) */ hasStringContent() { return utils.isStringNote(this.type, this.mime); } /** - * @param {string} [type] - (optional) attribute type to filter - * @param {string} [name] - (optional) attribute name to filter - * @returns {SAttribute[]} all note's attributes, including inherited ones + * @param type - (optional) attribute type to filter + * @param name - (optional) attribute name to filter + * @returns all note's attributes, including inherited ones */ - getAttributes(type, name) { - if (!this.__attributeCache) { - this.__getAttributes([]); + getAttributes(type?: string, name?: string) { + let attributeCache = this.__attributeCache; + if (!attributeCache) { + attributeCache = this.__getAttributes([]); } if (type && name) { - return this.__attributeCache.filter(attr => attr.type === type && attr.name === name && !isCredentials(attr)); + return attributeCache.filter(attr => attr.type === type && attr.name === name && !isCredentials(attr)); } else if (type) { - return this.__attributeCache.filter(attr => attr.type === type && !isCredentials(attr)); + return attributeCache.filter(attr => attr.type === type && !isCredentials(attr)); } else if (name) { - return this.__attributeCache.filter(attr => attr.name === name && !isCredentials(attr)); + return attributeCache.filter(attr => attr.name === name && !isCredentials(attr)); } else { - return this.__attributeCache.filter(attr => !isCredentials(attr)); + return attributeCache.filter(attr => !isCredentials(attr)); } } - /** @returns {SAttribute[]} */ getCredentials() { - this.__getAttributes([]); - - return this.__attributeCache.filter(isCredentials); + return this.__getAttributes([]).filter(isCredentials); } - __getAttributes(path) { + __getAttributes(path: string[]) { if (path.includes(this.noteId)) { return []; } @@ -176,7 +169,7 @@ class SNote extends AbstractShacaEntity { } } - const templateAttributes = []; + const templateAttributes: SAttribute[] = []; for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates if (ownedAttr.type === 'relation' && ['template', 'inherit'].includes(ownedAttr.name)) { @@ -212,8 +205,7 @@ class SNote extends AbstractShacaEntity { return this.__attributeCache; } - /** @returns {SAttribute[]} */ - __getInheritableAttributes(path) { + __getInheritableAttributes(path: string[]) { if (path.includes(this.noteId)) { return []; } @@ -222,204 +214,225 @@ class SNote extends AbstractShacaEntity { this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache } - return this.__inheritableAttributeCache; + return this.__inheritableAttributeCache || []; } - /** @returns {boolean} */ - hasAttribute(type, name) { + /** + * @throws Error in case of invalid JSON + */ + getJsonContent(): any | null { + const content = this.getContent(); + + if (typeof content !== "string" || !content || !content.trim()) { + return null; + } + + return JSON.parse(content); + } + + /** @returns valid object or null if the content cannot be parsed as JSON */ + getJsonContentSafely() { + try { + return this.getJsonContent(); + } + catch (e) { + return null; + } + } + + hasAttribute(type: string, name: string) { return !!this.getAttributes().find(attr => attr.type === type && attr.name === name); } - /** @returns {SNote|null} */ - getRelationTarget(name) { + getRelationTarget(name: string) { const relation = this.getAttributes().find(attr => attr.type === 'relation' && attr.name === name); return relation ? relation.targetNote : null; } /** - * @param {string} name - label name - * @returns {boolean} true if label exists (including inherited) + * @param name - label name + * @returns true if label exists (including inherited) */ - hasLabel(name) { return this.hasAttribute(LABEL, name); } + hasLabel(name: string) { return this.hasAttribute(LABEL, name); } /** - * @param {string} name - label name - * @returns {boolean} true if label exists (including inherited) and does not have "false" value. + * @param name - label name + * @returns true if label exists (including inherited) and does not have "false" value. */ - isLabelTruthy(name) { + isLabelTruthy(name: string) { const label = this.getLabel(name); if (!label) { return false; } - return label && label.value !== 'false'; + return !!label && label.value !== 'false'; } /** - * @param {string} name - label name - * @returns {boolean} true if label exists (excluding inherited) + * @param name - label name + * @returns true if label exists (excluding inherited) */ - hasOwnedLabel(name) { return this.hasOwnedAttribute(LABEL, name); } + hasOwnedLabel(name: string) { return this.hasOwnedAttribute(LABEL, name); } /** - * @param {string} name - relation name - * @returns {boolean} true if relation exists (including inherited) + * @param name - relation name + * @returns true if relation exists (including inherited) */ - hasRelation(name) { return this.hasAttribute(RELATION, name); } + hasRelation(name: string) { return this.hasAttribute(RELATION, name); } /** - * @param {string} name - relation name - * @returns {boolean} true if relation exists (excluding inherited) + * @param name - relation name + * @returns true if relation exists (excluding inherited) */ - hasOwnedRelation(name) { return this.hasOwnedAttribute(RELATION, name); } + hasOwnedRelation(name: string) { return this.hasOwnedAttribute(RELATION, name); } /** - * @param {string} name - label name - * @returns {SAttribute|null} label if it exists, null otherwise + * @param name - label name + * @returns label if it exists, null otherwise */ - getLabel(name) { return this.getAttribute(LABEL, name); } + getLabel(name: string) { return this.getAttribute(LABEL, name); } /** - * @param {string} name - label name - * @returns {SAttribute|null} label if it exists, null otherwise + * @param name - label name + * @returns label if it exists, null otherwise */ - getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); } + getOwnedLabel(name: string) { return this.getOwnedAttribute(LABEL, name); } /** - * @param {string} name - relation name - * @returns {SAttribute|null} relation if it exists, null otherwise + * @param name - relation name + * @returns relation if it exists, null otherwise */ - getRelation(name) { return this.getAttribute(RELATION, name); } + getRelation(name: string) { return this.getAttribute(RELATION, name); } /** - * @param {string} name - relation name - * @returns {SAttribute|null} relation if it exists, null otherwise + * @param name - relation name + * @returns relation if it exists, null otherwise */ - getOwnedRelation(name) { return this.getOwnedAttribute(RELATION, name); } + getOwnedRelation(name: string) { return this.getOwnedAttribute(RELATION, name); } /** - * @param {string} name - label name - * @returns {string|null} label value if label exists, null otherwise + * @param name - label name + * @returns label value if label exists, null otherwise */ - getLabelValue(name) { return this.getAttributeValue(LABEL, name); } + getLabelValue(name: string) { return this.getAttributeValue(LABEL, name); } /** - * @param {string} name - label name - * @returns {string|null} label value if label exists, null otherwise + * @param name - label name + * @returns label value if label exists, null otherwise */ - getOwnedLabelValue(name) { return this.getOwnedAttributeValue(LABEL, name); } + getOwnedLabelValue(name: string) { return this.getOwnedAttributeValue(LABEL, name); } /** - * @param {string} name - relation name - * @returns {string|null} relation value if relation exists, null otherwise + * @param name - relation name + * @returns relation value if relation exists, null otherwise */ - getRelationValue(name) { return this.getAttributeValue(RELATION, name); } + getRelationValue(name: string) { return this.getAttributeValue(RELATION, name); } /** - * @param {string} name - relation name - * @returns {string|null} relation value if relation exists, null otherwise + * @param name - relation name + * @returns relation value if relation exists, null otherwise */ - getOwnedRelationValue(name) { return this.getOwnedAttributeValue(RELATION, name); } + getOwnedRelationValue(name: string) { return this.getOwnedAttributeValue(RELATION, name); } /** - * @param {string} type - attribute type (label, relation, etc.) - * @param {string} name - attribute name - * @returns {boolean} true if note has an attribute with given type and name (excluding inherited) + * @param type - attribute type (label, relation, etc.) + * @param name - attribute name + * @returns true if note has an attribute with given type and name (excluding inherited) */ - hasOwnedAttribute(type, name) { + hasOwnedAttribute(type: string, name: string) { return !!this.getOwnedAttribute(type, name); } /** - * @param {string} type - attribute type (label, relation, etc.) - * @param {string} name - attribute name - * @returns {SAttribute} attribute of the given type and name. If there are more such attributes, first is returned. - * Returns null if there's no such attribute belonging to this note. + * @param type - attribute type (label, relation, etc.) + * @param name - attribute name + * @returns attribute of the given type and name. If there are more such attributes, first is returned. + * Returns null if there's no such attribute belonging to this note. */ - getAttribute(type, name) { + getAttribute(type: string, name: string) { const attributes = this.getAttributes(); return attributes.find(attr => attr.type === type && attr.name === name); } /** - * @param {string} type - attribute type (label, relation, etc.) - * @param {string} name - attribute name - * @returns {string|null} attribute value of the given type and name or null if no such attribute exists. + * @param type - attribute type (label, relation, etc.) + * @param name - attribute name + * @returns attribute value of the given type and name or null if no such attribute exists. */ - getAttributeValue(type, name) { + getAttributeValue(type: string, name: string) { const attr = this.getAttribute(type, name); return attr ? attr.value : null; } /** - * @param {string} type - attribute type (label, relation, etc.) - * @param {string} name - attribute name - * @returns {string|null} attribute value of the given type and name or null if no such attribute exists. + * @param type - attribute type (label, relation, etc.) + * @param name - attribute name + * @returns attribute value of the given type and name or null if no such attribute exists. */ - getOwnedAttributeValue(type, name) { + getOwnedAttributeValue(type: string, name: string) { const attr = this.getOwnedAttribute(type, name); - return attr ? attr.value : null; + return attr ? attr.value as string : null; // FIXME } /** - * @param {string} [name] - label name to filter - * @returns {SAttribute[]} all note's labels (attributes with type label), including inherited ones + * @param name - label name to filter + * @returns all note's labels (attributes with type label), including inherited ones */ - getLabels(name) { + getLabels(name: string) { return this.getAttributes(LABEL, name); } /** - * @param {string} [name] - label name to filter - * @returns {string[]} all note's label values, including inherited ones + * @param name - label name to filter + * @returns all note's label values, including inherited ones */ - getLabelValues(name) { - return this.getLabels(name).map(l => l.value); + getLabelValues(name: string) { + return this.getLabels(name).map(l => l.value) as string[]; // FIXME } /** - * @param {string} [name] - label name to filter - * @returns {SAttribute[]} all note's labels (attributes with type label), excluding inherited ones + * @param name - label name to filter + * @returns all note's labels (attributes with type label), excluding inherited ones */ - getOwnedLabels(name) { + getOwnedLabels(name: string) { return this.getOwnedAttributes(LABEL, name); } /** - * @param {string} [name] - label name to filter - * @returns {string[]} all note's label values, excluding inherited ones + * @param name - label name to filter + * @returns all note's label values, excluding inherited ones */ - getOwnedLabelValues(name) { + getOwnedLabelValues(name: string) { return this.getOwnedAttributes(LABEL, name).map(l => l.value); } /** - * @param {string} [name] - relation name to filter - * @returns {SAttribute[]} all note's relations (attributes with type relation), including inherited ones + * @param name - relation name to filter + * @returns all note's relations (attributes with type relation), including inherited ones */ - getRelations(name) { + getRelations(name: string) { return this.getAttributes(RELATION, name); } /** - * @param {string} [name] - relation name to filter - * @returns {SAttribute[]} all note's relations (attributes with type relation), excluding inherited ones + * @param name - relation name to filter + * @returns all note's relations (attributes with type relation), excluding inherited ones */ - getOwnedRelations(name) { + getOwnedRelations(name: string) { return this.getOwnedAttributes(RELATION, name); } /** - * @param {string} [type] - (optional) attribute type to filter - * @param {string} [name] - (optional) attribute name to filter - * @returns {SAttribute[]} note's "owned" attributes - excluding inherited ones + * @param type - (optional) attribute type to filter + * @param name - (optional) attribute name to filter + * @returns note's "owned" attributes - excluding inherited ones */ - getOwnedAttributes(type, name) { + getOwnedAttributes(type: string, name: string) { // it's a common mistake to include # or ~ into attribute name if (name && ["#", "~"].includes(name[0])) { name = name.substr(1); @@ -440,42 +453,36 @@ class SNote extends AbstractShacaEntity { } /** - * @returns {SAttribute} attribute belonging to this specific note (excludes inherited attributes) + * @returns attribute belonging to this specific note (excludes inherited attributes) * * This method can be significantly faster than the getAttribute() */ - getOwnedAttribute(type, name) { + getOwnedAttribute(type: string, name: string) { const attrs = this.getOwnedAttributes(type, name); return attrs.length > 0 ? attrs[0] : null; } - /** @returns {boolean} */ get isArchived() { return this.hasAttribute('label', 'archived'); } - /** @returns {boolean} */ isInherited() { return !!this.targetRelations.find(rel => rel.name === 'template' || rel.name === 'inherit'); } - /** @returns {SAttribute[]} */ getTargetRelations() { return this.targetRelations; } - /** @returns {SAttachment[]} */ getAttachments() { return this.attachments; } - /** @returns {SAttachment} */ - getAttachmentByTitle(title) { + getAttachmentByTitle(title: string) { return this.attachments.find(attachment => attachment.title === title); } - /** @returns {string} */ get shareId() { if (this.hasOwnedLabel('shareRoot')) { return ""; @@ -514,4 +521,4 @@ class SNote extends AbstractShacaEntity { } } -module.exports = SNote; +export = SNote; diff --git a/src/share/shaca/shaca.js b/src/share/shaca/shaca-interface.ts similarity index 56% rename from src/share/shaca/shaca.js rename to src/share/shaca/shaca-interface.ts index 4e1c3a1ba..9cff96eeb 100644 --- a/src/share/shaca/shaca.js +++ b/src/share/shaca/shaca-interface.ts @@ -1,45 +1,49 @@ -"use strict"; +import SAttachment = require("./entities/sattachment"); +import SAttribute = require("./entities/sattribute"); +import SBranch = require("./entities/sbranch"); +import SNote = require("./entities/snote"); + +export default class Shaca { + + notes!: Record; + branches!: Record; + childParentToBranch!: Record; + private attributes!: Record; + attachments!: Record; + aliasToNote!: Record; + shareRootNote!: SNote | null; + /** true if the index of all shared subtrees is enabled */ + shareIndexEnabled!: boolean; + loaded!: boolean; -class Shaca { constructor() { this.reset(); } reset() { - /** @type {Object.} */ this.notes = {}; - /** @type {Object.} */ this.branches = {}; - /** @type {Object.} */ this.childParentToBranch = {}; - /** @type {Object.} */ this.attributes = {}; - /** @type {Object.} */ this.attachments = {}; - /** @type {Object.} */ this.aliasToNote = {}; - /** @type {SNote|null} */ this.shareRootNote = null; - /** @type {boolean} true if the index of all shared subtrees is enabled */ this.shareIndexEnabled = false; this.loaded = false; } - /** @returns {SNote|null} */ - getNote(noteId) { + getNote(noteId: string) { return this.notes[noteId]; } - /** @returns {boolean} */ - hasNote(noteId) { + hasNote(noteId: string) { return noteId in this.notes; } - /** @returns {SNote[]} */ - getNotes(noteIds, ignoreMissing = false) { + getNotes(noteIds: string[], ignoreMissing = false) { const filteredNotes = []; for (const noteId of noteIds) { @@ -59,27 +63,23 @@ class Shaca { return filteredNotes; } - /** @returns {SBranch|null} */ - getBranch(branchId) { + getBranch(branchId: string) { return this.branches[branchId]; } - /** @returns {SBranch|null} */ - getBranchFromChildAndParent(childNoteId, parentNoteId) { + getBranchFromChildAndParent(childNoteId: string, parentNoteId: string) { return this.childParentToBranch[`${childNoteId}-${parentNoteId}`]; } - /** @returns {SAttribute|null} */ - getAttribute(attributeId) { + getAttribute(attributeId: string) { return this.attributes[attributeId]; } - /** @returns {SAttachment|null} */ - getAttachment(attachmentId) { + getAttachment(attachmentId: string) { return this.attachments[attachmentId]; } - getEntity(entityName, entityId) { + getEntity(entityName: string, entityId: string) { if (!entityName || !entityId) { return null; } @@ -91,10 +91,6 @@ class Shaca { .replace('_', '') ); - return this[camelCaseEntityName][entityId]; + return (this as any)[camelCaseEntityName][entityId]; } -} - -const shaca = new Shaca(); - -module.exports = shaca; +} \ No newline at end of file diff --git a/src/share/shaca/shaca.ts b/src/share/shaca/shaca.ts new file mode 100644 index 000000000..d256b17e1 --- /dev/null +++ b/src/share/shaca/shaca.ts @@ -0,0 +1,7 @@ +"use strict"; + +import Shaca from "./shaca-interface"; + +const shaca = new Shaca(); + +export = shaca; diff --git a/src/share/shaca/shaca_loader.js b/src/share/shaca/shaca_loader.ts similarity index 69% rename from src/share/shaca/shaca_loader.js rename to src/share/shaca/shaca_loader.ts index 5d1648f49..c6a3fc4da 100644 --- a/src/share/shaca/shaca_loader.js +++ b/src/share/shaca/shaca_loader.ts @@ -1,14 +1,14 @@ "use strict"; -const sql = require('../sql'); -const shaca = require('./shaca.js'); -const log = require('../../services/log'); -const SNote = require('./entities/snote.js'); -const SBranch = require('./entities/sbranch.js'); -const SAttribute = require('./entities/sattribute.js'); -const SAttachment = require('./entities/sattachment.js'); -const shareRoot = require('../share_root.js'); -const eventService = require('../../services/events'); +import sql = require('../sql'); +import shaca = require('./shaca'); +import log = require('../../services/log'); +import SNote = require('./entities/snote'); +import SBranch = require('./entities/sbranch'); +import SAttribute = require('./entities/sattribute'); +import SAttachment = require('./entities/sattachment'); +import shareRoot = require('../share_root'); +import eventService = require('../../services/events'); function load() { const start = Date.now(); @@ -35,7 +35,7 @@ function load() { const noteIdStr = noteIds.map(noteId => `'${noteId}'`).join(","); - const rawNoteRows = sql.getRawRows(` + const rawNoteRows = sql.getRawRows(` SELECT noteId, title, type, mime, blobId, utcDateModified, isProtected FROM notes WHERE isDeleted = 0 @@ -45,7 +45,7 @@ function load() { new SNote(row); } - const rawBranchRows = sql.getRawRows(` + const rawBranchRows = sql.getRawRows(` SELECT branchId, noteId, parentNoteId, prefix, isExpanded, utcDateModified FROM branches WHERE isDeleted = 0 @@ -56,7 +56,7 @@ function load() { new SBranch(row); } - const rawAttributeRows = sql.getRawRows(` + const rawAttributeRows = sql.getRawRows(` SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified FROM attributes WHERE isDeleted = 0 @@ -66,14 +66,12 @@ function load() { new SAttribute(row); } - const rawAttachmentRows = sql.getRawRows(` + const rawAttachmentRows = sql.getRawRows(` SELECT attachmentId, ownerId, role, mime, title, blobId, utcDateModified FROM attachments WHERE isDeleted = 0 AND ownerId IN (${noteIdStr})`); - rawAttachmentRows.sort((a, b) => a.position < b.position ? -1 : 1); - for (const row of rawAttachmentRows) { new SAttachment(row); } @@ -89,11 +87,11 @@ function ensureLoad() { } } -eventService.subscribe([ eventService.ENTITY_CREATED, eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED, eventService.ENTITY_CHANGE_SYNCED, eventService.ENTITY_DELETE_SYNCED ], ({ entityName, entity }) => { +eventService.subscribe([eventService.ENTITY_CREATED, eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED, eventService.ENTITY_CHANGE_SYNCED, eventService.ENTITY_DELETE_SYNCED], ({ entityName, entity }) => { shaca.reset(); }); -module.exports = { +export = { load, ensureLoad }; diff --git a/src/share/share_root.js b/src/share/share_root.ts similarity index 64% rename from src/share/share_root.js rename to src/share/share_root.ts index ec63597f2..a500d5ac4 100644 --- a/src/share/share_root.js +++ b/src/share/share_root.ts @@ -1,3 +1,3 @@ -module.exports = { +export = { SHARE_ROOT_NOTE_ID: '_share' } diff --git a/src/share/sql.js b/src/share/sql.ts similarity index 51% rename from src/share/sql.js rename to src/share/sql.ts index 485c87921..a5465f4c2 100644 --- a/src/share/sql.js +++ b/src/share/sql.ts @@ -1,7 +1,7 @@ "use strict"; -const Database = require('better-sqlite3'); -const dataDir = require('../services/data_dir'); +import Database = require('better-sqlite3'); +import dataDir = require('../services/data_dir'); const dbConnection = new Database(dataDir.DOCUMENT_PATH, { readonly: true }); @@ -15,19 +15,19 @@ const dbConnection = new Database(dataDir.DOCUMENT_PATH, { readonly: true }); }); }); -function getRawRows(query, params = []) { - return dbConnection.prepare(query).raw().all(params); +function getRawRows(query: string, params = []): T[] { + return dbConnection.prepare(query).raw().all(params) as T[]; } -function getRow(query, params = []) { - return dbConnection.prepare(query).get(params); +function getRow(query: string, params: string[] = []): T { + return dbConnection.prepare(query).get(params) as T; } -function getColumn(query, params = []) { - return dbConnection.prepare(query).pluck().all(params); +function getColumn(query: string, params: string[] = []): T[] { + return dbConnection.prepare(query).pluck().all(params) as T[]; } -module.exports = { +export = { getRawRows, getRow, getColumn