@apollo/federation-internals 2.0.0-alpha.0 → 2.0.0-alpha.4

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 (86) hide show
  1. package/CHANGELOG.md +18 -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/coreSpec.d.ts +1 -1
  6. package/dist/coreSpec.d.ts.map +1 -1
  7. package/dist/coreSpec.js +2 -0
  8. package/dist/coreSpec.js.map +1 -1
  9. package/dist/debug.d.ts.map +1 -1
  10. package/dist/debug.js +7 -23
  11. package/dist/debug.js.map +1 -1
  12. package/dist/definitions.d.ts +28 -13
  13. package/dist/definitions.d.ts.map +1 -1
  14. package/dist/definitions.js +132 -71
  15. package/dist/definitions.js.map +1 -1
  16. package/dist/error.d.ts +87 -3
  17. package/dist/error.d.ts.map +1 -1
  18. package/dist/error.js +143 -5
  19. package/dist/error.js.map +1 -1
  20. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  21. package/dist/extractSubgraphsFromSupergraph.js +60 -8
  22. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  23. package/dist/federation.d.ts +7 -6
  24. package/dist/federation.d.ts.map +1 -1
  25. package/dist/federation.js +227 -81
  26. package/dist/federation.js.map +1 -1
  27. package/dist/genErrorCodeDoc.d.ts +2 -0
  28. package/dist/genErrorCodeDoc.d.ts.map +1 -0
  29. package/dist/genErrorCodeDoc.js +55 -0
  30. package/dist/genErrorCodeDoc.js.map +1 -0
  31. package/dist/inaccessibleSpec.d.ts +1 -1
  32. package/dist/inaccessibleSpec.d.ts.map +1 -1
  33. package/dist/inaccessibleSpec.js +5 -5
  34. package/dist/inaccessibleSpec.js.map +1 -1
  35. package/dist/joinSpec.d.ts.map +1 -1
  36. package/dist/joinSpec.js +6 -5
  37. package/dist/joinSpec.js.map +1 -1
  38. package/dist/operations.d.ts.map +1 -1
  39. package/dist/operations.js +16 -16
  40. package/dist/operations.js.map +1 -1
  41. package/dist/print.d.ts +1 -1
  42. package/dist/print.d.ts.map +1 -1
  43. package/dist/print.js +4 -4
  44. package/dist/print.js.map +1 -1
  45. package/dist/suggestions.js +1 -1
  46. package/dist/suggestions.js.map +1 -1
  47. package/dist/tagSpec.d.ts +2 -2
  48. package/dist/tagSpec.d.ts.map +1 -1
  49. package/dist/tagSpec.js +10 -2
  50. package/dist/tagSpec.js.map +1 -1
  51. package/dist/utils.d.ts +16 -0
  52. package/dist/utils.d.ts.map +1 -1
  53. package/dist/utils.js +82 -1
  54. package/dist/utils.js.map +1 -1
  55. package/dist/validate.js.map +1 -1
  56. package/dist/values.d.ts +2 -1
  57. package/dist/values.d.ts.map +1 -1
  58. package/dist/values.js +29 -4
  59. package/dist/values.js.map +1 -1
  60. package/package.json +5 -6
  61. package/src/__tests__/definitions.test.ts +3 -3
  62. package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +432 -0
  63. package/src/__tests__/matchers/toMatchString.ts +2 -2
  64. package/src/__tests__/removeInaccessibleElements.test.ts +8 -8
  65. package/src/__tests__/subgraphValidation.test.ts +452 -0
  66. package/src/__tests__/utils.test.ts +92 -0
  67. package/src/buildSchema.ts +12 -11
  68. package/src/coreSpec.ts +12 -10
  69. package/src/debug.ts +8 -25
  70. package/src/definitions.ts +249 -115
  71. package/src/error.ts +334 -7
  72. package/src/extractSubgraphsFromSupergraph.ts +80 -19
  73. package/src/federation.ts +299 -138
  74. package/src/genErrorCodeDoc.ts +69 -0
  75. package/src/inaccessibleSpec.ts +13 -8
  76. package/src/joinSpec.ts +11 -8
  77. package/src/operations.ts +40 -38
  78. package/src/print.ts +8 -8
  79. package/src/suggestions.ts +1 -1
  80. package/src/tagSpec.ts +12 -7
  81. package/src/types.ts +1 -1
  82. package/src/utils.ts +109 -0
  83. package/src/validate.ts +4 -4
  84. package/src/values.ts +51 -9
  85. package/tsconfig.test.tsbuildinfo +1 -1
  86. package/tsconfig.tsbuildinfo +1 -1
