Skip to content

Commit

Permalink
Fix stitching from and to interfaces (#1443)
Browse files Browse the repository at this point in the history
* ExpandAbstractTypes to check transformed subschema

An abstract type may be present in the target schema, but renamed. ExpandAbstractTypes expands the abstract types not present in the target schema, but it should check the transformed target schema, to not be misled by renaming.

This may require manually passing the transformed schema in some cases. The default can stay to assume no renaming.

Within stitched schemas, the correct transformed schema can be saved to and then read from info.mergeInfo.transformedSchema.

* move ICreateRequest to delegate package with ICreateRequestFromInfo

* Add WrapConcreteTypes transform

= fixes #751

* allow redelegation of nested root query fields

in some circumstances

nested root fields may not always successfully stitch to subschemas, for example when the root field is a new  abstract field stitching to a concrete field or the reverse

* provide transformedSchema argument in more instances

= changes CreateResolverFn signature to take an options argument of type ICreateResolverOptions
  • Loading branch information
yaacovCR authored May 4, 2020
1 parent faed0ed commit 71c0445
Show file tree
Hide file tree
Showing 13 changed files with 478 additions and 73 deletions.
4 changes: 2 additions & 2 deletions packages/delegate/src/createRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import {
DocumentNode,
} from 'graphql';

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

export function getDelegatingOperation(parentType: GraphQLObjectType, schema: GraphQLSchema): OperationTypeNode {
if (parentType === schema.getMutationType()) {
Expand Down
14 changes: 12 additions & 2 deletions packages/delegate/src/delegateToSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '@graphql-tools/utils';

import ExpandAbstractTypes from './transforms/ExpandAbstractTypes';
import WrapConcreteTypes from './transforms/WrapConcreteTypes';
import FilterToSchema from './transforms/FilterToSchema';
import AddReplacementSelectionSets from './transforms/AddReplacementSelectionSets';
import AddReplacementFragments from './transforms/AddReplacementFragments';
Expand Down Expand Up @@ -76,6 +77,7 @@ function buildDelegationTransforms(
args: Record<string, any>,
returnType: GraphQLOutputType,
transforms: Array<Transform>,
transformedSchema: GraphQLSchema,
skipTypeMerging: boolean
): Array<Transform> {
let delegationTransforms: Array<Transform> = [
Expand All @@ -89,9 +91,15 @@ function buildDelegationTransforms(
);
}

delegationTransforms = delegationTransforms.concat(transforms);
const transformedTargetSchema =
info.mergeInfo == null
? transformedSchema ?? targetSchema
: transformedSchema ?? info.mergeInfo.transformedSchemas.get(subschemaOrSubschemaConfig) ?? targetSchema;

delegationTransforms.push(new WrapConcreteTypes(returnType, transformedTargetSchema));
delegationTransforms.push(new ExpandAbstractTypes(info.schema, transformedTargetSchema));

delegationTransforms.push(new ExpandAbstractTypes(info.schema, targetSchema));
delegationTransforms = delegationTransforms.concat(transforms);

if (info.mergeInfo != null) {
delegationTransforms.push(new AddReplacementFragments(targetSchema, info.mergeInfo.replacementFragments));
Expand All @@ -117,6 +125,7 @@ export function delegateRequest({
returnType = info.returnType,
context,
transforms = [],
transformedSchema,
skipValidation,
skipTypeMerging,
}: IDelegateRequestOptions) {
Expand Down Expand Up @@ -147,6 +156,7 @@ export function delegateRequest({
args,
returnType,
requestTransforms.reverse(),
transformedSchema,
skipTypeMerging
);

Expand Down
95 changes: 95 additions & 0 deletions packages/delegate/src/transforms/WrapConcreteTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
DocumentNode,
GraphQLSchema,
Kind,
getNamedType,
GraphQLOutputType,
isAbstractType,
TypeInfo,
visit,
visitWithTypeInfo,
isObjectType,
FieldNode,
} from 'graphql';

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

// For motivation, see https://github.com/ardatan/graphql-tools/issues/751

export default class WrapConcreteTypes implements Transform {
private readonly returnType: GraphQLOutputType;
private readonly targetSchema: GraphQLSchema;

constructor(returnType: GraphQLOutputType, targetSchema: GraphQLSchema) {
this.returnType = returnType;
this.targetSchema = targetSchema;
}

public transformRequest(originalRequest: Request): Request {
const document = wrapConcreteTypes(this.returnType, this.targetSchema, originalRequest.document);
return {
...originalRequest,
document,
};
}
}

function wrapConcreteTypes(
returnType: GraphQLOutputType,
targetSchema: GraphQLSchema,
document: DocumentNode
): DocumentNode {
const namedType = getNamedType(returnType);

if (!isObjectType(namedType)) {
return document;
}

const queryRootType = targetSchema.getQueryType();
const mutationRootType = targetSchema.getMutationType();
const subscriptionRootType = targetSchema.getSubscriptionType();

const typeInfo = new TypeInfo(targetSchema);
const newDocument = visit(
document,
visitWithTypeInfo(typeInfo, {
[Kind.FIELD](node: FieldNode) {
const maybeType = typeInfo.getParentType();
if (maybeType == null) {
return false;
}

const parentType = getNamedType(maybeType);
if (parentType !== queryRootType && parentType !== mutationRootType && parentType !== subscriptionRootType) {
return false;
}

if (!isAbstractType(getNamedType(typeInfo.getType()))) {
return false;
}

return {
...node,
selectionSet: {
kind: Kind.SELECTION_SET,
selections: [
{
kind: Kind.INLINE_FRAGMENT,
typeCondition: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: namedType.name,
},
},
selectionSet: node.selectionSet,
},
],
},
};
},
})
);

return newDocument;
}
32 changes: 26 additions & 6 deletions packages/delegate/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
GraphQLResolveInfo,
GraphQLFieldResolver,
InlineFragmentNode,
FragmentDefinitionNode,
GraphQLObjectType,
VariableDefinitionNode,
} from 'graphql';
import { Operation, Transform, Request, TypeMap, ExecutionResult } from '@graphql-tools/utils';

Expand All @@ -22,6 +25,7 @@ export interface IDelegateToSchemaOptions<TContext = Record<string, any>, TArgs
info: GraphQLResolveInfo;
rootValue?: Record<string, any>;
transforms?: Array<Transform>;
transformedSchema?: GraphQLSchema;
skipValidation?: boolean;
skipTypeMerging?: boolean;
}
Expand All @@ -38,6 +42,19 @@ export interface ICreateRequestFromInfo {
fieldNodes?: ReadonlyArray<FieldNode>;
}

export interface ICreateRequest {
sourceSchema?: GraphQLSchema;
sourceParentType?: GraphQLObjectType;
sourceFieldName?: string;
fragments?: Record<string, FragmentDefinitionNode>;
variableDefinitions?: ReadonlyArray<VariableDefinitionNode>;
variableValues?: Record<string, any>;
targetOperation: Operation;
targetFieldName: string;
selectionSet?: SelectionSetNode;
fieldNodes?: ReadonlyArray<FieldNode>;
}

export interface MergedTypeInfo {
subschemas: Array<SubschemaConfig>;
selectionSet?: SelectionSetNode;
Expand Down Expand Up @@ -70,12 +87,15 @@ export type Subscriber = <TReturn = Record<string, any>, TArgs = Record<string,
params: ExecutionParams<TArgs, TContext>
) => Promise<AsyncIterator<ExecutionResult<TReturn>> | ExecutionResult<TReturn>>;

export type CreateProxyingResolverFn = (
schema: GraphQLSchema | SubschemaConfig,
transforms: Array<Transform>,
operation: Operation,
fieldName: string
) => GraphQLFieldResolver<any, any>;
export interface ICreateProxyingResolverOptions {
schema: GraphQLSchema | SubschemaConfig;
transforms?: Array<Transform>;
transformedSchema?: GraphQLSchema;
operation?: Operation;
fieldName?: string;
}

export type CreateProxyingResolverFn = (options: ICreateProxyingResolverOptions) => GraphQLFieldResolver<any, any>;

export interface SubschemaConfig {
schema: GraphQLSchema;
Expand Down
2 changes: 2 additions & 0 deletions packages/stitch/src/mergeInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ import { delegateToSchema, isSubschemaConfig, SubschemaConfig } from '@graphql-t
import { MergeTypeCandidate, MergedTypeInfo, MergeInfo, MergeTypeFilter } from './types';

export function createMergeInfo(
transformedSchemas: Map<GraphQLSchema | SubschemaConfig, GraphQLSchema>,
typeCandidates: Record<string, Array<MergeTypeCandidate>>,
mergeTypes?: boolean | Array<string> | MergeTypeFilter
): MergeInfo {
return {
transformedSchemas,
fragments: [],
replacementSelectionSets: undefined,
replacementFragments: undefined,
Expand Down
24 changes: 13 additions & 11 deletions packages/stitch/src/stitchSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,6 @@ export function stitchSchemas({
throw new Error('Expected `resolverValidationOptions` to be an object');
}

const typeCandidates: Record<string, Array<MergeTypeCandidate>> = Object.create(null);
const extensions: Array<DocumentNode> = [];
const directives: Array<GraphQLDirective> = [];
const schemaDefs = Object.create(null);
const operationTypeNames = {
query: 'Query',
mutation: 'Mutation',
subscription: 'Subscription',
};

let schemaLikeObjects: Array<GraphQLSchema | SubschemaConfig | DocumentNode | GraphQLNamedType> = [...subschemas];
if (typeDefs) {
schemaLikeObjects.push(buildDocumentFromTypeDefinitions(typeDefs, parseOptions));
Expand All @@ -78,8 +68,20 @@ export function stitchSchemas({
}
});

const transformedSchemas: Map<GraphQLSchema | SubschemaConfig, GraphQLSchema> = new Map();
const typeCandidates: Record<string, Array<MergeTypeCandidate>> = Object.create(null);
const extensions: Array<DocumentNode> = [];
const directives: Array<GraphQLDirective> = [];
const schemaDefs = Object.create(null);
const operationTypeNames = {
query: 'Query',
mutation: 'Mutation',
subscription: 'Subscription',
};

buildTypeCandidates({
schemaLikeObjects,
transformedSchemas,
typeCandidates,
extensions,
directives,
Expand All @@ -90,7 +92,7 @@ export function stitchSchemas({

let mergeInfo: MergeInfo;

mergeInfo = createMergeInfo(typeCandidates, mergeTypes);
mergeInfo = createMergeInfo(transformedSchemas, typeCandidates, mergeTypes);

const typeMap = buildTypeMap({
typeCandidates,
Expand Down
4 changes: 4 additions & 0 deletions packages/stitch/src/typeCandidates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function isDocumentNode(schemaLikeObject: any): schemaLikeObject is DocumentNode

export function buildTypeCandidates({
schemaLikeObjects,
transformedSchemas,
typeCandidates,
extensions,
directives,
Expand All @@ -51,6 +52,7 @@ export function buildTypeCandidates({
mergeDirectives,
}: {
schemaLikeObjects: Array<GraphQLSchema | SubschemaConfig | DocumentNode | GraphQLNamedType>;
transformedSchemas: Map<GraphQLSchema | SubschemaConfig, GraphQLSchema>;
typeCandidates: Record<string, Array<MergeTypeCandidate>>;
extensions: Array<DocumentNode>;
directives: Array<GraphQLDirective>;
Expand Down Expand Up @@ -80,6 +82,8 @@ export function buildTypeCandidates({
if (isSchema(schemaLikeObject) || isSubschemaConfig(schemaLikeObject)) {
const schema = wrapSchema(schemaLikeObject);

transformedSchemas.set(schemaLikeObject, schema);

const operationTypes = {
query: schema.getQueryType(),
mutation: schema.getMutationType(),
Expand Down
1 change: 1 addition & 0 deletions packages/stitch/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface MergedTypeInfo {
}

export interface MergeInfo {
transformedSchemas: Map<GraphQLSchema | SubschemaConfig, GraphQLSchema>;
fragments: Array<{
field: string;
fragment: string;
Expand Down
Loading

0 comments on commit 71c0445

Please sign in to comment.