From 8fb6b64fa9e1301aca43b3a5d952d195813fcaf4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 25 Jul 2024 20:36:15 +0300 Subject: [PATCH] client-ts: Port services/app/entities --- .../{fattachment.js => fattachment.ts} | 47 ++- .../entities/{fattribute.js => fattribute.ts} | 43 +- .../app/entities/{fbranch.js => fbranch.ts} | 46 ++- .../app/entities/{fnote.js => fnote.ts} | 374 +++++++++--------- src/public/app/services/css_class_manager.ts | 2 +- src/public/app/services/froca-interface.ts | 24 ++ .../app/services/{froca.js => froca.ts} | 109 ++--- ...ibute_cache.js => note_attribute_cache.ts} | 5 +- 8 files changed, 352 insertions(+), 298 deletions(-) rename src/public/app/entities/{fattachment.js => fattachment.ts} (52%) rename src/public/app/entities/{fattribute.js => fattribute.ts} (72%) rename src/public/app/entities/{fbranch.js => fbranch.ts} (58%) rename src/public/app/entities/{fnote.js => fnote.ts} (70%) create mode 100644 src/public/app/services/froca-interface.ts rename src/public/app/services/{froca.js => froca.ts} (80%) rename src/public/app/services/{note_attribute_cache.js => note_attribute_cache.ts} (85%) diff --git a/src/public/app/entities/fattachment.js b/src/public/app/entities/fattachment.ts similarity index 52% rename from src/public/app/entities/fattachment.js rename to src/public/app/entities/fattachment.ts index e0c698e96..704a53ba7 100644 --- a/src/public/app/entities/fattachment.js +++ b/src/public/app/entities/fattachment.ts @@ -1,48 +1,61 @@ +import { Froca } from "../services/froca-interface.js"; + +export interface FAttachmentRow { + attachmentId: string; + ownerId: string; + role: string; + mime: string; + title: string; + dateModified: string; + utcDateModified: string; + utcDateScheduledForErasureSince: string; + contentLength: number; +} + /** * Attachment is a file directly tied into a note without * being a hidden child. */ class FAttachment { - constructor(froca, row) { + private froca: Froca; + attachmentId!: string; + private ownerId!: string; + role!: string; + private mime!: string; + private title!: string; + private dateModified!: string; + private utcDateModified!: string; + private utcDateScheduledForErasureSince!: string; + /** + * optionally added to the entity + */ + private contentLength!: number; + + constructor(froca: Froca, row: FAttachmentRow) { /** @type {Froca} */ this.froca = froca; this.update(row); } - update(row) { - /** @type {string} */ + update(row: FAttachmentRow) { this.attachmentId = row.attachmentId; - /** @type {string} */ this.ownerId = row.ownerId; - /** @type {string} */ this.role = row.role; - /** @type {string} */ this.mime = row.mime; - /** @type {string} */ this.title = row.title; - /** @type {string} */ this.dateModified = row.dateModified; - /** @type {string} */ this.utcDateModified = row.utcDateModified; - /** @type {string} */ this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince; - - /** - * optionally added to the entity - * @type {int} - */ this.contentLength = row.contentLength; this.froca.attachments[this.attachmentId] = this; } - /** @returns {FNote} */ getNote() { return this.froca.notes[this.ownerId]; } - /** @return {FBlob} */ async getBlob() { return await this.froca.getBlob('attachments', this.attachmentId); } diff --git a/src/public/app/entities/fattribute.js b/src/public/app/entities/fattribute.ts similarity index 72% rename from src/public/app/entities/fattribute.js rename to src/public/app/entities/fattribute.ts index 5e36c873f..379f0588a 100644 --- a/src/public/app/entities/fattribute.js +++ b/src/public/app/entities/fattribute.ts @@ -1,45 +1,56 @@ +import { Froca } from '../services/froca-interface.js'; import promotedAttributeDefinitionParser from '../services/promoted_attribute_definition_parser.js'; /** * There are currently only two types of attributes, labels or relations. - * @typedef {"label" | "relation"} AttributeType */ +export type AttributeType = "label" | "relation"; + +export interface FAttributeRow { + attributeId: string; + noteId: string; + type: AttributeType; + name: string; + value: string; + position: number; + isInheritable: boolean; +} + /** * Attribute is an abstract concept which has two real uses - label (key - value pair) * and relation (representing named relationship between source and target note) */ class FAttribute { - constructor(froca, row) { - /** @type {Froca} */ + private froca: Froca; + attributeId!: string; + noteId!: string; + type!: AttributeType; + name!: string; + value!: string; + position!: number; + isInheritable!: boolean; + + constructor(froca: Froca, row: FAttributeRow) { this.froca = froca; this.update(row); } - update(row) { - /** @type {string} */ + update(row: FAttributeRow) { this.attributeId = row.attributeId; - /** @type {string} */ this.noteId = row.noteId; - /** @type {AttributeType} */ this.type = row.type; - /** @type {string} */ this.name = row.name; - /** @type {string} */ this.value = row.value; - /** @type {int} */ this.position = row.position; - /** @type {boolean} */ this.isInheritable = !!row.isInheritable; } - /** @returns {FNote} */ getNote() { return this.froca.notes[this.noteId]; } - /** @returns {Promise} */ async getTargetNote() { const targetNoteId = this.targetNoteId; @@ -70,12 +81,12 @@ class FAttribute { return promotedAttributeDefinitionParser.parse(this.value); } - isDefinitionFor(attr) { + isDefinitionFor(attr: FAttribute) { return this.type === 'label' && this.name === `${attr.type}:${attr.name}`; } - get dto() { - const dto = Object.assign({}, this); + get dto(): Omit { + const dto: any = Object.assign({}, this); delete dto.froca; return dto; diff --git a/src/public/app/entities/fbranch.js b/src/public/app/entities/fbranch.ts similarity index 58% rename from src/public/app/entities/fbranch.js rename to src/public/app/entities/fbranch.ts index 3fa20934b..d497c70af 100644 --- a/src/public/app/entities/fbranch.js +++ b/src/public/app/entities/fbranch.ts @@ -1,51 +1,65 @@ +import { Froca } from "../services/froca-interface.js"; + +export interface FBranchRow { + branchId: string; + noteId: string; + parentNoteId: string; + notePosition: number; + prefix?: string; + isExpanded?: boolean; + fromSearchNote: boolean; +} + /** * Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple * parents. */ class FBranch { - constructor(froca, row) { - /** @type {Froca} */ + private froca: Froca; + + /** + * primary key + */ + branchId!: string; + noteId!: string; + parentNoteId!: string; + notePosition!: number; + prefix?: string; + isExpanded?: boolean; + fromSearchNote!: boolean; + + constructor(froca: Froca, row: FBranchRow) { this.froca = froca; this.update(row); } - update(row) { + update(row: FBranchRow) { /** * primary key - * @type {string} */ this.branchId = row.branchId; - /** @type {string} */ this.noteId = row.noteId; - /** @type {string} */ this.parentNoteId = row.parentNoteId; - /** @type {int} */ this.notePosition = row.notePosition; - /** @type {string} */ this.prefix = row.prefix; - /** @type {boolean} */ this.isExpanded = !!row.isExpanded; - /** @type {boolean} */ this.fromSearchNote = !!row.fromSearchNote; } - /** @returns {FNote} */ async getNote() { return this.froca.getNote(this.noteId); } - /** @returns {FNote} */ getNoteFromCache() { return this.froca.getNoteFromCache(this.noteId); } - /** @returns {FNote} */ async getParentNote() { return this.froca.getNote(this.parentNoteId); } - /** @returns {boolean} true if it's top level, meaning its parent is the root note */ + /** @returns true if it's top level, meaning its parent is the root note */ isTopLevel() { return this.parentNoteId === 'root'; } @@ -54,8 +68,8 @@ class FBranch { return `FBranch(branchId=${this.branchId})`; } - get pojo() { - const pojo = {...this}; + get pojo(): Omit { + const pojo = {...this} as any; delete pojo.froca; return pojo; } diff --git a/src/public/app/entities/fnote.js b/src/public/app/entities/fnote.ts similarity index 70% rename from src/public/app/entities/fnote.js rename to src/public/app/entities/fnote.ts index 46befda8e..515a6c11c 100644 --- a/src/public/app/entities/fnote.js +++ b/src/public/app/entities/fnote.ts @@ -4,6 +4,9 @@ import ws from "../services/ws.js"; import froca from "../services/froca.js"; import protectedSessionHolder from "../services/protected_session_holder.js"; import cssClassManager from "../services/css_class_manager.js"; +import { Froca } from '../services/froca-interface.js'; +import FAttachment from './fattachment.js'; +import FAttribute, { AttributeType } from './fattribute.js'; const LABEL = 'label'; const RELATION = 'relation'; @@ -29,76 +32,91 @@ const NOTE_TYPE_ICONS = { * There are many different Note types, some of which are entirely opaque to the * end user. Those types should be used only for checking against, they are * not for direct use. - * @typedef {"file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code"} NoteType */ +type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code"; -/** - * @typedef {Object} NotePathRecord - * @property {boolean} isArchived - * @property {boolean} isInHoistedSubTree - * @property {boolean} isSearch - * @property {Array} notePath - * @property {boolean} isHidden - */ +interface NotePathRecord { + isArchived: boolean; + isInHoistedSubTree: boolean; + isSearch: boolean; + notePath: string[]; + isHidden: boolean; +} + +export interface FNoteRow { + noteId: string; + title: string; + isProtected: boolean; + type: NoteType; + mime: string; + blobId: string; +} + +export interface NoteMetaData { + dateCreated: string; + utcDateCreated: string; + dateModified: string; + utcDateModified: string; +} /** * Note is the main node and concept in Trilium. */ class FNote { + + private froca: Froca; + + noteId!: string; + title!: string; + isProtected!: boolean; + type!: NoteType; /** - * @param {Froca} froca - * @param {Object.} row + * content-type, e.g. "application/json" */ - constructor(froca, row) { - /** @type {Froca} */ + mime!: string; + // the main use case to keep this is to detect content change which should trigger refresh + blobId!: string; + + attributes: string[]; + targetRelations: string[]; + parents: string[]; + children: string[]; + + parentToBranch: Record; + childToBranch: Record; + attachments: FAttachment[] | null; + + // Managed by Froca. + searchResultsLoaded?: boolean; + highlightedTokens?: unknown; + + constructor(froca: Froca, row: FNoteRow) { this.froca = froca; - - /** @type {string[]} */ this.attributes = []; - - /** @type {string[]} */ this.targetRelations = []; - - /** @type {string[]} */ this.parents = []; - /** @type {string[]} */ this.children = []; - /** @type {Object.} */ this.parentToBranch = {}; - - /** @type {Object.} */ this.childToBranch = {}; - /** @type {FAttachment[]|null} */ this.attachments = null; // lazy loaded this.update(row); } - update(row) { - /** @type {string} */ + update(row: FNoteRow) { this.noteId = row.noteId; - /** @type {string} */ this.title = row.title; - /** @type {boolean} */ this.isProtected = !!row.isProtected; - /** - * See {@see NoteType} for info on values. - * @type {NoteType} - */ this.type = row.type; - /** - * content-type, e.g. "application/json" - * @type {string} - */ + this.mime = row.mime; - // the main use case to keep this is to detect content change which should trigger refresh this.blobId = row.blobId; } - addParent(parentNoteId, branchId, sort = true) { + addParent(parentNoteId: string, branchId: string, sort = true) { if (parentNoteId === 'none') { return; } @@ -114,7 +132,7 @@ class FNote { } } - addChild(childNoteId, branchId, sort = true) { + addChild(childNoteId: string, branchId: string, sort = true) { if (!(childNoteId in this.childToBranch)) { this.children.push(childNoteId); } @@ -127,16 +145,18 @@ class FNote { } sortChildren() { - const branchIdPos = {}; + const branchIdPos: Record = {}; for (const branchId of Object.values(this.childToBranch)) { - branchIdPos[branchId] = this.froca.getBranch(branchId).notePosition; + const notePosition = this.froca.getBranch(branchId)?.notePosition; + if (notePosition) { + branchIdPos[branchId] = notePosition; + } } this.children.sort((a, b) => branchIdPos[this.childToBranch[a]] - branchIdPos[this.childToBranch[b]]); } - /** @returns {boolean} */ isJson() { return this.mime === "application/json"; } @@ -150,34 +170,32 @@ class FNote { async getJsonContent() { const content = await this.getContent(); + if (typeof content !== "string") { + console.log(`Unknown note content for '${this.noteId}'.`); + return null; + } + try { return JSON.parse(content); } - catch (e) { + catch (e: any) { console.log(`Cannot parse content of note '${this.noteId}': `, e.message); return null; } } - /** - * @returns {string[]} - */ getParentBranchIds() { return Object.values(this.parentToBranch); } /** - * @returns {string[]} * @deprecated use getParentBranchIds() instead */ getBranchIds() { return this.getParentBranchIds(); } - /** - * @returns {FBranch[]} - */ getParentBranches() { const branchIds = Object.values(this.parentToBranch); @@ -185,19 +203,16 @@ class FNote { } /** - * @returns {FBranch[]} * @deprecated use getParentBranches() instead */ getBranches() { return this.getParentBranches(); } - /** @returns {boolean} */ hasChildren() { return this.children.length > 0; } - /** @returns {FBranch[]} */ getChildBranches() { // don't use Object.values() to guarantee order const branchIds = this.children.map(childNoteId => this.childToBranch[childNoteId]); @@ -205,12 +220,10 @@ class FNote { return this.froca.getBranches(branchIds); } - /** @returns {string[]} */ getParentNoteIds() { return this.parents; } - /** @returns {FNote[]} */ getParentNotes() { return this.froca.getNotesFromCache(this.parents); } @@ -239,17 +252,14 @@ class FNote { return this.hasAttribute('label', 'archived'); } - /** @returns {string[]} */ getChildNoteIds() { return this.children; } - /** @returns {Promise} */ async getChildNotes() { return await this.froca.getNotes(this.children); } - /** @returns {Promise} */ async getAttachments() { if (!this.attachments) { this.attachments = await this.froca.getAttachmentsForNote(this.noteId); @@ -258,14 +268,12 @@ class FNote { return this.attachments; } - /** @returns {Promise} */ - async getAttachmentsByRole(role) { + async getAttachmentsByRole(role: string) { return (await this.getAttachments()) .filter(attachment => attachment.role === role); } - /** @returns {Promise} */ - async getAttachmentById(attachmentId) { + async getAttachmentById(attachmentId: string) { const attachments = await this.getAttachments(); return attachments.find(att => att.attachmentId === attachmentId); @@ -295,11 +303,11 @@ class FNote { } /** - * @param {string} [type] - (optional) attribute type to filter - * @param {string} [name] - (optional) attribute name to filter - * @returns {FAttribute[]} all note's attributes, including inherited ones + * @param [type] - attribute type to filter + * @param [name] - attribute name to filter + * @returns all note's attributes, including inherited ones */ - getOwnedAttributes(type, name) { + getOwnedAttributes(type?: AttributeType, name?: string) { const attrs = this.attributes .map(attributeId => this.froca.attributes[attributeId]) .filter(Boolean); // filter out nulls; @@ -308,20 +316,18 @@ class FNote { } /** - * @param {string} [type] - (optional) attribute type to filter - * @param {string} [name] - (optional) attribute name to filter - * @returns {FAttribute[]} all note's attributes, including inherited ones + * @param [type] - attribute type to filter + * @param [name] - attribute name to filter + * @returns all note's attributes, including inherited ones */ - getAttributes(type, name) { + getAttributes(type?: AttributeType, name?: string) { return this.__filterAttrs(this.__getCachedAttributes([]), type, name); } /** - * @param {string[]} path - * @return {FAttribute[]} * @private */ - __getCachedAttributes(path) { + __getCachedAttributes(path: string[]): FAttribute[] { // notes/clones cannot form tree cycles, it is possible to create attribute inheritance cycle via templates // when template instance is a parent of template itself if (path.includes(this.noteId)) { @@ -376,9 +382,9 @@ class FNote { /** * Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles) * - * @returns {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path) + * @returns array of notePaths (each represented by array of noteIds constituting the particular note path) */ - getAllNotePaths() { + getAllNotePaths(): string[][] { if (this.noteId === 'root') { return [['root']]; } @@ -396,10 +402,6 @@ class FNote { return notePaths; } - /** - * @param {string} [hoistedNoteId='root'] - * @return {Array} - */ getSortedNotePathRecords(hoistedNoteId = 'root') { const isHoistedRoot = hoistedNoteId === 'root'; @@ -475,14 +477,10 @@ class FNote { return true; } - /** - * @param {FAttribute[]} attributes - * @param {AttributeType} type - * @param {string} name - * @return {FAttribute[]} + /** * @private */ - __filterAttrs(attributes, type, name) { + __filterAttrs(attributes: FAttribute[], type?: AttributeType, name?: string): FAttribute[] { this.__validateTypeName(type, name); if (!type && !name) { @@ -494,15 +492,17 @@ class FNote { } else if (name) { return attributes.filter(attr => attr.name === name); } + + return []; } - __getInheritableAttributes(path) { + __getInheritableAttributes(path: string[]) { const attrs = this.__getCachedAttributes(path); return attrs.filter(attr => attr.isInheritable); } - __validateTypeName(type, name) { + __validateTypeName(type?: string, name?: string) { if (type && type !== 'label' && type !== 'relation') { throw new Error(`Unrecognized attribute type '${type}'. Only 'label' and 'relation' are possible values.`); } @@ -516,18 +516,18 @@ class FNote { } /** - * @param {string} [name] - label name to filter - * @returns {FAttribute[]} 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 */ - getOwnedLabels(name) { + getOwnedLabels(name: string) { return this.getOwnedAttributes(LABEL, name); } /** - * @param {string} [name] - label name to filter - * @returns {FAttribute[]} 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); } @@ -535,7 +535,7 @@ class FNote { const iconClassLabels = this.getLabels('iconClass'); const workspaceIconClass = this.getWorkspaceIconClass(); - if (iconClassLabels.length > 0) { + if (iconClassLabels && iconClassLabels.length > 0) { return iconClassLabels[0].value; } else if (workspaceIconClass) { @@ -578,7 +578,7 @@ class FNote { if (!childBranches) { ws.logError(`No children for '${this.noteId}'. This shouldn't happen.`); - return; + return []; } // we're not checking hideArchivedNotes since that would mean we need to lazy load the child notes @@ -590,102 +590,104 @@ class FNote { } /** - * @param {string} [name] - relation name to filter - * @returns {FAttribute[]} 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 */ - getOwnedRelations(name) { + getOwnedRelations(name: string) { return this.getOwnedAttributes(RELATION, name); } /** - * @param {string} [name] - relation name to filter - * @returns {FAttribute[]} 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 {AttributeType} type - attribute type (label, relation, etc.) - * @param {string} name - attribute name - * @returns {boolean} true if note has an attribute with given type and name (including inherited) + * @param type - attribute type (label, relation, etc.) + * @param name - attribute name + * @returns true if note has an attribute with given type and name (including inherited) */ - hasAttribute(type, name) { + hasAttribute(type: AttributeType, name: string) { const attributes = this.getAttributes(); return attributes.some(attr => attr.name === name && attr.type === type); } /** - * @param {AttributeType} type - attribute type (label, relation, etc.) - * @param {string} name - attribute name - * @returns {boolean} true if note has an attribute with given type and name (including inherited) + * @param type - attribute type (label, relation, etc.) + * @param name - attribute name + * @returns true if note has an attribute with given type and name (including inherited) */ - hasOwnedAttribute(type, name) { + hasOwnedAttribute(type: AttributeType, name: string) { return !!this.getOwnedAttribute(type, name); } /** - * @param {AttributeType} type - attribute type (label, relation, etc.) - * @param {string} name - attribute name - * @returns {FAttribute} attribute of the given type and name. If there are more such attributes, first is returned. Returns null if there's no such attribute belonging to this note. + * @param type - attribute type (label, relation, etc.) + * @param name - attribute name + * @returns attribute of the given type and name. If there are more such attributes, first is returned. Returns null if there's no such attribute belonging to this note. */ - getOwnedAttribute(type, name) { + getOwnedAttribute(type: AttributeType, name: string) { const attributes = this.getOwnedAttributes(); return attributes.find(attr => attr.name === name && attr.type === type); } /** - * @param {AttributeType} type - attribute type (label, relation, etc.) - * @param {string} name - attribute name - * @returns {FAttribute} attribute of the given type and name. If there are more such attributes, first is returned. Returns null if there's no such attribute belonging to this note. + * @param type - attribute type (label, relation, etc.) + * @param name - attribute name + * @returns attribute of the given type and name. If there are more such attributes, first is returned. Returns null if there's no such attribute belonging to this note. */ - getAttribute(type, name) { + getAttribute(type: AttributeType, name: string) { const attributes = this.getAttributes(); return attributes.find(attr => attr.name === name && attr.type === type); } /** - * @param {AttributeType} type - attribute type (label, relation, etc.) - * @param {string} name - attribute name - * @returns {string} 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: AttributeType, name: string) { const attr = this.getOwnedAttribute(type, name); return attr ? attr.value : null; } /** - * @param {AttributeType} type - attribute type (label, relation, etc.) - * @param {string} name - attribute name - * @returns {string} 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: AttributeType, name: string) { const attr = this.getAttribute(type, name); return attr ? attr.value : null; } /** - * @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 - 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) { @@ -696,80 +698,79 @@ class FNote { } /** - * @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 - 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 - label name - * @returns {FAttribute} 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 - label name - * @returns {FAttribute} 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 - relation name - * @returns {FAttribute} 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 - relation name - * @returns {FAttribute} 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 - label name - * @returns {string} 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 - label name - * @returns {string} 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 - relation name - * @returns {string} 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} name - relation name - * @returns {string} 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 - * @returns {Promise|null} target note of the relation or null (if target is empty or note was not found) + * @param name + * @returns target note of the relation or null (if target is empty or note was not found) */ - async getRelationTarget(name) { + async getRelationTarget(name: string) { const targets = await this.getRelationTargets(name); return targets.length > 0 ? targets[0] : null; } /** - * @param {string} [name] - relation name to filter - * @returns {Promise} + * @param [name] - relation name to filter */ - async getRelationTargets(name) { + async getRelationTargets(name: string) { const relations = this.getRelations(name); const targets = []; @@ -780,9 +781,6 @@ class FNote { return targets; } - /** - * @returns {FNote[]} - */ getNotesToInheritAttributesFrom() { const relations = [ ...this.getRelations('template'), @@ -818,7 +816,7 @@ class FNote { return promotedAttrs; } - hasAncestor(ancestorNoteId, followTemplates = false, visitedNoteIds = null) { + hasAncestor(ancestorNoteId: string, followTemplates = false, visitedNoteIds: Set | null = null) { if (this.noteId === ancestorNoteId) { return true; } @@ -860,8 +858,6 @@ class FNote { /** * Get relations which target this note - * - * @returns {FAttribute[]} */ getTargetRelations() { return this.targetRelations @@ -870,8 +866,6 @@ class FNote { /** * Get relations which target this note - * - * @returns {Promise} */ async getTargetRelationSourceNotes() { const targetRelations = this.getTargetRelations(); @@ -881,13 +875,11 @@ class FNote { /** * @deprecated use getBlob() instead - * @return {Promise} */ async getNoteComplement() { return this.getBlob(); } - /** @return {Promise} */ async getBlob() { return await this.froca.getBlob('notes', this.noteId); } @@ -896,8 +888,8 @@ class FNote { return `Note(noteId=${this.noteId}, title=${this.title})`; } - get dto() { - const dto = Object.assign({}, this); + get dto(): Omit { + const dto = Object.assign({}, this) as any; delete dto.froca; return dto; @@ -918,7 +910,7 @@ class FNote { return labels.length > 0 ? labels[0].value : ""; } - /** @returns {boolean} true if this note is JavaScript (code or file) */ + /** @returns true if this note is JavaScript (code or file) */ isJavaScript() { return (this.type === "code" || this.type === "file" || this.type === 'launcher') && (this.mime.startsWith("application/javascript") @@ -926,12 +918,12 @@ class FNote { || this.mime === "text/javascript"); } - /** @returns {boolean} true if this note is HTML */ + /** @returns true if this note is HTML */ isHtml() { return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html"; } - /** @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"; @@ -958,11 +950,9 @@ class FNote { if (env === "frontend") { const bundleService = (await import("../services/bundle.js")).default; return await bundleService.getAndExecuteBundle(this.noteId); - } - else if (env === "backend") { - const resp = await server.post(`script/run/${this.noteId}`); - } - else { + } else if (env === "backend") { + await server.post(`script/run/${this.noteId}`); + } else { throw new Error(`Unrecognized env type ${env} for note ${this.noteId}`); } } @@ -1001,11 +991,9 @@ class FNote { /** * Provides note's date metadata. - * - * @returns {Promise<{dateCreated: string, utcDateCreated: string, dateModified: string, utcDateModified: string}>} */ async getMetadata() { - return await server.get(`notes/${this.noteId}/metadata`); + return await server.get(`notes/${this.noteId}/metadata`); } } diff --git a/src/public/app/services/css_class_manager.ts b/src/public/app/services/css_class_manager.ts index a458e995a..f5d2e9649 100644 --- a/src/public/app/services/css_class_manager.ts +++ b/src/public/app/services/css_class_manager.ts @@ -1,6 +1,6 @@ const registeredClasses = new Set(); -function createClassForColor(color: string) { +function createClassForColor(color: string | null) { if (!color?.trim()) { return ""; } diff --git a/src/public/app/services/froca-interface.ts b/src/public/app/services/froca-interface.ts new file mode 100644 index 000000000..01dd83bc7 --- /dev/null +++ b/src/public/app/services/froca-interface.ts @@ -0,0 +1,24 @@ +import FAttachment from "../entities/fattachment.js"; +import FAttribute from "../entities/fattribute.js"; +import FBlob from "../entities/fblob.js"; +import FBranch from "../entities/fbranch.js"; +import FNote from "../entities/fnote.js"; + +export interface Froca { + notes: Record; + branches: Record; + attributes: Record; + attachments: Record; + blobPromises: Record | null>; + + getBlob(entityType: string, entityId: string): Promise; + getNote(noteId: string, silentNotFoundError?: boolean): Promise; + getNoteFromCache(noteId: string): FNote; + getNotesFromCache(noteIds: string[], silentNotFoundError?: boolean): FNote[]; + getNotes(noteIds: string[], silentNotFoundError?: boolean): Promise; + + getBranch(branchId: string, silentNotFoundError?: boolean): FBranch | undefined; + getBranches(branchIds: string[], silentNotFoundError?: boolean): FBranch[]; + + getAttachmentsForNote(noteId: string): Promise; +} \ No newline at end of file diff --git a/src/public/app/services/froca.js b/src/public/app/services/froca.ts similarity index 80% rename from src/public/app/services/froca.js rename to src/public/app/services/froca.ts index 17ddeccd6..974633783 100644 --- a/src/public/app/services/froca.js +++ b/src/public/app/services/froca.ts @@ -3,8 +3,22 @@ import FNote from "../entities/fnote.js"; import FAttribute from "../entities/fattribute.js"; import server from "./server.js"; import appContext from "../components/app_context.js"; -import FBlob from "../entities/fblob.js"; -import FAttachment from "../entities/fattachment.js"; +import FBlob, { FBlobRow } from "../entities/fblob.js"; +import FAttachment, { FAttachmentRow } from "../entities/fattachment.js"; +import { Froca } from "./froca-interface.js"; + + +interface SubtreeResponse { + notes: FNoteRow[]; + branches: FBranchRow[]; + attributes: FAttributeRow[]; +} + +interface SearchNoteResponse { + searchResultNoteIds: string[]; + highlightedTokens: string[]; + error: string | null; +} /** * Froca (FROntend CAche) keeps a read only cache of note tree structure in frontend's memory. @@ -16,48 +30,47 @@ import FAttachment from "../entities/fattachment.js"; * * Backend has a similar cache called Becca */ -class Froca { +class FrocaImpl implements Froca { + private initializedPromise: Promise; + + notes!: Record; + branches!: Record; + attributes!: Record; + attachments!: Record; + blobPromises!: Record | null>; + constructor() { this.initializedPromise = this.loadInitialTree(); } async loadInitialTree() { - const resp = await server.get('tree'); + const resp = await server.get('tree'); // clear the cache only directly before adding new content which is important for e.g., switching to protected session - /** @type {Object.} */ this.notes = {}; - - /** @type {Object.} */ this.branches = {}; - - /** @type {Object.} */ this.attributes = {}; - - /** @type {Object.} */ this.attachments = {}; - - /** @type {Object.>} */ this.blobPromises = {}; this.addResp(resp); } - async loadSubTree(subTreeNoteId) { - const resp = await server.get(`tree?subTreeNoteId=${subTreeNoteId}`); + async loadSubTree(subTreeNoteId: string) { + const resp = await server.get(`tree?subTreeNoteId=${subTreeNoteId}`); this.addResp(resp); return this.notes[subTreeNoteId]; } - addResp(resp) { + addResp(resp: SubtreeResponse) { const noteRows = resp.notes; const branchRows = resp.branches; const attributeRows = resp.attributes; - const noteIdsToSort = new Set(); + const noteIdsToSort = new Set(); for (const noteRow of noteRows) { const {noteId} = noteRow; @@ -160,28 +173,28 @@ class Froca { } } - async reloadNotes(noteIds) { + async reloadNotes(noteIds: string[]) { if (noteIds.length === 0) { return; } noteIds = Array.from(new Set(noteIds)); // make noteIds unique - const resp = await server.post('tree/load', { noteIds }); + const resp = await server.post('tree/load', { noteIds }); this.addResp(resp); appContext.triggerEvent('notesReloaded', {noteIds}); } - async loadSearchNote(noteId) { + async loadSearchNote(noteId: string) { const note = await this.getNote(noteId); if (!note || note.type !== 'search') { return; } - const {searchResultNoteIds, highlightedTokens, error} = await server.get(`search-note/${note.noteId}`); + const {searchResultNoteIds, highlightedTokens, error} = await server.get(`search-note/${note.noteId}`); if (!Array.isArray(searchResultNoteIds)) { throw new Error(`Search note '${note.noteId}' failed: ${searchResultNoteIds}`); @@ -217,8 +230,7 @@ class Froca { return {error}; } - /** @returns {FNote[]} */ - getNotesFromCache(noteIds, silentNotFoundError = false) { + getNotesFromCache(noteIds: string[], silentNotFoundError = false): FNote[] { return noteIds.map(noteId => { if (!this.notes[noteId] && !silentNotFoundError) { console.trace(`Can't find note '${noteId}'`); @@ -228,11 +240,10 @@ class Froca { else { return this.notes[noteId]; } - }).filter(note => !!note); + }).filter(note => !!note) as FNote[]; } - /** @returns {Promise} */ - async getNotes(noteIds, silentNotFoundError = false) { + async getNotes(noteIds: string[], silentNotFoundError = false): Promise { noteIds = Array.from(new Set(noteIds)); // make unique const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]); @@ -246,18 +257,16 @@ class Froca { } else { return this.notes[noteId]; } - }).filter(note => !!note); + }).filter(note => !!note) as FNote[]; } - /** @returns {Promise} */ - async noteExists(noteId) { + async noteExists(noteId: string): Promise { const notes = await this.getNotes([noteId], true); return notes.length === 1; } - /** @returns {Promise} */ - async getNote(noteId, silentNotFoundError = false) { + async getNote(noteId: string, silentNotFoundError = false): Promise { if (noteId === 'none') { console.trace(`No 'none' note.`); return null; @@ -270,8 +279,7 @@ class Froca { return (await this.getNotes([noteId], silentNotFoundError))[0]; } - /** @returns {FNote|null} */ - getNoteFromCache(noteId) { + getNoteFromCache(noteId: string) { if (!noteId) { throw new Error("Empty noteId"); } @@ -279,15 +287,13 @@ class Froca { return this.notes[noteId]; } - /** @returns {FBranch[]} */ - getBranches(branchIds, silentNotFoundError = false) { + getBranches(branchIds: string[], silentNotFoundError = false): FBranch[] { return branchIds .map(branchId => this.getBranch(branchId, silentNotFoundError)) - .filter(b => !!b); + .filter(b => !!b) as FBranch[]; } - /** @returns {FBranch} */ - getBranch(branchId, silentNotFoundError = false) { + getBranch(branchId: string, silentNotFoundError = false) { if (!(branchId in this.branches)) { if (!silentNotFoundError) { logError(`Not existing branch '${branchId}'`); @@ -298,7 +304,7 @@ class Froca { } } - async getBranchId(parentNoteId, childNoteId) { + async getBranchId(parentNoteId: string, childNoteId: string) { if (childNoteId === 'root') { return 'none_root'; } @@ -314,8 +320,7 @@ class Froca { return child.parentToBranch[parentNoteId]; } - /** @returns {Promise} */ - async getAttachment(attachmentId, silentNotFoundError = false) { + async getAttachment(attachmentId: string, silentNotFoundError = false) { const attachment = this.attachments[attachmentId]; if (attachment) { return attachment; @@ -324,9 +329,8 @@ class Froca { // load all attachments for the given note even if one is requested, don't load one by one let attachmentRows; try { - attachmentRows = await server.getWithSilentNotFound(`attachments/${attachmentId}/all`); - } - catch (e) { + attachmentRows = await server.getWithSilentNotFound(`attachments/${attachmentId}/all`); + } catch (e: any) { if (silentNotFoundError) { logInfo(`Attachment '${attachmentId}' not found, but silentNotFoundError is enabled: ` + e.message); return null; @@ -344,14 +348,12 @@ class Froca { return this.attachments[attachmentId]; } - /** @returns {Promise} */ - async getAttachmentsForNote(noteId) { - const attachmentRows = await server.get(`notes/${noteId}/attachments`); + async getAttachmentsForNote(noteId: string) { + const attachmentRows = await server.get(`notes/${noteId}/attachments`); return this.processAttachmentRows(attachmentRows); } - /** @returns {FAttachment[]} */ - processAttachmentRows(attachmentRows) { + processAttachmentRows(attachmentRows: FAttachmentRow[]): FAttachment[] { return attachmentRows.map(attachmentRow => { let attachment; @@ -367,22 +369,21 @@ class Froca { }); } - /** @returns {Promise} */ - async getBlob(entityType, entityId) { + async getBlob(entityType: string, entityId: string) { // I'm not sure why we're not using blobIds directly, it would save us this composite key ... // perhaps one benefit is that we're always requesting the latest blob, not relying on perhaps faulty/slow // websocket update? const key = `${entityType}-${entityId}`; if (!this.blobPromises[key]) { - this.blobPromises[key] = server.get(`${entityType}/${entityId}/blob`) + this.blobPromises[key] = server.get(`${entityType}/${entityId}/blob`) .then(row => new FBlob(row)) .catch(e => console.error(`Cannot get blob for ${entityType} '${entityId}'`, e)); // we don't want to keep large payloads forever in memory, so we clean that up quite quickly // this cache is more meant to share the data between different components within one business transaction (e.g. loading of the note into the tab context and all the components) // if the blob is updated within the cache lifetime, it should be invalidated by froca_updater - this.blobPromises[key].then( + this.blobPromises[key]?.then( () => setTimeout(() => this.blobPromises[key] = null, 1000) ); } @@ -391,6 +392,6 @@ class Froca { } } -const froca = new Froca(); +const froca = new FrocaImpl(); export default froca; diff --git a/src/public/app/services/note_attribute_cache.js b/src/public/app/services/note_attribute_cache.ts similarity index 85% rename from src/public/app/services/note_attribute_cache.js rename to src/public/app/services/note_attribute_cache.ts index e0696bda1..955744d71 100644 --- a/src/public/app/services/note_attribute_cache.js +++ b/src/public/app/services/note_attribute_cache.ts @@ -1,3 +1,5 @@ +import FAttribute from "../entities/fattribute.js"; + /** * The purpose of this class is to cache the list of attributes for notes. * @@ -6,8 +8,9 @@ * as loading the tree which uses attributes heavily. */ class NoteAttributeCache { + attributes: Record; + constructor() { - /** @property {Object.} */ this.attributes = {}; }