/** * @sylphx/lens-core - Unified Factory * * Single factory function that provides typed model, query, mutation, and resolver builders. * Eliminates repetitive context typing across your codebase. * * @example * ```typescript * import { lens, id, string, list } from '@sylphx/lens-core'; * * type AppContext = { db: DB; user: User }; * * // Create all typed builders at once * const { model, query, mutation, resolver } = lens(); * * // Define models with typed context (no need to repeat AppContext!) * const User = model("User", { * id: id(), * name: string(), * posts: list(() => Post), * }).resolve({ * posts: ({ source, ctx }) => ctx.db.posts.filter(...) * }); * * const getUser = query() * .args(z.object({ id: z.string() })) * .resolve(({ args, ctx }) => ctx.db.user.find(args.id)); * * const createPost = mutation() * .args(z.object({ title: z.string() })) * .resolve(({ args, ctx }) => ctx.db.post.create(args)); * ``` * * @example With plugins (type-safe extensions) * ```typescript * import { lens, optimisticPlugin } from '@sylphx/lens-core'; * * // With plugins - .optimistic() is type-safe * const { model, mutation } = lens({ plugins: [optimisticPlugin()] }); * * const updateUser = mutation() * .args(z.object({ id: z.string(), name: z.string() })) * .returns(User) * .optimistic('merge') // ✅ Available with optimisticPlugin * .resolve(({ args, ctx }) => ctx.db.user.update(args)); * ``` */ import type { StepBuilder } from "@sylphx/reify"; import type { InferReturnType, MutationBuilderWithOptimistic, MutationBuilderWithReturns2, MutationDef, OptimisticDSL, QueryBuilder, ResolverFn, ReturnSpec, SubscriptionBuilder, ZodLikeSchema, } from "./operations/index.js"; import { mutation as createMutation, query as createQuery, subscription as createSubscription, } from "./operations/index.js"; import type { ExtractPluginExtensions, HasPlugin, PluginExtension, RuntimePlugin, } from "./plugin/types.js"; import type { FieldBuilder, FieldDef, ResolverDef } from "./resolvers/index.js"; import { resolver as createResolver } from "./resolvers/index.js"; import type { ModelFactory } from "./schema/model.js"; import { model as createModel } from "./schema/model.js"; // ============================================================================= // Lens Factory Types // ============================================================================= /** * Structural type for resolver entity parameter. * Matches both EntityDef and ModelDef. */ type ResolverEntity = { readonly _name: string | undefined; readonly fields: Record; }; /** * Any field definition for resolver. */ type AnyResolverFieldDef = | FieldDef | ((params: { source: any; ctx: TContext }) => any); /** * Typed resolver factory function. * Enforces that ALL entity fields must have a resolver. */ export type LensResolver = ( entity: TEntity, builder: (f: FieldBuilder) => { [K in keyof TEntity["fields"]]: AnyResolverFieldDef; }, ) => ResolverDef>, TContext>; /** * Typed query factory function */ export interface LensQuery { (): QueryBuilder; (name: string): QueryBuilder; } /** * Typed subscription factory function */ export interface LensSubscription { (): SubscriptionBuilder; (name: string): SubscriptionBuilder; } /** * Typed model factory function. * Creates models with pre-typed context using plain object fields. */ export type LensModel = ModelFactory; // ============================================================================= // Plugin-Aware Mutation Builder Types // ============================================================================= /** * Mutation builder with plugin support. * Carries TPlugins through the entire chain. */ export interface LensMutationBuilder< _TInput, TOutput, TContext, TPlugins extends readonly PluginExtension[], > { /** Define args validation schema */ args(schema: ZodLikeSchema): LensMutationBuilderWithArgs; } /** * Mutation builder after .args() with plugin support. */ export interface LensMutationBuilderWithArgs< TInput, _TOutput, TContext, TPlugins extends readonly PluginExtension[], > { /** Define return type */ returns( spec: R, ): LensMutationBuilderWithReturns, TContext, TPlugins>; /** Define resolver directly (for simple return types) */ resolve(fn: ResolverFn): MutationDef; } /** * Mutation builder after .returns() with plugin support. * * Uses conditional type to select the appropriate interface: * - With optimistic plugin: interface with proper optimistic() overloads * - Without: base interface with just resolve() * * This approach preserves TypeScript's overload resolution for better * callback parameter inference. */ export type LensMutationBuilderWithReturns< TInput, TOutput, TContext, TPlugins extends readonly PluginExtension[], > = HasPlugin extends true ? LensMutationBuilderWithReturnsAndOptimistic : MutationBuilderWithReturns2; /** * Mutation builder with optimistic() overloads. * This interface preserves proper overload semantics for callback inference. * * IMPORTANT: Callback overload comes FIRST to enable proper TypeScript inference * for inline arrow functions. TypeScript tries overloads in order. */ export interface LensMutationBuilderWithReturnsAndOptimistic extends MutationBuilderWithReturns2 { /** * Define optimistic update behavior with typed callback. * The callback receives `{ args }` with the args type inferred from `.args()`. * * @param callback - Callback that receives typed args and returns step builders * @returns Builder with .resolve() method * * @example * ```typescript * .optimistic(({ args }) => [ * e.update(User, { id: args.id, name: args.name }), * ]) * ``` */ optimistic( callback: (ctx: { args: TInput }) => StepBuilder[], ): MutationBuilderWithOptimistic; /** * Define optimistic update behavior with DSL spec. * * @param spec - Optimistic update specification (sugar or Pipeline) * @returns Builder with .resolve() method * * @example * ```typescript * .optimistic("merge") // Merge args with existing entity * .optimistic("create") // Create new entity from args * .optimistic({ merge: { published: true } }) // Merge specific fields * ``` */ optimistic(spec: OptimisticDSL): MutationBuilderWithOptimistic; } /** * Typed mutation factory function with plugin support. */ export interface LensMutation { (): LensMutationBuilder; (name: string): LensMutationBuilder; } // ============================================================================= // Lens Result Types // ============================================================================= /** * Result of lens() without plugins. */ export interface Lens { /** * Create a model with pre-typed context. */ model: LensModel; /** * Create a resolver with pre-typed context. */ resolver: LensResolver; /** * Create a query with pre-typed context. */ query: LensQuery; /** * Create a mutation with pre-typed context. * No plugin methods available (use lens({ plugins }) to enable). */ mutation: LensMutation; /** * Create a subscription with pre-typed context. */ subscription: LensSubscription; } /** * Result of lens({ plugins }) with plugins. * * @typeParam TContext - User context type * @typeParam TPlugins - Tuple of plugin extension types */ export interface LensWithPlugins { /** * Create a model with pre-typed context. */ model: LensModel; /** * Create a resolver with pre-typed context. */ resolver: LensResolver; /** * Create a query with pre-typed context. */ query: LensQuery; /** * Create a mutation with pre-typed context and plugin methods. */ mutation: LensMutation; /** * Create a subscription with pre-typed context. */ subscription: LensSubscription; /** * Runtime plugins for use with createApp(). */ plugins: RuntimePlugin[]; } // ============================================================================= // Lens Configuration // ============================================================================= /** * Configuration for lens() with plugins. */ export interface LensConfig< TPlugins extends readonly RuntimePlugin[] = readonly RuntimePlugin[], > { /** * Plugins that extend builder functionality. * * @example * ```typescript * const { mutation } = lens({ * plugins: [optimisticPlugin()], * }); * ``` */ plugins?: TPlugins; } /** * Configuration for lens() with plugins (plugins required). * Used internally for better type inference. */ export interface LensConfigWithPlugins[]> { plugins: TPlugins; } // ============================================================================= // Lens Factory Implementation // ============================================================================= /** * Create typed query, mutation, and resolver builders with shared context. * * This is the primary API for Lens - call once with your context type, * then use the returned builders everywhere. * * @example Without plugins * ```typescript * const { query, mutation, resolver } = lens(); * * // .optimistic() is NOT available * const updateUser = mutation() * .args(z.object({ id: z.string() })) * .returns(User) * .resolve(({ args }) => ...); // Only .resolve() available * ``` * * @example With plugins * ```typescript * const { mutation, plugins } = lens({ * plugins: [optimisticPlugin()], * }); * * // .optimistic() is now available * const updateUser = mutation() * .args(z.object({ id: z.string() })) * .returns(User) * .optimistic('merge') // ✅ Plugin method available * .resolve(({ args }) => ...); * * // Pass plugins to server * createApp({ router, plugins }); * ``` */ /** * Lens builder that allows separate configuration of context type and plugins. * This avoids TypeScript overload resolution issues when providing explicit type params. */ export interface LensBuilder extends Lens { /** * Add plugins to the lens. * Returns LensWithPlugins which includes plugin-provided methods on builders. */ withPlugins[]>( plugins: TPlugins, ): LensWithPlugins>; } /** * Create a typed lens without plugins. * Use .withPlugins() to add plugins after the fact. * * @example * ```typescript * // Basic usage * const { query, mutation } = lens(); * * // With plugins - use .withPlugins() * const { mutation, plugins } = lens().withPlugins([optimisticPlugin()]); * mutation().args(...).returns(...).optimistic('merge'); * ``` */ export function lens(): LensBuilder; /** * Create a typed lens with plugins. * This overload is for when you want to provide plugins inline. * * NOTE: Due to TypeScript overload resolution, do NOT provide an explicit type * parameter when using this form. Let TypeScript infer TContext from usage, * or use `lens().withPlugins([...])` instead. */ export function lens< TContext, const TPlugins extends readonly RuntimePlugin[], >(config: { plugins: TPlugins }): LensWithPlugins>; /** * Implementation */ export function lens(config?: { plugins?: readonly RuntimePlugin[]; }): LensBuilder | LensWithPlugins { // Create typed resolver factory using curried form const typedResolver = createResolver(); // Create typed model factory - delegates to createModel with TContext baked in const typedModel = createModel() as LensModel; // Create mutation factory that returns plugin-aware builders const createPluginMutation = (name?: string) => { const base = createMutation(name as string); // The runtime builder is the same, but types differ based on TPlugins return base; }; // Helper to create withPlugins method const createWithPlugins = ( plugins: readonly RuntimePlugin[], ): LensWithPlugins => ({ model: typedModel, resolver: typedResolver as LensResolver, query: ((name?: string) => createQuery(name as string)) as LensQuery, mutation: createPluginMutation as unknown as LensMutation, subscription: ((name?: string) => createSubscription(name as string)) as LensSubscription, plugins: plugins as unknown as RuntimePlugin[], }); // If no config or no plugins, return LensBuilder with withPlugins method if (!config?.plugins || config.plugins.length === 0) { return { model: typedModel, resolver: typedResolver as LensResolver, query: ((name?: string) => createQuery(name as string)) as LensQuery, mutation: createPluginMutation as LensMutation, subscription: ((name?: string) => createSubscription(name as string)) as LensSubscription, withPlugins: createWithPlugins as LensBuilder["withPlugins"], } as LensBuilder; } // Return LensWithPlugins with plugin-extended types // Note: The actual plugin types are handled by overload signatures return createWithPlugins(config.plugins); } // ============================================================================= // Re-exports for convenience // ============================================================================= // Re-export optimistic plugin types export type { OptimisticPluginExtension, OptimisticPluginMarker, } from "./plugin/optimistic-extension.js"; export { isOptimisticPlugin, OPTIMISTIC_PLUGIN_SYMBOL } from "./plugin/optimistic-extension.js"; // Re-export plugin types export type { ExtractPluginExtensions, ExtractPluginMethods, NoPlugins, PluginExtension, RuntimePlugin, } from "./plugin/types.js";