Skip to main content

Grafast beta: last epic solved!

· 7 min read
Benjie
Jem

In the first Grafast Working Group, we outlined 4 major issues in Grafast that needed to be addressed before we could think about general release. The fourth, and final, epic has now been solved!

  • ✅ Global dependencies — solved via “unary” steps
  • ✅ Early exit — solved via “flags”
  • ✅ Eradicating eval
  • Polymorphism — this release!

In previous versions of Grafast there was the possibility of exponential plan branching due to the naive method of resolution of abstract types — a known issue raised in the first Grafast working group as one of four “epics” to be solved before v1.0. This release of [email protected] fixes this final epic through a complete overhaul of the polymorphism system. Let’s take a look!

Polymorphism epic achieved

By moving the responsibility of polymorphic resolution from field plan resolvers into the abstract types themselves, we’ve centralized this logic, simplified field plan resolvers, and unlocked more optimization opportunities and greater execution efficiency. We no longer have the concept of “polymorphic capable” steps: any step may now be used for polymorphism. Abstract types now gain a planType method responsible for taking a “specifier” from the field plan resolver and returning a step representing the name of its concrete object type along with subplans for each possible object type.

To solve the problem of exponential branching, we merge the new specifier steps from all previous polymorphic branches into a single “combined” step before planning the next level of polymorphism.

A plan diagram showing the old way polymorphism was handled. The nodes can branch exponentially. A plan diagram showing the new way, the branches are combined back together before moving to the next layer of resolution.

On the right is the new handling of polymorphic resolution. getPetIds and getServiceAnimals both fetch an Animal ID and so they are combined together in order to fetch all of the required Animals by their IDs. Once the IDs are fetched, the nodes can branch out to the different Animal types.

For the few of you who have been brave enough to hand write polymorphic plan resolvers: first of all, thank you for trying it out! Hand written polymorphic plan resolvers will need to be updated to match the new paradigm, this will involve moving the polymorphic resolution from field plan resolvers into the new planType method on the relevant abstract type (interface or union) and adjusting the logic to fit the new pattern. Steps such as polymorphicBranch, pgPolymorphism, and other polymorphism related steps no longer exist as they are no longer supported in this new paradigm. For guidance on how to write the planType method, see the updated polymorphism docs and please reach out to us on Discord — we’d love to help you get migrated.

Excitingly, this is the last change to hand written plan resolvers that we expect to make for the v1.0 release (other than some improvements around TypeScript types), so we're getting close to release candidate stage!

TypeDefs / plans overhaul

In order to make the libraries more type safe, makeGrafastSchema (from grafast) and makeExtendSchemaPlugin (from postgraphile/utils) have deprecated the typeDefs/plans pattern since plans (like resolvers in the traditional format) ended up being a mish-mash of lots of different types (objects, scalars, enums, etc) and __-prefixed fields (__resolveType, __isTypeOf, etc) for methods on the type itself.

Going forwards, the configuration should be split into typeDefs with objects, interfaces, unions, inputObjects, scalars and enums as appropriate. Type-level properties such as resolveType/isTypeOf/planType/scope/etc are no longer prefixed with __ and, to avoid conflicts with these type-level properties, object and input object fields should be specified inside a new plans property and enum values within the new values property.

The old pattern will still work (this is not a breaking change), but we recommend moving to the new shape and will use it for all of our examples in the documentation from now on.

Migration is quite straightforward:

  1. Add new top-level properties. Add objects, interfaces, unions, inputObjects, scalars, and enums as top level properties alongside typeDefs and plans. Each should be an empty object. You can skip any where you’re not defining types of that kind.

  2. Split definitions based on type kind. For each type defined in plans move it into the appropriate new object (based on keyword defining the type; i.e. type objects, interface interfaces, union unions, input object inputObjects, scalar scalars, enum enums).

  3. Move field plans into nested plans: {...} object. For each type defined in the new objects and inputObjects objects: create a plans: { ... } entry inside the type and move all fields (anything not prefixed with __) inside this new (nested) property.

  4. Move enum values into nested values: {...} object. For each type defined in the new enums object: create a values: { ... } entry inside the type and move all values (anything not prefixed with __) inside this new (nested) property.

  5. Remove __ prefixes. For each type across objects/interfaces/unions/interfaceObjects/scalars and enums: remove the __ prefix from any methods/properties.

Example:

 typeDefs: ...,
-plans: {
+objects: {
User: {
- __isTypeOf(v) {
+ isTypeOf(v) {
return v.username != null;
},
plans: {
fieldName($source, fieldArgs) {
// ...
},
+ },
},
+},
+interfaces: {,
MyInterface: {
- __resolveType($specifier) {
+ resolveType($specifier) {
// ...
}
}
+},
+enums: {
MyEnum: {
ONE
TWO
THREE
}
},

(Aside: we pasted the markdown version of these instructions into ChatGPT and it managed to convert a number of plugins perfectly! YMMV.)

Other changes:

  • ObjectPlans/GrafastPlans/FieldPlans/InputObjectPlans/ScalarPlans all changed to singular
  • InterfaceOrUnionPlans split to InterfacePlan/UnionPlan (identical currently)
  • Shape of ObjectPlan/InterfacePlan/UnionPlan has changed; DeprecatedObjectPlan/etc exist for back-compatibility
  • FieldArgs can now accept an input shape indicating the args and their types
  • FieldPlanResolver<TArgs, TParentStep, TResultStep> has switched the order of the first two generic parameters: FieldPlanResolver<TParentStep, TArgs, TResultStep> — this is to reflect the order of the arguments to the function. Also null has been removed from the generics.
  • Various generics (including GrafastFieldConfig) that used to take a GraphQL type instance as a generic parameter no longer do — you need to use external code generation because TypeScript cannot handle the dynamic creation.
  • GrafastFieldConfig last two generics swapped order.
  • GrafastArgumentConfig generics completely changed

New features

Steps

  • coalesce(): Accepts a number of steps and represents the first value from them that isn’t null or undefined

Step classes

  • Experimental support for adding “references” to other steps at plan-time only (via refId = this.addRef($step) and reciprocal $step = this.getRef(refId) methods). Useful for optimization; but use with great caution. Currently undocumented due to experimental nature.

Improved type-safety

  • each() now reflects the type of the list item even if it’s not a “list capable” step
  • loadOne()/loadMany() can now track the underlying nullability of the callback

🚨 This will potentially break your plan types quite a bit. In particular, the LoadOneCallback and LoadManyCallback types now have 5 (not 4) generic parameters, the new one is inserted in the middle (after the second parameter) and indicates the true return type of the callback (ignoring promises) — e.g. Maybe<ReadonlyArray<Maybe<ItemType>>> for LoadManyCallback. They have sensible defaults if you only specify the first two generics.

And more besides...

In reaching this epic milestone, we have bumped the minimum version of node.js to Node 22 (the latest LTS); we have also found and fixed a number of other issues both in Grafast and the wider Graphile suite, you can see a full list at graphile.org.

Thank you Sponsors

Grafast is crowd-funded open-source software, it relies on crowd-sourced funding from individuals and companies to keep advancing.

If your company benefits from Grafast, PostGraphile or the wider Graphile suite, you should consider asking them to fund our work. By significantly reducing the amount of work needed to achieve business goals and reducing running costs, Graphile’s software results in huge time and money savings for users. We encourage companies to contribute a portion of these savings back, enabling the projects to advance more rapidly, and result in even greater savings for your company. Find out more about sponsorship on graphile.org.

Thank you