Skip to main content

Polymorphism

GraphQL has two types of output polymorphism currently: interfaces and unions. An interface defines a list of fields; all objects that implement the interface must implement fields compatible with these. A union is a simple list of possible object types.

Grafast supports both of these forms of GraphQL polymorphism, both through resolvers and through plans. Resolvers work the same (basically) as they do in GraphQL.js so we won't dig into them here, but let's look into how Grafast supports polymorphism via plans.

Polymorphic positions

Let's define the term "polymorphic position" to make it easier to talk about planning our polymorphic GraphQL query. Imagine you have a GraphQL schema such as:

interface Animal {
name: String!
}
type Cat extends Animal {
name: String!
numberOfLives: Int!
}
type Dog extends Animal {
name: String!
wagsTail: Boolean!
}
type Query {
bestAnimal: Animal
randomAnimals: [Animal]
}

One query to this schema could be:

{
bestAnimal {
name
... on Cat {
numberOfLives
}
... on Dog {
wagsTail
}
}
}

Here the return type of the bestAnimal field is a polymorphic type (Animal, which is an interface), so the return type of bestAnimal is a polymorphic position in this query.

Another query could be:

{
randomAnimals {
name
... on Cat {
numberOfLives
}
... on Dog {
wagsTail
}
}
}

Here the return type of randomAnimals is a list ([Animal]). A list is not itself polymorphic, however the type inside the list is a polymorphic type (Animal again), so a polymorphic position in this query is inside the list returned by randomAnimals.

Operations may have any number (0 or higher) of polymorphic positions.

Polymorphic-capable steps

When a polymorphic position in an operation is being planned, Grafast will call the field's plan resolver function as usual (or the resulting step's itemPlan method for polymorphic positions occurring inside of lists) to get the step representing this polymorphic position. Grafast requires that this step must be a polymorphic-capable step, that is a step whose class implements the planForType method, otherwise a planning error will be raised.

  planForType(objectType: GraphQLObjectType): ExecutableStep;

Having determined the polymorphic-capable step that represents this polymorphic position, Grafast will create a polymorphic LayerPlan and loop through all of the possible concrete object types valid at that location. For each concrete object type, Grafast will pass the type to the polymorphic-capable step's planForType method and the resulting step will represent that concrete object type. Multiple concrete object types may be represented by the same step.

Planning then continues with the child selection sets being traversed for each possible concrete object type and corresponding step.

At run-time, when a polymorphic-capable step executes, each of the entries in the execute() result list must be either null, an error, or the result of calling Grafast's polymorphicWrap function, passing the concrete object type's name as the first argument, and optionally any associated data as the second argument. This allows Grafast to determine which "polymorphic branch" has been taken, which will control which future steps will be executed against this data.

export function polymorphicWrap<TType extends string>(
type: TType,
data?: unknown,
): PolymorphicData<TType>;

Caveats

Highly polymorphic operations may result in very significant planning time, this is something we're working to optimize but for now we recommend that you use persisted operations (aka persisted queries) to ensure that only your developers operations are allowed. Alternatively, use our plugin that validates that operations don't contain too much polymorphism (TODO: plugin not yet available).

info

Currently Grafast uses a rudimentary strategy where all possible polymorphic types at each point in the GraphQL operation are planned at planning time. This is a simple approach, but it can inflate the time spent planning an operation, especially for highly polymorphic operations where combinatorics is a significant concern.

At some point, Grafast will add support for on-demand polymorphic planning. With this strategy, each "polymorphic branch" of the plan will only be planned the first time that an object of that type is met at runtime. This on-demand polymorphic planning strategy should significantly decrease initial planning time for highly polymorphic operations, and may result in many of the paths never needing to be planned at all!