261 lines
6.8 KiB
TypeScript
261 lines
6.8 KiB
TypeScript
// Based on the code in the `express-graphql` package.
|
|
// https://github.com/graphql/express-graphql/blob/main/src/index.ts
|
|
|
|
import {
|
|
Source,
|
|
parse,
|
|
execute,
|
|
validateSchema,
|
|
validate,
|
|
specifiedRules,
|
|
getOperationAST,
|
|
GraphQLError,
|
|
} from "https://cdn.skypack.dev/graphql@16.5.0?dts";
|
|
|
|
import type {
|
|
GraphQLSchema,
|
|
DocumentNode,
|
|
ValidationRule,
|
|
FormattedExecutionResult,
|
|
GraphQLFormattedError,
|
|
} from "https://cdn.skypack.dev/graphql@16.5.0?dts";
|
|
|
|
import type { Context } from "https://raw.githubusercontent.com/honojs/hono/v2.0.2/deno_dist/mod.ts";
|
|
import type { Next } from "https://raw.githubusercontent.com/honojs/hono/v2.0.2/deno_dist/mod.ts";
|
|
import { parseBody } from "./parse-body.ts";
|
|
|
|
type Options = {
|
|
schema: GraphQLSchema;
|
|
rootValue?: unknown;
|
|
pretty?: boolean;
|
|
validationRules?: ReadonlyArray<ValidationRule>;
|
|
// graphiql?: boolean
|
|
};
|
|
|
|
export const graphqlServer = (options: Options) => {
|
|
const schema = options.schema;
|
|
const rootValue = options.rootValue;
|
|
const pretty = options.pretty ?? false;
|
|
const validationRules = options.validationRules ?? [];
|
|
// const showGraphiQL = options.graphiql ?? false
|
|
|
|
return async (c: Context, next: Next) => {
|
|
// GraphQL HTTP only supports GET and POST methods.
|
|
if (c.req.method !== "GET" && c.req.method !== "POST") {
|
|
return c.json(
|
|
errorMessages(["GraphQL only supports GET and POST requests."]),
|
|
405,
|
|
{
|
|
Allow: "GET, POST",
|
|
}
|
|
);
|
|
}
|
|
|
|
let params: GraphQLParams;
|
|
try {
|
|
params = await getGraphQLParams(c.req);
|
|
} catch (e) {
|
|
if (e instanceof Error) {
|
|
console.error(`${e.stack || e.message}`);
|
|
return c.json(errorMessages([e.message], [e]), 400);
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
const { query, variables, operationName } = params;
|
|
|
|
if (query == null) {
|
|
return c.json(errorMessages(["Must provide query string."]), 400);
|
|
}
|
|
|
|
const schemaValidationErrors = validateSchema(schema);
|
|
if (schemaValidationErrors.length > 0) {
|
|
// Return 500: Internal Server Error if invalid schema.
|
|
return c.json(
|
|
errorMessages(
|
|
["GraphQL schema validation error."],
|
|
schemaValidationErrors
|
|
),
|
|
500
|
|
);
|
|
}
|
|
|
|
let documentAST: DocumentNode;
|
|
try {
|
|
documentAST = parse(new Source(query, "GraphQL request"));
|
|
} catch (syntaxError: unknown) {
|
|
// Return 400: Bad Request if any syntax errors errors exist.
|
|
if (syntaxError instanceof Error) {
|
|
console.error(`${syntaxError.stack || syntaxError.message}`);
|
|
const e = new GraphQLError(syntaxError.message, {
|
|
originalError: syntaxError,
|
|
});
|
|
return c.json(errorMessages(["GraphQL syntax error."], [e]), 400);
|
|
}
|
|
throw syntaxError;
|
|
}
|
|
|
|
// Validate AST, reporting any errors.
|
|
const validationErrors = validate(schema, documentAST, [
|
|
...specifiedRules,
|
|
...validationRules,
|
|
]);
|
|
|
|
if (validationErrors.length > 0) {
|
|
// Return 400: Bad Request if any validation errors exist.
|
|
return c.json(
|
|
errorMessages(["GraphQL validation error."], validationErrors),
|
|
400
|
|
);
|
|
}
|
|
|
|
if (c.req.method === "GET") {
|
|
// Determine if this GET request will perform a non-query.
|
|
const operationAST = getOperationAST(documentAST, operationName);
|
|
if (operationAST && operationAST.operation !== "query") {
|
|
/*
|
|
Now , does not support GraphiQL
|
|
if (showGraphiQL) {
|
|
//return respondWithGraphiQL(response, graphiqlOptions, params)
|
|
}
|
|
*/
|
|
|
|
// Otherwise, report a 405: Method Not Allowed error.
|
|
return c.json(
|
|
errorMessages([
|
|
`Can only perform a ${operationAST.operation} operation from a POST request.`,
|
|
]),
|
|
405,
|
|
{ Allow: "POST" }
|
|
);
|
|
}
|
|
}
|
|
|
|
let result: FormattedExecutionResult;
|
|
|
|
try {
|
|
result = await execute({
|
|
schema,
|
|
document: documentAST,
|
|
rootValue,
|
|
variableValues: variables,
|
|
operationName: operationName,
|
|
});
|
|
} catch (contextError: unknown) {
|
|
if (contextError instanceof Error) {
|
|
console.error(`${contextError.stack || contextError.message}`);
|
|
const e = new GraphQLError(contextError.message, {
|
|
originalError: contextError,
|
|
nodes: documentAST,
|
|
});
|
|
// Return 400: Bad Request if any execution context errors exist.
|
|
return c.json(
|
|
errorMessages(["GraphQL execution context error."], [e]),
|
|
400
|
|
);
|
|
}
|
|
throw contextError;
|
|
}
|
|
|
|
if (!result.data) {
|
|
if (result.errors) {
|
|
return c.json(
|
|
errorMessages([result.errors.toString()], result.errors),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Now, does not support GraphiQL
|
|
if (showGraphiQL) {
|
|
}
|
|
*/
|
|
|
|
if (pretty) {
|
|
const payload = JSON.stringify(result, null, pretty ? 2 : 0);
|
|
return c.text(payload, 200, {
|
|
"Content-Type": "application/json",
|
|
});
|
|
} else {
|
|
return c.json(result);
|
|
}
|
|
|
|
await next(); // XXX
|
|
};
|
|
};
|
|
|
|
export interface GraphQLParams {
|
|
query: string | null;
|
|
variables: { readonly [name: string]: unknown } | null;
|
|
operationName: string | null;
|
|
raw: boolean;
|
|
}
|
|
|
|
export const getGraphQLParams = async (
|
|
request: Request
|
|
): Promise<GraphQLParams> => {
|
|
const urlData = new URLSearchParams(request.url.split("?")[1]);
|
|
const bodyData = await parseBody(request);
|
|
|
|
// GraphQL Query string.
|
|
let query = urlData.get("query") ?? (bodyData.query as string | null);
|
|
|
|
if (typeof query !== "string") {
|
|
query = null;
|
|
}
|
|
|
|
// Parse the variables if needed.
|
|
let variables = (urlData.get("variables") ?? bodyData.variables) as {
|
|
readonly [name: string]: unknown;
|
|
} | null;
|
|
if (typeof variables === "string") {
|
|
try {
|
|
variables = JSON.parse(variables);
|
|
} catch {
|
|
throw Error("Variables are invalid JSON.");
|
|
}
|
|
} else if (typeof variables !== "object") {
|
|
variables = null;
|
|
}
|
|
|
|
// Name of GraphQL operation to execute.
|
|
let operationName =
|
|
urlData.get("operationName") ?? (bodyData.operationName as string | null);
|
|
if (typeof operationName !== "string") {
|
|
operationName = null;
|
|
}
|
|
|
|
const raw = urlData.get("raw") != null || bodyData.raw !== undefined;
|
|
|
|
const params: GraphQLParams = {
|
|
query: query,
|
|
variables: variables,
|
|
operationName: operationName,
|
|
raw: raw,
|
|
};
|
|
|
|
return params;
|
|
};
|
|
|
|
export const errorMessages = (
|
|
messages: string[],
|
|
graphqlErrors?: readonly GraphQLError[] | readonly GraphQLFormattedError[]
|
|
) => {
|
|
if (graphqlErrors) {
|
|
return {
|
|
errors: graphqlErrors,
|
|
};
|
|
}
|
|
|
|
return {
|
|
errors: messages.map((message) => {
|
|
return {
|
|
message: message,
|
|
};
|
|
}),
|
|
};
|
|
};
|
|
|
|
// export const graphiQLResponse = () => {}
|