Skip to content

Instantly share code, notes, and snippets.

@ImRLopezAI
Forked from jacobsamo/convex-helpers.ts
Last active March 25, 2026 15:36
Show Gist options
  • Select an option

  • Save ImRLopezAI/13294581f3ed8e8478befe1bb664b690 to your computer and use it in GitHub Desktop.

Select an option

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.
// 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);
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