Problem
I'm using apollo-server to implement a GraphQL server that sits on top of a REST API.
The REST API returns an entity whose root-level field determines the shape of another root-level field. i.e...
{
"entityType": "foo",
"entityMetadata": { /* based on value of `entityType` */ },
}
In the GraphQL schema, the field whose shape is determined by the value of its sibling, is an abstract type. I want to write an apollo-server resolver for that abstract field. I think I should use __resolveType but I'm having trouble doing so because the args that I'm passing to the abstract field's __resolveType method do not include any way for me to reference the sibling field's value.
So what do I do?
Another Example
I'll use the following GraphQL schema to illustrate my issue.
type Character {
job: String!
weapon: Weapon
}
interface Weapon {
yearMade: Number
}
type LightSaber implements Weapon {
yearMade: Number
color: String
}
type Blaster implements Weapon {
yearMade: Number
ammo: Number
}
type Query {
searchCharacters: [Character]
}
With this schema, I have a Character type with job and weapon fields. That Character's job determines the type of the weapon they'll use.
So if the Character's job is "jedi" their weapon should be of the LightSaber type with an exclusive color field. If the Character's job is "stormtropper" their weapon will be of the Blaster type with an exclusive ammo field.
I want to search all available characters with the searchCharacters query and return their character data and their weapon's exclusive data depending on the weapon's type.
query {
searchCharacters {
type
weapon {
yearMade
... on LightSaber {
color
}
... on Blaster {
ammo
}
}
}
}
This works fine for all character data, but I hit a snag when I try to resolve the weapon's type. So far, I have this:
const Weapon = {
__resolveType: async ({ yearMade }, context, info): "LightSaber" | "Blaster" => {
// how do I resolve whether it's a LightSaber or Blaster?
},
};
Originally, I was under the impression that the parent field of the __resolveType() function would be the resolved Character object (i.e. { type: 'jedi', weapon: { yearMade: 8, ...otherWeaponProperties }, ...otherProperties }), but it's actually just the field's current object (i.e. { yearMade: 8, ...otherWeaponProperties }).
If I could access the Character's type field, I'd be able to determine what the weapon type is, but I can't.
Potential Solutions
I have seen two solutions to this problem.
Solution #1
The first solution was to append the Character's job value to the weapon object (i.e. { job: 'jedi', yearMade: 8, ...otherWeaponProperties }) and that way the __resolveType function for the Weapon field has access to the job value in its first argument.
const resolvers = {
Query: {
searchCharacters: async () => {
return api.getCharacters().map(apiCharacterData => {
return {
// --- start of important change ---
...apiCharacterData,
weapon: {
...apiCharacterData.weapon
job: apiCharacterData.job,
}
// --- end of important change ---
};
});
},
},
Weapon: {
// --- first arg is object `{ job, ...apiCharacterData.weapon }`
__resolveType: ({ job }) => {
return job === 'jedi' ? 'LightSaber' : 'Blaster';
}
},
}
Solution #2
The second solution was to change the underlying schema so that instead of a generalized Character type, there are Jedi and Stormtrooper types that implement a Character interface, and these types explicitly detail the type they'd have for the weapon field.
interface BaseCharacter {
job: String
weapon: Weapon
}
type Jedi implements BaseCharacter {
job: String
weapon: LightSaber
}
type Stormtrooper implements BaseCharacter {
job: String
weapon: Blaster
}
union Character = Jedi | Stormtrooper
type Query {
searchCharacters: [Character]
}
query {
searchCharacters {
... on Character {
job
weapon {
yearMade
... on LightSaber {
color
}
... on Blaster {
ammo
}
}
}
}
}
And then I only really need to add __resolveType to the Character union type and not the Weapon type.
const Character = {
__resolveType: ({ job }) => {
return job === 'jedi' ? 'LightSaber' : 'Blaster';
},
};
Help!
Between the two potential solutions, solution #2 seems like a step in the right direction. The biggest gain of solution #2 over solution #1 is that I don't have map the api data and I let the resolvers do what resolvers need to do per entity.
But the nested nature of the query and the inclusion of the ... on BaseCharacter inline fragment makes me feel like I'm misunderstanding how to properly write a GraphQL schema that models what I'm trying to achieve.
I'm pretty sure this isn't an uncommon problem, but I'm having trouble figuring out how to solve it. All help is appreciated.