@as-integrations/koa
Version:
Apollo server integration for koa framework
141 lines (129 loc) • 4.67 kB
text/typescript
import { Readable } from 'node:stream';
import { parse } from 'node:url';
import type { WithRequired } from '@apollo/utils.withrequired';
import {
type ApolloServer,
type BaseContext,
type ContextFunction,
type HTTPGraphQLRequest,
HeaderMap,
} from '@apollo/server';
import type Koa from 'koa';
// we need the extended `Request` type from `koa-bodyparser`,
// this is similar to an effectful import but for types, since
// the `koa-bodyparser` types "polyfill" the `koa` types
import type * as _ from 'koa-bodyparser';
export interface KoaContextFunctionArgument<
StateT = Koa.DefaultState,
ContextT = Koa.DefaultContext,
> {
ctx: Koa.ParameterizedContext<StateT, ContextT>;
}
interface KoaMiddlewareOptions<TContext extends BaseContext, StateT, ContextT> {
context?: ContextFunction<
[KoaContextFunctionArgument<StateT, ContextT>],
TContext
>;
}
export function koaMiddleware<
StateT = Koa.DefaultState,
ContextT = Koa.DefaultContext,
>(
server: ApolloServer<BaseContext>,
options?: KoaMiddlewareOptions<BaseContext, StateT, ContextT>,
): Koa.Middleware<StateT, ContextT>;
export function koaMiddleware<
TContext extends BaseContext,
StateT = Koa.DefaultState,
ContextT = Koa.DefaultContext,
>(
server: ApolloServer<TContext>,
options: WithRequired<
KoaMiddlewareOptions<TContext, StateT, ContextT>,
'context'
>,
): Koa.Middleware<StateT, ContextT>;
export function koaMiddleware<
TContext extends BaseContext,
StateT = Koa.DefaultState,
ContextT = Koa.DefaultContext,
>(
server: ApolloServer<TContext>,
options?: KoaMiddlewareOptions<TContext, StateT, ContextT>,
): Koa.Middleware<StateT, ContextT> {
server.assertStarted('koaMiddleware()');
// This `any` is safe because the overload above shows that context can
// only be left out if you're using BaseContext as your context, and {} is a
// valid BaseContext.
const defaultContext: ContextFunction<
[KoaContextFunctionArgument<StateT, ContextT>],
any
> = async () => ({});
const context: ContextFunction<
[KoaContextFunctionArgument<StateT, ContextT>],
TContext
> = options?.context ?? defaultContext;
return async (ctx) => {
if (!ctx.request.body) {
// The json koa-bodyparser *always* sets ctx.request.body to {} if it's unset (even
// if the Content-Type doesn't match), so if it isn't set, you probably
// forgot to set up koa-bodyparser.
ctx.status = 500;
ctx.body =
'`ctx.request.body` is not set; this probably means you forgot to set up the ' +
'`koa-bodyparser` middleware before the Apollo Server middleware.';
return;
}
const incomingHeaders = new HeaderMap();
for (const [key, value] of Object.entries(ctx.headers)) {
if (value !== undefined) {
// Node/Koa headers can be an array or a single value. We join
// multi-valued headers with `, ` just like the Fetch API's `Headers`
// does. We assume that keys are already lower-cased (as per the Node
// docs on IncomingMessage.headers) and so we don't bother to lower-case
// them or combine across multiple keys that would lower-case to the
// same value.
incomingHeaders.set(
key,
Array.isArray(value) ? value.join(', ') : value,
);
}
}
const httpGraphQLRequest: HTTPGraphQLRequest = {
method: ctx.method.toUpperCase(),
headers: incomingHeaders,
search: parse(ctx.url).search ?? '',
body: ctx.request.body,
};
const { body, headers, status } = await server.executeHTTPGraphQLRequest({
httpGraphQLRequest,
context: () => context({ ctx }),
});
if (body.kind === 'complete') {
ctx.body = body.string;
} else if (body.kind === 'chunked') {
ctx.body = Readable.from(
(async function* () {
for await (const chunk of body.asyncIterator) {
yield chunk;
if (typeof ctx.body.flush === 'function') {
// If this response has been piped to a writable compression stream then `flush` after
// each chunk.
// This is identical to the Express integration:
// https://github.com/apollographql/apollo-server/blob/a69580565dadad69de701da84092e89d0fddfa00/packages/server/src/express4/index.ts#L96-L105
ctx.body.flush();
}
}
})(),
);
} else {
throw Error(`Delivery method ${(body as any).kind} not implemented`);
}
if (status !== undefined) {
ctx.status = status;
}
for (const [key, value] of headers) {
ctx.set(key, value);
}
};
}