Skip to content

Commit 71c0445

Browse files
authored
Fix stitching from and to interfaces (#1443)
* 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
1 parent faed0ed commit 71c0445

File tree

13 files changed

+478
-73
lines changed

13 files changed

+478
-73
lines changed

packages/delegate/src/createRequest.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import {
1717
DocumentNode,
1818
} from 'graphql';
1919

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

2323
export function getDelegatingOperation(parentType: GraphQLObjectType, schema: GraphQLSchema): OperationTypeNode {
2424
if (parentType === schema.getMutationType()) {

packages/delegate/src/delegateToSchema.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '@graphql-tools/utils';
1919

2020
import ExpandAbstractTypes from './transforms/ExpandAbstractTypes';
21+
import WrapConcreteTypes from './transforms/WrapConcreteTypes';
2122
import FilterToSchema from './transforms/FilterToSchema';
2223
import AddReplacementSelectionSets from './transforms/AddReplacementSelectionSets';
2324
import AddReplacementFragments from './transforms/AddReplacementFragments';
@@ -76,6 +77,7 @@ function buildDelegationTransforms(
7677
args: Record<string, any>,
7778
returnType: GraphQLOutputType,
7879
transforms: Array<Transform>,
80+
transformedSchema: GraphQLSchema,
7981
skipTypeMerging: boolean
8082
): Array<Transform> {
8183
let delegationTransforms: Array<Transform> = [
@@ -89,9 +91,15 @@ function buildDelegationTransforms(
8991
);
9092
}
9193

92-
delegationTransforms = delegationTransforms.concat(transforms);
94+
const transformedTargetSchema =
95+
info.mergeInfo == null
96+
? transformedSchema ?? targetSchema
97+
: transformedSchema ?? info.mergeInfo.transformedSchemas.get(subschemaOrSubschemaConfig) ?? targetSchema;
98+
99+
delegationTransforms.push(new WrapConcreteTypes(returnType, transformedTargetSchema));
100+
delegationTransforms.push(new ExpandAbstractTypes(info.schema, transformedTargetSchema));
93101

94-
delegationTransforms.push(new ExpandAbstractTypes(info.schema, targetSchema));
102+
delegationTransforms = delegationTransforms.concat(transforms);
95103

96104
if (info.mergeInfo != null) {
97105
delegationTransforms.push(new AddReplacementFragments(targetSchema, info.mergeInfo.replacementFragments));
@@ -117,6 +125,7 @@ export function delegateRequest({
117125
returnType = info.returnType,
118126
context,
119127
transforms = [],
128+
transformedSchema,
120129
skipValidation,
121130
skipTypeMerging,
122131
}: IDelegateRequestOptions) {
@@ -147,6 +156,7 @@ export function delegateRequest({
147156
args,
148157
returnType,
149158
requestTransforms.reverse(),
159+
transformedSchema,
150160
skipTypeMerging
151161
);
152162

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
DocumentNode,
3+
GraphQLSchema,
4+
Kind,
5+
getNamedType,
6+
GraphQLOutputType,
7+
isAbstractType,
8+
TypeInfo,
9+
visit,
10+
visitWithTypeInfo,
11+
isObjectType,
12+
FieldNode,
13+
} from 'graphql';
14+
15+
import { Transform, Request } from '@graphql-tools/utils';
16+
17+
// For motivation, see https://github.com/ardatan/graphql-tools/issues/751
18+
19+
export default class WrapConcreteTypes implements Transform {
20+
private readonly returnType: GraphQLOutputType;
21+
private readonly targetSchema: GraphQLSchema;
22+
23+
constructor(returnType: GraphQLOutputType, targetSchema: GraphQLSchema) {
24+
this.returnType = returnType;
25+
this.targetSchema = targetSchema;
26+
}
27+
28+
public transformRequest(originalRequest: Request): Request {
29+
const document = wrapConcreteTypes(this.returnType, this.targetSchema, originalRequest.document);
30+
return {
31+
...originalRequest,
32+
document,
33+
};
34+
}
35+
}
36+
37+
function wrapConcreteTypes(
38+
returnType: GraphQLOutputType,
39+
targetSchema: GraphQLSchema,
40+
document: DocumentNode
41+
): DocumentNode {
42+
const namedType = getNamedType(returnType);
43+
44+
if (!isObjectType(namedType)) {
45+
return document;
46+
}
47+
48+
const queryRootType = targetSchema.getQueryType();
49+
const mutationRootType = targetSchema.getMutationType();
50+
const subscriptionRootType = targetSchema.getSubscriptionType();
51+
52+
const typeInfo = new TypeInfo(targetSchema);
53+
const newDocument = visit(
54+
document,
55+
visitWithTypeInfo(typeInfo, {
56+
[Kind.FIELD](node: FieldNode) {
57+
const maybeType = typeInfo.getParentType();
58+
if (maybeType == null) {
59+
return false;
60+
}
61+
62+
const parentType = getNamedType(maybeType);
63+
if (parentType !== queryRootType && parentType !== mutationRootType && parentType !== subscriptionRootType) {
64+
return false;
65+
}
66+
67+
if (!isAbstractType(getNamedType(typeInfo.getType()))) {
68+
return false;
69+
}
70+
71+
return {
72+
...node,
73+
selectionSet: {
74+
kind: Kind.SELECTION_SET,
75+
selections: [
76+
{
77+
kind: Kind.INLINE_FRAGMENT,
78+
typeCondition: {
79+
kind: Kind.NAMED_TYPE,
80+
name: {
81+
kind: Kind.NAME,
82+
value: namedType.name,
83+
},
84+
},
85+
selectionSet: node.selectionSet,
86+
},
87+
],
88+
},
89+
};
90+
},
91+
})
92+
);
93+
94+
return newDocument;
95+
}

packages/delegate/src/types.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {
77
GraphQLResolveInfo,
88
GraphQLFieldResolver,
99
InlineFragmentNode,
10+
FragmentDefinitionNode,
11+
GraphQLObjectType,
12+
VariableDefinitionNode,
1013
} from 'graphql';
1114
import { Operation, Transform, Request, TypeMap, ExecutionResult } from '@graphql-tools/utils';
1215

@@ -22,6 +25,7 @@ export interface IDelegateToSchemaOptions<TContext = Record<string, any>, TArgs
2225
info: GraphQLResolveInfo;
2326
rootValue?: Record<string, any>;
2427
transforms?: Array<Transform>;
28+
transformedSchema?: GraphQLSchema;
2529
skipValidation?: boolean;
2630
skipTypeMerging?: boolean;
2731
}
@@ -38,6 +42,19 @@ export interface ICreateRequestFromInfo {
3842
fieldNodes?: ReadonlyArray<FieldNode>;
3943
}
4044

45+
export interface ICreateRequest {
46+
sourceSchema?: GraphQLSchema;
47+
sourceParentType?: GraphQLObjectType;
48+
sourceFieldName?: string;
49+
fragments?: Record<string, FragmentDefinitionNode>;
50+
variableDefinitions?: ReadonlyArray<VariableDefinitionNode>;
51+
variableValues?: Record<string, any>;
52+
targetOperation: Operation;
53+
targetFieldName: string;
54+
selectionSet?: SelectionSetNode;
55+
fieldNodes?: ReadonlyArray<FieldNode>;
56+
}
57+
4158
export interface MergedTypeInfo {
4259
subschemas: Array<SubschemaConfig>;
4360
selectionSet?: SelectionSetNode;
@@ -70,12 +87,15 @@ export type Subscriber = <TReturn = Record<string, any>, TArgs = Record<string,
7087
params: ExecutionParams<TArgs, TContext>
7188
) => Promise<AsyncIterator<ExecutionResult<TReturn>> | ExecutionResult<TReturn>>;
7289

73-
export type CreateProxyingResolverFn = (
74-
schema: GraphQLSchema | SubschemaConfig,
75-
transforms: Array<Transform>,
76-
operation: Operation,
77-
fieldName: string
78-
) => GraphQLFieldResolver<any, any>;
90+
export interface ICreateProxyingResolverOptions {
91+
schema: GraphQLSchema | SubschemaConfig;
92+
transforms?: Array<Transform>;
93+
transformedSchema?: GraphQLSchema;
94+
operation?: Operation;
95+
fieldName?: string;
96+
}
97+
98+
export type CreateProxyingResolverFn = (options: ICreateProxyingResolverOptions) => GraphQLFieldResolver<any, any>;
7999

80100
export interface SubschemaConfig {
81101
schema: GraphQLSchema;

packages/stitch/src/mergeInfo.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ import { delegateToSchema, isSubschemaConfig, SubschemaConfig } from '@graphql-t
2424
import { MergeTypeCandidate, MergedTypeInfo, MergeInfo, MergeTypeFilter } from './types';
2525

2626
export function createMergeInfo(
27+
transformedSchemas: Map<GraphQLSchema | SubschemaConfig, GraphQLSchema>,
2728
typeCandidates: Record<string, Array<MergeTypeCandidate>>,
2829
mergeTypes?: boolean | Array<string> | MergeTypeFilter
2930
): MergeInfo {
3031
return {
32+
transformedSchemas,
3133
fragments: [],
3234
replacementSelectionSets: undefined,
3335
replacementFragments: undefined,

packages/stitch/src/stitchSchemas.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,6 @@ export function stitchSchemas({
4949
throw new Error('Expected `resolverValidationOptions` to be an object');
5050
}
5151

52-
const typeCandidates: Record<string, Array<MergeTypeCandidate>> = Object.create(null);
53-
const extensions: Array<DocumentNode> = [];
54-
const directives: Array<GraphQLDirective> = [];
55-
const schemaDefs = Object.create(null);
56-
const operationTypeNames = {
57-
query: 'Query',
58-
mutation: 'Mutation',
59-
subscription: 'Subscription',
60-
};
61-
6252
let schemaLikeObjects: Array<GraphQLSchema | SubschemaConfig | DocumentNode | GraphQLNamedType> = [...subschemas];
6353
if (typeDefs) {
6454
schemaLikeObjects.push(buildDocumentFromTypeDefinitions(typeDefs, parseOptions));
@@ -78,8 +68,20 @@ export function stitchSchemas({
7868
}
7969
});
8070

71+
const transformedSchemas: Map<GraphQLSchema | SubschemaConfig, GraphQLSchema> = new Map();
72+
const typeCandidates: Record<string, Array<MergeTypeCandidate>> = Object.create(null);
73+
const extensions: Array<DocumentNode> = [];
74+
const directives: Array<GraphQLDirective> = [];
75+
const schemaDefs = Object.create(null);
76+
const operationTypeNames = {
77+
query: 'Query',
78+
mutation: 'Mutation',
79+
subscription: 'Subscription',
80+
};
81+
8182
buildTypeCandidates({
8283
schemaLikeObjects,
84+
transformedSchemas,
8385
typeCandidates,
8486
extensions,
8587
directives,
@@ -90,7 +92,7 @@ export function stitchSchemas({
9092

9193
let mergeInfo: MergeInfo;
9294

93-
mergeInfo = createMergeInfo(typeCandidates, mergeTypes);
95+
mergeInfo = createMergeInfo(transformedSchemas, typeCandidates, mergeTypes);
9496

9597
const typeMap = buildTypeMap({
9698
typeCandidates,

packages/stitch/src/typeCandidates.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ function isDocumentNode(schemaLikeObject: any): schemaLikeObject is DocumentNode
4343

4444
export function buildTypeCandidates({
4545
schemaLikeObjects,
46+
transformedSchemas,
4647
typeCandidates,
4748
extensions,
4849
directives,
@@ -51,6 +52,7 @@ export function buildTypeCandidates({
5152
mergeDirectives,
5253
}: {
5354
schemaLikeObjects: Array<GraphQLSchema | SubschemaConfig | DocumentNode | GraphQLNamedType>;
55+
transformedSchemas: Map<GraphQLSchema | SubschemaConfig, GraphQLSchema>;
5456
typeCandidates: Record<string, Array<MergeTypeCandidate>>;
5557
extensions: Array<DocumentNode>;
5658
directives: Array<GraphQLDirective>;
@@ -80,6 +82,8 @@ export function buildTypeCandidates({
8082
if (isSchema(schemaLikeObject) || isSubschemaConfig(schemaLikeObject)) {
8183
const schema = wrapSchema(schemaLikeObject);
8284

85+
transformedSchemas.set(schemaLikeObject, schema);
86+
8387
const operationTypes = {
8488
query: schema.getQueryType(),
8589
mutation: schema.getMutationType(),

packages/stitch/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface MergedTypeInfo {
2929
}
3030

3131
export interface MergeInfo {
32+
transformedSchemas: Map<GraphQLSchema | SubschemaConfig, GraphQLSchema>;
3233
fragments: Array<{
3334
field: string;
3435
fragment: string;

0 commit comments

Comments
 (0)