Trigger nested resolvers with GraphQL

2020-02-02 03:17发布

问题:

I think I'm missing something obvious in the way GraphQL resolvers work. This is a simplified example of my schema (a Place that can have AdditionalInformation):

import { ApolloServer, gql } from 'apollo-server';

const typeDefs = gql`
  type Place {
    name: String!
    additionalInformation: AdditionalInformation
  }

  type AdditionalInformation {
    foo: String
  }

  type Query {
    places: [Place]
  }
`;

And the associated resolvers:

const resolvers = {
  Query: {
    places: () => {
        return [{name: 'Barcelona'}];
    }
  },
  AdditionalInformation: {
    foo: () => 'bar'
  }
};

const server = new ApolloServer({typeDefs, resolvers});

server.listen().then(({ url }) => {
  console.log(`API server ready at ${url}`);
});

When I execute a basic query:

{
  places {
    name,
    additionalInformation {
      foo
    }
  }
}

I always get null as the additionalInformation:

{
  "data": {
    "places": [
      {
        "name": "Barcelona",
        "additionalInformation": null
      }
    ]
  }
}

It's my first GraphQL app, and I still don't get why the AdditionalInformation resolver is not automatically executed. Is there some way to let GraphQL know it has to fire it?

I've found this workaround but I find it a bit tricky:

Place: {
  additionalInformation: () => { return {}; }
}}

回答1:

Let's assume for a moment that additionalInformation was a Scalar, and not an Object type:

type Place {
  name: String!
  additionalInformation: String
}

The value returned by the places resolver is:

[{name: 'Barcelona'}]

If you were to make a similar query...

query {
  places {
    name
    additionalInformation
  }
}

What would you expect additionalInformation to be? It's value will be null because there is no additionalInformation property on the Place object returned by the places resolver.

Even if we make additionalInformation an Object type (like AdditionalInformation), the result is the same -- the additionalInformation field will resolve to null. That's because the default resolver (the one used when you don't specify a resolver function for a field) simply looks for a property with the same name as the field on the parent object. If it fails to find that property, it returns null.

You may have specified a resolver for a field on AdditionalInformation (foo), but this resolver is never fired because there's no need -- the whole additionalInformation field is null so all of the resolvers for any fields of the associated type are skipped.

To understand why this is a desirable behavior, imagine a different schema:

type Article {
  title: String!
  content: String!
  image: Image
}

type Image {
  url: String!
  copyright: String!
}

type Query {
  articles: [Article!]!
}

We have a database with an articles table and an images table as our data layer. An article may or may not have an image associated with it. My resolvers might look like this:

const resolvers = {
  Query: {
    articles: () => db.getArticlesWithImages()
  }
  Image: {
    copyright: (image) => `©${image.year} ${image.author}`
  }
}

Let's say our call getArticlesWithImages resolves to a single article with no image:

[{ title: 'Foo', content: 'All about foos' }]

As a consumer of the API, I request:

query {
  articles {
    title
    content
    image
  }
}

The image field is optional. If I get back an article object with a null image field, I understand there was no associated image in the db. As a front end client, I know not to render any image.

What would happen if GraphQL returned a value for the image regardless? Obviously, our resolver would break, since it would not be passed any kind of parent value. Moreover, however, as a consumer of the API, I would have to now parse the contents of image and somehow determine whether an image was in fact associated with the article and I should do something with it.

TLDR;

As you already suggested, the solution here is to specify a resolver for additionalInfo. You can also simply return that value in your places resolver, i.e.:

return [{name: 'Barcelona', additionalInfo: {}}]

In reality, if the shape of your schema aligns with the shape of your underlying data layer, it's unlikely you'll encounter this sort of issue when working with real data.



标签: graphql