v2
Guides
Using GraphQL Features from the Future

Using GraphQL Features from the Future

Envelop allows to hook into and extend all phases of the GraphQL execution pipeline. Because of that envelop can be used to implement features that are not yet available in the GraphQL.js core and are only proposed as RFC candidates.

OneOf Input Objects and OneOf Fields

Unions and Interfaces are essential primitives for building scalable GraphQL schemas. But they cannot be used for describing polymorphic input types.

The following is illegal. Only object types can be union members:

type Rectangle {
  width: Float!
  height: Float!
}
 
type Circle {
  radius: Float!
}
 
union Geometry = Rectangle | Circle
 
input RectangleCreateInput {
  width: Float!
  height: Float!
}
 
input CircleCreateInput {
  radius: Float!
}
 
union GeometryCreateInput = RectangleCreateInput | CircleCreateInput
 
type Mutation {
  geometryCreate(input: GeometryCreateInput!): Geometry
}

The community expressed their needs for a way of describing such polymorphic inputs and three RFCs popped up:

All of them are proposed by the awesome @benjie!

Envelop supports the latest of those proposals “Oneof Input Objects and Oneof Fields”.

The invalid syntax from above can be correctly described with the @oneOf directive.

type Rectangle {
  width: Float!
  height: Float!
}
 
type Circle {
  radius: Float!
}
 
union Geometry = Rectangle | Circle
 
type RectangleCreateInput {
  width: Float!
  height: Float!
}
 
type CircleCreateInput {
  radius: Float!
}
 
input GeometryCreateInput @oneOf {
  rectangle: RectangleCreateInput
  circle: RectangleCreateInput
}
 
type Mutation {
  geometryCreate(input: GeometryCreateInput!): Geometry
}

Which means for the following operation:

mutation GeometryCreateMutation($input: GeometryCreateInput!) {
  geometryCreate(input: $input) {
    ... on Rectangle {
      width
      height
    }
    ... on Circle {
      radius
    }
  }
}

The following variable input would be legit:

{
  "input": {
    "rectangle": {
      "width": 10,
      "height": 20
    }
  }
}
{
  "input": {
    "circle": {
      "radius": 10
    }
  }
}

While the following input would be invalid and not pass the validation phase:

{
  "input": {
    "rectangle": {
      "width": 10,
      "height": 20
    },
    "circle": {
      "radius": 10
    }
  }
}

Adding support to the existing envelop setup is straight-forward:

import * as GraphQLJS from 'graphql'
import { envelop, useEngine } from '@envelop/core'
import { OneOfInputObjectsRule, useExtendedValidation } from '@envelop/extended-validation'
 
const getEnveloped = envelop({
  plugins: [
    useEngine(GraphQLJS),
    // ... other plugins
    useExtendedValidation({
      rules: [OneOfInputObjectsRule]
    })
  ]
})

As there are developers using either the SDL first or code first approach envelop supports both ways.

SDL first via the @oneOf directive.

directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION
 
type RectangleCreateInput {
  width: Float!
  height: Float!
}
 
type CircleCreateInput {
  radius: Float!
}
 
input GeometryCreateInput @oneOf {
  rectangle: RectangleCreateInput
  circle: RectangleCreateInput
}

Code-first via the oneOf extension.

import { GraphQLFloat, GraphQLInputObjectType, GraphQLNonNull } from 'graphql'
 
const GraphQLRectangleCreateInput = new GraphQLInputObjectType({
  name: 'RectangleCreateInput',
  fields: {
    width: {
      type: GraphQLNonNull(GraphQLFloat)
    },
    height: {
      type: GraphQLNonNull(GraphQLFloat)
    }
  }
})
 
const GraphQLCircleCreateInput = new GraphQLInputObjectType({
  name: 'CircleCreateInput',
  fields: {
    radius: {
      type: GraphQLNonNull(GraphQLFloat)
    }
  }
})
 
const GraphQLGeometryCreateInput = new GraphQLInputObjectType({
  name: 'GeometryCreateInput',
  fields: {
    rectangle: {
      type: GraphQLRectangleCreateInput
    },
    circle: {
      type: GraphQLCircleCreateInput
    }
  },
  extensions: {
    oneOf: true
  }
})

You can learn more in the plugin documentations.

Note: If you print the schema using import {printSchema} from 'graphql/utilities', this will not include the added @oneOf directive as the GraphQLSchema object doesn’t recognize directives as part of its AST. If you want to print your schema with those directives added to your SDL, use printSchemaWithDirectives instead.

Fragment Arguments

Mutations, Subscriptions and Queries can have variable definitions today. For fragments a way of passing down variables in a capsulated way is impossible. If you use variables in fragments today they always refer to the global variables object, which limits flexibility.

fragment UserAvatar on User {
  id
  avatar(size: $size) {
    url
  }
}
query UserProfile {
  me {
    id
    ...UserAvatar
    fiends {
      id
      name
      ...UserAvatar
    }
  }
}

It is impossible to reuse the UserAvatar with different values for the $size variable.

Fragment arguments allow doing exactly that!

fragment UserAvatar($size: Size!) on User {
  id
  avatar(size: $size) {
    url
  }
}
query UserProfile {
  me {
    id
    ...UserAvatar(size: "large")
    fiends {
      id
      name
      ...UserAvatar(size: "small")
    }
  }
}

Frameworks such as relay allow doing similar via directives today. It is time this becomes part of the GraphQL specification and is officially supported!

Envelop already allows using fragment arguments by extending the GraphQL parser. We don’t recommend using this for production usage! Please only use it for research or learning purposes!

import { envelop } from '@envelop/core'
import { useFragmentArguments } from '@envelop/fragment-arguments'
 
const getEnveloped = envelop({
  plugins: [
    // ... other plugins ...
    useFragmentArguments()
  ]
})