主题
Drizzle
Drizzle ORM 是一个无头 TypeScript ORM,专注于类型安全和开发者体验。
¥Drizzle ORM is a headless TypeScript ORM with a focus on type safety and developer experience.
我们可以使用 drizzle-typebox
将 Drizzle 模式转换为 Elysia 验证模型。
¥We may convert Drizzle schema to Elysia validation models using drizzle-typebox
Drizzle Typebox
Elysia.t 是 TypeBox 的一个分支,允许我们在 Elysia 中直接使用任何 TypeBox 类型。
¥Elysia.t is a fork of TypeBox, allowing us to use any TypeBox type in Elysia directly.
我们可以使用 "drizzle-typebox" 将 Drizzle 模式转换为 TypeBox 模式,并直接在 Elysia 的模式验证中使用它。
¥We can convert Drizzle schema into TypeBox schema using "drizzle-typebox", and use it directly on Elysia's schema validation.
工作原理如下:
¥Here's how it works:
- 在 Drizzle 中定义数据库模式。
- 使用
drizzle-typebox
将 Drizzle 模式转换为 Elysia 验证模型。 - 使用转换后的 Elysia 验证模型来确保类型验证。
- OpenAPI 模式由 Elysia 验证模型生成。
- 添加 Eden 条约,为前端添加类型安全。
* ——————————————— *
| |
| -> | Documentation |
* ————————— * * ———————— * OpenAPI | | |
| | drizzle- | | ——————— | * ——————————————— *
| Drizzle | —————————-> | Elysia |
| | -typebox | | ——————— | * ——————————————— *
* ————————— * * ———————— * Eden | | |
| -> | Frontend Code |
| |
* ——————————————— *
安装
¥Installation
要安装 Drizzle,请运行以下命令:
¥To install Drizzle, run the following command:
bash
bun add drizzle-orm drizzle-typebox
然后你需要固定 @sinclair/typebox
,因为 drizzle-typebox
和 Elysia
之间可能存在版本不匹配的情况,这可能会导致两个版本之间的符号冲突。
¥Then you need to pin @sinclair/typebox
as there might be a mismatch version between drizzle-typebox
and Elysia
, this may cause conflict of Symbols between two versions.
我们建议使用以下方法将 @sinclair/typebox
的版本固定为 elysia
使用的最低版本:
¥We recommend pinning the version of @sinclair/typebox
to the minimum version used by elysia
by using:
bash
grep "@sinclair/typebox" node_modules/elysia/package.json
我们可以使用 package.json
中的 overrides
字段来固定 @sinclair/typebox
的版本:
¥We may use overrides
field in package.json
to pin the version of @sinclair/typebox
:
json
{
"overrides": {
"@sinclair/typebox": "0.32.4"
}
}
Drizzle 模式
¥Drizzle schema
假设我们的代码库中有一个 user
表,如下所示:
¥Assuming we have a user
table in our codebase as follows:
ts
import { relations } from 'drizzle-orm'
import {
pgTable,
varchar,
timestamp
} from 'drizzle-orm/pg-core'
import { createId } from '@paralleldrive/cuid2'
export const user = pgTable(
'user',
{
id: varchar('id')
.$defaultFn(() => createId())
.primaryKey(),
username: varchar('username').notNull().unique(),
password: varchar('password').notNull(),
email: varchar('email').notNull().unique(),
salt: varchar('salt', { length: 64 }).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
}
)
export const table = {
user
} as const
export type Table = typeof table
drizzle-typebox
我们可以使用 drizzle-typebox
将 user
表转换为 TypeBox 模型:
¥We may convert the user
table into TypeBox models by using drizzle-typebox
:
ts
import { createInsertSchema } from 'drizzle-typebox'
import { table } from './database/schema'
const _createUser = createInsertSchema(table.user, {
// Replace email with Elysia's email type
email: t.String({ format: 'email' })
})
new Elysia()
.post('/sign-up', ({ body }) => {
// Create a new user
}, {
body: t.Omit(
_createUser,
['id', 'salt', 'createdAt']
)
})
这使我们能够在 Elysia 验证模型中重用数据库模式。
¥This allows us to reuse the database schema in Elysia validation models
类型实例化可能无限
¥Type instantiation is possibly infinite
如果你收到类似“类型实例化可能无限”的错误,这是由于 drizzle-typebox
和 Elysia
之间存在循环引用。
¥If you got an error like Type instantiation is possibly infinite this is because of the circular reference between drizzle-typebox
and Elysia
.
如果我们将 drizzle-typebox 中的类型嵌套到 Elysia 模式中,将导致类型实例化的无限循环。
¥If we nested a type from drizzle-typebox into Elysia schema, it will cause an infinite loop of type instantiation.
为了防止这种情况,我们需要在 drizzle-typebox
和 Elysia
模式之间显式定义一个类型:
¥To prevent this, we need to explicitly define a type between drizzle-typebox
and Elysia
schema:
ts
import { t } from 'elysia'
import { createInsertSchema } from 'drizzle-typebox'
import { table } from './database/schema'
const _createUser = createInsertSchema(table.user, {
email: t.String({ format: 'email' })
})
// ✅ This works, by referencing the type from `drizzle-typebox`
const createUser = t.Omit(
_createUser,
['id', 'salt', 'createdAt']
)
// ❌ This will cause an infinite loop of type instantiation
const createUser = t.Omit(
createInsertSchema(table.user, {
email: t.String({ format: 'email' })
}),
['id', 'salt', 'createdAt']
)
如果你想使用 Elysia 类型,请务必为 drizzle-typebox
声明一个变量并引用它。
¥Always declare a variable for drizzle-typebox
and reference it if you want to use Elysia type
实用程序
¥Utility
由于我们可能会使用 t.Pick
和 t.Omit
来排除或包含某些字段,因此重复此过程可能会很麻烦:
¥As we are likely going to use t.Pick
and t.Omit
to exclude or include certain fields, it may be cumbersome to repeat the process:
我们建议使用这些实用程序函数(按原样复制)来简化流程:
¥We recommend using these utility functions (copy as-is) to simplify the process:
ts
/**
* @lastModified 2025-02-04
* @see https://elysia.nodejs.cn/recipe/drizzle.html#utility
*/
import { Kind, type TObject } from '@sinclair/typebox'
import {
createInsertSchema,
createSelectSchema,
BuildSchema,
} from 'drizzle-typebox'
import { table } from './schema'
import type { Table } from 'drizzle-orm'
type Spread<
T extends TObject | Table,
Mode extends 'select' | 'insert' | undefined,
> =
T extends TObject<infer Fields>
? {
[K in keyof Fields]: Fields[K]
}
: T extends Table
? Mode extends 'select'
? BuildSchema<
'select',
T['_']['columns'],
undefined
>['properties']
: Mode extends 'insert'
? BuildSchema<
'insert',
T['_']['columns'],
undefined
>['properties']
: {}
: {}
/**
* Spread a Drizzle schema into a plain object
*/
export const spread = <
T extends TObject | Table,
Mode extends 'select' | 'insert' | undefined,
>(
schema: T,
mode?: Mode,
): Spread<T, Mode> => {
const newSchema: Record<string, unknown> = {}
let table
switch (mode) {
case 'insert':
case 'select':
if (Kind in schema) {
table = schema
break
}
table =
mode === 'insert'
? createInsertSchema(schema)
: createSelectSchema(schema)
break
default:
if (!(Kind in schema)) throw new Error('Expect a schema')
table = schema
}
for (const key of Object.keys(table.properties))
newSchema[key] = table.properties[key]
return newSchema as any
}
/**
* Spread a Drizzle Table into a plain object
* * If `mode` is 'insert', the schema will be refined for insert
* If `mode` is 'select', the schema will be refined for select
* If `mode` is undefined, the schema will be spread as is, models will need to be refined manually
*/
export const spreads = <
T extends Record<string, TObject | Table>,
Mode extends 'select' | 'insert' | undefined,
>(
models: T,
mode?: Mode,
): {
[K in keyof T]: Spread<T[K], Mode>
} => {
const newSchema: Record<string, unknown> = {}
const keys = Object.keys(models)
for (const key of keys) newSchema[key] = spread(models[key], mode)
return newSchema as any
}
此实用函数会将 Drizzle 模式转换为普通对象,该对象可以通过属性名称作为普通对象进行选择:
¥This utility function will convert Drizzle schema into a plain object, which can pick by property name as plain object:
ts
// ✅ Using spread utility function
const user = spread(table.user, 'insert')
const createUser = t.Object({
id: user.id, // { type: 'string' }
username: user.username, // { type: 'string' }
password: user.password // { type: 'string' }
})
// ⚠️ Using t.Pick
const _createUser = createInsertSchema(table.user)
const createUser = t.Pick(
_createUser,
['id', 'username', 'password']
)
表格单例
¥Table Singleton
我们建议使用单例模式来存储表结构,这将允许我们从代码库中的任何位置访问表结构:
¥We recommend using a singleton pattern to store the table schema, this will allow us to access the table schema from anywhere in the codebase:
ts
import { table } from './schema'
import { spreads } from './utils'
export const db = {
insert: spreads({
user: table.user,
}, 'insert'),
select: spreads({
user: table.user,
}, 'select')
} as const
这将使我们能够从代码库中的任何位置访问表模式:
¥This will allow us to access the table schema from anywhere in the codebase:
ts
import { Elysia } from 'elysia'
import { db } from './database/model'
const { user } = db.insert
new Elysia()
.post('/sign-up', ({ body }) => {
// Create a new user
}, {
body: t.Object({
id: user.username,
username: user.username,
password: user.password
})
})
细化
¥Refinement
如果需要类型优化,可以直接使用 createInsertSchema
和 createSelectSchema
来优化模式。
¥If type refinement is needed, you may use createInsertSchema
and createSelectSchema
to refine the schema directly.
ts
import { t } from 'elysia'
import { createInsertSchema, createSelectSchema } from 'drizzle-typebox'
import { table } from './schema'
import { spreads } from './utils'
export const db = {
insert: spreads({
user: createInsertSchema(table.user, {
email: t.String({ format: 'email' })
}),
}, 'insert'),
select: spreads({
user: createSelectSchema(table.user, {
email: t.String({ format: 'email' })
})
}, 'select')
} as const
在上面的代码中,我们改进了 user.email
模式,使其包含 format
属性。
¥In the code above, we refine a user.email
schema to include a format
property
spread
实用函数将跳过精炼架构,因此你可以按原样使用它。
¥The spread
utility function will skip a refined schema, so you can use it as is.
更多信息,请参阅 Drizzle ORM 和 Drizzle TypeBox 文档。
¥For more information, please refer to the Drizzle ORM and Drizzle TypeBox documentation.