Skip to content

Commit 346d2c6

Browse files
committed
Demand Control fixes
1 parent da748e4 commit 346d2c6

File tree

5 files changed

+202
-23
lines changed

5 files changed

+202
-23
lines changed

.changeset/cold-monkeys-approve.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-tools/batch-execute': patch
3+
---
4+
5+
Spread sync errors into an array with the same size of the requests to satisfy underlying DataLoader implementation to throw the error correctly

.changeset/giant-paws-battle.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-hive/gateway-runtime': patch
3+
---
4+
5+
If metadata is included the result with `includeExtensionMetadata`, `cost.esimated` should always be added to the result extensions even if no cost is calculated.

packages/batch-execute/src/createBatchingExecutor.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function createLoadFn(
4949
): PromiseLike<Array<ExecutionResult>> {
5050
if (requests.length === 1 && requests[0]) {
5151
const request = requests[0];
52-
return fakePromise<any>(
52+
return fakePromise(
5353
handleMaybePromise(
5454
() => executor(request),
5555
(result) => [result],
@@ -58,7 +58,7 @@ function createLoadFn(
5858
);
5959
}
6060
const mergedRequests = mergeRequests(requests, extensionsReducer);
61-
return fakePromise<any>(
61+
return fakePromise(
6262
handleMaybePromise(
6363
() => executor(mergedRequests),
6464
(resultBatches) => {
@@ -69,6 +69,7 @@ function createLoadFn(
6969
}
7070
return splitResult(resultBatches, requests.length);
7171
},
72+
(err) => requests.map(() => err),
7273
),
7374
);
7475
};

packages/runtime/src/plugins/useDemandControl.ts

+19-21
Original file line numberDiff line numberDiff line change
@@ -104,33 +104,31 @@ export function useDemandControl<TContext extends Record<string, any>>({
104104
},
105105
onExecutionResult({ result, setResult, context }) {
106106
if (includeExtensionMetadata) {
107-
const costByContext = costByContextMap.get(context);
108-
if (costByContext) {
109-
if (isAsyncIterable(result)) {
110-
setResult(
111-
mapAsyncIterator(result, (value) => ({
112-
...value,
113-
extensions: {
114-
...(value.extensions || {}),
115-
cost: {
116-
estimated: costByContext,
117-
max: maxCost,
118-
},
119-
},
120-
})),
121-
);
122-
} else {
123-
setResult({
124-
...(result || {}),
107+
const costByContext = costByContextMap.get(context) || 0;
108+
if (isAsyncIterable(result)) {
109+
setResult(
110+
mapAsyncIterator(result, (value) => ({
111+
...value,
125112
extensions: {
126-
...(result?.extensions || {}),
113+
...(value.extensions || {}),
127114
cost: {
128115
estimated: costByContext,
129116
max: maxCost,
130117
},
131118
},
132-
});
133-
}
119+
})),
120+
);
121+
} else {
122+
setResult({
123+
...(result || {}),
124+
extensions: {
125+
...(result?.extensions || {}),
126+
cost: {
127+
estimated: costByContext,
128+
max: maxCost,
129+
},
130+
},
131+
});
134132
}
135133
}
136134
},

packages/runtime/tests/demand-control.test.ts

+170
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,11 @@ describe('Demand Control', () => {
753753
path: ['items'],
754754
},
755755
],
756+
extensions: {
757+
cost: {
758+
estimated: 0,
759+
},
760+
},
756761
});
757762
});
758763
it('@listSize(slicingArguments:, requireOneSlicingArgument:false)', async () => {
@@ -1016,4 +1021,169 @@ describe('Demand Control', () => {
10161021
},
10171022
});
10181023
});
1024+
1025+
it('returns cost even if it does not hit the subgraph', async () => {
1026+
const subgraph = buildSubgraphSchema({
1027+
typeDefs: parse(/* GraphQL */ `
1028+
type Query {
1029+
foo: String
1030+
}
1031+
`),
1032+
});
1033+
await using subgraphServer = createYoga({
1034+
schema: subgraph,
1035+
});
1036+
await using gateway = createGatewayRuntime({
1037+
supergraph: await composeLocalSchemasWithApollo([
1038+
{
1039+
name: 'subgraph',
1040+
schema: subgraph,
1041+
url: 'http://subgraph/graphql',
1042+
},
1043+
]),
1044+
plugins: () => [
1045+
// @ts-expect-error TODO: MeshFetch is not compatible with @whatwg-node/server fetch
1046+
useCustomFetch(subgraphServer.fetch),
1047+
useDemandControl({
1048+
includeExtensionMetadata: true,
1049+
}),
1050+
],
1051+
});
1052+
const query = /* GraphQL */ `
1053+
query EmptyQuery {
1054+
__typename
1055+
a: __typename
1056+
}
1057+
`;
1058+
const response = await gateway.fetch('http://localhost:4000/graphql', {
1059+
method: 'POST',
1060+
headers: {
1061+
'Content-Type': 'application/json',
1062+
},
1063+
body: JSON.stringify({ query }),
1064+
});
1065+
const result = await response.json();
1066+
expect(result).toEqual({
1067+
data: {
1068+
__typename: 'Query',
1069+
a: 'Query',
1070+
},
1071+
extensions: {
1072+
cost: {
1073+
estimated: 0,
1074+
},
1075+
},
1076+
});
1077+
});
1078+
1079+
it('handles batched requests', async () => {
1080+
const subgraph = buildSubgraphSchema({
1081+
typeDefs: parse(/* GraphQL */ `
1082+
type Query {
1083+
foo: Foo
1084+
bar: Bar
1085+
}
1086+
1087+
type Foo {
1088+
id: ID
1089+
}
1090+
1091+
type Bar {
1092+
id: ID
1093+
}
1094+
`),
1095+
resolvers: {
1096+
Query: {
1097+
foo: async () => ({ id: 'foo' }),
1098+
bar: async () => ({ id: 'bar' }),
1099+
},
1100+
},
1101+
});
1102+
await using subgraphServer = createYoga({
1103+
schema: subgraph,
1104+
});
1105+
await using gateway = createGatewayRuntime({
1106+
supergraph: await composeLocalSchemasWithApollo([
1107+
{
1108+
name: 'subgraph',
1109+
schema: subgraph,
1110+
url: 'http://subgraph/graphql',
1111+
},
1112+
]),
1113+
plugins: () => [
1114+
// @ts-expect-error TODO: MeshFetch is not compatible with @whatwg-node/server fetch
1115+
useCustomFetch(subgraphServer.fetch),
1116+
useDemandControl({
1117+
includeExtensionMetadata: true,
1118+
maxCost: 1,
1119+
}),
1120+
],
1121+
});
1122+
const query = /* GraphQL */ `
1123+
query FooQuery {
1124+
foo {
1125+
id
1126+
}
1127+
bar {
1128+
id
1129+
}
1130+
}
1131+
`;
1132+
const response = await gateway.fetch('http://localhost:4000/graphql', {
1133+
method: 'POST',
1134+
headers: {
1135+
'Content-Type': 'application/json',
1136+
},
1137+
body: JSON.stringify({ query }),
1138+
});
1139+
const result = await response.json();
1140+
expect(result).toEqual({
1141+
data: {
1142+
foo: null,
1143+
bar: null,
1144+
},
1145+
errors: [
1146+
{
1147+
extensions: {
1148+
code: 'COST_ESTIMATED_TOO_EXPENSIVE',
1149+
cost: {
1150+
estimated: 2,
1151+
max: 1,
1152+
},
1153+
},
1154+
locations: [
1155+
{
1156+
column: 9,
1157+
line: 3,
1158+
},
1159+
],
1160+
message: 'Operation estimated cost 2 exceeded configured maximum 1',
1161+
path: ['foo'],
1162+
},
1163+
{
1164+
extensions: {
1165+
code: 'COST_ESTIMATED_TOO_EXPENSIVE',
1166+
cost: {
1167+
estimated: 2,
1168+
max: 1,
1169+
},
1170+
},
1171+
locations: [
1172+
{
1173+
column: 9,
1174+
line: 6,
1175+
},
1176+
],
1177+
message: 'Operation estimated cost 2 exceeded configured maximum 1',
1178+
path: ['bar'],
1179+
},
1180+
],
1181+
extensions: {
1182+
cost: {
1183+
estimated: 2,
1184+
max: 1,
1185+
},
1186+
},
1187+
});
1188+
});
10191189
});

0 commit comments

Comments
 (0)