mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	Merge pull request #47 from TriliumNext/feature/typescript_backend_10
Convert backend to TypeScript (84% -> 89%)
This commit is contained in:
		
							
								
								
									
										26
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										26
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -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", | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -125,9 +125,6 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @returns {BNote|null} | ||||
|      */ | ||||
|     getNote() { | ||||
|         const note = this.becca.getNote(this.noteId); | ||||
|  | ||||
| @@ -138,9 +135,6 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> { | ||||
|         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<BAttribute> { | ||||
|         return this.becca.getNote(this.value); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @returns {boolean} | ||||
|      */ | ||||
|     isDefinition() { | ||||
|         return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:')); | ||||
|     } | ||||
|   | ||||
| @@ -127,8 +127,6 @@ class BBranch extends AbstractBeccaEntity<BBranch> { | ||||
|      * 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); | ||||
|   | ||||
| @@ -167,39 +167,32 @@ class BNote extends AbstractBeccaEntity<BNote> { | ||||
|         return this.isContentAvailable() ? this.title : '[protected]'; | ||||
|     } | ||||
|  | ||||
|     /** @returns {BBranch[]} */ | ||||
|     getParentBranches() { | ||||
|         return this.parentBranches; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns <i>strong</i> (as opposed to <i>weak</i>) 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<BNote> { | ||||
|             .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<BNote> { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @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<BNote> { | ||||
|         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<BNote> { | ||||
|         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<BNote> { | ||||
|                 || 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<BNote> { | ||||
|         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<BNote> { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @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<BNote> { | ||||
|      * @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<BNote> { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @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<BNote> { | ||||
|      * - 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<BNote> { | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** @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<BNote> { | ||||
|         return this.getOwnedAttributes().length; | ||||
|     } | ||||
|  | ||||
|     /** @returns {BNote[]} */ | ||||
|     getAncestors() { | ||||
|         if (!this.__ancestorCache) { | ||||
|             const noteIds = new Set(); | ||||
| @@ -1075,7 +1068,6 @@ class BNote extends AbstractBeccaEntity<BNote> { | ||||
|         return this.noteId === '_hidden' || this.hasAncestor('_hidden'); | ||||
|     } | ||||
|  | ||||
|     /** @returns {BAttribute[]} */ | ||||
|     getTargetRelations() { | ||||
|         return this.targetRelations; | ||||
|     } | ||||
| @@ -1117,7 +1109,6 @@ class BNote extends AbstractBeccaEntity<BNote> { | ||||
|             .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<BNote> { | ||||
|             .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<BNote> { | ||||
|         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<BNote> { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {string} matchBy - choose by which property we detect if to update an existing attachment. | ||||
|      * @param matchBy - choose by which property we detect if to update an existing attachment. | ||||
|  *                      Supported values are either 'attachmentId' (default) or 'title' | ||||
|      * @returns {BAttachment} | ||||
|      */ | ||||
|     saveAttachment({attachmentId, role, mime, title, content, position}: AttachmentRow, matchBy = 'attachmentId') { | ||||
|         if (!['attachmentId', 'title'].includes(matchBy)) { | ||||
|   | ||||
| @@ -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'); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -17,8 +17,7 @@ type EventListener = (data: any) => void; | ||||
| const eventListeners: Record<string, EventListener[]> = {}; | ||||
|  | ||||
| /** | ||||
|  * @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)) { | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
| @@ -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 += '<ul id="index">'; | ||||
| 
 | ||||
|     const rootNote = shaca.getNote(shareRoot.SHARE_ROOT_NOTE_ID); | ||||
| @@ -53,10 +60,10 @@ function renderIndex(result) { | ||||
|     result.content += '</ul>'; | ||||
| } | ||||
| 
 | ||||
| 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}`; | ||||
|                     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 = ` | ||||
| <img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}"> | ||||
| <hr> | ||||
| @@ -145,11 +158,11 @@ function renderMermaid(result, note) { | ||||
| </details>` | ||||
| } | ||||
| 
 | ||||
| function renderImage(result, note) { | ||||
| function renderImage(result: Result, note: SNote) { | ||||
|     result.content = `<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">`; | ||||
| } | ||||
| 
 | ||||
| function renderFile(note, result) { | ||||
| function renderFile(note: SNote, result: Result) { | ||||
|     if (note.mime === 'application/pdf') { | ||||
|         result.content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view"></iframe>` | ||||
|     } else { | ||||
| @@ -157,6 +170,6 @@ function renderFile(note, result) { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
| export = { | ||||
|     getContent | ||||
| }; | ||||
| @@ -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 = '<svg/>' | ||||
|     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; | ||||
| @@ -160,27 +161,34 @@ function register(router) { | ||||
|         // 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}); | ||||
|                     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,6 +207,11 @@ function register(router) { | ||||
| 
 | ||||
|         shacaLoader.ensureLoad(); | ||||
| 
 | ||||
|         if (!shaca.shareRootNote) { | ||||
|             return res.status(404) | ||||
|                 .json({ message: "Share root note not found" }); | ||||
|         } | ||||
| 
 | ||||
|         renderNote(shaca.shareRootNote, req, res); | ||||
|     }); | ||||
| 
 | ||||
| @@ -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,6 +354,10 @@ 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; | ||||
| @@ -348,7 +365,7 @@ function register(router) { | ||||
| 
 | ||||
|         const { search } = req.query; | ||||
| 
 | ||||
|         if (!search?.trim()) { | ||||
|         if (typeof search !== "string" || !search?.trim()) { | ||||
|             return res.status(400).json({ message: "'search' parameter is mandatory." }); | ||||
|         } | ||||
| 
 | ||||
| @@ -366,6 +383,6 @@ function register(router) { | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
| export = { | ||||
|     register | ||||
| } | ||||
| @@ -1,14 +0,0 @@ | ||||
| let shaca; | ||||
|  | ||||
| class AbstractShacaEntity { | ||||
|     /** @return {Shaca} */ | ||||
|     get shaca() { | ||||
|         if (!shaca) { | ||||
|             shaca = require('../shaca.js'); | ||||
|         } | ||||
|  | ||||
|         return shaca; | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = AbstractShacaEntity; | ||||
							
								
								
									
										15
									
								
								src/share/shaca/entities/abstract_shaca_entity.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/share/shaca/entities/abstract_shaca_entity.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
							
								
								
									
										4
									
								
								src/share/shaca/entities/rows.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/share/shaca/entities/rows.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 ]; | ||||
| @@ -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<Pick<Blob, "content">>(`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; | ||||
| @@ -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; | ||||
| @@ -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; | ||||
| @@ -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<Pick<Blob, "content">>(`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. | ||||
|      * @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; | ||||
| @@ -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<string, SNote>; | ||||
|     branches!: Record<string, SBranch>; | ||||
|     childParentToBranch!: Record<string, SBranch>; | ||||
|     private attributes!: Record<string, SAttribute>; | ||||
|     attachments!: Record<string, SAttachment>; | ||||
|     aliasToNote!: Record<string, SNote>; | ||||
|     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.<String, SNote>} */ | ||||
|         this.notes = {}; | ||||
|         /** @type {Object.<String, SBranch>} */ | ||||
|         this.branches = {}; | ||||
|         /** @type {Object.<String, SBranch>} */ | ||||
|         this.childParentToBranch = {}; | ||||
|         /** @type {Object.<String, SAttribute>} */ | ||||
|         this.attributes = {}; | ||||
|         /** @type {Object.<String, SAttachment>} */ | ||||
|         this.attachments = {}; | ||||
|         /** @type {Object.<String, SNote>} */ | ||||
|         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; | ||||
							
								
								
									
										7
									
								
								src/share/shaca/shaca.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/share/shaca/shaca.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| "use strict"; | ||||
|  | ||||
| import Shaca from "./shaca-interface"; | ||||
|  | ||||
| const shaca = new Shaca(); | ||||
|  | ||||
| export = shaca; | ||||
| @@ -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<SNoteRow>(` | ||||
|         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<SBranchRow>(` | ||||
|         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<SAttributeRow>(` | ||||
|         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<SAttachmentRow>(` | ||||
|         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); | ||||
|     } | ||||
| @@ -93,7 +91,7 @@ eventService.subscribe([ eventService.ENTITY_CREATED, eventService.ENTITY_CHANGE | ||||
|     shaca.reset(); | ||||
| }); | ||||
| 
 | ||||
| module.exports = { | ||||
| export = { | ||||
|     load, | ||||
|     ensureLoad | ||||
| }; | ||||
| @@ -1,3 +1,3 @@ | ||||
| module.exports = { | ||||
| export = { | ||||
|     SHARE_ROOT_NOTE_ID: '_share' | ||||
| } | ||||
| @@ -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<T>(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<T>(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<T>(query: string, params: string[] = []): T[] { | ||||
|     return dbConnection.prepare(query).pluck().all(params) as T[]; | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
| export = { | ||||
|     getRawRows, | ||||
|     getRow, | ||||
|     getColumn | ||||
		Reference in New Issue
	
	Block a user