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

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 (63) hide show
  1. package/CHANGELOG.md +13 -3
  2. package/dist/buildSchema.d.ts.map +1 -1
  3. package/dist/buildSchema.js +50 -40
  4. package/dist/buildSchema.js.map +1 -1
  5. package/dist/coreSpec.d.ts +4 -2
  6. package/dist/coreSpec.d.ts.map +1 -1
  7. package/dist/coreSpec.js +107 -19
  8. package/dist/coreSpec.js.map +1 -1
  9. package/dist/definitions.d.ts +3 -0
  10. package/dist/definitions.d.ts.map +1 -1
  11. package/dist/definitions.js +72 -6
  12. package/dist/definitions.js.map +1 -1
  13. package/dist/error.d.ts +5 -0
  14. package/dist/error.d.ts.map +1 -1
  15. package/dist/error.js +29 -1
  16. package/dist/error.js.map +1 -1
  17. package/dist/federation.d.ts +4 -0
  18. package/dist/federation.d.ts.map +1 -1
  19. package/dist/federation.js +41 -11
  20. package/dist/federation.js.map +1 -1
  21. package/dist/federationSpec.d.ts +4 -0
  22. package/dist/federationSpec.d.ts.map +1 -1
  23. package/dist/federationSpec.js +20 -2
  24. package/dist/federationSpec.js.map +1 -1
  25. package/dist/inaccessibleSpec.d.ts +3 -0
  26. package/dist/inaccessibleSpec.d.ts.map +1 -1
  27. package/dist/inaccessibleSpec.js +22 -3
  28. package/dist/inaccessibleSpec.js.map +1 -1
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/joinSpec.d.ts +1 -0
  34. package/dist/joinSpec.d.ts.map +1 -1
  35. package/dist/joinSpec.js +18 -0
  36. package/dist/joinSpec.js.map +1 -1
  37. package/dist/knownCoreFeatures.d.ts +4 -0
  38. package/dist/knownCoreFeatures.d.ts.map +1 -0
  39. package/dist/knownCoreFeatures.js +16 -0
  40. package/dist/knownCoreFeatures.js.map +1 -0
  41. package/dist/tagSpec.d.ts +1 -0
  42. package/dist/tagSpec.d.ts.map +1 -1
  43. package/dist/tagSpec.js +6 -0
  44. package/dist/tagSpec.js.map +1 -1
  45. package/package.json +2 -2
  46. package/src/__tests__/coreSpec.test.ts +100 -0
  47. package/src/__tests__/definitions.test.ts +75 -0
  48. package/src/__tests__/removeInaccessibleElements.test.ts +1 -1
  49. package/src/__tests__/schemaUpgrader.test.ts +3 -2
  50. package/src/__tests__/subgraphValidation.test.ts +63 -4
  51. package/src/buildSchema.ts +97 -50
  52. package/src/coreSpec.ts +124 -21
  53. package/src/definitions.ts +99 -7
  54. package/src/error.ts +46 -0
  55. package/src/federation.ts +67 -12
  56. package/src/federationSpec.ts +27 -1
  57. package/src/inaccessibleSpec.ts +26 -10
  58. package/src/index.ts +1 -0
  59. package/src/joinSpec.ts +19 -0
  60. package/src/knownCoreFeatures.ts +13 -0
  61. package/src/tagSpec.ts +8 -0
  62. package/tsconfig.test.tsbuildinfo +1 -1
  63. package/tsconfig.tsbuildinfo +1 -1
@@ -39,6 +39,8 @@ import { SDLValidationRule } from "graphql/validation/ValidationContext";
39
39
  import { specifiedSDLRules } from "graphql/validation/specifiedRules";
40
40
  import { validateSchema } from "./validate";
41
41
  import { createDirectiveSpecification, createScalarTypeSpecification, DirectiveSpecification, TypeSpecification } from "./directiveAndTypeSpecification";
42
+ import { didYouMean, suggestionList } from "./suggestions";
43
+ import { withModifiedErrorMessage } from "./error";
42
44
 
43
45
  const validationErrorCode = 'GraphQLValidationFailed';
44
46
 
@@ -505,7 +507,10 @@ export abstract class SchemaElement<TOwnType extends SchemaElement<any, TParent>
505
507
  this.checkUpdate();
506
508
  const def = this.schema().directive(nameOrDefOrDirective) ?? this.schema().blueprint.onMissingDirectiveDefinition(this.schema(), nameOrDefOrDirective);
507
509
  if (!def) {
508
- throw new GraphQLError(`Cannot apply unknown directive "@${nameOrDefOrDirective}"`);
510
+ throw this.schema().blueprint.onGraphQLJSValidationError(
511
+ this.schema(),
512
+ new GraphQLError(`Unknown directive "@${nameOrDefOrDirective}".`)
513
+ );
509
514
  }
