How to resolve union/interface fields with GraphQL and ApolloStack
Asked Answered
V

1

15

I am making use of interfaces with my GraphQL instance, but this question perhaps applies to unions as well. There are 2 common fields across all types which implement the interface, however there are multiple additional fields on each type.

Given the following schema

interface FoodType {
  id: String
  type: String
}

type Pizza implements FoodType {
  id: String
  type: String
  pizzaType: String
  toppings: [String]
  size: String
}

type Salad implements FoodType {
  id: String
  type: String
  vegetarian: Boolean
  dressing: Boolean
}

type BasicFood implements FoodType {
  id: String
  type: String
}

and the following resolvers

{
  Query: {
    GetAllFood(root) {
      return fetchFromAllFoodsEndpoint()
        .then((items) => {
          return mergeExtraFieldsByType(items);
        });
    },
  },
  FoodType: {
    __resolveType(food) {
      switch (food.type) {
        case 'pizza': return 'Pizza';
        case 'salad': return 'Salad';
        default: return 'BasicFood';
      }
    },
  },
  Pizza: {
    toppings({pizzaType}) {
      return fetchFromPizzaEndpoint(pizzaType);
    }
  }
}

How do I obtain the additional fields for each type?

Currently, I have the allFood fetching all foods to obtain the basic fields of id and type. After this I am looping over the results, and if any of found of the type Pizza, I make a calling to fetchFromPizzaEndpoint, obtaining the additional fields and merging those onto the original basic type. I repeat this for each type.

I am also able to manually resolve specific fields, one at a type, such as the Pizza.toppings, as seen above.

Now my solution is not ideal, I would much rather be able to resolve multiple fields for each type, much the same way I do with the single field toppings. Is this possible with GraphQL? There must be a better way to achieve this, seeing as it's quite a common use case.

Ideally, I would like to be able to know in my resolver, what fragments my query is asking for, so I can only make calls to endpoints which are asked for (one endpoint per fragment).

{
  Query: {
    GetAllFood(root) {
      return fetchFromAllFoodsEndpoint();
    },
  },
  FoodType: {
    __resolveType(food) {
      switch (food.type) {
        case 'pizza': return 'Pizza';
        case 'salad': return 'Salad';
        default: return 'BasicFood';
      }
    },
  },
  Pizza: {
    __resolveMissingFields(food) {
      return fetchFromPizzaEndpoint(food.id);
    }
  },
  Salad: {
    __resolveMissingFields(food) {
      return fetchFromSaladEndpoint(food.id);
    }
  }
}
Vitiate answered 13/1, 2017 at 8:56 Comment(5)
Can you elaborate on what is returned by each dataLoader? This use-case looks quite foreign to me, although we do have several cases of union types in our server implementation. For us, we grab whatever data the union contains from the DB (at which point we already have all the data without needing to load more) and then decide which type GraphQL should return the data as (in its type resolver).Alkanet
The DataLoaders are simply the Facebook DataLoaders which fetch data, the source of the data is unimportant. Currently, I am doing exactly as you mentioned however it is inefficient as I have to fetch more data than I am going to use. Example, if I only fragment "Pizza" I should not pull "Salad" properties down. It would be great to have fragment level resolvers, but after speaking with the GraphQL devs, it seems it's a future feature and not currently supported.Vitiate
That's the bit that I don't understand. How would you get Salad properties if your data is actually a Pizza? That's why it matters which data are returned by the DataLoaders. We load data from MongoDB, for example. If you query a document from MongoDB, it contains what it contains (i.e. only the "Pizza" data for a Pizza document) and nothing else. There is no inefficiency there. If you mean there is an inefficiency in the data being returned from GraphQL, that's what the union type handles for you perfectly. If that's the inefficiency you're concerned about, I will write a more detailed answerAlkanet
You have the benefit of calling a database directly and structuring your query as you see fit (in this case, MongoDb), whilst in my instance I am speaking to a number of REST web services. So I call Query.GetAllFood - this hits on service to get everything. However if I do a fragment of "... on Pizza { toppings }", I want to at that point know about the fragment and call another web service endpoint to get the pizza data and merge it on my result. Currently I call all services and merge everything which isn't a great solution.Vitiate
Can you update your question to include that information? I personally found it hard to understand without it. When I get a moment I will write an answer to your question if I have one. In fact, it'd be better if you remove all mentions of dataLoader entirely and just use something like fetchFromPizzaEndpoint(), fetchFromAllFoodsEndpoint() etc.Alkanet
N
20

I know this question is 5 months old, but I hope this helps anyone else with this problem. He is passing his resolvers structured like

{
    Query: {
        GetAllFood(root) {
        return fetchFromAllFoodsEndpoint()
            .then((items) => {
            return mergeExtraFieldsByType(items);
            });
        },
    },
    FoodType: {
        __resolveType(food) {
        switch (food.type) {
            case 'pizza': return 'Pizza';
            case 'salad': return 'Salad';
            default: return 'BasicFood';
        }
        },
    },
    Pizza: {
        toppings({pizzaType}) {
        return fetchFromPizzaEndpoint(pizzaType);
        }
    }
}

But he really wanted something like (not exactly, but I'm stressing the location of __resolveType relative to Query)

{
    Query: {
        GetAllFood(root) {
        return fetchFromAllFoodsEndpoint()
            .then((items) => {
            return mergeExtraFieldsByType(items);
            });
        },
    },
    FoodType: {
        __resolveType(data, ctx, info) {
            return whatIsTheType(data, ctx, info)
        }
    }
}

The official documentation has an example here, but it ONLY includes the interface type, which I found confusing. I have an additional complete runnable example of Union types (configured identically to interfaces) available here

Nonresident answered 30/6, 2017 at 20:37 Comment(4)
I actually re-wrote it completely a few weeks ago doing exactly as you mentioned above. Almost forgot I had this question on here :)Vitiate
So what was the problem and how your answer resolved it?Greenwich
Does anyone know how to solve it when you build your schema with a string an buildSchema?Onieonion
To answer my comment. Functionality was added here and can be done by returning __typename on your object.Onieonion

© 2022 - 2024 — McMap. All rights reserved.