@apollo/federation-internals 2.0.0-preview.8 → 2.0.0-preview.9

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 (91) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/buildSchema.js +1 -1
  3. package/dist/buildSchema.js.map +1 -1
  4. package/dist/coreSpec.d.ts +11 -5
  5. package/dist/coreSpec.d.ts.map +1 -1
  6. package/dist/coreSpec.js +68 -27
  7. package/dist/coreSpec.js.map +1 -1
  8. package/dist/definitions.d.ts +7 -5
  9. package/dist/definitions.d.ts.map +1 -1
  10. package/dist/definitions.js +26 -13
  11. package/dist/definitions.js.map +1 -1
  12. package/dist/directiveAndTypeSpecification.d.ts +11 -1
  13. package/dist/directiveAndTypeSpecification.d.ts.map +1 -1
  14. package/dist/directiveAndTypeSpecification.js +67 -19
  15. package/dist/directiveAndTypeSpecification.js.map +1 -1
  16. package/dist/error.d.ts +3 -0
  17. package/dist/error.d.ts.map +1 -1
  18. package/dist/error.js +6 -0
  19. package/dist/error.js.map +1 -1
  20. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  21. package/dist/extractSubgraphsFromSupergraph.js +6 -0
  22. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  23. package/dist/federation.d.ts +15 -5
  24. package/dist/federation.d.ts.map +1 -1
  25. package/dist/federation.js +77 -57
  26. package/dist/federation.js.map +1 -1
  27. package/dist/federationSpec.d.ts +3 -3
  28. package/dist/federationSpec.d.ts.map +1 -1
  29. package/dist/federationSpec.js +34 -26
  30. package/dist/federationSpec.js.map +1 -1
  31. package/dist/inaccessibleSpec.d.ts +3 -2
  32. package/dist/inaccessibleSpec.d.ts.map +1 -1
  33. package/dist/inaccessibleSpec.js +13 -4
  34. package/dist/inaccessibleSpec.js.map +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/introspection.d.ts.map +1 -1
  40. package/dist/introspection.js +8 -3
  41. package/dist/introspection.js.map +1 -1
  42. package/dist/joinSpec.d.ts +4 -1
  43. package/dist/joinSpec.d.ts.map +1 -1
  44. package/dist/joinSpec.js +3 -0
  45. package/dist/joinSpec.js.map +1 -1
  46. package/dist/operations.d.ts +1 -0
  47. package/dist/operations.d.ts.map +1 -1
  48. package/dist/operations.js +16 -1
  49. package/dist/operations.js.map +1 -1
  50. package/dist/{sharing.d.ts → precompute.d.ts} +1 -1
  51. package/dist/precompute.d.ts.map +1 -0
  52. package/dist/{sharing.js → precompute.js} +1 -1
  53. package/dist/precompute.js.map +1 -0
  54. package/dist/schemaUpgrader.d.ts.map +1 -1
  55. package/dist/schemaUpgrader.js +1 -2
  56. package/dist/schemaUpgrader.js.map +1 -1
  57. package/dist/suggestions.d.ts +1 -1
  58. package/dist/suggestions.d.ts.map +1 -1
  59. package/dist/suggestions.js.map +1 -1
  60. package/dist/supergraphs.d.ts.map +1 -1
  61. package/dist/supergraphs.js +1 -0
  62. package/dist/supergraphs.js.map +1 -1
  63. package/dist/tagSpec.d.ts +6 -2
  64. package/dist/tagSpec.d.ts.map +1 -1
  65. package/dist/tagSpec.js +30 -14
  66. package/dist/tagSpec.js.map +1 -1
  67. package/package.json +3 -3
  68. package/src/__tests__/removeInaccessibleElements.test.ts +59 -6
  69. package/src/__tests__/subgraphValidation.test.ts +339 -0
  70. package/src/buildSchema.ts +1 -1
  71. package/src/coreSpec.ts +88 -33
  72. package/src/definitions.ts +29 -14
  73. package/src/directiveAndTypeSpecification.ts +80 -20
  74. package/src/error.ts +18 -0
  75. package/src/extractSubgraphsFromSupergraph.ts +6 -0
  76. package/src/federation.ts +89 -72
  77. package/src/federationSpec.ts +36 -29
  78. package/src/inaccessibleSpec.ts +16 -4
  79. package/src/index.ts +1 -0
  80. package/src/introspection.ts +8 -3
  81. package/src/joinSpec.ts +14 -3
  82. package/src/operations.ts +15 -0
  83. package/src/{sharing.ts → precompute.ts} +1 -2
  84. package/src/schemaUpgrader.ts +4 -7
  85. package/src/suggestions.ts +1 -1
  86. package/src/supergraphs.ts +1 -0
  87. package/src/tagSpec.ts +42 -16
  88. package/tsconfig.test.tsbuildinfo +1 -1
  89. package/tsconfig.tsbuildinfo +1 -1
  90. package/dist/sharing.d.ts.map +0 -1
  91. package/dist/sharing.js.map +0 -1
