drizzle-valibot 实战指南:从 Drizzle ORM Schema 自动生成 Valibot 校验 Schema
2026/9/19 23:12:29 网站建设 项目流程

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.0valibot >= 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
insertgenerated 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>>(); });

可以看到:generatedAlwaysAsIdentityid被直接剔除(无需也不应手动传入);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 之前,因此你是在"原始列类型"的基础上追加规则,非常适合补充minValuemaxLengthemailregex等业务约束。

从类型层面看(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,包含如下精细化规则:
    • PgUUIDv.pipe(v.string(), v.uuid())
    • PgVarchar/SQLiteText→ 按column.length追加v.maxLength
    • MySqlVarChar/SingleStoreVarChar→ 未指定长度时上限取INT16_UNSIGNED_MAX(65535);
    • MySqlText/SingleStoreText→ 按textTypetinytext/text/mediumtext/longtext)分别映射 255 / 65535 / 16777215 / 4294967295;
    • PgChar/MySqlChar/SingleStoreChar(定长)→ 使用v.length(max)精确匹配;
    • PgBinaryVector→ 追加/^[01]+$/正则校验,并限制长度为维度数。
  • JSON 列→ 导出并复用jsonSchemav.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.custominstanceof Buffer检查);
  • PostgreSQL 空间类型
    • PgGeometry/PgPointTuplev.tuple([v.number(), v.number()])
    • PgPointObject/PgGeometryObjectv.object({ x: v.number(), y: v.number() })
    • PgLine(元组形式)→ 三元组 tuple;PgLineABC{ a, b, c }对象;
    • PgVector/PgHalfVectorv.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 的GetValibotTypeHandleSelectColumn/HandleInsertColumn/HandleUpdateColumn等),并通过Expect<Equal<typeof result, typeof expected>>()之类的测试保证运行时结果与静态类型严格一致——这正是"类型安全校验"体验的来源。

视图与枚举:Select Schema 的额外来源

createSelectSchema的入参类型(schema.types.ts)不仅支持Table,还支持ViewPostgreSQL 枚举

  • 传入视图时,插件通过getViewSelectedFields获取视图的选中字段并同样生成v.object(...),且类型基于TView['$inferSelect'];底层对嵌套对象字段(子查询选择集)也会递归处理(见 schema.ts 的handleColumns递归分支)。测试覆盖了pgViewpgMaterializedView等场景(见 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.tsCreateSelectSchema/CreateInsertSchema/CreateUpdateSchema的类型签名
drizzle-valibot/src/schema.types.internal.tsBuildSchema/NoUnknownKeys等类型构建工具
drizzle-valibot/src/column.types.tsGetValibotType/HandleColumn类型级映射
drizzle-valibot/src/utils.tsisColumnType/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、构建产物信息

使用建议与注意事项

  1. 依赖版本drizzle-valibot要求drizzle-orm >= 0.36.0valibot >= 1.0.0-beta.7(当前仓库内测试固定使用valibot 1.0.0-beta.7,见 package.json),安装前请确认项目版本满足要求。
  2. 方言差异由列类型天然表达:PostgreSQL 的pgEnumPgVectorPgUUID,MySQL 的unsignedtextType,SQLite 的integer/real等都会被翻译为对应的 valibot 约束,无需为方言编写额外适配代码。
  3. 善用第二个参数:业务级校验(如minValue(0)、邮箱格式、正则、自定义精炼)应放在refine回调中追加到自动生成的 Schema 之上,既保留数据库约束,又避免手工重复定义整个 Schema。
  4. 不要在refine中使用未知字段名:类型系统会通过NoUnknownKeys报错,编译期即可发现问题。
  5. 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询