import { graphiQLHtml } from "@/graphql/graphiql.html.js";
import type { Schema } from "@/internal/types.js";
import type { ReadonlyDrizzle } from "@/types/db.js";
import { maxAliasesPlugin } from "@escape.tech/graphql-armor-max-aliases";
import { maxDepthPlugin } from "@escape.tech/graphql-armor-max-depth";
import { maxTokensPlugin } from "@escape.tech/graphql-armor-max-tokens";
import { type GraphQLSchema, printSchema } from "graphql";
import { createYoga } from "graphql-yoga";
import { createMiddleware } from "hono/factory";
import { buildDataLoaderCache, buildGraphQLSchema } from "./index.js";
/**
* Middleware for GraphQL with an interactive web view.
*
* - Docs: https://ponder.sh/docs/api-reference/ponder/api-endpoints#graphql
*
* @example
* import { db } from "ponder:api";
* import schema from "ponder:schema";
* import { graphql } from "@/index.js";
* import { Hono } from "hono";
*
* const app = new Hono();
*
* app.use("/graphql", graphql({ db, schema }));
*
* export default app;
*
*/
export const graphql = (
{ schema }: { db: ReadonlyDrizzle; schema: Schema },
{
maxOperationTokens = 1000,
maxOperationDepth = 100,
maxOperationAliases = 30,
}: {
maxOperationTokens?: number;
maxOperationDepth?: number;
maxOperationAliases?: number;
} = {
// Default limits are from Apollo:
// https://www.apollographql.com/blog/prevent-graph-misuse-with-operation-size-and-complexity-limits
maxOperationTokens: 1000,
maxOperationDepth: 100,
maxOperationAliases: 30,
},
) => {
if (globalThis.PONDER_DATABASE === undefined) {
throw new Error(
"graphql() middleware cannot be initialized outside of a Ponder project",
);
}
const graphqlSchema = buildGraphQLSchema({ schema });
generateSchema({ graphqlSchema }).catch(() => {});
const yoga = createYoga({
graphqlEndpoint: "*", // Disable built-in route validation, use Hono routing instead
schema: graphqlSchema,
context: () => {
const getDataLoader = buildDataLoaderCache(
globalThis.PONDER_DATABASE.readonlyQB,
);
return { qb: globalThis.PONDER_DATABASE.readonlyQB, getDataLoader };
},
maskedErrors: process.env.NODE_ENV === "production",
logging: false,
graphiql: false,
parserAndValidationCache: false,
plugins: [
maxTokensPlugin({ n: maxOperationTokens }),
maxDepthPlugin({ n: maxOperationDepth, ignoreIntrospection: false }),
maxAliasesPlugin({ n: maxOperationAliases, allowList: [] }),
],
});
return createMiddleware(async (c) => {
if (c.req.method === "GET") {
return c.html(graphiQLHtml);
}
const response = await yoga.handle(c.req.raw);
if (response.status === 200) return response;
// set response status to 200, must construct a new response object because Response.status is readonly
// TODO: Figure out why Yoga is returning 500 status codes for GraphQL errors.
const body = response.body;
const headers = new Headers(response.headers);
return new Response(body, {
status: 200,
statusText: "OK",
headers,
});
});
};
async function generateSchema({
graphqlSchema,
}: { graphqlSchema: GraphQLSchema }) {
const fs = await import(/* webpackIgnore: true */ "node:fs");
const path = await import(/* webpackIgnore: true */ "node:path");
fs.mkdirSync(path.join(process.cwd(), "generated"), { recursive: true });
fs.writeFileSync(
path.join(process.cwd(), "generated", "schema.graphql"),
printSchema(graphqlSchema),
"utf-8",
);
globalThis.PONDER_COMMON?.logger.debug({
msg: `Wrote file "generated/schema.graphql"`,
});
}