@apollo/federation-internals 2.1.4 → 2.2.1

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 (57) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/buildSchema.d.ts.map +1 -1
  3. package/dist/buildSchema.js +7 -0
  4. package/dist/buildSchema.js.map +1 -1
  5. package/dist/coreSpec.d.ts.map +1 -1
  6. package/dist/coreSpec.js +1 -1
  7. package/dist/coreSpec.js.map +1 -1
  8. package/dist/error.d.ts +1 -0
  9. package/dist/error.d.ts.map +1 -1
  10. package/dist/error.js +2 -0
  11. package/dist/error.js.map +1 -1
  12. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  13. package/dist/extractSubgraphsFromSupergraph.js +16 -2
  14. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  15. package/dist/federation.d.ts +13 -1
  16. package/dist/federation.d.ts.map +1 -1
  17. package/dist/federation.js +100 -39
  18. package/dist/federation.js.map +1 -1
  19. package/dist/federationSpec.d.ts +24 -16
  20. package/dist/federationSpec.d.ts.map +1 -1
  21. package/dist/federationSpec.js +94 -68
  22. package/dist/federationSpec.js.map +1 -1
  23. package/dist/introspection.d.ts +1 -0
  24. package/dist/introspection.d.ts.map +1 -1
  25. package/dist/introspection.js +11 -1
  26. package/dist/introspection.js.map +1 -1
  27. package/dist/operations.d.ts +1 -0
  28. package/dist/operations.d.ts.map +1 -1
  29. package/dist/operations.js +21 -2
  30. package/dist/operations.js.map +1 -1
  31. package/dist/schemaUpgrader.d.ts +7 -1
  32. package/dist/schemaUpgrader.d.ts.map +1 -1
  33. package/dist/schemaUpgrader.js +23 -3
  34. package/dist/schemaUpgrader.js.map +1 -1
  35. package/dist/utils.d.ts +1 -0
  36. package/dist/utils.d.ts.map +1 -1
  37. package/dist/utils.js +13 -1
  38. package/dist/utils.js.map +1 -1
  39. package/dist/validate.js +4 -1
  40. package/dist/validate.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +64 -1
  43. package/src/__tests__/schemaUpgrader.test.ts +3 -3
  44. package/src/__tests__/subgraphValidation.test.ts +59 -2
  45. package/src/buildSchema.ts +9 -0
  46. package/src/coreSpec.ts +3 -5
  47. package/src/error.ts +7 -0
  48. package/src/extractSubgraphsFromSupergraph.ts +17 -2
  49. package/src/federation.ts +150 -56
  50. package/src/federationSpec.ts +102 -75
  51. package/src/introspection.ts +10 -0
  52. package/src/operations.ts +28 -3
  53. package/src/schemaUpgrader.ts +24 -2
  54. package/src/utils.ts +15 -0
  55. package/src/validate.ts +9 -2
  56. package/tsconfig.test.tsbuildinfo +1 -1
  57. package/tsconfig.tsbuildinfo +1 -1
package/src/federation.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  Directive,
8
8
  DirectiveDefinition,
9
9
  ErrGraphQLValidationFailed,
10
+ Extension,
10
11
  FieldDefinition,
11
12
  InputFieldDefinition,
12
13
  InterfaceType,
@@ -27,7 +28,7 @@ import {
27
28
  sourceASTs,
28
29
  UnionType,
29
30
  } from "./definitions";
30
- import { assert, joinStrings, MultiMap, printHumanReadableList, OrderedMap } from "./utils";
31
+ import { assert, joinStrings, MultiMap, printHumanReadableList, OrderedMap, mapValues } from "./utils";
31
32
  import { SDLValidationRule } from "graphql/validation/ValidationContext";
32
33
  import { specifiedSDLRules } from "graphql/validation/specifiedRules";