@@ -1,8 +1,10 @@
1
- import { DirectiveLocation, GraphQLError } from "graphql";
1
+ import { ASTNode, DirectiveLocation, GraphQLError } from "graphql";
2
2
  import {
3
3
  ArgumentDefinition,
4
4
  DirectiveDefinition,
5
+ EnumType,
5
6
  InputType,
7
+ isEnumType,
6
8
  isNonNullType,
7
9
  isObjectType,
8
10
  isUnionType,
@@ -49,13 +51,16 @@ export function createDirectiveSpecification({
49
51
  name: string,
50
52
  locations: DirectiveLocation[],
51
53
  repeatable?: boolean,
52
- argumentFct?: (schema: Schema) => ArgumentSpecification[],
54
+ argumentFct?: (schema: Schema, nameInSchema?: string) => { args: ArgumentSpecification[], errors: GraphQLError[] },
53
55
  }): DirectiveSpecification {
54
56
  return {
55
57
  name,
56
58
  checkOrAdd: (schema: Schema, nameInSchema?: string, asBuiltIn?: boolean) => {
57
- const args = argumentFct ? argumentFct(schema) : [];
58
59
  const actualName = nameInSchema ?? name;
60
+ const {args, errors} = argumentFct ? argumentFct(schema, actualName) : { args: [], errors: []};
61
+ if (errors.length > 0) {
62
+ return errors;
63
+ }
59
64
  const existing = schema.directive(actualName);
60
65
  if (existing) {
61
66
  return ensureSameDirectiveStructure({name: actualName, locations, repeatable, args}, existing);
@@ -131,7 +136,7 @@ export function createObjectTypeSpecification({
131
136
  errors = errors.concat(ensureSameArguments(
132
137
  { name, args },
133
138
  existingField,
134
- `field ${existingField.coordinate}`,
139
+ `field "${existingField.coordinate}"`,
135
140
  ));
136
141
  }
137
142
  return errors;
@@ -198,6 +203,44 @@ export function createUnionTypeSpecification({
198
203
  }
199
204
  }
200
205
 
206
+ export function createEnumTypeSpecification({
207
+ name,
208
+ values,
209
+ }: {
210
+ name: string,
211
+ values: { name: string, description?: string}[],
212
+ }): TypeSpecification {
213
+ return {
214
+ name,
215
+ checkOrAdd: (schema: Schema, nameInSchema?: string, asBuiltIn?: boolean) => {
216
+ const actualName = nameInSchema ?? name;
217
+ const existing = schema.type(actualName);
218
+ const expectedValueNames = values.map((v) => v.name).sort((n1, n2) => n1.localeCompare(n2));
219
+ if (existing) {
220
+ let errors = ensureSameTypeKind('EnumType', existing);
221
+ if (errors.length > 0) {
222
+ return errors;
223
+ }
224
+ assert(isEnumType(existing), 'Should be an enum type');
225
+ const actualValueNames = existing.values.map(v => v.name).sort((n1, n2) => n1.localeCompare(n2));
226
+ if (!arrayEquals(expectedValueNames, actualValueNames)) {
227
+ errors = errors.concat(ERRORS.TYPE_DEFINITION_INVALID.err({
228
+ message: `Invalid definition of type ${name}: expected values [${expectedValueNames}] but found [${actualValueNames}].`,
229
+ nodes: existing.sourceAST
230
+ }));
231
+ }
232
+ return errors;
233
+ } else {
234
+ const type = schema.addType(new EnumType(actualName, asBuiltIn));
235
+ for (const {name, description} of values) {
236
+ type.addValue(name).description = description;
237
+ }
238
+ return [];
239
+ }
240
+ },
241
+ }
242
+ }
243
+
201
244
  function ensureSameTypeKind(expected: NamedType['kind'], actual: NamedType): GraphQLError[] {
202
245
  return expected === actual.kind
203
246
  ? []
@@ -216,18 +259,19 @@ function ensureSameDirectiveStructure(
216
259
  },
217
260
  actual: DirectiveDefinition<any>,
218
261
  ): GraphQLError[] {
219
- let errors = ensureSameArguments(expected, actual, `directive ${expected}`);
262
+ const directiveName = `"@${expected.name}"`
263
+ let errors = ensureSameArguments(expected, actual, `directive ${directiveName}`);
220
264
  // It's ok to say you'll never repeat a repeatable directive. It's not ok to repeat one that isn't.
221
265
  if (!expected.repeatable && actual.repeatable) {
222
266
  errors = errors.concat(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
223
- message: `Invalid definition for directive ${expected}: ${expected} should${expected.repeatable ? "" : " not"} be repeatable`,
267
+ message: `Invalid definition for directive ${directiveName}: ${directiveName} should${expected.repeatable ? "" : " not"} be repeatable`,
224
268
  nodes: actual.sourceAST
225
269
  }));
226
270
  }
227
271
  // Similarly, it's ok to say that you will never use a directive in some locations, but not that you will use it in places not allowed by what is expected.
228
272
  if (!actual.locations.every(loc => expected.locations.includes(loc))) {
229
273
  errors = errors.concat(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
230
- message: `Invalid efinition for directive ${expected}: ${expected} should have locations ${expected.locations.join(', ')}, but found (non-subset) ${actual.locations.join(', ')}`,
274
+ message: `Invalid definition for directive ${directiveName}: ${directiveName} should have locations ${expected.locations.join(', ')}, but found (non-subset) ${actual.locations.join(', ')}`,
231
275
  nodes: actual.sourceAST
232
276
  }));
233
277
  }
@@ -241,17 +285,24 @@ function ensureSameArguments(
241
285
  },
242
286
  actual: { argument(name: string): ArgumentDefinition<any> | undefined, arguments(): readonly ArgumentDefinition<any>[] },
243
287
  what: string,
288
+ containerSourceAST?: ASTNode,
244
289
  ): GraphQLError[] {
245
290
  const expectedArguments = expected.args ?? [];
246
- const foundArguments = actual.arguments();
247
- if (expectedArguments.length !== foundArguments.length) {
248
- return [ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
249
- message: `Invalid definition for ${what}: should have ${expectedArguments.length} arguments but ${foundArguments.length} found`,
250
- })];
251
- }
252
- let errors: GraphQLError[] = [];
291
+ const errors: GraphQLError[] = [];
253
292
  for (const { name, type, defaultValue } of expectedArguments) {
254
- const actualArgument = actual.argument(name)!;
293
+ const actualArgument = actual.argument(name);
294
+ if (!actualArgument) {
295
+ // Not declaring an optional argument is ok: that means you won't be able to pass a non-default value in your schema, but we allow you that.
296
+ // But missing a required argument it not ok.
297
+ if (isNonNullType(type) && defaultValue === undefined) {
298
+ errors.push(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
299
+ message: `Invalid definition for ${what}: missing required argument "${name}"`,
300
+ nodes: containerSourceAST
301
+ }));
302
+ }
303
+ continue;
304
+ }
305
+
255
306
  let actualType = actualArgument.type!;
