Skip to content

Commit

Permalink
feat(handler): onSubscribe can return an ExecutionResult for imme…
Browse files Browse the repository at this point in the history
…diate result response
  • Loading branch information
enisdenjo committed Aug 10, 2022
1 parent 5ce6841 commit 0dcaf89
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 9 deletions.
16 changes: 10 additions & 6 deletions docs/interfaces/HandlerOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@ ___

### onOperation

`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`\>\>\>
`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)\>

#### Type declaration

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

Executed after the operation call resolves.

Expand All @@ -117,21 +117,25 @@ further execution.

##### Returns

`void` \| [`Response`](../README.md#response) \| `ExecutionResult`<`ObjMap`<`unknown`\>, `ObjMap`<`unknown`\>\> \| `Promise`<`void` \| [`Response`](../README.md#response) \| `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)\>

___

### onSubscribe

`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`\>
`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`\>

#### Type declaration

▸ (`req`, `params`): `void` \| readonly `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| readonly `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
▸ (`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`\>

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

If you return `ExecutionResult` from the callback, it will be used
directly for responding to the request. Useful for implementing a response
cache.

If you return `ExecutionArgs` from the callback, it will be used instead of
trying to build one internally. In this case, you are responsible for providing
a ready set of arguments which will be directly plugged in the operation execution.
Expand Down Expand Up @@ -159,7 +163,7 @@ further execution.

##### Returns

`void` \| readonly `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs` \| `Promise`<`void` \| readonly `GraphQLError`[] \| [`Response`](../README.md#response) \| `ExecutionArgs`\>
`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`\>

___

Expand Down
18 changes: 18 additions & 0 deletions src/__tests__/handler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { jest } from '@jest/globals';
import { GraphQLError } from 'graphql';
import fetch from 'node-fetch';
import { startTServer } from './utils/tserver';
Expand Down Expand Up @@ -31,3 +32,20 @@ it('should report graphql errors returned from onSubscribe', async () => {
expect(res.status).toBe(400);
expect(res.json()).resolves.toEqual({ errors: [{ message: 'Woah!' }] });
});

it('should respond with result returned from onSubscribe', async () => {
const onOperationFn = jest.fn();
const server = startTServer({
onSubscribe: () => {
return { data: { __typename: 'Query' } };
},
onOperation: onOperationFn,
});

const url = new URL(server.url);
url.searchParams.set('query', '{ __typename }');
const res = await fetch(url.toString());
expect(res.status).toBe(200);
expect(res.json()).resolves.toEqual({ data: { __typename: 'Query' } });
expect(onOperationFn).not.toBeCalled(); // early result, operation did not happen
});
29 changes: 27 additions & 2 deletions src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
GraphQLError,
} from 'graphql';
import { isResponse, Request, RequestParams, Response } from './common';
import { areGraphQLErrors } from './utils';
import { areGraphQLErrors, isExecutionResult } from './utils';

/**
* A concrete GraphQL execution context value type.
Expand Down Expand Up @@ -96,6 +96,10 @@ export interface HandlerOptions<RawRequest = unknown> {
* The subscribe callback executed right after processing the request
* before proceeding with the GraphQL operation execution.
*
* If you return `ExecutionResult` from the callback, it will be used
* directly for responding to the request. Useful for implementing a response
* cache.
*
* If you return `ExecutionArgs` from the callback, it will be used instead of
* trying to build one internally. In this case, you are responsible for providing
* a ready set of arguments which will be directly plugged in the operation execution.
Expand All @@ -118,7 +122,14 @@ export interface HandlerOptions<RawRequest = unknown> {
req: Request<RawRequest>,
params: RequestParams,
) =>
| Promise<ExecutionArgs | readonly GraphQLError[] | Response | void>
| Promise<
| ExecutionResult
| ExecutionArgs
| readonly GraphQLError[]
| Response
| void
>
| ExecutionResult
| ExecutionArgs
| readonly GraphQLError[]
| Response
Expand Down Expand Up @@ -387,6 +398,20 @@ export function createHandler<RawRequest = unknown>(
let args: ExecutionArgs;
const maybeResErrsOrArgs = await onSubscribe?.(req, params);
if (isResponse(maybeResErrsOrArgs)) return maybeResErrsOrArgs;
else if (isExecutionResult(maybeResErrsOrArgs))
return [
JSON.stringify(maybeResErrsOrArgs),
{
status: 200,
statusText: 'OK',
headers: {
'content-type':
acceptedMediaType === 'application/json'
? 'application/json; charset=utf-8'
: 'application/graphql+json; charset=utf-8',
},
},
];
else if (areGraphQLErrors(maybeResErrsOrArgs))
return [
JSON.stringify({ errors: maybeResErrsOrArgs }),
Expand Down
9 changes: 8 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*
*/

import type { GraphQLError } from 'graphql';
import type { ExecutionResult, GraphQLError } from 'graphql';

/** @private */
export function isObject(val: unknown): val is Record<PropertyKey, unknown> {
Expand All @@ -21,3 +21,10 @@ export function areGraphQLErrors(obj: unknown): obj is readonly GraphQLError[] {
obj.every((ob) => 'message' in ob)
);
}

/** @private */
export function isExecutionResult(val: unknown): val is ExecutionResult {
return (
isObject(val) && ('data' in val || 'errors' in val || 'extensions' in val)
);
}

0 comments on commit 0dcaf89

Please sign in to comment.