/** * @sylphx/lens-core - Query Builder * * Fluent interface for defining queries. * * Query Types: * - .resolve() → Query (returns value, no ctx.emit/onCleanup) - can chain .subscribe() * - .resolve().subscribe() → Live Subscription (Publisher pattern) - RECOMMENDED */ import type { Publisher } from "../resolvers/resolver-types.js"; import type { InferReturnType, QueryResolverContext, QueryResolverFn, ReturnSpec, ZodLikeSchema, } from "./types.js"; // ============================================================================= // Query Definition // ============================================================================= /** Query mode - determines how the query is executed */ export type QueryMode = "query" | "subscribe" | "live"; /** Base query definition */ interface QueryDefBase { _type: "query"; /** Query name (optional - derived from export key if not provided) */ _name?: string | undefined; _input?: ZodLikeSchema | undefined; _output?: ReturnSpec | undefined; /** Branded phantom types for inference */ _brand: { input: TInput; output: TOutput }; } /** Query definition - one-shot query (returns value) */ export interface QueryDef extends QueryDefBase { _mode?: "query"; /** Method syntax for bivariance - allows flexible context types */ _resolve?( ctx: import("./types.js").QueryResolverContext, ): TOutput | Promise | AsyncGenerator | void | Promise; } /** Live subscription definition - uses Publisher pattern */ export interface LiveQueryDef extends QueryDefBase { _mode: "live"; /** One-shot resolver for initial value */ _resolve?( ctx: import("./types.js").QueryResolverContext, ): TOutput | Promise; /** Subscriber for live updates (returns Publisher) - method syntax for bivariance */ _subscriber?(ctx: QueryResolverContext): Publisher; } /** Any query definition */ export type AnyQueryDef = | QueryDef | LiveQueryDef; // ============================================================================= // Publisher Subscriber Types // ============================================================================= /** Publisher-based subscription resolver - returns Publisher */ export type PublisherResolverFn = ( ctx: QueryResolverContext, ) => Publisher; // ============================================================================= // Chainable Query Definition // ============================================================================= /** * Chainable query definition - returned by .resolve(), can chain .subscribe() */ export interface QueryDefChainable extends QueryDef { /** * Add live subscription to query (Publisher pattern). * Creates a LiveQueryDef that fetches initial value then streams updates. * * @example * ```typescript * query() * .args(z.object({ id: z.string() })) * .resolve(({ args, ctx }) => ctx.db.user.find(args.id)) * .subscribe(({ args, ctx }) => ({ emit, onCleanup }) => { * const unsub = pubsub.on(`user:${args.id}`, emit); * onCleanup(unsub); * }); * ``` */ subscribe( fn: PublisherResolverFn, ): LiveQueryDef; } // ============================================================================= // Query Builder Interface // ============================================================================= /** Query builder - fluent interface */ export interface QueryBuilder { /** Define args validation schema (optional for queries) */ args(schema: ZodLikeSchema): QueryBuilder; /** Define return type (optional - for entity outputs) */ returns(spec: R): QueryBuilder, TContext>; /** * Define query resolver (returns value). * ctx has NO emit/onCleanup - queries are one-shot. * Can chain .subscribe() for live updates (Publisher pattern). * * @example * ```typescript * // One-shot query * query() * .args(z.object({ id: z.string() })) * .resolve(({ args, ctx }) => db.user.find(args.id)); * * // Live query (resolve + subscribe) * query() * .args(z.object({ id: z.string() })) * .resolve(({ args, ctx }) => db.user.find(args.id)) * .subscribe(({ args, ctx }) => ({ emit, onCleanup }) => { * const unsub = pubsub.on(`user:${args.id}`, emit); * onCleanup(unsub); * }); * ``` */ resolve( fn: QueryResolverFn, ): QueryDefChainable; } // ============================================================================= // Query Builder Implementation // ============================================================================= export class QueryBuilderImpl implements QueryBuilder { private _name?: string | undefined; private _inputSchema?: ZodLikeSchema | undefined; private _outputSpec?: ReturnSpec | undefined; constructor(name?: string) { this._name = name; } args(schema: ZodLikeSchema): QueryBuilder { const builder = new QueryBuilderImpl(this._name); builder._inputSchema = schema; builder._outputSpec = this._outputSpec; return builder; } returns(spec: R): QueryBuilder, TContext> { const builder = new QueryBuilderImpl, TContext>(this._name); builder._inputSchema = this._inputSchema as ZodLikeSchema | undefined; builder._outputSpec = spec; return builder; } resolve(fn: QueryResolverFn): QueryDefChainable { const resolver = fn; const name = this._name; const inputSchema = this._inputSchema; const outputSpec = this._outputSpec; // Return QueryDef with chainable .subscribe() const queryDef: QueryDefChainable = { _type: "query", _mode: "query", _name: name, _input: inputSchema, _output: outputSpec, _brand: {} as { input: TInput; output: T }, _resolve: resolver, // Chainable subscribe - creates LiveQueryDef with Publisher pattern subscribe( subscribeFn: PublisherResolverFn, ): LiveQueryDef { return { _type: "query", _mode: "live", _name: name, _input: inputSchema, _output: outputSpec, _brand: {} as { input: TInput; output: T }, _resolve: resolver, _subscriber: subscribeFn, }; }, }; return queryDef; } } // ============================================================================= // Factory Function // ============================================================================= /** * Create a query builder * * @example * ```typescript * // Basic usage * export const getUser = query() * .args(z.object({ id: z.string() })) * .returns(User) * .resolve(({ args }) => db.user.findUnique({ where: { id: args.id } })); * * // With typed context * export const getUser = query() * .args(z.object({ id: z.string() })) * .resolve(({ args, ctx }) => ctx.db.user.find(args.id)); * ``` */ export function query(): QueryBuilder; export function query(name: string): QueryBuilder; export function query(name?: string): QueryBuilder { return new QueryBuilderImpl(name); } // ============================================================================= // Type Guards // ============================================================================= /** Check if value is a query definition (any mode) */ export function isQueryDef(value: unknown): value is AnyQueryDef { return typeof value === "object" && value !== null && (value as QueryDef)._type === "query"; } /** Check if value is a live query definition (Publisher pattern) */ export function isLiveQueryDef(value: unknown): value is LiveQueryDef { return isQueryDef(value) && (value as LiveQueryDef)._mode === "live"; }