@apollo/federation-internals 2.1.4 → 2.2.0

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 (53) hide show
  1. package/CHANGELOG.md +7 -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/error.d.ts +1 -0
  6. package/dist/error.d.ts.map +1 -1
  7. package/dist/error.js +2 -0
  8. package/dist/error.js.map +1 -1
  9. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  10. package/dist/extractSubgraphsFromSupergraph.js +16 -2
  11. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  12. package/dist/federation.d.ts +1 -1
  13. package/dist/federation.d.ts.map +1 -1
  14. package/dist/federation.js +76 -34
  15. package/dist/federation.js.map +1 -1
  16. package/dist/federationSpec.d.ts +24 -16
  17. package/dist/federationSpec.d.ts.map +1 -1
  18. package/dist/federationSpec.js +94 -68
  19. package/dist/federationSpec.js.map +1 -1
  20. package/dist/introspection.d.ts +1 -0
  21. package/dist/introspection.d.ts.map +1 -1
  22. package/dist/introspection.js +11 -1
  23. package/dist/introspection.js.map +1 -1
  24. package/dist/operations.d.ts +1 -0
  25. package/dist/operations.d.ts.map +1 -1
  26. package/dist/operations.js +21 -2
  27. package/dist/operations.js.map +1 -1
  28. package/dist/schemaUpgrader.d.ts +7 -1
  29. package/dist/schemaUpgrader.d.ts.map +1 -1
  30. package/dist/schemaUpgrader.js +23 -3
  31. package/dist/schemaUpgrader.js.map +1 -1
  32. package/dist/utils.d.ts +1 -0
  33. package/dist/utils.d.ts.map +1 -1
  34. package/dist/utils.js +13 -1
  35. package/dist/utils.js.map +1 -1
  36. package/dist/validate.js +4 -1
  37. package/dist/validate.js.map +1 -1
  38. package/package.json +3 -3
  39. package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +64 -1
  40. package/src/__tests__/schemaUpgrader.test.ts +3 -3
  41. package/src/__tests__/subgraphValidation.test.ts +60 -1
  42. package/src/buildSchema.ts +9 -0
  43. package/src/error.ts +7 -0
  44. package/src/extractSubgraphsFromSupergraph.ts +17 -2
  45. package/src/federation.ts +109 -49
  46. package/src/federationSpec.ts +102 -75
  47. package/src/introspection.ts +10 -0
  48. package/src/operations.ts +28 -3
  49. package/src/schemaUpgrader.ts +24 -2
  50. package/src/utils.ts +15 -0
  51. package/src/validate.ts +9 -2
  52. package/tsconfig.test.tsbuildinfo +1 -1
  53. 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>[]]) {
@@ -608,7 +642,7 @@ export class FederationMetadata {
608
642
  }
609
643
 
610
644
  private getFederationDirective<TApplicationArgs extends {[key: string]: any}>(
611
- name: string
645
+ name: FederationDirectiveName
612
646
  ): DirectiveDefinition<TApplicationArgs> {
613
647
  const directive = this.schema.directive(this.federationDirectiveNameInSchema(name));
614
648
  assert(directive, `The provided schema does not have federation directive @${name}`);
@@ -616,45 +650,43 @@ export class FederationMetadata {
616
650
  }
617
651
 
618
652
  keyDirective(): DirectiveDefinition<{fields: any, resolvable?: boolean}> {
619
- return this.getFederationDirective(keyDirectiveSpec.name);
653
+ return this.getFederationDirective(FederationDirectiveName.KEY);
620
654
  }
621
655
 
622
656
  overrideDirective(): DirectiveDefinition<{from: string}> {
623
- return this.getFederationDirective(overrideDirectiveSpec.name);
657
+ return this.getFederationDirective(FederationDirectiveName.OVERRIDE);
624
658
  }
625
659
 
626
660
  extendsDirective(): DirectiveDefinition<Record<string, never>> {
627
- return this.getFederationDirective(extendsDirectiveSpec.name);
661
+ return this.getFederationDirective(FederationDirectiveName.EXTENDS);
628
662
  }
629
663
 
630
664
  externalDirective(): DirectiveDefinition<{reason: string}> {
631
- return this.getFederationDirective(externalDirectiveSpec.name);
665
+ return this.getFederationDirective(FederationDirectiveName.EXTERNAL);
632
666
  }
633
667
 
634
668
  requiresDirective(): DirectiveDefinition<{fields: any}> {
635
- return this.getFederationDirective(requiresDirectiveSpec.name);
669
+ return this.getFederationDirective(FederationDirectiveName.REQUIRES);
636
670
  }
637
671
 
638
672
  providesDirective(): DirectiveDefinition<{fields: any}> {
639
- return this.getFederationDirective(providesDirectiveSpec.name);
673
+ return this.getFederationDirective(FederationDirectiveName.PROVIDES);
640
674
  }
641
675
 
642
676
  shareableDirective(): DirectiveDefinition<{}> {
643
- return this.getFederationDirective(shareableDirectiveSpec.name);
677
+ return this.getFederationDirective(FederationDirectiveName.SHAREABLE);
644
678
  }
645
679
 
646
680
  tagDirective(): DirectiveDefinition<{name: string}> {
647
- return this.getFederationDirective(tagSpec.tagDirectiveSpec.name);
681
+ return this.getFederationDirective(FederationDirectiveName.TAG);
648
682
  }
649
683
 
650
684
  composeDirective(): DirectiveDefinition<{name: string}> {
651
- return this.getFederationDirective(composeDirectiveSpec.name);
685
+ return this.getFederationDirective(FederationDirectiveName.COMPOSE_DIRECTIVE);
652
686
  }
653
687
 
654
688
  inaccessibleDirective(): DirectiveDefinition<{}> {
655
- return this.getFederationDirective(
656
- inaccessibleSpec.inaccessibleDirectiveSpec.name
657
- );
689
+ return this.getFederationDirective(FederationDirectiveName.INACCESSIBLE);
658
690
  }
659
691
 
660
692
  allFederationDirectives(): DirectiveDefinition[] {
@@ -685,7 +717,7 @@ export class FederationMetadata {
685
717
  }
686
718
 
687
719
  fieldSetType(): ScalarType {
688
- return this.schema.type(this.federationTypeNameInSchema(fieldSetTypeSpec.name)) as ScalarType;
720
+ return this.schema.type(this.federationTypeNameInSchema(FederationTypeName.FIELD_SET)) as ScalarType;
689
721
  }
690
722
 
691
723
  allFederationTypes(): NamedType[] {
@@ -873,6 +905,28 @@ export class FederationBlueprint extends SchemaBlueprint {
873
905
  validateInterfaceRuntimeImplementationFieldsTypes(itf, metadata, errorCollector);
874
906
  }
875
907
 
908
+ // While @shareable is "repeatable", this is only so one can use it on both a main
909
+ // type definition _and_ possible other type extensions. But putting 2 @shareable
910
+ // on the same type definition or field is both useless, and suggest some miscomprehension,
911
+ // so we reject it with an (hopefully helpful) error message.
912
+ for (const objectType of schema.objectTypes()) {
913
+ validateShareableNotRepeatedOnSameDeclaration(objectType, metadata, errorCollector);
914
+ for (const field of objectType.fields()) {
915
+ validateShareableNotRepeatedOnSameDeclaration(field, metadata, errorCollector);
916
+ }
917
+ }
918
+ // Additionally, reject using @shareable on an interface field, as that does not actually
919
+ // make sense.
920
+ for (const shareableApplication of metadata.shareableDirective().applications()) {
921
+ const element = shareableApplication.parent;
922
+ if (element instanceof FieldDefinition && !isObjectType(element.parent)) {
923
+ errorCollector.push(ERRORS.INVALID_SHAREABLE_USAGE.err(
924
+ `Invalid use of @shareable on field "${element.coordinate}": only object type fields can be marked with @shareable`,
925
+ { nodes: sourceASTs(shareableApplication, element.parent) },
926
+ ));
927
+ }
928
+ }
929
+
876
930
  return errorCollector;
877
931
  }
878
932
 
@@ -883,7 +937,7 @@ export class FederationBlueprint extends SchemaBlueprint {
883
937
  onUnknownDirectiveValidationError(schema: Schema, unknownDirectiveName: string, error: GraphQLError): GraphQLError {
884
938
  const metadata = federationMetadata(schema);
885
939
  assert(metadata, `This method should only have been called on a subgraph schema`)
886
- if (ALL_FEDERATION_DIRECTIVES_DEFAULT_NAMES.includes(unknownDirectiveName)) {
940
+ if (ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES.includes(unknownDirectiveName)) {
887
941
  // The directive name is "unknown" but it is a default federation directive name. So it means one of a few things
888
942
  // happened:
889
943
  // 1. it's a fed1 schema but the directive is a fed2 only one (only possible case for fed1 schema).
@@ -914,7 +968,7 @@ export class FederationBlueprint extends SchemaBlueprint {
914
968
  }
915
969
  } else if (!metadata.isFed2Schema()) {
916
970
  // 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));
971
+ const suggestions = suggestionList(unknownDirectiveName, ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES);
918
972
  if (suggestions.length > 0) {
919
973
  return withModifiedErrorMessage(
920
974
  error,
@@ -971,7 +1025,7 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
971
1025
  core.coreItself.nameInSchema,
972
1026
  {
973
1027
  url: federationSpec.url.toString(),
974
- import: FEDERATION2_SPEC_DIRECTIVES.map((spec) => `@${spec.name}`),
1028
+ import: federationSpec.directiveSpecs().map((spec) => `@${spec.name}`),
975
1029
  }
976
1030
  );
977
1031
  const errors = completeSubgraphSchema(schema);
@@ -982,7 +1036,7 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
982
1036
 
983
1037
  // This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
984
1038
  // 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"])';
1039
+ 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
1040
 
987
1041
  export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
988
1042
  const fed2LinkExtension: SchemaExtensionNode = {
@@ -998,7 +1052,7 @@ export function asFed2SubgraphDocument(document: DocumentNode): DocumentNode {
998
1052
  {
999
1053
  kind: Kind.ARGUMENT,
1000
1054
  name: { kind: Kind.NAME, value: 'import' },
1001
- value: { kind: Kind.LIST, values: FEDERATION2_SPEC_DIRECTIVES.map((spec) => ({ kind: Kind.STRING, value: `@${spec.name}` })) }
1055
+ value: { kind: Kind.LIST, values: federationSpec.directiveSpecs().map((spec) => ({ kind: Kind.STRING, value: `@${spec.name}` })) }
1002
1056
  }]
1003
1057
  }]
1004
1058
  };
@@ -1111,8 +1165,8 @@ function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
1111
1165
  // Note that, in a perfect world, we'd do this within the `SchemaUpgrader`. But the way the code
1112
1166
  // is organised, this method is called before we reach the `SchemaUpgrader`, and it doesn't seem
1113
1167
  // worth refactoring things drastically for that minor convenience.
1114
- for (const spec of [keyDirectiveSpec, providesDirectiveSpec, requiresDirectiveSpec]) {
1115
- const directive = schema.directive(spec.name);
1168
+ for (const name of [FederationDirectiveName.KEY, FederationDirectiveName.PROVIDES, FederationDirectiveName.REQUIRES]) {
1169
+ const directive = schema.directive(name);
1116
1170
  if (!directive) {
1117
1171
  continue;
1118
1172
  }
@@ -1153,15 +1207,9 @@ function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
1153
1207
  }
1154
1208
  }
1155
1209
 
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();
1210
+ return FEDERATION1_TYPES.map((spec) => spec.checkOrAdd(schema, '_' + spec.name))
1211
+ .concat(FEDERATION1_DIRECTIVES.map((spec) => spec.checkOrAdd(schema)))
1212
+ .flat();
1165
1213
  }
1166
1214
 
1167
1215
  function completeFed2SubgraphSchema(schema: Schema) {
@@ -1224,7 +1272,7 @@ export function parseFieldSetArgument({
1224
1272
  if (msg.endsWith('.')) {
1225
1273
  msg = msg.slice(0, msg.length - 1);
1226
1274
  }
1227
- if (directive.name === keyDirectiveSpec.name) {
1275
+ if (directive.name === FederationDirectiveName.KEY) {
1228
1276
  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
1277
  } else {
1230
1278
  msg = msg + ' (if the field is defined in another subgraph, you need to add it to this subgraph with @external).';
@@ -1594,11 +1642,13 @@ class ExternalTester {
1594
1642
  private readonly fakeExternalFields = new Set<string>();
1595
1643
  private readonly providedFields = new Set<string>();
1596
1644
  private readonly externalDirective: DirectiveDefinition<{}>;
1645
+ private readonly externalFieldsOnType = new Set<string>();
1597
1646
 
1598
1647
  constructor(readonly schema: Schema) {
1599
1648
  this.externalDirective = this.metadata().externalDirective();
1600
1649
  this.collectFakeExternals();
1601
1650
  this.collectProvidedFields();
1651
+ this.collectExternalsOnType();
1602
1652
  }
1603
1653
 
1604
1654
  private metadata(): FederationMetadata {
@@ -1637,8 +1687,18 @@ class ExternalTester {
1637
1687
  }
1638
1688
  }
1639
1689
 
1690
+ private collectExternalsOnType() {
1691
+ for (const type of this.schema.objectTypes()) {
1692
+ if (type.hasAppliedDirective(this.externalDirective)) {
1693
+ for (const field of type.fields()) {
1694
+ this.externalFieldsOnType.add(field.coordinate);
1695
+ }
1696
+ }
1697
+ }
1698
+ }
1699
+
1640
1700
  isExternal(field: FieldDefinition<any> | InputFieldDefinition) {
1641
- return field.hasAppliedDirective(this.externalDirective) && !this.isFakeExternal(field);
1701
+ return (field.hasAppliedDirective(this.externalDirective) || this.externalFieldsOnType.has(field.coordinate)) && !this.isFakeExternal(field);
1642
1702
  }
1643
1703
 
1644
1704
  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);
@@ -2,6 +2,16 @@ import { DirectiveLocation } from "graphql";
2
2
  import { EnumType, FieldDefinition, ListType, NonNullType, ObjectType, Schema } from "./definitions";
3
3
 
4
4
  export const introspectionFieldNames = [ '__schema', '__type' ];
5
+ export const introspectionTypeNames = [
6
+ '__Schema',
7
+ '__Directive',
8
+ '__DirectiveLocation',
9
+ '__Type',
10
+ '__Field',
11
+ '__InputValue',
12
+ '__EnumValue',
13
+ '__TypeKind',
14
+ ]
5
15
 
6
16
  export function isIntrospectionName(name: string): boolean {
7
17
  return name.startsWith('__');
package/src/operations.ts CHANGED
@@ -690,10 +690,35 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
690
690
  * @param type - the type at which we're looking at applying the fragment
691
691
  */
692
692
  canApplyAtType(type: CompositeType): boolean {
693
- return (
693
+ const applyAtType =
694
694
  sameType(this.typeCondition, type)
695
- || (isAbstractType(this.typeCondition) && !isUnionType(type) && isDirectSubtype(this.typeCondition, type))
696
- );
695
+ || (isAbstractType(this.typeCondition) && !isUnionType(type) && isDirectSubtype(this.typeCondition, type));
696
+ return applyAtType
697
+ && this.validForSchema(type.schema());
698
+ }
699
+
700
+ // Checks whether this named fragment can be applied to the provided schema, which might be different
701
+ // from the one the named fragment originate from.
702
+ private validForSchema(schema: Schema): boolean {
703
+ if (schema === this.schema()) {
704
+ return true;
705
+ }
706
+
707
+ const typeInSchema = schema.type(this.typeCondition.name);
708
+ if (!typeInSchema || !isCompositeType(typeInSchema)) {
709
+ return false;
710
+ }
711
+
712
+ // We try "rebasing" the selection into the provided schema and checks if that succeed.
713
+ try {
714
+ const rebasedSelection = new SelectionSet(typeInSchema);
715
+ rebasedSelection.mergeIn(this.selectionSet);
716
+ // If this succeed, it means the fragment could be applied to that schema and be valid.
717
+ return true;
718
+ } catch (e) {
719
+ // We don't really care what kind of error was triggered; only that it doesn't work.
720
+ return false;
721
+ }
697
722
  }
698
723
 
699
724
  toString(indent?: string): string {