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.
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!
There is a PR for graphql-js
and issue
discussing the matter (from 5 years ago!).
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 * as GraphQLJS from 'graphql'
import { envelop, useEngine } from '@envelop/core'
import { useFragmentArguments } from '@envelop/fragment-arguments'
const getEnveloped = envelop({
plugins: [
useEngine(GraphQLJS),
// ... other plugins ...
useFragmentArguments()
]
})