Drizzle
Drizzle 是一个现代化的、类型安全的 TypeScript ORM,专为 Node.js 设计。它提供了简洁易用的 API,支持 PostgreSQL、MySQL 和 SQLite 等数据库,具备强大的查询构建器、事务处理和数据库迁移功能,同时保持轻量级和无外部依赖的特点,非常适合需要高性能和类型安全的数据库操作场景。
@gqloom/drizzle
提供了 GQLoom 与 Drizzle 的集成:
- 使用 Drizzle Table 作为丝线使用;
- 使用解析器工厂从 Drizzle 快速生成 CRUD 操作。
安装
请参考 Drizzle 的入门指南安装 Drizzle 与对应的数据库集成。
在完成 Drizzle 安装后,安装 @gqloom/drizzle
:
npm i @gqloom/core @drizzle-orm@beta @gqloom/drizzle@beta
pnpm add @gqloom/core @drizzle-orm@beta @gqloom/drizzle@beta
yarn add @gqloom/core @drizzle-orm@beta @gqloom/drizzle@beta
bun add @gqloom/core @drizzle-orm@beta @gqloom/drizzle@beta
使用丝线
只需要使用 drizzleSilk
包裹 Drizzle Table,我们就可以轻松地将它们作为丝线使用。
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 查询需要选取哪些列:
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 中定义了 user
、users
两个查询和一个名为 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
方法能正确地选取这些列:
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
字段是加密后的密码,我们不希望把它暴露给客户端:
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
字段:
import { field, resolver } from "@gqloom/core"
import { users } from "./schema"
export const usersResolver = resolver.of(users, {
password: field.hidden,
})
解析器工厂
gqloom/drizzle
提供了解析器工厂 DrizzleResolverFactory
,用于从 Drizzle 轻松地生成 CRUD 解析器,同时支持自定参数和添加中间件。
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 字段。
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
: 根据条件统计对应表的记录数量
我们可以在解析器内使用来自解析器工厂的查询:
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
: 删除记录
我们可以在解析器内使用来自解析器工厂的变更:
export const usersResolver = resolver.of(users, {
user: usersResolverFactory.selectSingleQuery(),
users: usersResolverFactory.selectArrayQuery(),
createUser: usersResolverFactory.insertSingleMutation(),
createUsers: usersResolverFactory.insertArrayMutation(),
posts: usersResolverFactory.relationField("posts"),
})
自定义输入
解析器工厂预置的查询和变更支持自定义输入,你可以通过 input
选项来定义输入类型:
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
选项来定义中间件:
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:
// Readonly Resolver
const usersQueriesResolver = usersResolverFactory.queriesResolver()
// Full Resolver
const usersResolver = usersResolverFactory.resolver()
有两个用于创建解析器的方法:
usersResolverFactory.queriesResolver()
: 创建一个只包含查询、关系字段的解析器。usersResolverFactory.resolver()
: 创建一个包含所有查询、变更和关系字段的解析器。
自定义类型映射
为了适应更多的 Drizzle 类型,我们可以拓展 GQLoom 为其添加更多的类型映射。
首先我们使用 DrizzleWeaver.config
来定义类型映射的配置。这里我们导入来自 graphql-scalars 的 GraphQLDateTime
和 GraphQLJSONObject
,当遇到 date
和 json
类型时,我们将其映射到对应的 GraphQL 标量。
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 函数中:
import { weave } from "@gqloom/core"
export const schema = weave(drizzleWeaverConfig, usersResolver, postsResolver)
默认类型映射
下表列出了 GQLoom 中 Drizzle dataType
与 GraphQL 类型之间的默认映射关系:
Drizzle dataType | GraphQL 类型 |
---|---|
boolean | GraphQLBoolean |
number | GraphQLFloat |
json | GraphQLString |
date | GraphQLString |
bigint | GraphQLString |
string | GraphQLString |
buffer | GraphQLList |
array | GraphQLList |