From e5fadf2ba97e06b94a0afa1d0bf08a3edffb522f Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 2 Jun 2020 22:48:13 -0400 Subject: [PATCH] refactor application of transforms = 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 arguments of the original request/result, the delegationContext, and the individual transformationContext, which gives the transformRequest and transformResult methods access to the overall state, including the original graphql context, as well as any additional state they want to set up. 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. --- packages/delegate/src/Transformer.ts | 45 ++++++ packages/delegate/src/delegateToSchema.ts | 160 ++++++-------------- packages/delegate/src/delegationBindings.ts | 72 +++++++++ packages/delegate/src/index.ts | 2 +- packages/delegate/src/types.ts | 19 +++ packages/utils/src/Interfaces.ts | 14 +- 6 files changed, 197 insertions(+), 115 deletions(-) create mode 100644 packages/delegate/src/Transformer.ts create mode 100644 packages/delegate/src/delegationBindings.ts diff --git a/packages/delegate/src/Transformer.ts b/packages/delegate/src/Transformer.ts new file mode 100644 index 00000000000..6acee7a480f --- /dev/null +++ b/packages/delegate/src/Transformer.ts @@ -0,0 +1,45 @@ +import { Transform, Request, ExecutionResult } from '@graphql-tools/utils'; + +import { DelegationContext, DelegationBinding } from './types'; + +import { defaultDelegationBinding } from './delegationBindings'; + +interface Transformation { + transform: Transform; + context: Record; +} + +export class Transformer { + private transformations: Array = []; + private delegationContext: DelegationContext; + + constructor(context: DelegationContext, binding: DelegationBinding = defaultDelegationBinding) { + this.delegationContext = context; + const delegationTransforms: Array = 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, this.delegationContext, transformation.context) + : request, + originalRequest + ); + } + + public transformResult(originalResult: ExecutionResult) { + return this.transformations.reduceRight( + (result: ExecutionResult, transformation: Transformation) => + transformation.transform.transformResult != null + ? transformation.transform.transformResult(result, this.delegationContext, transformation.context) + : result, + originalResult + ); + } +} diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 90d13414643..01a705cfd79 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -4,42 +4,28 @@ import { validate, GraphQLSchema, ExecutionResult, - GraphQLOutputType, isSchema, - GraphQLResolveInfo, FieldDefinitionNode, getOperationAST, OperationTypeNode, - GraphQLObjectType, OperationDefinitionNode, + DocumentNode, + GraphQLOutputType, + GraphQLObjectType, } from 'graphql'; -import { - Transform, - applyRequestTransforms, - applyResultTransforms, - mapAsyncIterator, - CombinedError, -} from '@graphql-tools/utils'; +import { mapAsyncIterator, CombinedError, Transform } from '@graphql-tools/utils'; -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'; -import { createRequestFromInfo, getDelegatingOperation } from './createRequest'; import { IDelegateToSchemaOptions, IDelegateRequestOptions, SubschemaConfig, ExecutionParams, - StitchingInfo, } from './types'; + import { isSubschemaConfig } from './Subschema'; +import { createRequestFromInfo, getDelegatingOperation } from './createRequest'; +import { Transformer } from './Transformer'; export function delegateToSchema(options: IDelegateToSchemaOptions | GraphQLSchema): any { if (isSchema(options)) { @@ -75,15 +61,10 @@ export function delegateToSchema(options: IDelegateToSchemaOptions | GraphQLSche } function getDelegationReturnType( - info: GraphQLResolveInfo, targetSchema: GraphQLSchema, operation: OperationTypeNode, fieldName: string ): GraphQLOutputType { - if (info != null) { - return info.returnType; - } - let rootType: GraphQLObjectType; if (operation === 'query') { rootType = targetSchema.getQueryType(); @@ -96,57 +77,6 @@ function getDelegationReturnType( return rootType.getFields()[fieldName].type; } -function buildDelegationTransforms( - subschemaOrSubschemaConfig: GraphQLSchema | SubschemaConfig, - info: GraphQLResolveInfo, - context: Record, - targetSchema: GraphQLSchema, - fieldName: string, - args: Record, - returnType: GraphQLOutputType, - transforms: Array, - transformedSchema: GraphQLSchema, - skipTypeMerging: boolean -): Array { - const stitchingInfo: StitchingInfo = info?.schema.extensions?.stitchingInfo; - - let delegationTransforms: Array = [ - new CheckResultAndHandleErrors(info, fieldName, subschemaOrSubschemaConfig, context, returnType, skipTypeMerging), - ]; - - if (stitchingInfo != null) { - delegationTransforms.push( - new AddSelectionSetsByField(info.schema, stitchingInfo.selectionSetsByField), - new AddSelectionSetsByType(info.schema, stitchingInfo.selectionSetsByType) - ); - } - - const transformedTargetSchema = - stitchingInfo == null - ? transformedSchema ?? targetSchema - : transformedSchema ?? stitchingInfo.transformedSchemas.get(subschemaOrSubschemaConfig) ?? targetSchema; - - delegationTransforms.push(new WrapConcreteTypes(returnType, transformedTargetSchema)); - - if (info != null) { - delegationTransforms.push(new ExpandAbstractTypes(info.schema, transformedTargetSchema)); - } - - delegationTransforms = delegationTransforms.concat(transforms); - - if (stitchingInfo != null) { - delegationTransforms.push(new AddFragmentsByField(targetSchema, stitchingInfo.fragmentsByField)); - } - - if (args != null) { - delegationTransforms.push(new AddArgumentsAsVariables(targetSchema, args)); - } - - delegationTransforms.push(new FilterToSchema(targetSchema), new AddTypenameToAbstract(targetSchema)); - - return delegationTransforms; -} - export function delegateRequest({ request, schema: subschemaOrSubschemaConfig, @@ -161,6 +91,7 @@ export function delegateRequest({ transformedSchema, skipValidation, skipTypeMerging, + binding, }: IDelegateRequestOptions) { let operationDefinition: OperationDefinitionNode; let targetOperation: OperationTypeNode; @@ -182,46 +113,44 @@ export function delegateRequest({ let targetSchema: GraphQLSchema; let targetRootValue: Record; - let requestTransforms: Array = transforms.slice(); let subschemaConfig: SubschemaConfig; + let allTransforms: Array; if (isSubschemaConfig(subschemaOrSubschemaConfig)) { subschemaConfig = subschemaOrSubschemaConfig; targetSchema = subschemaConfig.schema; targetRootValue = rootValue ?? subschemaConfig?.rootValue ?? info?.rootValue; - if (subschemaConfig.transforms != null) { - requestTransforms = requestTransforms.concat(subschemaConfig.transforms); - } + allTransforms = + subschemaOrSubschemaConfig.transforms != null + ? subschemaOrSubschemaConfig.transforms.concat(transforms) + : transforms; } else { targetSchema = subschemaOrSubschemaConfig; targetRootValue = rootValue ?? info?.rootValue; + allTransforms = transforms; } - const delegationTransforms = buildDelegationTransforms( - subschemaOrSubschemaConfig, - info, - context, + const delegationContext = { + subschema: subschemaOrSubschemaConfig, targetSchema, - targetFieldName, + operation: targetOperation, + fieldName: targetFieldName, args, - returnType ?? getDelegationReturnType(info, targetSchema, targetOperation, targetFieldName), - requestTransforms.reverse(), - transformedSchema, - skipTypeMerging - ); + context, + info, + returnType: + returnType ?? info?.returnType ?? getDelegationReturnType(targetSchema, targetOperation, targetFieldName), + transforms: allTransforms, + transformedSchema: transformedSchema ?? targetSchema, + skipTypeMerging, + }; - const processedRequest = applyRequestTransforms(request, delegationTransforms); + const transformer = new Transformer(delegationContext, binding); + + const processedRequest = transformer.transformRequest(request); if (!skipValidation) { - const errors = validate(targetSchema, processedRequest.document); - if (errors.length > 0) { - if (errors.length > 1) { - const combinedError = new CombinedError(errors); - throw combinedError; - } - const error = errors[0]; - throw error.originalError || error; - } + validateRequest(targetSchema, processedRequest.document); } if (targetOperation === 'query' || targetOperation === 'mutation') { @@ -236,9 +165,9 @@ export function delegateRequest({ }); if (executionResult instanceof Promise) { - return executionResult.then((originalResult: any) => applyResultTransforms(originalResult, delegationTransforms)); + return executionResult.then(originalResult => transformer.transformResult(originalResult)); } - return applyResultTransforms(executionResult, delegationTransforms); + return transformer.transformResult(executionResult); } const subscriber = @@ -254,21 +183,28 @@ export function delegateRequest({ // "subscribe" to the subscription result and map the result through the transforms return mapAsyncIterator( subscriptionResult as AsyncIterableIterator, - result => { - const transformedResult = applyResultTransforms(result, delegationTransforms); - // wrap with fieldName to return for an additional round of resolutioon - // with payload as rootValue - return { - [targetFieldName]: transformedResult, - }; - } + originalResult => ({ + [targetFieldName]: transformer.transformResult(originalResult), + }) ); } - return applyResultTransforms(subscriptionResult, delegationTransforms); + return transformer.transformResult(subscriptionResult as ExecutionResult); }); } +function validateRequest(targetSchema: GraphQLSchema, document: DocumentNode) { + const errors = validate(targetSchema, document); + if (errors.length > 0) { + if (errors.length > 1) { + const combinedError = new CombinedError(errors); + throw combinedError; + } + const error = errors[0]; + throw error.originalError || error; + } +} + function createDefaultExecutor(schema: GraphQLSchema, rootValue: Record) { return ({ document, context, variables, info }: ExecutionParams) => execute({ diff --git a/packages/delegate/src/delegationBindings.ts b/packages/delegate/src/delegationBindings.ts new file mode 100644 index 00000000000..cd21f20e80e --- /dev/null +++ b/packages/delegate/src/delegationBindings.ts @@ -0,0 +1,72 @@ +import { Transform } from '@graphql-tools/utils'; + +import { StitchingInfo, 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'; + +export function defaultDelegationBinding(delegationContext: DelegationContext): Array { + const { + subschema: schemaOrSubschemaConfig, + targetSchema, + fieldName, + args, + context, + info, + returnType, + transforms = [], + skipTypeMerging, + } = delegationContext; + const stitchingInfo: StitchingInfo = info?.schema.extensions?.stitchingInfo; + + let transformedSchema = stitchingInfo?.transformedSchemas.get(schemaOrSubschemaConfig); + if (transformedSchema != null) { + delegationContext.transformedSchema = transformedSchema; + } else { + transformedSchema = delegationContext.transformedSchema; + } + + let delegationTransforms: Array = [ + new CheckResultAndHandleErrors(info, fieldName, schemaOrSubschemaConfig, context, returnType, skipTypeMerging), + ]; + + if (stitchingInfo != null) { + delegationTransforms = delegationTransforms.concat([ + new AddSelectionSetsByField(info.schema, stitchingInfo.selectionSetsByField), + new AddSelectionSetsByType(info.schema, stitchingInfo.selectionSetsByType), + new WrapConcreteTypes(returnType, transformedSchema), + new ExpandAbstractTypes(info.schema, transformedSchema), + ]); + } else if (info != null) { + delegationTransforms = delegationTransforms.concat([ + new WrapConcreteTypes(returnType, transformedSchema), + new ExpandAbstractTypes(info.schema, transformedSchema), + ]); + } else { + delegationTransforms.push(new WrapConcreteTypes(returnType, transformedSchema)); + } + + delegationTransforms = delegationTransforms.concat(transforms.slice().reverse()); + + if (stitchingInfo != null) { + delegationTransforms.push(new AddFragmentsByField(targetSchema, stitchingInfo.fragmentsByField)); + } + + if (args != null) { + delegationTransforms.push(new AddArgumentsAsVariables(targetSchema, args)); + } + + delegationTransforms = delegationTransforms.concat([ + new FilterToSchema(targetSchema), + new AddTypenameToAbstract(targetSchema), + ]); + + return delegationTransforms; +} diff --git a/packages/delegate/src/index.ts b/packages/delegate/src/index.ts index 1ef0bbed5a1..2111df5544c 100644 --- a/packages/delegate/src/index.ts +++ b/packages/delegate/src/index.ts @@ -5,6 +5,6 @@ export { createMergedResolver } from './createMergedResolver'; export { handleResult } from './results/handleResult'; export { Subschema, isSubschema, isSubschemaConfig, getSubschema } from './Subschema'; +export * from './delegationBindings'; export * from './transforms'; - export * from './types'; diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index 3203cc82701..78745a5d9b9 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -10,11 +10,29 @@ import { FragmentDefinitionNode, GraphQLObjectType, VariableDefinitionNode, + OperationTypeNode, } from 'graphql'; + import { Operation, Transform, Request, TypeMap, ExecutionResult } from '@graphql-tools/utils'; import { Subschema } from './Subschema'; +export interface DelegationContext { + subschema: GraphQLSchema | SubschemaConfig; + targetSchema: GraphQLSchema; + operation: OperationTypeNode; + fieldName: string; + args: Record; + context: Record; + info: GraphQLResolveInfo; + returnType: GraphQLOutputType; + transforms: Array; + transformedSchema: GraphQLSchema; + skipTypeMerging: boolean; +} + +export type DelegationBinding = (delegationContext: DelegationContext) => Array; + export interface IDelegateToSchemaOptions, TArgs = Record> { schema: GraphQLSchema | SubschemaConfig | Subschema; operation?: Operation; @@ -30,6 +48,7 @@ export interface IDelegateToSchemaOptions, TArgs transformedSchema?: GraphQLSchema; skipValidation?: boolean; skipTypeMerging?: boolean; + binding?: DelegationBinding; } export interface IDelegateRequestOptions extends Omit { diff --git a/packages/utils/src/Interfaces.ts b/packages/utils/src/Interfaces.ts index 307662f3de3..921d4d71ba1 100644 --- a/packages/utils/src/Interfaces.ts +++ b/packages/utils/src/Interfaces.ts @@ -133,9 +133,19 @@ export interface IFieldResolverOptions GraphQLSchema; -export type RequestTransform = (originalRequest: Request) => Request; -export type ResultTransform = (originalResult: ExecutionResult) => ExecutionResult; +export type RequestTransform = ( + originalRequest: Request, + delegationContext?: DelegationContext, + transformationContext?: Record +) => Request; +export type ResultTransform = ( + originalResult: ExecutionResult, + delegationContext?: DelegationContext, + transformationContext?: Record +) => ExecutionResult; export interface Transform { transformSchema?: SchemaTransform;