Skip to content

Commit

Permalink
refactor application of transforms
Browse files Browse the repository at this point in the history
= provide binding option to delegateToSchema

A Binding is a function that takes a delegationContext (just the relevant subset of options passed to delegateSchema) and produces the set of transforms used to actually delegate (i.e. that "binds" the fieldNodes from the sourceSchema to that of the targetSchema), this provides library users ability to completely customize delegation behavior, even on a per field level. These delegation transforms are the specified schema transforms, but also the additional delegation transforms that reduce friction between the source and target schemas, see below.

= export defaultBinding that enables:

(a) setting arguments after schema transformation = AddArgumentsAsVariables
(b) conversion of original result into object annotated with errors for easy return and use with defaultMergedResolver = CheckResultAndHandleErrors
(c) addition of required selectionSets for use when stitching = AddSelectionSetsByField/Type, AddFragmentsByField
(d) automatic filtering of fields not in target schema, useful for stitching and many other scenarios = FilterToSchema
(e) delegation from concrete types in source schema to abstract types in target schema = WrapConcreteTypes
(f) delegation from new abstract types in source schema (not found in target schema) to implementations within target schemas = ExpandAbstractTypes

= addition of internal Transformer class responsible for applying transforms

The Transformer class is constructed with a delegationContext and a binding function and uses them to produces the delegating transforms. The Transformer creates an individual transformationContext for each transform (for easy sharing of state between the transformRequest and transformResult methods, a new feature) and exposes transformRequest and transformResult methods that performs the individual transformations with the transformationContext, and the delegationContext, which gives the transformRequest and transformResult methods access to the overall state, including the original graphql context. These additional arguments may help enable advanced transforms such as specifying input fields in target schema not present or filtered from the source schema, see #1551.
  • Loading branch information
yaacovCR committed Jun 3, 2020
1 parent e2d0034 commit 83910dd
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 130 deletions.
45 changes: 45 additions & 0 deletions packages/delegate/src/Transformer.ts
@@ -0,0 +1,45 @@
import { Transform, Request, ExecutionResult } from '@graphql-tools/utils';

import { DelegationContext, Binding } from './types';

import { defaultBinding } from './defaultBinding';

interface Transformation {
transform: Transform;
context: Record<string, any>;
}

export class Transformer {
private transformations: Array<Transformation> = [];
private delegationContext: DelegationContext;

constructor(delegationContext: DelegationContext, binding: Binding = defaultBinding) {
this.delegationContext = delegationContext;
const delegationTransforms: Array<Transform> = binding(this.delegationContext);
delegationTransforms.forEach(transform => this.addTransform(transform, {}));
}

private addTransform(transform: Transform, context = {}) {
this.transformations.push({ transform, context });
}

public transformRequest(originalRequest: Request) {
return this.transformations.reduce(
(request: Request, transformation: Transformation) =>
transformation.transform.transformRequest != null
? transformation.transform.transformRequest(request, transformation.context, this.delegationContext)
: request,
originalRequest
);
}

public transformResult(originalResult: ExecutionResult) {
return this.transformations.reduceRight(
(result: ExecutionResult, transformation: Transformation) =>
transformation.transform.transformResult != null
? transformation.transform.transformResult(result, transformation.context, this.delegationContext)
: result,
originalResult
);
}
}
108 changes: 108 additions & 0 deletions packages/delegate/src/defaultBinding.ts
@@ -0,0 +1,108 @@
import { GraphQLSchema, GraphQLOutputType, OperationTypeNode, GraphQLObjectType } from 'graphql';

import { Transform } from '@graphql-tools/utils';

import { StitchingInfo, isSubschemaConfig, DelegationContext } from './types';

import ExpandAbstractTypes from './transforms/ExpandAbstractTypes';
import WrapConcreteTypes from './transforms/WrapConcreteTypes';
import FilterToSchema from './transforms/FilterToSchema';
import AddFragmentsByField from './transforms/AddFragmentsByField';
import AddSelectionSetsByField from './transforms/AddSelectionSetsByField';
import AddSelectionSetsByType from './transforms/AddSelectionSetsByType';
import AddTypenameToAbstract from './transforms/AddTypenameToAbstract';
import CheckResultAndHandleErrors from './transforms/CheckResultAndHandleErrors';
import AddArgumentsAsVariables from './transforms/AddArgumentsAsVariables';

function getDelegationReturnType(
targetSchema: GraphQLSchema,
operation: OperationTypeNode,
fieldName: string
): GraphQLOutputType {
let rootType: GraphQLObjectType<any, any>;
if (operation === 'query') {
rootType = targetSchema.getQueryType();
} else if (operation === 'mutation') {
rootType = targetSchema.getMutationType();
} else {
rootType = targetSchema.getSubscriptionType();
}

return rootType.getFields()[fieldName].type;
}

export function defaultBinding(delegationContext: DelegationContext): Array<Transform> {
const {
subschema: schemaOrSubschemaConfig,
targetSchema,
operation,
fieldName,
args,
context,
info,
returnType,
transforms = [],
transformedSchema,
skipTypeMerging,
} = delegationContext;
const stitchingInfo: StitchingInfo = info?.schema.extensions?.stitchingInfo;

const transformedTargetSchema =
stitchingInfo == null
? transformedSchema ?? targetSchema
: stitchingInfo.transformedSchemas.get(schemaOrSubschemaConfig) ?? transformedSchema ?? targetSchema;
delegationContext.transformedSchema = transformedTargetSchema;

const delegationReturnType =
returnType ?? info?.returnType ?? getDelegationReturnType(targetSchema, operation, fieldName);
delegationContext.returnType = delegationReturnType;

const delegationTransforms: Array<Transform> = [];

delegationTransforms.push(
new CheckResultAndHandleErrors(
info,
fieldName,
schemaOrSubschemaConfig,
context,
delegationReturnType,
skipTypeMerging
)
);

if (stitchingInfo != null) {
delegationTransforms.push(new AddSelectionSetsByField(info.schema, stitchingInfo.selectionSetsByField));
delegationTransforms.push(new AddSelectionSetsByType(info.schema, stitchingInfo.selectionSetsByType));
}

delegationTransforms.push(new WrapConcreteTypes(delegationReturnType, transformedTargetSchema));

if (info != null) {
delegationTransforms.push(new ExpandAbstractTypes(info.schema, transformedTargetSchema));
}

let finalSubschemaTransforms: Array<Transform>;
if (isSubschemaConfig(schemaOrSubschemaConfig)) {
finalSubschemaTransforms =
schemaOrSubschemaConfig.transforms != null ? schemaOrSubschemaConfig.transforms.concat(transforms) : transforms;
} else {
finalSubschemaTransforms = transforms;
}

for (let i = finalSubschemaTransforms.length - 1; i > -1; i--) {
delegationTransforms.push(finalSubschemaTransforms[i], {});
}

if (stitchingInfo != null) {
delegationTransforms.push(new AddFragmentsByField(targetSchema, stitchingInfo.fragmentsByField));
}

if (args != null) {
delegationTransforms.push(new AddArgumentsAsVariables(targetSchema, args));
}

delegationTransforms.push(new FilterToSchema(targetSchema));
delegationTransforms.push(new AddTypenameToAbstract(targetSchema));

return delegationTransforms;
}

0 comments on commit 83910dd

Please sign in to comment.