Merge pull request #47 from TriliumNext/feature/typescript_backend_10

Convert backend to TypeScript (84% -> 89%)
This commit is contained in:
Elian Doran 2024-04-20 09:36:08 +03:00 committed by GitHub
commit 2771bd4ece
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 458 additions and 415 deletions

26
package-lock.json generated
View File

@ -92,6 +92,7 @@
"@types/better-sqlite3": "^7.6.9", "@types/better-sqlite3": "^7.6.9",
"@types/cls-hooked": "^4.3.8", "@types/cls-hooked": "^4.3.8",
"@types/csurf": "^1.11.5", "@types/csurf": "^1.11.5",
"@types/ejs": "^3.1.5",
"@types/escape-html": "^1.0.4", "@types/escape-html": "^1.0.4",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/express-session": "^1.18.0", "@types/express-session": "^1.18.0",
@ -101,6 +102,7 @@
"@types/mime-types": "^2.1.4", "@types/mime-types": "^2.1.4",
"@types/multer": "^1.4.11", "@types/multer": "^1.4.11",
"@types/node": "^20.11.19", "@types/node": "^20.11.19",
"@types/safe-compare": "^1.1.2",
"@types/sanitize-html": "^2.11.0", "@types/sanitize-html": "^2.11.0",
"@types/sax": "^1.2.7", "@types/sax": "^1.2.7",
"@types/stream-throttle": "^0.1.4", "@types/stream-throttle": "^0.1.4",
@ -1271,6 +1273,12 @@
"@types/ms": "*" "@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": { "node_modules/@types/escape-html": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz",
@ -1537,6 +1545,12 @@
"@types/node": "*" "@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": { "node_modules/@types/sanitize-html": {
"version": "2.11.0", "version": "2.11.0",
"resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz", "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz",
@ -14276,6 +14290,12 @@
"@types/ms": "*" "@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": { "@types/escape-html": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz",
@ -14535,6 +14555,12 @@
"@types/node": "*" "@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": { "@types/sanitize-html": {
"version": "2.11.0", "version": "2.11.0",
"resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz", "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz",

View File

@ -113,6 +113,7 @@
"@types/better-sqlite3": "^7.6.9", "@types/better-sqlite3": "^7.6.9",
"@types/cls-hooked": "^4.3.8", "@types/cls-hooked": "^4.3.8",
"@types/csurf": "^1.11.5", "@types/csurf": "^1.11.5",
"@types/ejs": "^3.1.5",
"@types/escape-html": "^1.0.4", "@types/escape-html": "^1.0.4",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/express-session": "^1.18.0", "@types/express-session": "^1.18.0",
@ -122,6 +123,7 @@
"@types/mime-types": "^2.1.4", "@types/mime-types": "^2.1.4",
"@types/multer": "^1.4.11", "@types/multer": "^1.4.11",
"@types/node": "^20.11.19", "@types/node": "^20.11.19",
"@types/safe-compare": "^1.1.2",
"@types/sanitize-html": "^2.11.0", "@types/sanitize-html": "^2.11.0",
"@types/sax": "^1.2.7", "@types/sax": "^1.2.7",
"@types/stream-throttle": "^0.1.4", "@types/stream-throttle": "^0.1.4",

View File

@ -125,9 +125,6 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
} }
} }
/**
* @returns {BNote|null}
*/
getNote() { getNote() {
const note = this.becca.getNote(this.noteId); const note = this.becca.getNote(this.noteId);
@ -138,9 +135,6 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
return note; return note;
} }
/**
* @returns {BNote|null}
*/
getTargetNote() { getTargetNote() {
if (this.type !== 'relation') { if (this.type !== 'relation') {
throw new Error(`Attribute '${this.attributeId}' is not a 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); return this.becca.getNote(this.value);
} }
/**
* @returns {boolean}
*/
isDefinition() { isDefinition() {
return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:')); return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:'));
} }

View File

@ -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, * 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 * 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. * of deletion should not act as a clone.
*
* @returns {boolean}
*/ */
get isWeak() { get isWeak() {
return ['_share', '_lbBookmarks'].includes(this.parentNoteId); return ['_share', '_lbBookmarks'].includes(this.parentNoteId);

View File

@ -167,39 +167,32 @@ class BNote extends AbstractBeccaEntity<BNote> {
return this.isContentAvailable() ? this.title : '[protected]'; return this.isContentAvailable() ? this.title : '[protected]';
} }
/** @returns {BBranch[]} */
getParentBranches() { getParentBranches() {
return this.parentBranches; return this.parentBranches;
} }
/** /**
* Returns <i>strong</i> (as opposed to <i>weak</i>) parent branches. See isWeak for details. * Returns <i>strong</i> (as opposed to <i>weak</i>) parent branches. See isWeak for details.
*
* @returns {BBranch[]}
*/ */
getStrongParentBranches() { getStrongParentBranches() {
return this.getParentBranches().filter(branch => !branch.isWeak); return this.getParentBranches().filter(branch => !branch.isWeak);
} }
/** /**
* @returns {BBranch[]}
* @deprecated use getParentBranches() instead * @deprecated use getParentBranches() instead
*/ */
getBranches() { getBranches() {
return this.parentBranches; return this.parentBranches;
} }
/** @returns {BNote[]} */
getParentNotes() { getParentNotes() {
return this.parents; return this.parents;
} }
/** @returns {BNote[]} */
getChildNotes() { getChildNotes() {
return this.children; return this.children;
} }
/** @returns {boolean} */
hasChildren() { hasChildren() {
return this.children && this.children.length > 0; 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[]; .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 * 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: * 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 { getJsonContent(): any | null {
const content = this.getContent(); const content = this.getContent();
@ -233,7 +227,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
return JSON.parse(content); 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() { getJsonContentSafely() {
try { try {
return this.getJsonContent(); return this.getJsonContent();
@ -269,17 +263,17 @@ class BNote extends AbstractBeccaEntity<BNote> {
return this.utcDateModified === null ? null : dayjs.utc(this.utcDateModified); 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() { isRoot() {
return this.noteId === 'root'; 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() { isJson() {
return this.mime === "application/json"; 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() { isJavaScript() {
return (this.type === "code" || this.type === "file" || this.type === 'launcher') return (this.type === "code" || this.type === "file" || this.type === 'launcher')
&& (this.mime.startsWith("application/javascript") && (this.mime.startsWith("application/javascript")
@ -287,13 +281,13 @@ class BNote extends AbstractBeccaEntity<BNote> {
|| this.mime === "text/javascript"); || this.mime === "text/javascript");
} }
/** @returns {boolean} true if this note is HTML */ /** @returns true if this note is HTML */
isHtml() { isHtml() {
return ["code", "file", "render"].includes(this.type) return ["code", "file", "render"].includes(this.type)
&& this.mime === "text/html"; && this.mime === "text/html";
} }
/** @returns {boolean} true if this note is an image */ /** @returns true if this note is an image */
isImage() { isImage() {
return this.type === 'image' return this.type === 'image'
|| (this.type === 'file' && this.mime?.startsWith('image/')); || (this.type === 'file' && this.mime?.startsWith('image/'));
@ -304,12 +298,12 @@ class BNote extends AbstractBeccaEntity<BNote> {
return this.hasStringContent(); 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() { hasStringContent() {
return utils.isStringNote(this.type, this.mime); 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() { getScriptEnv() {
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) { if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
return "frontend"; return "frontend";
@ -518,8 +512,8 @@ class BNote extends AbstractBeccaEntity<BNote> {
} }
/** /**
* @param {string} name - label name * @param name - label name
* @returns {BAttribute|null} label if it exists, null otherwise * @returns label if it exists, null otherwise
*/ */
getLabel(name: string): BAttribute | null { getLabel(name: string): BAttribute | null {
return this.getAttribute(LABEL, name); return this.getAttribute(LABEL, name);
@ -680,7 +674,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
* @param type - (optional) attribute type to filter * @param type - (optional) attribute type to filter
* @param name - (optional) attribute name to filter * @param name - (optional) attribute name to filter
* @param value - (optional) attribute value 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) { getOwnedAttributes(type: string | null = null, name: string | null = null, value: string | null = null) {
this.__validateTypeName(type, name); 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() * This method can be significantly faster than the getAttribute()
*/ */
@ -780,7 +774,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
* - fast searching * - fast searching
* - note similarity evaluation * - 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() { getFlatText() {
if (!this.__flatTextCache) { 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} = {}) { getSubtreeNoteIds({includeArchived = true, includeHidden = false, resolveSearch = false} = {}) {
return this.getSubtree({includeArchived, includeHidden, resolveSearch}) return this.getSubtree({includeArchived, includeHidden, resolveSearch})
.notes .notes
@ -1031,7 +1025,6 @@ class BNote extends AbstractBeccaEntity<BNote> {
return this.getOwnedAttributes().length; return this.getOwnedAttributes().length;
} }
/** @returns {BNote[]} */
getAncestors() { getAncestors() {
if (!this.__ancestorCache) { if (!this.__ancestorCache) {
const noteIds = new Set(); const noteIds = new Set();
@ -1075,7 +1068,6 @@ class BNote extends AbstractBeccaEntity<BNote> {
return this.noteId === '_hidden' || this.hasAncestor('_hidden'); return this.noteId === '_hidden' || this.hasAncestor('_hidden');
} }
/** @returns {BAttribute[]} */
getTargetRelations() { getTargetRelations() {
return this.targetRelations; return this.targetRelations;
} }
@ -1117,7 +1109,6 @@ class BNote extends AbstractBeccaEntity<BNote> {
.map(row => new BRevision(row)); .map(row => new BRevision(row));
} }
/** @returns {BAttachment[]} */
getAttachments(opts: AttachmentOpts = {}) { getAttachments(opts: AttachmentOpts = {}) {
opts.includeContentLength = !!opts.includeContentLength; opts.includeContentLength = !!opts.includeContentLength;
// from testing, it looks like calculating length does not make a difference in performance even on large-ish DB // 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)); .map(row => new BAttachment(row));
} }
/** @returns {BAttachment|null} */
getAttachmentById(attachmentId: string, opts: AttachmentOpts = {}) { getAttachmentById(attachmentId: string, opts: AttachmentOpts = {}) {
opts.includeContentLength = !!opts.includeContentLength; opts.includeContentLength = !!opts.includeContentLength;
@ -1582,10 +1572,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
return !(this.noteId in this.becca.notes) || this.isBeingDeleted; return !(this.noteId in this.becca.notes) || this.isBeingDeleted;
} }
/** saveRevision(): BRevision {
* @returns {BRevision|null}
*/
saveRevision() {
return sql.transactional(() => { return sql.transactional(() => {
let noteContent = this.getContent(); 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' * Supported values are either 'attachmentId' (default) or 'title'
* @returns {BAttachment}
*/ */
saveAttachment({attachmentId, role, mime, title, content, position}: AttachmentRow, matchBy = 'attachmentId') { saveAttachment({attachmentId, role, mime, title, content, position}: AttachmentRow, matchBy = 'attachmentId') {
if (!['attachmentId', 'title'].includes(matchBy)) { if (!['attachmentId', 'title'].includes(matchBy)) {

View File

@ -59,7 +59,7 @@ const fontsRoute = require('./api/fonts');
const etapiTokensApiRoutes = require('./api/etapi_tokens'); const etapiTokensApiRoutes = require('./api/etapi_tokens');
const relationMapApiRoute = require('./api/relation-map'); const relationMapApiRoute = require('./api/relation-map');
const otherRoute = require('./api/other'); const otherRoute = require('./api/other');
const shareRoutes = require('../share/routes.js'); const shareRoutes = require('../share/routes');
const etapiAuthRoutes = require('../etapi/auth'); const etapiAuthRoutes = require('../etapi/auth');
const etapiAppInfoRoutes = require('../etapi/app_info'); const etapiAppInfoRoutes = require('../etapi/app_info');

View File

@ -127,8 +127,8 @@ interface Api {
/** /**
* Retrieves notes with given label name & value * Retrieves notes with given label name & value
* *
* @param name - attribute name * @param name - attribute name
* @param value - attribute value * @param value - attribute value
*/ */
getNotesWithLabel(name: string, value?: string): BNote[]; getNotesWithLabel(name: string, value?: string): BNote[];

View File

@ -68,7 +68,7 @@ class ConsistencyChecks {
childToParents[childNoteId].push(parentNoteId); 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[]) => { const checkTreeCycle = (noteId: string, path: string[]) => {
if (noteId === 'root') { if (noteId === 'root') {
return false; return false;

View File

@ -17,8 +17,7 @@ type EventListener = (data: any) => void;
const eventListeners: Record<string, EventListener[]> = {}; const eventListeners: Record<string, EventListener[]> = {};
/** /**
* @param {string|string[]}eventTypes - can be either single event or an array of events * @param eventTypes - can be either single event or an array of events
* @param listener
*/ */
function subscribe(eventTypes: EventType, listener: EventListener) { function subscribe(eventTypes: EventType, listener: EventListener) {
if (!Array.isArray(eventTypes)) { if (!Array.isArray(eventTypes)) {

View File

@ -323,9 +323,9 @@ export = {
* Get single value from the given query - first column from first returned row. * Get single value from the given query - first column from first returned row.
* *
* @method * @method
* @param {string} query - SQL query with ? used as parameter placeholder * @param query - SQL query with ? used as parameter placeholder
* @param {object[]} [params] - array of params if needed * @param params - array of params if needed
* @returns [object] - single value * @returns single value
*/ */
getValue, getValue,
@ -333,9 +333,9 @@ export = {
* Get first returned row. * Get first returned row.
* *
* @method * @method
* @param {string} query - SQL query with ? used as parameter placeholder * @param query - SQL query with ? used as parameter placeholder
* @param {object[]} [params] - array of params if needed * @param params - array of params if needed
* @returns {object} - map of column name to column value * @returns - map of column name to column value
*/ */
getRow, getRow,
getRowOrNull, getRowOrNull,
@ -344,9 +344,9 @@ export = {
* Get all returned rows. * Get all returned rows.
* *
* @method * @method
* @param {string} query - SQL query with ? used as parameter placeholder * @param query - SQL query with ? used as parameter placeholder
* @param {object[]} [params] - array of params if needed * @param params - array of params if needed
* @returns {object[]} - array of all rows, each row is a map of column name to column value * @returns - array of all rows, each row is a map of column name to column value
*/ */
getRows, getRows,
getRawRows, getRawRows,
@ -357,9 +357,9 @@ export = {
* Get a map of first column mapping to second column. * Get a map of first column mapping to second column.
* *
* @method * @method
* @param {string} query - SQL query with ? used as parameter placeholder * @param query - SQL query with ? used as parameter placeholder
* @param {object[]} [params] - array of params if needed * @param params - array of params if needed
* @returns {object} - map of first column to second column * @returns - map of first column to second column
*/ */
getMap, getMap,
@ -367,9 +367,9 @@ export = {
* Get a first column in an array. * Get a first column in an array.
* *
* @method * @method
* @param {string} query - SQL query with ? used as parameter placeholder * @param query - SQL query with ? used as parameter placeholder
* @param {object[]} [params] - array of params if needed * @param params - array of params if needed
* @returns {object[]} - array of first column of all returned rows * @returns array of first column of all returned rows
*/ */
getColumn, getColumn,
@ -377,8 +377,8 @@ export = {
* Execute SQL * Execute SQL
* *
* @method * @method
* @param {string} query - SQL query with ? used as parameter placeholder * @param query - SQL query with ? used as parameter placeholder
* @param {object[]} [params] - array of params if needed * @param params - array of params if needed
*/ */
execute, execute,
executeMany, executeMany,

View File

@ -156,9 +156,9 @@ const STRING_MIME_TYPES = [
"image/svg+xml" "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 // 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/') || mime.startsWith('text/')
|| STRING_MIME_TYPES.includes(mime); || STRING_MIME_TYPES.includes(mime);
} }

View File

@ -1,10 +1,17 @@
const {JSDOM} = require("jsdom"); import { JSDOM } from "jsdom";
const shaca = require('./shaca/shaca.js'); import shaca = require('./shaca/shaca');
const assetPath = require('../services/asset_path'); import assetPath = require('../services/asset_path');
const shareRoot = require('./share_root.js'); import shareRoot = require('./share_root');
const escapeHtml = require('escape-html'); 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) { if (note.isProtected) {
return { return {
header: '', header: '',
@ -13,7 +20,7 @@ function getContent(note) {
}; };
} }
const result = { const result: Result = {
content: note.getContent(), content: note.getContent(),
header: '', header: '',
isEmpty: false isEmpty: false
@ -38,7 +45,7 @@ function getContent(note) {
return result; return result;
} }
function renderIndex(result) { function renderIndex(result: Result) {
result.content += '<ul id="index">'; result.content += '<ul id="index">';
const rootNote = shaca.getNote(shareRoot.SHARE_ROOT_NOTE_ID); const rootNote = shaca.getNote(shareRoot.SHARE_ROOT_NOTE_ID);
@ -53,10 +60,10 @@ function renderIndex(result) {
result.content += '</ul>'; result.content += '</ul>';
} }
function renderText(result, note) { function renderText(result: Result, note: SNote) {
const document = new JSDOM(result.content || "").window.document; 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; && document.querySelectorAll("img").length === 0;
if (!result.isEmpty) { if (!result.isEmpty) {
@ -89,7 +96,9 @@ function renderText(result, note) {
if (linkedNote) { if (linkedNote) {
const isExternalLink = linkedNote.hasLabel("shareExternalLink"); const isExternalLink = linkedNote.hasLabel("shareExternalLink");
const href = isExternalLink ? linkedNote.getLabelValue("shareExternalLink") : `./${linkedNote.shareId}`; const href = isExternalLink ? linkedNote.getLabelValue("shareExternalLink") : `./${linkedNote.shareId}`;
linkEl.setAttribute("href", href); if (href) {
linkEl.setAttribute("href", href);
}
if (isExternalLink) { if (isExternalLink) {
linkEl.setAttribute("target", "_blank"); linkEl.setAttribute("target", "_blank");
linkEl.setAttribute("rel", "noopener noreferrer"); linkEl.setAttribute("rel", "noopener noreferrer");
@ -122,8 +131,8 @@ document.addEventListener("DOMContentLoaded", function() {
} }
} }
function renderCode(result) { function renderCode(result: Result) {
if (!result.content?.trim()) { if (typeof result.content !== "string" || !result.content?.trim()) {
result.isEmpty = true; result.isEmpty = true;
} else { } else {
const document = new JSDOM().window.document; 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 = ` result.content = `
<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}"> <img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">
<hr> <hr>
@ -145,11 +158,11 @@ function renderMermaid(result, note) {
</details>` </details>`
} }
function renderImage(result, note) { function renderImage(result: Result, note: SNote) {
result.content = `<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">`; 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') { if (note.mime === 'application/pdf') {
result.content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view"></iframe>` result.content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view"></iframe>`
} else { } else {
@ -157,6 +170,6 @@ function renderFile(note, result) {
} }
} }
module.exports = { export = {
getContent getContent
}; };

View File

@ -1,23 +1,22 @@
const express = require('express'); import safeCompare = require('safe-compare');
const path = require('path'); import ejs = require("ejs");
const safeCompare = require('safe-compare');
const ejs = require("ejs");
const shaca = require('./shaca/shaca.js'); import type { Request, Response, Router } from "express";
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 shaca = require('./shaca/shaca');
* @param {SNote} note import shacaLoader = require('./shaca/shaca_loader');
* @return {{note: SNote, branch: SBranch}|{}} import shareRoot = require('./share_root');
*/ import contentRenderer = require('./content_renderer');
function getSharedSubTreeRoot(note) { 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) { if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
// share root itself is not shared // share root itself is not shared
return {}; return {};
@ -37,19 +36,18 @@ function getSharedSubTreeRoot(note) {
return getSharedSubTreeRoot(parentBranch.getParentNote()); return getSharedSubTreeRoot(parentBranch.getParentNote());
} }
function addNoIndexHeader(note, res) { function addNoIndexHeader(note: SNote, res: Response) {
if (note.isLabelTruthy('shareDisallowRobotIndexing')) { if (note.isLabelTruthy('shareDisallowRobotIndexing')) {
res.setHeader('X-Robots-Tag', 'noindex'); 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"') res.setHeader('WWW-Authenticate', 'Basic realm="User Visible Realm", charset="UTF-8"')
.sendStatus(401); .sendStatus(401);
} }
/** @returns {SAttachment|boolean} */ function checkAttachmentAccess(attachmentId: string, req: Request, res: Response) {
function checkAttachmentAccess(attachmentId, req, res) {
const attachment = shaca.getAttachment(attachmentId); const attachment = shaca.getAttachment(attachmentId);
if (!attachment) { if (!attachment) {
@ -65,8 +63,7 @@ function checkAttachmentAccess(attachmentId, req, res) {
return note ? attachment : false; return note ? attachment : false;
} }
/** @returns {SNote|boolean} */ function checkNoteAccess(noteId: string, req: Request, res: Response) {
function checkNoteAccess(noteId, req, res) {
const note = shaca.getNote(noteId); const note = shaca.getNote(noteId);
if (!note) { if (!note) {
@ -109,12 +106,16 @@ function checkNoteAccess(noteId, req, res) {
return false; return false;
} }
function renderImageAttachment(image, res, attachmentName) { function renderImageAttachment(image: SNote, res: Response, attachmentName: string) {
let svgString = '<svg/>' let svgString = '<svg/>'
const attachment = image.getAttachmentByTitle(attachmentName); const attachment = image.getAttachmentByTitle(attachmentName);
if (!attachment) {
if (attachment) { res.status(404).render("share/404");
svgString = attachment.getContent(); return;
}
const content = attachment.getContent();
if (typeof content === "string") {
svgString = content;
} else { } else {
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key // backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
const contentSvg = image.getJsonContentSafely()?.svg; const contentSvg = image.getJsonContentSafely()?.svg;
@ -130,8 +131,8 @@ function renderImageAttachment(image, res, attachmentName) {
res.send(svg); res.send(svg);
} }
function register(router) { function register(router: Router) {
function renderNote(note, req, res) { function renderNote(note: SNote, req: Request, res: Response) {
if (!note) { if (!note) {
res.status(404).render("share/404"); res.status(404).render("share/404");
return; return;
@ -152,35 +153,42 @@ function register(router) {
return; return;
} }
const {header, content, isEmpty} = contentRenderer.getContent(note); const { header, content, isEmpty } = contentRenderer.getContent(note);
const subRoot = getSharedSubTreeRoot(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; let useDefaultView = true;
// Check if the user has their own template // Check if the user has their own template
if (note.hasRelation('shareTemplate')) { if (note.hasRelation('shareTemplate')) {
// Get the template note and content // Get the template note and content
const templateId = note.getRelation('shareTemplate').value; const templateId = note.getRelation('shareTemplate')?.value;
const templateNote = shaca.getNote(templateId); const templateNote = templateId && shaca.getNote(templateId);
// Make sure the note type is correct // 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 // 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); const childNote = templateNote.children.find(n => path === n.title);
if (!childNote) return null; if (!childNote) throw new Error("Unable to find child note.");
if (childNote.type !== 'code' || childNote.mime !== 'application/x-ejs') return null; if (childNote.type !== 'code' || childNote.mime !== 'application/x-ejs') throw new Error("Incorrect child note type.");
return { template: childNote.getContent() };
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 to render user's template, w/ fallback to default view
try { try {
const ejsResult = ejs.render(templateNote.getContent(), opts, {includer}); const content = templateNote.getContent();
res.send(ejsResult); if (typeof content === "string") {
useDefaultView = false; // Rendering went okay, don't use default view 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}`); 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(); shacaLoader.ensureLoad();
if (!shaca.shareRootNote) {
return res.status(404)
.json({ message: "Share root note not found" });
}
renderNote(shaca.shareRootNote, req, res); renderNote(shaca.shareRootNote, req, res);
}); });
router.get('/share/:shareId', (req, res, next) => { router.get('/share/:shareId', (req, res, next) => {
shacaLoader.ensureLoad(); shacaLoader.ensureLoad();
const {shareId} = req.params; const { shareId } = req.params;
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId]; const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
@ -214,7 +227,7 @@ function register(router) {
router.get('/share/api/notes/:noteId', (req, res, next) => { router.get('/share/api/notes/:noteId', (req, res, next) => {
shacaLoader.ensureLoad(); shacaLoader.ensureLoad();
let note; let note: SNote | boolean;
if (!(note = checkNoteAccess(req.params.noteId, req, res))) { if (!(note = checkNoteAccess(req.params.noteId, req, res))) {
return; return;
@ -228,7 +241,7 @@ function register(router) {
router.get('/share/api/notes/:noteId/download', (req, res, next) => { router.get('/share/api/notes/:noteId/download', (req, res, next) => {
shacaLoader.ensureLoad(); shacaLoader.ensureLoad();
let note; let note: SNote | boolean;
if (!(note = checkNoteAccess(req.params.noteId, req, res))) { if (!(note = checkNoteAccess(req.params.noteId, req, res))) {
return; return;
@ -252,7 +265,7 @@ function register(router) {
router.get('/share/api/images/:noteId/:filename', (req, res, next) => { router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
shacaLoader.ensureLoad(); shacaLoader.ensureLoad();
let image; let image: SNote | boolean;
if (!(image = checkNoteAccess(req.params.noteId, req, res))) { if (!(image = checkNoteAccess(req.params.noteId, req, res))) {
return; return;
@ -277,7 +290,7 @@ function register(router) {
router.get('/share/api/attachments/:attachmentId/image/:filename', (req, res, next) => { router.get('/share/api/attachments/:attachmentId/image/:filename', (req, res, next) => {
shacaLoader.ensureLoad(); shacaLoader.ensureLoad();
let attachment; let attachment: SAttachment | boolean;
if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) { if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) {
return; return;
@ -296,7 +309,7 @@ function register(router) {
router.get('/share/api/attachments/:attachmentId/download', (req, res, next) => { router.get('/share/api/attachments/:attachmentId/download', (req, res, next) => {
shacaLoader.ensureLoad(); shacaLoader.ensureLoad();
let attachment; let attachment: SAttachment | boolean;
if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) { if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) {
return; return;
@ -320,7 +333,7 @@ function register(router) {
router.get('/share/api/notes/:noteId/view', (req, res, next) => { router.get('/share/api/notes/:noteId/view', (req, res, next) => {
shacaLoader.ensureLoad(); shacaLoader.ensureLoad();
let note; let note: SNote | boolean;
if (!(note = checkNoteAccess(req.params.noteId, req, res))) { if (!(note = checkNoteAccess(req.params.noteId, req, res))) {
return; return;
@ -341,18 +354,22 @@ function register(router) {
const ancestorNoteId = req.query.ancestorNoteId ?? "_share"; const ancestorNoteId = req.query.ancestorNoteId ?? "_share";
let note; 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 // This will automatically return if no ancestorNoteId is provided and there is no shareIndex
if (!(note = checkNoteAccess(ancestorNoteId, req, res))) { if (!(note = checkNoteAccess(ancestorNoteId, req, res))) {
return; 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." }); 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 searchResults = searchService.findResultsWithQuery(search, searchContext);
const filteredResults = searchResults.map(sr => { const filteredResults = searchResults.map(sr => {
const fullNote = shaca.notes[sr.noteId]; const fullNote = shaca.notes[sr.noteId];
@ -366,6 +383,6 @@ function register(router) {
}); });
} }
module.exports = { export = {
register register
} }

View File

@ -1,14 +0,0 @@
let shaca;
class AbstractShacaEntity {
/** @return {Shaca} */
get shaca() {
if (!shaca) {
shaca = require('../shaca.js');
}
return shaca;
}
}
module.exports = AbstractShacaEntity;

View 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;

View 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 ];

View File

@ -1,39 +1,42 @@
"use strict"; "use strict";
const sql = require('../../sql'); import sql = require('../../sql');
const utils = require('../../../services/utils'); import utils = require('../../../services/utils');
const AbstractShacaEntity = require('./abstract_shaca_entity.js'); import AbstractShacaEntity = require('./abstract_shaca_entity');
import SNote = require('./snote');
import { Blob } from '../../../services/blob-interface';
class SAttachment extends AbstractShacaEntity { 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(); super();
/** @param {string} */
this.attachmentId = attachmentId; this.attachmentId = attachmentId;
/** @param {string} */
this.ownerId = ownerId; this.ownerId = ownerId;
/** @param {string} */
this.title = title; this.title = title;
/** @param {string} */
this.role = role; this.role = role;
/** @param {string} */
this.mime = mime; this.mime = mime;
/** @param {string} */
this.blobId = blobId; this.blobId = blobId;
/** @param {string} */ this.utcDateModified = utcDateModified;
this.utcDateModified = utcDateModified; // used for caching of images
this.shaca.attachments[this.attachmentId] = this; this.shaca.attachments[this.attachmentId] = this;
this.shaca.notes[this.ownerId].attachments.push(this); this.shaca.notes[this.ownerId].attachments.push(this);
} }
/** @returns {SNote} */ get note(): SNote {
get note() {
return this.shaca.notes[this.ownerId]; return this.shaca.notes[this.ownerId];
} }
getContent(silentNotFoundError = false) { 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 (!row) {
if (silentNotFoundError) { 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() { hasStringContent() {
return utils.isStringNote(null, this.mime); return utils.isStringNote(null, this.mime);
} }
@ -67,11 +70,10 @@ class SAttachment extends AbstractShacaEntity {
role: this.role, role: this.role,
mime: this.mime, mime: this.mime,
title: this.title, title: this.title,
position: this.position,
blobId: this.blobId, blobId: this.blobId,
utcDateModified: this.utcDateModified utcDateModified: this.utcDateModified
}; };
} }
} }
module.exports = SAttachment; export = SAttachment;

View File

@ -1,24 +1,28 @@
"use strict"; "use strict";
const AbstractShacaEntity = require('./abstract_shaca_entity.js'); import SNote = require("./snote");
const AbstractShacaEntity = require('./abstract_shaca_entity');
class SAttribute extends AbstractShacaEntity { 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(); super();
/** @param {string} */
this.attributeId = attributeId; this.attributeId = attributeId;
/** @param {string} */
this.noteId = noteId; this.noteId = noteId;
/** @param {string} */
this.type = type; this.type = type;
/** @param {string} */
this.name = name; this.name = name;
/** @param {int} */
this.position = position; this.position = position;
/** @param {string} */
this.value = value; this.value = value;
/** @param {boolean} */
this.isInheritable = !!isInheritable; this.isInheritable = !!isInheritable;
this.shaca.attributes[this.attributeId] = this; this.shaca.attributes[this.attributeId] = this;
@ -53,41 +57,34 @@ class SAttribute extends AbstractShacaEntity {
} }
} }
/** @returns {boolean} */
get isAffectingSubtree() { get isAffectingSubtree() {
return this.isInheritable return this.isInheritable
|| (this.type === 'relation' && ['template', 'inherit'].includes(this.name)); || (this.type === 'relation' && ['template', 'inherit'].includes(this.name));
} }
/** @returns {string} */
get targetNoteId() { // alias get targetNoteId() { // alias
return this.type === 'relation' ? this.value : undefined; return this.type === 'relation' ? this.value : undefined;
} }
/** @returns {boolean} */
isAutoLink() { isAutoLink() {
return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name); return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name);
} }
/** @returns {SNote} */ get note(): SNote {
get note() {
return this.shaca.notes[this.noteId]; return this.shaca.notes[this.noteId];
} }
/** @returns {SNote|null} */ get targetNote(): SNote | null | undefined {
get targetNote() {
if (this.type === 'relation') { if (this.type === 'relation') {
return this.shaca.notes[this.value]; return this.shaca.notes[this.value];
} }
} }
/** @returns {SNote|null} */ getNote(): SNote | null {
getNote() {
return this.shaca.getNote(this.noteId); return this.shaca.getNote(this.noteId);
} }
/** @returns {SNote|null} */ getTargetNote(): SNote | null {
getTargetNote() {
if (this.type !== 'relation') { if (this.type !== 'relation') {
throw new Error(`Attribute '${this.attributeId}' is not relation`); throw new Error(`Attribute '${this.attributeId}' is not relation`);
} }
@ -112,4 +109,4 @@ class SAttribute extends AbstractShacaEntity {
} }
} }
module.exports = SAttribute; export = SAttribute;

View File

@ -1,22 +1,25 @@
"use strict"; "use strict";
const AbstractShacaEntity = require('./abstract_shaca_entity.js'); import AbstractShacaEntity = require('./abstract_shaca_entity');
import SNote = require('./snote');
class SBranch extends AbstractShacaEntity { 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(); super();
/** @param {string} */
this.branchId = branchId; this.branchId = branchId;
/** @param {string} */
this.noteId = noteId; this.noteId = noteId;
/** @param {string} */
this.parentNoteId = parentNoteId; this.parentNoteId = parentNoteId;
/** @param {string} */
this.prefix = prefix; this.prefix = prefix;
/** @param {boolean} */
this.isExpanded = !!isExpanded; this.isExpanded = !!isExpanded;
/** @param {boolean} */
this.isHidden = false; this.isHidden = false;
const childNote = this.childNote; const childNote = this.childNote;
@ -38,25 +41,21 @@ class SBranch extends AbstractShacaEntity {
this.shaca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this; this.shaca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
} }
/** @returns {SNote} */ get childNote(): SNote {
get childNote() {
return this.shaca.notes[this.noteId]; return this.shaca.notes[this.noteId];
} }
/** @returns {SNote} */
getNote() { getNote() {
return this.childNote; return this.childNote;
} }
/** @returns {SNote} */ get parentNote(): SNote {
get parentNote() {
return this.shaca.notes[this.parentNoteId]; return this.shaca.notes[this.parentNoteId];
} }
/** @returns {SNote} */
getParentNote() { getParentNote() {
return this.parentNote; return this.parentNote;
} }
} }
module.exports = SBranch; export = SBranch;

View File

@ -1,108 +1,103 @@
"use strict"; "use strict";
const sql = require('../../sql'); import sql = require('../../sql');
const utils = require('../../../services/utils'); import utils = require('../../../services/utils');
const AbstractShacaEntity = require('./abstract_shaca_entity.js'); import AbstractShacaEntity = require('./abstract_shaca_entity');
const escape = require('escape-html'); 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 LABEL = 'label';
const RELATION = 'relation'; const RELATION = 'relation';
const CREDENTIALS = 'shareCredentials'; 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 { 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(); super();
/** @param {string} */
this.noteId = noteId; this.noteId = noteId;
/** @param {string} */
this.title = isProtected ? "[protected]" : title; this.title = isProtected ? "[protected]" : title;
/** @param {string} */
this.type = type; this.type = type;
/** @param {string} */
this.mime = mime; this.mime = mime;
/** @param {string} */
this.blobId = blobId; this.blobId = blobId;
/** @param {string} */
this.utcDateModified = utcDateModified; // used for caching of images this.utcDateModified = utcDateModified; // used for caching of images
/** @param {boolean} */
this.isProtected = isProtected; this.isProtected = isProtected;
/** @param {SBranch[]} */
this.parentBranches = []; this.parentBranches = [];
/** @param {SNote[]} */
this.parents = []; this.parents = [];
/** @param {SNote[]} */
this.children = []; this.children = [];
/** @param {SAttribute[]} */
this.ownedAttributes = []; this.ownedAttributes = [];
/** @param {SAttribute[]|null} */
this.__attributeCache = null; this.__attributeCache = null;
/** @param {SAttribute[]|null} */
this.__inheritableAttributeCache = null; this.__inheritableAttributeCache = null;
/** @param {SAttribute[]} */
this.targetRelations = []; this.targetRelations = [];
/** @param {SAttachment[]} */
this.attachments = []; this.attachments = [];
this.shaca.notes[this.noteId] = this; this.shaca.notes[this.noteId] = this;
} }
/** @returns {SBranch[]} */
getParentBranches() { getParentBranches() {
return this.parentBranches; return this.parentBranches;
} }
/** @returns {SBranch[]} */
getBranches() { getBranches() {
return this.parentBranches; return this.parentBranches;
} }
/** @returns {SBranch[]} */ getChildBranches(): SBranch[] {
getChildBranches() {
return this.children.map(childNote => this.shaca.getBranchFromChildAndParent(childNote.noteId, this.noteId)); return this.children.map(childNote => this.shaca.getBranchFromChildAndParent(childNote.noteId, this.noteId));
} }
/** @returns {SBranch[]} */
getVisibleChildBranches() { getVisibleChildBranches() {
return this.getChildBranches() return this.getChildBranches()
.filter(branch => !branch.isHidden .filter(branch => !branch.isHidden
&& !branch.getNote().isLabelTruthy('shareHiddenFromTree')); && !branch.getNote().isLabelTruthy('shareHiddenFromTree'));
} }
/** @returns {SNote[]} */
getParentNotes() { getParentNotes() {
return this.parents; return this.parents;
} }
/** @returns {SNote[]} */
getChildNotes() { getChildNotes() {
return this.children; return this.children;
} }
/** @returns {SNote[]} */
getVisibleChildNotes() { getVisibleChildNotes() {
return this.getVisibleChildBranches() return this.getVisibleChildBranches()
.map(branch => branch.getNote()); .map(branch => branch.getNote());
} }
/** @returns {boolean} */
hasChildren() { hasChildren() {
return this.children && this.children.length > 0; return this.children && this.children.length > 0;
} }
/** @returns {boolean} */
hasVisibleChildren() { hasVisibleChildren() {
return this.getVisibleChildNotes().length > 0; return this.getVisibleChildNotes().length > 0;
} }
getContent(silentNotFoundError = false) { 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 (!row) {
if (silentNotFoundError) { 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() { hasStringContent() {
return utils.isStringNote(this.type, this.mime); return utils.isStringNote(this.type, this.mime);
} }
/** /**
* @param {string} [type] - (optional) attribute type to filter * @param type - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter * @param name - (optional) attribute name to filter
* @returns {SAttribute[]} all note's attributes, including inherited ones * @returns all note's attributes, including inherited ones
*/ */
getAttributes(type, name) { getAttributes(type?: string, name?: string) {
if (!this.__attributeCache) { let attributeCache = this.__attributeCache;
this.__getAttributes([]); if (!attributeCache) {
attributeCache = this.__getAttributes([]);
} }
if (type && name) { 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) { 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) { else if (name) {
return this.__attributeCache.filter(attr => attr.name === name && !isCredentials(attr)); return attributeCache.filter(attr => attr.name === name && !isCredentials(attr));
} }
else { else {
return this.__attributeCache.filter(attr => !isCredentials(attr)); return attributeCache.filter(attr => !isCredentials(attr));
} }
} }
/** @returns {SAttribute[]} */
getCredentials() { getCredentials() {
this.__getAttributes([]); return this.__getAttributes([]).filter(isCredentials);
return this.__attributeCache.filter(isCredentials);
} }
__getAttributes(path) { __getAttributes(path: string[]) {
if (path.includes(this.noteId)) { if (path.includes(this.noteId)) {
return []; 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 for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates
if (ownedAttr.type === 'relation' && ['template', 'inherit'].includes(ownedAttr.name)) { if (ownedAttr.type === 'relation' && ['template', 'inherit'].includes(ownedAttr.name)) {
@ -212,8 +205,7 @@ class SNote extends AbstractShacaEntity {
return this.__attributeCache; return this.__attributeCache;
} }
/** @returns {SAttribute[]} */ __getInheritableAttributes(path: string[]) {
__getInheritableAttributes(path) {
if (path.includes(this.noteId)) { if (path.includes(this.noteId)) {
return []; return [];
} }
@ -222,204 +214,225 @@ class SNote extends AbstractShacaEntity {
this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache 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); return !!this.getAttributes().find(attr => attr.type === type && attr.name === name);
} }
/** @returns {SNote|null} */ getRelationTarget(name: string) {
getRelationTarget(name) {
const relation = this.getAttributes().find(attr => attr.type === 'relation' && attr.name === name); const relation = this.getAttributes().find(attr => attr.type === 'relation' && attr.name === name);
return relation ? relation.targetNote : null; return relation ? relation.targetNote : null;
} }
/** /**
* @param {string} name - label name * @param name - label name
* @returns {boolean} true if label exists (including inherited) * @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 * @param name - label name
* @returns {boolean} true if label exists (including inherited) and does not have "false" value. * @returns true if label exists (including inherited) and does not have "false" value.
*/ */
isLabelTruthy(name) { isLabelTruthy(name: string) {
const label = this.getLabel(name); const label = this.getLabel(name);
if (!label) { if (!label) {
return false; return false;
} }
return label && label.value !== 'false'; return !!label && label.value !== 'false';
} }
/** /**
* @param {string} name - label name * @param name - label name
* @returns {boolean} true if label exists (excluding inherited) * @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 * @param name - relation name
* @returns {boolean} true if relation exists (including inherited) * @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 * @param name - relation name
* @returns {boolean} true if relation exists (excluding inherited) * @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 * @param name - label name
* @returns {SAttribute|null} label if it exists, null otherwise * @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 * @param name - label name
* @returns {SAttribute|null} label if it exists, null otherwise * @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 * @param name - relation name
* @returns {SAttribute|null} relation if it exists, null otherwise * @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 * @param name - relation name
* @returns {SAttribute|null} relation if it exists, null otherwise * @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 * @param name - label name
* @returns {string|null} label value if label exists, null otherwise * @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 * @param name - label name
* @returns {string|null} label value if label exists, null otherwise * @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 * @param name - relation name
* @returns {string|null} relation value if relation exists, null otherwise * @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 * @param name - relation name
* @returns {string|null} relation value if relation exists, null otherwise * @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 type - attribute type (label, relation, etc.)
* @param {string} name - attribute name * @param name - attribute name
* @returns {boolean} true if note has an attribute with given type and name (excluding inherited) * @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); return !!this.getOwnedAttribute(type, name);
} }
/** /**
* @param {string} type - attribute type (label, relation, etc.) * @param type - attribute type (label, relation, etc.)
* @param {string} name - attribute name * @param name - attribute name
* @returns {SAttribute} attribute of the given type and name. If there are more such attributes, first is returned. * @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. * Returns null if there's no such attribute belonging to this note.
*/ */
getAttribute(type, name) { getAttribute(type: string, name: string) {
const attributes = this.getAttributes(); const attributes = this.getAttributes();
return attributes.find(attr => attr.type === type && attr.name === name); return attributes.find(attr => attr.type === type && attr.name === name);
} }
/** /**
* @param {string} type - attribute type (label, relation, etc.) * @param type - attribute type (label, relation, etc.)
* @param {string} name - attribute name * @param name - attribute name
* @returns {string|null} attribute value of the given type and name or null if no such attribute exists. * @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); const attr = this.getAttribute(type, name);
return attr ? attr.value : null; return attr ? attr.value : null;
} }
/** /**
* @param {string} type - attribute type (label, relation, etc.) * @param type - attribute type (label, relation, etc.)
* @param {string} name - attribute name * @param name - attribute name
* @returns {string|null} attribute value of the given type and name or null if no such attribute exists. * @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); 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 * @param name - label name to filter
* @returns {SAttribute[]} all note's labels (attributes with type label), including inherited ones * @returns all note's labels (attributes with type label), including inherited ones
*/ */
getLabels(name) { getLabels(name: string) {
return this.getAttributes(LABEL, name); return this.getAttributes(LABEL, name);
} }
/** /**
* @param {string} [name] - label name to filter * @param name - label name to filter
* @returns {string[]} all note's label values, including inherited ones * @returns all note's label values, including inherited ones
*/ */
getLabelValues(name) { getLabelValues(name: string) {
return this.getLabels(name).map(l => l.value); return this.getLabels(name).map(l => l.value) as string[]; // FIXME
} }
/** /**
* @param {string} [name] - label name to filter * @param name - label name to filter
* @returns {SAttribute[]} all note's labels (attributes with type label), excluding inherited ones * @returns all note's labels (attributes with type label), excluding inherited ones
*/ */
getOwnedLabels(name) { getOwnedLabels(name: string) {
return this.getOwnedAttributes(LABEL, name); return this.getOwnedAttributes(LABEL, name);
} }
/** /**
* @param {string} [name] - label name to filter * @param name - label name to filter
* @returns {string[]} all note's label values, excluding inherited ones * @returns all note's label values, excluding inherited ones
*/ */
getOwnedLabelValues(name) { getOwnedLabelValues(name: string) {
return this.getOwnedAttributes(LABEL, name).map(l => l.value); return this.getOwnedAttributes(LABEL, name).map(l => l.value);
} }
/** /**
* @param {string} [name] - relation name to filter * @param name - relation name to filter
* @returns {SAttribute[]} all note's relations (attributes with type relation), including inherited ones * @returns all note's relations (attributes with type relation), including inherited ones
*/ */
getRelations(name) { getRelations(name: string) {
return this.getAttributes(RELATION, name); return this.getAttributes(RELATION, name);
} }
/** /**
* @param {string} [name] - relation name to filter * @param name - relation name to filter
* @returns {SAttribute[]} all note's relations (attributes with type relation), excluding inherited ones * @returns all note's relations (attributes with type relation), excluding inherited ones
*/ */
getOwnedRelations(name) { getOwnedRelations(name: string) {
return this.getOwnedAttributes(RELATION, name); return this.getOwnedAttributes(RELATION, name);
} }
/** /**
* @param {string} [type] - (optional) attribute type to filter * @param type - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter * @param name - (optional) attribute name to filter
* @returns {SAttribute[]} note's "owned" attributes - excluding inherited ones * @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 // it's a common mistake to include # or ~ into attribute name
if (name && ["#", "~"].includes(name[0])) { if (name && ["#", "~"].includes(name[0])) {
name = name.substr(1); 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() * This method can be significantly faster than the getAttribute()
*/ */
getOwnedAttribute(type, name) { getOwnedAttribute(type: string, name: string) {
const attrs = this.getOwnedAttributes(type, name); const attrs = this.getOwnedAttributes(type, name);
return attrs.length > 0 ? attrs[0] : null; return attrs.length > 0 ? attrs[0] : null;
} }
/** @returns {boolean} */
get isArchived() { get isArchived() {
return this.hasAttribute('label', 'archived'); return this.hasAttribute('label', 'archived');
} }
/** @returns {boolean} */
isInherited() { isInherited() {
return !!this.targetRelations.find(rel => rel.name === 'template' || rel.name === 'inherit'); return !!this.targetRelations.find(rel => rel.name === 'template' || rel.name === 'inherit');
} }
/** @returns {SAttribute[]} */
getTargetRelations() { getTargetRelations() {
return this.targetRelations; return this.targetRelations;
} }
/** @returns {SAttachment[]} */
getAttachments() { getAttachments() {
return this.attachments; return this.attachments;
} }
/** @returns {SAttachment} */ getAttachmentByTitle(title: string) {
getAttachmentByTitle(title) {
return this.attachments.find(attachment => attachment.title === title); return this.attachments.find(attachment => attachment.title === title);
} }
/** @returns {string} */
get shareId() { get shareId() {
if (this.hasOwnedLabel('shareRoot')) { if (this.hasOwnedLabel('shareRoot')) {
return ""; return "";
@ -514,4 +521,4 @@ class SNote extends AbstractShacaEntity {
} }
} }
module.exports = SNote; export = SNote;

View File

@ -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() { constructor() {
this.reset(); this.reset();
} }
reset() { reset() {
/** @type {Object.<String, SNote>} */
this.notes = {}; this.notes = {};
/** @type {Object.<String, SBranch>} */
this.branches = {}; this.branches = {};
/** @type {Object.<String, SBranch>} */
this.childParentToBranch = {}; this.childParentToBranch = {};
/** @type {Object.<String, SAttribute>} */
this.attributes = {}; this.attributes = {};
/** @type {Object.<String, SAttachment>} */
this.attachments = {}; this.attachments = {};
/** @type {Object.<String, SNote>} */
this.aliasToNote = {}; this.aliasToNote = {};
/** @type {SNote|null} */
this.shareRootNote = null; this.shareRootNote = null;
/** @type {boolean} true if the index of all shared subtrees is enabled */
this.shareIndexEnabled = false; this.shareIndexEnabled = false;
this.loaded = false; this.loaded = false;
} }
/** @returns {SNote|null} */ getNote(noteId: string) {
getNote(noteId) {
return this.notes[noteId]; return this.notes[noteId];
} }
/** @returns {boolean} */ hasNote(noteId: string) {
hasNote(noteId) {
return noteId in this.notes; return noteId in this.notes;
} }
/** @returns {SNote[]} */ getNotes(noteIds: string[], ignoreMissing = false) {
getNotes(noteIds, ignoreMissing = false) {
const filteredNotes = []; const filteredNotes = [];
for (const noteId of noteIds) { for (const noteId of noteIds) {
@ -59,27 +63,23 @@ class Shaca {
return filteredNotes; return filteredNotes;
} }
/** @returns {SBranch|null} */ getBranch(branchId: string) {
getBranch(branchId) {
return this.branches[branchId]; return this.branches[branchId];
} }
/** @returns {SBranch|null} */ getBranchFromChildAndParent(childNoteId: string, parentNoteId: string) {
getBranchFromChildAndParent(childNoteId, parentNoteId) {
return this.childParentToBranch[`${childNoteId}-${parentNoteId}`]; return this.childParentToBranch[`${childNoteId}-${parentNoteId}`];
} }
/** @returns {SAttribute|null} */ getAttribute(attributeId: string) {
getAttribute(attributeId) {
return this.attributes[attributeId]; return this.attributes[attributeId];
} }
/** @returns {SAttachment|null} */ getAttachment(attachmentId: string) {
getAttachment(attachmentId) {
return this.attachments[attachmentId]; return this.attachments[attachmentId];
} }
getEntity(entityName, entityId) { getEntity(entityName: string, entityId: string) {
if (!entityName || !entityId) { if (!entityName || !entityId) {
return null; return null;
} }
@ -91,10 +91,6 @@ class Shaca {
.replace('_', '') .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
View File

@ -0,0 +1,7 @@
"use strict";
import Shaca from "./shaca-interface";
const shaca = new Shaca();
export = shaca;

View File

@ -1,14 +1,14 @@
"use strict"; "use strict";
const sql = require('../sql'); import sql = require('../sql');
const shaca = require('./shaca.js'); import shaca = require('./shaca');
const log = require('../../services/log'); import log = require('../../services/log');
const SNote = require('./entities/snote.js'); import SNote = require('./entities/snote');
const SBranch = require('./entities/sbranch.js'); import SBranch = require('./entities/sbranch');
const SAttribute = require('./entities/sattribute.js'); import SAttribute = require('./entities/sattribute');
const SAttachment = require('./entities/sattachment.js'); import SAttachment = require('./entities/sattachment');
const shareRoot = require('../share_root.js'); import shareRoot = require('../share_root');
const eventService = require('../../services/events'); import eventService = require('../../services/events');
function load() { function load() {
const start = Date.now(); const start = Date.now();
@ -35,7 +35,7 @@ function load() {
const noteIdStr = noteIds.map(noteId => `'${noteId}'`).join(","); const noteIdStr = noteIds.map(noteId => `'${noteId}'`).join(",");
const rawNoteRows = sql.getRawRows(` const rawNoteRows = sql.getRawRows<SNoteRow>(`
SELECT noteId, title, type, mime, blobId, utcDateModified, isProtected SELECT noteId, title, type, mime, blobId, utcDateModified, isProtected
FROM notes FROM notes
WHERE isDeleted = 0 WHERE isDeleted = 0
@ -45,7 +45,7 @@ function load() {
new SNote(row); new SNote(row);
} }
const rawBranchRows = sql.getRawRows(` const rawBranchRows = sql.getRawRows<SBranchRow>(`
SELECT branchId, noteId, parentNoteId, prefix, isExpanded, utcDateModified SELECT branchId, noteId, parentNoteId, prefix, isExpanded, utcDateModified
FROM branches FROM branches
WHERE isDeleted = 0 WHERE isDeleted = 0
@ -56,7 +56,7 @@ function load() {
new SBranch(row); new SBranch(row);
} }
const rawAttributeRows = sql.getRawRows(` const rawAttributeRows = sql.getRawRows<SAttributeRow>(`
SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified
FROM attributes FROM attributes
WHERE isDeleted = 0 WHERE isDeleted = 0
@ -66,14 +66,12 @@ function load() {
new SAttribute(row); new SAttribute(row);
} }
const rawAttachmentRows = sql.getRawRows(` const rawAttachmentRows = sql.getRawRows<SAttachmentRow>(`
SELECT attachmentId, ownerId, role, mime, title, blobId, utcDateModified SELECT attachmentId, ownerId, role, mime, title, blobId, utcDateModified
FROM attachments FROM attachments
WHERE isDeleted = 0 WHERE isDeleted = 0
AND ownerId IN (${noteIdStr})`); AND ownerId IN (${noteIdStr})`);
rawAttachmentRows.sort((a, b) => a.position < b.position ? -1 : 1);
for (const row of rawAttachmentRows) { for (const row of rawAttachmentRows) {
new SAttachment(row); 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(); shaca.reset();
}); });
module.exports = { export = {
load, load,
ensureLoad ensureLoad
}; };

View File

@ -1,3 +1,3 @@
module.exports = { export = {
SHARE_ROOT_NOTE_ID: '_share' SHARE_ROOT_NOTE_ID: '_share'
} }

View File

@ -1,7 +1,7 @@
"use strict"; "use strict";
const Database = require('better-sqlite3'); import Database = require('better-sqlite3');
const dataDir = require('../services/data_dir'); import dataDir = require('../services/data_dir');
const dbConnection = new Database(dataDir.DOCUMENT_PATH, { readonly: true }); 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 = []) { function getRawRows<T>(query: string, params = []): T[] {
return dbConnection.prepare(query).raw().all(params); return dbConnection.prepare(query).raw().all(params) as T[];
} }
function getRow(query, params = []) { function getRow<T>(query: string, params: string[] = []): T {
return dbConnection.prepare(query).get(params); return dbConnection.prepare(query).get(params) as T;
} }
function getColumn(query, params = []) { function getColumn<T>(query: string, params: string[] = []): T[] {
return dbConnection.prepare(query).pluck().all(params); return dbConnection.prepare(query).pluck().all(params) as T[];
} }
module.exports = { export = {
getRawRows, getRawRows,
getRow, getRow,
getColumn getColumn