chore(core): introduce becca_easy_mocking and becca_mocking

This commit is contained in:
Elian Doran
2026-03-26 20:24:44 +02:00
parent 11461221ba
commit 41a7d6738b
7 changed files with 232 additions and 221 deletions

View File

@@ -1,134 +1,3 @@
import { NoteType } from "@triliumnext/commons";
import BAttachment from "../becca/entities/battachment.js";
import BAttribute from "../becca/entities/battribute.js";
import BBranch from "../becca/entities/bbranch.js";
import BNote from "../becca/entities/bnote.js";
import utils, { randomString } from "../services/utils.js";
type AttributeDefinitions = { [key in `#${string}`]: string; };
type RelationDefinitions = { [key in `~${string}`]: string; };
interface NoteDefinition extends AttributeDefinitions, RelationDefinitions {
id?: string | undefined;
title?: string;
content?: string;
type?: NoteType;
mime?: string;
children?: NoteDefinition[];
attachments?: {
title: string;
role: string;
mime: string;
}[];
}
/**
* Creates the given notes with the given title and optionally one or more attributes.
*
* For a label to be created, simply pass on a key prefixed with `#` and any desired value.
*
* The notes and attributes will be injected in the froca.
*
* @param notes
* @returns an array containing the IDs of the created notes.
* @example
* buildNotes([
* { title: "A", "#startDate": "2025-05-05" },
* { title: "B", "#startDate": "2025-05-07" }
* ]);
*/
export function buildNotes(notes: NoteDefinition[]) {
const ids: string[] = [];
for (const noteDef of notes) {
ids.push(buildNote(noteDef).noteId);
}
return ids;
}
export function buildNote(noteDef: NoteDefinition) {
const note = new BNote({
noteId: noteDef.id ?? utils.randomString(12),
title: noteDef.title ?? "New note",
type: noteDef.type ?? "text",
mime: noteDef.mime ?? "text/html",
isProtected: false,
blobId: ""
});
// Handle content.
if (noteDef.content !== undefined) {
note.getContent = () => noteDef.content!;
}
// Handle children
if (noteDef.children) {
for (const childDef of noteDef.children) {
const childNote = buildNote(childDef);
new BBranch({
noteId: childNote.noteId,
parentNoteId: note.noteId,
branchId: `${note.noteId}_${childNote.noteId}`
});
}
}
// Handle labels and relations.
let position = 0;
for (const [ key, value ] of Object.entries(noteDef)) {
const attributeId = utils.randomString(12);
const name = key.substring(1);
let attribute: BAttribute | null = null;
if (key.startsWith("#")) {
attribute = new BAttribute({
noteId: note.noteId,
attributeId,
type: "label",
name,
value,
position,
isInheritable: false
});
}
if (key.startsWith("~")) {
attribute = new BAttribute({
noteId: note.noteId,
attributeId,
type: "relation",
name,
value,
position,
isInheritable: false
});
}
if (!attribute) {
continue;
}
position++;
}
// Handle attachments.
if (noteDef.attachments) {
const allAttachments: BAttachment[] = [];
for (const { title, role, mime } of noteDef.attachments) {
const attachment = new BAttachment({
attachmentId: randomString(10),
ownerId: note.noteId,
title,
role,
mime
});
allAttachments.push(attachment);
}
note.getAttachmentsByRole = (role) => allAttachments.filter(a => a.role === role);
}
return note;
}
import { becca_easy_mocking } from "@triliumnext/core";
export const buildNote = becca_easy_mocking.buildNote;
export const buildNotes = becca_easy_mocking.buildNotes;

View File

