context
You might have noticed that in our load
and resolve
functions that we don't have the request
parameters readily available to us, we might however need them to check i.e. if a user is authorized, ...
This is done through the context
object which will be carried around throughout the whole request lifecycle.
Defining context
NOTE: If you're using Next.js, please see below on how to define context.
If we need context we need to add an _context.ts
file at the root and export a getContext
function:
import type { GetContext, InitialContext } from 'fuse'
// Ensures static typing throughout our fuse schema
declare module 'fuse' {
export interface UserContext {
ua: string | null
}
}
export const getContext = (
ctx: InitialContext,
): GetContext<{ ua: string | null }> => {
return {
ua: ctx.request.headers.get('user-agent'),
}
}
Now our ctx
attribute in our resolvers will be statically typed and this function
will execute on every invocation.
Next.js
Context has to be defined a bit differently in Next.js than otherwise. Using it in your API, however, stays the same.
API-Route
By default we will expose params
and headers
on the context object. If you want to expose more data you can do so by
adding it to the function invocation of your API handler.
import { createAPIRouteHandler } from 'fuse/next'
createAPIRouteHandler<{ userAgent: string }>({
context: (ctx) => {
return {
userAgent: ctx.headers.get('user-agent') || 'unknown',
}
}
})
Now the userAgent
will be available to all of our GraphQL resolvers!
Server components
In server-components
we don't have the headers
available by default when using the execute
function, this
to avoid automatically opting people into dynamic functions (opens in a new tab).
We'll have to pass these in with the second argument of our execute
function.
import { headers } from 'next/headers'
import { createAPIRouteHandler } from '@/fuse/server'
execute({
query: x,
variables: {},
context: () => {
return {
userAgent: headers().get('user-agent') || 'unknown'
}
}
})
This means that if you use
context.headers
or a related property in your resolvers that you will need to define this yourself if your executed document would tap into those resolvers.
Static typing
You can ensure that your Context
is typed in both your server
as well as your client code by overriding the global type in fuse
:
// Important so the surrounding types won't override
import 'fuse'
declare module 'fuse' {
// This basically means that the `context` needs to define
// a userId
export interface UserContext {
userId: string | null
}
}
Using context
in your API
In our fields there are two opportunities we have to use context
the first being during resolve
and the second during load
,
below you can find an example of both.
import { node, addQueryFields, AuthenticationError } from 'fuse'
const UserNode = node<UserSource>({
name: 'User',
load: async (ids, ctx) => ctx.isAdmin ? getUsers(ids) : [],
fields: (t) => ({
name: t.exposeString('name'),
// rename to camel-case
avatarUrl: t.exposeString('avatar_url'),
// Add an additional firstName field
firstName: t.string({
resolve: (user) => user.name.split(' ')[0],
}),
}),
})
addQueryFields((t) => ({
me: t.list({
type: UserNode,
nullable: false,
args: {
offset: t.arg.int({}),
limit: t.arg.int(),
filter: t.arg({ type: FilterInput }),
},
resolve: async (_, args, context) => {
if (context.userId) {
return context.userId
}
throw new AuthenticationError('You must be logged in.')
},
}),
}))