mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 11:39:01 +01:00 
			
		
		
		
	client-ts: Port services/app/entities
This commit is contained in:
		
							parent
							
								
									047c3eea69
								
							
						
					
					
						commit
						8fb6b64fa9
					
				| @ -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); | ||||
|     } | ||||
| @ -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<FNote>} */ | ||||
|     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<FAttribute, "froca"> { | ||||
|         const dto: any = Object.assign({}, this); | ||||
|         delete dto.froca; | ||||
| 
 | ||||
|         return dto; | ||||
| @ -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<FBranch, "froca"> { | ||||
|         const pojo = {...this} as any; | ||||
|         delete pojo.froca; | ||||
|         return pojo; | ||||
|     } | ||||
| @ -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<string>} 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.<string, 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<string, string>; | ||||
|     childToBranch: Record<string, string>; | ||||
|     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.<string, string>} */ | ||||
|         this.parentToBranch = {}; | ||||
| 
 | ||||
|         /** @type {Object.<string, string>} */ | ||||
|         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<string, number> = {}; | ||||
| 
 | ||||
|         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<FNote[]>} */ | ||||
|     async getChildNotes() { | ||||
|         return await this.froca.getNotes(this.children); | ||||
|     } | ||||
| 
 | ||||
|     /** @returns {Promise<FAttachment[]>} */ | ||||
|     async getAttachments() { | ||||
|         if (!this.attachments) { | ||||
|             this.attachments = await this.froca.getAttachmentsForNote(this.noteId); | ||||
| @ -258,14 +268,12 @@ class FNote { | ||||
|         return this.attachments; | ||||
|     } | ||||
| 
 | ||||
|     /** @returns {Promise<FAttachment[]>} */ | ||||
|     async getAttachmentsByRole(role) { | ||||
|     async getAttachmentsByRole(role: string) { | ||||
|         return (await this.getAttachments()) | ||||
|             .filter(attachment => attachment.role === role); | ||||
|     } | ||||
| 
 | ||||
|     /** @returns {Promise<FAttachment>} */ | ||||
|     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<NotePathRecord>} | ||||
|      */ | ||||
|     getSortedNotePathRecords(hoistedNoteId = 'root') { | ||||
|         const isHoistedRoot = hoistedNoteId === 'root'; | ||||
| 
 | ||||
| @ -476,13 +478,9 @@ class FNote { | ||||
|     } | ||||
| 
 | ||||
|     /**     | ||||
|      * @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<FNote>|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<FNote[]>} | ||||
|      * @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<string> | 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<FNote[]>} | ||||
|      */ | ||||
|     async getTargetRelationSourceNotes() { | ||||
|         const targetRelations = this.getTargetRelations(); | ||||
| @ -881,13 +875,11 @@ class FNote { | ||||
| 
 | ||||
|     /** | ||||
|      * @deprecated use getBlob() instead | ||||
|      * @return {Promise<FBlob>} | ||||
|      */ | ||||
|     async getNoteComplement() { | ||||
|         return this.getBlob(); | ||||
|     } | ||||
| 
 | ||||
|     /** @return {Promise<FBlob>} */ | ||||
|     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<FNote, "froca"> { | ||||
|         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<NoteMetaData>(`notes/${this.noteId}/metadata`); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -1,6 +1,6 @@ | ||||
| const registeredClasses = new Set<string>(); | ||||
| 
 | ||||
| function createClassForColor(color: string) { | ||||
| function createClassForColor(color: string | null) { | ||||
|     if (!color?.trim()) { | ||||
|         return ""; | ||||
|     } | ||||
|  | ||||
							
								
								
									
										24
									
								
								src/public/app/services/froca-interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/public/app/services/froca-interface.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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<string, FNote>; | ||||
|     branches: Record<string, FBranch>; | ||||
|     attributes: Record<string, FAttribute>; | ||||
|     attachments: Record<string, FAttachment>; | ||||
|     blobPromises: Record<string, Promise<void | FBlob> | null>; | ||||
| 
 | ||||
|     getBlob(entityType: string, entityId: string): Promise<void | FBlob | null>; | ||||
|     getNote(noteId: string, silentNotFoundError?: boolean): Promise<FNote | null>; | ||||
|     getNoteFromCache(noteId: string): FNote; | ||||
|     getNotesFromCache(noteIds: string[], silentNotFoundError?: boolean): FNote[]; | ||||
|     getNotes(noteIds: string[], silentNotFoundError?: boolean): Promise<FNote[]>; | ||||
| 
 | ||||
|     getBranch(branchId: string, silentNotFoundError?: boolean): FBranch | undefined; | ||||
|     getBranches(branchIds: string[], silentNotFoundError?: boolean): FBranch[]; | ||||
| 
 | ||||
|     getAttachmentsForNote(noteId: string): Promise<FAttachment[]>; | ||||
| } | ||||
| @ -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<void>; | ||||
| 
 | ||||
|     notes!: Record<string, FNote>; | ||||
|     branches!: Record<string, FBranch>; | ||||
|     attributes!: Record<string, FAttribute>; | ||||
|     attachments!: Record<string, FAttachment>; | ||||
|     blobPromises!: Record<string, Promise<void | FBlob> | null>; | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.initializedPromise = this.loadInitialTree(); | ||||
|     } | ||||
| 
 | ||||
|     async loadInitialTree() { | ||||
|         const resp = await server.get('tree'); | ||||
|         const resp = await server.get<SubtreeResponse>('tree'); | ||||
| 
 | ||||
|         // clear the cache only directly before adding new content which is important for e.g., switching to protected session
 | ||||
| 
 | ||||
|         /** @type {Object.<string, FNote>} */ | ||||
|         this.notes = {}; | ||||
| 
 | ||||
|         /** @type {Object.<string, FBranch>} */ | ||||
|         this.branches = {}; | ||||
| 
 | ||||
|         /** @type {Object.<string, FAttribute>} */ | ||||
|         this.attributes = {}; | ||||
| 
 | ||||
|         /** @type {Object.<string, FAttachment>} */ | ||||
|         this.attachments = {}; | ||||
| 
 | ||||
|         /** @type {Object.<string, Promise<FBlob>>} */ | ||||
|         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<SubtreeResponse>(`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<string>(); | ||||
| 
 | ||||
|         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<SubtreeResponse>('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<SearchNoteResponse>(`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<FNote[]>} */ | ||||
|     async getNotes(noteIds, silentNotFoundError = false) { | ||||
|     async getNotes(noteIds: string[], silentNotFoundError = false): Promise<FNote[]> { | ||||
|         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<boolean>} */ | ||||
|     async noteExists(noteId) { | ||||
|     async noteExists(noteId: string): Promise<boolean> { | ||||
|         const notes = await this.getNotes([noteId], true); | ||||
| 
 | ||||
|         return notes.length === 1; | ||||
|     } | ||||
| 
 | ||||
|     /** @returns {Promise<FNote>} */ | ||||
|     async getNote(noteId, silentNotFoundError = false) { | ||||
|     async getNote(noteId: string, silentNotFoundError = false): Promise<FNote | null> { | ||||
|         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<FAttachment>} */ | ||||
|     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<FAttachmentRow[]>(`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<FAttachment[]>} */ | ||||
|     async getAttachmentsForNote(noteId) { | ||||
|         const attachmentRows = await server.get(`notes/${noteId}/attachments`); | ||||
|     async getAttachmentsForNote(noteId: string) { | ||||
|         const attachmentRows = await server.get<FAttachmentRow[]>(`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<FBlob>} */ | ||||
|     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<FBlobRow>(`${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; | ||||
| @ -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<string, FAttribute[]>; | ||||
|      | ||||
|     constructor() { | ||||
|         /** @property {Object.<string, BAttribute[]>} */ | ||||
|         this.attributes = {}; | ||||
|     } | ||||
| 
 | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Elian Doran
						Elian Doran