Created
December 24, 2025 08:57
-
-
Save jacobsamo/f1370c4d302fe9bdf78d803158f8fcdd to your computer and use it in GitHub Desktop.
I created this neat little helper function for defining tables for convex using zod, it utilises the convex-helpers (https://github.com/get-convex/convex-helpers) package for transforming zod into convex schemas, this apporach reduces code duplicated as well as reducing the change of missing a type or field in queries or mutations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // convex/helpers.ts | |
| import { NoOp } from "convex-helpers/server/customFunctions"; | |
| import { | |
| zCustomAction, | |
| zCustomMutation, | |
| zCustomQuery, | |
| } from "convex-helpers/server/zod4"; | |
| import { components } from "./_generated/api"; | |
| import type { Id } from "./_generated/dataModel"; | |
| import { | |
| type MutationCtx, | |
| type QueryCtx, | |
| action, | |
| internalMutation, | |
| mutation, | |
| query, | |
| } from "./_generated/server"; | |
| async function getUser(ctx: MutationCtx | QueryCtx) { | |
| const identity = await ctx.auth.getUserIdentity(); | |
| console.log("IDENTITY", identity); | |
| if (!identity) return null; | |
| const user = await ctx.db | |
| .query("users") | |
| .withIndex("by_id", (q) => | |
| q.eq("_id", identity.subject as any) | |
| ) | |
| .unique(); | |
| if (!user) return null; | |
| return user; | |
| } | |
| export const authedMutation = zCustomMutation(mutation, { | |
| args: {}, | |
| input: async (ctx, args) => { | |
| const user = await getUser(ctx); | |
| console.log("USER", user); | |
| if (!user) throw new Error("Unauthorized"); | |
| return { | |
| ctx: { | |
| ...ctx, | |
| user, | |
| }, | |
| args, | |
| }; | |
| }, | |
| }); | |
| export const authedQuery = zCustomQuery(query, { | |
| args: {}, | |
| input: async (ctx, args) => { | |
| const user = await getUser(ctx); | |
| console.log("USER", user); | |
| if (!user) throw new Error("Unauthorized"); | |
| return { ctx: { ...ctx, user }, args }; | |
| }, | |
| }); | |
| export const zodQuery = zCustomQuery(query, NoOp); | |
| export const zodMutation = zCustomMutation(mutation, NoOp); | |
| export const zodInternalMutation = zCustomMutation(internalMutation, NoOp); | |
| export const zodAction = zCustomAction(action, NoOp); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { zid, zodToConvex } from "convex-helpers/server/zod4"; | |
| import { defineTable } from "convex/server"; | |
| import * as z from "zod"; | |
| /** | |
| * Defines a Convex table schema with automatic _id and _creationTime fields using convex-helpers. | |
| * | |
| * @example | |
| * ```typescript | |
| * // Define a table schema | |
| * const userTable = defineTable("users", { | |
| * clerkUserId: z.string(), | |
| * name: z.string(), | |
| * email: z.string().email(), | |
| * role: z.enum(["admin", "user"]) | |
| * }); | |
| * | |
| * // Use as a normal Zod schema | |
| * export const userSchema = userTable.schema; | |
| * type User = z.infer<typeof userTable.schema>; | |
| * | |
| * // Get insert schema (optional _id/_creationTime that can't be overridden) | |
| * const insertUserSchema = userTable.insertSchema(); | |
| * type InsertUser = z.infer<typeof insertUserSchema>; | |
| * | |
| * // Get update schema (all fields partial, no _id/_creationTime) | |
| * const updateUserSchema = userTable.updateSchema(); | |
| * type UpdateUser = z.infer<typeof updateUserSchema>; | |
| * | |
| * | |
| * // Use in Convex schema.ts | |
| * import { defineSchema } from "convex/server"; | |
| * | |
| * export default defineSchema({ | |
| * users: userTable.table() | |
| * .index("by_email", ["email"]) | |
| * }); | |
| * | |
| * // Or export just the schema for mutations/queries | |
| * export const { schema: userSchema, insert: userInsert, update: userUpdate } = userTable; | |
| * ``` | |
| * | |
| * @param {string} tableName - The name of the Convex table | |
| * @param {{ [key: string]: z.ZodType }} schema - Zod object shape defining the table fields (without _id and _creationTime) | |
| * @returns Object with schema, insert(), update(), table() methods and tableName | |
| */ | |
| export const zodTable = < | |
| Table extends string, | |
| T extends { [key: string]: z.ZodType }, | |
| >( | |
| tableName: Table, | |
| schema: T, | |
| ) => { | |
| // add _id, _creationTime, and isArchived (for soft deletes) | |
| const fullSchema = z.object({ | |
| ...schema, | |
| _id: zid(tableName), | |
| _creationTime: z.number(), | |
| }); | |
| const insertSchema = fullSchema.partial({ | |
| _id: true, | |
| _creationTime: true, | |
| }); | |
| const updateSchema = fullSchema | |
| .omit({ _id: true, _creationTime: true }) | |
| .partial(); | |
| return { | |
| tableName, | |
| /** | |
| * The complete Zod schema including _id and _creationTime. | |
| * Use this for type inference and validation of full table rows. | |
| * | |
| * @example | |
| * type User = z.infer<typeof userTable.schema>; | |
| */ | |
| schema: fullSchema, | |
| /** | |
| * Returns an insert schema where _id and _creationTime are optional. | |
| * These fields cannot be overridden - Convex will generate them automatically. | |
| * | |
| * @example | |
| * ```typescript | |
| * | |
| * // In a mutation | |
| * export const createUser = mutation({ | |
| * args: userTable.insertSchema, | |
| * handler: async (ctx, args) => { | |
| * await ctx.db.insert("users", args); | |
| * } | |
| * }); | |
| * ``` | |
| */ | |
| insertSchema, | |
| /** | |
| * Returns an update schema where all fields are partial and _id/_creationTime are omitted. | |
| * Use this for patch operations where you only want to update specific fields. | |
| * | |
| * @example | |
| * ```typescript | |
| * | |
| * // In a mutation | |
| * export const updateUser = mutation({ | |
| * args: { | |
| * userId: zid("users"), | |
| * updates: userTable.updateSchema, | |
| * }, | |
| * handler: async (ctx, args) => { | |
| * await ctx.db.patch(args.userId, args.updates); | |
| * } | |
| * }); | |
| * ``` | |
| */ | |
| updateSchema, | |
| /** | |
| * Converts the Zod schema to a Convex Table | |
| * This uses the zodToConvex helper from convex-helpers. and the defineTable from "convex/server" to return a table | |
| * | |
| * @example | |
| * ```typescript | |
| * import { defineSchema } from "convex/server"; | |
| * | |
| * export default defineSchema({ | |
| * users: userTable.table() | |
| * .index("by_email", ["email"]) | |
| * }); | |
| * ``` | |
| */ | |
| table: () => { | |
| return defineTable(zodToConvex(fullSchema)); | |
| }, | |
| }; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment