πŸ“„ src/models/test/orm.test.ts
D-OPEN SOVEREIGN
1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library β€” The World Library
4 * 
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 * 
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15import { describe, it, expect, beforeEach } from 'vitest';
16import { Book, BookInstance, NewBookWithId, type BookExtendedCols, type BookSlimCols, type BookProvCols } from '../generated-src/Book.ts';
17import { Person, NewPersonWithId } from '../generated-src/Person.ts';
18import { QualityScore, BookMethod } from '@the_library/db_schemas/enums';
19import { setDefaultLanguage } from '../orm.ts';
20import { clear, configureDb, setReactivitySystem, LruCache } from '../db.ts';
21import { DSafeCheck } from '../dsafe.ts';
22import { PatchInstance } from '../patcher.ts';
23import { PatchOperation, PatchAction, LANG_ID_ENCODE, type EditRelationObj } from '../binary.ts';
24import type { InstanceData } from '../interfaces.ts';
25
26beforeEach(() => {
27    clear();
28    setDefaultLanguage('eng');
29    configureDb({ cacheLimit: 10_000 });
30    PatchInstance.clearQueue();
31});
32
33describe('Generated Book model', () => {
34    it('the Bible Problem languages are mapped (no lang-agnostic collisions)', async () => {
35        // amh/gez patches must NOT fall back to langId 0
36        expect(LANG_ID_ENCODE['amh']).toBe(23);
37        expect(LANG_ID_ENCODE['gez']).toBe(24);
38        expect(LANG_ID_ENCODE['tir']).toBe(25);
39        expect(LANG_ID_ENCODE['orm']).toBe(26);
40    });
41
42    it('per-language string props coexist (the Bible Problem)', () => {
43        const book = NewBookWithId(10);
44        book.setTitle('The Bible', 'eng');
45        book.setTitle('La Bible', 'fra');
46        book.setOriginalLang('lat', 'eng');
47        book.setOriginalLang('gez', 'amh');
48
49        expect(book.getTitle('eng')).toBe('The Bible');
50        expect(book.getTitle('fra')).toBe('La Bible');
51        expect(book.getOriginalLang('eng')).toBe('lat');
52        expect(book.getOriginalLang('amh')).toBe('gez');
53    });
54
55    it('enum props read/write through getProp with userLang default', () => {
56        setDefaultLanguage('fra');
57        const book = NewBookWithId(11);
58        book.textQuality = QualityScore.Excellent;
59        expect(book.textQuality).toBe(QualityScore.Excellent);
60        expect(book.getTextQuality('eng')).toBe(QualityScore.Excellent);
61    });
62
63    it('boolean props (safe/deleted) and DSafeCheck', () => {
64        const book = NewBookWithId(12);
65        // `safe` defaults true β€” an unreviewed record must read as dsafe-passing
66        // until moderation (or a corpus sidecar) explicitly flags it unsafe.
67        expect(book.safe).toBe(true);
68        expect(DSafeCheck(book)).toBe(true);
69        book.safe = false;
70        expect(book.safe).toBe(false);
71        expect(DSafeCheck(book)).toBe(false);
72        book.safe = true;
73        book.deleted = true;
74        expect(DSafeCheck(book)).toBe(false);
75    });
76
77    it('Load returns the same identity; Has reflects presence', () => {
78        const book = NewBookWithId(13);
79        expect(Book.Has(13)).toBe(true);
80        expect(Book.Load(13)).toBe(book);
81        expect(Book.Has(999)).toBe(false);
82        expect(() => Book.Load(999)).toThrow();
83    });
84
85    it('authors relation propagates to the Person inverse side', () => {
86        const book = NewBookWithId(14);
87        const person = NewPersonWithId(5);
88
89        book.addAuthor(5);
90        expect(book.authorsIds).toEqual([5]);
91        expect(book.hasAuthor(5)).toBe(true);
92        expect(person.inAuthorOfIds).toEqual([14]);
93        expect(book.authors[0]).toBe(person);
94
95        book.removeAuthor(5);
96        expect(book.authorsIds).toEqual([]);
97        expect(person.inAuthorOfIds).toEqual([]);
98    });
99
100    it('a relation edit enqueues exactly ONE canonical patch (no double gas)', () => {
101        const book = NewBookWithId(14);
102        NewPersonWithId(5);
103        PatchInstance.clearQueue();
104
105        book.addAuthor(5);
106
107        const relPatches = PatchInstance.patches
108            .filter(p => (p as EditRelationObj).operation === PatchOperation.EditRelation) as EditRelationObj[];
109        expect(relPatches.length).toBe(1);
110        // canonical = the has side (Book.authors, relationId 12)
111        expect(relPatches[0]).toMatchObject({
112            objectType: Book.type, objectId: 14, relationId: 1,
113            action: PatchAction.Add, targetId: 5,
114        });
115    });
116
117    it('in-side edits emit the same canonical has-side patch', () => {
118        const book = NewBookWithId(15);
119        const person = NewPersonWithId(6);
120        PatchInstance.clearQueue();
121
122        person.addIntoAuthorOf(15);
123
124        // graph coherent on both sides
125        expect(person.inAuthorOfIds).toEqual([15]);
126        expect(book.authorsIds).toEqual([6]);
127
128        // single patch, addressed to the has side (Book.authors)
129        const relPatches = PatchInstance.patches
130            .filter(p => (p as EditRelationObj).operation === PatchOperation.EditRelation) as EditRelationObj[];
131        expect(relPatches.length).toBe(1);
132        expect(relPatches[0]).toMatchObject({
133            objectType: Book.type, objectId: 15, relationId: 1,
134            action: PatchAction.Add, targetId: 6,
135        });
136    });
137
138    it('in-side edits stay coherent even when the target is NOT loaded', () => {
139        const person = NewPersonWithId(7);
140        PatchInstance.clearQueue();
141
142        person.addIntoAuthorOf(999); // Book 999 not in memory
143
144        expect(person.inAuthorOfIds).toEqual([999]);
145        const relPatches = PatchInstance.patches as EditRelationObj[];
146        expect(relPatches.length).toBe(1);
147        expect(relPatches[0]).toMatchObject({
148            objectType: Book.type, objectId: 999, relationId: 1, targetId: 7,
149        });
150    });
151
152    it('replayed canonical patches keep loaded inverse records coherent', () => {
153        const book = NewBookWithId(16);
154        const person = NewPersonWithId(8);
155
156        // Simulates a patch arriving from the blockchain / IndexedDB replay
157        book.patch({
158            operation: PatchOperation.EditRelation,
159            objectType: Book.type, objectId: 16, langId: 0,
160            relationId: 1, action: PatchAction.Add, targetId: 8,
161        } as EditRelationObj);
162
163        expect(book.authorsIds).toEqual([8]);
164        expect(person.inAuthorOfIds).toEqual([16]);
165    });
166});
167
168describe('Merge methods (sidecar hydration)', () => {
169    const slimCols: BookSlimCols = {
170        ids: new Uint16Array([20]),
171        titles: ['Permaculture Basics'],
172        originalLangs: ['eng'],
173        previewTxIds: ['preview-tx'],
174        ageGradeBands: new Uint8Array([5]),
175        safes: new Uint8Array([1]),
176        deleteds: new Uint8Array([0]),
177        defaultLanguages: ['eng'],
178        defaultDownloadTxIds: ['dl-tx'],
179        defaultMethod: new Uint8Array([BookMethod.Scan]),
180        defaultIsAiTranslation: new Uint8Array([0]),
181        defaultAiEngines: [null],
182        defaultAddedAt: new Uint32Array([1717000000]),
183        defaultNbPages: new Uint16Array([240]),
184        defaultSizeBytes: new Uint32Array([5_000_000]),
185        defaultIsbns: [null],
186        defaultPublishers: [null],
187        defaultPublicationDates: [null],
188        defaultPublishedAts: [null],
189        defaultQualityScore: new Uint8Array([QualityScore.Good]),
190    };
191
192    const fullInstance: InstanceData = {
193        downloadTxId: 'dl-tx-full', method: BookMethod.OCR, isAiTranslation: false,
194        aiEngine: null, addedAt: 1717000000, nbPages: 240, sizeBytes: 5_000_000,
195        isbn: null, publisher: 'Pond Press', publicationDate: '2020',
196        publishedAt: null, qualityScore: QualityScore.Good, isDefault: true,
197    };
198
199    const extendedCols = (): BookExtendedCols => ({
200        ids: new Uint16Array([20]),
201        descriptions: ['A practical guide.'],
202        prerequisites: [null],
203        deweyDecimals: ['631.58'],
204        locCallNumbers: [null],
205        copyrightStatus: new Uint8Array([1]),
206        textQuality: new Uint8Array([QualityScore.VeryGood]),
207        illustrations: new Uint8Array([QualityScore.Good]),
208        educationQuality: new Uint8Array([QualityScore.Excellent]),
209        practicality: new Uint8Array([QualityScore.Excellent]),
210        accessibilityLevel: new Uint8Array([QualityScore.Fair]),
211        teacherMaterials: new Uint8Array([1]),
212        pedagogicalScaffolding: new Uint8Array([0]),
213        clarityOfExplanation: new Uint8Array([0]),
214        workedExamples: new Uint8Array([0]),
215        exercisesAndAssessment: new Uint8Array([0]),
216        selfStudyReadiness: new Uint8Array([0]),
217        referenceApparatus: new Uint8Array([0]),
218        safetyGuidance: new Uint8Array([0]),
219        contentCurrency: new Uint8Array([0]),
220        ecoRegeneration: new Uint8Array([QualityScore.Excellent]),
221        communityLiving: new Uint8Array([0]),
222        promotesPeace: new Uint8Array([0]),
223        businessDevelopment: new Uint8Array([0]),
224        authorIds: [[5, 7]],
225        instances: [[fullInstance]],
226    });
227
228    it('mergeSlim populates base props and the inline default instance', () => {
229        const book = NewBookWithId(20);
230        book.mergeSlim(slimCols, 0, 'eng');
231
232        expect(book.getTitle('eng')).toBe('Permaculture Basics');
233        expect(book.previewTxId).toBe('preview-tx');
234        expect(book.safe).toBe(true);
235        expect(book.deleted).toBe(false);
236        expect(book.ageGradeBand).toBe(5);
237
238        const inst = book.getDefaultInstance('eng');
239        expect(inst).toBeDefined();
240        expect(inst!.downloadTxId).toBe('dl-tx');
241        expect(inst!.nbPages).toBe(240);
242        expect(inst!.isDefault).toBe(true);
243    });
244
245    it('mergeExtended populates props, authors (relationId 12), and instances', () => {
246        const book = NewBookWithId(20);
247        book.mergeSlim(slimCols, 0, 'eng');
248        book.mergeExtended(extendedCols(), 0, 'eng');
249
250        expect(book.getDescription('eng')).toBe('A practical guide.');
251        expect(book.deweyDecimal).toBe('631.58');
252        expect(book.textQuality).toBe(QualityScore.VeryGood);
253        expect(book.teacherMaterials).toBe(true);
254        expect(book.authorsIds.sort()).toEqual([5, 7]);
255
256        // full instance array replaces the slim inline default
257        const instances = book.getInstances('eng');
258        expect(instances.length).toBe(1);
259        expect(instances[0].downloadTxId).toBe('dl-tx-full');
260        expect(instances[0].method).toBe(BookMethod.OCR);
261        expect(instances[0].publisher).toBe('Pond Press');
262        expect(instances[0].qualityScore).toBe(QualityScore.Good);
263    });
264
265    it('mergeExtended keeps language realities separate', () => {
266        const book = NewBookWithId(20);
267        const fraCols = extendedCols();
268        fraCols.descriptions = ['Un guide pratique.'];
269        book.mergeExtended(extendedCols(), 0, 'eng');
270        book.mergeExtended(fraCols, 0, 'fra');
271
272        expect(book.getDescription('eng')).toBe('A practical guide.');
273        expect(book.getDescription('fra')).toBe('Un guide pratique.');
274    });
275
276    it('mergeProv maps provenance codes by prop name', () => {
277        const book = NewBookWithId(20);
278        const provCols: BookProvCols = {
279            ids: new Uint16Array([20]),
280            fields: {
281                textQuality: new Uint8Array([2]),   // Human-Verified
282                description: new Uint8Array([1]),   // AI-Generated
283            },
284        };
285        book.mergeProv(provCols, 0, 'eng');
286        expect(book.getProvenance(12, 'eng')).toBe(2); // textQuality propId 12
287        expect(book.getProvenance(1, 'eng')).toBe(1);  // description propId 1
288        expect(book.getProvenance(7, 'eng')).toBe(0);  // unset
289    });
290});
291
292describe('BookInstance wrapper', () => {
293    it('qualityScore setter applies the patch to in-memory state', () => {
294        const book = NewBookWithId(30);
295        book.setInstancesRaw('eng', [{
296            downloadTxId: 'tx', method: BookMethod.Scan, nbPages: 100,
297            sizeBytes: 1000, qualityScore: QualityScore.Poor, isDefault: true,
298        }]);
299
300        const inst = book.getInstance(0, 'eng')!;
301        expect(inst).toBeInstanceOf(BookInstance);
302        expect(inst.qualityScore).toBe(QualityScore.Poor);
303
304        inst.qualityScore = QualityScore.Excellent;
305        expect(inst.qualityScore).toBe(QualityScore.Excellent);
306        // the underlying ORM data was updated through patch()
307        expect(book.getInstancesData('eng')[0]['qualityScore']).toBe(QualityScore.Excellent);
308    });
309
310    it('delegates description to the parent per language', () => {
311        const book = NewBookWithId(31);
312        book.setInstancesRaw('fra', [{ downloadTxId: 'tx', isDefault: true }]);
313        book.setDescription('Description franΓ§aise', 'fra');
314        const inst = book.getInstance(0, 'fra')!;
315        expect(inst.description).toBe('Description franΓ§aise');
316    });
317});
318
319describe('LRU cache + lazy reactivity', () => {
320    it('evicts the least recently used record beyond the limit', () => {
321        const cache = new LruCache<number, string>(2);
322        cache.set(1, 'a');
323        cache.set(2, 'b');
324        cache.get(1);        // 1 becomes most recently used
325        cache.set(3, 'c');   // evicts 2
326        expect(cache.has(1)).toBe(true);
327        expect(cache.has(2)).toBe(false);
328        expect(cache.has(3)).toBe(true);
329    });
330
331    it('configureDb limit applies to the model store', () => {
332        configureDb({ cacheLimit: 2 });
333        NewBookWithId(1);
334        NewBookWithId(2);
335        NewBookWithId(3);
336        expect(Book.Has(1)).toBe(false); // evicted
337        expect(Book.Has(2)).toBe(true);
338        expect(Book.Has(3)).toBe(true);
339    });
340
341    it('track fires on getters, trigger fires on patches', () => {
342        let tracked = 0, triggered = 0;
343        setReactivitySystem({
344            track: () => { tracked++; },
345            trigger: () => { triggered++; },
346        });
347
348        const book = NewBookWithId(40);
349        void book.title;
350        expect(tracked).toBeGreaterThan(0);
351
352        book.title = 'Reactive Title';
353        expect(triggered).toBeGreaterThan(0);
354
355        setReactivitySystem(null);
356    });
357});
358