import DataPack from "./datapack.js"; import { DatabaseError } from "olmdb/lowlevel"; import { currentTxn } from "./edinburgh.js"; import { Model, modelRegistry } from "./models.js"; import type { AnyModelClass, ModelBase as ModelInstanceBase } from "./models.js"; import { assert, addErrorPath, dbGet } from "./utils.js"; /** * @internal Abstract base class for all type wrappers in the Edinburgh ORM system. * * This is an implementation detail and should not be referenced directly in user code. * Type wrappers define how values are serialized to/from the database and how they are validated. * Each type wrapper must implement serialization, deserialization, and validation logic. * * @template T - The TypeScript type this wrapper represents. */ export abstract class TypeWrapper { /** @internal Used for TypeScript type inference - this field is required for the type system */ _T!: T; /** A string identifier for this type, used during serialization */ abstract kind: string; constructor() {} /** * Serialize a value from an object property to a Pack. * @param value The value to serialize. * @param pack The Pack instance to write to. */ abstract serialize(value: T, pack: DataPack): void; /** * Deserialize a value from a Pack into an object property. * @param pack The Pack instance to read from. */ abstract deserialize(pack: DataPack): T; /** * Validate a value. * @param value The value to validate. * @returns - A DatabaseError if validation fails. */ abstract getError(value: T): DatabaseError | void; /** * Serialize type metadata to a Pack (for schema serialization). * @param pack The Pack instance to write to. */ serializeType(pack: DataPack) {} /** * Check if indexing should be skipped for this field value. * @param obj The object containing the value. * @param prop The property name or index. * @returns true if indexing should be skipped. */ containsNull(value: T): boolean { return false; } toString(): string { return `${this.kind}`; } clone(value: T): T { return value; } equals(value1: T, value2: T): boolean { return value1 === value2; } getLinkedModel(): undefined | AnyModelClass { return; } } export interface TypeWrapper { /** * Generate a default value for this type. * @param model The model instance. * @returns The default value. */ default?(model: any): T; } // Hidden type-only metadata used to widen lookup arguments without widening assignment types. export declare const QUERY_ARG: unique symbol; export type LinkTargetPKArgs = T extends { get(...args: infer PKA): any } ? PKA : never; export type LinkPrimaryKeyInput = PKA extends readonly [infer ONLY] ? ONLY | PKA : PKA; type QueryArgCarrier = { readonly [QUERY_ARG]?: QUERY; }; export type FieldValue> = TYPE extends TypeWrapper ? T : never; export type FieldQueryArg = T extends ModelInstanceBase ? T | LinkPrimaryKeyInput> : T extends { readonly [QUERY_ARG]?: infer QUERY } ? Exclude : T; class StringType extends TypeWrapper { kind = 'string'; serialize(value: string, pack: DataPack) { pack.write(value); } deserialize(pack: DataPack): string { return pack.readString(); } getError(value: string) { if (typeof value !== 'string') { return new DatabaseError(`Expected string, got ${typeof value}`, 'INVALID_TYPE'); } } } class OrderedStringType extends StringType { serialize(value: string, pack: DataPack) { pack.writeOrderedString(value); } } class NumberType extends TypeWrapper { kind = 'number'; serialize(value: number, pack: DataPack) { pack.write(value); } deserialize(pack: DataPack): number { return pack.readNumber(); } getError(value: number) { if (typeof value !== 'number' || isNaN(value)) { return new DatabaseError(`Expected number, got ${typeof value}`, 'INVALID_TYPE'); } } } class DateTimeType extends TypeWrapper { kind = 'dateTime'; serialize(value: Date, pack: DataPack) { pack.write(value); } deserialize(pack: DataPack): Date { return pack.readDate();; } getError(value: Date) { if (!(value instanceof Date)) { return new DatabaseError(`Expected Date, got ${typeof value}`, 'INVALID_TYPE'); } } default(): Date { return new Date(); } } class BooleanType extends TypeWrapper { kind = 'boolean'; serialize(value: boolean, pack: DataPack) { pack.write(value); } deserialize(pack: DataPack): boolean { return pack.readBoolean(); } getError(value: boolean) { if (typeof value !== 'boolean') { return new DatabaseError(`Expected boolean, got ${typeof value}`, 'INVALID_TYPE'); } } } /** * @internal Type wrapper for array values with optional length constraints. * @template T - The type of array elements. */ class ArrayType extends TypeWrapper { kind = 'array'; /** * Create a new ArrayType. * @param inner Type wrapper for array elements. * @param opts Array constraints (min/max length). */ constructor(public inner: TypeWrapper, public opts: {min?: number, max?: number} = {}) { super(); } serialize(value: T[], pack: DataPack) { pack.write(value.length); for(let i=0; i this.opts.max) { return new DatabaseError(`Array length ${value.length} is greater than maximum ${this.opts.max}`, 'OUT_OF_BOUNDS'); } for (let i = 0; i < value.length; i++) { let error = this.inner.getError(value[i]); if (error) return addErrorPath(error, i); } } serializeType(pack: DataPack): void { serializeType(this.inner, pack); } static deserializeType(pack: DataPack, featureFlags: number): ArrayType { const inner = deserializeType(pack, featureFlags); return new ArrayType(inner); } default(): T[] { return []; } clone(value: T[]): T[] { return value.map(this.inner.clone.bind(this.inner)); } equals(a: T[], b: T[]): boolean { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (!this.inner.equals(a[i], b[i])) return false; } return true; } toString() { return `array<${this.inner}>`; } getLinkedModel() { return this.inner.getLinkedModel(); } } /** * @internal Type wrapper for array values with optional length constraints. * @template T - The type of array elements. */ export class SetType extends TypeWrapper> { kind = 'set'; /** * Create a new SetType. * @param inner Type wrapper for set elements. */ constructor(public inner: TypeWrapper, public opts: {min?: number, max?: number} = {}) { super(); } serialize(value: Set, pack: DataPack) { pack.write(value.size); for (const item of value) { this.inner.serialize(item, pack); } } deserialize(pack: DataPack): Set { const length = pack.readNumber(); const result = new Set(); for (let i = 0; i < length; i++) { result.add(this.inner.deserialize(pack)); } return result; } getError(value: Set) { if (!(value instanceof Set)) { return new DatabaseError(`Expected Set, got ${typeof value}`, 'INVALID_TYPE'); } if (this.opts.min !== undefined && value.size < this.opts.min) { return new DatabaseError(`Set size ${value.size} is less than minimum ${this.opts.min}`, 'OUT_OF_BOUNDS'); } if (this.opts.max !== undefined && value.size > this.opts.max) { return new DatabaseError(`Set size ${value.size} is greater than maximum ${this.opts.max}`, 'OUT_OF_BOUNDS'); } try { for (const item of value) { this.inner.getError(item); } } catch (err) { throw addErrorPath(err, 'item'); } } serializeType(pack: DataPack): void { serializeType(this.inner, pack); } static deserializeType(pack: DataPack, featureFlags: number): SetType { const inner = deserializeType(pack, featureFlags); return new SetType(inner); } default(): Set { return new Set(); } clone(value: Set): Set { const cloned = new Set(); for (const item of value) { cloned.add(this.inner.clone(item)); } return cloned; } equals(a: Set, b: Set): boolean { if (a.size !== b.size) return false; for(const v of a) { if (!b.has(v)) return false; } return true; } toString() { return `set<${this.inner}>`; } getLinkedModel() { return this.inner.getLinkedModel(); } } /** * @internal Type wrapper for Record values. * @template T - The type of record values. */ class RecordType extends TypeWrapper> { kind = 'record'; constructor(public inner: TypeWrapper) { super(); } serialize(value: Record, pack: DataPack) { pack.writeCollectionBoundary('object'); for (const key in value) { pack.writeObjectKey(key); this.inner.serialize(value[key], pack); } pack.writeCollectionBoundary('end'); } deserialize(pack: DataPack): Record { pack.readCollectionBoundary('object'); const result: Record = {}; while (true) { const key = pack.read(); if (key === DataPack.EOD) break; result[key] = this.inner.deserialize(pack); } return result; } getError(value: Record) { if (typeof value !== 'object' || value === null || Array.isArray(value) || (typeof value.length === 'number' && typeof value[Symbol.iterator as any] === 'function')) { return new DatabaseError(`Expected object, got ${typeof value}`, 'INVALID_TYPE'); } for (const key of Object.keys(value)) { const error = this.inner.getError(value[key]); if (error) return addErrorPath(error, key); } } serializeType(pack: DataPack): void { serializeType(this.inner, pack); } static deserializeType(pack: DataPack, featureFlags: number): RecordType { const inner = deserializeType(pack, featureFlags); return new RecordType(inner); } default(): Record { return {}; } clone(value: Record): Record { const result: Record = {}; for (const key of Object.keys(value)) { result[key] = this.inner.clone(value[key]); } return result; } equals(a: Record, b: Record): boolean { const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!(key in b) || !this.inner.equals(a[key], b[key])) return false; } return true; } toString() { return `record<${this.inner}>`; } getLinkedModel() { return this.inner.getLinkedModel(); } } /** * @internal Type wrapper for fixed-shape objects (structs). * * Unlike {@link RecordType}, the set of keys is part of the *schema*, not the * stored data: values are serialized in a fixed key order and the keys are never * written per-record, so an object is as compact as the concatenation of its * fields. Schema serialization records the keys + their types so older records * can still be read (and migrated) after the shape changes. * * @template S - A map of property name → field type wrapper. */ class ObjectType>> extends TypeWrapper<{ [K in keyof S]: FieldValue }> { kind = 'object'; /** Property names in their canonical (definition) order. */ keys: string[]; constructor(public shape: S) { super(); this.keys = Object.keys(shape); } serialize(value: { [K in keyof S]: FieldValue }, pack: DataPack) { for (const key of this.keys) { this.shape[key].serialize((value as any)[key], pack); } } deserialize(pack: DataPack): { [K in keyof S]: FieldValue } { const result: any = {}; for (const key of this.keys) { const value = this.shape[key].deserialize(pack); // Keep optional (undefined) members absent rather than present-as-undefined, // matching the optional-property type the factory produces. if (value !== undefined) result[key] = value; } return result; } getError(value: any) { if (typeof value !== 'object' || value === null || Array.isArray(value)) { return new DatabaseError(`Expected object, got ${value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value}`, 'INVALID_TYPE'); } for (const key of this.keys) { const error = this.shape[key].getError(value[key]); if (error) return addErrorPath(error, key); } } serializeType(pack: DataPack): void { pack.write(this.keys.length); for (const key of this.keys) { pack.write(key); serializeType(this.shape[key], pack); } } static deserializeType(pack: DataPack, featureFlags: number): ObjectType { const count = pack.readNumber(); const shape: Record> = {}; for (let i = 0; i < count; i++) { const key = pack.readString(); shape[key] = deserializeType(pack, featureFlags); } return new ObjectType(shape); } default(model: any): { [K in keyof S]: FieldValue } { // Build a struct where each field takes its own type's default (fields // whose type has no default are left absent — validation will flag any // such field that isn't optional). const result: any = {}; for (const key of this.keys) { const type = this.shape[key]; const value = type.default ? type.default(model) : undefined; if (value !== undefined) result[key] = value; } return result; } clone(value: { [K in keyof S]: FieldValue }): { [K in keyof S]: FieldValue } { const result: any = {}; for (const key of this.keys) { const cloned = this.shape[key].clone((value as any)[key]); if (cloned !== undefined) result[key] = cloned; } return result; } equals(a: { [K in keyof S]: FieldValue }, b: { [K in keyof S]: FieldValue }): boolean { for (const key of this.keys) { if (!this.shape[key].equals((a as any)[key], (b as any)[key])) return false; } return true; } toString() { return `object<{${this.keys.map(k => `${k}: ${this.shape[k]}`).join(', ')}}>`; } getLinkedModel() { let model: AnyModelClass | undefined; for (const key of this.keys) { const m = this.shape[key].getLinkedModel(); if (m) { if (model && model !== m) throw new DatabaseError(`Object type has multiple linked models, unsupported by getLinkedModel()`, 'INVALID_TYPE'); model = m; } } return model; } } /** * @internal Type wrapper for union/discriminated union types. * @template T - The union type this wrapper represents. */ class OrType extends TypeWrapper { kind = 'or'; /** * Create a new OrType. * @param choices Array of type wrappers representing the union choices. */ constructor(public choices: TypeWrapper[]) { super(); } _getChoiceIndex(value: any): number { for (const [i, choice] of this.choices.entries()) { if (!choice.getError(value)) return i; } throw new DatabaseError(`Value does not match any union type: ${value}`, 'INVALID_TYPE'); } serialize(value: T, pack: DataPack) { const choiceIndex = this._getChoiceIndex(value); pack.write(choiceIndex); this.choices[choiceIndex].serialize(value, pack); } deserialize(pack: DataPack) { const index = pack.readNumber(); if (index < 0 || index >= this.choices.length) { throw new DatabaseError(`Could not deserialize invalid union index ${index}`, 'DESERIALIZATION_ERROR'); } const type = this.choices[index]; return type.deserialize(pack); } getError(value: any) { for (const choice of this.choices.values()) { if (!choice.getError(value)) return; } return new DatabaseError(`Value does not match any union type: ${value}`, 'INVALID_TYPE'); } containsNull(value: T): boolean { const choiceIndex = this._getChoiceIndex(value); return this.choices[choiceIndex].containsNull(value); } serializeType(pack: DataPack): void { pack.write(this.choices.length); for (const choice of this.choices) { serializeType(choice, pack); } } static deserializeType(pack: DataPack, featureFlags: number): OrType { const count = pack.readNumber(); const choices: TypeWrapper[] = []; for (let i = 0; i < count; i++) { choices.push(deserializeType(pack, featureFlags)); } return new OrType(choices); } clone(value: T): T { const choiceIndex = this._getChoiceIndex(value); return this.choices[choiceIndex].clone(value); } equals(a: T, b: T): boolean { const ca = this._getChoiceIndex(a); const cb = this._getChoiceIndex(b); return ca === cb && this.choices[ca].equals(a, b); } toString() { // Render the common `E.opt(T)` case (`or(undef, T)`) as `opt` for readability. if (this.choices.length === 2) { const undefIndex = this.choices.findIndex(c => c instanceof LiteralType && c.value === undefined); if (undefIndex >= 0) return `opt<${this.choices[1 - undefIndex]}>`; } return `or<${this.choices.join('|')}>`; } getLinkedModel() { let model; for (const choice of this.choices) { const m = choice.getLinkedModel(); if (m) { if (model && model !== m) throw new DatabaseError(`Union type has multiple linked models, unsupported by getLinkedModel()`, 'INVALID_TYPE'); model = m; } } return model; } } /** * @internal Type wrapper for literal values (constants). * @template T - The literal type this wrapper represents. */ class LiteralType extends TypeWrapper { kind = 'literal'; /** * Create a new LiteralType. * @param value The literal value this type represents. */ constructor(public value: T) { super(); } serialize(value: T, pack: DataPack) { // Literal values don't need to be serialized since they're constants } deserialize(pack: DataPack) { return this.value; } getError(value: any) { if (this.value!==value) { return new DatabaseError(`Invalid literal value ${value} instead of ${this.value}`, 'INVALID_TYPE'); } } serializeType(pack: DataPack): void { pack.write(this.value===undefined ? "" : JSON.stringify(this.value)); } containsNull(value: T): boolean { return value == null; } static deserializeType(pack: DataPack, featureFlags: number): LiteralType { const json = pack.readString(); const value = json==="" ? undefined : JSON.parse(json); return new LiteralType(value); } toString() { return `literal<${JSON.stringify(this.value)}>`; } default(): T { return this.value; } } const ID_SIZE = 8; /** * @internal Type wrapper for auto-generated unique identifier strings. */ class IdentifierType extends TypeWrapper { kind = 'id'; serialize(value: string, pack: DataPack): void { assert(value.length === ID_SIZE); pack.writeIdentifier(value); } deserialize(pack: DataPack) { return pack.readIdentifier(); } getError(value: any) { if (typeof value !== 'string' || value.length !== ID_SIZE) return new DatabaseError(`Invalid ID format: ${value}`, 'VALUE_ERROR'); } serializeType(pack: DataPack): void { } static deserializeType(pack: DataPack, featureFlags: number): IdentifierType { return new IdentifierType(); } default(model: Model): string { // Generate a random ID, and if it already exists in the database, retry. let id: string; do { id = DataPack.generateIdentifier(); } while (dbGet(model._txn.id, new DataPack().write(model.constructor._indexId!).writeIdentifier(id).toUint8Array())); return id; } } /** * @internal Type wrapper for model relationships (foreign keys). * @template T - The target model class type. */ export class LinkType Model> extends TypeWrapper> { kind = 'link'; private TargetModel: T | (() => T); /** * Create a new LinkType. * @param TargetModel The model class this link points to, or a thunk for forward references. */ constructor(TargetModel: T | (() => T)) { super(); this.TargetModel = TargetModel; } getLinkedModel(): AnyModelClass { if (!('getLazy' in (this.TargetModel as any))) this.TargetModel = (this.TargetModel as unknown as () => T)(); return this.TargetModel as any; } serialize(model: InstanceType, pack: DataPack) { pack.write(model.getPrimaryKey()); } deserialize(pack: DataPack): InstanceType { return this.getLinkedModel()._get(currentTxn(), pack.readUint8Array(), false) as InstanceType; } getError(value: InstanceType) { const TargetModel = this.getLinkedModel(); if (!((value as any) instanceof TargetModel)) { return new DatabaseError(`Expected instance of ${TargetModel.tableName}, got ${typeof value}`, 'VALUE_ERROR'); } } serializeType(pack: DataPack): void { pack.write(this.getLinkedModel().tableName); } static deserializeType(pack: DataPack, featureFlags: number): LinkType { const tableName = pack.readString(); const targetModel = modelRegistry[tableName]; if (!targetModel) throw new DatabaseError(`Could not deserialize undefined model ${tableName}`, 'DESERIALIZATION_ERROR'); return new LinkType(targetModel as any); } toString() { return `link<${this.getLinkedModel().tableName}>`; } } /** Type wrapper instance for the string type. */ export const string = new StringType() as TypeWrapper; /** Type wrapper instance for the ordered string type, which is just like a string * except that it sorts lexicographically in the database (instead of by incrementing * length first), making it suitable for index fields that want lexicographic range * scans. Ordered strings are implemented as null-terminated UTF-8 strings, so they * may not contain null characters. */ export const orderedString = new OrderedStringType() as TypeWrapper; /** Type wrapper instance for the number type. */ export const number = new NumberType() as TypeWrapper; /** Type wrapper instance for the date/time type. Stored without timezone info, rounded to whole seconds. */ export const dateTime = new DateTimeType() as TypeWrapper; /** Type wrapper instance for the boolean type. */ export const boolean = new BooleanType() as TypeWrapper; /** Type wrapper instance for the identifier type. */ export const identifier = new IdentifierType() as TypeWrapper; /** Type wrapper instance for the 'undefined' type. */ export const undef = new LiteralType(undefined) as TypeWrapper; /** * Create a literal type wrapper for a constant value. * @template T - The literal type. * @param value The literal value. * @returns A literal type instance. * * @example * ```typescript * const statusType = E.literal("active"); * const countType = E.literal(42); * ``` */ export function literal(value: T): TypeWrapper { return new LiteralType(value); } /** * Create a union type wrapper from multiple type choices. * @template T - Array of type wrapper or basic types. * @param choices The type choices for the union. * @returns A union type instance. * * @example * ```typescript * const stringOrNumber = E.or(E.string, E.number); * const status = E.or("active", "inactive", "pending"); * ``` */ export function or|BasicType)[]>(...choices: T): TypeWrapper> { return new OrType(choices.map(wrapIfLiteral)); } /** * Create an optional type wrapper (allows undefined). * @template T - Type wrapper or basic type to make optional. * @param inner The inner type to make optional. * @returns A union type that accepts the inner type or undefined. * * @example * ```typescript * const optionalString = E.opt(E.string); * const optionalNumber = E.opt(E.number); * ``` */ export function opt|BasicType>(inner: T): TypeWrapper> { return or(undef, inner); } /** * Create an array type wrapper with optional length constraints. * @template T - The element type. * @param inner Type wrapper for array elements. * @param opts Optional constraints (min/max length). * @returns An array type instance. * * @example * ```typescript * const stringArray = E.array(E.string); * const boundedArray = E.array(E.number, {min: 1, max: 10}); * ``` */ export function array(inner: TypeWrapper, opts: {min?: number, max?: number} = {}): TypeWrapper { return new ArrayType(wrapIfLiteral(inner), opts); } /** * Create a Set type wrapper with optional length constraints. * @template T - The element type. * @param inner Type wrapper for set elements. * @param opts Optional constraints (min/max length). * @returns A set type instance. * * @example * ```typescript * const stringSet = E.set(E.string); * const boundedSet = E.set(E.number, {min: 1, max: 10}); * ``` */ export function set(inner: TypeWrapper, opts: {min?: number, max?: number} = {}): TypeWrapper> { return new SetType(wrapIfLiteral(inner), opts); } /** * Create a Record type wrapper for key-value objects with string or number keys. * @template T - The value type. * @param inner Type wrapper for record values. * @returns A record type instance. * * @example * ```typescript * const scores = E.record(E.number); // Record * ``` */ export function record(inner: TypeWrapper): TypeWrapper> { return new RecordType(wrapIfLiteral(inner)); } /** @internal Flatten an intersection into a single, readable object type. */ type Prettify = { [K in keyof T]: T[K] } & {}; /** @internal The TS value type a shape entry (type wrapper or literal) represents. */ type UnwrapField = X extends TypeWrapper ? U : X; /** A shape for {@link object}: a map of property name → field type (or literal). */ export type ObjectShape = Record | BasicType>; /** * The value type produced by {@link object} for a given shape. Keys whose field * type permits `undefined` (e.g. wrapped in {@link opt}) become optional; all * others are required. */ export type ObjectValue = Prettify< { [K in keyof S as undefined extends UnwrapField ? never : K]: UnwrapField } & { [K in keyof S as undefined extends UnwrapField ? K : never]?: UnwrapField } >; /** * Create a fixed-shape object (struct) type wrapper. Unlike {@link record}, the * keys are part of the schema, so values are stored compactly in key order * without repeating the keys per record. Optional (`E.opt`) fields become * optional properties in the resulting type. * * @param shape A map of property name → field type wrapper (or literal value). * @returns An object type instance. * * @example * ```typescript * const point = E.object({ x: E.number, y: E.number }); // { x: number; y: number } * const entry = E.object({ * at: E.number, * what: E.or(E.string, point), // string | { x: number; y: number } * note: E.opt(E.string), // note?: string * }); * ``` */ export function object(shape: S): TypeWrapper> { const wrapped: Record> = {}; for (const key of Object.keys(shape)) wrapped[key] = wrapIfLiteral(shape[key] as any); return new ObjectType(wrapped) as unknown as TypeWrapper>; } /** * Create a link type wrapper for model relationships. * @template T - The target model class. * @param TargetModel The model class this link points to. * @returns A link type instance. * * @example * ```typescript * const Author = E.defineModel("Author", class { * id = E.field(E.identifier); * posts = E.field(E.array(E.link(() => Book))); * }, { pk: "id" }); * * const Book = E.defineModel("Book", class { * id = E.field(E.identifier); * author = E.field(E.link(Author)); * }, { pk: "id" }); * ``` */ export function link Model>( TargetModel: T, ): TypeWrapper>; export function link Model>(TargetModel: () => T): TypeWrapper>; export function link(TargetModel: any): TypeWrapper { return new LinkType(TargetModel); } // Utility types and functions export type BasicType = string | number | boolean | undefined | null; // TypeWrapper export type UnwrapTypes | BasicType)[]> = { [K in keyof T]: T[K] extends TypeWrapper ? U : T[K]; }[number]; function wrapIfLiteral(type: TypeWrapper): TypeWrapper; function wrapIfLiteral(type: T): LiteralType; function wrapIfLiteral(type: any) { return type instanceof TypeWrapper ? type : new LiteralType(type); } /** * Serialize a type wrapper to a Pack for schema persistence. * @param arg The type wrapper to serialize. * @param pack The Pack instance to write to. */ export function serializeType(arg: TypeWrapper, pack: DataPack) { pack.write(arg.kind); arg.serializeType(pack); } const TYPE_WRAPPERS: Record | {deserializeType: (pack: DataPack, featureFlags: number) => TypeWrapper}> = { string: string, number: number, dateTime: dateTime, boolean: boolean, array: ArrayType, set: SetType, record: RecordType, object: ObjectType, or: OrType, literal: LiteralType, id: identifier, link: LinkType, }; /** * Deserialize a type wrapper from a Pack. * @param pack The Pack instance to read from. * @param featureFlags Feature flags for version compatibility. * @returns The deserialized type wrapper. */ export function deserializeType(pack: DataPack, featureFlags: number): TypeWrapper { const kind = pack.readString(); const TypeWrapper = TYPE_WRAPPERS[kind]; if (!TypeWrapper) throw new DatabaseError(`Unknown field type in database: ${kind}`, 'CONSISTENCY_ERROR'); if ('deserializeType' in TypeWrapper) { return TypeWrapper.deserializeType(pack, featureFlags); } else { return TypeWrapper; } }