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;