Skip to content

Commit 8ff2e47

Browse files
Expose the query plan in graphql extensions for Rust QP (#1739)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 9cfe2a5 commit 8ff2e47

File tree

8 files changed

+263
-4
lines changed

8 files changed

+263
-4
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@graphql-hive/gateway-runtime': patch
3+
---
4+
5+
dependencies updates:
6+
7+
- Updated dependency [`@graphql-yoga/plugin-apollo-usage-report@^0.12.0` ↗︎](https://www.npmjs.com/package/@graphql-yoga/plugin-apollo-usage-report/v/0.12.0) (from `^0.11.2`, in `dependencies`)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@graphql-hive/router-runtime': patch
3+
---
4+
5+
dependencies updates:
6+
7+
- Updated dependency [`@graphql-hive/router-query-planner@^0.0.6` ↗︎](https://www.npmjs.com/package/@graphql-hive/router-query-planner/v/0.0.6) (from `^0.0.4`, in `dependencies`)

.changeset/violet-jokes-enjoy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-hive/router-runtime': patch
3+
---
4+
5+
Expose the query plan by using the `useQueryPlan` plugin

packages/router-runtime/src/handler.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import {
1515
} from '@whatwg-node/promise-helpers';
1616
import { BREAK, DocumentNode, visit } from 'graphql';
1717
import { executeQueryPlan } from './executor';
18-
import { getLazyFactory, getLazyValue } from './utils';
18+
import {
19+
getLazyFactory,
20+
getLazyValue,
21+
queryPlanForExecutionRequestContext,
22+
} from './utils';
1923

2024
export function unifiedGraphHandler(
2125
opts: UnifiedGraphHandlerOpts,
@@ -83,8 +87,13 @@ export function unifiedGraphHandler(
8387
executionRequest.document,
8488
executionRequest.operationName || null,
8589
),
86-
(queryPlan) =>
87-
executeQueryPlan({
90+
(queryPlan) => {
91+
queryPlanForExecutionRequestContext.set(
92+
// setter like getter
93+
executionRequest.context || executionRequest.document,
94+
queryPlan,
95+
);
96+
return executeQueryPlan({
8897
supergraphSchema,
8998
executionRequest,
9099
onSubgraphExecute(subgraphName, executionRequest) {
@@ -128,7 +137,8 @@ export function unifiedGraphHandler(
128137
return opts.onSubgraphExecute(subgraphName, executionRequest);
129138
},
130139
queryPlan,
131-
}),
140+
});
141+
},
132142
);
133143
},
134144
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './handler';
2+
export * from './useQueryPlan';
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { GatewayPlugin } from '@graphql-hive/gateway-runtime';
2+
import type { QueryPlan } from '@graphql-hive/router-query-planner';
3+
import { isAsyncIterable } from '@graphql-tools/utils';
4+
import { queryPlanForExecutionRequestContext } from './utils';
5+
6+
export interface QueryPlanOptions {
7+
/** Callback when the query plan has been successfuly generated. */
8+
onQueryPlan?(queryPlan: QueryPlan): void;
9+
/** Exposing the query plan inside the GraphQL result extensions. */
10+
expose?: boolean | ((request: Request) => boolean);
11+
}
12+
13+
export function useQueryPlan(opts: QueryPlanOptions = {}): GatewayPlugin {
14+
const { expose, onQueryPlan } = opts;
15+
return {
16+
onExecute({ context, args }) {
17+
return {
18+
onExecuteDone({ result, setResult }) {
19+
const queryPlan = queryPlanForExecutionRequestContext.get(
20+
// getter like setter
21+
context || args.document,
22+
);
23+
onQueryPlan?.(queryPlan!);
24+
const shouldExpose =
25+
typeof expose === 'function' ? expose(context.request) : expose;
26+
if (shouldExpose && !isAsyncIterable(result)) {
27+
setResult({
28+
...result,
29+
extensions: {
30+
...result.extensions,
31+
queryPlan,
32+
},
33+
});
34+
}
35+
},
36+
};
37+
},
38+
};
39+
}

packages/router-runtime/src/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import type { QueryPlan } from '@graphql-hive/router-query-planner';
12
import {
23
handleMaybePromise,
34
type MaybePromise,
45
} from '@whatwg-node/promise-helpers';
56

7+
export const queryPlanForExecutionRequestContext = new WeakMap<
8+
any,
9+
QueryPlan
10+
>();
11+
612
export function getLazyPromise<T>(
713
factory: () => MaybePromise<T>,
814
): () => MaybePromise<T> {
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { createGatewayTester } from '@graphql-hive/gateway-testing';
2+
import { QueryPlan } from '@graphql-hive/router-query-planner';
3+
import { expect, it } from 'vitest';
4+
import { unifiedGraphHandler, useQueryPlan } from '../src/index';
5+
6+
it('should callback when the query plan is available', async () => {
7+
let queryPlan!: QueryPlan;
8+
await using gw = createGatewayTester({
9+
unifiedGraphHandler,
10+
plugins: () => [
11+
useQueryPlan({
12+
onQueryPlan(_queryPlan) {
13+
queryPlan = _queryPlan;
14+
},
15+
}),
16+
],
17+
subgraphs: [
18+
{
19+
name: 'upstream',
20+
schema: {
21+
typeDefs: /* GraphQL */ `
22+
type Query {
23+
hello: String
24+
}
25+
`,
26+
resolvers: {
27+
Query: {
28+
hello: () => 'world',
29+
},
30+
},
31+
},
32+
},
33+
],
34+
});
35+
36+
await gw.execute({
37+
query: /* GraphQL */ `
38+
{
39+
hello
40+
}
41+
`,
42+
});
43+
44+
expect(queryPlan).toMatchInlineSnapshot(`
45+
{
46+
"kind": "QueryPlan",
47+
"node": {
48+
"kind": "Fetch",
49+
"operation": "{hello}",
50+
"operationKind": "query",
51+
"serviceName": "upstream",
52+
},
53+
}
54+
`);
55+
});
56+
57+
it('should include the query plan in result extensions when exposed', async () => {
58+
await using gw = createGatewayTester({
59+
unifiedGraphHandler,
60+
plugins: () => [
61+
useQueryPlan({
62+
expose: true,
63+
}),
64+
],
65+
subgraphs: [
66+
{
67+
name: 'upstream',
68+
schema: {
69+
typeDefs: /* GraphQL */ `
70+
type Query {
71+
hello: String
72+
}
73+
`,
74+
resolvers: {
75+
Query: {
76+
hello: () => 'world',
77+
},
78+
},
79+
},
80+
},
81+
],
82+
});
83+
84+
await expect(
85+
gw.execute({
86+
query: /* GraphQL */ `
87+
{
88+
hello
89+
}
90+
`,
91+
}),
92+
).resolves.toMatchInlineSnapshot(`
93+
{
94+
"data": {
95+
"hello": "world",
96+
},
97+
"extensions": {
98+
"queryPlan": {
99+
"kind": "QueryPlan",
100+
"node": {
101+
"kind": "Fetch",
102+
"operation": "{hello}",
103+
"operationKind": "query",
104+
"serviceName": "upstream",
105+
},
106+
},
107+
},
108+
}
109+
`);
110+
});
111+
112+
it('should include the query plan in result extensions when expose returns true', async () => {
113+
await using gw = createGatewayTester({
114+
unifiedGraphHandler,
115+
plugins: () => [
116+
useQueryPlan({
117+
expose: (req) => req.headers.get('x-expose-query-plan') === 'true',
118+
}),
119+
],
120+
subgraphs: [
121+
{
122+
name: 'upstream',
123+
schema: {
124+
typeDefs: /* GraphQL */ `
125+
type Query {
126+
hello: String
127+
}
128+
`,
129+
resolvers: {
130+
Query: {
131+
hello: () => 'world',
132+
},
133+
},
134+
},
135+
},
136+
],
137+
});
138+
139+
await expect(
140+
gw.execute({
141+
query: /* GraphQL */ `
142+
{
143+
hello
144+
}
145+
`,
146+
}),
147+
).resolves.toMatchInlineSnapshot(`
148+
{
149+
"data": {
150+
"hello": "world",
151+
},
152+
}
153+
`);
154+
155+
await expect(
156+
gw.execute({
157+
headers: {
158+
'x-expose-query-plan': 'true',
159+
},
160+
query: /* GraphQL */ `
161+
{
162+
hello
163+
}
164+
`,
165+
}),
166+
).resolves.toMatchInlineSnapshot(`
167+
{
168+
"data": {
169+
"hello": "world",
170+
},
171+
"extensions": {
172+
"queryPlan": {
173+
"kind": "QueryPlan",
174+
"node": {
175+
"kind": "Fetch",
176+
"operation": "{hello}",
177+
"operationKind": "query",
178+
"serviceName": "upstream",
179+
},
180+
},
181+
},
182+
}
183+
`);
184+
});

0 commit comments

Comments
 (0)