0

For some reason I'm having a hard time figuring out how to combine resolvers for a GraphQL interface and a type that implements said interface.

Say I have the following schema:

interface IPerson {
  id: ID!
  firstName: String!
  lastName: String!
}

type ClubMember implements IPerson {
  ...IPerson fields
  memberType: String!
  memberSince: DateTime!
}

type StaffMember implements IPerson {
  ...IPerson fields
  hireDate: DateTime!
  reportsTo: StaffMember
}

extend type Query {
  people(ids: [Int!]): [IPerson]
}

A full ClubMember query with all fields, such as:

query {
  people(ids: [123456,234567,345678]) {
    id
    firstName
    lastName
    ... on ClubMember {
      memberType
      memberSince
    }
  }
}

would produce a response like the following:

[
  {
    "id": 123456,
    "firstName": "Member",
    "lastName": "McMemberface",
    "memberType": "VIP",
    "memberSince": "2019-05-28T16:05:55+00:00"
  },
  ...etc.
]

I've used makeExecutableSchema() from apollo-server with inheritResolversFromInterfaces: true, and I want to be able to make use of default resolvers for each interface/type by having the model classes backing IPerson, ClubMember, etc. return objects with only the fields relevant to each type, i.e., the model class for IPerson fetches only the fields required by IPerson, etc. That is, the response above would execute 2 SQL statements:

SELECT id, firstName, lastName FROM Contacts WHERE id IN(?);

and

SELECT contactId, memberType, memberSince FROM Members WHERE contactId IN(?);

Of course, I could get all the data in one SQL statement by doing a JOIN at the database level, but I really want to have one (and only one) way of resolving the fields required by IPerson, and let the other types augment that data with their own resolvers.

My question is, do I need to "join" the resulting objects together myself in the resolver for the people query type? E.g.

const resolvers = {
  Query: {
    people: function( parent, args, context, info ) {
      let persons = context.models.Person.getByIds( args.ids );
      let members = context.models.Member.getByIds( args.ids );
      /*
      return an array of {...person, ...member} where person.id === member.id
      */
    }
  }
}

Or is there some way that Apollo handles this for us? Do I want something like apollo-resolvers? The docs on unions and interfaces isn't super helpful; I have __resolveType on IPerson, but the docs don't specify how the fields for each concrete type are resolved. Is there a better way to achieve this with Dataloader, or a different approach?

I think this question is related to my issue, in that I don't want to fetch data for a concrete type if the query doesn't request any of that type's fields via a fragment. There's also this issue on Github.

Many thanks!

Edit: __resolveType looks as follows:

{
  IPerson: {
    __resolveType: function ( parent, context, info ) {
      if ( parent.memberType ) {
        return 'ClubMember';
      }
      ...etc.
    }
  }
}
diekunstderfuge
  • 521
  • 1
  • 7
  • 15

1 Answers1

0

This problem really isn't specific to Apollo Server or even GraphQL -- querying multiple tables and getting a single set of results, especially when you're dealing with multiple data models, is going to get tricky.

You can, of course, query each table separately and combine the results, but it's not particularly efficient. I think by far the easiest way to handle this kind of scenario is to create a view in your database, something like:

CREATE VIEW people AS
    SELECT club_members.id           AS id,
           club_members.first_name   AS first_name,
           club_members.last_name    AS last_name,
           club_members.member_type  AS member_type,
           club_members.member_since AS member_since,
           null                      AS hire_date,
           null                      AS reports_to,
           'ClubMember'              AS __typename
    FROM club_members
    UNION
    SELECT staff_members.id         AS id,
           staff_members.first_name AS first_name,
           staff_members.last_name  AS last_name,
           null                     AS member_type,
           null                     AS member_since,
           staff_members.hire_date  AS hire_date,
           staff_members.reports_to AS reports_to
           'StaffMember'            AS __typename
    FROM staff_members;

You can also just use a single table instead, but a view allows you to keep your data in separate tables and query them together. Now you can add a data model for your view and use that to query all "people".

Note: I've added a __typename column here for convenience -- by returning a __typename, you can omit specifying your own __resolveType function -- GraphQL will assign the appropriate type at runtime for you.

Daniel Rearden
  • 80,636
  • 11
  • 185
  • 183
  • I'm not sure that I'll have the option to create new views, but I'll certainly see if I can. I suppose I just wish that I had something like factories/subclasses with DI like you'd find in some other data access patterns, such that the resulting concrete instance would match what's required by the schema and thus could leverage default resolvers. I think a lot of the docs/examples of GraphQL interfaces aren't good at specifying how to resolve extra fields while keeping the resolvers for interface fields DRY. – diekunstderfuge May 31 '19 at 13:44
  • Furthermore, is there even really a reason to return an interface type from a query, rather than simply exposing more specific query types? So instead of `query { people(ids: [...]) { ... } }`, why not just have a `members()` query and a `staff()` query? At least then you'd know immediately that you have to fetch specific data for each query, and the challenge would be just be making as much of the common SQL reusable as possible. – diekunstderfuge May 31 '19 at 13:51
  • Wrt to your second comment, that's totally dependent on the needs of the client consuming the API – Daniel Rearden May 31 '19 at 13:53
  • Wrt to your first comment, you really shouldn't have to write resolvers for fields like `firstName`, `lastName` etc. Ideally your model is already returning data with properties named `firstName`, `lastName`, etc. On the off chance you do need a resolver shared across multiple types that all implement the same interface, you can then leverage `inheritResolversFromInterfaces` and provide a single resolver in your resolvers map for the interface type rather than each implementing type. – Daniel Rearden May 31 '19 at 13:56
  • Yes, I mentioned that I'm using `inheritResolversFromInterfaces` and my model is already returning data with the correct properties for `IPerson` so that I don't have to write those resolvers, as you say, and the model for, e.g. `StaffMember` is returning data with the correct properties for _those_ fields, so I don't have to write resolvers for those fields, either. I guess I don't understand how the default resolvers for the interface can be "inherited" if there's no actual data fetching logic on the interface in the resolvers map. – diekunstderfuge May 31 '19 at 14:53
  • Apollo literally just iterates through the types in the schema and injects the interface's resolvers into the resolver map for each type that implements it. That's it. You can see the code [here](https://github.com/apollographql/graphql-tools/blob/43ab7efd51a5d96d408d2eb5bebe6cf398c80b36/src/generate/extendResolversFromInterfaces.ts). Maybe the word "inherit" is a bit misleading here. – Daniel Rearden May 31 '19 at 17:15