Skip to content

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:

  1. 在 Drizzle 中定义数据库模式。
  2. 使用 drizzle-typebox 将 Drizzle 模式转换为 Elysia 验证模型。
  3. 使用转换后的 Elysia 验证模型来确保类型验证。
  4. OpenAPI 模式由 Elysia 验证模型生成。
  5. 添加 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-typeboxElysia 之间可能存在版本不匹配的情况,这可能会导致两个版本之间的符号冲突。

¥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-typeboxuser 表转换为 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-typeboxElysia 之间存在循环引用。

¥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-typeboxElysia 模式之间显式定义一个类型:

¥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.Pickt.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

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

¥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 ORMDrizzle TypeBox 文档。

¥For more information, please refer to the Drizzle ORM and Drizzle TypeBox documentation.