@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/buildSchema.d.ts.map +1 -1
  3. package/dist/buildSchema.js +3 -3
  4. package/dist/buildSchema.js.map +1 -1
  5. package/dist/debug.d.ts.map +1 -1
  6. package/dist/debug.js +2 -18
  7. package/dist/debug.js.map +1 -1
  8. package/dist/definitions.d.ts +18 -7
  9. package/dist/definitions.d.ts.map +1 -1
  10. package/dist/definitions.js +80 -26
  11. package/dist/definitions.js.map +1 -1
  12. package/dist/error.d.ts +88 -3
  13. package/dist/error.d.ts.map +1 -1
  14. package/dist/error.js +145 -5
  15. package/dist/error.js.map +1 -1
  16. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  17. package/dist/extractSubgraphsFromSupergraph.js +41 -4
  18. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  19. package/dist/federation.d.ts +4 -1
  20. package/dist/federation.d.ts.map +1 -1
  21. package/dist/federation.js +231 -58
  22. package/dist/federation.js.map +1 -1
  23. package/dist/genErrorCodeDoc.d.ts +2 -0
  24. package/dist/genErrorCodeDoc.d.ts.map +1 -0
  25. package/dist/genErrorCodeDoc.js +55 -0
  26. package/dist/genErrorCodeDoc.js.map +1 -0
  27. package/dist/inaccessibleSpec.d.ts.map +1 -1
  28. package/dist/inaccessibleSpec.js +1 -1
  29. package/dist/inaccessibleSpec.js.map +1 -1
  30. package/dist/joinSpec.d.ts.map +1 -1
  31. package/dist/joinSpec.js +6 -5
  32. package/dist/joinSpec.js.map +1 -1
  33. package/dist/operations.d.ts.map +1 -1
  34. package/dist/operations.js +15 -15
  35. package/dist/operations.js.map +1 -1
  36. package/dist/tagSpec.d.ts +2 -2
  37. package/dist/tagSpec.d.ts.map +1 -1
  38. package/dist/tagSpec.js +10 -2
  39. package/dist/tagSpec.js.map +1 -1
  40. package/dist/utils.d.ts +2 -0
  41. package/dist/utils.d.ts.map +1 -1
  42. package/dist/utils.js +34 -1
  43. package/dist/utils.js.map +1 -1
  44. package/dist/values.d.ts +2 -1
  45. package/dist/values.d.ts.map +1 -1
  46. package/dist/values.js +27 -1
  47. package/dist/values.js.map +1 -1
  48. package/jest.config.js +5 -1
  49. package/package.json +3 -6
  50. package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +535 -0
  51. package/src/__tests__/subgraphValidation.test.ts +480 -0
  52. package/src/buildSchema.ts +7 -6
  53. package/src/debug.ts +2 -19
  54. package/src/definitions.ts +151 -40
  55. package/src/error.ts +340 -7
  56. package/src/extractSubgraphsFromSupergraph.ts +50 -5
  57. package/src/federation.ts +297 -92
  58. package/src/genErrorCodeDoc.ts +69 -0
  59. package/src/inaccessibleSpec.ts +7 -2
  60. package/src/joinSpec.ts +11 -5
  61. package/src/operations.ts +20 -18
  62. package/src/tagSpec.ts +11 -6
  63. package/src/utils.ts +49 -0
  64. package/src/values.ts +47 -5
  65. package/tsconfig.test.tsbuildinfo +1 -1
  66. 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 { ASTNode, DocumentNode, GraphQLError, KnownTypeNamesRule, parse, PossibleTypeExtensionsRule, Source } from "graphql";
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 { error } from "./error";
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 new GraphQLError(`field ${field.coordinate} cannot be included because it has arguments (fields with argument are not allowed in @${directiveName})`, field.sourceAST);
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
- const isExternal = externalTester.isExternal(field);
116
- if (isExternal) {
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 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);
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 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);
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
- 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);
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
- 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).';
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
- 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
- ));
215
- }
216
- const error = validateFieldSet(type, application, targetDescription, externalTester, externalFieldCoordinatesCollector, allowOnNonExternalLeafFields);
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(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;`
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('OBJECT', 'INTERFACE');
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('OBJECT', 'INTERFACE');
387
+ .addLocations(DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE);
285
388
 
286
389
  this.addBuiltInDirective(schema, externalDirectiveName)
287
- .addLocations('OBJECT', 'FIELD_DEFINITION');
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('FIELD_DEFINITION')
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(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
- ));
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 new GraphQLError(
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: 'Document',
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
- throw addSubgraphToError(e, name);
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
- return parseSelectionSet(parentType, validateFieldSetValue(directive), new VariableDefinitions(), undefined, fieldAccessor);
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 new GraphQLError(
556
- `Invalid value for argument ${directive.definition!.argument('fields')!.coordinate} on ${directive.parent.coordinate}: must be a string.`,
557
- directive.sourceAST
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 new GraphQLError(`Invalid name ${FEDERATION_RESERVED_SUBGRAPH_NAME} for a subgraph: this name is reserved`);
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 => 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
- ));
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
+ );
@@ -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('FIELD_DEFINITION', 'OBJECT', 'INTERFACE', 'UNION');
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>> {