Skip to content

Commit 9238e14

Browse files
committed
enhance(stitch/federation): improvements on field merging and extraction of unavailable fields
1 parent f7f7ce3 commit 9238e14

File tree

6 files changed

+213
-60
lines changed

6 files changed

+213
-60
lines changed

.changeset/soft-otters-mix.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@graphql-tools/federation": patch
3+
"@graphql-tools/stitch": patch
4+
---
5+
6+
Improvements on field merging and extraction of unavailable fields

packages/federation/src/supergraph.ts

+65-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
EnumTypeDefinitionNode,
55
EnumValueDefinitionNode,
66
FieldDefinitionNode,
7+
GraphQLOutputType,
78
GraphQLSchema,
89
InputValueDefinitionNode,
910
InterfaceTypeDefinitionNode,
@@ -22,8 +23,14 @@ import {
2223
} from 'graphql';
2324
import { MergedTypeConfig, SubschemaConfig } from '@graphql-tools/delegate';
2425
import { buildHTTPExecutor } from '@graphql-tools/executor-http';
25-
import { stitchSchemas } from '@graphql-tools/stitch';
26-
import { type Executor } from '@graphql-tools/utils';
26+
import {
27+
getDefaultFieldConfigMerger,
28+
MergeFieldConfigCandidate,
29+
stitchSchemas,
30+
TypeMergingOptions,
31+
ValidationLevel,
32+
} from '@graphql-tools/stitch';
33+
import { memoize1, type Executor } from '@graphql-tools/utils';
2734
import {
2835
filterInternalFieldsAndTypes,
2936
getArgsFromKeysForFederation,
@@ -42,13 +49,60 @@ export interface GetSubschemasFromSupergraphSdlOpts {
4249
batch?: boolean;
4350
}
4451

52+
export function ensureSupergraphSDLAst(supergraphSdl: string | DocumentNode): DocumentNode {
53+
return typeof supergraphSdl === 'string'
54+
? parse(supergraphSdl, { noLocation: true })
55+
: supergraphSdl;
56+
}
57+
58+
function getTypeFieldMapFromSupergraphAST(supergraphAST: DocumentNode) {
59+
const typeFieldASTMap = new Map<
60+
string,
61+
Map<string, FieldDefinitionNode | InputValueDefinitionNode>
62+
>();
63+
for (const definition of supergraphAST.definitions) {
64+
if ('fields' in definition) {
65+
const fieldMap = new Map<string, FieldDefinitionNode | InputValueDefinitionNode>();
66+
typeFieldASTMap.set(definition.name.value, fieldMap);
67+
for (const field of definition.fields || []) {
68+
fieldMap.set(field.name.value, field);
69+
}
70+
}
71+
}
72+
return typeFieldASTMap;
73+
}
74+
75+
export function getFieldMergerFromSupergraphSdl(
76+
supergraphSdl: DocumentNode | string,
77+
): TypeMergingOptions['fieldConfigMerger'] {
78+
const supergraphAST = ensureSupergraphSDLAst(supergraphSdl);
79+
const typeFieldASTMap = getTypeFieldMapFromSupergraphAST(supergraphAST);
80+
const defaultMerger = getDefaultFieldConfigMerger(true);
81+
const memoizedASTPrint = memoize1(print);
82+
const memoizedTypePrint = memoize1((type: GraphQLOutputType) => type.toString());
83+
return function (candidates: MergeFieldConfigCandidate[]) {
84+
const filteredCandidates = candidates.filter(candidate => {
85+
const fieldASTMap = typeFieldASTMap.get(candidate.type.name);
86+
if (fieldASTMap) {
87+
const fieldAST = fieldASTMap.get(candidate.fieldName);
88+
if (fieldAST) {
89+
const typeNodeInAST = memoizedASTPrint(fieldAST.type);
90+
const typeNodeInCandidate = memoizedTypePrint(candidate.fieldConfig.type);
91+
return typeNodeInAST === typeNodeInCandidate;
92+
}
93+
}
94+
return false;
95+
});
96+
return defaultMerger(filteredCandidates.length ? filteredCandidates : candidates);
97+
};
98+
}
99+
45100
export function getSubschemasFromSupergraphSdl({
46101
supergraphSdl,
47102
onExecutor = ({ endpoint }) => buildHTTPExecutor({ endpoint }),
48103
batch = false,
49104
}: GetSubschemasFromSupergraphSdlOpts) {
50-
const ast =
51-
typeof supergraphSdl === 'string' ? parse(supergraphSdl, { noLocation: true }) : supergraphSdl;
105+
const supergraphAst = ensureSupergraphSDLAst(supergraphSdl);
52106
const subgraphEndpointMap = new Map<string, string>();
53107
const subgraphTypesMap = new Map<string, TypeDefinitionNode[]>();
54108
const typeNameKeysBySubgraphMap = new Map<string, Map<string, string[]>>();
@@ -58,7 +112,7 @@ export function getSubschemasFromSupergraphSdl({
58112
const orphanTypeMap = new Map<string, TypeDefinitionNode>();
59113
// TODO: Temporary fix to add missing join__type directives to Query
60114
const subgraphNames: string[] = [];
61-
visit(ast, {
115+
visit(supergraphAst, {
62116
EnumTypeDefinition(node) {
63117
if (node.name.value === 'join__Graph') {
64118
node.values?.forEach(valueNode => {
@@ -191,7 +245,7 @@ export function getSubschemasFromSupergraphSdl({
191245
extraFields = [];
192246
typeNameExtraFieldsMap.set(fieldNodeType.name.value, extraFields);
193247
}
194-
const extraFieldTypeNode = ast.definitions.find(
248+
const extraFieldTypeNode = supergraphAst.definitions.find(
195249
def => 'name' in def && def.name?.value === fieldNodeType.name.value,
196250
) as ObjectTypeDefinitionNode;
197251
providedExtraField.value.value.split(' ').forEach(extraField => {
@@ -311,7 +365,7 @@ export function getSubschemasFromSupergraphSdl({
311365
orphanTypeMap.set(typeNode.name.value, typeNode);
312366
}
313367
}
314-
visit(ast, {
368+
visit(supergraphAst, {
315369
ScalarTypeDefinition(node) {
316370
let isOrphan = !node.name.value.startsWith('link__') && !node.name.value.startsWith('join__');
317371
node.directives?.forEach(directiveNode => {
@@ -721,6 +775,10 @@ export function getStitchedSchemaFromSupergraphSdl(opts: GetSubschemasFromSuperg
721775
assumeValidSDL: true,
722776
typeMergingOptions: {
723777
useNonNullableFieldOnConflict: true,
778+
validationSettings: {
779+
validationLevel: ValidationLevel.Off,
780+
},
781+
fieldConfigMerger: getFieldMergerFromSupergraphSdl(opts.supergraphSdl),
724782
},
725783
});
726784
return filterInternalFieldsAndTypes(supergraphSchema);

packages/stitch/src/createDelegationPlanBuilder.ts

+16-12
Original file line numberDiff line numberDiff line change
@@ -145,17 +145,22 @@ function calculateDelegationStage(
145145
const fields = typeInSubschema.getFields();
146146
const field = fields[fieldNode.name.value];
147147
if (field != null) {
148-
const unavailableFields = extractUnavailableFields(field, fieldNode, fieldType => {
149-
if (!nonUniqueSubschema.merge?.[fieldType.name]) {
150-
delegationMap.set(nonUniqueSubschema, {
151-
kind: Kind.SELECTION_SET,
152-
selections: [fieldNode],
153-
});
154-
// Ignore unresolvable fields
155-
return false;
156-
}
157-
return true;
158-
});
148+
const unavailableFields = extractUnavailableFields(
149+
nonUniqueSubschema.transformedSchema,
150+
field,
151+
fieldNode,
152+
fieldType => {
153+
if (!nonUniqueSubschema.merge?.[fieldType.name]) {
154+
delegationMap.set(nonUniqueSubschema, {
155+
kind: Kind.SELECTION_SET,
156+
selections: [fieldNode],
157+
});
158+
// Ignore unresolvable fields
159+
return false;
160+
}
161+
return true;
162+
},
163+
);
159164
const currentScore = calculateScore(unavailableFields);
160165
if (currentScore < bestScore) {
161166
bestScore = currentScore;
@@ -256,7 +261,6 @@ export function createDelegationPlanBuilder(mergedTypeInfo: MergedTypeInfo): Del
256261
);
257262
delegationMap = delegationStage.delegationMap;
258263
}
259-
260264
return delegationMaps;
261265
});
262266
}

packages/stitch/src/getFieldsNotInSubschema.ts

+112-39
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@ import {
44
getNamedType,
55
GraphQLField,
66
GraphQLInterfaceType,
7+
GraphQLNamedOutputType,
8+
GraphQLNamedType,
79
GraphQLObjectType,
810
GraphQLSchema,
11+
isInterfaceType,
12+
isLeafType,
13+
isObjectType,
14+
isUnionType,
915
Kind,
1016
SelectionNode,
17+
SelectionSetNode,
1118
} from 'graphql';
1219
import { StitchingInfo } from '@graphql-tools/delegate';
13-
import { collectSubFields } from '@graphql-tools/utils';
20+
import { collectSubFields, Maybe } from '@graphql-tools/utils';
1421

1522
export function getFieldsNotInSubschema(
1623
schema: GraphQLSchema,
@@ -45,6 +52,7 @@ export function getFieldsNotInSubschema(
4552
const field = fields[fieldName];
4653
for (const subFieldNode of subFieldNodes) {
4754
const unavailableFields = extractUnavailableFields(
55+
schema,
4856
field,
4957
subFieldNode,
5058
(fieldType, selection) => !fieldNodesByField?.[fieldType.name]?.[selection.name.value],
@@ -77,51 +85,116 @@ export function getFieldsNotInSubschema(
7785
return Array.from(fieldsNotInSchema);
7886
}
7987

80-
export function extractUnavailableFields(
81-
field: GraphQLField<any, any>,
82-
fieldNode: FieldNode,
88+
export function extractUnavailableFieldsFromSelectionSet(
89+
schema: GraphQLSchema,
90+
fieldType: GraphQLNamedOutputType,
91+
fieldSelectionSet: SelectionSetNode,
8392
shouldAdd: (fieldType: GraphQLObjectType | GraphQLInterfaceType, selection: FieldNode) => boolean,
8493
) {
85-
if (fieldNode.selectionSet) {
86-
const fieldType = getNamedType(field.type);
87-
// TODO: Only object types are supported
88-
if (!('getFields' in fieldType)) {
89-
return [];
90-
}
91-
const subFields = fieldType.getFields();
94+
if (isLeafType(fieldType)) {
95+
return [];
96+
}
97+
if (isUnionType(fieldType)) {
9298
const unavailableSelections: SelectionNode[] = [];
93-
for (const selection of fieldNode.selectionSet.selections) {
94-
if (selection.kind === Kind.FIELD) {
95-
if (selection.name.value === '__typename') {
96-
continue;
99+
for (const type of fieldType.getTypes()) {
100+
// Exclude other inline fragments
101+
const fieldSelectionExcluded: SelectionSetNode = {
102+
...fieldSelectionSet,
103+
selections: fieldSelectionSet.selections.filter(selection =>
104+
selection.kind === Kind.INLINE_FRAGMENT
105+
? selection.typeCondition
106+
? selection.typeCondition.name.value === type.name
107+
: false
108+
: true,
109+
),
110+
};
111+
unavailableSelections.push(
112+
...extractUnavailableFieldsFromSelectionSet(
113+
schema,
114+
type,
115+
fieldSelectionExcluded,
116+
shouldAdd,
117+
),
118+
);
119+
}
120+
return unavailableSelections;
121+
}
122+
const subFields = fieldType.getFields();
123+
const unavailableSelections: SelectionNode[] = [];
124+
for (const selection of fieldSelectionSet.selections) {
125+
if (selection.kind === Kind.FIELD) {
126+
if (selection.name.value === '__typename') {
127+
continue;
128+
}
129+
const fieldName = selection.name.value;
130+
const selectionField = subFields[fieldName];
131+
if (!selectionField) {
132+
if (shouldAdd(fieldType, selection)) {
133+
unavailableSelections.push(selection);
97134
}
98-
const fieldName = selection.name.value;
99-
const selectionField = subFields[fieldName];
100-
if (!selectionField) {
101-
if (shouldAdd(fieldType, selection)) {
102-
unavailableSelections.push(selection);
103-
}
104-
} else {
105-
const unavailableSubFields = extractUnavailableFields(
106-
selectionField,
107-
selection,
108-
shouldAdd,
109-
);
110-
if (unavailableSubFields.length) {
111-
unavailableSelections.push({
112-
...selection,
113-
selectionSet: {
114-
kind: Kind.SELECTION_SET,
115-
selections: unavailableSubFields,
116-
},
117-
});
118-
}
135+
} else {
136+
const unavailableSubFields = extractUnavailableFields(
137+
schema,
138+
selectionField,
139+
selection,
140+
shouldAdd,
141+
);
142+
if (unavailableSubFields.length) {
143+
unavailableSelections.push({
144+
...selection,
145+
selectionSet: {
146+
kind: Kind.SELECTION_SET,
147+
selections: unavailableSubFields,
148+
},
149+
});
150+
}
151+
}
152+
} else if (selection.kind === Kind.INLINE_FRAGMENT) {
153+
const subFieldType: Maybe<GraphQLNamedType> = selection.typeCondition
154+
? schema.getType(selection.typeCondition.name.value)
155+
: fieldType;
156+
if (
157+
!(isInterfaceType(subFieldType) && isObjectType(subFieldType)) ||
158+
subFieldType === fieldType ||
159+
(isInterfaceType(fieldType) && schema.isSubType(fieldType, subFieldType))
160+
) {
161+
const unavailableFields = extractUnavailableFieldsFromSelectionSet(
162+
schema,
163+
fieldType,
164+
selection.selectionSet,
165+
shouldAdd,
166+
);
167+
if (unavailableFields.length) {
168+
unavailableSelections.push({
169+
...selection,
170+
selectionSet: {
171+
kind: Kind.SELECTION_SET,
172+
selections: unavailableFields,
173+
},
174+
});
119175
}
120-
} else if (selection.kind === Kind.INLINE_FRAGMENT) {
121-
// TODO: Support for inline fragments
176+
} else {
177+
unavailableSelections.push(selection);
122178
}
123179
}
124-
return unavailableSelections;
180+
}
181+
return unavailableSelections;
182+
}
183+
184+
export function extractUnavailableFields(
185+
schema: GraphQLSchema,
186+
field: GraphQLField<any, any>,
187+
fieldNode: FieldNode,
188+
shouldAdd: (fieldType: GraphQLObjectType | GraphQLInterfaceType, selection: FieldNode) => boolean,
189+
) {
190+
if (fieldNode.selectionSet) {
191+
const fieldType = getNamedType(field.type);
192+
return extractUnavailableFieldsFromSelectionSet(
193+
schema,
194+
fieldType,
195+
fieldNode.selectionSet,
196+
shouldAdd,
197+
);
125198
}
126199
return [];
127200
}

packages/stitch/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from './subschemaConfigTransforms/index.js';
66
export * from './types.js';
77
export * from './relay.js';
88
export * from './executor.js';
9+
export { getDefaultFieldConfigMerger } from './mergeCandidates.js';

0 commit comments

Comments
 (0)