@apollo/federation-internals 2.4.10 → 2.5.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 (79) hide show
  1. package/dist/argumentCompositionStrategies.d.ts +12 -7
  2. package/dist/argumentCompositionStrategies.d.ts.map +1 -1
  3. package/dist/argumentCompositionStrategies.js +26 -7
  4. package/dist/argumentCompositionStrategies.js.map +1 -1
  5. package/dist/authenticatedSpec.d.ts +13 -0
  6. package/dist/authenticatedSpec.d.ts.map +1 -0
  7. package/dist/authenticatedSpec.js +36 -0
  8. package/dist/authenticatedSpec.js.map +1 -0
  9. package/dist/coreSpec.d.ts +6 -5
  10. package/dist/coreSpec.d.ts.map +1 -1
  11. package/dist/coreSpec.js +42 -32
  12. package/dist/coreSpec.js.map +1 -1
  13. package/dist/definitions.d.ts +2 -3
  14. package/dist/definitions.d.ts.map +1 -1
  15. package/dist/definitions.js +12 -7
  16. package/dist/definitions.js.map +1 -1
  17. package/dist/directiveAndTypeSpecification.d.ts +8 -8
  18. package/dist/directiveAndTypeSpecification.d.ts.map +1 -1
  19. package/dist/directiveAndTypeSpecification.js +21 -16
  20. package/dist/directiveAndTypeSpecification.js.map +1 -1
  21. package/dist/error.d.ts +1 -1
  22. package/dist/extractSubgraphsFromSupergraph.d.ts +1 -1
  23. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  24. package/dist/extractSubgraphsFromSupergraph.js +450 -295
  25. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  26. package/dist/federation.d.ts +10 -5
  27. package/dist/federation.d.ts.map +1 -1
  28. package/dist/federation.js +58 -16
  29. package/dist/federation.js.map +1 -1
  30. package/dist/federationSpec.d.ts +3 -1
  31. package/dist/federationSpec.d.ts.map +1 -1
  32. package/dist/federationSpec.js +12 -3
  33. package/dist/federationSpec.js.map +1 -1
  34. package/dist/inaccessibleSpec.d.ts +1 -1
  35. package/dist/inaccessibleSpec.d.ts.map +1 -1
  36. package/dist/inaccessibleSpec.js +4 -4
  37. package/dist/inaccessibleSpec.js.map +1 -1
  38. package/dist/index.d.ts +2 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +2 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/joinSpec.d.ts +19 -17
  43. package/dist/joinSpec.d.ts.map +1 -1
  44. package/dist/joinSpec.js +3 -3
  45. package/dist/joinSpec.js.map +1 -1
  46. package/dist/requiresScopesSpec.d.ts +16 -0
  47. package/dist/requiresScopesSpec.d.ts.map +1 -0
  48. package/dist/requiresScopesSpec.js +55 -0
  49. package/dist/requiresScopesSpec.js.map +1 -0
  50. package/dist/supergraphs.d.ts +19 -4
  51. package/dist/supergraphs.d.ts.map +1 -1
  52. package/dist/supergraphs.js +40 -14
  53. package/dist/supergraphs.js.map +1 -1
  54. package/dist/tagSpec.d.ts +1 -1
  55. package/dist/tagSpec.d.ts.map +1 -1
  56. package/dist/tagSpec.js +4 -4
  57. package/dist/tagSpec.js.map +1 -1
  58. package/dist/utils.d.ts +1 -0
  59. package/dist/utils.d.ts.map +1 -1
  60. package/dist/utils.js +11 -1
  61. package/dist/utils.js.map +1 -1
  62. package/package.json +1 -1
  63. package/src/argumentCompositionStrategies.ts +32 -9
  64. package/src/authenticatedSpec.ts +62 -0
  65. package/src/coreSpec.ts +57 -35
  66. package/src/definitions.ts +22 -9
  67. package/src/directiveAndTypeSpecification.ts +25 -24
  68. package/src/error.ts +2 -2
  69. package/src/extractSubgraphsFromSupergraph.ts +647 -393
  70. package/src/federation.ts +95 -16
  71. package/src/federationSpec.ts +13 -5
  72. package/src/inaccessibleSpec.ts +4 -4
  73. package/src/index.ts +2 -1
  74. package/src/joinSpec.ts +23 -13
  75. package/src/precompute.ts +1 -1
  76. package/src/requiresScopesSpec.ts +76 -0
  77. package/src/supergraphs.ts +64 -16
  78. package/src/tagSpec.ts +4 -4
  79. package/src/utils.ts +10 -0
package/src/federation.ts CHANGED
@@ -68,6 +68,9 @@ import {
68
68
  linkDirectiveDefaultName,
69
69
  linkIdentity,
70
70
  FeatureUrl,
71
+ CoreImport,
72
+ extractCoreFeatureImports,
73
+ CoreOrLinkDirectiveArgs,
71
74
  } from "./coreSpec";
