mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 03:29:02 +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 |  * Attachment is a file directly tied into a note without | ||||||
|  * being a hidden child. |  * being a hidden child. | ||||||
|  */ |  */ | ||||||
| class FAttachment { | 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} */ |         /** @type {Froca} */ | ||||||
|         this.froca = froca; |         this.froca = froca; | ||||||
| 
 | 
 | ||||||
|         this.update(row); |         this.update(row); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     update(row) { |     update(row: FAttachmentRow) { | ||||||
|         /** @type {string} */ |  | ||||||
|         this.attachmentId = row.attachmentId; |         this.attachmentId = row.attachmentId; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.ownerId = row.ownerId; |         this.ownerId = row.ownerId; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.role = row.role; |         this.role = row.role; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.mime = row.mime; |         this.mime = row.mime; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.title = row.title; |         this.title = row.title; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.dateModified = row.dateModified; |         this.dateModified = row.dateModified; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.utcDateModified = row.utcDateModified; |         this.utcDateModified = row.utcDateModified; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince; |         this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince; | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * optionally added to the entity  |  | ||||||
|          * @type {int} |  | ||||||
|          */ |  | ||||||
|         this.contentLength = row.contentLength; |         this.contentLength = row.contentLength; | ||||||
| 
 | 
 | ||||||
|         this.froca.attachments[this.attachmentId] = this; |         this.froca.attachments[this.attachmentId] = this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {FNote} */ |  | ||||||
|     getNote() { |     getNote() { | ||||||
|         return this.froca.notes[this.ownerId]; |         return this.froca.notes[this.ownerId]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @return {FBlob} */ |  | ||||||
|     async getBlob() { |     async getBlob() { | ||||||
|         return await this.froca.getBlob('attachments', this.attachmentId); |         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'; | import promotedAttributeDefinitionParser from '../services/promoted_attribute_definition_parser.js'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * There are currently only two types of attributes, labels or relations. |  * 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) |  * Attribute is an abstract concept which has two real uses - label (key - value pair) | ||||||
|  * and relation (representing named relationship between source and target note) |  * and relation (representing named relationship between source and target note) | ||||||
|  */ |  */ | ||||||
| class FAttribute { | class FAttribute { | ||||||
|     constructor(froca, row) { |     private froca: Froca; | ||||||
|         /** @type {Froca} */ |     attributeId!: string; | ||||||
|  |     noteId!: string; | ||||||
|  |     type!: AttributeType; | ||||||
|  |     name!: string; | ||||||
|  |     value!: string; | ||||||
|  |     position!: number; | ||||||
|  |     isInheritable!: boolean; | ||||||
|  | 
 | ||||||
|  |     constructor(froca: Froca, row: FAttributeRow) { | ||||||
|         this.froca = froca; |         this.froca = froca; | ||||||
| 
 | 
 | ||||||
|         this.update(row); |         this.update(row); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     update(row) { |     update(row: FAttributeRow) { | ||||||
|         /** @type {string} */ |  | ||||||
|         this.attributeId = row.attributeId; |         this.attributeId = row.attributeId; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.noteId = row.noteId; |         this.noteId = row.noteId; | ||||||
|         /** @type {AttributeType} */ |  | ||||||
|         this.type = row.type; |         this.type = row.type; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.name = row.name; |         this.name = row.name; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.value = row.value; |         this.value = row.value; | ||||||
|         /** @type {int} */ |  | ||||||
|         this.position = row.position; |         this.position = row.position; | ||||||
|         /** @type {boolean} */ |  | ||||||
|         this.isInheritable = !!row.isInheritable; |         this.isInheritable = !!row.isInheritable; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {FNote} */ |  | ||||||
|     getNote() { |     getNote() { | ||||||
|         return this.froca.notes[this.noteId]; |         return this.froca.notes[this.noteId]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {Promise<FNote>} */ |  | ||||||
|     async getTargetNote() { |     async getTargetNote() { | ||||||
|         const targetNoteId = this.targetNoteId; |         const targetNoteId = this.targetNoteId; | ||||||
| 
 | 
 | ||||||
| @ -70,12 +81,12 @@ class FAttribute { | |||||||
|         return promotedAttributeDefinitionParser.parse(this.value); |         return promotedAttributeDefinitionParser.parse(this.value); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     isDefinitionFor(attr) { |     isDefinitionFor(attr: FAttribute) { | ||||||
|         return this.type === 'label' && this.name === `${attr.type}:${attr.name}`; |         return this.type === 'label' && this.name === `${attr.type}:${attr.name}`; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     get dto() { |     get dto(): Omit<FAttribute, "froca"> { | ||||||
|         const dto = Object.assign({}, this); |         const dto: any = Object.assign({}, this); | ||||||
|         delete dto.froca; |         delete dto.froca; | ||||||
| 
 | 
 | ||||||
|         return dto; |         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 |  * Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple | ||||||
|  * parents. |  * parents. | ||||||
|  */ |  */ | ||||||
| class FBranch { | class FBranch { | ||||||
|     constructor(froca, row) { |     private froca: Froca; | ||||||
|         /** @type {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.froca = froca; | ||||||
| 
 | 
 | ||||||
|         this.update(row); |         this.update(row); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     update(row) { |     update(row: FBranchRow) { | ||||||
|         /** |         /** | ||||||
|          * primary key |          * primary key | ||||||
|          * @type {string} |  | ||||||
|          */ |          */ | ||||||
|         this.branchId = row.branchId; |         this.branchId = row.branchId; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.noteId = row.noteId; |         this.noteId = row.noteId; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.parentNoteId = row.parentNoteId; |         this.parentNoteId = row.parentNoteId; | ||||||
|         /** @type {int} */ |  | ||||||
|         this.notePosition = row.notePosition; |         this.notePosition = row.notePosition; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.prefix = row.prefix; |         this.prefix = row.prefix; | ||||||
|         /** @type {boolean} */ |  | ||||||
|         this.isExpanded = !!row.isExpanded; |         this.isExpanded = !!row.isExpanded; | ||||||
|         /** @type {boolean} */ |  | ||||||
|         this.fromSearchNote = !!row.fromSearchNote; |         this.fromSearchNote = !!row.fromSearchNote; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {FNote} */ |  | ||||||
|     async getNote() { |     async getNote() { | ||||||
|         return this.froca.getNote(this.noteId); |         return this.froca.getNote(this.noteId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {FNote} */ |  | ||||||
|     getNoteFromCache() { |     getNoteFromCache() { | ||||||
|         return this.froca.getNoteFromCache(this.noteId); |         return this.froca.getNoteFromCache(this.noteId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {FNote} */ |  | ||||||
|     async getParentNote() { |     async getParentNote() { | ||||||
|         return this.froca.getNote(this.parentNoteId); |         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() { |     isTopLevel() { | ||||||
|         return this.parentNoteId === 'root'; |         return this.parentNoteId === 'root'; | ||||||
|     } |     } | ||||||
| @ -54,8 +68,8 @@ class FBranch { | |||||||
|         return `FBranch(branchId=${this.branchId})`; |         return `FBranch(branchId=${this.branchId})`; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     get pojo() { |     get pojo(): Omit<FBranch, "froca"> { | ||||||
|         const pojo = {...this}; |         const pojo = {...this} as any; | ||||||
|         delete pojo.froca; |         delete pojo.froca; | ||||||
|         return pojo; |         return pojo; | ||||||
|     } |     } | ||||||
| @ -4,6 +4,9 @@ import ws from "../services/ws.js"; | |||||||
| import froca from "../services/froca.js"; | import froca from "../services/froca.js"; | ||||||
| import protectedSessionHolder from "../services/protected_session_holder.js"; | import protectedSessionHolder from "../services/protected_session_holder.js"; | ||||||
| import cssClassManager from "../services/css_class_manager.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 LABEL = 'label'; | ||||||
| const RELATION = 'relation'; | 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 |  * 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 |  * end user. Those types should be used only for checking against, they are | ||||||
|  * not for direct use. |  * 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"; | ||||||
| 
 | 
 | ||||||
| /** | interface NotePathRecord { | ||||||
|  * @typedef {Object} NotePathRecord |     isArchived: boolean; | ||||||
|  * @property {boolean} isArchived |     isInHoistedSubTree: boolean; | ||||||
|  * @property {boolean} isInHoistedSubTree |     isSearch: boolean; | ||||||
|  * @property {boolean} isSearch |     notePath: string[]; | ||||||
|  * @property {Array<string>} notePath |     isHidden: boolean; | ||||||
|  * @property {boolean} isHidden | } | ||||||
|  */ | 
 | ||||||
|  | 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. |  * Note is the main node and concept in Trilium. | ||||||
|  */ |  */ | ||||||
| class FNote { | class FNote { | ||||||
|  | 
 | ||||||
|  |     private froca: Froca; | ||||||
|  | 
 | ||||||
|  |     noteId!: string; | ||||||
|  |     title!: string; | ||||||
|  |     isProtected!: boolean; | ||||||
|  |     type!: NoteType; | ||||||
|     /** |     /** | ||||||
|      * @param {Froca} froca |      * content-type, e.g. "application/json" | ||||||
|      * @param {Object.<string, Object>} row |  | ||||||
|      */ |      */ | ||||||
|     constructor(froca, row) { |     mime!: string; | ||||||
|         /** @type {Froca} */ |     // 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; |         this.froca = froca; | ||||||
| 
 |  | ||||||
|         /** @type {string[]} */ |  | ||||||
|         this.attributes = []; |         this.attributes = []; | ||||||
| 
 |  | ||||||
|         /** @type {string[]} */ |  | ||||||
|         this.targetRelations = []; |         this.targetRelations = []; | ||||||
| 
 |  | ||||||
|         /** @type {string[]} */ |  | ||||||
|         this.parents = []; |         this.parents = []; | ||||||
|         /** @type {string[]} */ |  | ||||||
|         this.children = []; |         this.children = []; | ||||||
| 
 | 
 | ||||||
|         /** @type {Object.<string, string>} */ |  | ||||||
|         this.parentToBranch = {}; |         this.parentToBranch = {}; | ||||||
| 
 |  | ||||||
|         /** @type {Object.<string, string>} */ |  | ||||||
|         this.childToBranch = {}; |         this.childToBranch = {}; | ||||||
| 
 | 
 | ||||||
|         /** @type {FAttachment[]|null} */ |  | ||||||
|         this.attachments = null; // lazy loaded
 |         this.attachments = null; // lazy loaded
 | ||||||
| 
 | 
 | ||||||
|         this.update(row); |         this.update(row); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     update(row) { |     update(row: FNoteRow) { | ||||||
|         /** @type {string} */ |  | ||||||
|         this.noteId = row.noteId; |         this.noteId = row.noteId; | ||||||
|         /** @type {string} */ |  | ||||||
|         this.title = row.title; |         this.title = row.title; | ||||||
|         /** @type {boolean} */ |  | ||||||
|         this.isProtected = !!row.isProtected; |         this.isProtected = !!row.isProtected; | ||||||
|         /** |  | ||||||
|          * See {@see NoteType} for info on values. |  | ||||||
|          * @type {NoteType} |  | ||||||
|          */ |  | ||||||
|         this.type = row.type; |         this.type = row.type; | ||||||
|         /** |          | ||||||
|          * content-type, e.g. "application/json" |  | ||||||
|          * @type {string} |  | ||||||
|          */ |  | ||||||
|         this.mime = row.mime; |         this.mime = row.mime; | ||||||
| 
 | 
 | ||||||
|         // the main use case to keep this is to detect content change which should trigger refresh
 |  | ||||||
|         this.blobId = row.blobId; |         this.blobId = row.blobId; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addParent(parentNoteId, branchId, sort = true) { |     addParent(parentNoteId: string, branchId: string, sort = true) { | ||||||
|         if (parentNoteId === 'none') { |         if (parentNoteId === 'none') { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @ -114,7 +132,7 @@ class FNote { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addChild(childNoteId, branchId, sort = true) { |     addChild(childNoteId: string, branchId: string, sort = true) { | ||||||
|         if (!(childNoteId in this.childToBranch)) { |         if (!(childNoteId in this.childToBranch)) { | ||||||
|             this.children.push(childNoteId); |             this.children.push(childNoteId); | ||||||
|         } |         } | ||||||
| @ -127,16 +145,18 @@ class FNote { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     sortChildren() { |     sortChildren() { | ||||||
|         const branchIdPos = {}; |         const branchIdPos: Record<string, number> = {}; | ||||||
| 
 | 
 | ||||||
|         for (const branchId of Object.values(this.childToBranch)) { |         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]]); |         this.children.sort((a, b) => branchIdPos[this.childToBranch[a]] - branchIdPos[this.childToBranch[b]]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {boolean} */ |  | ||||||
|     isJson() { |     isJson() { | ||||||
|         return this.mime === "application/json"; |         return this.mime === "application/json"; | ||||||
|     } |     } | ||||||
| @ -150,34 +170,32 @@ class FNote { | |||||||
|     async getJsonContent() { |     async getJsonContent() { | ||||||
|         const content = await this.getContent(); |         const content = await this.getContent(); | ||||||
| 
 | 
 | ||||||
|  |         if (typeof content !== "string") { | ||||||
|  |             console.log(`Unknown note content for '${this.noteId}'.`); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         try { |         try { | ||||||
|             return JSON.parse(content); |             return JSON.parse(content); | ||||||
|         } |         } | ||||||
|         catch (e) { |         catch (e: any) { | ||||||
|             console.log(`Cannot parse content of note '${this.noteId}': `, e.message); |             console.log(`Cannot parse content of note '${this.noteId}': `, e.message); | ||||||
| 
 | 
 | ||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * @returns {string[]} |  | ||||||
|      */ |  | ||||||
|     getParentBranchIds() { |     getParentBranchIds() { | ||||||
|         return Object.values(this.parentToBranch); |         return Object.values(this.parentToBranch); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @returns {string[]} |  | ||||||
|      * @deprecated use getParentBranchIds() instead |      * @deprecated use getParentBranchIds() instead | ||||||
|      */ |      */ | ||||||
|     getBranchIds() { |     getBranchIds() { | ||||||
|         return this.getParentBranchIds(); |         return this.getParentBranchIds(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * @returns {FBranch[]} |  | ||||||
|      */ |  | ||||||
|     getParentBranches() { |     getParentBranches() { | ||||||
|         const branchIds = Object.values(this.parentToBranch); |         const branchIds = Object.values(this.parentToBranch); | ||||||
| 
 | 
 | ||||||
| @ -185,19 +203,16 @@ class FNote { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @returns {FBranch[]} |  | ||||||
|      * @deprecated use getParentBranches() instead |      * @deprecated use getParentBranches() instead | ||||||
|      */ |      */ | ||||||
|     getBranches() { |     getBranches() { | ||||||
|         return this.getParentBranches(); |         return this.getParentBranches(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {boolean} */ |  | ||||||
|     hasChildren() { |     hasChildren() { | ||||||
|         return this.children.length > 0; |         return this.children.length > 0; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {FBranch[]} */ |  | ||||||
|     getChildBranches() { |     getChildBranches() { | ||||||
|         // don't use Object.values() to guarantee order
 |         // don't use Object.values() to guarantee order
 | ||||||
|         const branchIds = this.children.map(childNoteId => this.childToBranch[childNoteId]); |         const branchIds = this.children.map(childNoteId => this.childToBranch[childNoteId]); | ||||||
| @ -205,12 +220,10 @@ class FNote { | |||||||
|         return this.froca.getBranches(branchIds); |         return this.froca.getBranches(branchIds); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {string[]} */ |  | ||||||
|     getParentNoteIds() { |     getParentNoteIds() { | ||||||
|         return this.parents; |         return this.parents; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {FNote[]} */ |  | ||||||
|     getParentNotes() { |     getParentNotes() { | ||||||
|         return this.froca.getNotesFromCache(this.parents); |         return this.froca.getNotesFromCache(this.parents); | ||||||
|     } |     } | ||||||
| @ -239,17 +252,14 @@ class FNote { | |||||||
|         return this.hasAttribute('label', 'archived'); |         return this.hasAttribute('label', 'archived'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {string[]} */ |  | ||||||
|     getChildNoteIds() { |     getChildNoteIds() { | ||||||
|         return this.children; |         return this.children; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {Promise<FNote[]>} */ |  | ||||||
|     async getChildNotes() { |     async getChildNotes() { | ||||||
|         return await this.froca.getNotes(this.children); |         return await this.froca.getNotes(this.children); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {Promise<FAttachment[]>} */ |  | ||||||
|     async getAttachments() { |     async getAttachments() { | ||||||
|         if (!this.attachments) { |         if (!this.attachments) { | ||||||
|             this.attachments = await this.froca.getAttachmentsForNote(this.noteId); |             this.attachments = await this.froca.getAttachmentsForNote(this.noteId); | ||||||
| @ -258,14 +268,12 @@ class FNote { | |||||||
|         return this.attachments; |         return this.attachments; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {Promise<FAttachment[]>} */ |     async getAttachmentsByRole(role: string) { | ||||||
|     async getAttachmentsByRole(role) { |  | ||||||
|         return (await this.getAttachments()) |         return (await this.getAttachments()) | ||||||
|             .filter(attachment => attachment.role === role); |             .filter(attachment => attachment.role === role); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {Promise<FAttachment>} */ |     async getAttachmentById(attachmentId: string) { | ||||||
|     async getAttachmentById(attachmentId) { |  | ||||||
|         const attachments = await this.getAttachments(); |         const attachments = await this.getAttachments(); | ||||||
| 
 | 
 | ||||||
|         return attachments.find(att => att.attachmentId === attachmentId); |         return attachments.find(att => att.attachmentId === attachmentId); | ||||||
| @ -295,11 +303,11 @@ class FNote { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} [type] - (optional) attribute type to filter |      * @param [type] - attribute type to filter | ||||||
|      * @param {string} [name] - (optional) attribute name to filter |      * @param [name] - attribute name to filter | ||||||
|      * @returns {FAttribute[]} all note's attributes, including inherited ones |      * @returns all note's attributes, including inherited ones | ||||||
|      */ |      */ | ||||||
|     getOwnedAttributes(type, name) { |     getOwnedAttributes(type?: AttributeType, name?: string) { | ||||||
|         const attrs = this.attributes |         const attrs = this.attributes | ||||||
|             .map(attributeId => this.froca.attributes[attributeId]) |             .map(attributeId => this.froca.attributes[attributeId]) | ||||||
|             .filter(Boolean); // filter out nulls;
 |             .filter(Boolean); // filter out nulls;
 | ||||||
| @ -308,20 +316,18 @@ class FNote { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} [type] - (optional) attribute type to filter |      * @param [type] - attribute type to filter | ||||||
|      * @param {string} [name] - (optional) attribute name to filter |      * @param [name] - attribute name to filter | ||||||
|      * @returns {FAttribute[]} all note's attributes, including inherited ones |      * @returns all note's attributes, including inherited ones | ||||||
|      */ |      */ | ||||||
|     getAttributes(type, name) { |     getAttributes(type?: AttributeType, name?: string) { | ||||||
|         return this.__filterAttrs(this.__getCachedAttributes([]), type, name); |         return this.__filterAttrs(this.__getCachedAttributes([]), type, name); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string[]} path |  | ||||||
|      * @return {FAttribute[]} |  | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     __getCachedAttributes(path) { |     __getCachedAttributes(path: string[]): FAttribute[] { | ||||||
|         // notes/clones cannot form tree cycles, it is possible to create attribute inheritance cycle via templates
 |         // 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
 |         // when template instance is a parent of template itself
 | ||||||
|         if (path.includes(this.noteId)) { |         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) |      * 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') { |         if (this.noteId === 'root') { | ||||||
|             return [['root']]; |             return [['root']]; | ||||||
|         } |         } | ||||||
| @ -396,10 +402,6 @@ class FNote { | |||||||
|         return notePaths; |         return notePaths; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * @param {string} [hoistedNoteId='root'] |  | ||||||
|      * @return {Array<NotePathRecord>} |  | ||||||
|      */ |  | ||||||
|     getSortedNotePathRecords(hoistedNoteId = 'root') { |     getSortedNotePathRecords(hoistedNoteId = 'root') { | ||||||
|         const isHoistedRoot = hoistedNoteId === 'root'; |         const isHoistedRoot = hoistedNoteId === 'root'; | ||||||
| 
 | 
 | ||||||
| @ -476,13 +478,9 @@ class FNote { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /**     |     /**     | ||||||
|      * @param {FAttribute[]} attributes |  | ||||||
|      * @param {AttributeType} type |  | ||||||
|      * @param {string} name |  | ||||||
|      * @return {FAttribute[]} |  | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     __filterAttrs(attributes, type, name) { |     __filterAttrs(attributes: FAttribute[], type?: AttributeType, name?: string): FAttribute[] { | ||||||
|         this.__validateTypeName(type, name); |         this.__validateTypeName(type, name); | ||||||
| 
 | 
 | ||||||
|         if (!type && !name) { |         if (!type && !name) { | ||||||
| @ -494,15 +492,17 @@ class FNote { | |||||||
|         } else if (name) { |         } else if (name) { | ||||||
|             return attributes.filter(attr => attr.name === name); |             return attributes.filter(attr => attr.name === name); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         return []; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     __getInheritableAttributes(path) { |     __getInheritableAttributes(path: string[]) { | ||||||
|         const attrs = this.__getCachedAttributes(path); |         const attrs = this.__getCachedAttributes(path); | ||||||
| 
 | 
 | ||||||
|         return attrs.filter(attr => attr.isInheritable); |         return attrs.filter(attr => attr.isInheritable); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     __validateTypeName(type, name) { |     __validateTypeName(type?: string, name?: string) { | ||||||
|         if (type && type !== 'label' && type !== 'relation') { |         if (type && type !== 'label' && type !== 'relation') { | ||||||
|             throw new Error(`Unrecognized attribute type '${type}'. Only 'label' and 'relation' are possible values.`); |             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 |      * @param [name] - label name to filter | ||||||
|      * @returns {FAttribute[]} all note's labels (attributes with type label), including inherited ones |      * @returns all note's labels (attributes with type label), including inherited ones | ||||||
|      */ |      */ | ||||||
|     getOwnedLabels(name) { |     getOwnedLabels(name: string) { | ||||||
|         return this.getOwnedAttributes(LABEL, name); |         return this.getOwnedAttributes(LABEL, name); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} [name] - label name to filter |      * @param [name] - label name to filter | ||||||
|      * @returns {FAttribute[]} all note's labels (attributes with type label), including inherited ones |      * @returns all note's labels (attributes with type label), including inherited ones | ||||||
|      */ |      */ | ||||||
|     getLabels(name) { |     getLabels(name: string) { | ||||||
|         return this.getAttributes(LABEL, name); |         return this.getAttributes(LABEL, name); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -535,7 +535,7 @@ class FNote { | |||||||
|         const iconClassLabels = this.getLabels('iconClass'); |         const iconClassLabels = this.getLabels('iconClass'); | ||||||
|         const workspaceIconClass = this.getWorkspaceIconClass(); |         const workspaceIconClass = this.getWorkspaceIconClass(); | ||||||
| 
 | 
 | ||||||
|         if (iconClassLabels.length > 0) { |         if (iconClassLabels && iconClassLabels.length > 0) { | ||||||
|             return iconClassLabels[0].value; |             return iconClassLabels[0].value; | ||||||
|         } |         } | ||||||
|         else if (workspaceIconClass) { |         else if (workspaceIconClass) { | ||||||
| @ -578,7 +578,7 @@ class FNote { | |||||||
| 
 | 
 | ||||||
|         if (!childBranches) { |         if (!childBranches) { | ||||||
|             ws.logError(`No children for '${this.noteId}'. This shouldn't happen.`); |             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
 |         // 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 |      * @param [name] - relation name to filter | ||||||
|      * @returns {FAttribute[]} all note's relations (attributes with type relation), including inherited ones |      * @returns all note's relations (attributes with type relation), including inherited ones | ||||||
|      */ |      */ | ||||||
|     getOwnedRelations(name) { |     getOwnedRelations(name: string) { | ||||||
|         return this.getOwnedAttributes(RELATION, name); |         return this.getOwnedAttributes(RELATION, name); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} [name] - relation name to filter |      * @param [name] - relation name to filter | ||||||
|      * @returns {FAttribute[]} all note's relations (attributes with type relation), including inherited ones |      * @returns all note's relations (attributes with type relation), including inherited ones | ||||||
|      */ |      */ | ||||||
|     getRelations(name) { |     getRelations(name: string) { | ||||||
|         return this.getAttributes(RELATION, name); |         return this.getAttributes(RELATION, name); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {AttributeType} type - attribute type (label, relation, etc.) |      * @param type - attribute type (label, relation, etc.) | ||||||
|      * @param {string} name - attribute name |      * @param name - attribute name | ||||||
|      * @returns {boolean} true if note has an attribute with given type and name (including inherited) |      * @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(); |         const attributes = this.getAttributes(); | ||||||
| 
 | 
 | ||||||
|         return attributes.some(attr => attr.name === name && attr.type === type); |         return attributes.some(attr => attr.name === name && attr.type === type); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {AttributeType} type - attribute type (label, relation, etc.) |      * @param type - attribute type (label, relation, etc.) | ||||||
|      * @param {string} name - attribute name |      * @param name - attribute name | ||||||
|      * @returns {boolean} true if note has an attribute with given type and name (including inherited) |      * @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); |         return !!this.getOwnedAttribute(type, name); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {AttributeType} type - attribute type (label, relation, etc.) |      * @param type - attribute type (label, relation, etc.) | ||||||
|      * @param {string} name - attribute name |      * @param 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. |      * @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(); |         const attributes = this.getOwnedAttributes(); | ||||||
| 
 | 
 | ||||||
|         return attributes.find(attr => attr.name === name && attr.type === type); |         return attributes.find(attr => attr.name === name && attr.type === type); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {AttributeType} type - attribute type (label, relation, etc.) |      * @param type - attribute type (label, relation, etc.) | ||||||
|      * @param {string} name - attribute name |      * @param 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. |      * @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(); |         const attributes = this.getAttributes(); | ||||||
| 
 | 
 | ||||||
|         return attributes.find(attr => attr.name === name && attr.type === type); |         return attributes.find(attr => attr.name === name && attr.type === type); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {AttributeType} type - attribute type (label, relation, etc.) |      * @param type - attribute type (label, relation, etc.) | ||||||
|      * @param {string} name - attribute name |      * @param name - attribute name | ||||||
|      * @returns {string} attribute value of the given type and name or null if no such attribute exists. |      * @returns attribute value of the given type and name or null if no such attribute exists. | ||||||
|      */ |      */ | ||||||
|     getOwnedAttributeValue(type, name) { |     getOwnedAttributeValue(type: AttributeType, name: string) { | ||||||
|         const attr = this.getOwnedAttribute(type, name); |         const attr = this.getOwnedAttribute(type, name); | ||||||
| 
 | 
 | ||||||
|         return attr ? attr.value : null; |         return attr ? attr.value : null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {AttributeType} type - attribute type (label, relation, etc.) |      * @param type - attribute type (label, relation, etc.) | ||||||
|      * @param {string} name - attribute name |      * @param name - attribute name | ||||||
|      * @returns {string} attribute value of the given type and name or null if no such attribute exists. |      * @returns attribute value of the given type and name or null if no such attribute exists. | ||||||
|      */ |      */ | ||||||
|     getAttributeValue(type, name) { |     getAttributeValue(type: AttributeType, name: string) { | ||||||
|         const attr = this.getAttribute(type, name); |         const attr = this.getAttribute(type, name); | ||||||
| 
 | 
 | ||||||
|         return attr ? attr.value : null; |         return attr ? attr.value : null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name - label name |      * @param name - label name | ||||||
|      * @returns {boolean} true if label exists (excluding inherited) |      * @returns true if label exists (excluding inherited) | ||||||
|      */ |      */ | ||||||
|     hasOwnedLabel(name) { return this.hasOwnedAttribute(LABEL, name); } |     hasOwnedLabel(name: string) { | ||||||
|  |         return this.hasOwnedAttribute(LABEL, name); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name - label name |      * @param name - label name | ||||||
|      * @returns {boolean} true if label exists (including inherited) |      * @returns true if label exists (including inherited) | ||||||
|      */ |      */ | ||||||
|     hasLabel(name) { return this.hasAttribute(LABEL, name); } |     hasLabel(name: string) { return this.hasAttribute(LABEL, name); } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name - label name |      * @param name - label name | ||||||
|      * @returns {boolean} true if label exists (including inherited) and does not have "false" value. |      * @returns true if label exists (including inherited) and does not have "false" value. | ||||||
|      */ |      */ | ||||||
|     isLabelTruthy(name) { |     isLabelTruthy(name: string) { | ||||||
|         const label = this.getLabel(name); |         const label = this.getLabel(name); | ||||||
| 
 | 
 | ||||||
|         if (!label) { |         if (!label) { | ||||||
| @ -696,80 +698,79 @@ class FNote { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name - relation name |      * @param name - relation name | ||||||
|      * @returns {boolean} true if relation exists (excluding inherited) |      * @returns true if relation exists (excluding inherited) | ||||||
|      */ |      */ | ||||||
|     hasOwnedRelation(name) { return this.hasOwnedAttribute(RELATION, name); } |     hasOwnedRelation(name: string) { return this.hasOwnedAttribute(RELATION, name); } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name - relation name |      * @param name - relation name | ||||||
|      * @returns {boolean} true if relation exists (including inherited) |      * @returns true if relation exists (including inherited) | ||||||
|      */ |      */ | ||||||
|     hasRelation(name) { return this.hasAttribute(RELATION, name); } |     hasRelation(name: string) { return this.hasAttribute(RELATION, name); } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name - label name |      * @param name - label name | ||||||
|      * @returns {FAttribute} label if it exists, null otherwise |      * @returns label if it exists, null otherwise | ||||||
|      */ |      */ | ||||||
|     getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); } |     getOwnedLabel(name: string) { return this.getOwnedAttribute(LABEL, name); } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name - label name |      * @param name - label name | ||||||
|      * @returns {FAttribute} label if it exists, null otherwise |      * @returns label if it exists, null otherwise | ||||||
|      */ |      */ | ||||||
|     getLabel(name) { return this.getAttribute(LABEL, name); } |     getLabel(name: string) { return this.getAttribute(LABEL, name); } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name - relation name |      * @param name - relation name | ||||||
|      * @returns {FAttribute} relation if it exists, null otherwise |      * @returns relation if it exists, null otherwise | ||||||
|      */ |      */ | ||||||
|     getOwnedRelation(name) { return this.getOwnedAttribute(RELATION, name); } |     getOwnedRelation(name: string) { return this.getOwnedAttribute(RELATION, name); } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name - relation name |      * @param name - relation name | ||||||
|      * @returns {FAttribute} relation if it exists, null otherwise |      * @returns relation if it exists, null otherwise | ||||||
|      */ |      */ | ||||||
|     getRelation(name) { return this.getAttribute(RELATION, name); } |     getRelation(name: string) { return this.getAttribute(RELATION, name); } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name - label name |      * @param name - label name | ||||||
|      * @returns {string} label value if label exists, null otherwise |      * @returns label value if label exists, null otherwise | ||||||
|      */ |      */ | ||||||
|     getOwnedLabelValue(name) { return this.getOwnedAttributeValue(LABEL, name); } |     getOwnedLabelValue(name: string) { return this.getOwnedAttributeValue(LABEL, name); } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name - label name |      * @param name - label name | ||||||
|      * @returns {string} label value if label exists, null otherwise |      * @returns label value if label exists, null otherwise | ||||||
|      */ |      */ | ||||||
|     getLabelValue(name) { return this.getAttributeValue(LABEL, name); } |     getLabelValue(name: string) { return this.getAttributeValue(LABEL, name); } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name - relation name |      * @param name - relation name | ||||||
|      * @returns {string} relation value if relation exists, null otherwise |      * @returns relation value if relation exists, null otherwise | ||||||
|      */ |      */ | ||||||
|     getOwnedRelationValue(name) { return this.getOwnedAttributeValue(RELATION, name); } |     getOwnedRelationValue(name: string) { return this.getOwnedAttributeValue(RELATION, name); } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name - relation name |      * @param name - relation name | ||||||
|      * @returns {string} relation value if relation exists, null otherwise |      * @returns relation value if relation exists, null otherwise | ||||||
|      */ |      */ | ||||||
|     getRelationValue(name) { return this.getAttributeValue(RELATION, name); } |     getRelationValue(name: string) { return this.getAttributeValue(RELATION, name); } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} name |      * @param name | ||||||
|      * @returns {Promise<FNote>|null} target note of the relation or null (if target is empty or note was not found) |      * @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); |         const targets = await this.getRelationTargets(name); | ||||||
| 
 | 
 | ||||||
|         return targets.length > 0 ? targets[0] : null; |         return targets.length > 0 ? targets[0] : null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @param {string} [name] - relation name to filter |      * @param [name] - relation name to filter | ||||||
|      * @returns {Promise<FNote[]>} |  | ||||||
|      */ |      */ | ||||||
|     async getRelationTargets(name) { |     async getRelationTargets(name: string) { | ||||||
|         const relations = this.getRelations(name); |         const relations = this.getRelations(name); | ||||||
|         const targets = []; |         const targets = []; | ||||||
| 
 | 
 | ||||||
| @ -780,9 +781,6 @@ class FNote { | |||||||
|         return targets; |         return targets; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |  | ||||||
|      * @returns {FNote[]} |  | ||||||
|      */ |  | ||||||
|     getNotesToInheritAttributesFrom() { |     getNotesToInheritAttributesFrom() { | ||||||
|         const relations = [ |         const relations = [ | ||||||
|             ...this.getRelations('template'), |             ...this.getRelations('template'), | ||||||
| @ -818,7 +816,7 @@ class FNote { | |||||||
|         return promotedAttrs; |         return promotedAttrs; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     hasAncestor(ancestorNoteId, followTemplates = false, visitedNoteIds = null) { |     hasAncestor(ancestorNoteId: string, followTemplates = false, visitedNoteIds: Set<string> | null = null) { | ||||||
|         if (this.noteId === ancestorNoteId) { |         if (this.noteId === ancestorNoteId) { | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
| @ -860,8 +858,6 @@ class FNote { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get relations which target this note |      * Get relations which target this note | ||||||
|      * |  | ||||||
|      * @returns {FAttribute[]} |  | ||||||
|      */ |      */ | ||||||
|     getTargetRelations() { |     getTargetRelations() { | ||||||
|         return this.targetRelations |         return this.targetRelations | ||||||
| @ -870,8 +866,6 @@ class FNote { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get relations which target this note |      * Get relations which target this note | ||||||
|      * |  | ||||||
|      * @returns {Promise<FNote[]>} |  | ||||||
|      */ |      */ | ||||||
|     async getTargetRelationSourceNotes() { |     async getTargetRelationSourceNotes() { | ||||||
|         const targetRelations = this.getTargetRelations(); |         const targetRelations = this.getTargetRelations(); | ||||||
| @ -881,13 +875,11 @@ class FNote { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * @deprecated use getBlob() instead |      * @deprecated use getBlob() instead | ||||||
|      * @return {Promise<FBlob>} |  | ||||||
|      */ |      */ | ||||||
|     async getNoteComplement() { |     async getNoteComplement() { | ||||||
|         return this.getBlob(); |         return this.getBlob(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @return {Promise<FBlob>} */ |  | ||||||
|     async getBlob() { |     async getBlob() { | ||||||
|         return await this.froca.getBlob('notes', this.noteId); |         return await this.froca.getBlob('notes', this.noteId); | ||||||
|     } |     } | ||||||
| @ -896,8 +888,8 @@ class FNote { | |||||||
|         return `Note(noteId=${this.noteId}, title=${this.title})`; |         return `Note(noteId=${this.noteId}, title=${this.title})`; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     get dto() { |     get dto(): Omit<FNote, "froca"> { | ||||||
|         const dto = Object.assign({}, this); |         const dto = Object.assign({}, this) as any; | ||||||
|         delete dto.froca; |         delete dto.froca; | ||||||
| 
 | 
 | ||||||
|         return dto; |         return dto; | ||||||
| @ -918,7 +910,7 @@ class FNote { | |||||||
|         return labels.length > 0 ? labels[0].value : ""; |         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() { |     isJavaScript() { | ||||||
|         return (this.type === "code" || this.type === "file" || this.type === 'launcher') |         return (this.type === "code" || this.type === "file" || this.type === 'launcher') | ||||||
|             && (this.mime.startsWith("application/javascript") |             && (this.mime.startsWith("application/javascript") | ||||||
| @ -926,12 +918,12 @@ class FNote { | |||||||
|                 || this.mime === "text/javascript"); |                 || this.mime === "text/javascript"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {boolean} true if this note is HTML */ |     /** @returns true if this note is HTML */ | ||||||
|     isHtml() { |     isHtml() { | ||||||
|         return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html"; |         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() { |     getScriptEnv() { | ||||||
|         if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) { |         if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) { | ||||||
|             return "frontend"; |             return "frontend"; | ||||||
| @ -958,11 +950,9 @@ class FNote { | |||||||
|         if (env === "frontend") { |         if (env === "frontend") { | ||||||
|             const bundleService = (await import("../services/bundle.js")).default; |             const bundleService = (await import("../services/bundle.js")).default; | ||||||
|             return await bundleService.getAndExecuteBundle(this.noteId); |             return await bundleService.getAndExecuteBundle(this.noteId); | ||||||
|         } |         } else if (env === "backend") { | ||||||
|         else if (env === "backend") { |             await server.post(`script/run/${this.noteId}`); | ||||||
|             const resp = await server.post(`script/run/${this.noteId}`); |         } else { | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             throw new Error(`Unrecognized env type ${env} for note ${this.noteId}`); |             throw new Error(`Unrecognized env type ${env} for note ${this.noteId}`); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -1001,11 +991,9 @@ class FNote { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Provides note's date metadata. |      * Provides note's date metadata. | ||||||
|      * |  | ||||||
|      * @returns {Promise<{dateCreated: string, utcDateCreated: string, dateModified: string, utcDateModified: string}>} |  | ||||||
|      */ |      */ | ||||||
|     async getMetadata() { |     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>(); | const registeredClasses = new Set<string>(); | ||||||
| 
 | 
 | ||||||
| function createClassForColor(color: string) { | function createClassForColor(color: string | null) { | ||||||
|     if (!color?.trim()) { |     if (!color?.trim()) { | ||||||
|         return ""; |         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 FAttribute from "../entities/fattribute.js"; | ||||||
| import server from "./server.js"; | import server from "./server.js"; | ||||||
| import appContext from "../components/app_context.js"; | import appContext from "../components/app_context.js"; | ||||||
| import FBlob from "../entities/fblob.js"; | import FBlob, { FBlobRow } from "../entities/fblob.js"; | ||||||
| import FAttachment from "../entities/fattachment.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. |  * 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 |  * 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() { |     constructor() { | ||||||
|         this.initializedPromise = this.loadInitialTree(); |         this.initializedPromise = this.loadInitialTree(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async 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
 |         // 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 = {}; |         this.notes = {}; | ||||||
| 
 |  | ||||||
|         /** @type {Object.<string, FBranch>} */ |  | ||||||
|         this.branches = {}; |         this.branches = {}; | ||||||
| 
 |  | ||||||
|         /** @type {Object.<string, FAttribute>} */ |  | ||||||
|         this.attributes = {}; |         this.attributes = {}; | ||||||
| 
 |  | ||||||
|         /** @type {Object.<string, FAttachment>} */ |  | ||||||
|         this.attachments = {}; |         this.attachments = {}; | ||||||
| 
 |  | ||||||
|         /** @type {Object.<string, Promise<FBlob>>} */ |  | ||||||
|         this.blobPromises = {}; |         this.blobPromises = {}; | ||||||
| 
 | 
 | ||||||
|         this.addResp(resp); |         this.addResp(resp); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async loadSubTree(subTreeNoteId) { |     async loadSubTree(subTreeNoteId: string) { | ||||||
|         const resp = await server.get(`tree?subTreeNoteId=${subTreeNoteId}`); |         const resp = await server.get<SubtreeResponse>(`tree?subTreeNoteId=${subTreeNoteId}`); | ||||||
| 
 | 
 | ||||||
|         this.addResp(resp); |         this.addResp(resp); | ||||||
| 
 | 
 | ||||||
|         return this.notes[subTreeNoteId]; |         return this.notes[subTreeNoteId]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addResp(resp) { |     addResp(resp: SubtreeResponse) { | ||||||
|         const noteRows = resp.notes; |         const noteRows = resp.notes; | ||||||
|         const branchRows = resp.branches; |         const branchRows = resp.branches; | ||||||
|         const attributeRows = resp.attributes; |         const attributeRows = resp.attributes; | ||||||
| 
 | 
 | ||||||
|         const noteIdsToSort = new Set(); |         const noteIdsToSort = new Set<string>(); | ||||||
| 
 | 
 | ||||||
|         for (const noteRow of noteRows) { |         for (const noteRow of noteRows) { | ||||||
|             const {noteId} = noteRow; |             const {noteId} = noteRow; | ||||||
| @ -160,28 +173,28 @@ class Froca { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async reloadNotes(noteIds) { |     async reloadNotes(noteIds: string[]) { | ||||||
|         if (noteIds.length === 0) { |         if (noteIds.length === 0) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         noteIds = Array.from(new Set(noteIds)); // make noteIds unique
 |         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); |         this.addResp(resp); | ||||||
| 
 | 
 | ||||||
|         appContext.triggerEvent('notesReloaded', {noteIds}); |         appContext.triggerEvent('notesReloaded', {noteIds}); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async loadSearchNote(noteId) { |     async loadSearchNote(noteId: string) { | ||||||
|         const note = await this.getNote(noteId); |         const note = await this.getNote(noteId); | ||||||
| 
 | 
 | ||||||
|         if (!note || note.type !== 'search') { |         if (!note || note.type !== 'search') { | ||||||
|             return; |             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)) { |         if (!Array.isArray(searchResultNoteIds)) { | ||||||
|             throw new Error(`Search note '${note.noteId}' failed: ${searchResultNoteIds}`); |             throw new Error(`Search note '${note.noteId}' failed: ${searchResultNoteIds}`); | ||||||
| @ -217,8 +230,7 @@ class Froca { | |||||||
|         return {error}; |         return {error}; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {FNote[]} */ |     getNotesFromCache(noteIds: string[], silentNotFoundError = false): FNote[] { | ||||||
|     getNotesFromCache(noteIds, silentNotFoundError = false) { |  | ||||||
|         return noteIds.map(noteId => { |         return noteIds.map(noteId => { | ||||||
|             if (!this.notes[noteId] && !silentNotFoundError) { |             if (!this.notes[noteId] && !silentNotFoundError) { | ||||||
|                 console.trace(`Can't find note '${noteId}'`); |                 console.trace(`Can't find note '${noteId}'`); | ||||||
| @ -228,11 +240,10 @@ class Froca { | |||||||
|             else { |             else { | ||||||
|                 return this.notes[noteId]; |                 return this.notes[noteId]; | ||||||
|             } |             } | ||||||
|         }).filter(note => !!note); |         }).filter(note => !!note) as FNote[]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {Promise<FNote[]>} */ |     async getNotes(noteIds: string[], silentNotFoundError = false): Promise<FNote[]> { | ||||||
|     async getNotes(noteIds, silentNotFoundError = false) { |  | ||||||
|         noteIds = Array.from(new Set(noteIds)); // make unique
 |         noteIds = Array.from(new Set(noteIds)); // make unique
 | ||||||
|         const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]); |         const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]); | ||||||
| 
 | 
 | ||||||
| @ -246,18 +257,16 @@ class Froca { | |||||||
|             } else { |             } else { | ||||||
|                 return this.notes[noteId]; |                 return this.notes[noteId]; | ||||||
|             } |             } | ||||||
|         }).filter(note => !!note); |         }).filter(note => !!note) as FNote[]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {Promise<boolean>} */ |     async noteExists(noteId: string): Promise<boolean> { | ||||||
|     async noteExists(noteId) { |  | ||||||
|         const notes = await this.getNotes([noteId], true); |         const notes = await this.getNotes([noteId], true); | ||||||
| 
 | 
 | ||||||
|         return notes.length === 1; |         return notes.length === 1; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {Promise<FNote>} */ |     async getNote(noteId: string, silentNotFoundError = false): Promise<FNote | null> { | ||||||
|     async getNote(noteId, silentNotFoundError = false) { |  | ||||||
|         if (noteId === 'none') { |         if (noteId === 'none') { | ||||||
|             console.trace(`No 'none' note.`); |             console.trace(`No 'none' note.`); | ||||||
|             return null; |             return null; | ||||||
| @ -270,8 +279,7 @@ class Froca { | |||||||
|         return (await this.getNotes([noteId], silentNotFoundError))[0]; |         return (await this.getNotes([noteId], silentNotFoundError))[0]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {FNote|null} */ |     getNoteFromCache(noteId: string) { | ||||||
|     getNoteFromCache(noteId) { |  | ||||||
|         if (!noteId) { |         if (!noteId) { | ||||||
|             throw new Error("Empty noteId"); |             throw new Error("Empty noteId"); | ||||||
|         } |         } | ||||||
| @ -279,15 +287,13 @@ class Froca { | |||||||
|         return this.notes[noteId]; |         return this.notes[noteId]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {FBranch[]} */ |     getBranches(branchIds: string[], silentNotFoundError = false): FBranch[] { | ||||||
|     getBranches(branchIds, silentNotFoundError = false) { |  | ||||||
|         return branchIds |         return branchIds | ||||||
|             .map(branchId => this.getBranch(branchId, silentNotFoundError)) |             .map(branchId => this.getBranch(branchId, silentNotFoundError)) | ||||||
|             .filter(b => !!b); |             .filter(b => !!b) as FBranch[]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {FBranch} */ |     getBranch(branchId: string, silentNotFoundError = false) { | ||||||
|     getBranch(branchId, silentNotFoundError = false) { |  | ||||||
|         if (!(branchId in this.branches)) { |         if (!(branchId in this.branches)) { | ||||||
|             if (!silentNotFoundError) { |             if (!silentNotFoundError) { | ||||||
|                 logError(`Not existing branch '${branchId}'`); |                 logError(`Not existing branch '${branchId}'`); | ||||||
| @ -298,7 +304,7 @@ class Froca { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getBranchId(parentNoteId, childNoteId) { |     async getBranchId(parentNoteId: string, childNoteId: string) { | ||||||
|         if (childNoteId === 'root') { |         if (childNoteId === 'root') { | ||||||
|             return 'none_root'; |             return 'none_root'; | ||||||
|         } |         } | ||||||
| @ -314,8 +320,7 @@ class Froca { | |||||||
|         return child.parentToBranch[parentNoteId]; |         return child.parentToBranch[parentNoteId]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {Promise<FAttachment>} */ |     async getAttachment(attachmentId: string, silentNotFoundError = false) { | ||||||
|     async getAttachment(attachmentId, silentNotFoundError = false) { |  | ||||||
|         const attachment = this.attachments[attachmentId]; |         const attachment = this.attachments[attachmentId]; | ||||||
|         if (attachment) { |         if (attachment) { | ||||||
|             return 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
 |         // load all attachments for the given note even if one is requested, don't load one by one
 | ||||||
|         let attachmentRows; |         let attachmentRows; | ||||||
|         try { |         try { | ||||||
|             attachmentRows = await server.getWithSilentNotFound(`attachments/${attachmentId}/all`); |             attachmentRows = await server.getWithSilentNotFound<FAttachmentRow[]>(`attachments/${attachmentId}/all`); | ||||||
|         } |         } catch (e: any) { | ||||||
|         catch (e) { |  | ||||||
|             if (silentNotFoundError) { |             if (silentNotFoundError) { | ||||||
|                 logInfo(`Attachment '${attachmentId}' not found, but silentNotFoundError is enabled: ` + e.message); |                 logInfo(`Attachment '${attachmentId}' not found, but silentNotFoundError is enabled: ` + e.message); | ||||||
|                 return null; |                 return null; | ||||||
| @ -344,14 +348,12 @@ class Froca { | |||||||
|         return this.attachments[attachmentId]; |         return this.attachments[attachmentId]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {Promise<FAttachment[]>} */ |     async getAttachmentsForNote(noteId: string) { | ||||||
|     async getAttachmentsForNote(noteId) { |         const attachmentRows = await server.get<FAttachmentRow[]>(`notes/${noteId}/attachments`); | ||||||
|         const attachmentRows = await server.get(`notes/${noteId}/attachments`); |  | ||||||
|         return this.processAttachmentRows(attachmentRows); |         return this.processAttachmentRows(attachmentRows); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {FAttachment[]} */ |     processAttachmentRows(attachmentRows: FAttachmentRow[]): FAttachment[] { | ||||||
|     processAttachmentRows(attachmentRows) { |  | ||||||
|         return attachmentRows.map(attachmentRow => { |         return attachmentRows.map(attachmentRow => { | ||||||
|             let attachment; |             let attachment; | ||||||
| 
 | 
 | ||||||
| @ -367,22 +369,21 @@ class Froca { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** @returns {Promise<FBlob>} */ |     async getBlob(entityType: string, entityId: string) { | ||||||
|     async getBlob(entityType, entityId) { |  | ||||||
|         // I'm not sure why we're not using blobIds directly, it would save us this composite key ...
 |         // 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
 |         // perhaps one benefit is that we're always requesting the latest blob, not relying on perhaps faulty/slow
 | ||||||
|         // websocket update?
 |         // websocket update?
 | ||||||
|         const key = `${entityType}-${entityId}`; |         const key = `${entityType}-${entityId}`; | ||||||
| 
 | 
 | ||||||
|         if (!this.blobPromises[key]) { |         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)) |                 .then(row => new FBlob(row)) | ||||||
|                 .catch(e => console.error(`Cannot get blob for ${entityType} '${entityId}'`, e)); |                 .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
 |             // 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)
 |             // 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
 |             // 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) |                 () => setTimeout(() => this.blobPromises[key] = null, 1000) | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
| @ -391,6 +392,6 @@ class Froca { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const froca = new Froca(); | const froca = new FrocaImpl(); | ||||||
| 
 | 
 | ||||||
| export default froca; | 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. |  * 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. |  * as loading the tree which uses attributes heavily. | ||||||
|  */ |  */ | ||||||
| class NoteAttributeCache { | class NoteAttributeCache { | ||||||
|  |     attributes: Record<string, FAttribute[]>; | ||||||
|  |      | ||||||
|     constructor() { |     constructor() { | ||||||
|         /** @property {Object.<string, BAttribute[]>} */ |  | ||||||
|         this.attributes = {}; |         this.attributes = {}; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Elian Doran
						Elian Doran