@apollo/federation-internals 2.1.0-alpha.3 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -7
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +1 -1
- package/dist/buildSchema.js.map +1 -1
- package/dist/coreSpec.d.ts +1 -4
- package/dist/coreSpec.d.ts.map +1 -1
- package/dist/coreSpec.js +1 -5
- package/dist/coreSpec.js.map +1 -1
- package/dist/definitions.d.ts +26 -29
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +202 -158
- package/dist/definitions.js.map +1 -1
- package/dist/error.d.ts +5 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +47 -1
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +126 -5
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts +2 -2
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +6 -6
- package/dist/federation.js.map +1 -1
- package/dist/operations.d.ts +2 -2
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +26 -23
- package/dist/operations.js.map +1 -1
- package/dist/print.js.map +1 -1
- package/dist/schemaUpgrader.d.ts.map +1 -1
- package/dist/schemaUpgrader.js +6 -6
- package/dist/schemaUpgrader.js.map +1 -1
- package/dist/supergraphs.d.ts +1 -3
- package/dist/supergraphs.d.ts.map +1 -1
- package/dist/supergraphs.js +9 -22
- package/dist/supergraphs.js.map +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +12 -1
- package/dist/utils.js.map +1 -1
- package/package.json +3 -4
- package/src/__tests__/coreSpec.test.ts +1 -1
- package/src/__tests__/definitions.test.ts +13 -4
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +9 -5
- package/src/__tests__/removeInaccessibleElements.test.ts +1 -3
- package/src/__tests__/schemaUpgrader.test.ts +0 -1
- package/src/__tests__/subgraphValidation.test.ts +1 -2
- package/src/buildSchema.ts +1 -2
- package/src/coreSpec.ts +2 -7
- package/src/definitions.ts +173 -148
- package/src/error.ts +62 -0
- package/src/extractSubgraphsFromSupergraph.ts +178 -7
- package/src/federation.ts +4 -3
- package/src/operations.ts +42 -30
- package/src/print.ts +2 -2
- package/src/schemaUpgrader.ts +7 -6
- package/src/supergraphs.ts +16 -25
- package/src/utils.ts +15 -0
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/error.ts
CHANGED
|
@@ -56,6 +56,62 @@ export function extractGraphQLErrorOptions(e: GraphQLError): GraphQLErrorOptions
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
class AggregateGraphQLError extends GraphQLError {
|
|
60
|
+
constructor(
|
|
61
|
+
code: String,
|
|
62
|
+
message: string,
|
|
63
|
+
readonly causes: GraphQLError[],
|
|
64
|
+
options?: GraphQLErrorOptions,
|
|
65
|
+
) {
|
|
66
|
+
super(
|
|
67
|
+
message + '. Caused by:\n' + causes.map((c) => c.toString()).join('\n\n'),
|
|
68
|
+
{
|
|
69
|
+
...options,
|
|
70
|
+
extensions: { code },
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
toString() {
|
|
76
|
+
let output = `[${this.extensions.code}] ${super.toString()}`
|
|
77
|
+
output += "\ncaused by:";
|
|
78
|
+
for (const cause of this.causes) {
|
|
79
|
+
output += "\n\n - ";
|
|
80
|
+
output += cause.toString().split("\n").join("\n ");
|
|
81
|
+
}
|
|
82
|
+
return output;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function aggregateError(code: String, message: string, causes: GraphQLError[]): GraphQLError {
|
|
87
|
+
return new AggregateGraphQLError(code, message, causes);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Given an error, check if it is a graphQL error and potentially extract its causes if is aggregate.
|
|
92
|
+
* If the error is not a graphQL error, undefined is returned.
|
|
93
|
+
*/
|
|
94
|
+
export function errorCauses(e: Error): GraphQLError[] | undefined {
|
|
95
|
+
if (e instanceof AggregateGraphQLError) {
|
|
96
|
+
return e.causes;
|
|
97
|
+
}
|
|
98
|
+
if (e instanceof GraphQLError) {
|
|
99
|
+
return [e];
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function printGraphQLErrorsOrRethrow(e: Error): string {
|
|
105
|
+
const causes = errorCauses(e);
|
|
106
|
+
if (!causes) {
|
|
107
|
+
throw e;
|
|
108
|
+
}
|
|
109
|
+
return causes.map(e => e.toString()).join('\n\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function printErrors(errors: GraphQLError[]): string {
|
|
113
|
+
return errors.map(e => e.toString()).join('\n\n');
|
|
114
|
+
}
|
|
59
115
|
/*
|
|
60
116
|
* Most codes currently originate from the initial fed 2 release so we use this for convenience.
|
|
61
117
|
* This can be changed later, inline versions everywhere, if that becomes irrelevant.
|
|
@@ -144,6 +200,11 @@ const TYPE_DEFINITION_INVALID = makeCodeDefinition(
|
|
|
144
200
|
'A built-in or federation type has an invalid definition in the schema.',
|
|
145
201
|
);
|
|
146
202
|
|
|
203
|
+
const UNSUPPORTED_LINKED_FEATURE = makeCodeDefinition(
|
|
204
|
+
'UNSUPPORTED_LINKED_FEATURE',
|
|
205
|
+
'Indicates that a feature used in a @link is either unsupported or is used with unsupported options.',
|
|
206
|
+
);
|
|
207
|
+
|
|
147
208
|
const UNKNOWN_FEDERATION_LINK_VERSION = makeCodeDefinition(
|
|
148
209
|
'UNKNOWN_FEDERATION_LINK_VERSION',
|
|
149
210
|
'The version of federation in a @link directive on the schema is unknown.',
|
|
@@ -479,6 +540,7 @@ export const ERRORS = {
|
|
|
479
540
|
INVALID_GRAPHQL,
|
|
480
541
|
DIRECTIVE_DEFINITION_INVALID,
|
|
481
542
|
TYPE_DEFINITION_INVALID,
|
|
543
|
+
UNSUPPORTED_LINKED_FEATURE,
|
|
482
544
|
UNKNOWN_FEDERATION_LINK_VERSION,
|
|
483
545
|
UNKNOWN_LINK_VERSION,
|
|
484
546
|
KEY_FIELDS_HAS_ARGS,
|
|
@@ -34,6 +34,7 @@ import { validateSupergraph } from "./supergraphs";
|
|
|
34
34
|
import { builtTypeReference } from "./buildSchema";
|
|
35
35
|
import { isSubtype } from "./types";
|
|
36
36
|
import { printSchema } from "./print";
|
|
37
|
+
import { parseSelectionSet } from "./operations";
|
|
37
38
|
import fs from 'fs';
|
|
38
39
|
import path from 'path';
|
|
39
40
|
import { validateStringContainsBoolean } from "./utils";
|
|
@@ -82,6 +83,114 @@ class SubgraphExtractionError {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
function collectFieldReachableTypesForSubgraph(
|
|
87
|
+
supergraph: Schema,
|
|
88
|
+
subgraphName: string,
|
|
89
|
+
addReachableType: (t: NamedType) => void,
|
|
90
|
+
fieldInfoInSubgraph: (f: FieldDefinition<any> | InputFieldDefinition, subgraphName: string) => { isInSubgraph: boolean, typesInFederationDirectives: NamedType[] },
|
|
91
|
+
typeInfoInSubgraph: (t: NamedType, subgraphName: string) => { isEntityWithKeyInSubgraph: boolean, typesInFederationDirectives: NamedType[] },
|
|
92
|
+
): void {
|
|
93
|
+
const seenTypes = new Set<string>();
|
|
94
|
+
// The types reachable at "top-level" are both the root types, plus any entity type with a key in this subgraph.
|
|
95
|
+
const stack = supergraph.schemaDefinition.roots().map((root) => root.type as NamedType)
|
|
96
|
+
for (const type of supergraph.types()) {
|
|
97
|
+
const { isEntityWithKeyInSubgraph, typesInFederationDirectives } = typeInfoInSubgraph(type, subgraphName);
|
|
98
|
+
if (isEntityWithKeyInSubgraph) {
|
|
99
|
+
stack.push(type);
|
|
100
|
+
}
|
|
101
|
+
typesInFederationDirectives.forEach((t) => stack.push(t));
|
|
102
|
+
}
|
|
103
|
+
while (stack.length > 0) {
|
|
104
|
+
const type = stack.pop()!;
|
|
105
|
+
addReachableType(type);
|
|
106
|
+
if (seenTypes.has(type.name)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
seenTypes.add(type.name);
|
|
110
|
+
switch (type.kind) {
|
|
111
|
+
// @ts-expect-error: we fall-through to ObjectType for fields and implemented interfaces.
|
|
112
|
+
case 'InterfaceType':
|
|
113
|
+
// If an interface if reachable, then all of its implementation are too (a field returning the interface could return any of the
|
|
114
|
+
// implementation at runtime typically).
|
|
115
|
+
type.allImplementations().forEach((t) => stack.push(t));
|
|
116
|
+
case 'ObjectType':
|
|
117
|
+
type.interfaces().forEach((t) => stack.push(t));
|
|
118
|
+
for (const field of type.fields()) {
|
|
119
|
+
const { isInSubgraph, typesInFederationDirectives } = fieldInfoInSubgraph(field, subgraphName);
|
|
120
|
+
if (isInSubgraph) {
|
|
121
|
+
field.arguments().forEach((arg) => stack.push(baseType(arg.type!)));
|
|
122
|
+
stack.push(baseType(field.type!));
|
|
123
|
+
typesInFederationDirectives.forEach((t) => stack.push(t));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
case 'InputObjectType':
|
|
128
|
+
for (const field of type.fields()) {
|
|
129
|
+
const { isInSubgraph, typesInFederationDirectives } = fieldInfoInSubgraph(field, subgraphName);
|
|
130
|
+
if (isInSubgraph) {
|
|
131
|
+
stack.push(baseType(field.type!));
|
|
132
|
+
typesInFederationDirectives.forEach((t) => stack.push(t));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
case 'UnionType':
|
|
137
|
+
type.members().forEach((m) => stack.push(m.type));
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const directive of supergraph.directives()) {
|
|
143
|
+
// In fed1 supergraphs, which is the only place this is called, only executable directive from subgraph only ever made
|
|
144
|
+
// it to the supergraph. Skipping anything else saves us from worrying about supergraph-specific directives too.
|
|
145
|
+
if (!directive.hasExecutableLocations()) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
directive.arguments().forEach((arg) => stack.push(baseType(arg.type!)));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function collectFieldReachableTypesForAllSubgraphs(
|
|
153
|
+
supergraph: Schema,
|
|
154
|
+
allSubgraphs: readonly string[],
|
|
155
|
+
fieldInfoInSubgraph: (f: FieldDefinition<any> | InputFieldDefinition, subgraphName: string) => { isInSubgraph: boolean, typesInFederationDirectives: NamedType[] },
|
|
156
|
+
typeInfoInSubgraph: (t: NamedType, subgraphName: string) => { isEntityWithKeyInSubgraph: boolean, typesInFederationDirectives: NamedType[] },
|
|
157
|
+
): Map<string, Set<string>> {
|
|
158
|
+
const reachableTypesBySubgraphs = new Map<string, Set<string>>();
|
|
159
|
+
for (const subgraphName of allSubgraphs) {
|
|
160
|
+
const reachableTypes = new Set<string>();
|
|
161
|
+
collectFieldReachableTypesForSubgraph(
|
|
162
|
+
supergraph,
|
|
163
|
+
subgraphName,
|
|
164
|
+
(t) => reachableTypes.add(t.name),
|
|
165
|
+
fieldInfoInSubgraph,
|
|
166
|
+
typeInfoInSubgraph,
|
|
167
|
+
);
|
|
168
|
+
reachableTypesBySubgraphs.set(subgraphName, reachableTypes);
|
|
169
|
+
}
|
|
170
|
+
return reachableTypesBySubgraphs;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function typesUsedInFederationDirective(fieldSet: string | undefined, parentType: CompositeType): NamedType[] {
|
|
174
|
+
if (!fieldSet) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const usedTypes: NamedType[] = [];
|
|
179
|
+
parseSelectionSet({
|
|
180
|
+
parentType,
|
|
181
|
+
source: fieldSet,
|
|
182
|
+
fieldAccessor: (type, fieldName) => {
|
|
183
|
+
const field = type.field(fieldName);
|
|
184
|
+
if (field) {
|
|
185
|
+
usedTypes.push(baseType(field.type!));
|
|
186
|
+
}
|
|
187
|
+
return field;
|
|
188
|
+
},
|
|
189
|
+
validate: false,
|
|
190
|
+
});
|
|
191
|
+
return usedTypes;
|
|
192
|
+
}
|
|
193
|
+
|
|
85
194
|
export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
|
|
86
195
|
const [coreFeatures, joinSpec] = validateSupergraph(supergraph);
|
|
87
196
|
const isFed1 = joinSpec.version.equals(new FeatureVersion(0, 1));
|
|
@@ -90,6 +199,61 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
|
|
|
90
199
|
const [subgraphs, graphEnumNameToSubgraphName] = collectEmptySubgraphs(supergraph, joinSpec);
|
|
91
200
|
const typeDirective = joinSpec.typeDirective(supergraph);
|
|
92
201
|
const implementsDirective = joinSpec.implementsDirective(supergraph);
|
|
202
|
+
const ownerDirective = joinSpec.ownerDirective(supergraph);
|
|
203
|
+
const fieldDirective = joinSpec.fieldDirective(supergraph);
|
|
204
|
+
|
|
205
|
+
const getSubgraph = (application: Directive<any, { graph: string }>) => graphEnumNameToSubgraphName.get(application.arguments().graph);
|
|
206
|
+
|
|
207
|
+
/*
|
|
208
|
+
* Fed2 supergraph have "provenance" information for all types and fields, so we can faithfully extract subgraph relatively easily.
|
|
209
|
+
* For fed1 supergraph however, only entity types are marked with `@join__type` and `@join__field`. Which mean that for value types,
|
|
210
|
+
* we cannot directly know in which subgraphs they were initially defined. One strategy consists in "extracting" value types into
|
|
211
|
+
* all subgraphs blindly: functionally, having some unused types in an extracted subgraph schema does not matter much. However, adding
|
|
212
|
+
* those useless types increases memory usage, and we've seen some case with lots of subgraphs and lots of value types where those
|
|
213
|
+
* unused types balloon up memory usage (from 100MB to 1GB in one example; obviously, this is made worst by the fact that javascript
|
|
214
|
+
* is pretty memory heavy in the first place). So to avoid that problem, for fed1 supergraph, we do a first pass where we collect
|
|
215
|
+
* for all the subgraphs the set of types that are actually reachable in that subgraph. As we extract do the actual type extraction,
|
|
216
|
+
* we use this to ignore non-reachable types for any given subgraph.
|
|
217
|
+
*/
|
|
218
|
+
let includeTypeInSubgraph: (t: NamedType, name: string) => boolean = () => true;
|
|
219
|
+
if (isFed1) {
|
|
220
|
+
const reachableTypesBySubgraph = collectFieldReachableTypesForAllSubgraphs(
|
|
221
|
+
supergraph,
|
|
222
|
+
subgraphs.names(),
|
|
223
|
+
(f, name) => {
|
|
224
|
+
const fieldApplications: Directive<any, { graph: string, requires?: string, provides?: string }>[] = f.appliedDirectivesOf(fieldDirective);
|
|
225
|
+
if (fieldApplications.length) {
|
|
226
|
+
const application = fieldApplications.find((application) => getSubgraph(application) === name);
|
|
227
|
+
if (application) {
|
|
228
|
+
const args = application.arguments();
|
|
229
|
+
const typesInFederationDirectives =
|
|
230
|
+
typesUsedInFederationDirective(args.provides, baseType(f.type!) as CompositeType)
|
|
231
|
+
.concat(typesUsedInFederationDirective(args.requires, f.parent));
|
|
232
|
+
return { isInSubgraph: true, typesInFederationDirectives };
|
|
233
|
+
} else {
|
|
234
|
+
return { isInSubgraph: false, typesInFederationDirectives: [] };
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
// No field application depends on the "owner" directive on the type. If we have no owner, then the
|
|
238
|
+
// field is in all subgraph and we return true. Otherwise, the field is only in the owner subgraph.
|
|
239
|
+
// In any case, the field cannot have a requires or provides
|
|
240
|
+
const ownerApplications = ownerDirective ? f.parent.appliedDirectivesOf(ownerDirective) : [];
|
|
241
|
+
return { isInSubgraph: !ownerApplications.length || getSubgraph(ownerApplications[0]) == name, typesInFederationDirectives: [] };
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
(t, name) => {
|
|
245
|
+
const typeApplications: Directive<any, { graph: string, key?: string}>[] = t.appliedDirectivesOf(typeDirective);
|
|
246
|
+
const application = typeApplications.find((application) => (application.arguments().key && (getSubgraph(application) === name)));
|
|
247
|
+
if (application) {
|
|
248
|
+
const typesInFederationDirectives = typesUsedInFederationDirective(application.arguments().key, t as CompositeType);
|
|
249
|
+
return { isEntityWithKeyInSubgraph: true, typesInFederationDirectives };
|
|
250
|
+
} else {
|
|
251
|
+
return { isEntityWithKeyInSubgraph: false, typesInFederationDirectives: [] };
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
includeTypeInSubgraph = (t, name) => reachableTypesBySubgraph.get(name)?.has(t.name) ?? false;
|
|
256
|
+
}
|
|
93
257
|
|
|
94
258
|
// Next, we iterate on all types and add it to the proper subgraphs (along with any @key).
|
|
95
259
|
// Note that we first add all types empty and populate the types next. This avoids having to care about the iteration
|
|
@@ -97,13 +261,15 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
|
|
|
97
261
|
for (const type of filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition)) {
|
|
98
262
|
const typeApplications = type.appliedDirectivesOf(typeDirective);
|
|
99
263
|
if (!typeApplications.length) {
|
|
100
|
-
// Imply the type is
|
|
101
|
-
|
|
102
|
-
|
|
264
|
+
// Imply we don't know in which subgraph the type is, so we had it in all subgraph in which the type is reachable.
|
|
265
|
+
subgraphs
|
|
266
|
+
.values()
|
|
267
|
+
.filter((sg) => includeTypeInSubgraph(type, sg.name))
|
|
268
|
+
.map(sg => sg.schema).forEach(schema => schema.addType(newNamedType(type.kind, type.name)));
|
|
103
269
|
} else {
|
|
104
270
|
for (const application of typeApplications) {
|
|
105
271
|
const args = application.arguments();
|
|
106
|
-
const subgraphName =
|
|
272
|
+
const subgraphName = getSubgraph(application)!;
|
|
107
273
|
const schema = subgraphs.get(subgraphName)!.schema;
|
|
108
274
|
// We can have more than one type directive for a given subgraph
|
|
109
275
|
let subgraphType = schema.type(type.name);
|
|
@@ -121,8 +287,6 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
|
|
|
121
287
|
}
|
|
122
288
|
}
|
|
123
289
|
|
|
124
|
-
const ownerDirective = joinSpec.ownerDirective(supergraph);
|
|
125
|
-
const fieldDirective = joinSpec.fieldDirective(supergraph);
|
|
126
290
|
// We can now populate all those types (with relevant @provides and @requires on fields).
|
|
127
291
|
for (const type of filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition)) {
|
|
128
292
|
switch (type.kind) {
|
|
@@ -181,7 +345,14 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
|
|
|
181
345
|
const args = application.arguments();
|
|
182
346
|
const subgraph = subgraphs.get(graphEnumNameToSubgraphName.get(args.graph)!)!;
|
|
183
347
|
const subgraphField = addSubgraphField(field, subgraph, args.type);
|
|
184
|
-
|
|
348
|
+
if (!subgraphField) {
|
|
349
|
+
// It's unlikely but possible that a fed1 supergraph has a `@provides` on a field of a value type,
|
|
350
|
+
// and that value type is actually unreachable. Because we trim unreachable types for fed1 supergraph
|
|
351
|
+
// (see comment on `includeTypeInSubgraph` above), it would mean we get `undefined` here. It's fine
|
|
352
|
+
// however: the type is unreachable in this subgraph, so ignoring that field application is fine too.
|
|
353
|
+
assert(!includeTypeInSubgraph(type, subgraph.name), () => `Found join__field directive for graph ${subgraph.name} on field ${field.coordinate} but no corresponding join__type on ${type}`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
185
356
|
if (args.requires) {
|
|
186
357
|
subgraphField.applyDirective(subgraph.metadata().requiresDirective(), {'fields': args.requires});
|
|
187
358
|
}
|
package/src/federation.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
Directive,
|
|
8
8
|
DirectiveDefinition,
|
|
9
9
|
ErrGraphQLValidationFailed,
|
|
10
|
-
errorCauses,
|
|
11
10
|
FieldDefinition,
|
|
12
11
|
InputFieldDefinition,
|
|
13
12
|
InterfaceType,
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
ScalarType,
|
|
23
22
|
Schema,
|
|
24
23
|
SchemaBlueprint,
|
|
24
|
+
SchemaConfig,
|
|
25
25
|
SchemaDefinition,
|
|
26
26
|
SchemaElement,
|
|
27
27
|
sourceASTs,
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
ERRORS,
|
|
55
55
|
withModifiedErrorMessage,
|
|
56
56
|
extractGraphQLErrorOptions,
|
|
57
|
+
errorCauses,
|
|
57
58
|
} from "./error";
|
|
58
59
|
import { computeShareables } from "./precompute";
|
|
59
60
|
import {
|
|
@@ -1024,8 +1025,8 @@ export function buildSubgraph(
|
|
|
1024
1025
|
return subgraph.validate();
|
|
1025
1026
|
}
|
|
1026
1027
|
|
|
1027
|
-
export function newEmptyFederation2Schema(): Schema {
|
|
1028
|
-
const schema = new Schema(new FederationBlueprint(true));
|
|
1028
|
+
export function newEmptyFederation2Schema(config?: SchemaConfig): Schema {
|
|
1029
|
+
const schema = new Schema(new FederationBlueprint(true), config);
|
|
1029
1030
|
setSchemaAsFed2Subgraph(schema);
|
|
1030
1031
|
return schema;
|
|
1031
1032
|
}
|
package/src/operations.ts
CHANGED
|
@@ -831,10 +831,11 @@ class DeferNormalizer {
|
|
|
831
831
|
const deferArgs = selection.element().deferDirectiveArgs();
|
|
832
832
|
if (deferArgs) {
|
|
833
833
|
hasDefers = true;
|
|
834
|
+
if (!deferArgs.label || deferArgs.if !== undefined) {
|
|
835
|
+
hasNonLabelledOrConditionalDefers = true;
|
|
836
|
+
}
|
|
834
837
|
if (deferArgs.label) {
|
|
835
838
|
this.usedLabels.add(deferArgs.label);
|
|
836
|
-
} else {
|
|
837
|
-
hasNonLabelledOrConditionalDefers = true;
|
|
838
839
|
}
|
|
839
840
|
}
|
|
840
841
|
}
|
|
@@ -951,7 +952,6 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
951
952
|
if (names && names.length === 0) {
|
|
952
953
|
return this;
|
|
953
954
|
}
|
|
954
|
-
|
|
955
955
|
const newFragments = updateSelectionSetFragments
|
|
956
956
|
? (names ? this.fragments?.without(names) : undefined)
|
|
957
957
|
: this.fragments;
|
|
@@ -1052,7 +1052,7 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1052
1052
|
* This is very similar to `mergeIn` except that it takes a direct array of selection, and the direct aliasing
|
|
1053
1053
|
* remarks from `mergeInd` applies here too.
|
|
1054
1054
|
*/
|
|
1055
|
-
addAll(selections: Selection[]): SelectionSet {
|
|
1055
|
+
addAll(selections: readonly Selection[]): SelectionSet {
|
|
1056
1056
|
selections.forEach(s => this.add(s));
|
|
1057
1057
|
return this;
|
|
1058
1058
|
}
|
|
@@ -1605,6 +1605,8 @@ export abstract class FragmentSelection extends Freezable<FragmentSelection> {
|
|
|
1605
1605
|
|
|
1606
1606
|
abstract withNormalizedDefer(normalizer: DeferNormalizer): FragmentSelection | SelectionSet;
|
|
1607
1607
|
|
|
1608
|
+
abstract updateForAddingTo(selectionSet: SelectionSet): FragmentSelection;
|
|
1609
|
+
|
|
1608
1610
|
protected us(): FragmentSelection {
|
|
1609
1611
|
return this;
|
|
1610
1612
|
}
|
|
@@ -1624,30 +1626,6 @@ export abstract class FragmentSelection extends Freezable<FragmentSelection> {
|
|
|
1624
1626
|
return mergeVariables(this.element().variables(), this.selectionSet.usedVariables());
|
|
1625
1627
|
}
|
|
1626
1628
|
|
|
1627
|
-
updateForAddingTo(selectionSet: SelectionSet): FragmentSelection {
|
|
1628
|
-
const updatedFragment = this.element().updateForAddingTo(selectionSet);
|
|
1629
|
-
if (this.element() === updatedFragment) {
|
|
1630
|
-
return this.cloneIfFrozen();
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
// Like for fields, we create a new selection that not only uses the updated fragment, but also ensures
|
|
1634
|
-
// the underlying selection set uses the updated type as parent type.
|
|
1635
|
-
const updatedCastedType = updatedFragment.castedType();
|
|
1636
|
-
let updatedSelectionSet : SelectionSet | undefined;
|
|
1637
|
-
if (this.selectionSet.parentType !== updatedCastedType) {
|
|
1638
|
-
updatedSelectionSet = new SelectionSet(updatedCastedType);
|
|
1639
|
-
// Note that re-adding every selection ensures that anything frozen will be cloned as needed, on top of handling any knock-down
|
|
1640
|
-
// effect of the type change.
|
|
1641
|
-
for (const selection of this.selectionSet.selections()) {
|
|
1642
|
-
updatedSelectionSet.add(selection);
|
|
1643
|
-
}
|
|
1644
|
-
} else {
|
|
1645
|
-
updatedSelectionSet = this.selectionSet?.cloneIfFrozen();
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
return new InlineFragmentSelection(updatedFragment, updatedSelectionSet);
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
1629
|
filter(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
|
|
1652
1630
|
if (!predicate(this)) {
|
|
1653
1631
|
return undefined;
|
|
@@ -1713,6 +1691,31 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
1713
1691
|
this.selectionSet.validate();
|
|
1714
1692
|
}
|
|
1715
1693
|
|
|
1694
|
+
updateForAddingTo(selectionSet: SelectionSet): FragmentSelection {
|
|
1695
|
+
const updatedFragment = this.element().updateForAddingTo(selectionSet);
|
|
1696
|
+
if (this.element() === updatedFragment) {
|
|
1697
|
+
return this.cloneIfFrozen();
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// Like for fields, we create a new selection that not only uses the updated fragment, but also ensures
|
|
1701
|
+
// the underlying selection set uses the updated type as parent type.
|
|
1702
|
+
const updatedCastedType = updatedFragment.castedType();
|
|
1703
|
+
let updatedSelectionSet : SelectionSet | undefined;
|
|
1704
|
+
if (this.selectionSet.parentType !== updatedCastedType) {
|
|
1705
|
+
updatedSelectionSet = new SelectionSet(updatedCastedType);
|
|
1706
|
+
// Note that re-adding every selection ensures that anything frozen will be cloned as needed, on top of handling any knock-down
|
|
1707
|
+
// effect of the type change.
|
|
1708
|
+
for (const selection of this.selectionSet.selections()) {
|
|
1709
|
+
updatedSelectionSet.add(selection);
|
|
1710
|
+
}
|
|
1711
|
+
} else {
|
|
1712
|
+
updatedSelectionSet = this.selectionSet?.cloneIfFrozen();
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
return new InlineFragmentSelection(updatedFragment, updatedSelectionSet);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
|
|
1716
1719
|
get selectionSet(): SelectionSet {
|
|
1717
1720
|
return this._selectionSet;
|
|
1718
1721
|
}
|
|
@@ -1785,7 +1788,6 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
1785
1788
|
if (updatedSubSelections === this.selectionSet && !hasDeferToRemove) {
|
|
1786
1789
|
return this;
|
|
1787
1790
|
}
|
|
1788
|
-
|
|
1789
1791
|
const newFragment = hasDeferToRemove ? this.fragmentElement.withoutDefer() : this.fragmentElement;
|
|
1790
1792
|
if (!newFragment) {
|
|
1791
1793
|
return updatedSubSelections;
|
|
@@ -1877,13 +1879,23 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
1877
1879
|
return this;
|
|
1878
1880
|
}
|
|
1879
1881
|
|
|
1882
|
+
updateForAddingTo(_selectionSet: SelectionSet): FragmentSelection {
|
|
1883
|
+
// This is a little bit iffy, because the fragment could link to a schema (typically the supergraph API one)
|
|
1884
|
+
// that is different from the one of `_selectionSet` (say, a subgraph fetch selection in which we're trying to
|
|
1885
|
+
// reuse a user fragment). But in practice, we expand all fragments when we do query planning and only re-add
|
|
1886
|
+
// fragments back at the very end, so this should be fine. Importantly, we don't want this method to mistakenly
|
|
1887
|
+
// expand the spread, as that would compromise the code that optimize subgraph fetches to re-use named
|
|
1888
|
+
// fragments.
|
|
1889
|
+
return this;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1880
1892
|
expandFragments(names?: string[], updateSelectionSetFragments: boolean = true): FragmentSelection | readonly Selection[] {
|
|
1881
1893
|
if (names && !names.includes(this.namedFragment.name)) {
|
|
1882
1894
|
return this;
|
|
1883
1895
|
}
|
|
1884
1896
|
|
|
1885
1897
|
const expandedSubSelections = this.selectionSet.expandFragments(names, updateSelectionSetFragments);
|
|
1886
|
-
return sameType(this._element.parentType, this.namedFragment.typeCondition)
|
|
1898
|
+
return sameType(this._element.parentType, this.namedFragment.typeCondition) && this._element.appliedDirectives.length === 0
|
|
1887
1899
|
? expandedSubSelections.selections()
|
|
1888
1900
|
: new InlineFragmentSelection(this._element, expandedSubSelections);
|
|
1889
1901
|
}
|
package/src/print.ts
CHANGED
|
@@ -94,7 +94,7 @@ export function printSchema(schema: Schema, options: PrintOptions = defaultPrint
|
|
|
94
94
|
return definitions.flat().join('\n\n');
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function definitionAndExtensions<T extends ExtendableElement>(element: {extensions():
|
|
97
|
+
function definitionAndExtensions<T extends ExtendableElement>(element: {extensions(): readonly Extension<T>[]}, options: PrintOptions): (Extension<any> | null | undefined)[] {
|
|
98
98
|
return options.mergeTypesAndExtensions ? [undefined] : [null, ...element.extensions()];
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -102,7 +102,7 @@ function printSchemaDefinitionAndExtensions(schemaDefinition: SchemaDefinition,
|
|
|
102
102
|
return printDefinitionAndExtensions(schemaDefinition, options, printSchemaDefinitionOrExtension);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function printDefinitionAndExtensions<T extends {extensions():
|
|
105
|
+
function printDefinitionAndExtensions<T extends {extensions(): readonly Extension<any>[]}>(
|
|
106
106
|
t: T,
|
|
107
107
|
options: PrintOptions,
|
|
108
108
|
printer: (t: T, options: PrintOptions, extension?: Extension<any> | null) => string | undefined
|
package/src/schemaUpgrader.ts
CHANGED
|
@@ -4,12 +4,11 @@ import {
|
|
|
4
4
|
Kind,
|
|
5
5
|
print as printAST,
|
|
6
6
|
} from "graphql";
|
|
7
|
-
import { ERRORS } from "./error";
|
|
7
|
+
import { errorCauses, ERRORS } from "./error";
|
|
8
8
|
import {
|
|
9
9
|
baseType,
|
|
10
10
|
CompositeType,
|
|
11
11
|
Directive,
|
|
12
|
-
errorCauses,
|
|
13
12
|
Extension,
|
|
14
13
|
FieldDefinition,
|
|
15
14
|
isCompositeType,
|
|
@@ -432,7 +431,8 @@ class SchemaUpgrader {
|
|
|
432
431
|
|
|
433
432
|
private fixFederationDirectivesArguments() {
|
|
434
433
|
for (const directive of [this.metadata.keyDirective(), this.metadata.requiresDirective(), this.metadata.providesDirective()]) {
|
|
435
|
-
|
|
434
|
+
// Note that we may remove (to replace) some of the application we iterate on, so we need to copy the list we iterate on first.
|
|
435
|
+
for (const application of Array.from(directive.applications())) {
|
|
436
436
|
const fields = application.arguments().fields;
|
|
437
437
|
if (typeof fields !== 'string') {
|
|
438
438
|
// The one case we have seen in practice is user passing an array of string, so we handle that. If it's something else,
|
|
@@ -676,14 +676,14 @@ class SchemaUpgrader {
|
|
|
676
676
|
continue;
|
|
677
677
|
}
|
|
678
678
|
const otherResolvingSubgraphs = this.otherSubgraphs.filter((s) => resolvesField(s, field));
|
|
679
|
-
if (otherResolvingSubgraphs.length > 0) {
|
|
679
|
+
if (otherResolvingSubgraphs.length > 0 && !field.hasAppliedDirective(shareableDirective)) {
|
|
680
680
|
field.applyDirective(shareableDirective);
|
|
681
681
|
this.addChange(new ShareableFieldAddition(field.coordinate, otherResolvingSubgraphs.map((s) => s.name)));
|
|
682
682
|
}
|
|
683
683
|
}
|
|
684
684
|
} else {
|
|
685
685
|
const otherDeclaringSubgraphs = this.otherSubgraphs.filter((s) => s.schema.type(type.name));
|
|
686
|
-
if (otherDeclaringSubgraphs.length > 0) {
|
|
686
|
+
if (otherDeclaringSubgraphs.length > 0 && !type.hasAppliedDirective(shareableDirective)) {
|
|
687
687
|
type.applyDirective(shareableDirective);
|
|
688
688
|
this.addChange(new ShareableTypeAddition(type.coordinate, otherDeclaringSubgraphs.map((s) => s.name)));
|
|
689
689
|
}
|
|
@@ -697,7 +697,8 @@ class SchemaUpgrader {
|
|
|
697
697
|
return;
|
|
698
698
|
}
|
|
699
699
|
|
|
700
|
-
|
|
700
|
+
// Copying the list we iterate on as we remove in the loop.
|
|
701
|
+
for (const application of Array.from(tagDirective.applications())) {
|
|
701
702
|
const element = application.parent;
|
|
702
703
|
if (!(element instanceof FieldDefinition)) {
|
|
703
704
|
continue;
|
package/src/supergraphs.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { err } from '@apollo/core-schema';
|
|
1
|
+
import { DocumentNode, GraphQLError } from "graphql";
|
|
3
2
|
import { ErrCoreCheckFailed, FeatureUrl, FeatureVersion } from "./coreSpec";
|
|
4
|
-
import {
|
|
3
|
+
import { CoreFeatures, Schema, sourceASTs } from "./definitions";
|
|
5
4
|
import { joinIdentity, JoinSpecDefinition, JOIN_VERSIONS } from "./joinSpec";
|
|
6
5
|
import { buildSchema, buildSchemaFromAST } from "./buildSchema";
|
|
7
6
|
import { extractSubgraphsNamesAndUrlsFromSupergraph } from "./extractSubgraphsFromSupergraph";
|
|
@@ -18,24 +17,6 @@ const SUPPORTED_FEATURES = new Set([
|
|
|
18
17
|
'https://specs.apollo.dev/inaccessible/v0.2',
|
|
19
18
|
]);
|
|
20
19
|
|
|
21
|
-
export function ErrUnsupportedFeature(feature: CoreFeature): Error {
|
|
22
|
-
return err('UnsupportedFeature', {
|
|
23
|
-
message: `feature ${feature.url} is for: ${feature.purpose} but is unsupported`,
|
|
24
|
-
feature,
|
|
25
|
-
nodes: feature.directive.sourceAST,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function ErrForUnsupported(core: CoreFeature, ...features: readonly CoreFeature[]): Error {
|
|
30
|
-
return err('ForUnsupported', {
|
|
31
|
-
message:
|
|
32
|
-
`the \`for:\` argument is unsupported by version ${core.url.version} ` +
|
|
33
|
-
`of the core spec. Please upgrade to at least @core v0.2 (https://specs.apollo.dev/core/v0.2).`,
|
|
34
|
-
features,
|
|
35
|
-
nodes: [core.directive.sourceAST, ...features.map(f => f.directive.sourceAST)].filter(n => !!n) as ASTNode[]
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
20
|
const coreVersionZeroDotOneUrl = FeatureUrl.parse('https://specs.apollo.dev/core/v0.1');
|
|
40
21
|
|
|
41
22
|
export function buildSupergraphSchema(supergraphSdl: string | DocumentNode): [Schema, {name: string, url: string}[]] {
|
|
@@ -56,18 +37,28 @@ export function buildSupergraphSchema(supergraphSdl: string | DocumentNode): [Sc
|
|
|
56
37
|
* Throws if that is not true.
|
|
57
38
|
*/
|
|
58
39
|
function checkFeatureSupport(coreFeatures: CoreFeatures) {
|
|
59
|
-
const errors = [];
|
|
60
|
-
|
|
40
|
+
const errors: GraphQLError[] = [];
|
|
41
|
+
const coreItself = coreFeatures.coreItself;
|
|
42
|
+
if (coreItself.url.equals(coreVersionZeroDotOneUrl)) {
|
|
61
43
|
const purposefulFeatures = [...coreFeatures.allFeatures()].filter(f => f.purpose)
|
|
62
44
|
if (purposefulFeatures.length > 0) {
|
|
63
|
-
errors.push(
|
|
45
|
+
errors.push(ERRORS.UNSUPPORTED_LINKED_FEATURE.err(
|
|
46
|
+
`the \`for:\` argument is unsupported by version ${coreItself.url.version} ` +
|
|
47
|
+
`of the core spec. Please upgrade to at least @core v0.2 (https://specs.apollo.dev/core/v0.2).`,
|
|
48
|
+
{
|
|
49
|
+
nodes: sourceASTs(coreItself.directive, ...purposefulFeatures.map(f => f.directive))
|
|
50
|
+
}
|
|
51
|
+
));
|
|
64
52
|
}
|
|
65
53
|
}
|
|
66
54
|
|
|
67
55
|
for (const feature of coreFeatures.allFeatures()) {
|
|
68
56
|
if (feature.url.equals(coreVersionZeroDotOneUrl) || feature.purpose === 'EXECUTION' || feature.purpose === 'SECURITY') {
|
|
69
57
|
if (!SUPPORTED_FEATURES.has(feature.url.base.toString())) {
|
|
70
|
-
errors.push(
|
|
58
|
+
errors.push(ERRORS.UNSUPPORTED_LINKED_FEATURE.err(
|
|
59
|
+
`feature ${feature.url} is for: ${feature.purpose} but is unsupported`,
|
|
60
|
+
{ nodes: feature.directive.sourceAST },
|
|
61
|
+
));
|
|
71
62
|
}
|
|
72
63
|
}
|
|
73
64
|
}
|
package/src/utils.ts
CHANGED
|
@@ -394,3 +394,18 @@ export type Concrete<Type> = {
|
|
|
394
394
|
// const x = [1,2,undefined];
|
|
395
395
|
// const y: number[] = x.filter(isDefined);
|
|
396
396
|
export const isDefined = <T>(t: T | undefined): t is T => t === undefined ? false : true;
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Removes the first occurrence of the provided element in the provided array, if said array contains said elements.
|
|
400
|
+
*
|
|
401
|
+
* @return whether the element was removed.
|
|
402
|
+
*/
|
|
403
|
+
export function removeArrayElement<T>(element: T, array: T[]): boolean {
|
|
404
|
+
const index = array.indexOf(element);
|
|
405
|
+
if (index >= 0) {
|
|
406
|
+
array.splice(index, 1);
|
|
407
|
+
return true;
|
|
408
|
+
} else {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
}
|