510
515
  name = nameOrDefOrDirective;
511
516
  } else {
@@ -857,6 +862,38 @@ export class SchemaBlueprint {
857
862
  validationRules(): readonly SDLValidationRule[] {
858
863
  return specifiedSDLRules;
859
864
  }
865
+
866
+ /**
867
+ * Allows to intercept some graphQL-js error messages when we can provide additional guidance to users.
868
+ */
869
+ onGraphQLJSValidationError(schema: Schema, error: GraphQLError): GraphQLError {
870
+ // For now, the main additional guidance we provide is around directives, where we could provide additional help in 2 main ways:
871
+ // - if a directive name is likely misspelled (somehow, graphQL-js has methods to offer suggestions on likely mispelling, but don't use this (at the
872
+ // time of this writting) for directive names).
873
+ // - for fed 2 schema, if a federation directive is refered under it's "default" naming but is not properly imported (not enforced
874
+ // in the method but rather in the `FederationBlueprint`).
875
+ //
876
+ // Note that intercepting/parsing error messages to modify them is never ideal, but pragmatically, it's probably better than rewriting the relevant
877
+ // rules entirely (in that later case, our "copied" rule would stop getting any potential graphQL-js made improvements for instance). And while such
878
+ // parsing is fragile, in that it'll break if the original message change, we have unit tests to surface any such breakage so it's not really a risk.
879
+ const matcher = /^Unknown directive "@(?<directive>[_A-Za-z][_0-9A-Za-z]*)"\.$/.exec(error.message);
880
+ const name = matcher?.groups?.directive;
881
+ if (!name) {
882
+ return error;
883
+ }
884
+
885
+ const allDefinedDirectiveNames = schema.allDirectives().map((d) => d.name);
886
+ const suggestions = suggestionList(name, allDefinedDirectiveNames);
887
+ if (suggestions.length === 0) {
888
+ return this.onUnknownDirectiveValidationError(schema, name, error);
889
+ } else {
890
+ return withModifiedErrorMessage(error, `${error.message}${didYouMean(suggestions.map((s) => '@' + s))}`);
891
+ }
892
+ }
893
+
894
+ onUnknownDirectiveValidationError(_schema: Schema, _unknownDirectiveName: string, error: GraphQLError): GraphQLError {
895
+ return error;
896
+ }
860
897
  }
861
898
 
862
899
  export const defaultSchemaBlueprint = new SchemaBlueprint();
@@ -873,7 +910,8 @@ export class CoreFeature {
873
910
 
874
911
  isFeatureDefinition(element: NamedType | DirectiveDefinition): boolean {
875
912
  return element.name.startsWith(this.nameInSchema + '__')
876
- || (element.kind === 'DirectiveDefinition' && element.name === this.nameInSchema);
913
+ || (element.kind === 'DirectiveDefinition' && element.name === this.nameInSchema)
914
+ || !!this.imports.find((i) => element.name === (i.as ?? i.name));
877
915
  }
878
916
 
879
917
  directiveNameInSchema(name: string): string {
@@ -931,7 +969,7 @@ export class CoreFeatures {
931
969
  if (existing) {
932
970
  throw error(`Duplicate inclusion of feature ${url.identity}`);
933
971
  }
934
- const imports = extractCoreFeatureImports(typedDirective);
972
+ const imports = extractCoreFeatureImports(url, typedDirective);
935
973
  const feature = new CoreFeature(url, args.as ?? url.name, directive, imports, args.for);
936
974
  this.add(feature);
937
975
  directive.schema().blueprint.onAddedCoreFeature(directive.schema(), feature);
@@ -995,6 +1033,10 @@ const graphQLBuiltInDirectivesSpecifications: readonly DirectiveSpecification[]
995
1033
  }),
996
1034
  ];
997
1035
 
1036
+
1037
+ // A coordinate is up to 3 "graphQL name" ([_A-Za-z][_0-9A-Za-z]*).
1038
+ const coordinateRegexp = /^@?[_A-Za-z][_0-9A-Za-z]*(\.[_A-Za-z][_0-9A-Za-z]*)?(\([_A-Za-z][_0-9A-Za-z]*:\))?$/;
1039
+
998
1040
  export class Schema {
999
1041
  private _schemaDefinition: SchemaDefinition;
1000
1042
  private readonly _builtInTypes = new MapWithCachedArrays<string, NamedType>();
@@ -1311,7 +1353,7 @@ export class Schema {
1311
1353
 
1312
1354
  // TODO: we check that all types are properly set (aren't undefined) in `validateSchema`, but `validateSDL` will error out beforehand. We should
1313
1355
  // probably extract that part of `validateSchema` and run `validateSDL` conditionally on that first check.
1314
- let errors = validateSDL(this.toAST(), undefined, this.blueprint.validationRules());
1356
+ let errors = validateSDL(this.toAST(), undefined, this.blueprint.validationRules()).map((e) => this.blueprint.onGraphQLJSValidationError(this, e));
1315
1357
  errors = errors.concat(validateSchema(this));
1316
1358
 
1317
1359
  // We avoid adding federation-specific validations if the base schema is not proper graphQL as the later can easily trigger
@@ -1364,6 +1406,56 @@ export class Schema {
1364
1406
  specifiedByDirective(schema: Schema): DirectiveDefinition<{url: string}> {
1365
1407
  return this.getBuiltInDirective(schema, 'specifiedBy');
1366
1408
  }
1409
+
1410
+ /**
1411
+ * Gets an element of the schema given its "schema coordinate".
1412
+ *
1413
+ * Note that the syntax for schema coordinates is the one from the upcoming GraphQL spec: https://github.com/graphql/graphql-spec/pull/794.
1414
+ */
1415
+ elementByCoordinate(coordinate: string): NamedSchemaElement<any, any, any> | undefined {
1416
+ if (!coordinate.match(coordinateRegexp)) {
1417
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1418
+ }
1419
+
1420
+ const argStartIdx = coordinate.indexOf('(');
1421
+ const start = argStartIdx < 0 ? coordinate : coordinate.slice(0, argStartIdx);
1422
+ // Argument syntax is `foo(argName:)`, so the arg name start after the open parenthesis and go until the final ':)'.
1423
+ const argName = argStartIdx < 0 ? undefined : coordinate.slice(argStartIdx + 1, coordinate.length - 2);
1424
+ const splittedStart = start.split('.');
1425
+ const typeOrDirectiveName = splittedStart[0];
1426
+ const fieldOrEnumName = splittedStart[1];
1427
+ const isDirective = typeOrDirectiveName.startsWith('@');
1428
+ if (isDirective) {
1429
+ if (fieldOrEnumName) {
1430
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1431
+ }
1432
+ const directive = this.directive(typeOrDirectiveName.slice(1));
1433
+ return argName ? directive?.argument(argName) : directive;
1434
+ } else {
1435
+ const type = this.type(typeOrDirectiveName);
1436
+ if (!type || !fieldOrEnumName) {
1437
+ return type;
1438
+ }
1439
+ switch (type.kind) {
1440
+ case 'ObjectType':
1441
+ case 'InterfaceType':
1442
+ const field = type.field(fieldOrEnumName);
1443
+ return argName ? field?.argument(argName) : field;
1444
+ case 'InputObjectType':
1445
+ if (argName) {
1446
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1447
+ }
1448
+ return type.field(fieldOrEnumName);
1449
+ case 'EnumType':
1450
+ if (argName) {
1451
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1452
+ }
1453
+ return type.value(fieldOrEnumName);
1454
+ default:
1455
+ throw error(`Invalid argument "${coordinate}: it is not a syntactically valid graphQL coordinate."`);
1456
+ }
1457
+ }
1458
+ }
1367
1459
  }
1368
1460
 
1369
1461
  export class RootType extends BaseExtensionMember<SchemaDefinition> {
@@ -1403,7 +1495,7 @@ export class SchemaDefinition extends SchemaElement<SchemaDefinition, Schema> {
1403
1495
  const schemaDirective = applied as Directive<SchemaDefinition, CoreOrLinkDirectiveArgs>;
1404
1496
  const args = schemaDirective.arguments();
1405
1497
  const url = FeatureUrl.parse((args.url ?? args.feature)!);
1406
- const imports = extractCoreFeatureImports(schemaDirective);
1498
+ const imports = extractCoreFeatureImports(url, schemaDirective);
1407
1499
  const core = new CoreFeature(url, args.as ?? url.name, schemaDirective, imports, args.for);
1408
1500
  Schema.prototype['markAsCoreSchema'].call(schema, core);
1409
1501
  } else if (coreFeatures) {
@@ -2711,9 +2803,9 @@ export class Directive<
2711
2803
  this.onModification();
2712
2804
  const coreFeatures = this.schema().coreFeatures;
2713
2805
  if (coreFeatures && this.name === coreFeatures.coreItself.nameInSchema) {
2714
- // We're removing a @core directive application, so we remove it from the list of core features. And
2806
+ // We're removing a @core/@link directive application, so we remove it from the list of core features. And
2715
2807
  // if it is @core itself, we clean all features (to avoid having things too inconsistent).
2716
- const url = FeatureUrl.parse(this._args['feature']!);
2808
+ const url = FeatureUrl.parse(this._args[coreFeatures.coreDefinition.urlArgName()]!);
2717
2809
  if (url.identity === coreFeatures.coreItself.url.identity) {
2718
2810
  // Note that we unmark first because the loop after that will nuke our parent.
2719
2811
  Schema.prototype['unmarkAsCoreSchema'].call(this.schema());
package/src/error.ts CHANGED
@@ -105,6 +105,34 @@ export function errorCodeDef(e: GraphQLError | string): ErrorCodeDefinition | un
105
105
  return code ? codeDefByCode[code] : undefined;
106
106
  }
107
107
 
108
+ export function withModifiedErrorMessage(e: GraphQLError, newMessage: string): GraphQLError {
109
+ return new GraphQLError(
110
+ newMessage,
111
+ {
112
+ nodes: e.nodes,
113
+ source: e.source,
114
+ positions: e.positions,
115
+ path: e.path,
116
+ originalError: e.originalError,
117
+ extensions: e.extensions
118
+ }
119
+ );
120
+ }
121
+
122
+ export function withModifiedErrorNodes(e: GraphQLError, newNodes: readonly ASTNode[] | ASTNode | undefined): GraphQLError {
123
+ return new GraphQLError(
124
+ e.message,
125
+ {
126
+ nodes: newNodes,
127
+ source: e.source,
128
+ positions: e.positions,
129
+ path: e.path,
130
+ originalError: e.originalError,
131
+ extensions: e.extensions
132
+ }
133
+ );
134
+ }
135
+
108
136
  const INVALID_GRAPHQL = makeCodeDefinition(
109
137
  'INVALID_GRAPHQL',
110
138
  'A schema is invalid GraphQL: it violates one of the rule of the specification.'
@@ -252,6 +280,11 @@ const EXTERNAL_ON_INTERFACE = makeCodeDefinition(
252
280
  'The field of an interface type is marked with `@external`: as external is about marking field not resolved by the subgraph and as interface field are not resolved (only implementations of those fields are), an "external" interface field is nonsensical',
253
281
  );
254
282
 
283
+ const MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL = makeCodeDefinition(
284
+ 'MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL',
285
+ 'In a subgraph, a field is both marked @external and has a merged directive applied to it',
286
+ );
287
+
255
288
  const FIELD_TYPE_MISMATCH = makeCodeDefinition(
256
289
  'FIELD_TYPE_MISMATCH',
257
290
  'A field has a type that is incompatible with other declarations of that field in other subgraphs.',
@@ -306,6 +339,16 @@ const INVALID_LINK_DIRECTIVE_USAGE = makeCodeDefinition(
306
339
  'An application of the @link directive is invalid/does not respect the specification.'
307
340
  );
308
341
 
342
+ const LINK_IMPORT_NAME_MISMATCH = makeCodeDefinition(
343
+ 'LINK_IMPORT_NAME_MISMATCH',
344
+ 'The import name for a merged directive (as declared by the relevant `@link(import:)` argument) is inconsistent between subgraphs.'
345
+ );
346
+
347
+ const REFERENCED_INACCESSIBLE = makeCodeDefinition(
348
+ 'REFERENCED_INACCESSIBLE',
349
+ 'An element is marked as @inaccessible but is referenced by a non-inaccessible element.'
350
+ );
351
+
309
352
  const REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH = makeCodeDefinition(
310
353
  'REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH',
311
354
  'An argument of a field or directive definition is mandatory in some subgraphs, but the argument is not defined in all subgraphs that define the field or directive definition.'
@@ -360,6 +403,7 @@ export const ERRORS = {
360
403
  EXTERNAL_ARGUMENT_TYPE_MISMATCH,
361
404
  EXTERNAL_ARGUMENT_DEFAULT_MISMATCH,
362
405
  EXTERNAL_ON_INTERFACE,
406
+ MERGED_DIRECTIVE_APPLICATION_ON_EXTERNAL,
363
407
  FIELD_TYPE_MISMATCH,
364
408
  ARGUMENT_TYPE_MISMATCH,
365
409
  INPUT_FIELD_DEFAULT_MISMATCH,
@@ -370,6 +414,8 @@ export const ERRORS = {
370
414
  INTERFACE_FIELD_IMPLEM_TYPE_MISMATCH,
371
415
  INVALID_FIELD_SHARING,
372
416
  INVALID_LINK_DIRECTIVE_USAGE,
417
+ LINK_IMPORT_NAME_MISMATCH,
418
+ REFERENCED_INACCESSIBLE,
373
419
  REQUIRED_ARGUMENT_MISSING_IN_SOME_SUBGRAPH,
374
420
  SATISFIABILITY_ERROR,
375
421
  };
package/src/federation.ts CHANGED
@@ -50,6 +50,7 @@ import {
50
50
  ErrorCodeDefinition,
51
51
  ERROR_CATEGORIES,
52
52
  ERRORS,
53
+ withModifiedErrorMessage,
53
54
  } from "./error";
54
55
  import { computeShareables } from "./sharing";
55
56
  import {
@@ -71,10 +72,14 @@ import {
71
72
  extendsDirectiveSpec,
72
73
  shareableDirectiveSpec,
73
74
  tagDirectiveSpec,
75
+ inaccessibleDirectiveSpec,
74
76
  FEDERATION2_SPEC_DIRECTIVES,
77
+ ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES,
78
+ FEDERATION2_ONLY_SPEC_DIRECTIVES,
75
79
  } from "./federationSpec";
76
80
  import { defaultPrintOptions, PrintOptions as PrintOptions, printSchema } from "./print";
77
81
  import { createObjectTypeSpecification, createScalarTypeSpecification, createUnionTypeSpecification } from "./directiveAndTypeSpecification";
82
+ import { didYouMean, suggestionList } from "./suggestions";
78
83
 
79
84
  const linkSpec = LINK_VERSIONS.latest();
80
85
  const tagSpec = TAG_VERSIONS.latest();
@@ -102,6 +107,7 @@ const FEDERATION_SPECIFIC_VALIDATION_RULES = [
102
107
 
103
108
  const FEDERATION_VALIDATION_RULES = specifiedSDLRules.filter(rule => !FEDERATION_OMITTED_VALIDATION_RULES.includes(rule)).concat(FEDERATION_SPECIFIC_VALIDATION_RULES);
104
109
 
110
+
105
111
  // Returns a list of the coordinate of all the fields in the selection that are marked external.
106
112
  function validateFieldSetSelections(
107
113
  directiveName: string,
@@ -417,16 +423,6 @@ function formatFieldsToReturnType([type, implems]: [string, FieldDefinition<Obje
417
423
  return `${joinStrings(implems.map(printFieldCoordinate))} ${implems.length == 1 ? 'has' : 'have'} type "${type}"`;
418
424
  }
419
425
 
420
- function checkIfFed2Schema(schema: Schema): boolean {
421
- const core = schema.coreFeatures;
422
- if (!core) {
423
- return false
424
- }
425
-
426
- const federationFeature = core.getByIdentity(federationSpec.identity);
427
- return !!federationFeature && federationFeature.url.version.satisfies(new FeatureVersion(2, 0));
428
- }
429
-
430
426
  export class FederationMetadata {
431
427
  private _externalTester?: ExternalTester;
432
428
  private _sharingPredicate?: (field: FieldDefinition<CompositeType>) => boolean;
@@ -443,11 +439,16 @@ export class FederationMetadata {
443
439
 
444
440
  isFed2Schema(): boolean {
445
441
  if (!this._isFed2Schema) {
446
- this._isFed2Schema = checkIfFed2Schema(this.schema);
442
+ const feature = this.federationFeature();
443
+ this._isFed2Schema = !!feature && feature.url.version.satisfies(new FeatureVersion(2, 0))
447
444
  }
448
445
  return this._isFed2Schema;
449
446
  }
450
447
 
448
+ federationFeature(): CoreFeature | undefined {
449
+ return this.schema.coreFeatures?.getByIdentity(federationSpec.identity);
450
+ }
451
+
451
452
  private externalTester(): ExternalTester {
452
453
  if (!this._externalTester) {
453
454
  this._externalTester = new ExternalTester(this.schema);
@@ -557,6 +558,10 @@ export class FederationMetadata {
557
558
  return this.getFederationDirective(tagDirectiveSpec.name);
558
559
  }
559
560
 
561
+ inaccessibleDirective(): DirectiveDefinition<{}> {
562
+ return this.getFederationDirective(inaccessibleDirectiveSpec.name);
563
+ }
564
+
560
565
  allFederationDirectives(): DirectiveDefinition[] {
561
566
  const baseDirectives = [
562
567
  this.keyDirective(),
@@ -567,7 +572,7 @@ export class FederationMetadata {
567
572
  this.extendsDirective(),
568
573
  ];
569
574
  return this.isFed2Schema()
570
- ? baseDirectives.concat(this.shareableDirective())
575
+ ? baseDirectives.concat(this.shareableDirective(), this.inaccessibleDirective())
571
576
  : baseDirectives;
572
577
  }
573
578
 
@@ -769,6 +774,52 @@ export class FederationBlueprint extends SchemaBlueprint {
769
774
  validationRules(): readonly SDLValidationRule[] {
770
775
  return FEDERATION_VALIDATION_RULES;
771
776
  }
777
+
778
+ onUnknownDirectiveValidationError(schema: Schema, unknownDirectiveName: string, error: GraphQLError): GraphQLError {
779
+ const metadata = federationMetadata(schema);
780
+ assert(metadata, `This method should only have been called on a subgraph schema`)
781
+ if (ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES.includes(unknownDirectiveName)) {
782
+ // The directive name is "unknown" but it is a default federation directive name. So it means one of a few things
783
+ // happened:
784
+ // 1. it's a fed1 schema but the directive is a fed2 only one (only possible case for fed1 schema).
785
+ // 2. the directive has not been imported at all (so needs to be prefixed for it to work).
786
+ // 3. the directive has an `import`, but it's been aliased to another name.
787
+ if (metadata.isFed2Schema()) {
788
+ const federationFeature = metadata.federationFeature();
789
+ assert(federationFeature, 'Fed2 subgraph _must_ link to the federation feature')
790
+ const directiveNameInSchema = federationFeature.directiveNameInSchema(unknownDirectiveName);
791
+ console.log(`For ${unknownDirectiveName}, name in schema = ${directiveNameInSchema}`);
792
+ if (directiveNameInSchema.startsWith(federationFeature.nameInSchema + '__')) {
793
+ // There is no import for that directive
794
+ return withModifiedErrorMessage(
795
+ error,
796
+ `${error.message} If you meant the "@${unknownDirectiveName}" federation directive, you should use fully-qualified name "@${directiveNameInSchema}" or add "@${unknownDirectiveName}" to the \`import\` argument of the @link to the federation specification.`
797
+ );
798
+ } else {
799
+ // There's an import, but it's renamed
800
+ return withModifiedErrorMessage(
801
+ error,
802
+ `${error.message} If you meant the "@${unknownDirectiveName}" federation directive, you should use "@${directiveNameInSchema}" as it is imported under that name in the @link to the federation specification of this schema.`
803
+ );
804
+ }
805
+ } else {
806
+ return withModifiedErrorMessage(
807
+ error,
808
+ `${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
+ );
810
+ }
811
+ } else if (!metadata.isFed2Schema()) {
812
+ // We could get here in the case where a fed1 schema has tried to use a fed2 directive but mispelled it.
813
+ const suggestions = suggestionList(unknownDirectiveName, FEDERATION2_ONLY_SPEC_DIRECTIVES.map((spec) => spec.name));
814
+ if (suggestions.length > 0) {
815
+ return withModifiedErrorMessage(
816
+ error,
817
+ `${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
+ );
819
+ }
820
+ }
821
+ return error;
822
+ }
772
823
  }
773
824
 
774
825
  const federationBlueprint = new FederationBlueprint();
@@ -817,6 +868,10 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
817
868
  completeSubgraphSchema(schema);
818
869
  }
819
870
 
871
+ // This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
872
+ // 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"])';
874
+
820
875
  export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
821
876
  const fed2LinkExtension: SchemaExtensionNode = {
822
877
  kind: Kind.SCHEMA_EXTENSION,
@@ -13,6 +13,8 @@ import { DirectiveLocation } from "graphql";
13
13
  import { assert } from "./utils";
14
14
  import { tagLocations } from "./tagSpec";
15
15
  import { federationMetadata } from "./federation";
16
+ import { registerKnownFeature } from "./knownCoreFeatures";
17
+ import { inaccessibleLocations } from "./inaccessibleSpec";
16
18
 
17
19
  export const federationIdentity = 'https://specs.apollo.dev/federation';
18
20
 
@@ -62,11 +64,17 @@ export const shareableDirectiveSpec = createDirectiveSpecification({
62
64
  export const tagDirectiveSpec = createDirectiveSpecification({
63
65
  name:'tag',
64
66
  locations: tagLocations,
67
+ repeatable: true,
65
68
  argumentFct: (schema) => {
66
69
  return [{ name: 'name', type: new NonNullType(schema.stringType()) }];
67
70
  }
68
71
  });
69
72
 
73
+ export const inaccessibleDirectiveSpec = createDirectiveSpecification({
74
+ name:'inaccessible',
75
+ locations: inaccessibleLocations,
76
+ });
77
+
70
78
  function fieldsArgument(schema: Schema): ArgumentSpecification {
71
79
  return { name: 'fields', type: fieldSetType(schema) };
72
80
  }
@@ -77,17 +85,27 @@ function fieldSetType(schema: Schema): InputType {
77
85
  return new NonNullType(metadata.fieldSetType());
78
86
  }
79
87
 
88
+ export const FEDERATION2_ONLY_SPEC_DIRECTIVES = [
89
+ shareableDirectiveSpec,
90
+ inaccessibleDirectiveSpec,
91
+ ];
92
+
80
93
  // Note that this is only used for federation 2+ (federation 1 adds the same directive, but not through a core spec).
81
94
  export const FEDERATION2_SPEC_DIRECTIVES = [
82
95
  keyDirectiveSpec,
83
96
  requiresDirectiveSpec,
84
97
  providesDirectiveSpec,
85
98
  externalDirectiveSpec,
86
- shareableDirectiveSpec,
87
99
  tagDirectiveSpec,
88
100
  extendsDirectiveSpec, // TODO: should we stop supporting that?
101
+ ...FEDERATION2_ONLY_SPEC_DIRECTIVES,
89
102
  ];
90
103
 
104
+ // Note that this is meant to contain _all_ federation directive names ever supported, regardless of which version.
105
+ // But currently, fed2 directives are a superset of fed1's so ... (but this may change if we stop supporting `@extends`
106
+ // in fed2).
107
+ export const ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES = FEDERATION2_SPEC_DIRECTIVES.map((spec) => spec.name);
108
+
91
109
  export const FEDERATION_SPEC_TYPES = [
92
110
  fieldSetTypeSpec,
93
111
  ]
@@ -107,7 +125,15 @@ export class FederationSpecDefinition extends FeatureDefinition {
107
125
  directive.checkOrAdd(schema, feature.directiveNameInSchema(directive.name));
108
126
  }
109
127
  }
128
+
129
+ allElementNames(): string[] {
130
+ return FEDERATION2_SPEC_DIRECTIVES.map((spec) => `@${spec.name}`).concat([
131
+ fieldSetTypeSpec.name,
132
+ ])
133
+ }
110
134
  }
111
135
 
112
136
  export const FEDERATION_VERSIONS = new FeatureDefinitions<FederationSpecDefinition>(federationIdentity)
113
137
  .add(new FederationSpecDefinition(new FeatureVersion(2, 0)));
138
+
139
+ registerKnownFeature(FEDERATION_VERSIONS);
@@ -8,31 +8,41 @@ import {
8
8
  Schema,
9
9
  } from "./definitions";
10
10
  import { GraphQLError, DirectiveLocation } from "graphql";
11
+ import { registerKnownFeature } from "./knownCoreFeatures";
12
+ import { ERRORS } from "./error";
11
13
 
12
14
  export const inaccessibleIdentity = 'https://specs.apollo.dev/inaccessible';
13
15
 
16
+ export const inaccessibleLocations = [
17
+ DirectiveLocation.FIELD_DEFINITION,
18
+ DirectiveLocation.OBJECT,
19
+ DirectiveLocation.INTERFACE,
20
+ DirectiveLocation.UNION,
21
+ ];
22
+
14
23
  export class InaccessibleSpecDefinition extends FeatureDefinition {
15
24
  constructor(version: FeatureVersion) {
16
25
  super(new FeatureUrl(inaccessibleIdentity, 'inaccessible', version));
17
26
  }
18
27
 
19
28
  addElementsToSchema(schema: Schema) {
20
- this.addDirective(schema, 'inaccessible').addLocations(
21
- DirectiveLocation.FIELD_DEFINITION,
22
- DirectiveLocation.OBJECT,
23
- DirectiveLocation.INTERFACE,
24
- DirectiveLocation.UNION,
25
- );
29
+ this.addDirective(schema, 'inaccessible').addLocations(...inaccessibleLocations);
26
30
  }
27
31
 
28
32
  inaccessibleDirective(schema: Schema): DirectiveDefinition<Record<string, never>> {
29
33
  return this.directive(schema, 'inaccessible')!;
30
34
  }
35
+
36
+ allElementNames(): string[] {
37
+ return ['@inaccessible'];
38
+ }
31
39
  }
32
40
 
33
41
  export const INACCESSIBLE_VERSIONS = new FeatureDefinitions<InaccessibleSpecDefinition>(inaccessibleIdentity)
34
42
  .add(new InaccessibleSpecDefinition(new FeatureVersion(0, 1)));
35
43
 
44
+ registerKnownFeature(INACCESSIBLE_VERSIONS);
45
+
36
46
  export function removeInaccessibleElements(schema: Schema) {
37
47
  const coreFeatures = schema.coreFeatures;
38
48
  if (!coreFeatures) {
@@ -69,10 +79,16 @@ export function removeInaccessibleElements(schema: Schema) {
69
79
  // field type will be `undefined`).
70
80
  if (reference.kind === 'FieldDefinition') {
71
81
  if (!reference.hasAppliedDirective(inaccessibleDirective)) {
72
- throw new GraphQLError(
73
- `Field ${reference.coordinate} returns an @inaccessible type without being marked @inaccessible itself.`,
74
- reference.sourceAST,
75
- );
82
+ // We ship the inaccessible type and it's invalid reference in the extensions so composition can extract those and add proper links
83
+ // in the subgraphs for those elements.
84
+ throw ERRORS.REFERENCED_INACCESSIBLE.err({
85
+ message: `Field "${reference.coordinate}" returns @inaccessible type "${type}" without being marked @inaccessible itself.`,
86
+ nodes: reference.sourceAST,
87
+ extensions: {
88
+ "inaccessible_element": type.coordinate,
89
+ "inaccessible_reference": reference.coordinate,
90
+ }
91
+ });
76
92
  }
77
93
  }
78
94
  // Other references can be:
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from './debug';
10
10
  export * from './coreSpec';
11
11
  export * from './joinSpec';
12
12
  export * from './tagSpec';
13
+ export * from './inaccessibleSpec';
13
14
  export * from './federationSpec';
14
15
  export * from './supergraphs';
15
16
  export * from './extractSubgraphsFromSupergraph';
package/src/joinSpec.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  NonNullType,
9
9
  } from "./definitions";
10
10
  import { Subgraph, Subgraphs } from "./federation";
11
+ import { registerKnownFeature } from './knownCoreFeatures';
11
12
  import { MultiMap } from "./utils";
12
13
 
13
14
  export const joinIdentity = 'https://specs.apollo.dev/join';
@@ -90,6 +91,22 @@ export class JoinSpecDefinition extends FeatureDefinition {
90
91
  }
91
92
  }
92
93
 
94
+ allElementNames(): string[] {
95
+ const names = [
96
+ 'graph',
97
+ 'Graph',
98
+ 'FieldSet',
99
+ '@type',
100
+ '@field',
101
+ ];
102
+ if (this.isV01()) {
103
+ names.push('@owner');
104
+ } else {
105
+ names.push('@implements');
106
+ }
107
+ return names;
108
+ }
109
+
93
110
  populateGraphEnum(schema: Schema, subgraphs: Subgraphs): Map<string, string> {
94
111
  // Duplicate enum values can occur due to sanitization and must be accounted for
95
112
  // collect the duplicates in an array so we can uniquify them in a second pass.
@@ -160,3 +177,5 @@ export class JoinSpecDefinition extends FeatureDefinition {
160
177
  export const JOIN_VERSIONS = new FeatureDefinitions<JoinSpecDefinition>(joinIdentity)
161
178
  .add(new JoinSpecDefinition(new FeatureVersion(0, 1)))
162
179
  .add(new JoinSpecDefinition(new FeatureVersion(0, 2)));
180
+
181
+ registerKnownFeature(JOIN_VERSIONS);
@@ -0,0 +1,13 @@
1
+ import { FeatureDefinition, FeatureDefinitions, FeatureUrl } from "./coreSpec";
2
+
3
+ const registeredFeatures: Map<string, FeatureDefinitions> = new Map();
4
+
5
+ export function registerKnownFeature(definitions: FeatureDefinitions) {
6
+ if (!registeredFeatures.has(definitions.identity)) {
7
+ registeredFeatures.set(definitions.identity, definitions);
8
+ }
9
+ }
10
+
11
+ export function coreFeatureDefinitionIfKnown(url: FeatureUrl): FeatureDefinition | undefined {
12
+ return registeredFeatures.get(url.identity)?.find(url.version);
13
+ }
package/src/tagSpec.ts CHANGED
@@ -2,6 +2,7 @@ import { DirectiveLocation, GraphQLError } from "graphql";
2
2
  import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from "./coreSpec";
3
3
  import { DirectiveDefinition, NonNullType, Schema } from "./definitions";
4
4
  import { ERRORS } from "./error";
5
+ import { registerKnownFeature } from "./knownCoreFeatures";
5
6
  import { sameType } from "./types";
6
7
 
7
8
  export const tagIdentity = 'https://specs.apollo.dev/tag';
@@ -22,6 +23,7 @@ export class TagSpecDefinition extends FeatureDefinition {
22
23
 
23
24
  addElementsToSchema(schema: Schema) {
24
25
  const directive = this.addDirective(schema, 'tag').addLocations(...tagLocations);
26
+ directive.repeatable = true;
25
27
  directive.addArgument("name", new NonNullType(schema.stringType()));
26
28
  }
27
29
 
@@ -42,7 +44,13 @@ export class TagSpecDefinition extends FeatureDefinition {
42
44
  }
43
45
  return undefined;
44
46
  }
47
+
48
+ allElementNames(): string[] {
49
+ return ["@tag"];
50
+ }
45
51
  }
46
52
 
47
53
  export const TAG_VERSIONS = new FeatureDefinitions<TagSpecDefinition>(tagIdentity)
48
54
  .add(new TagSpecDefinition(new FeatureVersion(0, 1)));
55
+
56
+ registerKnownFeature(TAG_VERSIONS);