---
title: Context
icon: Shuffle
file: ./content/docs/context.mdx
---
In the Node.js world, Context allows us to share data and state within the same request. In GraphQL, contexts allow data to be shared between multiple [resolver functions](./resolver) and [middleware](./middleware) for the same request.
A common use case is to store the identity of the current visitor in the context to be accessed in the resolver function and middleware.
## Accessing Contexts
In `GQLoom`, we access the context through the `useContext()` function.
GQLoom's `useContext` function is designed to reference [React](https://zh-hans.react.dev/)'s `useContext` function.
You can call the `useContext` function from anywhere within the [resolver](./resolver) to access the context of the current request without explicitly passing the `context` function.
Behind the scenes, `useContext` uses Node.js' [AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage) to pass the context implicitly.
### Enabling Context
We enable context by passing `asyncContextProvider` to the `weave` function. `asyncContextProvider` is essentially a global middleware.
```ts
import { weave } from "@gqloom/core"
import { asyncContextProvider } from "@gqloom/core/context" // [!code hl]
const schema = weave(ValibotWeaver, asyncContextProvider, ...resolvers)
```
Next, let's try to access the context in various places.
We will use [graphql-yoga](https://the-guild.dev/graphql/yoga-server) as an adapter.
### Accessing contexts in resolve functions
```ts twoslash
import { query, resolver, weave } from "@gqloom/core"
import { useContext } from "@gqloom/core/context"
import { ValibotWeaver } from "@gqloom/valibot"
import * as v from "valibot"
import { type YogaInitialContext, createYoga } from "graphql-yoga"
import { createServer } from "http"
async function useUser() {
const authorization =
useContext().request.headers.get("Authorization")
const user = await UserService.getUserByAuthorization(authorization)
return user
}
const helloResolver = resolver({
hello: query(v.string(), {
input: {
name: v.pipeAsync(
v.nullish(v.string()),
v.transformAsync(async (value) => { // [!code hl]
if (value != null) return value // [!code hl]
const user = await useUser() // [!code hl]
return user.name // [!code hl]
}) // [!code hl]
),
},
resolve: ({ name }) => `Hello, ${name}`,
}),
})
const yoga = createYoga({ schema: weave(ValibotWeaver, helloResolver) })
createServer(yoga).listen(4000, () => {
console.info("Server is running on http://localhost:4000/graphql")
})
```
In the code above, we use `useUser()` in `v.transformAsync` to get the user information in the context and return it as the value of `name`.
We can customize the validation or transformation in `zod` and access the context directly within it:
```ts twoslash
import { query, resolver, weave } from "@gqloom/core"
import { useContext } from "@gqloom/core/context"
import { ZodWeaver } from "@gqloom/zod"
import { z } from "zod"
import { type YogaInitialContext, createYoga } from "graphql-yoga"
import { createServer } from "http"
async function useUser() {
const authorization =
useContext().request.headers.get("Authorization")
const user = await UserService.getUserByAuthorization(authorization)
return user
}
const helloResolver = resolver({
hello: query(z.string(), {
input: {
name: z
.string()
.nullish()
.transform(async (value) => { // [!code hl]
if (value != null) return value // [!code hl]
const user = await useUser() // [!code hl]
return user.name // [!code hl]
}), // [!code hl]
},
resolve: ({ name }) => `Hello, ${name}`,
}),
})
const yoga = createYoga({ schema: weave(ZodWeaver, helloResolver) })
createServer(yoga).listen(4000, () => {
console.info("Server is running on http://localhost:4000/graphql")
})
```
In the code above, we use `useUser()` in `z.transform` to get the user information in the context and return it as the value of `name`.
## Memorization
Consider that we access the user through the following custom function:
```ts twoslash
import { useContext } from "@gqloom/core/context"
import { type YogaInitialContext } from "graphql-yoga"
async function useUser() {
const authorization =
useContext().request.headers.get("Authorization")
const user = await UserService.getUserByAuthorization(authorization)
return user
}
```
We may execute some expensive operations in `useUser()`, such as fetching user information from the database, and we may also call it multiple times in the same request.
To avoid the extra overhead of multiple calls, we can use memoization to cache the results and reuse them in subsequent calls.
In GQLoom, we use the `createMemoization` function to create a memoized function.
A memoization function caches its results in the context after the first call and returns the cached results directly in subsequent calls.
That is, the memoized function will only be executed once in the same request, no matter how many times it is called.
Let's memoize the `useUser()` function:
```ts twoslash
import { createMemoization, useContext } from "@gqloom/core/context"
const useUser = createMemoization(async () => {
const authorization =
useContext().request.headers.get("Authorization")
const user = await UserService.getUserByAuthorization(authorization)
return user
})
```
As you can see, we simply wrap the function in the `createMemoization` function.
We can then call `useUser()` from anywhere within the resolver without worrying about the overhead of multiple calls.
## Injecting Context
`asyncContextProvider` also allows us to inject context. This is typically used in conjunction with [Executors](./advanced/executor).
```ts
const giraffeExecutor = giraffeResolver.toExecutor(
asyncContextProvider.with(useCurrentUser.provide({ id: 9, roles: ["admin"] }))
)
```
## Accessing Resolver Payload
In addition to the `useContext` function, GQLoom provides the `useResolverPayload` function for accessing all parameters in the resolver:
* root: the previous object, not normally used for fields on the root query type;
* args: the arguments provided for the field in the GraphQL query;
* context: the context object shared across parser functions and middleware, which is the return value of `useContext`;
* info: contains information about the current resolver call, such as the path to the GraphQL query, field names, etc;
* field: the definition of the field being resolved by the current resolver;
### Direct Access to Resolver Payload
For environments that do not provide `AsyncLocalStorage`, such as browsers or Cloudflare Workers, we can directly access the resolver payload within resolver functions and middleware.
#### Resolver Functions
In resolver functions, `payload` is always the last parameter.
```ts
const helloResolver = resolver({
hello: query(v.string()).resolve((_input, payload) => {
const user = // [!code hl]
(payload!.context as YogaInitialContext).request.headers.get("Authorization") // [!code hl]
return `Hello, ${user ?? "World"}`
}),
})
```
```ts
const helloResolver = resolver({
hello: query(z.string()).resolve((_input, payload) => {
const user = // [!code hl]
(payload!.context as YogaInitialContext).request.headers.get("Authorization") // [!code hl]
return `Hello, ${user ?? "World"}`
}),
})
```
#### Middleware
```ts twoslash
import { Middleware, ResolverPayload } from "@gqloom/core"
import { type YogaInitialContext } from "graphql-yoga"
function getUser(payload: ResolverPayload) {
const user = (payload.context as YogaInitialContext).request.headers.get(
"Authorization"
)
return user
}
const authGuard: Middleware = ({ next, payload }) => {
const user = getUser(payload!)
if (!user) throw new Error("Please login first")
return next()
}
```
## Using Contexts across Adapters
In the GraphQL ecosystem, each adapter provides a different context object, and you can learn how to use it in the Adapters chapter:
* [Yoga](./advanced/adapters/yoga)
* [Apollo](./advanced/adapters/apollo)
* [Mercurius](./advanced/adapters/mercurius)
---
title: Dataloader
icon: HardDriveDownload
file: ./content/docs/dataloader.mdx
---
Due to the flexibility of GraphQL, when we load the associated objects of a certain object, we usually need to execute multiple queries.
This leads to the well-known N+1 query problem. To solve this problem, we can use [DataLoader](https://github.com/graphql/dataloader).
`DataLoader` can combine multiple requests into one request, thus reducing the number of database queries. It can also cache query results to avoid duplicate queries.
## Examples
### Table Definition
Consider that we have two tables, `users` and `posts`, where `posts` is associated with the `id` of `users` through `posts.authorId`:
```ts twoslash
import { field, query, resolver, weave } from "@gqloom/core"
import { eq, inArray } from "drizzle-orm"
import { drizzle } from "drizzle-orm/node-postgres"
import { config } from "./env.config"
import * as tables from "./schema"
import { posts, users } from "./schema"
const db = drizzle(config.databaseUrl, { schema: tables, logger: true })
const userResolver = resolver.of(users, {
users: query(users.$list()).resolve(() => db.select().from(users)),
posts: field(posts.$list()).resolve((user) =>
db.select().from(posts).where(eq(posts.authorId, user.id))
),
})
export const schema = weave(userResolver)
```
In the above code, we defined a user resolver, which includes:
* `users` query: used to get all users
* `posts` field: used to get all posts of the corresponding user
Here is an example query that will return the information of all users and their corresponding posts:
```graphql title="GraphQL Query"
query usersWithPosts {
users {
id
name
email
posts {
id
title
}
}
}
```
This query will query the posts for each user separately. We populated 20 users in the database in the previous step, so this query will cause 20 queries to the `posts` table.
This is obviously a very inefficient way. Let's see how to use DataLoader to reduce the number of queries.
### Using DataLoader
Next, we will use DataLoader to optimize our query.
```ts twoslash
import { field, query, resolver, weave } from "@gqloom/core"
import { eq, inArray } from "drizzle-orm"
import { drizzle } from "drizzle-orm/node-postgres"
import { config } from "./env.config"
import * as tables from "./schema"
import { posts, users } from "./schema"
const db = drizzle(config.databaseUrl, { schema: tables, logger: true })
const userResolver = resolver.of(users, {
users: query(users.$list()).resolve(() => db.select().from(users)),
posts_: field(posts.$list()).resolve((user) => // [!code --]
db.select().from(posts).where(eq(posts.authorId, user.id)) // [!code --]
), // [!code --]
posts: field(posts.$list()).load(async (userList) => { // [!code ++]
const postList = await db // [!code ++]
.select() // [!code ++]
.from(posts) // [!code ++]
.where( // [!code ++]
inArray( // [!code ++]
posts.authorId, // [!code ++]
userList.map((u) => u.id) // [!code ++]
) // [!code ++]
) // [!code ++]
const postMap = Map.groupBy(postList, (p) => p.authorId) // [!code ++]
return userList.map((u) => postMap.get(u.id) ?? []) // [!code ++]
}), // [!code ++]
})
export const schema = weave(userResolver)
```
In the above code, we use `field().load()` to enable batch data loading. Behind the scenes, this will use `DataLoader` to batch load data.
Inside `load()`, we implement batch data loading through the following steps:
1. Use the `in` operation to get all the posts of the currently loaded users from the `posts` table at once;
2. Use `Map.groupBy()` to group the list of posts by the author ID;
3. Map the list of users to the list of posts in order. If a user has no posts, return an empty array.
In this way, we combine the original 20 queries into 1 query, thus achieving performance optimization.
It is necessary to ensure that the order of the returned array of the query function is consistent with the order of the `IDs` array. `DataLoader` relies on this order to correctly merge the results.
---
title: Getting Started
icon: PencilRuler
file: ./content/docs/getting-started.mdx
---
import { File, Folder, Files } from 'fumadocs-ui/components/files';
To quickly get started with GQLoom, we will build a simple GraphQL backend application together.
We will build a cattery application and provide a GraphQL API to the outside.
This application will include some simple functions:
* Cat basic information management: Enter the basic information of cats, including name, birthday, etc., update, delete and query cats;
* User (cat owner) registration management: Enter user information, a simple login function, and view one's own or other users' cats;
We will use the following technologies:
* [TypeScript](https://www.typescriptlang.org/): As our development language;
* [Node.js](https://nodejs.org/): As the runtime of our application;
* [graphql.js](https://github.com/graphql/graphql-js): The JavaScript implementation of GraphQL;
* [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server): A comprehensive GraphQL HTTP adapter;
* [Drizzle ORM](https://orm.drizzle.team/): A fast and type-safe ORM that helps us operate the database;
* [Valibot](https://valibot.dev/) or [Zod](https://zod.dev/): Used to define and validate inputs;
* `GQLoom`: Allows us to define GraphQL Schema comfortably and efficiently and write resolvers;
## Prerequisites
We only need to install [Node.js](https://nodejs.org/) version 20 or higher to run our application.
## Create the Application
### Project Structure
Our application will have the following structure:
Among them, the functions of each folder or file under the `src` directory are as follows:
* `contexts`: Store contexts, such as the current user;
* `providers`: Store functions that need to interact with external services, such as database connections and Redis connections;
* `resolvers`: Store GraphQL resolvers;
* `schema`: Store the schema, mainly the database table structure;
* `index.ts`: Used to run the GraphQL application in the form of an HTTP service;
GQLoom has no requirements for the project's file structure. Here is just for reference. In practice, you can organize the files according to your needs and preferences.
### Initialize the Project
First, let's create a new folder and initialize the project:
```sh
mkdir cattery
cd ./cattery
npm init -y
```
```sh
mkdir cattery
cd ./cattery
pnpm init
```
```sh
mkdir cattery
cd ./cattery
yarn init -y
```
Then, we will install some necessary dependencies to run a TypeScript application in Node.js:
```sh
npm i -D typescript @types/node tsx
npx tsc --init
```
```sh
pnpm add -D typescript @types/node tsx
pnpm exec tsc --init
```
```sh
yarn add -D typescript @types/node tsx
yarn dlx -q -p typescript tsc --init
```
Next, we will install GQLoom and related dependencies. We can choose [Valibot](https://valibot.dev/) or [Zod](https://zod.dev/) to define and validate inputs:
```sh
# use Valibot
npm i graphql graphql-yoga @gqloom/core valibot @gqloom/valibot
# use Zod
npm i graphql graphql-yoga @gqloom/core zod @gqloom/zod
```
```sh
# use Valibot
pnpm add graphql graphql-yoga @gqloom/core valibot @gqloom/valibot
# use Zod
pnpm add graphql graphql-yoga @gqloom/core zod @gqloom/zod
```
```sh
# use Valibot
yarn add graphql graphql-yoga @gqloom/core valibot @gqloom/valibot
# use zod
yarn add graphql graphql-yoga @gqloom/core zod @gqloom/zod
```
### Hello World
Let's write our first [resolver](./resolver):
```ts twoslash
import { mutation, query, resolver } from "@gqloom/core"
import { eq } from "drizzle-orm"
import * as v from "valibot"
import { db } from "../providers"
import { users } from "../schema"
export const userResolver = resolver.of(users, {
usersByName: query(users.$list())
.input({ name: v.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: v.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: v.object({
name: v.string(),
phone: v.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
```
```ts twoslash
// @filename: resolvers/user.ts
import { mutation, query, resolver } from "@gqloom/core"
import { eq } from "drizzle-orm"
import { z } from "zod"
import { db } from "../providers"
import { users } from "../schema"
export const userResolver = resolver.of(users, {
usersByName: query(users.$list())
.input({ name: z.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: z.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: z.object({
name: z.string(),
phone: z.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
```
```ts title="src/resolvers/index.ts"
import { helloResolver } from "./hello"
import { userResolver } from "./user" // [!code ++]
export const resolvers = [helloResolver, userResolver] // [!code ++]
```
Great, now let's try it in the playground:
```gql title="GraphQL Mutation" tab="Mutation"
mutation {
createUser(data: {name: "Bob", phone: "001"}) {
id
name
phone
}
}
```
```json tab="Response"
{
"data": {
"createUser": {
"id": 1,
"name": "Bob",
"phone": "001"
}
}
}
```
Let's continue to try to retrieve the user we just created:
```gql title="GraphQL Query" tab="Query"
{
usersByName(name: "Bob") {
id
name
phone
}
}
```
```json tab="Response"
{
"data": {
"usersByName": [
{
"id": 1,
"name": "Bob",
"phone": "001"
}
]
}
}
```
### Current User Context
First, let's add the `asyncContextProvider` middleware to enable asynchronous context:
```ts title="src/index.ts"
import { createServer } from "node:http"
import { weave } from "@gqloom/core"
import { ValibotWeaver } from "@gqloom/valibot" // [!code ++]
import { createYoga } from "graphql-yoga"
import { resolvers } from "./resolvers"
const schema = weave(asyncContextProvider, ValibotWeaver, ...resolvers) // [!code ++]
const yoga = createYoga({ schema })
createServer(yoga).listen(4000, () => {
console.info("Server is running on http://localhost:4000/graphql")
})
```
```ts title="src/index.ts"
import { createServer } from "node:http"
import { weave } from "@gqloom/core"
import { ZodWeaver } from "@gqloom/zod" // [!code ++]
import { createYoga } from "graphql-yoga"
import { resolvers } from "./resolvers"
const schema = weave(asyncContextProvider, ZodWeaver, ...resolvers) // [!code ++]
const yoga = createYoga({ schema })
createServer(yoga).listen(4000, () => {
console.info("Server is running on http://localhost:4000/graphql")
})
```
Next, let's try to add a simple login function and add a query operation to the user resolver:
* `mine`: Return the current user information
To implement this query, we first need to have a login function. Let's write a simple one:
```ts twoslash
import { createMemoization, useContext } from "@gqloom/core/context"
import { eq } from "drizzle-orm"
import { GraphQLError } from "graphql"
import type { YogaInitialContext } from "graphql-yoga"
import { db } from "../providers"
import { users } from "../schema"
export const useCurrentUser = createMemoization(async () => {
const phone =
useContext().request.headers.get("authorization")
if (phone == null) throw new GraphQLError("Unauthorized")
const user = await db.query.users.findFirst({ where: eq(users.phone, phone) })
if (user == null) throw new GraphQLError("Unauthorized")
return user
})
```
In the above code, we created a [context](./context) function for getting the current user, which will return the information of the current user. We use `createMemoization()` to memoize this function, which ensures that this function is only executed once within the same request to avoid unnecessary database queries.
We used `useContext()` to get the context provided by Yoga, and obtained the user's phone number from the request header, and found the user according to the phone number. If the user does not exist, a `GraphQLError` will be thrown.
As you can see, this login function is very simple and is only used for demonstration purposes, and it does not guarantee security at all. In practice, it is usually recommended to use solutions such as `session` or `jwt`.
Now, we add the new query operation in the resolver:
```ts title="src/resolvers/user.ts"
import { mutation, query, resolver } from "@gqloom/core"
import { eq } from "drizzle-orm"
import * as v from "valibot"
import { useCurrentUser } from "../contexts" // [!code ++]
import { db } from "../providers"
import { users } from "../schema"
export const userResolver = resolver.of(users, {
mine: query(users).resolve(() => useCurrentUser()), // [!code ++]
usersByName: query(users.$list())
.input({ name: v.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: v.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: v.object({
name: v.string(),
phone: v.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
```
```ts title="src/resolvers/user.ts"
import { mutation, query, resolver } from "@gqloom/core"
import { eq } from "drizzle-orm"
import { z } from "zod"
import { useCurrentUser } from "../contexts" // [!code ++]
import { db } from "../providers"
import { users } from "../schema"
export const userResolver = resolver.of(users, {
mine: query(users).resolve(() => useCurrentUser()), // [!code ++]
usersByName: query(users.$list())
.input({ name: z.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: z.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: z.object({
name: z.string(),
phone: z.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
```
If we directly call this new query in the playground, the application will give us an unauthorized error:
```gql title="Graphql Query" tab="Query"
{
mine {
id
name
phone
}
}
```
```json tab="Response"
{
"errors": [
{
"message": "Unauthorized",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"mine"
]
}
],
"data": null
}
```
Open the `Headers` at the bottom of the playground and add the `authorization` field to the request header. Here we use the phone number of `Bob` created in the previous step, so we are logged in as `Bob`:
```json tab="Headers"
{
"authorization": "001"
}
```
```gql title="Graphql Query" tab="Query"
{
mine {
id
name
phone
}
}
```
```json tab="Response"
{
"data": {
"mine": {
"id": 1,
"name": "Bob",
"phone": "001"
}
}
}
```
### Resolver Factory
Next, we will add the business logic related to cats.
We use the [resolver factory](./schema/drizzle#resolver-factory) to quickly create interfaces:
```ts twoslash
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import { z } from "zod"
import { db } from "../providers"
import { cats } from "../schema"
const catResolverFactory = drizzleResolverFactory(db, "cats")
export const catResolver = resolver.of(cats, {
cats: catResolverFactory.selectArrayQuery(),
age: field(z.number().int())
.derivedFrom("birthday")
.input({
currentYear: z
.number()
.int()
.nullish()
.transform((x) => x ?? new Date().getFullYear()),
})
.resolve((cat, { currentYear }) => {
return currentYear - cat.birthday.getFullYear()
}),
})
```
```ts title="src/resolvers/index.ts"
import { catResolver } from "./cat" // [!code ++]
import { helloResolver } from "./hello"
import { userResolver } from "./user"
export const resolvers = [helloResolver, userResolver, catResolver] // [!code ++]
```
In the above code, we used `drizzleResolverFactory()` to create `catResolverFactory` for quickly building resolvers.
We added a query that uses `catResolverFactory` to select data and named it `cats`. This query will provide full query operations on the `cats` table.
In addition, we also added an additional `age` field for cats to get the age of the cats.
Next, let's try to add a `createCat` mutation. We want only logged-in users to access this interface, and the created cats will belong to the current user:
```ts twoslash
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import * as v from "valibot"
import { useCurrentUser } from "../contexts"
import { db } from "../providers"
import { cats } from "../schema"
const catResolverFactory = drizzleResolverFactory(db, "cats")
export const catResolver = resolver.of(cats, {
cats: catResolverFactory.selectArrayQuery(),
age: field(v.pipe(v.number()))
.derivedFrom("birthday")
.input({
currentYear: v.nullish(v.pipe(v.number(), v.integer()), () =>
new Date().getFullYear()
),
})
.resolve((cat, { currentYear }) => {
return currentYear - cat.birthday.getFullYear()
}),
createCats: catResolverFactory.insertArrayMutation({ // [!code ++]
input: v.pipeAsync( // [!code ++]
v.objectAsync({ // [!code ++]
values: v.arrayAsync( // [!code ++]
v.pipeAsync( // [!code ++]
v.object({ // [!code ++]
name: v.string(), // [!code ++]
birthday: v.pipe( // [!code ++]
v.string(), // [!code ++]
v.transform((x) => new Date(x)) // [!code ++]
), // [!code ++]
}), // [!code ++]
v.transformAsync(async ({ name, birthday }) => ({ // [!code ++]
name, // [!code ++]
birthday, // [!code ++]
ownerId: (await useCurrentUser()).id, // [!code ++]
})) // [!code ++]
) // [!code ++]
), // [!code ++]
}) // [!code ++]
), // [!code ++]
}), // [!code ++]
})
```
```ts twoslash
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import { z } from "zod"
import { useCurrentUser } from "../contexts"
import { db } from "../providers"
import { cats } from "../schema"
const catResolverFactory = drizzleResolverFactory(db, "cats")
export const catResolver = resolver.of(cats, {
cats: catResolverFactory.selectArrayQuery(),
age: field(z.number().int())
.derivedFrom("birthday")
.input({
currentYear: z
.number()
.int()
.nullish()
.transform((x) => x ?? new Date().getFullYear()),
})
.resolve((cat, { currentYear }) => {
return currentYear - cat.birthday.getFullYear()
}),
createCats: catResolverFactory.insertArrayMutation({ // [!code ++]
input: z.object({ // [!code ++]
values: z // [!code ++]
.object({ // [!code ++]
name: z.string(), // [!code ++]
birthday: z.coerce.date(), // [!code ++]
}) // [!code ++]
.transform(async ({ name, birthday }) => ({ // [!code ++]
name, // [!code ++]
birthday, // [!code ++]
ownerId: (await useCurrentUser()).id, // [!code ++]
})) // [!code ++]
.array(), // [!code ++]
}), // [!code ++]
}), // [!code ++]
})
```
In the above code, we used `catResolverFactory` to create a mutation that adds more data to the `cats` table, and we overwrote the input of this mutation. When validating the input, we used `useCurrentUser()` to get the ID of the currently logged-in user and pass it as the value of `ownerId` to the `cats` table.
Now let's try to add a few cats in the playground:
```gql tab="mutation" title="GraphQL Mutation"
mutation {
createCats(values: [
{ name: "Mittens", birthday: "2021-01-01" },
{ name: "Fluffy", birthday: "2022-02-02" },
]) {
id
name
age
}
}
```
```json tab="Headers"
{
"authorization": "001"
}
```
```json tab="Response"
{
"data": {
"createCats": [
{
"id": 1,
"name": "Mittens",
"age": 4
},
{
"id": 2,
"name": "Fluffy",
"age": 3
}
]
}
}
```
Let's use the `cats` query to confirm the data in the database again:
```gql tab="query" title="GraphQL Query"
{
cats {
id
name
age
}
}
```
```json tab="Response"
{
"data": {
"cats": [
{
"id": 1,
"name": "Mittens",
"age": 4
},
{
"id": 2,
"name": "Fluffy",
"age": 3
}
]
}
}
```
### Associated Objects
We want to be able to get the owner of a cat when querying the cat, and also be able to get all the cats of a user when querying the user.
This is very easy to achieve in GraphQL.
Let's add an additional `owner` field to `cats` and an additional `cats` field to `users`:
```ts title="src/resolvers/cat.ts"
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import * as v from "valibot"
import { useCurrentUser } from "../contexts"
import { db } from "../providers"
import { cats } from "../schema"
const catResolverFactory = drizzleResolverFactory(db, "cats")
export const catResolver = resolver.of(cats, {
cats: catResolverFactory.selectArrayQuery(),
age: field(v.pipe(v.number()))
.derivedFrom("birthday")
.input({
currentYear: v.nullish(v.pipe(v.number(), v.integer()), () =>
new Date().getFullYear()
),
})
.resolve((cat, { currentYear }) => {
return currentYear - cat.birthday.getFullYear()
}),
owner: catResolverFactory.relationField("owner"), // [!code ++]
createCats: catResolverFactory.insertArrayMutation({
input: v.pipeAsync(
v.objectAsync({
values: v.arrayAsync(
v.pipeAsync(
v.object({
name: v.string(),
birthday: v.pipe(
v.string(),
v.transform((x) => new Date(x))
),
}),
v.transformAsync(async ({ name, birthday }) => ({
name,
birthday,
ownerId: (await useCurrentUser()).id,
}))
)
),
})
),
}),
})
```
```ts title="src/resolvers/cat.ts"
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import { z } from "zod"
import { useCurrentUser } from "../contexts"
import { db } from "../providers"
import { cats } from "../schema"
const catResolverFactory = drizzleResolverFactory(db, "cats")
export const catResolver = resolver.of(cats, {
cats: catResolverFactory.selectArrayQuery(),
age: field(z.number().int())
.derivedFrom("birthday")
.input({
currentYear: z
.number()
.int()
.nullish()
.transform((x) => x ?? new Date().getFullYear()),
})
.resolve((cat, { currentYear }) => {
return currentYear - cat.birthday.getFullYear()
}),
owner: catResolverFactory.relationField("owner"), // [!code ++]
createCats: catResolverFactory.insertArrayMutation({
input: z.object({
values: z
.object({
name: z.string(),
birthday: z.coerce.date(),
})
.transform(async ({ name, birthday }) => ({
name,
birthday,
ownerId: (await useCurrentUser()).id,
}))
.array(),
}),
}),
})
```
```ts title="src/resolvers/user.ts"
import { mutation, query, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle" // [!code ++]
import { eq } from "drizzle-orm"
import * as v from "valibot"
import { useCurrentUser } from "../contexts"
import { db } from "../providers"
import { users } from "../schema"
const userResolverFactory = drizzleResolverFactory(db, "users") // [!code ++]
export const userResolver = resolver.of(users, {
cats: userResolverFactory.relationField("cats"), // [!code ++]
mine: query(users).resolve(() => useCurrentUser()),
usersByName: query(users.$list())
.input({ name: v.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: v.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: v.object({
name: v.string(),
phone: v.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
```
```ts title="src/resolvers/user.ts"
import { mutation, query, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle" // [!code ++]
import { eq } from "drizzle-orm"
import { z } from "zod"
import { useCurrentUser } from "../contexts"
import { db } from "../providers"
import { users } from "../schema"
const userResolverFactory = drizzleResolverFactory(db, "users") // [!code ++]
export const userResolver = resolver.of(users, {
cats: userResolverFactory.relationField("cats"), // [!code ++]
mine: query(users).resolve(() => useCurrentUser()),
usersByName: query(users.$list())
.input({ name: z.string() })
.resolve(({ name }) => {
return db.query.users.findMany({
where: eq(users.name, name),
})
}),
userByPhone: query(users.$nullable())
.input({ phone: z.string() })
.resolve(({ phone }) => {
return db.query.users.findFirst({
where: eq(users.phone, phone),
})
}),
createUser: mutation(users)
.input({
data: z.object({
name: z.string(),
phone: z.string(),
}),
})
.resolve(async ({ data }) => {
const [user] = await db.insert(users).values(data).returning()
return user
}),
})
```
In the above code, we used the resolver factory to create the `owner` field for `cats`; similarly, we also created the `cats` field for `users`.
Behind the scenes, the relationship fields created by the resolver factory will use `DataLoader` to query from the database to avoid the N+1 problem.
Let's try to query the owner of a cat in the playground:
```gql title="GraphQL Query" tab="query"
{
cats {
id
name
age
owner {
id
name
phone
}
}
}
```
```json tab="Response"
{
"data": {
"cats": [
{
"id": 1,
"name": "Mittens",
"age": 4,
"owner": {
"id": 1,
"name": "Bob",
"phone": "001"
}
},
{
"id": 2,
"name": "Fluffy",
"age": 3,
"owner": {
"id": 1,
"name": "Bob",
"phone": "001"
}
}
]
}
}
```
Let's try to query the cats of the current user:
```gql title="GraphQL Query" tab="query"
{
mine {
name
cats {
id
name
age
}
}
}
```
```json tab="Headers"
{
"authorization": "001"
}
```
```json tab="Response"
{
"data": {
"mine": {
"name": "Bob",
"cats": [
{
"id": 1,
"name": "Mittens",
"age": 4
},
{
"id": 2,
"name": "Fluffy",
"age": 3
}
]
}
}
}
```
## Conclusion
In this article, we created a simple GraphQL server-side application. We used the following tools:
* `Valibot` or `Zod`: Used to define and validate inputs;
* `Drizzle`: Used to operate the database, and directly use the `Drizzle` table as the `GraphQL` output type;
* Context: Used to share data between different parts of the program, which is very useful for scenarios such as implementing login and tracking logs;
* Resolver factory: Used to quickly create resolvers and operations;
* `GraphQL Yoga`: Used to create a GraphQL HTTP service and provides a GraphiQL playground;
Our application has implemented the functions of adding and querying `users` and `cats`, but due to space limitations, the update and delete functions have not been implemented. They can be quickly added through the resolver factory.
## Next Steps
* Check out the core concepts of GQLoom: [Silk](./silk), [Resolver](./resolver), [Weave](./weave);
* Learn about common functions: [Context](./context), [DataLoader](./dataloader), [Middleware](./middleware)
* Add a GraphQL client to the front-end project: [gql.tada](https://gql-tada.0no.co/), [Urql](https://commerce.nearform.com/open-source/urql/), [Apollo Client](https://www.apollographql.com/docs/react), [TanStack Query](https://tanstack.com/query/latest/docs/framework/react/graphql), [Graffle](https://graffle.js.org/)
---
title: Introduction
icon: BookMarked
file: ./content/docs/index.mdx
---
## What is GraphQL
GraphQL is a query language for APIs, developed and open-sourced by Facebook. It allows clients to specify the required data structure, reducing unnecessary data transfer and improving the performance and maintainability of the API.
GraphQL brings the following advantages:
* **Type Safety**: Strong type system to ensure the consistency and security of data from the server to the client.
* **Flexible Aggregation**: Automatically aggregate multiple queries, reducing the number of client requests and ensuring the simplicity of the server-side API.
* **Efficient Querying**: The client can specify the required data structure, reducing unnecessary data transfer and improving the performance and maintainability of the API.
* **Easy to Extend**: Extending the API by adding new fields and types without modifying existing code.
* **Efficient Collaboration**: Using Schema as documentation, which can reduce communication costs and improve development efficiency in team development.
* **Thriving Ecosystem**: Tools and frameworks are emerging constantly. The active community, with diverse applications, is growing fast and has bright prospects.
## What is GQLoom
GQLoom is a **Code First** GraphQL Schema Loom, used to weave **runtime types** in the **TypeScript/JavaScript** ecosystem into a GraphQL Schema.
Runtime validation libraries such as [Zod](https://zod.dev/), [Valibot](https://valibot.dev/), and [Yup](https://github.com/jquense/yup) have been widely used in backend application development. At the same time, when using ORM libraries such as [Prisma](https://www.prisma.io/), [MikroORM](https://mikro-orm.io/), and [Drizzle](https://orm.drizzle.team/), we also pre-define the database table structures or entity models that contain runtime types.
The responsibility of GQLoom is to weave these runtime types into a GraphQL Schema.
When developing backend applications with GQLoom, you only need to write types using the Schema libraries you are familiar with. Modern Schema libraries will infer TypeScript types for you, and GQLoom will weave GraphQL types for you.
In addition, the **resolver factory** of GQLoom can also create CRUD interfaces for [Prisma](./schema/prisma.mdx#resolver-factory), [MikroORM](./schema/mikro-orm.mdx#resolver-factory), and [Drizzle](./schema/drizzle.mdx#resolver-factory), and supports custom input and adding middleware.
The design of GQLoom is inspired by [tRPC](https://trpc.io/) and [TypeGraphQL](https://typegraphql.com/), and some technical implementations refer to [Pothos](https://pothos-graphql.dev/).
### Hello, World
```ts twoslash tab="valibot"
import { resolver, query, weave } from "@gqloom/core"
import { ValibotWeaver } from "@gqloom/valibot"
import * as v from "valibot"
const helloResolver = resolver({
hello: query(v.string())
.input({ name: v.nullish(v.string(), "World") })
.resolve(({ name }) => `Hello, ${name}!`),
})
export const schema = weave(ValibotWeaver, helloResolver)
```
```ts twoslash tab="zod"
import { resolver, query, weave } from "@gqloom/core"
import { ZodWeaver } from "@gqloom/zod"
import { z } from "zod"
const helloResolver = resolver({
hello: query(z.string())
.input({ name: z.string().nullish() })
.resolve(({ name }) => `Hello, ${name ?? "World"}!`),
})
export const schema = weave(ZodWeaver, helloResolver)
```
### Highlights you should not miss
* ๐งโ๐ป **Development Experience**: Fewer boilerplate codes, semantic API design, and extensive ecosystem integration make development enjoyable.
* ๐ **Type Safety**: Automatically infer types from the Schema, enjoy intelligent code completion during development, and detect potential problems during compilation.
* ๐ฏ **Interface Factory**: Ordinary CRUD interfaces are too simple yet too cumbersome. Let the resolver factory create them quickly.
* ๐ **Fully Prepared**: Middleware, context, subscriptions, and federated graphs are ready.
* ๐ฎ **No Magic**: No decorators, no metadata and reflection, no code generation. It can run anywhere with just JavaScript/TypeScript.
* ๐งฉ **Rich Integration**: Use your favorite validation libraries and ORMs to build your next GraphQL application.
---
title: Middleware
icon: Fence
file: ./content/docs/middleware.mdx
---
Middleware is a function that intervenes in the processing flow of a parsed function. It provides a way to insert logic into the request and response flow to execute code before a response is sent or before a request is processed.
`GQLoom`'s middleware follows the onion middleware pattern of [Koa](https://koajs.com/#application).
## Define Middleware
Middleware is a function that will be injected with an `options` object as a parameter when called. The `options` object contains the following fields:
* `outputSilk`: output silk, which includes the output type of the field currently being parsed;
* `parent`: the parent node of the current field, equivalent to `useResolverPayload().root`;
* `parseInput`: a function used to obtain the input of the current field;
* `type`: the type of the current field, whose value can be `query`, `mutation`, `subscription`, or `field`;
* `next`: a function used to call the next middleware;
The `options` object can also be directly used as the `next` function.
Additionally, we can use `useContext()` and `useResolverPayload()` to get the context and more information of the current resolver function.
A minimal middleware function is as follows:
```ts twoslash
import { ValibotWeaver, weave, resolver, query } from "@gqloom/valibot"
import * as v from "valibot"
import { createServer } from "node:http"
import { createYoga } from "graphql-yoga"
import { outputValidator, valibotExceptionFilter } from "./middlewares"
const helloResolver = resolver({
hello: query(v.pipe(v.string(), v.minLength(10)))
.input({ name: v.string() })
.use(outputValidator) // [!code hl]
.resolve(({ name }) => `Hello, ${name}`),
})
export const schema = weave(ValibotWeaver, helloResolver, valibotExceptionFilter) // [!code hl]
const yoga = createYoga({ schema })
createServer(yoga).listen(4000, () => {
// eslint-disable-next-line no-console
console.info("Server is running on http://localhost:4000/graphql")
})
```
In the code above, we added the `v.minLength(10)` requirement to the output of the `hello` query and added the `outputValidator` middleware to the parser function.
We also added a global middleware `ValibotExceptionFilter` to `weave`.
```ts twoslash
import { weave, resolver, query } from "@gqloom/zod"
import { z } from "zod"
import { createServer } from "node:http"
import { createYoga } from "graphql-yoga"
import { outputValidator, zodExceptionFilter } from "./middlewares"
const helloResolver = resolver({
hello: query(z.string().min(10))
.input({ name: z.string() })
.use(outputValidator) // [!code hl]
.resolve(({ name }) => `Hello, ${name}`),
})
export const schema = weave(helloResolver, zodExceptionFilter) // [!code hl]
const yoga = createYoga({ schema })
createServer(yoga).listen(4000, () => {
// eslint-disable-next-line no-console
console.info("Server is running on http://localhost:4000/graphql")
})
```
In the code above, we added a `z.string().min(10)` requirement to the output of the `hello` query and added the `outputValidator` middleware to the parser function.
We also added a global middleware `ValibotExceptionFilter` to `weave`.
When we make the following query:
```graphql title="GraphQL Query"
{
hello(name: "W")
}
```
A result similar to the following will be given:
```json
{
"errors": [
{
"message": "Invalid length: Expected >=10 but received 8",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"hello"
],
"extensions": {
"issues": [
{
"kind": "validation",
"type": "min_length",
"input": "Hello, W",
"expected": ">=10",
"received": "8",
"message": "Invalid length: Expected >=10 but received 8",
"requirement": 10
}
]
}
}
],
"data": null
}
```
```json
{
"errors": [
{
"message": "String must contain at least 10 character(s)",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"hello"
],
"extensions": {
"issues": [
{
"code": "too_small",
"minimum": 10,
"type": "string",
"inclusive": true,
"exact": false,
"message": "String must contain at least 10 character(s)",
"path": []
}
]
}
}
],
"data": null
}
```
If we adjust the input so that the returned string is the required length:
```graphql title="GraphQL Query"
{
hello(name: "World")
}
```
It will get a response with no exceptions:
```json
{
"data": {
"hello": "Hello, World"
}
}
```
### Authentication
Checking a user's permissions is a common requirement that we can easily implement with middleware.
Consider that our user has the roles `โadminโ` and `โeditorโ`, and we want the administrator and editor to have access to their own actions, respectively.
First, we implement an `authGuard` middleware that checks the user's role:
```ts twoslash
import { type Middleware } from "@gqloom/core"
import { useUser } from "./context"
import { GraphQLError } from "graphql"
export function authGuard(role: "admin" | "editor"): Middleware {
return async (next) => {
const user = await useUser()
if (user == null) throw new GraphQLError("Not authenticated")
if (!user.roles.includes(role)) throw new GraphQLError("Not authorized")
return next()
}
}
```
In the code above, we declare an `authGuard` middleware that takes a role parameter and returns a middleware function.
The middleware function checks that the user is authenticated and has the specified role, and throws a `GraphQLError` exception if the requirements are not satisfied.
We can apply different middleware for different resolvers:
```ts twoslash
import { resolver, mutation } from "@gqloom/core"
import * as v from "valibot"
import { authGuard } from "./middlewares"
const adminResolver = resolver(
{
deleteArticle: mutation(v.boolean(), () => true),
},
{
middlewares: [authGuard("admin")], // [!code hl]
}
)
const editorResolver = resolver(
{
createArticle: mutation(v.boolean(), () => true),
updateArticle: mutation(v.boolean(), () => true),
},
{ middlewares: [authGuard("editor")] } // [!code hl]
)
```
```ts twoslash
import { resolver, mutation } from "@gqloom/zod"
import { z } from "zod"
import { authGuard } from "./middlewares"
const adminResolver = resolver(
{
deleteArticle: mutation(z.boolean(), () => true),
},
{
middlewares: [authGuard("admin")], // [!code hl]
}
)
const editorResolver = resolver(
{
createArticle: mutation(z.boolean(), () => true),
updateArticle: mutation(z.boolean(), () => true),
},
{ middlewares: [authGuard("editor")] } // [!code hl]
)
```
In the code above, we have applied the `authGuard` middleware to `AdminResolver` and `EditorResolver` and assigned different roles to them. In this way, only users with the corresponding roles can access the actions within the corresponding resolvers.
### Logging
We can also implement logging functionality through middleware. For example, we can create a `logger` middleware to log the execution time of each field parsing function:
```ts twoslash
import { mutation, resolver } from "@gqloom/core"
import * as v from "valibot"
const Post = v.object({
__typename: v.nullish(v.literal("Post")),
id: v.number(),
title: v.string(),
content: v.string(),
authorId: v.number(),
})
interface IPost extends v.InferOutput {}
const posts: IPost[] = []
export const postsResolver = resolver({
createPost: mutation(Post)
.input(
v.object({
title: v.string(),
content: v.string(),
authorId: v.number(),
})
)
.use(async ({ next, parseInput }) => { // [!code hl]
const inputResult = await parseInput.getResult() // [!code hl]
inputResult.authorId = (await useUser()).id // [!code hl]
parseInput.result = { value: inputResult } // [!code hl]
return next() // [!code hl]
})
.resolve(({ title, content, authorId }) => {
const post = {
id: Math.random(),
title,
content,
authorId,
}
posts.push(post)
return post
}),
})
```
```ts twoslash
import { mutation, resolver } from "@gqloom/core"
import { z } from "zod"
const Post = z.object({
__typename: z.literal("Post").nullish(),
id: z.number(),
title: z.string(),
content: z.string(),
authorId: z.number(),
})
interface IPost extends z.output {}
const posts: IPost[] = []
export const postsResolver = resolver({
createPost: mutation(Post)
.input(
z.object({
title: z.string(),
content: z.string(),
authorId: z.number(),
})
)
.use(async ({ next, parseInput }) => { // [!code hl]
const inputResult = await parseInput.getResult() // [!code hl]
inputResult.authorId = (await useUser()).id // [!code hl]
parseInput.result = { value: inputResult } // [!code hl]
return next() // [!code hl]
})
.resolve(({ title, content, authorId }) => {
const post = {
id: Math.random(),
title,
content,
authorId,
}
posts.push(post)
return post
}),
})
```
## Using middleware
GQLoom is able to apply middleware in a variety of scopes, including resolver functions, resolver local middleware, and global middleware.
### Resolve function middleware
We can use middleware directly in the resolver function by using the `use` method during its construction, for example:
```ts twoslash
import { resolver, query } from "@gqloom/core"
import * as v from "valibot"
import { outputValidator } from "./middlewares"
const helloResolver = resolver({
hello: query(v.pipe(v.string(), v.minLength(10)))
.input({ name: v.string() })
.use(outputValidator) // [!code hl]
.resolve(({ name }) => `Hello, ${name}`),
})
```
```ts twoslash
import { resolver, query } from "@gqloom/zod"
import { z } from "zod"
import { outputValidator } from "./middlewares"
const helloResolver = resolver({
hello: query(z.string().min(10))
.input({ name: z.string() })
.use(outputValidator) // [!code hl]
.resolve(({ name }) => `Hello, ${name}`),
})
```
### Resolver-scoped middleware
We can also apply middleware at the resolver level, so the middleware will take effect for all operations within the resolver.
We just need to use the `use` method to add `middlewares` to the resolver:
```ts twoslash
import { resolver, mutation } from "@gqloom/core"
import * as v from "valibot"
import { authGuard } from "./middlewares"
const adminResolver = resolver({
deleteArticle: mutation(v.boolean(), () => true),
}).use(authGuard("admin")) // [!code hl]
const editorResolver = resolver({
createArticle: mutation(v.boolean(), () => true),
updateArticle: mutation(v.boolean(), () => true),
}).use(authGuard("editor")) // [!code hl]
```
```ts twoslash
import { resolver, mutation } from "@gqloom/zod"
import { z } from "zod"
import { authGuard } from "./middlewares"
const adminResolver = resolver({
deleteArticle: mutation(z.boolean(), () => true),
}).use(authGuard("admin")) // [!code hl]
const editorResolver = resolver({
createArticle: mutation(z.boolean(), () => true),
updateArticle: mutation(z.boolean(), () => true),
}).use(authGuard("editor")) // [!code hl]
```
### Global middleware
In order to apply global middleware, we need to pass in the middleware fields in the `weave` function, for example:
```ts
import { weave } from "@gqloom/core"
import { exceptionFilter } from "./middlewares"
export const schema = weave(helloResolver, exceptionFilter) // [!code hl]
```
### Applying Middleware Based on Operation Type
We can specify for which operation types a middleware should take effect.
```ts twoslash
import type { Middleware } from "@gqloom/core"
import { GraphQLError } from "graphql"
export const transaction: Middleware = async ({ next }) => {
try {
await db.beginTransaction()
const result = await next()
await db.commit()
return result
} catch (error) {
await db.rollback()
throw new GraphQLError("Transaction failed", {
extensions: { originalError: error },
})
}
}
transaction.operations = ["mutation"]
```
`Middleware.operations` is an array of strings used to specify on which operation types the middleware should take effect. The available values are:
* `"query"`;
* `"mutation"`;
* `"subscription"`;
* `"field"`;
* `"subscription.resolve"`;
* `"subscription.subscribe"`;
The default value for `Middleware.operations` is `["field", "query", "mutation", "subscription.subscribe"]`.
---
title: Resolver
icon: RadioTower
file: ./content/docs/resolver.mdx
---
A resolver is a place to put GraphQL operations (`query`, `mutation`, `subscription`).
Usually we put operations that are close to each other in the same resolver, for example user related operations in a resolver called `userResolver`.
## Distinguishing Operations
First, let's take a brief look at the basic operations of GraphQL and when you should use them:
* **Query** is an operation that is used to get data, such as getting user information, getting a list of products, and so on. Queries usually do not change the persistent data of the service.
* **Mutation** is an operation used to modify data, such as creating a user, updating user information, deleting a user, and so on. Mutation operations usually change the persistent data of a service.
* **Subscription** is an operation in which the server actively pushes data to the client. Subscription usually does not change the persistent data of the service. In other words, subscription is a real-time query.
## Defining a resolver
We use the `resolver` function to define the resolver:
```ts
import { resolver } from "@gqloom/core"
const helloResolver = resolver({})
```
In the code above, we have defined a resolver called `helloResolver`, which has no operations for now.
## Defining operations
Let's try to define the operation using the `query` function:
```ts twoslash
const userMap: Map = new Map(
[
{ id: 1, name: "Cao Xueqin" },
{ id: 2, name: "Wu Chengen" },
].map((user) => [user.id, user])
)
const bookMap: Map = new Map(
[
{ id: 1, title: "Dream of Red Mansions", authorID: 1 },
{ id: 2, title: "Journey to the West", authorID: 2 },
].map((book) => [book.id, book])
)
```
Next, we define a `bookResolver`:
```ts twoslash
import { resolver, query } from '@gqloom/core'
import * as v from "valibot"
const bookResolver = resolver.of(Book, {
books: query(v.array(Book)).resolve(() => Array.from(bookMap.values())),
})
```
```ts twoslash
import { resolver, query } from '@gqloom/zod'
import { z } from "zod"
const bookResolver = resolver.of(Book, {
books: query(z.array(Book)).resolve(() => Array.from(bookMap.values())),
})
```
In the above code, we have used the `resolver.of` function to define `bookResolver`, which is an object resolver for resolving `Book` objects.
In `bookResolver`, we define a `books` field, which is a query operation to get all the books.
Next, we will add an additional field called `author` to the `Book` object to get the author of the book:
```ts twoslash
import { resolver, query, field } from '@gqloom/core'
import * as v from "valibot"
const bookResolver = resolver.of(Book, {
books: query(v.array(Book)).resolve(() => Array.from(bookMap.values())),
author: field(v.nullish(User)).resolve((book) => userMap.get(book.authorID)), // [!code hl]
})
```
```ts twoslash
import { resolver, query, field } from '@gqloom/zod'
import { z } from "zod"
const bookResolver = resolver.of(Book, {
books: query(z.array(Book)).resolve(() => Array.from(bookMap.values())),
author: field(User.nullish()).resolve((book) => userMap.get(book.authorID)), // [!code hl]
})
```
In the above code, we used the `field` function to define the `author` field.
The `field` function takes two parameters:
* The first argument is the return type of the field;
* The second parameter is a parsing function or option, in this case we use a parsing function: we get the `Book` instance from the first parameter of the parsing function, and then we get the corresponding `User` instance from the `userMap` based on the `authorID` field.
### Defining Field Inputs
In GraphQL, we can define input parameters for fields in order to pass additional data at query time.
In `GQLoom`, we can use the second argument of the `field` function to define the input parameters of a field.
```ts twoslash
import { resolver, query, field } from '@gqloom/core'
import * as v from "valibot"
const bookResolver = resolver.of(Book, {
books: query(v.array(Book)).resolve(() => Array.from(bookMap.values())),
author: field(v.nullish(User)).resolve((book) => userMap.get(book.authorID)),
signature: field(v.string()) // [!code hl]
.input({ name: v.string() }) // [!code hl]
.resolve((book, { name }) => { // [!code hl]
return `The book ${book.title} is in ${name}'s collection.` // [!code hl]
}), // [!code hl]
})
```
```ts twoslash
import { resolver, query, field } from '@gqloom/zod'
import { z } from "zod"
const bookResolver = resolver.of(Book, {
books: query(z.array(Book)).resolve(() => Array.from(bookMap.values())),
author: field(User.nullish()).resolve((book) => userMap.get(book.authorID)),
signature: field(z.string()) // [!code hl]
.input({ name: z.string() }) // [!code hl]
.resolve((book, { name }) => { // [!code hl]
return `The book ${book.title} is in ${name}'s collection.` // [!code hl]
}), // [!code hl]
})
```
In the above code, we used the `field` function to define the `signature` field.
The second argument to the `field` function is an object which contains two fields:
* `input`: the input parameter of the field, which is an object containing a `name` field, which is of type `string`;
* `resolve`: the field's resolver function, which takes two arguments: the first argument is the source object of the resolver constructed by `resolver.of`, which is an instance of `Book`; the second argument is the field's input parameter, which is an object that contains an input of the `name` field.
The `bookResolver` object we just defined can be woven into a GraphQL schema using the [weave](../weave) function:
```ts
import { weave } from '@gqloom/core'
import { ValibotWeaver } from '@gqloom/valibot'
export const schema = weave(ValibotWeaver, bookResolver)
```
```ts
import { weave } from '@gqloom/core'
import { ZodWeaver } from '@gqloom/zod'
export const schema = weave(ZodWeaver, bookResolver)
```
The resulting GraphQL schema is as follows:
```graphql title="GraphQL Schema"
type Book {
id: ID!
title: String!
authorID: ID!
author: User
signature(name: String!): String!
}
type User {
id: ID!
name: String!
}
type Query {
books: [Book!]!
}
```
### Define Derived Fields
When writing resolvers for database tables or other persistent data, we often need to calculate new fields based on the data in the table, which are derived fields.
Derived fields require selecting the data they depend on when retrieving data. We can use `field().derivedFrom()` to declare the dependent data.
The derived dependencies will be used by `useResolvingFields()`, and this function is used to accurately obtain the fields required for the current query.
```ts
import { field, resolver } from "@gqloom/core"
import * as v from "valibot"
import { giraffes } from "./table"
export const giraffeResolver = resolver.of(giraffes, {
age: field(v.number())
.derivedFrom("birthDate")
.resolve((giraffe) => {
const today = new Date()
const age = today.getFullYear() - giraffe.birthDate.getFullYear()
return age
}),
})
```
```ts
import { field, resolver } from "@gqloom/core"
import * as z from "zod"
import { giraffes } from "./table"
export const giraffeResolver = resolver.of(giraffes, {
age: field(z.number())
.derivedFrom("birthDate")
.resolve((giraffe) => {
const today = new Date()
const age = today.getFullYear() - giraffe.birthDate.getFullYear()
return age
}),
})
```
```ts
import * as sqlite from "drizzle-orm/sqlite-core"
import { drizzleSilk } from "@gqloom/drizzle"
export const giraffes = drizzleSilk(
sqlite.sqliteTable("giraffes", {
id: sqlite.integer("id").primaryKey(),
name: sqlite.text("name").notNull(),
birthDate: sqlite.integer({ mode: "timestamp" }).notNull(),
})
)
```
---
title: Silk
icon: Volleyball
file: ./content/docs/silk.mdx
---
The full name of `GQLoom` is GraphQL Loom.
The silk is the basic material of the loom, and it reflects both GraphQL types and TypeScript types.
At development time, we use the Schema of an existing schema library as the silk, and eventually `GQLoom` will weave the silk into the GraphQL Schema.
## Simple scalar silk
We can create a simple scalar silk using the `silk` function:
```ts twoslash
import { silk } from "@gqloom/core"
import { GraphQLString, GraphQLInt, GraphQLNonNull } from "graphql"
const StringSilk = silk(GraphQLString)
const IntSilk = silk(GraphQLInt)
const NonNullStringSilk = silk(new GraphQLNonNull(GraphQLString))
const NonNullStringSilk1 = silk.nonNull(StringSilk)
```
## Object silk
We can construct GraphQL objects directly using [graphql.js](https://graphql.org/graphql-js/constructing-types/):
```ts twoslash
import { silk } from "@gqloom/core"
import {
GraphQLObjectType,
GraphQLNonNull,
GraphQLString,
GraphQLInt,
} from "graphql"
interface ICat {
name: string
age: number
}
const Cat = silk(
new GraphQLObjectType({
name: "Cat",
fields: {
name: { type: new GraphQLNonNull(GraphQLString) },
age: { type: new GraphQLNonNull(GraphQLInt) },
},
})
)
```
In the above code: we define an `ICat` interface and a silk named `Cat` using the `silk` function.
The `silk` function accepts `ICat` as a generic parameter and also accepts a `GraphQLObjectType` instance to elaborate the structure of `Cat` in GraphQL.
`Cat` will be presented in GraphQL as:
```graphql title="GraphQL Schema"
type Cat {
name: String!
age: Int!
}
```
You may have noticed that using `graphql.js` to create silk requires the declaration of both the `ICat` interface and the `GraphQLObjectType`, which means that we have created two definitions for `Cat`.
Duplicate definitions cost the code simplicity and increased maintenance costs.
## Creating silk with schema libraries
The good thing is that we have schema libraries like [Valibot](https://valibot.dev/), [Zod](https://zod.dev/) that create Schemas that will carry TypeScript types and remain typed at runtime.
`GQLoom` can use these Schemas directly as silks without duplicating definitions.
`GQLoom` has currently integrated Schemas from the following libraries:
* [Valibot](../schema/valibot)
* [Zod](../schema/zod)
* [Yup](../schema/yup)
* [Mikro ORM](../schema/mikro-orm)
* [Prisma](../schema/prisma)
* [Drizzle](../schema/drizzle)
### Use Valibot to create silk
```ts twoslash
import * as v from "valibot"
const StringSilk = v.string()
const BooleanSilk = v.boolean()
const Cat = v.object({
__typename: v.literal("Cat"),
name: v.string(),
age: v.number(),
})
```
In the code above, we use [Valibot](https://valibot.dev/) to create some simple schemas as silk. You can learn more about how to create more complex types with [Valibot](https://valibot.dev/) in the [Valibot Integration](../schema/valibot) chapter.
### Use Zod to create silk
```ts twoslash
import { z } from "zod"
const StringSilk = z.string()
const BooleanSilk = z.boolean()
const Cat = z.object({
__typename: z.literal("Cat"),
name: z.string(),
age: z.number(),
})
```
In the code above, we have created some simple Schema as silk using [Zod](https://zod.dev/), you can learn how to create more complex types using [Zod integration](../schema/zod) section to learn how to create more complex types using [Zod](https://zod.dev/).
The core library of `GQLoom` follows the [Standard Schema specification](https://github.com/standard-schema/standard-schema). Thanks to `Valibot` and `Zod` also following this specification, we don't need to use additional wrapper functions to use the Schema from Valibot and Zod as Silk.
---
title: Weave
icon: Waves
file: ./content/docs/weave.mdx
---
In GQLoom, the `weave` function is used to weave multiple Resolvers or Silks into a single GraphQL Schema.
The `weave` function can take [resolver](./resolver), [silk](./silk), weaver configuration, global [middleware](./middleware)
## Weaving resolvers
The most common usage is to weave multiple resolvers together, for example:
```ts
import { weave } from '@gqloom/core';
export const schema = weave(helloResolver, catResolver);
```
## Weaving a single silk
Sometimes we need to weave a single [silk](../silk) woven into a GraphQL Schema, for example:
```ts twoslash
import { weave } from '@gqloom/core'
import { ValibotWeaver } from '@gqloom/valibot'
import * as v from "valibot"
const Dog = v.object({
__typename: v.nullish(v.literal("Dog")),
name: v.string(),
age: v.number(),
})
export const schema = weave(ValibotWeaver, helloResolver, catResolver, Dog);
```
```ts twoslash
import { weave } from '@gqloom/core'
import { ZodWeaver } from '@gqloom/zod'
import { z } from "zod"
const Dog = z.object({
__typename: z.literal("Dog").nullish(),
name: z.string(),
age: z.number(),
})
export const schema = weave(ZodWeaver, helloResolver, catResolver, Dog);
```
## Weaver configuration
### Input type naming conversion
In GraphQL, objects are recognized as [type](https://graphql.org/graphql-js/object-types/) and [input](https://graphql.org/graphql-js/mutations-and-input-types/).
When using `GQLoom`, we usually only use the `object` type, and behind the scenes `GQLoom` will automatically convert the `object` type to the `input` type.
The advantage of this is that we can use the `object` type directly to define input parameters without having to define the `input` type manually.
However, when we use the same `object` type for both `type` and `input`, it will not be woven into GraphQL Schema due to naming conflict.
Let's look at an example:
```ts twoslash
import { resolver, mutation, weave } from '@gqloom/core'
import { ValibotWeaver } from '@gqloom/valibot'
import * as v from "valibot"
const Cat = v.object({
__typename: v.nullish(v.literal("Cat")),
name: v.string(),
birthDate: v.string(),
})
const catResolver = resolver({
createCat: mutation(Cat, {
input: {
data: Cat,
},
resolve: ({ data }) => data,
}),
})
export const schema = weave(ValibotWeaver, catResolver);
```
```ts twoslash
import { resolver, mutation, weave } from '@gqloom/zod'
import { ZodWeaver } from '@gqloom/zod'
import { z } from "zod"
const Cat = z.object({
__typename: z.literal("Cat").nullish(),
name: z.string(),
birthDate: z.string(),
})
const catResolver = resolver({
createCat: mutation(Cat, {
input: {
data: Cat,
},
resolve: ({ data }) => data,
}),
})
export const schema = weave(ZodWeaver, catResolver);
```
In the above code, we defined a `Cat` object and used it for `type` and `input`. But when we try to weave `catResolver` into the GraphQL Schema, an error is thrown with a duplicate `Cat` name:
```bash
Error: Schema must contain uniquely named types but contains multiple types named "Cat".
```
To solve this problem, we need to specify a different name for the `input` type. We can do this using the `getInputObjectName` option in the `SchemaWeaver.config` configuration:
```ts twoslash
import { resolver, mutation, weave, GraphQLSchemaLoom } from '@gqloom/core'
import { ValibotWeaver } from '@gqloom/valibot'
import * as v from "valibot"
const Cat = v.object({
__typename: v.nullish(v.literal("Cat")),
name: v.string(),
birthDate: v.string(),
})
const catResolver = resolver({
createCat: mutation(Cat, {
input: {
data: Cat,
},
resolve: ({ data }) => data,
}),
})
export const schema = weave(
catResolver,
ValibotWeaver,
GraphQLSchemaLoom.config({ getInputObjectName: (name) => `${name}Input` }) // [!code hl]
)
```
```ts twoslash
import { resolver, mutation, weave, GraphQLSchemaLoom } from '@gqloom/zod'
import { z } from "zod"
const Cat = z.object({
__typename: z.literal("Cat").nullish(),
name: z.string(),
birthDate: z.string(),
})
const catResolver = resolver({
createCat: mutation(Cat, {
input: {
data: Cat,
},
resolve: ({ data }) => data,
}),
})
export const schema = weave(
catResolver,
GraphQLSchemaLoom.config({ getInputObjectName: (name) => `${name}Input` }) // [!code hl]
)
```
Thus, `Cat` objects will be converted to `CatInput` types, thus avoiding naming conflicts.
The above `catResolver` will weave the following GraphQL Schema:
```graphql title="GraphQL Schema"
type Mutation {
createCat(data: CatInput!): Cat!
}
type Cat {
name: String!
birthDate: String!
}
input CatInput {
name: String!
birthDate: String!
}
```
## Global middleware
```ts
import { weave } from '@gqloom/core';
import { logger } from './middlewares';
export const schema = weave(helloResolver, catResolver, logger)
```
See more about middleware usage in [middleware section](./middleware).