@graphql-tools/delegate 12.0.16 → 12.0.17-alpha-96a4674a514e93d69c299c07916e06f49feed477

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @graphql-tools/delegate
2
2
 
3
+ ## 12.0.17-alpha-96a4674a514e93d69c299c07916e06f49feed477
4
+ ### Patch Changes
5
+
6
+
7
+
8
+ - [#2351](https://github.com/graphql-hive/gateway/pull/2351) [`96a4674`](https://github.com/graphql-hive/gateway/commit/96a4674a514e93d69c299c07916e06f49feed477) Thanks [@ardatan](https://github.com/ardatan)! - Fix `@provides` so the gateway only requests the provided fields the client actually selected.
9
+
10
+ Previously, when a subgraph declared `@provides(fields: "...")` on a query field, the gateway would forward **every** field listed in the `@provides` argument to that subgraph, even when the client never asked for those fields. For example with:
11
+
12
+ ```graphql
13
+ # subgraph B
14
+ type Query {
15
+ entity: Entity @provides(fields: "name description")
16
+ }
17
+
18
+ type Entity @key(fields: "id") {
19
+ id: ID!
20
+ name: String! @external
21
+ description: String! @external
22
+ }
23
+ ```
24
+
25
+ a client query of `{ entity { id name } }` would still cause the gateway to ask subgraph B for `description`. After this fix the gateway only forwards `name` (because that is what the client asked for and what `@provides` allows the subgraph to resolve directly), bringing the behavior in line with Federation's `@provides` semantics where it acts as a hint that the providing subgraph **can** resolve those fields locally — not a directive to always fetch them.
26
+
27
+ Aliases, fragments, fragment spreads, and nested `@provides` selections are preserved.
28
+
3
29
  ## 12.0.16
4
30
  ### Patch Changes
5
31
 
package/dist/index.cjs CHANGED
@@ -590,7 +590,7 @@ const handleOverrideByDelegation = utils.memoize3(
590
590
  }
591
591
  );
592
592
 
