0
votes

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.
    }
  }
}
1
what's your __resolveType function look like? - Daniel Rearden
Sorry for the formatting: { IPerson: { __resolveType: function( parent, context, info ) { if ( parent.memberType ) { return 'ClubMember'; } ...etc. } - diekunstderfuge

1 Answers

0
votes

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.