diff --git a/.changeset/petite-peaches-watch.md b/.changeset/petite-peaches-watch.md new file mode 100644 index 000000000..2fb43b5ae --- /dev/null +++ b/.changeset/petite-peaches-watch.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-schema": minor +--- + +Split database schemas into Ponder Schema, ENSIndexer Schema, and ENSNode Schema. diff --git a/.changeset/vast-comics-burn.md b/.changeset/vast-comics-burn.md new file mode 100644 index 000000000..98e82272e --- /dev/null +++ b/.changeset/vast-comics-burn.md @@ -0,0 +1,5 @@ +--- +"ensapi": minor +--- + +Updated ENSDb connections to be always read-only. diff --git a/apps/ensapi/src/lib/db.ts b/apps/ensapi/src/lib/db.ts index 0148d37ec..db88e5e2c 100644 --- a/apps/ensapi/src/lib/db.ts +++ b/apps/ensapi/src/lib/db.ts @@ -1,11 +1,14 @@ import config from "@/config"; -import * as schema from "@ensnode/ensnode-schema"; +import * as ensIndexerSchema from "@ensnode/ensnode-schema/ensindexer"; -import { makeDrizzle } from "@/lib/handlers/drizzle"; +import { makeReadOnlyDrizzle } from "@/lib/handlers/drizzle"; -export const db = makeDrizzle({ +/** + * Read-only Drizzle instance for ENSDb queries to ENSIndexer Schema + */ +export const db = makeReadOnlyDrizzle({ databaseUrl: config.databaseUrl, databaseSchema: config.databaseSchemaName, - schema, + schema: ensIndexerSchema, }); diff --git a/apps/ensapi/src/lib/handlers/drizzle.ts b/apps/ensapi/src/lib/handlers/drizzle.ts index a5d8f36da..173252c90 100644 --- a/apps/ensapi/src/lib/handlers/drizzle.ts +++ b/apps/ensapi/src/lib/handlers/drizzle.ts @@ -1,5 +1,6 @@ import { setDatabaseSchema } from "@ponder/client"; import { drizzle } from "drizzle-orm/node-postgres"; +import { parseIntoClientConfig } from "pg-connection-string"; import { makeLogger } from "@/lib/logger"; @@ -8,21 +9,32 @@ type Schema = { [name: string]: unknown }; const logger = makeLogger("drizzle"); /** - * Makes a Drizzle DB object. + * Makes a read-only Drizzle DB object. */ -export const makeDrizzle = ({ +export const makeReadOnlyDrizzle = ({ schema, databaseUrl, databaseSchema, }: { schema: SCHEMA; databaseUrl: string; - databaseSchema: string; + databaseSchema?: string; }) => { - // monkeypatch schema onto tables - setDatabaseSchema(schema, databaseSchema); + // monkeypatch schema onto tables if databaseSchema is provided + if (databaseSchema) { + setDatabaseSchema(schema, databaseSchema); + } - return drizzle(databaseUrl, { + const parsedConfig = parseIntoClientConfig(databaseUrl); + const existingOptions = parsedConfig.options || ""; + const readOnlyOption = "-c default_transaction_read_only=on"; + + return drizzle({ + connection: { + ...parsedConfig, + // Combine existing options from URL with read-only requirement + options: existingOptions ? `${existingOptions} ${readOnlyOption}` : readOnlyOption, + }, schema, casing: "snake_case", logger: { diff --git a/apps/ensindexer/src/lib/ensdb-client/drizzle.ts b/apps/ensindexer/src/lib/ensdb-client/drizzle.ts index b5cc9b5c7..079e18c58 100644 --- a/apps/ensindexer/src/lib/ensdb-client/drizzle.ts +++ b/apps/ensindexer/src/lib/ensdb-client/drizzle.ts @@ -2,7 +2,6 @@ // We currently duplicate the makeDrizzle function, as we don't have // a shared package for backend code yet. When we do, we can move // this function to the shared package and import it in both places. -import { setDatabaseSchema } from "@ponder/client"; import { drizzle } from "drizzle-orm/node-postgres"; type Schema = { [name: string]: unknown }; @@ -13,14 +12,9 @@ type Schema = { [name: string]: unknown }; export const makeDrizzle = ({ schema, databaseUrl, - databaseSchema, }: { schema: SCHEMA; databaseUrl: string; - databaseSchema: string; }) => { - // monkeypatch schema onto tables - setDatabaseSchema(schema, databaseSchema); - return drizzle(databaseUrl, { schema, casing: "snake_case" }); }; diff --git a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.mock.ts b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.mock.ts index 7387cc19f..91f7cf273 100644 --- a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.mock.ts +++ b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.mock.ts @@ -23,6 +23,10 @@ export const databaseUrl = "postgres://user:pass@localhost:5432/ensdb"; export const databaseSchemaName = "public"; +// This is the same as the default value of config.databaseSchemaName, +// which is used as the ensIndexerRef for multi-tenancy in ENSDb. +export const ensIndexerRef = databaseSchemaName; + export const publicConfig = { databaseSchemaName, ensRainbowPublicConfig: { diff --git a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts index 0462b7788..480bc8a74 100644 --- a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts +++ b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { ensNodeMetadata } from "@ensnode/ensnode-schema"; +import * as ensNodeSchema from "@ensnode/ensnode-schema/ensnode"; import { deserializeCrossChainIndexingStatusSnapshot, EnsNodeMetadataKeys, @@ -50,26 +50,20 @@ describe("EnsDbClient", () => { describe("getEnsDbVersion", () => { it("returns undefined when no record exists", async () => { // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); // act & assert await expect(client.getEnsDbVersion()).resolves.toBeUndefined(); expect(selectMock).toHaveBeenCalledTimes(1); - expect(fromMock).toHaveBeenCalledWith(ensNodeMetadata); + expect(fromMock).toHaveBeenCalledWith(ensNodeSchema.ensNodeMetadata); }); it("returns value when one record exists", async () => { // arrange selectResult.current = [{ value: "0.1.0" }]; - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); // act & assert await expect(client.getEnsDbVersion()).resolves.toBe("0.1.0"); @@ -81,10 +75,7 @@ describe("EnsDbClient", () => { // arrange selectResult.current = [{ value: "0.1.0" }, { value: "0.1.1" }]; - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); // act & assert await expect(client.getEnsDbVersion()).rejects.toThrowError(/ensdb_version/i); @@ -94,10 +85,7 @@ describe("EnsDbClient", () => { describe("getEnsIndexerPublicConfig", () => { it("returns undefined when no record exists", async () => { // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); // act & assert await expect(client.getEnsIndexerPublicConfig()).resolves.toBeUndefined(); @@ -108,10 +96,7 @@ describe("EnsDbClient", () => { const serializedConfig = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig); selectResult.current = [{ value: serializedConfig }]; - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); // act & assert await expect(client.getEnsIndexerPublicConfig()).resolves.toStrictEqual( @@ -125,10 +110,7 @@ describe("EnsDbClient", () => { // arrange selectResult.current = [{ value: ensDbClientMock.serializedSnapshot }]; - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); const expected = deserializeCrossChainIndexingStatusSnapshot( ensDbClientMock.serializedSnapshot, ); @@ -141,22 +123,20 @@ describe("EnsDbClient", () => { describe("upsertEnsDbVersion", () => { it("writes the database version metadata", async () => { // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); // act await client.upsertEnsDbVersion("0.2.0"); // assert - expect(insertMock).toHaveBeenCalledWith(ensNodeMetadata); + expect(insertMock).toHaveBeenCalledWith(ensNodeSchema.ensNodeMetadata); expect(valuesMock).toHaveBeenCalledWith({ + ensIndexerRef: ensDbClientMock.ensIndexerRef, key: EnsNodeMetadataKeys.EnsDbVersion, value: "0.2.0", }); expect(onConflictDoUpdateMock).toHaveBeenCalledWith({ - target: ensNodeMetadata.key, + target: [ensNodeSchema.ensNodeMetadata.ensIndexerRef, ensNodeSchema.ensNodeMetadata.key], set: { value: "0.2.0" }, }); }); @@ -165,10 +145,7 @@ describe("EnsDbClient", () => { describe("upsertEnsIndexerPublicConfig", () => { it("serializes and writes the public config", async () => { // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); const expectedValue = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig); // act @@ -176,6 +153,7 @@ describe("EnsDbClient", () => { // assert expect(valuesMock).toHaveBeenCalledWith({ + ensIndexerRef: ensDbClientMock.ensIndexerRef, key: EnsNodeMetadataKeys.EnsIndexerPublicConfig, value: expectedValue, }); @@ -185,10 +163,7 @@ describe("EnsDbClient", () => { describe("upsertIndexingStatusSnapshot", () => { it("serializes and writes the indexing status snapshot", async () => { // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); const snapshot = deserializeCrossChainIndexingStatusSnapshot( ensDbClientMock.serializedSnapshot, ); @@ -199,6 +174,7 @@ describe("EnsDbClient", () => { // assert expect(valuesMock).toHaveBeenCalledWith({ + ensIndexerRef: ensDbClientMock.ensIndexerRef, key: EnsNodeMetadataKeys.EnsIndexerIndexingStatus, value: expectedValue, }); diff --git a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts index 3af5de58f..d743f3590 100644 --- a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts +++ b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts @@ -1,7 +1,7 @@ import type { NodePgDatabase } from "drizzle-orm/node-postgres"; -import { eq, sql } from "drizzle-orm/sql"; +import { and, eq, sql } from "drizzle-orm/sql"; -import { ensNodeMetadata } from "@ensnode/ensnode-schema"; +import * as ensNodeSchema from "@ensnode/ensnode-schema/ensnode"; import { type CrossChainIndexingStatusSnapshot, deserializeCrossChainIndexingStatusSnapshot, @@ -20,21 +20,12 @@ import { import { makeDrizzle } from "./drizzle"; -/** - * ENSDb Client Schema - * - * Includes schema definitions for {@link EnsDbClient} queries and mutations. - */ -const schema = { - ensNodeMetadata, -}; - /** * Drizzle database * * Allows interacting with Postgres database for ENSDb, using Drizzle ORM. */ -interface DrizzleDb extends NodePgDatabase {} +interface DrizzleDb extends NodePgDatabase {} /** * ENSDb Client @@ -53,16 +44,22 @@ export class EnsDbClient implements EnsDbClientQuery, EnsDbClientMutation { */ private db: DrizzleDb; + /** + * ENSIndexer reference string for multi-tenancy in ENSDb. + */ + private ensIndexerRef: string; + /** * @param databaseUrl connection string for ENSDb Postgres database - * @param databaseSchemaName Postgres schema name for ENSDb tables + * @param ensIndexerRef reference string for ENSIndexer instance (used for multi-tenancy in ENSDb) */ - constructor(databaseUrl: string, databaseSchemaName: string) { + constructor(databaseUrl: string, ensIndexerRef: string) { this.db = makeDrizzle({ - databaseSchema: databaseSchemaName, databaseUrl, - schema, + schema: ensNodeSchema, }); + + this.ensIndexerRef = ensIndexerRef; } /** @@ -147,15 +144,21 @@ export class EnsDbClient implements EnsDbClientQuery, EnsDbClientMutation { * * @returns selected record in ENSDb. * @throws when more than one matching metadata record is found - * (should be impossible given the PK constraint on 'key') + * (should be impossible given the composite PK constraint on + * 'ensIndexerRef' and 'key'). */ private async getEnsNodeMetadata( metadata: Pick, ): Promise { const result = await this.db .select() - .from(ensNodeMetadata) - .where(eq(ensNodeMetadata.key, metadata.key)); + .from(ensNodeSchema.ensNodeMetadata) + .where( + and( + eq(ensNodeSchema.ensNodeMetadata.ensIndexerRef, this.ensIndexerRef), + eq(ensNodeSchema.ensNodeMetadata.key, metadata.key), + ), + ); if (result.length === 0) { return undefined; @@ -176,25 +179,16 @@ export class EnsDbClient implements EnsDbClientQuery, EnsDbClientMutation { private async upsertEnsNodeMetadata< EnsNodeMetadataType extends SerializedEnsNodeMetadata = SerializedEnsNodeMetadata, >(metadata: EnsNodeMetadataType): Promise { - await this.db.transaction(async (tx) => { - // Ponder live-query triggers insert into live_query_tables. - // Because this worker writes outside the Ponder runtime connection pool, - // the temp table must be ensured to exist on this connection. Without this, - // the upsert would fail with "relation 'live_query_tables' does not exist" error. - await tx.execute( - sql`CREATE TEMP TABLE IF NOT EXISTS live_query_tables (table_name TEXT PRIMARY KEY)`, - ); - - await tx - .insert(ensNodeMetadata) - .values({ - key: metadata.key, - value: metadata.value, - }) - .onConflictDoUpdate({ - target: ensNodeMetadata.key, - set: { value: metadata.value }, - }); - }); + await this.db + .insert(ensNodeSchema.ensNodeMetadata) + .values({ + ensIndexerRef: this.ensIndexerRef, + key: metadata.key, + value: metadata.value, + }) + .onConflictDoUpdate({ + target: [ensNodeSchema.ensNodeMetadata.ensIndexerRef, ensNodeSchema.ensNodeMetadata.key], + set: { value: metadata.value }, + }); } } diff --git a/apps/ensindexer/src/lib/ensdb-client/singleton.ts b/apps/ensindexer/src/lib/ensdb-client/singleton.ts index 3ab2225fd..a47ef727c 100644 --- a/apps/ensindexer/src/lib/ensdb-client/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb-client/singleton.ts @@ -2,7 +2,11 @@ import config from "@/config"; import { EnsDbClient } from "./ensdb-client"; +// config.databaseSchemaName is unique per ENSIndexer instance and is used as the ensIndexerRef +// tenant key in the shared ENSNode schema (ensnode.*). +const ensIndexerRef = config.databaseSchemaName; + /** * Singleton instance of {@link EnsDbClient} for use in ENSIndexer. */ -export const ensDbClient = new EnsDbClient(config.databaseUrl, config.databaseSchemaName); +export const ensDbClient = new EnsDbClient(config.databaseUrl, ensIndexerRef); diff --git a/packages/ensnode-schema/package.json b/packages/ensnode-schema/package.json index 94cd3393a..7e4726696 100644 --- a/packages/ensnode-schema/package.json +++ b/packages/ensnode-schema/package.json @@ -16,7 +16,10 @@ "Ponder" ], "exports": { - ".": "./src/ponder.schema.ts" + ".": "./src/index.ts", + "./ponder": "./src/ponder-schema/index.ts", + "./ensindexer": "./src/ensindexer-schema/index.ts", + "./ensnode": "./src/ensnode-schema/index.ts" }, "files": [ "dist" @@ -24,26 +27,49 @@ "publishConfig": { "access": "public", "exports": { - "types": "./dist/ponder.schema.d.ts", - "default": "./dist/ponder.schema.js" - }, - "main": "./dist/ponder.schema.js", - "module": "./dist/ponder.schema.mjs", - "types": "./dist/ponder.schema.d.ts" + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./ponder": { + "import": { + "types": "./dist/ponder-schema/index.d.ts", + "default": "./dist/ponder-schema/index.js" + } + }, + "./ensindexer": { + "import": { + "types": "./dist/ensindexer-schema/index.d.ts", + "default": "./dist/ensindexer-schema/index.js" + } + }, + "./ensnode": { + "import": { + "types": "./dist/ensnode-schema/index.d.ts", + "default": "./dist/ensnode-schema/index.js" + } + } + } }, "scripts": { "prepublish": "tsup", "lint": "biome check --write .", "lint:ci": "biome ci" }, - "dependencies": { + "peerDependencies": { + "drizzle-orm": "catalog:", "ponder": "catalog:", "viem": "catalog:" }, "devDependencies": { "@ensnode/ensnode-sdk": "workspace:", "@ensnode/shared-configs": "workspace:*", + "drizzle-orm": "catalog:", + "ponder": "catalog:", "tsup": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "viem": "catalog:" } } diff --git a/packages/ensnode-schema/src/README.md b/packages/ensnode-schema/src/README.md new file mode 100644 index 000000000..022addd1a --- /dev/null +++ b/packages/ensnode-schema/src/README.md @@ -0,0 +1,28 @@ +# ENSDb Schemas + +Package defining database schemas used in ENSDb. + +## Ponder Schema + +- Owned by Ponder app. +- Shared across ENSIndexer instances. +- Includes responses to RPC requests made by cached public clients. +- Usually large in size, depending on selected ENS Namespace. +- Takes a long time to build. +- Backups highly recommended for sharing RPC cache across different ENSNode environments. + - For example, pulling production backup into local environment in order to test production workflows in isolation. + +## ENSIndexer Schema + +- Owned by an ENSIndexer instance and also influenced by Ponder implementation details, as ENSIndexer is a Ponder app. +- Isolated for specific ENSIndexer instance. +- Includes indexed data based on event handlers logic from active ENSIndexer plugins. +- May be large in size, depending on selected ENS Namespace and active plugins. +- May take a long time to build. Must be re-built from scratch in case of indexing logic changes (i.e. event handler code change, active plugins change). + +## ENSNode Schema + +- Owned by ENSNode services. +- Includes metadata describing configuration and state of various ENSNode services. +- Tiny in size. +- Takes virtually no time to be built. diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/ensindexer-schema/ensv2.subschema.ts similarity index 100% rename from packages/ensnode-schema/src/schemas/ensv2.schema.ts rename to packages/ensnode-schema/src/ensindexer-schema/ensv2.subschema.ts diff --git a/packages/ensnode-schema/src/ensindexer-schema/index.ts b/packages/ensnode-schema/src/ensindexer-schema/index.ts new file mode 100644 index 000000000..10ce6e45d --- /dev/null +++ b/packages/ensnode-schema/src/ensindexer-schema/index.ts @@ -0,0 +1,9 @@ +/** + * Merge the various sub-schemas into ENSIndexer Schema. + */ + +export * from "./ensv2.subschema"; +export * from "./protocol-acceleration.subschema"; +export * from "./registrars.subschema"; +export * from "./subgraph.subschema"; +export * from "./tokenscope.subschema"; diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/ensindexer-schema/protocol-acceleration.subschema.ts similarity index 100% rename from packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts rename to packages/ensnode-schema/src/ensindexer-schema/protocol-acceleration.subschema.ts diff --git a/packages/ensnode-schema/src/schemas/registrars.schema.ts b/packages/ensnode-schema/src/ensindexer-schema/registrars.subschema.ts similarity index 100% rename from packages/ensnode-schema/src/schemas/registrars.schema.ts rename to packages/ensnode-schema/src/ensindexer-schema/registrars.subschema.ts diff --git a/packages/ensnode-schema/src/schemas/subgraph.schema.ts b/packages/ensnode-schema/src/ensindexer-schema/subgraph.subschema.ts similarity index 100% rename from packages/ensnode-schema/src/schemas/subgraph.schema.ts rename to packages/ensnode-schema/src/ensindexer-schema/subgraph.subschema.ts diff --git a/packages/ensnode-schema/src/schemas/tokenscope.schema.ts b/packages/ensnode-schema/src/ensindexer-schema/tokenscope.subschema.ts similarity index 100% rename from packages/ensnode-schema/src/schemas/tokenscope.schema.ts rename to packages/ensnode-schema/src/ensindexer-schema/tokenscope.subschema.ts diff --git a/packages/ensnode-schema/src/ensnode-schema/index.ts b/packages/ensnode-schema/src/ensnode-schema/index.ts new file mode 100644 index 000000000..2f92b82fa --- /dev/null +++ b/packages/ensnode-schema/src/ensnode-schema/index.ts @@ -0,0 +1,66 @@ +/** + * ENSNode Schema + * + * Defines the database objects describing the ENSNode services state. + */ + +import { primaryKey } from "drizzle-orm/pg-core"; + +import { ENSNODE_SCHEMA } from "./schema"; + +export { ENSNODE_SCHEMA_NAME } from "./schema"; + +/** + * ENSNode Metadata + * + * Possible key value pairs are defined by 'EnsNodeMetadata' type: + * - `EnsNodeMetadataEnsDbVersion` + * - `EnsNodeMetadataEnsIndexerPublicConfig` + * - `EnsNodeMetadataEnsIndexerIndexingStatus` + */ +export const ensNodeMetadata = ENSNODE_SCHEMA.table( + "ensnode_metadata", + (t) => ({ + /** + * ENSIndexer Reference + * + * References the ENSIndexer instance by a unique ENSIndexer schema name + * that a metadata record is associated with. This allows us to support + * multiple ENSIndexer instances using the same database, while ensuring + * that their metadata records do not conflict with each other. + */ + ensIndexerRef: t.text().notNull(), + + /** + * Key + * + * Allowed keys: + * - `EnsNodeMetadataEnsDbVersion['key']` + * - `EnsNodeMetadataEnsIndexerPublicConfig['key']` + * - `EnsNodeMetadataEnsIndexerIndexingStatus['key']` + */ + key: t.text().notNull(), + + /** + * Value + * + * Allowed values: + * - `EnsNodeMetadataEnsDbVersion['value']` + * - `EnsNodeMetadataEnsIndexerPublicConfig['value']` + * - `EnsNodeMetadataEnsIndexerIndexingStatus['value']` + * + * Guaranteed to be a serialized representation of JSON object. + */ + value: t.jsonb().notNull(), + }), + (table) => [ + /** + * Primary key constraint on 'ensIndexerRef' and 'key' columns, + * to ensure that there is only one record for each key per ENSIndexer instance. + */ + primaryKey({ + name: "ensnode_metadata_pkey", + columns: [table.ensIndexerRef, table.key], + }), + ], +); diff --git a/packages/ensnode-schema/src/ensnode-schema/schema.ts b/packages/ensnode-schema/src/ensnode-schema/schema.ts new file mode 100644 index 000000000..1cc926c6b --- /dev/null +++ b/packages/ensnode-schema/src/ensnode-schema/schema.ts @@ -0,0 +1,5 @@ +import { pgSchema } from "drizzle-orm/pg-core"; + +export const ENSNODE_SCHEMA_NAME = "ensnode"; + +export const ENSNODE_SCHEMA = pgSchema(ENSNODE_SCHEMA_NAME); diff --git a/packages/ensnode-schema/src/index.ts b/packages/ensnode-schema/src/index.ts new file mode 100644 index 000000000..49b2d5e31 --- /dev/null +++ b/packages/ensnode-schema/src/index.ts @@ -0,0 +1,2 @@ +// Re-export relevant schema definitions for backward compatibility. +export * from "./ensindexer-schema"; diff --git a/packages/ensnode-schema/src/ponder-schema/index.ts b/packages/ensnode-schema/src/ponder-schema/index.ts new file mode 100644 index 000000000..bd2514647 --- /dev/null +++ b/packages/ensnode-schema/src/ponder-schema/index.ts @@ -0,0 +1,8 @@ +/** + * Ponder Schema + * + * Definition of the Ponder Schema can be found in the Ponder repository. + * @see https://github.com/ponder-sh/ponder/blob/ponder%400.16.3/packages/core/src/sync-store/schema.ts + */ + +export const PONDER_SCHEMA_NAME = "ponder_sync"; diff --git a/packages/ensnode-schema/src/ponder.schema.ts b/packages/ensnode-schema/src/ponder.schema.ts deleted file mode 100644 index 58508341e..000000000 --- a/packages/ensnode-schema/src/ponder.schema.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Merge the various sub-schemas into a single ponder (drizzle) schema. - */ - -export * from "./schemas/ensnode-metadata.schema"; -export * from "./schemas/ensv2.schema"; -export * from "./schemas/protocol-acceleration.schema"; -export * from "./schemas/registrars.schema"; -export * from "./schemas/subgraph.schema"; -export * from "./schemas/tokenscope.schema"; diff --git a/packages/ensnode-schema/src/schemas/ensnode-metadata.schema.ts b/packages/ensnode-schema/src/schemas/ensnode-metadata.schema.ts deleted file mode 100644 index bac75fd62..000000000 --- a/packages/ensnode-schema/src/schemas/ensnode-metadata.schema.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Schema Definitions that hold metadata about the ENSNode instance. - */ - -import { onchainTable } from "ponder"; - -/** - * ENSNode Metadata - * - * Possible key value pairs are defined by 'EnsNodeMetadata' type: - * - `EnsNodeMetadataEnsDbVersion` - * - `EnsNodeMetadataEnsIndexerPublicConfig` - * - `EnsNodeMetadataEnsIndexerIndexingStatus` - */ -export const ensNodeMetadata = onchainTable("ensnode_metadata", (t) => ({ - /** - * Key - * - * Allowed keys: - * - `EnsNodeMetadataEnsDbVersion['key']` - * - `EnsNodeMetadataEnsIndexerPublicConfig['key']` - * - `EnsNodeMetadataEnsIndexerIndexingStatus['key']` - */ - key: t.text().primaryKey(), - - /** - * Value - * - * Allowed values: - * - `EnsNodeMetadataEnsDbVersion['value']` - * - `EnsNodeMetadataEnsIndexerPublicConfig['value']` - * - `EnsNodeMetadataEnsIndexerIndexingStatus['value']` - * - * Guaranteed to be a serialized representation of JSON object. - */ - value: t.jsonb().notNull(), -})); diff --git a/packages/ensnode-schema/tsconfig.json b/packages/ensnode-schema/tsconfig.json index d34e39a61..d4861e993 100644 --- a/packages/ensnode-schema/tsconfig.json +++ b/packages/ensnode-schema/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": "@ensnode/shared-configs/tsconfig.ponder.json", + "extends": "@ensnode/shared-configs/tsconfig.lib.json", + "compilerOptions": { + "rootDir": "." // necessary for 'The project root is ambiguous' + }, "include": ["./**/*.ts"], "exclude": ["node_modules"] } diff --git a/packages/ensnode-schema/tsup.config.ts b/packages/ensnode-schema/tsup.config.ts index afadde0d3..de83ab0d2 100644 --- a/packages/ensnode-schema/tsup.config.ts +++ b/packages/ensnode-schema/tsup.config.ts @@ -1,13 +1,19 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["src/ponder.schema.ts"], + entry: [ + "src/index.ts", + "src/ponder-schema/index.ts", + "src/ensindexer-schema/index.ts", + "src/ensnode-schema/index.ts", + ], platform: "neutral", format: ["esm"], target: "es2022", bundle: true, splitting: false, sourcemap: true, + dts: true, clean: true, external: ["viem", "ponder", "drizzle-orm", "drizzle-orm/pg-core"], outDir: "./dist", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e64a2bd7..b0c9b5384 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -862,13 +862,6 @@ importers: version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) packages/ensnode-schema: - dependencies: - ponder: - specifier: 'catalog:' - version: 0.16.3(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.7)(lightningcss@1.30.2)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(zod@3.25.76) - viem: - specifier: 'catalog:' - version: 2.38.5(typescript@5.9.3)(zod@3.25.76) devDependencies: '@ensnode/ensnode-sdk': specifier: 'workspace:' @@ -876,12 +869,21 @@ importers: '@ensnode/shared-configs': specifier: workspace:* version: link:../shared-configs + drizzle-orm: + specifier: 'catalog:' + version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.26.3)(pg@8.16.3) + ponder: + specifier: 'catalog:' + version: 0.16.3(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.7)(lightningcss@1.30.2)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(zod@3.25.76) tsup: specifier: 'catalog:' version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: 'catalog:' version: 5.9.3 + viem: + specifier: 'catalog:' + version: 2.38.5(typescript@5.9.3)(zod@3.25.76) packages/ensnode-sdk: dependencies: