Graphql-Access arguments in child resolvers
Asked Answered
U

3

20

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?

Umberto answered 22/1, 2018 at 13:37 Comment(0)
N
24

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.

Nath answered 22/1, 2018 at 14:11 Comment(3)
Please have look at this comprehensive link also prisma.io/blog/…Bos
Please don't do this. It leads to tight coupling between resolvers and doesn't scale up well. Each resolver should receive its own args directly from the query. Please check cYee's answer for more: https://mcmap.net/q/610175/-graphql-access-arguments-in-child-resolversGeneralist
This looks like a workaround, it's not a proper solution.Thomasson
R
22

TLDR: Add your arguments to the field

(Client Side) change from:

Car(type: $type, materialType: $materialType){
  id
  material
  name
  ...
}

(Client Side) To:

Car(type: $type){
  id,
  material(materialType: $materialType) // moved here
  name
  ...
}

Then, You can access args in server fieldResolver (material field in this case).

Longer version

Do not pass your argument through root, except IDs or parent object, anything from client, use field level argument.

Why?

  1. Tight Coupling and hard to scale up schemas,

  2. Difficult to troubleshoot and debug,

  3. Leaking unnecessary information to children,

  4. Mixing up parent object with arguments

A simple query can grow from this:

[Root] Car(
  color:white
) {
  id,
  seat,
  ...
}

To this:

[Root] Car(
  color:white, 
  type:sedan, 
  seat:leather, 
  seatColor:black,
  rimColor: blue,
  rimShape: OutOfTheWorld,
  ...
) {
  id,
  seat,
  ...
}

Instead of passing the argument around, you can do this

[Root] Car(
  color:white, 
  type:sedan
  ...
) {
  id
  seat(type:leather, color:black),
  rim(color: blue, shape: OutOfTheWorld){
    // nested query
    material(hardness: high), // solved `Why no.2`: deep argument. 
    
    // More nested
    brand(trustWorthy: high) {
      priceRange(range: mid),
      area,
      ...
    },
    id
  }
  numberOfPassengers,
  ...
}

instead of squeezing all args into single root, now each field is responsible for its args and resolver.

Roley answered 7/8, 2020 at 10:40 Comment(2)
This is the correct answer. Args shouldn't be shared between resolvers; this leads to coupling and it's very hard to scale up schemas that wayGeneralist
the best and only correct answer! thanks! it is rather hard to find clair documentation on how to handl this issue!Seaquake
M
20

If you know you are using variables there is another way, other than the accepted answer, using the fourth argument of the resolver function: info.

This info argument contains a field variableValues amongst other fields. This field doesn't strictly contain the parent's args, but if your operation is executed with variables that are passed to the parent resolver, then you'll have access to them via the info.variableValues from all the relevant resolver functions.

So if your operation is called like this for example:

query GetTotalVehicalsOperation($color: String) {
  getTotalVehicals(color: $color) {
    totalCars
    totalTrucks   
  }
}

... with variables: {color: 'something'}

you'll have access to the variables from the other resolvers:

{
  RootQuery: {
    getTotalVehicles: async (root, args, context, info) => {
      //info.variableValues contains {color: 'something'}          
      return {};
    },
    TotalVehicleResponse: {
      totalCars: async (root, args, context, info) => {
        //same here: info.variableValues contains {color: 'something'}
      },
      totalTrucks: async (root, args, context, info) => {
        //and also here: info.variableValues contains {color: 'something'}
      }
    }
  }
}
Muleteer answered 22/1, 2018 at 14:38 Comment(3)
FYI, this only works when using variables. So it may not a good idea to rely on info.Brainard
Thanks, @Trevor. I've updated my answer to clarify that.Muleteer
Please have look at this comprehensive link also prisma.io/blog/…Bos

© 2022 - 2024 — McMap. All rights reserved.