256
307
  if (isNonNullType(actualType) && !isNonNullType(type)) {
257
308
  // It's ok to redefine an optional argument as mandatory. For instance, if you want to force people on your team to provide a "deprecation reason", you can
@@ -260,13 +311,22 @@ function ensureSameArguments(
260
311
  actualType = actualType.ofType;
261
312
  }
262
313
  if (!sameType(type, actualType)) {
263
- errors = errors.concat(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
264
- message: `Invalid definition of ${what}: ${name} should have type ${type} but found type ${actualArgument.type!}`,
314
+ errors.push(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
315
+ message: `Invalid definition for ${what}: argument "${name}" should have type "${type}" but found type "${actualArgument.type!}"`,
265
316
  nodes: actualArgument.sourceAST
266
317
  }));
267
- } else if (!isNonNullType(actualType) && !valueEquals(defaultValue, actualArgument.defaultValue)) {
268
- errors = errors.concat(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
269
- message: `Invalid definition of ${what}: ${name} should have default value ${valueToString(defaultValue)} but found default value ${valueToString(actualArgument.defaultValue)}`,
318
+ } else if (!isNonNullType(actualArgument.type!) && !valueEquals(defaultValue, actualArgument.defaultValue)) {
319
+ errors.push(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
320
+ message: `Invalid definition for ${what}: argument "${name}" should have default value ${valueToString(defaultValue)} but found default value ${valueToString(actualArgument.defaultValue)}`,
321
+ nodes: actualArgument.sourceAST
322
+ }));
323
+ }
324
+ }
325
+ for (const actualArgument of actual.arguments()) {
326
+ // If it's an expect argument, we already validated it. But we still need to reject unkown argument.
327
+ if (!expectedArguments.some((arg) => arg.name === actualArgument.name)) {
328
+ errors.push(ERRORS.DIRECTIVE_DEFINITION_INVALID.err({
329
+ message: `Invalid definition for ${what}: unknown/unsupported argument "${actualArgument.name}"`,
270
330
  nodes: actualArgument.sourceAST
271
331
  }));
272
332
  }
package/src/error.ts CHANGED
@@ -359,6 +359,21 @@ const SATISFIABILITY_ERROR = makeCodeDefinition(
359
359
  'Subgraphs can be merged, but the resulting supergraph API would have queries that cannot be satisfied by those subgraphs.',
360
360
  );
361
361
 
362
+ const OVERRIDE_FROM_SELF_ERROR = makeCodeDefinition(
363
+ 'OVERRIDE_FROM_SELF_ERROR',
364
+ 'Field with `@override` directive has "from" location that references its own subgraph.',
365
+ );
366
+
367
+ const OVERRIDE_SOURCE_HAS_OVERRIDE = makeCodeDefinition(
368
+ 'OVERRIDE_SOURCE_HAS_OVERRIDE',
369
+ 'Field which is overridden to another subgraph is also marked @override.',
370
+ );
371
+
372
+ const OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE = makeCodeDefinition(
373
+ 'OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE',
374
+ 'The @override directive cannot be used on external fields, nor to override fields with either @external, @provides, or @requires.',
375
+ );
376
+
362
377
  export const ERROR_CATEGORIES = {
363
378
  DIRECTIVE_FIELDS_MISSING_EXTERNAL,
364
379
  DIRECTIVE_UNSUPPORTED_ON_INTERFACE,
@@ -418,6 +433,9 @@ export const ERRORS = {
418
433
  REFERENCED_INACCESSIBLE,
419
434
  REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH,
420
435
  SATISFIABILITY_ERROR,
436
+ OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE,
437
+ OVERRIDE_FROM_SELF_ERROR,
438
+ OVERRIDE_SOURCE_HAS_OVERRIDE,
421
439
  };
422
440
 
423
441
  const codeDefByCode = Object.values(ERRORS).reduce((obj: {[code: string]: ErrorCodeDefinition}, codeDef: ErrorCodeDefinition) => { obj[codeDef.code] = codeDef; return obj; }, {});
@@ -181,6 +181,12 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema): Subgraphs {
181
181
  if (args.external) {
182
182
  subgraphField.applyDirective(subgraph.metadata().externalDirective());
183
183
  }
184
+ if (args.usedOverridden) {
185
+ subgraphField.applyDirective(subgraph.metadata().externalDirective(), {'reason': '[overridden]'});
186
+ }
187
+ if (args.override) {
188
+ subgraphField.applyDirective(subgraph.metadata().overrideDirective(), {'from': args.override});
189
+ }
184
190
  }
185
191
  }
