diff --git a/.changeset/custom-yoga-plugins.md b/.changeset/custom-yoga-plugins.md new file mode 100644 index 000000000..a38812bec --- /dev/null +++ b/.changeset/custom-yoga-plugins.md @@ -0,0 +1,5 @@ +--- +"ponder": patch +--- + +Added `plugins` option to `graphql()` middleware for custom Yoga/Envelop plugins (e.g., response caching, tracing). Also exported `buildGraphQLSchema` and `buildDataLoaderCache` for advanced use cases. diff --git a/packages/core/src/graphql/middleware.test.ts b/packages/core/src/graphql/middleware.test.ts index afe40a5b3..d56ae328b 100644 --- a/packages/core/src/graphql/middleware.test.ts +++ b/packages/core/src/graphql/middleware.test.ts @@ -5,8 +5,9 @@ import { setupIsolatedDatabase, } from "@/_test/setup.js"; import { onchainTable } from "@/drizzle/onchain.js"; +import type { Plugin } from "graphql-yoga"; import { Hono } from "hono"; -import { beforeEach, expect, test } from "vitest"; +import { beforeEach, expect, test, vi } from "vitest"; import { graphql } from "./middleware.js"; beforeEach(setupCommon); @@ -355,3 +356,51 @@ test("graphQLMiddleware interactive", async () => { expect(response.status).toBe(200); }); + +test("graphQLMiddleware supports custom plugins", async () => { + const schema = { + table: onchainTable("table", (t) => ({ + id: t.text().primaryKey(), + })), + }; + + const { database } = await setupDatabaseServices({ + schemaBuild: { schema }, + }); + + globalThis.PONDER_DATABASE = database; + + await database.userQB.raw.insert(schema.table).values({ + id: "0", + }); + + // Create a simple plugin that tracks execution + const onExecuteSpy = vi.fn(); + const customPlugin: Plugin = { + onExecute: onExecuteSpy, + }; + + const app = new Hono().use( + "/graphql", + graphql( + { schema, db: database.readonlyQB.raw }, + { plugins: [customPlugin] }, + ), + ); + + const response = await app.request("/graphql", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: `query { table(id: "0") { id } }`, + }), + }); + + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ + data: { table: { id: "0" } }, + }); + + // Verify the custom plugin was called + expect(onExecuteSpy).toHaveBeenCalled(); +}); diff --git a/packages/core/src/graphql/middleware.ts b/packages/core/src/graphql/middleware.ts index 103ea50ba..679c61a48 100644 --- a/packages/core/src/graphql/middleware.ts +++ b/packages/core/src/graphql/middleware.ts @@ -5,7 +5,7 @@ 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 { type Plugin, createYoga } from "graphql-yoga"; import { createMiddleware } from "hono/factory"; import { buildDataLoaderCache, buildGraphQLSchema } from "./index.js"; @@ -33,16 +33,20 @@ export const graphql = ( maxOperationTokens = 1000, maxOperationDepth = 100, maxOperationAliases = 30, + plugins: customPlugins = [], }: { maxOperationTokens?: number; maxOperationDepth?: number; maxOperationAliases?: number; + /** Custom GraphQL Yoga plugins to extend functionality (e.g., response caching, tracing) */ + plugins?: Plugin[]; } = { // 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, + plugins: [], }, ) => { if (globalThis.PONDER_DATABASE === undefined) { @@ -73,6 +77,7 @@ export const graphql = ( maxTokensPlugin({ n: maxOperationTokens }), maxDepthPlugin({ n: maxOperationDepth, ignoreIntrospection: false }), maxAliasesPlugin({ n: maxOperationAliases, allowList: [] }), + ...customPlugins, ], }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7c227c4fc..a26bcd5a2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -65,6 +65,7 @@ export type { ReadonlyDrizzle } from "@/types/db.js"; export { client } from "@/client/index.js"; export { graphql } from "@/graphql/middleware.js"; +export { buildGraphQLSchema, buildDataLoaderCache } from "@/graphql/index.js"; export { sql,