package/src/federation.ts CHANGED
@@ -18,21 +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
- InputFieldDefinition
26
+ InputFieldDefinition,
27
+ isCompositeType
25
28
  } from "./definitions";
26
- import { assert } from "./utils";
29
+ import { assert, OrderedMap } from "./utils";
27
30
  import { SDLValidationRule } from "graphql/validation/ValidationContext";
28
31
  import { specifiedSDLRules } from "graphql/validation/specifiedRules";
29
- 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";
30
44
  import { defaultPrintOptions, printDirectiveDefinition } from "./print";
31
45
  import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFederationRule";
32
46
  import { buildSchema, buildSchemaFromAST } from "./buildSchema";
33
47
  import { parseSelectionSet, SelectionSet } from './operations';
34
48
  import { tagLocations, TAG_VERSIONS } from "./tagSpec";
35
- import { error } from "./error";
49
+ import {
50
+ errorCodeDef,
51
+ ErrorCodeDefinition,
52
+ ERRORS,
53
+ } from "./error";
54
+ import { ERROR_CATEGORIES } from ".";
36
55
 
37
56
  export const entityTypeName = '_Entity';
38
57
  export const serviceTypeName = '_Service';
@@ -44,16 +63,16 @@ export const extendsDirectiveName = 'extends';
44
63
  export const externalDirectiveName = 'external';
45
64
  export const requiresDirectiveName = 'requires';
46
65
  export const providesDirectiveName = 'provides';
47
- // TODO: so far, it seems we allow tag to appear without a corresponding definitio, so we add it as a built-in.
66
+ // TODO: so far, it seems we allow tag to appear without a corresponding definition, so we add it as a built-in.
48
67
  // If we change our mind, we should change this.
49
68
  export const tagDirectiveName = 'tag';
50
69
 
51
70
  export const serviceFieldName = '_service';
52
71
  export const entitiesFieldName = '_entities';
53
72
 
54
- const tagSpec = TAG_VERSIONS.latest()!;
73
+ const tagSpec = TAG_VERSIONS.latest();
55
74
 
56
- // We don't let user use this as a subgraph name. That allows us to use it in `query graphs` to name the source of roots
75
+ // We don't let user use this as a subgraph name. That allows us to use it in `query graphs` to name the source of roots
57
76
  // in the "federated query graph" without worrying about conflict (see `FEDERATED_GRAPH_ROOT_SOURCE` in `querygraph.ts`).
58
77
  // (note that we could deal with this in other ways, but having a graph named '_' feels like a terrible idea anyway, so
59
78
  // disallowing it feels like more a good thing than a real restriction).
@@ -106,26 +125,51 @@ function validateFieldSetSelections(
106
125
  for (const selection of selectionSet.selections()) {
107
126
  if (selection.kind === 'FieldSelection') {
108
127
  const field = selection.element().definition;
109
- if (field.hasArguments()) {
110
- throw new GraphQLError(`field ${field.coordinate} cannot be included because it has arguments (fields with argument are not allowed in @${directiveName})`, field.sourceAST);
111
- }
112
- // The field must be external if we don't allow non-external leaf fields, it's a leaft, and we haven't traversed an external field in parent chain leading here.
113
- const mustBeExternal = !selection.selectionSet && !allowOnNonExternalLeafFields && !hasExternalInParents;
114
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.
115
131
  if (isExternal) {
116
132
  externalFieldCoordinatesCollector.push(field.coordinate);
117
- } else if (mustBeExternal) {
133
+ }
134
+ if (field.hasArguments()) {
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
+ });
139
+ }
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.
141
+ const mustBeExternal = !selection.selectionSet && !allowOnNonExternalLeafFields && !hasExternalInParents;
142
+ if (!isExternal && mustBeExternal) {
143
+ const errorCode = ERROR_CATEGORIES.DIRECTIVE_FIELDS_MISSING_EXTERNAL.get(directiveName);
118
144
  if (externalTester.isFakeExternal(field)) {
119
- throw new GraphQLError(
120
- `field "${field.coordinate}" should not be part of a @${directiveName} since it is already "effectively" provided by this subgraph `
121
- + `(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)`,
122
- 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
+ });
123
150
  } else {
124
- 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
+ });
125
155
  }
126
156
  }
