GQLoom

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

使用丝线

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

schema.ts
import {  } from "@gqloom/drizzle"
import {  } from "drizzle-orm"
import * as  from "drizzle-orm/sqlite-core"

export const  = (
  .("users", {
    : .().({ : true }),
    : .().(),
    : .(),
    : .(),
    : .(),
  })
)

export const  = (, ({  }) => ({
  : (),
}))

export const  = (
  .("posts", {
    : .().({ : true }),
    : .().(),
    : .(),
    : .().(() => ., { : "cascade" }),
  })
)

export const  = (, ({  }) => ({
  : (, {
    : [.],
    : [.],
  }),
}))

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

resolver.ts
import { , ,  } from "@gqloom/core"
import {  } from "@gqloom/drizzle/context"
import { ,  } from "drizzle-orm"
import {  } from "drizzle-orm/libsql"
import * as  from "valibot"
import * as  from "./schema"
import { ,  } from "./schema"

const  = ({
  ,
  : { : ..! },
})

export const  = .(, {
  : 
    .(.())
    .({ : .() })
    .(({  }) => {
      return 
        .(())
        .()
        .((., ))
        .()
    }),

  : .(.()).(() => {
    return .(()).().()
  }),

  : 
    .(.())
    .("id")
    .(async () => {
      const  = await 
        .()
        .()
        .(
          (
            .,
            .(() => .)
          )
        )
      const  = new <number, (typeof .)[]>()

      for (const  of ) {
        const  = .
        if ( == null) continue
        .(, [...(.() ?? []), ])
      }
      return .(() => .(.) ?? [])
    }),
})

如上面的代码所示,我们可以直接在 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 方法能正确地选取这些列:

schema.ts
import { ,  } from "@gqloom/core"
import * as  from "valibot"
import {  } from "./schema"

export const  = .(, {
  : (.())
    .("title", "content")
    .(() => {
      return `${.} ${.?.(0, 60)}...`
    }),
})

隐藏字段

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

schema.ts
import {  } from "@gqloom/drizzle"
import * as  from "drizzle-orm/sqlite-core"

export const  = (
  .("users", {
    : .().({ : true }),
    : .().(),
    : .(),
    : .(),
    : .(),
  })
)

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

resolver.ts
import { ,  } from "@gqloom/core"
import {  } from "./schema"

export const  = .(, {
  : .,
})

解析器工厂

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

resolver.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 字段。

resolver.ts
import { ,  } from "@gqloom/core"
import {  } from "@gqloom/drizzle"
import { ,  } from "drizzle-orm"
import {  } from "drizzle-orm/libsql"
import * as  from "valibot"
import * as  from "./schema"
import {  } from "./schema"

const  = ({
  ,
  : { : ..! },
})

const  = (, )

const  = ( 
  () =>
    new <
      { : number }, 
      (typeof .)[] 
    >(async () => { 
      const  = await  
        .() 
        .() 
        .( 
          ( 
            ., 
            .(() => .) 
          ) 
        ) 
      const  = new <number, (typeof .)[]>() 
      for (const  of ) { 
        const  = . 
        if ( == null) continue
        .(, [...(.() ?? []), ]) 
      } 
      return .(() => .(.) ?? []) 
    }) 
) 
 
export const  = .(, {
  : 
    .(.())
    .({ : .() })
    .(({  }) => {
      return .().().((., )).()
    }),

  : .(.()).(() => {
    return .().().()
  }),

  : .(.()) 
    .('id') 
    .(() => { 
      return ().() 
    }), 

  : .("posts"), 
})

查询

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

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

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

export const  = .(, {
  :  
    .(.()) 
    .({ : .() }) 
    .(({  }) => { 
      return .().().((., )).() 
    }), 

  : .(), 

  : .(.()).(() => { 
    return .().().() 
  }), 

  : .(), 

  : .("posts"), 
})

变更

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

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

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

export const  = .(, {
  : .(),

  : .(),

  : .(), 

  : .(), 

  : .("posts"),
})

自定义输入

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

export const  = .(, {
  : .().(
    .( 
      .({ : .() }), 
      .(({  }) => ({ : (., ) })) 
    ) 
  ),

  : .(),

  : .("posts"),
})

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

添加中间件

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

const  = .(, {
  : .().(async () => { 
    const  = await () 
    if ( == null) throw new ("Please login first") 
    return () 
  }), 

  : .("author"),

  : .,
})

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

完整解析器

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

// Readonly Resolver
const  = .()

// Full Resolver
const  = .()

有两个用于创建 Resolver 的函数:

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

自定义类型映射

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

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

import { ,  } from "graphql-scalars"
import {  } from "@gqloom/drizzle"

const  = .({
  : () => {
    if (. === "date") {
      return 
    }
    if (. === "json") {
      return 
    }
  },
})

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

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

目录