Skip to content

Commit 0dcaf89

Browse files
committed
feat(handler): onSubscribe can return an ExecutionResult for immediate result response
1 parent 5ce6841 commit 0dcaf89

File tree

4 files changed

+63
-9
lines changed

4 files changed

+63
-9
lines changed

docs/interfaces/HandlerOptions.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ ___
8989

9090
### onOperation
9191

92-
`Optional` **onOperation**: (`req`: [`Request`](Request.md)<`RawRequest`\>, `args`: `ExecutionArgs`, `result`: `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\>) => `void` \| [`Response`](../README.md#response) \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| `Promise`<`void` \| [`Response`](../README.md#response) \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\>\>
92+
`Optional` **onOperation**: (`req`: [`Request`](Request.md)<`RawRequest`\>, `args`: `ExecutionArgs`, `result`: `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\>) => `void` \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| [`Response`](../README.md#response) \| `Promise`<`void` \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| [`Response`](../README.md#response)\>
9393

9494
#### Type declaration
9595

96-
▸ (`req`, `args`, `result`): `void` \| [`Response`](../README.md#response) \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| `Promise`<`void` \| [`Response`](../README.md#response) \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\>\>
96+
▸ (`req`, `args`, `result`): `void` \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| [`Response`](../README.md#response) \| `Promise`<`void` \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| [`Response`](../README.md#response)\>
9797

9898
Executed after the operation call resolves.
9999

@@ -117,21 +117,25 @@ further execution.
117117

118118
##### Returns
119119

120-
`void` \| [`Response`](../README.md#response) \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| `Promise`<`void` \| [`Response`](../README.md#response) \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\>\>
120+
`void` \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| [`Response`](../README.md#response) \| `Promise`<`void` \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| [`Response`](../README.md#response)\>
121121

122122
___
123123

124124
### onSubscribe
125125

126-
`Optional` **onSubscribe**: (`req`: [`Request`](Request.md)<`RawRequest`\>, `params`: [`RequestParams`](RequestParams.md)) => `void` \| readonly `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| readonly `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
126+
`Optional` **onSubscribe**: (`req`: [`Request`](Request.md)<`RawRequest`\>, `params`: [`RequestParams`](RequestParams.md)) => `void` \| readonly `GraphQLError`[] \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| readonly `GraphQLError`[] \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
127127

128128
#### Type declaration
129129

130-
▸ (`req`, `params`): `void` \| readonly `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| readonly `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
130+
▸ (`req`, `params`): `void` \| readonly `GraphQLError`[] \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| readonly `GraphQLError`[] \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
131131

132132
The subscribe callback executed right after processing the request
133133
before proceeding with the GraphQL operation execution.
134134

135+
If you return `ExecutionResult` from the callback, it will be used
136+
directly for responding to the request. Useful for implementing a response
137+
cache.
138+
135139
If you return `ExecutionArgs` from the callback, it will be used instead of
136140
trying to build one internally. In this case, you are responsible for providing
137141
a ready set of arguments which will be directly plugged in the operation execution.
@@ -159,7 +163,7 @@ further execution.
159163

160164
##### Returns
161165

162-
`void` \| readonly `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| readonly `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
166+
`void` \| readonly `GraphQLError`[] \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| readonly `GraphQLError`[] \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
163167

164168
___
165169

src/__tests__/handler.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { jest } from '@jest/globals';
12
import { GraphQLError } from 'graphql';
23
import fetch from 'node-fetch';
34
import { startTServer } from './utils/tserver';
@@ -31,3 +32,20 @@ it('should report graphql errors returned from onSubscribe', async () => {
3132
expect(res.status).toBe(400);
3233
expect(res.json()).resolves.toEqual({ errors: [{ message: 'Woah!' }] });
3334
});
35+
36+
it('should respond with result returned from onSubscribe', async () => {
37+
const onOperationFn = jest.fn();
38+
const server = startTServer({
39+
onSubscribe: () => {
40+
return { data: { __typename: 'Query' } };
41+
},
42+
onOperation: onOperationFn,
43+
});
44+
45+
const url = new URL(server.url);
46+
url.searchParams.set('query', '{ __typename }');
47+
const res = await fetch(url.toString());
48+
expect(res.status).toBe(200);
49+
expect(res.json()).resolves.toEqual({ data: { __typename: 'Query' } });
50+
expect(onOperationFn).not.toBeCalled(); // early result, operation did not happen
51+
});

src/handler.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
GraphQLError,
1818
} from 'graphql';
1919
import { isResponse, Request, RequestParams, Response } from './common';
20-
import { areGraphQLErrors } from './utils';
20+
import { areGraphQLErrors, isExecutionResult } from './utils';
2121

2222
/**
2323
* A concrete GraphQL execution context value type.
@@ -96,6 +96,10 @@ export interface HandlerOptions<RawRequest = unknown> {
9696
* The subscribe callback executed right after processing the request
9797
* before proceeding with the GraphQL operation execution.
9898
*
99+
* If you return `ExecutionResult` from the callback, it will be used
100+
* directly for responding to the request. Useful for implementing a response
101+
* cache.
102+
*
99103
* If you return `ExecutionArgs` from the callback, it will be used instead of
100104
* trying to build one internally. In this case, you are responsible for providing
101105
* a ready set of arguments which will be directly plugged in the operation execution.
@@ -118,7 +122,14 @@ export interface HandlerOptions<RawRequest = unknown> {
118122
req: Request<RawRequest>,
119123
params: RequestParams,
120124
) =>
121-
| Promise<ExecutionArgs | readonly GraphQLError[] | Response | void>
125+
| Promise<
126+
| ExecutionResult
127+
| ExecutionArgs
128+
| readonly GraphQLError[]
129+
| Response
130+
| void
131+
>
132+
| ExecutionResult
122133
| ExecutionArgs
123134
| readonly GraphQLError[]
124135
| Response
@@ -387,6 +398,20 @@ export function createHandler<RawRequest = unknown>(
387398
let args: ExecutionArgs;
388399
const maybeResErrsOrArgs = await onSubscribe?.(req, params);
389400
if (isResponse(maybeResErrsOrArgs)) return maybeResErrsOrArgs;
401+
else if (isExecutionResult(maybeResErrsOrArgs))
402+
return [
403+
JSON.stringify(maybeResErrsOrArgs),
404+
{
405+
status: 200,
406+
statusText: 'OK',
407+
headers: {
408+
'content-type':
409+
acceptedMediaType === 'application/json'
410+
? 'application/json; charset=utf-8'
411+
: 'application/graphql+json; charset=utf-8',
412+
},
413+
},
414+
];
390415
else if (areGraphQLErrors(maybeResErrsOrArgs))
391416
return [
392417
JSON.stringify({ errors: maybeResErrsOrArgs }),

src/utils.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*
55
*/
66

7-
import type { GraphQLError } from 'graphql';
7+
import type { ExecutionResult, GraphQLError } from 'graphql';
88

99
/** @private */
1010
export function isObject(val: unknown): val is Record<PropertyKey, unknown> {
@@ -21,3 +21,10 @@ export function areGraphQLErrors(obj: unknown): obj is readonly GraphQLError[] {
2121
obj.every((ob) => 'message' in ob)
2222
);
2323
}
24+
25+
/** @private */
26+
export function isExecutionResult(val: unknown): val is ExecutionResult {
27+
return (
28+
isObject(val) && ('data' in val || 'errors' in val || 'extensions' in val)
29+
);
30+
}

0 commit comments

Comments
 (0)