import type { FunctionSpec, RuntimeAndFunctionType } from "@confect/core"; import type * as FunctionProvenance from "@confect/core/FunctionProvenance"; import * as MiddlewareSpec from "@confect/core/MiddlewareSpec"; import { compileArgsSchema, compileReturnsSchema, } from "@confect/core/SchemaToValidator"; import { type DefaultFunctionArgs, type FunctionVisibility, type GenericActionCtx, type GenericDataModel, type RegisteredAction, type RegisteredMutation, type RegisteredQuery, } from "convex/server"; import type { Value } from "convex/values"; import { ConvexError } from "convex/values"; import { pipe } from "effect/Function"; import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; import * as Result from "effect/Result"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import type * as EffectScheduler from "effect/Scheduler"; import * as ActionCtx from "./ActionCtx"; import * as ActionRunner from "./ActionRunner"; import * as Auth from "./Auth"; import * as ConvexLogger from "./ConvexLogger"; import type * as DatabaseSchema from "./DatabaseSchema"; import type * as DataModel from "./DataModel"; import * as ExecutionMetadata from "./ExecutionMetadata"; import * as RequestMetadata from "./RequestMetadata"; import * as MutationRunner from "./MutationRunner"; import * as QueryRunner from "./QueryRunner"; import type * as ResolvedMiddleware from "./ResolvedMiddleware"; import * as Scheduler from "./Scheduler"; import * as Storage from "./Storage"; import * as StorageActionWriter from "./StorageActionWriter"; import type * as StorageReader from "./StorageReader"; import * as StorageWriter from "./StorageWriter"; import * as VectorSearch from "./VectorSearch"; export type Any = | RegisteredQuery | RegisteredMutation | RegisteredAction; type ConfectRegisteredFunction< FunctionSpec_ extends FunctionSpec.AnyWithProps, > = FunctionSpec.EncodedArgs extends infer Args_ extends DefaultFunctionArgs ? RuntimeAndFunctionType.GetFunctionType< FunctionSpec_["runtimeAndFunctionType"] > extends "query" ? RegisteredQuery< FunctionSpec.GetFunctionVisibility, Args_, Promise> > : RuntimeAndFunctionType.GetFunctionType< FunctionSpec_["runtimeAndFunctionType"] > extends "mutation" ? RegisteredMutation< FunctionSpec.GetFunctionVisibility, Args_, Promise> > : RuntimeAndFunctionType.GetFunctionType< FunctionSpec_["runtimeAndFunctionType"] > extends "action" ? RegisteredAction< FunctionSpec.GetFunctionVisibility, Args_, Promise> > : never : never; export type ConvexRegisteredFunction< FunctionSpec_ extends FunctionSpec.AnyWithProps, > = FunctionSpec_ extends FunctionSpec.AnyWithProps ? FunctionSpec.GetFunctionProvenance extends FunctionProvenance.Convex< infer Args_ extends DefaultFunctionArgs, infer Returns_ > ? RuntimeAndFunctionType.GetFunctionType< FunctionSpec_["runtimeAndFunctionType"] > extends "query" ? RegisteredQuery< FunctionSpec.GetFunctionVisibility, Args_, Returns_ > : RuntimeAndFunctionType.GetFunctionType< FunctionSpec_["runtimeAndFunctionType"] > extends "mutation" ? RegisteredMutation< FunctionSpec.GetFunctionVisibility, Args_, Returns_ > : RuntimeAndFunctionType.GetFunctionType< FunctionSpec_["runtimeAndFunctionType"] > extends "action" ? RegisteredAction< FunctionSpec.GetFunctionVisibility, Args_, Returns_ > : never : never : never; export type RegisteredFunction< FunctionSpec_ extends FunctionSpec.AnyWithProps, > = FunctionSpec_ extends FunctionSpec.WithFunctionProvenance< FunctionSpec_, FunctionProvenance.AnyConvex > ? ConvexRegisteredFunction : FunctionSpec_ extends FunctionSpec.WithFunctionProvenance< FunctionSpec_, FunctionProvenance.AnyConfect > ? ConfectRegisteredFunction : never; /** * Wrap a function's handler effect in its resolved middleware chain, per * invocation, after args decode. Iterated innermost-first so that the * first-attached (group-level, in attachment order) middleware ends up * outermost and runs first. Erased types: the public safety story lives at the * `MiddlewareSpec.MiddlewareImpl`/`MiddlewareImpl.make` signatures, and the * composed effect's error channel is re-accounted by * {@link combineErrorSchemas}. */ export const applyMiddleware = ( effect: Effect.Effect, resolvedMiddlewares: ReadonlyArray, invocation: MiddlewareSpec.MiddlewareOptions["invocation"], ): Effect.Effect => { return resolvedMiddlewares.reduceRight>( // oxlint-disable-next-line effecttsgo/any-unknown-in-error-context -- The erased middleware error remains intact so runHandlerPromise can validate it against the combined error schema. (wrapped, middleware) => { const context = "options" in middleware.middlewareSpec ? { options: middleware.options, invocation } : { invocation }; // oxlint-disable-next-line effecttsgo/any-unknown-in-error-context -- Resolved middleware is type-erased after its public implementation boundary; its error and service channels are restored by the surrounding function contract. return middleware.middlewareImpl(wrapped, context); }, effect, ); }; /** * The error-schema allowlist for a function with middleware: the function's own * declared `error` schema unioned with every covering middleware's error * schema. `undefined` (⇒ every failure dies) only when neither declares * one—mirroring the ref-side union clients decode against. */ export const combineErrorSchemas = ( error: Schema.Codec | undefined, resolvedMiddlewares: ReadonlyArray, ): Schema.Codec | undefined => { const schemas = [ ...(error !== undefined ? [error] : []), ...MiddlewareSpec.errorSchemas( resolvedMiddlewares.map(({ middlewareSpec }) => middlewareSpec), ), ]; return schemas.length === 0 ? undefined : schemas.length === 1 ? schemas[0] : Schema.Union(schemas); }; /** * Run the `Effect` as a `Promise`. The error schema acts as an allowlist of * failures that may be surfaced to the client as a `ConvexError`: * * - With a schema: typed errors are schema-encoded and wrapped in a * `ConvexError`, then thrown so Convex surfaces the data to the client. * `Effect.either` escapes the failure channel before `runPromise` so the * thrown `ConvexError` retains its `Symbol.for("ConvexError")` identity * instead of being wrapped in Effect's `FiberFailure`. * - Without a schema: every failure is converted to a defect via `Effect.orDie`, * so nothing—not even a `ConvexError` the handler placed in its error * channel—reaches the client as a `ConvexError`. The fiber dies and * `runPromise` rejects with a generic failure. * * Either way, a `ConvexError` _defect_—thrown imperatively rather than placed * in the error channel, e.g. the pagination protocol's `InvalidCursor` signal * from `QueryStream.paginate`—is rethrown bare so it retains its identity and * Convex serializes its `data` to the client, matching how a thrown * `ConvexError` behaves in a plain Convex handler. * * A `scheduler` in `runOptions` must be passed here as a run option rather than * provided via `Effect.provideService` inside `effect`: the run option lands in * the fiber's root context, while a service provided within `effect` pops * before the `orDie`/`catch`/`result` wrappers this function adds. The fiber's * op counter survives context pops, so a cooperative yield can fire inside * those wrappers—only a root-context scheduler covers them. */ export const runHandlerPromise = ( errorSchema: Schema.Codec | undefined, runOptions?: { readonly scheduler?: EffectScheduler.Scheduler | undefined; }, ) => (effect: Effect.Effect): Promise => { // A `ConvexError` defect escapes into the (escaped) failure channel so // the `throw` below rethrows it with its identity intact. const rethrowConvexErrorDefects = Effect.catchDefect( (defect: unknown): Effect.Effect> => defect instanceof ConvexError ? Effect.fail(defect) : Effect.die(defect), ); const withConvexError = errorSchema === undefined ? Effect.orDie(effect) : effect.pipe( Effect.catch((typedError) => pipe( Schema.encodeEffect(errorSchema)(typedError), Effect.catchTag("SchemaError", Effect.die), Effect.andThen((encodedError) => Effect.fail(new ConvexError(encodedError)), ), ), ), ); return Effect.runPromise( Effect.result(rethrowConvexErrorDefects(withConvexError)).pipe( Effect.provide(ConvexLogger.layer), Effect.provideService(Console.Console, globalThis.console), ), runOptions, ).then( Result.match({ onFailure: (error) => { throw error; }, onSuccess: (value) => value, }), ); }; export const actionFunctionBase = < Schema extends DatabaseSchema.AnyWithProps, Args, ConvexArgs extends DefaultFunctionArgs, Returns, ConvexReturns, E, R, >({ name, functionVisibility, args, returns, error, handler, resolvedMiddlewares = [], createLayer, }: { name: string; functionVisibility: FunctionVisibility; args: Schema.Codec; returns: Schema.Codec; error: Schema.Codec | undefined; handler: (a: Args) => Effect.Effect; resolvedMiddlewares?: ReadonlyArray; createLayer: ( ctx: GenericActionCtx>>, ) => Layer.Layer; }) => ({ args: compileArgsSchema(args), returns: compileReturnsSchema(returns), handler: ( ctx: GenericActionCtx>>, actualArgs: ConvexArgs, ): Promise => Effect.gen(function* () { const decodedArgs = yield* pipe( actualArgs, Schema.decodeUnknownEffect(args), Effect.catchTag("SchemaError", Effect.die), ); // oxlint-disable-next-line effecttsgo/any-unknown-in-error-context -- Middleware errors are intentionally erased here and validated by runHandlerPromise against the combined error schema below. const decodedReturns = yield* applyMiddleware( handler(decodedArgs), resolvedMiddlewares, { name, functionType: "action", functionVisibility, args: decodedArgs, }, ).pipe(Effect.provide(createLayer(ctx))); return yield* pipe( decodedReturns, Schema.encodeEffect(returns), Effect.catchTag("SchemaError", Effect.die), ); }).pipe(runHandlerPromise(combineErrorSchemas(error, resolvedMiddlewares))), }); export type ActionServices< DatabaseSchema_ extends DatabaseSchema.AnyWithProps, > = | Scheduler.Scheduler | Auth.Auth | ExecutionMetadata.ExecutionMetadata | RequestMetadata.RequestMetadata | Storage.Storage | StorageReader.StorageReader | StorageWriter.StorageWriter | StorageActionWriter.StorageActionWriter | QueryRunner.QueryRunner | MutationRunner.MutationRunner | ActionRunner.ActionRunner | VectorSearch.VectorSearch> | ActionCtx.ActionCtx< DataModel.ToConvex> >; /** * The ctx-backed action services that don't depend on a Confect database * schema. {@link actionLayer} adds the schema-typed `VectorSearch` on top; the * HTTP API handler uses this base directly. */ export const baseActionLayer = ( ctx: GenericActionCtx, ) => Layer.mergeAll( Scheduler.layer(ctx.scheduler), Auth.layer(ctx.auth), ExecutionMetadata.layer(ctx.meta), RequestMetadata.layer(ctx.meta), Storage.layer(ctx.storage), StorageWriter.StorageWriter.layer(ctx.storage), StorageActionWriter.StorageActionWriter.layer(ctx.storage), QueryRunner.layer(ctx.runQuery.bind(ctx)), MutationRunner.layer(ctx.runMutation.bind(ctx)), ActionRunner.layer(ctx.runAction.bind(ctx)), Layer.succeed(ActionCtx.ActionCtx(), ctx), ); export const actionLayer = < DatabaseSchema_ extends DatabaseSchema.AnyWithProps, >( databaseSchema: DatabaseSchema_, ctx: GenericActionCtx< DataModel.ToConvex> >, ) => Layer.mergeAll( baseActionLayer(ctx), VectorSearch.layer(ctx.vectorSearch.bind(ctx)), );