@@ -1,77 +1,2 @@
import type { NoteRow, NoteType } from "@triliumnext/commons";
import { SearchResult } from "@triliumnext/core";
import randtoken from "rand-token";
import becca from "../becca/becca.js";
import BAttribute from "../becca/entities/battribute.js";
import BBranch from "../becca/entities/bbranch.js";
import BNote from "../becca/entities/bnote.js";
randtoken.generator({ source: "crypto" });
export function findNoteByTitle(searchResults: Array<SearchResult>, title: string): BNote | undefined {
return searchResults.map((sr) => becca.notes[sr.noteId]).find((note) => note.title === title);
}
export class NoteBuilder {
note: BNote;
constructor(note: BNote) {
this.note = note;
}
label(name: string, value = "", isInheritable = false) {
new BAttribute({
attributeId: id(),
noteId: this.note.noteId,
type: "label",
isInheritable,
name,
value
});
return this;
}
relation(name: string, targetNote: BNote) {
new BAttribute({
attributeId: id(),
noteId: this.note.noteId,
type: "relation",
name,
value: targetNote.noteId
});
return this;
}
child(childNoteBuilder: NoteBuilder, prefix = "") {
new BBranch({
branchId: id(),
noteId: childNoteBuilder.note.noteId,
parentNoteId: this.note.noteId,
prefix,
notePosition: 10
});
return this;
}
}
export function id() {
return randtoken.generate(10);
}
export function note(title: string, extraParams: Partial<NoteRow> = {}) {
const row = Object.assign(
{
noteId: id(),
title,
type: "text" as NoteType,
mime: "text/html"
},
extraParams
);
const note = new BNote(row);
return new NoteBuilder(note);
}
import { becca_mocking } from "@triliumnext/core";
export const NoteBuilder = becca_mocking.NoteBuilder;

View File

