-
-
Save ImRLopezAI/13294581f3ed8e8478befe1bb664b690 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 type { GenericId } from 'convex/values' | |
| import { defineTable } from 'convex/server' | |
| import { zid, zodToConvex } from 'convex-helpers/server/zod4' | |
| import * as z from 'zod' | |
| // JSON-schema-safe Convex ID for tools: still typed as GenericId<TableName> but uses primitive string checks. | |
| const jsonSafeZid = <TableName extends string>( | |
| tableName: TableName, | |
| ): z.ZodType<GenericId<TableName>> => | |
| z | |
| .string() | |
| .describe(`Convex Id<${tableName}>`) as unknown as z.ZodType<GenericId<TableName>> | |
| /** | |
| * 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: (id: typeof zid) => T, | |
| ) => { | |
| // add _id, _creationTime, and inject zid for relational validation | |
| const fullSchema = z.object({ | |
| ...schema(zid), | |
| _id: zid(tableName), | |
| _creationTime: z.number(), | |
| updatedAt: z.iso.date().optional().default(new Date().toISOString()), | |
| }) | |
| // JSON-schema-safe version for JSON SCHEMAS (avoid z.custom while keeping typed IDs). | |
| const toolSafeFullSchema = z.object({ | |
| ...schema(jsonSafeZid as typeof zid), | |
| _id: jsonSafeZid(tableName), | |
| _creationTime: z.number(), | |
| updatedAt: z.iso.date().optional().default(new Date().toISOString()), | |
| }) | |
| const insertSchema = fullSchema.omit({ _id: true, _creationTime: true, updatedAt: true }) | |
| const updateSchema = insertSchema.partial() | |
| const toolInsertSchema = toolSafeFullSchema.omit({ | |
| _id: true, | |
| _creationTime: true, | |
| updatedAt: true, | |
| }) | |
| const toolUpdateSchema = toolInsertSchema.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)) | |
| }, | |
| /** | |
| * Convex insert validator for this table | |
| * This converts the insertSchema to Convex format using zodToConvex | |
| * @example | |
| * ```typescript | |
| * import { mutation } from "./_generated/server"; | |
| * | |
| * export const createUser = mutation({ | |
| * args: userTable.insertSchema, | |
| * handler: async (ctx, args) => { | |
| * await ctx.db.insert("users", args); | |
| * } | |
| * }); | |
| * ``` | |
| */ | |
| insert: () => zodToConvex(insertSchema), | |
| /** | |
| * Convex update validator for this table | |
| * This converts the updateSchema to Convex format using zodToConvex | |
| * @example | |
| * ```typescript | |
| * import { mutation } from "./_generated/server"; | |
| * | |
| * export const updateUser = mutation({ | |
| * args: { | |
| * userId: zid("users"), | |
| * updates: userTable.updateSchema, | |
| * }, | |
| * handler: async (ctx, args) => { | |
| * await ctx.db.patch(args.userId, args.updates); | |
| * } | |
| * }); | |
| * ``` | |
| */ | |
| update: () => zodToConvex(updateSchema), | |
| /** | |
| * Typed schemas for JSON-schema-safe tool inputs (avoid z.custom while keeping typed IDs). | |
| */ | |
| tools: { | |
| insert: toolInsertSchema, | |
| update: z.object({ | |
| data: toolUpdateSchema, | |
| id: jsonSafeZid(tableName), | |
| }) | |
| , | |
| id: z.object({ | |
| id: jsonSafeZid(tableName), | |
| }), | |
| }, | |
| } | |
| } | |
| /** | |
| * Example usage: | |
| export const threads = zodTable('threads', () => ({ | |
| name: z.string(), | |
| source: z.string(), | |
| })) | |
| export const messages = zodTable('messages', (id) => ({ | |
| thread: id('threads'), | |
| content: z.string(), | |
| from: z.enum(['user', 'agent', 'system']), | |
| metadata: z.record(z.string(), z.any()).optional(), | |
| })) | |
| export const attachments = zodTable('attachments', (id) => ({ | |
| title: z.string(), | |
| type: z.enum(E.ATTACHMENT_TYPES), | |
| status: z.enum(E.ATTACHMENT_STATUSES), | |
| owner: z.string(), | |
| format: z.enum(E.ATTACHMENT_FORMATS), | |
| pages: z.number(), | |
| sensitivity: z.enum(E.ATTACHMENT_SENSITIVITY), | |
| summary: z.string(), | |
| url: z.string().optional(), | |
| message: id('messages').optional(), | |
| })) | |
| */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment