4

I didn't know how to title this question but here's what I am uncertain about.

I have a React frontend that is making a GraphQl query to our GraphQl midlayer which aggregates data by making calls to our legacy REST api.

So for example in React I can call the getCustomer query:

query getCustomer($id: Int!) {
    getCustomer(id: $id) {
      name
      email
  }
}

which will hit the getCustomer resolver which then makes a request to our REST customers/{id} endpoint to return our data.

async function getCustomer(_, { id }, ctx) {
  const customer = await ctx.models.customer.getCustomer(id);
  return customer;
}

This request is fine if I am printing a list of customers. But where my questions comes into play is how can I make conditional API requests in my resolver based on data I am querying?

Say each customer can have multiple addresses and these addresses live on a different endpoint. I would love to get those addresses like this in my frontend:

query getCustomer($id: Int!) {
    getCustomer(id: $id) {
      name
      email
      address {
        city
      }
  }
}

How could I have my resolver handle this based on my types and schemas? Something fundamentally like this:

async function getCustomer(_, { id }, ctx) {
  const customer = await ctx.models.customer.getCustomer(id);

  [If the query includes the address field]
    const addresses = await ctx.models.customer.getAddressesByCustomer(id);
    customer.addresses = addresses;
  [/If]

  return customer;
}

Ultimately, the goal is to have the getCustomer resolver be capable of returning all customer data across various endpoints based on what fields are sent in the query but not making those additional API requests if the field isn't requested.

Yuschick
  • 2,642
  • 7
  • 32
  • 45

3 Answers3

4

There are effectively two ways to do this. The first relies on how GraphQL executes requests. A field's resolver will only be called if 1) the "parent" field is not null and 2) the field in question is actually requested. This means we can provide a resolver for the address field explicitly:

const resolvers = {
  Customer: {
    addresses: () => {
      return ctx.models.customer.getAddressesByCustomer(id)
    },
  },
}

In this way, the resolver will be called for a query like

{
  query getCustomer($id: Int!) {
    getCustomer(id: $id) {
      name
      address {
        city
      }
    }
  }
}

but will not be called for

{
  query getCustomer($id: Int!) {
    getCustomer(id: $id) {
      name
    }
  }
}

This approach works well enough when wrapping a simple REST API. Some REST APIs, however, allow you to request related resources through optional parameters. Similarly, if you're pulling the data out of a database, you can join additional tables to your query. The end result is more data in fewer roundtrips. In this case, you would do all the fetching at the root level (inside the getCustomer resolver). However, you would want to determine what fields the custom actually requested. To do this, you would parse the resolve info object, which is the fourth parameter passed to every resolver. Once you determine which fields were actually requested, you can make the appropriate changes to your URL or SQL query.

Daniel Rearden
  • 80,636
  • 11
  • 185
  • 183
  • This is super helpful. But one connection I am missing is when I query `address` how does that field map to `addresseses` in the resolver? Is it a naming convention I need to follow or what? – Yuschick Jan 24 '20 at 12:21
  • This is just example code -- if your field is named `address` then the `resolvers` map should reflect that. I don't know if it makes sense to name a field `address` when it returns an array, but it's your schema :) – Daniel Rearden Jan 24 '20 at 12:27
  • Thank you. Yeah, it's a lot of psuedo code but I look to have this working. Thanks a lot! – Yuschick Jan 24 '20 at 12:53
0

Apollo checks/knows which fields are fullfilled and what is needed to ask additionally. It calls one (main, top level) resolver (for returning f.e. name and email) and calls additional resolvers when returned object doesn't contain required data (field/childs/related).

Quick workaround: use [the power of] ctx info paramater to check what fields was required.

xadm
  • 8,219
  • 3
  • 14
  • 25
0

Your resolvers should look like this:

const resolvers = {
  Query: {
    getCustomer(id) {
      // return customer object
    }
  },
  Customer: {
    addresses(customer) {
      // return addresses of the customer
    }
  }
}

The graphql server will take care of calling appropriate resolver.

If you are defining your schema pragmatically, you can define resolver along with field definition.