33
34
  import {
@@ -46,7 +47,6 @@ import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFed
46
47
  import { buildSchema, buildSchemaFromAST } from "./buildSchema";
47
48
  import { parseSelectionSet, selectionOfElement, SelectionSet } from './operations';
48
49
  import { TAG_VERSIONS } from "./tagSpec";
49
- import { INACCESSIBLE_VERSIONS } from "./inaccessibleSpec";
50
50
  import {
51
51
  errorCodeDef,
52
52
  ErrorCodeDefinition,
@@ -69,18 +69,10 @@ import {
69
69
  import {
70
70
  FEDERATION_VERSIONS,
71
71
  federationIdentity,
72
- fieldSetTypeSpec,
73
- keyDirectiveSpec,
74
- requiresDirectiveSpec,
75
- providesDirectiveSpec,
76
- externalDirectiveSpec,
77
- extendsDirectiveSpec,
78
- shareableDirectiveSpec,
79
- overrideDirectiveSpec,
80
- composeDirectiveSpec,
81
- FEDERATION2_SPEC_DIRECTIVES,
82
- ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES,
83
- FEDERATION2_ONLY_SPEC_DIRECTIVES,
72
+ FederationDirectiveName,
73
+ FederationTypeName,
74
+ FEDERATION1_TYPES,
75
+ FEDERATION1_DIRECTIVES,
84
76
  } from "./federationSpec";
85
77
  import { defaultPrintOptions, PrintOptions as PrintOptions, printSchema } from "./print";
86
78
  import { createObjectTypeSpecification, createScalarTypeSpecification, createUnionTypeSpecification } from "./directiveAndTypeSpecification";
@@ -88,7 +80,6 @@ import { didYouMean, suggestionList } from "./suggestions";
88
80
 
89
81
  const linkSpec = LINK_VERSIONS.latest();
90
82
  const tagSpec = TAG_VERSIONS.latest();
91
- const inaccessibleSpec = INACCESSIBLE_VERSIONS.latest();
92
83
  const federationSpec = FEDERATION_VERSIONS.latest();
93
84
 
94
85
  // 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
@@ -115,6 +106,7 @@ const FEDERATION_SPECIFIC_VALIDATION_RULES = [
115
106
 
116
107
  const FEDERATION_VALIDATION_RULES = specifiedSDLRules.filter(rule => !FEDERATION_OMITTED_VALIDATION_RULES.includes(rule)).concat(FEDERATION_SPECIFIC_VALIDATION_RULES);
117
108
 
109
+ const ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES: string[] = Object.values(FederationDirectiveName);
118
110
 
119
111
  function validateFieldSetSelections({
120
112
  directiveName,
@@ -157,12 +149,12 @@ function validateFieldSetSelections({
157
149
  if (metadata.isFieldFakeExternal(field)) {
158
150
  onError(errorCode.err(
159
151
  `field "${field.coordinate}" should not be part of a @${directiveName} since it is already "effectively" provided by this subgraph `
160
- + `(while it is marked @${externalDirectiveSpec.name}, it is a @${keyDirectiveSpec.name} field of an extension type, which are not internally considered external for historical/backward compatibility reasons)`,
152
+ + `(while it is marked @${FederationDirectiveName.EXTERNAL}, it is a @${FederationDirectiveName.KEY} field of an extension type, which are not internally considered external for historical/backward compatibility reasons)`,
161
153
  { nodes: field.sourceAST }
162
154
  ));
163
155
  } else {
164
156
  onError(errorCode.err(
165
- `field "${field.coordinate}" should not be part of a @${directiveName} since it is already provided by this subgraph (it is not marked @${externalDirectiveSpec.name})`,
157
+ `field "${field.coordinate}" should not be part of a @${directiveName} since it is already provided by this subgraph (it is not marked @${FederationDirectiveName.EXTERNAL})`,
166
158
  { nodes: field.sourceAST }
167
159
  ));
168
160
  }
@@ -488,6 +480,48 @@ function validateInterfaceRuntimeImplementationFieldsTypes(
488
480
  }
489
481
  }
490
482
 
483
+ function validateShareableNotRepeatedOnSameDeclaration(
484
+ element: ObjectType | FieldDefinition<ObjectType>,
485
+ metadata: FederationMetadata,
486
+ errorCollector: GraphQLError[],
487
+ ) {
488
+ const shareableApplications: Directive[] = element.appliedDirectivesOf(metadata.shareableDirective());
489
+ if (shareableApplications.length <= 1) {
490
+ return;
491
+ }
492
+
493
+ type ByExtensions = {
494
+ without: Directive<any, {}>[],
495
+ with: MultiMap<Extension<any>, Directive<any, {}>>,
496
+ };
497
+ const byExtensions = shareableApplications.reduce<ByExtensions>(
498
+ (acc, v) => {
499
+ const ext = v.ofExtension();
500
+ if (ext) {
501
+ acc.with.add(ext, v);
502
+ } else {
503
+ acc.without.push(v);
504
+ }
505
+ return acc;
506
+ },
507
+ { without: [], with: new MultiMap() }
508
+ );
509
+ const groups = [ byExtensions.without ].concat(mapValues(byExtensions.with));
510
+ for (const group of groups) {
511
+ if (group.length > 1) {
512
+ const eltStr = element.kind === 'ObjectType'
513
+ ? `the same type declaration of "${element.coordinate}"`
514
+ : `field "${element.coordinate}"`;
515
+ errorCollector.push(ERRORS.INVALID_SHAREABLE_USAGE.err(
516
+ `Invalid duplicate application of @shareable on ${eltStr}: `
517
+ + '@shareable is only repeatable on types so it can be used simultaneously on a type definition and its extensions, but it should not be duplicated on the same definition/extension declaration',
518
+ { nodes: sourceASTs(...group) },
519
+ ));
520
+ }
521
+ }
522
+ }
523
+
524
+
491
525
  const printFieldCoordinate = (f: FieldDefinition<CompositeType>): string => `"${f.coordinate}"`;
492
526
 
493
527
  function formatFieldsToReturnType([type, implems]: [string, FieldDefinition<ObjectType>[]]) {
@@ -607,54 +641,66 @@ export class FederationMetadata {
607
641
  }
608
642
  }
609
643
 
610
- private getFederationDirective<TApplicationArgs extends {[key: string]: any}>(
611
- name: string
644
+ // Should only be be called for "legacy" directives, those that existed in 2.0. This
645
+ // allow to avoiding have to double-check the directive exists every time when we
646
+ // know it will always exists (note that even though we accept fed1 schema as inputs,
647
+ // those are almost immediately converted to fed2 ones by the `SchemaUpgrader`, so
648
+ // we include @shareable or @override in those "legacy" directives).
649
+ private getLegacyFederationDirective<TApplicationArgs extends {[key: string]: any}>(
650
+ name: FederationDirectiveName
612
651
  ): DirectiveDefinition<TApplicationArgs> {
613
- const directive = this.schema.directive(this.federationDirectiveNameInSchema(name));
652
+ const directive = this.getFederationDirective<TApplicationArgs>(name);
614
653
  assert(directive, `The provided schema does not have federation directive @${name}`);
615
- return directive as DirectiveDefinition<TApplicationArgs>;
654
+ return directive;
655
+ }
656
+
657
+ private getFederationDirective<TApplicationArgs extends {[key: string]: any}>(
658
+ name: FederationDirectiveName
659
+ ): DirectiveDefinition<TApplicationArgs> | undefined {
660
+ return this.schema.directive(this.federationDirectiveNameInSchema(name)) as DirectiveDefinition<TApplicationArgs> | undefined;
616
661
  }
617
662
 
618
663
  keyDirective(): DirectiveDefinition<{fields: any, resolvable?: boolean}> {
619
- return this.getFederationDirective(keyDirectiveSpec.name);
664
+ return this.getLegacyFederationDirective(FederationDirectiveName.KEY);
620
665
  }
621
666
 
622
667
  overrideDirective(): DirectiveDefinition<{from: string}> {
623
- return this.getFederationDirective(overrideDirectiveSpec.name);
668
+ return this.getLegacyFederationDirective(FederationDirectiveName.OVERRIDE);
624
669
  }
625
670
 
626
671
  extendsDirective(): DirectiveDefinition<Record<string, never>> {
627
- return this.getFederationDirective(extendsDirectiveSpec.name);
672
+ return this.getLegacyFederationDirective(FederationDirectiveName.EXTENDS);
628
673
  }
629
674
 
630
675
  externalDirective(): DirectiveDefinition<{reason: string}> {
631
- return this.getFederationDirective(externalDirectiveSpec.name);
676
+ return this.getLegacyFederationDirective(FederationDirectiveName.EXTERNAL);
632
677
  }
633
678
 
634
679
  requiresDirective(): DirectiveDefinition<{fields: any}> {
635
- return this.getFederationDirective(requiresDirectiveSpec.name);
680
+ return this.getLegacyFederationDirective(FederationDirectiveName.REQUIRES);
636
681
  }
637
682
 
638
683
  providesDirective(): DirectiveDefinition<{fields: any}> {
639
- return this.getFederationDirective(providesDirectiveSpec.name);
684
+ return this.getLegacyFederationDirective(FederationDirectiveName.PROVIDES);
640
685
  }
641
686
 
642
687
  shareableDirective(): DirectiveDefinition<{}> {
643
- return this.getFederationDirective(shareableDirectiveSpec.name);
688
+ return this.getLegacyFederationDirective(FederationDirectiveName.SHAREABLE);
644
689
  }
645
690
 
646
691
  tagDirective(): DirectiveDefinition<{name: string}> {
647
- return this.getFederationDirective(tagSpec.tagDirectiveSpec.name);
692
+ return this.getLegacyFederationDirective(FederationDirectiveName.TAG);
648
693
  }
649
694
 
650
- composeDirective(): DirectiveDefinition<{name: string}> {
651
- return this.getFederationDirective(composeDirectiveSpec.name);
695
+ composeDirective(): DirectiveDefinition<{name: string}> | FederationDirectiveNotDefinedInSchema<{name: string}> {
696
+ return this.getFederationDirective<{name: string}>(FederationDirectiveName.COMPOSE_DIRECTIVE) ?? {
697
+ name: FederationDirectiveName.COMPOSE_DIRECTIVE,
698
+ applications: () => new Array<Directive<any, {name: string}>>(),
699
+ };
652
700
  }
653
701
 
654
702
  inaccessibleDirective(): DirectiveDefinition<{}> {
655
- return this.getFederationDirective(
656
- inaccessibleSpec.inaccessibleDirectiveSpec.name
657
- );
703
+ return this.getLegacyFederationDirective(FederationDirectiveName.INACCESSIBLE);
658
704
  }
659
705
 
660
706
  allFederationDirectives(): DirectiveDefinition[] {
@@ -666,9 +712,18 @@ export class FederationMetadata {
666
712
  this.tagDirective(),
667
713
  this.extendsDirective(),
668
714
  ];
669
- return this.isFed2Schema()
670
- ? baseDirectives.concat(this.shareableDirective(), this.inaccessibleDirective(), this.overrideDirective(), this.composeDirective())
671
- : baseDirectives;
715
+ if (!this.isFed2Schema()) {
716
+ return baseDirectives;
717
+ }
718
+
719
+ baseDirectives.push(this.shareableDirective());
720
+ baseDirectives.push(this.inaccessibleDirective());
721
+ baseDirectives.push(this.overrideDirective());
722
+ const composeDirective = this.composeDirective();
723
+ if (isFederationDirectiveDefinedInSchema(composeDirective)) {
724
+ baseDirectives.push(composeDirective);
725
+ }
726
+ return baseDirectives;
672
727
  }
673
728
 
674
729
  // Note that a subgraph may have no "entities" and so no _EntityType.
@@ -685,7 +740,7 @@ export class FederationMetadata {
685
740
  }
686
741
 
687
742
  fieldSetType(): ScalarType {
688
- return this.schema.type(this.federationTypeNameInSchema(fieldSetTypeSpec.name)) as ScalarType;
743
+ return this.schema.type(this.federationTypeNameInSchema(FederationTypeName.FIELD_SET)) as ScalarType;
689
744
  }
690
745
 
691
746
  allFederationTypes(): NamedType[] {
@@ -702,6 +757,17 @@ export class FederationMetadata {
702
757
  }
703
758
  }
704
759
 
760
+ export type FederationDirectiveNotDefinedInSchema<TApplicationArgs extends {[key: string]: any}> = {
761
+ name: string,
762
+ applications: () => readonly Directive<any, TApplicationArgs>[],
763
+ }
764
+
765
+ export function isFederationDirectiveDefinedInSchema<TApplicationArgs extends {[key: string]: any}>(
766
+ definition: DirectiveDefinition<TApplicationArgs> | FederationDirectiveNotDefinedInSchema<TApplicationArgs>
767
+ ): definition is DirectiveDefinition<TApplicationArgs> {
768
+ return definition instanceof DirectiveDefinition;
769
+ }
770
+
705
771
  export class FederationBlueprint extends SchemaBlueprint {
706
772
  constructor(private readonly withRootTypeRenaming: boolean) {
707
773
  super();
@@ -873,6 +939,28 @@ export class FederationBlueprint extends SchemaBlueprint {
873
939
  validateInterfaceRuntimeImplementationFieldsTypes(itf, metadata, errorCollector);
874
940
  }
875
941
 
942
+ // While @shareable is "repeatable", this is only so one can use it on both a main
943
+ // type definition _and_ possible other type extensions. But putting 2 @shareable
944
+ // on the same type definition or field is both useless, and suggest some miscomprehension,
945
+ // so we reject it with an (hopefully helpful) error message.
946
+ for (const objectType of schema.objectTypes()) {
947
+ validateShareableNotRepeatedOnSameDeclaration(objectType, metadata, errorCollector);
948
+ for (const field of objectType.fields()) {
949
+ validateShareableNotRepeatedOnSameDeclaration(field, metadata, errorCollector);
950
+ }
951
+ }
952
+ // Additionally, reject using @shareable on an interface field, as that does not actually
953
+ // make sense.
954
+ for (const shareableApplication of metadata.shareableDirective().applications()) {
955
+ const element = shareableApplication.parent;
956
+ if (element instanceof FieldDefinition && !isObjectType(element.parent)) {
957
+ errorCollector.push(ERRORS.INVALID_SHAREABLE_USAGE.err(
958
+ `Invalid use of @shareable on field "${element.coordinate}": only object type fields can be marked with @shareable`,
959
+ { nodes: sourceASTs(shareableApplication, element.parent) },
960
+ ));
961
+ }
962
+ }
963
+
876
964
  return errorCollector;
877
965
  }
878
966
 
@@ -883,7 +971,7 @@ export class FederationBlueprint extends SchemaBlueprint {
883
971
  onUnknownDirectiveValidationError(schema: Schema, unknownDirectiveName: string, error: GraphQLError): GraphQLError {
884
972
  const metadata = federationMetadata(schema);
885
973
  assert(metadata, `This method should only have been called on a subgraph schema`)
886
- if (ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES.includes(unknownDirectiveName)) {
974
+ if (ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES.includes(unknownDirectiveName)) {
887
975
  // The directive name is "unknown" but it is a default federation directive name. So it means one of a few things
888
976
  // happened:
889
977
  // 1. it's a fed1 schema but the directive is a fed2 only one (only possible case for fed1 schema).
@@ -914,7 +1002,7 @@ export class FederationBlueprint extends SchemaBlueprint {
914
1002
  }
915
1003
  } else if (!metadata.isFed2Schema()) {
916
1004
  // We could get here in the case where a fed1 schema has tried to use a fed2 directive but mispelled it.
917
- const suggestions = suggestionList(unknownDirectiveName, FEDERATION2_ONLY_SPEC_DIRECTIVES.map((spec) => spec.name));
1005
+ const suggestions = suggestionList(unknownDirectiveName, ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES);
918
1006
  if (suggestions.length > 0) {
919
1007
  return withModifiedErrorMessage(
920
1008
  error,
@@ -971,7 +1059,7 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
971
1059
  core.coreItself.nameInSchema,
972
1060
  {
973
1061
  url: federationSpec.url.toString(),
974
- import: FEDERATION2_SPEC_DIRECTIVES.map((spec) => `@${spec.name}`),
1062
+ import: federationSpec.directiveSpecs().map((spec) => `@${spec.name}`),
975
1063
  }
976
1064
  );
977
1065
  const errors = completeSubgraphSchema(schema);
@@ -982,7 +1070,7 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
982
1070
 
983
1071
  // This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
984
1072
  // subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
985
- export const FEDERATION2_LINK_WTH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.1", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective"])';
1073
+ export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.2", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective"])';
986
1074
 
987
1075
  export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
988
1076
  const fed2LinkExtension: SchemaExtensionNode = {
@@ -998,7 +1086,7 @@ export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
998
1086
  {
999
1087
  kind: Kind.ARGUMENT,
1000
1088
  name: { kind: Kind.NAME, value: 'import' },
1001
- value: { kind: Kind.LIST, values: FEDERATION2_SPEC_DIRECTIVES.map((spec) => ({ kind: Kind.STRING, value: `@${spec.name}` })) }
1089
+ value: { kind: Kind.LIST, values: federationSpec.directiveSpecs().map((spec) => ({ kind: Kind.STRING, value: `@${spec.name}` })) }
1002
1090
  }]
1003
1091
  }]
1004
1092
  };
@@ -1111,8 +1199,8 @@ function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
1111
1199
  // Note that, in a perfect world, we'd do this within the `SchemaUpgrader`. But the way the code
1112
1200
  // is organised, this method is called before we reach the `SchemaUpgrader`, and it doesn't seem
1113
1201
  // worth refactoring things drastically for that minor convenience.
1114
- for (const spec of [keyDirectiveSpec, providesDirectiveSpec, requiresDirectiveSpec]) {
1115
- const directive = schema.directive(spec.name);
1202
+ for (const name of [FederationDirectiveName.KEY, FederationDirectiveName.PROVIDES, FederationDirectiveName.REQUIRES]) {
1203
+ const directive = schema.directive(name);
1116
1204
  if (!directive) {
1117
1205
  continue;
1118
1206
  }
@@ -1153,15 +1241,9 @@ function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
1153
1241
  }
1154
1242
  }
1155
1243
 
1156
- return [
1157
- fieldSetTypeSpec.checkOrAdd(schema, '_' + fieldSetTypeSpec.name),
1158
- keyDirectiveSpec.checkOrAdd(schema),
1159
- requiresDirectiveSpec.checkOrAdd(schema),
1160
- providesDirectiveSpec.checkOrAdd(schema),
1161
- extendsDirectiveSpec.checkOrAdd(schema),
1162
- externalDirectiveSpec.checkOrAdd(schema),
1163
- tagSpec.tagDirectiveSpec.checkOrAdd(schema),
1164
- ].flat();
1244
+ return FEDERATION1_TYPES.map((spec) => spec.checkOrAdd(schema, '_' + spec.name))
1245
+ .concat(FEDERATION1_DIRECTIVES.map((spec) => spec.checkOrAdd(schema)))
1246
+ .flat();
1165
1247
  }
1166
1248
 
1167
1249
  function completeFed2SubgraphSchema(schema: Schema) {
@@ -1224,7 +1306,7 @@ export function parseFieldSetArgument({
1224
1306
  if (msg.endsWith('.')) {
1225
1307
  msg = msg.slice(0, msg.length - 1);
1226
1308
  }
1227
- if (directive.name === keyDirectiveSpec.name) {
1309
+ if (directive.name === FederationDirectiveName.KEY) {
1228
1310
  msg = msg + ' (the field should 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).';
1229
1311
  } else {
1230
1312
  msg = msg + ' (if the field is defined in another subgraph, you need to add it to this subgraph with @external).';
@@ -1594,11 +1676,13 @@ class ExternalTester {
1594
1676
  private readonly fakeExternalFields = new Set<string>();
1595
1677
  private readonly providedFields = new Set<string>();
1596
1678
  private readonly externalDirective: DirectiveDefinition<{}>;
1679
+ private readonly externalFieldsOnType = new Set<string>();
1597
1680
 
1598
1681
  constructor(readonly schema: Schema) {
1599
1682
  this.externalDirective = this.metadata().externalDirective();
1600
1683
  this.collectFakeExternals();
1601
1684
  this.collectProvidedFields();
1685
+ this.collectExternalsOnType();
1602
1686
  }
1603
1687
 
1604
1688
  private metadata(): FederationMetadata {
@@ -1637,8 +1721,18 @@ class ExternalTester {
1637
1721
  }
1638
1722
  }
1639
1723
 
1724
+ private collectExternalsOnType() {
1725
+ for (const type of this.schema.objectTypes()) {
1726
+ if (type.hasAppliedDirective(this.externalDirective)) {
1727
+ for (const field of type.fields()) {
1728
+ this.externalFieldsOnType.add(field.coordinate);
1729
+ }
1730
+ }
1731
+ }
1732
+ }
1733
+
1640
1734
  isExternal(field: FieldDefinition<any> | InputFieldDefinition) {
1641
- return field.hasAppliedDirective(this.externalDirective) && !this.isFakeExternal(field);
1735
+ return (field.hasAppliedDirective(this.externalDirective) || this.externalFieldsOnType.has(field.coordinate)) && !this.isFakeExternal(field);
1642
1736
  }
1643
1737
 
1644
1738
  isFakeExternal(field: FieldDefinition<any> | InputFieldDefinition) {
@@ -9,9 +9,10 @@ import {
9
9
  createDirectiveSpecification,
10
10
  createScalarTypeSpecification,
11
11
  DirectiveSpecification,
12
+ TypeSpecification,
12
13
  } from "./directiveAndTypeSpecification";
13
14
  import { DirectiveLocation, GraphQLError } from "graphql";
14
- import { assert } from "./utils";
15
+ import { assert, MapWithCachedArrays } from "./utils";
15
16
  import { TAG_VERSIONS } from "./tagSpec";
16
17
  import { federationMetadata } from "./federation";
17
18
  import { registerKnownFeature } from "./knownCoreFeatures";
@@ -19,10 +20,27 @@ import { INACCESSIBLE_VERSIONS } from "./inaccessibleSpec";
19
20
 
20
21
  export const federationIdentity = 'https://specs.apollo.dev/federation';
21
22
 
22
- export const fieldSetTypeSpec = createScalarTypeSpecification({ name: 'FieldSet' });
23
+ export enum FederationTypeName {
24
+ FIELD_SET = 'FieldSet',
25
+ }
26
+
27
+ export enum FederationDirectiveName {
28
+ KEY = 'key',
29
+ EXTERNAL = 'external',
30
+ REQUIRES = 'requires',
31
+ PROVIDES = 'provides',
32
+ EXTENDS = 'extends',
33
+ SHAREABLE = 'shareable',
34
+ OVERRIDE = 'override',
35
+ TAG = 'tag',
36
+ INACCESSIBLE = 'inaccessible',
37
+ COMPOSE_DIRECTIVE = 'composeDirective',
38
+ }
23
39
 
24
- export const keyDirectiveSpec = createDirectiveSpecification({
25
- name:'key',
40
+ const fieldSetTypeSpec = createScalarTypeSpecification({ name: FederationTypeName.FIELD_SET });
41
+
42
+ const keyDirectiveSpec = createDirectiveSpecification({
43
+ name: FederationDirectiveName.KEY,
26
44
  locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE],
27
45
  repeatable: true,
28
46
  argumentFct: (schema) => ({
@@ -34,13 +52,13 @@ export const keyDirectiveSpec = createDirectiveSpecification({
34
52
  }),
35
53
  });
36
54
 
37
- export const extendsDirectiveSpec = createDirectiveSpecification({
38
- name:'extends',
55
+ const extendsDirectiveSpec = createDirectiveSpecification({
56
+ name: FederationDirectiveName.EXTENDS,
39
57
  locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE],
40
58
  });
41
59
 
42
- export const externalDirectiveSpec = createDirectiveSpecification({
43
- name:'external',
60
+ const externalDirectiveSpec = createDirectiveSpecification({
61
+ name: FederationDirectiveName.EXTERNAL,
44
62
  locations: [DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION],
45
63
  argumentFct: (schema) => ({
46
64
  args: [{ name: 'reason', type: schema.stringType() }],
@@ -48,8 +66,8 @@ export const externalDirectiveSpec = createDirectiveSpecification({
48
66
  }),
49
67
  });
50
68
 
51
- export const requiresDirectiveSpec = createDirectiveSpecification({
52
- name:'requires',
69
+ const requiresDirectiveSpec = createDirectiveSpecification({
70
+ name: FederationDirectiveName.REQUIRES,
53
71
  locations: [DirectiveLocation.FIELD_DEFINITION],
54
72
  argumentFct: (schema) => ({
55
73
  args: [fieldsArgument(schema)],
@@ -57,8 +75,8 @@ export const requiresDirectiveSpec = createDirectiveSpecification({
57
75
  }),
58
76
  });
59
77
 
60
- export const providesDirectiveSpec = createDirectiveSpecification({
61
- name:'provides',
78
+ const providesDirectiveSpec = createDirectiveSpecification({
79
+ name: FederationDirectiveName.PROVIDES,
62
80
  locations: [DirectiveLocation.FIELD_DEFINITION],
63
81
  argumentFct: (schema) => ({
64
82
  args: [fieldsArgument(schema)],
@@ -66,29 +84,21 @@ export const providesDirectiveSpec = createDirectiveSpecification({
66
84
  }),
67
85
  });
68
86
 
69
- export const shareableDirectiveSpec = createDirectiveSpecification({
70
- name: 'shareable',
71
- locations: [DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION],
72
- });
87
+ const legacyFederationTypes = [
88
+ fieldSetTypeSpec,
89
+ ];
73
90
 
74
- export const overrideDirectiveSpec = createDirectiveSpecification({
75
- name: 'override',
76
- locations: [DirectiveLocation.FIELD_DEFINITION],
77
- argumentFct: (schema) => ({
78
- args: [{ name: 'from', type: new NonNullType(schema.stringType()) }],
79
- errors: [],
80
- }),
81
- });
91
+ const legacyFederationDirectives = [
92
+ keyDirectiveSpec,
93
+ requiresDirectiveSpec,
94
+ providesDirectiveSpec,
95
+ externalDirectiveSpec,
96
+ TAG_VERSIONS.latest().tagDirectiveSpec,
97
+ extendsDirectiveSpec,
98
+ ];
82
99
 
83
- export const composeDirectiveSpec = createDirectiveSpecification({
84
- name: 'composeDirective',
85
- locations: [DirectiveLocation.SCHEMA],
86
- repeatable: true,
87
- argumentFct: (schema) => ({
88
- args: [{ name: 'name', type: schema.stringType() }],
89
- errors: [],
90
- }),
91
- })
100
+ export const FEDERATION1_TYPES = legacyFederationTypes;
101
+ export const FEDERATION1_DIRECTIVES = legacyFederationDirectives;
92
102
 
93
103
  function fieldsArgument(schema: Schema): ArgumentSpecification {
94
104
  return { name: 'fields', type: fieldSetType(schema) };
@@ -100,50 +110,65 @@ function fieldSetType(schema: Schema): InputType {
100
110
  return new NonNullType(metadata.fieldSetType());
101
111
  }
102
112
 
103
- export const FEDERATION2_ONLY_SPEC_DIRECTIVES = [
104
- shareableDirectiveSpec,
105
- INACCESSIBLE_VERSIONS.latest().inaccessibleDirectiveSpec,
106
- overrideDirectiveSpec,
107
- ];
113
+ export class FederationSpecDefinition extends FeatureDefinition {
114
+ private readonly _directiveSpecs = new MapWithCachedArrays<string, DirectiveSpecification>();
115
+ private readonly _typeSpecs = new MapWithCachedArrays<string, TypeSpecification>();
108
116
 
109
- export const FEDERATION2_1_ONLY_SPEC_DIRECTIVES = [
110
- composeDirectiveSpec,
111
- ];
117
+ constructor(version: FeatureVersion) {
118
+ super(new FeatureUrl(federationIdentity, 'federation', version));
112
119
 
113
- const PRE_FEDERATION2_SPEC_DIRECTIVES = [
114
- keyDirectiveSpec,
115
- requiresDirectiveSpec,
116
- providesDirectiveSpec,
117
- externalDirectiveSpec,
118
- TAG_VERSIONS.latest().tagDirectiveSpec,
119
- extendsDirectiveSpec, // TODO: should we stop supporting that?
120
- ];
120
+ for (const type of legacyFederationTypes) {
121
+ this.registerType(type);
122
+ }
121
123
 
122
- // Note that this is only used for federation 2+ (federation 1 adds the same directive, but not through a core spec).
123
- export const FEDERATION2_SPEC_DIRECTIVES = [
124
- ...PRE_FEDERATION2_SPEC_DIRECTIVES,
125
- ...FEDERATION2_ONLY_SPEC_DIRECTIVES,
126
- ...FEDERATION2_1_ONLY_SPEC_DIRECTIVES,
127
- ];
124
+ for (const directive of legacyFederationDirectives) {
125
+ this.registerDirective(directive);
126
+ }
128
127
 
129
- // Note that this is meant to contain _all_ federation directive names ever supported, regardless of which version.
130
- // But currently, fed2 directives are a superset of fed1's so ... (but this may change if we stop supporting `@extends`
131
- // in fed2).
132
- export const ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES = FEDERATION2_SPEC_DIRECTIVES.map((spec) => spec.name);
128
+ this.registerDirective(createDirectiveSpecification({
129
+ name: FederationDirectiveName.SHAREABLE,
130
+ locations: [DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION],
131
+ repeatable: version >= (new FeatureVersion(2, 2)),
132
+ }));
133
+
134
+ this.registerDirective(INACCESSIBLE_VERSIONS.latest().inaccessibleDirectiveSpec);
135
+
136
+ this.registerDirective(createDirectiveSpecification({
137
+ name: FederationDirectiveName.OVERRIDE,
138
+ locations: [DirectiveLocation.FIELD_DEFINITION],
139
+ argumentFct: (schema) => ({
140
+ args: [{ name: 'from', type: new NonNullType(schema.stringType()) }],
141
+ errors: [],
142
+ }),
143
+ }));
144
+
145
+ if (version >= (new FeatureVersion(2, 1))) {
146
+ this.registerDirective(createDirectiveSpecification({
147
+ name: FederationDirectiveName.COMPOSE_DIRECTIVE,
148
+ locations: [DirectiveLocation.SCHEMA],
149
+ repeatable: true,
150
+ argumentFct: (schema) => ({
151
+ args: [{ name: 'name', type: schema.stringType() }],
152
+ errors: [],
153
+ }),
154
+ }));
155
+ }
156
+ }
133
157
 
134
- export const FEDERATION_SPEC_TYPES = [
135
- fieldSetTypeSpec,
136
- ]
158
+ private registerDirective(spec: DirectiveSpecification) {
159
+ this._directiveSpecs.set(spec.name, spec);
160
+ }
137
161
 
138
- export class FederationSpecDefinition extends FeatureDefinition {
139
- constructor(version: FeatureVersion) {
140
- super(new FeatureUrl(federationIdentity, 'federation', version));
162
+ private registerType(spec: TypeSpecification) {
163
+ this._typeSpecs.set(spec.name, spec);
141
164
  }
142
165
 
143
- private allFedDirectives(): DirectiveSpecification[] {
144
- return PRE_FEDERATION2_SPEC_DIRECTIVES
145
- .concat(FEDERATION2_ONLY_SPEC_DIRECTIVES)
146
- .concat(this.url.version >= (new FeatureVersion(2, 1)) ? FEDERATION2_1_ONLY_SPEC_DIRECTIVES : []);
166
+ directiveSpecs(): readonly DirectiveSpecification[] {
167
+ return this._directiveSpecs.values();
168
+ }
169
+
170
+ typeSpecs(): readonly TypeSpecification[] {
171
+ return this._typeSpecs.values();
147
172
  }
148
173
 
149
174
  addElementsToSchema(schema: Schema): GraphQLError[] {
@@ -151,23 +176,25 @@ export class FederationSpecDefinition extends FeatureDefinition {
151
176
  assert(feature, 'The federation specification should have been added to the schema before this is called');
152
177
 
153
178
  let errors: GraphQLError[] = [];
154
- errors = errors.concat(this.addTypeSpec(schema, fieldSetTypeSpec));
179
+ for (const type of this.typeSpecs()) {
180
+ errors = errors.concat(this.addTypeSpec(schema, type));
181
+ }
155
182
 
156
- for (const directive of this.allFedDirectives()) {
183
+ for (const directive of this.directiveSpecs()) {
157
184
  errors = errors.concat(this.addDirectiveSpec(schema, directive));
158
185
  }
159
186
  return errors;
160
187
  }
161
188
 
162
189
  allElementNames(): string[] {
163
- return this.allFedDirectives().map((spec) => `@${spec.name}`).concat([
164
- fieldSetTypeSpec.name,
165
- ])
190
+ return this.directiveSpecs().map((spec) => `@${spec.name}`)
191
+ .concat(this.typeSpecs().map((spec) => spec.name));
166
192
  }
167
193
  }
168
194
 
169
195
  export const FEDERATION_VERSIONS = new FeatureDefinitions<FederationSpecDefinition>(federationIdentity)
170
196
  .add(new FederationSpecDefinition(new FeatureVersion(2, 0)))
171
- .add(new FederationSpecDefinition(new FeatureVersion(2, 1)));
197
+ .add(new FederationSpecDefinition(new FeatureVersion(2, 1)))
198
+ .add(new FederationSpecDefinition(new FeatureVersion(2, 2)));
172
199
 
173
200
  registerKnownFeature(FEDERATION_VERSIONS);