利用 monorepo 在项目中共享 drizzle schema 定义和类型

我想要前后端可以共享数据库 schema 的类型,这样的话,在更改数据库 schema 后。我们不需要修改类型文件,直接使用 drizzle 生成的类型即可。本文我们就一起看看如何实现这个功能吧!

首先我们要准备一个 momorepo 的环境,目前使用 pnpm 可以很方便的实现。

初始化 monorepo 环境

创建一个 pnpm-workspace.yaml文件,配置如下:

yaml
packages:
  - 'packages/*'
  - 'apps/*'

创建 shared 包用来放置前后端公用的代码

我在 packages 中放置可以复用的包。我的数据库schema文件放在 packages/shared 中。并使用 tsdown 来作为打包工具使用。

tsdown.config.tsts
import { defineConfig } from 'tsdown'

export default defineConfig({
    entry: ['./src/index.ts'],
    dts: true,
    format: ['esm'],
    outDir: 'dist',
    shims: true,
    target: 'es2020',
    platform: 'browser',
    external: [
        'node:*',
    ],
})

创建数据库 schema 文件并导出

接下来我们就可以在 src/schame 中写我们的 drizzle 了,我这里就用一个user的schema来做示例

我们这里规定所有schema文件都要以 entity.ts 结尾,方面后面使用

timestamps内容如下

我们几乎每个表都会有创建时间和修改时间,抽离出来方便复用

schema/global.tsts
import { timestamp } from 'drizzle-orm/pg-core';

export const timestamps = {
    createdAt: timestamp('created_at').notNull().defaultNow(),
    updatedAt: timestamp('updated_at').notNull().defaultNow(),
};

我们把它们都导出

schema/index.tsts
import { user } from './user.entity'

export * from './user.entity'

// 这里组装好并导出方便在API端直接使用
export const schema = {
    user
}
src/index.tsts
export * from './schema' // 导出schema的配置和类型,供其他包使用

然后就可以构建使用了,在开发环境我们可以使用刚才编写好的 dev 脚本(它会使用 tsdown --watch 监听文件的变化实时编译),打包时使用 build脚本。

在这个monorepo仓库中的其他包如果要使用,就使用 pnpm add --workspace @labs/shared 安装,安装后就可以导入使用啦。

在其他环境中使用

我这里有一个fetch的包用来放置所有的API请求文件,使用tanstack-query作为请求库。一个API包用来创建所有接口。我们分别在两个包中安装刚才创建好的 @labs/shared 包。

我这里就展示一些重要的代码,具体代码就不展示了。在API端,我们需要根据导出的schema来创建连接。在fetch包中,我们需要导出的类型来进行请求或者响应的类型约束,方便前端对接时使用。

在 API 端使用

apps/api/drizzle.config.tsts
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
    schema: '../../packages/shared/src/schema/*.entity.ts',
    out: './drizzle/migrations',
    dialect: 'postgresql',
    dbCredentials: {
        url: process.env.DATABASE_URL!,
    },
});

创建drizzle migrate需要的文件

apps/api/drizzle/index.tsts
import { schema } from '@labs/shared'
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';

import env from '@/utils/env'

const connectionString = env.database.DATABASE_URL;
const pool = new Pool({ connectionString });

const db = drizzle(pool, { schema })

export default db

这样就可以使用了,我们可以使用 npx drizzle-kit generate生成数据库迁移文件,并使用 npx drizzle-kit migrate 来进行数据库迁移。其余增删改查的操作就和以前一样,只不过schema相关的文件记得要在 @labs/shared 中导入。

pnpm monorepo 和 ts 结合起来的话编辑器会自动提示,这个还是很方便的。

在 fetch 包中使用

提醒

这里抽离fetch包的作用是如果你有两个或者几个不同的端,可以同时使用这个包进行对API的请求。同事们或者你自己使用的时候可以只关注请求那个接口,请求的过程和处理的过程会在这个包中完成。

这样做的好处

我们把数据库schema和它对应的生成的类型都抽离到 packages/shared 中,如果要用到相关的类型就直接用它生成的就好了,没必要再重新生成一份。到时候其他的类型相关的包也可以放到这里,在这里类型修改后,前后端使用到的地方都会进行类型校验并进行错误提示。

请求相关的包都要抽离到 packages/fetch 包中,如果哪个地方要请求API,你就可以使用这个包了,所有API端暴露出来的接口都会在这里使用tanstack query操作一次返回,你可以不用管请求的细节,只关注传参和结果即可。而且使用tanstack query,他会自动生成响应式数据,有请求状态的变化和接口缓存,之前手动封装的许多包就可以废弃掉了。

因为这些包都有ts的类型限制,所以在API响应改变或者请求改变后,这里改了后用的地方都会有类型不一致的提示,配合构建前进行类型校验,我们可以方便的找出错误,避免在API改变后忘记更改前端请求参数或者响应造成的错误。

总结

有了这套东西后,如果你的公司使用node作为BFF层或者你开一个新的node项目,可以推广这一种方法。我们可以把几种东西都分开,可以践行关注点分离的思想。而且有完备的类型提示和校验。

或许你有什么别的想法?欢迎在下方留言一起讨论!

Drizzle ORM 使用
【翻译】使用 Nuxt Layers 进行领域驱动设计