π 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