/** * Schema definition system for Loro Mirror * * This module provides utilities to define schemas that map between JavaScript types and Loro CRDT types. */ import { AnySchemaOptions, AnySchemaType, BooleanSchemaType, ContainerSchemaType, IgnoreSchemaType, LoroListSchema, LoroMapSchema, LoroMapSchemaWithCatchall, LoroMovableListSchema, LoroTextSchemaType, LoroTreeSchema, NumberSchemaType, RootSchemaDefinition, RootSchemaType, SchemaDefinition, SchemaOptions, SchemaType, StringSchemaType, InferType, TransformDefinition, } from "./types.js"; /** * String schema builder with transform method. * Transform decode/encode never receive null/undefined - they pass through as-is. */ type StringSchemaBuilder = StringSchemaType & { options: O } & { transform: ( def: TransformDefinition, ) => StringSchemaType & { options: O; transform: TransformDefinition; }; }; type StringSchemaFactory = { (): StringSchemaBuilder; ( options: O, ): StringSchemaBuilder; ( options: O, ): StringSchemaBuilder; }; /** * Number schema builder with transform method. * Transform decode/encode never receive null/undefined - they pass through as-is. */ type NumberSchemaBuilder = NumberSchemaType & { options: O; } & { transform: ( def: TransformDefinition, ) => NumberSchemaType & { options: O; transform: TransformDefinition; }; }; /** * Boolean schema builder with transform method. * Transform decode/encode never receive null/undefined - they pass through as-is. */ type BooleanSchemaBuilder = BooleanSchemaType & { options: O; } & { transform: ( def: TransformDefinition, ) => BooleanSchemaType & { options: O; transform: TransformDefinition; }; }; export * from "./types.js"; export * from "./validators.js"; /** * Create a schema definition */ export function schema< T extends Record, O extends SchemaOptions = {}, >( definition: RootSchemaDefinition, options?: O, ): RootSchemaType & { options: O } { return { type: "schema" as const, definition, options: options || ({} as O), getContainerType() { return "Map"; }, } as RootSchemaType & { options: O }; } /** * Define a string field */ schema.String = (function < T extends string = string, O extends SchemaOptions = {}, >(options?: O): StringSchemaBuilder { const baseSchema = { type: "string" as const, options: (options || {}) as O, getContainerType: () => { return null; }, }; return { ...baseSchema, transform: (def: TransformDefinition) => ({ ...baseSchema, transform: def, }), } as StringSchemaBuilder; }) as StringSchemaFactory; /** * Define an any field (runtime-inferred by Mirror) */ schema.Any = function ( options?: O, ) { return { type: "any" as const, options: options || ({} as O), getContainerType: () => { return null; }, } as AnySchemaType & { options: O }; }; /** * Define a number field */ schema.Number = function ( options?: O, ): NumberSchemaBuilder { const baseSchema = { type: "number" as const, options: options || ({} as O), getContainerType: () => { return null; // Primitive type, no container }, }; return { ...baseSchema, transform: (def: TransformDefinition) => ({ ...baseSchema, transform: def, }), } as NumberSchemaBuilder; }; /** * Define a boolean field */ schema.Boolean = function ( options?: O, ): BooleanSchemaBuilder { const baseSchema = { type: "boolean" as const, options: options || ({} as O), getContainerType: () => { return null; // Primitive type, no container }, }; return { ...baseSchema, transform: (def: TransformDefinition) => ({ ...baseSchema, transform: def, }), } as BooleanSchemaBuilder; }; /** * Define a field to be ignored (not synced with Loro) */ schema.Ignore = function (options?: O) { return { type: "ignore" as const, options: options || ({} as O), getContainerType: () => { return null; }, } as IgnoreSchemaType & { options: O }; }; /** * Define a Loro map */ schema.LoroMap = function < T extends Record = {}, O extends SchemaOptions = {}, >( definition: SchemaDefinition, options?: O, ): LoroMapSchema & { options: O } & { catchall: ( catchallSchema: C, ) => LoroMapSchemaWithCatchall; } { const baseSchema = { type: "loro-map" as const, definition, options: options || ({} as O), getContainerType: () => { return "Map"; }, } as LoroMapSchema & { options: O }; // Add catchall method like zod const schemaWithCatchall = { ...baseSchema, catchall: ( catchallSchema: C, ): LoroMapSchemaWithCatchall => { return { ...baseSchema, catchallType: catchallSchema, catchall: ( newCatchallSchema: NewC, ) => { return { ...baseSchema, catchallType: newCatchallSchema, catchall: schemaWithCatchall.catchall, } as LoroMapSchemaWithCatchall; }, } as LoroMapSchemaWithCatchall; }, }; return schemaWithCatchall as LoroMapSchema & { options: O } & { catchall: ( catchallSchema: C, ) => LoroMapSchemaWithCatchall; }; }; /** * Create a dynamic record schema (like zod's z.record) */ schema.LoroMapRecord = function < T extends SchemaType, O extends SchemaOptions = {}, >( valueSchema: T, options?: O, ): LoroMapSchemaWithCatchall<{}, T> & { options: O } { return { type: "loro-map" as const, definition: {}, catchallType: valueSchema, options: options || ({} as O), getContainerType: () => { return "Map"; }, catchall: ( newCatchallSchema: NewC, ): LoroMapSchemaWithCatchall<{}, NewC> => { return schema.LoroMapRecord(newCatchallSchema, options); }, } as LoroMapSchemaWithCatchall<{}, T> & { options: O }; }; /** * Define a Loro list */ schema.LoroList = function ( itemSchema: T, idSelector?: (item: InferType) => string, options?: O, ): LoroListSchema & { options: O } { return { type: "loro-list" as const, itemSchema, idSelector: idSelector as unknown as (item: unknown) => string, options: options || ({} as O), getContainerType: () => { return "List"; }, } as LoroListSchema & { options: O }; }; schema.LoroMovableList = function < T extends SchemaType, O extends SchemaOptions = {}, >( itemSchema: T, idSelector: (item: InferType) => string, options?: O, ): LoroMovableListSchema & { options: O } { return { type: "loro-movable-list" as const, itemSchema, idSelector: idSelector as unknown as (item: unknown) => string, options: options || ({} as O), getContainerType: () => { return "MovableList"; }, } as LoroMovableListSchema & { options: O }; }; /** * Define a Loro text field */ schema.LoroText = function ( options?: O, ): LoroTextSchemaType & { options: O } { return { type: "loro-text" as const, options: options || ({} as O), getContainerType: () => { return "Text"; }, } as LoroTextSchemaType & { options: O }; }; /** * Define a Loro tree * * Each tree node has a `data` map described by `nodeSchema`. */ // oxlint-disable-next-line no-explicit-any schema.LoroTree = function >( nodeSchema: LoroMapSchema, options?: SchemaOptions, ): LoroTreeSchema { return { type: "loro-tree" as const, nodeSchema, options: options || {}, getContainerType() { return "Tree"; }, }; };