graphqlapollographql-jsapollo-server

Graphql-Access arguments in child resolvers


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?


Solution

  • 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:

    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.