DenisB
DenisB4w ago

How to use Zod for validating database writes?

I read in https://stack.convex.dev/typescript-zod-function-validation that it is possible to define a Zod schema and validate all database writes/updates using Zod. The article mentioned that, in order to do this, ctx.db must be wrapped. Does anyone have an example of how to do this? I tried to understand how rowLevelSecurity works (as it was mentioned in the article), but it seems to be beyond my comprehension. For example, I want the field of an object in db be validated like z.number().int().gt(0).safe()
Zod with TypeScript for Server-side Validation and End-to-End Types
Use Zod with TypeScript for argument validation on your server functions allows you to both protect against invalid data, and define TypeScript types ...
6 Replies
Convex Bot
Convex Bot4w ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
erquhart
erquhart4w ago
You'd have to understand the example in rowLevelSecurity well enough to write the helper. I'd recommend reading over Ian's recommendation from the article (tl;dr: using Zod to validate server functions is probably enough): https://stack.convex.dev/typescript-zod-function-validation#what-would-i-do
Zod with TypeScript for Server-side Validation and End-to-End Types
Use Zod with TypeScript for argument validation on your server functions allows you to both protect against invalid data, and define TypeScript types ...
DenisB
DenisBOP4w ago
Yes, I have seen this recommendation. But it seems to me that identifying validation errors at the stage of writing to the DB is the best option, instead of getting a mountain of problems after the fact. I would not envy anyone who has to fix errors in an inconsistent DB state in PROD environment.
erquhart
erquhart4w ago
I don't disagree, but this approach still doesn't give the system level guarantee of an actual schema validator, it just ensures that zod validation runs on reads and writes wherever the custom query/mutation is used - which isn't all that dissimilar from just validating the functions themselves. But the root issue is the helper doesn't currently exist, so that limits your options a bit if writing it isn't in the cards. Fwiw I personally handle it in a more manual way: - each table has a dedicated helper for insert and for patch, one each per table - ctx.db.insert/patch are only used in these helpers - validate however you like, Zod or otherwise, in the helpers - nothing gets written that doesn't pass validation Actually, the schema approach is a better guarantee than function validation because the schema is centrally defined for each table, I take that back But again, with the helper not written, you could also go the manual route with something like what I outlined. Both provide centralized validation, both require remembering to use a helper every time you write. I actually keep my table schemas in table specific files along with the insert/patch helpers, in which case you could parse with Zod using the same schema that you're using for table definition. Hope this is at all helpful lol, I know you were really just wanting to use a helper
DenisB
DenisBOP4w ago
thanks, interesting option with these helpers, do you have an usage example ?
erquhart
erquhart4w ago
Something like:
export const insert = (
ctx: MutationCtx,
input: InsertInput // this can be a Zod type or something,
) => {
const data = parseInput(input) // validate here
return ctx.db.insert('tableName', data)
}

// Now this
ctx.db.insert('tableName', input)

// Becomes this
insert(ctx, input)
export const insert = (
ctx: MutationCtx,
input: InsertInput // this can be a Zod type or something,
) => {
const data = parseInput(input) // validate here
return ctx.db.insert('tableName', data)
}

// Now this
ctx.db.insert('tableName', input)

// Becomes this
insert(ctx, input)
I have a convex/db folder with an index that exports an object with each table file's exports, so for me it looks like:
import * as db from './db'

// Insert (or patch) within a mutation
await db.myTable.insert(ctx, input)
import * as db from './db'

// Insert (or patch) within a mutation
await db.myTable.insert(ctx, input)
I actually keep all use of ctx.db inside that db/ folder, so all my convex functions just use this db object to interact with the db, and they never have to think about validation, indexes, etc, that all belongs to the db layer.

Did you find this page helpful?