Skip to content
GQLoom

Drizzle

Drizzle 是一个现代化的、类型安全的 TypeScript ORM,专为 Node.js 设计。它提供了简洁易用的 API,支持 PostgreSQL、MySQL 和 SQLite 等数据库,具备强大的查询构建器、事务处理和数据库迁移功能,同时保持轻量级和无外部依赖的特点,非常适合需要高性能和类型安全的数据库操作场景。

@gqloom/drizzle 提供了 GQLoom 与 Drizzle 的集成:

  • 使用 Drizzle Table 作为丝线使用;
  • 使用解析器工厂从 Drizzle 快速生成 CRUD 操作。

安装

请参考 Drizzle 的入门指南安装 Drizzle 与对应的数据库集成。

在完成 Drizzle 安装后,安装 @gqloom/drizzle

sh
npm i @gqloom/core @drizzle-orm@beta @gqloom/drizzle@beta
sh
pnpm add @gqloom/core @drizzle-orm@beta @gqloom/drizzle@beta
sh
yarn add @gqloom/core @drizzle-orm@beta @gqloom/drizzle@beta
sh
bun add @gqloom/core @drizzle-orm@beta @gqloom/drizzle@beta

使用丝线

只需要使用 drizzleSilk 包裹 Drizzle Table,我们就可以轻松地将它们作为丝线使用。

ts
import { 
drizzleSilk
} from "@gqloom/drizzle"
import {
relations
} from "drizzle-orm"
import * as
t
from "drizzle-orm/sqlite-core"
export const
users
=
drizzleSilk
(
t
.
sqliteTable
("users", {
id
:
t
.
int
().
primaryKey
({
autoIncrement
: true }),
name
:
t
.
text
().
notNull
(),
age
:
t
.
int
(),
email
:
t
.
text
(),
password
:
t
.
text
(),
}) ) export const
usersRelations
=
relations
(
users
, ({
many
}) => ({
posts
:
many
(
posts
),
})) export const
posts
=
drizzleSilk
(
t
.
sqliteTable
("posts", {
id
:
t
.
int
().
primaryKey
({
autoIncrement
: true }),
title
:
t
.
text
().
notNull
(),
content
:
t
.
text
(),
authorId
:
t
.
int
().
references
(() =>
users
.
id
, {
onDelete
: "cascade" }),
}) ) export const
postsRelations
=
relations
(
posts
, ({
one
}) => ({
author
:
one
(
users
, {
fields
: [
posts
.
authorId
],
references
: [
users
.
id
],
}), }))

让我们在解析器中使用它们,同时我们使用 useSelectedColumns() 函数以得知当前 GraphQL 查询需要选取哪些列:

ts
import { 
field
,
query
,
resolver
} from "@gqloom/core"
import {
useSelectedColumns
} from "@gqloom/drizzle/context"
import {
eq
,
inArray
} from "drizzle-orm"
import {
drizzle
} from "drizzle-orm/libsql"
import * as
v
from "valibot"
import * as
schema
from "./schema"
import {
posts
,
users
} from "./schema"
const
db
=
drizzle
({
schema
,
connection
: {
url
:
process
.
env
.
DB_FILE_NAME
! },
}) export const
usersResolver
=
resolver
.
of
(
users
, {
user
:
query
.
output
(
users
.
$nullable
())
.
input
({
id
:
v
.
number
() })
.
resolve
(({
id
}) => {
return
db
.
select
(
useSelectedColumns
(
users
))
.
from
(
users
)
.
where
(
eq
(
users
.
id
,
id
))
.
get
()
}),
users
:
query
.
output
(
users
.
$list
()).
resolve
(() => {
return
db
.
select
(
useSelectedColumns
(
users
)).
from
(
users
).
all
()
}),
posts
:
field
.
output
(
posts
.
$list
())
.
derivedFrom
("id")
.
load
(async (
userList
) => {
const
postList
= await
db
.
select
()
.
from
(
posts
)
.
where
(
inArray
(
users
.
id
,
userList
.
map
((
user
) =>
user
.
id
)
) ) const
groups
= new
Map
<number, (typeof
posts
.
$inferSelect
)[]>()
for (const
post
of
postList
) {
const
key
=
post
.
authorId
if (
key
== null) continue
groups
.
set
(
key
, [...(
groups
.
get
(
key
) ?? []),
post
])
} return
userList
.
map
((
user
) =>
groups
.
get
(
user
.
id
) ?? [])
}), })

如上面的代码所示,我们可以直接在 resolver 里使用 drizzleSilk 包裹的 Drizzle Table。
在这里我们使用了 users 作为 resolver.of 的父类型,并在 resolver 中定义了 userusers 两个查询和一个名为 posts 的字段。其中:

  • user 的返回类型是 users.$nullable(),表示 user 可能为空;
  • users 的返回类型是 users.$list(),表示 users 将返回一个 users 的列表;
  • posts 字段的返回类型是 posts.$list(),在 posts 字段中,我们在 load 方法中使用了 userList 参数,TypeScript 将帮助我们推断其类型,load 方法是对 DataLoader 的封装,允许我们快速定义一个 DataLoader 方法,并使用它来批量获取 posts

