2024-07-18 21:35:17 +03:00
import sql from "../services/sql.js" ;
import NoteSet from "../services/search/note_set.js" ;
import NotFoundError from "../errors/not_found_error.js" ;
2025-01-13 23:18:10 +02:00
import type BOption from "./entities/boption.js" ;
import type BNote from "./entities/bnote.js" ;
import type BEtapiToken from "./entities/betapi_token.js" ;
import type BAttribute from "./entities/battribute.js" ;
import type BBranch from "./entities/bbranch.js" ;
2024-07-18 21:35:17 +03:00
import BRevision from "./entities/brevision.js" ;
import BAttachment from "./entities/battachment.js" ;
2025-04-18 12:33:50 +03:00
import type { AttachmentRow , BlobRow , RevisionRow } from "@triliumnext/commons" ;
2024-07-18 21:35:17 +03:00
import BBlob from "./entities/bblob.js" ;
import BRecentNote from "./entities/brecent_note.js" ;
2025-01-13 23:18:10 +02:00
import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js" ;
2025-06-02 15:26:32 +00:00
import type BNoteEmbedding from "./entities/bnote_embedding.js" ;
2024-02-17 11:24:50 +02:00
interface AttachmentOpts {
includeContentLength? : boolean ;
}
2021-04-26 22:18:14 +02:00
2021-10-16 22:13:34 +02:00
/ * *
2023-06-29 23:32:19 +02:00
* Becca is a backend cache of all notes , branches , and attributes .
* There ' s a similar frontend cache Froca , and share cache Shaca .
2021-10-16 22:13:34 +02:00
* /
2024-04-02 23:00:04 +03:00
export default class Becca {
2024-02-17 01:24:37 +02:00
loaded ! : boolean ;
2024-02-17 00:44:44 +02:00
notes ! : Record < string , BNote > ;
2024-02-17 02:02:08 +02:00
branches ! : Record < string , BBranch > ;
childParentToBranch ! : Record < string , BBranch > ;
2024-02-17 01:19:49 +02:00
attributes ! : Record < string , BAttribute > ;
/** Points from attribute type-name to list of attributes */
attributeIndex ! : Record < string , BAttribute [ ] > ;
2024-02-17 01:00:38 +02:00
options ! : Record < string , BOption > ;
2024-02-17 01:03:38 +02:00
etapiTokens ! : Record < string , BEtapiToken > ;
2025-06-02 15:26:32 +00:00
noteEmbeddings ! : Record < string , BNoteEmbedding > ;
2024-02-17 00:44:44 +02:00
2024-02-17 11:24:50 +02:00
allNoteSetCache : NoteSet | null ;
2020-05-16 23:12:29 +02:00
constructor ( ) {
2020-05-22 09:38:30 +02:00
this . reset ( ) ;
2024-02-17 11:24:50 +02:00
this . allNoteSetCache = null ;
2020-05-22 09:38:30 +02:00
}
reset() {
2021-04-30 23:10:25 +02:00
this . notes = { } ;
this . branches = { } ;
2020-05-16 23:12:29 +02:00
this . childParentToBranch = { } ;
2024-12-22 15:45:54 +02:00
this . attributes = { } ;
2020-05-22 09:38:30 +02:00
this . attributeIndex = { } ;
2021-04-30 23:10:25 +02:00
this . options = { } ;
2022-01-10 17:09:20 +01:00
this . etapiTokens = { } ;
2025-06-02 15:26:32 +00:00
this . noteEmbeddings = { } ;
2020-05-16 23:12:29 +02:00
2022-09-19 23:12:12 +02:00
this . dirtyNoteSetCache ( ) ;
2020-05-16 23:12:29 +02:00
this . loaded = false ;
}
2022-12-23 15:07:48 +01:00
getRoot() {
2025-01-09 18:07:02 +02:00
return this . getNote ( "root" ) ;
2022-12-23 15:07:48 +01:00
}
2024-02-17 11:24:50 +02:00
findAttributes ( type : string , name : string ) : BAttribute [ ] {
2021-10-08 22:13:39 +02:00
name = name . trim ( ) . toLowerCase ( ) ;
2025-01-09 18:07:02 +02:00
if ( name . startsWith ( "#" ) || name . startsWith ( "~" ) ) {
2021-10-08 22:13:39 +02:00
name = name . substr ( 1 ) ;
}
return this . attributeIndex [ ` ${ type } - ${ name } ` ] || [ ] ;
2020-05-16 23:12:29 +02:00
}
2024-02-17 11:24:50 +02:00
findAttributesWithPrefix ( type : string , name : string ) : BAttribute [ ] {
2024-02-17 18:55:41 +02:00
const resArr : BAttribute [ ] [ ] = [ ] ;
2020-05-21 12:05:12 +02:00
const key = ` ${ type } - ${ name } ` ;
for ( const idx in this . attributeIndex ) {
if ( idx . startsWith ( key ) ) {
resArr . push ( this . attributeIndex [ idx ] ) ;
}
}
return resArr . flat ( ) ;
}
2020-05-16 23:12:29 +02:00
decryptProtectedNotes() {
for ( const note of Object . values ( this . notes ) ) {
2020-05-17 09:48:24 +02:00
note . decrypt ( ) ;
2020-05-16 23:12:29 +02:00
}
}
2020-05-17 09:48:24 +02:00
2024-02-17 11:24:50 +02:00
addNote ( noteId : string , note : BNote ) {
2021-10-06 19:59:29 +02:00
this . notes [ noteId ] = note ;
this . dirtyNoteSetCache ( ) ;
}
2024-02-17 11:24:50 +02:00
getNote ( noteId : string ) : BNote | null {
2021-04-26 22:18:14 +02:00
return this . notes [ noteId ] ;
}
2024-03-30 10:49:40 +02:00
getNoteOrThrow ( noteId : string ) : BNote {
2023-05-08 00:02:08 +02:00
const note = this . notes [ noteId ] ;
if ( ! note ) {
throw new NotFoundError ( ` Note ' ${ noteId } ' doesn't exist. ` ) ;
}
return note ;
}
2024-02-17 11:24:50 +02:00
getNotes ( noteIds : string [ ] , ignoreMissing : boolean = false ) : BNote [ ] {
2024-02-17 18:55:41 +02:00
const filteredNotes : BNote [ ] = [ ] ;
2021-05-08 11:20:20 +02:00
for ( const noteId of noteIds ) {
const note = this . notes [ noteId ] ;
if ( ! note ) {
2021-07-22 21:23:01 +02:00
if ( ignoreMissing ) {
continue ;
}
2021-05-08 11:20:20 +02:00
throw new Error ( ` Note ' ${ noteId } ' was not found in becca. ` ) ;
}
filteredNotes . push ( note ) ;
}
return filteredNotes ;
2021-04-26 22:24:55 +02:00
}
2024-02-17 11:24:50 +02:00
getBranch ( branchId : string ) : BBranch | null {
2021-04-26 22:18:14 +02:00
return this . branches [ branchId ] ;
}
2024-04-05 20:33:04 +03:00
getBranchOrThrow ( branchId : string ) : BBranch {
2023-05-08 00:02:08 +02:00
const branch = this . getBranch ( branchId ) ;
if ( ! branch ) {
throw new NotFoundError ( ` Branch ' ${ branchId } ' was not found in becca. ` ) ;
}
return branch ;
}
2024-04-09 21:43:42 +03:00
getAttribute ( attributeId : string ) : BAttribute | null {
2021-04-26 22:18:14 +02:00
return this . attributes [ attributeId ] ;
}
2024-02-17 11:24:50 +02:00
getAttributeOrThrow ( attributeId : string ) : BAttribute {
2023-05-08 00:02:08 +02:00
const attribute = this . getAttribute ( attributeId ) ;
if ( ! attribute ) {
throw new NotFoundError ( ` Attribute ' ${ attributeId } ' does not exist. ` ) ;
}
return attribute ;
}
2024-02-17 11:24:50 +02:00
getBranchFromChildAndParent ( childNoteId : string , parentNoteId : string ) : BBranch | null {
2020-05-17 09:48:24 +02:00
return this . childParentToBranch [ ` ${ childNoteId } - ${ parentNoteId } ` ] ;
}
2021-04-26 22:18:14 +02:00
2024-02-17 11:24:50 +02:00
getRevision ( revisionId : string ) : BRevision | null {
2024-07-18 22:30:16 +03:00
const row = sql . getRow < RevisionRow | null > ( "SELECT * FROM revisions WHERE revisionId = ?" , [ revisionId ] ) ;
2023-06-04 23:01:40 +02:00
return row ? new BRevision ( row ) : null ;
2021-04-26 22:18:14 +02:00
}
2021-05-01 21:52:22 +02:00
2024-04-06 22:32:03 +03:00
getRevisionOrThrow ( revisionId : string ) : BRevision {
const revision = this . getRevision ( revisionId ) ;
if ( ! revision ) {
throw new NotFoundError ( ` Revision ' ${ revisionId } ' has not been found. ` ) ;
}
return revision ;
}
2024-02-17 11:24:50 +02:00
getAttachment ( attachmentId : string , opts : AttachmentOpts = { } ) : BAttachment | null {
2023-05-21 18:14:17 +02:00
opts . includeContentLength = ! ! opts . includeContentLength ;
const query = opts . includeContentLength
2025-04-01 23:30:21 +03:00
? /*sql*/ ` SELECT attachments.*, LENGTH(blobs.content) AS contentLength
2024-12-22 15:45:54 +02:00
FROM attachments
JOIN blobs USING ( blobId )
WHERE attachmentId = ? AND isDeleted = 0 `
2025-04-01 23:30:21 +03:00
: /*sql*/ ` SELECT * FROM attachments WHERE attachmentId = ? AND isDeleted = 0 ` ;
2023-03-08 09:01:23 +01:00
2025-01-09 18:07:02 +02:00
return sql . getRows < AttachmentRow > ( query , [ attachmentId ] ) . map ( ( row ) = > new BAttachment ( row ) ) [ 0 ] ;
2023-03-08 09:01:23 +01:00
}
2024-02-17 11:24:50 +02:00
getAttachmentOrThrow ( attachmentId : string , opts : AttachmentOpts = { } ) : BAttachment {
2023-05-21 18:14:17 +02:00
const attachment = this . getAttachment ( attachmentId , opts ) ;
2023-05-08 00:02:08 +02:00
if ( ! attachment ) {
throw new NotFoundError ( ` Attachment ' ${ attachmentId } ' has not been found. ` ) ;
}
return attachment ;
}
2024-02-17 11:24:50 +02:00
getAttachments ( attachmentIds : string [ ] ) : BAttachment [ ] {
2025-01-09 18:07:02 +02:00
return sql . getManyRows < AttachmentRow > ( "SELECT * FROM attachments WHERE attachmentId IN (???) AND isDeleted = 0" , attachmentIds ) . map ( ( row ) = > new BAttachment ( row ) ) ;
2023-04-25 00:01:58 +02:00
}
2024-03-30 10:49:40 +02:00
getBlob ( entity : { blobId? : string } ) : BBlob | null {
if ( ! entity . blobId ) {
return null ;
}
2024-07-24 20:39:50 +03:00
const row = sql . getRow < BlobRow | null > ( "SELECT *, LENGTH(content) AS contentLength FROM blobs WHERE blobId = ?" , [ entity . blobId ] ) ;
2023-05-05 16:37:39 +02:00
return row ? new BBlob ( row ) : null ;
}
2024-02-17 11:24:50 +02:00
getOption ( name : string ) : BOption | null {
2021-05-01 21:52:22 +02:00
return this . options [ name ] ;
}
2021-05-02 12:02:32 +02:00
2024-02-17 11:24:50 +02:00
getEtapiTokens ( ) : BEtapiToken [ ] {
2022-01-10 17:09:20 +01:00
return Object . values ( this . etapiTokens ) ;
}
2024-02-17 11:24:50 +02:00
getEtapiToken ( etapiTokenId : string ) : BEtapiToken | null {
2022-01-10 17:09:20 +01:00
return this . etapiTokens [ etapiTokenId ] ;
}
2024-03-30 10:49:40 +02:00
getEntity < T extends AbstractBeccaEntity < T > > ( entityName : string , entityId : string ) : AbstractBeccaEntity < T > | null {
2021-05-02 12:02:32 +02:00
if ( ! entityName || ! entityId ) {
return null ;
}
2025-01-09 18:07:02 +02:00
if ( entityName === "revisions" ) {
2023-06-04 23:01:40 +02:00
return this . getRevision ( entityId ) ;
2025-01-09 18:07:02 +02:00
} else if ( entityName === "attachments" ) {
2023-03-16 12:17:55 +01:00
return this . getAttachment ( entityId ) ;
2021-08-07 21:21:30 +02:00
}
2025-01-09 18:07:02 +02:00
const camelCaseEntityName = entityName . toLowerCase ( ) . replace ( /(_[a-z])/g , ( group ) = > group . toUpperCase ( ) . replace ( "_" , "" ) ) ;
2021-05-02 12:02:32 +02:00
2023-02-10 09:16:32 +01:00
if ( ! ( camelCaseEntityName in this ) ) {
throw new Error ( ` Unknown entity name ' ${ camelCaseEntityName } ' (original argument ' ${ entityName } ') ` ) ;
}
2024-02-17 11:24:50 +02:00
return ( this as any ) [ camelCaseEntityName ] [ entityId ] ;
2021-05-02 12:02:32 +02:00
}
2021-05-02 19:59:16 +02:00
2024-04-05 20:26:45 +03:00
getRecentNotesFromQuery ( query : string , params : string [ ] = [ ] ) : BRecentNote [ ] {
2024-07-18 22:30:16 +03:00
const rows = sql . getRows < BRecentNote > ( query , params ) ;
2025-01-09 18:07:02 +02:00
return rows . map ( ( row ) = > new BRecentNote ( row ) ) ;
2021-05-02 19:59:16 +02:00
}
2024-04-06 22:32:03 +03:00
getRevisionsFromQuery ( query : string , params : string [ ] = [ ] ) : BRevision [ ] {
2024-02-17 11:24:50 +02:00
const rows = sql . getRows < RevisionRow > ( query , params ) ;
2025-01-09 18:07:02 +02:00
return rows . map ( ( row ) = > new BRevision ( row ) ) ;
2021-05-02 19:59:16 +02:00
}
2021-10-06 19:59:29 +02:00
/** Should be called when the set of all non-skeleton notes changes (added/removed) */
dirtyNoteSetCache() {
this . allNoteSetCache = null ;
}
getAllNoteSet() {
// caching this since it takes 10s of milliseconds to fill this initial NoteSet for many notes
if ( ! this . allNoteSetCache ) {
2025-05-28 19:03:53 +03:00
const allNotes : BNote [ ] = [ ] ;
2021-10-06 19:59:29 +02:00
2024-02-17 11:24:50 +02:00
for ( const noteId in this . notes ) {
const note = this . notes [ noteId ] ;
2021-10-06 19:59:29 +02:00
// in the process of loading data sometimes we create "skeleton" note instances which are expected to be filled later
// in case of inconsistent data this might not work and search will then crash on these
if ( note . type !== undefined ) {
allNotes . push ( note ) ;
}
}
2021-10-07 07:46:13 +02:00
this . allNoteSetCache = new NoteSet ( allNotes ) ;
2021-10-06 19:59:29 +02:00
}
2021-10-07 07:46:13 +02:00
return this . allNoteSetCache ;
2021-10-06 19:59:29 +02:00
}
2020-05-16 23:12:29 +02:00
}
2024-04-02 23:00:04 +03:00
/ * *
* This interface contains the data that is shared across all the objects of a given derived class of { @link AbstractBeccaEntity } .
2024-12-22 15:45:54 +02:00
* For example , all BAttributes will share their content , but all BBranches will have another set of this data .
2024-04-02 23:00:04 +03:00
* /
export interface ConstructorData < T extends AbstractBeccaEntity < T > > {
primaryKeyName : string ;
entityName : string ;
hashedProperties : ( keyof T ) [ ] ;
2024-04-06 22:32:03 +03:00
}
export interface NotePojo {
noteId : string ;
title? : string ;
isProtected? : boolean ;
type : string ;
mime : string ;
blobId? : string ;
isDeleted : boolean ;
dateCreated? : string ;
dateModified? : string ;
utcDateCreated : string ;
utcDateModified? : string ;
2024-12-22 15:45:54 +02:00
}