client-ts: Port services/app/entities

This commit is contained in:
Elian Doran 2024-07-25 20:36:15 +03:00
parent 047c3eea69
commit 8fb6b64fa9
No known key found for this signature in database
8 changed files with 352 additions and 298 deletions

View File

@ -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);
}

View File

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

View File

@ -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;
}

View File

@ -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';
@ -475,14 +477,10 @@ class FNote {
return true;
}
/**
* @param {FAttribute[]} attributes
* @param {AttributeType} type
* @param {string} name
* @return {FAttribute[]}
/**
* @private
*/
__filterAttrs(attributes, type, name) {
__filterAttrs(attributes: FAttribute[], type?: AttributeType, name?: string): FAttribute[] {
this.__validateTypeName(type, name);
if (!type && !name) {
@ -494,15 +492,17 @@ class FNote {
} else if (name) {
return attributes.filter(attr => attr.name === name);
}
return [];
}
__getInheritableAttributes(path) {
__getInheritableAttributes(path: string[]) {
const attrs = this.__getCachedAttributes(path);
return attrs.filter(attr => attr.isInheritable);
}
__validateTypeName(type, name) {
__validateTypeName(type?: string, name?: string) {
if (type && type !== 'label' && type !== 'relation') {
throw new Error(`Unrecognized attribute type '${type}'. Only 'label' and 'relation' are possible values.`);
}
@ -516,18 +516,18 @@ class FNote {
}
/**
* @param {string} [name] - label name to filter
* @returns {FAttribute[]} all note's labels (attributes with type label), including inherited ones
* @param [name] - label name to filter
* @returns all note's labels (attributes with type label), including inherited ones
*/
getOwnedLabels(name) {
getOwnedLabels(name: string) {
return this.getOwnedAttributes(LABEL, name);
}
/**
* @param {string} [name] - label name to filter
* @returns {FAttribute[]} all note's labels (attributes with type label), including inherited ones
* @param [name] - label name to filter
* @returns all note's labels (attributes with type label), including inherited ones
*/
getLabels(name) {
getLabels(name: string) {
return this.getAttributes(LABEL, name);
}
@ -535,7 +535,7 @@ class FNote {
const iconClassLabels = this.getLabels('iconClass');
const workspaceIconClass = this.getWorkspaceIconClass();
if (iconClassLabels.length > 0) {
if (iconClassLabels && iconClassLabels.length > 0) {
return iconClassLabels[0].value;
}
else if (workspaceIconClass) {
@ -578,7 +578,7 @@ class FNote {
if (!childBranches) {
ws.logError(`No children for '${this.noteId}'. This shouldn't happen.`);
return;
return [];
}
// we're not checking hideArchivedNotes since that would mean we need to lazy load the child notes
@ -590,102 +590,104 @@ class FNote {
}
/**
* @param {string} [name] - relation name to filter
* @returns {FAttribute[]} all note's relations (attributes with type relation), including inherited ones
* @param [name] - relation name to filter
* @returns all note's relations (attributes with type relation), including inherited ones
*/
getOwnedRelations(name) {
getOwnedRelations(name: string) {
return this.getOwnedAttributes(RELATION, name);
}
/**
* @param {string} [name] - relation name to filter
* @returns {FAttribute[]} all note's relations (attributes with type relation), including inherited ones
* @param [name] - relation name to filter
* @returns all note's relations (attributes with type relation), including inherited ones
*/
getRelations(name) {
getRelations(name: string) {
return this.getAttributes(RELATION, name);
}
/**
* @param {AttributeType} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {boolean} true if note has an attribute with given type and name (including inherited)
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @returns true if note has an attribute with given type and name (including inherited)
*/
hasAttribute(type, name) {
hasAttribute(type: AttributeType, name: string) {
const attributes = this.getAttributes();
return attributes.some(attr => attr.name === name && attr.type === type);
}
/**
* @param {AttributeType} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {boolean} true if note has an attribute with given type and name (including inherited)
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @returns true if note has an attribute with given type and name (including inherited)
*/
hasOwnedAttribute(type, name) {
hasOwnedAttribute(type: AttributeType, name: string) {
return !!this.getOwnedAttribute(type, name);
}
/**
* @param {AttributeType} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {FAttribute} attribute of the given type and name. If there are more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @returns attribute of the given type and name. If there are more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
*/
getOwnedAttribute(type, name) {
getOwnedAttribute(type: AttributeType, name: string) {
const attributes = this.getOwnedAttributes();
return attributes.find(attr => attr.name === name && attr.type === type);
}
/**
* @param {AttributeType} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {FAttribute} attribute of the given type and name. If there are more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @returns attribute of the given type and name. If there are more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
*/
getAttribute(type, name) {
getAttribute(type: AttributeType, name: string) {
const attributes = this.getAttributes();
return attributes.find(attr => attr.name === name && attr.type === type);
}
/**
* @param {AttributeType} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {string} attribute value of the given type and name or null if no such attribute exists.
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @returns attribute value of the given type and name or null if no such attribute exists.
*/
getOwnedAttributeValue(type, name) {
getOwnedAttributeValue(type: AttributeType, name: string) {
const attr = this.getOwnedAttribute(type, name);
return attr ? attr.value : null;
}
/**
* @param {AttributeType} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {string} attribute value of the given type and name or null if no such attribute exists.
* @param type - attribute type (label, relation, etc.)
* @param name - attribute name
* @returns attribute value of the given type and name or null if no such attribute exists.
*/
getAttributeValue(type, name) {
getAttributeValue(type: AttributeType, name: string) {
const attr = this.getAttribute(type, name);
return attr ? attr.value : null;
}
/**
* @param {string} name - label name
* @returns {boolean} true if label exists (excluding inherited)
* @param name - label name
* @returns true if label exists (excluding inherited)
*/
hasOwnedLabel(name) { return this.hasOwnedAttribute(LABEL, name); }
hasOwnedLabel(name: string) {
return this.hasOwnedAttribute(LABEL, name);
}
/**
* @param {string} name - label name
* @returns {boolean} true if label exists (including inherited)
* @param name - label name
* @returns true if label exists (including inherited)
*/
hasLabel(name) { return this.hasAttribute(LABEL, name); }
hasLabel(name: string) { return this.hasAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {boolean} true if label exists (including inherited) and does not have "false" value.
* @param name - label name
* @returns true if label exists (including inherited) and does not have "false" value.
*/
isLabelTruthy(name) {
isLabelTruthy(name: string) {
const label = this.getLabel(name);
if (!label) {
@ -696,80 +698,79 @@ class FNote {
}
/**
* @param {string} name - relation name
* @returns {boolean} true if relation exists (excluding inherited)
* @param name - relation name
* @returns true if relation exists (excluding inherited)
*/
hasOwnedRelation(name) { return this.hasOwnedAttribute(RELATION, name); }
hasOwnedRelation(name: string) { return this.hasOwnedAttribute(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {boolean} true if relation exists (including inherited)
* @param name - relation name
* @returns true if relation exists (including inherited)
*/
hasRelation(name) { return this.hasAttribute(RELATION, name); }
hasRelation(name: string) { return this.hasAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {FAttribute} label if it exists, null otherwise
* @param name - label name
* @returns label if it exists, null otherwise
*/
getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); }
getOwnedLabel(name: string) { return this.getOwnedAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {FAttribute} label if it exists, null otherwise
* @param name - label name
* @returns label if it exists, null otherwise
*/
getLabel(name) { return this.getAttribute(LABEL, name); }
getLabel(name: string) { return this.getAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {FAttribute} relation if it exists, null otherwise
* @param name - relation name
* @returns relation if it exists, null otherwise
*/
getOwnedRelation(name) { return this.getOwnedAttribute(RELATION, name); }
getOwnedRelation(name: string) { return this.getOwnedAttribute(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {FAttribute} relation if it exists, null otherwise
* @param name - relation name
* @returns relation if it exists, null otherwise
*/
getRelation(name) { return this.getAttribute(RELATION, name); }
getRelation(name: string) { return this.getAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {string} label value if label exists, null otherwise
* @param name - label name
* @returns label value if label exists, null otherwise
*/
getOwnedLabelValue(name) { return this.getOwnedAttributeValue(LABEL, name); }
getOwnedLabelValue(name: string) { return this.getOwnedAttributeValue(LABEL, name); }
/**
* @param {string} name - label name
* @returns {string} label value if label exists, null otherwise
* @param name - label name
* @returns label value if label exists, null otherwise
*/
getLabelValue(name) { return this.getAttributeValue(LABEL, name); }
getLabelValue(name: string) { return this.getAttributeValue(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {string} relation value if relation exists, null otherwise
* @param name - relation name
* @returns relation value if relation exists, null otherwise
*/
getOwnedRelationValue(name) { return this.getOwnedAttributeValue(RELATION, name); }
getOwnedRelationValue(name: string) { return this.getOwnedAttributeValue(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {string} relation value if relation exists, null otherwise
* @param name - relation name
* @returns relation value if relation exists, null otherwise
*/
getRelationValue(name) { return this.getAttributeValue(RELATION, name); }
getRelationValue(name: string) { return this.getAttributeValue(RELATION, name); }
/**
* @param {string} name
* @returns {Promise<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`);
}
}

View File

@ -1,6 +1,6 @@
const registeredClasses = new Set<string>();
function createClassForColor(color: string) {
function createClassForColor(color: string | null) {
if (!color?.trim()) {
return "";
}

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

View File

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

View File

@ -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 = {};
}