Skip to content

Commit b919d7e

Browse files
authored
feat(handler): Custom request params parser (#100)
1 parent 5378678 commit b919d7e

File tree

5 files changed

+389
-104
lines changed

5 files changed

+389
-104
lines changed

README.md

+72
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,78 @@ console.log('Listening to port 4000');
725725

726726
</details>
727727

728+
<details id="graphql-upload-http">
729+
<summary><a href="#graphql-upload-http">🔗</a> Server handler usage with <a href="https://github.com/jaydenseric/graphql-upload">graphql-upload</a> and <a href="https://nodejs.org/api/http.html">http</a></summary>
730+
731+
```js
732+
import http from 'http';
733+
import { createHandler } from 'graphql-http/lib/use/http';
734+
import processRequest from 'graphql-upload/processRequest.mjs'; // yarn add graphql-upload
735+
import { schema } from './my-graphql';
736+
737+
const handler = createHandler({
738+
schema,
739+
async parseRequestParams(req) {
740+
const params = await processRequest(req.raw, req.context.res);
741+
if (Array.isArray(params)) {
742+
throw new Error('Batching is not supported');
743+
}
744+
return {
745+
...params,
746+
// variables must be an object as per the GraphQL over HTTP spec
747+
variables: Object(params.variables),
748+
};
749+
},
750+
});
751+
752+
const server = http.createServer((req, res) => {
753+
if (req.url.startsWith('/graphql')) {
754+
handler(req, res);
755+
} else {
756+
res.writeHead(404).end();
757+
}
758+
});
759+
760+
server.listen(4000);
761+
console.log('Listening to port 4000');
762+
```
763+
764+
</details>
765+
766+
<details id="graphql-upload-express">
767+
<summary><a href="#graphql-upload-express">🔗</a> Server handler usage with <a href="https://github.com/jaydenseric/graphql-upload">graphql-upload</a> and <a href="https://expressjs.com/">express</a></summary>
768+
769+
```js
770+
import express from 'express'; // yarn add express
771+
import { createHandler } from 'graphql-http/lib/use/express';
772+
import processRequest from 'graphql-upload/processRequest.mjs'; // yarn add graphql-upload
773+
import { schema } from './my-graphql';
774+
775+
const app = express();
776+
app.all(
777+
'/graphql',
778+
createHandler({
779+
schema,
780+
async parseRequestParams(req) {
781+
const params = await processRequest(req.raw, req.context.res);
782+
if (Array.isArray(params)) {
783+
throw new Error('Batching is not supported');
784+
}
785+
return {
786+
...params,
787+
// variables must be an object as per the GraphQL over HTTP spec
788+
variables: Object(params.variables),
789+
};
790+
},
791+
}),
792+
);
793+
794+
app.listen({ port: 4000 });
795+
console.log('Listening to port 4000');
796+
```
797+
798+
</details>
799+
728800
<details id="audit-jest">
729801
<summary><a href="#audit-jest">🔗</a> Audit for servers usage in <a href="https://jestjs.io">Jest</a> environment</summary>
730802

docs/interfaces/handler.HandlerOptions.md

+11
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
- [onOperation](handler.HandlerOptions.md#onoperation)
2424
- [onSubscribe](handler.HandlerOptions.md#onsubscribe)
2525
- [parse](handler.HandlerOptions.md#parse)
26+
- [parseRequestParams](handler.HandlerOptions.md#parserequestparams)
2627
- [rootValue](handler.HandlerOptions.md#rootvalue)
2728
- [schema](handler.HandlerOptions.md#schema)
2829
- [validate](handler.HandlerOptions.md#validate)
@@ -203,6 +204,16 @@ GraphQL parse function allowing you to apply a custom parser.
203204

204205
___
205206

207+
### parseRequestParams
208+
209+
`Optional` **parseRequestParams**: [`ParseRequestParams`](../modules/handler.md#parserequestparams)<`RequestRaw`, `RequestContext`\>
210+
211+
The request parser for an incoming GraphQL request.
212+
213+
Read more about it in [ParseRequestParams](../modules/handler.md#parserequestparams).
214+
215+
___
216+
206217
### rootValue
207218

208219
`Optional` **rootValue**: `unknown`

docs/modules/handler.md

+43
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [Handler](handler.md#handler)
1717
- [OperationArgs](handler.md#operationargs)
1818
- [OperationContext](handler.md#operationcontext)
19+
- [ParseRequestParams](handler.md#parserequestparams)
1920
- [RequestHeaders](handler.md#requestheaders)
2021
- [Response](handler.md#response)
2122
- [ResponseBody](handler.md#responsebody)
@@ -108,6 +109,48 @@ the `context` server option.
108109

109110
___
110111

112+
### ParseRequestParams
113+
114+
Ƭ **ParseRequestParams**<`RequestRaw`, `RequestContext`\>: (`req`: [`Request`](../interfaces/handler.Request.md)<`RequestRaw`, `RequestContext`\>) => `Promise`<[`RequestParams`](../interfaces/common.RequestParams.md) \| [`Response`](handler.md#response) \| `void`\> \| [`RequestParams`](../interfaces/common.RequestParams.md) \| [`Response`](handler.md#response) \| `void`
115+
116+
#### Type parameters
117+
118+
| Name | Type |
119+
| :------ | :------ |
120+
| `RequestRaw` | `unknown` |
121+
| `RequestContext` | `unknown` |
122+
123+
#### Type declaration
124+
125+
▸ (`req`): `Promise`<[`RequestParams`](../interfaces/common.RequestParams.md) \| [`Response`](handler.md#response) \| `void`\> \| [`RequestParams`](../interfaces/common.RequestParams.md) \| [`Response`](handler.md#response) \| `void`
126+
127+
The request parser for an incoming GraphQL request. It parses and validates the
128+
request itself, including the request method and the content-type of the body.
129+
130+
In case you are extending the server to handle more request types, this is the
131+
perfect place to do so.
132+
133+
If an error is thrown, it will be formatted using the provided [FormatError](handler.md#formaterror)
134+
and handled following the spec to be gracefully reported to the client.
135+
136+
Throwing an instance of `Error` will _always_ have the client respond with a `400: Bad Request`
137+
and the error's message in the response body; however, if an instance of `GraphQLError` is thrown,
138+
it will be reported depending on the accepted content-type.
139+
140+
If you return nothing, the default parser will be used instead.
141+
142+
##### Parameters
143+
144+
| Name | Type |
145+
| :------ | :------ |
146+
| `req` | [`Request`](../interfaces/handler.Request.md)<`RequestRaw`, `RequestContext`\> |
147+
148+
##### Returns
149+
150+
`Promise`<[`RequestParams`](../interfaces/common.RequestParams.md) \| [`Response`](handler.md#response) \| `void`\> \| [`RequestParams`](../interfaces/common.RequestParams.md) \| [`Response`](handler.md#response) \| `void`
151+
152+
___
153+
111154
### RequestHeaders
112155

113156
Ƭ **RequestHeaders**: { `[key: string]`: `string` \| `string`[] \| `undefined`; `set-cookie?`: `string` \| `string`[] } \| { `get`: (`key`: `string`) => `string` \| ``null`` }

src/__tests__/handler.ts

+113
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,116 @@ it('should respect plain errors toJSON implementation', async () => {
242242
}
243243
`);
244244
});
245+
246+
it('should use the custom request params parser', async () => {
247+
const server = startTServer({
248+
parseRequestParams() {
249+
return {
250+
query: '{ hello }',
251+
};
252+
},
253+
});
254+
255+
const url = new URL(server.url);
256+
url.searchParams.set('query', '{ __typename }');
257+
const res = await fetch(url.toString(), {
258+
// different methods and content-types are not disallowed by the spec
259+
method: 'PUT',
260+
headers: { 'content-type': 'application/lol' },
261+
});
262+
263+
await expect(res.json()).resolves.toMatchInlineSnapshot(`
264+
{
265+
"data": {
266+
"hello": "world",
267+
},
268+
}
269+
`);
270+
});
271+
272+
it('should use the response returned from the custom request params parser', async () => {
273+
const server = startTServer({
274+
parseRequestParams() {
275+
return [
276+
'Hello',
277+
{ status: 200, statusText: 'OK', headers: { 'x-hi': 'there' } },
278+
];
279+
},
280+
});
281+
282+
const url = new URL(server.url);
283+
url.searchParams.set('query', '{ __typename }');
284+
const res = await fetch(url.toString());
285+
286+
expect(res.ok).toBeTruthy();
287+
expect(res.headers.get('x-hi')).toBe('there');
288+
await expect(res.text()).resolves.toBe('Hello');
289+
});
290+
291+
it('should report thrown Error from custom request params parser', async () => {
292+
const server = startTServer({
293+
parseRequestParams() {
294+
throw new Error('Wrong.');
295+
},
296+
});
297+
298+
const url = new URL(server.url);
299+
url.searchParams.set('query', '{ __typename }');
300+
const res = await fetch(url.toString());
301+
302+
expect(res.status).toBe(400);
303+
await expect(res.json()).resolves.toMatchInlineSnapshot(`
304+
{
305+
"errors": [
306+
{
307+
"message": "Wrong.",
308+
},
309+
],
310+
}
311+
`);
312+
});
313+
314+
it('should report thrown GraphQLError from custom request params parser', async () => {
315+
const server = startTServer({
316+
parseRequestParams() {
317+
throw new GraphQLError('Wronger.');
318+
},
319+
});
320+
321+
const url = new URL(server.url);
322+
url.searchParams.set('query', '{ __typename }');
323+
const res = await fetch(url.toString(), {
324+
headers: { accept: 'application/json' },
325+
});
326+
327+
expect(res.status).toBe(200);
328+
await expect(res.json()).resolves.toMatchInlineSnapshot(`
329+
{
330+
"errors": [
331+
{
332+
"message": "Wronger.",
333+
},
334+
],
335+
}
336+
`);
337+
});
338+
339+
it('should use the default if nothing is returned from the custom request params parser', async () => {
340+
const server = startTServer({
341+
parseRequestParams() {
342+
return;
343+
},
344+
});
345+
346+
const url = new URL(server.url);
347+
url.searchParams.set('query', '{ hello }');
348+
const res = await fetch(url.toString());
349+
350+
await expect(res.json()).resolves.toMatchInlineSnapshot(`
351+
{
352+
"data": {
353+
"hello": "world",
354+
},
355+
}
356+
`);
357+
});

0 commit comments

Comments
 (0)