593
- function finalizeGatewayDocument(targetSchema, fragments, operations, onOverlappingAliases, delegationContext) {
593
+ function finalizeGatewayDocument(targetSchema, originalDocument, fragments, operations, onOverlappingAliases, delegationContext) {
594
594
  let usedVariables = [];
595
595
  let usedFragments = [];
596
596
  const newOperations = [];
@@ -688,37 +688,82 @@ function finalizeGatewayDocument(targetSchema, fragments, operations, onOverlapp
688
688
  };
689
689
  const stitchingInfo = delegationContext.info?.schema?.extensions?.["stitchingInfo"];
690
690
  if (stitchingInfo != null) {
691
+ const {
692
+ selectionSetsByPath: originalFieldSelectionSetsByPath,
693
+ fragments: originalFragmentsByName
694
+ } = collectFieldSelectionSetsByPath(originalDocument);
691
695
  const typeInfo = getTypeInfo(targetSchema);
696
+ const pathStack = [];
692
697
  newDocument = graphql.visit(
693
698
  newDocument,
694
699
  graphql.visitWithTypeInfo(typeInfo, {
695
- [graphql.Kind.FIELD](fieldNode) {
696
- const parentType = typeInfo.getParentType();
697
- if (parentType) {
698
- const parentTypeName = parentType.name;
699
- const typeConfig = stitchingInfo?.mergedTypes?.[parentTypeName];
700
- if (typeConfig) {
700
+ [graphql.Kind.OPERATION_DEFINITION]: {
701
+ enter(node) {
702
+ pathStack.push(`op:${node.operation}:${node.name?.value ?? ""}`);
703
+ },
704
+ leave() {
705
+ pathStack.pop();
706
+ }
707
+ },
708
+ [graphql.Kind.FRAGMENT_DEFINITION]: {
709
+ enter(node) {
710
+ pathStack.push(`frag:${node.name.value}`);
711
+ },
712
+ leave() {
713
+ pathStack.pop();
714
+ }
715
+ },
716
+ [graphql.Kind.INLINE_FRAGMENT]: {
717
+ enter(node) {
718
+ pathStack.push(`inline:${node.typeCondition?.name.value ?? ""}`);
719
+ },
720
+ leave() {
721
+ pathStack.pop();
722
+ }
723
+ },
724
+ [graphql.Kind.FIELD]: {
725
+ enter(fieldNode) {
726
+ pathStack.push(fieldNode.alias?.value ?? fieldNode.name.value);
727
+ },
728
+ leave(fieldNode) {
729
+ try {
730
+ const parentType = typeInfo.getParentType();
731
+ if (!parentType) {
732
+ return void 0;
733
+ }
734
+ const typeConfig = stitchingInfo?.mergedTypes?.[parentType.name];
701
735
  const providedSelectionsByField = typeConfig?.providedSelectionsByField?.get(
702
736
  delegationContext.subschema
703
737
  );
704
- if (providedSelectionsByField) {
705
- const providedSelection = providedSelectionsByField[fieldNode.name.value];
706
- if (providedSelection) {
707
- return {
708
- ...fieldNode,
709
- selectionSet: {
710
- kind: graphql.Kind.SELECTION_SET,
711
- selections: [
712
- ...providedSelection.selections,
713
- ...fieldNode.selectionSet?.selections ?? []
714
- ]
715
- }
716
- };
717
- }
738
+ const providedSelection = providedSelectionsByField?.[fieldNode.name.value];
739
+ if (!providedSelection) {
740
+ return void 0;
741
+ }
742
+ const originalSelectionSet = originalFieldSelectionSetsByPath.get(
743
+ pathStack.join(">")
744
+ );
745
+ const requestedProvidedSelections = intersectProvidedSelections(
746
+ providedSelection.selections,
747
+ originalSelectionSet,
748
+ originalFragmentsByName
749
+ );
750
+ if (!requestedProvidedSelections.length) {
751
+ return void 0;
718
752
  }
753
+ return {
754
+ ...fieldNode,
755
+ selectionSet: {
756
+ kind: graphql.Kind.SELECTION_SET,
757
+ selections: [
758
+ ...requestedProvidedSelections,
759
+ ...fieldNode.selectionSet?.selections ?? []
760
+ ]
761
+ }
762
+ };
763
+ } finally {
764
+ pathStack.pop();
719
765
  }
720
766
  }
721
- return fieldNode;
722
767
  }
723
768
  })
724
769
  );
@@ -733,6 +778,7 @@ function finalizeGatewayRequest(originalRequest, delegationContext, onOverlappin
733
778
  const { targetSchema } = delegationContext;
734
779
  const { usedVariables, newDocument } = finalizeGatewayDocument(
735
780
  targetSchema,
781
+ originalRequest.document,
736
782
  fragments,
737
783
  operations,
738
784
  onOverlappingAliases,
@@ -761,6 +807,141 @@ function finalizeGatewayRequest(originalRequest, delegationContext, onOverlappin
761
807
  function isTypeNameField(selection) {
762
808
  return selection.kind === graphql.Kind.FIELD && !selection.alias && selection.name.value === "__typename";
763
809
  }
810
+ function collectFieldSelectionSetsByPath(document) {
811
+ const selectionSetsByPath = /* @__PURE__ */ new Map();
812
+ const fragments = /* @__PURE__ */ new Map();
813
+ for (const def of document.definitions) {
814
+ if (def.kind === graphql.Kind.FRAGMENT_DEFINITION) {
815
+ fragments.set(def.name.value, def);
816
+ }
817
+ }
818
+ const pathStack = [];
819
+ graphql.visit(document, {
820
+ [graphql.Kind.OPERATION_DEFINITION]: {
821
+ enter(node) {
822
+ pathStack.push(`op:${node.operation}:${node.name?.value ?? ""}`);
823
+ },
824
+ leave() {
825
+ pathStack.pop();
826
+ }
827
+ },
828
+ [graphql.Kind.FRAGMENT_DEFINITION]: {
829
+ enter(node) {
830
+ pathStack.push(`frag:${node.name.value}`);
831
+ },
832
+ leave() {
833
+ pathStack.pop();
834
+ }
835
+ },
836
+ [graphql.Kind.INLINE_FRAGMENT]: {
837
+ enter(node) {
838
+ pathStack.push(`inline:${node.typeCondition?.name.value ?? ""}`);
839
+ },
840
+ leave() {
841
+ pathStack.pop();
842
+ }
843
+ },
844
+ [graphql.Kind.FIELD]: {
845
+ enter(node) {
846
+ pathStack.push(node.alias?.value ?? node.name.value);
847
+ if (node.selectionSet) {
848
+ selectionSetsByPath.set(pathStack.join(">"), node.selectionSet);
849
+ }
850
+ },
851
+ leave() {
852
+ pathStack.pop();
853
+ }
854
+ }
855
+ });
856
+ return { selectionSetsByPath, fragments };
857
+ }
858
+ function intersectProvidedSelections(providedSelections, originalSelectionSet, fragments) {
859
+ if (!originalSelectionSet) {
860
+ return [];
861
+ }
862
+ const providedFieldsByName = /* @__PURE__ */ new Map();
863
+ const otherProvided = [];
864
+ for (const provided of providedSelections) {
865
+ if (provided.kind === graphql.Kind.FIELD) {
866
+ providedFieldsByName.set(provided.name.value, provided);
867
+ } else {
868
+ otherProvided.push(provided);
869
+ }
870
+ }
871
+ const result = [];
872
+ const seenFieldNames = /* @__PURE__ */ new Set();
873
+ const seenFragmentNames = /* @__PURE__ */ new Set();
874
+ collectMatchingOriginalSelections(
875
+ originalSelectionSet,
876
+ providedFieldsByName,
877
+ fragments,
878
+ seenFieldNames,
879
+ seenFragmentNames,
880
+ result
881
+ );
882
+ return [...result, ...otherProvided];
883
+ }
884
+ function collectMatchingOriginalSelections(selectionSet, providedFieldsByName, fragments, seenFieldNames, seenFragmentNames, result) {
885
+ for (const sel of selectionSet.selections) {
886
+ if (sel.kind === graphql.Kind.FIELD) {
887
+ const provided = providedFieldsByName.get(sel.name.value);
888
+ if (!provided) {
889
+ continue;
890
+ }
891
+ const dedupKey = sel.alias?.value ?? sel.name.value;
892
+ if (seenFieldNames.has(dedupKey)) {
893
+ continue;
894
+ }
895
+ seenFieldNames.add(dedupKey);
896
+ if (provided.selectionSet && sel.selectionSet) {
897
+ const nested = intersectProvidedSelections(
898
+ provided.selectionSet.selections,
899
+ sel.selectionSet,
900
+ fragments
901
+ );
902
+ if (!nested.length) {
903
+ continue;
904
+ }
905
+ result.push({
906
+ ...sel,
907
+ selectionSet: {
908
+ kind: graphql.Kind.SELECTION_SET,
909
+ selections: nested
910
+ }
911
+ });
912
+ } else {
913
+ result.push(sel);
914
+ }
915
+ } else if (sel.kind === graphql.Kind.INLINE_FRAGMENT && sel.selectionSet) {
916
+ collectMatchingOriginalSelections(
917
+ sel.selectionSet,
918
+ providedFieldsByName,
919
+ fragments,
920
+ seenFieldNames,
921
+ seenFragmentNames,
922
+ result
923
+ );
924
+ } else if (sel.kind === graphql.Kind.FRAGMENT_SPREAD) {
925
+ const fragmentName = sel.name.value;
926
+ if (seenFragmentNames.has(fragmentName)) {
927
+ continue;
928
+ }
929
+ seenFragmentNames.add(fragmentName);
930
+ const fragmentDef = fragments.get(fragmentName);
931
+ if (!fragmentDef) {
932
+ continue;
933
+ }
934
+ collectMatchingOriginalSelections(
935
+ fragmentDef.selectionSet,
936
+ providedFieldsByName,
937
+ fragments,
938
+ seenFieldNames,
939
+ seenFragmentNames,
940
+ result
941
+ );
942
+ }
943
+ }
944
+ }
764
945
  function filterTypenameFields(selections) {
765
946
  let hasTypeNameField = false;
766
947
  const filteredSelections = selections.filter((selection) => {
package/dist/index.js CHANGED
@@ -590,7 +590,7 @@ const handleOverrideByDelegation = memoize3(
590
590
  }
591
591
  );
592
592
 
593
- function finalizeGatewayDocument(targetSchema, fragments, operations, onOverlappingAliases, delegationContext) {
593
+ function finalizeGatewayDocument(targetSchema, originalDocument, fragments, operations, onOverlappingAliases, delegationContext) {
594
594
  let usedVariables = [];
595
595
  let usedFragments = [];
596
596
  const newOperations = [];
@@ -688,37 +688,82 @@ function finalizeGatewayDocument(targetSchema, fragments, operations, onOverlapp
688
688
  };
689
689
  const stitchingInfo = delegationContext.info?.schema?.extensions?.["stitchingInfo"];
690
690
  if (stitchingInfo != null) {
691
+ const {
692
+ selectionSetsByPath: originalFieldSelectionSetsByPath,
693
+ fragments: originalFragmentsByName
694
+ } = collectFieldSelectionSetsByPath(originalDocument);
691
695
  const typeInfo = getTypeInfo(targetSchema);
696
+ const pathStack = [];
692
697
  newDocument = visit(
693
698
  newDocument,
694
699
  visitWithTypeInfo(typeInfo, {
695
- [Kind.FIELD](fieldNode) {
696
- const parentType = typeInfo.getParentType();
697
- if (parentType) {
698
- const parentTypeName = parentType.name;
699
- const typeConfig = stitchingInfo?.mergedTypes?.[parentTypeName];
700
- if (typeConfig) {
700
+ [Kind.OPERATION_DEFINITION]: {
701
+ enter(node) {
702
+ pathStack.push(`op:${node.operation}:${node.name?.value ?? ""}`);
703
+ },
704
+ leave() {
705
+ pathStack.pop();
706
+ }
707
+ },
708
+ [Kind.FRAGMENT_DEFINITION]: {
709
+ enter(node) {
710
+ pathStack.push(`frag:${node.name.value}`);
711
+ },
712
+ leave() {
713
+ pathStack.pop();
714
+ }
715
+ },
716
+ [Kind.INLINE_FRAGMENT]: {
717
+ enter(node) {
718
+ pathStack.push(`inline:${node.typeCondition?.name.value ?? ""}`);
719
+ },
720
+ leave() {
721
+ pathStack.pop();
722
+ }
723
+ },
724
+ [Kind.FIELD]: {
725
+ enter(fieldNode) {
726
+ pathStack.push(fieldNode.alias?.value ?? fieldNode.name.value);
727
+ },
728
+ leave(fieldNode) {
729
+ try {
730
+ const parentType = typeInfo.getParentType();
731
+ if (!parentType) {
732
+ return void 0;
733
+ }
734
+ const typeConfig = stitchingInfo?.mergedTypes?.[parentType.name];
701
735
  const providedSelectionsByField = typeConfig?.providedSelectionsByField?.get(
702
736
  delegationContext.subschema
703
737
  );
704
- if (providedSelectionsByField) {
705
- const providedSelection = providedSelectionsByField[fieldNode.name.value];
706
- if (providedSelection) {
707
- return {
708
- ...fieldNode,
709
- selectionSet: {
710
- kind: Kind.SELECTION_SET,
711
- selections: [
712
- ...providedSelection.selections,
713
- ...fieldNode.selectionSet?.selections ?? []
714
- ]
715
- }
716
- };
717
- }
738
+ const providedSelection = providedSelectionsByField?.[fieldNode.name.value];
739
+ if (!providedSelection) {
740
+ return void 0;
741
+ }
742
+ const originalSelectionSet = originalFieldSelectionSetsByPath.get(
743
+ pathStack.join(">")
744
+ );
745
+ const requestedProvidedSelections = intersectProvidedSelections(
746
+ providedSelection.selections,
747
+ originalSelectionSet,
748
+ originalFragmentsByName
749
+ );
750
+ if (!requestedProvidedSelections.length) {
751
+ return void 0;
718
752
  }
753
+ return {
754
+ ...fieldNode,
755
+ selectionSet: {
756
+ kind: Kind.SELECTION_SET,
757
+ selections: [
758
+ ...requestedProvidedSelections,
759
+ ...fieldNode.selectionSet?.selections ?? []
760
+ ]
761
+ }
762
+ };
763
+ } finally {
764
+ pathStack.pop();
719
765
  }
720
766
  }
721
- return fieldNode;
722
767
  }
723
768
  })
724
769
  );
@@ -733,6 +778,7 @@ function finalizeGatewayRequest(originalRequest, delegationContext, onOverlappin
733
778
  const { targetSchema } = delegationContext;
734
779
  const { usedVariables, newDocument } = finalizeGatewayDocument(
735
780
  targetSchema,
781
+ originalRequest.document,
736
782
  fragments,
737
783
  operations,
738
784
  onOverlappingAliases,
@@ -761,6 +807,141 @@ function finalizeGatewayRequest(originalRequest, delegationContext, onOverlappin
761
807
  function isTypeNameField(selection) {
762
808
  return selection.kind === Kind.FIELD && !selection.alias && selection.name.value === "__typename";
763
809
  }
810
+ function collectFieldSelectionSetsByPath(document) {
811
+ const selectionSetsByPath = /* @__PURE__ */ new Map();
812
+ const fragments = /* @__PURE__ */ new Map();
813
+ for (const def of document.definitions) {
814
+ if (def.kind === Kind.FRAGMENT_DEFINITION) {
815
+ fragments.set(def.name.value, def);
816
+ }
817
+ }
818
+ const pathStack = [];
819
+ visit(document, {
820
+ [Kind.OPERATION_DEFINITION]: {
821
+ enter(node) {
822
+ pathStack.push(`op:${node.operation}:${node.name?.value ?? ""}`);
823
+ },
824
+ leave() {
825
+ pathStack.pop();
826
+ }
827
+ },
828
+ [Kind.FRAGMENT_DEFINITION]: {
829
+ enter(node) {
830
+ pathStack.push(`frag:${node.name.value}`);
831
+ },
832
+ leave() {
833
+ pathStack.pop();
834
+ }
835
+ },
836
+ [Kind.INLINE_FRAGMENT]: {
837
+ enter(node) {
838
+ pathStack.push(`inline:${node.typeCondition?.name.value ?? ""}`);
839
+ },
840
+ leave() {
841
+ pathStack.pop();
842
+ }
843
+ },
844
+ [Kind.FIELD]: {
845
+ enter(node) {
846
+ pathStack.push(node.alias?.value ?? node.name.value);
847
+ if (node.selectionSet) {
848
+ selectionSetsByPath.set(pathStack.join(">"), node.selectionSet);
849
+ }
850
+ },
851
+ leave() {
852
+ pathStack.pop();
853
+ }
854
+ }
855
+ });
856
+ return { selectionSetsByPath, fragments };
857
+ }
858
+ function intersectProvidedSelections(providedSelections, originalSelectionSet, fragments) {
859
+ if (!originalSelectionSet) {
860
+ return [];
861
+ }
862
+ const providedFieldsByName = /* @__PURE__ */ new Map();
863
+ const otherProvided = [];
864
+ for (const provided of providedSelections) {
865
+ if (provided.kind === Kind.FIELD) {
866
+ providedFieldsByName.set(provided.name.value, provided);
867
+ } else {
868
+ otherProvided.push(provided);
869
+ }
870
+ }
871
+ const result = [];
872
+ const seenFieldNames = /* @__PURE__ */ new Set();
873
+ const seenFragmentNames = /* @__PURE__ */ new Set();
874
+ collectMatchingOriginalSelections(
875
+ originalSelectionSet,
876
+ providedFieldsByName,
877
+ fragments,
878
+ seenFieldNames,
879
+ seenFragmentNames,
880
+ result
881
+ );
882
+ return [...result, ...otherProvided];
883
+ }
884
+ function collectMatchingOriginalSelections(selectionSet, providedFieldsByName, fragments, seenFieldNames, seenFragmentNames, result) {
885
+ for (const sel of selectionSet.selections) {
886
+ if (sel.kind === Kind.FIELD) {
887
+ const provided = providedFieldsByName.get(sel.name.value);
888
+ if (!provided) {
889
+ continue;
890
+ }
891
+ const dedupKey = sel.alias?.value ?? sel.name.value;
892
+ if (seenFieldNames.has(dedupKey)) {
893
+ continue;
894
+ }
895
+ seenFieldNames.add(dedupKey);
896
+ if (provided.selectionSet && sel.selectionSet) {
897
+ const nested = intersectProvidedSelections(
898
+ provided.selectionSet.selections,
899
+ sel.selectionSet,
900
+ fragments
901
+ );
902
+ if (!nested.length) {
903
+ continue;
904
+ }
905
+ result.push({
906
+ ...sel,
907
+ selectionSet: {
908
+ kind: Kind.SELECTION_SET,
909
+ selections: nested
910
+ }
911
+ });
912
+ } else {
913
+ result.push(sel);
914
+ }
915
+ } else if (sel.kind === Kind.INLINE_FRAGMENT && sel.selectionSet) {
916
+ collectMatchingOriginalSelections(
917
+ sel.selectionSet,
918
+ providedFieldsByName,
919
+ fragments,
920
+ seenFieldNames,
921
+ seenFragmentNames,
922
+ result
923
+ );
924
+ } else if (sel.kind === Kind.FRAGMENT_SPREAD) {
925
+ const fragmentName = sel.name.value;
926
+ if (seenFragmentNames.has(fragmentName)) {
927
+ continue;
928
+ }
929
+ seenFragmentNames.add(fragmentName);
930
+ const fragmentDef = fragments.get(fragmentName);
931
+ if (!fragmentDef) {
932
+ continue;
933
+ }
934
+ collectMatchingOriginalSelections(
935
+ fragmentDef.selectionSet,
936
+ providedFieldsByName,
937
+ fragments,
938
+ seenFieldNames,
939
+ seenFragmentNames,
940
+ result
941
+ );
942
+ }
943
+ }
944
+ }
764
945
  function filterTypenameFields(selections) {
765
946
  let hasTypeNameField = false;
766
947
  const filteredSelections = selections.filter((selection) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphql-tools/delegate",
3
- "version": "12.0.16",
3
+ "version": "12.0.17-alpha-96a4674a514e93d69c299c07916e06f49feed477",
4
4
  "type": "module",
5
5
  "description": "A set of utils for faster development of GraphQL tools",
6
6
  "repository": {