Drizzle
Drizzle is a modern, type-safe TypeScript ORM designed for Node.js. It offers a concise and easy-to-use API, supports databases such as PostgreSQL, MySQL, and SQLite, and has powerful query builders, transaction processing, and database migration capabilities. At the same time, it remains lightweight and has no external dependencies, making it very suitable for database operation scenarios that require high performance and type safety.
@gqloom/drizzle
provides the integration of GQLoom and Drizzle:
- Use Drizzle Table as Silk;
- Use the resolver factory to quickly create CRUD operations from Drizzle.
Installation
Please refer to Drizzle's Getting Started Guide to install Drizzle and its corresponding database integration.
After installing Drizzle, install @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
Using Silk
We can easily use Drizzle Schemas as Silk by simply wrapping them with drizzleSilk
.
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],
}),
}))
Let's use them in the resolver. At the same time, we use the useSelectedColumns()
function to know which columns are needed for the current GraphQL query:
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) ?? [])
}),
})
As shown in the code above, we can directly use the Drizzle Table wrapped by drizzleSilk
in the resolver
. Here, we use users
as the parent type of resolver.of
, and define two queries named user
and users
and a field named posts
in the resolver. Among them:
- The return type of
user
isusers.$nullable()
, indicating thatuser
may be null; - The return type of
users
isusers.$list()
, indicating thatusers
will return a list ofusers
; - The return type of the
posts
field isposts.$list()
. In theposts
field, we use theuserList
parameter in theload
method. TypeScript will help us infer its type. Theload
method is a wrapper ofDataLoader
, allowing us to quickly define aDataLoader
method and use it to batch fetchposts
.
We also use the useSelectedColumns()
function to determine which columns need to be selected for the current GraphQL query. This function requires enabling context.
For runtimes where the useSelectedColumns()
function cannot be used, we can also use the getSelectedColumns()
function to obtain the columns that need to be selected for the current query.
Derived Fields
Adding derived Fields to a database table is quite simple. However, it's important to use the field().derivedFrom()
method to declare the columns on which the computed property depends, so that the useSelectedColumns
method can correctly select these columns:
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)}...`
}),
})
Hiding Fields
Sometimes we don't want to expose all fields of the database table to the client. Consider that we have a users
table containing a password field, where the password
field is an encrypted password, and we don't want to expose it to the client:
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(),
})
)
We can use field.hidden
in the resolver to hide the password
field:
import { field, resolver } from "@gqloom/core"
import { users } from "./schema"
export const usersResolver = resolver.of(users, {
password: field.hidden,
})
Resolver Factory
gqloom/drizzle
provides a resolver factory DrizzleResolverFactory
to easily create CRUD resolvers from Drizzle, and it also supports custom parameters and adding middleware.
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)
Relationship Fields
In Drizzle Table, we can easily create relationships. We can use the relationField
method of the resolver factory to create corresponding GraphQL fields for relationships.
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"),
})
Queries
The Drizzle resolver factory pre-defines some commonly used queries:
selectArrayQuery
: Find multiple records in the corresponding table according to the conditions.selectSingleQuery
: Find a single record in the corresponding table according to the conditions.countQuery
: Count the number of records in the corresponding table according to the conditions.
We can use the queries from the resolver factory in the resolver:
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"),
})
Mutations
The Drizzle resolver factory predefines some commonly used mutations:
insertArrayMutation
: Insert multiple records.insertSingleMutation
: Insert a single record.updateMutation
: Update records.deleteMutation
: Delete records.
We can use the mutations from the resolver factory in the resolver:
export const usersResolver = resolver.of(users, {
user: usersResolverFactory.selectSingleQuery(),
users: usersResolverFactory.selectArrayQuery(),
createUser: usersResolverFactory.insertSingleMutation(),
createUsers: usersResolverFactory.insertArrayMutation(),
posts: usersResolverFactory.relationField("posts"),
})
Custom Input
The pre-defined queries and mutations of the resolver factory support custom input. You can define the input type through the input
option:
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"),
})
In the above code, we use valibot
to define the input type. v.object({ id: v.number() })
defines the type of the input object, and v.transform(({ id }) => ({ where: eq(users.id, id) }))
converts the input parameters into Drizzle query parameters.
Adding Middleware
The pre-defined queries, mutations, and fields of the resolver factory support adding middleware. You can define middleware through the middlewares
option:
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,
})
In the above code, we use the middlewares
option to define middleware. async (next) => { ... }
defines a middleware. useAuthedUser()
is a custom function used to get the currently logged-in user. If the user is not logged in, an error is thrown; otherwise, next()
is called to continue execution.
Complete Resolver
We can directly create a complete Resolver with the resolver factory:
// Readonly Resolver
const usersQueriesResolver = usersResolverFactory.queriesResolver()
// Full Resolver
const usersResolver = usersResolverFactory.resolver()
There are two functions for creating Resolvers:
usersResolverFactory.queriesResolver()
: Creates a Resolver that only includes queries and relational fields.usersResolverFactory.resolver()
: Creates a Resolver that includes all queries, mutations, and relational fields.
Custom Type Mapping
To adapt to more Drizzle types, we can extend GQLoom to add more type mappings.
First, we use DrizzleWeaver.config
to define the configuration of type mapping. Here we import GraphQLDateTime
and GraphQLJSONObject
from graphql-scalars. When encountering date
and json
types, we map them to the corresponding GraphQL scalars.
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
}
},
})
Pass the configuration to the weave
function when weaving the GraphQL Schema:
import { weave } from "@gqloom/core"
export const schema = weave(drizzleWeaverConfig, usersResolver, postsResolver)
Default Type Mapping
The following table lists the default mapping relationships between Drizzle dataType
and GraphQL types in GQLoom:
Drizzle dataType | GraphQL Type |
---|---|
boolean | GraphQLBoolean |
number | GraphQLFloat |
json | GraphQLString |
date | GraphQLString |
bigint | GraphQLString |
string | GraphQLString |
buffer | GraphQLList |
array | GraphQLList |