Resolvers
Resolvers extend your GraphQL API and can be used to run custom business logic.
You can build locally using the Grafbase CLI and control access to resolvers using auth rules. Resolvers automatically deployed when you push to GitHub.
Resolvers can be added to extend connected APIs, or the schema with query and mutation resolvers.
Resolvers are made up of two major components:
- The field definition with a
resolver
property - The resolver function implementation inside
resolvers
Inside grafbase.config.ts
add the query hello
:
import { g } from '@grafbase/sdk'
g.query('hello', {
args: { name: g.string().optional() },
returns: g.string(),
resolver: 'hello',
})
Add the following to the file resolvers/hello.js
:
export default function Resolver(_, { name }) {
return `Hello ${name || 'world'}!`
}
Now run the Grafbase CLI to start the local development server:
npx grafbase dev
Finally execute the following GraphQL query:
query {
hello(name: "Grafbase")
}
You must declare any input or return types and pass the name of the resolver to connect.
import { g } from '@grafbase/sdk'
const user = g.type('User', {
name: g.string().optional(),
email: g.email().optional(),
})
g.extend(user, {
gravatar: {
returns: g.url(),
resolver: 'user/gravatar',
},
})
All resolvers must be placed inside the folder resolvers
.
You can also use folders to separate your resolver types for queries, mutations, and fields.
.
└── package.json
└── resolvers
└── user
└── gravatar.js
└── grafbase.config.ts
Resolver functions have the following arguments in this order:
root
: The root of the current query/mutation/fieldargs
: The arguments provided to the query/mutation/fieldcontext
: The context from the Grafbase server/projectinfo
: The execution information for the current operation
export default function Resolver(root, args, context) {
// return ...
}
The first parameter root
(sometimes called parent
) is the result of the previous call.
import { g } from '@grafbase/sdk'
const user = g.model('User', {
name: g.string().optional(),
email: g.email().optional(),
gravatar: g.url().optional().resolver('user/gravatar'),
})
The second parameter passed to the resolver function is known as args
.
These arguments are defined by you and work with query, mutation and field resolvers.
import { g } from '@grafbase/sdk'
const checkoutLineItem = g.input('CheckoutLineItem', {
price: g.string(),
quantity: g.int().default(1),
})
const checkoutSessionInput = g.input('CheckoutSessionInput', {
lineItems: g.inputRef(checkoutLineItem).list(),
})
const checkoutSession = g.type('CheckoutSession', {
url: g.url()
sum: g.int().resolver('sum').arguments({
a: g.int().optional().default(0),
b: g.int().optional().default(0)
})
})
g.query('sum', {
args: { a: g.int().optional().default(0), b: g.int().optional().default(0) },
returns: g.int(),
resolver: 'sum',
})
g.mutation('checkout', {
args: { input: g.inputRef(checkoutSessionInput) },
returns: g.ref(checkoutSession),
resolver: 'checkout',
})
The third argument referred to as context
contains important information from Grafbase about the request.
You can obtain the forwarded request headers and any claims from valid JWTs from inside the context
argument:
export default function Resolver(_, __, { request }) {
const { headers, jwtClaims } = request
// ...
}
This is useful if you need to use the current Authorization
header from the user management platform with whatever next action you take inside the resolver.
The fourth argument passed to the resolver function is called info
. We don't currently expose the full GraphQLResolverInfo
object but instead a limited set:
fieldName
(String
) — The name of the field that called the resolver.path
(ResponsePath
) — The fields traversed prior to the called resolver.variableValues
({ [variableName: string]: mixed }
) — A map of any variables passed to the query.
export default function Resolver(_, __, ___, info) {
const { fieldName, path, variableValues } = info
// ...
}
The Query type is at the root of your GraphQL API which contains any auto-generated queries by the Grafbase Database, and also any custom resolvers you have added.
import { g } from '@grafbase/sdk'
g.query('hello', {
returns: g.string(),
resolver: 'hello-world',
})
Mutations are used when you want to mutate data. The GraphQL API contains mutations auto-generated by the Grafbase Database for all models and any custom resolvers you have added.
import { g } from '@grafbase/sdk'
g.mutation('say', {
args: { word: g.string() },
returns: g.string(),
resolver: 'say',
})
Resolvers can be attached to fields and fields can access the data stored inside the model using the first argument (root
):
import { g } from '@grafbase/sdk'
const location = g.type('Location', {
latitude: g.float(),
longitude: g.float(),
})
const place = g.type('Place', {
name: g.string().optional(),
location: g.ref(location).optional(),
})
g.extend(place, {
weather: {
returns: g.float().optional(),
resolver: 'place/weather',
},
})
Grafbase is designed to automatically conceal any errors that occur within resolvers, ensuring that internal operations remain hidden from the client. However, in certain scenarios, it may be necessary to reveal expected errors.
To accomplish this, you can throw a GraphQLError
from the graphql
package.
import { GraphQLError } from 'graphql'
function Resolver(_, { args }) {
const { number } = args
if (number >= 10) {
throw new GraphQLError(
`You must provide a number less than 10. You passed ${number}!`,
)
}
// ...
}
Make sure you have graphql
installed as a dependency.
Resolvers can use environment variables set using the CLI or in your project settings.
import { g } from '@grafbase/sdk'
g.query('hello', {
returns: g.string(),
resolver: 'hello',
})
Make sure the .env
file is inside your project's grafbase
folder.
You should have a package.json
inside your project root folder to install dependencies to be used by resolvers.
import { g } from '@grafbase/sdk'
const address = g.type('Address', {
line1: g.string().optional(),
city: g.string().optional(),
country: g.string().optional(),
})
const customer = g.type('Customer', {
id: g.id(),
balance: g.int().optional(),
email: g.email().optional(),
address: g.ref(address).optional(),
})
g.query(place, {
weather: {
returns: g.ref(customer).list(),
resolver: 'customers',
},
})
Resolvers can be written in TypeScript (.ts
) and JavaScript (.js
).
The package manager used to install your dependencies and build your resolvers is inferred from the packageManager
key from your project's package.json
, or by the presence of different lock files.
You can inject configuration by defining the NPM_RC
environment variable. When defined, its contents will be written to a .npmrc
file at the project root.
You can enable caching for resolvers using the cache config:
import { g } from '@grafbase/sdk'
const user = g.model('User', {
name: g.string().optional(),
email: g.email().optional(),
gravatar: g
.url()
.optional()
.resolver('user/gravatar')
.cache({ maxAge: 60, staleWhileRevalidate: 60 }),
})