@apollo/federation-internals 2.0.0-alpha.2 → 2.0.0-alpha.6
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 +15 -0
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +3 -3
- package/dist/buildSchema.js.map +1 -1
- package/dist/debug.d.ts.map +1 -1
- package/dist/debug.js +2 -18
- package/dist/debug.js.map +1 -1
- package/dist/definitions.d.ts +18 -7
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +80 -26
- package/dist/definitions.js.map +1 -1
- package/dist/error.d.ts +88 -3
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +145 -5
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +41 -4
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts +4 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +231 -58
- package/dist/federation.js.map +1 -1
- package/dist/genErrorCodeDoc.d.ts +2 -0
- package/dist/genErrorCodeDoc.d.ts.map +1 -0
- package/dist/genErrorCodeDoc.js +55 -0
- package/dist/genErrorCodeDoc.js.map +1 -0
- package/dist/inaccessibleSpec.d.ts.map +1 -1
- package/dist/inaccessibleSpec.js +1 -1
- package/dist/inaccessibleSpec.js.map +1 -1
- package/dist/joinSpec.d.ts.map +1 -1
- package/dist/joinSpec.js +6 -5
- package/dist/joinSpec.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +15 -15
- package/dist/operations.js.map +1 -1
- package/dist/tagSpec.d.ts +2 -2
- package/dist/tagSpec.d.ts.map +1 -1
- package/dist/tagSpec.js +10 -2
- package/dist/tagSpec.js.map +1 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +34 -1
- package/dist/utils.js.map +1 -1
- package/dist/values.d.ts +2 -1
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +27 -1
- package/dist/values.js.map +1 -1
- package/jest.config.js +5 -1
- package/package.json +3 -6
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +535 -0
- package/src/__tests__/subgraphValidation.test.ts +480 -0
- package/src/buildSchema.ts +7 -6
- package/src/debug.ts +2 -19
- package/src/definitions.ts +151 -40
- package/src/error.ts +340 -7
- package/src/extractSubgraphsFromSupergraph.ts +50 -5
- package/src/federation.ts +297 -92
- package/src/genErrorCodeDoc.ts +69 -0
- package/src/inaccessibleSpec.ts +7 -2
- package/src/joinSpec.ts +11 -5
- package/src/operations.ts +20 -18
- package/src/tagSpec.ts +11 -6
- package/src/utils.ts +49 -0
- package/src/values.ts +47 -5
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/federation.ts
CHANGED
|
@@ -18,22 +18,40 @@ import {
|
|
|
18
18
|
baseType,
|
|
19
19
|
isInterfaceType,
|
|
20
20
|
isObjectType,
|
|
21
|
+
isListType,
|
|
22
|
+
isUnionType,
|
|
21
23
|
sourceASTs,
|
|
22
24
|
VariableDefinitions,
|
|
23
25
|
InterfaceType,
|
|
24
26
|
InputFieldDefinition,
|
|
25
27
|
isCompositeType
|
|
26
28
|
} from "./definitions";
|
|
27
|
-
import { assert, OrderedMap } from "./utils";
|
|
29
|
+
import { assert, joinStrings, MultiMap, OrderedMap } from "./utils";
|
|
28
30
|
import { SDLValidationRule } from "graphql/validation/ValidationContext";
|
|
29
31
|
import { specifiedSDLRules } from "graphql/validation/specifiedRules";
|
|
30
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
ASTNode,
|
|
34
|
+
DocumentNode,
|
|
35
|
+
GraphQLError,
|
|
36
|
+
Kind,
|
|
37
|
+
KnownTypeNamesRule,
|
|
38
|
+
parse,
|
|
39
|
+
PossibleTypeExtensionsRule,
|
|
40
|
+
print as printAST,
|
|
41
|
+
Source,
|
|
42
|
+
DirectiveLocation,
|
|
43
|
+
} from "graphql";
|
|
31
44
|
import { defaultPrintOptions, printDirectiveDefinition } from "./print";
|
|
32
45
|
import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFederationRule";
|
|
33
46
|
import { buildSchema, buildSchemaFromAST } from "./buildSchema";
|
|
34
47
|
import { parseSelectionSet, SelectionSet } from './operations';
|
|
35
48
|
import { tagLocations, TAG_VERSIONS } from "./tagSpec";
|
|
36
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
errorCodeDef,
|
|
51
|
+
ErrorCodeDefinition,
|
|
52
|
+
ERROR_CATEGORIES,
|
|
53
|
+
ERRORS,
|
|
54
|
+
} from "./error";
|
|
37
55
|
|
|
38
56
|
export const entityTypeName = '_Entity';
|
|
39
57
|
export const serviceTypeName = '_Service';
|
|
@@ -107,22 +125,33 @@ function validateFieldSetSelections(
|
|
|
107
125
|
for (const selection of selectionSet.selections()) {
|
|
108
126
|
if (selection.kind === 'FieldSelection') {
|
|
109
127
|
const field = selection.element().definition;
|
|
128
|
+
const isExternal = externalTester.isExternal(field);
|
|
129
|
+
// We collect the field as external before any other validation to avoid getting a (confusing)
|
|
130
|
+
// "external unused" error on top of another error due to exiting that method too early.
|
|
131
|
+
if (isExternal) {
|
|
132
|
+
externalFieldCoordinatesCollector.push(field.coordinate);
|
|
133
|
+
}
|
|
110
134
|
if (field.hasArguments()) {
|
|
111
|
-
throw
|
|
135
|
+
throw ERROR_CATEGORIES.FIELDS_HAS_ARGS.get(directiveName).err({
|
|
136
|
+
message: `field ${field.coordinate} cannot be included because it has arguments (fields with argument are not allowed in @${directiveName})`,
|
|
137
|
+
nodes: field.sourceAST
|
|
138
|
+
});
|
|
112
139
|
}
|
|
113
140
|
// The field must be external if we don't allow non-external leaf fields, it's a leaf, and we haven't traversed an external field in parent chain leading here.
|
|
114
141
|
const mustBeExternal = !selection.selectionSet && !allowOnNonExternalLeafFields && !hasExternalInParents;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
externalFieldCoordinatesCollector.push(field.coordinate);
|
|
118
|
-
} else if (mustBeExternal) {
|
|
142
|
+
if (!isExternal && mustBeExternal) {
|
|
143
|
+
const errorCode = ERROR_CATEGORIES.DIRECTIVE_FIELDS_MISSING_EXTERNAL.get(directiveName);
|
|
119
144
|
if (externalTester.isFakeExternal(field)) {
|
|
120
|
-
throw
|
|
121
|
-
`field "${field.coordinate}" should not be part of a @${directiveName} since it is already "effectively" provided by this subgraph `
|
|
122
|
-
|
|
123
|
-
field.sourceAST
|
|
145
|
+
throw errorCode.err({
|
|
146
|
+
message: `field "${field.coordinate}" should not be part of a @${directiveName} since it is already "effectively" provided by this subgraph `
|
|
147
|
+
+ `(while it is marked @${externalDirectiveName}, it is a @${keyDirectiveName} field of an extension type, which are not internally considered external for historical/backward compatibility reasons)`,
|
|
148
|
+
nodes: field.sourceAST
|
|
149
|
+
});
|
|
124
150
|
} else {
|
|
125
|
-
throw
|
|
151
|
+
throw errorCode.err({
|
|
152
|
+
message: `field "${field.coordinate}" should not be part of a @${directiveName} since it is already provided by this subgraph (it is not marked @${externalDirectiveName})`,
|
|
153
|
+
nodes: field.sourceAST
|
|
154
|
+
});
|
|
126
155
|
}
|
|
127
156
|
}
|
|
128
157
|
if (selection.selectionSet) {
|
|
@@ -151,69 +180,97 @@ function validateFieldSetSelections(
|
|
|
151
180
|
function validateFieldSet(
|
|
152
181
|
type: CompositeType,
|
|
153
182
|
directive: Directive<any, {fields: any}>,
|
|
154
|
-
targetDescription: string,
|
|
155
183
|
externalTester: ExternalTester,
|
|
156
184
|
externalFieldCoordinatesCollector: string[],
|
|
157
185
|
allowOnNonExternalLeafFields: boolean,
|
|
186
|
+
onFields?: (field: FieldDefinition<any>) => void,
|
|
158
187
|
): GraphQLError | undefined {
|
|
159
188
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const nodes = sourceASTs(directive);
|
|
169
|
-
if (e.nodes) {
|
|
170
|
-
nodes.push(...e.nodes);
|
|
171
|
-
}
|
|
172
|
-
let msg = e.message.trim();
|
|
173
|
-
// The rule for validating @requires in fed 1 was not properly recursive, so people upgrading
|
|
174
|
-
// may have a @require that selects some fields but without declaring those fields on the
|
|
175
|
-
// subgraph. As we fixed the validation, this will now fail, but we try here to provide some
|
|
176
|
-
// hint for those users for how to fix the problem.
|
|
177
|
-
// Note that this is a tad fragile to rely on the error message like that, but worth case, a
|
|
178
|
-
// future change make us not show the hint and that's not the end of the world.
|
|
179
|
-
if (msg.startsWith('Cannot query field')) {
|
|
180
|
-
if (msg.endsWith('.')) {
|
|
181
|
-
msg = msg.slice(0, msg.length - 1);
|
|
189
|
+
// Note that `parseFieldSetArgument` already properly format the error, hence the separate try-catch.
|
|
190
|
+
const fieldAcessor = onFields
|
|
191
|
+
? (type: CompositeType, fieldName: string) => {
|
|
192
|
+
const field = type.field(fieldName);
|
|
193
|
+
if (field) {
|
|
194
|
+
onFields(field);
|
|
195
|
+
}
|
|
196
|
+
return field;
|
|
182
197
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
198
|
+
: undefined;
|
|
199
|
+
const selectionSet = parseFieldSetArgument(type, directive, fieldAcessor);
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
validateFieldSetSelections(directive.name, selectionSet, false, externalTester, externalFieldCoordinatesCollector, allowOnNonExternalLeafFields);
|
|
203
|
+
return undefined;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
if (!(e instanceof GraphQLError)) {
|
|
206
|
+
throw e;
|
|
207
|
+
}
|
|
208
|
+
const nodes = sourceASTs(directive);
|
|
209
|
+
if (e.nodes) {
|
|
210
|
+
nodes.push(...e.nodes);
|
|
187
211
|
}
|
|
212
|
+
const codeDef = errorCodeDef(e) ?? ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS.get(directive.name);
|
|
213
|
+
return codeDef.err({
|
|
214
|
+
message: `${fieldSetErrorDescriptor(directive)}: ${e.message.trim()}`,
|
|
215
|
+
nodes,
|
|
216
|
+
originalError: e,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} catch (e) {
|
|
220
|
+
if (e instanceof GraphQLError) {
|
|
221
|
+
return e;
|
|
222
|
+
} else {
|
|
223
|
+
throw e;
|
|
188
224
|
}
|
|
189
|
-
return new GraphQLError(`On ${targetDescription}, for ${directive}: ${msg}`, nodes);
|
|
190
225
|
}
|
|
191
226
|
}
|
|
192
227
|
|
|
228
|
+
function fieldSetErrorDescriptor(directive: Directive<any, {fields: any}>): string {
|
|
229
|
+
return `On ${fieldSetTargetDescription(directive)}, for ${directiveStrUsingASTIfPossible(directive)}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// This method is called to display @key, @provides or @requires directives in error message in place where the directive `fields`
|
|
233
|
+
// argument might be invalid because it was not a string in the underlying AST. If that's the case, we want to use the AST to
|
|
234
|
+
// print the directive or the message might be a bit confusing for the user.
|
|
235
|
+
function directiveStrUsingASTIfPossible(directive: Directive<any>): string {
|
|
236
|
+
return directive.sourceAST ? printAST(directive.sourceAST) : directive.toString();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function fieldSetTargetDescription(directive: Directive<any, {fields: any}>): string {
|
|
240
|
+
const targetKind = directive.parent instanceof FieldDefinition ? "field" : "type";
|
|
241
|
+
return `${targetKind} "${directive.parent?.coordinate}"`;
|
|
242
|
+
}
|
|
243
|
+
|
|
193
244
|
function validateAllFieldSet<TParent extends SchemaElement<any, any>>(
|
|
194
245
|
definition: DirectiveDefinition<{fields: any}>,
|
|
195
246
|
targetTypeExtractor: (element: TParent) => CompositeType,
|
|
196
|
-
targetDescriptionExtractor: (element: TParent) => string,
|
|
197
247
|
errorCollector: GraphQLError[],
|
|
198
248
|
externalTester: ExternalTester,
|
|
199
249
|
externalFieldCoordinatesCollector: string[],
|
|
200
250
|
isOnParentType: boolean,
|
|
201
251
|
allowOnNonExternalLeafFields: boolean,
|
|
252
|
+
onFields?: (field: FieldDefinition<any>) => void,
|
|
202
253
|
): void {
|
|
203
254
|
for (const application of definition.applications()) {
|
|
204
255
|
const elt = application.parent as TParent;
|
|
205
256
|
const type = targetTypeExtractor(elt);
|
|
206
|
-
const targetDescription = targetDescriptionExtractor(elt);
|
|
207
257
|
const parentType = isOnParentType ? type : (elt.parent as NamedType);
|
|
208
258
|
if (isInterfaceType(parentType)) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
259
|
+
const code = ERROR_CATEGORIES.DIRECTIVE_UNSUPPORTED_ON_INTERFACE.get(definition.name);
|
|
260
|
+
errorCollector.push(code.err({
|
|
261
|
+
message: isOnParentType
|
|
262
|
+
? `Cannot use ${definition.coordinate} on interface "${parentType.coordinate}": ${definition.coordinate} is not yet supported on interfaces`
|
|
263
|
+
: `Cannot use ${definition.coordinate} on ${fieldSetTargetDescription(application)} of parent type "${parentType}": ${definition.coordinate} is not yet supported within interfaces`,
|
|
264
|
+
nodes: sourceASTs(application).concat(isOnParentType ? [] : sourceASTs(type)),
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
const error = validateFieldSet(
|
|
268
|
+
type,
|
|
269
|
+
application,
|
|
270
|
+
externalTester,
|
|
271
|
+
externalFieldCoordinatesCollector,
|
|
272
|
+
allowOnNonExternalLeafFields,
|
|
273
|
+
onFields);
|
|
217
274
|
if (error) {
|
|
218
275
|
errorCollector.push(error);
|
|
219
276
|
}
|
|
@@ -240,11 +297,11 @@ function validateAllExternalFieldsUsed(
|
|
|
240
297
|
}
|
|
241
298
|
|
|
242
299
|
if (!isFieldSatisfyingInterface(field)) {
|
|
243
|
-
errorCollector.push(
|
|
244
|
-
`Field ${field.coordinate} is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface;`
|
|
300
|
+
errorCollector.push(ERRORS.EXTERNAL_UNUSED.err({
|
|
301
|
+
message: `Field "${field.coordinate}" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface;`
|
|
245
302
|
+ ' the field declaration has no use and should be removed (or the field should not be @external).',
|
|
246
|
-
field.sourceAST
|
|
247
|
-
));
|
|
303
|
+
nodes: field.sourceAST,
|
|
304
|
+
}));
|
|
248
305
|
}
|
|
249
306
|
}
|
|
250
307
|
}
|
|
@@ -254,6 +311,52 @@ function isFieldSatisfyingInterface(field: FieldDefinition<ObjectType | Interfac
|
|
|
254
311
|
return field.parent.interfaces().some(itf => itf.field(field.name));
|
|
255
312
|
}
|
|
256
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Register errors when, for an interface field, some of the implementations of that field are @external
|
|
316
|
+
* _and_ not all of those field implementation have the same type (which otherwise allowed because field
|
|
317
|
+
* implementation types can be a subtype of the interface field they implement).
|
|
318
|
+
* This is done because if that is the case, federation may later generate invalid query plans (see details
|
|
319
|
+
* on https://github.com/apollographql/federation/issues/1257).
|
|
320
|
+
* This "limitation" will be removed when we stop generating invalid query plans for it.
|
|
321
|
+
*/
|
|
322
|
+
function validateInterfaceRuntimeImplementationFieldsTypes(
|
|
323
|
+
itf: InterfaceType,
|
|
324
|
+
externalTester: ExternalTester,
|
|
325
|
+
errorCollector: GraphQLError[],
|
|
326
|
+
): void {
|
|
327
|
+
const runtimeTypes = itf.possibleRuntimeTypes();
|
|
328
|
+
for (const field of itf.fields()) {
|
|
329
|
+
const withExternalOrRequires: FieldDefinition<ObjectType>[] = [];
|
|
330
|
+
const typeToImplems: MultiMap<string, FieldDefinition<ObjectType>> = new MultiMap();
|
|
331
|
+
const nodes: ASTNode[] = [];
|
|
332
|
+
for (const type of runtimeTypes) {
|
|
333
|
+
const implemField = type.field(field.name);
|
|
334
|
+
if (!implemField) continue;
|
|
335
|
+
if (implemField.sourceAST) {
|
|
336
|
+
nodes.push(implemField.sourceAST);
|
|
337
|
+
}
|
|
338
|
+
if (externalTester.isExternal(implemField) || implemField.hasAppliedDirective(requiresDirectiveName)) {
|
|
339
|
+
withExternalOrRequires.push(implemField);
|
|
340
|
+
}
|
|
341
|
+
const returnType = implemField.type!;
|
|
342
|
+
typeToImplems.add(returnType.toString(), implemField);
|
|
343
|
+
}
|
|
344
|
+
if (withExternalOrRequires.length > 0 && typeToImplems.size > 1) {
|
|
345
|
+
const typeToImplemsArray = [...typeToImplems.entries()];
|
|
346
|
+
errorCollector.push(ERRORS.INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH.err({
|
|
347
|
+
message: `Some of the runtime implementations of interface field "${field.coordinate}" are marked @external or have a @require (${withExternalOrRequires.map(printFieldCoordinate)}) so all the implementations should use the same type (a current limitation of federation; see https://github.com/apollographql/federation/issues/1257), but ${formatFieldsToReturnType(typeToImplemsArray[0])} while ${joinStrings(typeToImplemsArray.slice(1).map(formatFieldsToReturnType), ' and ')}.`,
|
|
348
|
+
nodes
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const printFieldCoordinate = (f: FieldDefinition<CompositeType>): string => `"${f.coordinate}"`;
|
|
355
|
+
|
|
356
|
+
function formatFieldsToReturnType([type, implems]: [string, FieldDefinition<ObjectType>[]]) {
|
|
357
|
+
return `${joinStrings(implems.map(printFieldCoordinate))} ${implems.length == 1 ? 'has' : 'have'} type "${type}"`;
|
|
358
|
+
}
|
|
359
|
+
|
|
257
360
|
export class FederationBuiltIns extends BuiltIns {
|
|
258
361
|
addBuiltInTypes(schema: Schema) {
|
|
259
362
|
super.addBuiltInTypes(schema);
|
|
@@ -272,7 +375,7 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
272
375
|
// Note that we allow @key on interfaces in the definition to not break backward compatibility, because it has historically unfortunately be declared this way, but
|
|
273
376
|
// @key is actually not supported on interfaces at the moment, so if if is "used" then it is rejected.
|
|
274
377
|
const keyDirective = this.addBuiltInDirective(schema, keyDirectiveName)
|
|
275
|
-
.addLocations(
|
|
378
|
+
.addLocations(DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE);
|
|
276
379
|
// TODO: I believe fed 1 does not mark key repeatable and relax validation to accept repeating non-repeatable directive.
|
|
277
380
|
// Do we want to perpetuate this? (Obviously, this is for historical reason and some graphQL implementations still do
|
|
278
381
|
// not support 'repeatable'. But since this code does not kick in within users' code, not sure we have to accommodate
|
|
@@ -281,14 +384,14 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
281
384
|
keyDirective.addArgument('fields', fieldSetType);
|
|
282
385
|
|
|
283
386
|
this.addBuiltInDirective(schema, extendsDirectiveName)
|
|
284
|
-
.addLocations(
|
|
387
|
+
.addLocations(DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE);
|
|
285
388
|
|
|
286
389
|
this.addBuiltInDirective(schema, externalDirectiveName)
|
|
287
|
-
.addLocations(
|
|
390
|
+
.addLocations(DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION);
|
|
288
391
|
|
|
289
392
|
for (const name of [requiresDirectiveName, providesDirectiveName]) {
|
|
290
393
|
this.addBuiltInDirective(schema, name)
|
|
291
|
-
.addLocations(
|
|
394
|
+
.addLocations(DirectiveLocation.FIELD_DEFINITION)
|
|
292
395
|
.addArgument('fields', fieldSetType);
|
|
293
396
|
}
|
|
294
397
|
|
|
@@ -363,13 +466,12 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
363
466
|
// composition error.
|
|
364
467
|
const existing = schema.type(defaultName);
|
|
365
468
|
if (existing) {
|
|
366
|
-
errors.push(
|
|
367
|
-
`
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
));
|
|
469
|
+
errors.push(ERROR_CATEGORIES.ROOT_TYPE_USED.get(k).err({
|
|
470
|
+
message: `The schema has a type named "${defaultName}" but it is not set as the ${k} root type ("${type.name}" is instead): `
|
|
471
|
+
+ 'this is not supported by federation. '
|
|
472
|
+
+ 'If a root type does not use its default name, there should be no other type with that default name.',
|
|
473
|
+
nodes: sourceASTs(type, existing),
|
|
474
|
+
}));
|
|
373
475
|
}
|
|
374
476
|
type.rename(defaultName);
|
|
375
477
|
}
|
|
@@ -383,12 +485,20 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
383
485
|
validateAllFieldSet<CompositeType>(
|
|
384
486
|
keyDirective,
|
|
385
487
|
type => type,
|
|
386
|
-
type => `type "${type}"`,
|
|
387
488
|
errors,
|
|
388
489
|
externalTester,
|
|
389
490
|
externalFieldsInFedDirectivesCoordinates,
|
|
390
491
|
true,
|
|
391
|
-
true
|
|
492
|
+
true,
|
|
493
|
+
field => {
|
|
494
|
+
if (isListType(field.type!) || isUnionType(field.type!) || isInterfaceType(field.type!)) {
|
|
495
|
+
let kind: string = field.type!.kind;
|
|
496
|
+
kind = kind.slice(0, kind.length - 'Type'.length);
|
|
497
|
+
throw ERRORS.KEY_FIELDS_SELECT_INVALID_TYPE.err({
|
|
498
|
+
message: `field "${field.coordinate}" is a ${kind} type which is not allowed in @key`
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
392
502
|
);
|
|
393
503
|
// Note that we currently reject @requires where a leaf field of the selection is not external,
|
|
394
504
|
// because if it's provided by the current subgraph, why "requires" it? That said, it's not 100%
|
|
@@ -400,7 +510,6 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
400
510
|
validateAllFieldSet<FieldDefinition<CompositeType>>(
|
|
401
511
|
this.requiresDirective(schema),
|
|
402
512
|
field => field.parent,
|
|
403
|
-
field => `field "${field.coordinate}"`,
|
|
404
513
|
errors,
|
|
405
514
|
externalTester,
|
|
406
515
|
externalFieldsInFedDirectivesCoordinates,
|
|
@@ -419,13 +528,13 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
419
528
|
}
|
|
420
529
|
const type = baseType(field.type!);
|
|
421
530
|
if (!isCompositeType(type)) {
|
|
422
|
-
throw
|
|
423
|
-
`Invalid @provides directive on field "${field.coordinate}": field has type "${field.type}" which is not a Composite Type`,
|
|
424
|
-
field.sourceAST
|
|
531
|
+
throw ERRORS.PROVIDES_ON_NON_OBJECT_FIELD.err({
|
|
532
|
+
message: `Invalid @provides directive on field "${field.coordinate}": field has type "${field.type}" which is not a Composite Type`,
|
|
533
|
+
nodes: field.sourceAST,
|
|
534
|
+
});
|
|
425
535
|
}
|
|
426
536
|
return type;
|
|
427
537
|
},
|
|
428
|
-
field => `field ${field.coordinate}`,
|
|
429
538
|
errors,
|
|
430
539
|
externalTester,
|
|
431
540
|
externalFieldsInFedDirectivesCoordinates,
|
|
@@ -444,6 +553,10 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
444
553
|
}
|
|
445
554
|
}
|
|
446
555
|
|
|
556
|
+
for (const itf of schema.types<InterfaceType>('InterfaceType')) {
|
|
557
|
+
validateInterfaceRuntimeImplementationFieldsTypes(itf, externalTester, errors);
|
|
558
|
+
}
|
|
559
|
+
|
|
447
560
|
return errors;
|
|
448
561
|
}
|
|
449
562
|
|
|
@@ -491,7 +604,7 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
491
604
|
}
|
|
492
605
|
|
|
493
606
|
return {
|
|
494
|
-
kind:
|
|
607
|
+
kind: Kind.DOCUMENT,
|
|
495
608
|
loc: document.loc,
|
|
496
609
|
definitions
|
|
497
610
|
};
|
|
@@ -527,14 +640,17 @@ export function isEntityType(type: NamedType): boolean {
|
|
|
527
640
|
return type.kind == "ObjectType" && type.hasAppliedDirective(keyDirectiveName);
|
|
528
641
|
}
|
|
529
642
|
|
|
530
|
-
function buildSubgraph(name: string, source: DocumentNode | string): Schema {
|
|
643
|
+
export function buildSubgraph(name: string, source: DocumentNode | string): Schema {
|
|
531
644
|
try {
|
|
532
645
|
return typeof source === 'string'
|
|
533
646
|
? buildSchema(new Source(source, name), federationBuiltIns)
|
|
534
647
|
: buildSchemaFromAST(source, federationBuiltIns);
|
|
535
648
|
} catch (e) {
|
|
536
649
|
if (e instanceof GraphQLError) {
|
|
537
|
-
|
|
650
|
+
// Note that `addSubgraphToError` only adds the provided code if the original error
|
|
651
|
+
// didn't have one, and the only one that will not have a code are GraphQL errors
|
|
652
|
+
// (since we assign specific codes to the federation errors).
|
|
653
|
+
throw addSubgraphToError(e, name, ERRORS.INVALID_GRAPHQL);
|
|
538
654
|
} else {
|
|
539
655
|
throw e;
|
|
540
656
|
}
|
|
@@ -546,17 +662,72 @@ export function parseFieldSetArgument(
|
|
|
546
662
|
directive: Directive<NamedType | FieldDefinition<CompositeType>, {fields: any}>,
|
|
547
663
|
fieldAccessor: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined = (type, name) => type.field(name)
|
|
548
664
|
): SelectionSet {
|
|
549
|
-
|
|
665
|
+
try {
|
|
666
|
+
const selectionSet = parseSelectionSet(parentType, validateFieldSetValue(directive), new VariableDefinitions(), undefined, fieldAccessor);
|
|
667
|
+
selectionSet.validate();
|
|
668
|
+
return selectionSet;
|
|
669
|
+
} catch (e) {
|
|
670
|
+
if (!(e instanceof GraphQLError)) {
|
|
671
|
+
throw e;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const nodes = sourceASTs(directive);
|
|
675
|
+
if (e.nodes) {
|
|
676
|
+
nodes.push(...e.nodes);
|
|
677
|
+
}
|
|
678
|
+
let msg = e.message.trim();
|
|
679
|
+
// The rule for validating @requires in fed 1 was not properly recursive, so people upgrading
|
|
680
|
+
// may have a @require that selects some fields but without declaring those fields on the
|
|
681
|
+
// subgraph. As we fixed the validation, this will now fail, but we try here to provide some
|
|
682
|
+
// hint for those users for how to fix the problem.
|
|
683
|
+
// Note that this is a tad fragile to rely on the error message like that, but worth case, a
|
|
684
|
+
// future change make us not show the hint and that's not the end of the world.
|
|
685
|
+
if (msg.startsWith('Cannot query field')) {
|
|
686
|
+
if (msg.endsWith('.')) {
|
|
687
|
+
msg = msg.slice(0, msg.length - 1);
|
|
688
|
+
}
|
|
689
|
+
if (directive.name === keyDirectiveName) {
|
|
690
|
+
msg = msg + ' (the field should be either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).';
|
|
691
|
+
} else {
|
|
692
|
+
msg = msg + ' (if the field is defined in another subgraph, you need to add it to this subgraph with @external).';
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const codeDef = errorCodeDef(e) ?? ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS.get(directive.name);
|
|
697
|
+
throw codeDef.err({
|
|
698
|
+
message: `${fieldSetErrorDescriptor(directive)}: ${msg}`,
|
|
699
|
+
nodes,
|
|
700
|
+
originalError: e,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
550
703
|
}
|
|
551
704
|
|
|
552
705
|
function validateFieldSetValue(directive: Directive<NamedType | FieldDefinition<CompositeType>, {fields: any}>): string {
|
|
553
706
|
const fields = directive.arguments().fields;
|
|
707
|
+
const nodes = directive.sourceAST;
|
|
554
708
|
if (typeof fields !== 'string') {
|
|
555
|
-
throw
|
|
556
|
-
`Invalid value for argument ${directive.definition!.argument('fields')!.
|
|
557
|
-
|
|
558
|
-
);
|
|
709
|
+
throw ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err({
|
|
710
|
+
message: `Invalid value for argument "${directive.definition!.argument('fields')!.name}": must be a string.`,
|
|
711
|
+
nodes,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
// While validating if the field is a string will work in most cases, this will not catch the case where the field argument was
|
|
715
|
+
// unquoted but parsed as an enum value (see federation/issues/850 in particular). So if we have the AST (which we will usually
|
|
716
|
+
// have in practice), use that to check that the argument was truly a string.
|
|
717
|
+
if (nodes && nodes.kind === 'Directive') {
|
|
718
|
+
for (const argNode of nodes.arguments ?? []) {
|
|
719
|
+
if (argNode.name.value === 'fields') {
|
|
720
|
+
if (argNode.value.kind !== 'StringValue') {
|
|
721
|
+
throw ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err({
|
|
722
|
+
message: `Invalid value for argument "${directive.definition!.argument('fields')!.name}": must be a string.`,
|
|
723
|
+
nodes,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
559
729
|
}
|
|
730
|
+
|
|
560
731
|
return fields;
|
|
561
732
|
}
|
|
562
733
|
|
|
@@ -598,7 +769,7 @@ export class Subgraphs {
|
|
|
598
769
|
: subgraphOrName;
|
|
599
770
|
|
|
600
771
|
if (toAdd.name === FEDERATION_RESERVED_SUBGRAPH_NAME) {
|
|
601
|
-
throw
|
|
772
|
+
throw ERRORS.INVALID_SUBGRAPH_NAME.err({ message: `Invalid name ${FEDERATION_RESERVED_SUBGRAPH_NAME} for a subgraph: this name is reserved` });
|
|
602
773
|
}
|
|
603
774
|
|
|
604
775
|
if (this.subgraphs.has(toAdd.name)) {
|
|
@@ -661,16 +832,36 @@ export function addSubgraphToASTNode(node: ASTNode, subgraph: string): SubgraphA
|
|
|
661
832
|
};
|
|
662
833
|
}
|
|
663
834
|
|
|
664
|
-
export function addSubgraphToError(e: GraphQLError, subgraphName: string): GraphQLError {
|
|
665
|
-
const updatedCauses = errorCauses(e)!.map(cause =>
|
|
666
|
-
`[${subgraphName}] ${cause.message}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
cause
|
|
672
|
-
|
|
673
|
-
|
|
835
|
+
export function addSubgraphToError(e: GraphQLError, subgraphName: string, errorCode?: ErrorCodeDefinition): GraphQLError {
|
|
836
|
+
const updatedCauses = errorCauses(e)!.map(cause => {
|
|
837
|
+
const message = `[${subgraphName}] ${cause.message}`;
|
|
838
|
+
const nodes = cause.nodes
|
|
839
|
+
? cause.nodes.map(node => addSubgraphToASTNode(node, subgraphName))
|
|
840
|
+
: undefined;
|
|
841
|
+
|
|
842
|
+
const code = errorCodeDef(cause) ?? errorCode;
|
|
843
|
+
if (code) {
|
|
844
|
+
return code.err({
|
|
845
|
+
message,
|
|
846
|
+
nodes,
|
|
847
|
+
source: cause.source,
|
|
848
|
+
positions: cause.positions,
|
|
849
|
+
path: cause.path,
|
|
850
|
+
originalError: cause.originalError,
|
|
851
|
+
extensions: cause.extensions,
|
|
852
|
+
});
|
|
853
|
+
} else {
|
|
854
|
+
return new GraphQLError(
|
|
855
|
+
message,
|
|
856
|
+
nodes,
|
|
857
|
+
cause.source,
|
|
858
|
+
cause.positions,
|
|
859
|
+
cause.path,
|
|
860
|
+
cause.originalError,
|
|
861
|
+
cause.extensions
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
674
865
|
|
|
675
866
|
return ErrGraphQLValidationFailed(updatedCauses);
|
|
676
867
|
}
|
|
@@ -714,4 +905,18 @@ export class ExternalTester {
|
|
|
714
905
|
isFakeExternal(field: FieldDefinition<any> | InputFieldDefinition) {
|
|
715
906
|
return this.fakeExternalFields.has(field.coordinate);
|
|
716
907
|
}
|
|
908
|
+
|
|
909
|
+
selectsAnyExternalField(selectionSet: SelectionSet): boolean {
|
|
910
|
+
for (const selection of selectionSet.selections()) {
|
|
911
|
+
if (selection.kind === 'FieldSelection' && this.isExternal(selection.element().definition)) {
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
if (selection.selectionSet) {
|
|
915
|
+
if (this.selectsAnyExternalField(selection.selectionSet)) {
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
717
922
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { assert } from './utils';
|
|
2
|
+
import { ERRORS, REMOVED_ERRORS } from './error';
|
|
3
|
+
|
|
4
|
+
const header = `---
|
|
5
|
+
title: Federation error codes
|
|
6
|
+
sidebar_title: Error codes
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
When Apollo Gateway attempts to **compose** the schemas provided by your [subgraphs](./subgraphs/) into a **supergraph schema**, it confirms that:
|
|
10
|
+
|
|
11
|
+
* The subgraphs are valid
|
|
12
|
+
* The resulting supergraph schema is valid
|
|
13
|
+
* The gateway has all of the information it needs to execute operations against the resulting schema
|
|
14
|
+
|
|
15
|
+
If Apollo Gateway encounters an error, composition fails. This document lists subgraphs and composition error codes and their root causes.
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
function makeMardownArray(
|
|
19
|
+
headers: string[],
|
|
20
|
+
rows: string[][]
|
|
21
|
+
): string {
|
|
22
|
+
const columns = headers.length;
|
|
23
|
+
let out = '| ' + headers.join(' | ') + ' |\n';
|
|
24
|
+
out += '|' + headers.map(_ => '---').join('|') + '|\n';
|
|
25
|
+
for (const row of rows) {
|
|
26
|
+
assert(row.length <= columns, `Row [${row}] has too columns (expect ${columns} but got ${row.length})`);
|
|
27
|
+
const frow = row.length === columns
|
|
28
|
+
? row
|
|
29
|
+
: row.concat(new Array<string>(columns - row.length).fill(''));
|
|
30
|
+
out += '| ' + frow.join(' | ') + ' |\n'
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const rows = Object.values(ERRORS).map(def => [
|
|
36
|
+
'`' + def.code + '`',
|
|
37
|
+
def.description,
|
|
38
|
+
def.metadata.addedIn,
|
|
39
|
+
def.metadata.replaces ? `Replaces: ${def.metadata.replaces.map(c => '`' + c + '`').join(', ')}` : ''
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const sortRowsByCode = (r1: string[], r2: string[]) => r1[0].localeCompare(r2[0]);
|
|
43
|
+
|
|
44
|
+
rows.sort(sortRowsByCode);
|
|
45
|
+
|
|
46
|
+
const errorsSection = `## Errors
|
|
47
|
+
|
|
48
|
+
The following errors may be raised by composition:
|
|
49
|
+
|
|
50
|
+
` + makeMardownArray(
|
|
51
|
+
[ 'Code', 'Description', 'Since', 'Comment' ],
|
|
52
|
+
rows
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const removedErrors = REMOVED_ERRORS
|
|
56
|
+
.map(([code, comment]) => ['`' + code + '`', comment])
|
|
57
|
+
.sort(sortRowsByCode);
|
|
58
|
+
|
|
59
|
+
const removedSection = `## Removed codes
|
|
60
|
+
|
|
61
|
+
The following section lists code that have been removed and are not longer generated by the gateway version this is the documentation for.
|
|
62
|
+
|
|
63
|
+
` + makeMardownArray(['Removed Code', 'Comment'], removedErrors);
|
|
64
|
+
|
|
65
|
+
console.log(
|
|
66
|
+
header + '\n\n'
|
|
67
|
+
+ errorsSection + '\n\n'
|
|
68
|
+
+ removedSection
|
|
69
|
+
);
|
package/src/inaccessibleSpec.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
isObjectType,
|
|
8
8
|
Schema,
|
|
9
9
|
} from "./definitions";
|
|
10
|
-
import { GraphQLError } from "graphql";
|
|
10
|
+
import { GraphQLError, DirectiveLocation } from "graphql";
|
|
11
11
|
|
|
12
12
|
export const inaccessibleIdentity = 'https://specs.apollo.dev/inaccessible';
|
|
13
13
|
|
|
@@ -17,7 +17,12 @@ export class InaccessibleSpecDefinition extends FeatureDefinition {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
addElementsToSchema(schema: Schema) {
|
|
20
|
-
this.addDirective(schema, 'inaccessible').addLocations(
|
|
20
|
+
this.addDirective(schema, 'inaccessible').addLocations(
|
|
21
|
+
DirectiveLocation.FIELD_DEFINITION,
|
|
22
|
+
DirectiveLocation.OBJECT,
|
|
23
|
+
DirectiveLocation.INTERFACE,
|
|
24
|
+
DirectiveLocation.UNION,
|
|
25
|
+
);
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
inaccessibleDirective(schema: Schema): DirectiveDefinition<Record<string, never>> {
|