我们还使用 useSelectedColumns() 函数来知道当前 GraphQL 查询需要选取哪些列。这个函数需要启用上下文
对于无法使用 useSelectedColumns() 函数的运行时,我们也可以使用 getSelectedColumns() 函数来获取当前查询需要选取的列。

派生字段

为数据库表添加派生字段非常简单,但需要注意使用 field().derivedFrom() 方法声明所依赖的列,以便 useSelectedColumns 方法能正确地选取这些列:

ts
import { 
field
,
resolver
} from "@gqloom/core"
import * as
v
from "valibot"
import {
posts
} from "./schema"
export const
postsResolver
=
resolver
.
of
(
posts
, {
abstract
:
field
(
v
.
string
())
.
derivedFrom
("title", "content")
.
resolve
((
post
) => {
return `${
post
.
title
} ${
post
.
content
?.
slice
(0, 60)}...`
}), })

隐藏字段

有时候我们并不想把数据库表格的所有字段都暴露给客户端。
考虑我们有一张包含密码字段的 users 表,其中 password 字段是加密后的密码,我们不希望把它暴露给客户端:

ts
import { 
drizzleSilk
} from "@gqloom/drizzle"
import * as
t
from "drizzle-orm/sqlite-core"
export const
users
=
drizzleSilk
(
t
.
sqliteTable
("users", {
id
:
t
.
int
().
primaryKey
({
autoIncrement
: true }),
name
:
t
.
text
().
notNull
(),
age
:
t
.
int
(),
email
:
t
.
text
(),
password
:
t
.
text
(),
}) )

我们可以在解析器中使用 field.hidden 来隐藏 password 字段:

ts
import { 
field
,
resolver
} from "@gqloom/core"
import {
users
} from "./schema"
export const
usersResolver
=
resolver
.
of
(
users
, {
password
:
field
.
hidden
,
})

解析器工厂

gqloom/drizzle 提供了解析器工厂 DrizzleResolverFactory,用于从 Drizzle 轻松地生成 CRUD 解析器,同时支持自定参数和添加中间件。

ts
import { drizzleResolverFactory } from "@gqloom/drizzle"
import { drizzle } from "drizzle-orm/libsql"
import { users } from "./schema"

const db = drizzle({
  connection: { url: process.env.DB_FILE_NAME! },
})

const usersResolverFactory = drizzleResolverFactory(db, users)

关系字段

在 Drizzle Table 中,我们可以轻松地创建关系,使用解析器工厂的 relationField 方法可以为关系创建对应的 GraphQL 字段。

ts
import { 
query
,
resolver
} from "@gqloom/core"
import {
drizzleResolverFactory
} from "@gqloom/drizzle"
import {
eq
,
inArray
} from "drizzle-orm"
import {
drizzle
} from "drizzle-orm/libsql"
import * as
v
from "valibot"
import * as
schema
from "./schema"
import {
users
} from "./schema"
const
db
=
drizzle
({
schema
,
connection
: {
url
:
process
.
env
.
DB_FILE_NAME
! },
}) const
usersResolverFactory
=
drizzleResolverFactory
(
db
,
users
)
const
usePostsLoader
=
createMemoization
(
() => new
EasyDataLoader
<
{
id
: number },
(typeof
posts
.
$inferSelect
)[]
>(async (
userList
) => {
const
postList
= await
db
.
select
()
.
from
(
posts
)
.
where
(
inArray
(
users
.
id
,
userList
.
map
((
user
) =>
user
.
id
)
) ) const
groups
= new
Map
<number, (typeof
posts
.
$inferSelect
)[]>()
for (const
post
of
postList
) {
const
key
=
post
.
authorId
if (
key
== null) continue
groups
.
set
(
key
, [...(
groups
.
get
(
key
) ?? []),
post
])
} return
userList
.
map
((
user
) =>
groups
.
get
(
user
.
id
) ?? [])
}) ) export const
usersResolver
=
resolver
.
of
(
users
, {
user
:
query
.
output
(
users
.
$nullable
())
.
input
({
id
:
v
.
number
() })
.
resolve
(({
id
}) => {
return
db
.
select
().
from
(
users
).
where
(
eq
(
users
.
id
,
id
)).
get
()
}),
users
:
query
.
output
(
users
.
$list
()).
resolve
(() => {
return
db
.
select
().
from
(
users
).
all
()
}),
posts_
:
field
.
output
(
posts
.
$list
())
.
derivedFrom
('id')
.
resolve
((
user
) => {
return
usePostsLoader
().
load
(
user
)
}),
posts
:
usersResolverFactory
.
relationField
("posts"),
})

查询

Drizzle 解析器工厂预置了一些常用的查询:

  • selectArrayQuery: 根据条件查找对应表的多条记录
  • selectSingleQuery: 根据条件查找对应表的一条记录
  • countQuery: 根据条件统计对应表的记录数量

我们可以在解析器内使用来自解析器工厂的查询:

