Skip to content

Commit

Permalink
feat(handler): Custom request params parser (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
enisdenjo committed Jul 8, 2023
1 parent 5378678 commit b919d7e
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 104 deletions.
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,78 @@ console.log('Listening to port 4000');

</details>

<details id="graphql-upload-http">
<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>

```js
import http from 'http';
import { createHandler } from 'graphql-http/lib/use/http';
import processRequest from 'graphql-upload/processRequest.mjs'; // yarn add graphql-upload
import { schema } from './my-graphql';

const handler = createHandler({
schema,
async parseRequestParams(req) {
const params = await processRequest(req.raw, req.context.res);
if (Array.isArray(params)) {
throw new Error('Batching is not supported');
}
return {
...params,
// variables must be an object as per the GraphQL over HTTP spec
variables: Object(params.variables),
};
},
});

const server = http.createServer((req, res) => {
if (req.url.startsWith('/graphql')) {
handler(req, res);
} else {
res.writeHead(404).end();
}
});

server.listen(4000);
console.log('Listening to port 4000');
```

</details>

<details id="graphql-upload-express">
<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>

```js
import express from 'express'; // yarn add express
import { createHandler } from 'graphql-http/lib/use/express';
import processRequest from 'graphql-upload/processRequest.mjs'; // yarn add graphql-upload
import { schema } from './my-graphql';

const app = express();
app.all(
'/graphql',
createHandler({
schema,
async parseRequestParams(req) {
const params = await processRequest(req.raw, req.context.res);
if (Array.isArray(params)) {
throw new Error('Batching is not supported');
}
return {
...params,
// variables must be an object as per the GraphQL over HTTP spec
variables: Object(params.variables),
};
},
}),
);

app.listen({ port: 4000 });
console.log('Listening to port 4000');
```

</details>

<details id="audit-jest">
<summary><a href="#audit-jest">🔗</a> Audit for servers usage in <a href="https://jestjs.io">Jest</a> environment</summary>

Expand Down
11 changes: 11 additions & 0 deletions docs/interfaces/handler.HandlerOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- [onOperation](handler.HandlerOptions.md#onoperation)
- [onSubscribe](handler.HandlerOptions.md#onsubscribe)
- [parse](handler.HandlerOptions.md#parse)
- [parseRequestParams](handler.HandlerOptions.md#parserequestparams)
- [rootValue](handler.HandlerOptions.md#rootvalue)
- [schema](handler.HandlerOptions.md#schema)
- [validate](handler.HandlerOptions.md#validate)
Expand Down Expand Up @@ -203,6 +204,16 @@ GraphQL parse function allowing you to apply a custom parser.

___

### parseRequestParams

`Optional` **parseRequestParams**: [`ParseRequestParams`](../modules/handler.md#parserequestparams)<`RequestRaw`, `RequestContext`\>

The request parser for an incoming GraphQL request.

Read more about it in [ParseRequestParams](../modules/handler.md#parserequestparams).

___

### rootValue

`Optional` **rootValue**: `unknown`
Expand Down
43 changes: 43 additions & 0 deletions docs/modules/handler.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [Handler](handler.md#handler)
- [OperationArgs](handler.md#operationargs)
- [OperationContext](handler.md#operationcontext)
- [ParseRequestParams](handler.md#parserequestparams)
- [RequestHeaders](handler.md#requestheaders)
- [Response](handler.md#response)
- [ResponseBody](handler.md#responsebody)
Expand Down Expand Up @@ -108,6 +109,48 @@ the `context` server option.

___

### ParseRequestParams

Ƭ **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`

#### Type parameters

| Name | Type |
| :------ | :------ |
| `RequestRaw` | `unknown` |
| `RequestContext` | `unknown` |

#### Type declaration

▸ (`req`): `Promise`<[`RequestParams`](../interfaces/common.RequestParams.md) \| [`Response`](handler.md#response) \| `void`\> \| [`RequestParams`](../interfaces/common.RequestParams.md) \| [`Response`](handler.md#response) \| `void`

The request parser for an incoming GraphQL request. It parses and validates the
request itself, including the request method and the content-type of the body.

In case you are extending the server to handle more request types, this is the
perfect place to do so.

If an error is thrown, it will be formatted using the provided [FormatError](handler.md#formaterror)
and handled following the spec to be gracefully reported to the client.

Throwing an instance of `Error` will _always_ have the client respond with a `400: Bad Request`
and the error's message in the response body; however, if an instance of `GraphQLError` is thrown,
it will be reported depending on the accepted content-type.

If you return nothing, the default parser will be used instead.

##### Parameters

| Name | Type |
| :------ | :------ |
| `req` | [`Request`](../interfaces/handler.Request.md)<`RequestRaw`, `RequestContext`\> |

##### Returns

`Promise`<[`RequestParams`](../interfaces/common.RequestParams.md) \| [`Response`](handler.md#response) \| `void`\> \| [`RequestParams`](../interfaces/common.RequestParams.md) \| [`Response`](handler.md#response) \| `void`

___

### RequestHeaders

Ƭ **RequestHeaders**: { `[key: string]`: `string` \| `string`[] \| `undefined`; `set-cookie?`: `string` \| `string`[] } \| { `get`: (`key`: `string`) => `string` \| ``null`` }
Expand Down
113 changes: 113 additions & 0 deletions src/__tests__/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,116 @@ it('should respect plain errors toJSON implementation', async () => {
}
`);
});

it('should use the custom request params parser', async () => {
const server = startTServer({
parseRequestParams() {
return {
query: '{ hello }',
};
},
});

const url = new URL(server.url);
url.searchParams.set('query', '{ __typename }');
const res = await fetch(url.toString(), {
// different methods and content-types are not disallowed by the spec
method: 'PUT',
headers: { 'content-type': 'application/lol' },
});

await expect(res.json()).resolves.toMatchInlineSnapshot(`
{
"data": {
"hello": "world",
},
}
`);
});

it('should use the response returned from the custom request params parser', async () => {
const server = startTServer({
parseRequestParams() {
return [
'Hello',
{ status: 200, statusText: 'OK', headers: { 'x-hi': 'there' } },
];
},
});

const url = new URL(server.url);
url.searchParams.set('query', '{ __typename }');
const res = await fetch(url.toString());

expect(res.ok).toBeTruthy();
expect(res.headers.get('x-hi')).toBe('there');
await expect(res.text()).resolves.toBe('Hello');
});

it('should report thrown Error from custom request params parser', async () => {
const server = startTServer({
parseRequestParams() {
throw new Error('Wrong.');
},
});

const url = new URL(server.url);
url.searchParams.set('query', '{ __typename }');
const res = await fetch(url.toString());

expect(res.status).toBe(400);
await expect(res.json()).resolves.toMatchInlineSnapshot(`
{
"errors": [
{
"message": "Wrong.",
},
],
}
`);
});

it('should report thrown GraphQLError from custom request params parser', async () => {
const server = startTServer({
parseRequestParams() {
throw new GraphQLError('Wronger.');
},
});

const url = new URL(server.url);
url.searchParams.set('query', '{ __typename }');
const res = await fetch(url.toString(), {
headers: { accept: 'application/json' },
});

expect(res.status).toBe(200);
await expect(res.json()).resolves.toMatchInlineSnapshot(`
{
"errors": [
{
"message": "Wronger.",
},
],
}
`);
});

it('should use the default if nothing is returned from the custom request params parser', async () => {
const server = startTServer({
parseRequestParams() {
return;
},
});

const url = new URL(server.url);
url.searchParams.set('query', '{ hello }');
const res = await fetch(url.toString());

await expect(res.json()).resolves.toMatchInlineSnapshot(`
{
"data": {
"hello": "world",
},
}
`);
});
Loading

0 comments on commit b919d7e

Please sign in to comment.