Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(handler): Custom request params parser #100

Merged
merged 7 commits into from
Jul 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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