72
75
  import {
73
76
  FEDERATION_VERSIONS,
@@ -86,6 +89,9 @@ import { joinIdentity } from "./joinSpec";
86
89
  const linkSpec = LINK_VERSIONS.latest();
87
90
  const tagSpec = TAG_VERSIONS.latest();
88
91
  const federationSpec = FEDERATION_VERSIONS.latest();
92
+ // Some users rely on auto-expanding fed v1 graphs with fed v2 directives. While technically we should only expand @tag
93
+ // directive from v2 definitions, we will continue expanding other directives (up to v2.4) to ensure backwards compatibility.
94
+ const autoExpandedFederationSpec = FEDERATION_VERSIONS.find(new FeatureVersion(2, 4))!;
89
95
 
90
96
  // 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
91
97
  // in the "federated query graph" without worrying about conflict (see `FEDERATED_GRAPH_ROOT_SOURCE` in `querygraph.ts`).
@@ -112,6 +118,24 @@ const FEDERATION_SPECIFIC_VALIDATION_RULES = [
112
118
  const FEDERATION_VALIDATION_RULES = specifiedSDLRules.filter(rule => !FEDERATION_OMITTED_VALIDATION_RULES.includes(rule)).concat(FEDERATION_SPECIFIC_VALIDATION_RULES);
113
119
 
114
120
  const ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES: string[] = Object.values(FederationDirectiveName);
121
+
122
+ /**
123
+ * Federation 1 has that specificity that it wasn't using @link to name-space federation elements,
124
+ * and so to "distinguish" the few federation type names, it prefixed those with a `_`. That is,
125
+ * the `FieldSet` type was named `_FieldSet` in federation1. To handle this without too much effort,
126
+ * we use a fake `CoreFeature` with imports for all the fed1 types to use those specific "aliases"
127
+ * and we pass it when adding those types. This allows to reuse the same `TypeSpecification` objects
128
+ * for both fed1 and fed2. Note that in the object below, all that is used is the imports, the rest
129
+ * is just filling the blanks.
130
+ */
131
+ const FAKE_FED1_CORE_FEATURE_TO_RENAME_TYPES: CoreFeature = new CoreFeature(
132
+ new FeatureUrl('<fed1>', 'fed1', new FeatureVersion(0, 1)),
133
+ 'fed1',
134
+ new Directive('fed1'),
135
+ FEDERATION1_TYPES.map((spec) => ({ name: spec.name, as: '_' + spec.name})),
136
+ );
137
+
138
+
115
139
  function validateFieldSetSelections({
116
140
  directiveName,
117
141
  selectionSet,
@@ -738,6 +762,14 @@ export class FederationMetadata {
738
762
  return this.getPost20FederationDirective(FederationDirectiveName.INTERFACE_OBJECT);
739
763
  }
740
764
 
765
+ authenticatedDirective(): Post20FederationDirectiveDefinition<{}> {
766
+ return this.getPost20FederationDirective(FederationDirectiveName.AUTHENTICATED);
767
+ }
768
+
769
+ requiresScopesDirective(): Post20FederationDirectiveDefinition<{scopes: string[]}> {
770
+ return this.getPost20FederationDirective(FederationDirectiveName.REQUIRES_SCOPES);
771
+ }
772
+
741
773
  allFederationDirectives(): DirectiveDefinition[] {
742
774
  const baseDirectives: DirectiveDefinition[] = [
743
775
  this.keyDirective(),
@@ -763,6 +795,16 @@ export class FederationMetadata {
763
795
  baseDirectives.push(interfaceObjectDirective);
764
796
  }
765
797
 
798
+ const authenticatedDirective = this.authenticatedDirective();
799
+ if (isFederationDirectiveDefinedInSchema(authenticatedDirective)) {
800
+ baseDirectives.push(authenticatedDirective);
801
+ }
802
+
803
+ const requiresScopesDirective = this.requiresScopesDirective();
804
+ if (isFederationDirectiveDefinedInSchema(requiresScopesDirective)) {
805
+ baseDirectives.push(requiresScopesDirective);
806
+ }
807
+
766
808
  return baseDirectives;
767
809
  }
768
810
 
@@ -784,16 +826,33 @@ export class FederationMetadata {
784
826
  }
785
827
 
786
828
  allFederationTypes(): NamedType[] {
787
- const baseTypes: NamedType[] = [
829
+ // We manually include the `_Any`, `_Service` and `Entity` types because there are not strictly
830
+ // speaking part of the federation @link spec.
831
+ const fedTypes: NamedType[] = [
788
832
  this.anyType(),
789
833
  this.serviceType(),
790
- this.fieldSetType(),
791
834
  ];
835
+
836
+ const fedFeature = this.federationFeature();
837
+ if (fedFeature) {
838
+ const featureDef = FEDERATION_VERSIONS.find(fedFeature.url.version);
839
+ assert(featureDef, () => `Federation spec should be known, but got ${fedFeature.url}`);
840
+ for (const typeSpec of featureDef.typeSpecs()) {
841
+ const type = this.schema.type(fedFeature.typeNameInSchema(typeSpec.name));
842
+ if (type) {
843
+ fedTypes.push(type);
844
+ }
845
+ }
846
+ } else {
847
+ // Fed1: the only type we had was _FieldSet.
848
+ fedTypes.push(this.fieldSetType());
849
+ }
850
+
792
851
  const entityType = this.entityType();
793
852
  if (entityType) {
794
- baseTypes.push(entityType);
853
+ fedTypes.push(entityType);
795
854
  }
796
- return baseTypes;
855
+ return fedTypes;
797
856
  }
798
857
  }
799
858
 
@@ -831,14 +890,20 @@ export class FederationBlueprint extends SchemaBlueprint {
831
890
  }
832
891
  }
833
892
 
834
- onMissingDirectiveDefinition(schema: Schema, name: string, args?: {[key: string]: any}): DirectiveDefinition | GraphQLError[] | undefined {
835
- if (name === linkDirectiveDefaultName) {
893
+ onMissingDirectiveDefinition(schema: Schema, directive: Directive): DirectiveDefinition | GraphQLError[] | undefined {
894
+ if (directive.name === linkDirectiveDefaultName) {
895
+ const args = directive.arguments();
836
896
  const url = args && (args['url'] as string | undefined);
837
- const as = url && url.startsWith(linkSpec.identity) ? (args['as'] as string | undefined) : undefined;
838
- const errors = linkSpec.addDefinitionsToSchema(schema, as);
839
- return errors.length > 0 ? errors : schema.directive(name);
897
+ let as: string | undefined = undefined;
898
+ let imports: CoreImport[] = [];
899
+ if (url && url.startsWith(linkSpec.identity)) {
900
+ as = args['as'] as string | undefined;
901
+ imports = extractCoreFeatureImports(linkSpec.url, directive as Directive<SchemaDefinition, CoreOrLinkDirectiveArgs>);
902
+ }
903
+ const errors = linkSpec.addDefinitionsToSchema(schema, as, imports);
904
+ return errors.length > 0 ? errors : schema.directive(directive.name);
840
905
  }
841
- return super.onMissingDirectiveDefinition(schema, name, args);
906
+ return super.onMissingDirectiveDefinition(schema, directive);
842
907
  }
843
908
 
844
909
  ignoreParsedField(type: NamedType, fieldName: string): boolean {
@@ -1073,7 +1138,7 @@ function findUnusedNamedForLinkDirective(schema: Schema): string | undefined {
1073
1138
  // The schema already defines a directive named `@link` so we need to use an alias.
1074
1139
  // To keep it simple, we add a number in the end (so we try `@link1`, and if that's taken `@link2`, ...)
1075
1140
  const baseName = linkSpec.url.name;
1076
- let n = 1;
1141
+ const n = 1;
1077
1142
  for (;;) {
1078
1143
  const candidate = baseName + n;
1079
1144
  if (!schema.directive(candidate)) {
@@ -1106,7 +1171,7 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
1106
1171
  core.coreItself.nameInSchema,
1107
1172
  {
1108
1173
  url: federationSpec.url.toString(),
1109
- import: federationSpec.directiveSpecs().map((spec) => `@${spec.name}`),
1174
+ import: autoExpandedFederationSpec.directiveSpecs().map((spec) => `@${spec.name}`),
1110
1175
  }
1111
1176
  );
1112
1177
  const errors = completeSubgraphSchema(schema);
@@ -1117,7 +1182,9 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
1117
1182
 
1118
1183
  // This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
1119
1184
  // subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
1120
- export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
1185
+ export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject", "@authenticated", "@requiresScopes"])';
1186
+ // This is the full @link declaration that is added when upgrading fed v1 subgraphs to v2 version. It should only be used by tests.
1187
+ export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
1121
1188
 
1122
1189
  /**
1123
1190
  * Given a document that is assumed to _not_ be a fed2 schema (it does not have a `@link` to the federation spec),
@@ -1126,8 +1193,11 @@ export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apo
1126
1193
  * @param document - the document to "augment".
1127
1194
  * @param options.addAsSchemaExtension - defines whethere the added `@link` is added as a schema extension (`extend schema`) or
1128
1195
  * added to the schema definition. Defaults to `true` (added as an extension), as this mimics what we tends to write manually.
1196
+ * @param options.includeAllImports - defines whether we should auto import ALL latest federation v2 directive definitions or include
1197
+ * only limited set of directives (i.e. federation v2.4 definitions)
1129
1198
  */
1130
- export function asFed2SubgraphDocument(document: DocumentNode, options?: { addAsSchemaExtension: boolean }): DocumentNode {
1199
+ export function asFed2SubgraphDocument(document: DocumentNode, options?: { addAsSchemaExtension?: boolean, includeAllImports?: boolean }): DocumentNode {
1200
+ const importedDirectives = options?.includeAllImports ? federationSpec.directiveSpecs() : autoExpandedFederationSpec.directiveSpecs();
1131
1201
  const directiveToAdd: ConstDirectiveNode = ({
1132
1202
  kind: Kind.DIRECTIVE,
1133
1203
  name: { kind: Kind.NAME, value: linkDirectiveDefaultName },
@@ -1140,7 +1210,7 @@ export function asFed2SubgraphDocument(document: DocumentNode, options?: { addAs
1140
1210
  {
1141
1211
  kind: Kind.ARGUMENT,
1142
1212
  name: { kind: Kind.NAME, value: 'import' },
1143
- value: { kind: Kind.LIST, values: federationSpec.directiveSpecs().map((spec) => ({ kind: Kind.STRING, value: `@${spec.name}` })) }
1213
+ value: { kind: Kind.LIST, values: importedDirectives.map((spec) => ({ kind: Kind.STRING, value: `@${spec.name}` })) }
1144
1214
  }
1145
1215
  ]
1146
1216
  });
@@ -1343,7 +1413,7 @@ function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
1343
1413
  }
1344
1414
  }
1345
1415
 
1346
- const errors = FEDERATION1_TYPES.map((spec) => spec.checkOrAdd(schema, '_' + spec.name))
1416
+ const errors = FEDERATION1_TYPES.map((spec) => spec.checkOrAdd(schema, FAKE_FED1_CORE_FEATURE_TO_RENAME_TYPES))
1347
1417
  .concat(FEDERATION1_DIRECTIVES.map((spec) => spec.checkOrAdd(schema)))
1348
1418
  .flat();
1349
1419
 
@@ -1681,6 +1751,15 @@ export class Subgraph {
1681
1751
  }
1682
1752
  }
1683
1753
 
1754
+ /**
1755
+ * Same as `Schema.assumeValid`. Use carefully.
1756
+ */
1757
+ assumeValid(): Subgraph {
1758
+ this.addFederationOperations();
1759
+ this.schema.assumeValid();
1760
+ return this;
1761
+ }
1762
+
1684
1763
  validate(): Subgraph {
1685
1764
  try {
1686
1765
  this.addFederationOperations();
@@ -15,6 +15,8 @@ import { TAG_VERSIONS } from "./tagSpec";
15
15
  import { federationMetadata } from "./federation";
16
16
  import { registerKnownFeature } from "./knownCoreFeatures";
17
17
  import { INACCESSIBLE_VERSIONS } from "./inaccessibleSpec";
18
+ import { AUTHENTICATED_VERSIONS } from "./authenticatedSpec";
19
+ import { REQUIRES_SCOPES_VERSIONS } from "./requiresScopesSpec";
18
20
 
19
21
  export const federationIdentity = 'https://specs.apollo.dev/federation';
20
22
 
@@ -34,6 +36,8 @@ export enum FederationDirectiveName {
34
36
  INACCESSIBLE = 'inaccessible',
35
37
  COMPOSE_DIRECTIVE = 'composeDirective',
36
38
  INTERFACE_OBJECT = 'interfaceObject',
39
+ AUTHENTICATED = 'authenticated',
40
+ REQUIRES_SCOPES = 'requiresScopes',
37
41
  }
38
42
 
39
43
  const fieldSetTypeSpec = createScalarTypeSpecification({ name: FederationTypeName.FIELD_SET });
@@ -116,7 +120,7 @@ export class FederationSpecDefinition extends FeatureDefinition {
116
120
  repeatable: version >= (new FeatureVersion(2, 2)),
117
121
  }));
118
122
 
119
- this.registerDirective(INACCESSIBLE_VERSIONS.latest().inaccessibleDirectiveSpec);
123
+ this.registerSubFeature(INACCESSIBLE_VERSIONS.getMinimumRequiredVersion(version));
120
124
 
121
125
  this.registerDirective(createDirectiveSpecification({
122
126
  name: FederationDirectiveName.OVERRIDE,
@@ -138,9 +142,12 @@ export class FederationSpecDefinition extends FeatureDefinition {
138
142
  name: FederationDirectiveName.INTERFACE_OBJECT,
139
143
  locations: [DirectiveLocation.OBJECT],
140
144
  }));
141
- this.registerDirective(
142
- TAG_VERSIONS.find(new FeatureVersion(0, 3))!.tagDirectiveSpec
143
- );
145
+ this.registerSubFeature(TAG_VERSIONS.find(new FeatureVersion(0, 3))!);
146
+ }
147
+
148
+ if (version >= (new FeatureVersion(2, 5))) {
149
+ this.registerSubFeature(AUTHENTICATED_VERSIONS.find(new FeatureVersion(0, 1))!);
150
+ this.registerSubFeature(REQUIRES_SCOPES_VERSIONS.find(new FeatureVersion(0, 1))!);
144
151
  }
145
152
  }
146
153
  }
@@ -150,6 +157,7 @@ export const FEDERATION_VERSIONS = new FeatureDefinitions<FederationSpecDefiniti
150
157
  .add(new FederationSpecDefinition(new FeatureVersion(2, 1)))
151
158
  .add(new FederationSpecDefinition(new FeatureVersion(2, 2)))
152
159
  .add(new FederationSpecDefinition(new FeatureVersion(2, 3)))
153
- .add(new FederationSpecDefinition(new FeatureVersion(2, 4)));
160
+ .add(new FederationSpecDefinition(new FeatureVersion(2, 4)))
161
+ .add(new FederationSpecDefinition(new FeatureVersion(2, 5)));
154
162
 
155
163
  registerKnownFeature(FEDERATION_VERSIONS);
@@ -39,8 +39,8 @@ export class InaccessibleSpecDefinition extends FeatureDefinition {
39
39
  public readonly inaccessibleDirectiveSpec: DirectiveSpecification;
40
40
  private readonly printedInaccessibleDefinition: string;
41
41
 
42
- constructor(version: FeatureVersion) {
43
- super(new FeatureUrl(inaccessibleIdentity, 'inaccessible', version));
42
+ constructor(version: FeatureVersion, minimumFederationVersion?: FeatureVersion) {
43
+ super(new FeatureUrl(inaccessibleIdentity, 'inaccessible', version), minimumFederationVersion);
44
44
  this.inaccessibleLocations = [
45
45
  DirectiveLocation.FIELD_DEFINITION,
46
46
  DirectiveLocation.OBJECT,
@@ -63,7 +63,7 @@ export class InaccessibleSpecDefinition extends FeatureDefinition {
63
63
  name: 'inaccessible',
64
64
  locations: this.inaccessibleLocations,
65
65
  composes: true,
66
- supergraphSpecification: () => INACCESSIBLE_VERSIONS.latest(),
66
+ supergraphSpecification: (fedVersion) => INACCESSIBLE_VERSIONS.getMinimumRequiredVersion(fedVersion),
67
67
  });
68
68
  this.registerDirective(this.inaccessibleDirectiveSpec);
69
69
  }
@@ -95,7 +95,7 @@ export class InaccessibleSpecDefinition extends FeatureDefinition {
95
95
 
96
96
  export const INACCESSIBLE_VERSIONS = new FeatureDefinitions<InaccessibleSpecDefinition>(inaccessibleIdentity)
97
97
  .add(new InaccessibleSpecDefinition(new FeatureVersion(0, 1)))
98
- .add(new InaccessibleSpecDefinition(new FeatureVersion(0, 2)));
98
+ .add(new InaccessibleSpecDefinition(new FeatureVersion(0, 2), new FeatureVersion(2, 0)));
99
99
 
100
100
  registerKnownFeature(INACCESSIBLE_VERSIONS);
101
101
 
package/src/index.ts CHANGED
@@ -13,7 +13,6 @@ export * from './tagSpec';
13
13
  export * from './inaccessibleSpec';
14
14
  export * from './federationSpec';
15
15
  export * from './supergraphs';
16
- export * from './extractSubgraphsFromSupergraph';
17
16
  export * from './error';
18
17
  export * from './schemaUpgrader';
19
18
  export * from './suggestions';
@@ -21,3 +20,5 @@ export * from './graphQLJSSchemaToAST';
21
20
  export * from './directiveAndTypeSpecification';
22
21
  export { coreFeatureDefinitionIfKnown } from './knownCoreFeatures';
23
22
  export * from './argumentCompositionStrategies';
23
+ export * from './authenticatedSpec';
24
+ export * from './requiresScopesSpec';
package/src/joinSpec.ts CHANGED
@@ -30,9 +30,27 @@ function sanitizeGraphQLName(name: string) {
30
30
  return toUpper;
31
31
  }
32
32
 
33
+ export type JoinTypeDirectiveArguments = {
34
+ graph: string,
35
+ key?: string,
36
+ extension?: boolean,
37
+ resolvable?: boolean,
38
+ isInterfaceObject?: boolean
39
+ };
40
+
41
+ export type JoinFieldDirectiveArguments = {
42
+ graph?: string,
43
+ requires?: string,
44
+ provides?: string,
45
+ override?: string,
46
+ type?: string,
47
+ external?: boolean,
48
+ usedOverridden?: boolean,
49
+ }
50
+
33
51
  export class JoinSpecDefinition extends FeatureDefinition {
34
- constructor(version: FeatureVersion) {
35
- super(new FeatureUrl(joinIdentity, 'join', version));
52
+ constructor(version: FeatureVersion, minimumFederationVersion?: FeatureVersion) {
53
+ super(new FeatureUrl(joinIdentity, 'join', version), minimumFederationVersion);
36
54
  }
37
55
 
38
56
  private isV01() {
@@ -174,7 +192,7 @@ export class JoinSpecDefinition extends FeatureDefinition {
174
192
  return this.directive(schema, 'graph')!;
175
193
  }
176
194
 
177
- typeDirective(schema: Schema): DirectiveDefinition<{graph: string, key?: string, extension?: boolean, resolvable?: boolean, isInterfaceObject?: boolean}> {
195
+ typeDirective(schema: Schema): DirectiveDefinition<JoinTypeDirectiveArguments> {
178
196
  return this.directive(schema, 'type')!;
179
197
  }
180
198
 
@@ -182,15 +200,7 @@ export class JoinSpecDefinition extends FeatureDefinition {
182
200
  return this.directive(schema, 'implements');
183
201
  }
184
202
 
185
- fieldDirective(schema: Schema): DirectiveDefinition<{
186
- graph?: string,
187
- requires?: string,
188
- provides?: string,
189
- override?: string,
190
- type?: string,
191
- external?: boolean,
192
- usedOverridden?: boolean,
193
- }> {
203
+ fieldDirective(schema: Schema): DirectiveDefinition<JoinFieldDirectiveArguments> {
194
204
  return this.directive(schema, 'field')!;
195
205
  }
196
206
 
@@ -220,6 +230,6 @@ export class JoinSpecDefinition extends FeatureDefinition {
220
230
  export const JOIN_VERSIONS = new FeatureDefinitions<JoinSpecDefinition>(joinIdentity)
221
231
  .add(new JoinSpecDefinition(new FeatureVersion(0, 1)))
222
232
  .add(new JoinSpecDefinition(new FeatureVersion(0, 2)))
223
- .add(new JoinSpecDefinition(new FeatureVersion(0, 3)));
233
+ .add(new JoinSpecDefinition(new FeatureVersion(0, 3), new FeatureVersion(2, 0)));
224
234
 
225
235
  registerKnownFeature(JOIN_VERSIONS);
package/src/precompute.ts CHANGED
@@ -20,7 +20,7 @@ export function computeShareables(schema: Schema): (field: FieldDefinition<Compo
20
20
  // by default are key fields).
21
21
  const shareableDirective = metadata.isFed2Schema() ? metadata.shareableDirective() : undefined;
22
22
 
23
- const shareableFields: Set<String> = new Set();
23
+ const shareableFields: Set<string> = new Set();
24
24
  const addKeyFields = (type: CompositeType) => {
25
25
  for (const key of type.appliedDirectivesOf(keyDirective)) {
26
26
  collectTargetFields({
@@ -0,0 +1,76 @@
1
+ import { DirectiveLocation } from "graphql";
2
+ import {
3
+ CorePurpose,
4
+ FeatureDefinition,
5
+ FeatureDefinitions,
6
+ FeatureUrl,
7
+ FeatureVersion,
8
+ } from "./coreSpec";
9
+ import { DirectiveDefinition, ListType, NonNullType, Schema } from "./definitions";
10
+ import { createDirectiveSpecification, createScalarTypeSpecification } from "./directiveAndTypeSpecification";
11
+ import { registerKnownFeature } from "./knownCoreFeatures";
12
+ import { ARGUMENT_COMPOSITION_STRATEGIES } from "./argumentCompositionStrategies";
13
+ import { assert } from "./utils";
14
+
15
+ export enum RequiresScopesTypeName {
16
+ SCOPE = 'Scope',
17
+ }
18
+
19
+ export class RequiresScopesSpecDefinition extends FeatureDefinition {
20
+ public static readonly directiveName = "requiresScopes";
21
+ public static readonly identity =
22
+ `https://specs.apollo.dev/${RequiresScopesSpecDefinition.directiveName}`;
23
+
24
+ constructor(version: FeatureVersion) {
25
+ super(
26
+ new FeatureUrl(
27
+ RequiresScopesSpecDefinition.identity,
28
+ RequiresScopesSpecDefinition.directiveName,
29
+ version,
30
+ )
31
+ );
32
+
33
+ this.registerType(createScalarTypeSpecification({ name: RequiresScopesTypeName.SCOPE }));
34
+
35
+ this.registerDirective(createDirectiveSpecification({
36
+ name: RequiresScopesSpecDefinition.directiveName,
37
+ args: [{
38
+ name: 'scopes',
39
+ type: (schema, feature) => {
40
+ assert(feature, "Shouldn't be added without being attached to a @link spec");
41
+ const scopeName = feature.typeNameInSchema(RequiresScopesTypeName.SCOPE);
42
+ const scopeType = schema.type(scopeName);
43
+ assert(scopeType, () => `Expected "${scopeName}" to be defined`);
44
+ return new NonNullType(new ListType(new NonNullType(scopeType)));
45
+ },
46
+ compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.UNION,
47
+ }],
48
+ locations: [
49
+ DirectiveLocation.FIELD_DEFINITION,
50
+ DirectiveLocation.OBJECT,
51
+ DirectiveLocation.INTERFACE,
52
+ DirectiveLocation.SCALAR,
53
+ DirectiveLocation.ENUM,
54
+ ],
55
+ composes: true,
56
+ supergraphSpecification: () => REQUIRES_SCOPES_VERSIONS.latest(),
57
+ }));
58
+ }
59
+
60
+ requiresScopesDirective(
61
+ schema: Schema
62
+ ): DirectiveDefinition<{ name: string }> {
63
+ return this.directive(schema, RequiresScopesSpecDefinition.directiveName)!;
64
+ }
65
+
66
+ get defaultCorePurpose(): CorePurpose {
67
+ return 'SECURITY';
68
+ }
69
+ }
70
+
71
+ export const REQUIRES_SCOPES_VERSIONS =
72
+ new FeatureDefinitions<RequiresScopesSpecDefinition>(
73
+ RequiresScopesSpecDefinition.identity
74
+ ).add(new RequiresScopesSpecDefinition(new FeatureVersion(0, 1)));
75
+
76
+ registerKnownFeature(REQUIRES_SCOPES_VERSIONS);
@@ -3,10 +3,11 @@ import { ErrCoreCheckFailed, FeatureUrl, FeatureVersion } from "./coreSpec";
3
3
  import { CoreFeatures, Schema, sourceASTs } from "./definitions";
4
4
  import { joinIdentity, JoinSpecDefinition, JOIN_VERSIONS } from "./joinSpec";
5
5
  import { buildSchema, buildSchemaFromAST } from "./buildSchema";
6
- import { extractSubgraphsNamesAndUrlsFromSupergraph } from "./extractSubgraphsFromSupergraph";
6
+ import { extractSubgraphsNamesAndUrlsFromSupergraph, extractSubgraphsFromSupergraph } from "./extractSubgraphsFromSupergraph";
7
7
  import { ERRORS } from "./error";
8
+ import { Subgraphs } from ".";
8
9
 
9
- const SUPPORTED_FEATURES = new Set([
10
+ export const DEFAULT_SUPPORTED_SUPERGRAPH_FEATURES = new Set([
10
11
  'https://specs.apollo.dev/core/v0.1',
11
12
  'https://specs.apollo.dev/core/v0.2',
12
13
  'https://specs.apollo.dev/join/v0.1',
@@ -21,24 +22,12 @@ const SUPPORTED_FEATURES = new Set([
21
22
 
22
23
  const coreVersionZeroDotOneUrl = FeatureUrl.parse('https://specs.apollo.dev/core/v0.1');
23
24
 
24
- export function buildSupergraphSchema(supergraphSdl: string | DocumentNode): [Schema, {name: string, url: string}[]] {
25
- // We delay validation because `checkFeatureSupport` gives slightly more useful errors if, say, 'for' is used with core v0.1.
26
- const schema = typeof supergraphSdl === 'string'
27
- ? buildSchema(supergraphSdl, { validate: false })
28
- : buildSchemaFromAST(supergraphSdl, { validate: false });
29
-
30
- const [coreFeatures] = validateSupergraph(schema);
31
- checkFeatureSupport(coreFeatures);
32
- schema.validate();
33
- return [schema, extractSubgraphsNamesAndUrlsFromSupergraph(schema)];
34
- }
35
-
36
25
  /**
37
26
  * Checks that only our hard-coded list of features are part of the provided schema, and that if
38
27
  * the schema uses core v0.1, then it doesn't use the 'for' (purpose) argument.
39
28
  * Throws if that is not true.
40
29
  */
41
- function checkFeatureSupport(coreFeatures: CoreFeatures) {
30
+ function checkFeatureSupport(coreFeatures: CoreFeatures, supportedFeatures: Set<string>) {
42
31
  const errors: GraphQLError[] = [];
43
32
  const coreItself = coreFeatures.coreItself;
44
33
  if (coreItself.url.equals(coreVersionZeroDotOneUrl)) {
@@ -56,7 +45,7 @@ function checkFeatureSupport(coreFeatures: CoreFeatures) {
56
45
 
57
46
  for (const feature of coreFeatures.allFeatures()) {
58
47
  if (feature.url.equals(coreVersionZeroDotOneUrl) || feature.purpose === 'EXECUTION' || feature.purpose === 'SECURITY') {
59
- if (!SUPPORTED_FEATURES.has(feature.url.base.toString())) {
48
+ if (!supportedFeatures.has(feature.url.base.toString())) {
60
49
  errors.push(ERRORS.UNSUPPORTED_LINKED_FEATURE.err(
61
50
  `feature ${feature.url} is for: ${feature.purpose} but is unsupported`,
62
51
  { nodes: feature.directive.sourceAST },
@@ -89,3 +78,62 @@ export function validateSupergraph(supergraph: Schema): [CoreFeatures, JoinSpecD
89
78
  export function isFed1Supergraph(supergraph: Schema): boolean {
90
79
  return validateSupergraph(supergraph)[1].version.equals(new FeatureVersion(0, 1));
91
80
  }
81
+
82
+ export class Supergraph {
83
+ private readonly containedSubgraphs: readonly {name: string, url: string}[];
84
+ // Lazily computed as that requires a bit of work.
85
+ private _subgraphs?: Subgraphs;
86
+
87
+ constructor(
88
+ readonly schema: Schema,
89
+ supportedFeatures: Set<string> | null = DEFAULT_SUPPORTED_SUPERGRAPH_FEATURES,
90
+ private readonly shouldValidate: boolean = true,
91
+ ) {
92
+ const [coreFeatures] = validateSupergraph(schema);
93
+
94
+ if (supportedFeatures !== null) {
95
+ checkFeatureSupport(coreFeatures, supportedFeatures);
96
+ }
97
+
98
+ if (shouldValidate) {
99
+ schema.validate();
100
+ } else {
101
+ schema.assumeValid();
102
+ }
103
+
104
+ this.containedSubgraphs = extractSubgraphsNamesAndUrlsFromSupergraph(schema);
105
+ }
106
+
107
+ static build(supergraphSdl: string | DocumentNode, options?: { supportedFeatures?: Set<string>, validateSupergraph?: boolean }) {
108
+ // We delay validation because `checkFeatureSupport` in the constructor gives slightly more useful errors if, say, 'for' is used with core v0.1.
109
+ const schema = typeof supergraphSdl === 'string'
110
+ ? buildSchema(supergraphSdl, { validate: false })
111
+ : buildSchemaFromAST(supergraphSdl, { validate: false });
112
+
113
+ return new Supergraph(schema, options?.supportedFeatures, options?.validateSupergraph);
114
+ }
115
+
116
+ /**
117
+ * The list of names/urls of the subgraphs contained in this subgraph.
118
+ *
119
+ * Note that this is a subset of what `this.subgraphs()` returns, but contrarily to that method, this method does not do a full extraction of the
120
+ * subgraphs schema.
121
+ */
122
+ subgraphsMetadata(): readonly {name: string, url: string}[] {
123
+ return this.containedSubgraphs;
124
+ }
125
+
126
+ subgraphs(): Subgraphs {
127
+ if (!this._subgraphs) {
128
+ // Note that `extractSubgraphsFromSupergraph` redo a little bit of work we're already one, like validating
129
+ // the supergraph. We could refactor things to avoid it, but it's completely negligible in practice so we
130
+ // can leave that to "some day, maybe".
131
+ this._subgraphs = extractSubgraphsFromSupergraph(this.schema, this.shouldValidate);
132
+ }
133
+ return this._subgraphs;
134
+ }
135
+
136
+ apiSchema(): Schema {
137
+ return this.schema.toAPISchema();
138
+ }
139
+ }
package/src/tagSpec.ts CHANGED
@@ -13,8 +13,8 @@ export class TagSpecDefinition extends FeatureDefinition {
13
13
  public readonly tagDirectiveSpec: DirectiveSpecification;
14
14
  private readonly printedTagDefinition: string;
15
15
 
16
- constructor(version: FeatureVersion) {
17
- super(new FeatureUrl(tagIdentity, 'tag', version));
16
+ constructor(version: FeatureVersion, minimumFederationVersion?: FeatureVersion) {
17
+ super(new FeatureUrl(tagIdentity, 'tag', version), minimumFederationVersion);
18
18
  this.tagLocations = [
19
19
  DirectiveLocation.FIELD_DEFINITION,
20
20
  DirectiveLocation.OBJECT,
@@ -43,7 +43,7 @@ export class TagSpecDefinition extends FeatureDefinition {
43
43
  repeatable: true,
44
44
  args: [{ name: 'name', type: (schema) => new NonNullType(schema.stringType()) }],
45
45
  composes: true,
46
- supergraphSpecification: () => TAG_VERSIONS.latest(),
46
+ supergraphSpecification: (fedVersion) => TAG_VERSIONS.getMinimumRequiredVersion(fedVersion),
47
47
  });
48
48
  this.registerDirective(this.tagDirectiveSpec);
49
49
  }
@@ -77,6 +77,6 @@ export class TagSpecDefinition extends FeatureDefinition {
77
77
  export const TAG_VERSIONS = new FeatureDefinitions<TagSpecDefinition>(tagIdentity)
78
78
  .add(new TagSpecDefinition(new FeatureVersion(0, 1)))
79
79
  .add(new TagSpecDefinition(new FeatureVersion(0, 2)))
80
- .add(new TagSpecDefinition(new FeatureVersion(0, 3)));
80
+ .add(new TagSpecDefinition(new FeatureVersion(0, 3), new FeatureVersion(2, 0)));
81
81
 
82
82
  registerKnownFeature(TAG_VERSIONS);
package/src/utils.ts CHANGED
@@ -431,3 +431,13 @@ export function isNonEmptyArray<T>(array: T[]): array is NonEmptyArray<T> {
431
431
  return array.length > 0;
432
432
  }
433
433
 
434
+ // We can switch to `Array.prototype.findLast` when we drop support for Node 16
435
+ export function findLast<T>(array: T[], predicate: (t: T) => boolean): T | undefined {
436
+ for (let i = array.length - 1; i >= 0; i--) {
437
+ const t = array[i];
438
+ if (predicate(t)) {
439
+ return t;
440
+ }
441
+ }
442
+ return undefined;
443
+ }