127
157
  if (selection.selectionSet) {
128
- validateFieldSetSelections(directiveName, selection.selectionSet, hasExternalInParents || isExternal, externalTester, externalFieldCoordinatesCollector, allowOnNonExternalLeafFields);
158
+ // When passing the 'hasExternalInParents', the field might be external himself, but we may also have
159
+ // the case where the field parent is an interface and some implementation of the field are external, in
160
+ // which case we should say we have an external on the path, because we may have one.
161
+ let newHasExternalInParents = hasExternalInParents || isExternal;
162
+ const parentType = field.parent;
163
+ if (!newHasExternalInParents && isInterfaceType(parentType)) {
164
+ for (const implem of parentType.possibleRuntimeTypes()) {
165
+ const fieldInImplem = implem.field(field.name);
166
+ if (fieldInImplem && externalTester.isExternal(fieldInImplem)) {
167
+ newHasExternalInParents = true;
168
+ break;
169
+ }
170
+ }
171
+ }
172
+ validateFieldSetSelections(directiveName, selection.selectionSet, newHasExternalInParents, externalTester, externalFieldCoordinatesCollector, allowOnNonExternalLeafFields);
129
173
  }
130
174
  } else {
131
175
  validateFieldSetSelections(directiveName, selection.selectionSet, hasExternalInParents, externalTester, externalFieldCoordinatesCollector, allowOnNonExternalLeafFields);
@@ -136,69 +180,97 @@ function validateFieldSetSelections(
136
180
  function validateFieldSet(
137
181
  type: CompositeType,
138
182
  directive: Directive<any, {fields: any}>,
139
- targetDescription: string,
140
183
  externalTester: ExternalTester,
141
184
  externalFieldCoordinatesCollector: string[],
142
185
  allowOnNonExternalLeafFields: boolean,
186
+ onFields?: (field: FieldDefinition<any>) => void,
143
187
  ): GraphQLError | undefined {
144
188
  try {
145
- const selectionSet = parseFieldSetArgument(type, directive);
146
- selectionSet.validate();
147
- validateFieldSetSelections(directive.name, selectionSet, false, externalTester, externalFieldCoordinatesCollector, allowOnNonExternalLeafFields);
148
- return undefined;
149
- } catch (e) {
150
- if (!(e instanceof GraphQLError)) {
151
- throw e;
152
- }
153
- const nodes = sourceASTs(directive);
154
- if (e.nodes) {
155
- nodes.push(...e.nodes);
156
- }
157
- let msg = e.message.trim();
158
- // The rule for validating @requires in fed 1 was not properly recursive, so people upgrading
159
- // may have a @require that selects some fields but without declaring those fields on the
160
- // subgraph. As we fixed the validation, this will now fail, but we try here to provide some
161
- // hint for those users for how to fix the problem.
162
- // Note that this is a tad fragile to rely on the error message like that, but worth case, a
163
- // future change make us not show the hint and that's not the end of the world.
164
- if (msg.startsWith('Cannot query field')) {
165
- if (msg.endsWith('.')) {
166
- 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;
167
197
  }
168
- if (directive.name === keyDirectiveName) {
169
- 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).';
170
- } else {
171
- 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);
172
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;
173
224
  }
174
- return new GraphQLError(`On ${targetDescription}, for ${directive}: ${msg}`, nodes);
175
225
  }
176
226
  }
177
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
+
178
244
  function validateAllFieldSet<TParent extends SchemaElement<any, any>>(
179
245
  definition: DirectiveDefinition<{fields: any}>,
180
246
  targetTypeExtractor: (element: TParent) => CompositeType,
181
- targetDescriptionExtractor: (element: TParent) => string,
182
247
  errorCollector: GraphQLError[],
183
248
  externalTester: ExternalTester,
184
249
  externalFieldCoordinatesCollector: string[],
185
250
  isOnParentType: boolean,
186
251
  allowOnNonExternalLeafFields: boolean,
252
+ onFields?: (field: FieldDefinition<any>) => void,
187
253
  ): void {
188
254
  for (const application of definition.applications()) {
189
- const elt = application.parent! as TParent;
255
+ const elt = application.parent as TParent;
190
256
  const type = targetTypeExtractor(elt);
191
- const targetDescription = targetDescriptionExtractor(elt);
192
257
  const parentType = isOnParentType ? type : (elt.parent as NamedType);
193
258
  if (isInterfaceType(parentType)) {
194
- errorCollector.push(new GraphQLError(
195
- isOnParentType
196
- ? `Cannot use ${definition.coordinate} on interface ${parentType.coordinate}: ${definition.coordinate} is not yet supported on interfaces`
197
- : `Cannot use ${definition.coordinate} on ${targetDescription} of parent type ${parentType}: ${definition.coordinate} is not yet supported within interfaces`,
198
- sourceASTs(application).concat(isOnParentType ? [] : sourceASTs(type))
199
- ));
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
+ }));
200
266
  }
