Resolver

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:

Queries

A 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

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

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:

import { loom } from "@gqloom/core" const { resolver } = loom 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:

import { loom } from "@gqloom/core" import { GraphQLNonNull, GraphQLString } from "graphql" const { resolver, query } = loom const HelloResolver = resolver({ hello: query( silk<string>(new GraphQLNonNull(GraphQLString)), () => "Hello, World" ), })

In the code above, we have defined a query operation called hello which returns a non-null string. Here, we're using the type definition provided by graphql.js directly, which as you can see can be slightly verbose, and we could have chosen to simplify the code by using the schema library:

valibot
zod

We can define the return type of the hello operation using [valibot](. /schema-integration/valibot) to define the return type of the hello operation:

import { resolver, query } from "@gqloom/core" import * as v from "valibot" const HelloResolver = resolver({ hello: query(v.string(), () => "Hello, World"), })

In the code above, we use v.string() to define the return type of the hello operation. We can directly use the valibot schema as the silk.

Define the inputs to operations

The query, mutation, and subscription operations can all accept input parameters.

Let's add an input parameter name to the hello operation:

valibot
zod
import { resolver, query } from '@gqloom/core' import * as v from "valibot" const HelloResolver = resolver({ hello: query(v.string(), { input: { name: v.nullish(v.string(), "World"), }, resolve: ({ name }) => `Hello, ${name}`, }), })

In the code above, we passed in the input property in the second argument of the query function to define the input parameter: the input property is an object whose key is the name of the input parameter, and whose value is the type definition of the input parameter.

Here, we use v.nullish(v.string(), “World”) to define the name parameter, which is an optional string with a default value of “World”. In the resolve function, we can get the value of the input parameter by the first parameter, and TypeScript will derive its type for us, in this case, we directly deconstruct to get the value of the name parameter.

Adding more information to operations

We can also add more information to the action, such as description, deprecationReason and extensions:

valibot
zod
import { resolver, query } from '@gqloom/core' import * as v from "valibot" const HelloResolver = resolver({ description: "Say hello to someone", hello: query(v.string(), { input: { name: v.nullish(v.string(), "World"), }, resolve: ({ name }) => `Hello, ${name}`, }), })

Object resolvers

In GraphQL, we can define resolvers for fields on an object to add additional properties to the object and create relationships between objects. This allows GraphQL to build very flexible APIs while maintaining simplicity.

When using GQLoom, we can use the resolver.of function to define object resolvers.

We start by defining two simple objects User and Book:

valibot
zod
import * as v from "valibot" const User = v.object({ __typename: v.nullish(v.literal("User")), id: v.number(), name: v.string(), }) interface IUser extends v.InferOutput<typeof User> {} const Book = v.object({ __typename: v.nullish(v.literal("Book")), id: v.number(), title: v.string(), authorID: v.number(), }) interface IBook extends v.InferOutput<typeof Book> {}

In the above code, we have defined two objects User and Book which represent user and book. In Book, we define an authorID field, which represents the author ID of the book.

In addition, we define two simple Map objects to store some predefined data:

const userMap: Map<number, IUser> = new Map( [ { id: 1, name: "Cao Xueqin" }, { id: 2, name: "Wu Chengen" }, ].map((user) => [user.id, user]) ) const bookMap: Map<number, IBook> = 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:

valibot
zod
import { resolver, query } from '@gqloom/core' import * as v from "valibot" const BookResolver = resolver.of(Book, { books: query(v.array(Book), () => 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:

valibot
zod
import { resolver, query, field } from '@gqloom/core' import * as v from "valibot" const BookResolver = resolver.of(Book, { books: query(v.array(Book), () => Array.from(bookMap.values())), author: field(v.nullish(User), (book) => userMap.get(book.authorID)), })

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.

valibot
zod
import { resolver, query, field } from '@gqloom/core' import * as v from "valibot" const BookResolver = resolver.of(Book, { books: query(v.array(Book), () => Array.from(bookMap.values())), author: field(v.nullish(User), (book) => userMap.get(book.authorID)), signature: field(v.string(), { input: { name: v.string(), }, resolve: (book, { name }) => `The book ${book.title} is in ${name}'s collection.`, }), })

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 function:

import { weave } from '@gqloom/core' export const schema = weave(BookResolver)

The resulting GraphQL schema is as follows:

type Book { id: ID! title: String! authorID: ID! author: User signature(name: String!): String! } type User { id: ID! name: String! } type Query { books: [Book!]! }