@apollo/federation-internals 2.7.0 → 2.7.2

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.
package/src/federation.ts CHANGED
@@ -79,6 +79,7 @@ import {
79
79
  FederationTypeName,
80
80
  FEDERATION1_TYPES,
81
81
  FEDERATION1_DIRECTIVES,
82
+ FederationSpecDefinition,
82
83
  } from "./specs/federationSpec";
83
84
  import { defaultPrintOptions, PrintOptions as PrintOptions, printSchema } from "./print";
84
85
  import { createObjectTypeSpecification, createScalarTypeSpecification, createUnionTypeSpecification } from "./directiveAndTypeSpecification";
@@ -93,10 +94,18 @@ import {
93
94
 
94
95
  const linkSpec = LINK_VERSIONS.latest();
95
96
  const tagSpec = TAG_VERSIONS.latest();
96
- const federationSpec = FEDERATION_VERSIONS.latest();
97
+ const federationSpec = (version?: FeatureVersion): FederationSpecDefinition => {
98
+ if (!version) return FEDERATION_VERSIONS.latest();
99
+ const spec = FEDERATION_VERSIONS.find(version);
100
+ assert(spec, `Federation spec version ${version} is not known`);
101
+ return spec;
102
+ };
103
+
97
104
  // Some users rely on auto-expanding fed v1 graphs with fed v2 directives. While technically we should only expand @tag
98
105
  // directive from v2 definitions, we will continue expanding other directives (up to v2.4) to ensure backwards compatibility.
99
- const autoExpandedFederationSpec = FEDERATION_VERSIONS.find(new FeatureVersion(2, 4))!;
106
+ const autoExpandedFederationSpec = federationSpec(new FeatureVersion(2, 4));
107
+
108
+ const latestFederationSpec = federationSpec();
100
109
 
101
110
  // 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
102
111
  // in the "federated query graph" without worrying about conflict (see `FEDERATED_GRAPH_ROOT_SOURCE` in `querygraph.ts`).
@@ -601,7 +610,7 @@ export class FederationMetadata {
601
610
  }
602
611
 
603
612
  federationFeature(): CoreFeature | undefined {
604
- return this.schema.coreFeatures?.getByIdentity(federationSpec.identity);
613
+ return this.schema.coreFeatures?.getByIdentity(latestFederationSpec.identity);
605
614
  }
606
615
 
607
616
  private externalTester(): ExternalTester {
@@ -663,7 +672,7 @@ export class FederationMetadata {
663
672
  if (this.isFed2Schema()) {
664
673
  const coreFeatures = this.schema.coreFeatures;
665
674
  assert(coreFeatures, 'Schema should be a core schema');
666
- const federationFeature = coreFeatures.getByIdentity(federationSpec.identity);
675
+ const federationFeature = coreFeatures.getByIdentity(latestFederationSpec.identity);
667
676
  assert(federationFeature, 'Schema should have the federation feature');
668
677
  return federationFeature.directiveNameInSchema(name);
669
678
  } else {
@@ -685,7 +694,7 @@ export class FederationMetadata {
685
694
  if (this.isFed2Schema()) {
686
695
  const coreFeatures = this.schema.coreFeatures;
687
696
  assert(coreFeatures, 'Schema should be a core schema');
688
- const federationFeature = coreFeatures.getByIdentity(federationSpec.identity);
697
+ const federationFeature = coreFeatures.getByIdentity(latestFederationSpec.identity);
689
698
  assert(federationFeature, 'Schema should have the federation feature');
690
699
  return federationFeature.typeNameInSchema(name);
691
700
  } else {
@@ -1190,7 +1199,7 @@ function findUnusedNamedForLinkDirective(schema: Schema): string | undefined {
1190
1199
  }
1191
1200
  }
1192
1201
 
1193
- export function setSchemaAsFed2Subgraph(schema: Schema) {
1202
+ export function setSchemaAsFed2Subgraph(schema: Schema, useLatest: boolean = false) {
1194
1203
  let core = schema.coreFeatures;
1195
1204
  let spec: CoreSpecDefinition;
1196
1205
  if (core) {
@@ -1209,11 +1218,16 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
1209
1218
  assert(core, 'Schema should now be a core schema');
1210
1219
  }
1211
1220
 
1212
- assert(!core.getByIdentity(federationSpec.identity), 'Schema already set as a federation subgraph');
1221
+ const fedSpec = useLatest ? latestFederationSpec : autoExpandedFederationSpec;
1222
+
1223
+ assert(!core.getByIdentity(fedSpec.identity), 'Schema already set as a federation subgraph');
1213
1224
  schema.schemaDefinition.applyDirective(
1214
1225
  core.coreItself.nameInSchema,
1215
1226
  {
1216
- url: federationSpec.url.toString(),
1227
+ // note that there is a mismatch between url and directives that are imported. This is because
1228
+ // we want to maintain backward compatibility for those who have already upgraded and we had been upgrading the url to
1229
+ // latest, but we never automatically import directives that exist past 2.4
1230
+ url: fedSpec.url.toString(),
1217
1231
  import: autoExpandedFederationSpec.directiveSpecs().map((spec) => `@${spec.name}`),
1218
1232
  }
1219
1233
  );
@@ -1226,21 +1240,25 @@ export function setSchemaAsFed2Subgraph(schema: Schema) {
1226
1240
  // This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
1227
1241
  // subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
1228
1242
  export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject", "@authenticated", "@requiresScopes", "@policy", "@sourceAPI", "@sourceType", "@sourceField"])';
1229
- // This is the full @link declaration that is added when upgrading fed v1 subgraphs to v2 version. It should only be used by tests.
1243
+
1244
+ // This is the federation @link for tests that go through the asFed2SubgraphDocument function.
1230
1245
  export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
1231
1246
 
1247
+ // This is the federation @link for tests that go through the SchemaUpgrader.
1248
+ export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS_UPGRADED = '@link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
1249
+
1232
1250
  /**
1233
1251
  * Given a document that is assumed to _not_ be a fed2 schema (it does not have a `@link` to the federation spec),
1234
1252
  * returns an equivalent document that `@link` to the last known federation spec.
1235
1253
  *
1236
1254
  * @param document - the document to "augment".
1237
- * @param options.addAsSchemaExtension - defines whethere the added `@link` is added as a schema extension (`extend schema`) or
1255
+ * @param options.addAsSchemaExtension - defines whether the added `@link` is added as a schema extension (`extend schema`) or
1238
1256
  * added to the schema definition. Defaults to `true` (added as an extension), as this mimics what we tends to write manually.
1239
1257
  * @param options.includeAllImports - defines whether we should auto import ALL latest federation v2 directive definitions or include
1240
1258
  * only limited set of directives (i.e. federation v2.4 definitions)
1241
1259
  */
1242
1260
  export function asFed2SubgraphDocument(document: DocumentNode, options?: { addAsSchemaExtension?: boolean, includeAllImports?: boolean }): DocumentNode {
1243
- const importedDirectives = options?.includeAllImports ? federationSpec.directiveSpecs() : autoExpandedFederationSpec.directiveSpecs();
1261
+ const importedDirectives = options?.includeAllImports ? latestFederationSpec.directiveSpecs() : autoExpandedFederationSpec.directiveSpecs();
1244
1262
  const directiveToAdd: ConstDirectiveNode = ({
1245
1263
  kind: Kind.DIRECTIVE,
1246
1264
  name: { kind: Kind.NAME, value: linkDirectiveDefaultName },
@@ -1248,7 +1266,7 @@ export function asFed2SubgraphDocument(document: DocumentNode, options?: { addAs
1248
1266
  {
1249
1267
  kind: Kind.ARGUMENT,
1250
1268
  name: { kind: Kind.NAME, value: 'url' },
1251
- value: { kind: Kind.STRING, value: federationSpec.url.toString() }
1269
+ value: { kind: Kind.STRING, value: latestFederationSpec.url.toString() }
1252
1270
  },
1253
1271
  {
1254
1272
  kind: Kind.ARGUMENT,
@@ -1374,7 +1392,7 @@ export function buildSubgraph(
1374
1392
 
1375
1393
  export function newEmptyFederation2Schema(config?: SchemaConfig): Schema {
1376
1394
  const schema = new Schema(new FederationBlueprint(true), config);
1377
- setSchemaAsFed2Subgraph(schema);
1395
+ setSchemaAsFed2Subgraph(schema, true);
1378
1396
  return schema;
1379
1397
  }
1380
1398
 
package/src/operations.ts CHANGED
@@ -973,6 +973,18 @@ export class Operation {
973
973
  return this.withUpdatedSelectionSetAndFragments(optimizedSelection, finalFragments ?? undefined);
974
974
  }
975
975
 
976
+ generateQueryFragments(): Operation {
977
+ const [minimizedSelectionSet, fragments] = this.selectionSet.minimizeSelectionSet();
978
+ return new Operation(
979
+ this.schema,
980
+ this.rootKind,
981
+ minimizedSelectionSet,
982
+ this.variableDefinitions,
983
+ fragments,
984
+ this.name,
985
+ );
986
+ }
987
+
976
988
  expandAllFragments(): Operation {
977
989
  // We clear up the fragments since we've expanded all.
978
990
  // Also note that expanding fragment usually generate unecessary fragments/inefficient selections, so it
@@ -1378,28 +1390,6 @@ export class NamedFragments {
1378
1390
  });
1379
1391
  }
1380
1392
 
1381
- // When we rebase named fragments on a subgraph schema, only a subset of what the fragment handles may belong
1382
- // to that particular subgraph. And there are a few sub-cases where that subset is such that we basically need or
1383
- // want to consider to ignore the fragment for that subgraph, and that is when:
1384
- // 1. the subset that apply is actually empty. The fragment wouldn't be valid in this case anyway.
1385
- // 2. the subset is a single leaf field: in that case, using the one field directly is just shorter than using
1386
- // the fragment, so we consider the fragment don't really apply to that subgraph. Technically, using the
1387
- // fragment could still be of value if the fragment name is a lot smaller than the one field name, but it's
1388
- // enough of a niche case that we ignore it. Note in particular that one sub-case of this rule that is likely
1389
- // to be common is when the subset ends up being just `__typename`: this would basically mean the fragment
1390
- // don't really apply to the subgraph, and that this will ensure this is the case.
1391
- private selectionSetIsWorthUsing(selectionSet: SelectionSet): boolean {
1392
- const selections = selectionSet.selections();
1393
- if (selections.length === 0) {
1394
- return false;
1395
- }
1396
- if (selections.length === 1) {
1397
- const s = selections[0];
1398
- return !(s.kind === 'FieldSelection' && s.element.isLeafField());
1399
- }
1400
- return true;
1401
- }
1402
-
1403
1393
  rebaseOn(schema: Schema): NamedFragments | undefined {
1404
1394
  return this.mapInDependencyOrder((fragment, newFragments) => {
1405
1395
  const rebasedType = schema.type(fragment.selectionSet.parentType.name);
@@ -1411,7 +1401,7 @@ export class NamedFragments {
1411
1401
  // Rebasing can leave some inefficiencies in some case (particularly when a spread has to be "expanded", see `FragmentSpreadSelection.rebaseOn`),
1412
1402
  // so we do a top-level normalization to keep things clean.
1413
1403
  rebasedSelection = rebasedSelection.normalize({ parentType: rebasedType });
1414
- return this.selectionSetIsWorthUsing(rebasedSelection)
1404
+ return rebasedSelection.isWorthUsing()
1415
1405
  ? new NamedFragmentDefinition(schema, fragment.name, rebasedType).setSelectionSet(rebasedSelection)
1416
1406
  : undefined;
1417
1407
  });
@@ -1535,6 +1525,60 @@ export class SelectionSet {
1535
1525
  this._selections = mapValues(keyedSelections);
1536
1526
  }
1537
1527
 
1528
+ /**
1529
+ * Takes a selection set and extracts inline fragments into named fragments,
1530
+ * reusing generated named fragments when possible.
1531
+ */
1532
+ minimizeSelectionSet(
1533
+ namedFragments: NamedFragments = new NamedFragments(),
1534
+ seenSelections: Map<string, [SelectionSet, NamedFragmentDefinition][]> = new Map(),
1535
+ ): [SelectionSet, NamedFragments] {
1536
+ const minimizedSelectionSet = this.lazyMap((selection) => {
1537
+ if (selection.kind === 'FragmentSelection' && selection.element.typeCondition && selection.element.appliedDirectives.length === 0
1538
+ && selection.selectionSet && selection.selectionSet.isWorthUsing() ) {
1539
+ // No proper hash code, so we use a unique enough number that's cheap to
1540
+ // compute and handle collisions as necessary.
1541
+ const mockHashCode = `on${selection.element.typeCondition}` + selection.selectionSet.selections().length;
1542
+ const equivalentSelectionSetCandidates = seenSelections.get(mockHashCode);
1543
+ if (equivalentSelectionSetCandidates) {
1544
+ // See if any candidates have an equivalent selection set, i.e. {x y} and {y x}.
1545
+ const match = equivalentSelectionSetCandidates.find(([candidateSet]) => candidateSet.equals(selection.selectionSet!));
1546
+ if (match) {
1547
+ // If we found a match, we can reuse the fragment (but we still need
1548
+ // to create a new FragmentSpread since parent types may differ).
1549
+ return new FragmentSpreadSelection(this.parentType, namedFragments, match[1], []);
1550
+ }
1551
+ }
1552
+
1553
+ // No match, so we need to create a new fragment. First, we minimize the
1554
+ // selection set before creating the fragment with it.
1555
+ const [minimizedSelectionSet] = selection.selectionSet.minimizeSelectionSet(namedFragments, seenSelections);
1556
+ const fragmentDefinition = new NamedFragmentDefinition(
1557
+ this.parentType.schema(),
1558
+ `_generated_${mockHashCode}_${equivalentSelectionSetCandidates?.length ?? 0}`,
1559
+ selection.element.typeCondition
1560
+ ).setSelectionSet(minimizedSelectionSet);
1561
+
1562
+ // Create a new "hash code" bucket or add to the existing one.
1563
+ if (!equivalentSelectionSetCandidates) {
1564
+ seenSelections.set(mockHashCode, [[selection.selectionSet, fragmentDefinition]]);
1565
+ namedFragments.add(fragmentDefinition);
1566
+ } else {
1567
+ equivalentSelectionSetCandidates.push([selection.selectionSet, fragmentDefinition]);
1568
+ }
1569
+
1570
+ return new FragmentSpreadSelection(this.parentType, namedFragments, fragmentDefinition, []);
1571
+ } else if (selection.kind === 'FieldSelection') {
1572
+ if (selection.selectionSet) {
1573
+ selection = selection.withUpdatedSelectionSet(selection.selectionSet.minimizeSelectionSet(namedFragments, seenSelections)[0]);
1574
+ }
1575
+ }
1576
+ return selection;
1577
+ });
1578
+
1579
+ return [minimizedSelectionSet, namedFragments];
1580
+ }
1581
+
1538
1582
  selectionsInReverseOrder(): readonly Selection[] {
1539
1583
  const length = this._selections.length;
1540
1584
  const reversed = new Array<Selection>(length);
@@ -2077,6 +2121,34 @@ export class SelectionSet {
2077
2121
  : selectionsToString;
2078
2122
  }
2079
2123
  }
2124
+
2125
+ // `isWorthUsing` method is used to determine whether we want to factor out
2126
+ // given selection set into a named fragment so it can be reused across the query.
2127
+ // Currently, it is used in these cases:
2128
+ // 1) to reuse existing named fragments in subgraph queries (when reuseQueryFragments is on)
2129
+ // 2) to factor selection sets into named fragments (when generateQueryFragments is on).
2130
+ //
2131
+ // When we rebase named fragments on a subgraph schema, only a subset of what the fragment handles may belong
2132
+ // to that particular subgraph. And there are a few sub-cases where that subset is such that we basically need or
2133
+ // want to consider to ignore the fragment for that subgraph, and that is when:
2134
+ // 1. the subset that apply is actually empty. The fragment wouldn't be valid in this case anyway.
2135
+ // 2. the subset is a single leaf field: in that case, using the one field directly is just shorter than using
2136
+ // the fragment, so we consider the fragment don't really apply to that subgraph. Technically, using the
2137
+ // fragment could still be of value if the fragment name is a lot smaller than the one field name, but it's
2138
+ // enough of a niche case that we ignore it. Note in particular that one sub-case of this rule that is likely
2139
+ // to be common is when the subset ends up being just `__typename`: this would basically mean the fragment
2140
+ // don't really apply to the subgraph, and that this will ensure this is the case.
2141
+ isWorthUsing(): boolean {
2142
+ const selections = this.selections();
2143
+ if (selections.length === 0) {
2144
+ return false;
2145
+ }
2146
+ if (selections.length === 1) {
2147
+ const s = selections[0];
2148
+ return !(s.kind === 'FieldSelection' && s.element.isLeafField());
2149
+ }
2150
+ return true;
2151
+ }
2080
2152
  }
2081
2153
 
2082
2154
  type PathBasedUpdate = { path: OperationPath, selections?: Selection | SelectionSet | readonly Selection[] };
@@ -640,6 +640,28 @@ export class FeatureVersion {
640
640
  return new this(+match[1], +match[2])
641
641
  }
642
642
 
643
+ /**
644
+ * Find the maximum version in a collection of versions, returning undefined in the case
645
+ * that the collection is empty.
646
+ *
647
+ * # Example
648
+ * ```
649
+ * expect(FeatureVersion.max([new FeatureVersion(1, 0), new FeatureVersion(2, 0)])).toBe(new FeatureVersion(2, 0))
650
+ * expect(FeatureVersion.max([])).toBe(undefined)
651
+ * ```
652
+ */
653
+ public static max(versions: Iterable<FeatureVersion>): FeatureVersion | undefined {
654
+ let max: FeatureVersion | undefined;
655
+
656
+ for (const version of versions) {
657
+ if (!max || version > max) {
658
+ max = version;
659
+ }
660
+ }
661
+
662
+ return max;
663
+ }
664
+
643
665
  /**
644
666
  * Return true if and only if this FeatureVersion satisfies the `required` version
645
667
  *