kitcn
Version:
kitcn - React Query integration and CLI tools for Convex
1,428 lines (1,423 loc) • 75.7 kB
JavaScript
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)