201
- const error = validateFieldSet(type, application, targetDescription, externalTester, externalFieldCoordinatesCollector, allowOnNonExternalLeafFields);
267
+ const error = validateFieldSet(
268
+ type,
269
+ application,
270
+ externalTester,
271
+ externalFieldCoordinatesCollector,
272
+ allowOnNonExternalLeafFields,
273
+ onFields);
202
274
  if (error) {
203
275
  errorCollector.push(error);
204
276
  }
@@ -225,18 +297,18 @@ function validateAllExternalFieldsUsed(
225
297
  }
226
298
 
227
299
  if (!isFieldSatisfyingInterface(field)) {
228
- errorCollector.push(new GraphQLError(
229
- `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;`
230
302
  + ' the field declaration has no use and should be removed (or the field should not be @external).',
231
- field.sourceAST
232
- ));
303
+ nodes: field.sourceAST,
304
+ }));
233
305
  }
234
306
  }
235
307
  }
236
308
  }
237
309
 
238
310
  function isFieldSatisfyingInterface(field: FieldDefinition<ObjectType | InterfaceType>): boolean {
239
- return field.parent!.interfaces().some(itf => itf.field(field.name));
311
+ return field.parent.interfaces().some(itf => itf.field(field.name));
240
312
  }
241
313
 
