drizzle-valibot 实战指南:从 Drizzle ORM Schema 自动生成 Valibot 校验 Schema
【免费下载链接】drizzle-ormORM项目地址: https://gitcode.com/gh_mirrors/dr/drizzle-orm
导读
本文围绕 Drizzle ORM 官方插件drizzle-valibot,讲解如何从 Drizzle ORM 的表、视图与枚举定义自动生成 valibot 运行时校验 Schema,用于 API 请求参数(插入/更新)与响应数据(查询)的校验。阅读本文后,你将掌握createSelectSchema/createInsertSchema/createUpdateSchema三个核心 API 的用法、字段覆盖与精炼(refine)技巧,并理解底层"列类型 → valibot Schema"的映射规则、可空/可选/默认值推导逻辑以及各数据库方言的支持范围。
drizzle-valibot 是什么
drizzle-valibot是 Drizzle ORM 官方维护的插件(位于仓库的 drizzle-valibot 目录,npm 包版本 0.4.2),其职责是从 Drizzle ORM 的 Schema 定义自动生成 valibot 校验 Schema,从而消除"数据库表定义"与"运行时数据校验规则"之间的手工重复维护。
它提供的核心能力(见 README):
- 为表、视图和枚举生成select(查询)Schema;
- 为表生成insert(插入)与update(更新)Schema;
- 支持全部方言:PostgreSQL、MySQL 与 SQLite。
从 package.json 可以确认其安装前提:drizzle-orm >= 0.36.0,valibot >= 1.0.0-beta.7,并且通过exports字段同时提供了 ESM(.mjs/.d.mts)与 CJS(.cjs/.d.cjs)产物,兼容两种模块体系的项目。
快速上手:从一张表生成三种 Schema
在 README 给出的标准用法中,先定义一张 PostgreSQL 用户表,然后即可一次性得到插入、更新、查询三种 valibot Schema:
import { pgEnum, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'; import { createInsertSchema, createSelectSchema } from 'drizzle-valibot'; import { string, parse, number, pipe } from 'valibot'; const users = pgTable('users', { id: serial('id').primaryKey(), name: text('name').notNull(), email: text('email').notNull(), role: text('role', { enum: ['admin', 'user'] }).notNull(), createdAt: timestamp('created_at').notNull().defaultNow(), }); // Schema for inserting a user - can be used to validate API requests const insertUserSchema = createInsertSchema(users); // Schema for updating a user - can be used to validate API requests const updateUserSchema = createUpdateSchema(users); // Schema for selecting a user - can be used to validate API responses const selectUserSchema = createSelectSchema(users); // Usage const isUserValid = parse(insertUserSchema, { name: 'John Doe', email: 'johndoe@test.com', role: 'admin', });注意:示例中实际还使用了createUpdateSchema(在 schema.ts 中有完整实现,并在 pg.test.ts 中有对应测试),README 的导入语句中未列出它,但你只需从'drizzle-valibot'一并导入即可。
生成的三个 Schema 行为不同,这正是插件最有价值的地方:
insertUserSchema:用于校验"新增一条记录"时提交的请求体;updateUserSchema:用于校验"更新记录"时的请求体;selectUserSchema:用于校验从数据库查询返回的结果(API 响应)。
字段的默认推导规则:nullable / optional / 剔除
生成结果并非简单地把列类型"一比一"翻译,而是根据列修饰符推导出精确的可选性。核心逻辑集中在 schema.ts 的handleColumns与三个工厂函数的conditions配置中:
export const createSelectSchema = (entity, refine?) => { // ... return handleColumns(columns, refine ?? {}, { never: () => false, optional: () => false, nullable: (column) => !column.notNull, }); }; export const createInsertSchema = (entity, refine?) => { return handleColumns(columns, refine ?? {}, { never: (column) => column?.generated?.type === 'always' || column?.generatedIdentity?.type === 'always', optional: (column) => !column.notNull || (column.notNull && column.hasDefault), nullable: (column) => !column.notNull, }); }; export const createUpdateSchema = (entity, refine?) => { return handleColumns(columns, refine ?? {}, { never: (column) => column?.generated?.type === 'always' || column?.generatedIdentity?.type === 'always', optional: () => true, nullable: (column) => !column.notNull, }); };推导规则可总结如下:
| Schema 类型 | 剔除(never) | 可选(optional) | 可空(nullable) |
|---|---|---|---|
| select | 无 | 永不 | 列非notNull时 |
| insert | generated always/generatedIdentity = always的列 | 非notNull的列,以及"有默认值"的notNull列 | 列非notNull时 |
| update | 同上(生成列) | 所有列 | 列非notNull时 |
这些行为在 pg.test.ts 中有精确的类型级断言,例如:
test('table - insert', (t) => { const table = pgTable('test', { id: integer().generatedAlwaysAsIdentity().primaryKey(), name: text().notNull(), age: integer(), }); const result = createInsertSchema(table); const expected = v.object({ name: textSchema, age: v.optional(v.nullable(integerSchema)) }); expectSchemaShape(t, expected).from(result); Expect<Equal<typeof result, typeof expected>>(); });可以看到:generatedAlwaysAsIdentity的id被直接剔除(无需也不应手动传入);notNull且无默认值的name成为必填项;可空的age变成v.optional(v.nullable(...))。update场景下所有字段都变为可选(name: v.optional(textSchema)、age: v.optional(v.nullable(integerSchema))),因为部分更新天然允许只传少数字段。
覆盖与精炼:按需调整字段
自动生成的 Schema 未必完全符合业务要求,drizzle-valibot为每个工厂函数都提供了第二个参数refine,支持两种调整方式(README):
1. 直接覆盖字段(以 valibot Schema 替换):
const insertUserSchema = createInsertSchema(users, { role: string(), });此时role列原本由text('role', { enum: ['admin', 'user'] })推导出的枚举校验,会被替换为宽松的string()。
2. 精炼字段(对生成的 Schema 再做管道处理):
const insertUserSchema = createInsertSchema(users, { id: (schema) => pipe([schema, minValue(0)]), role: string(), });精炼回调接收当前列已生成的 valibot Schema 作为输入,返回经过pipe追加约束后的新 Schema——注意 README 注释强调:精炼发生在字段被处理为 nullable/optional 之前,因此你是在"原始列类型"的基础上追加规则,非常适合补充minValue、maxLength、email、regex等业务约束。
从类型层面看(schema.types.internal.ts 的NoUnknownKeys),refine对象中不允许出现不存在的列名——如果误写了未知键,会触发DrizzleTypeError<Found unknown key in refinement: "xxx">,从而在编译期就拦截拼写错误。
列类型到 valibot Schema 的映射原理
自动生成的核心是 column.ts 中的columnToSchema函数,它按列的类型元信息(column.columnType/column.dataType)分派:
- 枚举列:列带
enumValues时生成v.enum(mapEnumValues(column.enumValues))(通过mapEnumValues将字符串数组映射为同名键值对象),否则退化为v.string(); - 数值列:走
numberColumnToSchema,依据具体列类型套用最小/最大值(常量定义在 constants.ts),并视类型追加v.integer():MySqlTinyInt/SingleStoreTinyInt→ INT8 范围(-128 ~ 127,unsigned 0 ~ 255);PgSmallInt/MySqlSmallInt/SingleStoreSmallInt/PgSmallSerial→ INT16 范围;PgInteger/PgSerial/MySqlInt/SingleStoreInt→ INT32 范围;PgDoublePrecision/MySqlDouble/MySqlReal/SQLiteReal等 → INT48 范围;PgBigInt53/MySqlBigInt53/SQLiteInteger等 →Number.MIN_SAFE_INTEGER~Number.MAX_SAFE_INTEGER;MySqlYear/SingleStoreYear→ 1901 ~ 2155;- MySQL/SingleStore 的
unsigned列会从 0 起算(通过column.getSQLType().includes('unsigned')判断),MySqlSerial等自增类型也按无符号处理。
- bigint 列(
dataType === 'bigint'):走bigintColumnToSchema,生成v.pipe(v.bigint(), v.minValue(INT64_MIN), v.maxValue(INT64_MAX)),使用BigInt字面量参与比较; - 布尔列→
v.boolean(); - 日期列→
v.date(); - 字符串列:走
stringColumnToSchema,包含如下精细化规则:PgUUID→v.pipe(v.string(), v.uuid());PgVarchar/SQLiteText→ 按column.length追加v.maxLength;MySqlVarChar/SingleStoreVarChar→ 未指定长度时上限取INT16_UNSIGNED_MAX(65535);MySqlText/SingleStoreText→ 按textType(tinytext/text/mediumtext/longtext)分别映射 255 / 65535 / 16777215 / 4294967295;PgChar/MySqlChar/SingleStoreChar(定长)→ 使用v.length(max)精确匹配;PgBinaryVector→ 追加/^[01]+$/正则校验,并限制长度为维度数。
- JSON 列→ 导出并复用
jsonSchema:v.union([literalSchema, v.array(v.any()), v.record(v.string(), v.any())]),其中literalSchema = v.union([v.string(), v.number(), v.boolean(), v.null()]); - buffer 列→
bufferSchema(基于v.custom的instanceof Buffer检查); - PostgreSQL 空间类型:
PgGeometry/PgPointTuple→v.tuple([v.number(), v.number()]);PgPointObject/PgGeometryObject→v.object({ x: v.number(), y: v.number() });PgLine(元组形式)→ 三元组 tuple;PgLineABC→{ a, b, c }对象;PgVector/PgHalfVector→v.array(v.number()),有维度时追加v.length(dimensions)。
- 数组列:
PgArray递归处理baseColumn生成v.array(...),带size时追加v.length(size);其他dataType === 'array'退化为v.array(v.any()); - 未知类型:兜底
v.any()。
这种映射同时存在于运行时(columnToSchema)与类型层(column.types.ts 的GetValibotType、HandleSelectColumn/HandleInsertColumn/HandleUpdateColumn等),并通过Expect<Equal<typeof result, typeof expected>>()之类的测试保证运行时结果与静态类型严格一致——这正是"类型安全校验"体验的来源。
视图与枚举:Select Schema 的额外来源
createSelectSchema的入参类型(schema.types.ts)不仅支持Table,还支持View与PostgreSQL 枚举:
- 传入视图时,插件通过
getViewSelectedFields获取视图的选中字段并同样生成v.object(...),且类型基于TView['$inferSelect'];底层对嵌套对象字段(子查询选择集)也会递归处理(见 schema.ts 的handleColumns递归分支)。测试覆盖了pgView、pgMaterializedView等场景(见 pg.test.ts); - 传入
pgEnum枚举时,直接生成v.enum(...):
const roleEnum = pgEnum('role', ['admin', 'user']); const roleSchema = createSelectSchema(roleEnum); // v.enum({ admin: 'admin', user: 'user' })其运行时判定依据是 utils.ts 中的isPgEnum(检测enumValues是否为非空字符串数组),对应类型为v.EnumSchema<...>。
源码模块速览
想要深入阅读实现,可以按以下文件脉络展开:
| 文件 | 职责 |
|---|---|
| drizzle-valibot/src/schema.ts | 三个工厂函数与handleColumns核心循环 |
| drizzle-valibot/src/column.ts | 列类型 → valibot Schema 的映射(数值/字符串/bigint/JSON/buffer) |
| drizzle-valibot/src/constants.ts | 各整数类型的 min/max 常量(INT8 ~ INT64) |
| drizzle-valibot/src/schema.types.ts | CreateSelectSchema/CreateInsertSchema/CreateUpdateSchema的类型签名 |
| drizzle-valibot/src/schema.types.internal.ts | BuildSchema/NoUnknownKeys等类型构建工具 |
| drizzle-valibot/src/column.types.ts | GetValibotType/HandleColumn类型级映射 |
| drizzle-valibot/src/utils.ts | isColumnType/isWithEnum/Json等辅助工具 |
| drizzle-valibot/src/index.ts | 包对外导出入口 |
| drizzle-valibot/tests/pg.test.ts | 行为与类型双重测试(pg 方言) |
| drizzle-valibot/tests/mysql.test.ts 等 | MySQL / SQLite / SingleStore 方言测试 |
| drizzle-valibot/package.json | 版本、peerDependencies、构建产物信息 |
使用建议与注意事项
- 依赖版本:
drizzle-valibot要求drizzle-orm >= 0.36.0且valibot >= 1.0.0-beta.7(当前仓库内测试固定使用valibot 1.0.0-beta.7,见 package.json),安装前请确认项目版本满足要求。 - 方言差异由列类型天然表达:PostgreSQL 的
pgEnum、PgVector、PgUUID,MySQL 的unsigned与textType,SQLite 的integer/real等都会被翻译为对应的 valibot 约束,无需为方言编写额外适配代码。 - 善用第二个参数:业务级校验(如
minValue(0)、邮箱格式、正则、自定义精炼)应放在refine回调中追加到自动生成的 Schema 之上,既保留数据库约束,又避免手工重复定义整个 Schema。 - 不要在
refine中使用未知字段名:类型系统会通过NoUnknownKeys报错,编译期即可发现问题。 - select 与 insert/update 用途要区分:查询响应校验不应对字段做 optional 化(
createSelectSchema的 optional 恒为 false),而请求体校验则应使用 insert/update 版本以获得正确的默认值推导。
综上,drizzle-valibot把"数据库表结构"变成了唯一的校验规则事实来源:定义一次 Drizzle Schema,即可同时获得类型安全的查询结果与请求体校验 Schema,适合任何以 Drizzle ORM 为数据层的 TypeScript 服务端项目。
【免费下载链接】drizzle-ormORM项目地址: https://gitcode.com/gh_mirrors/dr/drizzle-orm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考