@@ -98,6 +98,9 @@ export { t } from "i18next";
export type { RequestProvider, ExecOpts, CookieJar } from "./services/request";
export type * from "./meta";
export * as becca_easy_mocking from "./test/becca_easy_mocking";
export * as becca_mocking from "./test/becca_mocking";
export async function initializeCore({ dbConfig, executionContext, crypto, translations, messaging, request, schema, extraAppInfo, platform }: {
dbConfig: SqlServiceParams,
executionContext: ExecutionContext,

View File

@@ -1,5 +1,9 @@
export default {
saveImageToAttachment(noteId: string, imageBuffer: Uint8Array, title: string, b1: boolean, b2: boolean) {
console.warn("Image save ignored", noteId, title);
return {
attachmentId: null,
};
}
}

View File

@@ -710,22 +710,22 @@ describe("Search", () => {
// Create a moderate-sized dataset to test performance
const countries = ["Austria", "Belgium", "Croatia", "Denmark", "Estonia", "Finland", "Germany", "Hungary", "Ireland", "Japan"];
const europeanCountries = note("Europe");
countries.forEach(country => {
europeanCountries.child(note(country).label("type", "country").label("continent", "Europe"));
});
rootNote.child(europeanCountries);
const searchContext = new SearchContext();
const startTime = Date.now();
// Perform a search that exercises multiple features
const searchResults = searchService.findResultsWithQuery("#type=country AND continent", searchContext);
const endTime = Date.now();
const duration = endTime - startTime;
// Search should complete in under 1 second for reasonable dataset
expect(duration).toBeLessThan(1000);
expect(searchResults.length).toEqual(10);
@@ -748,14 +748,14 @@ describe("Search", () => {
// Get note titles in result order
const resultTitles = searchResults.map(r => becca.notes[r.noteId].title);
// Find all exact matches (contain "analysis")
const exactMatchIndices = resultTitles.map((title, index) =>
const exactMatchIndices = resultTitles.map((title, index) =>
title.toLowerCase().includes("analysis") ? index : -1
).filter(index => index !== -1);
// Find all fuzzy matches (contain typos)
const fuzzyMatchIndices = resultTitles.map((title, index) =>
const fuzzyMatchIndices = resultTitles.map((title, index) =>
(title.includes("Anaylsis") || title.includes("Anlaysis")) ? index : -1
).filter(index => index !== -1);
@@ -765,7 +765,7 @@ describe("Search", () => {
// CRITICAL: All exact matches must appear before all fuzzy matches
const lastExactIndex = Math.max(...exactMatchIndices);
const firstFuzzyIndex = Math.min(...fuzzyMatchIndices);
expect(lastExactIndex).toBeLessThan(firstFuzzyIndex);
});

View File

@@ -0,0 +1,134 @@
import { NoteType } from "@triliumnext/commons";
import BAttachment from "../becca/entities/battachment.js";
import BAttribute from "../becca/entities/battribute.js";
import BBranch from "../becca/entities/bbranch.js";
import BNote from "../becca/entities/bnote.js";
import { randomString } from "../services/utils/index.js";
type AttributeDefinitions = { [key in `#${string}`]: string; };
type RelationDefinitions = { [key in `~${string}`]: string; };
interface NoteDefinition extends AttributeDefinitions, RelationDefinitions {
id?: string | undefined;
title?: string;
content?: string;
type?: NoteType;
mime?: string;
children?: NoteDefinition[];
attachments?: {
title: string;
role: string;
mime: string;
}[];
}
/**
* Creates the given notes with the given title and optionally one or more attributes.
*
* For a label to be created, simply pass on a key prefixed with `#` and any desired value.
*
* The notes and attributes will be injected in the froca.
*
* @param notes
* @returns an array containing the IDs of the created notes.
* @example
* buildNotes([
* { title: "A", "#startDate": "2025-05-05" },
* { title: "B", "#startDate": "2025-05-07" }
* ]);
*/
export function buildNotes(notes: NoteDefinition[]) {
const ids: string[] = [];
for (const noteDef of notes) {
ids.push(buildNote(noteDef).noteId);
}
return ids;
}
export function buildNote(noteDef: NoteDefinition) {
const note = new BNote({
noteId: noteDef.id ?? randomString(12),
title: noteDef.title ?? "New note",
type: noteDef.type ?? "text",
mime: noteDef.mime ?? "text/html",
isProtected: false,
blobId: ""
});
// Handle content.
if (noteDef.content !== undefined) {
note.getContent = () => noteDef.content!;
}
// Handle children
if (noteDef.children) {
for (const childDef of noteDef.children) {
const childNote = buildNote(childDef);
new BBranch({
noteId: childNote.noteId,
parentNoteId: note.noteId,
branchId: `${note.noteId}_${childNote.noteId}`
});
}
}
// Handle labels and relations.
let position = 0;
for (const [ key, value ] of Object.entries(noteDef)) {
const attributeId = randomString(12);
const name = key.substring(1);
let attribute: BAttribute | null = null;
if (key.startsWith("#")) {
attribute = new BAttribute({
noteId: note.noteId,
attributeId,
type: "label",
name,
value,
position,
isInheritable: false
});
}
if (key.startsWith("~")) {
attribute = new BAttribute({
noteId: note.noteId,
attributeId,
type: "relation",
name,
value,
position,
isInheritable: false
});
}
if (!attribute) {
continue;
}
position++;
}
// Handle attachments.
if (noteDef.attachments) {
const allAttachments: BAttachment[] = [];
for (const { title, role, mime } of noteDef.attachments) {
const attachment = new BAttachment({
attachmentId: randomString(10),
ownerId: note.noteId,
title,
role,
mime
});
allAttachments.push(attachment);
}
note.getAttachmentsByRole = (role) => allAttachments.filter(a => a.role === role);
}
return note;
}

View File

@@ -0,0 +1,76 @@
import type { NoteRow, NoteType } from "@triliumnext/commons";
import becca from "../becca/becca.js";
import BAttribute from "../becca/entities/battribute.js";
import BBranch from "../becca/entities/bbranch.js";
import BNote from "../becca/entities/bnote.js";
import { randomString } from "../services/utils/index.js";
import SearchResult from "../services/search/search_result.js";
export function findNoteByTitle(searchResults: Array<SearchResult>, title: string): BNote | undefined {
return searchResults.map((sr) => becca.notes[sr.noteId]).find((note) => note.title === title);
}
export class NoteBuilder {
note: BNote;
constructor(note: BNote) {
this.note = note;
}
label(name: string, value = "", isInheritable = false) {
new BAttribute({
attributeId: id(),
noteId: this.note.noteId,
type: "label",
isInheritable,
name,
value
});
return this;
}
relation(name: string, targetNote: BNote) {
new BAttribute({
attributeId: id(),
noteId: this.note.noteId,
type: "relation",
name,
value: targetNote.noteId
});
return this;
}
child(childNoteBuilder: NoteBuilder, prefix = "") {
new BBranch({
branchId: id(),
noteId: childNoteBuilder.note.noteId,
parentNoteId: this.note.noteId,
prefix,
notePosition: 10
});
return this;
}
}
export function id() {
return randomString(10);
}
export function note(title: string, extraParams: Partial<NoteRow> = {}) {
const row = Object.assign(
{
noteId: id(),
title,
type: "text" as NoteType,
mime: "text/html"
},
extraParams
);
const note = new BNote(row);
return new NoteBuilder(note);
}