UNPKG

@as-integrations/koa

Version:
141 lines (129 loc) 4.67 kB
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); } }; }