apollo-server/packages/apollo-server-micro/src/ApolloServer.ts
Jesse Rosenberger aae212f869 Triggers for newly introduced serverWillStart lifecycle hooks.
This commit follows-up on #1795, which introduced the new request pipeline, and
implements the triggers for its new life-cycle hooks within the various
integration packages.  Previously, the only implementation was within the
`apollo-server` package and didn't get triggered for those invoking the
`applyMiddleware` method on integrations (e.g. `apollo-server-express`,
`...-hapi` and `...-koa`).

While in an ideal world, ALL existing `applyMiddleware` functions would be
marked as `async` functions and allow us to `await ApolloServer#willStart`,
in practice the only `applyMiddleware` which is currently `async` is the one
within the Hapi implementation.  Therefore, we'll instead kick off the
`willStart` lifecycle hook as soon as `applyMiddleware` is started, return
as quickly as we have before and then (essentially) yield the completion of
Apollo Server's `willStart` prior to serving the first request — thus
ensuring the completion of server-startup activities.

Similarly, we'll do the same for `createHandler` methods on integrations
which don't utilize Node.js server frameworks but don't have `async`
handlers (e.g. AWS, Lambda, etc.).
2018-10-26 15:07:13 +03:00

174 lines
4.8 KiB
TypeScript

import { ApolloServerBase, GraphQLOptions } from 'apollo-server-core';
import { processRequest as processFileUploads } from '@apollographql/apollo-upload-server';
import { ServerResponse } from 'http';
import { send } from 'micro';
import { renderPlaygroundPage } from '@apollographql/graphql-playground-html';
import { parseAll } from 'accept';
import { graphqlMicro } from './microApollo';
import { MicroRequest } from './types';
export interface ServerRegistration {
path?: string;
disableHealthCheck?: boolean;
onHealthCheck?: (req: MicroRequest) => Promise<any>;
}
export class ApolloServer extends ApolloServerBase {
// Extract Apollo Server options from the request.
async createGraphQLServerOptions(
req: MicroRequest,
res: ServerResponse,
): Promise<GraphQLOptions> {
return super.graphQLServerOptions({ req, res });
}
// Prepares and returns an async function that can be used by Micro to handle
// GraphQL requests.
public createHandler({
path,
disableHealthCheck,
onHealthCheck,
}: ServerRegistration = {}) {
// We'll kick off the `willStart` right away, so hopefully it'll finish
// before the first request comes in.
const promiseWillStart = this.willStart();
return async (req, res) => {
this.graphqlPath = path || '/graphql';
await promiseWillStart;
await this.handleFileUploads(req);
(await this.handleHealthCheck({
req,
res,
disableHealthCheck,
onHealthCheck,
})) ||
this.handleGraphqlRequestsWithPlayground({ req, res }) ||
(await this.handleGraphqlRequestsWithServer({ req, res })) ||
send(res, 404, null);
};
}
// This integration supports file uploads.
protected supportsUploads(): boolean {
return true;
}
// This integration supports subscriptions.
protected supportsSubscriptions(): boolean {
return true;
}
// If health checking is enabled, trigger the `onHealthCheck`
// function when the health check URL is requested.
private async handleHealthCheck({
req,
res,
disableHealthCheck,
onHealthCheck,
}: {
req: MicroRequest;
res: ServerResponse;
disableHealthCheck?: boolean;
onHealthCheck?: (req: MicroRequest) => Promise<any>;
}): Promise<boolean> {
let handled = false;
if (
!disableHealthCheck &&
req.url === '/.well-known/apollo/server-health'
) {
// Response follows
// https://tools.ietf.org/html/draft-inadarei-api-health-check-01
res.setHeader('Content-Type', 'application/health+json');
if (onHealthCheck) {
try {
await onHealthCheck(req);
} catch (error) {
send(res, 503, { status: 'fail' });
handled = true;
}
}
if (!handled) {
send(res, 200, { status: 'pass' });
handled = true;
}
}
return handled;
}
// If the `playgroundOptions` are set, register a `graphql-playground` instance
// (not available in production) that is then used to handle all
// incoming GraphQL requests.
private handleGraphqlRequestsWithPlayground({
req,
res,
}: {
req: MicroRequest;
res: ServerResponse;
}): boolean {
let handled = false;
if (this.playgroundOptions && req.method === 'GET') {
const accept = parseAll(req.headers);
const types = accept.mediaTypes as string[];
const prefersHTML =
types.find(
(x: string) => x === 'text/html' || x === 'application/json',
) === 'text/html';
if (prefersHTML) {
const middlewareOptions = {
endpoint: this.graphqlPath,
subscriptionEndpoint: this.subscriptionsPath,
...this.playgroundOptions,
};
send(res, 200, renderPlaygroundPage(middlewareOptions));
handled = true;
}
}
return handled;
}
// Handle incoming GraphQL requests using Apollo Server.
private async handleGraphqlRequestsWithServer({
req,
res,
}: {
req: MicroRequest;
res: ServerResponse;
}): Promise<boolean> {
let handled = false;
const url = req.url.split('?')[0];
if (url === this.graphqlPath) {
const graphqlHandler = graphqlMicro(
this.createGraphQLServerOptions.bind(this),
);
const responseData = await graphqlHandler(req, res);
send(res, 200, responseData);
handled = true;
}
return handled;
}
// If file uploads are detected, prepare them for easier handling with
// the help of `apollo-upload-server`.
private async handleFileUploads(req: MicroRequest) {
const contentType = req.headers['content-type'];
if (
this.uploadsConfig &&
contentType &&
contentType.startsWith('multipart/form-data')
) {
req.filePayload = await processFileUploads(req, this.uploadsConfig);
}
}
}