@apollo/federation-internals 2.4.5 → 2.4.7
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/operations.d.ts +43 -113
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +370 -297
- package/dist/operations.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/operations.ts +697 -453
- package/src/utils.ts +1 -1
- package/CHANGELOG.md +0 -211
- package/jest.config.js +0 -11
- package/src/__tests__/coreSpec.test.ts +0 -212
- package/src/__tests__/definitions.test.ts +0 -982
- package/src/__tests__/directiveAndTypeSpecifications.test.ts +0 -41
- package/src/__tests__/extractSubgraphsFromSupergraph.test.ts +0 -748
- package/src/__tests__/federation.test.ts +0 -31
- package/src/__tests__/graphQLJSSchemaToAST.test.ts +0 -156
- package/src/__tests__/matchers/index.ts +0 -1
- package/src/__tests__/matchers/toMatchString.ts +0 -87
- package/src/__tests__/operations.test.ts +0 -1266
- package/src/__tests__/removeInaccessibleElements.test.ts +0 -2471
- package/src/__tests__/schemaUpgrader.test.ts +0 -287
- package/src/__tests__/subgraphValidation.test.ts +0 -1254
- package/src/__tests__/supergraphSdl.graphql +0 -281
- package/src/__tests__/testUtils.ts +0 -28
- package/src/__tests__/toAPISchema.test.ts +0 -53
- package/src/__tests__/tsconfig.json +0 -7
- package/src/__tests__/utils.test.ts +0 -92
- package/src/__tests__/values.test.ts +0 -390
- package/tsconfig.json +0 -10
- package/tsconfig.test.json +0 -8
- package/tsconfig.tsbuildinfo +0 -1
package/src/operations.ts
CHANGED
|
@@ -46,11 +46,12 @@ import {
|
|
|
46
46
|
isLeafType,
|
|
47
47
|
Variables,
|
|
48
48
|
isObjectType,
|
|
49
|
+
NamedType,
|
|
49
50
|
} from "./definitions";
|
|
50
51
|
import { isInterfaceObjectType } from "./federation";
|
|
51
52
|
import { ERRORS } from "./error";
|
|
52
53
|
import { isSubtype, sameType } from "./types";
|
|
53
|
-
import { assert,
|
|
54
|
+
import { assert, mapKeys, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
|
|
54
55
|
import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
|
|
55
56
|
import { v1 as uuidv1 } from 'uuid';
|
|
56
57
|
|
|
@@ -152,7 +153,11 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
isLeafField(): boolean {
|
|
155
|
-
return isLeafType(baseType(
|
|
156
|
+
return isLeafType(this.baseType());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
baseType(): NamedType {
|
|
160
|
+
return baseType(this.definition.type!);
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
withUpdatedDefinition(newDefinition: FieldDefinition<any>): Field<TArgs> {
|
|
@@ -674,7 +679,7 @@ export function concatOperationPaths(head: OperationPath, tail: OperationPath):
|
|
|
674
679
|
|
|
675
680
|
function isUselessFollowupElement(first: OperationElement, followup: OperationElement, conditionals: Directive<any, any>[]): boolean {
|
|
676
681
|
const typeOfFirst = first.kind === 'Field'
|
|
677
|
-
? baseType(
|
|
682
|
+
? first.baseType()
|
|
678
683
|
: first.typeCondition;
|
|
679
684
|
|
|
680
685
|
// The followup is useless if it's a fragment (with no directives we would want to preserve) whose type
|
|
@@ -691,6 +696,164 @@ export type RootOperationPath = {
|
|
|
691
696
|
path: OperationPath
|
|
692
697
|
}
|
|
693
698
|
|
|
699
|
+
// Computes for every fragment, which other fragments use it (so the reverse of it's dependencies, the other fragment it uses).
|
|
700
|
+
function computeFragmentsDependents(fragments: NamedFragments): SetMultiMap<string, string> {
|
|
701
|
+
const reverseDeps = new SetMultiMap<string, string>();
|
|
702
|
+
for (const fragment of fragments.definitions()) {
|
|
703
|
+
for (const dependency of fragment.fragmentUsages().keys()) {
|
|
704
|
+
reverseDeps.add(dependency, fragment.name);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return reverseDeps;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function clearKeptFragments(
|
|
711
|
+
usages: Map<string, number>,
|
|
712
|
+
fragments: NamedFragments,
|
|
713
|
+
minUsagesToOptimize: number
|
|
714
|
+
) {
|
|
715
|
+
// `toCheck` will contain only fragments that we know we want to keep (but haven't handled/removed from `usages` yet).
|
|
716
|
+
let toCheck = Array.from(usages.entries()).filter(([_, count]) => count >= minUsagesToOptimize).map(([name, _]) => name);
|
|
717
|
+
while (toCheck.length > 0) {
|
|
718
|
+
const newToCheck = [];
|
|
719
|
+
for (const name of toCheck) {
|
|
720
|
+
// We "keep" that fragment so clear it.
|
|
721
|
+
usages.delete(name);
|
|
722
|
+
// But as it is used, bump the usage for every fragment it uses.
|
|
723
|
+
const ownUsages = fragments.get(name)!.fragmentUsages();
|
|
724
|
+
for (const [otherName, otherCount] of ownUsages.entries()) {
|
|
725
|
+
const prevCount = usages.get(otherName);
|
|
726
|
+
// We're interested in fragment not in `usages` anymore.
|
|
727
|
+
if (prevCount !== undefined) {
|
|
728
|
+
const newCount = prevCount + otherCount;
|
|
729
|
+
usages.set(otherName, newCount);
|
|
730
|
+
if (prevCount < minUsagesToOptimize && newCount >= minUsagesToOptimize) {
|
|
731
|
+
newToCheck.push(otherName);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
toCheck = newToCheck;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Checks, in `selectionSet`, which fragments (of `fragments`) are used at least `minUsagesToOptimize` times.
|
|
741
|
+
// Returns the updated set of fragments containing only the fragment definitions with usage above our threshold,
|
|
742
|
+
// and `undefined` or `null` if no such fragment meets said threshold. When this method returns `null`, it
|
|
743
|
+
// additionally means that no fragments are use at all in `selectionSet` (and so `undefined` means that
|
|
744
|
+
// "some" fragments are used in `selectionSet`, but just none of them is used at least `minUsagesToOptimize`
|
|
745
|
+
// times).
|
|
746
|
+
function computeFragmentsToKeep(
|
|
747
|
+
selectionSet: SelectionSet,
|
|
748
|
+
fragments: NamedFragments,
|
|
749
|
+
minUsagesToOptimize: number
|
|
750
|
+
): NamedFragments | undefined | null {
|
|
751
|
+
// We start by collecting the usages within the selection set.
|
|
752
|
+
const usages = new Map<string, number>();
|
|
753
|
+
selectionSet.collectUsedFragmentNames(usages);
|
|
754
|
+
|
|
755
|
+
// If we have no fragment in the selection set, then it's simple, we just don't keep any fragments.
|
|
756
|
+
if (usages.size === 0) {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// We're going to remove fragments from usages as we categorize them as kept or expanded, so we
|
|
761
|
+
// first ensure that it has entries for every fragment, default to 0.
|
|
762
|
+
for (const fragment of fragments.definitions()) {
|
|
763
|
+
if (usages.get(fragment.name) === undefined) {
|
|
764
|
+
usages.set(fragment.name, 0);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// At this point, `usages` contains the usages of fragments "in the selection". From that, we want
|
|
769
|
+
// to decide which fragment to "keep", and which to re-expand. But there is 2 subtlety:
|
|
770
|
+
// 1. when we decide to keep some fragment F, then we should could it's own usages of other fragments. That
|
|
771
|
+
// is, if a fragment G is use once in the selection, but also use once in a fragment F that we
|
|
772
|
+
// keep, then the usages for G is really 2 (but if F is unused, then we don't want to count
|
|
773
|
+
// it's usage of G for instance).
|
|
774
|
+
// 2. when we decide to expand a fragment, then this also impact the usages of other fragments it
|
|
775
|
+
// uses, as those gets "inlined" into the selection. But that also mean we have to be careful
|
|
776
|
+
// of the order in which we pick fragments to expand. Say we have:
|
|
777
|
+
// ```graphql
|
|
778
|
+
// query {
|
|
779
|
+
// ...F1
|
|
780
|
+
// }
|
|
781
|
+
//
|
|
782
|
+
// fragment F1 {
|
|
783
|
+
// a { ...F2 }
|
|
784
|
+
// b { ...F2 }
|
|
785
|
+
// }
|
|
786
|
+
//
|
|
787
|
+
// fragment F2 {
|
|
788
|
+
// // something
|
|
789
|
+
// }
|
|
790
|
+
// ```
|
|
791
|
+
// then at this point where we've only counted usages in the query selection, `usages` will be
|
|
792
|
+
// `{ F1: 1, F2: 0 }`. But we do not want to expand _both_ F1 and F2. Instead, we want to expand
|
|
793
|
+
// F1 first, and then realize that this increases F2 usages to 2, which means we stop there and keep F2.
|
|
794
|
+
// Generalizing this, it means we want to first pick up fragments to expand that are _not_ used by any
|
|
795
|
+
// other fragments that may be expanded.
|
|
796
|
+
const reverseDependencies = computeFragmentsDependents(fragments);
|
|
797
|
+
// We'll add to `toExpand` fragment we will definitively expand.
|
|
798
|
+
const toExpand = new Set<string>;
|
|
799
|
+
let shouldContinue = true;
|
|
800
|
+
while (shouldContinue) {
|
|
801
|
+
// We'll do an iteration, but if we make no progress, we won't continue (we don't want to loop forever).
|
|
802
|
+
shouldContinue = false;
|
|
803
|
+
clearKeptFragments(usages, fragments, minUsagesToOptimize);
|
|
804
|
+
for (const name of mapKeys(usages)) {
|
|
805
|
+
// Note that we modify `usages` as we iterate it, so 1) we use `mapKeys` above which copy into a list and 2)
|
|
806
|
+
// we get the `count` manually instead of relying on (possibly outdated) entries.
|
|
807
|
+
const count = usages.get(name)!;
|
|
808
|
+
// A unused fragment is not technically expanded, it is just removed and we can ignore for now (it's count
|
|
809
|
+
// count increase later but ...).
|
|
810
|
+
if (count === 0) {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// If we find a fragment to keep, it means some fragment we expanded earlier in this iteration bump this
|
|
815
|
+
// one count. We unsure `shouldContinue` is set so `clearKeptFragments` is called again, but let that
|
|
816
|
+
// method deal with it otherwise.
|
|
817
|
+
if (count >= minUsagesToOptimize) {
|
|
818
|
+
shouldContinue = true;
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const fragmentsUsingName = reverseDependencies.get(name);
|
|
823
|
+
if (!fragmentsUsingName || [...fragmentsUsingName].every((fragName) => toExpand.has(fragName) || !usages.get(fragName))) {
|
|
824
|
+
// This fragment is not used enough, and is only used by fragments we keep, so we
|
|
825
|
+
// are guaranteed that expanding another fragment will not increase its usage. So
|
|
826
|
+
// we definitively expand it.
|
|
827
|
+
toExpand.add(name);
|
|
828
|
+
usages.delete(name);
|
|
829
|
+
|
|
830
|
+
// We've added to `toExpand`, so it's worth redoing another iteration
|
|
831
|
+
// after that to see if something changes.
|
|
832
|
+
shouldContinue = true;
|
|
833
|
+
|
|
834
|
+
// Now that we expand it, we should bump the usage for every fragment it uses.
|
|
835
|
+
const nameUsages = fragments.get(name)!.fragmentUsages();
|
|
836
|
+
for (const [otherName, otherCount] of nameUsages.entries()) {
|
|
837
|
+
const prev = usages.get(otherName);
|
|
838
|
+
// Note that if `otherName` is not part of usages, it means it's a fragment we
|
|
839
|
+
// already decided to keep/expand, so we just ignore it.
|
|
840
|
+
if (prev !== undefined) {
|
|
841
|
+
usages.set(otherName, prev + count * otherCount);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Finally, we know that to expand, which is `toExpand` plus whatever remains in `usage` (typically
|
|
849
|
+
// genuinely unused fragments).
|
|
850
|
+
for (const name of usages.keys()) {
|
|
851
|
+
toExpand.add(name);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return toExpand.size === 0 ? fragments : fragments.filter((f) => !toExpand.has(f.name));
|
|
855
|
+
}
|
|
856
|
+
|
|
694
857
|
// TODO Operations can also have directives
|
|
695
858
|
export class Operation {
|
|
696
859
|
constructor(
|
|
@@ -698,6 +861,7 @@ export class Operation {
|
|
|
698
861
|
readonly rootKind: SchemaRootKind,
|
|
699
862
|
readonly selectionSet: SelectionSet,
|
|
700
863
|
readonly variableDefinitions: VariableDefinitions,
|
|
864
|
+
readonly fragments?: NamedFragments,
|
|
701
865
|
readonly name?: string) {
|
|
702
866
|
}
|
|
703
867
|
|
|
@@ -712,39 +876,28 @@ export class Operation {
|
|
|
712
876
|
return this;
|
|
713
877
|
}
|
|
714
878
|
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
879
|
+
const finalFragments = computeFragmentsToKeep(optimizedSelection, fragments, minUsagesToOptimize);
|
|
880
|
+
if (finalFragments === null || finalFragments?.size === fragments.size) {
|
|
881
|
+
// This means either that there is no fragment usage whatsoever in `optimizedSelection`, or that
|
|
882
|
+
// we're keeping all fragments. In both cases, we need no additional work on `optimizedSelection`.
|
|
883
|
+
return new Operation(this.schema, this.rootKind, optimizedSelection, this.variableDefinitions, finalFragments ?? undefined, this.name);
|
|
721
884
|
}
|
|
722
885
|
|
|
723
|
-
//
|
|
724
|
-
// re-expand some is not entirely optimal, but it's
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
// Also note `toDeoptimize` will always contains the unused fragments, which will allow `expandFragments`
|
|
730
|
-
// to remove them from the listed fragments in `optimizedSelection` (here again, this could make use call
|
|
731
|
-
// `expandFragments` on _only_ unused fragments and that case could be dealt with more efficiently, but
|
|
732
|
-
// probably not noticeable in practice so ...).
|
|
733
|
-
const toDeoptimize = mapEntries(usages).filter(([_, count]) => count < minUsagesToOptimize).map(([name]) => name);
|
|
886
|
+
// If we get here, it means some fragments need to be expanded, so we do so.
|
|
887
|
+
// Optimizing all fragments to potentially re-expand some is not entirely optimal, but it's unclear
|
|
888
|
+
// how to do otherwise, and it probably don't matter too much in practice (we only call this optimization
|
|
889
|
+
// on the final computed query plan, so not a very hot path; plus in most cases we won't even reach that
|
|
890
|
+
// point either because there is no fragment, or none will have been optimized away so we'll exit above).
|
|
891
|
+
optimizedSelection = optimizedSelection.expandFragments(finalFragments);
|
|
734
892
|
|
|
735
|
-
if
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
}
|
|
742
|
-
|
|
743
|
-
return new Operation(this.schema, this.rootKind, optimizedSelection, this.variableDefinitions, this.name);
|
|
893
|
+
// Expanding fragments could create some "inefficiencies" that we wouldn't have if we hadn't re-optimized
|
|
894
|
+
// the fragments to de-optimize it later, so we do a final "trim" pass to remove those.
|
|
895
|
+
optimizedSelection = optimizedSelection.trimUnsatisfiableBranches(optimizedSelection.parentType);
|
|
896
|
+
return new Operation(this.schema, this.rootKind, optimizedSelection, this.variableDefinitions, finalFragments, this.name);
|
|
744
897
|
}
|
|
745
898
|
|
|
746
899
|
expandAllFragments(): Operation {
|
|
747
|
-
const expandedSelections = this.selectionSet.
|
|
900
|
+
const expandedSelections = this.selectionSet.expandFragments();
|
|
748
901
|
if (expandedSelections === this.selectionSet) {
|
|
749
902
|
return this;
|
|
750
903
|
}
|
|
@@ -754,6 +907,7 @@ export class Operation {
|
|
|
754
907
|
this.rootKind,
|
|
755
908
|
expandedSelections,
|
|
756
909
|
this.variableDefinitions,
|
|
910
|
+
undefined,
|
|
757
911
|
this.name
|
|
758
912
|
);
|
|
759
913
|
}
|
|
@@ -769,6 +923,7 @@ export class Operation {
|
|
|
769
923
|
this.rootKind,
|
|
770
924
|
trimmedSelections,
|
|
771
925
|
this.variableDefinitions,
|
|
926
|
+
this.fragments,
|
|
772
927
|
this.name
|
|
773
928
|
);
|
|
774
929
|
}
|
|
@@ -781,14 +936,10 @@ export class Operation {
|
|
|
781
936
|
* applications are removed.
|
|
782
937
|
*/
|
|
783
938
|
withoutDefer(labelsToRemove?: Set<string>): Operation {
|
|
784
|
-
// If we have named fragments, we should be looking inside those and either expand those having @defer or,
|
|
785
|
-
// probably better, replace them with a verison without @defer. But as we currently only call this method
|
|
786
|
-
// after `expandAllFragments`, we'll implement this when/if we need it.
|
|
787
|
-
assert(!this.selectionSet.fragments || this.selectionSet.fragments.isEmpty(), 'Removing @defer currently only work on "expanded" selections (no named fragments)');
|
|
788
939
|
const updated = this.selectionSet.withoutDefer(labelsToRemove);
|
|
789
940
|
return updated == this.selectionSet
|
|
790
941
|
? this
|
|
791
|
-
: new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.name);
|
|
942
|
+
: new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.fragments, this.name);
|
|
792
943
|
}
|
|
793
944
|
|
|
794
945
|
/**
|
|
@@ -810,15 +961,12 @@ export class Operation {
|
|
|
810
961
|
assignedDeferLabels: Set<string>,
|
|
811
962
|
deferConditions: SetMultiMap<string, string>,
|
|
812
963
|
} {
|
|
813
|
-
// Similar comment than in `withoutDefer`
|
|
814
|
-
assert(!this.selectionSet.fragments || this.selectionSet.fragments.isEmpty(), 'Assigning @defer lables currently only work on "expanded" selections (no named fragments)');
|
|
815
|
-
|
|
816
964
|
const normalizer = new DeferNormalizer();
|
|
817
965
|
const { hasDefers, hasNonLabelledOrConditionalDefers } = normalizer.init(this.selectionSet);
|
|
818
966
|
let updatedOperation: Operation = this;
|
|
819
967
|
if (hasNonLabelledOrConditionalDefers) {
|
|
820
968
|
const updated = this.selectionSet.withNormalizedDefer(normalizer);
|
|
821
|
-
updatedOperation = new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.name);
|
|
969
|
+
updatedOperation = new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.fragments, this.name);
|
|
822
970
|
}
|
|
823
971
|
return {
|
|
824
972
|
operation: updatedOperation,
|
|
@@ -839,14 +987,20 @@ export class Operation {
|
|
|
839
987
|
}
|
|
840
988
|
|
|
841
989
|
toString(expandFragments: boolean = false, prettyPrint: boolean = true): string {
|
|
842
|
-
return this.selectionSet.toOperationString(this.rootKind, this.variableDefinitions, this.name, expandFragments, prettyPrint);
|
|
990
|
+
return this.selectionSet.toOperationString(this.rootKind, this.variableDefinitions, this.fragments, this.name, expandFragments, prettyPrint);
|
|
843
991
|
}
|
|
844
992
|
}
|
|
845
993
|
|
|
846
994
|
export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmentDefinition> {
|
|
847
995
|
private _selectionSet: SelectionSet | undefined;
|
|
848
996
|
|
|
849
|
-
|
|
997
|
+
// Lazily computed cache of the expanded selection set.
|
|
998
|
+
private _expandedSelectionSet: SelectionSet | undefined;
|
|
999
|
+
|
|
1000
|
+
private _fragmentUsages: Map<string, number> | undefined;
|
|
1001
|
+
private _includedFragmentNames: Set<string> | undefined;
|
|
1002
|
+
|
|
1003
|
+
private readonly expandedSelectionSetsAtTypesCache = new Map<string, SelectionSet>();
|
|
850
1004
|
|
|
851
1005
|
constructor(
|
|
852
1006
|
schema: Schema,
|
|
@@ -871,12 +1025,31 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
871
1025
|
return this._selectionSet;
|
|
872
1026
|
}
|
|
873
1027
|
|
|
1028
|
+
expandedSelectionSet(): SelectionSet {
|
|
1029
|
+
if (!this._expandedSelectionSet) {
|
|
1030
|
+
this._expandedSelectionSet = this.selectionSet.expandFragments().trimUnsatisfiableBranches(this.typeCondition);
|
|
1031
|
+
}
|
|
1032
|
+
return this._expandedSelectionSet;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
874
1035
|
withUpdatedSelectionSet(newSelectionSet: SelectionSet): NamedFragmentDefinition {
|
|
875
1036
|
return new NamedFragmentDefinition(this.schema(), this.name, this.typeCondition).setSelectionSet(newSelectionSet);
|
|
876
1037
|
}
|
|
877
1038
|
|
|
1039
|
+
fragmentUsages(): ReadonlyMap<string, number> {
|
|
1040
|
+
if (!this._fragmentUsages) {
|
|
1041
|
+
this._fragmentUsages = new Map();
|
|
1042
|
+
this.selectionSet.collectUsedFragmentNames(this._fragmentUsages);
|
|
1043
|
+
}
|
|
1044
|
+
return this._fragmentUsages;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
878
1047
|
collectUsedFragmentNames(collector: Map<string, number>) {
|
|
879
|
-
this.
|
|
1048
|
+
const usages = this.fragmentUsages();
|
|
1049
|
+
for (const [name, count] of usages.entries()) {
|
|
1050
|
+
const prevCount = collector.get(name);
|
|
1051
|
+
collector.set(name, prevCount ? prevCount + count : count);
|
|
1052
|
+
}
|
|
880
1053
|
}
|
|
881
1054
|
|
|
882
1055
|
toFragmentDefinitionNode() : FragmentDefinitionNode {
|
|
@@ -898,13 +1071,32 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
898
1071
|
}
|
|
899
1072
|
|
|
900
1073
|
/**
|
|
901
|
-
* Whether this fragment may apply at the provided type, that is if
|
|
902
|
-
*
|
|
1074
|
+
* Whether this fragment may apply at the provided type, that is if either:
|
|
1075
|
+
* - its type condition is equal to the provided type.
|
|
1076
|
+
* - or the runtime types of the provided type include all of those of the fragment condition.
|
|
903
1077
|
*
|
|
904
1078
|
* @param type - the type at which we're looking at applying the fragment
|
|
905
1079
|
*/
|
|
906
1080
|
canApplyAtType(type: CompositeType): boolean {
|
|
907
|
-
|
|
1081
|
+
if (sameType(type, this.typeCondition)) {
|
|
1082
|
+
return true;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// No point computing runtime types if the condition is an object (it can never cover all of
|
|
1086
|
+
// the runtimes of `type` unless it's the same type, which is already covered).
|
|
1087
|
+
if (!isAbstractType(this.typeCondition)) {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const conditionRuntimes = possibleRuntimeTypes(this.typeCondition);
|
|
1092
|
+
const typeRuntimes = possibleRuntimeTypes(type);
|
|
1093
|
+
|
|
1094
|
+
// The fragment condition must be at least as general as the provided type (so that if we use the fragment
|
|
1095
|
+
// inside `type`, then it doesn't add restriction that weren't there without the fragment).
|
|
1096
|
+
// Note: the `length` test is technically redundant, but just avoid the more costly sub-set check if we
|
|
1097
|
+
// can cheaply show it's unnecessary.
|
|
1098
|
+
return conditionRuntimes.length >= typeRuntimes.length
|
|
1099
|
+
&& typeRuntimes.every((t1) => conditionRuntimes.some((t2) => sameType(t1, t2)));
|
|
908
1100
|
}
|
|
909
1101
|
|
|
910
1102
|
/**
|
|
@@ -925,10 +1117,12 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
925
1117
|
* 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
1118
|
* us that part.
|
|
927
1119
|
*/
|
|
928
|
-
|
|
1120
|
+
expandedSelectionSetAtType(type: CompositeType): SelectionSet {
|
|
1121
|
+
const expandedSelectionSet = this.expandedSelectionSet();
|
|
1122
|
+
|
|
929
1123
|
// First, if the candidate condition is an object or is the type passed, then there isn't any additional restriction to do.
|
|
930
1124
|
if (sameType(type, this.typeCondition) || isObjectType(this.typeCondition)) {
|
|
931
|
-
return
|
|
1125
|
+
return expandedSelectionSet;
|
|
932
1126
|
}
|
|
933
1127
|
|
|
934
1128
|
// We should not call `trimUnsatisfiableBranches` where `type` is an abstract type (`interface` or `union`) as it currently could
|
|
@@ -940,17 +1134,45 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
940
1134
|
// Concretely, this means that there may be corner cases where a named fragment could be reused but isn't, but waiting on finding
|
|
941
1135
|
// concrete examples where this matter to decide if it's worth the complexity.
|
|
942
1136
|
if (!isObjectType(type)) {
|
|
943
|
-
return
|
|
1137
|
+
return expandedSelectionSet;
|
|
944
1138
|
}
|
|
945
1139
|
|
|
946
|
-
let
|
|
947
|
-
if (!
|
|
1140
|
+
let selectionAtType = this.expandedSelectionSetsAtTypesCache.get(type.name);
|
|
1141
|
+
if (!selectionAtType) {
|
|
948
1142
|
// 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
1143
|
// in going recursive however: any simplification due to `type` stops as soon as we traverse a field. And so we don't bother.
|
|
950
|
-
|
|
951
|
-
this.
|
|
1144
|
+
selectionAtType = expandedSelectionSet.trimUnsatisfiableBranches(type, { recursive: false });
|
|
1145
|
+
this.expandedSelectionSetsAtTypesCache.set(type.name, selectionAtType);
|
|
952
1146
|
}
|
|
953
|
-
return
|
|
1147
|
+
return selectionAtType;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Whether this fragment fully includes `otherFragment`.
|
|
1152
|
+
* Note that this is slightly different from `this` "using" `otherFragment` in that this essentially checks
|
|
1153
|
+
* if the full selection set of `otherFragment` is contained by `this`, so this only look at "top-level" usages.
|
|
1154
|
+
*
|
|
1155
|
+
* Note that this is guaranteed to return `false` if passed `this` name.
|
|
1156
|
+
*/
|
|
1157
|
+
includes(otherFragment: string): boolean {
|
|
1158
|
+
if (this.name === otherFragment) {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if (!this._includedFragmentNames) {
|
|
1163
|
+
this._includedFragmentNames = this.computeIncludedFragmentNames();
|
|
1164
|
+
}
|
|
1165
|
+
return this._includedFragmentNames.has(otherFragment);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
private computeIncludedFragmentNames(): Set<string> {
|
|
1169
|
+
const included = new Set<string>();
|
|
1170
|
+
for (const selection of this.selectionSet.selections()) {
|
|
1171
|
+
if (selection instanceof FragmentSpreadSelection) {
|
|
1172
|
+
included.add(selection.namedFragment.name);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return included;
|
|
954
1176
|
}
|
|
955
1177
|
|
|
956
1178
|
toString(indent?: string): string {
|
|
@@ -962,7 +1184,11 @@ export class NamedFragments {
|
|
|
962
1184
|
private readonly fragments = new MapWithCachedArrays<string, NamedFragmentDefinition>();
|
|
963
1185
|
|
|
964
1186
|
isEmpty(): boolean {
|
|
965
|
-
return this.
|
|
1187
|
+
return this.size === 0;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
get size(): number {
|
|
1191
|
+
return this.fragments.size;
|
|
966
1192
|
}
|
|
967
1193
|
|
|
968
1194
|
names(): readonly string[] {
|
|
@@ -986,26 +1212,6 @@ export class NamedFragments {
|
|
|
986
1212
|
return this.fragments.values().filter(f => f.canApplyAtType(type));
|
|
987
1213
|
}
|
|
988
1214
|
|
|
989
|
-
without(names: string[]): NamedFragments | undefined {
|
|
990
|
-
if (!names.some(n => this.fragments.has(n))) {
|
|
991
|
-
return this;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
const newFragments = new NamedFragments();
|
|
995
|
-
for (const fragment of this.fragments.values()) {
|
|
996
|
-
if (!names.includes(fragment.name)) {
|
|
997
|
-
// We want to keep that fragment. But that fragment might use a fragment we
|
|
998
|
-
// remove, and if so, we need to expand that removed fragment.
|
|
999
|
-
const updatedSelectionSet = fragment.selectionSet.expandFragments(names, newFragments);
|
|
1000
|
-
const newFragment = updatedSelectionSet === fragment.selectionSet
|
|
1001
|
-
? fragment
|
|
1002
|
-
: fragment.withUpdatedSelectionSet(updatedSelectionSet);
|
|
1003
|
-
newFragments.add(newFragment);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
return newFragments.isEmpty() ? undefined : newFragments;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
1215
|
get(name: string): NamedFragmentDefinition | undefined {
|
|
1010
1216
|
return this.fragments.get(name);
|
|
1011
1217
|
}
|
|
@@ -1027,48 +1233,44 @@ export class NamedFragments {
|
|
|
1027
1233
|
}
|
|
1028
1234
|
|
|
1029
1235
|
/**
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1236
|
+
* The mapper is called on every fragment definition (`fragment` argument), but in such a way that if a fragment A uses another fragment B,
|
|
1237
|
+
* then the mapper is guaranteed to be called on B _before_ being called on A. Further, the `newFragments` argument is a new `NamedFragments`
|
|
1238
|
+
* containing all the previously mapped definition (minus those for which the mapper returned `undefined`). So if A uses B (and the mapper
|
|
1239
|
+
* on B do not return undefined), then when mapper is called on A `newFragments` will have the mapped value for B.
|
|
1034
1240
|
*/
|
|
1035
|
-
|
|
1036
|
-
mapper: (
|
|
1037
|
-
recreateFct: (frag: NamedFragmentDefinition, newSelectionSet: SelectionSet) => NamedFragmentDefinition = (f, s) => f.withUpdatedSelectionSet(s),
|
|
1241
|
+
mapInDependencyOrder(
|
|
1242
|
+
mapper: (fragment: NamedFragmentDefinition, newFragments: NamedFragments) => NamedFragmentDefinition | undefined
|
|
1038
1243
|
): NamedFragments | undefined {
|
|
1039
|
-
type
|
|
1040
|
-
|
|
1041
|
-
mappedSelectionSet: SelectionSet,
|
|
1244
|
+
type FragmentDependencies = {
|
|
1245
|
+
fragment: NamedFragmentDefinition,
|
|
1042
1246
|
dependsOn: string[],
|
|
1043
1247
|
};
|
|
1044
|
-
const fragmentsMap = new Map<string,
|
|
1045
|
-
|
|
1046
|
-
const removedFragments = new Set<string>();
|
|
1248
|
+
const fragmentsMap = new Map<string, FragmentDependencies>();
|
|
1047
1249
|
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
1250
|
fragmentsMap.set(fragment.name, {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
dependsOn: Array.from(otherFragmentsUsages.keys()),
|
|
1251
|
+
fragment,
|
|
1252
|
+
dependsOn: Array.from(fragment.fragmentUsages().keys()),
|
|
1060
1253
|
});
|
|
1061
1254
|
}
|
|
1062
1255
|
|
|
1256
|
+
const removedFragments = new Set<string>();
|
|
1063
1257
|
const mappedFragments = new NamedFragments();
|
|
1064
1258
|
while (fragmentsMap.size > 0) {
|
|
1065
1259
|
for (const [name, info] of fragmentsMap) {
|
|
1066
1260
|
// Note that graphQL specifies that named fragments cannot have cycles (https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles)
|
|
1067
1261
|
// 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
1262
|
if (info.dependsOn.every((n) => mappedFragments.has(n) || removedFragments.has(n))) {
|
|
1069
|
-
const
|
|
1070
|
-
mappedFragments.add(recreateFct(info.original, reoptimizedSelectionSet));
|
|
1263
|
+
const mapped = mapper(info.fragment, mappedFragments);
|
|
1071
1264
|
fragmentsMap.delete(name);
|
|
1265
|
+
if (!mapped) {
|
|
1266
|
+
removedFragments.add(name);
|
|
1267
|
+
} else {
|
|
1268
|
+
mappedFragments.add(mapped);
|
|
1269
|
+
}
|
|
1270
|
+
// We just deleted from `fragmentsMap` so continuing our current `for` iteration is dangerous,
|
|
1271
|
+
// so we break to the `while` loop (besides, there is no reason why continuing the inner iteration
|
|
1272
|
+
// would be better than restarting it right away).
|
|
1273
|
+
break;
|
|
1072
1274
|
}
|
|
1073
1275
|
}
|
|
1074
1276
|
}
|
|
@@ -1076,20 +1278,58 @@ export class NamedFragments {
|
|
|
1076
1278
|
return mappedFragments.isEmpty() ? undefined : mappedFragments;
|
|
1077
1279
|
}
|
|
1078
1280
|
|
|
1281
|
+
/**
|
|
1282
|
+
* This method:
|
|
1283
|
+
* - expands all nested fragments,
|
|
1284
|
+
* - applies the provided mapper to the selection set of the fragments,
|
|
1285
|
+
* - and finally re-fragments the nested fragments.
|
|
1286
|
+
*/
|
|
1287
|
+
mapToExpandedSelectionSets(
|
|
1288
|
+
mapper: (selectionSet: SelectionSet) => SelectionSet | undefined,
|
|
1289
|
+
): NamedFragments | undefined {
|
|
1290
|
+
return this.mapInDependencyOrder((fragment, newFragments) => {
|
|
1291
|
+
const mappedSelectionSet = mapper(fragment.selectionSet.expandFragments().trimUnsatisfiableBranches(fragment.typeCondition));
|
|
1292
|
+
if (!mappedSelectionSet) {
|
|
1293
|
+
return undefined;
|
|
1294
|
+
}
|
|
1295
|
+
const reoptimizedSelectionSet = mappedSelectionSet.optimize(newFragments);
|
|
1296
|
+
return fragment.withUpdatedSelectionSet(reoptimizedSelectionSet);
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1079
1300
|
rebaseOn(schema: Schema): NamedFragments | undefined {
|
|
1080
|
-
return this.
|
|
1081
|
-
(
|
|
1082
|
-
|
|
1083
|
-
|
|
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.
|
|
1301
|
+
return this.mapInDependencyOrder((fragment, newFragments) => {
|
|
1302
|
+
const rebasedType = schema.type(fragment.selectionSet.parentType.name);
|
|
1303
|
+
try {
|
|
1304
|
+
if (!rebasedType || !isCompositeType(rebasedType)) {
|
|
1088
1305
|
return undefined;
|
|
1089
1306
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1307
|
+
|
|
1308
|
+
const rebasedSelection = fragment.selectionSet.rebaseOn(rebasedType, newFragments);
|
|
1309
|
+
return new NamedFragmentDefinition(schema, fragment.name, rebasedType).setSelectionSet(rebasedSelection);
|
|
1310
|
+
} catch (e) {
|
|
1311
|
+
// This means we cannot rebase this selection on the schema and thus cannot reuse that fragment on that
|
|
1312
|
+
// particular schema.
|
|
1313
|
+
return undefined;
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
filter(predicate: (fragment: NamedFragmentDefinition) => boolean): NamedFragments | undefined {
|
|
1319
|
+
return this.mapInDependencyOrder((fragment, newFragments) => {
|
|
1320
|
+
if (predicate(fragment)) {
|
|
1321
|
+
// We want to keep that fragment. But that fragment might use a fragment we remove, and if so,
|
|
1322
|
+
// we need to expand that removed fragment. Note that because we're running in
|
|
1323
|
+
// dependency order, we know that `newFragments` will have every fragments that should be
|
|
1324
|
+
// kept/not expanded.
|
|
1325
|
+
const updatedSelectionSet = fragment.selectionSet.expandFragments(newFragments);
|
|
1326
|
+
return updatedSelectionSet === fragment.selectionSet
|
|
1327
|
+
? fragment
|
|
1328
|
+
: fragment.withUpdatedSelectionSet(updatedSelectionSet);
|
|
1329
|
+
} else {
|
|
1330
|
+
return undefined;
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1093
1333
|
}
|
|
1094
1334
|
|
|
1095
1335
|
validate(variableDefinitions: VariableDefinitions) {
|
|
@@ -1169,6 +1409,14 @@ class DeferNormalizer {
|
|
|
1169
1409
|
}
|
|
1170
1410
|
}
|
|
1171
1411
|
|
|
1412
|
+
export enum ContainsResult {
|
|
1413
|
+
// Note: enum values are numbers in the end, and 0 means false in JS, so we should keep `NOT_CONTAINED` first
|
|
1414
|
+
// so that using the result of `contains` as a boolean works.
|
|
1415
|
+
NOT_CONTAINED,
|
|
1416
|
+
STRICTLY_CONTAINED,
|
|
1417
|
+
EQUAL,
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1172
1420
|
export class SelectionSet {
|
|
1173
1421
|
private readonly _keyedSelections: Map<string, Selection>;
|
|
1174
1422
|
private readonly _selections: readonly Selection[];
|
|
@@ -1176,7 +1424,6 @@ export class SelectionSet {
|
|
|
1176
1424
|
constructor(
|
|
1177
1425
|
readonly parentType: CompositeType,
|
|
1178
1426
|
keyedSelections: Map<string, Selection> = new Map(),
|
|
1179
|
-
readonly fragments?: NamedFragments,
|
|
1180
1427
|
) {
|
|
1181
1428
|
this._keyedSelections = keyedSelections;
|
|
1182
1429
|
this._selections = mapValues(keyedSelections);
|
|
@@ -1230,7 +1477,7 @@ export class SelectionSet {
|
|
|
1230
1477
|
|
|
1231
1478
|
collectUsedFragmentNames(collector: Map<string, number>) {
|
|
1232
1479
|
for (const selection of this.selections()) {
|
|
1233
|
-
|
|
1480
|
+
selection.collectUsedFragmentNames(collector);
|
|
1234
1481
|
}
|
|
1235
1482
|
}
|
|
1236
1483
|
|
|
@@ -1261,7 +1508,7 @@ export class SelectionSet {
|
|
|
1261
1508
|
// and in that case we return a singleton selection with just that. Otherwise, it's our wrapping inline fragment
|
|
1262
1509
|
// with the sub-selections optimized, and we just return that subselection.
|
|
1263
1510
|
return optimized instanceof FragmentSpreadSelection
|
|
1264
|
-
? selectionSetOf(this.parentType, optimized
|
|
1511
|
+
? selectionSetOf(this.parentType, optimized)
|
|
1265
1512
|
: optimized.selectionSet;
|
|
1266
1513
|
}
|
|
1267
1514
|
|
|
@@ -1269,26 +1516,11 @@ export class SelectionSet {
|
|
|
1269
1516
|
// may not match fragments that would apply at top-level, so you should usually use `optimize` instead (this exists mostly
|
|
1270
1517
|
// for the recursion).
|
|
1271
1518
|
optimizeSelections(fragments: NamedFragments): SelectionSet {
|
|
1272
|
-
|
|
1273
|
-
// not only because we need to deal with merging new and existing fragments, but also because
|
|
1274
|
-
// things get weird if some fragment names are in common to both. Since we currently only care
|
|
1275
|
-
// about this method when optimizing subgraph fetch selections and those are initially created
|
|
1276
|
-
// without any fragments, we don't bother handling this more complex case.
|
|
1277
|
-
assert(!this.fragments || this.fragments.isEmpty(), `Should not be called on selection that already has named fragments, but got ${this.fragments}`)
|
|
1278
|
-
|
|
1279
|
-
return this.lazyMap((selection) => selection.optimize(fragments), { fragments });
|
|
1519
|
+
return this.lazyMap((selection) => selection.optimize(fragments));
|
|
1280
1520
|
}
|
|
1281
1521
|
|
|
1282
|
-
|
|
1283
|
-
return this.lazyMap((selection) => selection.
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): SelectionSet {
|
|
1287
|
-
if (names.length === 0) {
|
|
1288
|
-
return this;
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
return this.lazyMap((selection) => selection.expandFragments(names, updatedFragments), { fragments: updatedFragments ?? null });
|
|
1522
|
+
expandFragments(updatedFragments?: NamedFragments): SelectionSet {
|
|
1523
|
+
return this.lazyMap((selection) => selection.expandFragments(updatedFragments));
|
|
1292
1524
|
}
|
|
1293
1525
|
|
|
1294
1526
|
trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): SelectionSet {
|
|
@@ -1306,14 +1538,10 @@ export class SelectionSet {
|
|
|
1306
1538
|
lazyMap(
|
|
1307
1539
|
mapper: (selection: Selection) => Selection | readonly Selection[] | SelectionSet | undefined,
|
|
1308
1540
|
options?: {
|
|
1309
|
-
fragments?: NamedFragments | null,
|
|
1310
1541
|
parentType?: CompositeType,
|
|
1311
1542
|
}
|
|
1312
1543
|
): SelectionSet {
|
|
1313
1544
|
const selections = this.selections();
|
|
1314
|
-
const updatedFragments = options?.fragments;
|
|
1315
|
-
const newFragments = updatedFragments === undefined ? this.fragments : (updatedFragments ?? undefined);
|
|
1316
|
-
|
|
1317
1545
|
let updatedSelections: SelectionSetUpdates | undefined = undefined;
|
|
1318
1546
|
for (let i = 0; i < selections.length; i++) {
|
|
1319
1547
|
const selection = selections[i];
|
|
@@ -1329,22 +1557,16 @@ export class SelectionSet {
|
|
|
1329
1557
|
}
|
|
1330
1558
|
}
|
|
1331
1559
|
if (!updatedSelections) {
|
|
1332
|
-
return this
|
|
1560
|
+
return this;
|
|
1333
1561
|
}
|
|
1334
|
-
return updatedSelections.toSelectionSet(options?.parentType ?? this.parentType
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
private withUpdatedFragments(newFragments: NamedFragments | undefined): SelectionSet {
|
|
1338
|
-
return this.fragments === newFragments ? this : new SelectionSet(this.parentType, this._keyedSelections, newFragments);
|
|
1562
|
+
return updatedSelections.toSelectionSet(options?.parentType ?? this.parentType);
|
|
1339
1563
|
}
|
|
1340
1564
|
|
|
1341
1565
|
withoutDefer(labelsToRemove?: Set<string>): SelectionSet {
|
|
1342
|
-
assert(!this.fragments, 'Not yet supported');
|
|
1343
1566
|
return this.lazyMap((selection) => selection.withoutDefer(labelsToRemove));
|
|
1344
1567
|
}
|
|
1345
1568
|
|
|
1346
1569
|
withNormalizedDefer(normalizer: DeferNormalizer): SelectionSet {
|
|
1347
|
-
assert(!this.fragments, 'Not yet supported');
|
|
1348
1570
|
return this.lazyMap((selection) => selection.withNormalizedDefer(normalizer));
|
|
1349
1571
|
}
|
|
1350
1572
|
|
|
@@ -1367,17 +1589,17 @@ export class SelectionSet {
|
|
|
1367
1589
|
return updated.isEmpty() ? undefined : updated;
|
|
1368
1590
|
}
|
|
1369
1591
|
|
|
1370
|
-
rebaseOn(parentType: CompositeType): SelectionSet {
|
|
1592
|
+
rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): SelectionSet {
|
|
1371
1593
|
if (this.parentType === parentType) {
|
|
1372
1594
|
return this;
|
|
1373
1595
|
}
|
|
1374
1596
|
|
|
1375
1597
|
const newSelections = new Map<string, Selection>();
|
|
1376
1598
|
for (const selection of this.selections()) {
|
|
1377
|
-
newSelections.set(selection.key(), selection.rebaseOn(parentType));
|
|
1599
|
+
newSelections.set(selection.key(), selection.rebaseOn(parentType, fragments));
|
|
1378
1600
|
}
|
|
1379
1601
|
|
|
1380
|
-
return new SelectionSet(parentType, newSelections
|
|
1602
|
+
return new SelectionSet(parentType, newSelections);
|
|
1381
1603
|
}
|
|
1382
1604
|
|
|
1383
1605
|
equals(that: SelectionSet): boolean {
|
|
@@ -1398,70 +1620,62 @@ export class SelectionSet {
|
|
|
1398
1620
|
return true;
|
|
1399
1621
|
}
|
|
1400
1622
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
if (selection.isUnecessaryInlineFragment(parentType)) {
|
|
1405
|
-
const selectionForKey = selection.selectionSet._keyedSelections.get(key);
|
|
1406
|
-
if (selectionForKey) {
|
|
1407
|
-
found.push(selectionForKey);
|
|
1408
|
-
}
|
|
1409
|
-
for (const nestedSelection of selection.selectionSet.triviallyNestedSelectionsForKey(parentType, key)) {
|
|
1410
|
-
found.push(nestedSelection);
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
return found;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
private mergeSameKeySelections(selections: Selection[]): Selection | undefined {
|
|
1418
|
-
if (selections.length === 0) {
|
|
1419
|
-
return undefined;
|
|
1420
|
-
}
|
|
1421
|
-
const first = selections[0];
|
|
1422
|
-
// We know that all the selections passed are for exactly the same element (same "key"). So if it is a
|
|
1423
|
-
// leaf field or a named fragment, then we know that even if we have more than 1 selection, all of them
|
|
1424
|
-
// are the exact same and we can just return the first one. Only if we have a composite field or an
|
|
1425
|
-
// inline fragment do we need to merge the underlying sub-selection (which may differ).
|
|
1426
|
-
if (!first.selectionSet || (first instanceof FragmentSpreadSelection) || selections.length === 1) {
|
|
1427
|
-
return first;
|
|
1623
|
+
contains(that: SelectionSet): ContainsResult {
|
|
1624
|
+
if (that._selections.length > this._selections.length) {
|
|
1625
|
+
return ContainsResult.NOT_CONTAINED;
|
|
1428
1626
|
}
|
|
1429
|
-
const mergedSubselections = new SelectionSetUpdates();
|
|
1430
|
-
for (const selection of selections) {
|
|
1431
|
-
mergedSubselections.add(selection.selectionSet!);
|
|
1432
|
-
}
|
|
1433
|
-
return first.withUpdatedSelectionSet(mergedSubselections.toSelectionSet(first.selectionSet.parentType));
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
contains(that: SelectionSet): boolean {
|
|
1437
|
-
// Note that we cannot really rely on the number of selections in `this` and `that` to short-cut this method
|
|
1438
|
-
// due to the handling of "trivially nested selections". That is, `this` might have less top-level selections
|
|
1439
|
-
// than `that`, and yet contains a named fragment directly on the parent type that includes everything in `that`.
|
|
1440
1627
|
|
|
1628
|
+
let isEqual = true;
|
|
1441
1629
|
for (const [key, thatSelection] of that._keyedSelections) {
|
|
1442
1630
|
const thisSelection = this._keyedSelections.get(key);
|
|
1443
|
-
const
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
if (!(mergedSelection && mergedSelection.contains(thatSelection))
|
|
1447
|
-
&& !(thatSelection.isUnecessaryInlineFragment(this.parentType) && this.contains(thatSelection.selectionSet))
|
|
1448
|
-
) {
|
|
1449
|
-
return false
|
|
1631
|
+
const selectionResult = thisSelection?.contains(thatSelection);
|
|
1632
|
+
if (selectionResult === undefined || selectionResult === ContainsResult.NOT_CONTAINED) {
|
|
1633
|
+
return ContainsResult.NOT_CONTAINED;
|
|
1450
1634
|
}
|
|
1635
|
+
isEqual &&= selectionResult === ContainsResult.EQUAL;
|
|
1451
1636
|
}
|
|
1452
|
-
|
|
1637
|
+
|
|
1638
|
+
return isEqual && that._selections.length === this._selections.length
|
|
1639
|
+
? ContainsResult.EQUAL
|
|
1640
|
+
: ContainsResult.STRICTLY_CONTAINED;
|
|
1453
1641
|
}
|
|
1454
1642
|
|
|
1455
1643
|
// Please note that this method assumes that `candidate.canApplyAtType(parentType) === true` but it is left to the caller to
|
|
1456
1644
|
// validate this (`canApplyAtType` is not free, and we want to avoid repeating it multiple times).
|
|
1457
|
-
diffWithNamedFragmentIfContained(
|
|
1458
|
-
|
|
1645
|
+
diffWithNamedFragmentIfContained(
|
|
1646
|
+
candidate: NamedFragmentDefinition,
|
|
1647
|
+
parentType: CompositeType,
|
|
1648
|
+
fragments: NamedFragments,
|
|
1649
|
+
): { contains: boolean, diff?: SelectionSet } {
|
|
1650
|
+
const that = candidate.expandedSelectionSetAtType(parentType);
|
|
1651
|
+
// It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
|
|
1652
|
+
// `parentType` is empty, or contains only `__typename`. For instance, suppose we have
|
|
1653
|
+
// a union `U = A | B | C`, and then a fragment:
|
|
1654
|
+
// ```graphql
|
|
1655
|
+
// fragment F on U {
|
|
1656
|
+
// ... on A {
|
|
1657
|
+
// x
|
|
1658
|
+
// }
|
|
1659
|
+
// ... on b {
|
|
1660
|
+
// y
|
|
1661
|
+
// }
|
|
1662
|
+
// }
|
|
1663
|
+
// ```
|
|
1664
|
+
// It is then possible to apply `F` when the parent type is `C`, but this ends up selecting
|
|
1665
|
+
// nothing at all.
|
|
1666
|
+
//
|
|
1667
|
+
// Returning `contains: true` in those cases is, while not 100% incorrect, at least not productive,
|
|
1668
|
+
// and so we skip right away in that case. This is essentially an optimisation.
|
|
1669
|
+
if (that.isEmpty() || (that.selections().length === 1 && that.selections()[0].isTypenameField())) {
|
|
1670
|
+
return { contains: false };
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1459
1673
|
if (this.contains(that)) {
|
|
1460
1674
|
// One subtlety here is that at "this" sub-selections may already have been optimized with some fragments. It's
|
|
1461
1675
|
// usually ok because `candidate` will also use those fragments, but one fragments that `candidate` can never be
|
|
1462
1676
|
// using is itself (the `contains` check is fine with this, but it's harder to deal in `minus`). So we expand
|
|
1463
1677
|
// the candidate we're currently looking at in "this" to avoid some issues.
|
|
1464
|
-
let updatedThis = this.expandFragments(
|
|
1678
|
+
let updatedThis = this.expandFragments(fragments.filter((f) => f.name !== candidate.name));
|
|
1465
1679
|
if (updatedThis !== this) {
|
|
1466
1680
|
updatedThis = updatedThis.trimUnsatisfiableBranches(parentType);
|
|
1467
1681
|
}
|
|
@@ -1480,18 +1694,16 @@ export class SelectionSet {
|
|
|
1480
1694
|
|
|
1481
1695
|
for (const [key, thisSelection] of this._keyedSelections) {
|
|
1482
1696
|
const thatSelection = that._keyedSelections.get(key);
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
} else {
|
|
1488
|
-
const selectionDiff = allSelections.reduce<Selection | undefined>((prev, val) => prev?.minus(val), thisSelection);
|
|
1489
|
-
if (selectionDiff) {
|
|
1490
|
-
updated.add(selectionDiff);
|
|
1697
|
+
if (thatSelection) {
|
|
1698
|
+
const remainder = thisSelection.minus(thatSelection);
|
|
1699
|
+
if (remainder) {
|
|
1700
|
+
updated.add(remainder);
|
|
1491
1701
|
}
|
|
1702
|
+
} else {
|
|
1703
|
+
updated.add(thisSelection);
|
|
1492
1704
|
}
|
|
1493
1705
|
}
|
|
1494
|
-
return updated.toSelectionSet(this.parentType
|
|
1706
|
+
return updated.toSelectionSet(this.parentType);
|
|
1495
1707
|
}
|
|
1496
1708
|
|
|
1497
1709
|
canRebaseOn(parentTypeToTest: CompositeType): boolean {
|
|
@@ -1590,13 +1802,14 @@ export class SelectionSet {
|
|
|
1590
1802
|
toOperationString(
|
|
1591
1803
|
rootKind: SchemaRootKind,
|
|
1592
1804
|
variableDefinitions: VariableDefinitions,
|
|
1805
|
+
fragments: NamedFragments | undefined,
|
|
1593
1806
|
operationName?: string,
|
|
1594
1807
|
expandFragments: boolean = false,
|
|
1595
1808
|
prettyPrint: boolean = true
|
|
1596
1809
|
): string {
|
|
1597
1810
|
const indent = prettyPrint ? '' : undefined;
|
|
1598
|
-
const fragmentsDefinitions = !expandFragments &&
|
|
1599
|
-
?
|
|
1811
|
+
const fragmentsDefinitions = !expandFragments && fragments && !fragments.isEmpty()
|
|
1812
|
+
? fragments.toString(indent) + "\n\n"
|
|
1600
1813
|
: "";
|
|
1601
1814
|
if (rootKind == "query" && !operationName && variableDefinitions.isEmpty()) {
|
|
1602
1815
|
return fragmentsDefinitions + this.toString(expandFragments, true, indent);
|
|
@@ -1786,11 +1999,11 @@ function makeSelection(parentType: CompositeType, updates: SelectionUpdate[], fr
|
|
|
1786
1999
|
|
|
1787
2000
|
// Optimize for the simple case of a single selection, as we don't have to do anything complex to merge the sub-selections.
|
|
1788
2001
|
if (updates.length === 1 && first instanceof AbstractSelection) {
|
|
1789
|
-
return first.rebaseOn(parentType);
|
|
2002
|
+
return first.rebaseOn(parentType, fragments);
|
|
1790
2003
|
}
|
|
1791
2004
|
|
|
1792
2005
|
const element = updateElement(first).rebaseOn(parentType);
|
|
1793
|
-
const subSelectionParentType = element.kind === 'Field' ? baseType(
|
|
2006
|
+
const subSelectionParentType = element.kind === 'Field' ? element.baseType() : element.castedType();
|
|
1794
2007
|
if (!isCompositeType(subSelectionParentType)) {
|
|
1795
2008
|
// This is a leaf, so all updates should correspond ot the same field and we just use the first.
|
|
1796
2009
|
return selectionOfElement(element);
|
|
@@ -1833,7 +2046,7 @@ function makeSelectionSet(parentType: CompositeType, keyedUpdates: MultiMap<stri
|
|
|
1833
2046
|
for (const [key, updates] of keyedUpdates.entries()) {
|
|
1834
2047
|
selections.set(key, makeSelection(parentType, updates, fragments));
|
|
1835
2048
|
}
|
|
1836
|
-
return new SelectionSet(parentType, selections
|
|
2049
|
+
return new SelectionSet(parentType, selections);
|
|
1837
2050
|
}
|
|
1838
2051
|
|
|
1839
2052
|
/**
|
|
@@ -1943,14 +2156,14 @@ export function allFieldDefinitionsInSelectionSet(selection: SelectionSet): Fiel
|
|
|
1943
2156
|
return allFields;
|
|
1944
2157
|
}
|
|
1945
2158
|
|
|
1946
|
-
export function selectionSetOf(parentType: CompositeType, selection: Selection
|
|
2159
|
+
export function selectionSetOf(parentType: CompositeType, selection: Selection): SelectionSet {
|
|
1947
2160
|
const map = new Map<string, Selection>()
|
|
1948
2161
|
map.set(selection.key(), selection);
|
|
1949
|
-
return new SelectionSet(parentType, map
|
|
2162
|
+
return new SelectionSet(parentType, map);
|
|
1950
2163
|
}
|
|
1951
2164
|
|
|
1952
|
-
export function selectionSetOfElement(element: OperationElement, subSelection?: SelectionSet
|
|
1953
|
-
return selectionSetOf(element.parentType, selectionOfElement(element, subSelection)
|
|
2165
|
+
export function selectionSetOfElement(element: OperationElement, subSelection?: SelectionSet): SelectionSet {
|
|
2166
|
+
return selectionSetOf(element.parentType, selectionOfElement(element, subSelection));
|
|
1954
2167
|
}
|
|
1955
2168
|
|
|
1956
2169
|
export function selectionOfElement(element: OperationElement, subSelection?: SelectionSet): Selection {
|
|
@@ -1978,12 +2191,17 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
1978
2191
|
|
|
1979
2192
|
abstract validate(variableDefinitions: VariableDefinitions): void;
|
|
1980
2193
|
|
|
1981
|
-
abstract rebaseOn(parentType: CompositeType): TOwnType;
|
|
2194
|
+
abstract rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): TOwnType;
|
|
1982
2195
|
|
|
1983
2196
|
get parentType(): CompositeType {
|
|
1984
2197
|
return this.element.parentType;
|
|
1985
2198
|
}
|
|
1986
2199
|
|
|
2200
|
+
isTypenameField(): boolean {
|
|
2201
|
+
// Overridden where appropriate
|
|
2202
|
+
return false;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
1987
2205
|
collectVariables(collector: VariableCollector) {
|
|
1988
2206
|
this.element.collectVariables(collector);
|
|
1989
2207
|
this.selectionSet?.collectVariables(collector)
|
|
@@ -1993,10 +2211,6 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
1993
2211
|
this.selectionSet?.collectUsedFragmentNames(collector);
|
|
1994
2212
|
}
|
|
1995
2213
|
|
|
1996
|
-
namedFragments(): NamedFragments | undefined {
|
|
1997
|
-
return this.selectionSet?.fragments;
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
2214
|
abstract withUpdatedComponents(element: TElement, selectionSet: SelectionSet | TIsLeaf): TOwnType;
|
|
2001
2215
|
|
|
2002
2216
|
withUpdatedSelectionSet(selectionSet: SelectionSet | TIsLeaf): TOwnType {
|
|
@@ -2024,12 +2238,14 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2024
2238
|
|
|
2025
2239
|
abstract hasDefer(): boolean;
|
|
2026
2240
|
|
|
2027
|
-
abstract
|
|
2028
|
-
|
|
2029
|
-
abstract expandFragments(names: string[], updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
|
|
2241
|
+
abstract expandFragments(updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
|
|
2030
2242
|
|
|
2031
2243
|
abstract trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): TOwnType | SelectionSet | undefined;
|
|
2032
2244
|
|
|
2245
|
+
isFragmentSpread(): boolean {
|
|
2246
|
+
return false;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2033
2249
|
minus(that: Selection): TOwnType | undefined {
|
|
2034
2250
|
// If there is a subset, then we compute the diff of the subset and add that (if not empty).
|
|
2035
2251
|
// Otherwise, we have no diff.
|
|
@@ -2042,53 +2258,131 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2042
2258
|
return undefined;
|
|
2043
2259
|
}
|
|
2044
2260
|
|
|
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`).
|
|
2049
|
-
protected tryOptimizeSubselectionOnce(_: {
|
|
2050
|
-
parentType: CompositeType,
|
|
2051
|
-
subSelection: SelectionSet,
|
|
2052
|
-
candidates: NamedFragmentDefinition[],
|
|
2053
|
-
fragments: NamedFragments,
|
|
2054
|
-
}): {
|
|
2055
|
-
spread?: FragmentSpreadSelection,
|
|
2056
|
-
optimizedSelection?: SelectionSet,
|
|
2057
|
-
hasDiff?: boolean,
|
|
2058
|
-
} {
|
|
2059
|
-
// Field and inline fragment override this, but this should never be called for a spread.
|
|
2060
|
-
assert(false, `UNSUPPORTED`);
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
2261
|
protected tryOptimizeSubselectionWithFragments({
|
|
2064
2262
|
parentType,
|
|
2065
2263
|
subSelection,
|
|
2066
2264
|
fragments,
|
|
2067
|
-
|
|
2265
|
+
canUseFullMatchingFragment,
|
|
2068
2266
|
}: {
|
|
2069
2267
|
parentType: CompositeType,
|
|
2070
2268
|
subSelection: SelectionSet,
|
|
2071
2269
|
fragments: NamedFragments,
|
|
2072
|
-
|
|
2073
|
-
}): SelectionSet |
|
|
2270
|
+
canUseFullMatchingFragment: (match: NamedFragmentDefinition) => boolean,
|
|
2271
|
+
}): SelectionSet | NamedFragmentDefinition {
|
|
2074
2272
|
let candidates = fragments.maybeApplyingAtType(parentType);
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2273
|
+
|
|
2274
|
+
// First, we check which of the candidates do apply inside `subSelection`, if any.
|
|
2275
|
+
// If we find a candidate that applies to the whole `subSelection`, then we stop and only return
|
|
2276
|
+
// that one candidate. Otherwise, we cumulate in `applyingFragments` the list of fragments that
|
|
2277
|
+
// applies to a subset of `subSelection`.
|
|
2278
|
+
const applyingFragments: NamedFragmentDefinition[] = [];
|
|
2279
|
+
for (const candidate of candidates) {
|
|
2280
|
+
const fragmentSSet = candidate.expandedSelectionSetAtType(parentType);
|
|
2281
|
+
// It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
|
|
2282
|
+
// `parentType` is empty, or contains only `__typename`. For instance, suppose we have
|
|
2283
|
+
// a union `U = A | B | C`, and then a fragment:
|
|
2284
|
+
// ```graphql
|
|
2285
|
+
// fragment F on U {
|
|
2286
|
+
// ... on A {
|
|
2287
|
+
// x
|
|
2288
|
+
// }
|
|
2289
|
+
// ... on b {
|
|
2290
|
+
// y
|
|
2291
|
+
// }
|
|
2292
|
+
// }
|
|
2293
|
+
// ```
|
|
2294
|
+
// It is then possible to apply `F` when the parent type is `C`, but this ends up selecting
|
|
2295
|
+
// nothing at all.
|
|
2296
|
+
//
|
|
2297
|
+
// Using `F` in those cases is, while not 100% incorrect, at least not productive, and so we
|
|
2298
|
+
// skip it that case. This is essentially an optimisation.
|
|
2299
|
+
if (fragmentSSet.isEmpty() || (fragmentSSet.selections().length === 1 && fragmentSSet.selections()[0].isTypenameField())) {
|
|
2300
|
+
continue;
|
|
2085
2301
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2302
|
+
|
|
2303
|
+
const res = subSelection.contains(fragmentSSet);
|
|
2304
|
+
|
|
2305
|
+
if (res === ContainsResult.EQUAL) {
|
|
2306
|
+
if (canUseFullMatchingFragment(candidate)) {
|
|
2307
|
+
return candidate;
|
|
2308
|
+
}
|
|
2309
|
+
// If we're not going to replace the full thing, then same reasoning a below.
|
|
2310
|
+
if (candidate.appliedDirectives.length === 0) {
|
|
2311
|
+
applyingFragments.push(candidate);
|
|
2312
|
+
}
|
|
2313
|
+
// Note that if a fragment applies to only a subset of the subSelection, then we really only can use
|
|
2314
|
+
// it if that fragment is defined _without_ directives.
|
|
2315
|
+
} else if (res === ContainsResult.STRICTLY_CONTAINED && candidate.appliedDirectives.length === 0) {
|
|
2316
|
+
applyingFragments.push(candidate);
|
|
2089
2317
|
}
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
if (applyingFragments.length === 0) {
|
|
2321
|
+
return subSelection;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// We have found the list of fragments that applies to some subset of `subSelection`. In general, we
|
|
2325
|
+
// want ot now produce the selection set with spread for those fragments plus any selection that is not
|
|
2326
|
+
// covered by any of the fragments. For instance, suppose that `subselection` is `{ a b c d e }`
|
|
2327
|
+
// and we have found that `fragment F1 on X { a b c }` and `fragment F2 on X { c d }` applies, then
|
|
2328
|
+
// we will generate `{ ...F1 ...F2 e }`.
|
|
2329
|
+
//
|
|
2330
|
+
// In that example, `c` is covered by both fragments. And this is fine in this example as it is
|
|
2331
|
+
// worth using both fragments in general. A special case of this however is if a fragment is entirely
|
|
2332
|
+
// included into another. That is, consider that we now have `fragment F1 on X { a ...F2 }` and
|
|
2333
|
+
// `fragment F2 on X { b c }`. In that case, the code above would still match both `F1 and `F2`,
|
|
2334
|
+
// but as `F1` includes `F2` already, we really want to only use `F1`. So in practice, we filter
|
|
2335
|
+
// away any fragment spread that is known to be included in another one that applies.
|
|
2336
|
+
//
|
|
2337
|
+
// TODO: note that the logic used for this is theoretically a bit sub-optimial. That is, we only
|
|
2338
|
+
// check if one of the fragment happens to directly include a spread for another fragment at
|
|
2339
|
+
// top-level as in the example above. We do this because it is cheap to check and is likely the
|
|
2340
|
+
// most common case of this kind of inclusion. But in theory, we would have
|
|
2341
|
+
// `fragment F1 on X { a b c }` and `fragment F2 on X { b c }`, in which case `F2` is still
|
|
2342
|
+
// included in `F1`, but we'd have to work harder to figure this out and it's unclear it's
|
|
2343
|
+
// a good tradeoff. And while you could argue that it's on the user to define its fragments
|
|
2344
|
+
// a bit more optimally, it's actually a tad more complex because we're looking at fragments
|
|
2345
|
+
// in a particular context/parent type. Consider an interface `I` and:
|
|
2346
|
+
// ```graphql
|
|
2347
|
+
// fragment F3 on I {
|
|
2348
|
+
// ... on X {
|
|
2349
|
+
// a
|
|
2350
|
+
// }
|
|
2351
|
+
// ... on Y {
|
|
2352
|
+
// b
|
|
2353
|
+
// c
|
|
2354
|
+
// }
|
|
2355
|
+
// }
|
|
2356
|
+
//
|
|
2357
|
+
// fragment F4 on I {
|
|
2358
|
+
// ... on Y {
|
|
2359
|
+
// c
|
|
2360
|
+
// }
|
|
2361
|
+
// ... on Z {
|
|
2362
|
+
// d
|
|
2363
|
+
// }
|
|
2364
|
+
// }
|
|
2365
|
+
// ```
|
|
2366
|
+
// In that case, neither fragment include the other per-se. But what if we have sub-selection
|
|
2367
|
+
// `{ b c }` but where parent type is `Y`. In that case, both `F3` and `F4` applies, and in that
|
|
2368
|
+
// particular context, `F3` is fully included in `F4`. Long story short, we'll currently
|
|
2369
|
+
// return `{ ...F3 ...F4 }` in that case, but it would be technically better to return only `F4`.
|
|
2370
|
+
// However, this feels niche, and it might be costly to verify such inclusions, so not doing it
|
|
2371
|
+
// for now.
|
|
2372
|
+
const filteredApplyingFragments = applyingFragments.filter((f) => !applyingFragments.some((o) => o.includes(f.name)));
|
|
2373
|
+
|
|
2374
|
+
let notCoveredByFragments = subSelection;
|
|
2375
|
+
const optimized = new SelectionSetUpdates();
|
|
2376
|
+
// TODO: doing repeated calls to `minus` for every fragment is simple, but a `minusAll` method that
|
|
2377
|
+
// takes the fragment selections at once would be more efficient in pratice.
|
|
2378
|
+
for (const fragment of filteredApplyingFragments) {
|
|
2379
|
+
// Note: we call `expandedSelectionSetAType` twice in this method for the applying fragments, but
|
|
2380
|
+
// we know it's cached so rely on that fact.
|
|
2381
|
+
notCoveredByFragments = notCoveredByFragments.minus(fragment.expandedSelectionSetAtType(parentType));
|
|
2382
|
+
optimized.add(new FragmentSpreadSelection(parentType, fragments, fragment, []));
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
return optimized.add(notCoveredByFragments).toSelectionSet(parentType, fragments)
|
|
2092
2386
|
}
|
|
2093
2387
|
}
|
|
2094
2388
|
|
|
@@ -2110,6 +2404,10 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2110
2404
|
return this;
|
|
2111
2405
|
}
|
|
2112
2406
|
|
|
2407
|
+
isTypenameField(): boolean {
|
|
2408
|
+
return this.element.definition.name === typenameFieldName;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2113
2411
|
withUpdatedComponents(field: Field<any>, selectionSet: SelectionSet | undefined): FieldSelection {
|
|
2114
2412
|
return new FieldSelection(field, selectionSet);
|
|
2115
2413
|
}
|
|
@@ -2119,57 +2417,36 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2119
2417
|
}
|
|
2120
2418
|
|
|
2121
2419
|
optimize(fragments: NamedFragments): Selection {
|
|
2122
|
-
let optimizedSelection = this.selectionSet ? this.selectionSet.optimizeSelections(fragments) : undefined;
|
|
2123
2420
|
const fieldBaseType = baseType(this.element.definition.type!);
|
|
2124
|
-
if (isCompositeType(fieldBaseType)
|
|
2421
|
+
if (!isCompositeType(fieldBaseType) || !this.selectionSet) {
|
|
2422
|
+
return this;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
// First, see if we can reuse fragments for the selection of this field.
|
|
2426
|
+
let optimizedSelection = this.selectionSet;
|
|
2427
|
+
if (isCompositeType(fieldBaseType) && this.selectionSet) {
|
|
2125
2428
|
const optimized = this.tryOptimizeSubselectionWithFragments({
|
|
2126
2429
|
parentType: fieldBaseType,
|
|
2127
|
-
subSelection:
|
|
2430
|
+
subSelection: this.selectionSet,
|
|
2128
2431
|
fragments,
|
|
2129
|
-
// We can never apply a fragments that has directives on it at the field level
|
|
2130
|
-
|
|
2131
|
-
// be handled by `InlineFragmentSelection.optimize` anyway).
|
|
2132
|
-
fragmentFilter: (f) => f.appliedDirectives.length === 0,
|
|
2432
|
+
// We can never apply a fragments that has directives on it at the field level.
|
|
2433
|
+
canUseFullMatchingFragment: (fragment) => fragment.appliedDirectives.length === 0,
|
|
2133
2434
|
});
|
|
2134
2435
|
|
|
2135
|
-
|
|
2136
|
-
|
|
2436
|
+
if (optimized instanceof NamedFragmentDefinition) {
|
|
2437
|
+
optimizedSelection = selectionSetOf(fieldBaseType, new FragmentSpreadSelection(fieldBaseType, fragments, optimized, []));
|
|
2438
|
+
} else {
|
|
2439
|
+
optimizedSelection = optimized;
|
|
2440
|
+
}
|
|
2137
2441
|
}
|
|
2138
2442
|
|
|
2443
|
+
// Then, recurse inside the field sub-selection (note that if we matched some fragments above,
|
|
2444
|
+
// this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
|
|
2445
|
+
optimizedSelection = optimizedSelection.optimize(fragments);
|
|
2446
|
+
|
|
2139
2447
|
return this.selectionSet === optimizedSelection
|
|
2140
2448
|
? this
|
|
2141
|
-
:
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
protected tryOptimizeSubselectionOnce({
|
|
2145
|
-
parentType,
|
|
2146
|
-
subSelection,
|
|
2147
|
-
candidates,
|
|
2148
|
-
fragments,
|
|
2149
|
-
}: {
|
|
2150
|
-
parentType: CompositeType,
|
|
2151
|
-
subSelection: SelectionSet,
|
|
2152
|
-
candidates: NamedFragmentDefinition[],
|
|
2153
|
-
fragments: NamedFragments,
|
|
2154
|
-
}): {
|
|
2155
|
-
spread?: FragmentSpreadSelection,
|
|
2156
|
-
optimizedSelection?: SelectionSet,
|
|
2157
|
-
hasDiff?: boolean,
|
|
2158
|
-
}{
|
|
2159
|
-
let optimizedSelection = subSelection;
|
|
2160
|
-
for (const candidate of candidates) {
|
|
2161
|
-
const { contains, diff } = optimizedSelection.diffWithNamedFragmentIfContained(candidate, parentType);
|
|
2162
|
-
if (contains) {
|
|
2163
|
-
// We can optimize the selection with this fragment. The replaced sub-selection will be
|
|
2164
|
-
// comprised of this new spread and the remaining `diff` if there is any.
|
|
2165
|
-
const spread = new FragmentSpreadSelection(parentType, fragments, candidate, []);
|
|
2166
|
-
optimizedSelection = diff
|
|
2167
|
-
? new SelectionSetUpdates().add(spread).add(diff).toSelectionSet(parentType, fragments)
|
|
2168
|
-
: selectionSetOf(parentType, spread);
|
|
2169
|
-
return { spread, optimizedSelection, hasDiff: !!diff }
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
return {};
|
|
2449
|
+
: this.withUpdatedSelectionSet(optimizedSelection);
|
|
2173
2450
|
}
|
|
2174
2451
|
|
|
2175
2452
|
filter(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
|
|
@@ -2203,7 +2480,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2203
2480
|
* Obviously, this operation will only succeed if this selection (both the field itself and its subselections)
|
|
2204
2481
|
* make sense from the provided parent type. If this is not the case, this method will throw.
|
|
2205
2482
|
*/
|
|
2206
|
-
rebaseOn(parentType: CompositeType): FieldSelection {
|
|
2483
|
+
rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FieldSelection {
|
|
2207
2484
|
if (this.element.parentType === parentType) {
|
|
2208
2485
|
return this;
|
|
2209
2486
|
}
|
|
@@ -2213,13 +2490,13 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2213
2490
|
return this.withUpdatedElement(rebasedElement);
|
|
2214
2491
|
}
|
|
2215
2492
|
|
|
2216
|
-
const rebasedBase = baseType(
|
|
2493
|
+
const rebasedBase = rebasedElement.baseType();
|
|
2217
2494
|
if (rebasedBase === this.selectionSet.parentType) {
|
|
2218
2495
|
return this.withUpdatedElement(rebasedElement);
|
|
2219
2496
|
}
|
|
2220
2497
|
|
|
2221
2498
|
validate(isCompositeType(rebasedBase), () => `Cannot rebase field selection ${this} on ${parentType}: rebased field base return type ${rebasedBase} is not composite`);
|
|
2222
|
-
return this.withUpdatedComponents(rebasedElement, this.selectionSet.rebaseOn(rebasedBase));
|
|
2499
|
+
return this.withUpdatedComponents(rebasedElement, this.selectionSet.rebaseOn(rebasedBase, fragments));
|
|
2223
2500
|
}
|
|
2224
2501
|
|
|
2225
2502
|
/**
|
|
@@ -2270,16 +2547,12 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2270
2547
|
return !!this.selectionSet?.hasDefer();
|
|
2271
2548
|
}
|
|
2272
2549
|
|
|
2273
|
-
expandAllFragments(): FieldSelection {
|
|
2274
|
-
return this.mapToSelectionSet((s) => s.expandAllFragments());
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
2550
|
trimUnsatisfiableBranches(_: CompositeType, options?: { recursive? : boolean }): FieldSelection {
|
|
2278
2551
|
if (!this.selectionSet) {
|
|
2279
2552
|
return this;
|
|
2280
2553
|
}
|
|
2281
2554
|
|
|
2282
|
-
const base =
|
|
2555
|
+
const base = this.element.baseType()
|
|
2283
2556
|
assert(isCompositeType(base), () => `Field ${this.element} should not have a sub-selection`);
|
|
2284
2557
|
const trimmed = (options?.recursive ?? true) ? this.mapToSelectionSet((s) => s.trimUnsatisfiableBranches(base)) : this;
|
|
2285
2558
|
// In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
|
|
@@ -2299,8 +2572,8 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2299
2572
|
}
|
|
2300
2573
|
}
|
|
2301
2574
|
|
|
2302
|
-
expandFragments(
|
|
2303
|
-
return this.mapToSelectionSet((s) => s.expandFragments(
|
|
2575
|
+
expandFragments(updatedFragments?: NamedFragments): FieldSelection {
|
|
2576
|
+
return this.mapToSelectionSet((s) => s.expandFragments(updatedFragments));
|
|
2304
2577
|
}
|
|
2305
2578
|
|
|
2306
2579
|
equals(that: Selection): boolean {
|
|
@@ -2317,20 +2590,17 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2317
2590
|
return !!that.selectionSet && this.selectionSet.equals(that.selectionSet);
|
|
2318
2591
|
}
|
|
2319
2592
|
|
|
2320
|
-
contains(that: Selection):
|
|
2593
|
+
contains(that: Selection): ContainsResult {
|
|
2321
2594
|
if (!(that instanceof FieldSelection) || !this.element.equals(that.element)) {
|
|
2322
|
-
return
|
|
2595
|
+
return ContainsResult.NOT_CONTAINED;
|
|
2323
2596
|
}
|
|
2324
2597
|
|
|
2325
|
-
if (!
|
|
2326
|
-
|
|
2598
|
+
if (!this.selectionSet) {
|
|
2599
|
+
assert(!that.selectionSet, '`this` and `that` have the same element, so if one does not have a sub-selection, neither should the other one')
|
|
2600
|
+
return ContainsResult.EQUAL;
|
|
2327
2601
|
}
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
isUnecessaryInlineFragment(_: CompositeType): this is InlineFragmentSelection {
|
|
2332
|
-
// Overridden by inline fragments
|
|
2333
|
-
return false;
|
|
2602
|
+
assert(that.selectionSet, '`this` and `that` have the same element, so if one has sub-selection, the other one should too')
|
|
2603
|
+
return this.selectionSet.contains(that.selectionSet);
|
|
2334
2604
|
}
|
|
2335
2605
|
|
|
2336
2606
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
@@ -2375,17 +2645,7 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
|
|
|
2375
2645
|
|
|
2376
2646
|
abstract equals(that: Selection): boolean;
|
|
2377
2647
|
|
|
2378
|
-
abstract contains(that: Selection):
|
|
2379
|
-
|
|
2380
|
-
isUnecessaryInlineFragment(parentType: CompositeType): boolean {
|
|
2381
|
-
return this.element.appliedDirectives.length === 0
|
|
2382
|
-
&& !!this.element.typeCondition
|
|
2383
|
-
&& (
|
|
2384
|
-
this.element.typeCondition.name === parentType.name
|
|
2385
|
-
|| (isObjectType(parentType) && possibleRuntimeTypes(this.element.typeCondition).some((t) => t.name === parentType.name))
|
|
2386
|
-
);
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2648
|
+
abstract contains(that: Selection): ContainsResult;
|
|
2389
2649
|
}
|
|
2390
2650
|
|
|
2391
2651
|
class InlineFragmentSelection extends FragmentSelection {
|
|
@@ -2419,7 +2679,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2419
2679
|
this.selectionSet.validate(variableDefinitions);
|
|
2420
2680
|
}
|
|
2421
2681
|
|
|
2422
|
-
rebaseOn(parentType: CompositeType): FragmentSelection {
|
|
2682
|
+
rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
|
|
2423
2683
|
if (this.parentType === parentType) {
|
|
2424
2684
|
return this;
|
|
2425
2685
|
}
|
|
@@ -2430,7 +2690,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2430
2690
|
return this.withUpdatedElement(rebasedFragment);
|
|
2431
2691
|
}
|
|
2432
2692
|
|
|
2433
|
-
return this.withUpdatedComponents(rebasedFragment, this.selectionSet.rebaseOn(rebasedCastedType));
|
|
2693
|
+
return this.withUpdatedComponents(rebasedFragment, this.selectionSet.rebaseOn(rebasedCastedType, fragments));
|
|
2434
2694
|
}
|
|
2435
2695
|
|
|
2436
2696
|
canAddTo(parentType: CompositeType): boolean {
|
|
@@ -2468,86 +2728,60 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2468
2728
|
}
|
|
2469
2729
|
|
|
2470
2730
|
optimize(fragments: NamedFragments): FragmentSelection {
|
|
2471
|
-
let optimizedSelection = this.selectionSet
|
|
2731
|
+
let optimizedSelection = this.selectionSet;
|
|
2732
|
+
|
|
2733
|
+
// First, see if we can reuse fragments for the selection of this field.
|
|
2472
2734
|
const typeCondition = this.element.typeCondition;
|
|
2473
2735
|
if (typeCondition) {
|
|
2474
2736
|
const optimized = this.tryOptimizeSubselectionWithFragments({
|
|
2475
2737
|
parentType: typeCondition,
|
|
2476
2738
|
subSelection: optimizedSelection,
|
|
2477
2739
|
fragments,
|
|
2740
|
+
canUseFullMatchingFragment: (fragment) => {
|
|
2741
|
+
// To be able to use a matching fragment, it needs to have either no directives, or if it has
|
|
2742
|
+
// some, then:
|
|
2743
|
+
// 1. all it's directives should also be on the current element.
|
|
2744
|
+
// 2. the directives of this element should be the fragment condition.
|
|
2745
|
+
// because if those 2 conditions are true, we cna replace the whole current inline fragment
|
|
2746
|
+
// with the match spread and directives will still match.
|
|
2747
|
+
return fragment.appliedDirectives.length === 0
|
|
2748
|
+
|| (
|
|
2749
|
+
sameType(typeCondition, fragment.typeCondition)
|
|
2750
|
+
&& fragment.appliedDirectives.every((d) => this.element.appliedDirectives.some((s) => sameDirectiveApplication(d, s)))
|
|
2751
|
+
);
|
|
2752
|
+
},
|
|
2478
2753
|
});
|
|
2479
|
-
if (optimized instanceof FragmentSpreadSelection) {
|
|
2480
|
-
// This means the whole inline fragment can be replaced by the spread.
|
|
2481
|
-
return optimized;
|
|
2482
|
-
}
|
|
2483
|
-
optimizedSelection = optimized;
|
|
2484
|
-
}
|
|
2485
|
-
return this.selectionSet === optimizedSelection
|
|
2486
|
-
? this
|
|
2487
|
-
: new InlineFragmentSelection(this.element, optimizedSelection);
|
|
2488
|
-
}
|
|
2489
2754
|
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
optimizedSelection?: SelectionSet,
|
|
2503
|
-
hasDiff?: boolean,
|
|
2504
|
-
}{
|
|
2505
|
-
let optimizedSelection = subSelection;
|
|
2506
|
-
for (const candidate of candidates) {
|
|
2507
|
-
const { contains, diff } = optimizedSelection.diffWithNamedFragmentIfContained(candidate, parentType);
|
|
2508
|
-
if (contains) {
|
|
2509
|
-
// The candidate selection is included in our sub-selection. One remaining thing to take into account
|
|
2510
|
-
// is applied directives: if the candidate has directives, then we can only use it if 1) there is
|
|
2511
|
-
// no `diff`, 2) the type condition of this fragment matches the candidate one and 3) the directives
|
|
2512
|
-
// in question are also on this very fragment. In that case, we can replace this whole inline fragment
|
|
2513
|
-
// by a spread of the candidate.
|
|
2514
|
-
if (!diff && sameType(this.element.typeCondition!, candidate.typeCondition)) {
|
|
2515
|
-
// We can potentially replace the whole fragment by the candidate; but as said above, still needs
|
|
2516
|
-
// to check the directives.
|
|
2517
|
-
let spreadDirectives: Directive<any>[] = this.element.appliedDirectives;
|
|
2518
|
-
if (candidate.appliedDirectives.length > 0) {
|
|
2519
|
-
const { isSubset, difference } = diffDirectives(this.element.appliedDirectives, candidate.appliedDirectives);
|
|
2520
|
-
if (!isSubset) {
|
|
2521
|
-
// While the candidate otherwise match, it has directives that are not on this element, so we
|
|
2522
|
-
// cannot reuse it.
|
|
2523
|
-
continue;
|
|
2524
|
-
}
|
|
2525
|
-
// Otherwise, any directives on this element that are not on the candidate should be kept and used
|
|
2526
|
-
// on the spread created.
|
|
2527
|
-
spreadDirectives = difference;
|
|
2755
|
+
if (optimized instanceof NamedFragmentDefinition) {
|
|
2756
|
+
// We're fully matching the sub-selection. If the fragment condition is also this element condition,
|
|
2757
|
+
// then we can replace the whole element by the spread (not just the sub-selection).
|
|
2758
|
+
if (sameType(typeCondition, optimized.typeCondition)) {
|
|
2759
|
+
// Note that `canUseFullMatchingFragment` above guarantees us that this element directives
|
|
2760
|
+
// are a superset of the fragment directives. But there can be additional directives, and in
|
|
2761
|
+
// that case they should be kept on the spread.
|
|
2762
|
+
let spreadDirectives = this.element.appliedDirectives;
|
|
2763
|
+
if (optimized.appliedDirectives) {
|
|
2764
|
+
spreadDirectives = spreadDirectives.filter(
|
|
2765
|
+
(s) => !optimized.appliedDirectives.some((d) => sameDirectiveApplication(d, s))
|
|
2766
|
+
);
|
|
2528
2767
|
}
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
// we
|
|
2532
|
-
|
|
2533
|
-
spread: new FragmentSpreadSelection(this.parentType, fragments, candidate, spreadDirectives),
|
|
2534
|
-
};
|
|
2535
|
-
}
|
|
2536
|
-
|
|
2537
|
-
// We're already dealt with the one case where we might be able to handle a candidate that has directives.
|
|
2538
|
-
if (candidate.appliedDirectives.length > 0) {
|
|
2539
|
-
continue;
|
|
2768
|
+
return new FragmentSpreadSelection(this.parentType, fragments, optimized, spreadDirectives);
|
|
2769
|
+
} else {
|
|
2770
|
+
// Otherwise, we keep this element and use a sub-selection with just the spread.
|
|
2771
|
+
optimizedSelection = selectionSetOf(typeCondition, new FragmentSpreadSelection(typeCondition, fragments, optimized, []));
|
|
2540
2772
|
}
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
optimizedSelection = diff
|
|
2544
|
-
? new SelectionSetUpdates().add(spread).add(diff).toSelectionSet(parentType, fragments)
|
|
2545
|
-
: selectionSetOf(parentType, spread);
|
|
2546
|
-
|
|
2547
|
-
return { spread, optimizedSelection, hasDiff: !!diff };
|
|
2773
|
+
} else {
|
|
2774
|
+
optimizedSelection = optimized;
|
|
2548
2775
|
}
|
|
2549
2776
|
}
|
|
2550
|
-
|
|
2777
|
+
|
|
2778
|
+
// Then, recurse inside the field sub-selection (note that if we matched some fragments above,
|
|
2779
|
+
// this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
|
|
2780
|
+
optimizedSelection = optimizedSelection.optimizeSelections(fragments);
|
|
2781
|
+
|
|
2782
|
+
return this.selectionSet === optimizedSelection
|
|
2783
|
+
? this
|
|
2784
|
+
: new InlineFragmentSelection(this.element, optimizedSelection);
|
|
2551
2785
|
}
|
|
2552
2786
|
|
|
2553
2787
|
withoutDefer(labelsToRemove?: Set<string>): InlineFragmentSelection | SelectionSet {
|
|
@@ -2597,7 +2831,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2597
2831
|
if (isObjectType(thisCondition) || !possibleRuntimeTypes(thisCondition).includes(currentType)) {
|
|
2598
2832
|
return undefined;
|
|
2599
2833
|
} else {
|
|
2600
|
-
const trimmed =this.selectionSet.trimUnsatisfiableBranches(currentType, options);
|
|
2834
|
+
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType, options);
|
|
2601
2835
|
return trimmed.isEmpty() ? undefined : trimmed;
|
|
2602
2836
|
}
|
|
2603
2837
|
}
|
|
@@ -2664,12 +2898,8 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2664
2898
|
return this.selectionSet === trimmedSelectionSet ? this : this.withUpdatedSelectionSet(trimmedSelectionSet);
|
|
2665
2899
|
}
|
|
2666
2900
|
|
|
2667
|
-
|
|
2668
|
-
return this.mapToSelectionSet((s) => s.
|
|
2669
|
-
}
|
|
2670
|
-
|
|
2671
|
-
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FragmentSelection {
|
|
2672
|
-
return this.mapToSelectionSet((s) => s.expandFragments(names, updatedFragments));
|
|
2901
|
+
expandFragments(updatedFragments: NamedFragments | undefined): FragmentSelection {
|
|
2902
|
+
return this.mapToSelectionSet((s) => s.expandFragments(updatedFragments));
|
|
2673
2903
|
}
|
|
2674
2904
|
|
|
2675
2905
|
equals(that: Selection): boolean {
|
|
@@ -2682,10 +2912,12 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2682
2912
|
&& this.selectionSet.equals(that.selectionSet);
|
|
2683
2913
|
}
|
|
2684
2914
|
|
|
2685
|
-
contains(that: Selection):
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2915
|
+
contains(that: Selection): ContainsResult {
|
|
2916
|
+
if (!(that instanceof FragmentSelection) || !this.element.equals(that.element)) {
|
|
2917
|
+
return ContainsResult.NOT_CONTAINED;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
return this.selectionSet.contains(that.selectionSet);
|
|
2689
2921
|
}
|
|
2690
2922
|
|
|
2691
2923
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
@@ -2693,14 +2925,6 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2693
2925
|
}
|
|
2694
2926
|
}
|
|
2695
2927
|
|
|
2696
|
-
function diffDirectives(superset: readonly Directive<any>[], maybeSubset: readonly Directive<any>[]): { isSubset: boolean, difference: Directive[] } {
|
|
2697
|
-
if (maybeSubset.every((d) => superset.some((s) => sameDirectiveApplication(d, s)))) {
|
|
2698
|
-
return { isSubset: true, difference: superset.filter((s) => !maybeSubset.some((d) => sameDirectiveApplication(d, s))) };
|
|
2699
|
-
} else {
|
|
2700
|
-
return { isSubset: false, difference: [] };
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
2703
|
-
|
|
2704
2928
|
class FragmentSpreadSelection extends FragmentSelection {
|
|
2705
2929
|
private computedKey: string | undefined;
|
|
2706
2930
|
|
|
@@ -2713,6 +2937,10 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2713
2937
|
super(new FragmentElement(sourceType, namedFragment.typeCondition, namedFragment.appliedDirectives.concat(spreadDirectives)));
|
|
2714
2938
|
}
|
|
2715
2939
|
|
|
2940
|
+
isFragmentSpread(): boolean {
|
|
2941
|
+
return true;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2716
2944
|
get selectionSet(): SelectionSet {
|
|
2717
2945
|
return this.namedFragment.selectionSet;
|
|
2718
2946
|
}
|
|
@@ -2728,12 +2956,11 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2728
2956
|
assert(false, `Unsupported`);
|
|
2729
2957
|
}
|
|
2730
2958
|
|
|
2731
|
-
trimUnsatisfiableBranches(
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
return this.fragments;
|
|
2959
|
+
trimUnsatisfiableBranches(parentType: CompositeType): FragmentSelection {
|
|
2960
|
+
// We must update the spread parent type if necessary since we're not going deeper,
|
|
2961
|
+
// or we'll be fundamentally losing context.
|
|
2962
|
+
assert(parentType.schema() === this.parentType.schema(), 'Should not try to trim using a type from another schema');
|
|
2963
|
+
return this.rebaseOn(parentType, this.fragments);
|
|
2737
2964
|
}
|
|
2738
2965
|
|
|
2739
2966
|
validate(): void {
|
|
@@ -2766,41 +2993,55 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2766
2993
|
return this;
|
|
2767
2994
|
}
|
|
2768
2995
|
|
|
2769
|
-
rebaseOn(
|
|
2770
|
-
//
|
|
2771
|
-
//
|
|
2772
|
-
// reuse a user fragment). But in practice, we expand all fragments when we do query planning and only re-add
|
|
2773
|
-
// fragments back at the very end, so this should be fine. Importantly, we don't want this method to mistakenly
|
|
2774
|
-
// expand the spread, as that would compromise the code that optimize subgraph fetches to re-use named
|
|
2996
|
+
rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
|
|
2997
|
+
// We preserve the parent type here, to make sure we don't lose context, but we actually don't
|
|
2998
|
+
// want to expand the spread as that would compromise the code that optimize subgraph fetches to re-use named
|
|
2775
2999
|
// fragments.
|
|
2776
|
-
|
|
3000
|
+
//
|
|
3001
|
+
// This is a little bit iffy, because the fragment may not apply at this parent type, but we
|
|
3002
|
+
// currently leave it to the caller to ensure this is not a mistake. But most of the
|
|
3003
|
+
// QP code works on selections with fully expanded fragments, so this code (and that of `canAddTo`
|
|
3004
|
+
// on come into play in the code for reusing fragments, and that code calls those methods
|
|
3005
|
+
// appropriately.
|
|
3006
|
+
if (this.parentType === parentType) {
|
|
3007
|
+
return this;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// If we're rebasing on a _different_ schema, then we *must* have fragments, since reusing
|
|
3011
|
+
// `this.fragments` would be incorrect. If we're on the same schema though, we're happy to default
|
|
3012
|
+
// to `this.fragments`.
|
|
3013
|
+
assert(fragments || this.parentType.schema() === parentType.schema(), `Must provide fragments is rebasing on other schema`);
|
|
3014
|
+
const newFragments = fragments ?? this.fragments;
|
|
3015
|
+
const namedFragment = newFragments.get(this.namedFragment.name);
|
|
3016
|
+
assert(namedFragment, () => `Cannot rebase ${this} if it isn't part of the provided fragments`);
|
|
3017
|
+
return new FragmentSpreadSelection(
|
|
3018
|
+
parentType,
|
|
3019
|
+
newFragments,
|
|
3020
|
+
namedFragment,
|
|
3021
|
+
this.spreadDirectives,
|
|
3022
|
+
);
|
|
2777
3023
|
}
|
|
2778
3024
|
|
|
2779
3025
|
canAddTo(_: CompositeType): boolean {
|
|
2780
|
-
//
|
|
3026
|
+
// Since `rebaseOn` never fail, we copy the logic here and always return `true`. But as
|
|
3027
|
+
// mentioned in `rebaseOn`, this leave it a bit to the caller to know what he is doing.
|
|
2781
3028
|
return true;
|
|
2782
3029
|
}
|
|
2783
3030
|
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
: new InlineFragmentSelection(this.element, expandedSubSelections);
|
|
2789
|
-
}
|
|
2790
|
-
|
|
2791
|
-
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FragmentSelection | readonly Selection[] {
|
|
2792
|
-
if (!names.includes(this.namedFragment.name)) {
|
|
3031
|
+
expandFragments(updatedFragments: NamedFragments | undefined): FragmentSelection | readonly Selection[] {
|
|
3032
|
+
// Note that this test will always fail if `updatedFragments` is `undefined`, making us expand everything.
|
|
3033
|
+
if (updatedFragments?.has(this.namedFragment.name)) {
|
|
3034
|
+
// This one is still there, it's not expanded.
|
|
2793
3035
|
return this;
|
|
2794
3036
|
}
|
|
2795
3037
|
|
|
2796
|
-
const expandedSubSelections = this.selectionSet.expandFragments(
|
|
3038
|
+
const expandedSubSelections = this.selectionSet.expandFragments(updatedFragments);
|
|
2797
3039
|
return sameType(this.parentType, this.namedFragment.typeCondition) && this.element.appliedDirectives.length === 0
|
|
2798
3040
|
? expandedSubSelections.selections()
|
|
2799
3041
|
: new InlineFragmentSelection(this.element, expandedSubSelections);
|
|
2800
3042
|
}
|
|
2801
3043
|
|
|
2802
3044
|
collectUsedFragmentNames(collector: Map<string, number>): void {
|
|
2803
|
-
this.selectionSet.collectUsedFragmentNames(collector);
|
|
2804
3045
|
const usageCount = collector.get(this.namedFragment.name);
|
|
2805
3046
|
collector.set(this.namedFragment.name, usageCount === undefined ? 1 : usageCount + 1);
|
|
2806
3047
|
}
|
|
@@ -2828,14 +3069,16 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2828
3069
|
&& sameDirectiveApplications(this.spreadDirectives, that.spreadDirectives);
|
|
2829
3070
|
}
|
|
2830
3071
|
|
|
2831
|
-
contains(that: Selection):
|
|
3072
|
+
contains(that: Selection): ContainsResult {
|
|
2832
3073
|
if (this.equals(that)) {
|
|
2833
|
-
return
|
|
3074
|
+
return ContainsResult.EQUAL;
|
|
2834
3075
|
}
|
|
2835
3076
|
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
3077
|
+
if (!(that instanceof FragmentSelection) || !this.element.equals(that.element)) {
|
|
3078
|
+
return ContainsResult.NOT_CONTAINED;
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
return this.selectionSet.contains(that.selectionSet);
|
|
2839
3082
|
}
|
|
2840
3083
|
|
|
2841
3084
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
@@ -2860,7 +3103,6 @@ function selectionSetOfNode(
|
|
|
2860
3103
|
return selectionSetOf(
|
|
2861
3104
|
parentType,
|
|
2862
3105
|
selectionOfNode(parentType, node.selections[0], variableDefinitions, fragments, fieldAccessor),
|
|
2863
|
-
fragments,
|
|
2864
3106
|
);
|
|
2865
3107
|
}
|
|
2866
3108
|
|
|
@@ -2992,6 +3234,7 @@ function operationFromAST({
|
|
|
2992
3234
|
}) : Operation {
|
|
2993
3235
|
const rootType = schema.schemaDefinition.root(operation.operation);
|
|
2994
3236
|
validate(rootType, () => `The schema has no "${operation.operation}" root type defined`);
|
|
3237
|
+
const fragmentsIfAny = fragments.isEmpty() ? undefined : fragments;
|
|
2995
3238
|
return new Operation(
|
|
2996
3239
|
schema,
|
|
2997
3240
|
operation.operation,
|
|
@@ -2999,10 +3242,11 @@ function operationFromAST({
|
|
|
2999
3242
|
parentType: rootType.type,
|
|
3000
3243
|
source: operation.selectionSet,
|
|
3001
3244
|
variableDefinitions,
|
|
3002
|
-
fragments:
|
|
3245
|
+
fragments: fragmentsIfAny,
|
|
3003
3246
|
validate: validateInput,
|
|
3004
3247
|
}),
|
|
3005
3248
|
variableDefinitions,
|
|
3249
|
+
fragmentsIfAny,
|
|
3006
3250
|
operation.name?.value
|
|
3007
3251
|
);
|
|
3008
3252
|
}
|
|
@@ -3059,8 +3303,8 @@ export function operationToDocument(operation: Operation): DocumentNode {
|
|
|
3059
3303
|
selectionSet: operation.selectionSet.toSelectionSetNode(),
|
|
3060
3304
|
variableDefinitions: operation.variableDefinitions.toVariableDefinitionNodes(),
|
|
3061
3305
|
};
|
|
3062
|
-
const fragmentASTs: DefinitionNode[] = operation.
|
|
3063
|
-
? operation.
|
|
3306
|
+
const fragmentASTs: DefinitionNode[] = operation.fragments
|
|
3307
|
+
? operation.fragments?.toFragmentDefinitionNodes()
|
|
3064
3308
|
: [];
|
|
3065
3309
|
return {
|
|
3066
3310
|
kind: Kind.DOCUMENT,
|