ts
export const 
usersResolver
=
resolver
.
of
(
users
, {
user_
:
query
.
output
(
users
.
$nullable
())
.
input
({
id
:
v
.
number
() })
.
resolve
(({
id
}) => {
return
db
.
select
().
from
(
users
).
where
(
eq
(
users
.
id
,
id
)).
get
()
}),
user
:
usersResolverFactory
.
selectSingleQuery
(),
users_
:
query
.
output
(
users
.
$list
()).
resolve
(() => {
return
db
.
select
().
from
(
users
).
all
()
}),
users
:
usersResolverFactory
.
selectArrayQuery
(),
posts
:
usersResolverFactory
.
relationField
("posts"),
})

变更

Drizzle 解析器工厂预置了一些常用的变更:

  • insertArrayMutation: 插入多条记录
  • insertSingleMutation: 插入一条记录
  • updateMutation: 更新记录
  • deleteMutation: 删除记录

我们可以在解析器内使用来自解析器工厂的变更:

ts
export const 
usersResolver
=
resolver
.
of
(
users
, {
user
:
usersResolverFactory
.
selectSingleQuery
(),
users
:
usersResolverFactory
.
selectArrayQuery
(),
createUser
:
usersResolverFactory
.
insertSingleMutation
(),
createUsers
:
usersResolverFactory
.
insertArrayMutation
(),
posts
:
usersResolverFactory
.
relationField
("posts"),
})

自定义输入

解析器工厂预置的查询和变更支持自定义输入,你可以通过 input 选项来定义输入类型:

ts
export const 
usersResolver
=
resolver
.
of
(
users
, {
user
:
usersResolverFactory
.
selectSingleQuery
().
input
(
v
.
pipe
(
v
.
object
({
id
:
v
.
number
() }),
v
.
transform
(({
id
}) => ({
where
:
eq
(
users
.
id
,
id
) }))
) ),
users
:
usersResolverFactory
.
selectArrayQuery
(),
posts
:
usersResolverFactory
.
relationField
("posts"),
})

在上面的代码中,我们使用 valibot 来定义输入类型, v.object({ id: v.number() }) 定义了输入对象的类型,v.transform(({ id }) => ({ where: eq(users.id, id) })) 将输入参数转换为 Drizzle 的查询参数。

添加中间件

解析器工厂预置的查询、变更和字段支持添加中间件,你可以通过 middlewares 选项来定义中间件:

ts
const 
postResolver
=
resolver
.
of
(
posts
, {
createPost
:
postsResolverFactory
.
insertSingleMutation
().
use
(async (
next
) => {
const
user
= await
useAuthedUser
()
if (
user
== null) throw new
GraphQLError
("Please login first")
return
next
()
}),
author
:
postsResolverFactory
.
relationField
("author"),
authorId
:
field
.
hidden
,
})

在上面的代码中,我们使用 middlewares 选项来定义中间件,async (next) => { ... } 定义了一个中间件,useAuthedUser() 是一个自定义的函数,用于获取当前登录的用户,如果用户未登录,则抛出一个错误,否则调用 next() 继续执行。

完整解析器

我们可以用解析器工厂直接创建一个完整的 Resolver:

ts
// Readonly Resolver
const 
usersQueriesResolver
=
usersResolverFactory
.
queriesResolver
()
// Full Resolver const
usersResolver
=
usersResolverFactory
.
resolver
()

有两个用于创建解析器的方法:

  • usersResolverFactory.queriesResolver(): 创建一个只包含查询、关系字段的解析器。
  • usersResolverFactory.resolver(): 创建一个包含所有查询、变更和关系字段的解析器。

自定义类型映射

为了适应更多的 Drizzle 类型,我们可以拓展 GQLoom 为其添加更多的类型映射。

首先我们使用 DrizzleWeaver.config 来定义类型映射的配置。这里我们导入来自 graphql-scalarsGraphQLDateTimeGraphQLJSONObject ,当遇到 datejson 类型时,我们将其映射到对应的 GraphQL 标量。

ts
import { 
GraphQLDateTime
,
GraphQLJSON
} from "graphql-scalars"
import {
DrizzleWeaver
} from "@gqloom/drizzle"
const
drizzleWeaverConfig
=
DrizzleWeaver
.
config
({
presetGraphQLType
: (
column
) => {
if (
column
.
dataType
=== "date") {
return
GraphQLDateTime
} if (
column
.
dataType
=== "json") {
return
GraphQLJSON
} }, })

在编织 GraphQL Schema 时传入配置到 weave 函数中:

ts
import { weave } from "@gqloom/core"

export const schema = weave(drizzleWeaverConfig, usersResolver, postsResolver)

默认类型映射

下表列出了 GQLoom 中 Drizzle dataType 与 GraphQL 类型之间的默认映射关系:

Drizzle dataTypeGraphQL 类型
booleanGraphQLBoolean
numberGraphQLFloat
jsonGraphQLString
dateGraphQLString
bigintGraphQLString
stringGraphQLString
bufferGraphQLList
arrayGraphQLList