@apollo/federation-internals 2.4.1 → 2.4.3

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 (64) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/argumentCompositionStrategies.d.ts +34 -0
  3. package/dist/argumentCompositionStrategies.d.ts.map +1 -0
  4. package/dist/argumentCompositionStrategies.js +35 -0
  5. package/dist/argumentCompositionStrategies.js.map +1 -0
  6. package/dist/coreSpec.d.ts +12 -3
  7. package/dist/coreSpec.d.ts.map +1 -1
  8. package/dist/coreSpec.js +68 -17
  9. package/dist/coreSpec.js.map +1 -1
  10. package/dist/definitions.d.ts +1 -0
  11. package/dist/definitions.d.ts.map +1 -1
  12. package/dist/definitions.js +30 -27
  13. package/dist/definitions.js.map +1 -1
  14. package/dist/directiveAndTypeSpecification.d.ts +26 -7
  15. package/dist/directiveAndTypeSpecification.d.ts.map +1 -1
  16. package/dist/directiveAndTypeSpecification.js +56 -4
  17. package/dist/directiveAndTypeSpecification.js.map +1 -1
  18. package/dist/federation.d.ts.map +1 -1
  19. package/dist/federation.js +24 -2
  20. package/dist/federation.js.map +1 -1
  21. package/dist/federationSpec.d.ts +2 -13
  22. package/dist/federationSpec.d.ts.map +1 -1
  23. package/dist/federationSpec.js +10 -60
  24. package/dist/federationSpec.js.map +1 -1
  25. package/dist/inaccessibleSpec.d.ts +0 -2
  26. package/dist/inaccessibleSpec.d.ts.map +1 -1
  27. package/dist/inaccessibleSpec.js +3 -6
  28. package/dist/inaccessibleSpec.js.map +1 -1
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +5 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/knownCoreFeatures.d.ts +1 -0
  34. package/dist/knownCoreFeatures.d.ts.map +1 -1
  35. package/dist/knownCoreFeatures.js +5 -1
  36. package/dist/knownCoreFeatures.js.map +1 -1
  37. package/dist/operations.d.ts +18 -6
  38. package/dist/operations.d.ts.map +1 -1
  39. package/dist/operations.js +102 -37
  40. package/dist/operations.js.map +1 -1
  41. package/dist/print.d.ts +7 -1
  42. package/dist/print.d.ts.map +1 -1
  43. package/dist/print.js +33 -5
  44. package/dist/print.js.map +1 -1
  45. package/dist/tagSpec.d.ts +0 -2
  46. package/dist/tagSpec.d.ts.map +1 -1
  47. package/dist/tagSpec.js +4 -10
  48. package/dist/tagSpec.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/__tests__/directiveAndTypeSpecifications.test.ts +41 -0
  51. package/src/__tests__/operations.test.ts +175 -10
  52. package/src/argumentCompositionStrategies.ts +39 -0
  53. package/src/coreSpec.ts +94 -34
  54. package/src/definitions.ts +35 -29
  55. package/src/directiveAndTypeSpecification.ts +101 -14
  56. package/src/federation.ts +33 -4
  57. package/src/federationSpec.ts +13 -73
  58. package/src/inaccessibleSpec.ts +4 -11
  59. package/src/index.ts +3 -0
  60. package/src/knownCoreFeatures.ts +9 -0
  61. package/src/operations.ts +198 -40
  62. package/src/print.ts +39 -4
  63. package/src/tagSpec.ts +4 -12
  64. package/tsconfig.tsbuildinfo +1 -1
package/src/operations.ts CHANGED
@@ -49,7 +49,7 @@ import {
49
49
  } from "./definitions";
50
50
  import { isInterfaceObjectType } from "./federation";
51
51
  import { ERRORS } from "./error";
