๐ src/models/schemaGenerator.ts
D-OPEN SOVEREIGN
Documentation & Insights
V2 ORM code generator.
Consumes the enriched schema JSONs from `@the_library/db_schemas/generated-src`
(frozen numeric IDs) plus the compiled `versions.json` layout, and emits one
TypeScript model file per schema with:
- generated column-bag interfaces (<Name>SlimCols / <Name>ExtendedCols /
<Name>ProvCols) whose TypedArray widths derive from the schema `bits`
- the model class (prop getters/setters dispatching AnyPatch, relation
helpers, mergeSlim / mergeExtended / mergeProv, Load / LoadAsync)
- a <Name>Instance wrapper when `multipleInstances` is enabled
- the SYMMETRIC write side: a <Name>Input record type plus
serialize<Name>{Slim,Extended,Prov}(records) free functions (and static
aliases on the class), emitted from the SAME colName/propBits/base-col/
default-instance/relation/instance logic as the merge* readers, so writer
and reader are provably the inverse of one another (see the round-trip
contract in test/serializer.test.ts).All schemas, keyed by model name (for relation target resolution).Fully-merged current layout from versions.json.Column name: pluralize string props ("description" -> "descriptions"), keep others.Method base for in-relations: "inTags" -> "Tag", "authorOf" -> "AuthorOf".TS type of a prop getter: enums use the generated enum type.propName -> propId (provenance column mapping)Synchronous LRU read โ throws on miss (use LoadAsync to re-hydrate).On-demand hydration from the worker after LRU eviction (SPEC ยง2.5).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 */
15/**
16 * V2 ORM code generator.
17 *
18 * Consumes the enriched schema JSONs from `@the_library/db_schemas/generated-src`
19 * (frozen numeric IDs) plus the compiled `versions.json` layout, and emits one
20 * TypeScript model file per schema with:
21 * - generated column-bag interfaces (<Name>SlimCols / <Name>ExtendedCols /
22 * <Name>ProvCols) whose TypedArray widths derive from the schema `bits`
23 * - the model class (prop getters/setters dispatching AnyPatch, relation
24 * helpers, mergeSlim / mergeExtended / mergeProv, Load / LoadAsync)
25 * - a <Name>Instance wrapper when `multipleInstances` is enabled
26 * - the SYMMETRIC write side: a <Name>Input record type plus
27 * serialize<Name>{Slim,Extended,Prov}(records) free functions (and static
28 * aliases on the class), emitted from the SAME colName/propBits/base-col/
29 * default-instance/relation/instance logic as the merge* readers, so writer
30 * and reader are provably the inverse of one another (see the round-trip
31 * contract in test/serializer.test.ts).
32 */
33import { relationColName } from "./registry.ts";
34import type { ModelDefinition, SchemaProp, SchemaInstanceField, SchemaRelation } from "./registry.ts";
35import type { VersionLayout } from "./binary.ts";
36
37export interface GeneratorContext {
38 /** All schemas, keyed by model name (for relation target resolution). */
39 schemas: Map<string, ModelDefinition>;
40 /** Fully-merged current layout from versions.json. */
41 layout: VersionLayout;
42}
43
44// โโโ Naming helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
45
46const capitalize = (name: string) => name.charAt(0).toUpperCase() + name.slice(1)
47
48const toSingular = (name: string) => name.endsWith('s') ? name.slice(0, -1) : name
49
50/** Column name: pluralize string props ("description" -> "descriptions"), keep others. */
51const colName = (name: string, type: string) =>
52 type === 'string' && !name.endsWith('s') ? `${name}s` : name
53
54/** Method base for in-relations: "inTags" -> "Tag", "authorOf" -> "AuthorOf". */
55const inMethodBase = (name: string) => {
56 const stripped = (name.startsWith('in') && name[2] === name[2]?.toUpperCase())
57 ? name.slice(2)
58 : capitalize(name);
59 return toSingular(stripped);
60}
61
62// โโโ Type helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
63
64const typedArrayFor = (bits: number): string =>
65 bits <= 8 ? 'Uint8Array' : bits <= 16 ? 'Uint16Array' : 'Uint32Array'
66
67const propBits = (prop: SchemaProp, layout: VersionLayout): number => {
68 const fromLayout = layout.props[prop.propId]?.bits;
69 if (typeof fromLayout === 'number' && fromLayout > 0) return fromLayout;
70 if (prop.type === 'boolean') return 1;
71 return prop.bits ?? 32;
72}
73
74const instanceFieldBits = (field: SchemaInstanceField, index: number, objectId: number, layout: VersionLayout): number => {
75 const propId = (layout as any).instanceFields?.[objectId]?.[index];
76 const fromLayout = typeof propId === 'number' ? layout.props[propId]?.bits : undefined;
77 if (typeof fromLayout === 'number' && fromLayout > 0) return fromLayout;
78 if (field.type === 'boolean') return 1;
79 return field.bits ?? 32;
80}
81
82/** TS type of a prop getter: enums use the generated enum type. */
83const tsType = (p: { type: string, enum?: string }): string => {
84 switch (p.type) {
85 case 'string': return 'string';
86 case 'boolean': return 'boolean';
87 case 'enum': return p.enum ?? 'number';
88 default: return 'number';
89 }
90}
91
92const usedEnums = (schema: ModelDefinition): string[] => {
93 const names = new Set<string>();
94 for (const p of schema.props) if (p.type === 'enum' && p.enum) names.add(p.enum);
95 for (const f of schema.instanceFields) if (f.type === 'enum' && f.enum) names.add(f.enum);
96 return Array.from(names).sort();
97}
98
99// โโโ Cols interfaces โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
100
101const slimColsSource = (schema: ModelDefinition, ctx: GeneratorContext): string => {
102 const lines: string[] = [
103 `export interface ${schema.name}SlimCols {`,
104 ` ids: Uint16Array;`,
105 ` titles: (string | null)[];`,
106 ` originalLangs: (string | null)[];`,
107 ` previewTxIds: (string | null)[];`,
108 ` ageGradeBands: Uint8Array;`,
109 ` safes: Uint8Array;`,
110 ` deleteds: Uint8Array;`,
111 ];
112 for (const prop of schema.props.filter(p => p.source === 'slim')) {
113 const name = colName(prop.name, prop.type);
114 if (prop.type === 'string') {
115 lines.push(` ${name}: (string | null)[];`);
116 } else {
117 lines.push(` ${name}: ${typedArrayFor(propBits(prop, ctx.layout))};`);
118 }
119 }
120 if (schema.multipleInstances) {
121 lines.push(` // Default instance inlined (slim tier carries one default instance per record):`);
122 for (let i = 0; i < schema.instanceFields.length; i++) {
123 const field = schema.instanceFields[i];
124 const capName = colName(capitalize(field.name), field.type);
125 if (field.type === 'string') {
126 lines.push(` default${capName}: (string | null)[];`);
127 } else if (field.type === 'boolean') {
128 lines.push(` default${capName}: Uint8Array;`);
129 } else {
130 lines.push(` default${capName}: ${typedArrayFor(instanceFieldBits(field, i, schema.objectId, ctx.layout))};`);
131 }
132 }
133 }
134 lines.push(`}`);
135 return lines.join('\n');
136}
137
138const extendedColsSource = (schema: ModelDefinition, ctx: GeneratorContext): string => {
139 const lines: string[] = [
140 `export interface ${schema.name}ExtendedCols {`,
141 ` ids: Uint16Array;`,
142 ` descriptions: (string | null)[];`,
143 ];
144 for (const prop of schema.props.filter(p => p.source === 'extended')) {
145 const name = colName(prop.name, prop.type);
146 if (prop.type === 'string') {
147 lines.push(` ${name}: (string | null)[];`);
148 } else {
149 lines.push(` ${name}: ${typedArrayFor(propBits(prop, ctx.layout))};`);
150 }
151 }
152 const hasRelations = schema.relations.filter(r => r.direction === 'has');
153 if (hasRelations.length > 0) {
154 lines.push(` // Structured relation tails (parsed from the sidecar tail, not columnar):`);
155 for (const rel of hasRelations) {
156 lines.push(` ${relationColName(rel)}?: number[][];`);
157 }
158 }
159 if (schema.multipleInstances) {
160 lines.push(` instances?: InstanceData[][];`);
161 }
162 lines.push(`}`);
163 return lines.join('\n');
164}
165
166const provColsSource = (schema: ModelDefinition): string => `export interface ${schema.name}ProvCols {
167 ids: Uint16Array;
168 fields: Record<string, Uint8Array>;
169}`
170
171// โโโ Input & Serializer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
172
173const inputInterfaceSource = (schema: ModelDefinition, ctx: GeneratorContext): string => {
174 const lines: string[] = [
175 `export interface ${schema.name}Input {`,
176 ` id: number;`,
177 ` title: string | null;`,
178 ` description: string | null;`,
179 ` originalLang: string | null;`,
180 ` previewTxId: string | null;`,
181 ` ageGradeBand: number;`,
182 ` safe: boolean;`,
183 ` deleted: boolean;`,
184 ];
185 for (const prop of schema.props) {
186 if (prop.type === 'string') {
187 lines.push(` ${prop.name}: string | null;`);
188 } else if (prop.type === 'boolean') {
189 lines.push(` ${prop.name}: boolean;`);
190 } else {
191 lines.push(` ${prop.name}: ${tsType(prop)};`);
192 }
193 }
194 const hasRelations = schema.relations.filter(r => r.direction === 'has');
195 for (const rel of hasRelations) {
196 lines.push(` ${relationColName(rel)}: number[];`);
197 }
198 if (schema.multipleInstances) {
199 for (const field of schema.instanceFields) {
200 const cap = capitalize(field.name);
201 const ts = field.type === 'string' ? 'string | null' : field.type === 'boolean' ? 'boolean' : 'number';
202 lines.push(` default${cap}: ${ts};`);
203 }
204 lines.push(` instances: InstanceData[];`);
205 }
206 lines.push(`}`);
207 return lines.join('\n');
208}
209
210const serializeSlimSource = (schema: ModelDefinition, ctx: GeneratorContext): string => {
211 return `export function serialize${schema.name}Slim(records: ${schema.name}Input[]): ArrayBuffer {
212 return commonSerializeSlim(schema, records, currentLayout());
213}`;
214}
215
216const serializeExtendedSource = (schema: ModelDefinition, ctx: GeneratorContext): string => {
217 return `export function serialize${schema.name}Extended(records: ${schema.name}Input[]): ArrayBuffer {
218 return commonSerializeExtended(schema, records, currentLayout());
219}`;
220}
221
222const serializeProvSource = (schema: ModelDefinition): string => {
223 return `export function serialize${schema.name}Prov(records: ${schema.name}Input[]): ArrayBuffer {
224 return commonSerializeProv(schema, records);
225}`;
226}
227
228// โโโ Relations โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
229
230const invertedRelationId = (rel: SchemaRelation, ctx: GeneratorContext): number | undefined => {
231 const target = ctx.schemas.get(rel.targetType);
232 return target?.relations.find(r => r.name === rel.inverted)?.relationId;
233}
234
235const hasRelationSource = (schema: ModelDefinition, rel: SchemaRelation, _ctx: GeneratorContext): string => {
236 const single = capitalize(toSingular(rel.name));
237 // The has side IS the canonical side: dispatchRelation emits the single
238 // EditRelation patch; Orm.patch() propagates in-memory to the loaded
239 // inverse record generically. No second patch, no propagate flag.
240 return `
241 get ${rel.name}Ids(): Array<uid> {
242 return Array.from(this._has.get(${rel.relationId})!.values());
243 }
244
245 get ${rel.name}(): Array<${rel.targetType}> {
246 return this.${rel.name}Ids.map(id => ${rel.targetType}.Load(id));
247 }
248
249 has${single}(id: uid): boolean {
250 return this._has.get(${rel.relationId})!.has(id);
251 }
252
253 add${single}(id: uid) {
254 if (this.has${single}(id)) return;
255 this.dispatchRelation(${rel.relationId}, PatchAction.Add, id);
256 }
257
258 remove${single}(id: uid) {
259 if (!this.has${single}(id)) return;
260 this.dispatchRelation(${rel.relationId}, PatchAction.Remove, id);
261 }`;
262}
263
264const inRelationSource = (schema: ModelDefinition, rel: SchemaRelation, ctx: GeneratorContext): string => {
265 const getterBase = rel.name.startsWith('in') ? rel.name : `in${capitalize(rel.name)}`;
266 const single = inMethodBase(rel.name);
267 const invId = invertedRelationId(rel, ctx);
268
269 const getters = `
270 get ${getterBase}Ids(): Array<uid> {
271 return Array.from(this._in.get(${rel.relationId})!.values());
272 }
273
274 get ${getterBase}(): Array<${rel.targetType}> {
275 return this.${getterBase}Ids.map(id => ${rel.targetType}.Load(id));
276 }
277
278 isIn${single}(id: uid): boolean {
279 return this._in.get(${rel.relationId})!.has(id);
280 }`;
281
282 // No inverse declared: this side is the only representation of the edge,
283 // so it dispatches its own patch.
284 if (invId === undefined) {
285 return `${getters}
286
287 addInto${single}(id: uid) {
288 if (this.isIn${single}(id)) return;
289 this.dispatchRelation(${rel.relationId}, PatchAction.Add, id);
290 }
291
292 removeFrom${single}(id: uid) {
293 if (!this.isIn${single}(id)) return;
294 this.dispatchRelation(${rel.relationId}, PatchAction.Remove, id);
295 }`;
296 }
297
298 // Inverse exists: the canonical patch is expressed on the has side
299 // (${rel.targetType}.${rel.inverted}), regardless of which side the user
300 // called. Own _in is updated directly (idempotent) so the edge is
301 // coherent even when the target record is not loaded.
302 const edit = (method: string, guard: string, action: 'Add' | 'Remove') => `
303 ${method}(id: uid) {
304 if (${guard}) return;
305 const op: EditRelationObj = {
306 operation: PatchOperation.EditRelation,
307 objectType: ${rel.targetType}.type,
308 objectId: id,
309 langId: 0,
310 relationId: ${invId}, // canonical: ${rel.targetType}.${rel.inverted}
311 action: PatchAction.${action},
312 targetId: this.id,
313 };
314 if (hasRecord(${rel.targetType}.type, id)) {
315 (getRecord(${rel.targetType}.type, id) as ${rel.targetType}).patch(op);
316 }
317 this.applyRelationRaw('in', ${rel.relationId}, PatchAction.${action}, id);
318 PatchInstance.addPatch(op);
319 }`;
320
321 return `${getters}
322${edit(`addInto${single}`, `this.isIn${single}(id)`, 'Add')}
323${edit(`removeFrom${single}`, `!this.isIn${single}(id)`, 'Remove')}`;
324}
325
326// โโโ Props โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
327
328const propAccessorsSource = (prop: SchemaProp): string => {
329 const type = tsType(prop);
330 const cap = capitalize(prop.name);
331 switch (prop.type) {
332 case 'string': {
333 const rt = prop.nullable ? 'string | null' : 'string';
334 const val = prop.nullable ? 'value ?? ""' : 'value';
335 return `
336 get ${prop.name}(): ${rt} { return this.getProp(${prop.propId})${prop.nullable ? ' ?? null' : ''}; }
337 set ${prop.name}(value: ${rt}) { this.dispatchString(${prop.propId}, ${val}); }
338 get${cap}(lang: Lang): ${rt} { return this.getProp(${prop.propId}, lang)${prop.nullable ? ' ?? null' : ''}; }
339 set${cap}(value: ${rt}, lang: Lang) { this.dispatchString(${prop.propId}, ${val}, lang); }`;
340 }
341 case 'boolean':
342 return `
343 get ${prop.name}(): boolean { return this.getProp(${prop.propId}) ?? false; }
344 set ${prop.name}(value: boolean) { this.dispatchNumber(${prop.propId}, value ? 1 : 0); }
345 get${cap}(lang: Lang): boolean { return this.getProp(${prop.propId}, lang) ?? false; }
346 set${cap}(value: boolean, lang: Lang) { this.dispatchNumber(${prop.propId}, value ? 1 : 0, lang); }`;
347 default: // number | enum
348 return `
349 get ${prop.name}(): ${type} { return this.getProp(${prop.propId}) ?? 0; }
350 set ${prop.name}(value: ${type}) { this.dispatchNumber(${prop.propId}, value); }
351 get${cap}(lang: Lang): ${type} { return this.getProp(${prop.propId}, lang) ?? 0; }
352 set${cap}(value: ${type}, lang: Lang) { this.dispatchNumber(${prop.propId}, value, lang); }`;
353 }
354}
355
356// โโโ Merge methods โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
357
358// Generated methods for merging sidecars have been removed and implemented in Orm.
359
360// โโโ Instance wrapper โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
361
362const instanceWrapperSource = (schema: ModelDefinition): string => {
363 const accessors: string[] = [];
364 const instStrats = Array.isArray(schema.instanceUidStrategy) ? schema.instanceUidStrategy : (schema.instanceUidStrategy ? [schema.instanceUidStrategy] : []);
365 const isInstStandardsFirst = instStrats.length > 0 && instStrats[0] !== 'native-only';
366 let instUIDMethods = '';
367
368 const idFields = schema.instanceFields?.filter(f => (f as any).identityComponent) || [];
369
370 if (idFields.length > 0) {
371 instUIDMethods += `
372 derivedSfcuid(sourceSfcuid: string): string {
373 const parts = [
374 ${idFields.map(f => `this.${f.name} ?? ''`).join(',\n ')}
375 ];
376 return \`DL:C:DERIVED:\${sourceSfcuid}:\${parts.map(p => encodeURIComponent(String(p))).join(':')}\`;
377 }`;
378 }
379
380 if (isInstStandardsFirst || idFields.length > 0) {
381 let sfcReturns = '';
382 let aliases = '';
383 if (isInstStandardsFirst) {
384 const stds = instStrats.filter(s => s !== 'native-only');
385 for (const std of stds) {
386 const pk = schema.standardPks?.[std] ?? std;
387 instUIDMethods += `
388 // โโ Inherited from ${std} standard โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
389 get ${std}Normalized(): string | null {
390 return this.${pk} ? ${std.toUpperCase()}Adapter.normalize(this.${pk}) : null;
391 }
392 validate${capitalize(std)}(): ReturnType<typeof ${std.toUpperCase()}Adapter.validate> {
393 return ${std.toUpperCase()}Adapter.validate(this.${pk});
394 }`;
395 }
396
397 sfcReturns = stds.map(std => {
398 const pk = schema.standardPks?.[std] ?? std;
399 return `if (this.${pk}) return \`${std}:\${${std.toUpperCase()}Adapter.normalize(this.${pk})}\`;`;
400 }).join('\n ');
401
402 aliases = stds.map(std => {
403 const pk = schema.standardPks?.[std] ?? std;
404 return `this.${pk} ? \`${std}:\${${std.toUpperCase()}Adapter.normalize(this.${pk})}\` : null,`;
405 }).join('\n ');
406 }
407
408 const fallback = idFields.length > 0
409 ? `return this.derivedSfcuid(this.parent.toPID());`
410 : `return this.parent.toPID();`;
411
412 instUIDMethods += `
413 sfcuid(): string {
414 ${sfcReturns}
415 ${fallback}
416 }`;
417
418 instUIDMethods += `
419 get identityCluster(): RecordIdentityCluster {
420 const primary = this.sfcuid();
421 const allAliases = [
422 ${aliases}
423 this.parent.toPID()
424 ].filter(Boolean) as string[];
425 return {
426 sfcuid: primary,
427 native: this.parent.toPID(),
428 aliases: allAliases.filter(a => a !== primary),
429 wireId: this.parent.id,
430 };
431 }`;
432 }
433
434 schema.instanceFields.forEach((field, fieldIndex) => {
435 const type = tsType(field);
436 const nullable = field.type === 'string' && field.nullable;
437 const returnType = field.type === 'string' ? (nullable ? 'string | null' : 'string') : type;
438 const fallback = field.type === 'string'
439 ? (nullable ? 'null' : `""`)
440 : field.type === 'boolean' ? 'false' : '0';
441 accessors.push(`
442 get ${field.name}(): ${returnType} { return (this.data['${field.name}'] as ${returnType}) ?? ${fallback}; }
443 set ${field.name}(val: ${returnType}) {
444 this.data['${field.name}'] = val;
445 const op: ${field.type === 'string' ? 'EditInstanceFieldStringObj' : 'EditInstanceFieldNumberObj'} = {
446 operation: PatchOperation.${field.type === 'string' ? 'EditInstanceFieldString' : 'EditInstanceFieldNumber'},
447 objectType: ${schema.name}.type,
448 objectId: this.parent.id,
449 langId: LANG_ID_ENCODE[this.lang] ?? 0,
450 instanceIndex: this.index,
451 propertyId: ${field.propId}, // ${field.name} โ instanceField global propId
452 value: ${field.type === 'string' ? `val ?? ""` : field.type === 'boolean' ? 'val ? 1 : 0' : 'val'},
453 };
454 this.parent.patch(op);
455 PatchInstance.addPatch(op);
456 }`);
457 });
458
459 return `
460export class ${schema.name}Instance {
461 constructor(
462 public readonly parent: ${schema.name},
463 public readonly lang: string,
464 public readonly index: number,
465 private data: InstanceData,
466 ) {}
467
468 get isDefault(): boolean { return this.data['isDefault'] === true; }
469${accessors.join('\n')}
470${instUIDMethods}
471
472 // High-ease parent delegates
473 get description(): string {
474 return this.parent.getDescription(this.lang) ?? "";
475 }
476 set description(val: string) {
477 this.parent.setDescription(val, this.lang);
478 }
479}`;
480}
481
482const generateLoadInstance = (schema: ModelDefinition): string => {
483 return `
484 static async LoadInstance(pkValue: string, lang: Lang): Promise<{ record: ${schema.name}; instanceIndex?: number } | null> {
485 const crosswalk = (globalThis as any).ExternalIdCrosswalk?.instance;
486 if (!crosswalk) throw new Error("ExternalIdCrosswalk not initialized โ bootup incomplete");
487 await crosswalk.whenReady();
488
489 // 1. Mode DERIVED
490 if (pkValue.startsWith('DL:C:DERIVED:')) {
491 const result = await crosswalk.resolveDerived(pkValue, lang);
492 if (!result) return null;
493 const record = await ${schema.name}.LoadAsync(result.wireId);
494 return record ? { record, instanceIndex: result.instanceIndex } : null;
495 }
496
497 // 2. Mode N (Parent Work)
498 if (pkValue.startsWith('DL:C:')) {
499 const parsed = ${schema.name}.fromPID(pkValue);
500 if (parsed) {
501 const record = await ${schema.name}.LoadAsync(parsed.wireId);
502 return record ? { record } : null;
503 }
504 return null;
505 }
506
507 // 3. Mode S (Standards)
508 const colon = pkValue.indexOf(':');
509 if (colon === -1) return null;
510 const standardId = pkValue.slice(0, colon);
511 const value = pkValue.slice(colon + 1);
512 const result = await crosswalk.resolveForInstance(standardId, value, lang);
513 if (!result) return null;
514 const record = await ${schema.name}.LoadAsync(result.wireId);
515 return record ? { record, instanceIndex: result.instanceIndex } : null;
516 }`;
517}
518
519export const generateModelSource = (schema: ModelDefinition, ctx: GeneratorContext): string => {
520 const NAME = schema.name;
521
522 const targets = Array.from(new Set(
523 schema.relations
524 .map(r => r.targetType)
525 .filter(t => t !== NAME && ctx.schemas.has(t))
526 )).sort();
527
528 const enums = usedEnums(schema);
529
530 const propIdEntries: string[] = [
531 `title: 0`, `description: 1`, `originalLang: 2`, `ageGradeBand: 3`, `previewTxId: 4`, `safe: 5`, `deleted: 6`, `lifecycleStatus: 7`,
532 ...schema.props.map(p => `${p.name}: ${p.propId}`),
533 ...schema.instanceFields.map(f => `${f.name}: ${f.propId}`),
534 ];
535
536 const imports: string[] = [
537 `// AUTO-GENERATED by @the_library/db_schemas schemaGenerator โ run \`pnpm generate\` to rebuild.`,
538 `import { Orm, Config } from "../orm.ts";`,
539 `import { Registry, type ModelDefinition } from "../registry.ts";`,
540 `import { PatchOperation, PatchAction, LANG_ID_ENCODE, type EditInstanceFieldNumberObj, type EditInstanceFieldStringObj, type EditRelationObj } from "../binary.ts";`,
541 `import { PatchInstance } from "../patcher.ts";`,
542 `import { addRecord, getRecord, hasRecord } from "../db.ts";`,
543 `import type { uid, Lang, CommonOrmJson, SerializedCommonOrmJson, InstanceData, RecordIdentityCluster } from "../interfaces.ts";`,
544 `import { WorkerCommands, SidecarType } from "../enums.ts";`,
545 `import { ColumnType, type SidecarInput, type SidecarRelationInput, instanceSpecFromFields } from "../enriched-index.ts";`,
546 `import { encodePID, decodePID } from "../pid.ts";`,
547 `import { schemas } from "@the_library/db_schemas/schemas";`,
548 `import { commonSerializeSlim, commonSerializeExtended, commonSerializeProv } from "../serializer.ts";`,
549 `import { currentLayout } from "../versions.ts";`,
550 ];
551 if (enums.length > 0) {
552 imports.push(`import { ${enums.join(', ')} } from "@the_library/db_schemas/enums";`);
553 }
554 for (const target of targets) {
555 imports.push(`import { ${target} } from "./${target}.ts";`);
556 }
557
558 const parentStrats = Array.isArray(schema.uidStrategy) ? schema.uidStrategy : (schema.uidStrategy ? [schema.uidStrategy] : []);
559 const instStrats = Array.isArray(schema.instanceUidStrategy) ? schema.instanceUidStrategy : (schema.instanceUidStrategy ? [schema.instanceUidStrategy] : []);
560 const allStds = new Set([...parentStrats, ...instStrats].filter(s => s !== 'native-only'));
561 for (const std of allStds) {
562 imports.push(`import { ${std.toUpperCase()}Adapter } from "@the_library/standard-adapters/${std}";`);
563 }
564
565 const schemaBlock = `const schema = schemas["${schema.name}"] as ModelDefinition;`;
566
567 const relationBlocks = schema.relations.map(rel =>
568 rel.direction === 'has'
569 ? hasRelationSource(schema, rel, ctx)
570 : inRelationSource(schema, rel, ctx)
571 );
572
573 const propBlocks = schema.props.map(propAccessorsSource);
574
575 const instancesApi = !schema.multipleInstances ? '' : `
576 getInstances(lang: Lang = Config.userLang): ${NAME}Instance[] {
577 return this.getInstancesData(lang).map((data, i) => new ${NAME}Instance(this, lang, i, data));
578 }
579
580 getInstance(index: number, lang: Lang = Config.userLang): ${NAME}Instance | undefined {
581 return this.getInstances(lang)[index];
582 }
583
584 getDefaultInstance(lang: Lang = Config.userLang): ${NAME}Instance | undefined {
585 const instances = this.getInstances(lang);
586 return instances.find(i => i.isDefault) ?? instances[0];
587 }`;
588
589 const isStandardsFirst = parentStrats.length > 0 && parentStrats[0] !== 'native-only';
590 let classUIDMethods = `
591 // โโ UID methods โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
592 toPID(): string {
593 const scope = (this.id & 0x80000000) ? 'U' : 'C';
594 return \`DL:\${scope}:${schema.typePrefix}:\${encodePID('${schema.typePrefix}', this.id)}\`;
595 }
596 toNativePID(): string { return this.toPID(); }
597
598 static fromPID(pid: string): { wireId: number } | null {
599 try {
600 if (!pid.startsWith('DL:')) return decodePID('${schema.typePrefix}', pid);
601 const parts = pid.split(':');
602 if (parts.length >= 4 && parts[2] === '${schema.typePrefix}') {
603 return decodePID('${schema.typePrefix}', parts[3]);
604 }
605 return null;
606 } catch {
607 return null;
608 }
609 }`;
610
611 if (isStandardsFirst) {
612 const stds = parentStrats.filter(s => s !== 'native-only');
613 for (const std of stds) {
614 const pk = schema.standardPks?.[std] ?? std;
615 classUIDMethods += `
616 // โโ Inherited from ${std} standard โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
617 get ${std}Normalized(): string | null {
618 return this.${pk} ? ${std.toUpperCase()}Adapter.normalize(this.${pk}) : null;
619 }
620 validate${capitalize(std)}(): ReturnType<typeof ${std.toUpperCase()}Adapter.validate> {
621 return ${std.toUpperCase()}Adapter.validate(this.${pk});
622 }`;
623 }
624
625 const fallback = 'this.toNativePID()';
626 const sfcReturns = stds.map(std => {
627 const pk = schema.standardPks?.[std] ?? std;
628 return `if (this.${pk}) return \`${std}:\${${std.toUpperCase()}Adapter.normalize(this.${pk})}\`;`;
629 }).join('\n ');
630
631 classUIDMethods += `
632 sfcuid(): string {
633 ${sfcReturns}
634 return ${fallback};
635 }`;
636
637 const aliases = stds.map(std => {
638 const pk = schema.standardPks?.[std] ?? std;
639 return `this.${pk} ? \`${std}:\${${std.toUpperCase()}Adapter.normalize(this.${pk})}\` : null,`;
640 }).join('\n ');
641
642 classUIDMethods += `
643 get identityCluster(): RecordIdentityCluster {
644 const primary = this.sfcuid();
645 const allAliases = [
646 ${aliases}
647 this.toNativePID()
648 ].filter(Boolean) as string[];
649 return {
650 sfcuid: primary,
651 native: this.toNativePID(),
652 aliases: allAliases.filter(a => a !== primary),
653 wireId: this.id,
654 };
655 }`;
656 }
657
658 return `${imports.join('\n')}
659
660${schemaBlock}
661
662Registry.register(schema);
663
664/** propName -> propId (provenance column mapping) */
665export const ${NAME.toUpperCase()}_PROP_IDS: Record<string, number> = { ${propIdEntries.join(', ')} };
666
667${slimColsSource(schema, ctx)}
668
669${extendedColsSource(schema, ctx)}
670
671${provColsSource(schema)}
672
673${inputInterfaceSource(schema, ctx)}
674${serializeSlimSource(schema, ctx)}
675${serializeExtendedSource(schema, ctx)}
676${serializeProvSource(schema)}
677
678export class ${NAME} extends Orm {
679 static readonly type: number = ${schema.objectId};
680 static readonly UID_STRATEGY = '${isStandardsFirst ? 'standards-first' : 'native-only'}' as const;
681 static readonly STANDARD_PRIORITY = ${isStandardsFirst ? JSON.stringify(parentStrats) : '[]'} as const;
682
683 static readonly serializeSlim = serialize${NAME}Slim;
684 static readonly serializeExtended = serialize${NAME}Extended;
685 static readonly serializeProv = serialize${NAME}Prov;
686
687 constructor(json: CommonOrmJson | SerializedCommonOrmJson, creatorUsername?: string) {
688 super(${NAME}.type, json, creatorUsername);
689 }
690
691 /** Synchronous LRU read โ throws on miss (use LoadAsync to re-hydrate). */
692 static Load(id: uid, creatorUsername?: string): ${NAME} {
693 return getRecord(${NAME}.type, id, creatorUsername) as ${NAME};
694 }
695
696 static Has(id: uid, creatorUsername?: string): boolean {
697 return hasRecord(${NAME}.type, id, creatorUsername);
698 }
699
700 /** On-demand hydration from the worker after LRU eviction (SPEC ยง2.5). */
701 static async LoadAsync(id: uid, creatorUsername?: string): Promise<${NAME}> {
702 if (hasRecord(${NAME}.type, id, creatorUsername)) {
703 return getRecord(${NAME}.type, id, creatorUsername) as ${NAME};
704 }
705 const thread = PatchInstance.threadInstance;
706 if (!thread) throw new Error("worker thread not initialized โ call PatchInstance.initThread first");
707 const res = await thread.sendRequest({
708 cmd: WorkerCommands.GetRecord,
709 payload: { objectType: ${NAME}.type, id }
710 }) as Array<{ lang: Lang, slim?: ${NAME}SlimCols, extended?: ${NAME}ExtendedCols, prov?: ${NAME}ProvCols }> | null;
711 if (!res || res.length === 0) throw new Error(\`${NAME} \${id} not found in worker storage\`);
712 const record = New${NAME}WithId(id, creatorUsername);
713 for (const data of res) {
714 if (data.slim) record.mergeSlim(data.slim, 0, data.lang);
715 if (data.extended) record.mergeExtended(data.extended, 0, data.lang);
716 if (data.prov) record.mergeProv(data.prov, 0, data.lang);
717 }
718 return record;
719 }
720${propBlocks.join('\n')}
721${relationBlocks.join('\n')}
722${instancesApi}
723${classUIDMethods}
724
725 // Merge methods are now implemented in Orm using the schema definition.
726${schema.multipleInstances ? generateLoadInstance(schema) : ''}
727}
728${schema.multipleInstances ? instanceWrapperSource(schema) : ''}
729
730export const New${NAME}WithId = (id: uid, creatorUsername?: string): ${NAME} => {
731 if (hasRecord(${NAME}.type, id, creatorUsername)) {
732 return getRecord(${NAME}.type, id, creatorUsername) as ${NAME};
733 }
734 const record = new ${NAME}(${NAME}.createNewRequest(id), creatorUsername);
735 addRecord(${NAME}.type, record, creatorUsername);
736 return record;
737}
738`;
739}
740
741export const generateIndexSource = (schemas: ModelDefinition[]): string => {
742 const lines = [
743 `// AUTO-GENERATED by @the_library/public/models schemaGenerator โ run \`pnpm generate\` to rebuild.`,
744 ...schemas.map(s => `export * from "./${s.name}.ts";`),
745 ``,
746 ...schemas.map(s => `import { ${s.name} } from "./${s.name}.ts";`),
747 `export type ModelClass = { new(json: any, creatorUsername?: string): any; type: number; LoadAsync(id: number, creatorUsername?: string): Promise<any> };`,
748 `export const ModelTypes: Record<number, ModelClass> = {`,
749 ...schemas.map(s => ` [${s.objectId}]: ${s.name},`),
750 `};`
751 ];
752 return lines.join('\n') + '\n';
753}
754
755export const generateCrosswalkWorkerSource = (schemas: ModelDefinition[]): string => {
756 const maps: string[] = [];
757 const extractors: string[] = [];
758
759 // Mode DERIVED map
760 maps.push(`const derivedToInstance = new Map<string, { wireId: number; instanceIndex: number }>();`);
761
762 const entityStds = new Set<string>();
763 const instanceStds = new Set<string>();
764
765 for (const schema of schemas) {
766 const parentStrats = Array.isArray(schema.uidStrategy) ? schema.uidStrategy : (schema.uidStrategy ? [schema.uidStrategy] : []);
767 for (const std of parentStrats) if (std !== 'native-only') entityStds.add(std);
768
769 if (schema.multipleInstances) {
770 const instStrats = Array.isArray(schema.instanceUidStrategy) ? schema.instanceUidStrategy : (schema.instanceUidStrategy ? [schema.instanceUidStrategy] : []);
771 for (const std of instStrats) if (std !== 'native-only') instanceStds.add(std);
772 }
773 }
774
775 for (const std of entityStds) maps.push(`const ${std}ToEntity = new Map<string, { wireId: number }>();`);
776 for (const std of instanceStds) maps.push(`const ${std}ToInstance = new Map<string, { wireId: number; instanceIndex: number }>();`);
777
778 for (const schema of schemas) {
779 const parentStrats = Array.isArray(schema.uidStrategy) ? schema.uidStrategy : (schema.uidStrategy ? [schema.uidStrategy] : []);
780 const instStrats = schema.multipleInstances ? (Array.isArray(schema.instanceUidStrategy) ? schema.instanceUidStrategy : (schema.instanceUidStrategy ? [schema.instanceUidStrategy] : [])) : [];
781 const prefix = schema.typePrefix || 'XX';
782 const idFields = schema.instanceFields?.filter(f => (f as any).identityComponent) || [];
783
784 if (parentStrats.length === 0 && instStrats.length === 0 && idFields.length === 0) continue;
785
786 extractors.push(`
787 if (set.objectType === ${schema.objectId}) {
788 const slim = set.slim;
789 const ext = set.extended;
790
791 // Read parent standards arrays
792 ${parentStrats.filter(s => s !== 'native-only').map(std => {
793 const pk = schema.standardPks?.[std] ?? std;
794 const colNameStr = colName(pk, 'string');
795 return `const ${std}Col = slim?.columns.get('${colNameStr}') || ext?.columns.get('${colNameStr}');`;
796 }).join('\n ')}
797
798 for (const [id, rowIndex] of set.idIndex) {
799 // Populate parent standards
800 ${parentStrats.filter(s => s !== 'native-only').map(std => `
801 if (${std}Col && ${std}Col[rowIndex]) {
802 ${std}ToEntity.set(\`\${lang}:\${${std}Col[rowIndex]}\`, { wireId: id });
803 }
804 `).join('')}
805
806 ${schema.multipleInstances ? `
807 const instances = ext?.instances?.[rowIndex];
808 if (instances) {
809 // 1. Gather all source PIDs for this record
810 const sourcePIDs: string[] = [\`\${encodePID('${prefix}', id)}\`];
811
812 ${parentStrats.filter(s => s !== 'native-only').map(std => `
813 if (${std}Col && ${std}Col[rowIndex]) sourcePIDs.push(\`${std}:\${${std}Col[rowIndex]}\`);
814 `).join('')}
815
816 // Add instance-level Mode S
817 for (let i = 0; i < instances.length; i++) {
818 ${instStrats.filter(s => s !== 'native-only').map(std => `
819 if (instances[i]['${std}']) sourcePIDs.push(\`${std}:\${instances[i]['${std}']}\`);
820 `).join('')}
821 }
822
823 // 2. Emit mappings
824 for (let i = 0; i < instances.length; i++) {
825 ${instStrats.filter(s => s !== 'native-only').map(std => `
826 const ${std}Val = instances[i]['${std}'];
827 if (${std}Val) {
828 ${std}ToInstance.set(\`\${lang}:\${${std}Val}\`, { wireId: id, instanceIndex: i });
829 }`).join('\n ')}
830
831 ${idFields.length > 0 ? `
832 const identityParts = [${idFields.map(f => `instances[i]['${f.name}']`).join(', ')}];
833 if (identityParts.every(p => p != null && p !== "")) {
834 const disc = identityParts.map(p => encodeURIComponent(String(p))).join(':');
835 for (const src of sourcePIDs) {
836 const sfcuid = \`DL:C:DERIVED:\${src}:\${disc}\`;
837 derivedToInstance.set(\`\${lang}:\${sfcuid}\`, { wireId: id, instanceIndex: i });
838 }
839 }` : ''}
840 }
841 }
842 ` : ''}
843 }
844 }`);
845 }
846
847 return `// AUTO-GENERATED by @the_library/public/models schemaGenerator
848import type { Lang } from "../../interfaces.ts";
849import type { SidecarSet } from "../../worker/libs/initialLoad.ts";
850import { encodePID } from "../../pid.ts";
851
852${maps.join('\n')}
853
854export function processSidecarSets(sets: SidecarSet[]) {
855 for (const set of sets) {
856 const lang = set.lang;
857${extractors.join('\n')}
858 }
859}
860
861self.onmessage = async (e: MessageEvent) => {
862 const { cmd, payload, msgId } = e.data;
863
864 if (cmd === 'init_crosswalk' || cmd === 'load_lang_crosswalk') {
865 const sets = payload.sidecars as SidecarSet[];
866 processSidecarSets(sets);
867 self.postMessage({ msgId, status: 'registered' });
868 } else if (cmd === 'resolve_derived') {
869 const { sfcuid, lang } = payload;
870 const result = derivedToInstance.get(\`\${lang}:\${sfcuid}\`);
871 self.postMessage({ msgId, result: result || null });
872 } else if (cmd === 'resolve_standard') {
873 const { standardId, value, lang } = payload;
874 const key = \`\${lang}:\${value}\`;
875 let result: any = null;
876 switch (standardId) {
877 ${Array.from(entityStds).map(std => `case '${std}': result = ${std}ToEntity.get(key) || null; break;`).join('\n ')}
878 ${Array.from(instanceStds).map(std => `case '${std}': result = ${std}ToInstance.get(key) || null; break;`).join('\n ')}
879 }
880 self.postMessage({ msgId, result });
881 }
882};
883`;
884}
885
886export const generateCrosswalkClientSource = (): string => {
887 return `// AUTO-GENERATED by @the_library/public/models schemaGenerator
888import type { Lang } from "../interfaces.ts";
889import type { SidecarSet } from "../worker/libs/initialLoad.ts";
890
891export class ExternalIdCrosswalk {
892 static instance: ExternalIdCrosswalk | null = null;
893 private worker: Worker;
894 private readyPromise: Promise<void>;
895 private resolveReady!: () => void;
896 private rejectReady!: (e: Error) => void;
897 public ready = false;
898 private nextMsgId = 1;
899 private pending = new Map<number, (res: any) => void>();
900
901 constructor(workerPath = '/ExternalIdCrosswalkWorker.js') {
902 this.worker = new Worker(workerPath, { type: 'module' });
903 this.readyPromise = new Promise((resolve, reject) => {
904 this.resolveReady = resolve;
905 this.rejectReady = reject;
906 });
907 ExternalIdCrosswalk.instance = this;
908 (globalThis as any).ExternalIdCrosswalk = ExternalIdCrosswalk;
909
910 this.worker.onmessage = (e) => {
911 const { msgId, result, status } = e.data;
912 if (msgId && this.pending.has(msgId)) {
913 this.pending.get(msgId)!(result ?? status);
914 this.pending.delete(msgId);
915 }
916 };
917 this.worker.onerror = (err) => {
918 console.error("[ExternalIdCrosswalk] Worker error:", err);
919 // If it crashes before ready, reject the ready promise
920 if (!this.ready) {
921 this.rejectReady(new Error("ExternalIdCrosswalk worker crashed during initialization: " + err.message));
922 }
923 };
924 this.worker.onmessageerror = (err) => {
925 console.error("[ExternalIdCrosswalk] Worker message error:", err);
926 };
927 }
928
929 async whenReady() {
930 return this.readyPromise;
931 }
932
933 private request(cmd: string, payload: any): Promise<any> {
934 return new Promise(resolve => {
935 const msgId = this.nextMsgId++;
936 this.pending.set(msgId, resolve);
937 this.worker.postMessage({ cmd, payload, msgId });
938 });
939 }
940
941 async init(sidecars: SidecarSet[]) {
942 await this.request('init_crosswalk', { sidecars });
943 this.ready = true;
944 this.resolveReady();
945 }
946
947 async loadLang(sidecars: SidecarSet[]) {
948 await this.request('load_lang_crosswalk', { sidecars });
949 }
950
951 async resolveDerived(sfcuid: string, lang: Lang) {
952 return this.request('resolve_derived', { sfcuid, lang });
953 }
954
955 async resolveForInstance(standardId: string, value: string, lang: Lang) {
956 return this.request('resolve_standard', { standardId, value, lang });
957 }
958}
959`;
960}
961
962