graphql-helix
Version:
A highly evolved GraphQL HTTP Server 🧬
284 lines (283 loc) • 12.7 kB
JavaScript
import { execute as defaultExecute, getOperationAST, parse as defaultParse, subscribe as defaultSubscribe, validate as defaultValidate, GraphQLError, } from "graphql";
import { stopAsyncIteration, isAsyncIterable, isHttpMethod } from './util/index.mjs';
import { HttpError } from './errors.mjs';
import { getRankedResponseProtocols } from './util/get-ranked-response-protocols.mjs';
const parseQuery = (query, parse) => {
if (typeof query !== "string" && query.kind === "Document") {
return query;
}
try {
const parseResult = parse(query);
if (parseResult instanceof Promise) {
return parseResult.catch((syntaxError) => {
throw new HttpError(400, "GraphQL syntax error.", {
graphqlErrors: [syntaxError],
});
});
}
return parseResult;
}
catch (syntaxError) {
throw new HttpError(400, "GraphQL syntax error.", {
graphqlErrors: [syntaxError],
});
}
};
export const validateDocument = (schema, document, validate, validationRules) => {
const validationErrors = validate(schema, document, validationRules);
if (validationErrors.length) {
throw new HttpError(400, "GraphQL validation error.", {
graphqlErrors: validationErrors,
});
}
};
const getExecutableOperation = (document, operationName) => {
const operation = getOperationAST(document, operationName);
if (!operation) {
throw new HttpError(400, "Could not determine what operation to execute.");
}
return operation;
};
// If clients do not accept application/graphql+json use application/json - otherwise respect the order in the accept header
const getSingleResponseContentType = (protocols) => {
if (protocols["application/graphql+json"] === -1) {
return "application/json";
}
return protocols["application/graphql+json"] > protocols["application/json"] ? "application/graphql+json" : "application/json";
};
const getHeader = (request, headerName) => typeof request.headers.get === "function" ? request.headers.get(headerName) : request.headers[headerName];
export const processRequest = async (options) => {
const { contextFactory, execute = defaultExecute, formatPayload = ({ payload }) => payload, operationName, extensions, parse = defaultParse, query, request, rootValueFactory, schema, subscribe = defaultSubscribe, validate = defaultValidate, validationRules, variables, allowedSubscriptionHttpMethods = ["GET", "POST"], } = options;
let context;
let rootValue;
let document;
let operation;
const result = await (async () => {
const accept = getHeader(request, "accept");
const contentType = getHeader(request, "contentType");
const rankedProtocols = getRankedResponseProtocols(accept, contentType);
const isEventStreamAccepted = rankedProtocols["text/event-stream"] !== -1;
const defaultSingleResponseHeaders = [
{
name: "Content-Type",
value: getSingleResponseContentType(rankedProtocols),
},
];
try {
if (!isHttpMethod("GET", request.method) && !isHttpMethod("POST", request.method)) {
throw new HttpError(405, "GraphQL only supports GET and POST requests.", {
headers: [...defaultSingleResponseHeaders, { name: "Allow", value: "GET, POST" }],
});
}
if (query == null) {
throw new HttpError(400, "Must provide query string.", { headers: defaultSingleResponseHeaders });
}
document = await parseQuery(query, parse);
validateDocument(schema, document, validate, validationRules);
operation = getExecutableOperation(document, operationName);
if (operation.operation === "mutation" && isHttpMethod("GET", request.method)) {
throw new HttpError(405, "Can only perform a mutation operation from a POST request.", {
headers: [...defaultSingleResponseHeaders, { name: "Allow", value: "POST" }],
});
}
let variableValues;
try {
if (variables) {
variableValues = typeof variables === "string" ? JSON.parse(variables) : variables;
}
}
catch (_error) {
throw new HttpError(400, "Variables are invalid JSON.", {
headers: defaultSingleResponseHeaders,
});
}
try {
const executionContext = {
request,
document,
operation,
operationName,
extensions,
variables: variableValues,
};
context = contextFactory ? await contextFactory(executionContext) : {};
rootValue = rootValueFactory ? await rootValueFactory(executionContext) : {};
if (operation.operation === "subscription") {
if (!allowedSubscriptionHttpMethods.some((method) => isHttpMethod(method, request.method))) {
throw new HttpError(405, `Can only perform subscription operation from a ${allowedSubscriptionHttpMethods.join(" or ")} request.`, {
headers: [
...defaultSingleResponseHeaders,
{
name: "Allow",
value: allowedSubscriptionHttpMethods.join(", "),
},
],
});
}
const result = await subscribe({
schema,
document,
rootValue,
contextValue: context,
variableValues,
operationName,
});
// If errors are encountered while subscribing to the operation, an execution result
// instead of an AsyncIterable.
if (isAsyncIterable(result)) {
return {
type: "PUSH",
subscribe: async (onResult) => {
try {
for await (const payload of result) {
onResult(formatPayload({
payload,
context,
rootValue,
document,
operation,
}));
}
}
catch (error) {
const payload = {
errors: error.graphqlErrors || [
new GraphQLError(error.message),
],
};
onResult(formatPayload({
payload,
context,
rootValue,
document,
operation,
}));
stopAsyncIteration(result);
}
},
unsubscribe: () => {
stopAsyncIteration(result);
},
};
}
return {
type: "PUSH",
subscribe: async (onResult) => {
onResult(formatPayload({
payload: result,
context,
rootValue,
document,
operation,
}));
},
unsubscribe: () => undefined,
};
}
else {
const result = await execute({
schema,
document,
rootValue,
contextValue: context,
variableValues,
operationName,
});
// Operations that use @defer, @stream and @live will return an `AsyncIterable` instead of an
// execution result.
if (isAsyncIterable(result)) {
return {
type: isHttpMethod("GET", request.method) ? "PUSH" : "MULTIPART_RESPONSE",
subscribe: async (onResult) => {
for await (const payload of result) {
onResult(formatPayload({
payload,
context,
rootValue,
document,
operation,
}));
}
},
unsubscribe: () => {
stopAsyncIteration(result);
},
};
}
else {
return {
type: "RESPONSE",
status: 200,
headers: defaultSingleResponseHeaders,
payload: formatPayload({
payload: result,
context,
rootValue,
document,
operation,
}),
};
}
}
}
catch (executionError) {
if (executionError instanceof GraphQLError) {
throw new HttpError(200, "GraphQLError encountered white executed GraphQL request.", {
graphqlErrors: [executionError],
headers: defaultSingleResponseHeaders,
});
}
else if (executionError instanceof HttpError) {
throw executionError;
}
else {
throw new HttpError(500, "Unexpected error encountered while executing GraphQL request.", {
graphqlErrors: [new GraphQLError(executionError.message)],
headers: defaultSingleResponseHeaders,
});
}
}
}
catch (error) {
const payload = {
errors: error.graphqlErrors || [new GraphQLError(error.message)],
};
if (isEventStreamAccepted) {
return {
type: "PUSH",
subscribe: async (onResult) => {
onResult(formatPayload({
payload,
context,
rootValue,
document,
operation,
}));
},
unsubscribe: () => undefined,
};
}
else {
return {
type: "RESPONSE",
status: error.status || 500,
headers: error.headers || [],
payload: formatPayload({
payload,
context,
rootValue,
document,
operation,
}),
};
}
}
})();
return {
...result,
context,
rootValue,
document,
operation,
};
};