Skip to content

Commit

Permalink
refactor application of transforms
Browse files Browse the repository at this point in the history
= addition of Transformer class responsible for applying transforms
= addition of transformedSchemas option within delegateToSchema that can
provide every schema along transformation path to
delegateToSchema/delegateRequest/and appropriate transforms (this
obviates the need for utilizing the transformSchema method simply for
saving state)
= provide shared transformationContext to transformRequest and transformResult methods which may allow more sophisticated transforms.
= provide overall delegationContext to transforms
= next steps are to allow overriding of the standard prefix and postfix
delegationTransforms with custom sets of transforms generated from the
delegationContext
  • Loading branch information
yaacovCR committed Jun 2, 2020
1 parent e2d0034 commit e9a63c5
Show file tree
Hide file tree
Showing 39 changed files with 391 additions and 228 deletions.
174 changes: 174 additions & 0 deletions packages/delegate/src/Transformer.ts
@@ -0,0 +1,174 @@
import { GraphQLSchema, GraphQLOutputType, GraphQLResolveInfo, OperationTypeNode, GraphQLObjectType } from 'graphql';

import { Request, ExecutionResult } from '@graphql-tools/utils';

import {
SubschemaConfig,
StitchingInfo,
isSubschemaConfig,
Transform,
Transformation,
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(
info: GraphQLResolveInfo,
targetSchema: GraphQLSchema,
operation: OperationTypeNode,
fieldName: string
): GraphQLOutputType {
if (info != null) {
return info.returnType;
}

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 class Transformer {
private transformations: Array<Transformation> = [];
private delegationContext: DelegationContext;

constructor(options: {
subschema: GraphQLSchema | SubschemaConfig;
targetSchema: GraphQLSchema;
operation: OperationTypeNode;
fieldName: string;
args: Record<string, any>;
context: Record<string, any>;
info: GraphQLResolveInfo;
returnType: GraphQLOutputType;
transforms: Array<Transform>;
transformedSchema: GraphQLSchema;
transformedSchemas: Array<GraphQLSchema>;
skipTypeMerging: boolean;
}) {
const {
subschema: schemaOrSubschemaConfig,
targetSchema,
operation,
fieldName,
args,
context,
info,
returnType,
transforms = [],
transformedSchema,
transformedSchemas = [],
skipTypeMerging,
} = options;

let subschemaTransforms: Array<Transform> = [];

if (isSubschemaConfig(schemaOrSubschemaConfig)) {
subschemaTransforms =
schemaOrSubschemaConfig.transforms != null ? schemaOrSubschemaConfig.transforms.concat(transforms) : transforms;
} else {
subschemaTransforms = transforms;
}

const stitchingInfo: StitchingInfo = info?.schema.extensions?.stitchingInfo;

const transformedTargetSchema =
stitchingInfo == null
? transformedSchemas.length
? transformedSchemas[0]
: transformedSchema ?? targetSchema
: stitchingInfo.transformedSchemas.get(schemaOrSubschemaConfig) ??
(transformedSchemas.length ? transformedSchemas[0] : transformedSchema ?? targetSchema);

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

this.delegationContext = {
...options,
stitchingInfo,
transformedSchema: transformedTargetSchema,
returnType: delegationReturnType,
};

this.addTransform(
new CheckResultAndHandleErrors(
info,
fieldName,
schemaOrSubschemaConfig,
context,
delegationReturnType,
skipTypeMerging
)
);

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

this.addTransform(new WrapConcreteTypes(delegationReturnType, transformedTargetSchema));

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

for (let i = subschemaTransforms.length - 1; i > -1; i--) {
const transform = subschemaTransforms[i];
const targetSchema = transformedSchemas[i + 1];
const transformedSchema = transformedSchemas[i];
this.addTransform(transform, {
transformedSchema,
targetSchema,
});
}

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

if (args != null) {
this.addTransform(new AddArgumentsAsVariables(targetSchema, args));
}

this.addTransform(new FilterToSchema(targetSchema));
this.addTransform(new AddTypenameToAbstract(targetSchema));
}

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
);
}
}
139 changes: 21 additions & 118 deletions packages/delegate/src/delegateToSchema.ts
Expand Up @@ -4,42 +4,24 @@ import {
validate,
GraphQLSchema,
ExecutionResult,
GraphQLOutputType,
isSchema,
GraphQLResolveInfo,
FieldDefinitionNode,
getOperationAST,
OperationTypeNode,
GraphQLObjectType,
OperationDefinitionNode,
} 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 { Transformer } from './Transformer';

export function delegateToSchema(options: IDelegateToSchemaOptions | GraphQLSchema): any {
if (isSchema(options)) {
Expand Down Expand Up @@ -74,79 +56,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<any, any>;
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<string, any>,
targetSchema: GraphQLSchema,
fieldName: string,
args: Record<string, any>,
returnType: GraphQLOutputType,
transforms: Array<Transform>,
transformedSchema: GraphQLSchema,
skipTypeMerging: boolean
): Array<Transform> {
const stitchingInfo: StitchingInfo = info?.schema.extensions?.stitchingInfo;

let delegationTransforms: Array<Transform> = [
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,
Expand All @@ -159,6 +68,7 @@ export function delegateRequest({
context,
transforms = [],
transformedSchema,
transformedSchemas = [],
skipValidation,
skipTypeMerging,
}: IDelegateRequestOptions) {
Expand All @@ -182,35 +92,33 @@ export function delegateRequest({

let targetSchema: GraphQLSchema;
let targetRootValue: Record<string, any>;
let requestTransforms: Array<Transform> = 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 transformer = new Transformer({
subschema: subschemaOrSubschemaConfig,
targetSchema,
targetFieldName,
operation: targetOperation,
fieldName: targetFieldName,
args,
returnType ?? getDelegationReturnType(info, targetSchema, targetOperation, targetFieldName),
requestTransforms.reverse(),
context,
info,
returnType,
transforms,
transformedSchema,
skipTypeMerging
);
transformedSchemas,
skipTypeMerging,
});

const processedRequest = applyRequestTransforms(request, delegationTransforms);
const processedRequest = transformer.transformRequest(request);

if (!skipValidation) {
const errors = validate(targetSchema, processedRequest.document);
Expand All @@ -236,9 +144,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 =
Expand All @@ -254,18 +162,13 @@ export function delegateRequest({
// "subscribe" to the subscription result and map the result through the transforms
return mapAsyncIterator<ExecutionResult, any>(
subscriptionResult as AsyncIterableIterator<ExecutionResult>,
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);
});
}

Expand Down

0 comments on commit e9a63c5

Please sign in to comment.