diff --git a/.changeset/proud-pigs-sniff.md b/.changeset/proud-pigs-sniff.md new file mode 100644 index 000000000..f152585c7 --- /dev/null +++ b/.changeset/proud-pigs-sniff.md @@ -0,0 +1,5 @@ +--- +"@ponder/core": patch +--- + +Fixed a bug where the `codegen` command did not generate `generated/schema.graphql`. diff --git a/packages/core/src/bin/commands/codegen.ts b/packages/core/src/bin/commands/codegen.ts index 9d79892ca..66b0e0805 100644 --- a/packages/core/src/bin/commands/codegen.ts +++ b/packages/core/src/bin/commands/codegen.ts @@ -57,7 +57,8 @@ export async function codegen({ cliOptions }: { cliOptions: CliOptions }) { properties: { cli_command: "codegen" }, }); - runCodegen({ common }); + const graphqlSchema = buildResult.indexingBuild.graphqlSchema; + runCodegen({ common, graphqlSchema }); logger.info({ service: "codegen", msg: "Wrote ponder-env.d.ts" }); logger.info({ service: "codegen", msg: "Wrote schema.graphql" }); diff --git a/packages/core/src/bin/commands/serve.ts b/packages/core/src/bin/commands/serve.ts index 4e9534518..a088c1237 100644 --- a/packages/core/src/bin/commands/serve.ts +++ b/packages/core/src/bin/commands/serve.ts @@ -89,10 +89,10 @@ export async function serve({ cliOptions }: { cliOptions: CliOptions }) { }); const server = await createServer({ + common, app: buildResult.apiBuild.app, routes: buildResult.apiBuild.routes, - common, - schema, + graphqlSchema: buildResult.indexingBuild.graphqlSchema, database, instanceId: process.env.PONDER_EXPERIMENTAL_INSTANCE_ID === undefined diff --git a/packages/core/src/bin/utils/run.test.ts b/packages/core/src/bin/utils/run.test.ts index 6e437f978..a36b532fc 100644 --- a/packages/core/src/bin/utils/run.test.ts +++ b/packages/core/src/bin/utils/run.test.ts @@ -7,6 +7,7 @@ import type { IndexingBuild } from "@/build/index.js"; import { buildSchema } from "@/build/schema.js"; import { createDatabase } from "@/database/index.js"; import { onchainTable } from "@/drizzle/index.js"; +import { buildGraphQLSchema } from "@/graphql/index.js"; import { promiseWithResolvers } from "@ponder/common"; import { beforeEach, expect, test, vi } from "vitest"; import { run } from "./run.js"; @@ -20,7 +21,8 @@ const account = onchainTable("account", (p) => ({ balance: p.bigint().notNull(), })); -// const graphqlSchema = buildGraphQLSchema({ schema: { account } }); +const schema = { account }; +const graphqlSchema = buildGraphQLSchema(schema); test("run() setup", async (context) => { const indexingFunctions = { @@ -28,14 +30,15 @@ test("run() setup", async (context) => { }; const { statements, namespace } = buildSchema({ - schema: { account }, + schema, instanceId: "1234", }); const build: IndexingBuild = { buildId: "buildId", instanceId: "1234", - schema: { account }, + schema, + graphqlSchema, databaseConfig: context.databaseConfig, networks: context.networks, sources: context.sources, @@ -46,7 +49,7 @@ test("run() setup", async (context) => { const database = createDatabase({ common: context.common, - schema: { account }, + schema, databaseConfig: context.databaseConfig, instanceId: "1234", buildId: "buildId", @@ -76,14 +79,15 @@ test("run() setup error", async (context) => { const onReloadableErrorPromiseResolver = promiseWithResolvers(); const { statements, namespace } = buildSchema({ - schema: { account }, + schema, instanceId: "1234", }); const build: IndexingBuild = { buildId: "buildId", instanceId: "1234", - schema: { account }, + schema, + graphqlSchema, databaseConfig: context.databaseConfig, networks: context.networks, sources: context.sources, @@ -94,7 +98,7 @@ test("run() setup error", async (context) => { const database = createDatabase({ common: context.common, - schema: { account }, + schema, databaseConfig: context.databaseConfig, instanceId: "1234", buildId: "buildId", diff --git a/packages/core/src/bin/utils/run.ts b/packages/core/src/bin/utils/run.ts index 48e3945fc..ecae740ae 100644 --- a/packages/core/src/bin/utils/run.ts +++ b/packages/core/src/bin/utils/run.ts @@ -32,7 +32,14 @@ export async function run({ onFatalError: (error: Error) => void; onReloadableError: (error: Error) => void; }) { - const { instanceId, networks, sources, schema, indexingFunctions } = build; + const { + instanceId, + networks, + sources, + schema, + indexingFunctions, + graphqlSchema, + } = build; let isKilled = false; @@ -52,7 +59,7 @@ export async function run({ // starting the server so the app can become responsive more quickly. await database.migrateSync(); - runCodegen({ common }); + runCodegen({ common, graphqlSchema }); // Note: can throw const sync = await createSync({ diff --git a/packages/core/src/bin/utils/runServer.ts b/packages/core/src/bin/utils/runServer.ts index 7cc5f5174..9e0eb0163 100644 --- a/packages/core/src/bin/utils/runServer.ts +++ b/packages/core/src/bin/utils/runServer.ts @@ -15,13 +15,13 @@ export async function runServer({ build: ApiBuild; database: Database; }) { - const { schema, instanceId } = build; + const { instanceId, graphqlSchema } = build; const server = await createServer({ app: build.app, routes: build.routes, common, - schema, + graphqlSchema, database, instanceId, }); diff --git a/packages/core/src/build/schema.ts b/packages/core/src/build/schema.ts index 838475ab6..6d2e431f0 100644 --- a/packages/core/src/build/schema.ts +++ b/packages/core/src/build/schema.ts @@ -1,6 +1,7 @@ import { BuildError } from "@/common/errors.js"; import { type Schema, isPgEnumSym } from "@/drizzle/index.js"; import { getSql } from "@/drizzle/kit/index.js"; +import { buildGraphQLSchema } from "@/graphql/index.js"; import { SQL, getTableColumns, is } from "drizzle-orm"; import { PgBigSerial53, @@ -192,10 +193,12 @@ export const safeBuildSchema = ({ }: { schema: Schema; instanceId: string }) => { try { const result = buildSchema({ schema, instanceId }); + const graphqlSchema = buildGraphQLSchema(schema); return { status: "success", ...result, + graphqlSchema, } as const; } catch (_error) { const buildError = new BuildError((_error as Error).message); diff --git a/packages/core/src/build/service.ts b/packages/core/src/build/service.ts index 933d4975c..28f411555 100644 --- a/packages/core/src/build/service.ts +++ b/packages/core/src/build/service.ts @@ -13,6 +13,7 @@ import type { PonderRoutes } from "@/hono/index.js"; import type { Source } from "@/sync/source.js"; import { serialize } from "@/utils/serialize.js"; import { glob } from "glob"; +import type { GraphQLSchema } from "graphql"; import type { Hono } from "hono"; import { type ViteDevServer, createServer } from "vite"; import { ViteNodeRunner } from "vite-node/client"; @@ -57,6 +58,7 @@ type BaseBuild = { schema: Schema; statements: SqlStatements; namespace: string; + graphqlSchema: GraphQLSchema; }; export type IndexingBuild = BaseBuild & { @@ -748,6 +750,7 @@ const validateAndBuild = async ( schema: schema.schema, statements: buildSchemaResult.statements, namespace: buildSchemaResult.namespace, + graphqlSchema: buildSchemaResult.graphqlSchema, }, }; }; diff --git a/packages/core/src/common/codegen.ts b/packages/core/src/common/codegen.ts index fa4857c6c..0090ab926 100644 --- a/packages/core/src/common/codegen.ts +++ b/packages/core/src/common/codegen.ts @@ -1,6 +1,7 @@ -import { writeFileSync } from "node:fs"; +import { mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; import type { Common } from "@/common/common.js"; +import { type GraphQLSchema, printSchema } from "graphql"; export const ponderEnv = `// This file enables type checking and editor autocomplete for this Ponder project. // After upgrading, you may find that changes have been made to this file. @@ -31,7 +32,10 @@ declare module "@/generated" { } `; -export function runCodegen({ common }: { common: Common }) { +export function runCodegen({ + common, + graphqlSchema, +}: { common: Common; graphqlSchema: GraphQLSchema }) { writeFileSync( path.join(common.options.rootDir, "ponder-env.d.ts"), ponderEnv, @@ -42,4 +46,16 @@ export function runCodegen({ common }: { common: Common }) { service: "codegen", msg: "Wrote new file at ponder-env.d.ts", }); + + mkdirSync(common.options.generatedDir, { recursive: true }); + writeFileSync( + path.join(common.options.generatedDir, "schema.graphql"), + printSchema(graphqlSchema), + "utf-8", + ); + + common.logger.debug({ + service: "codegen", + msg: "Wrote new file at generated/schema.graphql", + }); } diff --git a/packages/core/src/graphql/index.test.ts b/packages/core/src/graphql/index.test.ts index 9c221e88a..6da6be612 100644 --- a/packages/core/src/graphql/index.test.ts +++ b/packages/core/src/graphql/index.test.ts @@ -3,7 +3,9 @@ import { setupDatabaseServices, setupIsolatedDatabase, } from "@/_test/setup.js"; +import type { Database } from "@/database/index.js"; import { onchainEnum, onchainTable, primaryKey } from "@/drizzle/index.js"; +import type { MetadataStore } from "@/indexing-store/metadata.js"; import { relations } from "drizzle-orm"; import { type GraphQLType, execute, parse } from "graphql"; import { beforeEach, expect, test, vi } from "vitest"; @@ -12,6 +14,12 @@ import { buildDataLoaderCache, buildGraphQLSchema } from "./index.js"; beforeEach(setupCommon); beforeEach(setupIsolatedDatabase); +function buildContextValue(database: Database, metadataStore: MetadataStore) { + const drizzle = database.drizzle; + const getDataLoader = buildDataLoaderCache({ drizzle }); + return { drizzle, metadataStore, getDataLoader }; +} + test("metadata", async (context) => { const schema = {}; @@ -19,12 +27,11 @@ test("metadata", async (context) => { context, { schema }, ); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); await metadataStore.setStatus({ mainnet: { @@ -99,8 +106,7 @@ test("scalar, scalar not null, scalar array, scalar array not null", async (cont const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -135,7 +141,7 @@ test("scalar, scalar not null, scalar array, scalar array not null", async (cont bigintArrayNotNull: [0n], }); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); const result = await query(` query { @@ -224,8 +230,7 @@ test("enum, enum not null, enum array, enum array not null", async (context) => const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -237,7 +242,7 @@ test("enum, enum not null, enum array, enum array not null", async (context) => enumArrayNotNull: ["A"], }); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); const result = await query(` query { @@ -276,8 +281,7 @@ test("json, json not null", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -287,7 +291,7 @@ test("json, json not null", async (context) => { jsonNotNull: { kevin: 52 }, }); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); const result = await query(` query { @@ -332,8 +336,7 @@ test("singular", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -347,7 +350,7 @@ test("singular", async (context) => { { owner: "1", spender: "0", amount: 100n }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); let result = await query(` query { @@ -430,8 +433,7 @@ test("singular with one relation", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -443,7 +445,7 @@ test("singular with one relation", async (context) => { .insert(schema.pet) .values({ id: "dog1", ownerIdNotNull: "jake" }); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); const result = await query(` query { @@ -496,8 +498,7 @@ test("singular with many relation", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -510,7 +511,7 @@ test("singular with many relation", async (context) => { { id: "dog3", ownerId: "kyle" }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); const result = await query(` query { @@ -554,8 +555,7 @@ test("singular with many relation using filter", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -568,7 +568,7 @@ test("singular with many relation using filter", async (context) => { { id: "dog3", age: 3, ownerId: "jake" }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); const result = await query(` query { @@ -617,8 +617,7 @@ test("plural with one relation uses dataloader", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -631,7 +630,7 @@ test("plural with one relation uses dataloader", async (context) => { { id: "dog3", ownerId: "kyle" }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); // @ts-expect-error const personFindManySpy = vi.spyOn(database.drizzle.query.person, "findMany"); @@ -689,11 +688,9 @@ test("filter input type", async (context) => { })); const schema = { simpleEnum, table }; - const { database, cleanup } = await setupDatabaseServices(context, { - schema, - }); + const { cleanup } = await setupDatabaseServices(context, { schema }); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); const typeMap = graphqlSchema.getTypeMap(); const tableFilterType = typeMap.tableFilter!; const fields = (tableFilterType.toConfig() as any).fields as Record< @@ -823,8 +820,7 @@ test("filter universal", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -832,7 +828,7 @@ test("filter universal", async (context) => { .insert(schema.person) .values([{ id: 1n }, { id: 2n }, { id: 3n }]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); let result = await query(` query { @@ -873,8 +869,7 @@ test("filter singular", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -882,7 +877,7 @@ test("filter singular", async (context) => { .insert(schema.person) .values([{ id: "0x01" }, { id: "0x02" }, { id: "0x03" }]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); let result = await query(` query { @@ -926,8 +921,7 @@ test("filter plural", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -937,7 +931,7 @@ test("filter plural", async (context) => { { id: "3", number: [5, 6, 7] }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); let result = await query(` query { @@ -1035,8 +1029,7 @@ test("filter numeric", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -1067,7 +1060,7 @@ test("filter numeric", async (context) => { }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); let result = await query(` query { @@ -1160,8 +1153,7 @@ test("filter string", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -1171,7 +1163,7 @@ test("filter string", async (context) => { { id: "3", text: "three", hex: "0xef0" }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); let result = await query(` query { @@ -1230,8 +1222,7 @@ test("filter and/or", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -1243,7 +1234,7 @@ test("filter and/or", async (context) => { { id: "id5", name: "Winston", age: 12 }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); const result = await query(` query { @@ -1281,8 +1272,7 @@ test("order by", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -1313,7 +1303,7 @@ test("order by", async (context) => { }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); let result = await query(` query { @@ -1401,8 +1391,7 @@ test("limit", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -1410,7 +1399,7 @@ test("limit", async (context) => { await indexingStore.insert(schema.person).values({ id: String(i) }); } - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); // Default limit of 50 let result = await query(` @@ -1468,8 +1457,7 @@ test("cursor pagination ascending", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -1485,7 +1473,7 @@ test("cursor pagination ascending", async (context) => { { id: "id9", name: "Last" }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); let result = await query(` query { @@ -1611,8 +1599,7 @@ test("cursor pagination descending", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -1624,7 +1611,7 @@ test("cursor pagination descending", async (context) => { { id: "id5", name: "Winston", age: 12 }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); let result = await query(` query { @@ -1743,8 +1730,7 @@ test("cursor pagination start and end cursors", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -1756,7 +1742,7 @@ test("cursor pagination start and end cursors", async (context) => { { id: "id5", name: "Winston", age: 12 }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); const result = await query(` query { @@ -1809,8 +1795,7 @@ test("cursor pagination has previous page", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -1822,7 +1807,7 @@ test("cursor pagination has previous page", async (context) => { { id: "id5", name: "Winston", age: 12 }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); let result = await query(` query { @@ -1895,8 +1880,7 @@ test("cursor pagination composite primary key", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -1909,7 +1893,7 @@ test("cursor pagination composite primary key", async (context) => { { owner: "jenny", spender: "bill", amount: 800n }, ]); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); let result = await query(` query { @@ -2035,8 +2019,7 @@ test("column casing", async (context) => { const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); - const getDataLoader = buildDataLoaderCache({ drizzle: database.drizzle }); - const contextValue = { metadataStore, getDataLoader }; + const contextValue = buildContextValue(database, metadataStore); const query = (source: string) => execute({ schema: graphqlSchema, contextValue, document: parse(source) }); @@ -2046,7 +2029,7 @@ test("column casing", async (context) => { camelCase: "0", }); - const graphqlSchema = buildGraphQLSchema(database.drizzle); + const graphqlSchema = buildGraphQLSchema(schema); const result = await query(` query { diff --git a/packages/core/src/graphql/index.ts b/packages/core/src/graphql/index.ts index ba8edd4db..ab43152d3 100644 --- a/packages/core/src/graphql/index.ts +++ b/packages/core/src/graphql/index.ts @@ -12,8 +12,10 @@ import { arrayContained, arrayContains, asc, + createTableRelationsHelpers, desc, eq, + extractTablesRelationalConfig, getTableColumns, gt, gte, @@ -59,6 +61,7 @@ type Parent = Record; type Context = { getDataLoader: ReturnType; metadataStore: MetadataStore; + drizzle: Drizzle<{ [key: string]: OnchainTable }>; }; type PluralArgs = { @@ -73,10 +76,15 @@ type PluralArgs = { const DEFAULT_LIMIT = 50 as const; const MAX_LIMIT = 1000 as const; -export function buildGraphQLSchema(db: Drizzle): GraphQLSchema { - const tables = Object.values(db._.schema ?? {}) as TableRelationalConfig[]; +export function buildGraphQLSchema(schema: Schema): GraphQLSchema { + const tablesConfig = extractTablesRelationalConfig( + schema, + createTableRelationsHelpers, + ); + + const tables = Object.values(tablesConfig.tables) as TableRelationalConfig[]; - const enums = Object.entries(db._.fullSchema ?? {}).filter( + const enums = Object.entries(schema).filter( (el): el is [string, PgEnum<[string, ...string[]]>] => isPgEnum(el[1]), ); const enumTypes: Record = {}; @@ -208,13 +216,6 @@ export function buildGraphQLSchema(db: Drizzle): GraphQLSchema { `Internal error: Referenced entity type not found for table "${referencedTable.tsName}" `, ); - const baseQuery = (db as Drizzle<{ [key: string]: OnchainTable }>) - .query[referencedTable.tsName]; - if (!baseQuery) - throw new Error( - `Internal error: Referenced table "${referencedTable.tsName}" not found in RQB`, - ); - if (is(relation, One)) { const fields = relation.config?.fields ?? []; const references = relation.config?.references ?? []; @@ -279,7 +280,7 @@ export function buildGraphQLSchema(db: Drizzle): GraphQLSchema { after: { type: GraphQLString }, limit: { type: GraphQLInt }, }, - resolve: async (parent, args: PluralArgs, _context) => { + resolve: async (parent, args: PluralArgs, context) => { const relationalConditions = []; for (let i = 0; i < references.length; i++) { const column = fields[i]!; @@ -287,6 +288,12 @@ export function buildGraphQLSchema(db: Drizzle): GraphQLSchema { relationalConditions.push(eq(column, value)); } + const baseQuery = context.drizzle.query[referencedTable.tsName]; + if (!baseQuery) + throw new Error( + `Internal error: Referenced table "${referencedTable.tsName}" not found in RQB`, + ); + return executePluralQuery( table, baseQuery, @@ -329,14 +336,6 @@ export function buildGraphQLSchema(db: Drizzle): GraphQLSchema { table.tsName.charAt(0).toLowerCase() + table.tsName.slice(1); const pluralFieldName = `${singularFieldName}s`; - const baseQuery = (db as Drizzle<{ [key: string]: OnchainTable }>).query[ - table.tsName - ]; - if (!baseQuery) - throw new Error( - `Internal error: Table "${table.tsName}" not found in RQB`, - ); - queryFields[singularFieldName] = { type: entityType, // Find the primary key columns and GraphQL core types and include them @@ -351,7 +350,13 @@ export function buildGraphQLSchema(db: Drizzle): GraphQLSchema { }, ]), ), - resolve: async (_parent, args, _context) => { + resolve: async (_parent, args, context) => { + const baseQuery = context.drizzle.query[table.tsName]; + if (!baseQuery) + throw new Error( + `Internal error: Table "${table.tsName}" not found in RQB`, + ); + // The `args` object here should be a valid `where` argument that // uses the `eq` shorthand for each primary key column. const whereConditions = buildWhereConditions(args, table.columns); @@ -373,7 +378,13 @@ export function buildGraphQLSchema(db: Drizzle): GraphQLSchema { after: { type: GraphQLString }, limit: { type: GraphQLInt }, }, - resolve: async (_parent, args: PluralArgs, _context) => { + resolve: async (_parent, args: PluralArgs, context) => { + const baseQuery = context.drizzle.query[table.tsName]; + if (!baseQuery) + throw new Error( + `Internal error: Table "${table.tsName}" not found in RQB`, + ); + return executePluralQuery(table, baseQuery, args); }, }; diff --git a/packages/core/src/graphql/middleware.test.ts b/packages/core/src/graphql/middleware.test.ts index 1fb2b545d..b6444a3ba 100644 --- a/packages/core/src/graphql/middleware.test.ts +++ b/packages/core/src/graphql/middleware.test.ts @@ -7,6 +7,7 @@ import { onchainTable } from "@/drizzle/index.js"; import { Hono } from "hono"; import { createMiddleware } from "hono/factory"; import { beforeEach, expect, test } from "vitest"; +import { buildGraphQLSchema } from "./index.js"; import { graphql } from "./middleware.js"; beforeEach(setupCommon); @@ -25,13 +26,15 @@ test("middleware serves request", async (context) => { })), }; + const graphqlSchema = buildGraphQLSchema(schema); + const { database, indexingStore, metadataStore, cleanup } = await setupDatabaseServices(context, { schema }); const contextMiddleware = createMiddleware(async (c, next) => { - c.set("common", context.common); - c.set("db", database.drizzle); c.set("metadataStore", metadataStore); + c.set("graphqlSchema", graphqlSchema); + c.set("db", database.drizzle); await next(); }); @@ -101,15 +104,17 @@ test("middleware throws error when extra filter is applied", async (context) => })), }; + const graphqlSchema = buildGraphQLSchema(schema); + const { database, metadataStore, cleanup } = await setupDatabaseServices( context, { schema }, ); const contextMiddleware = createMiddleware(async (c, next) => { - c.set("common", context.common); - c.set("db", database.drizzle); c.set("metadataStore", metadataStore); + c.set("graphqlSchema", graphqlSchema); + c.set("db", database.drizzle); await next(); }); @@ -151,15 +156,17 @@ test("graphQLMiddleware throws error for token limit", async (context) => { })), }; + const graphqlSchema = buildGraphQLSchema(schema); + const { database, metadataStore, cleanup } = await setupDatabaseServices( context, { schema }, ); const contextMiddleware = createMiddleware(async (c, next) => { - c.set("common", context.common); - c.set("db", database.drizzle); c.set("metadataStore", metadataStore); + c.set("graphqlSchema", graphqlSchema); + c.set("db", database.drizzle); await next(); }); @@ -207,15 +214,17 @@ test("graphQLMiddleware throws error for depth limit", async (context) => { })), }; + const graphqlSchema = buildGraphQLSchema(schema); + const { database, metadataStore, cleanup } = await setupDatabaseServices( context, { schema }, ); const contextMiddleware = createMiddleware(async (c, next) => { - c.set("common", context.common); - c.set("db", database.drizzle); c.set("metadataStore", metadataStore); + c.set("graphqlSchema", graphqlSchema); + c.set("db", database.drizzle); await next(); }); @@ -263,15 +272,17 @@ test("graphQLMiddleware throws error for max aliases", async (context) => { })), }; + const graphqlSchema = buildGraphQLSchema(schema); + const { database, metadataStore, cleanup } = await setupDatabaseServices( context, { schema }, ); const contextMiddleware = createMiddleware(async (c, next) => { - c.set("common", context.common); - c.set("db", database.drizzle); c.set("metadataStore", metadataStore); + c.set("graphqlSchema", graphqlSchema); + c.set("db", database.drizzle); await next(); }); @@ -323,15 +334,18 @@ test("graphQLMiddleware throws error for max aliases", async (context) => { }); test("graphQLMiddleware interactive", async (context) => { + const schema = {}; + const graphqlSchema = buildGraphQLSchema(schema); + const { database, metadataStore, cleanup } = await setupDatabaseServices( context, - { schema: {} }, + { schema }, ); const contextMiddleware = createMiddleware(async (c, next) => { - c.set("common", context.common); - c.set("db", database.drizzle); c.set("metadataStore", metadataStore); + c.set("graphqlSchema", graphqlSchema); + c.set("db", database.drizzle); await next(); }); diff --git a/packages/core/src/graphql/middleware.ts b/packages/core/src/graphql/middleware.ts index 3f1b5d0be..fe1089844 100644 --- a/packages/core/src/graphql/middleware.ts +++ b/packages/core/src/graphql/middleware.ts @@ -1,13 +1,10 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import path from "node:path"; import { graphiQLHtml } from "@/ui/graphiql.html.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 { printSchema } from "graphql"; import { type YogaServerInstance, createYoga } from "graphql-yoga"; import { createMiddleware } from "hono/factory"; -import { buildDataLoaderCache, buildGraphQLSchema } from "./index.js"; +import { buildDataLoaderCache } from "./index.js"; /** * Middleware for GraphQL with an interactive web view. @@ -47,29 +44,14 @@ export const graphql = ( if (yoga === undefined) { const metadataStore = c.get("metadataStore"); - const common = c.get("common"); + const graphqlSchema = c.get("graphqlSchema"); const drizzle = c.get("db"); - const graphqlSchema = buildGraphQLSchema(drizzle); - - // Write schema.graphql once on startup - mkdirSync(common.options.generatedDir, { recursive: true }); - writeFileSync( - path.join(common.options.generatedDir, "schema.graphql"), - printSchema(graphqlSchema), - "utf-8", - ); - - common.logger.debug({ - service: "codegen", - msg: "Wrote new file at generated/schema.graphql", - }); - yoga = createYoga({ schema: graphqlSchema, context: () => { const getDataLoader = buildDataLoaderCache({ drizzle }); - return { metadataStore, getDataLoader }; + return { drizzle, metadataStore, getDataLoader }; }, graphqlEndpoint: c.req.path, maskedErrors: process.env.NODE_ENV === "production", diff --git a/packages/core/src/server/index.test.ts b/packages/core/src/server/index.test.ts index dca312acf..2a1350bea 100644 --- a/packages/core/src/server/index.test.ts +++ b/packages/core/src/server/index.test.ts @@ -3,6 +3,7 @@ import { setupDatabaseServices, setupIsolatedDatabase, } from "@/_test/setup.js"; +import { buildGraphQLSchema } from "@/graphql/index.js"; import type { Context } from "@/hono/context.js"; import { getMetadataStore } from "@/indexing-store/metadata.js"; import { Hono } from "hono"; @@ -19,7 +20,7 @@ test("port", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -27,7 +28,7 @@ test("port", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -45,7 +46,7 @@ test("listens on ipv4", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -63,7 +64,7 @@ test("listens on ipv6", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -81,7 +82,7 @@ test("not ready", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -100,7 +101,7 @@ test("ready", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -124,7 +125,7 @@ test("health", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -143,7 +144,7 @@ test("healthy PUT", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -164,7 +165,7 @@ test("metrics", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -183,7 +184,7 @@ test("metrics error", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -205,7 +206,7 @@ test("metrics PUT", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -226,7 +227,7 @@ test("metrics unmatched route", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -251,7 +252,7 @@ test("missing route", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -272,7 +273,7 @@ test("custom api route", async (context) => { routes: [ { method: "GET", pathOrHandlers: ["/hi", (c: Context) => c.text("hi")] }, ], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -294,7 +295,7 @@ test("custom hono route", async (context) => { common: context.common, app, routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); @@ -316,7 +317,7 @@ test.skip("kill", async (context) => { common: context.common, app: new Hono(), routes: [], - schema: {}, + graphqlSchema: buildGraphQLSchema({}), database, }); diff --git a/packages/core/src/server/index.ts b/packages/core/src/server/index.ts index dcffffbb3..8b0d7e6eb 100644 --- a/packages/core/src/server/index.ts +++ b/packages/core/src/server/index.ts @@ -1,7 +1,6 @@ import http from "node:http"; import type { Common } from "@/common/common.js"; import type { Database } from "@/database/index.js"; -import type { Schema } from "@/drizzle/index.js"; import { graphql } from "@/graphql/middleware.js"; import { type PonderRoutes, applyHonoRoutes } from "@/hono/index.js"; import { @@ -10,6 +9,7 @@ import { } from "@/indexing-store/metadata.js"; import { startClock } from "@/utils/timer.js"; import { serve } from "@hono/node-server"; +import type { GraphQLSchema } from "graphql"; import { Hono } from "hono"; import { cors } from "hono/cors"; import { createMiddleware } from "hono/factory"; @@ -26,14 +26,14 @@ export async function createServer({ app: userApp, routes: userRoutes, common, - schema, + graphqlSchema, database, instanceId, }: { app: Hono; routes: PonderRoutes; common: Common; - schema: Schema; + graphqlSchema: GraphQLSchema; database: Database; instanceId?: string; }): Promise { @@ -93,10 +93,9 @@ export async function createServer({ // context required for graphql middleware and hono middleware const contextMiddleware = createMiddleware(async (c, next) => { - c.set("common", common); c.set("db", database.drizzle); c.set("metadataStore", metadataStore); - c.set("schema", schema); + c.set("graphqlSchema", graphqlSchema); await next(); });