grafserv
Version:
A highly optimized server for GraphQL, powered by Grafast
690 lines • 28.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertErrorToErrorResult = exports.GrafservBase = void 0;
exports.convertHandlerResultToResult = convertHandlerResultToResult;
const tslib_1 = require("tslib");
const eventemitter3_1 = tslib_1.__importDefault(require("eventemitter3"));
const grafast_1 = require("grafast");
const graphql = tslib_1.__importStar(require("grafast/graphql"));
const graphile_config_1 = require("graphile-config");
const hooks_js_1 = require("../hooks.js");
const mapIterator_js_1 = require("../mapIterator.js");
const graphiql_js_1 = require("../middleware/graphiql.js");
const graphql_js_1 = require("../middleware/graphql.js");
const options_js_1 = require("../options.js");
const utils_js_1 = require("../utils.js");
const buffer404 = Buffer.from(`<!doctype html><html><head><title>Not found</title></head><body><h1>Not found</h1><p>Please try again with a different URL</p></body></html>`, "utf8");
const buffer503 = Buffer.from("Service unavailable", "utf8");
const failedToBuildHandlersError = new graphql.GraphQLError("Unknown error occurred.");
const { isSchema, validateSchema } = graphql;
class GrafservBase {
getExecutionConfig(_ctx) {
throw new Error("Overwritten in constructor");
}
constructor(config) {
this.releaseHandlers = [];
this.releasing = false;
this.initialized = false;
this._settingPreset = false;
this.waitForGraphqlHandler = function (...args) {
const [request] = args;
const deferred = (0, grafast_1.defer)();
const { dynamicOptions } = this;
const onReady = () => {
this.eventEmitter.off("dynamicOptions:ready", onReady);
this.eventEmitter.off("dynamicOptions:error", onError);
Promise.resolve()
.then(() => this.graphqlHandler(...args))
.then(deferred.resolve, deferred.reject);
};
const onError = (e) => {
this.eventEmitter.off("dynamicOptions:ready", onReady);
this.eventEmitter.off("dynamicOptions:error", onError);
const graphqlError = new graphql.GraphQLError("Unknown error occurred", null, null, null, null, e);
deferred.resolve({
type: "graphql",
request,
dynamicOptions,
payload: { errors: [graphqlError] },
statusCode: graphqlError.extensions?.statusCode ?? 503,
// Fall back to application/json; this is when an unexpected error happens
// so it shouldn't be hit.
contentType: graphql_js_1.APPLICATION_JSON,
});
};
this.eventEmitter.on("dynamicOptions:ready", onReady);
this.eventEmitter.on("dynamicOptions:error", onError);
setTimeout(onError, 5000, new Error("Server initialization timed out"));
return Promise.resolve(deferred);
};
this.waitForGraphiqlHandler = function (...args) {
const [request] = args;
const { dynamicOptions } = this;
const deferred = (0, grafast_1.defer)();
const onReady = () => {
this.eventEmitter.off("dynamicOptions:ready", onReady);
this.eventEmitter.off("dynamicOptions:error", onError);
Promise.resolve()
.then(() => this.graphiqlHandler(...args))
.then(deferred.resolve, deferred.reject);
};
const onError = (e) => {
this.eventEmitter.off("dynamicOptions:ready", onReady);
this.eventEmitter.off("dynamicOptions:error", onError);
const graphqlError = new graphql.GraphQLError("Unknown error occurred", null, null, null, null, e);
// TODO: this should be an HTML response
deferred.resolve({
type: "graphql",
request,
dynamicOptions,
payload: { errors: [graphqlError] },
statusCode: graphqlError.extensions?.statusCode ?? 503,
// Fall back to application/json; this is when an unexpected error happens
// so it shouldn't be hit.
contentType: graphql_js_1.APPLICATION_JSON,
});
};
this.eventEmitter.on("dynamicOptions:ready", onReady);
this.eventEmitter.on("dynamicOptions:error", onError);
setTimeout(onError, 5000, new Error("Server initialization timed out"));
return Promise.resolve(deferred);
};
this.waitForGraphiqlStaticHandler = function (...args) {
const [request] = args;
const { dynamicOptions } = this;
const deferred = (0, grafast_1.defer)();
const onReady = () => {
this.eventEmitter.off("dynamicOptions:ready", onReady);
this.eventEmitter.off("dynamicOptions:error", onError);
Promise.resolve()
.then(() => this.graphiqlStaticHandler(...args))
.then(deferred.resolve, deferred.reject);
};
const onError = (e) => {
this.eventEmitter.off("dynamicOptions:ready", onReady);
this.eventEmitter.off("dynamicOptions:error", onError);
deferred.resolve({
type: "raw",
request,
dynamicOptions,
statusCode: e.statusCode ?? 503,
headers: { "content-type": "text/plain" },
payload: Buffer.from("Service unavailable", "utf8"),
});
};
this.eventEmitter.on("dynamicOptions:ready", onReady);
this.eventEmitter.on("dynamicOptions:error", onError);
setTimeout(onError, 5000, new Error("Server initialization timed out"));
return Promise.resolve(deferred);
};
this.failedGraphqlHandler = function (...args) {
const [request] = args;
const { dynamicOptions } = this;
return {
type: "graphql",
request,
dynamicOptions,
payload: { errors: [failedToBuildHandlersError] },
statusCode: 503,
// Fall back to application/json; this is when an unexpected error happens
// so it shouldn't be hit.
contentType: graphql_js_1.APPLICATION_JSON,
};
};
this.failedGraphiqlHandler = function (...args) {
const [request] = args;
const { dynamicOptions } = this;
return {
type: "graphql",
request,
dynamicOptions,
payload: { errors: [failedToBuildHandlersError] },
statusCode: 503,
// Fall back to application/json; this is when an unexpected error happens
// so it shouldn't be hit.
contentType: graphql_js_1.APPLICATION_JSON,
};
};
this.failedGraphiqlStaticHandler = function (...args) {
const [request] = args;
const { dynamicOptions } = this;
return {
type: "text",
request,
dynamicOptions,
statusCode: 503,
payload: buffer503,
};
};
this.eventEmitter = new eventemitter3_1.default();
this.resolvedPreset = (0, graphile_config_1.resolvePreset)(config.preset ? config.preset : {});
this.dynamicOptions = {
validationRules: [...graphql.specifiedRules],
getExecutionConfig: defaultMakeGetExecutionConfig(),
...(0, options_js_1.optionsFromConfig)(this.resolvedPreset),
};
this.getExecutionConfig = this.dynamicOptions.getExecutionConfig;
this.middleware = (0, hooks_js_1.getGrafservMiddleware)(this.resolvedPreset);
this.grafastMiddleware = (0, grafast_1.getGrafastMiddleware)(this.resolvedPreset);
this.schemaError = null;
this.schema = config.schema;
if ((0, grafast_1.isPromiseLike)(config.schema)) {
const promise = config.schema;
promise.then((schema) => {
this.setSchema(schema);
}, (error) => {
this.schemaError = promise;
this.schema = null;
this.eventEmitter.emit("schema:error", error);
});
}
else {
this.eventEmitter.emit("schema:ready", config.schema);
}
// These are overwritten by setPreset() resolving
this.graphqlHandler = this.waitForGraphqlHandler;
this.graphiqlHandler = this.waitForGraphiqlHandler;
this.graphiqlStaticHandler = this.waitForGraphiqlStaticHandler;
this.setPreset(this.resolvedPreset);
}
/** @internal */
_processRequest(inRequest) {
const request = (0, utils_js_1.normalizeRequest)(inRequest);
if (!this.dynamicOptions) {
throw new Error(`GrafservInternalError<1377f225-31b7-4a81-a56e-a28e18a19899>: Attempted to process request prematurely`);
}
const dynamicOptions = this.dynamicOptions;
const forceCORS = !!this.resolvedPreset.grafserv?.dangerouslyAllowAllCORSRequests &&
request.method === "OPTIONS";
try {
if (request.path === dynamicOptions.graphqlPath) {
if (forceCORS)
return optionsResponse(request, dynamicOptions);
return this.graphqlHandler(request, this.graphiqlHandler);
}
if (dynamicOptions.graphiql &&
request.method === "GET" &&
request.path === dynamicOptions.graphiqlPath) {
if (forceCORS)
return optionsResponse(request, dynamicOptions);
return this.graphiqlHandler(request);
}
if (dynamicOptions.watch &&
request.method === "GET" &&
request.path === dynamicOptions.eventStreamPath) {
if (forceCORS)
return optionsResponse(request, dynamicOptions);
const stream = this.makeStream();
return {
type: "event-stream",
request,
dynamicOptions,
payload: stream,
statusCode: 200,
};
}
if (dynamicOptions.graphiql &&
request.method === "GET" &&
request.path.startsWith(dynamicOptions.graphiqlStaticPath)) {
if (forceCORS)
return optionsResponse(request, dynamicOptions);
return this.graphiqlStaticHandler(request);
}
// Unhandled
return null;
}
catch (e) {
console.error("Unexpected error occurred in _processRequest", e);
return {
type: "html",
request,
dynamicOptions,
status: 500,
payload: Buffer.from("ERROR", "utf8"),
};
}
}
processRequest(requestDigest) {
const { resolvedPreset } = this;
const event = {
resolvedPreset,
requestDigest,
instance: this,
};
return this.middleware != null
? this.middleware.run("processRequest", event, processRequestWithEvent)
: processRequestWithEvent(event);
}
getPreset() {
return this.resolvedPreset;
}
getSchema() {
return this.schema ?? this.schemaError;
}
async release() {
if (this.releasing) {
throw new Error("Release has already been called");
}
this.releasing = true;
for (let i = this.releaseHandlers.length - 1; i >= 0; i--) {
const handler = this.releaseHandlers[i];
try {
await handler();
}
catch (e) {
/* nom nom nom */
}
}
}
onRelease(cb) {
if (this.releasing) {
throw new Error("Release has already been called; cannot add more onRelease callbacks");
}
this.releaseHandlers.push(cb);
}
setPreset(newPreset) {
if (this._settingPreset) {
throw new Error(`Setting a preset is currently in progress; please wait for it to complete.`);
}
this._settingPreset = true;
const resolvedPreset = (0, graphile_config_1.resolvePreset)(newPreset);
const middleware = (0, hooks_js_1.getGrafservMiddleware)(this.resolvedPreset);
const grafastMiddleware = (0, grafast_1.getGrafastMiddleware)(this.resolvedPreset);
// Note: this gets directly mutated
const dynamicOptions = {
validationRules: [...graphql.specifiedRules],
getExecutionConfig: defaultMakeGetExecutionConfig(),
...(0, options_js_1.optionsFromConfig)(resolvedPreset),
};
const storeDynamicOptions = (dynamicOptions) => {
const { resolvedPreset } = dynamicOptions;
// Overwrite all the `this.*` properties at once
this.resolvedPreset = resolvedPreset;
this.middleware = middleware;
this.grafastMiddleware = grafastMiddleware;
this.dynamicOptions = dynamicOptions;
this.initialized = true;
// ENHANCE: this.graphqlHandler?.release()?
this.refreshHandlers();
this.getExecutionConfig = dynamicOptions.getExecutionConfig;
// MUST come after the handlers have been refreshed, otherwise we'll
// get infinite loops
this.eventEmitter.emit("dynamicOptions:ready", {});
};
return (new Promise((resolve) => resolve(middleware != null
? middleware.run("setPreset", dynamicOptions, storeDynamicOptions)
: storeDynamicOptions(dynamicOptions)))
.then(null, (e) => {
this.graphqlHandler = this.failedGraphqlHandler;
this.graphiqlHandler = this.failedGraphiqlHandler;
this.graphiqlStaticHandler = this.failedGraphiqlStaticHandler;
this.eventEmitter.emit("dynamicOptions:error", e);
})
// Finally:
.then(() => {
this._settingPreset = false;
}));
}
setSchema(newSchema) {
if (!newSchema) {
throw new Error(`setSchema must be called with a GraphQL schema`);
}
if (!isSchema(newSchema)) {
throw new Error(`setParams called with invalid schema (is there more than one 'graphql' module loaded?)`);
}
const errors = validateSchema(newSchema);
if (errors.length > 0) {
throw new Error(`setParams called with invalid schema; first error: ${errors[0]}`);
}
if (this.schema !== newSchema) {
this.schemaError = null;
this.schema = newSchema;
this.eventEmitter.emit("schema:ready", newSchema);
this.refreshHandlers();
}
}
refreshHandlers() {
if (!this.initialized) {
// This will be handled once `setPreset` completes
return;
}
this.graphqlHandler = (0, graphql_js_1.makeGraphQLHandler)(this);
this.graphiqlHandler = (0, graphiql_js_1.makeGraphiQLHandler)(this.resolvedPreset, this.middleware, this.dynamicOptions);
this.graphiqlStaticHandler = (0, graphiql_js_1.makeGraphiQLStaticHandler)(this.resolvedPreset, this.middleware, this.dynamicOptions);
}
// TODO: Rename this, or make it a middleware, or something
makeStream() {
const queue = [];
let finished = false;
const bump = () => {
const next = queue.shift();
if (next !== undefined) {
next.resolve({
done: false,
value: { event: "change", data: "schema" },
});
}
};
const flushQueue = (e) => {
const entries = queue.splice(0, queue.length);
for (const entry of entries) {
if (e != null) {
entry.reject(e);
}
else {
entry.resolve({ done: true });
}
}
};
this.eventEmitter.on("schema:ready", bump);
return {
[Symbol.asyncIterator]() {
return this;
},
next() {
if (finished) {
return Promise.resolve({
done: true,
});
}
return new Promise((resolve, reject) => {
queue.push({ resolve, reject });
});
},
return() {
finished = true;
if (queue.length !== 0) {
flushQueue();
}
return Promise.resolve({
done: true,
});
},
throw(e) {
if (queue.length !== 0) {
flushQueue(e);
}
return Promise.reject(e);
},
};
}
}
exports.GrafservBase = GrafservBase;
function defaultMakeGetExecutionConfig() {
let latestSchema;
let latestSchemaOrPromise;
let latestParseAndValidate;
let schemaPrepare = null;
return function getExecutionConfig() {
// Get up to date schema, in case we're in watch mode
const schemaOrPromise = this.getSchema();
const { resolvedPreset, dynamicOptions } = this;
if (schemaOrPromise !== latestSchemaOrPromise) {
latestSchemaOrPromise = schemaOrPromise;
if ("then" in schemaOrPromise) {
schemaPrepare = (async () => {
latestSchema = await schemaOrPromise;
latestSchemaOrPromise = schemaOrPromise;
latestParseAndValidate = (0, graphql_js_1.makeParseAndValidateFunction)(latestSchema, resolvedPreset, dynamicOptions);
schemaPrepare = null;
return true;
})();
}
else {
if (latestSchema === schemaOrPromise) {
// No action necessary
}
else {
latestSchema = schemaOrPromise;
latestParseAndValidate = (0, graphql_js_1.makeParseAndValidateFunction)(latestSchema, resolvedPreset, dynamicOptions);
}
}
}
if (schemaPrepare !== null) {
const sleeper = (0, utils_js_1.sleep)(dynamicOptions.schemaWaitTime);
const schemaReadyPromise = Promise.race([schemaPrepare, sleeper.promise]);
return schemaReadyPromise.then((schemaReady) => {
sleeper.release();
if (schemaReady !== true) {
// Handle missing schema
throw new Error(`Schema isn't ready`);
}
return {
schema: latestSchema,
parseAndValidate: latestParseAndValidate,
resolvedPreset,
execute: grafast_1.execute,
subscribe: grafast_1.subscribe,
contextValue: Object.create(null),
};
});
}
/*
if (schemaOrPromise == null) {
const err = Promise.reject(
new GraphQLError(
"The schema is currently unavailable",
null,
null,
null,
null,
null,
{
statusCode: 503,
},
),
);
return () => err;
}
*/
return {
schema: latestSchema,
parseAndValidate: latestParseAndValidate,
resolvedPreset,
execute: grafast_1.execute,
subscribe: grafast_1.subscribe,
contextValue: Object.create(null),
};
};
}
const END = Buffer.from("\r\n-----\r\n", "utf8");
const DIVIDE = Buffer.from(`\r\n---\r\nContent-Type: application/json\r\n\r\n`, "utf8");
function eventStreamHeaders(dynamicOptions) {
if (!dynamicOptions.watch)
return null;
return { ["x-graphql-event-stream"]: dynamicOptions.eventStreamPath };
}
const CONTENT_TYPE_HEADERS = {
raw: null,
html: { "content-type": "text/html; charset=utf-8" },
text: { "content-type": "text/plain; charset=utf-8" },
};
function convertHandlerResultToResult(handlerResult) {
if (handlerResult === null) {
return null;
}
switch (handlerResult.type) {
case "graphql": {
const { payload, statusCode = 200, contentType, outputDataAsString, dynamicOptions, request: { preferJSON }, } = handlerResult;
(0, utils_js_1.handleErrors)(payload);
const headers = {
__proto__: null,
...eventStreamHeaders(dynamicOptions),
"content-type": contentType,
...handlerResult.headers,
};
if (preferJSON && !outputDataAsString) {
return { type: "json", statusCode, headers, json: payload };
}
else {
const buffer = Buffer.from((0, grafast_1.stringifyPayload)(payload, outputDataAsString), "utf8");
headers["content-length"] = String(buffer.length);
return { type: "buffer", statusCode, headers, buffer };
}
}
case "graphqlIncremental": {
const { iterator, statusCode = 200, outputDataAsString, dynamicOptions, } = handlerResult;
const headers = {
__proto__: null,
...eventStreamHeaders(dynamicOptions),
"content-type": 'multipart/mixed; boundary="-"',
"transfer-encoding": "chunked",
...handlerResult.headers,
};
const bufferIterator = (0, mapIterator_js_1.mapIterator)(iterator, (payload) => {
(0, utils_js_1.handleErrors)(payload);
const payloadBuffer = Buffer.from((0, grafast_1.stringifyPayload)(payload, outputDataAsString), "utf8");
return Buffer.concat([DIVIDE, payloadBuffer]);
}, () => {
return END;
});
return {
type: "bufferStream",
headers,
statusCode,
lowLatency: true,
bufferIterator,
};
}
case "text":
case "html":
case "raw": {
const { payload: buffer, statusCode = 200 } = handlerResult;
const headers = {
__proto__: null,
...CONTENT_TYPE_HEADERS[handlerResult.type],
"content-length": String(buffer.length),
...handlerResult.headers,
};
return { type: "buffer", statusCode, headers, buffer };
}
case "noContent": {
const { statusCode = 204 } = handlerResult;
const headers = {
__proto__: null,
...handlerResult.headers,
};
return { type: "noContent", statusCode, headers };
}
case "notFound": {
const { statusCode = 404, payload: buffer = buffer404 } = handlerResult;
const headers = {
__proto__: null,
...CONTENT_TYPE_HEADERS.html,
...handlerResult.headers,
};
return { type: "buffer", statusCode, headers, buffer };
}
case "event-stream": {
const { payload: stream, statusCode = 200, request: { httpVersionMajor }, } = handlerResult;
// Making sure these options are set.
// Set headers for Server-Sent Events.
const headers = {
__proto__: null,
// Don't buffer EventStream in nginx
"x-accel-buffering": "no",
"content-type": "text/event-stream",
"cache-control": "no-cache, no-transform",
...(httpVersionMajor >= 2 ? null : { connection: "keep-alive" }),
...handlerResult.headers,
};
// Creates a stream for the response
const event2buffer = (event) => {
let payload = "";
if (event.event !== undefined) {
payload += `event: ${event.event}\n`;
}
if (event.id !== undefined) {
payload += `id: ${event.id}\n`;
}
if (event.retry !== undefined) {
payload += `retry: ${event.retry}\n`;
}
if (event.data != null) {
payload += `data: ${event.data.replace(/\n/g, "\ndata: ")}\n`;
}
payload += "\n";
return Buffer.from(payload, "utf8");
};
const bufferIterator = (0, mapIterator_js_1.mapIterator)(stream, event2buffer, undefined, () => event2buffer({ event: "open" }));
return {
type: "bufferStream",
statusCode,
headers,
lowLatency: true,
bufferIterator,
};
}
default: {
const never = handlerResult;
console.error(`Did not understand '${never}' passed to convertHandlerResultToResult`);
const statusCode = 500;
const buffer = Buffer.from("Unexpected input to convertHandlerResultToResult", "utf8");
const headers = {
__proto__: null,
"content-type": "text/plain; charset=utf-8",
"content-length": String(buffer.length),
};
return { type: "buffer", statusCode, headers, buffer };
}
}
}
const convertErrorToErrorResult = (error) => {
// TODO: need to assert `error` is not a GraphQLError, that should be handled elsewhere.
const statusCode = error.statusCode ?? 500;
return {
type: "error",
statusCode,
headers: Object.create(null),
error,
};
};
exports.convertErrorToErrorResult = convertErrorToErrorResult;
function dangerousCorsWrap(result) {
if (result === null) {
return result;
}
if (!result.headers["access-control-allow-origin"]) {
result.headers["access-control-allow-origin"] = "*";
}
if (!result.headers["access-control-allow-headers"]) {
result.headers["access-control-allow-headers"] = "*";
}
return result;
}
function optionsResponse(request, dynamicOptions) {
return {
type: "noContent",
request,
dynamicOptions,
statusCode: 204,
};
}
function processRequestWithEvent(event) {
const { requestDigest: request, instance } = event;
let returnValue;
try {
const result = instance._processRequest(request);
if ((0, grafast_1.isPromiseLike)(result)) {
returnValue = result.then(convertHandlerResultToResult, exports.convertErrorToErrorResult);
}
else {
returnValue = convertHandlerResultToResult(result);
}
}
catch (e) {
returnValue = (0, exports.convertErrorToErrorResult)(e);
}
if (instance.resolvedPreset.grafserv?.dangerouslyAllowAllCORSRequests) {
if ((0, grafast_1.isPromiseLike)(returnValue)) {
return returnValue.then(dangerousCorsWrap);
}
else {
return dangerousCorsWrap(returnValue);
}
}
else {
return returnValue;
}
}
//# sourceMappingURL=base.js.map