/** * @sylphx/lens-core - Model Definition * * v3.0 API - Plain object model definitions only. * No builder functions, no t. prefix. * * @example * ```typescript * import { model, id, string, int, list, nullable } from '@sylphx/lens-core' * * const User = model('User', { * id: id(), * name: string(), * bio: nullable(string()), * tags: list(string()), * posts: list(() => Post), * profile: Profile, // direct model reference * }) * ``` */ import type { EntityMarker } from "@sylphx/standard-entity"; import type { FieldDef } from "./fields.js"; import type { InferEntity } from "./infer.js"; import type { EntityDefinition, FieldType } from "./types.js"; // ============================================================================= // Model Symbol // ============================================================================= /** Symbol to identify model definitions */ export const MODEL_SYMBOL: unique symbol = Symbol("lens:model"); // ============================================================================= // Model Definition Types // ============================================================================= /** * Plain object field definition. * Each field can be a scalar type, model reference, or wrapped type. * * @example * ```typescript * { * id: id(), * name: string(), * bio: nullable(string()), * tags: list(string()), * posts: list(() => Post), * profile: Profile, * } * ``` */ export type PlainFieldDefinition = Record; /** * Model definition with name and fields. * * Implements StandardEntity protocol for type-safe Reify operations. * Extends EntityMarker from @sylphx/standard-entity for protocol compliance. */ export interface ModelDef< Name extends string = string, Fields extends EntityDefinition = EntityDefinition, > extends EntityMarker { [MODEL_SYMBOL]: true; /** Model name */ _name: Name; /** Model fields */ readonly fields: Fields; /** Whether this model has an id field (normalizable) */ readonly _hasId: boolean; } /** * Extract the inferred model type from a ModelDef. * Use this when you need the actual TypeScript type of a model. * * @example * ```typescript * type UserData = InferModelType; * // { id: string; name: string; email: string; ... } * ``` */ export type InferModelType = M extends ModelDef ? InferEntity : never; // ============================================================================= // Type Helper // ============================================================================= /** * Type helper to process plain field definitions. * Maps FieldDef types to their processed FieldType equivalents. */ type ProcessedFields = { [K in keyof T]: T[K] extends FieldType ? FieldType : FieldType; }; // ============================================================================= // Model Factory (Typed Context) // ============================================================================= /** * Factory for creating models with typed context. * Returns a function that creates models with plain object fields. * * @example * ```typescript * const { model } = lens(); * * const User = model("User", { * id: id(), * name: string(), * }); * ``` */ export type ModelFactory<_TContext> = ( name: Name, fields: FieldDefs, ) => ModelDef>; function createModelFactory(): ModelFactory { return ( name: Name, fields: FieldDefs, ): ModelDef> => { const processedFields = processPlainFields(fields); return createModelDef>( name, processedFields as ProcessedFields, ); }; } // ============================================================================= // Model Definition Function // ============================================================================= /** * Define a model with fields. * * Models with `id` field are normalizable and cacheable. * Models without `id` are pure types. * * @example * ```typescript * import { model, id, string, list, nullable } from '@sylphx/lens-core' * * const User = model('User', { * id: id(), * name: string(), * bio: nullable(string()), * posts: list(() => Post), * }) * ``` */ // model() - returns factory for typed context export function model(): ModelFactory; // model("Name", { fields }) - plain object definition export function model( name: Name, fields: FieldDefs, ): ModelDef>; export function model< TContext = unknown, Name extends string = string, FieldDefs extends PlainFieldDefinition = PlainFieldDefinition, >( nameOrNothing?: Name, fields?: FieldDefs, ): ModelFactory | ModelDef> { // model() - returns factory if (nameOrNothing === undefined) { return createModelFactory(); } // model("Name", { fields }) - plain object definition if (fields === undefined) { throw new Error( `model("${nameOrNothing}") requires fields. Use: model("${nameOrNothing}", { id: id(), ... })`, ); } const processedFields = processPlainFields(fields); return createModelDef>( nameOrNothing, processedFields as ProcessedFields, ); } /** * Process plain field definitions into EntityDefinition. * Converts field defs (scalars, model refs, list/nullable wrappers) to FieldType instances. */ function processPlainFields(fieldDefs: PlainFieldDefinition): EntityDefinition { // FieldDef includes FieldType, so we can use the values directly // Cast through unknown since FieldDef is a union that includes FieldType return fieldDefs as unknown as EntityDefinition; } // ============================================================================= // Internal Helper // ============================================================================= /** * Create a model definition. */ function createModelDef( name: Name, fields: Fields, ): ModelDef { // Check if model has an id field const hasId = "id" in fields; const modelDef = { [MODEL_SYMBOL]: true, _name: name, fields, _hasId: hasId, // StandardEntity protocol - runtime marker for type-safe Reify operations "~entity": { name: name, type: undefined as unknown, // Phantom type - not used at runtime }, }; return modelDef as ModelDef; } // ============================================================================= // Type Guards // ============================================================================= /** Check if value is a ModelDef */ export function isModelDef(value: unknown): value is ModelDef { return typeof value === "object" && value !== null && MODEL_SYMBOL in value; } /** Check if model is normalizable (has id) */ export function isNormalizableModel(value: unknown): boolean { return isModelDef(value) && value._hasId; }