📄 src/models/serializer.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 {
16 ColumnType,
17 writeSidecar,
18 instanceSpecFromFields,
19 columnTypeForBits,
20 type SidecarInput,
21 type SidecarColumn,
22 type SidecarRelationInput
23} from "./enriched-index.ts";
24import { SidecarType } from "./enums.ts";
25import type { ModelDefinition, SchemaProp, SchemaInstanceField, SchemaRelation } from "./registry.ts";
26import type { VersionLayout } from "./binary.ts";
27
28// ─── Shared Helpers ──────────────────────────────────────────────────────────
29
30export const capitalize = (name: string): string => name.charAt(0).toUpperCase() + name.slice(1);
31
32export const colName = (name: string, type: string): string =>
33 type === 'string' && !name.endsWith('s') ? `${name}s` : name;
34
35export const propBits = (prop: SchemaProp, layout: VersionLayout): number => {
36 const fromLayout = layout.props[prop.propId]?.bits;
37 if (typeof fromLayout === 'number' && fromLayout > 0) return fromLayout;
38 if (prop.type === 'boolean') return 1;
39 return prop.bits ?? 32;
40};
41
42export const instanceFieldBits = (field: SchemaInstanceField, index: number, objectId: number, layout: VersionLayout): number => {
43 const propId = (layout as any).instanceFields?.[objectId]?.[index];
44 const fromLayout = typeof propId === 'number' ? layout.props[propId]?.bits : undefined;
45 if (typeof fromLayout === 'number' && fromLayout > 0) return fromLayout;
46 if (field.type === 'boolean') return 1;
47 return field.bits ?? 32;
48};
49
50const relationColName = (rel: SchemaRelation): string =>
51 rel.name === 'authors' ? 'authorIds' : rel.name;
52
53const extractTypedArray = (bits: number, values: number[]): Uint8Array | Uint16Array | Uint32Array => {
54 if (bits <= 8) return Uint8Array.from(values);
55 if (bits <= 16) return Uint16Array.from(values);
56 return Uint32Array.from(values);
57};
58
59// ─── Generic Serializers ─────────────────────────────────────────────────────
60
61export function commonSerializeSlim(schema: ModelDefinition, records: any[], layout: VersionLayout): ArrayBuffer {
62 const uids = records.map(r => r.id);
63 const columns: SidecarColumn[] = [
64 { name: "ids", type: ColumnType.Uint32, data: Uint32Array.from(uids) },
65 { name: "titles", type: ColumnType.NullableString, data: records.map(r => r.title) },
66 { name: "originalLangs", type: ColumnType.NullableString, data: records.map(r => r.originalLang) },
67 { name: "previewTxIds", type: ColumnType.NullableString, data: records.map(r => r.previewTxId) },
68 { name: "ageGradeBands", type: ColumnType.Uint8, data: Uint8Array.from(records.map(r => r.ageGradeBand)) },
69 { name: "safes", type: ColumnType.Uint8, data: Uint8Array.from(records.map(r => r.safe ? 1 : 0)) },
70 { name: "deleteds", type: ColumnType.Uint8, data: Uint8Array.from(records.map(r => r.deleted ? 1 : 0)) },
71 ];
72
73 for (const prop of schema.props.filter(p => p.source === 'slim')) {
74 const name = colName(prop.name, prop.type);
75 if (prop.type === 'string') {
76 columns.push({ name, type: ColumnType.NullableString, data: records.map(r => r[prop.name]) });
77 } else if (prop.type === 'boolean') {
78 columns.push({ name, type: ColumnType.Uint8, data: Uint8Array.from(records.map(r => r[prop.name] ? 1 : 0)) });
79 } else {
80 const bits = propBits(prop, layout);
81 columns.push({ name, type: columnTypeForBits(bits), data: extractTypedArray(bits, records.map(r => r[prop.name])) });
82 }
83 }
84
85 if (schema.multipleInstances) {
86 for (let i = 0; i < schema.instanceFields.length; i++) {
87 const field = schema.instanceFields[i];
88 const capField = capitalize(field.name);
89 const capName = colName(capField, field.type);
90 const values = records.map(r => r[`default${capField}`]);
91
92 if (field.type === 'string') {
93 columns.push({ name: `default${capName}`, type: ColumnType.NullableString, data: values });
94 } else if (field.type === 'boolean') {
95 columns.push({ name: `default${capName}`, type: ColumnType.Uint8, data: Uint8Array.from(values.map(v => v ? 1 : 0)) });
96 } else {
97 const bits = instanceFieldBits(field, i, schema.objectId, layout);
98 columns.push({ name: `default${capName}`, type: columnTypeForBits(bits), data: extractTypedArray(bits, values) });
99 }
100 }
101 }
102
103 const input: SidecarInput = {
104 sidecarType: SidecarType.Slim,
105 recordCount: records.length,
106 columns
107 };
108 return writeSidecar(input);
109}
110
111export function commonSerializeExtended(schema: ModelDefinition, records: any[], layout: VersionLayout): ArrayBuffer {
112 const uids = records.map(r => r.id);
113 const columns: SidecarColumn[] = [
114 { name: "ids", type: ColumnType.Uint32, data: Uint32Array.from(uids) },
115 { name: "descriptions", type: ColumnType.NullableString, data: records.map(r => r.description) },
116 ];
117
118 for (const prop of schema.props.filter(p => p.source === 'extended')) {
119 const name = colName(prop.name, prop.type);
120 if (prop.type === 'string') {
121 columns.push({ name, type: ColumnType.NullableString, data: records.map(r => r[prop.name]) });
122 } else if (prop.type === 'boolean') {
123 columns.push({ name, type: ColumnType.Uint8, data: Uint8Array.from(records.map(r => r[prop.name] ? 1 : 0)) });
124 } else {
125 const bits = propBits(prop, layout);
126 columns.push({ name, type: columnTypeForBits(bits), data: extractTypedArray(bits, records.map(r => r[prop.name])) });
127 }
128 }
129
130 const hasRelations = schema.relations.filter(r => r.direction === 'has');
131 const relations: SidecarRelationInput[] = hasRelations.map(rel => ({
132 name: rel.name,
133 data: records.map(r => r[relationColName(rel)])
134 }));
135
136 const input: SidecarInput = {
137 sidecarType: SidecarType.Extended,
138 recordCount: records.length,
139 columns,
140 ...(relations.length > 0 && { relations })
141 };
142
143 if (schema.multipleInstances) {
144 input.instanceFields = instanceSpecFromFields(schema.instanceFields as any);
145 input.instances = records.map(r => r.instances);
146 }
147
148 return writeSidecar(input);
149}
150
151export function commonSerializeProv(schema: ModelDefinition, records: any[]): ArrayBuffer {
152 const input: SidecarInput = {
153 sidecarType: SidecarType.Provenance,
154 recordCount: records.length,
155 columns: [
156 { name: "ids", type: ColumnType.Uint32, data: Uint32Array.from(records.map(r => r.id)) },
157 ]
158 };
159 return writeSidecar(input);
160}
161