drizzle-validator
v0.0.1
Published
Declarative async validation for Drizzle ORM
Maintainers
Readme
drizzle-validator
Declarative async validation for Drizzle ORM — the checks a normal schema validator can't do because they need a query: "does this category exist?", "is this email already taken?".
You point a rule at a Drizzle column, pass it your request body, and get back a plain result. The library never throws — you decide what an invalid result means. Works on Postgres, MySQL and SQLite.
import { validate, exists, unique, existsAll } from 'drizzle-validator';
const result = await validate(db, body, {
email: unique(users.email),
categoryId: exists(categories.id),
tagIds: existsAll(tags.id),
});
if (!result.res) {
console.log(result.errors); // [{ key, message, rule, table, column?, value? }]
}Install
npm install drizzle-validatorHow it reads
validate(db, body, rules)—dbis your Drizzle client,bodyis the object you're validating (e.g. a request body), andrulesis keyed by field name.- Each rule takes a Drizzle column. The column tells the rule what table to query and what type the field must be — so pointing a rule at the wrong column is a TypeScript error.
- A field whose value is
undefinedis skipped (handy for optional/partial updates). - You get back either
{ res: true }or{ res: false, errors: [...] }.
The rules
exists(column)
Passes when a row with that value exists.
await validate(db, body, { categoryId: exists(categories.id) });unique(column)
Passes when no row has that value. Use it for "email already taken" style checks.
await validate(db, body, { email: unique(users.email) });On update, exclude the record itself so it doesn't clash with its own value:
await validate(db, body, {
email: unique(users.email).exclude(users.id, currentUserId),
});existsAll(column)
For an array field — passes when every element exists. Great for many-to-many links (e.g. attaching a list of tags). An empty array passes.
await validate(db, body, { tagIds: existsAll(tags.id) });Chaining rules on one field
Give a field an array of rules. They run in order and stop at the first failure — so you don't get a confusing "not allowed" error for an id that simply doesn't exist:
await validate(db, body, {
categoryId: [ exists(categories.id), /* ...more checks... */ ],
});Different fields are checked in parallel; you get one error per failing field.
The result
type ValidationResult =
| { res: true }
| { res: false; errors: ValidationFailure[] };
type ValidationFailure = {
key: string; // the field that failed
message: string; // human-readable
rule: string; // 'exists' | 'unique' | 'existsAll'
table: string; // the DB table checked
column?: string; // the column checked
value?: unknown; // the offending value (for existsAll, the missing items)
};Because failures are structured, you can render whatever error shape your app needs instead of parsing strings.
Throwing, if you want it
The library returns; you throw. A common pattern in a web framework:
const result = await validate(db, body, rules);
if (!result.res) {
throw new UnprocessableEntityError(result.errors); // your framework's 422
}Validating in the same place a lot? Bind the db once:
import { createValidator } from 'drizzle-validator';
const validator = createValidator(db);
const result = await validator.validate(body, rules);License
MIT
