Resolvers
Introduction
Resolvers allows you to define Queries, Mutations and Field resolvers like normal class methods similar to REST controllers.
This allows you to define your schema and plug your business-logic in the same place.
Query and Mutation
Resolver Class
A Resolver is simply a class decorated with @Resolver.
import { } from '@foadonis/graphql'
@()
class {}Resolvers must be registered to be taken in account in your GraphQL schema:
import from '@foadonis/graphql/services/main'
.([
() => import('#graphql/resolvers/recipe_resolver'),
() => import('#graphql/resolvers/user_resolver'),
])It supports DI allowing you to inject services directly into the constructor:
import { } from '@foadonis/graphql'
@()
class {
constructor(private : ) {}
}Resolvers can be created using the make:resolver command:
node ace make:resolver recipeQuery
By decorating a class method using @Query it will mark it as a query and automatically use it to handle the operations.
import { } from '@foadonis/graphql'
@()
class {
constructor(private : ) {}
@Query(() => [Recipe])
() {
return this..all()
}
}As RecipeService.all() returns a Promise<Recipe[]> it is not possible to infer the returned type and must be explicitly defined.
Arguments
Usually, queries have arguments allowing the client to perform pagination, search, etc. This can be done in two ways.
The @Arg decorator allows you to define each argument directly in your function parameters.
import { , } from '@foadonis/graphql'
@()
class {
@Query(() => [Recipe])
(
@('page') : number
@('limit', { : 10 }) ?: number,
@('query', { : true }) ?: string,
) {
return Recipe.query()
.if(, () => .limit(!))
.if(, () => .whereLike('title', `%${}%`))
.paginate()
}
}The @Args decorator allows you to use a class decorated with @ArgType as the arguments.
import { , } from '@foadonis/graphql'
@ArgsType()
class {
@Field()
declare : number
@Field({ : 10 })
declare : number
@Field({ : true })
declare ?: number
}
@()
class {
@Query(() => [Recipe])
(@Args() { , , }: ) {
return Recipe.query()
.if(, () => .limit(!))
.if(, () => .whereLike('title', `%${}%`))
.paginate()
}
}Mutation
Mutations are really similar to queries and are defined using the @Mutation decorator:
import { , } from '@foadonis/graphql'
@()
class {
constructor(private : ) {}
@(() => Recipe)
() {
return this..create()
}
}Inputs
In most cases for Mutation we use InputType rather than ArgType, this can be done using the @InputType() decorator
import { , , } from '@foadonis/graphql'
@()
class {
@Field()
: string
}
@()
class {
constructor(private : ) {}
@(() => Recipe)
(@Arg('data') : ) {
return this..create({ : . })
}
}Field Resolver
Queries and Mutations are the root elements of your GraphQL schema and field resolvers allow you to query relations when explicitly asked.
For example our Recipe as an author (User) and rather than fetching it every time we fetch a recipe we can do it only when asked by the client.
Field resolvers are really similar to queries. We first have to add our "root" type in @Resolver() and decorate our method using @FieldResolver.
import { , , } from '@foadonis/graphql'
@(() => Recipe)
class {
constructor(private : ) {}
@(() => Recipe)
(@Arg('recipeId') : number) {
return this..findOrFail()
}
@(() => User)
(@Root() : ) {
return .related('author').query().firstOrFail()
}
}Computed fields
For simpler resolvers you can use the @Field property directly on your ObjectType similar to computed fields.
However this must be avoided when your logic has side effects (api calls, database queries, etc).
@ObjectType()
class Recipe {
@Field()
title: string
@Field(() => [Number])
ratings: Rate[]
@Field({ deprecationReason: 'Use `title` instead' })
get name() {
return this.title
}
@Field(() => Float, { nullable: true })
averageRating(@Arg('since') sinceDate: Date): number | null {
const ratings = this.ratings.filter((rate) => rate.date > sinceDate)
if (!ratings.length) return null
const ratingsSum = ratings.reduce((a, b) => a + b, 0)
return ratingsSum / ratings.length
}
}