Skip to content
Our Sponsors
Open in Anthropic

title: 与 Drizzle 集成 - ElysiaJS head:

    • meta
    • property: 'og:title' content: 与 Drizzle 集成 - ElysiaJS
    • meta
    • name: 'description' content: 我们可以使用 Drizzle 通过 drizzle-typebox 创建从数据库到验证再到前端的端到端类型安全
    • meta
    • name: 'og:description' content: 我们可以使用 Drizzle 通过 drizzle-typebox 创建从数据库到验证再到前端的端到端类型安全

Drizzle

Drizzle ORM 是一个无头 TypeScript ORM,专注于类型安全和开发者体验。

我们可以使用 drizzle-typebox 将 Drizzle schema 转换为 Elysia 验证模型

Drizzle Typebox

Elysia.t 是 TypeBox 的一个分支,允许我们直接在 Elysia 中使用任何 TypeBox 类型。

我们可以使用 "drizzle-typebox" 将 Drizzle schema 转换为 TypeBox schema,并直接在 Elysia 的 schema 验证中使用。

工作原理如下:

  1. 在 Drizzle 中定义数据库 schema。
  2. 使用 drizzle-typebox 将 Drizzle schema 转换为 Elysia 验证模型。
  3. 使用转换后的 Elysia 验证模型确保类型验证。
  4. 从 Elysia 验证模型生成 OpenAPI schema。
  5. 添加 Eden Treaty 为你的前端添加类型安全。
                                                    * ——————————————— *
                                                    |                 |
                                               | -> |    文档     |
* ————————— *             * ———————— * OpenAPI |    |                 |
|           |   drizzle-  |          | ——————— |    * ——————————————— *
|  Drizzle  | —————————-> |  Elysia  |
|           |  -typebox   |          | ——————— |    * ——————————————— *
* ————————— *             * ———————— *   Eden  |    |                 |
                                               | -> |   前端代码   |
												    |                 |
												    * ——————————————— *

安装

要安装 Drizzle,请运行以下命令:

bash
bun add drizzle-orm drizzle-typebox

然后你需要固定 @sinclair/typebox 的版本,因为 drizzle-typeboxElysia 之间可能存在版本不匹配,这可能会导致两个版本之间的 Symbols 冲突。

我们建议使用以下命令将 @sinclair/typebox 的版本固定到 elysia 使用的最低版本

bash
grep "@sinclair/typebox" node_modules/elysia/package.json

我们可以在 package.json 中使用 overrides 字段来固定 @sinclair/typebox 的版本:

json
{
  "overrides": {
  	"@sinclair/typebox": "0.32.4"
  }
}

Drizzle schema

假设我们的代码库中有一个 user 表,如下所示:

ts
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-typeboxuser 表转换为 TypeBox 模型:

ts
import { t } from 'elysia'
import { createInsertSchema } from 'drizzle-typebox'
import { table } from './database/schema'

const _createUser = createInsertSchema(table.user, {
	// 用 Elysia 的 email 类型替换 email
	email: t.String({ format: 'email' })
})

new Elysia()
	.post('/sign-up', ({ body }) => {
		// 创建新用户
	}, {
		body: t.Omit(
			_createUser,
			['id', 'salt', 'createdAt']
		)
	})

这允许我们在 Elysia 验证模型中重用数据库 schema

类型实例化可能无限

如果你遇到类型实例化可能无限这样的错误,这是因为 drizzle-typeboxElysia 之间的循环引用。

如果我们将 drizzle-typebox 的类型嵌套到 Elysia schema 中,会导致类型实例化的无限循环。

为了防止这种情况,我们需要drizzle-typeboxElysia 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' })
})

// ✅ 这样可以工作,通过引用 `drizzle-typebox` 的类型
const createUser = t.Omit(
	_createUser,
	['id', 'salt', 'createdAt']
)

// ❌ 这将导致类型实例化的无限循环
const createUser = t.Omit(
	createInsertSchema(table.user, {
		email: t.String({ format: 'email' })
	}),
	['id', 'salt', 'createdAt']
)

如果你想使用 Elysia 类型,总是为 drizzle-typebox 声明一个变量并引用它

工具函数

由于我们可能会使用 t.Pickt.Omit 来排除或包含某些字段,重复这个过程可能会很繁琐:

我们建议使用这些工具函数**(按原样复制)**来简化过程:

ts
/**
 * @lastModified 2025-02-04
 * @see https://elysiajs.com/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']
                : {}
          : {}

/**
 * 将 Drizzle schema 展开为普通对象
 */
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
}

/**
 * 将 Drizzle Table 展开为普通对象
 *
 * 如果 `mode` 是 'insert',schema 将为插入进行优化
 * 如果 `mode` 是 'select',schema 将为选择进行优化
 * 如果 `mode` 是 undefined,schema 将按原样展开,模型需要手动优化
 */
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 schema 转换为普通对象,可以按属性名作为普通对象进行选择:

ts
// ✅ 使用 spread 工具函数
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' }
})

// ⚠️ 使用 t.Pick
const _createUser = createInsertSchema(table.user)

const createUser = t.Pick(
	_createUser,
	['id', 'username', 'password']
)

表单例模式

我们建议使用单例模式来存储表 schema,这将允许我们从代码库的任何位置访问表 schema:

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

这将允许我们从代码库的任何位置访问表 schema:

ts
import { Elysia, t } from 'elysia'
import { db } from './database/model'

const { user } = db.insert

new Elysia()
	.post('/sign-up', ({ body }) => {
		// 创建新用户
	}, {
		body: t.Object({
			id: user.username,
			username: user.username,
			password: user.password
		})
	})

优化

如果需要类型优化,你可以使用 createInsertSchemacreateSelectSchema 来直接优化 schema。

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 schema 以包含 format 属性

spread 工具函数会跳过已优化的 schema,因此你可以按原样使用它。


更多信息,请参考 Drizzle ORMDrizzle TypeBox 文档。