diff --git a/packages/delegate/src/Transformer.ts b/packages/delegate/src/Transformer.ts new file mode 100644 index 00000000000..2640b3f04eb --- /dev/null +++ b/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; +} + +export class Transformer { + private transformations: Array = []; + private delegationContext: DelegationContext; + + constructor(delegationContext: DelegationContext, binding: Binding = defaultBinding) { + this.delegationContext = delegationContext; + 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, 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 + ); + } +} diff --git a/packages/delegate/src/defaultBinding.ts b/packages/delegate/src/defaultBinding.ts new file mode 100644 index 00000000000..72b6ea1c1ab --- /dev/null +++ b/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; + 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 { + 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 = []; + + 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; + 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; +} diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 4d55f2f0af1..a1f7d836856 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -4,42 +4,25 @@ import { validate, GraphQLSchema, ExecutionResult, - GraphQLOutputType, isSchema, - GraphQLResolveInfo, FieldDefinitionNode, getOperationAST, OperationTypeNode, - GraphQLObjectType, OperationDefinitionNode, + DocumentNode, } from 'graphql'; -import { - Transform, - applyRequestTransforms, - applyResultTransforms, - mapAsyncIterator, - CombinedError, -} from '@graphql-tools/utils'; +import { mapAsyncIterator, CombinedError } 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, isSubschemaConfig, ExecutionParams, - StitchingInfo, } from './types'; +import { createRequestFromInfo, getDelegatingOperation } from './createRequest'; +import { Transformer } from './Transformer'; export function delegateToSchema(options: IDelegateToSchemaOptions | GraphQLSchema): any { if (isSchema(options)) { @@ -74,79 +57,6 @@ 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(); - } else if (operation === 'mutation') { - rootType = targetSchema.getMutationType(); - } else { - rootType = targetSchema.getSubscriptionType(); - } - - 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 +71,7 @@ export function delegateRequest({ transformedSchema, skipValidation, skipTypeMerging, + binding, }: IDelegateRequestOptions) { let operationDefinition: OperationDefinitionNode; let targetOperation: OperationTypeNode; @@ -182,46 +93,37 @@ export function delegateRequest({ let targetSchema: GraphQLSchema; let targetRootValue: Record; - let requestTransforms: Array = transforms.slice(); let subschemaConfig: SubschemaConfig; if (isSubschemaConfig(subschemaOrSubschemaConfig)) { subschemaConfig = subschemaOrSubschemaConfig; targetSchema = subschemaConfig.schema; targetRootValue = rootValue ?? subschemaConfig?.rootValue ?? info?.rootValue; - if (subschemaConfig.transforms != null) { - requestTransforms = requestTransforms.concat(subschemaConfig.transforms); - } } else { targetSchema = subschemaOrSubschemaConfig; targetRootValue = rootValue ?? info?.rootValue; } - 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(), + context, + info, + returnType, + transforms, transformedSchema, - skipTypeMerging - ); + 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 +138,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 +156,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/types.ts b/packages/delegate/src/types.ts index fa115821913..5040d5eb3fc 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -10,9 +10,26 @@ import { FragmentDefinitionNode, GraphQLObjectType, VariableDefinitionNode, + OperationTypeNode, } from 'graphql'; import { Operation, Transform, Request, TypeMap, ExecutionResult } from '@graphql-tools/utils'; +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 Binding = (delegationContext: DelegationContext) => Array; + export interface IDelegateToSchemaOptions, TArgs = Record> { schema: GraphQLSchema | SubschemaConfig; operation?: Operation; @@ -28,6 +45,7 @@ export interface IDelegateToSchemaOptions, TArgs transformedSchema?: GraphQLSchema; skipValidation?: boolean; skipTypeMerging?: boolean; + binding?: Binding; } export interface IDelegateRequestOptions extends Omit { diff --git a/packages/utils/src/Interfaces.ts b/packages/utils/src/Interfaces.ts index 307662f3de3..1e61f965630 100644 --- a/packages/utils/src/Interfaces.ts +++ b/packages/utils/src/Interfaces.ts @@ -134,8 +134,16 @@ export interface IFieldResolverOptions GraphQLSchema; -export type RequestTransform = (originalRequest: Request) => Request; -export type ResultTransform = (originalResult: ExecutionResult) => ExecutionResult; +export type RequestTransform = ( + originalRequest: Request, + transformationContext?: Record, + delegationContext?: Record +) => Request; +export type ResultTransform = ( + originalResult: ExecutionResult, + transformationContext?: Record, + delegationContext?: Record +) => ExecutionResult; export interface Transform { transformSchema?: SchemaTransform;