@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +6 -1
  2. package/dist/debug.d.ts.map +1 -1
  3. package/dist/debug.js +2 -18
  4. package/dist/debug.js.map +1 -1
  5. package/dist/definitions.d.ts +11 -0
  6. package/dist/definitions.d.ts.map +1 -1
  7. package/dist/definitions.js +54 -0
  8. package/dist/definitions.js.map +1 -1
  9. package/dist/error.d.ts +87 -3
  10. package/dist/error.d.ts.map +1 -1
  11. package/dist/error.js +143 -5
  12. package/dist/error.js.map +1 -1
  13. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  14. package/dist/extractSubgraphsFromSupergraph.js +40 -3
  15. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  16. package/dist/federation.d.ts +4 -1
  17. package/dist/federation.d.ts.map +1 -1
  18. package/dist/federation.js +192 -53
  19. package/dist/federation.js.map +1 -1
  20. package/dist/genErrorCodeDoc.d.ts +2 -0
  21. package/dist/genErrorCodeDoc.d.ts.map +1 -0
  22. package/dist/genErrorCodeDoc.js +55 -0
  23. package/dist/genErrorCodeDoc.js.map +1 -0
  24. package/dist/tagSpec.js +3 -1
  25. package/dist/tagSpec.js.map +1 -1
  26. package/dist/utils.d.ts +1 -0
  27. package/dist/utils.d.ts.map +1 -1
  28. package/dist/utils.js +19 -1
  29. package/dist/utils.js.map +1 -1
  30. package/package.json +3 -3
  31. package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +432 -0
  32. package/src/__tests__/subgraphValidation.test.ts +452 -0
  33. package/src/debug.ts +2 -19
  34. package/src/definitions.ts +98 -0
  35. package/src/error.ts +334 -7
  36. package/src/extractSubgraphsFromSupergraph.ts +49 -4
  37. package/src/federation.ts +229 -85
  38. package/src/genErrorCodeDoc.ts +69 -0
  39. package/src/tagSpec.ts +4 -4
  40. package/src/utils.ts +27 -0
  41. package/tsconfig.test.tsbuildinfo +1 -1
  42. 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 { error } from "./error";
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 new GraphQLError(`field ${field.coordinate} cannot be included because it has arguments (fields with argument are not allowed in @${directiveName})`, field.sourceAST);
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
- const isExternal = externalTester.isExternal(field);
116
- if (isExternal) {
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 new GraphQLError(
121
- `field "${field.coordinate}" should not be part of a @${directiveName} since it is already "effectively" provided by this subgraph `
122
- + `(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)`,
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 new GraphQLError(`field "${field.coordinate}" should not be part of a @${directiveName} since it is already provided by this subgraph (it is not marked @${externalDirectiveName})`, field.sourceAST);
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
- const selectionSet = parseFieldSetArgument(type, directive);
161
- selectionSet.validate();
162
- validateFieldSetSelections(directive.name, selectionSet, false, externalTester, externalFieldCoordinatesCollector, allowOnNonExternalLeafFields);
163
- return undefined;
164
- } catch (e) {
165
- if (!(e instanceof GraphQLError)) {
166
- throw e;
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
- if (directive.name === keyDirectiveName) {
184
- 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).';
185
- } else {
186
- msg = msg + ' (if the field is defined in another subgraph, you need to add it to this subgraph with @external).';
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
- errorCollector.push(new GraphQLError(
210
- isOnParentType
211
- ? `Cannot use ${definition.coordinate} on interface ${parentType.coordinate}: ${definition.coordinate} is not yet supported on interfaces`
212
- : `Cannot use ${definition.coordinate} on ${targetDescription} of parent type ${parentType}: ${definition.coordinate} is not yet supported within interfaces`,
213
- sourceASTs(application).concat(isOnParentType ? [] : sourceASTs(type))
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(type, application, targetDescription, externalTester, externalFieldCoordinatesCollector, allowOnNonExternalLeafFields);
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(new GraphQLError(
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(error(
367
- `ROOT_${k.toUpperCase()}_USED`,
368
- `The schema has a type named "${defaultName}" but it is not set as the ${k} root type ("${type.name}" is instead): `
369
- + 'this is not supported by federation. '
370
- + 'If a root type does not use its default name, there should be no other type with that default name.',
371
- sourceASTs(type, existing)
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 new GraphQLError(
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
- throw addSubgraphToError(e, name);
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
- return parseSelectionSet(parentType, validateFieldSetValue(directive), new VariableDefinitions(), undefined, fieldAccessor);
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 new GraphQLError(
556
- `Invalid value for argument ${directive.definition!.argument('fields')!.coordinate} on ${directive.parent.coordinate}: must be a string.`,
557
- directive.sourceAST
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 new GraphQLError(`Invalid name ${FEDERATION_RESERVED_SUBGRAPH_NAME} for a subgraph: this name is reserved`);
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 => new GraphQLError(
666
- `[${subgraphName}] ${cause.message}`,
667
- cause.nodes ? cause.nodes.map(node => addSubgraphToASTNode(node, subgraphName)) : undefined,
668
- cause.source,
669
- cause.positions,
670
- cause.path,
671
- cause.originalError,
672
- cause.extensions
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 { error } from "./error";
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 error(
36
- 'TAG_DIRECTIVE_DEFINITION_INVALID',
37
- `Found invalid @tag directive definition. Please ensure the directive definition in your schema's definitions matches the following:\n\t${printedTagDefinition}`,
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
+ }