📄 src/models/test/Tag.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 { Tag, NewTagWithId } from '../generated-src/Tag.ts';
17import { Book, NewBookWithId } from '../generated-src/Book.ts';
18import { parseSidecar, colsFromSidecar } from '../enriched-index.ts';
19import { clear, configureDb } from '../db.ts';
20import { setDefaultLanguage } from '../orm.ts';
21import { PatchInstance } from '../patcher.ts';
22import { PatchOperation, PatchAction, type EditRelationObj } from '../binary.ts';
23import { createTagFixture } from './fixtures.ts';
24
25beforeEach(() => {
26    clear();
27    configureDb({ cacheLimit: 10_000 });
28    setDefaultLanguage('eng');
29    PatchInstance.clearQueue();
30});
31
32describe('Tag Model - Relationships and Sidecar', () => {
33    it('should serialize and deserialize extended sidecar with bi-directional tails', () => {
34        const fixture1 = createTagFixture(10);
35        fixture1.tags = [11, 12];
36        fixture1.books = [100];
37        
38        const fixture2 = createTagFixture(11);
39        fixture2.tags = [];
40        fixture2.books = [100, 101];
41
42        const buffer = Tag.serializeExtended([fixture1, fixture2]);
43        const parsed = parseSidecar(buffer);
44        const cols = colsFromSidecar(parsed);
45
46        expect(Array.from(cols['ids'] as Uint16Array)).toEqual([10, 11]);
47        expect(cols['tags']).toEqual([[11, 12], []]);
48        expect(cols['books']).toEqual([[100], [100, 101]]);
49
50        const tag1 = NewTagWithId(10);
51        const tag2 = NewTagWithId(11);
52        
53        tag1.mergeExtended(cols as any, 0, 'eng'); // index 0 for tag1 is simulated here by index mapping but actually mergeExtended will map them by ID.
54        // Wait, mergeExtended takes the whole columnar block, so we should merge the whole block onto BOTH or just rely on the ID mapping.
55        // Actually, Orm.mergeExtended uses `index` which we pass. Let's extract specific rows.
56        tag1.mergeExtended(cols as any, 0, 'eng');
57        tag2.mergeExtended(cols as any, 1, 'eng');
58
59        expect(tag1.tagsIds).toEqual([11, 12]);
60        expect(tag1.booksIds).toEqual([100]);
61        
62        expect(tag2.tagsIds).toEqual([]);
63        expect(tag2.booksIds).toEqual([100, 101]);
64    });
65});
66
67describe('Tag Model - Patching Scenarios for Relations', () => {
68    it('should generate canonical relation patches when mutating relationships', () => {
69        const tag = NewTagWithId(50);
70        const book = NewBookWithId(60);
71        const childTag = NewTagWithId(51);
72        
73        PatchInstance.clearQueue();
74
75        // 1. Add Book (Tag has Book)
76        tag.addBook(60);
77        expect(tag.booksIds).toEqual([60]);
78        // Also book should have it in its inverse (Book in Tag)
79        expect(book.inTagsIds).toEqual([50]);
80
81        // 2. Add child Tag (Tag has Tag)
82        tag.addTag(51);
83        expect(tag.tagsIds).toEqual([51]);
84        expect(childTag.inTagsIds).toEqual([50]);
85
86        // 3. Add into Tag (Tag in Tag)
87        tag.addIntoTag(52); // ID 52 not loaded
88        expect(tag.inTagsIds).toEqual([52]);
89
90        const patches = PatchInstance.patches as EditRelationObj[];
91        expect(patches.length).toBe(3);
92
93        // Tag.books relationId is 4
94        expect(patches[0]).toMatchObject({
95            operation: PatchOperation.EditRelation,
96            objectType: Tag.type,
97            objectId: 50,
98            relationId: 4,
99            action: PatchAction.Add,
100            targetId: 60
101        });
102
103        // Tag.tags relationId is 3
104        expect(patches[1]).toMatchObject({
105            operation: PatchOperation.EditRelation,
106            objectType: Tag.type,
107            objectId: 50,
108            relationId: 3,
109            action: PatchAction.Add,
110            targetId: 51
111        });
112
113        // Tag in Tag (inverse of Tag.tags)
114        expect(patches[2]).toMatchObject({
115            operation: PatchOperation.EditRelation,
116            objectType: Tag.type,
117            objectId: 52,
118            relationId: 3,
119            action: PatchAction.Add,
120            targetId: 50
121        });
122    });
123
124    it('should handle removing relationships', () => {
125        const tag = NewTagWithId(70);
126        
127        tag.addTag(71);
128        PatchInstance.clearQueue();
129
130        tag.removeTag(71);
131        expect(tag.tagsIds).toEqual([]);
132
133        const patches = PatchInstance.patches as EditRelationObj[];
134        expect(patches.length).toBe(1);
135
136        expect(patches[0]).toMatchObject({
137            operation: PatchOperation.EditRelation,
138            objectType: Tag.type,
139            objectId: 70,
140            relationId: 3,
141            action: PatchAction.Remove,
142            targetId: 71
143        });
144    });
145});
146