52
- import { sameType } from "./types";
52
+ import { isSubtype, sameType } from "./types";
53
53
  import { assert, isDefined, mapEntries, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
54
54
  import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
55
55
  import { v1 as uuidv1 } from 'uuid';
@@ -678,12 +678,12 @@ function isUselessFollowupElement(first: OperationElement, followup: OperationEl
678
678
  : first.typeCondition;
679
679
 
680
680
  // The followup is useless if it's a fragment (with no directives we would want to preserve) whose type
681
- // is already that of the first element.
681
+ // is already that of the first element (or a supertype).
682
682
  return !!typeOfFirst
683
683
  && followup.kind === 'FragmentElement'
684
684
  && !!followup.typeCondition
685
685
  && (followup.appliedDirectives.length === 0 || isDirectiveApplicationsSubset(conditionals, followup.appliedDirectives))
686
- && sameType(typeOfFirst, followup.typeCondition);
686
+ && isSubtype(followup.typeCondition, typeOfFirst);
687
687
  }
688
688
 
689
689
  export type RootOperationPath = {
@@ -732,8 +732,13 @@ export class Operation {
732
732
  // probably not noticeable in practice so ...).
733
733
  const toDeoptimize = mapEntries(usages).filter(([_, count]) => count < minUsagesToOptimize).map(([name]) => name);
734
734
 
735
- const newFragments = optimizedSelection.fragments?.without(toDeoptimize);
736
- optimizedSelection = optimizedSelection.expandFragments(toDeoptimize, newFragments);
735
+ if (toDeoptimize.length > 0) {
736
+ const newFragments = optimizedSelection.fragments?.without(toDeoptimize);
737
+ optimizedSelection = optimizedSelection.expandFragments(toDeoptimize, newFragments);
738
+ // Expanding fragments could create some "inefficiencies" that we wouldn't have if we hadn't re-optimized
739
+ // the fragments to de-optimize it later, so we do a final "trim" pass to remove those.
740
+ optimizedSelection = optimizedSelection.trimUnsatisfiableBranches(optimizedSelection.parentType);
741
+ }
737
742
 
738
743
  return new Operation(this.schema, this.rootKind, optimizedSelection, this.variableDefinitions, this.name);
739
744
  }
@@ -841,6 +846,8 @@ export class Operation {
841
846
  export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmentDefinition> {
842
847
  private _selectionSet: SelectionSet | undefined;
843
848
 
849
+ private readonly selectionSetsAtTypesCache = new Map<string, SelectionSet>();
850
+
844
851
  constructor(
845
852
  schema: Schema,
846
853
  readonly name: string,
@@ -852,6 +859,9 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
852
859
 
853
860
  setSelectionSet(selectionSet: SelectionSet): NamedFragmentDefinition {
854
861
  assert(!this._selectionSet, 'Attempting to set the selection set of a fragment definition already built')
862
+ // We set the selection set post-construction to simplify the handling of fragments that use other fragments,
863
+ // but let's make sure we've properly used the fragment type condition as parent type of the selection set, as we should.
864
+ assert(selectionSet.parentType === this.typeCondition, `Fragment selection set parent is ${selectionSet.parentType} differs from the fragment condition type ${this.typeCondition}`);
855
865
  this._selectionSet = selectionSet;
856
866
  return this;
857
867
  }
@@ -894,32 +904,53 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
894
904
  * @param type - the type at which we're looking at applying the fragment
895
905
  */
896
906
  canApplyAtType(type: CompositeType): boolean {
897
- const applyAtType = sameType(type, this.typeCondition) || runtimeTypesIntersects(type, this.typeCondition);
898
- return applyAtType
899
- && this.validForSchema(type.schema());
907
+ return sameType(type, this.typeCondition) || runtimeTypesIntersects(type, this.typeCondition);
900
908
  }
901
909
 
902
- // Checks whether this named fragment can be applied to the provided schema, which might be different
903
- // from the one the named fragment originate from.
904
- private validForSchema(schema: Schema): boolean {
905
- if (schema === this.schema()) {
906
- return true;
910
+ /**
911
+ * This methods *assumes* that `this.canApplyAtType(type)` is `true` (and may crash if this is not true), and returns
912
+ * a version fo this named fragment selection set that corresponds to the "expansion" of this named fragment at `type`
913
+ *
914
+ * The overall idea here is that if we have an interface I with 2 implementations T1 and T2, and we have a fragment like:
915
+ * ```graphql
916
+ * fragment X on I {
917
+ * ... on T1 {
918
+ * <stuff>
919
+ * }
920
+ * ... on T2 {
921
+ * <stuff>
922
+ * }
923
+ * }
924
+ * ```
925
+ * then if the current type is `T1`, then all we care about matching for this fragment is the `... on T1` part, and this method gives
926
+ * us that part.
927
+ */
928
+ selectionSetAtType(type: CompositeType): SelectionSet {
929
+ // First, if the candidate condition is an object or is the type passed, then there isn't any additional restriction to do.
930
+ if (sameType(type, this.typeCondition) || isObjectType(this.typeCondition)) {
931
+ return this.selectionSet;
907
932
  }
908
933
 
909
- const typeInSchema = schema.type(this.typeCondition.name);
910
- if (!typeInSchema || !isCompositeType(typeInSchema)) {
911
- return false;
934
+ // We should not call `trimUnsatisfiableBranches` where `type` is an abstract type (`interface` or `union`) as it currently could
935
+ // create an invalid selection set (and throw down the line). In theory, when `type` is an abstract type, we could look at the
936
+ // intersection of its runtime types with those of `this.typeCondition`, call `trimUnsatisfiableBranches` for each of the resulting
937
+ // object types, and merge all those selection sets, and this "may" result in a smaller selection at times. This is a bit complex
938
+ // and costly to do however, so we just return the selection unchanged for now, which is always valid but simply may not be absolutely
939
+ // optimal.
940
+ // Concretely, this means that there may be corner cases where a named fragment could be reused but isn't, but waiting on finding
941
+ // concrete examples where this matter to decide if it's worth the complexity.
942
+ if (!isObjectType(type)) {
943
+ return this.selectionSet;
912
944
  }
913
945
 
914
- // We try "rebasing" the selection into the provided schema and checks if that succeed.
915
- try {
916
- this.selectionSet.rebaseOn(typeInSchema);
917
- // If this succeed, it means the fragment could be applied to that schema and be valid.
918
- return true;
919
- } catch (e) {
920
- // We don't really care what kind of error was triggered; only that it doesn't work.
921
- return false;
946
+ let selectionSet = this.selectionSetsAtTypesCache.get(type.name);
947
+ if (!selectionSet) {
948
+ // Note that all we want is removing any top-level branches that don't apply due to the current type. There is no point
949
+ // in going recursive however: any simplification due to `type` stops as soon as we traverse a field. And so we don't bother.
950
+ selectionSet = this.selectionSet.trimUnsatisfiableBranches(type, { recursive: false });
951
+ this.selectionSetsAtTypesCache.set(type.name, selectionSet);
922
952
  }
953
+ return selectionSet;
923
954
  }
924
955
 
925
956
  toString(indent?: string): string {
@@ -995,6 +1026,72 @@ export class NamedFragments {
995
1026
  return mapped;
996
1027
  }
997
1028
 
1029
+ /**
1030
+ * This method:
1031
+ * - expands all nested fragments,
1032
+ * - applies the provided mapper to the selection set of the fragments,
1033
+ * - and finally re-fragments the nested fragments.
1034
+ */
1035
+ mapToExpandedSelectionSets(
1036
+ mapper: (selectionSet: SelectionSet) => SelectionSet | undefined,
1037
+ recreateFct: (frag: NamedFragmentDefinition, newSelectionSet: SelectionSet) => NamedFragmentDefinition = (f, s) => f.withUpdatedSelectionSet(s),
1038
+ ): NamedFragments | undefined {
1039
+ type FragmentInfo = {
1040
+ original: NamedFragmentDefinition,
1041
+ mappedSelectionSet: SelectionSet,
1042
+ dependsOn: string[],
1043
+ };
1044
+ const fragmentsMap = new Map<string, FragmentInfo>();
1045
+
1046
+ const removedFragments = new Set<string>();
1047
+ for (const fragment of this.definitions()) {
1048
+ const mappedSelectionSet = mapper(fragment.selectionSet.expandAllFragments().trimUnsatisfiableBranches(fragment.typeCondition));
1049
+ if (!mappedSelectionSet) {
1050
+ removedFragments.add(fragment.name);
1051
+ continue;
1052
+ }
1053
+
1054
+ const otherFragmentsUsages = new Map<string, number>();
1055
+ fragment.collectUsedFragmentNames(otherFragmentsUsages);
1056
+ fragmentsMap.set(fragment.name, {
1057
+ original: fragment,
1058
+ mappedSelectionSet,
1059
+ dependsOn: Array.from(otherFragmentsUsages.keys()),
1060
+ });
1061
+ }
1062
+
1063
+ const mappedFragments = new NamedFragments();
1064
+ while (fragmentsMap.size > 0) {
1065
+ for (const [name, info] of fragmentsMap) {
1066
+ // Note that graphQL specifies that named fragments cannot have cycles (https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles)
1067
+ // and so we're guaranteed that on every iteration, at least one element of the map is removed (so the `while` loop will terminate).
1068
+ if (info.dependsOn.every((n) => mappedFragments.has(n) || removedFragments.has(n))) {
1069
+ const reoptimizedSelectionSet = info.mappedSelectionSet.optimize(mappedFragments);
1070
+ mappedFragments.add(recreateFct(info.original, reoptimizedSelectionSet));
1071
+ fragmentsMap.delete(name);
1072
+ }
1073
+ }
1074
+ }
1075
+
1076
+ return mappedFragments.isEmpty() ? undefined : mappedFragments;
1077
+ }
1078
+
1079
+ rebaseOn(schema: Schema): NamedFragments | undefined {
1080
+ return this.mapToExpandedSelectionSets(
1081
+ (s) => {
1082
+ const rebasedType = schema.type(s.parentType.name);
1083
+ try {
1084
+ return rebasedType && isCompositeType(rebasedType) ? s.rebaseOn(rebasedType) : undefined;
1085
+ } catch (e) {
1086
+ // This means we cannot rebase this selection on the schema and thus cannot reuse that fragment on that
1087
+ // particular schema.
1088
+ return undefined;
1089
+ }
1090
+ },
1091
+ (orig, newSelection) => new NamedFragmentDefinition(schema, orig.name, newSelection.parentType).setSelectionSet(newSelection),
1092
+ );
1093
+ }
1094
+
998
1095
  validate(variableDefinitions: VariableDefinitions) {
999
1096
  for (const fragment of this.fragments.values()) {
1000
1097
  fragment.selectionSet.validate(variableDefinitions);
@@ -1142,6 +1239,36 @@ export class SelectionSet {
1142
1239
  return this;
1143
1240
  }
1144
1241
 
1242
+ // Calling optimizeSelections() will not match a fragment that would have expanded at top-level.
1243
+ // That is, say we have the selection set `{ x y }` for a top-level `Query`, and we have a fragment
1244
+ // ```
1245
+ // fragment F on Query {
1246
+ // x
1247
+ // y
1248
+ // }
1249
+ // ```
1250
+ // then calling `this.optimizeSelections(fragments)` would only apply check if F apply to `x` and
1251
+ // then `y`.
1252
+ //
1253
+ // To ensure the fragment match in this case, we "wrap" the selection into a trivial fragment of
1254
+ // the selection parent, so in the example above, we create selection `... on Query { x y}`.
1255
+ // With that, `optimizeSelections` will correctly match on the `on Query` fragment; after which
1256
+ // we can unpack the final result.
1257
+ const wrapped = new InlineFragmentSelection(new FragmentElement(this.parentType, this.parentType), this);
1258
+ const optimized = wrapped.optimize(fragments);
1259
+
1260
+ // Now, it's possible we matched a full fragment, in which case `optimized` will be just the named fragment,
1261
+ // and in that case we return a singleton selection with just that. Otherwise, it's our wrapping inline fragment
1262
+ // with the sub-selections optimized, and we just return that subselection.
1263
+ return optimized instanceof FragmentSpreadSelection
1264
+ ? selectionSetOf(this.parentType, optimized, fragments)
1265
+ : optimized.selectionSet;
1266
+ }
1267
+
1268
+ // Tries to match fragments inside each selections of this selection set, and this recursively. However, note that this
1269
+ // may not match fragments that would apply at top-level, so you should usually use `optimize` instead (this exists mostly
1270
+ // for the recursion).
1271
+ optimizeSelections(fragments: NamedFragments): SelectionSet {
1145
1272
  // Handling the case where the selection may alreayd have some fragments adds complexity,
1146
1273
  // not only because we need to deal with merging new and existing fragments, but also because
1147
1274
  // things get weird if some fragment names are in common to both. Since we currently only care
@@ -1164,8 +1291,8 @@ export class SelectionSet {
1164
1291
  return this.lazyMap((selection) => selection.expandFragments(names, updatedFragments), { fragments: updatedFragments ?? null });
1165
1292
  }
1166
1293
 
1167
- trimUnsatisfiableBranches(parentType: CompositeType): SelectionSet {
1168
- return this.lazyMap((selection) => selection.trimUnsatisfiableBranches(parentType), { parentType });
1294
+ trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): SelectionSet {
1295
+ return this.lazyMap((selection) => selection.trimUnsatisfiableBranches(parentType, options), { parentType });
1169
1296
  }
1170
1297
 
1171
1298
  /**
@@ -1325,12 +1452,22 @@ export class SelectionSet {
1325
1452
  return true;
1326
1453
  }
1327
1454
 
1328
- diffIfContains(that: SelectionSet): { contains: boolean, diff?: SelectionSet } {
1455
+ // Please note that this method assumes that `candidate.canApplyAtType(parentType) === true` but it is left to the caller to
1456
+ // validate this (`canApplyAtType` is not free, and we want to avoid repeating it multiple times).
1457
+ diffWithNamedFragmentIfContained(candidate: NamedFragmentDefinition, parentType: CompositeType): { contains: boolean, diff?: SelectionSet } {
1458
+ const that = candidate.selectionSetAtType(parentType);
1329
1459
  if (this.contains(that)) {
1330
- const diff = this.minus(that);
1460
+ // One subtlety here is that at "this" sub-selections may already have been optimized with some fragments. It's
1461
+ // usually ok because `candidate` will also use those fragments, but one fragments that `candidate` can never be
1462
+ // using is itself (the `contains` check is fine with this, but it's harder to deal in `minus`). So we expand
1463
+ // the candidate we're currently looking at in "this" to avoid some issues.
1464
+ let updatedThis = this.expandFragments([candidate.name], this.fragments);
1465
+ if (updatedThis !== this) {
1466
+ updatedThis = updatedThis.trimUnsatisfiableBranches(parentType);
1467
+ }
1468
+ const diff = updatedThis.minus(that);
1331
1469
  return { contains: true, diff: diff.isEmpty() ? undefined : diff };
1332
1470
  }
1333
-
1334
1471
  return { contains: false };
1335
1472
  }
1336
1473
 
@@ -1598,9 +1735,19 @@ function addOneToKeyedUpdates(keyedUpdates: MultiMap<string, SelectionUpdate>, s
1598
1735
  }
1599
1736
  }
1600
1737
 
1738
+ function maybeRebaseOnSchema(toRebase: CompositeType, schema: Schema): CompositeType {
1739
+ if (toRebase.schema() === schema) {
1740
+ return toRebase;
1741
+ }
1742
+
1743
+ const rebased = schema.type(toRebase.name);
1744
+ assert(rebased && isCompositeType(rebased), () => `Expected ${toRebase} to exists and be composite in the rebased schema, but got ${rebased?.kind}`);
1745
+ return rebased;
1746
+ }
1747
+
1601
1748
  function isUnecessaryFragment(parentType: CompositeType, fragment: FragmentSelection): boolean {
1602
1749
  return fragment.element.appliedDirectives.length === 0
1603
- && (!fragment.element.typeCondition || sameType(parentType, fragment.element.typeCondition));
1750
+ && (!fragment.element.typeCondition || isSubtype(maybeRebaseOnSchema(fragment.element.typeCondition, parentType.schema()), parentType));
1604
1751
  }
1605
1752
 
1606
1753
  function withUnecessaryFragmentsRemoved(
@@ -1881,7 +2028,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
1881
2028
 
1882
2029
  abstract expandFragments(names: string[], updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
1883
2030
 
1884
- abstract trimUnsatisfiableBranches(parentType: CompositeType): TOwnType | SelectionSet | undefined;
2031
+ abstract trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): TOwnType | SelectionSet | undefined;
1885
2032
 
1886
2033
  minus(that: Selection): TOwnType | undefined {
1887
2034
  // If there is a subset, then we compute the diff of the subset and add that (if not empty).
@@ -1895,6 +2042,10 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
1895
2042
  return undefined;
1896
2043
  }
1897
2044
 
2045
+ // Attempts to optimize the subselection of this field selection using named fragments `candidates` _assuming_ that
2046
+ // those candidates do apply at `parentType` (that is, `candidates.every((c) => c.canApplyAtType(parentType))` is true,
2047
+ // which is ensured by the fact that `tryOptimizeSubselectionWithFragments` calls this on a subset of the candidates
2048
+ // returned by `maybeApplyingAtType`).
1898
2049
  protected tryOptimizeSubselectionOnce(_: {
1899
2050
  parentType: CompositeType,
1900
2051
  subSelection: SelectionSet,
@@ -1968,7 +2119,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
1968
2119
  }
1969
2120
 
1970
2121
  optimize(fragments: NamedFragments): Selection {
1971
- let optimizedSelection = this.selectionSet ? this.selectionSet.optimize(fragments) : undefined;
2122
+ let optimizedSelection = this.selectionSet ? this.selectionSet.optimizeSelections(fragments) : undefined;
1972
2123
  const fieldBaseType = baseType(this.element.definition.type!);
1973
2124
  if (isCompositeType(fieldBaseType) && optimizedSelection) {
1974
2125
  const optimized = this.tryOptimizeSubselectionWithFragments({
@@ -2007,7 +2158,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2007
2158
  }{
2008
2159
  let optimizedSelection = subSelection;
2009
2160
  for (const candidate of candidates) {
2010
- const { contains, diff } = optimizedSelection.diffIfContains(candidate.selectionSet);
2161
+ const { contains, diff } = optimizedSelection.diffWithNamedFragmentIfContained(candidate, parentType);
2011
2162
  if (contains) {
2012
2163
  // We can optimize the selection with this fragment. The replaced sub-selection will be
2013
2164
  // comprised of this new spread and the remaining `diff` if there is any.
@@ -2123,14 +2274,14 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2123
2274
  return this.mapToSelectionSet((s) => s.expandAllFragments());
2124
2275
  }
2125
2276
 
2126
- trimUnsatisfiableBranches(_: CompositeType): FieldSelection {
2277
+ trimUnsatisfiableBranches(_: CompositeType, options?: { recursive? : boolean }): FieldSelection {
2127
2278
  if (!this.selectionSet) {
2128
2279
  return this;
2129
2280
  }
2130
2281
 
2131
2282
  const base = baseType(this.element.definition.type!)
2132
2283
  assert(isCompositeType(base), () => `Field ${this.element} should not have a sub-selection`);
2133
- const trimmed = this.mapToSelectionSet((s) => s.trimUnsatisfiableBranches(base));
2284
+ const trimmed = (options?.recursive ?? true) ? this.mapToSelectionSet((s) => s.trimUnsatisfiableBranches(base)) : this;
2134
2285
  // In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
2135
2286
  // sub-selection is empty. Which suggest something may be wrong with this part of the query
2136
2287
  // intent, but the query was valid while keeping an empty sub-selection isn't. So in that
@@ -2317,7 +2468,7 @@ class InlineFragmentSelection extends FragmentSelection {
2317
2468
  }
2318
2469
 
2319
2470
  optimize(fragments: NamedFragments): FragmentSelection {
2320
- let optimizedSelection = this.selectionSet.optimize(fragments);
2471
+ let optimizedSelection = this.selectionSet.optimizeSelections(fragments);
2321
2472
  const typeCondition = this.element.typeCondition;
2322
2473
  if (typeCondition) {
2323
2474
  const optimized = this.tryOptimizeSubselectionWithFragments({
@@ -2353,7 +2504,7 @@ class InlineFragmentSelection extends FragmentSelection {
2353
2504
  }{
2354
2505
  let optimizedSelection = subSelection;
2355
2506
  for (const candidate of candidates) {
2356
- const { contains, diff } = optimizedSelection.diffIfContains(candidate.selectionSet);
2507
+ const { contains, diff } = optimizedSelection.diffWithNamedFragmentIfContained(candidate, parentType);
2357
2508
  if (contains) {
2358
2509
  // The candidate selection is included in our sub-selection. One remaining thing to take into account
2359
2510
  // is applied directives: if the candidate has directives, then we can only use it if 1) there is
@@ -2424,12 +2575,14 @@ class InlineFragmentSelection extends FragmentSelection {
2424
2575
  : this.withUpdatedComponents(newElement, newSelection);
2425
2576
  }
2426
2577
 
2427
- trimUnsatisfiableBranches(currentType: CompositeType): FragmentSelection | SelectionSet | undefined {
2578
+ trimUnsatisfiableBranches(currentType: CompositeType, options?: { recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
2579
+ const recursive = options?.recursive ?? true;
2580
+
2428
2581
  const thisCondition = this.element.typeCondition;
2429
2582
  // Note that if the condition has directives, we preserve the fragment no matter what.
2430
2583
  if (this.element.appliedDirectives.length === 0) {
2431
2584
  if (!thisCondition || currentType === this.element.typeCondition) {
2432
- const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType);
2585
+ const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType, options);
2433
2586
  return trimmed.isEmpty() ? undefined : trimmed;
2434
2587
  }
2435
2588
 
@@ -2444,12 +2597,17 @@ class InlineFragmentSelection extends FragmentSelection {
2444
2597
  if (isObjectType(thisCondition) || !possibleRuntimeTypes(thisCondition).includes(currentType)) {
2445
2598
  return undefined;
2446
2599
  } else {
2447
- const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType);
2600
+ const trimmed =this.selectionSet.trimUnsatisfiableBranches(currentType, options);
2448
2601
  return trimmed.isEmpty() ? undefined : trimmed;
2449
2602
  }
2450
2603
  }
2451
2604
  }
2452
2605
 
2606
+ // As we preserve the current fragment, the rest is about recursing. If we don't recurse, we're done
2607
+ if (!recursive) {
2608
+ return this;
2609
+ }
2610
+
2453
2611
  // In all other cases, we first recurse on the sub-selection.
2454
2612
  const trimmedSelectionSet = this.selectionSet.trimUnsatisfiableBranches(this.element.typeCondition ?? this.parentType);
2455
2613
 
package/src/print.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  Directive,
4
4
  DirectiveDefinition,
5
5
  EnumType,
6
+ EnumValue,
6
7
  ExtendableElement,
7
8
  Extension,
8
9
  FieldDefinition,
@@ -18,6 +19,7 @@ import {
18
19
  SchemaDefinition,
19
20
  SchemaElement,
20
21
  SchemaRootKind,
22
+ UnionMember,
21
23
  UnionType
22
24
  } from "./definitions";
23
25
  import { assert } from "./utils";
@@ -28,6 +30,11 @@ export type PrintOptions = {
28
30
  definitionsOrder: ('schema' | 'types' | 'directives')[],
29
31
  rootTypesOrder: SchemaRootKind[],
30
32
  typeCompareFn?: (t1: NamedType, t2: NamedType) => number;
33
+ implementedInterfaceCompareFn?: (t1: InterfaceImplementation<any>, t2: InterfaceImplementation<any>) => number;
34
+ fieldCompareFn?: (t1: FieldDefinition<any>, t2: FieldDefinition<any>) => number;
35
+ unionMemberCompareFn?: (t1: UnionMember, t2: UnionMember) => number;
36
+ enumValueCompareFn?: (t1: EnumValue, t2: EnumValue) => number;
37
+ inputObjectFieldCompareFn?: (t1: InputFieldDefinition, t2: InputFieldDefinition) => number;
31
38
  directiveCompareFn?: (d1: DirectiveDefinition, d2: DirectiveDefinition) => number;
32
39
  mergeTypesAndExtensions: boolean;
33
40
  showAllBuiltIns: boolean;
@@ -51,6 +58,19 @@ export const defaultPrintOptions: PrintOptions = {
51
58
  }
52
59
 
53
60
  export function orderPrintedDefinitions(options: PrintOptions): PrintOptions {
61
+ return {
62
+ ...options,
63
+ typeCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
64
+ implementedInterfaceCompareFn: (t1, t2) => t1.interface.name.localeCompare(t2.interface.name),
65
+ fieldCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
66
+ unionMemberCompareFn: (t1, t2) => t1.type.name.localeCompare(t2.type.name),
67
+ enumValueCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
68
+ inputObjectFieldCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
69
+ directiveCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
70
+ };
71
+ }
72
+
73
+ export function shallowOrderPrintedDefinitions(options: PrintOptions): PrintOptions {
54
74
  return {
55
75
  ...options,
56
76
  typeCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
@@ -255,7 +275,7 @@ function printImplementedInterfaces(implementations: readonly InterfaceImplement
255
275
 
256
276
  function printFieldBasedTypeDefinitionOrExtension(kind: string, type: ObjectType | InterfaceType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
257
277
  const directives = appliedDirectives(type, options, extension);
258
- const interfaces = forExtension<InterfaceImplementation<any>>(type.interfaceImplementations(), extension);
278
+ let interfaces = forExtension<InterfaceImplementation<any>>(type.interfaceImplementations(), extension);
259
279
  let fields = forExtension<FieldDefinition<any>>(type.fields(), extension);
260
280
  if (options.fieldFilter) {
261
281
  fields = fields.filter(options.fieldFilter);
@@ -263,6 +283,12 @@ function printFieldBasedTypeDefinitionOrExtension(kind: string, type: ObjectType
263
283
  if (!directives.length && !interfaces.length && !fields.length && (extension || !type.preserveEmptyDefinition)) {
264
284
  return undefined;
265
285
  }
286
+ if (options.implementedInterfaceCompareFn) {
287
+ interfaces = interfaces.concat().sort(options.implementedInterfaceCompareFn);
288
+ }
289
+ if (options.fieldCompareFn) {
290
+ fields = fields.concat().sort(options.fieldCompareFn);
291
+ }
266
292
  return printDescription(type, options, extension)
267
293
  + printIsExtension(extension)
268
294
  + kind + ' ' + type
@@ -274,10 +300,13 @@ function printFieldBasedTypeDefinitionOrExtension(kind: string, type: ObjectType
274
300
 
275
301
  function printUnionDefinitionOrExtension(type: UnionType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
276
302
  const directives = appliedDirectives(type, options, extension);
277
- const members = forExtension(type.members(), extension);
303
+ let members = forExtension(type.members(), extension);
278
304
  if (!directives.length && !members.length && (extension || !type.preserveEmptyDefinition)) {
279
305
  return undefined;
280
306
  }
307
+ if (options.unionMemberCompareFn) {
308
+ members = members.concat().sort(options.unionMemberCompareFn);
309
+ }
281
310
  const possibleTypes = members.length ? ' = ' + members.map(m => m.type).join(' | ') : '';
282
311
  return printDescription(type, options, extension)
283
312
  + printIsExtension(extension)
@@ -288,10 +317,13 @@ function printUnionDefinitionOrExtension(type: UnionType, options: PrintOptions,
288
317
 
289
318
  function printEnumDefinitionOrExtension(type: EnumType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
290
319
  const directives = appliedDirectives(type, options, extension);
291
- const values = forExtension(type.values, extension);
320
+ let values = forExtension(type.values, extension);
292
321
  if (!directives.length && !values.length && (extension || !type.preserveEmptyDefinition)) {
293
322
  return undefined;
294
323
  }
324
+ if (options.enumValueCompareFn) {
325
+ values = values.concat().sort(options.enumValueCompareFn);
326
+ }
295
327
  const vals = values.map((v, i) =>
296
328
  printDescription(v, options, extension, options.indentString, !i)
297
329
  + options.indentString
@@ -307,10 +339,13 @@ function printEnumDefinitionOrExtension(type: EnumType, options: PrintOptions, e
307
339
 
308
340
  function printInputDefinitionOrExtension(type: InputObjectType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
309
341
  const directives = appliedDirectives(type, options, extension);
310
- const fields = forExtension(type.fields(), extension);
342
+ let fields = forExtension(type.fields(), extension);
311
343
  if (!directives.length && !fields.length && (extension || !type.preserveEmptyDefinition)) {
312
344
  return undefined;
313
345
  }
346
+ if (options.inputObjectFieldCompareFn) {
347
+ fields = fields.concat().sort(options.inputObjectFieldCompareFn);
348
+ }
314
349
  return printDescription(type, options, extension)
315
350
  + printIsExtension(extension)
316
351
  + 'input ' + type
package/src/tagSpec.ts CHANGED
@@ -41,11 +41,11 @@ export class TagSpecDefinition extends FeatureDefinition {
41
41
  name:'tag',
42
42
  locations: this.tagLocations,
43
43
  repeatable: true,
44
- argumentFct: (schema) => ({
45
- args: [{ name: 'name', type: new NonNullType(schema.stringType()) }],
46
- errors: [],
47
- }),
44
+ args: [{ name: 'name', type: (schema) => new NonNullType(schema.stringType()) }],
45
+ composes: true,
46
+ supergraphSpecification: () => TAG_VERSIONS.latest(),
48
47
  });
48
+ this.registerDirective(this.tagDirectiveSpec);
49
49
  }
50
50
 
51
51
  private isV01() {
@@ -56,10 +56,6 @@ export class TagSpecDefinition extends FeatureDefinition {
56
56
  return this.version.equals(new FeatureVersion(0, 2))
57
57
  }
58
58
 
59
- addElementsToSchema(schema: Schema): GraphQLError[] {
60
- return this.addDirectiveSpec(schema, this.tagDirectiveSpec);
61
- }
62
-
63
59
  tagDirective(schema: Schema): DirectiveDefinition<{name: string}> {
64
60
  return this.directive(schema, 'tag')!;
65
61
  }
@@ -76,10 +72,6 @@ export class TagSpecDefinition extends FeatureDefinition {
76
72
  }
77
73
  return undefined;
78
74
  }
79
-
80
- allElementNames(): string[] {
81
- return ["@tag"];
82
- }
83
75
  }
84
76
 
85
77
  export const TAG_VERSIONS = new FeatureDefinitions<TagSpecDefinition>(tagIdentity)