@apollo/federation-internals 2.0.1 → 2.0.2-alpha.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/CHANGELOG.md +16 -0
- package/dist/buildSchema.d.ts.map +1 -1
- package/dist/buildSchema.js +17 -6
- package/dist/buildSchema.js.map +1 -1
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +13 -6
- package/dist/definitions.js.map +1 -1
- package/dist/error.js +7 -7
- package/dist/error.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +170 -152
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +15 -0
- package/dist/federation.js.map +1 -1
- package/dist/genErrorCodeDoc.js +12 -6
- package/dist/genErrorCodeDoc.js.map +1 -1
- package/dist/inaccessibleSpec.js +1 -1
- package/dist/inaccessibleSpec.js.map +1 -1
- package/dist/operations.d.ts +38 -4
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +107 -8
- package/dist/operations.js.map +1 -1
- package/dist/schemaUpgrader.d.ts +8 -1
- package/dist/schemaUpgrader.d.ts.map +1 -1
- package/dist/schemaUpgrader.js +40 -1
- package/dist/schemaUpgrader.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/definitions.test.ts +87 -0
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +83 -0
- package/src/__tests__/operations.test.ts +119 -1
- package/src/__tests__/removeInaccessibleElements.test.ts +84 -1
- package/src/__tests__/schemaUpgrader.test.ts +38 -0
- package/src/__tests__/subgraphValidation.test.ts +74 -0
- package/src/buildSchema.ts +32 -5
- package/src/definitions.ts +20 -7
- package/src/error.ts +7 -7
- package/src/extractSubgraphsFromSupergraph.ts +229 -204
- package/src/federation.ts +50 -0
- package/src/genErrorCodeDoc.ts +13 -7
- package/src/inaccessibleSpec.ts +12 -2
- package/src/operations.ts +179 -8
- package/src/schemaUpgrader.ts +45 -0
- package/tsconfig.test.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/federation.ts
CHANGED
|
@@ -1013,6 +1013,56 @@ function isFedSpecLinkDirective(directive: Directive<SchemaDefinition>): directi
|
|
|
1013
1013
|
}
|
|
1014
1014
|
|
|
1015
1015
|
function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] {
|
|
1016
|
+
|
|
1017
|
+
// We special case @key, @requires and @provides because we've seen existing user schema where those
|
|
1018
|
+
// have been defined in an invalid way, but in a way that fed1 wasn't rejecting. So for convenience,
|
|
1019
|
+
// if we detect one of those case, we just remove the definition and let the code afteward add the
|
|
1020
|
+
// proper definition back.
|
|
1021
|
+
// Note that, in a perfect world, we'd do this within the `SchemaUpgrader`. But the way the code
|
|
1022
|
+
// is organised, this method is called before we reach the `SchemaUpgrader`, and it doesn't seem
|
|
1023
|
+
// worth refactoring things drastically for that minor convenience.
|
|
1024
|
+
for (const spec of [keyDirectiveSpec, providesDirectiveSpec, requiresDirectiveSpec]) {
|
|
1025
|
+
const directive = schema.directive(spec.name);
|
|
1026
|
+
if (!directive) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// We shouldn't have applications at the time of this writing because `completeSubgraphSchema`, which calls this,
|
|
1031
|
+
// is only called:
|
|
1032
|
+
// 1. during schema parsing, by `FederationBluePrint.onDirectiveDefinitionAndSchemaParsed`, and that is called
|
|
1033
|
+
// before we process any directive applications.
|
|
1034
|
+
// 2. by `setSchemaAsFed2Subgraph`, but as the name imply, this trickles to `completeFed2SubgraphSchema`, not
|
|
1035
|
+
// this one method.
|
|
1036
|
+
// In other words, there is currently no way to create a full fed1 schema first, and get that method called
|
|
1037
|
+
// second. If that changes (no real reason but...), we'd have to modify this because when we remove the
|
|
1038
|
+
// definition to re-add the "correct" version, we'd have to re-attach existing applications (doable but not
|
|
1039
|
+
// done). This assert is so we notice it quickly if that ever happens (again, unlikely, because fed1 schema
|
|
1040
|
+
// is a backward compatibility thing and there is no reason to expand that too much in the future).
|
|
1041
|
+
assert(directive.applications().length === 0, `${directive} shouldn't have had validation at that places`);
|
|
1042
|
+
|
|
1043
|
+
// The patterns we recognize and "correct" (by essentially ignoring the definition)
|
|
1044
|
+
// are:
|
|
1045
|
+
// 1. if the definition has no arguments at all.
|
|
1046
|
+
// 2. if the `fields` argument is declared as nullable.
|
|
1047
|
+
// 3. if the `fields` argument type is named "FieldSet" instead of "_FieldSet".
|
|
1048
|
+
//
|
|
1049
|
+
// Note that they all correspong to things we've seen in use schema.
|
|
1050
|
+
const fieldType = directive.argument('fields')?.type?.toString();
|
|
1051
|
+
// Note that to be on the safe side, we check that `fields` is the only argument. That's
|
|
1052
|
+
// because while fed2 accepts the optional `resolvable` arg for @key, fed1 only ever
|
|
1053
|
+
// accepted that one argument for all those directives. But if the use had definited
|
|
1054
|
+
// more arguments _and_ provided value for such extra argument in some applications,
|
|
1055
|
+
// us removing the definition would create validation errors that would be hard to
|
|
1056
|
+
// understand for the user.
|
|
1057
|
+
const fieldTypeIsWrongInKnownWays = !!fieldType
|
|
1058
|
+
&& directive.arguments().length === 1
|
|
1059
|
+
&& (fieldType === 'String' || fieldType === '_FieldSet' || fieldType === 'FieldSet');
|
|
1060
|
+
|
|
1061
|
+
if (directive.arguments().length === 0 || fieldTypeIsWrongInKnownWays) {
|
|
1062
|
+
directive.remove();
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1016
1066
|
return [
|
|
1017
1067
|
fieldSetTypeSpec.checkOrAdd(schema, '_' + fieldSetTypeSpec.name),
|
|
1018
1068
|
keyDirectiveSpec.checkOrAdd(schema),
|
package/src/genErrorCodeDoc.ts
CHANGED
|
@@ -12,10 +12,10 @@ When Apollo Gateway attempts to **compose** the schemas provided by your [subgra
|
|
|
12
12
|
* The resulting supergraph schema is valid
|
|
13
13
|
* The gateway has all of the information it needs to execute operations against the resulting schema
|
|
14
14
|
|
|
15
|
-
If Apollo Gateway encounters an error, composition fails. This document lists
|
|
15
|
+
If Apollo Gateway encounters an error, composition fails. This document lists subgraph validation and composition error codes, along with their root causes.
|
|
16
16
|
`;
|
|
17
17
|
|
|
18
|
-
function
|
|
18
|
+
function makeMarkdownArray(
|
|
19
19
|
headers: string[],
|
|
20
20
|
rows: string[][]
|
|
21
21
|
): string {
|
|
@@ -45,12 +45,15 @@ rows.sort(sortRowsByCode);
|
|
|
45
45
|
|
|
46
46
|
const errorsSection = `## Errors
|
|
47
47
|
|
|
48
|
-
The following errors
|
|
48
|
+
The following errors might be raised during composition:
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
<div class="sticky-table">
|
|
51
|
+
|
|
52
|
+
${makeMarkdownArray(
|
|
51
53
|
[ 'Code', 'Description', 'Since', 'Comment' ],
|
|
52
54
|
rows
|
|
53
|
-
)
|
|
55
|
+
)}
|
|
56
|
+
</div>`;
|
|
54
57
|
|
|
55
58
|
const removedErrors = REMOVED_ERRORS
|
|
56
59
|
.map(([code, comment]) => ['`' + code + '`', comment])
|
|
@@ -58,9 +61,12 @@ const removedErrors = REMOVED_ERRORS
|
|
|
58
61
|
|
|
59
62
|
const removedSection = `## Removed codes
|
|
60
63
|
|
|
61
|
-
The following
|
|
64
|
+
The following error codes have been removed and are no longer generated by the most recent version of the \`@apollo/gateway\` library:
|
|
65
|
+
|
|
66
|
+
<div class="sticky-table">
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
${makeMarkdownArray(['Removed Code', 'Comment'], removedErrors)}
|
|
69
|
+
</div>`;
|
|
64
70
|
|
|
65
71
|
console.log(
|
|
66
72
|
header + '\n\n'
|
package/src/inaccessibleSpec.ts
CHANGED
|
@@ -211,8 +211,18 @@ function validateInaccessibleElements(
|
|
|
211
211
|
) {
|
|
212
212
|
// These are top-level elements. If they're not @inaccessible, the only
|
|
213
213
|
// way they won't be in the API schema is if they're definitions of some
|
|
214
|
-
// core feature.
|
|
215
|
-
|
|
214
|
+
// core feature. However, we do intend on introducing mechanisms for
|
|
215
|
+
// exposing core feature elements in the API schema in the near feature.
|
|
216
|
+
// Because such mechanisms aren't completely nailed down yet, we opt to
|
|
217
|
+
// pretend here that all core feature elements are in the API schema for
|
|
218
|
+
// simplicity sake.
|
|
219
|
+
//
|
|
220
|
+
// This has the effect that if a non-core schema element is referenced by
|
|
221
|
+
// a core schema element, that non-core schema element can't be marked
|
|
222
|
+
// @inaccessible, despite that the core schema element may likely not be
|
|
223
|
+
// in the API schema. This may be relaxed in a later version of the
|
|
224
|
+
// inaccessible spec.
|
|
225
|
+
return true;
|
|
216
226
|
} else if (
|
|
217
227
|
(element instanceof FieldDefinition) ||
|
|
218
228
|
(element instanceof ArgumentDefinition) ||
|
package/src/operations.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
typenameFieldName,
|
|
43
43
|
NamedType,
|
|
44
44
|
} from "./definitions";
|
|
45
|
+
import { sameType } from "./types";
|
|
45
46
|
import { assert, mapEntries, MapWithCachedArrays, MultiMap } from "./utils";
|
|
46
47
|
import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
|
|
47
48
|
|
|
@@ -305,6 +306,37 @@ export function sameOperationPaths(p1: OperationPath, p2: OperationPath): boolea
|
|
|
305
306
|
return true;
|
|
306
307
|
}
|
|
307
308
|
|
|
309
|
+
export function concatOperationPaths(head: OperationPath, tail: OperationPath): OperationPath {
|
|
310
|
+
// While this is mainly a simple array concatenation, we optimize slightly by recognizing if the
|
|
311
|
+
// tail path starts by a fragment selection that is useless given the end of the head path.
|
|
312
|
+
if (head.length === 0) {
|
|
313
|
+
return tail;
|
|
314
|
+
}
|
|
315
|
+
if (tail.length === 0) {
|
|
316
|
+
return head;
|
|
317
|
+
}
|
|
318
|
+
const lastOfHead = head[head.length - 1];
|
|
319
|
+
const firstOfTail = tail[0];
|
|
320
|
+
if (isUselessFollowupElement(lastOfHead, firstOfTail)) {
|
|
321
|
+
tail = tail.slice(1);
|
|
322
|
+
}
|
|
323
|
+
return head.concat(tail);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isUselessFollowupElement(first: OperationElement, followup: OperationElement): boolean {
|
|
327
|
+
const typeOfFirst = first.kind === 'Field'
|
|
328
|
+
? baseType(first.definition.type!)
|
|
329
|
+
: first.typeCondition;
|
|
330
|
+
|
|
331
|
+
// The followup is useless if it's a fragment (with no directives we would want to preserve) whose type
|
|
332
|
+
// is already that of the first element.
|
|
333
|
+
return !!typeOfFirst
|
|
334
|
+
&& followup.kind === 'FragmentElement'
|
|
335
|
+
&& !!followup.typeCondition
|
|
336
|
+
&& followup.appliedDirectives.length === 0
|
|
337
|
+
&& sameType(typeOfFirst, followup.typeCondition);
|
|
338
|
+
}
|
|
339
|
+
|
|
308
340
|
export type RootOperationPath = {
|
|
309
341
|
rootKind: SchemaRootKind,
|
|
310
342
|
path: OperationPath
|
|
@@ -504,7 +536,56 @@ export class NamedFragments {
|
|
|
504
536
|
}
|
|
505
537
|
}
|
|
506
538
|
|
|
507
|
-
|
|
539
|
+
abstract class Freezable<T> {
|
|
540
|
+
private _isFrozen: boolean = false;
|
|
541
|
+
|
|
542
|
+
protected abstract us(): T;
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Freezes this selection/selection set, making it immutable after that point (that is, attempts to modify it will error out).
|
|
546
|
+
*
|
|
547
|
+
* This method should be used when a selection/selection set should not be modified. It ensures both that:
|
|
548
|
+
* 1. direct attempts to modify the selection afterward fails (at runtime, but the goal is to fetch bugs early and easily).
|
|
549
|
+
* 2. if this selection/selection set is "added" to another non-frozen selection (say, if this is input to `anotherSet.mergeIn(this)`),
|
|
550
|
+
* then it is automatically cloned first (thus ensuring this copy is not modified). Note that this properly is not guaranteed for
|
|
551
|
+
* non frozen selections. Meaning that if one does `s1.mergeIn(s2)` and `s2` is not frozen, then `s1` may (or may not) reference
|
|
552
|
+
* `s2` directly (without cloning) and thus later modifications to `s1` may (or may not) modify `s2`. This
|
|
553
|
+
* do-not-defensively-clone-by-default behaviour is done for performance reasons.
|
|
554
|
+
*
|
|
555
|
+
* Note that freezing is a "deep" operation, in that the whole structure of the selection/selection set is frozen by this method
|
|
556
|
+
* (and so this is not an excessively cheap operation).
|
|
557
|
+
*
|
|
558
|
+
* @return this selection/selection set (for convenience, to allow method chaining).
|
|
559
|
+
*/
|
|
560
|
+
freeze(): T {
|
|
561
|
+
if (!this.isFrozen()) {
|
|
562
|
+
this.freezeInternals();
|
|
563
|
+
this._isFrozen = true;
|
|
564
|
+
}
|
|
565
|
+
return this.us();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
protected abstract freezeInternals(): void;
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Whether this selection/selection set is frozen. See `freeze` for details.
|
|
572
|
+
*/
|
|
573
|
+
isFrozen(): boolean {
|
|
574
|
+
return this._isFrozen;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* A shortcut for returning a mutable version of this selection/selection set by cloning it if it is frozen, but returning this set directly
|
|
579
|
+
* if it is not frozen.
|
|
580
|
+
*/
|
|
581
|
+
cloneIfFrozen(): T {
|
|
582
|
+
return this.isFrozen() ? this.clone() : this.us();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
abstract clone(): T;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export class SelectionSet extends Freezable<SelectionSet> {
|
|
508
589
|
// The argument is either the responseName (for fields), or the type name (for fragments), with the empty string being used as a special
|
|
509
590
|
// case for a fragment with no type condition.
|
|
510
591
|
private readonly _selections = new MultiMap<string, Selection>();
|
|
@@ -515,9 +596,14 @@ export class SelectionSet {
|
|
|
515
596
|
readonly parentType: CompositeType,
|
|
516
597
|
readonly fragments?: NamedFragments
|
|
517
598
|
) {
|
|
599
|
+
super();
|
|
518
600
|
validate(!isLeafType(parentType), () => `Cannot have selection on non-leaf type ${parentType}`);
|
|
519
601
|
}
|
|
520
602
|
|
|
603
|
+
protected us(): SelectionSet {
|
|
604
|
+
return this;
|
|
605
|
+
}
|
|
606
|
+
|
|
521
607
|
selections(reversedOrder: boolean = false): readonly Selection[] {
|
|
522
608
|
if (!this._cachedSelections) {
|
|
523
609
|
const selections = new Array(this._selectionCount);
|
|
@@ -604,18 +690,66 @@ export class SelectionSet {
|
|
|
604
690
|
return withExpanded;
|
|
605
691
|
}
|
|
606
692
|
|
|
693
|
+
/**
|
|
694
|
+
* Returns the selection select from filtering out any selection that does not match the provided predicate.
|
|
695
|
+
*
|
|
696
|
+
* Please that this method will expand *ALL* fragments as the result of applying it's filtering. You should
|
|
697
|
+
* call `optimize` on the result if you want to re-apply some fragments.
|
|
698
|
+
*/
|
|
699
|
+
filter(predicate: (selection: Selection) => boolean): SelectionSet {
|
|
700
|
+
const filtered = new SelectionSet(this.parentType, this.fragments);
|
|
701
|
+
for (const selection of this.selections()) {
|
|
702
|
+
const filteredSelection = selection.filter(predicate);
|
|
703
|
+
if (filteredSelection) {
|
|
704
|
+
filtered.add(filteredSelection);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return filtered;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
protected freezeInternals(): void {
|
|
711
|
+
for (const selection of this.selections()) {
|
|
712
|
+
selection.freeze();
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Adds the selections of the provided selection set to this selection, merging common selection as necessary.
|
|
718
|
+
*
|
|
719
|
+
* Please note that by default, the selection from the input may (or may not) be directly referenced by this selection
|
|
720
|
+
* set after this method return. That is, future modification of this selection set may end up modifying the input
|
|
721
|
+
* set due to direct aliasing. If direct aliasing should be prevented, the input selection set should be frozen (see
|
|
722
|
+
* `freeze` for details).
|
|
723
|
+
*/
|
|
607
724
|
mergeIn(selectionSet: SelectionSet) {
|
|
608
725
|
for (const selection of selectionSet.selections()) {
|
|
609
726
|
this.add(selection);
|
|
610
727
|
}
|
|
611
728
|
}
|
|
612
729
|
|
|
730
|
+
/**
|
|
731
|
+
* Adds the provided selections to this selection, merging common selection as necessary.
|
|
732
|
+
*
|
|
733
|
+
* This is very similar to `mergeIn` except that it takes a direct array of selection, and the direct aliasing
|
|
734
|
+
* remarks from `mergeInd` applies here too.
|
|
735
|
+
*/
|
|
613
736
|
addAll(selections: Selection[]): SelectionSet {
|
|
614
737
|
selections.forEach(s => this.add(s));
|
|
615
738
|
return this;
|
|
616
739
|
}
|
|
617
740
|
|
|
741
|
+
/**
|
|
742
|
+
* Adds the provided selection to this selection, merging it to any existing selection of this set as appropriate.
|
|
743
|
+
*
|
|
744
|
+
* Please note that by default, the input selection may (or may not) be directly referenced by this selection
|
|
745
|
+
* set after this method return. That is, future modification of this selection set may end up modifying the input
|
|
746
|
+
* selection due to direct aliasing. If direct aliasing should be prevented, the input selection should be frozen
|
|
747
|
+
* (see `freeze` for details).
|
|
748
|
+
*/
|
|
618
749
|
add(selection: Selection): Selection {
|
|
750
|
+
// It's a bug to try to add to a frozen selection set
|
|
751
|
+
assert(!this.isFrozen(), () => `Cannot add to frozen selection: ${this}`);
|
|
752
|
+
|
|
619
753
|
const toAdd = selection.updateForAddingTo(this);
|
|
620
754
|
const key = toAdd.key();
|
|
621
755
|
const existing: Selection[] | undefined = this._selections.get(key);
|
|
@@ -902,7 +1036,7 @@ export function selectionSetOfPath(path: OperationPath, onPathEnd?: (finalSelect
|
|
|
902
1036
|
|
|
903
1037
|
export type Selection = FieldSelection | FragmentSelection;
|
|
904
1038
|
|
|
905
|
-
export class FieldSelection {
|
|
1039
|
+
export class FieldSelection extends Freezable<FieldSelection> {
|
|
906
1040
|
readonly kind = 'FieldSelection' as const;
|
|
907
1041
|
readonly selectionSet?: SelectionSet;
|
|
908
1042
|
|
|
@@ -910,9 +1044,14 @@ export class FieldSelection {
|
|
|
910
1044
|
readonly field: Field<any>,
|
|
911
1045
|
initialSelectionSet? : SelectionSet
|
|
912
1046
|
) {
|
|
1047
|
+
super();
|
|
913
1048
|
const type = baseType(field.definition.type!);
|
|
914
1049
|
// Field types are output type, and a named typethat is an output one and isn't a leaf is guaranteed to be selectable.
|
|
915
|
-
this.selectionSet = isLeafType(type) ? undefined : (initialSelectionSet ? initialSelectionSet : new SelectionSet(type as CompositeType));
|
|
1050
|
+
this.selectionSet = isLeafType(type) ? undefined : (initialSelectionSet ? initialSelectionSet.cloneIfFrozen() : new SelectionSet(type as CompositeType));
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
protected us(): FieldSelection {
|
|
1054
|
+
return this;
|
|
916
1055
|
}
|
|
917
1056
|
|
|
918
1057
|
key(): string {
|
|
@@ -940,6 +1079,20 @@ export class FieldSelection {
|
|
|
940
1079
|
: new FieldSelection(this.field, optimizedSelection);
|
|
941
1080
|
}
|
|
942
1081
|
|
|
1082
|
+
filter(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
|
|
1083
|
+
if (!predicate(this)) {
|
|
1084
|
+
return undefined;
|
|
1085
|
+
}
|
|
1086
|
+
if (!this.selectionSet) {
|
|
1087
|
+
return this;
|
|
1088
|
+
}
|
|
1089
|
+
return new FieldSelection(this.field, this.selectionSet.filter(predicate));
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
protected freezeInternals(): void {
|
|
1093
|
+
this.selectionSet?.freeze();
|
|
1094
|
+
}
|
|
1095
|
+
|
|
943
1096
|
expandFragments(names?: string[], updateSelectionSetFragments: boolean = true): FieldSelection {
|
|
944
1097
|
const expandedSelection = this.selectionSet ? this.selectionSet.expandFragments(names, updateSelectionSetFragments) : undefined;
|
|
945
1098
|
return this.selectionSet === expandedSelection
|
|
@@ -975,7 +1128,9 @@ export class FieldSelection {
|
|
|
975
1128
|
|
|
976
1129
|
updateForAddingTo(selectionSet: SelectionSet): FieldSelection {
|
|
977
1130
|
const updatedField = this.field.updateForAddingTo(selectionSet);
|
|
978
|
-
return this.field === updatedField
|
|
1131
|
+
return this.field === updatedField
|
|
1132
|
+
? this.cloneIfFrozen()
|
|
1133
|
+
: new FieldSelection(updatedField, this.selectionSet?.cloneIfFrozen());
|
|
979
1134
|
}
|
|
980
1135
|
|
|
981
1136
|
toSelectionNode(): FieldNode {
|
|
@@ -1034,7 +1189,7 @@ export class FieldSelection {
|
|
|
1034
1189
|
}
|
|
1035
1190
|
}
|
|
1036
1191
|
|
|
1037
|
-
export abstract class FragmentSelection {
|
|
1192
|
+
export abstract class FragmentSelection extends Freezable<FragmentSelection> {
|
|
1038
1193
|
readonly kind = 'FragmentSelection' as const;
|
|
1039
1194
|
|
|
1040
1195
|
abstract key(): string;
|
|
@@ -1055,6 +1210,9 @@ export abstract class FragmentSelection {
|
|
|
1055
1210
|
|
|
1056
1211
|
abstract validate(): void;
|
|
1057
1212
|
|
|
1213
|
+
protected us(): FragmentSelection {
|
|
1214
|
+
return this;
|
|
1215
|
+
}
|
|
1058
1216
|
|
|
1059
1217
|
usedVariables(): Variables {
|
|
1060
1218
|
return mergeVariables(this.element().variables(), this.selectionSet.usedVariables());
|
|
@@ -1062,7 +1220,21 @@ export abstract class FragmentSelection {
|
|
|
1062
1220
|
|
|
1063
1221
|
updateForAddingTo(selectionSet: SelectionSet): FragmentSelection {
|
|
1064
1222
|
const updatedFragment = this.element().updateForAddingTo(selectionSet);
|
|
1065
|
-
return this.element() === updatedFragment
|
|
1223
|
+
return this.element() === updatedFragment
|
|
1224
|
+
? this.cloneIfFrozen()
|
|
1225
|
+
: new InlineFragmentSelection(updatedFragment, this.selectionSet.cloneIfFrozen());
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
filter(predicate: (selection: Selection) => boolean): InlineFragmentSelection | undefined {
|
|
1229
|
+
if (!predicate(this)) {
|
|
1230
|
+
return undefined;
|
|
1231
|
+
}
|
|
1232
|
+
// Note that we essentially expand all fragments as part of this.
|
|
1233
|
+
return new InlineFragmentSelection(this.element(), this.selectionSet.filter(predicate));
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
protected freezeInternals() {
|
|
1237
|
+
this.selectionSet.freeze();
|
|
1066
1238
|
}
|
|
1067
1239
|
|
|
1068
1240
|
equals(that: Selection): boolean {
|
|
@@ -1095,7 +1267,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
1095
1267
|
super();
|
|
1096
1268
|
// TODO: we should do validate the type of the initial selection set.
|
|
1097
1269
|
this._selectionSet = initialSelectionSet
|
|
1098
|
-
? initialSelectionSet
|
|
1270
|
+
? initialSelectionSet.cloneIfFrozen()
|
|
1099
1271
|
: new SelectionSet(fragmentElement.typeCondition ? fragmentElement.typeCondition : fragmentElement.parentType);
|
|
1100
1272
|
}
|
|
1101
1273
|
|
|
@@ -1144,7 +1316,6 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
1144
1316
|
};
|
|
1145
1317
|
}
|
|
1146
1318
|
|
|
1147
|
-
|
|
1148
1319
|
optimize(fragments: NamedFragments): FragmentSelection {
|
|
1149
1320
|
const optimizedSelection = this.selectionSet.optimize(fragments);
|
|
1150
1321
|
const typeCondition = this.element().typeCondition;
|
package/src/schemaUpgrader.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { ERRORS } from "./error";
|
|
8
8
|
import {
|
|
9
9
|
baseType,
|
|
10
|
+
CompositeType,
|
|
10
11
|
Directive,
|
|
11
12
|
errorCauses,
|
|
12
13
|
Extension,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
} from "./federation";
|
|
34
35
|
import { assert, firstOf, MultiMap } from "./utils";
|
|
35
36
|
import { FEDERATION_SPEC_TYPES } from "./federationSpec";
|
|
37
|
+
import { valueEquals } from "./values";
|
|
36
38
|
|
|
37
39
|
export type UpgradeResult = UpgradeSuccess | UpgradeFailure;
|
|
38
40
|
|
|
@@ -66,6 +68,7 @@ export type UpgradeChange =
|
|
|
66
68
|
| ProvidesOrRequiresOnInterfaceFieldRemoval
|
|
67
69
|
| ProvidesOnNonCompositeRemoval
|
|
68
70
|
| FieldsArgumentCoercionToString
|
|
71
|
+
| RemovedTagOnExternal
|
|
69
72
|
;
|
|
70
73
|
|
|
71
74
|
export class ExternalOnTypeExtensionRemoval {
|
|
@@ -198,6 +201,16 @@ export class FieldsArgumentCoercionToString {
|
|
|
198
201
|
}
|
|
199
202
|
}
|
|
200
203
|
|
|
204
|
+
export class RemovedTagOnExternal {
|
|
205
|
+
readonly id = 'REMOVED_TAG_ON_EXTERNAL' as const;
|
|
206
|
+
|
|
207
|
+
constructor(readonly application: string, readonly element: string) {}
|
|
208
|
+
|
|
209
|
+
toString() {
|
|
210
|
+
return `Removed ${this.application} application on @external "${this.element}" as the @tag application is on another definition`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
201
214
|
export function upgradeSubgraphsIfNecessary(inputs: Subgraphs): UpgradeResult {
|
|
202
215
|
const changes: Map<string, UpgradeChanges> = new Map();
|
|
203
216
|
if (inputs.values().every((s) => s.isFed2Subgraph())) {
|
|
@@ -265,6 +278,11 @@ function resolvesField(subgraph: Subgraph, field: FieldDefinition<ObjectType>):
|
|
|
265
278
|
return !!f && (!metadata.isFieldExternal(f) || metadata.isFieldPartiallyExternal(f));
|
|
266
279
|
}
|
|
267
280
|
|
|
281
|
+
function getField(schema: Schema, typeName: string, fieldName: string): FieldDefinition<CompositeType> | undefined {
|
|
282
|
+
const type = schema.type(typeName);
|
|
283
|
+
return type && isCompositeType(type) ? type.field(fieldName) : undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
268
286
|
class SchemaUpgrader {
|
|
269
287
|
private readonly changes = new MultiMap<UpgradeChangeID, UpgradeChange>();
|
|
270
288
|
private readonly schema: Schema;
|
|
@@ -384,6 +402,8 @@ class SchemaUpgrader {
|
|
|
384
402
|
|
|
385
403
|
this.addShareable();
|
|
386
404
|
|
|
405
|
+
this.removeTagOnExternal();
|
|
406
|
+
|
|
387
407
|
// If we had errors during the upgrade, we throw them before trying to validate the resulting subgraph, because any invalidity in the
|
|
388
408
|
// migrated subgraph may well due to those migration errors and confuse users.
|
|
389
409
|
if (this.errors.length > 0) {
|
|
@@ -670,4 +690,29 @@ class SchemaUpgrader {
|
|
|
670
690
|
}
|
|
671
691
|
}
|
|
672
692
|
}
|
|
693
|
+
|
|
694
|
+
private removeTagOnExternal() {
|
|
695
|
+
const tagDirective = this.schema.directive('tag');
|
|
696
|
+
if (!tagDirective) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
for (const application of tagDirective.applications()) {
|
|
701
|
+
const element = application.parent;
|
|
702
|
+
if (!(element instanceof FieldDefinition)) {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (this.external(element)) {
|
|
706
|
+
const tagIsUsedInOtherDefinition = this.otherSubgraphs
|
|
707
|
+
.map((s) => getField(s.schema, element.parent.name, element.name))
|
|
708
|
+
.filter((f) => !(f && f.hasAppliedDirective('external')))
|
|
709
|
+
.some((f) => f && f.appliedDirectivesOf('tag').some((d) => valueEquals(application.arguments(), d.arguments())));
|
|
710
|
+
|
|
711
|
+
if (tagIsUsedInOtherDefinition) {
|
|
712
|
+
this.addChange(new RemovedTagOnExternal(application.toString(), element.coordinate));
|
|
713
|
+
application.remove();
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
673
718
|
}
|