I am using apollo-server and apollo-graphql-tools and I have following schema
type TotalVehicleResponse {
totalCars: Int
totalTrucks: Int
}
type RootQuery {
getTotalVehicals(color: String): TotalVehicleResponse
}
schema {
query: RootQuery
}
and Resolver functions are like this
{
RootQuery: {
getTotalVehicals: async (root, args, context) => {
// args = {color: 'something'}
return {};
},
TotalVehicleResponse: {
totalCars: async (root, args, conext) => {
// args is empty({}) here
.........
.........
},
totalTrucks: async (root, args, conext) => {
// args is empty({}) here
.........
.........
}
}
}
}
My question is that how can I access args
which is available in root resolver(getTotalVehicals
) in any of the child resolvers?
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;
}
}
}
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.
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.
The real issue here is one of schema design. Good schema design eliminates tight coupling between types and enables reuse across the schema.
I tend to think of resolvers in two classes:
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
}
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
}
}
}
}
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.