210 lines
7 KiB
JavaScript
210 lines
7 KiB
JavaScript
|
// Adapted from https://github.com/apollographql/apollo-server/blob/201630ad284754248fc9ab6ebedc7506fcc3d951/packages/apollo-server-lambda/src/ApolloServer.ts
|
||
|
|
||
|
const { ApolloServerBase, runHttpQuery } = require("apollo-server-core");
|
||
|
const { Headers } = require("apollo-server-env");
|
||
|
const {
|
||
|
renderPlaygroundPage,
|
||
|
} = require("@apollographql/graphql-playground-html");
|
||
|
|
||
|
function graphqlVercel(options) {
|
||
|
if (!options) {
|
||
|
throw new Error("Apollo Server requires options.");
|
||
|
}
|
||
|
|
||
|
if (arguments.length > 1) {
|
||
|
throw new Error(
|
||
|
`Apollo Server expects exactly one argument, got ${arguments.length}`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
const graphqlHandler = (req, res) => {
|
||
|
if (req.httpMethod === "POST" && !req.body) {
|
||
|
return res.status(500).send("POST body missing.");
|
||
|
}
|
||
|
runHttpQuery([req, res], {
|
||
|
method: req.method,
|
||
|
options: options,
|
||
|
query: req.method === "POST" && req.body ? req.body : req.query,
|
||
|
request: {
|
||
|
url: req.path,
|
||
|
method: req.method,
|
||
|
headers: new Headers(req.headers),
|
||
|
},
|
||
|
}).then(
|
||
|
({ graphqlResponse, responseInit }) => {
|
||
|
setHeaders(res, new Headers(responseInit.headers)).send(
|
||
|
graphqlResponse
|
||
|
);
|
||
|
},
|
||
|
(error) => {
|
||
|
if ("HttpQueryError" !== error.name) {
|
||
|
console.error(error);
|
||
|
return;
|
||
|
}
|
||
|
setHeaders(res, new Headers(error.headers))
|
||
|
.status(error.statusCode)
|
||
|
.send(error.message);
|
||
|
}
|
||
|
);
|
||
|
};
|
||
|
|
||
|
return graphqlHandler;
|
||
|
}
|
||
|
|
||
|
class ApolloServer extends ApolloServerBase {
|
||
|
// If you feel tempted to add an option to this constructor. Please consider
|
||
|
// another place, since the documentation becomes much more complicated when
|
||
|
// the constructor is not longer shared between all integration
|
||
|
constructor(options) {
|
||
|
if (process.env.ENGINE_API_KEY || options.engine) {
|
||
|
options.engine = {
|
||
|
sendReportsImmediately: true,
|
||
|
...(typeof options.engine !== "boolean" ? options.engine : {}),
|
||
|
};
|
||
|
}
|
||
|
super(options);
|
||
|
}
|
||
|
|
||
|
// This translates the arguments from the middleware into graphQL options It
|
||
|
// provides typings for the integration specific behavior, ideally this would
|
||
|
// be propagated with a generic to the super class
|
||
|
createGraphQLServerOptions(req, res) {
|
||
|
return super.graphQLServerOptions({ req, res });
|
||
|
}
|
||
|
|
||
|
createHandler({ cors } = { cors: undefined }) {
|
||
|
// We will kick off the `willStart` event once for the server, and then
|
||
|
// await it before processing any requests by incorporating its `await` into
|
||
|
// the GraphQLServerOptions function which is called before each request.
|
||
|
const promiseWillStart = this.willStart();
|
||
|
|
||
|
const corsHeaders = new Headers();
|
||
|
|
||
|
if (cors) {
|
||
|
if (cors.methods) {
|
||
|
if (typeof cors.methods === "string") {
|
||
|
corsHeaders.set("access-control-allow-methods", cors.methods);
|
||
|
} else if (Array.isArray(cors.methods)) {
|
||
|
corsHeaders.set(
|
||
|
"access-control-allow-methods",
|
||
|
cors.methods.join(",")
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (cors.allowedHeaders) {
|
||
|
if (typeof cors.allowedHeaders === "string") {
|
||
|
corsHeaders.set("access-control-allow-headers", cors.allowedHeaders);
|
||
|
} else if (Array.isArray(cors.allowedHeaders)) {
|
||
|
corsHeaders.set(
|
||
|
"access-control-allow-headers",
|
||
|
cors.allowedHeaders.join(",")
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (cors.exposedHeaders) {
|
||
|
if (typeof cors.exposedHeaders === "string") {
|
||
|
corsHeaders.set("access-control-expose-headers", cors.exposedHeaders);
|
||
|
} else if (Array.isArray(cors.exposedHeaders)) {
|
||
|
corsHeaders.set(
|
||
|
"access-control-expose-headers",
|
||
|
cors.exposedHeaders.join(",")
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (cors.credentials) {
|
||
|
corsHeaders.set("access-control-allow-credentials", "true");
|
||
|
}
|
||
|
if (typeof cors.maxAge === "number") {
|
||
|
corsHeaders.set("access-control-max-age", cors.maxAge.toString());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return (req, res) => {
|
||
|
// Make a request-specific copy of the CORS headers, based on the server
|
||
|
// global CORS headers we've set above.
|
||
|
const requestCorsHeaders = new Headers(corsHeaders);
|
||
|
|
||
|
if (cors && cors.origin) {
|
||
|
const requestOrigin = req.headers["origin"];
|
||
|
if (typeof cors.origin === "string") {
|
||
|
requestCorsHeaders.set("access-control-allow-origin", cors.origin);
|
||
|
} else if (
|
||
|
requestOrigin &&
|
||
|
(typeof cors.origin === "boolean" ||
|
||
|
(Array.isArray(cors.origin) &&
|
||
|
requestOrigin &&
|
||
|
cors.origin.includes(requestOrigin)))
|
||
|
) {
|
||
|
requestCorsHeaders.set("access-control-allow-origin", requestOrigin);
|
||
|
}
|
||
|
|
||
|
const requestAccessControlRequestHeaders =
|
||
|
req.headers["access-control-request-headers"];
|
||
|
if (!cors.allowedHeaders && requestAccessControlRequestHeaders) {
|
||
|
requestCorsHeaders.set(
|
||
|
"access-control-allow-headers",
|
||
|
requestAccessControlRequestHeaders
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Convert the `Headers` into an object which can be spread into the
|
||
|
// various headers objects below.
|
||
|
// Note: while Object.fromEntries simplifies this code, it's only currently
|
||
|
// supported in Node 12 (we support >=6)
|
||
|
const requestCorsHeadersObject = Array.from(requestCorsHeaders).reduce(
|
||
|
(headersObject, [key, value]) => {
|
||
|
headersObject[key] = value;
|
||
|
return headersObject;
|
||
|
},
|
||
|
{}
|
||
|
);
|
||
|
|
||
|
if (res.method === "OPTIONS") {
|
||
|
setHeaders(res, requestCorsHeadersObject).status(204).send("");
|
||
|
}
|
||
|
|
||
|
if (this.playgroundOptions && req.method === "GET") {
|
||
|
const acceptHeader = req.headers["accept"];
|
||
|
if (acceptHeader && acceptHeader.includes("text/html")) {
|
||
|
const path = req.path || "/";
|
||
|
|
||
|
const playgroundRenderPageOptions = {
|
||
|
endpoint: path,
|
||
|
...this.playgroundOptions,
|
||
|
};
|
||
|
|
||
|
return setHeaders(res, {
|
||
|
"Content-Type": "text/html",
|
||
|
...requestCorsHeadersObject,
|
||
|
}).send(renderPlaygroundPage(playgroundRenderPageOptions));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
graphqlVercel(async () => {
|
||
|
// In a world where this `createHandler` was async, we might avoid this
|
||
|
// but since we don't want to introduce a breaking change to this API
|
||
|
// (by switching it to `async`), we'll leverage the
|
||
|
// `GraphQLServerOptions`, which are dynamically built on each request,
|
||
|
// to `await` the `promiseWillStart` which we kicked off at the top of
|
||
|
// this method to ensure that it runs to completion (which is part of
|
||
|
// its contract) prior to processing the request.
|
||
|
await promiseWillStart;
|
||
|
return this.createGraphQLServerOptions(req, res);
|
||
|
})(req, res);
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function setHeaders(res, headers) {
|
||
|
for (const [name, value] of headers.entries()) {
|
||
|
res.setHeader(name, value);
|
||
|
}
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
module.exports = { ApolloServer };
|