/** * @sylphx/lens-core - Mutation Builder * * Fluent interface for defining mutations. */ import type { Pipeline, StepBuilder } from "../optimistic/reify.js"; import type { ExtractPluginMethods, PluginExtension } from "../plugin/types.js"; import type { InferReturnType, OptimisticCallback, OptimisticDSL, ResolverFn, ReturnSpec, ZodLikeSchema, } from "./types.js"; // ============================================================================= // Mutation Definition // ============================================================================= /** Mutation definition */ export interface MutationDef { _type: "mutation"; /** Mutation name (optional - derived from export key if not provided) */ _name?: string | undefined; _input: ZodLikeSchema; _output?: ReturnSpec | undefined; /** Branded phantom types for inference */ _brand: { input: TInput; output: TOutput }; /** Optimistic update DSL (declarative, serializable for client) */ _optimistic?: OptimisticDSL | undefined; /** Method syntax for bivariance - allows flexible context types */ _resolve(ctx: { args: TInput; ctx: TContext; }): TOutput | Promise | AsyncGenerator; } // ============================================================================= // Mutation Builder Interfaces // ============================================================================= /** Mutation builder - fluent interface */ export interface MutationBuilder< _TInput = unknown, TOutput = unknown, TContext = unknown, TPlugins extends readonly PluginExtension[] = readonly PluginExtension[], > { /** Define args validation schema (required for mutations) */ args(schema: ZodLikeSchema): MutationBuilderWithArgs; } /** Mutation builder after args is defined */ export interface MutationBuilderWithArgs< TInput, _TOutput = unknown, TContext = unknown, TPlugins extends readonly PluginExtension[] = readonly PluginExtension[], > { /** Define return type (optional - for entity outputs) */ returns( spec: R, ): MutationBuilderWithReturns2, TContext> & ExtractPluginMethods< TPlugins, "MutationBuilderWithReturns", TInput, InferReturnType, TContext >; /** Define resolver function directly (without .returns()) */ resolve(fn: ResolverFn): MutationDef; } /** * Mutation builder after returns is defined (strict version). * Only has .resolve() - no .optimistic(). */ export interface MutationBuilderWithReturns2 { /** Define resolver function */ resolve(fn: ResolverFn): MutationDef; } /** * Mutation builder after returns is defined (with optimistic). * Has .optimistic() and .resolve(). */ export interface MutationBuilderWithReturns extends MutationBuilderWithReturns2 { /** * Define optimistic update (optional) * * @example * ```typescript * // Sugar syntax * .optimistic('merge') * .optimistic('create') * .optimistic('delete') * * // Callback with typed args * .optimistic(({ args }) => [ * e.update("User", { id: args.id, name: args.name }) * ]) * ``` */ optimistic(spec: OptimisticDSL): MutationBuilderWithOptimistic; optimistic( callback: OptimisticCallback, ): MutationBuilderWithOptimistic; } /** Mutation builder after optimistic is defined */ export interface MutationBuilderWithOptimistic { /** Define resolver function */ resolve(fn: ResolverFn): MutationDef; } // ============================================================================= // Mutation Builder Implementation // ============================================================================= export class MutationBuilderImpl implements MutationBuilder, MutationBuilderWithArgs, MutationBuilderWithReturns, MutationBuilderWithOptimistic { private _name?: string | undefined; private _inputSchema?: ZodLikeSchema | undefined; private _outputSpec?: ReturnSpec | undefined; private _optimisticSpec?: OptimisticDSL | undefined; constructor(name?: string) { this._name = name; } args(schema: ZodLikeSchema): MutationBuilderWithArgs { const builder = new MutationBuilderImpl(this._name); builder._inputSchema = schema; return builder; } returns( spec: R, ): MutationBuilderWithReturns, TContext> { const builder = new MutationBuilderImpl, TContext>(this._name); builder._inputSchema = this._inputSchema as ZodLikeSchema | undefined; builder._outputSpec = spec; return builder; } optimistic( specOrCallback: OptimisticDSL | OptimisticCallback, ): MutationBuilderWithOptimistic { const builder = new MutationBuilderImpl(this._name); builder._inputSchema = this._inputSchema; builder._outputSpec = this._outputSpec; if (typeof specOrCallback === "function") { const argsProxy = new Proxy( {}, { get(_, prop: string) { return { $input: prop }; }, }, ) as TInput; const stepBuilders: StepBuilder[] = specOrCallback({ args: argsProxy }); const steps = stepBuilders.map((s: StepBuilder) => s.build()); builder._optimisticSpec = { $pipe: steps } as Pipeline; } else { builder._optimisticSpec = specOrCallback; } return builder; } resolve(fn: ResolverFn): MutationDef { if (!this._inputSchema) { throw new Error("Mutation requires args schema. Use .args(schema) first."); } return { _type: "mutation", _name: this._name, _input: this._inputSchema, _output: this._outputSpec, _brand: {} as { input: TInput; output: TOut }, _optimistic: this._optimisticSpec, _resolve: fn, }; } } // ============================================================================= // Factory Function // ============================================================================= /** * Create a mutation builder * * @example * ```typescript * // Basic usage * export const createPost = mutation() * .args(z.object({ title: z.string(), content: z.string() })) * .returns(Post) * .resolve(({ args }) => db.post.create({ data: args })); * * // With typed context * export const createPost = mutation() * .args(z.object({ title: z.string() })) * .resolve(({ args, ctx }) => ctx.db.post.create({ data: args })); * ``` */ export function mutation(): MutationBuilder; export function mutation( name: string, ): MutationBuilder; export function mutation( name?: string, ): MutationBuilder { return new MutationBuilderImpl(name); } // ============================================================================= // Type Guard // ============================================================================= /** Check if value is a mutation definition */ export function isMutationDef(value: unknown): value is MutationDef { return typeof value === "object" && value !== null && (value as MutationDef)._type === "mutation"; } /** Check if value is any operation definition */ export function isOperationDef( value: unknown, ): value is import("./query.js").QueryDef | MutationDef { if (typeof value !== "object" || value === null) return false; const type = (value as { _type?: string })._type; return type === "mutation" || type === "query"; }