InteractiveFrameworks

Resolvers and arguments

Let the graph connect: a cat's owner and an owner's cats resolved only when a query asks for them, and the cats query taking a filter and paging through a class of arguments with defaults.

What you'll learn

  • Resolve a field on demand with @ResolveField() and @Parent(), so that it costs nothing unless a query selects it
  • Split one type's fields across resolvers, so each module resolves what it knows
  • Gather a query's arguments in an @ArgsType() class, with optional arguments and defaults
  • Read the arguments and their defaults the schema publishes, through introspection

A cat has an owner, and an owner has cats. In a REST API that becomes a design question: embed the owner in every cat response, which is heavy when nobody wants it, or make clients call /owners/:id afterwards, which is slow when they do. GraphQL answers with a graph. Cat.owner is a field like any other, it costs nothing until a query selects it, and a client can walk from a cat to its owner to the owner's other cats in one request. The same holds for arguments: a list can take a filter and a page size, declared once and documented in the schema.

Fields resolved on demand

So far every field has been a property that already exists on the object the query returned. A field resolver computes a field instead. It is a method on a resolver, marked @ResolveField(), and it receives the object being resolved through @Parent():

@Resolver(() => Volunteer)
export class VolunteersResolver {
  @ResolveField(() => Shelter)
  shelter(@Parent() volunteer: Volunteer): Shelter {
    return this.shelters.findOne(volunteer.shelterId);
  }
}

GraphQL runs it once for every Volunteer in the answer whose selection includes shelter, and never otherwise. The stored shelterId does not need to be a field at all: a property without @Field() stays on the object for the resolver and out of the schema.

A type's fields do not all have to come from one resolver class. Any @Resolver(() => Shelter) can add a field to Shelter, so a field that needs another module's data can live in that module. A Shelter.volunteers field can be resolved in the volunteers module, and the shelters module never needs to know volunteers exist. That also avoids a cycle of modules importing each other. Every resolver must be a provider in some module, or its fields are not in the schema.

Arguments, gathered in a class

@Args('id') declares one argument. When a query takes several, an @ArgsType() class gathers them, each a @Field() with the same type rules as object types, and @Args() with no name takes the whole class:

@ArgsType()
export class ShiftsArgs {
  @Field(() => String, { nullable: true })
  day?: string;

  @Field(() => Int, { defaultValue: 20 })
  limit: number;
}

@Query(() => [Shift])
shifts(@Args() { day, limit }: ShiftsArgs) { ... }

A nullable argument is optional and arrives as undefined when the query leaves it out. defaultValue makes an argument optional in the query, with the default filled in by GraphQL itself. The schema publishes the default, so every client can read it. Without a default, an omitted integer arrives as undefined, and arithmetic on it quietly gives NaN.

The schema is inspectable at runtime: GraphiQL's documentation panel is built from an introspection query, and so is the last request graded here, which reads the arguments of cats straight from __type(name: "Query").

Your task

Owner is a type now, with an owners query and an OwnersService. Each cat stores an ownerId, which is not a field. CatsModule imports OwnersModule to reach its service.

  1. Write CatsArgs: an optional breed, then integers skip (default 0) and take (default 10). The cats query takes them: keep only cats of breed when it is given, then skip and take, in the service's order.
  2. Give Cat an owner field: the owner with the cat's ownerId, or null for a cat that has not gone home.
  3. Give Owner a cats field, the cats that went home with them, from a second resolver for Owner in the cats module, OwnerCatsResolver. Register it.

Then ask GraphiQL for a cat, its owner, and the owner's cats in one query.

When it fails

  • Cannot query field "cats" on type "Owner".: OwnerCatsResolver is not in any module's providers, so its field never reached the schema.
  • Unknown argument "skip" on field "Query.cats".: that property of CatsArgs has no @Field().
  • { cats } answers []: skip or take has no default, so an omitted one is undefined and the page is empty.
  • Cannot determine a GraphQL input type ("CatsArgs") for the "args": @Args('args') treats the class as one input object. An @ArgsType() is taken with a bare @Args().

Remember

  • @ResolveField() computes a field from its @Parent(), only when the query selects it.
  • Several resolvers may add fields to one type; each must be a provider.
  • @ArgsType() gathers arguments; nullable makes one optional, defaultValue gives it a published default.
  • Properties without @Field() stay on the object and out of the schema.
Stuck? Show a hint

cats.args.ts: @ArgsType() class with @Field(() => String, { nullable: true }) breed and @Field(() => Int, { defaultValue: 0 }) skip / { defaultValue: 10 } take; the query takes @Args() args: CatsArgs. The owner field: @ResolveField(() => Owner, { nullable: true }) owner(@Parent() cat: Cat) in CatsResolver, with OwnersService injected. owner-cats.resolver.ts: @Resolver(() => Owner) with @ResolveField(() => [Cat]) cats(@Parent() owner: Owner), listed in CatsModule's providers.