title | description |
---|---|
Schema transforms |
Automatically transforming schemas |
Schema transforms are a tool for making modified copies of GraphQLSchema
objects, without changing the original schema implementation. This is especially useful when the original schema cannot be changed, i.e. when using remote schemas.
Schema transforms can be useful when building GraphQL gateways that combine multiple schemas using schema stitching to combine schemas together without conflicts between types or fields.
Schema transforms work by wrapping the original schema in a new outer schema that simply delegates all operations to the original inner schema. Each schema transform includes a function that changes the outer wrapping schema. It may also include an operation transform, i.e. functions that either modify the operation prior to delegation or modify the result prior to its return.
interface Transform = {
transformSchema?: (schema: GraphQLSchema) => GraphQLSchema;
transformRequest?: (request: Request) => Request;
transformResult?: (result: Result) => Result;
};
For example, let's consider changing the name of the type in a simple schema. Imagine we've written a function that takes a GraphQLSchema
and replaces all instances of type Test
with NewTest
.
# old schema
type Test {
id: ID!
name: String
}
type Query {
returnTest: Test
}
# new schema
type NewTest {
id: ID!
name: String
}
type Query {
returnTest: NewTest
}
On delegation to the inner, original schema, we want the NewTest
type to be automatically mapped to the old Test
type.
At first glance, it might seem as though most queries work the same way as before:
query {
returnTest {
id
name
}
}
Since the fields of the type have not changed, delegating to the old schema is relatively easy here.
However, the new name begins to matter more when fragments and variables are used:
query {
returnTest {
id
... on NewTest {
name
}
}
}
Since the NewTest
type did not exist on old schema, this fragment will not match anything in the old schema, so it will be filtered out during delegation.
What we need is a transformRequest
function that knows how to rename any occurrences of NewTest
to Test
before delegating to the old schema.
By the same reasoning, we also need a transformResult
function, because any results contain a __typename
field whose value is Test
, that name needs to be updated to NewTest
in the final result.
interface Transform = {
transformSchema?: (schema: GraphQLSchema) => GraphQLSchema;
transformRequest?: (request: Request) => Request;
transformResult?: (result: Result) => Result;
};
type Request = {
document: DocumentNode;
variables: Record<string, any>;
extensions?: Record<string, any>;
};
type Result = ExecutionResult & {
extensions?: Record<string, any>;
};
Given a GraphQLSchema
and an array of Transform
objects, produce a new schema with those transforms applied.
Delegating resolvers are generated to map from new schema root fields to old schema root fields. These automatic resolvers should be sufficient, so you don't have to implement your own.
The delegating resolvers will apply the operation transforms defined by the Transform
objects. Each provided transformRequest
functions will be applies in reverse order, until the request matches the original schema. The tranformResult
functions will be applied in the opposite order until the result matches the outer schema.
For convenience, when using transformSchema
, after schema transformation, the transforms
property on a returned transformedSchema
object will contains the operation transforms that were applied. This could be useful when manually delegating to the original schema from an outer schema when schema stitching, but has been deprecated in favor of specifying subschema ids. See the schema stitching docs for further details.
Built-in transforms are ready-made classes implementing the Transform
interface. They are intended to cover many of the most common schema transformation use cases, but they also serve as examples of how to implement transforms for your own needs.
-
FilterTypes(filter: (type: GraphQLNamedType) => boolean)
: Remove all types for which thefilter
function returnsfalse
. -
RenameTypes(renamer, options?)
: Rename types by applyingrenamer
to each type name. Ifrenamer
returnsundefined
, the name will be left unchanged. Options controls whether built-in types and scalars are renamed. Root objects are never renamed by this transform.
RenameTypes(
(name: string) => string | void,
options?: {
renameBuiltins: Boolean;
renameScalars: Boolean;
},
)
TransformRootFields(transformer: RootTransformer)
: Given a transformer, arbitrarily transform root fields. Thetransformer
can return aGraphQLFieldConfig
definition, a object with newname
and afield
,null
to remove the field, orundefined
to leave the field unchanged.
TransformRootFields(transformer: RootTransformer)
type RootTransformer = (
operation: 'Query' | 'Mutation' | 'Subscription',
fieldName: string,
field: GraphQLField<any, any>,
) =>
| GraphQLFieldConfig<any, any>
| { name: string; field: GraphQLFieldConfig<any, any> }
| null
| void;
FilterRootFields(filter: RootFilter)
: LikeFilterTypes
, removes root fields for which thefilter
function returnsfalse
.
FilterRootFields(filter: RootFilter)
type RootFilter = (
operation: 'Query' | 'Mutation' | 'Subscription',
fieldName: string,
field: GraphQLField<any, any>,
) => boolean;
RenameRootFields(renamer)
: Rename root fields, by applying therenamer
function to their names.
RenameRootFields(
renamer: (
operation: 'Query' | 'Mutation' | 'Subscription',
name: string,
field: GraphQLField<any, any>,
) => string,
)
TransformObjectFields(objectFieldTransformer: ObjectFieldTransformer, fieldNodeTransformer?: FieldNodeTransformer))
: Given an object field transformer, arbitrarily transform fields. TheobjectFieldTransformer
can return aGraphQLFieldConfig
definition, a object with newname
and afield
,null
to remove the field, orundefined
to leave the field unchanged. The optionalfieldNodeTransformer
, if specified, is called upon any field of that type in the request; result transformation can be specified by wrapping the resolve function within theobjectFieldTransformer
. In this way, a field can be fully arbitrarily modified in place.
TransformObjectFields(objectFieldTransformer: ObjectFieldTransformer, fieldNodeTransformer: FieldNodeTransformer)
type ObjectFieldTransformer = (
typeName: string,
fieldName: string,
field: GraphQLField<any, any>,
) =>
| GraphQLFieldConfig<any, any>
| { name: string; field: GraphQLFieldConfig<any, any> }
| null
| void;
type FieldNodeTransformer = (
typeName: string,
fieldName: string,
fieldNode: FieldNode
) => FieldNode;
FilterObjectFields(filter: ObjectFilter)
: Removes object fields for which thefilter
function returnsfalse
.
FilterObjectFields(filter: ObjectFilter)
type ObjectFilter = (
typeName: string,
fieldName: string,
field: GraphQLField<any, any>,
) => boolean;
RenameObjectFields(renamer)
: Rename object fields, by applying therenamer
function to their names.
RenameObjectFields(
renamer: (
typeName: string,
fieldName: string,
field: GraphQLField<any, any>,
) => string,
)
It may be sometimes useful to add additional transforms to manually change an operation request or result when using delegateToSchema
. Common use cases may be move selections around or to wrap them. The following built-in transforms may be useful in those cases.
-
ExtractField({ from: Array<string>, to: Array<string> })
- move selection atfrom
path toto
path. -
WrapQuery( path: Array<string>, wrapper: QueryWrapper, extractor: (result: any) => any, )
- wrap a selection atpath
using functionwrapper
. Applyextractor
at the same path to get the result. This is used to get a result nested inside other result
transforms: [
// Wrap document takes a subtree as an AST node
new WrapQuery(
// path at which to apply wrapping and extracting
['userById'],
(subtree: SelectionSetNode) => ({
// we create a wrapping AST Field
kind: Kind.FIELD,
name: {
kind: Kind.NAME,
// that field is `address`
value: 'address',
},
// Inside the field selection
selectionSet: subtree,
}),
// how to process the data result at path
result => result && result.address,
),
],
WrapQuery
can also be used to expand multiple top level query fields
transforms: [
// Wrap document takes a subtree as an AST node
new WrapQuery(
// path at which to apply wrapping and extracting
['userById'],
(subtree: SelectionSetNode) => {
const newSelectionSet = {
kind: Kind.SELECTION_SET,
selections: subtree.selections.map(selection => {
// just append fragments, not interesting for this
// test
if (selection.kind === Kind.INLINE_FRAGMENT ||
selection.kind === Kind.FRAGMENT_SPREAD) {
return selection;
}
// prepend `address` to name and camelCase
const oldFieldName = selection.name.value;
return {
kind: Kind.FIELD,
name: {
kind: Kind.NAME,
value: 'address' +
oldFieldName.charAt(0).toUpperCase() +
oldFieldName.slice(1)
}
};
})
};
return newSelectionSet;
},
// how to process the data result at path
result => ({
streetAddress: result.addressStreetAddress,
zip: result.addressZip
})
ReplaceFieldWithFragment(targetSchema: GraphQLSchema, fragments: Array<{ field: string; fragment: string; }>)
: Replace the given fields with an inline fragment. Used bymergeSchemas
to handle thefragment
option.
The following transforms are automatically applied by delegateToSchema
during schema delegation, to translate between new and old types and fields:
AddArgumentsAsVariables
: Given a schema and arguments passed to a root field, make those arguments document variables.ExpandAbstractTypes
: If an abstract type within a document does not exist in the inner schema, expand the type to each and any of its implementations that do exist in the inner schema.FilterToSchema
: Given a schema and document, remove all fields, variables and fragments for types that don't exist in that schema.AddTypenameToAbstract
: Add__typename
to all abstract types in the document, necessary for type resolution of interfaces within the outer schema to work.CheckResultAndHandleErrors
: Given a result from a subschema, propagate errors so that they match the correct subfield. Also provide the correct key if aliases are used.
By passing a custom transforms
array to delegateToSchema
, it's possible to run additional operation (request/result) transforms before these default transforms.