@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/dist/definitions.d.ts +2 -1
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +5 -0
- package/dist/definitions.js.map +1 -1
- package/dist/error.d.ts +1 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +2 -0
- package/dist/error.js.map +1 -1
- package/dist/federation.d.ts +2 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +21 -12
- package/dist/federation.js.map +1 -1
- package/dist/operations.d.ts +3 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +49 -12
- package/dist/operations.js.map +1 -1
- package/dist/specs/coreSpec.d.ts +1 -0
- package/dist/specs/coreSpec.d.ts.map +1 -1
- package/dist/specs/coreSpec.js +9 -0
- package/dist/specs/coreSpec.js.map +1 -1
- package/dist/specs/sourceSpec.d.ts +2 -1
- package/dist/specs/sourceSpec.d.ts.map +1 -1
- package/dist/specs/sourceSpec.js +78 -78
- package/dist/specs/sourceSpec.js.map +1 -1
- package/package.json +1 -1
- package/src/definitions.ts +6 -0
- package/src/error.ts +7 -0
- package/src/federation.ts +31 -13
- package/src/operations.ts +95 -23
- package/src/specs/coreSpec.ts +22 -0
- package/src/specs/sourceSpec.ts +125 -116
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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:
|
|
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
|
|
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[] };
|
package/src/specs/coreSpec.ts
CHANGED
|
@@ -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
|
*
|