Skip to content

Commit

Permalink
enhance(delegate): refactor core delegation transforms (#3206)
Browse files Browse the repository at this point in the history
* refactor ExpandAbstractTypes as PrepareGatewayRequest

will consolidate remaining pre-user supplied transforms into this transform to hopefully reduce repetition

also adds memoization of schema metadata

* small refactors

* add visitorKeys argument

* small refactor

* import WrapConcreteFields into PrepareGatewayRequest

this does not use the same visitors and does not visit beneath fields, so can just be inlined

* info should be optional in DelegationContext

* fix types

* Consolidate VisitSelectionSets

* rename var

* internalize PrepareGatewayDocument as transform

as prepareGatewayDocument function

* move varName generator out of loop

to prevent recurrent looping

* refactor AddArgumentsAsVariables

* rename AddArgumentsAsVariables

to FinalizeGatewayRequest to consolidate post custom transforms

* arguments should be added to target final targetSchema

not transformedSchema

* move FilterToSchema tests to delegate package

* starting refacotr of FilterToSchema

* add visitorArgs

also -- unfortunately -- requires another visit pass to properly remove variables if an object or interface field does not have selections -- another approach could be to keep track of variable uses, and subtract at the end of the first pass

* integrate FilterToSchema into FinalizeGatewayRequest

* fold AddTypenameToAbstract into FinalizeGatewayRequest

* extract finalizeGatewayRequest function from the transform

* retire all delegation transforms

* fix build

* typo

* Add changeset

Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
  • Loading branch information
yaacovCR and ardatan committed Jul 26, 2021
1 parent 91155ab commit d53e3be
Show file tree
Hide file tree
Showing 24 changed files with 1,237 additions and 1,335 deletions.
10 changes: 10 additions & 0 deletions .changeset/empty-news-wave.md
@@ -0,0 +1,10 @@
---
'@graphql-tools/delegate': major
---

BREAKING CHANGES;

Refactor the core delegation transforms into individual functions to modify request and results. This will improve the performance considerably by reducing the number of visits over the request document.
- Replace `CheckResultAndHandleErrors` with `checkResultAndHandleErrors`
- Remove `delegationBindings`
- Replace `AddArgumentsAsVariables`, `AddSelectionSets`, `AddTypenameToAbstract`, `ExpandAbstractTypes`, `FilterToSchema`, `VisitSelectionSets` and `WrapConcreteTypes` with `prepareGatewayRequest` and `finalizeGatewayRequest`
30 changes: 21 additions & 9 deletions packages/delegate/src/Transformer.ts
@@ -1,8 +1,10 @@
import { ExecutionRequest, ExecutionResult } from '@graphql-tools/utils';

import { DelegationContext, DelegationBinding, Transform } from './types';
import { DelegationContext, Transform } from './types';

import { defaultDelegationBinding } from './delegationBindings';
import { prepareGatewayDocument } from './prepareGatewayDocument';
import { finalizeGatewayRequest } from './finalizeGatewayRequest';
import { checkResultAndHandleErrors } from './checkResultAndHandleErrors';

interface Transformation {
transform: Transform;
Expand All @@ -13,9 +15,10 @@ export class Transformer<TContext = Record<string, any>> {
private transformations: Array<Transformation> = [];
private delegationContext: DelegationContext<any>;

constructor(context: DelegationContext<TContext>, binding: DelegationBinding<TContext> = defaultDelegationBinding) {
constructor(context: DelegationContext<TContext>) {
this.delegationContext = context;
const delegationTransforms: Array<Transform> = binding(this.delegationContext);
const transforms = context.transforms;
const delegationTransforms = transforms.slice().reverse();
for (const transform of delegationTransforms) {
this.addTransform(transform, {});
}
Expand All @@ -25,23 +28,32 @@ export class Transformer<TContext = Record<string, any>> {
this.transformations.push({ transform, context });
}

public transformRequest(originalRequest: ExecutionRequest) {
return this.transformations.reduce(
public transformRequest(originalRequest: ExecutionRequest): ExecutionRequest {
const preparedRequest = {
...originalRequest,
document: prepareGatewayDocument(originalRequest.document, this.delegationContext),
};

const transformedRequest = this.transformations.reduce(
(request: ExecutionRequest, transformation: Transformation) =>
transformation.transform.transformRequest != null
? transformation.transform.transformRequest(request, this.delegationContext, transformation.context)
: request,
originalRequest
preparedRequest
);

return finalizeGatewayRequest(transformedRequest, this.delegationContext);
}

public transformResult(originalResult: ExecutionResult) {
return this.transformations.reduceRight(
public transformResult(originalResult: ExecutionResult): any {
const transformedResult = this.transformations.reduceRight(
(result: ExecutionResult, transformation: Transformation) =>
transformation.transform.transformResult != null
? transformation.transform.transformResult(result, this.delegationContext, transformation.context)
: result,
originalResult
);

return checkResultAndHandleErrors(transformedResult, this.delegationContext);
}
}
@@ -1,46 +1,21 @@
import {
GraphQLResolveInfo,
GraphQLOutputType,
GraphQLSchema,
GraphQLError,
responsePathAsArray,
locatedError,
} from 'graphql';
import { GraphQLResolveInfo, GraphQLOutputType, GraphQLError, responsePathAsArray, locatedError } from 'graphql';

import { AggregateError, getResponseKeyFromInfo, ExecutionResult, relocatedError } from '@graphql-tools/utils';

import { SubschemaConfig, Transform, DelegationContext } from '../types';
import { resolveExternalValue } from '../resolveExternalValue';

export default class CheckResultAndHandleErrors implements Transform {
public transformResult(
originalResult: ExecutionResult,
delegationContext: DelegationContext,
_transformationContext: Record<string, any>
): ExecutionResult {
return checkResultAndHandleErrors(
originalResult,
delegationContext.context!,
delegationContext.info,
delegationContext.fieldName,
delegationContext.subschema,
delegationContext.returnType,
delegationContext.skipTypeMerging,
delegationContext.onLocatedError
);
}
}
import { DelegationContext } from './types';
import { resolveExternalValue } from './resolveExternalValue';

export function checkResultAndHandleErrors(result: ExecutionResult, delegationContext: DelegationContext): any {
const {
context,
info,
fieldName: responseKey = getResponseKey(info),
subschema,
returnType = getReturnType(info),
skipTypeMerging,
onLocatedError,
} = delegationContext;

export function checkResultAndHandleErrors(
result: ExecutionResult,
context: Record<string, any>,
info: GraphQLResolveInfo,
responseKey: string = getResponseKeyFromInfo(info),
subschema: GraphQLSchema | SubschemaConfig,
returnType: GraphQLOutputType = info.returnType,
skipTypeMerging?: boolean,
onLocatedError?: (originalError: GraphQLError) => GraphQLError
): any {
const { data, unpathedErrors } = mergeDataAndErrors(
result.data == null ? undefined : result.data[responseKey],
result.errors == null ? [] : result.errors,
Expand Down Expand Up @@ -120,3 +95,17 @@ export function mergeDataAndErrors(

return { data, unpathedErrors };
}

function getResponseKey(info: GraphQLResolveInfo | undefined): string {
if (info == null) {
throw new Error(`Data cannot be extracted from result without an explicit key or source schema.`);
}
return getResponseKeyFromInfo(info);
}

function getReturnType(info: GraphQLResolveInfo | undefined): GraphQLOutputType {
if (info == null) {
throw new Error(`Return type cannot be inferred without a source schema.`);
}
return info.returnType;
}
14 changes: 11 additions & 3 deletions packages/delegate/src/createRequest.ts
Expand Up @@ -16,7 +16,12 @@ import {
DocumentNode,
} from 'graphql';

import { ExecutionRequest, serializeInputValue, updateArgument } from '@graphql-tools/utils';
import {
createVariableNameGenerator,
ExecutionRequest,
serializeInputValue,
updateArgument,
} from '@graphql-tools/utils';
import { ICreateRequestFromInfo, ICreateRequest } from './types';

export function getDelegatingOperation(parentType: GraphQLObjectType, schema: GraphQLSchema): OperationTypeNode {
Expand Down Expand Up @@ -194,6 +199,8 @@ function updateArgumentsWithDefaults(
variableDefinitionMap: Record<string, VariableDefinitionNode>,
variableValues: Record<string, any>
): void {
const generateVariableName = createVariableNameGenerator(variableDefinitionMap);

const sourceField = sourceParentType.getFields()[sourceFieldName];
for (const argument of sourceField.args) {
const argName = argument.name;
Expand All @@ -204,11 +211,12 @@ function updateArgumentsWithDefaults(

if (defaultValue !== undefined) {
updateArgument(
argName,
sourceArgType,
argumentNodeMap,
variableDefinitionMap,
variableValues,
argName,
generateVariableName(argName),
sourceArgType,
serializeInputValue(sourceArgType, defaultValue)
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/delegate/src/delegateToSchema.ts
Expand Up @@ -85,7 +85,7 @@ export function delegateRequest<TContext = Record<string, any>, TArgs = any>(
) {
const delegationContext = getDelegationContext(options);

const transformer = new Transformer<TContext>(delegationContext, options.binding);
const transformer = new Transformer<TContext>(delegationContext);

const processedRequest = transformer.transformRequest(options.request);

Expand Down
49 changes: 0 additions & 49 deletions packages/delegate/src/delegationBindings.ts

This file was deleted.

0 comments on commit d53e3be

Please sign in to comment.