242
314
  export class FederationBuiltIns extends BuiltIns {
@@ -255,25 +327,25 @@ export class FederationBuiltIns extends BuiltIns {
255
327
  const fieldSetType = new NonNullType(schema.type(fieldSetTypeName)!);
256
328
 
257
329
  // 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
258
- // @key is actually not suppported on interfaces at the moment, so if if is "used" then it is rejected.
330
+ // @key is actually not supported on interfaces at the moment, so if if is "used" then it is rejected.
259
331
  const keyDirective = this.addBuiltInDirective(schema, keyDirectiveName)
260
- .addLocations('OBJECT', 'INTERFACE');
332
+ .addLocations(DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE);
261
333
  // TODO: I believe fed 1 does not mark key repeatable and relax validation to accept repeating non-repeatable directive.
262
334
  // Do we want to perpetuate this? (Obviously, this is for historical reason and some graphQL implementations still do
263
- // not support 'repeatable'. But since this code does not kick in within users' code, not sure we have to accomodate
335
+ // not support 'repeatable'. But since this code does not kick in within users' code, not sure we have to accommodate
264
336
  // for those implementations. Besides, we _do_ accept if people re-defined @key as non-repeatable).
265
337
  keyDirective.repeatable = true;
266
338
  keyDirective.addArgument('fields', fieldSetType);
267
339
 
268
340
  this.addBuiltInDirective(schema, extendsDirectiveName)
269
- .addLocations('OBJECT', 'INTERFACE');
341
+ .addLocations(DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE);
270
342
 
271
343
  this.addBuiltInDirective(schema, externalDirectiveName)
272
- .addLocations('OBJECT', 'FIELD_DEFINITION');
344
+ .addLocations(DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION);
273
345
 
274
346
  for (const name of [requiresDirectiveName, providesDirectiveName]) {
275
347
  this.addBuiltInDirective(schema, name)
276
- .addLocations('FIELD_DEFINITION')
348
+ .addLocations(DirectiveLocation.FIELD_DEFINITION)
277
349
  .addArgument('fields', fieldSetType);
278
350
  }
279
351
 
@@ -312,7 +384,7 @@ export class FederationBuiltIns extends BuiltIns {
312
384
  // Adds the _entities and _service fields to the root query type.
313
385
  const queryRoot = schema.schemaDefinition.root("query");
314
386
  const queryType = queryRoot ? queryRoot.type : schema.addType(new ObjectType("Query"));
315
- let entityField = queryType.field(entitiesFieldName);
387
+ const entityField = queryType.field(entitiesFieldName);
316
388
  if (hasEntities) {
317
389
  const anyType = schema.type(anyTypeName);
318
390
  assert(anyType, `The schema should have the _Any type`);
@@ -348,13 +420,12 @@ export class FederationBuiltIns extends BuiltIns {
348
420
  // composition error.
349
421
  const existing = schema.type(defaultName);
350
422
  if (existing) {
351
- errors.push(error(
352
- `ROOT_${k.toUpperCase()}_USED`,
353
- `The schema has a type named "${defaultName}" but it is not set as the ${k} root type ("${type.name}" is instead): `
354
- + 'this is not supported by federation. '
355
- + 'If a root type does not use its default name, there should be no other type with that default name.',
356
- sourceASTs(type, existing)
357
- ));
423
+ errors.push(ERROR_CATEGORIES.ROOT_TYPE_USED.get(k).err({
424
+ message: `The schema has a type named "${defaultName}" but it is not set as the ${k} root type ("${type.name}" is instead): `
425
+ + 'this is not supported by federation. '
426
+ + 'If a root type does not use its default name, there should be no other type with that default name.',
427
+ nodes: sourceASTs(type, existing),
428
+ }));
358
429
  }
359
430
  type.rename(defaultName);
360
431
  }
@@ -368,24 +439,31 @@ export class FederationBuiltIns extends BuiltIns {
368
439
  validateAllFieldSet<CompositeType>(
369
440
  keyDirective,
370
441
  type => type,
371
- type => `type "${type}"`,
372
442
  errors,
373
443
  externalTester,
374
444
  externalFieldsInFedDirectivesCoordinates,
375
445
  true,
376
- true
446
+ true,
447
+ field => {
448
+ if (isListType(field.type!) || isUnionType(field.type!) || isInterfaceType(field.type!)) {
449
+ let kind: string = field.type!.kind;
450
+ kind = kind.slice(0, kind.length - 'Type'.length);
451
+ throw ERRORS.KEY_FIELDS_SELECT_INVALID_TYPE.err({
452
+ message: `field "${field.coordinate}" is a ${kind} type which is not allowed in @key`
453
+ });
454
+ }
455
+ }
377
456
  );
378
457
  // Note that we currently reject @requires where a leaf field of the selection is not external,
379
458
  // because if it's provided by the current subgraph, why "requires" it? That said, it's not 100%
380
- // non-sensical if you wanted a local field to be part of the subgraph fetch even if it's not
459
+ // nonsensical if you wanted a local field to be part of the subgraph fetch even if it's not
381
460
  // truly queried _for some reason_. But it's unclear such reasons exists, so for now we prefer
382
461
  // rejecting it as it also make it less likely user misunderstand what @requires actually do.
383
462
  // But we could consider lifting that limitation if users comes with a good rational for allowing
384
463
  // it.
385
464
  validateAllFieldSet<FieldDefinition<CompositeType>>(
386
465
  this.requiresDirective(schema),
387
- field => field.parent!,
388
- field => `field "${field.coordinate}"`,
466
+ field => field.parent,
389
467
  errors,
390
468
  externalTester,
391
469
  externalFieldsInFedDirectivesCoordinates,
@@ -394,20 +472,23 @@ export class FederationBuiltIns extends BuiltIns {
394
472
  );
395
473
  // Note that like for @requires above, we error out if a leaf field of the selection is not
396
474
  // external in a @provides (we pass `false` for the `allowOnNonExternalLeafFields` parameter),
397
- // but contrarily to @requires, there is probaly no reason to ever change this, as a @provides
398
- // of a field already provides is 100% non-sensical.
475
+ // but contrarily to @requires, there is probably no reason to ever change this, as a @provides
476
+ // of a field already provides is 100% nonsensical.
399
477
  validateAllFieldSet<FieldDefinition<CompositeType>>(
400
478
  this.providesDirective(schema),
401
479
  field => {
480
+ if (externalTester.isExternal(field)) {
481
+ throw new GraphQLError(`Cannot have both @provides and @external on field "${field.coordinate}"`, field.sourceAST);
482
+ }
402
483
  const type = baseType(field.type!);
403
- if (!isObjectType(type)) {
404
- throw new GraphQLError(
405
- `Invalid @provides directive on field "${field.coordinate}": field has type "${field.type}" which is not an Object Type`,
406
- field.sourceAST);
484
+ if (!isCompositeType(type)) {
485
+ throw ERRORS.PROVIDES_ON_NON_OBJECT_FIELD.err({
486
+ message: `Invalid @provides directive on field "${field.coordinate}": field has type "${field.type}" which is not a Composite Type`,
487
+ nodes: field.sourceAST,
488
+ });
407
489
  }
408
490
  return type;
409
491
  },
410
- field => `field ${field.coordinate}`,
411
492
  errors,
412
493
  externalTester,
413
494
  externalFieldsInFedDirectivesCoordinates,
@@ -437,11 +518,11 @@ export class FederationBuiltIns extends BuiltIns {
437
518
  return this.getTypedDirective(schema, keyDirectiveName);
438
519
  }
439
520
 
440
- extendsDirective(schema: Schema): DirectiveDefinition<{}> {
521
+ extendsDirective(schema: Schema): DirectiveDefinition<Record<string, never>> {
441
522
  return this.getTypedDirective(schema, extendsDirectiveName);
442
523
  }
443
524
 
444
- externalDirective(schema: Schema): DirectiveDefinition<{}> {
525
+ externalDirective(schema: Schema): DirectiveDefinition<Record<string, never>> {
445
526
  return this.getTypedDirective(schema, externalDirectiveName);
446
527
  }
447
528
 
@@ -460,7 +541,7 @@ export class FederationBuiltIns extends BuiltIns {
460
541
  maybeUpdateSubgraphDocument(schema: Schema, document: DocumentNode): DocumentNode {
461
542
  document = super.maybeUpdateSubgraphDocument(schema, document);
462
543
 
463
- let definitions = document.definitions.concat();
544
+ const definitions = document.definitions.concat();
464
545
  for (const directiveName of FEDERATION_DIRECTIVES) {
465
546
  const directive = schema.directive(directiveName);
466
547
  assert(directive, 'This method should only have been called on a schema with federation built-ins')
@@ -473,7 +554,7 @@ export class FederationBuiltIns extends BuiltIns {
473
554
  }
474
555
 
475
556
  return {
476
- kind: 'Document',
557
+ kind: Kind.DOCUMENT,
477
558
  loc: document.loc,
478
559
  definitions
479
560
  };
@@ -495,7 +576,7 @@ export function isFederationTypeName(typeName: string): boolean {
495
576
  }
496
577
 
497
578
  export function isFederationField(field: FieldDefinition<CompositeType>): boolean {
498
- if (field.parent === field.schema()!.schemaDefinition.root("query")?.type) {
579
+ if (field.parent === field.schema().schemaDefinition.root("query")?.type) {
499
580
  return FEDERATION_ROOT_FIELDS.includes(field.name);
500
581
  }
501
582
  return false;
@@ -509,14 +590,17 @@ export function isEntityType(type: NamedType): boolean {
509
590
  return type.kind == "ObjectType" && type.hasAppliedDirective(keyDirectiveName);
510
591
  }
511
592
 
512
- function buildSubgraph(name: string, source: DocumentNode | string): Schema {
593
+ export function buildSubgraph(name: string, source: DocumentNode | string): Schema {
513
594
  try {
514
595
  return typeof source === 'string'
515
596
  ? buildSchema(new Source(source, name), federationBuiltIns)
516
597
  : buildSchemaFromAST(source, federationBuiltIns);
517
598
  } catch (e) {
518
599
  if (e instanceof GraphQLError) {
519
- throw addSubgraphToError(e, name);
600
+ // Note that `addSubgraphToError` only adds the provided code if the original error
601
+ // didn't have one, and the only one that will not have a code are GraphQL errors
602
+ // (since we assign specific codes to the federation errors).
603
+ throw addSubgraphToError(e, name, ERRORS.INVALID_GRAPHQL);
520
604
  } else {
521
605
  throw e;
522
606
  }
@@ -528,22 +612,75 @@ export function parseFieldSetArgument(
528
612
  directive: Directive<NamedType | FieldDefinition<CompositeType>, {fields: any}>,
529
613
  fieldAccessor: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined = (type, name) => type.field(name)
530
614
  ): SelectionSet {
531
- return parseSelectionSet(parentType, validateFieldSetValue(directive), new VariableDefinitions(), undefined, fieldAccessor);
615
+ try {
616
+ const selectionSet = parseSelectionSet(parentType, validateFieldSetValue(directive), new VariableDefinitions(), undefined, fieldAccessor);
617
+ selectionSet.validate();
618
+ return selectionSet;
619
+ } catch (e) {
620
+ if (!(e instanceof GraphQLError)) {
621
+ throw e;
622
+ }
623
+
624
+ const nodes = sourceASTs(directive);
625
+ if (e.nodes) {
626
+ nodes.push(...e.nodes);
627
+ }
628
+ let msg = e.message.trim();
629
+ // The rule for validating @requires in fed 1 was not properly recursive, so people upgrading
630
+ // may have a @require that selects some fields but without declaring those fields on the
631
+ // subgraph. As we fixed the validation, this will now fail, but we try here to provide some
632
+ // hint for those users for how to fix the problem.
633
+ // Note that this is a tad fragile to rely on the error message like that, but worth case, a
634
+ // future change make us not show the hint and that's not the end of the world.
635
+ if (msg.startsWith('Cannot query field')) {
636
+ if (msg.endsWith('.')) {
637
+ msg = msg.slice(0, msg.length - 1);
638
+ }
639
+ if (directive.name === keyDirectiveName) {
640
+ 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).';
641
+ } else {
642
+ msg = msg + ' (if the field is defined in another subgraph, you need to add it to this subgraph with @external).';
643
+ }
644
+ }
645
+
646
+ const codeDef = errorCodeDef(e) ?? ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS.get(directive.name);
647
+ throw codeDef.err({
648
+ message: `${fieldSetErrorDescriptor(directive)}: ${msg}`,
649
+ nodes,
650
+ originalError: e,
651
+ });
652
+ }
532
653
  }
533
654
 
534
655
  function validateFieldSetValue(directive: Directive<NamedType | FieldDefinition<CompositeType>, {fields: any}>): string {
535
656
  const fields = directive.arguments().fields;
657
+ const nodes = directive.sourceAST;
536
658
  if (typeof fields !== 'string') {
537
- throw new GraphQLError(
538
- `Invalid value for argument ${directive.definition!.argument('fields')!.coordinate} on ${directive.parent!.coordinate}: must be a string.`,
539
- directive.sourceAST
540
- );
659
+ throw ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err({
660
+ message: `Invalid value for argument "${directive.definition!.argument('fields')!.name}": must be a string.`,
661
+ nodes,
662
+ });
663
+ }
664
+ // While validating if the field is a string will work in most cases, this will not catch the case where the field argument was
665
+ // unquoted but parsed as an enum value (see federation/issues/850 in particular). So if we have the AST (which we will usually
666
+ // have in practice), use that to check that the argument was truly a string.
667
+ if (nodes && nodes.kind === 'Directive') {
668
+ for (const argNode of nodes.arguments ?? []) {
669
+ if (argNode.name.value === 'fields') {
670
+ if (argNode.value.kind !== 'StringValue') {
671
+ throw ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err({
672
+ message: `Invalid value for argument "${directive.definition!.argument('fields')!.name}": must be a string.`,
673
+ nodes,
674
+ });
675
+ }
676
+ break;
677
+ }
678
+ }
541
679
  }
680
+
542
681
  return fields;
543
682
  }
544
683
 
545
- // 'ServiceDefinition' is originally defined in federation-js and we don't want to create a dependency
546
- // of internals-js to that just for that interface.
547
684
  export interface ServiceDefinition {
548
685
  typeDefs: DocumentNode;
549
686
  name: string;
@@ -568,16 +705,11 @@ export function subgraphsFromServiceList(serviceList: ServiceDefinition[]): Subg
568
705
  return errors.length === 0 ? subgraphs : errors;
569
706
  }
570
707
 
571
- // Simple wrapper around a Subraph[] that ensures that 1) we never mistakenly get 2 subgraph with the same name,
708
+ // Simple wrapper around a Subgraph[] that ensures that 1) we never mistakenly get 2 subgraph with the same name,
572
709
  // 2) keep the subgraphs sorted by name (makes iteration more predictable). It also allow convenient access to
573
710
  // a subgraph by name so behave like a map<string, Subgraph> in most ways (but with the previously mentioned benefits).
574
711
  export class Subgraphs {
575
- private readonly subgraphs: Subgraph[] = [];
576
-
577
- private idx(name: string): number {
578
- // Note: we could do a binary search if we ever worry that a linear scan is too costly.
579
- return this.subgraphs.findIndex(s => s.name === name);
580
- }
712
+ private readonly subgraphs = new OrderedMap<string, Subgraph>();
581
713
 
582
714
  add(subgraph: Subgraph): Subgraph;
583
715
  add(name: string, url: string, schema: Schema | DocumentNode | string): Subgraph;
@@ -587,51 +719,46 @@ export class Subgraphs {
587
719
  : subgraphOrName;
588
720
 
589
721
  if (toAdd.name === FEDERATION_RESERVED_SUBGRAPH_NAME) {
590
- throw new GraphQLError(`Invalid name ${FEDERATION_RESERVED_SUBGRAPH_NAME} for a subgraph: this name is reserved`);
722
+ throw ERRORS.INVALID_SUBGRAPH_NAME.err({ message: `Invalid name ${FEDERATION_RESERVED_SUBGRAPH_NAME} for a subgraph: this name is reserved` });
591
723
  }
592
724
 
593
- const idx = this.idx(toAdd.name);
594
- if (idx >= 0) {
725
+ if (this.subgraphs.has(toAdd.name)) {
595
726
  throw new Error(`A subgraph named ${toAdd.name} already exists` + (toAdd.url ? ` (with url '${toAdd.url}')` : ''));
596
727
  }
597
- this.subgraphs.push(toAdd);
598
- this.subgraphs.sort();
728
+ this.subgraphs.add(toAdd.name, toAdd);
599
729
  return toAdd;
600
730
  }
601
731
 
602
732
  get(name: string): Subgraph | undefined {
603
- const idx = this.idx(name);
604
- return idx >= 0 ? this.subgraphs[idx] : undefined;
605
- }
606
-
607
- getByIdx(idx: number): Subgraph {
608
- return this.subgraphs[idx];
733
+ return this.subgraphs.get(name);
609
734
  }
610
735
 
611
736
  size(): number {
612
- return this.subgraphs.length;
737
+ return this.subgraphs.size;
613
738
  }
614
739
 
615
740
  names(): readonly string[] {
616
- return this.subgraphs.map(s => s.name);
741
+ return this.subgraphs.keys();
617
742
  }
618
743
 
619
744
  values(): readonly Subgraph[] {
620
- return this.subgraphs;
745
+ return this.subgraphs.values();
621
746
  }
622
747
 
623
- [Symbol.iterator]() {
624
- return this.subgraphs.values();
748
+ *[Symbol.iterator]() {
749
+ for (const subgraph of this.subgraphs) {
750
+ yield subgraph;
751
+ }
625
752
  }
626
753
 
627
754
  toString(): string {
628
- return '[' + this.subgraphs.map(s => s.name).join(', ') + ']'
755
+ return '[' + this.subgraphs.keys().join(', ') + ']'
629
756
  }
630
757
  }
631
758
 
632
759
  export class Subgraph {
633
760
  constructor(
634
- readonly name: string,
761
+ readonly name: string,
635
762
  readonly url: string,
636
763
  readonly schema: Schema,
637
764
  validateSchema: boolean = true
@@ -655,16 +782,36 @@ export function addSubgraphToASTNode(node: ASTNode, subgraph: string): SubgraphA
655
782
  };
656
783
  }
657
784
 
658
- export function addSubgraphToError(e: GraphQLError, subgraphName: string): GraphQLError {
659
- const updatedCauses = errorCauses(e)!.map(cause => new GraphQLError(
660
- `[${subgraphName}] ${cause.message}`,
661
- cause.nodes ? cause.nodes.map(node => addSubgraphToASTNode(node, subgraphName)) : undefined,
662
- cause.source,
663
- cause.positions,
664
- cause.path,
665
- cause.originalError,
666
- cause.extensions
667
- ));
785
+ export function addSubgraphToError(e: GraphQLError, subgraphName: string, errorCode?: ErrorCodeDefinition): GraphQLError {
786
+ const updatedCauses = errorCauses(e)!.map(cause => {
787
+ const message = `[${subgraphName}] ${cause.message}`;
788
+ const nodes = cause.nodes
789
+ ? cause.nodes.map(node => addSubgraphToASTNode(node, subgraphName))
790
+ : undefined;
791
+
792
+ const code = errorCodeDef(cause) ?? errorCode;
793
+ if (code) {
794
+ return code.err({
795
+ message,
796
+ nodes,
797
+ source: cause.source,
798
+ positions: cause.positions,
799
+ path: cause.path,
800
+ originalError: cause.originalError,
801
+ extensions: cause.extensions,
802
+ });
803
+ } else {
804
+ return new GraphQLError(
805
+ message,
806
+ nodes,
807
+ cause.source,
808
+ cause.positions,
809
+ cause.path,
810
+ cause.originalError,
811
+ cause.extensions
812
+ );
813
+ }
814
+ });
668
815
 
669
816
  return ErrGraphQLValidationFailed(updatedCauses);
670
817
  }
@@ -682,7 +829,7 @@ export class ExternalTester {
682
829
  return;
683
830
  }
684
831
  for (const key of keyDirective.applications()) {
685
- const parent = key.parent! as CompositeType;
832
+ const parent = key.parent as CompositeType;
686
833
  if (!(key.ofExtension() || parent.hasAppliedDirective(extendsDirectiveName))) {
687
834
  continue;
688
835
  }
@@ -708,4 +855,18 @@ export class ExternalTester {
708
855
  isFakeExternal(field: FieldDefinition<any> | InputFieldDefinition) {
709
856
  return this.fakeExternalFields.has(field.coordinate);
710
857
  }
858
+
859
+ selectsAnyExternalField(selectionSet: SelectionSet): boolean {
860
+ for (const selection of selectionSet.selections()) {
861
+ if (selection.kind === 'FieldSelection' && this.isExternal(selection.element().definition)) {
862
+ return true;
863
+ }
864
+ if (selection.selectionSet) {
865
+ if (this.selectsAnyExternalField(selection.selectionSet)) {
866
+ return true;
867
+ }
868
+ }
869
+ }
870
+ return false;
871
+ }
711
872
  }