@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/buildSchema.d.ts.map +1 -1
  3. package/dist/buildSchema.js +17 -6
  4. package/dist/buildSchema.js.map +1 -1
  5. package/dist/definitions.d.ts.map +1 -1
  6. package/dist/definitions.js +13 -6
  7. package/dist/definitions.js.map +1 -1
  8. package/dist/error.js +7 -7
  9. package/dist/error.js.map +1 -1
  10. package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
  11. package/dist/extractSubgraphsFromSupergraph.js +170 -152
  12. package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
  13. package/dist/federation.d.ts.map +1 -1
  14. package/dist/federation.js +15 -0
  15. package/dist/federation.js.map +1 -1
  16. package/dist/genErrorCodeDoc.js +12 -6
  17. package/dist/genErrorCodeDoc.js.map +1 -1
  18. package/dist/inaccessibleSpec.js +1 -1
  19. package/dist/inaccessibleSpec.js.map +1 -1
  20. package/dist/operations.d.ts +38 -4
  21. package/dist/operations.d.ts.map +1 -1
  22. package/dist/operations.js +107 -8
  23. package/dist/operations.js.map +1 -1
  24. package/dist/schemaUpgrader.d.ts +8 -1
  25. package/dist/schemaUpgrader.d.ts.map +1 -1
  26. package/dist/schemaUpgrader.js +40 -1
  27. package/dist/schemaUpgrader.js.map +1 -1
  28. package/package.json +2 -2
  29. package/src/__tests__/definitions.test.ts +87 -0
  30. package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +83 -0
  31. package/src/__tests__/operations.test.ts +119 -1
  32. package/src/__tests__/removeInaccessibleElements.test.ts +84 -1
  33. package/src/__tests__/schemaUpgrader.test.ts +38 -0
  34. package/src/__tests__/subgraphValidation.test.ts +74 -0
  35. package/src/buildSchema.ts +32 -5
  36. package/src/definitions.ts +20 -7
  37. package/src/error.ts +7 -7
  38. package/src/extractSubgraphsFromSupergraph.ts +229 -204
  39. package/src/federation.ts +50 -0
  40. package/src/genErrorCodeDoc.ts +13 -7
  41. package/src/inaccessibleSpec.ts +12 -2
  42. package/src/operations.ts +179 -8
  43. package/src/schemaUpgrader.ts +45 -0
  44. package/tsconfig.test.tsbuildinfo +1 -1
  45. 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),
@@ -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 subgraphs and composition error codes and their root causes.
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 makeMardownArray(
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 may be raised by composition:
48
+ The following errors might be raised during composition:
49
49
 
50
- ` + makeMardownArray(
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 section lists code that have been removed and are not longer generated by the gateway version this is the documentation for.
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
- ` + makeMardownArray(['Removed Code', 'Comment'], removedErrors);
68
+ ${makeMarkdownArray(['Removed Code', 'Comment'], removedErrors)}
69
+ </div>`;
64
70
 
65
71
  console.log(
66
72
  header + '\n\n'
@@ -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
- return !isFeatureDefinition(element);
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
- export class SelectionSet {
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 ? this : new FieldSelection(updatedField, this.selectionSet);
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 ? this : new InlineFragmentSelection(updatedFragment, this.selectionSet);
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;
@@ -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
  }