@apollo/federation-internals 2.0.0-alpha.2 → 2.0.0-alpha.3
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 +6 -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 +11 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +54 -0
- package/dist/definitions.js.map +1 -1
- package/dist/error.d.ts +87 -3
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +143 -5
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +40 -3
- 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 +192 -53
- 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/tagSpec.js +3 -1
- package/dist/tagSpec.js.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +19 -1
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +432 -0
- package/src/__tests__/subgraphValidation.test.ts +452 -0
- package/src/debug.ts +2 -19
- package/src/definitions.ts +98 -0
- package/src/error.ts +334 -7
- package/src/extractSubgraphsFromSupergraph.ts +49 -4
- package/src/federation.ts +229 -85
- package/src/genErrorCodeDoc.ts +69 -0
- package/src/tagSpec.ts +4 -4
- package/src/utils.ts +27 -0
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/federation.ts
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
baseType,
|
|
19
19
|
isInterfaceType,
|
|
20
20
|
isObjectType,
|
|
21
|
+
isListType,
|
|
22
|
+
isUnionType,
|
|
21
23
|
sourceASTs,
|
|
22
24
|
VariableDefinitions,
|
|
23
25
|
InterfaceType,
|
|
@@ -27,13 +29,18 @@ import {
|
|
|
27
29
|
import { assert, OrderedMap } from "./utils";
|
|
28
30
|
import { SDLValidationRule } from "graphql/validation/ValidationContext";
|
|
29
31
|
import { specifiedSDLRules } from "graphql/validation/specifiedRules";
|
|
30
|
-
import { ASTNode, DocumentNode, GraphQLError, KnownTypeNamesRule, parse, PossibleTypeExtensionsRule, Source } from "graphql";
|
|
32
|
+
import { ASTNode, DocumentNode, GraphQLError, KnownTypeNamesRule, parse, PossibleTypeExtensionsRule, print as printAST, Source } from "graphql";
|
|
31
33
|
import { defaultPrintOptions, printDirectiveDefinition } from "./print";
|
|
32
34
|
import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFederationRule";
|
|
33
35
|
import { buildSchema, buildSchemaFromAST } from "./buildSchema";
|
|
34
36
|
import { parseSelectionSet, SelectionSet } from './operations';
|
|
35
37
|
import { tagLocations, TAG_VERSIONS } from "./tagSpec";
|
|
36
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
errorCodeDef,
|
|
40
|
+
ErrorCodeDefinition,
|
|
41
|
+
ERRORS,
|
|
42
|
+
} from "./error";
|
|
43
|
+
import { ERROR_CATEGORIES } from ".";
|
|
37
44
|
|
|
38
45
|
export const entityTypeName = '_Entity';
|
|
39
46
|
export const serviceTypeName = '_Service';
|
|
@@ -107,22 +114,33 @@ function validateFieldSetSelections(
|
|
|
107
114
|
for (const selection of selectionSet.selections()) {
|
|
108
115
|
if (selection.kind === 'FieldSelection') {
|
|
109
116
|
const field = selection.element().definition;
|
|
117
|
+
const isExternal = externalTester.isExternal(field);
|
|
118
|
+
// We collect the field as external before any other validation to avoid getting a (confusing)
|
|
119
|
+
// "external unused" error on top of another error due to exiting that method too early.
|
|
120
|
+
if (isExternal) {
|
|
121
|
+
externalFieldCoordinatesCollector.push(field.coordinate);
|
|
122
|
+
}
|
|
110
123
|
if (field.hasArguments()) {
|
|
111
|
-
throw
|
|
124
|
+
throw ERROR_CATEGORIES.FIELDS_HAS_ARGS.get(directiveName).err({
|
|
125
|
+
message: `field ${field.coordinate} cannot be included because it has arguments (fields with argument are not allowed in @${directiveName})`,
|
|
126
|
+
nodes: field.sourceAST
|
|
127
|
+
});
|
|
112
128
|
}
|
|
113
129
|
// 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
130
|
const mustBeExternal = !selection.selectionSet && !allowOnNonExternalLeafFields && !hasExternalInParents;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
externalFieldCoordinatesCollector.push(field.coordinate);
|
|
118
|
-
} else if (mustBeExternal) {
|
|
131
|
+
if (!isExternal && mustBeExternal) {
|
|
132
|
+
const errorCode = ERROR_CATEGORIES.DIRECTIVE_FIELDS_MISSING_EXTERNAL.get(directiveName);
|
|
119
133
|
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
|
|
134
|
+
throw errorCode.err({
|
|
135
|
+
message: `field "${field.coordinate}" should not be part of a @${directiveName} since it is already "effectively" provided by this subgraph `
|
|
136
|
+
+ `(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)`,
|
|
137
|
+
nodes: field.sourceAST
|
|
138
|
+
});
|
|
124
139
|
} else {
|
|
125
|
-
throw
|
|
140
|
+
throw errorCode.err({
|
|
141
|
+
message: `field "${field.coordinate}" should not be part of a @${directiveName} since it is already provided by this subgraph (it is not marked @${externalDirectiveName})`,
|
|
142
|
+
nodes: field.sourceAST
|
|
143
|
+
});
|
|
126
144
|
}
|
|
127
145
|
}
|
|
128
146
|
if (selection.selectionSet) {
|
|
@@ -151,69 +169,97 @@ function validateFieldSetSelections(
|
|
|
151
169
|
function validateFieldSet(
|
|
152
170
|
type: CompositeType,
|
|
153
171
|
directive: Directive<any, {fields: any}>,
|
|
154
|
-
targetDescription: string,
|
|
155
172
|
externalTester: ExternalTester,
|
|
156
173
|
externalFieldCoordinatesCollector: string[],
|
|
157
174
|
allowOnNonExternalLeafFields: boolean,
|
|
175
|
+
onFields?: (field: FieldDefinition<any>) => void,
|
|
158
176
|
): GraphQLError | undefined {
|
|
159
177
|
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);
|
|
178
|
+
// Note that `parseFieldSetArgument` already properly format the error, hence the separate try-catch.
|
|
179
|
+
const fieldAcessor = onFields
|
|
180
|
+
? (type: CompositeType, fieldName: string) => {
|
|
181
|
+
const field = type.field(fieldName);
|
|
182
|
+
if (field) {
|
|
183
|
+
onFields(field);
|
|
184
|
+
}
|
|
185
|
+
return field;
|
|
182
186
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
+
: undefined;
|
|
188
|
+
const selectionSet = parseFieldSetArgument(type, directive, fieldAcessor);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
validateFieldSetSelections(directive.name, selectionSet, false, externalTester, externalFieldCoordinatesCollector, allowOnNonExternalLeafFields);
|
|
192
|
+
return undefined;
|
|
193
|
+
} catch (e) {
|
|
194
|
+
if (!(e instanceof GraphQLError)) {
|
|
195
|
+
throw e;
|
|
187
196
|
}
|
|
197
|
+
const nodes = sourceASTs(directive);
|
|
198
|
+
if (e.nodes) {
|
|
199
|
+
nodes.push(...e.nodes);
|
|
200
|
+
}
|
|
201
|
+
const codeDef = errorCodeDef(e) ?? ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS.get(directive.name);
|
|
202
|
+
return codeDef.err({
|
|
203
|
+
message: `${fieldSetErrorDescriptor(directive)}: ${e.message.trim()}`,
|
|
204
|
+
nodes,
|
|
205
|
+
originalError: e,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
if (e instanceof GraphQLError) {
|
|
210
|
+
return e;
|
|
211
|
+
} else {
|
|
212
|
+
throw e;
|
|
188
213
|
}
|
|
189
|
-
return new GraphQLError(`On ${targetDescription}, for ${directive}: ${msg}`, nodes);
|
|
190
214
|
}
|
|
191
215
|
}
|
|
192
216
|
|
|
217
|
+
function fieldSetErrorDescriptor(directive: Directive<any, {fields: any}>): string {
|
|
218
|
+
return `On ${fieldSetTargetDescription(directive)}, for ${directiveStrUsingASTIfPossible(directive)}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// This method is called to display @key, @provides or @requires directives in error message in place where the directive `fields`
|
|
222
|
+
// 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
|
|
223
|
+
// print the directive or the message might be a bit confusing for the user.
|
|
224
|
+
function directiveStrUsingASTIfPossible(directive: Directive<any>): string {
|
|
225
|
+
return directive.sourceAST ? printAST(directive.sourceAST) : directive.toString();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function fieldSetTargetDescription(directive: Directive<any, {fields: any}>): string {
|
|
229
|
+
const targetKind = directive.parent instanceof FieldDefinition ? "field" : "type";
|
|
230
|
+
return `${targetKind} "${directive.parent?.coordinate}"`;
|
|
231
|
+
}
|
|
232
|
+
|
|
193
233
|
function validateAllFieldSet<TParent extends SchemaElement<any, any>>(
|
|
194
234
|
definition: DirectiveDefinition<{fields: any}>,
|
|
195
235
|
targetTypeExtractor: (element: TParent) => CompositeType,
|
|
196
|
-
targetDescriptionExtractor: (element: TParent) => string,
|
|
197
236
|
errorCollector: GraphQLError[],
|
|
198
237
|
externalTester: ExternalTester,
|
|
199
238
|
externalFieldCoordinatesCollector: string[],
|
|
200
239
|
isOnParentType: boolean,
|
|
201
240
|
allowOnNonExternalLeafFields: boolean,
|
|
241
|
+
onFields?: (field: FieldDefinition<any>) => void,
|
|
202
242
|
): void {
|
|
203
243
|
for (const application of definition.applications()) {
|
|
204
244
|
const elt = application.parent as TParent;
|
|
205
245
|
const type = targetTypeExtractor(elt);
|
|
206
|
-
const targetDescription = targetDescriptionExtractor(elt);
|
|
207
246
|
const parentType = isOnParentType ? type : (elt.parent as NamedType);
|
|
208
247
|
if (isInterfaceType(parentType)) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
248
|
+
const code = ERROR_CATEGORIES.DIRECTIVE_UNSUPPORTED_ON_INTERFACE.get(definition.name);
|
|
249
|
+
errorCollector.push(code.err({
|
|
250
|
+
message: isOnParentType
|
|
251
|
+
? `Cannot use ${definition.coordinate} on interface "${parentType.coordinate}": ${definition.coordinate} is not yet supported on interfaces`
|
|
252
|
+
: `Cannot use ${definition.coordinate} on ${fieldSetTargetDescription(application)} of parent type "${parentType}": ${definition.coordinate} is not yet supported within interfaces`,
|
|
253
|
+
nodes: sourceASTs(application).concat(isOnParentType ? [] : sourceASTs(type)),
|
|
254
|
+
}));
|
|
215
255
|
}
|
|
216
|
-
const error = validateFieldSet(
|
|
256
|
+
const error = validateFieldSet(
|
|
257
|
+
type,
|
|
258
|
+
application,
|
|
259
|
+
externalTester,
|
|
260
|
+
externalFieldCoordinatesCollector,
|
|
261
|
+
allowOnNonExternalLeafFields,
|
|
262
|
+
onFields);
|
|
217
263
|
if (error) {
|
|
218
264
|
errorCollector.push(error);
|
|
219
265
|
}
|
|
@@ -240,11 +286,11 @@ function validateAllExternalFieldsUsed(
|
|
|
240
286
|
}
|
|
241
287
|
|
|
242
288
|
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;`
|
|
289
|
+
errorCollector.push(ERRORS.EXTERNAL_UNUSED.err({
|
|
290
|
+
message: `Field "${field.coordinate}" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface;`
|
|
245
291
|
+ ' the field declaration has no use and should be removed (or the field should not be @external).',
|
|
246
|
-
field.sourceAST
|
|
247
|
-
));
|
|
292
|
+
nodes: field.sourceAST,
|
|
293
|
+
}));
|
|
248
294
|
}
|
|
249
295
|
}
|
|
250
296
|
}
|
|
@@ -363,13 +409,12 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
363
409
|
// composition error.
|
|
364
410
|
const existing = schema.type(defaultName);
|
|
365
411
|
if (existing) {
|
|
366
|
-
errors.push(
|
|
367
|
-
`
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
));
|
|
412
|
+
errors.push(ERROR_CATEGORIES.ROOT_TYPE_USED.get(k).err({
|
|
413
|
+
message: `The schema has a type named "${defaultName}" but it is not set as the ${k} root type ("${type.name}" is instead): `
|
|
414
|
+
+ 'this is not supported by federation. '
|
|
415
|
+
+ 'If a root type does not use its default name, there should be no other type with that default name.',
|
|
416
|
+
nodes: sourceASTs(type, existing),
|
|
417
|
+
}));
|
|
373
418
|
}
|
|
374
419
|
type.rename(defaultName);
|
|
375
420
|
}
|
|
@@ -383,12 +428,20 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
383
428
|
validateAllFieldSet<CompositeType>(
|
|
384
429
|
keyDirective,
|
|
385
430
|
type => type,
|
|
386
|
-
type => `type "${type}"`,
|
|
387
431
|
errors,
|
|
388
432
|
externalTester,
|
|
389
433
|
externalFieldsInFedDirectivesCoordinates,
|
|
390
434
|
true,
|
|
391
|
-
true
|
|
435
|
+
true,
|
|
436
|
+
field => {
|
|
437
|
+
if (isListType(field.type!) || isUnionType(field.type!) || isInterfaceType(field.type!)) {
|
|
438
|
+
let kind: string = field.type!.kind;
|
|
439
|
+
kind = kind.slice(0, kind.length - 'Type'.length);
|
|
440
|
+
throw ERRORS.KEY_FIELDS_SELECT_INVALID_TYPE.err({
|
|
441
|
+
message: `field "${field.coordinate}" is a ${kind} type which is not allowed in @key`
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
392
445
|
);
|
|
393
446
|
// Note that we currently reject @requires where a leaf field of the selection is not external,
|
|
394
447
|
// because if it's provided by the current subgraph, why "requires" it? That said, it's not 100%
|
|
@@ -400,7 +453,6 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
400
453
|
validateAllFieldSet<FieldDefinition<CompositeType>>(
|
|
401
454
|
this.requiresDirective(schema),
|
|
402
455
|
field => field.parent,
|
|
403
|
-
field => `field "${field.coordinate}"`,
|
|
404
456
|
errors,
|
|
405
457
|
externalTester,
|
|
406
458
|
externalFieldsInFedDirectivesCoordinates,
|
|
@@ -419,13 +471,13 @@ export class FederationBuiltIns extends BuiltIns {
|
|
|
419
471
|
}
|
|
420
472
|
const type = baseType(field.type!);
|
|
421
473
|
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
|
|
474
|
+
throw ERRORS.PROVIDES_ON_NON_OBJECT_FIELD.err({
|
|
475
|
+
message: `Invalid @provides directive on field "${field.coordinate}": field has type "${field.type}" which is not a Composite Type`,
|
|
476
|
+
nodes: field.sourceAST,
|
|
477
|
+
});
|
|
425
478
|
}
|
|
426
479
|
return type;
|
|
427
480
|
},
|
|
428
|
-
field => `field ${field.coordinate}`,
|
|
429
481
|
errors,
|
|
430
482
|
externalTester,
|
|
431
483
|
externalFieldsInFedDirectivesCoordinates,
|
|
@@ -527,14 +579,17 @@ export function isEntityType(type: NamedType): boolean {
|
|
|
527
579
|
return type.kind == "ObjectType" && type.hasAppliedDirective(keyDirectiveName);
|
|
528
580
|
}
|
|
529
581
|
|
|
530
|
-
function buildSubgraph(name: string, source: DocumentNode | string): Schema {
|
|
582
|
+
export function buildSubgraph(name: string, source: DocumentNode | string): Schema {
|
|
531
583
|
try {
|
|
532
584
|
return typeof source === 'string'
|
|
533
585
|
? buildSchema(new Source(source, name), federationBuiltIns)
|
|
534
586
|
: buildSchemaFromAST(source, federationBuiltIns);
|
|
535
587
|
} catch (e) {
|
|
536
588
|
if (e instanceof GraphQLError) {
|
|
537
|
-
|
|
589
|
+
// Note that `addSubgraphToError` only adds the provided code if the original error
|
|
590
|
+
// didn't have one, and the only one that will not have a code are GraphQL errors
|
|
591
|
+
// (since we assign specific codes to the federation errors).
|
|
592
|
+
throw addSubgraphToError(e, name, ERRORS.INVALID_GRAPHQL);
|
|
538
593
|
} else {
|
|
539
594
|
throw e;
|
|
540
595
|
}
|
|
@@ -546,17 +601,72 @@ export function parseFieldSetArgument(
|
|
|
546
601
|
directive: Directive<NamedType | FieldDefinition<CompositeType>, {fields: any}>,
|
|
547
602
|
fieldAccessor: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined = (type, name) => type.field(name)
|
|
548
603
|
): SelectionSet {
|
|
549
|
-
|
|
604
|
+
try {
|
|
605
|
+
const selectionSet = parseSelectionSet(parentType, validateFieldSetValue(directive), new VariableDefinitions(), undefined, fieldAccessor);
|
|
606
|
+
selectionSet.validate();
|
|
607
|
+
return selectionSet;
|
|
608
|
+
} catch (e) {
|
|
609
|
+
if (!(e instanceof GraphQLError)) {
|
|
610
|
+
throw e;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const nodes = sourceASTs(directive);
|
|
614
|
+
if (e.nodes) {
|
|
615
|
+
nodes.push(...e.nodes);
|
|
616
|
+
}
|
|
617
|
+
let msg = e.message.trim();
|
|
618
|
+
// The rule for validating @requires in fed 1 was not properly recursive, so people upgrading
|
|
619
|
+
// may have a @require that selects some fields but without declaring those fields on the
|
|
620
|
+
// subgraph. As we fixed the validation, this will now fail, but we try here to provide some
|
|
621
|
+
// hint for those users for how to fix the problem.
|
|
622
|
+
// Note that this is a tad fragile to rely on the error message like that, but worth case, a
|
|
623
|
+
// future change make us not show the hint and that's not the end of the world.
|
|
624
|
+
if (msg.startsWith('Cannot query field')) {
|
|
625
|
+
if (msg.endsWith('.')) {
|
|
626
|
+
msg = msg.slice(0, msg.length - 1);
|
|
627
|
+
}
|
|
628
|
+
if (directive.name === keyDirectiveName) {
|
|
629
|
+
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).';
|
|
630
|
+
} else {
|
|
631
|
+
msg = msg + ' (if the field is defined in another subgraph, you need to add it to this subgraph with @external).';
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const codeDef = errorCodeDef(e) ?? ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS.get(directive.name);
|
|
636
|
+
throw codeDef.err({
|
|
637
|
+
message: `${fieldSetErrorDescriptor(directive)}: ${msg}`,
|
|
638
|
+
nodes,
|
|
639
|
+
originalError: e,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
550
642
|
}
|
|
551
643
|
|
|
552
644
|
function validateFieldSetValue(directive: Directive<NamedType | FieldDefinition<CompositeType>, {fields: any}>): string {
|
|
553
645
|
const fields = directive.arguments().fields;
|
|
646
|
+
const nodes = directive.sourceAST;
|
|
554
647
|
if (typeof fields !== 'string') {
|
|
555
|
-
throw
|
|
556
|
-
`Invalid value for argument ${directive.definition!.argument('fields')!.
|
|
557
|
-
|
|
558
|
-
);
|
|
648
|
+
throw ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err({
|
|
649
|
+
message: `Invalid value for argument "${directive.definition!.argument('fields')!.name}": must be a string.`,
|
|
650
|
+
nodes,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
// While validating if the field is a string will work in most cases, this will not catch the case where the field argument was
|
|
654
|
+
// unquoted but parsed as an enum value (see federation/issues/850 in particular). So if we have the AST (which we will usually
|
|
655
|
+
// have in practice), use that to check that the argument was truly a string.
|
|
656
|
+
if (nodes && nodes.kind === 'Directive') {
|
|
657
|
+
for (const argNode of nodes.arguments ?? []) {
|
|
658
|
+
if (argNode.name.value === 'fields') {
|
|
659
|
+
if (argNode.value.kind !== 'StringValue') {
|
|
660
|
+
throw ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err({
|
|
661
|
+
message: `Invalid value for argument "${directive.definition!.argument('fields')!.name}": must be a string.`,
|
|
662
|
+
nodes,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
559
668
|
}
|
|
669
|
+
|
|
560
670
|
return fields;
|
|
561
671
|
}
|
|
562
672
|
|
|
@@ -598,7 +708,7 @@ export class Subgraphs {
|
|
|
598
708
|
: subgraphOrName;
|
|
599
709
|
|
|
600
710
|
if (toAdd.name === FEDERATION_RESERVED_SUBGRAPH_NAME) {
|
|
601
|
-
throw
|
|
711
|
+
throw ERRORS.INVALID_SUBGRAPH_NAME.err({ message: `Invalid name ${FEDERATION_RESERVED_SUBGRAPH_NAME} for a subgraph: this name is reserved` });
|
|
602
712
|
}
|
|
603
713
|
|
|
604
714
|
if (this.subgraphs.has(toAdd.name)) {
|
|
@@ -661,16 +771,36 @@ export function addSubgraphToASTNode(node: ASTNode, subgraph: string): SubgraphA
|
|
|
661
771
|
};
|
|
662
772
|
}
|
|
663
773
|
|
|
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
|
-
|
|
774
|
+
export function addSubgraphToError(e: GraphQLError, subgraphName: string, errorCode?: ErrorCodeDefinition): GraphQLError {
|
|
775
|
+
const updatedCauses = errorCauses(e)!.map(cause => {
|
|
776
|
+
const message = `[${subgraphName}] ${cause.message}`;
|
|
777
|
+
const nodes = cause.nodes
|
|
778
|
+
? cause.nodes.map(node => addSubgraphToASTNode(node, subgraphName))
|
|
779
|
+
: undefined;
|
|
780
|
+
|
|
781
|
+
const code = errorCodeDef(cause) ?? errorCode;
|
|
782
|
+
if (code) {
|
|
783
|
+
return code.err({
|
|
784
|
+
message,
|
|
785
|
+
nodes,
|
|
786
|
+
source: cause.source,
|
|
787
|
+
positions: cause.positions,
|
|
788
|
+
path: cause.path,
|
|
789
|
+
originalError: cause.originalError,
|
|
790
|
+
extensions: cause.extensions,
|
|
791
|
+
});
|
|
792
|
+
} else {
|
|
793
|
+
return new GraphQLError(
|
|
794
|
+
message,
|
|
795
|
+
nodes,
|
|
796
|
+
cause.source,
|
|
797
|
+
cause.positions,
|
|
798
|
+
cause.path,
|
|
799
|
+
cause.originalError,
|
|
800
|
+
cause.extensions
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
674
804
|
|
|
675
805
|
return ErrGraphQLValidationFailed(updatedCauses);
|
|
676
806
|
}
|
|
@@ -714,4 +844,18 @@ export class ExternalTester {
|
|
|
714
844
|
isFakeExternal(field: FieldDefinition<any> | InputFieldDefinition) {
|
|
715
845
|
return this.fakeExternalFields.has(field.coordinate);
|
|
716
846
|
}
|
|
847
|
+
|
|
848
|
+
selectsAnyExternalField(selectionSet: SelectionSet): boolean {
|
|
849
|
+
for (const selection of selectionSet.selections()) {
|
|
850
|
+
if (selection.kind === 'FieldSelection' && this.isExternal(selection.element().definition)) {
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
if (selection.selectionSet) {
|
|
854
|
+
if (this.selectsAnyExternalField(selection.selectionSet)) {
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
717
861
|
}
|
|
@@ -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/tagSpec.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DirectiveLocationEnum, GraphQLError } from "graphql";
|
|
2
2
|
import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from "./coreSpec";
|
|
3
3
|
import { DirectiveDefinition, NonNullType, Schema } from "./definitions";
|
|
4
|
-
import {
|
|
4
|
+
import { ERRORS } from "./error";
|
|
5
5
|
import { sameType } from "./types";
|
|
6
6
|
import { assert } from "./utils";
|
|
7
7
|
|
|
@@ -32,9 +32,9 @@ export class TagSpecDefinition extends FeatureDefinition {
|
|
|
32
32
|
const hasValidNameArg = nameArg && sameType(nameArg.type!, new NonNullType(definition.schema().stringType()));
|
|
33
33
|
const hasValidLocations = definition.locations.every(loc => tagLocations.includes(loc));
|
|
34
34
|
if (hasUnknownArguments || !hasValidNameArg || !hasValidLocations) {
|
|
35
|
-
return
|
|
36
|
-
'
|
|
37
|
-
|
|
35
|
+
return ERRORS.TAG_DEFINITION_INVALID.err({
|
|
36
|
+
message: `Found invalid @tag directive definition. Please ensure the directive definition in your schema's definitions matches the following:\n\t${printedTagDefinition}`,
|
|
37
|
+
}
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
return undefined;
|
package/src/utils.ts
CHANGED
|
@@ -242,3 +242,30 @@ export function copyWitNewLength<T>(arr: T[], newLength: number): T[] {
|
|
|
242
242
|
}
|
|
243
243
|
return copy;
|
|
244
244
|
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Checks whether the provided string value is defined and represents a "boolean-ish"
|
|
248
|
+
* value, returning that boolean value.
|
|
249
|
+
*
|
|
250
|
+
* @param str - the string to check.
|
|
251
|
+
* @return the boolean value contains in `str` if `str` represents a boolean-ish value,
|
|
252
|
+
* where "boolean-ish" is one of "true"/"false", "yes"/"no" or "0"/"1" (where the check
|
|
253
|
+
* is case-insensitive). Otherwise, `undefined` is returned.
|
|
254
|
+
*/
|
|
255
|
+
export function validateStringContainsBoolean(str?: string) : boolean | undefined {
|
|
256
|
+
if (!str) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
switch (str.toLocaleLowerCase()) {
|
|
260
|
+
case "true":
|
|
261
|
+
case "yes":
|
|
262
|
+
case "1":
|
|
263
|
+
return true;
|
|
264
|
+
case "false":
|
|
265
|
+
case "no":
|
|
266
|
+
case "0":
|
|
267
|
+
return false;
|
|
268
|
+
default:
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
}
|