📄 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