Skip to content

Commit

Permalink
feat(handler): Use Koa (#80)
Browse files Browse the repository at this point in the history
Closes #78
  • Loading branch information
enisdenjo authored Sep 7, 2023
1 parent 893c103 commit 283b453
Show file tree
Hide file tree
Showing 5 changed files with 437 additions and 16 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@
"@types/eventsource": "^1.1.11",
"@types/express": "^4.17.17",
"@types/glob": "^8.1.0",
"@types/koa": "^2.13.8",
"@types/koa-mount": "^4.0.2",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"eslint": "^8.47.0",
Expand All @@ -115,6 +117,8 @@
"fastify": "^4.21.0",
"glob": "^10.3.3",
"graphql": "^16.8.0",
"koa": "^2.14.2",
"koa-mount": "^4.0.0",
"prettier": "^3.0.2",
"rollup": "^3.28.1",
"rollup-plugin-gzip": "^3.1.0",
Expand Down
119 changes: 119 additions & 0 deletions src/use/koa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type { Middleware, Response } from 'koa';
import type { IncomingMessage } from 'http';
import {
createHandler as createRawHandler,
HandlerOptions as RawHandlerOptions,
OperationContext,
} from '../handler';

/**
* @category Server/koa
*/
export interface RequestContext {
res: Response;
}

/**
* Handler options when using the koa adapter.
*
* @category Server/koa
*/
export type HandlerOptions<Context extends OperationContext = undefined> =
RawHandlerOptions<IncomingMessage, RequestContext, Context>;

/**
* The ready-to-use handler for [Koa](https://expressjs.com).
*
* Errors thrown from the provided options or callbacks (or even due to
* library misuse or potential bugs) will reject the handler or bubble to the
* returned iterator. They are considered internal errors and you should take care
* of them accordingly.
*
* For production environments, its recommended not to transmit the exact internal
* error details to the client, but instead report to an error logging tool or simply
* the console.
*
* ```js
* import Koa from 'koa'; // yarn add koa
* import mount from 'koa-mount'; // yarn add koa-mount
* import { createHandler } from 'graphql-sse/lib/use/koa';
* import { schema } from './my-graphql';
*
* const app = new Koa();
* app.use(
* mount('/graphql/stream', async (ctx, next) => {
* try {
* await handler(ctx, next);
* } catch (err) {
* console.error(err);
* ctx.response.status = 500;
* ctx.response.message = 'Internal Server Error';
* }
* }),
* );
*
* app.listen({ port: 4000 });
* console.log('Listening to port 4000');
* ```
*
* @category Server/koa
*/
export function createHandler<Context extends OperationContext = undefined>(
options: HandlerOptions<Context>,
): Middleware {
const handler = createRawHandler(options);
return async function requestListener(ctx) {
const [body, init] = await handler({
url: ctx.url,
method: ctx.method,
headers: {
get(key) {
const header = ctx.headers[key];
return Array.isArray(header) ? header.join('\n') : header;
},
},
body: () => {
if (ctx.body) {
// in case koa has a body parser
return ctx.body;
}
return new Promise<string>((resolve) => {
let body = '';
ctx.req.on('data', (chunk) => (body += chunk));
ctx.req.on('end', () => resolve(body));
});
},
raw: ctx.req,
context: { res: ctx.response },
});
ctx.response.status = init.status;
ctx.response.message = init.statusText;
if (init.headers) {
for (const [name, value] of Object.entries(init.headers)) {
ctx.response.set(name, value);
}
}

if (!body || typeof body === 'string') {
ctx.body = body;
return;
}

ctx.res.once('close', body.return);
for await (const value of body) {
const closed = await new Promise((resolve, reject) => {
if (!ctx.res.writable) {
// response's close event might be late
resolve(true);
} else {
ctx.res.write(value, (err) => (err ? reject(err) : resolve(false)));
}
});
if (closed) {
break;
}
}
ctx.res.off('close', body.return);
return new Promise((resolve) => ctx.res.end(resolve));
};
}
16 changes: 16 additions & 0 deletions tests/use.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import net from 'net';
import http from 'http';
import express from 'express';
import Fastify from 'fastify';
import Koa from 'koa';
import mount from 'koa-mount';
import { schema, pong } from './fixtures/simple';

import { createHandler as createHttpHandler } from '../src/use/http';
import { createHandler as createExpressHandler } from '../src/use/express';
import { createHandler as createFastifyHandler } from '../src/use/fastify';
import { createHandler as createKoaHandler } from '../src/use/koa';

type Dispose = () => Promise<void>;

Expand Down Expand Up @@ -92,6 +95,19 @@ it.each([
return [url, makeDisposeForServer(fastify.server)] as const;
},
},
{
name: 'koa',
startServer: async () => {
const app = new Koa();
app.use(mount('/', createKoaHandler({ schema })));
const server = app.listen({ port: 0 });
const port = (server.address() as net.AddressInfo).port;
return [
`http://localhost:${port}`,
makeDisposeForServer(server),
] as const;
},
},
// no need to test fetch because the handler is pure (gets request, returns response)
// {
// name: 'fetch',
Expand Down
18 changes: 18 additions & 0 deletions website/src/pages/get-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,24 @@ fastify.listen({ port: 4000 });
console.log('Listening to port 4000');
```

##### With [`Koa`](https://koajs.com/)

```js
import Koa from 'koa'; // yarn add koa
import mount from 'koa-mount'; // yarn add koa-mount
import { createHandler } from 'graphql-sse/lib/use/koa';
import { schema } from './previous-step';

// Create a Koa app
const app = new Koa();

// Serve all methods on `/graphql/stream`
app.use(mount('/graphql/stream', createHandler({ schema })));

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

### With [`Deno`](https://deno.land/)

```ts
Expand Down
Loading

0 comments on commit 283b453

Please sign in to comment.