186
192
  }
package/src/federation.ts CHANGED
@@ -52,7 +52,7 @@ import {
52
52
  ERRORS,
53
53
  withModifiedErrorMessage,
54
54
  } from "./error";
55
- import { computeShareables } from "./sharing";
55
+ import { computeShareables } from "./precompute";
56
56
  import {
57
57
  CoreSpecDefinition,
58
58
  FeatureVersion,
@@ -71,8 +71,7 @@ import {
71
71
  externalDirectiveSpec,
72
72
  extendsDirectiveSpec,
73
73
  shareableDirectiveSpec,
74
- tagDirectiveSpec,
75
- inaccessibleDirectiveSpec,
74
+ overrideDirectiveSpec,
76
75
  FEDERATION2_SPEC_DIRECTIVES,
77
76
  ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES,
78
77
  FEDERATION2_ONLY_SPEC_DIRECTIVES,
@@ -80,6 +79,7 @@ import {
80
79
  import { defaultPrintOptions, PrintOptions as PrintOptions, printSchema } from "./print";
81
80
  import { createObjectTypeSpecification, createScalarTypeSpecification, createUnionTypeSpecification } from "./directiveAndTypeSpecification";
82
81
  import { didYouMean, suggestionList } from "./suggestions";
82
+ import { inaccessibleDirectiveSpec } from "./inaccessibleSpec";
83
83
 
84
84
  const linkSpec = LINK_VERSIONS.latest();
85
85
  const tagSpec = TAG_VERSIONS.latest();
@@ -263,53 +263,49 @@ function validateAllFieldSet<TParent extends SchemaElement<any, any>>(
263
263
  }
264
264
  }
265
265
 
266
- export function collectUsedExternalFieldsCoordinates(metadata: FederationMetadata): Set<string> {
267
- const usedExternalCoordinates = new Set<string>();
266
+ export function collectUsedFields(metadata: FederationMetadata): Set<FieldDefinition<CompositeType>> {
267
+ const usedFields = new Set<FieldDefinition<CompositeType>>();
268
268
 
269
269
  // Collects all external fields used by a key, requires or provides
270
- collectUsedExternaFieldsForDirective<CompositeType>(
271
- metadata,
270
+ collectUsedFieldsForDirective<CompositeType>(
272
271
  metadata.keyDirective(),
273
272
  type => type,
274
- usedExternalCoordinates,
273
+ usedFields,
275
274
  );
276
- collectUsedExternaFieldsForDirective<FieldDefinition<CompositeType>>(
277
- metadata,
275
+ collectUsedFieldsForDirective<FieldDefinition<CompositeType>>(
278
276
  metadata.requiresDirective(),
279
277
  field => field.parent!,
280
- usedExternalCoordinates,
278
+ usedFields,
281
279
  );
282
- collectUsedExternaFieldsForDirective<FieldDefinition<CompositeType>>(
283
- metadata,
280
+ collectUsedFieldsForDirective<FieldDefinition<CompositeType>>(
284
281
  metadata.providesDirective(),
285
282
  field => {
286
283
  const type = baseType(field.type!);
287
284
  return isCompositeType(type) ? type : undefined;
288
285
  },
289
- usedExternalCoordinates,
286
+ usedFields,
290
287
  );
291
288
 
292
- // Collects all external fields used to satisfy an interface constraint
289
+ // Collects all fields used to satisfy an interface constraint
293
290
  for (const itfType of metadata.schema.types<InterfaceType>('InterfaceType')) {
294
291
  const runtimeTypes = itfType.possibleRuntimeTypes();
295
292
  for (const field of itfType.fields()) {
296
293
  for (const runtimeType of runtimeTypes) {
297
294
  const implemField = runtimeType.field(field.name);
298
- if (implemField && metadata.isFieldExternal(implemField)) {
299
- usedExternalCoordinates.add(implemField.coordinate);
295
+ if (implemField) {
296
+ usedFields.add(implemField);
300
297
  }
301
298
  }
302
299
  }
303
300
  }
304
301
 
305
- return usedExternalCoordinates;
302
+ return usedFields;
306
303
  }
307
304
 
308
- function collectUsedExternaFieldsForDirective<TParent extends SchemaElement<any, any>>(
309
- metadata: FederationMetadata,
305
+ function collectUsedFieldsForDirective<TParent extends SchemaElement<any, any>>(
310
306
  definition: DirectiveDefinition<{fields: any}>,
311
307
  targetTypeExtractor: (element: TParent) => CompositeType | undefined,
312
- usedExternalCoordinates: Set<string>
308
+ usedFieldDefs: Set<FieldDefinition<CompositeType>>
313
309
  ) {
314
310
  for (const application of definition.applications()) {
315
311
  const type = targetTypeExtractor(application.parent! as TParent);
@@ -327,8 +323,7 @@ function collectUsedExternaFieldsForDirective<TParent extends SchemaElement<any,
327
323
  directive: application as Directive<any, {fields: any}>,
328
324
  includeInterfaceFieldsImplementations: true,
329
325
  validate: false,
330
- }).filter((field) => metadata.isFieldExternal(field))
331
- .forEach((field) => usedExternalCoordinates.add(field.coordinate));
326
+ }).forEach((field) => usedFieldDefs.add(field));
332
327
  }
333
328
  }
334
329
 
@@ -337,23 +332,20 @@ function collectUsedExternaFieldsForDirective<TParent extends SchemaElement<any,
337
332
  * interface implementation. Otherwise, the field declaration is somewhat useless.
338
333
  */
339
334
  function validateAllExternalFieldsUsed(metadata: FederationMetadata, errorCollector: GraphQLError[]): void {
340
- const allUsedExternals = collectUsedExternalFieldsCoordinates(metadata);
341
335
  for (const type of metadata.schema.types()) {
342
336
  if (!isObjectType(type) && !isInterfaceType(type)) {
343
337
  continue;
344
338
  }
345
339
  for (const field of type.fields()) {
346
- if (!metadata.isFieldExternal(field) || allUsedExternals.has(field.coordinate)) {
340
+ if (!metadata.isFieldExternal(field) || metadata.isFieldUsed(field)) {
347
341
  continue;
348
342
  }
349
343
 
350
- if (!isFieldSatisfyingInterface(field)) {
351
- errorCollector.push(ERRORS.EXTERNAL_UNUSED.err({
352
- message: `Field "${field.coordinate}" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface;`
353
- + ' the field declaration has no use and should be removed (or the field should not be @external).',
354
- nodes: field.sourceAST,
355
- }));
356
- }
344
+ errorCollector.push(ERRORS.EXTERNAL_UNUSED.err({
345
+ message: `Field "${field.coordinate}" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface;`
346
+ + ' the field declaration has no use and should be removed (or the field should not be @external).',
347
+ nodes: field.sourceAST,
348
+ }));
357
349
  }
358
350
  }
359
351
  }
@@ -371,10 +363,6 @@ function validateNoExternalOnInterfaceFields(metadata: FederationMetadata, error
371
363
  }
372
364
  }
373
365
 
374
- function isFieldSatisfyingInterface(field: FieldDefinition<ObjectType | InterfaceType>): boolean {
375
- return field.parent.interfaces().some(itf => itf.field(field.name));
376
- }
377
-
378
366
  /**
379
367
  * Register errors when, for an interface field, some of the implementations of that field are @external
380
368
  * _and_ not all of those field implementation have the same type (which otherwise allowed because field
@@ -385,7 +373,7 @@ function isFieldSatisfyingInterface(field: FieldDefinition<ObjectType | Interfac
385
373
  */
386
374
  function validateInterfaceRuntimeImplementationFieldsTypes(
387
375
  itf: InterfaceType,
388
- metadata: FederationMetadata,
376
+ metadata: FederationMetadata,
389
377
  errorCollector: GraphQLError[],
390
378
  ): void {
391
379
  const requiresDirective = federationMetadata(itf.schema())?.requiresDirective();
@@ -426,6 +414,7 @@ function formatFieldsToReturnType([type, implems]: [string, FieldDefinition<Obje
426
414
  export class FederationMetadata {
427
415
  private _externalTester?: ExternalTester;
428
416
  private _sharingPredicate?: (field: FieldDefinition<CompositeType>) => boolean;
417
+ private _fieldUsedPredicate?: (field: FieldDefinition<CompositeType>) => boolean;
429
418
  private _isFed2Schema?: boolean;
430
419
 
431
420
  constructor(readonly schema: Schema) {
@@ -435,6 +424,7 @@ export class FederationMetadata {
435
424
  this._externalTester = undefined;
436
425
  this._sharingPredicate = undefined;
437
426
  this._isFed2Schema = undefined;
427
+ this._fieldUsedPredicate = undefined;
438
428
  }
439
429
 
440
430
  isFed2Schema(): boolean {
@@ -463,6 +453,18 @@ export class FederationMetadata {
463
453
  return this._sharingPredicate;
464
454
  }
465
455
 
456
+ private fieldUsedPredicate(): (field: FieldDefinition<CompositeType>) => boolean {
457
+ if (!this._fieldUsedPredicate) {
458
+ const usedFields = collectUsedFields(this);
459
+ this._fieldUsedPredicate = (field: FieldDefinition<CompositeType>) => !!usedFields.has(field);
460
+ }
461
+ return this._fieldUsedPredicate;
462
+ }
463
+
464
+ isFieldUsed(field: FieldDefinition<CompositeType>): boolean {
465
+ return this.fieldUsedPredicate()(field);
466
+ }
467
+
466
468
  isFieldExternal(field: FieldDefinition<any> | InputFieldDefinition) {
467
469
  return this.externalTester().isExternal(field);
468
470
  }
@@ -534,11 +536,15 @@ export class FederationMetadata {
534
536
  return this.getFederationDirective(keyDirectiveSpec.name);
535
537
  }
536
538
 
539
+ overrideDirective(): DirectiveDefinition<{from: string}> {
540
+ return this.getFederationDirective(overrideDirectiveSpec.name);
541
+ }
542
+
537
543
  extendsDirective(): DirectiveDefinition<Record<string, never>> {
538
544
  return this.getFederationDirective(extendsDirectiveSpec.name);
539
545
  }
540
546
 
541
- externalDirective(): DirectiveDefinition<Record<string, never>> {
547
+ externalDirective(): DirectiveDefinition<{reason: string}> {
542
548
  return this.getFederationDirective(externalDirectiveSpec.name);
543
549
  }
544
550
 
@@ -555,7 +561,7 @@ export class FederationMetadata {
555
561
  }
556
562
 
557
563
  tagDirective(): DirectiveDefinition<{name: string}> {
558
- return this.getFederationDirective(tagDirectiveSpec.name);
564
+ return this.getFederationDirective(tagSpec.tagDirectiveSpec.name);
559
565
  }
560
566
 
561
567
  inaccessibleDirective(): DirectiveDefinition<{}> {
@@ -563,7 +569,7 @@ export class FederationMetadata {
563
569
  }
564
570
 
565
571
  allFederationDirectives(): DirectiveDefinition[] {
566
- const baseDirectives = [
572
+ const baseDirectives: DirectiveDefinition[] = [
567
573
  this.keyDirective(),
568
574
  this.externalDirective(),
569
575
  this.requiresDirective(),
@@ -572,7 +578,7 @@ export class FederationMetadata {
572
578
  this.extendsDirective(),
573
579
  ];
574
580
  return this.isFed2Schema()
575
- ? baseDirectives.concat(this.shareableDirective(), this.inaccessibleDirective())
581
+ ? baseDirectives.concat(this.shareableDirective(), this.inaccessibleDirective(), this.overrideDirective())
576
582
  : baseDirectives;
577
583
  }
578
584
 
@@ -618,19 +624,21 @@ export class FederationBlueprint extends SchemaBlueprint {
618
624
  }
619
625
  }
620
626
 
621
- onMissingDirectiveDefinition(schema: Schema, name: string): DirectiveDefinition | undefined {
627
+ onMissingDirectiveDefinition(schema: Schema, name: string, args?: {[key: string]: any}): DirectiveDefinition | GraphQLError[] | undefined {
622
628
  if (name === linkDirectiveDefaultName) {
623
- linkSpec.addToSchema(schema);
624
- return schema.directive(name);
629
+ const url = args && (args['url'] as string | undefined);
630
+ const as = url && url.startsWith(linkSpec.identity) ? (args['as'] as string | undefined) : undefined;
631
+ const errors = linkSpec.addDefinitionsToSchema(schema, as);
632
+ return errors.length > 0 ? errors : schema.directive(name);
625
633
  }
626
- return super.onMissingDirectiveDefinition(schema, name);
634
+ return super.onMissingDirectiveDefinition(schema, name, args);
627
635
  }
628
636
 
629
637
  ignoreParsedField(type: NamedType, fieldName: string): boolean {
630
638
  // Historically, federation 1 has accepted invalid schema, including some where the Query type included
631
639
  // the definition of `_entities` (so `_entities(representations: [_Any!]!): [_Entity]!`) but _without_
632
640
  // defining the `_Any` or `_Entity` type. So while we want to be stricter for fed2 (so this kind of
633
- // really weird case can be fixed), we want fed2 to accept as much fed1 schema as possible.
641
+ // really weird case can be fixed), we want fed2 to accept as much fed1 schema as possible.
634
642
  //
635
643
  // So, to avoid this problem, we ignore the _entities and _service fields if we parse them from
636
644
  // a fed1 input schema. Those will be added back anyway (along with the proper types) post-parsing.
@@ -648,8 +656,8 @@ export class FederationBlueprint extends SchemaBlueprint {
648
656
  }
649
657
  }
650
658
 
651
- onDirectiveDefinitionAndSchemaParsed(schema: Schema) {
652
- completeSubgraphSchema(schema);
659
+ onDirectiveDefinitionAndSchemaParsed(schema: Schema): GraphQLError[] {
660
+ return completeSubgraphSchema(schema);
653
661
  }
654
662
 
655
663
  onInvalidation(schema: Schema) {
@@ -788,7 +796,6 @@ export class FederationBlueprint extends SchemaBlueprint {
788
796
  const federationFeature = metadata.federationFeature();
789
797
  assert(federationFeature, 'Fed2 subgraph _must_ link to the federation feature')
790
798
  const directiveNameInSchema = federationFeature.directiveNameInSchema(unknownDirectiveName);
791
- console.log(`For ${unknownDirectiveName}, name in schema = ${directiveNameInSchema}`);
792
799
  if (directiveNameInSchema.startsWith(federationFeature.nameInSchema + '__')) {
793
800
  // There is no import for that directive
794
801
  return withModifiedErrorMessage(
@@ -804,7 +811,7 @@ export class FederationBlueprint extends SchemaBlueprint {
804
811
  }
805
812
  } else {
806
813
  return withModifiedErrorMessage(
807
- error,
814
+ error,
808
815
  `${error.message} If you meant the "@${unknownDirectiveName}" federation 2 directive, note that this schema is a federation 1 schema. To be a federation 2 schema, it needs to @link to the federation specifcation v2.`
809
816
  );
810
817
  }
@@ -813,7 +820,7 @@ export class FederationBlueprint extends SchemaBlueprint {
813
820
  const suggestions = suggestionList(unknownDirectiveName, FEDERATION2_ONLY_SPEC_DIRECTIVES.map((spec) => spec.name));
814
821
  if (suggestions.length > 0) {
815
822
  return withModifiedErrorMessage(
816
- error,
823
+ error,
817
824
  `${error.message}${didYouMean(suggestions.map((s) => '@' + s))} If so, note that ${suggestions.length === 1 ? 'it is a federation 2 directive' : 'they are federation 2 directives'} but this schema is a federation 1 one. To be a federation 2 schema, it needs to @link to the federation specifcation v2.`
818
825
  );
819
826
  }
@@ -851,7 +858,10 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
851
858
  assert(spec.url.version.satisfies(linkSpec.version), `Fed2 schema must use @link with version >= 1.0, but schema uses ${spec.url}`);
852
859
  } else {
853
860
  const alias = findUnusedNamedForLinkDirective(schema);
854
- linkSpec.addToSchema(schema, alias);
861
+ const errors = linkSpec.addToSchema(schema, alias);
862
+ if (errors.length > 0) {
863
+ throw ErrGraphQLValidationFailed(errors);
864
+ }
855
865
  spec = linkSpec;
856
866
  core = schema.coreFeatures;
857
867
  assert(core, 'Schema should now be a core schema');
@@ -865,12 +875,15 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
865
875
  import: FEDERATION2_SPEC_DIRECTIVES.map((spec) => `@${spec.name}`),
866
876
  }
867
877
  );
868
- completeSubgraphSchema(schema);
878
+ const errors = completeSubgraphSchema(schema);
879
+ if (errors.length > 0) {
880
+ throw ErrGraphQLValidationFailed(errors);
881
+ }
869
882
  }
870
883
 
871
884
  // This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
872
885
  // subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
873
- export const FEDERATION2_LINK_WTH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible"])';
886
+ export const FEDERATION2_LINK_WTH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override"])';
874
887
 
875
888
  export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
876
889
  const fed2LinkExtension: SchemaExtensionNode = {
@@ -961,22 +974,25 @@ export function newEmptyFederation2Schema(): Schema {
961
974
  return schema;
962
975
  }
963
976
 
964
- function completeSubgraphSchema(schema: Schema) {
977
+ function completeSubgraphSchema(schema: Schema): GraphQLError[] {
965
978
  const coreFeatures = schema.coreFeatures;
966
979
  if (coreFeatures) {
967
980
  const fedFeature = coreFeatures.getByIdentity(federationIdentity);
968
981
  if (fedFeature) {
969
- completeFed2SubgraphSchema(schema);
982
+ return completeFed2SubgraphSchema(schema);
970
983
  } else {
971
- completeFed1SubgraphSchema(schema);
984
+ return completeFed1SubgraphSchema(schema);
972
985
  }
973
986
  } else {
974
987
  const fedLink = schema.schemaDefinition.appliedDirectivesOf(linkDirectiveDefaultName).find(isFedSpecLinkDirective);
975
988
  if (fedLink) {
976
- linkSpec.addToSchema(schema);
977
- completeFed2SubgraphSchema(schema);
989
+ const errors = linkSpec.addToSchema(schema);
990
+ if (errors.length > 0) {
991
+ return errors;
992
+ }
993
+ return completeFed2SubgraphSchema(schema);
978
994
  } else {
979
- completeFed1SubgraphSchema(schema);
995
+ return completeFed1SubgraphSchema(schema);
980
996
  }
981
997
  }
982
998
  }
@@ -986,15 +1002,16 @@ function isFedSpecLinkDirective(directive: Directive<SchemaDefinition>): directi
986
1002
  return directive.name === linkDirectiveDefaultName && args['url'] && (args['url'] as string).startsWith(federationIdentity);
987
1003
  }
988
1004
 
989
- function completeFed1SubgraphSchema(schema: Schema) {
990
- fieldSetTypeSpec.checkOrAdd(schema, '_' + fieldSetTypeSpec.name);
991
-
992
- keyDirectiveSpec.checkOrAdd(schema);
993
- requiresDirectiveSpec.checkOrAdd(schema);
994
- providesDirectiveSpec.checkOrAdd(schema);
995
- extendsDirectiveSpec.checkOrAdd(schema);
996
- externalDirectiveSpec.checkOrAdd(schema);
997
- tagDirectiveSpec.checkOrAdd(schema);
1005
+ function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
1006
+ return [
1007
+ fieldSetTypeSpec.checkOrAdd(schema, '_' + fieldSetTypeSpec.name),
1008
+ keyDirectiveSpec.checkOrAdd(schema),
1009
+ requiresDirectiveSpec.checkOrAdd(schema),
1010
+ providesDirectiveSpec.checkOrAdd(schema),
1011
+ extendsDirectiveSpec.checkOrAdd(schema),
1012
+ externalDirectiveSpec.checkOrAdd(schema),
1013
+ tagSpec.tagDirectiveSpec.checkOrAdd(schema),
1014
+ ].flat();
998
1015
  }
999
1016
 
1000
1017
  function completeFed2SubgraphSchema(schema: Schema) {
@@ -1006,13 +1023,13 @@ function completeFed2SubgraphSchema(schema: Schema) {
1006
1023
 
1007
1024
  const spec = FEDERATION_VERSIONS.find(fedFeature.url.version);
1008
1025
  if (!spec) {
1009
- throw ERRORS.UNKNOWN_FEDERATION_LINK_VERSION.err({
1026
+ return [ERRORS.UNKNOWN_FEDERATION_LINK_VERSION.err({
1010
1027
  message: `Invalid version ${fedFeature.url.version} for the federation feature in @link direction on schema`,
1011
1028
  nodes: fedFeature.directive.sourceAST
1012
- });
1029
+ })];
1013
1030
  }
1014
1031
 
1015
- spec.addElementsToSchema(schema);
1032
+ return spec.addElementsToSchema(schema);
1016
1033
  }
1017
1034
 
1018
1035
  export function parseFieldSetArgument({