UNPKG

kitcn

Version:

kitcn - React Query integration and CLI tools for Convex

1,428 lines (1,423 loc) 75.7 kB
import { i as pick } from "./upstream-BR6sBLg3.js"; import { o as vRequired, t as addFieldsToValidator } from "./validators-C7LelqTN.js"; import { n as customCtx, t as NoOp } from "./customFunctions-DxEEO4Dq.js"; import { s as getTransformer } from "./transformer-C6pGVHqx.js"; import { ConvexError, v } from "convex/values"; import { HttpRouter, actionGeneric, httpActionGeneric, internalActionGeneric, internalMutationGeneric, internalQueryGeneric, mutationGeneric, queryGeneric } from "convex/server"; import { z } from "zod"; import * as z$1 from "zod/v4"; import * as zCore from "zod/v4/core"; //#region src/internal/upstream/server/zod4.ts /** * zCustomQuery is like customQuery, but allows validation via zod. * You can define custom behavior on top of `query` or `internalQuery` * by passing a function that modifies the ctx and args. Or NoOp to do nothing. * * Example usage: * ```ts * const myQueryBuilder = zCustomQuery(query, { * args: { sessionId: v.id("sessions") }, * input: async (ctx, args) => { * const user = await getUserOrNull(ctx); * const session = await db.get(sessionId); * const db = wrapDatabaseReader({ user }, ctx.db, rlsRules); * return { ctx: { db, user, session }, args: {} }; * }, * }); * * // Using the custom builder * export const getSomeData = myQueryBuilder({ * args: { someArg: z.string() }, * handler: async (ctx, args) => { * const { db, user, session, scheduler } = ctx; * const { someArg } = args; * // ... * } * }); * ``` * * Simple usage only modifying ctx: * ```ts * const myInternalQuery = zCustomQuery( * internalQuery, * customCtx(async (ctx) => { * return { * // Throws an exception if the user isn't logged in * user: await getUserByTokenIdentifier(ctx), * }; * }) * ); * * // Using it * export const getUser = myInternalQuery({ * args: { email: z.string().email() }, * handler: async (ctx, args) => { * console.log(args.email); * return ctx.user; * }, * }); * * @param query The query to be modified. Usually `query` or `internalQuery` * from `_generated/server`. * @param customization The customization to be applied to the query, changing ctx and args. * @returns A new query builder using zod validation to define queries. */ function zCustomQuery(query, customization) { return customFnBuilder(query, customization); } /** * zCustomMutation is like customMutation, but allows validation via zod. * You can define custom behavior on top of `mutation` or `internalMutation` * by passing a function that modifies the ctx and args. Or NoOp to do nothing. * * Example usage: * ```ts * const myMutationBuilder = zCustomMutation(mutation, { * args: { sessionId: v.id("sessions") }, * input: async (ctx, args) => { * const user = await getUserOrNull(ctx); * const session = await db.get(sessionId); * const db = wrapDatabaseReader({ user }, ctx.db, rlsRules); * return { ctx: { db, user, session }, args: {} }; * }, * }); * * // Using the custom builder * export const getSomeData = myMutationBuilder({ * args: { someArg: z.string() }, * handler: async (ctx, args) => { * const { db, user, session, scheduler } = ctx; * const { someArg } = args; * // ... * } * }); * ``` * * Simple usage only modifying ctx: * ```ts * const myInternalMutation = zCustomMutation( * internalMutation, * customCtx(async (ctx) => { * return { * // Throws an exception if the user isn't logged in * user: await getUserByTokenIdentifier(ctx), * }; * }) * ); * * // Using it * export const getUser = myInternalMutation({ * args: { email: z.string().email() }, * handler: async (ctx, args) => { * console.log(args.email); * return ctx.user; * }, * }); * * @param mutation The mutation to be modified. Usually `mutation` or `internalMutation` * from `_generated/server`. * @param customization The customization to be applied to the mutation, changing ctx and args. * @returns A new mutation builder using zod validation to define queries. */ function zCustomMutation(mutation, customization) { return customFnBuilder(mutation, customization); } /** * zCustomAction is like customAction, but allows validation via zod. * You can define custom behavior on top of `action` or `internalAction` * by passing a function that modifies the ctx and args. Or NoOp to do nothing. * * Example usage: * ```ts * const myActionBuilder = zCustomAction(action, { * args: { sessionId: v.id("sessions") }, * input: async (ctx, args) => { * const user = await getUserOrNull(ctx); * const session = await db.get(sessionId); * const db = wrapDatabaseReader({ user }, ctx.db, rlsRules); * return { ctx: { db, user, session }, args: {} }; * }, * }); * * // Using the custom builder * export const getSomeData = myActionBuilder({ * args: { someArg: z.string() }, * handler: async (ctx, args) => { * const { db, user, session, scheduler } = ctx; * const { someArg } = args; * // ... * } * }); * ``` * * Simple usage only modifying ctx: * ```ts * const myInternalAction = zCustomAction( * internalAction, * customCtx(async (ctx) => { * return { * // Throws an exception if the user isn't logged in * user: await getUserByTokenIdentifier(ctx), * }; * }) * ); * * // Using it * export const getUser = myInternalAction({ * args: { email: z.string().email() }, * handler: async (ctx, args) => { * console.log(args.email); * return ctx.user; * }, * }); * * @param action The action to be modified. Usually `action` or `internalAction` * from `_generated/server`. * @param customization The customization to be applied to the action, changing ctx and args. * @returns A new action builder using zod validation to define queries. */ function zCustomAction(action, customization) { return customFnBuilder(action, customization); } /** * Creates a validator for a Convex `Id`. * * - When **used within Zod**, it will only check that the ID is a string. * - When **converted to a Convex validator** (e.g. through {@link zodToConvex}), * it will check that it's for the right table. * * @param tableName - The table that the `Id` references. i.e. `Id<tableName>` * @returns A Zod schema representing a Convex `Id` */ const zid = (tableName) => { const result = z$1.custom((val) => typeof val === "string"); _zidRegistry.add(result, { tableName }); return result; }; /** * Turns a Zod or Zod Mini validator into a Convex validator. * * The Convex validator will be as close to possible to the Zod validator, * but might be broader than the Zod validator: * * ```ts * zodToConvex(z.string().email()) // → v.string() * ``` * * This function is useful when running the Zod validator _after_ running the Convex validator * (i.e. the Convex validator validates the input of the Zod validator). Hence, the Convex types * will match the _input type_ of Zod transformations: * ```ts * zodToConvex(z.object({ * name: z.string().default("Nicolas"), * })) // → v.object({ name: v.optional(v.string()) }) * * zodToConvex(z.object({ * name: z.string().transform(s => s.length) * })) // → v.object({ name: v.string() }) * ```` * * This function is useful for: * * **Validating function arguments with Zod**: through {@link zCustomQuery}, * {@link zCustomMutation} and {@link zCustomAction}, you can define the argument validation logic * using Zod validators instead of Convex validators. `zodToConvex` will generate a Convex validator * from your Zod validator. This will allow you to: * - validate at run time that Convex IDs are from the right table (using {@link zid}) * - allow some features of Convex to understand the expected shape of the arguments * (e.g. argument validation/prefilling in the function runner on the Convex dashboard) * - still run the full Zod validation when the function runs * (which is useful for more advanced Zod validators like `z.string().email()`) * * **Validating data after reading it from the database**: if you want to write your DB schema * with Zod, you can run Zod whenever you read from the database to check that the data * still matches the schema. Note that this approach won’t ensure that the data stored in the DB * matches the Zod schema; see * https://stack.convex.dev/typescript-zod-function-validation#can-i-use-zod-to-define-my-database-types-too * for more details. * * Note that some values might be valid in Zod but not in Convex, * in the same way that valid JavaScript values might not be valid * Convex values for the corresponding Convex type. * (see the limits of Convex data types on https://docs.convex.dev/database/types). * * ``` * ┌─────────────────────────────────────┬─────────────────────────────────────┐ * │ **zodToConvex** │ zodOutputToConvex │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ For when the Zod validator runs │ For when the Zod validator runs │ * │ _after_ the Convex validator │ _before_ the Convex validator │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ Convex types use the _input types_ │ Convex types use the _return types_ │ * │ of Zod transformations │ of Zod transformations │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ The Convex validator can be less │ The Convex validator can be less │ * │ strict (i.e. some inputs might be │ strict (i.e. the type in Convex can │ * │ accepted by Convex then rejected │ be less precise than the type in │ * │ by Zod) │ the Zod output) │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ When using Zod schemas │ When using Zod schemas │ * │ for function definitions: │ for function definitions: │ * │ used for _arguments_ │ used for _return values_ │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ When validating contents of the │ When validating contents of the │ * │ database with a Zod schema: │ database with a Zod schema: │ * │ used to validate data │ used to validate data │ * │ _after reading_ │ _before writing_ │ * └─────────────────────────────────────┴─────────────────────────────────────┘ * ``` * * @param zod Zod validator can be a Zod object, or a Zod type like `z.string()` * @returns Convex Validator (e.g. `v.string()` from "convex/values") * @throws If there is no equivalent Convex validator for the value (e.g. `z.date()`) */ function zodToConvex(validator) { const visited = /* @__PURE__ */ new WeakSet(); function zodToConvexInner(validator) { if (visited.has(validator)) return v.any(); visited.add(validator); const result = validator instanceof zCore.$ZodDefault ? v.optional(zodToConvexInner(validator._zod.def.innerType)) : validator instanceof zCore.$ZodPipe ? zodToConvexInner(validator._zod.def.in) : zodToConvexCommon(validator, zodToConvexInner); visited.delete(validator); return result; } return zodToConvexInner(validator); } /** * Converts a Zod or Zod Mini validator to a Convex validator that checks the value _after_ * it has been validated (and possibly transformed) by the Zod validator. * * This is similar to {@link zodToConvex}, but is meant for cases where the Convex * validator runs _after_ the Zod validator. Thus, the Convex type refers to the * _output_ type of the Zod transformations: * ```ts * zodOutputToConvex(z.object({ * name: z.string().default("Nicolas"), * })) // → v.object({ name: v.string() }) * * zodOutputToConvex(z.object({ * name: z.string().transform(s => s.length) * })) // → v.object({ name: v.number() }) * ```` * * This function can be useful for: * - **Validating function return values with Zod**: through {@link zCustomQuery}, * {@link zCustomMutation} and {@link zCustomAction}, you can define the `returns` property * of a function using Zod validators instead of Convex validators. * - **Validating data after reading it from the database**: if you want to write your DB schema * Zod validators, you can run Zod whenever you write to the database to ensure your data matches * the expected format. Note that this approach won’t ensure that the data stored in the DB * isn’t modified manually in a way that doesn’t match your Zod schema; see * https://stack.convex.dev/typescript-zod-function-validation#can-i-use-zod-to-define-my-database-types-too * for more details. * * ``` * ┌─────────────────────────────────────┬─────────────────────────────────────┐ * │ zodToConvex │ **zodOutputToConvex** │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ For when the Zod validator runs │ For when the Zod validator runs │ * │ _after_ the Convex validator │ _before_ the Convex validator │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ Convex types use the _input types_ │ Convex types use the _return types_ │ * │ of Zod transformations │ of Zod transformations │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ The Convex validator can be less │ The Convex validator can be less │ * │ strict (i.e. some inputs might be │ strict (i.e. the type in Convex can │ * │ accepted by Convex then rejected │ be less precise than the type in │ * │ by Zod) │ the Zod output) │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ When using Zod schemas │ When using Zod schemas │ * │ for function definitions: │ for function definitions: │ * │ used for _arguments_ │ used for _return values_ │ * ├─────────────────────────────────────┼─────────────────────────────────────┤ * │ When validating contents of the │ When validating contents of the │ * │ database with a Zod schema: │ database with a Zod schema: │ * │ used to validate data │ used to validate data │ * │ _after reading_ │ _before writing_ │ * └─────────────────────────────────────┴─────────────────────────────────────┘ * ``` * * @param z The zod validator * @returns Convex Validator (e.g. `v.string()` from "convex/values") * @throws If there is no equivalent Convex validator for the value (e.g. `z.date()`) */ function zodOutputToConvex(validator) { const visited = /* @__PURE__ */ new WeakSet(); function zodOutputToConvexInner(validator) { if (visited.has(validator)) return v.any(); visited.add(validator); const result = validator instanceof zCore.$ZodDefault ? zodOutputToConvexInner(validator._zod.def.innerType) : validator instanceof zCore.$ZodPipe ? zodOutputToConvexInner(validator._zod.def.out) : validator instanceof zCore.$ZodTransform ? v.any() : zodToConvexCommon(validator, zodOutputToConvexInner); visited.delete(validator); return result; } return zodOutputToConvexInner(validator); } /** * Like {@link zodToConvex}, but it takes in a bare object, as expected by Convex * function arguments, or the argument to {@link defineTable}. * * ```ts * zodToConvexFields({ * name: z.string().default("Nicolas"), * }) // → { name: v.optional(v.string()) } * ``` * * @param fields Object with string keys and Zod validators as values * @returns Object with the same keys, but with Convex validators as values */ function zodToConvexFields(fields) { return Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, zodToConvex(v)])); } /** * Like {@link zodOutputToConvex}, but it takes in a bare object, as expected by * Convex function arguments, or the argument to {@link defineTable}. * * ```ts * zodOutputToConvexFields({ * name: z.string().default("Nicolas"), * }) // → { name: v.string() } * ``` * * This is different from {@link zodToConvexFields} because it generates the * Convex validator for the output of the Zod validator, not the input; * see the documentation of {@link zodToConvex} and {@link zodOutputToConvex} * for more details. * * @param zod Object with string keys and Zod validators as values * @returns Object with the same keys, but with Convex validators as values */ function zodOutputToConvexFields(fields) { return Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, zodOutputToConvex(v)])); } /** * Turns a Convex validator into a Zod validator. * * This is useful when you want to use types you defined using Convex validators * with external libraries that expect to receive a Zod validator. * * ```ts * convexToZod(v.string()) // → z.string() * ``` * * This function returns Zod validators, not Zod Mini validators. * * @param convexValidator Convex validator can be any validator from "convex/values" e.g. `v.string()` * @returns Zod validator (e.g. `z.string()`) with inferred type matching the Convex validator */ function convexToZod(convexValidator) { const isOptional = convexValidator.isOptional === "optional"; let zodValidator; const { kind } = convexValidator; switch (kind) { case "id": zodValidator = zid(convexValidator.tableName); break; case "string": zodValidator = z$1.string(); break; case "float64": zodValidator = z$1.number(); break; case "int64": zodValidator = z$1.bigint(); break; case "boolean": zodValidator = z$1.boolean(); break; case "null": zodValidator = z$1.null(); break; case "any": zodValidator = z$1.any(); break; case "array": zodValidator = z$1.array(convexToZod(convexValidator.element)); break; case "object": zodValidator = z$1.object(convexToZodFields(convexValidator.fields)); break; case "union": { if (convexValidator.members.length === 0) { zodValidator = z$1.never(); break; } if (convexValidator.members.length === 1) { zodValidator = convexToZod(convexValidator.members[0]); break; } const memberValidators = convexValidator.members.map((member) => convexToZod(member)); zodValidator = z$1.union([...memberValidators]); break; } case "literal": { const literalValidator = convexValidator; zodValidator = z$1.literal(literalValidator.value); break; } case "record": zodValidator = z$1.record(convexToZod(convexValidator.key), convexToZod(convexValidator.value)); break; case "bytes": throw new Error("v.bytes() is not supported"); default: throw new Error(`Unknown convex validator type: ${kind}`); } return isOptional ? z$1.optional(zodValidator) : zodValidator; } /** * Like {@link convexToZod}, but it takes in a bare object, as expected by Convex * function arguments, or the argument to {@link defineTable}. * * ```ts * convexToZodFields({ * name: v.string(), * }) // → { name: z.string() } * ``` * * @param convexValidators Object with string keys and Convex validators as values * @returns Object with the same keys, but with Zod validators as values */ function convexToZodFields(convexValidators) { return Object.fromEntries(Object.entries(convexValidators).map(([k, v]) => [k, convexToZod(v)])); } /** * Zod helper for adding Convex system fields to a record to return. * * ```ts * withSystemFields("users", { * name: z.string(), * }) * // → { * // name: z.string(), * // _id: zid("users"), * // _creationTime: z.number(), * // } * ``` * * @param tableName - The table where records are from, i.e. Doc<tableName> * @param zObject - Validators for the user-defined fields on the document. * @returns Zod shape for use with `z.object(shape)` that includes system fields. */ function withSystemFields(tableName, zObject) { return { ...zObject, _id: zid(tableName), _creationTime: z$1.number() }; } function customFnBuilder(builder, customization) { const customInput = customization.input ?? NoOp.input; const inputArgs = customization.args ?? NoOp.args; return function customBuilder(fn) { const { args, handler = fn, skipConvexValidation = false, returns: maybeObject, ...extra } = fn; const returns = maybeObject && !(maybeObject instanceof zCore.$ZodType) ? z$1.object(maybeObject) : maybeObject; const returnValidator = returns && !skipConvexValidation ? { returns: zodOutputToConvex(returns) } : null; if (args) { let argsValidator = args; if (argsValidator instanceof zCore.$ZodType) if (argsValidator instanceof zCore.$ZodObject) argsValidator = argsValidator._zod.def.shape; else throw new Error("Unsupported zod type as args validator: " + argsValidator.constructor.name); const convexValidator = zodToConvexFields(argsValidator); return builder({ args: skipConvexValidation ? void 0 : addFieldsToValidator(convexValidator, inputArgs), ...returnValidator, handler: async (ctx, allArgs) => { const added = await customInput(ctx, pick(allArgs, Object.keys(inputArgs)), extra); const rawArgs = pick(allArgs, Object.keys(argsValidator)); const parsed = await z$1.object(argsValidator).safeParseAsync(rawArgs); if (!parsed.success) throw new ConvexError({ ZodError: JSON.parse(JSON.stringify(parsed.error.issues, null, 2)) }); const args = parsed.data; const ret = await handler({ ...ctx, ...added.ctx }, { ...args, ...added.args }); const result = returns ? await returns.parseAsync(ret === void 0 ? null : ret) : ret; if (added.onSuccess) await added.onSuccess({ ctx, args, result }); return result; } }); } if (skipConvexValidation && Object.keys(inputArgs).length > 0) throw new Error("If you're using a custom function with arguments for the input customization, you cannot skip convex validation."); return builder({ ...returnValidator, handler: async (ctx, args) => { const added = await customInput(ctx, args, extra); const ret = await handler({ ...ctx, ...added.ctx }, { ...args, ...added.args }); const result = returns ? await returns.parseAsync(ret === void 0 ? null : ret) : ret; if (added.onSuccess) await added.onSuccess({ ctx, args, result }); return result; } }); }; } function zodToConvexCommon(validator, toConvex) { if (validator instanceof zCore.$ZodString) return v.string(); if (validator instanceof zCore.$ZodNumber || validator instanceof zCore.$ZodNaN) return v.number(); if (validator instanceof zCore.$ZodBigInt) return v.int64(); if (validator instanceof zCore.$ZodBoolean) return v.boolean(); if (validator instanceof zCore.$ZodNull) return v.null(); if (validator instanceof zCore.$ZodAny || validator instanceof zCore.$ZodUnknown) return v.any(); if (validator instanceof zCore.$ZodArray) { const inner = toConvex(validator._zod.def.element); if (inner.isOptional === "optional") throw new Error("Arrays of optional values are not supported"); return v.array(inner); } if (validator instanceof zCore.$ZodObject) return v.object(Object.fromEntries(Object.entries(validator._zod.def.shape).map(([k, v]) => [k, toConvex(v)]))); if (validator instanceof zCore.$ZodUnion) return v.union(...validator._zod.def.options.map(toConvex)); if (validator instanceof zCore.$ZodNever) return v.union(); if (validator instanceof zCore.$ZodTuple) { const { items, rest } = validator._zod.def; return v.array(v.union(...[...items, ...rest !== null ? [rest] : []].map(toConvex))); } if (validator instanceof zCore.$ZodLiteral) { const { values } = validator._zod.def; if (values.length === 1) return convexToZodLiteral(values[0]); return v.union(...values.map(convexToZodLiteral)); } if (validator instanceof zCore.$ZodEnum) return v.union(...Object.entries(validator._zod.def.entries).filter(([key, value]) => key === value || isNaN(Number(key))).map(([_key, value]) => v.literal(value))); if (validator instanceof zCore.$ZodOptional) return v.optional(toConvex(validator._zod.def.innerType)); if (validator instanceof zCore.$ZodNonOptional) return vRequired(toConvex(validator._zod.def.innerType)); if (validator instanceof zCore.$ZodNullable) { const inner = toConvex(validator._zod.def.innerType); if (inner.isOptional === "optional") return v.optional(v.union(vRequired(inner), v.null())); return v.union(inner, v.null()); } if (validator instanceof zCore.$ZodRecord) { const { keyType, valueType } = validator._zod.def; const isPartial = keyType._zod.values === void 0; const valueValidator = toConvex(valueType); const keyValidator = toConvex(keyType); const stringLiterals = extractStringLiterals(keyValidator); if (stringLiterals !== null) { const fieldValue = isPartial || valueValidator.isOptional === "optional" ? v.optional(valueValidator) : vRequired(valueValidator); const fields = {}; for (const literal of stringLiterals) fields[literal] = fieldValue; return v.object(fields); } return v.record(isValidRecordKey(keyValidator) ? keyValidator : v.string(), vRequired(valueValidator)); } if (validator instanceof zCore.$ZodReadonly) return toConvex(validator._zod.def.innerType); if (validator instanceof zCore.$ZodLazy) return toConvex(validator._zod.def.getter()); if (validator instanceof zCore.$ZodTemplateLiteral) return v.string(); if (validator instanceof zCore.$ZodCustom) { const idTableName = _zidRegistry.get(validator); if (idTableName !== void 0 && typeof idTableName.tableName === "string") return v.id(idTableName.tableName); return v.any(); } if (validator instanceof zCore.$ZodIntersection) return v.any(); if (validator instanceof zCore.$ZodCatch) return toConvex(validator._zod.def.innerType); if (validator instanceof zCore.$ZodDate || validator instanceof zCore.$ZodSymbol || validator instanceof zCore.$ZodMap || validator instanceof zCore.$ZodSet || validator instanceof zCore.$ZodPromise || validator instanceof zCore.$ZodFile || validator instanceof zCore.$ZodFunction || validator instanceof zCore.$ZodVoid || validator instanceof zCore.$ZodUndefined) throw new Error(`Validator ${validator.constructor.name} is not supported in Convex`); return v.any(); } function convexToZodLiteral(literal) { if (literal === void 0) throw new Error("undefined is not a valid Convex value"); if (literal === null) return v.null(); return v.literal(literal); } function extractStringLiterals(validator) { if (validator.kind === "literal") { const literalValidator = validator; if (typeof literalValidator.value === "string") return [literalValidator.value]; return null; } if (validator.kind === "union") { const unionValidator = validator; const literals = []; for (const member of unionValidator.members) { const memberLiterals = extractStringLiterals(member); if (memberLiterals === null) return null; literals.push(...memberLiterals); } return literals; } return null; } function isValidRecordKey(validator) { if (validator.kind === "string" || validator.kind === "id") return true; if (validator.kind === "union") return validator.members.every(isValidRecordKey); return false; } /** Stores the table names for each `Zid` instance that is created. */ const _zidRegistry = zCore.registry(); //#endregion //#region src/server/error.ts /** * CRPC Error - tRPC-style error handling for Convex * * Extends ConvexError with typed error codes and HTTP status mapping. */ /** JSON-RPC 2.0 error codes (tRPC-style) */ const CRPC_ERROR_CODES_BY_KEY = { PARSE_ERROR: -32700, BAD_REQUEST: -32600, INTERNAL_SERVER_ERROR: -32603, NOT_IMPLEMENTED: -32603, BAD_GATEWAY: -32603, SERVICE_UNAVAILABLE: -32603, GATEWAY_TIMEOUT: -32603, UNAUTHORIZED: -32001, PAYMENT_REQUIRED: -32002, FORBIDDEN: -32003, NOT_FOUND: -32004, METHOD_NOT_SUPPORTED: -32005, TIMEOUT: -32008, CONFLICT: -32009, PRECONDITION_FAILED: -32012, PAYLOAD_TOO_LARGE: -32013, UNSUPPORTED_MEDIA_TYPE: -32015, UNPROCESSABLE_CONTENT: -32022, PRECONDITION_REQUIRED: -32028, TOO_MANY_REQUESTS: -32029, CLIENT_CLOSED_REQUEST: -32099 }; /** Map error codes to HTTP status codes */ const CRPC_ERROR_CODE_TO_HTTP = { PARSE_ERROR: 400, BAD_REQUEST: 400, UNAUTHORIZED: 401, PAYMENT_REQUIRED: 402, FORBIDDEN: 403, NOT_FOUND: 404, METHOD_NOT_SUPPORTED: 405, TIMEOUT: 408, CONFLICT: 409, PRECONDITION_FAILED: 412, PAYLOAD_TOO_LARGE: 413, UNSUPPORTED_MEDIA_TYPE: 415, UNPROCESSABLE_CONTENT: 422, PRECONDITION_REQUIRED: 428, TOO_MANY_REQUESTS: 429, CLIENT_CLOSED_REQUEST: 499, INTERNAL_SERVER_ERROR: 500, NOT_IMPLEMENTED: 501, BAD_GATEWAY: 502, SERVICE_UNAVAILABLE: 503, GATEWAY_TIMEOUT: 504 }; /** Extract Error from unknown cause (from tRPC) */ function getCauseFromUnknown(cause) { if (cause instanceof Error) return cause; if (typeof cause === "undefined" || typeof cause === "function" || cause === null) return; if (typeof cause !== "object") return new Error(String(cause)); } /** * tRPC-style error extending ConvexError * * @example * ```typescript * throw new CRPCError({ * code: 'BAD_REQUEST', * message: 'Invalid input', * cause: originalError, * }); * ``` */ var CRPCError = class extends ConvexError { code; cause; constructor(opts) { const cause = getCauseFromUnknown(opts.cause); const message = opts.message ?? cause?.message ?? opts.code; super({ ...opts.data ?? {}, code: opts.code, message }); this.name = "CRPCError"; this.code = opts.code; this.cause = cause; this.message = message; } }; function isOrmNotFoundErrorLike(cause) { return cause instanceof Error && cause.name === "OrmNotFoundError"; } function isApiErrorLike(cause) { return cause instanceof Error && cause.name === "APIError" && typeof cause.statusCode === "number"; } function mapHttpStatusCodeToCRPCCode(statusCode) { switch (statusCode) { case 400: return "BAD_REQUEST"; case 401: return "UNAUTHORIZED"; case 402: return "PAYMENT_REQUIRED"; case 403: return "FORBIDDEN"; case 404: return "NOT_FOUND"; case 405: return "METHOD_NOT_SUPPORTED"; case 408: return "TIMEOUT"; case 409: return "CONFLICT"; case 412: return "PRECONDITION_FAILED"; case 413: return "PAYLOAD_TOO_LARGE"; case 415: return "UNSUPPORTED_MEDIA_TYPE"; case 422: return "UNPROCESSABLE_CONTENT"; case 428: return "PRECONDITION_REQUIRED"; case 429: return "TOO_MANY_REQUESTS"; case 499: return "CLIENT_CLOSED_REQUEST"; default: return "INTERNAL_SERVER_ERROR"; } } function getApiErrorMessage(cause) { const body = cause.body; if (body && typeof body === "object") { const message = body.message; if (typeof message === "string" && message.length > 0) return message; } if (typeof cause.message === "string" && cause.message.length > 0) return cause.message; return "Request failed"; } /** * Convert known framework/library errors into CRPCError. * * Intended for cRPC internals so callers don't need per-endpoint try/catch. */ function toCRPCError(cause) { if (cause instanceof CRPCError) return cause; if (cause instanceof Error && cause.name === "CRPCError") return cause; if (isOrmNotFoundErrorLike(cause)) { const err = new CRPCError({ code: "NOT_FOUND", message: cause.message, cause }); if (cause.stack) err.stack = cause.stack; return err; } if (isApiErrorLike(cause)) { const status = cause.status; const statusCode = cause.statusCode; const err = new CRPCError({ code: typeof status === "string" && status in CRPC_ERROR_CODES_BY_KEY ? status : typeof statusCode === "number" ? mapHttpStatusCodeToCRPCCode(statusCode) : "INTERNAL_SERVER_ERROR", message: getApiErrorMessage(cause), cause }); if (cause.stack) err.stack = cause.stack; return err; } return null; } /** * Wrap unknown error in CRPCError (from tRPC) * * @example * ```typescript * try { * await someOperation(); * } catch (error) { * throw getCRPCErrorFromUnknown(error); * } * ``` */ function getCRPCErrorFromUnknown(cause) { const handled = toCRPCError(cause); if (handled) return handled; const error = new CRPCError({ code: "INTERNAL_SERVER_ERROR", cause }); if (cause instanceof Error && cause.stack) error.stack = cause.stack; return error; } /** * Get HTTP status code from CRPCError * * @example * ```typescript * const httpStatus = getHTTPStatusCodeFromError(error); // 400 * ``` */ function getHTTPStatusCodeFromError(error) { return CRPC_ERROR_CODE_TO_HTTP[error.code] ?? 500; } /** Type guard for CRPCError */ function isCRPCError(error) { return error instanceof CRPCError; } //#endregion //#region src/server/http-builder.ts function extractPathParams(path) { const matches = path.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g); return matches ? matches.map((m) => m.slice(1)) : []; } function matchPathParams(template, pathname) { const templateParts = template.split("/").filter(Boolean); const pathParts = pathname.split("/").filter(Boolean); if (templateParts.length !== pathParts.length) return null; const params = {}; for (let i = 0; i < templateParts.length; i++) { const templatePart = templateParts[i]; const pathPart = pathParts[i]; if (templatePart.startsWith(":")) params[templatePart.slice(1)] = decodeURIComponent(pathPart); else if (templatePart !== pathPart) return null; } return params; } function handleHttpError(error) { if (error instanceof CRPCError) { const status = { BAD_REQUEST: 400, UNAUTHORIZED: 401, FORBIDDEN: 403, NOT_FOUND: 404, METHOD_NOT_SUPPORTED: 405, CONFLICT: 409, UNPROCESSABLE_CONTENT: 422, TOO_MANY_REQUESTS: 429, INTERNAL_SERVER_ERROR: 500 }[error.code] ?? 500; return Response.json({ error: { code: error.code, message: error.message } }, { status }); } console.error("Unhandled HTTP error:", error); return Response.json({ error: { code: "INTERNAL_SERVER_ERROR", message: "An unexpected error occurred" } }, { status: 500 }); } function getBaseSchema(schema) { if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) return getBaseSchema(schema.unwrap()); if (schema instanceof z.ZodDefault) return getBaseSchema(schema._def.innerType); return schema; } function isArraySchema(schema) { return getBaseSchema(schema) instanceof z.ZodArray; } function isNumberSchema(schema) { return getBaseSchema(schema) instanceof z.ZodNumber; } function isBooleanSchema(schema) { return getBaseSchema(schema) instanceof z.ZodBoolean; } function parseQueryParams(url, schema) { const params = {}; const keys = new Set(url.searchParams.keys()); const shape = schema instanceof z.ZodObject ? schema.shape : {}; for (const key of keys) { const values = url.searchParams.getAll(key); const fieldSchema = shape[key]; if (fieldSchema) if (isArraySchema(fieldSchema)) params[key] = values; else if (isNumberSchema(fieldSchema)) params[key] = Number(values[0]); else if (isBooleanSchema(fieldSchema)) { const val = values[0].toLowerCase(); params[key] = val === "true" || val === "1"; } else params[key] = values.length === 1 ? values[0] : values; else params[key] = values.length === 1 ? values[0] : values; } return params; } /** Factory function to create a new builder with merged def */ function createNewHttpBuilder(def1, def2) { return createHttpBuilder({ ...def1, ...def2 }); } function resolveHttpProcedureInfo(def) { const route = def.route; return { type: "httpAction", name: def.procedureName ?? (route ? `${route.method} ${route.path}` : void 0), method: route?.method, path: route?.path }; } /** Internal method to create the HTTP procedure */ function createProcedure(def, handler, _type) { if (!def.route) throw new Error("Route must be defined before action. Use .route(path, method) first."); const middlewareProcedure = resolveHttpProcedureInfo(def); /** * Hono-compatible handler function. * When used with HttpRouterWithHono, Convex ctx is passed via c.env. */ const honoHandler = async (c) => { const convexCtx = c.env; const request = c.req.raw; try { const url = new URL(request.url); const pathParams = c.req.param() ?? matchPathParams(def.route.path, url.pathname) ?? {}; let ctx = def.functionConfig.createContext(convexCtx); const getRawInput = async () => { if ((request.headers.get("content-type") ?? "").includes("application/json")) return request.clone().json(); return null; }; let currentInput; for (const middleware of def.middlewares) { const result = await middleware({ ctx, procedure: middlewareProcedure, input: currentInput, getRawInput, next: async (opts) => { if (opts?.ctx) ctx = { ...ctx, ...opts.ctx }; if (opts?.input !== void 0) currentInput = opts.input; return { ctx, marker: void 0 }; }, meta: def.meta }); if (result?.ctx) ctx = { ...ctx, ...result.ctx }; } let parsedParams; if (def.paramsSchema) try { parsedParams = def.paramsSchema.parse(pathParams); } catch (error) { if (error instanceof z.ZodError) throw new CRPCError({ code: "BAD_REQUEST", message: "Invalid path params", cause: error }); throw error; } let parsedQuery; if (def.querySchema) { const queryParams = parseQueryParams(url, def.querySchema); try { parsedQuery = def.querySchema.parse(queryParams); } catch (error) { if (error instanceof z.ZodError) throw new CRPCError({ code: "BAD_REQUEST", message: "Invalid query params", cause: error }); throw error; } } let parsedInput; if (def.inputSchema && request.method !== "GET") { const contentType = request.headers.get("content-type") ?? ""; let body; if (contentType.includes("application/json")) body = await request.json(); else if (contentType.includes("application/x-www-form-urlencoded")) { const formData = await request.formData(); body = Object.fromEntries(formData.entries()); } else body = await request.json().catch(() => ({})); try { parsedInput = def.inputSchema.parse(def.functionConfig.transformer.input.deserialize(body)); } catch (error) { if (error instanceof z.ZodError) throw new CRPCError({ code: "BAD_REQUEST", message: "Invalid input", cause: error }); throw error; } } let parsedForm; if (def.formSchema && request.method !== "GET") { const formData = await request.formData(); const formObj = {}; for (const [key, value] of formData.entries()) formObj[key] = value; try { parsedForm = def.formSchema.parse(formObj); } catch (error) { if (error instanceof z.ZodError) throw new CRPCError({ code: "BAD_REQUEST", message: "Invalid form data", cause: error }); throw error; } } const handlerOpts = { ctx, c }; if (parsedInput !== void 0) handlerOpts.input = parsedInput; if (parsedParams !== void 0) handlerOpts.params = parsedParams; if (parsedQuery !== void 0) handlerOpts.searchParams = parsedQuery; if (parsedForm !== void 0) handlerOpts.form = parsedForm; const result = await handler(handlerOpts); if (result instanceof Response) return result; const output = def.outputSchema ? def.outputSchema.parse(result) : result; return c.json(def.functionConfig.transformer.output.serialize(output)); } catch (error) { return handleHttpError(error); } }; honoHandler._crpcRoute = { path: def.route.path, method: def.route.method }; const procedure = def.functionConfig.base(async (convexCtx, request) => { return honoHandler({ env: convexCtx, req: { raw: request, param: () => matchPathParams(def.route.path, new URL(request.url).pathname) ?? {} }, json: (data, status) => Response.json(data, { status }), text: (text, status) => new Response(text, { status }), body: (body, init) => new Response(body, init), html: (html, status) => new Response(html, { status, headers: { "Content-Type": "text/html" } }), redirect: (url, status) => Response.redirect(url, status ?? 302), header: (_name, _value) => {} }); }); procedure.isHttp = true; procedure._crpcHttpRoute = def.route; procedure._def = { inputSchema: def.inputSchema, outputSchema: def.outputSchema, paramsSchema: def.paramsSchema, querySchema: def.querySchema, formSchema: def.formSchema }; procedure._honoHandler = honoHandler; return procedure; } /** Create the builder implementation object */ function createHttpBuilder(def) { return { _def: def, use(middlewareOrBuilder) { const middlewares = "_middlewares" in middlewareOrBuilder ? middlewareOrBuilder._middlewares : [middlewareOrBuilder]; return createNewHttpBuilder(def, { middlewares: [...def.middlewares, ...middlewares] }); }, meta(value) { return createNewHttpBuilder(def, { meta: def.meta ? { ...def.meta, ...value } : value }); }, name(value) { return createNewHttpBuilder(def, { procedureName: value }); }, route(path, method) { const pathParamNames = extractPathParams(path); return createNewHttpBuilder(def, { route: { path, method, pathParamNames, usePathPrefix: pathParamNames.length > 0 } }); }, get(path) { const pathParamNames = extractPathParams(path); return createNewHttpBuilder(def, { route: { path, method: "GET", pathParamNames, usePathPrefix: pathParamNames.length > 0 } }); }, post(path) { const pathParamNames = extractPathParams(path); return createNewHttpBuilder(def, { route: { path, method: "POST", pathParamNames, usePathPrefix: pathParamNames.length > 0 } }); }, put(path) { const pathParamNames = extractPathParams(path); return createNewHttpBuilder(def, { route: { path, method: "PUT", pathParamNames, usePathPrefix: pathParamNames.length > 0 } }); }, patch(path) { const pathParamNames = extractPathParams(path); return createNewHttpBuilder(def, { route: { path, method: "PATCH", pathParamNames, usePathPrefix: pathParamNames.length > 0 } }); }, delete(path) { const pathParamNames = extractPathParams(path); return createNewHttpBuilder(def, { route: { path, method: "DELETE", pathParamNames, usePathPrefix: pathParamNames.length > 0 } }); }, params(schema) { return createNewHttpBuilder(def, { paramsSchema: schema }); }, searchParams(schema) { return createNewHttpBuilder(def, { querySchema: schema }); }, input(schema) { return createNewHttpBuilder(def, { inputSchema: schema }); }, output(schema) { return createNewHttpBuilder(def, { outputSchema: schema }); }, form(schema) { return createNewHttpBuilder(def, { formSchema: schema }); }, query(handler) { return createProcedure(def, handler, "query"); }, mutation(handler) { return createProcedure(def, handler, "mutation"); } }; } /** * Create initial HttpProcedureBuilder */ function createHttpProcedureBuilder(config) { return createHttpBuilder({ middlewares: [], meta: config.meta, functionConfig: { base: config.base, createContext: config.createContext, transformer: getTransformer(config.transformer) } }); } //#endregion //#region src/server/http-router.ts /** * Check if an export is a cRPC HTTP procedure * Note: Procedures are functions with attached properties, not plain objects */ function isCRPCHttpProcedure(value) { return typeof value === "function" && "isHttp" in value && value.isHttp === true && "_crpcHttpRoute" in value; } /** * Check if a value is a cRPC HTTP router */ function isCRPCHttpRouter(value) { return typeof value === "object" && value !== null && "_def" in value && value._def?.router === true; } /** * HTTP Router that wraps a Hono app for use with Convex. * Internal class - use `createHttpRouter()` factory instead. */ var HttpRouterWithHono = class extends HttpRouter { _app; _handler; constructor(app) { super(); this._app = app; this._handler = httpActionGeneric(async (ctx, request) => { return await app.fetch(request, ctx); }); const parentGetRoutes = this.getRoutes.bind(this); const parentLookup = this.lookup.bind(this); /** * Get routes from the Hono app for Convex dashboard display. * Returns route definitions in the format expected by Convex. */ this.getRoutes = () => { const parentRoutes = parentGetRoutes(); const honoRoutes = []; for (const route of this._app.routes) { const method = route.method.toUpperCase(); if ([ "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD" ].includes(method)) honoRoutes.push([ route.path, method, this._handler ]); } return [...parentRoutes.map((r) => [...r]), ...honoRoutes]; }; /** * Look up the handler for a given path and method. * Checks traditional routes first, then delegates to Hono's router. */ this.lookup = (path, method) => { const parentMatch = parentLookup(path, method); if (parentMatch !== null) return parentMatch; const normalizedMethod = method === "HEAD" ? "GET" : method; const matchResult = this._app.router.match(normalizedMethod, path); if (matchResult && matchResult[0].length > 0) return [ this._handler, normalizedMethod, path ]; return null; }; } }; /** * Create a router factory function (like tRPC's createRouterFactory) * * @example * ```ts * // In crpc.ts * export const router = c.router; * * // In api/todos.ts * export const todosRouter = router({ * get: publicRoute.get('/api/todos/:id')..., * create: authRoute.post('/api/todos')..., * }); * * // In http.ts * export const httpRouter = router({ * todos: todosRouter, * health, * }); * export type AppRouter = typeof httpRouter; * ``` */ function createHttpRouterFactory() { return function router(record) { const procedures = {}; /** * Recursively flatten procedures with dot-notation paths * Like tRPC's step() function in router.ts */ function step(obj, path = []) { for (const [key, value] of Object.entries(obj)) { const newPath = [...path, key]; const pathKey = newPath.join("."); if (isCRPCHttpProcedure(value)) procedures[pathKey] = value; else if (isCRPCHttpRouter(value)) for (const [procPath, proc] of Object.entries(value._def.procedures)) procedures[`${pathKey}.${procPath}`] = proc; else if (typeof value === "object" && value !== null) step(value, newPath); } } step(record); return { _def: { router: true, procedures, record } }; }; } /** * Create an HTTP router with cRPC routes registered. * * @example * ```ts * import { Hono } from 'hono'; * import { cors } from 'hono/cors'; * import { createHttpRouter } from 'kitcn/server'; * * const app = new Hono(); * app.use('/api/*', cors({ origin: process.env.SITE_URL, credentials: true })); * * export default createHttpRouter(app, httpRouter); * ``` */ function createHttpRouter(app, router) { for (const procedure of Object.values(router._def.procedures)) { const { path, method } = procedure._crpcHttpRoute; const honoHandler = procedure._honoHandler; if (!honoHandler) { console.warn(`Procedure at ${path} does not have a Hono handler. Make sure you are using the latest version of kitcn.`); continue; } switch (method) { case "GET": app.get(path, honoHandler); break; case "POST": app.post(path, honoHandler); break; case "PUT": app.put(path, honoHandler); break; case "PATCH": app.patch(path, honoHandler); break; case "DELETE": app.delete(path, honoHandler); break; } } return new HttpRouterWithHono(app); } /** * Extract route map from procedures for client runtime * * @example * ```ts * export const httpRoutes = extractRouteMap(httpRouter._def.procedures); * ``` */ function extractRouteMap(procedures) { const result = {}; for (const [name, proc] of Object.entries(procedures)) if (isCRPCHttpProcedure(proc)) result[name] = { path: proc._crpcHttpRoute.path, method: proc._crpcHttpRoute.method }; return result; } //#endregion //#region src/server/procedure-name.ts const LOOKUP_KEY = "__KITCN_PROCEDURE_NAME_LOOKUP__"; const HINTS_KEY = "__KITCN_PROCEDURE_NAME_HINTS__"; const PATH_SEPARATOR_RE = /\\/g; const TRIM_SLASHES_RE = /^\/+|\/+$/g; const PACKAGE_FRAME_MARKERS = ["/node_modules/kitcn/", "/packages/kitcn/"]; function decodeFileName(value) { if (!value.startsWith("file://")) return value; try { return decodeURIComponent(new URL(value).pathname); } catch { return value; } } function normalizePath(value) { return value.replace(PATH_SEPARATOR_RE, "/"); } function normalizeHint(value) { return normalizePath(value).replace(TRIM_SLASHES_RE, ""); } function getGlobalLookup() { const globalScope = globalThis; const existing = globalScope[LOOKUP_KEY]; if (existing && typeof existing === "object") return existing; const lookup = {}; globalScope[LOOKUP_KEY] = lookup; return lookup; } function getGlobalHints() { const globalScope = globalThis; const existing = globalScope[HINTS_KEY]; if (Array.isArray(existing)) return existing; const hints = []; globalScope[HINTS_KEY] = hints; return hints; } function registerProcedureNameLookup(lookup, functionsDirHint) { const globalLookup = getGlobalLookup(); for (const [relativeFilePath, entries] of Object.entries(lookup)) { const key = normalizePath(relativeFilePath); const existing = globalLookup[key] ?? []; const deduped = /* @__PURE__ */ new Map(); for (const entry of [...existing, ...entries]) deduped.set(`${entry.line}:${entry.column}:${entry.name}`, entry); globalLookup[key] = [...deduped.values()].sort((left, right) => left.line - right.line || left.column - right.column || left.name.localeCompare(right.name)); } const normalizedHint = normalizeHint(functionsDirHint); if (!normalizedHint)