📄 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