args
refer strictly to the arguments provided in the query to that field. If you want values to be made available to child resolvers, you can simply return them from the parent resolver, however, this isn't a good solution since it introduces coupling between types.
const resolvers = {
RootQuery: {
getTotalVehicles: async (root, args, context) => {
return { color: args.color };
},
},
TotalVehicleResponse: {
totalCars: async (root, args, context) => {
// root contains color here
const color = root.color;
// Logic to get totalCars based on color
const totalCars = await getTotalCarsByColor(color);
return totalCars;
},
totalTrucks: async (root, args, context) => {
// root contains color here
const color = root.color;
// Logic to get totalTrucks based on color
const totalTrucks = await getTotalTrucksByColor(color);
return totalTrucks;
}
}
}
Update 06/2024
In hindsight, I would have answered this question differently. GraphQL offers endless flexibility with very few guardrails and it is very easy to design schema and resolver patterns that are tightly coupled. The question asked here is a symptom of a design that does exactly that. At the time when this question was asked and answered, the trend for typed schemas was still in its infancy, but nowadays, with fully typed schemas, it is easier to fall into good patterns.
1. Add arguments to the fields
The simplest solution is to add the arguments to the fields that require them, thereby providing access to the args
in the associated resolver. Adding this to the schema in question would look like the following:
type TotalVehicleResponse {
totalCars(color: String): Int
totalCars(color: String): Int
totalTrucks(color: String): Int
}
type Query {
getTotalVehicles(color: String): TotalVehicleResponse
}
When multiple fields require the same argument, you may find the query becomes verbose and repetitive. This is managed using query variables.
The query might look as follows:
query($color: String) {
getTotalVehicles(color: $color) {
totalCars(color: $color)
totalTrucks(color: $color)
}
}
This approach is preferable to my original answer since it removes the tight coupling between schema types and resolvers, but it doesn't make for a pleasant consumable API.
2. Schema Design Principles
The real issue here is one of schema design. Good schema design eliminates tight coupling between types and enables reuse across the schema.
Type Resolvers and Field Resolvers
I tend to think of resolvers in two classes:
- Type Resolvers: Fields that return an object type.
- Field Resolvers: Fields that return scalar values (e.g., String, Int).
Consider the following schema:
type Query {
car(id: ID!): Car! # Type resolver
carsByColor(color: String!): [Car!] # Type resolver with argument
brands: [Brand!] # Type resolver
}
type Car {
id: ID!
brand: Brand! # Type resolver
model: String
color: String
}
type Brand {
id: ID!
name: String
cars(color: String): [Car!] # Type resolver with argument
}
Example Query
Using the above schema, here’s an example query to get cars by a specific color and the brand of each car:
query($color: String!) {
# Type resolver with argument
carsByColor(color: $color) {
id
model
color
brand {
id
name
# Type resolver with argument
cars(color: $color) {
id
model
color
}
}
}
}
Resolver Implementation
The goal is to have consistency across the schema for a given type. Wherever a type is resolved, we should return a known type.
For example, a car object should always return an identifier or typed parent object. This provides us with guarantees that wherever a car is presented as a parent, it is of a known type, and therefore has expected properties, e.g., ID which can be used to compute other fields, or fetch related entities.
The effect is that it enables simple and reusable data fetching patterns to fulfil types as they are requested.
const resolvers = {
Query: {
car: async (_, args) => {
const car = await getCar(args.id);
return car;
},
carsByColor: async (_, args) => {
const cars = await getCarsByColor(args.color);
return cars;
},
brands: async () => {
const brands = await getAllBrands();
return brands;
}
},
Car: {
brand: async (parent) => {
const brand = await getBrand(parent.brandId);
return brand;
}
},
Brand: {
cars: async (parent, args) => {
const cars = await getCarsByBrandAndColor(parent.id, args.color);
return cars;
}
}
};
Notice that only the type resolvers have been implemented in this example. Field resolvers are gnerally only necessary when their results are either computed or fetched from a different source. In most cases, simply relating the types and resolver type resolvers will achieve the desired result.