From 2e5c6a68bd532da6d7a871558a6965282936ba93 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 18 Mar 2026 20:15:47 +0100 Subject: [PATCH 01/34] Move `ensdb` module contents from ENSNode SDK to ENSDb SDK --- .../src/client/ensnode-db-mutations.ts | 32 ++++++++++ .../src/client/ensnode-db-queries.ts | 32 ++++++++++ .../src/client}/ensnode-metadata.ts | 6 +- .../src/client}/serialize/ensnode-metadata.ts | 7 ++- packages/ensnode-sdk/src/ensdb/client.ts | 58 ------------------- packages/ensnode-sdk/src/ensdb/index.ts | 3 - packages/ensnode-sdk/src/index.ts | 1 - 7 files changed, 73 insertions(+), 66 deletions(-) create mode 100644 packages/ensdb-sdk/src/client/ensnode-db-mutations.ts create mode 100644 packages/ensdb-sdk/src/client/ensnode-db-queries.ts rename packages/{ensnode-sdk/src/ensdb => ensdb-sdk/src/client}/ensnode-metadata.ts (85%) rename packages/{ensnode-sdk/src/ensdb => ensdb-sdk/src/client}/serialize/ensnode-metadata.ts (84%) delete mode 100644 packages/ensnode-sdk/src/ensdb/client.ts delete mode 100644 packages/ensnode-sdk/src/ensdb/index.ts diff --git a/packages/ensdb-sdk/src/client/ensnode-db-mutations.ts b/packages/ensdb-sdk/src/client/ensnode-db-mutations.ts new file mode 100644 index 0000000000..3fd4d01568 --- /dev/null +++ b/packages/ensdb-sdk/src/client/ensnode-db-mutations.ts @@ -0,0 +1,32 @@ +import type { + CrossChainIndexingStatusSnapshot, + EnsIndexerPublicConfig, +} from "@ensnode/ensnode-sdk"; + +/** + * Mutations for ENSNode Schema in ENSDb + * + * Includes methods for writing into ENSNode Schema in ENSDb. + */ +export interface EnsNodeDbMutations { + /** + * Upsert ENSDb Version + * + * @throws when upsert operation failed. + */ + upsertEnsDbVersion(ensDbVersion: string): Promise; + + /** + * Upsert ENSIndexer Public Config + * + * @throws when upsert operation failed. + */ + upsertEnsIndexerPublicConfig(ensIndexerPublicConfig: EnsIndexerPublicConfig): Promise; + + /** + * Upsert Indexing Status Snapshot + * + * @throws when upsert operation failed. + */ + upsertIndexingStatusSnapshot(indexingStatus: CrossChainIndexingStatusSnapshot): Promise; +} diff --git a/packages/ensdb-sdk/src/client/ensnode-db-queries.ts b/packages/ensdb-sdk/src/client/ensnode-db-queries.ts new file mode 100644 index 0000000000..2e47882920 --- /dev/null +++ b/packages/ensdb-sdk/src/client/ensnode-db-queries.ts @@ -0,0 +1,32 @@ +import type { + CrossChainIndexingStatusSnapshot, + EnsIndexerPublicConfig, +} from "@ensnode/ensnode-sdk"; + +/** + * Queries for ENSNode Schema in ENSDb + * + * Includes methods for querying ENSNode Schema in ENSDb. + */ +export interface EnsNodeDbQueries { + /** + * Get ENSDb Version + * + * @returns the existing record, or `undefined`. + */ + getEnsDbVersion(): Promise; + + /** + * Get ENSIndexer Public Config + * + * @returns the existing record, or `undefined`. + */ + getEnsIndexerPublicConfig(): Promise; + + /** + * Get Indexing Status Snapshot + * + * @returns the existing record, or `undefined`. + */ + getIndexingStatusSnapshot(): Promise; +} diff --git a/packages/ensnode-sdk/src/ensdb/ensnode-metadata.ts b/packages/ensdb-sdk/src/client/ensnode-metadata.ts similarity index 85% rename from packages/ensnode-sdk/src/ensdb/ensnode-metadata.ts rename to packages/ensdb-sdk/src/client/ensnode-metadata.ts index 15c9bfefc3..bdb35c4069 100644 --- a/packages/ensnode-sdk/src/ensdb/ensnode-metadata.ts +++ b/packages/ensdb-sdk/src/client/ensnode-metadata.ts @@ -1,5 +1,7 @@ -import type { EnsIndexerPublicConfig } from "../ensindexer/config"; -import type { CrossChainIndexingStatusSnapshot } from "../indexing-status/cross-chain-indexing-status-snapshot"; +import type { + CrossChainIndexingStatusSnapshot, + EnsIndexerPublicConfig, +} from "@ensnode/ensnode-sdk"; /** * Keys used to distinguish records in `ensnode_metadata` table in the ENSDb. diff --git a/packages/ensnode-sdk/src/ensdb/serialize/ensnode-metadata.ts b/packages/ensdb-sdk/src/client/serialize/ensnode-metadata.ts similarity index 84% rename from packages/ensnode-sdk/src/ensdb/serialize/ensnode-metadata.ts rename to packages/ensdb-sdk/src/client/serialize/ensnode-metadata.ts index d74b71abcb..cae7fcdd34 100644 --- a/packages/ensnode-sdk/src/ensdb/serialize/ensnode-metadata.ts +++ b/packages/ensdb-sdk/src/client/serialize/ensnode-metadata.ts @@ -1,5 +1,8 @@ -import type { SerializedEnsIndexerPublicConfig } from "../../ensindexer/config"; -import type { SerializedCrossChainIndexingStatusSnapshot } from "../../indexing-status/serialize/cross-chain-indexing-status-snapshot"; +import type { + SerializedCrossChainIndexingStatusSnapshot, + SerializedEnsIndexerPublicConfig, +} from "@ensnode/ensnode-sdk"; + import type { EnsNodeMetadata, EnsNodeMetadataEnsDbVersion, diff --git a/packages/ensnode-sdk/src/ensdb/client.ts b/packages/ensnode-sdk/src/ensdb/client.ts deleted file mode 100644 index 0cbf9b287e..0000000000 --- a/packages/ensnode-sdk/src/ensdb/client.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { EnsIndexerPublicConfig } from "../ensindexer/config"; -import type { CrossChainIndexingStatusSnapshot } from "../indexing-status/cross-chain-indexing-status-snapshot"; - -/** - * ENSDb Client Query - * - * Includes methods for reading from ENSDb. - */ -export interface EnsDbClientQuery { - /** - * Get ENSDb Version - * - * @returns the existing record, or `undefined`. - */ - getEnsDbVersion(): Promise; - - /** - * Get ENSIndexer Public Config - * - * @returns the existing record, or `undefined`. - */ - getEnsIndexerPublicConfig(): Promise; - - /** - * Get Indexing Status Snapshot - * - * @returns the existing record, or `undefined`. - */ - getIndexingStatusSnapshot(): Promise; -} - -/** - * ENSDb Client Mutation - * - * Includes methods for writing into ENSDb. - */ -export interface EnsDbClientMutation { - /** - * Upsert ENSDb Version - * - * @throws when upsert operation failed. - */ - upsertEnsDbVersion(ensDbVersion: string): Promise; - - /** - * Upsert ENSIndexer Public Config - * - * @throws when upsert operation failed. - */ - upsertEnsIndexerPublicConfig(ensIndexerPublicConfig: EnsIndexerPublicConfig): Promise; - - /** - * Upsert Indexing Status Snapshot - * - * @throws when upsert operation failed. - */ - upsertIndexingStatusSnapshot(indexingStatus: CrossChainIndexingStatusSnapshot): Promise; -} diff --git a/packages/ensnode-sdk/src/ensdb/index.ts b/packages/ensnode-sdk/src/ensdb/index.ts deleted file mode 100644 index bec975949b..0000000000 --- a/packages/ensnode-sdk/src/ensdb/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./client"; -export * from "./ensnode-metadata"; -export * from "./serialize/ensnode-metadata"; diff --git a/packages/ensnode-sdk/src/index.ts b/packages/ensnode-sdk/src/index.ts index 83faed6663..64551c5894 100644 --- a/packages/ensnode-sdk/src/index.ts +++ b/packages/ensnode-sdk/src/index.ts @@ -1,6 +1,5 @@ export * from "./ens"; export * from "./ensapi"; -export * from "./ensdb"; export * from "./ensindexer"; export * from "./ensrainbow"; export * from "./ensv2"; From d3329fd1637a77fc060303706d5dfdb8fc895710 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Wed, 18 Mar 2026 20:16:16 +0100 Subject: [PATCH 02/34] Add `EnsNodeDbMigrations` interface to ENSDb SDK --- .../ensdb-sdk/src/client/ensnode-db-migrations.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 packages/ensdb-sdk/src/client/ensnode-db-migrations.ts diff --git a/packages/ensdb-sdk/src/client/ensnode-db-migrations.ts b/packages/ensdb-sdk/src/client/ensnode-db-migrations.ts new file mode 100644 index 0000000000..0a8ef40209 --- /dev/null +++ b/packages/ensdb-sdk/src/client/ensnode-db-migrations.ts @@ -0,0 +1,15 @@ +/** + * Migrations for ENSNode Schema in ENSDb + * + * Includes methods for migrating ENSNode Schema in ENSDb. + */ +export interface EnsNodeDbMigrations { + /** + * Execute pending database migrations for ENSNode Schema in ENSDb. + * + * @param migrationsDirPath - The file path to the directory containing + * database migration files for ENSNode Schema. + * @throws error when migration execution fails. + */ + migrate(migrationsDirPath: string): Promise; +} From a844af0e4e0990d9835dd05663043c3abbb9c69f Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 12:02:06 +0100 Subject: [PATCH 03/34] Introduce ENSDb Reader This is a client implementation for ENSDb focusing on read operations. --- packages/ensdb-sdk/src/client/ensdb-reader.ts | 168 ++++++++++++++++++ packages/ensdb-sdk/src/lib/drizzle.ts | 41 +++++ 2 files changed, 209 insertions(+) create mode 100644 packages/ensdb-sdk/src/client/ensdb-reader.ts create mode 100644 packages/ensdb-sdk/src/lib/drizzle.ts diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.ts b/packages/ensdb-sdk/src/client/ensdb-reader.ts new file mode 100644 index 0000000000..34ff175c55 --- /dev/null +++ b/packages/ensdb-sdk/src/client/ensdb-reader.ts @@ -0,0 +1,168 @@ +import { isTable, Table } from "drizzle-orm"; +import { isPgEnum } from "drizzle-orm/pg-core"; +import { and, eq } from "drizzle-orm/sql"; + +import { + type CrossChainIndexingStatusSnapshot, + deserializeCrossChainIndexingStatusSnapshot, + deserializeEnsIndexerPublicConfig, + type EnsIndexerPublicConfig, +} from "@ensnode/ensnode-sdk"; + +import * as ensIndexerSchema from "../ensindexer"; +import * as ensNodeSchema from "../ensnode"; +import { buildEnsDbDrizzleClient, type EnsDbDrizzle } from "../lib/drizzle"; +import type { EnsNodeDbQueries } from "./ensnode-db-queries"; +import { EnsNodeMetadataKeys } from "./ensnode-metadata"; +import type { + SerializedEnsNodeMetadata, + SerializedEnsNodeMetadataEnsDbVersion, + SerializedEnsNodeMetadataEnsIndexerIndexingStatus, + SerializedEnsNodeMetadataEnsIndexerPublicConfig, +} from "./serialize/ensnode-metadata"; + +/** + * ENSDb Reader + * + * Allows querying an ENSDb instance, including ENSNode Metadata records, + * as well as complex queries across multiple database schemas in ENSDb. + */ +export class EnsDbReader implements EnsNodeDbQueries { + /** + * Drizzle client for ENSDb. + */ + protected drizzleClient: EnsDbDrizzle; + + /** + * References the ENSIndexer instance's database schema in ENSDb. + * + * It is required to scope the ENSNode metadata records to + * a specific ENSIndexer instance in ENSDb. + */ + protected ensIndexerSchemaName: string; + + /** + * @param ensDbConnectionString connection string for ENSDb Postgres database + * @param ensIndexerSchemaName reference string for ENSIndexer instance + */ + constructor(ensDbConnectionString: string, ensIndexerSchemaName: string) { + EnsDbReader.bindEnsIndexerSchemaWithName(ensIndexerSchemaName); + this.drizzleClient = buildEnsDbDrizzleClient(ensDbConnectionString); + this.ensIndexerSchemaName = ensIndexerSchemaName; + } + + /** + * Getter for the Drizzle client for ENSDb instance + * + * Useful while working on complex queries for ENSDb. + */ + get client(): EnsDbDrizzle { + return this.drizzleClient; + } + + /** + * Bind a ENSIndexer Schema definition with a specific instance of + * ENSIndexer Schema in ENSDb. + * + * ENSIndexer Schema definition does not have a fixed database schema name, + * as it is determined by Ponder when starting up the ENSIndexer instance. + * + * This function allows to bind the ENSIndexer Schema definition with + * the correct database schema name for the ENSIndexer instance we want to + * reference when interacting with ENSDb. + * + * @param ensIndexerSchemaName - The name of the ENSIndexer Schema instance in ENSDb. + * + * Note: this function is a replacement for `setDatabaseSchema` from `@ponder/client`. + */ + static bindEnsIndexerSchemaWithName(ensIndexerSchemaName: string): void { + for (const dbObjectDef of Object.values(ensIndexerSchema)) { + if (isTable(dbObjectDef)) { + // @ts-expect-error - Drizzle's Table type for the schema symbol is + // not typed in a way that allows us to set it directly, + // but we know it exists and can be set. + dbObjectDef[Table.Symbol.Schema] = ensIndexerSchemaName; + } else if (isPgEnum(dbObjectDef)) { + // @ts-expect-error - Drizzle's PgEnum type for the schema symbol is + // typed as readonly, but we need to set it here so + // the output schema definition has the correct schema for + // all table and enum objects. + dbObjectDef.schema = ensIndexerSchemaName; + } + } + } + /** + * @inheritdoc + */ + async getEnsDbVersion(): Promise { + const record = await this.getEnsNodeMetadata({ + key: EnsNodeMetadataKeys.EnsDbVersion, + }); + + return record; + } + + /** + * @inheritdoc + */ + async getEnsIndexerPublicConfig(): Promise { + const record = await this.getEnsNodeMetadata({ + key: EnsNodeMetadataKeys.EnsIndexerPublicConfig, + }); + + if (!record) { + return undefined; + } + + return deserializeEnsIndexerPublicConfig(record); + } + + /** + * @inheritdoc + */ + async getIndexingStatusSnapshot(): Promise { + const record = await this.getEnsNodeMetadata( + { + key: EnsNodeMetadataKeys.EnsIndexerIndexingStatus, + }, + ); + + if (!record) { + return undefined; + } + + return deserializeCrossChainIndexingStatusSnapshot(record); + } + + /** + * Get ENSNode Metadata record + * + * @returns selected record in ENSDb. + * @throws when more than one matching metadata record is found + * (should be impossible given the composite PK constraint on + * 'ensIndexerSchemaName' and 'key') + */ + private async getEnsNodeMetadata( + metadata: Pick, + ): Promise { + const result = await this.drizzleClient + .select() + .from(ensNodeSchema.metadata) + .where( + and( + eq(ensNodeSchema.metadata.ensIndexerSchemaName, this.ensIndexerSchemaName), + eq(ensNodeSchema.metadata.key, metadata.key), + ), + ); + + if (result.length === 0) { + return undefined; + } + + if (result.length === 1 && result[0]) { + return result[0].value as EnsNodeMetadataType["value"]; + } + + throw new Error(`There must be exactly one ENSNodeMetadata record for '${metadata.key}' key`); + } +} diff --git a/packages/ensdb-sdk/src/lib/drizzle.ts b/packages/ensdb-sdk/src/lib/drizzle.ts new file mode 100644 index 0000000000..0e6976cdea --- /dev/null +++ b/packages/ensdb-sdk/src/lib/drizzle.ts @@ -0,0 +1,41 @@ +/** + * Utilities for Drizzle ORM integration with ENSDb. + */ +import type { Logger as DrizzleLogger } from "drizzle-orm/logger"; +import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres"; + +import * as ensIndexerSchema from "../ensindexer"; +import * as ensNodeSchema from "../ensnode"; + +/** + * ENSDb Schema + * + * Represents the combined database schema for ENSDb, + * including both the ENSIndexer Schema and the ENSNode Schema. + */ +export const ensDbSchema = { ...ensIndexerSchema, ...ensNodeSchema }; + +export type EnsDbSchema = typeof ensDbSchema; + +/** + * Drizzle client for ENSDb. + */ +export type EnsDbDrizzle = NodePgDatabase; + +/** + * Build a Drizzle client for ENSDb. + * @param connectionString - The connection string for the ENSDb. + * @param logger - Optional Drizzle logger for query logging. + * @returns A Drizzle client for ENSDb. + */ +export function buildEnsDbDrizzleClient( + connectionString: string, + logger?: DrizzleLogger, +): EnsDbDrizzle { + return drizzle({ + connection: connectionString, + schema: ensDbSchema, + casing: "snake_case", + logger, + }); +} From 316fe6797f0c87e2756bc9aef2734a6c32cd1837 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 12:06:15 +0100 Subject: [PATCH 04/34] Introduce ENSDb Writer This is a client implementation for ENSDb focusing on write operations, like database migrations and mutations. --- packages/ensdb-sdk/src/client/ensdb-writer.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 packages/ensdb-sdk/src/client/ensdb-writer.ts diff --git a/packages/ensdb-sdk/src/client/ensdb-writer.ts b/packages/ensdb-sdk/src/client/ensdb-writer.ts new file mode 100644 index 0000000000..f275f0a269 --- /dev/null +++ b/packages/ensdb-sdk/src/client/ensdb-writer.ts @@ -0,0 +1,87 @@ +import { migrate } from "drizzle-orm/node-postgres/migrator"; + +import { + type CrossChainIndexingStatusSnapshot, + type EnsIndexerPublicConfig, + serializeCrossChainIndexingStatusSnapshot, + serializeEnsIndexerPublicConfig, +} from "@ensnode/ensnode-sdk"; + +import * as ensNodeSchema from "../ensnode"; +import { EnsDbReader } from "./ensdb-reader"; +import type { EnsNodeDbMigrations } from "./ensnode-db-migrations"; +import type { EnsNodeDbMutations } from "./ensnode-db-mutations"; +import { EnsNodeMetadataKeys } from "./ensnode-metadata"; +import type { SerializedEnsNodeMetadata } from "./serialize/ensnode-metadata"; + +/** + * ENSDb Writer + * + * Allows updating an ENSDb instance, including: + * - executing database migrations for ENSNode Schema, + * - updating ENSNode Metadata records in ENSDb for the given ENSIndexer instance. + */ +export class EnsDbWriter extends EnsDbReader implements EnsNodeDbMutations, EnsNodeDbMigrations { + /** + * @inheritdoc + */ + async migrate(migrationsDirPath: string): Promise { + return migrate(this.drizzleClient, { + migrationsFolder: migrationsDirPath, + migrationsSchema: "ensnode", + }); + } + + /** + * @inheritdoc + */ + async upsertEnsDbVersion(ensDbVersion: string): Promise { + await this.upsertEnsNodeMetadata({ + key: EnsNodeMetadataKeys.EnsDbVersion, + value: ensDbVersion, + }); + } + + /** + * @inheritdoc + */ + async upsertEnsIndexerPublicConfig( + ensIndexerPublicConfig: EnsIndexerPublicConfig, + ): Promise { + await this.upsertEnsNodeMetadata({ + key: EnsNodeMetadataKeys.EnsIndexerPublicConfig, + value: serializeEnsIndexerPublicConfig(ensIndexerPublicConfig), + }); + } + + /** + * @inheritdoc + */ + async upsertIndexingStatusSnapshot( + indexingStatus: CrossChainIndexingStatusSnapshot, + ): Promise { + await this.upsertEnsNodeMetadata({ + key: EnsNodeMetadataKeys.EnsIndexerIndexingStatus, + value: serializeCrossChainIndexingStatusSnapshot(indexingStatus), + }); + } + + /** + * Upsert ENSNode metadata + * + * @throws when upsert operation failed. + */ + private async upsertEnsNodeMetadata(metadata: SerializedEnsNodeMetadata): Promise { + await this.drizzleClient + .insert(ensNodeSchema.metadata) + .values({ + ensIndexerSchemaName: this.ensIndexerSchemaName, + key: metadata.key, + value: metadata.value, + }) + .onConflictDoUpdate({ + target: [ensNodeSchema.metadata.ensIndexerSchemaName, ensNodeSchema.metadata.key], + set: { value: metadata.value }, + }); + } +} From 90ff103c2a89b09794d10aaeb697d6a205dd5c0e Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 12:10:50 +0100 Subject: [PATCH 05/34] Setup exports fro ENSDb client module --- packages/ensdb-sdk/src/client/index.ts | 6 ++++++ packages/ensdb-sdk/src/index.ts | 1 + 2 files changed, 7 insertions(+) create mode 100644 packages/ensdb-sdk/src/client/index.ts diff --git a/packages/ensdb-sdk/src/client/index.ts b/packages/ensdb-sdk/src/client/index.ts new file mode 100644 index 0000000000..f10d25f9b6 --- /dev/null +++ b/packages/ensdb-sdk/src/client/index.ts @@ -0,0 +1,6 @@ +export * from "./ensdb-reader"; +export * from "./ensdb-writer"; +export * from "./ensnode-db-migrations"; +export * from "./ensnode-db-mutations"; +export * from "./ensnode-db-queries"; +export * from "./ensnode-metadata"; diff --git a/packages/ensdb-sdk/src/index.ts b/packages/ensdb-sdk/src/index.ts index aba35b8105..4a601090c2 100644 --- a/packages/ensdb-sdk/src/index.ts +++ b/packages/ensdb-sdk/src/index.ts @@ -1 +1,2 @@ +export * from "./client"; export * from "./ensindexer"; From 72e1f9b591e9ac986994d1625249e0dfec286cd1 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 12:42:51 +0100 Subject: [PATCH 06/34] Replace ENSDb Client implementation in ENSIndexer Use implementation from ENSDb SDK --- .../ponder/src/api/handlers/ensnode-api.ts | 2 +- .../src/lib/ensdb-client/drizzle.ts | 26 --- .../src/lib/ensdb-client/ensdb-client.mock.ts | 72 ------ .../src/lib/ensdb-client/ensdb-client.test.ts | 207 ------------------ .../src/lib/ensdb-client/ensdb-client.ts | 200 ----------------- .../src/lib/ensdb-client/singleton.ts | 8 - .../ensdb-writer-worker.ts | 10 +- .../src/lib/ensdb-writer-worker/singleton.ts | 2 +- apps/ensindexer/src/lib/ensdb/singleton.ts | 10 + 9 files changed, 19 insertions(+), 518 deletions(-) delete mode 100644 apps/ensindexer/src/lib/ensdb-client/drizzle.ts delete mode 100644 apps/ensindexer/src/lib/ensdb-client/ensdb-client.mock.ts delete mode 100644 apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts delete mode 100644 apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts delete mode 100644 apps/ensindexer/src/lib/ensdb-client/singleton.ts create mode 100644 apps/ensindexer/src/lib/ensdb/singleton.ts diff --git a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts index 994b987040..fc6a46e796 100644 --- a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts +++ b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts @@ -10,7 +10,7 @@ import { serializeIndexingStatusResponse, } from "@ensnode/ensnode-sdk"; -import { ensDbClient } from "@/lib/ensdb-client/singleton"; +import { ensDbClient } from "@/lib/ensdb/singleton"; const app = new Hono(); diff --git a/apps/ensindexer/src/lib/ensdb-client/drizzle.ts b/apps/ensindexer/src/lib/ensdb-client/drizzle.ts deleted file mode 100644 index b5cc9b5c73..0000000000 --- a/apps/ensindexer/src/lib/ensdb-client/drizzle.ts +++ /dev/null @@ -1,26 +0,0 @@ -// This file is based on `packages/ponder-subgraph/src/drizzle.ts` file. -// 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 }; - -/** - * Makes a Drizzle DB object. - */ -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 deleted file mode 100644 index 7387cc19fd..0000000000 --- a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.mock.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - type BlockRef, - ChainIndexingStatusIds, - CrossChainIndexingStrategyIds, - type EnsIndexerPublicConfig, - OmnichainIndexingStatusIds, - PluginName, - RangeTypeIds, - type SerializedCrossChainIndexingStatusSnapshot, -} from "@ensnode/ensnode-sdk"; - -export const earlierBlockRef = { - timestamp: 1672531199, - number: 1024, -} as const satisfies BlockRef; - -export const laterBlockRef = { - timestamp: 1672531200, - number: 1025, -} as const satisfies BlockRef; - -export const databaseUrl = "postgres://user:pass@localhost:5432/ensdb"; - -export const databaseSchemaName = "public"; - -export const publicConfig = { - databaseSchemaName, - ensRainbowPublicConfig: { - version: "0.32.0", - labelSet: { - labelSetId: "subgraph", - highestLabelSetVersion: 0, - }, - recordsCount: 100, - }, - labelSet: { - labelSetId: "subgraph", - labelSetVersion: 0, - }, - indexedChainIds: new Set([1]), - isSubgraphCompatible: true, - namespace: "mainnet", - plugins: [PluginName.Subgraph], - versionInfo: { - nodejs: "v22.10.12", - ponder: "0.11.25", - ensDb: "0.32.0", - ensIndexer: "0.32.0", - ensNormalize: "1.11.1", - }, -} satisfies EnsIndexerPublicConfig; - -export const serializedSnapshot = { - strategy: CrossChainIndexingStrategyIds.Omnichain, - slowestChainIndexingCursor: earlierBlockRef.timestamp, - snapshotTime: earlierBlockRef.timestamp + 20, - omnichainSnapshot: { - omnichainStatus: OmnichainIndexingStatusIds.Following, - chains: { - "1": { - chainStatus: ChainIndexingStatusIds.Following, - config: { - rangeType: RangeTypeIds.LeftBounded, - startBlock: earlierBlockRef, - }, - latestIndexedBlock: earlierBlockRef, - latestKnownBlock: laterBlockRef, - }, - }, - omnichainIndexingCursor: earlierBlockRef.timestamp, - }, -} satisfies SerializedCrossChainIndexingStatusSnapshot; diff --git a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts deleted file mode 100644 index eeb3a1e47e..0000000000 --- a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -import { ensNodeMetadata } from "@ensnode/ensdb-sdk"; -import { - deserializeCrossChainIndexingStatusSnapshot, - EnsNodeMetadataKeys, - serializeCrossChainIndexingStatusSnapshot, - serializeEnsIndexerPublicConfig, -} from "@ensnode/ensnode-sdk"; - -import { makeDrizzle } from "./drizzle"; -import { EnsDbClient } from "./ensdb-client"; -import * as ensDbClientMock from "./ensdb-client.mock"; - -// Mock the config module to prevent it from trying to load actual environment variables during tests -vi.mock("@/config", () => ({ default: {} })); - -// Mock the makeDrizzle function to return a mock database instance -vi.mock("./drizzle", () => ({ makeDrizzle: vi.fn() })); - -describe("EnsDbClient", () => { - // Mock database query results and methods - const selectResult = { current: [] as Array<{ value: unknown }> }; - const whereMock = vi.fn(async () => selectResult.current); - const fromMock = vi.fn(() => ({ where: whereMock })); - const selectMock = vi.fn(() => ({ from: fromMock })); - const onConflictDoUpdateMock = vi.fn(async () => undefined); - const valuesMock = vi.fn(() => ({ onConflictDoUpdate: onConflictDoUpdateMock })); - const insertMock = vi.fn(() => ({ values: valuesMock })); - const executeMock = vi.fn(async () => undefined); - const txMock = { insert: insertMock, execute: executeMock }; - const transactionMock = vi.fn(async (callback: (tx: typeof txMock) => Promise) => - callback(txMock), - ); - const dbMock = { select: selectMock, insert: insertMock, transaction: transactionMock }; - - beforeEach(() => { - selectResult.current = []; - whereMock.mockClear(); - fromMock.mockClear(); - selectMock.mockClear(); - onConflictDoUpdateMock.mockClear(); - valuesMock.mockClear(); - insertMock.mockClear(); - executeMock.mockClear(); - transactionMock.mockClear(); - vi.mocked(makeDrizzle).mockReturnValue(dbMock as unknown as ReturnType); - }); - - describe("getEnsDbVersion", () => { - it("returns undefined when no record exists", async () => { - // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); - - // act & assert - await expect(client.getEnsDbVersion()).resolves.toBeUndefined(); - - expect(selectMock).toHaveBeenCalledTimes(1); - expect(fromMock).toHaveBeenCalledWith(ensNodeMetadata); - }); - - it("returns value when one record exists", async () => { - // arrange - selectResult.current = [{ value: "0.1.0" }]; - - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); - - // act & assert - await expect(client.getEnsDbVersion()).resolves.toBe("0.1.0"); - }); - - // This scenario should be impossible due to the primary key constraint on - // the 'key' column of 'ensnode_metadata' table. - it("throws when multiple records exist", async () => { - // arrange - selectResult.current = [{ value: "0.1.0" }, { value: "0.1.1" }]; - - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); - - // act & assert - await expect(client.getEnsDbVersion()).rejects.toThrowError(/ensdb_version/i); - }); - }); - - describe("getEnsIndexerPublicConfig", () => { - it("returns undefined when no record exists", async () => { - // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); - - // act & assert - await expect(client.getEnsIndexerPublicConfig()).resolves.toBeUndefined(); - }); - - it("deserializes the stored config", async () => { - // arrange - const serializedConfig = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig); - selectResult.current = [{ value: serializedConfig }]; - - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); - - // act & assert - await expect(client.getEnsIndexerPublicConfig()).resolves.toStrictEqual( - ensDbClientMock.publicConfig, - ); - }); - }); - - describe("getIndexingStatusSnapshot", () => { - it("deserializes the stored indexing status snapshot", async () => { - // arrange - selectResult.current = [{ value: ensDbClientMock.serializedSnapshot }]; - - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); - const expected = deserializeCrossChainIndexingStatusSnapshot( - ensDbClientMock.serializedSnapshot, - ); - - // act & assert - await expect(client.getIndexingStatusSnapshot()).resolves.toStrictEqual(expected); - }); - }); - - describe("upsertEnsDbVersion", () => { - it("writes the database version metadata", async () => { - // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); - - // act - await client.upsertEnsDbVersion("0.2.0"); - - // assert - expect(insertMock).toHaveBeenCalledWith(ensNodeMetadata); - expect(valuesMock).toHaveBeenCalledWith({ - key: EnsNodeMetadataKeys.EnsDbVersion, - value: "0.2.0", - }); - expect(onConflictDoUpdateMock).toHaveBeenCalledWith({ - target: ensNodeMetadata.key, - set: { value: "0.2.0" }, - }); - }); - }); - - describe("upsertEnsIndexerPublicConfig", () => { - it("serializes and writes the public config", async () => { - // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); - const expectedValue = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig); - - // act - await client.upsertEnsIndexerPublicConfig(ensDbClientMock.publicConfig); - - // assert - expect(valuesMock).toHaveBeenCalledWith({ - key: EnsNodeMetadataKeys.EnsIndexerPublicConfig, - value: expectedValue, - }); - }); - }); - - describe("upsertIndexingStatusSnapshot", () => { - it("serializes and writes the indexing status snapshot", async () => { - // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); - const snapshot = deserializeCrossChainIndexingStatusSnapshot( - ensDbClientMock.serializedSnapshot, - ); - const expectedValue = serializeCrossChainIndexingStatusSnapshot(snapshot); - - // act - await client.upsertIndexingStatusSnapshot(snapshot); - - // assert - expect(valuesMock).toHaveBeenCalledWith({ - 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 deleted file mode 100644 index 1d62ecb924..0000000000 --- a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts +++ /dev/null @@ -1,200 +0,0 @@ -import type { NodePgDatabase } from "drizzle-orm/node-postgres"; -import { eq, sql } from "drizzle-orm/sql"; - -import { ensNodeMetadata } from "@ensnode/ensdb-sdk"; -import { - type CrossChainIndexingStatusSnapshot, - deserializeCrossChainIndexingStatusSnapshot, - deserializeEnsIndexerPublicConfig, - type EnsDbClientMutation, - type EnsDbClientQuery, - type EnsIndexerPublicConfig, - EnsNodeMetadataKeys, - type SerializedEnsNodeMetadata, - type SerializedEnsNodeMetadataEnsDbVersion, - type SerializedEnsNodeMetadataEnsIndexerIndexingStatus, - type SerializedEnsNodeMetadataEnsIndexerPublicConfig, - serializeCrossChainIndexingStatusSnapshot, - serializeEnsIndexerPublicConfig, -} from "@ensnode/ensnode-sdk"; - -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 {} - -/** - * ENSDb Client - * - * This client exists to provide an abstraction layer for interacting with ENSDb. - * It enables ENSIndexer and ENSApi to decouple from each other, and use - * ENSDb as the integration point between the two (via ENSDb Client). - * - * Enables querying and mutating ENSDb data, such as: - * - ENSDb version - * - ENSIndexer Public Config, and Indexing Status Snapshot and CrossChainIndexingStatusSnapshot. - */ -export class EnsDbClient implements EnsDbClientQuery, EnsDbClientMutation { - /** - * Drizzle database instance for ENSDb. - */ - private db: DrizzleDb; - - /** - * @param databaseUrl connection string for ENSDb Postgres database - * @param databaseSchemaName Postgres schema name for ENSDb tables - */ - constructor(databaseUrl: string, databaseSchemaName: string) { - this.db = makeDrizzle({ - databaseSchema: databaseSchemaName, - databaseUrl, - schema, - }); - } - - /** - * @inheritdoc - */ - async getEnsDbVersion(): Promise { - const record = await this.getEnsNodeMetadata({ - key: EnsNodeMetadataKeys.EnsDbVersion, - }); - - return record; - } - - /** - * @inheritdoc - */ - async getEnsIndexerPublicConfig(): Promise { - const record = await this.getEnsNodeMetadata({ - key: EnsNodeMetadataKeys.EnsIndexerPublicConfig, - }); - - if (!record) { - return undefined; - } - - return deserializeEnsIndexerPublicConfig(record); - } - - /** - * @inheritdoc - */ - async getIndexingStatusSnapshot(): Promise { - const record = await this.getEnsNodeMetadata( - { - key: EnsNodeMetadataKeys.EnsIndexerIndexingStatus, - }, - ); - - if (!record) { - return undefined; - } - - return deserializeCrossChainIndexingStatusSnapshot(record); - } - - /** - * @inheritdoc - */ - async upsertEnsDbVersion(ensDbVersion: string): Promise { - await this.upsertEnsNodeMetadata({ - key: EnsNodeMetadataKeys.EnsDbVersion, - value: ensDbVersion, - }); - } - - /** - * @inheritdoc - */ - async upsertEnsIndexerPublicConfig( - ensIndexerPublicConfig: EnsIndexerPublicConfig, - ): Promise { - await this.upsertEnsNodeMetadata({ - key: EnsNodeMetadataKeys.EnsIndexerPublicConfig, - value: serializeEnsIndexerPublicConfig(ensIndexerPublicConfig), - }); - } - - /** - * @inheritdoc - */ - async upsertIndexingStatusSnapshot( - indexingStatus: CrossChainIndexingStatusSnapshot, - ): Promise { - await this.upsertEnsNodeMetadata({ - key: EnsNodeMetadataKeys.EnsIndexerIndexingStatus, - value: serializeCrossChainIndexingStatusSnapshot(indexingStatus), - }); - } - - /** - * Get ENSNode metadata record - * - * @returns selected record in ENSDb. - * @throws when more than one matching metadata record is found - * (should be impossible given the PK constraint on 'key') - */ - private async getEnsNodeMetadata( - metadata: Pick, - ): Promise { - const result = await this.db - .select() - .from(ensNodeMetadata) - .where(eq(ensNodeMetadata.key, metadata.key)); - - if (result.length === 0) { - return undefined; - } - - if (result.length === 1 && result[0]) { - return result[0].value as EnsNodeMetadataType["value"]; - } - - throw new Error(`There must be exactly one ENSNodeMetadata record for '${metadata.key}' key`); - } - - /** - * Upsert ENSNode metadata - * - * @throws when upsert operation failed. - */ - 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 }, - }); - }); - } -} diff --git a/apps/ensindexer/src/lib/ensdb-client/singleton.ts b/apps/ensindexer/src/lib/ensdb-client/singleton.ts deleted file mode 100644 index 3ab2225fd2..0000000000 --- a/apps/ensindexer/src/lib/ensdb-client/singleton.ts +++ /dev/null @@ -1,8 +0,0 @@ -import config from "@/config"; - -import { EnsDbClient } from "./ensdb-client"; - -/** - * Singleton instance of {@link EnsDbClient} for use in ENSIndexer. - */ -export const ensDbClient = new EnsDbClient(config.databaseUrl, config.databaseSchemaName); diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts index 78bcd921a0..e9fd38dbcc 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts @@ -1,6 +1,7 @@ import { getUnixTime, secondsToMilliseconds } from "date-fns"; import pRetry from "p-retry"; +import type { EnsNodeDbMutations, EnsNodeDbQueries } from "@ensnode/ensdb-sdk"; import { buildCrossChainIndexingStatusSnapshotOmnichain, type CrossChainIndexingStatusSnapshot, @@ -11,7 +12,6 @@ import { validateEnsIndexerPublicConfigCompatibility, } from "@ensnode/ensnode-sdk"; -import type { EnsDbClient } from "@/lib/ensdb-client/ensdb-client"; import type { IndexingStatusBuilder } from "@/lib/indexing-status-builder/indexing-status-builder"; import type { PublicConfigBuilder } from "@/lib/public-config-builder/public-config-builder"; @@ -21,6 +21,10 @@ import type { PublicConfigBuilder } from "@/lib/public-config-builder/public-con */ const INDEXING_STATUS_RECORD_UPDATE_INTERVAL: Duration = 1; +// Helper type to precisely define the shape of the ENSDb Client +// used by the ENSDb Writer Worker. +type EnsDbClientForEnsDbWriterWorker = EnsNodeDbMutations & EnsNodeDbQueries; + /** * ENSDb Writer Worker * @@ -38,7 +42,7 @@ export class EnsDbWriterWorker { /** * ENSDb Client instance used by the worker to interact with ENSDb. */ - private ensDbClient: EnsDbClient; + private ensDbClient: EnsDbClientForEnsDbWriterWorker; /** * Indexing Status Builder instance used by the worker to read ENSIndexer Indexing Status. @@ -56,7 +60,7 @@ export class EnsDbWriterWorker { * @param indexingStatusBuilder Indexing Status Builder instance used by the worker to read ENSIndexer Indexing Status. */ constructor( - ensDbClient: EnsDbClient, + ensDbClient: EnsDbClientForEnsDbWriterWorker, publicConfigBuilder: PublicConfigBuilder, indexingStatusBuilder: IndexingStatusBuilder, ) { diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts index d58ddc9e9d..48ef90dffe 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts @@ -1,4 +1,4 @@ -import { ensDbClient } from "@/lib/ensdb-client/singleton"; +import { ensDbClient } from "@/lib/ensdb/singleton"; import { indexingStatusBuilder } from "@/lib/indexing-status-builder/singleton"; import { publicConfigBuilder } from "@/lib/public-config-builder/singleton"; diff --git a/apps/ensindexer/src/lib/ensdb/singleton.ts b/apps/ensindexer/src/lib/ensdb/singleton.ts new file mode 100644 index 0000000000..1250e4f410 --- /dev/null +++ b/apps/ensindexer/src/lib/ensdb/singleton.ts @@ -0,0 +1,10 @@ +import config from "@/config"; + +import { EnsDbWriter } from "@ensnode/ensdb-sdk"; + +const { databaseUrl: ensDbUrl, databaseSchemaName: ensIndexerSchemaName } = config; + +/** + * Singleton instance of ENSDbWriter for the ENSIndexer application. + */ +export const ensDbClient = new EnsDbWriter(ensDbUrl, ensIndexerSchemaName); From 2df0ab256cd275420327c15b5e5076a24800a457 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 12:43:13 +0100 Subject: [PATCH 07/34] Make ENSIndexer to execute ENSNode Schema migrations in ENSDb --- apps/ensindexer/ponder/src/api/index.ts | 16 ++++++++++----- .../src/lib/ensdb/migrate-ensnode-db.ts | 20 +++++++++++++++++++ packages/ensdb-sdk/package.json | 4 +++- 3 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 apps/ensindexer/src/lib/ensdb/migrate-ensnode-db.ts diff --git a/apps/ensindexer/ponder/src/api/index.ts b/apps/ensindexer/ponder/src/api/index.ts index 68f28820fa..37b2692cc7 100644 --- a/apps/ensindexer/ponder/src/api/index.ts +++ b/apps/ensindexer/ponder/src/api/index.ts @@ -5,15 +5,21 @@ import { cors } from "hono/cors"; import type { ErrorResponse } from "@ensnode/ensnode-sdk"; +import { migrateEnsNodeDb } from "@/lib/ensdb/migrate-ensnode-db"; +import { ensDbClient } from "@/lib/ensdb/singleton"; import { startEnsDbWriterWorker } from "@/lib/ensdb-writer-worker/singleton"; import ensNodeApi from "./handlers/ensnode-api"; -// The entry point for the ENSDb Writer Worker. It must be placed inside -// the `api` directory of the Ponder app to avoid the following build issue: -// Error: Invalid dependency graph. Config, schema, and indexing function files -// cannot import objects from the API function file "src/api/index.ts". -startEnsDbWriterWorker(); +// Before starting the ENSDb Writer Worker, we need to ensure that +// the ENSNode Schema in ENSDb is up to date by running any pending migrations. +migrateEnsNodeDb(ensDbClient).then(() => { + // The entry point for the ENSDb Writer Worker. It must be placed inside + // the `api` directory of the Ponder app to avoid the following build issue: + // Error: Invalid dependency graph. Config, schema, and indexing function files + // cannot import objects from the API function file "src/api/index.ts". + startEnsDbWriterWorker(); +}); const app = new Hono(); diff --git a/apps/ensindexer/src/lib/ensdb/migrate-ensnode-db.ts b/apps/ensindexer/src/lib/ensdb/migrate-ensnode-db.ts new file mode 100644 index 0000000000..c96d8056b0 --- /dev/null +++ b/apps/ensindexer/src/lib/ensdb/migrate-ensnode-db.ts @@ -0,0 +1,20 @@ +import { createRequire } from "node:module"; +import { join } from "node:path"; + +import type { EnsNodeDbMigrations } from "@ensnode/ensdb-sdk"; + +// Resolve the path to the migrations directory within the ENSDb SDK package +const migrationsDirPath = join( + createRequire(import.meta.url).resolve("@ensnode/ensdb-sdk"), + "../../migrations", +); + +/** + * Execute database migrations for ENSNode Schema in ENSDb, using the given ENSDb client. + * @param ensDbClient - The ENSDb client to use for executing migrations. + */ +export async function migrateEnsNodeDb(ensDbClient: EnsNodeDbMigrations): Promise { + console.log(`Running database migrations for ENSNode Schema in ENSDb.`); + await ensDbClient.migrate(migrationsDirPath); + console.log(`Database migrations for ENSNode Schema in ENSDb completed successfully.`); +} diff --git a/packages/ensdb-sdk/package.json b/packages/ensdb-sdk/package.json index 8626a194c8..035301c792 100644 --- a/packages/ensdb-sdk/package.json +++ b/packages/ensdb-sdk/package.json @@ -57,6 +57,7 @@ "drizzle-kit:generate": "drizzle-kit generate" }, "peerDependencies": { + "@ensnode/ensnode-sdk": "workspace:", "drizzle-orm": "catalog:", "ponder": "catalog:", "viem": "catalog:" @@ -69,6 +70,7 @@ "ponder": "catalog:", "tsup": "catalog:", "typescript": "catalog:", - "viem": "catalog:" + "viem": "catalog:", + "vitest": "catalog:" } } From 835728517bc71e4d2be8e07da0088c71ac94e255 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 12:55:39 +0100 Subject: [PATCH 08/34] Update tests for ENSDb Writer Worker --- .../ensdb-writer-worker.mock.ts | 46 +++++++++++++++++-- .../ensdb-writer-worker.test.ts | 20 ++++---- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts index 0b98b5e516..4f0b422888 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts @@ -1,31 +1,67 @@ import { vi } from "vitest"; +import type { EnsNodeDbMutations, EnsNodeDbQueries } from "@ensnode/ensdb-sdk"; import { type CrossChainIndexingStatusSnapshot, CrossChainIndexingStrategyIds, + ENSNamespaceIds, type EnsIndexerPublicConfig, + type EnsIndexerVersionInfo, + type EnsRainbowPublicConfig, OmnichainIndexingStatusIds, type OmnichainIndexingStatusSnapshot, + PluginName, } from "@ensnode/ensnode-sdk"; -import type { EnsDbClient } from "@/lib/ensdb-client/ensdb-client"; -import * as ensDbClientMock from "@/lib/ensdb-client/ensdb-client.mock"; import type { IndexingStatusBuilder } from "@/lib/indexing-status-builder"; import type { PublicConfigBuilder } from "@/lib/public-config-builder"; +// Helper type for the combined client interface used by EnsDbWriterWorker +type EnsDbClientForWorker = EnsNodeDbMutations & EnsNodeDbQueries; + +// Test fixture for EnsRainbowPublicConfig +export const mockEnsRainbowPublicConfig: EnsRainbowPublicConfig = { + version: "1.0.0", + labelSet: { labelSetId: "subgraph", highestLabelSetVersion: 0 }, + recordsCount: 1000, +}; + +// Test fixture for EnsIndexerVersionInfo +export const mockVersionInfo: EnsIndexerVersionInfo = { + nodejs: "v20.0.0", + ponder: "0.9.0", + ensDb: "1.0.0", + ensIndexer: "1.0.0", + ensNormalize: "1.10.0", +}; + +// Test fixture for EnsIndexerPublicConfig +export const mockPublicConfig: EnsIndexerPublicConfig = { + databaseSchemaName: "public", + labelSet: { labelSetId: "subgraph", labelSetVersion: 0 }, + ensRainbowPublicConfig: mockEnsRainbowPublicConfig, + indexedChainIds: new Set([1, 8453]), + isSubgraphCompatible: true, + namespace: ENSNamespaceIds.Mainnet, + plugins: [PluginName.Subgraph], + versionInfo: mockVersionInfo, +}; + // Helper to create mock objects with consistent typing export function createMockEnsDbClient( overrides: Partial> = {}, -): EnsDbClient { +): EnsDbClientForWorker { return { ...baseEnsDbClient(), ...overrides, - } as unknown as EnsDbClient; + } as unknown as EnsDbClientForWorker; } export function baseEnsDbClient() { return { + getEnsDbVersion: vi.fn().mockResolvedValue(undefined), getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(undefined), + getIndexingStatusSnapshot: vi.fn().mockResolvedValue(undefined), upsertEnsDbVersion: vi.fn().mockResolvedValue(undefined), upsertEnsIndexerPublicConfig: vi.fn().mockResolvedValue(undefined), upsertIndexingStatusSnapshot: vi.fn().mockResolvedValue(undefined), @@ -33,7 +69,7 @@ export function baseEnsDbClient() { } export function createMockPublicConfigBuilder( - resolvedConfig: EnsIndexerPublicConfig = ensDbClientMock.publicConfig, + resolvedConfig: EnsIndexerPublicConfig = mockPublicConfig, ): PublicConfigBuilder { return { getPublicConfig: vi.fn().mockResolvedValue(resolvedConfig), diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts index f46ad5f938..2c015d5059 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts @@ -6,7 +6,6 @@ import { validateEnsIndexerPublicConfigCompatibility, } from "@ensnode/ensnode-sdk"; -import * as ensDbClientMock from "@/lib/ensdb-client/ensdb-client.mock"; import { EnsDbWriterWorker } from "@/lib/ensdb-writer-worker/ensdb-writer-worker"; import type { IndexingStatusBuilder } from "@/lib/indexing-status-builder/indexing-status-builder"; import type { PublicConfigBuilder } from "@/lib/public-config-builder/public-config-builder"; @@ -17,6 +16,7 @@ import { createMockIndexingStatusBuilder, createMockOmnichainSnapshot, createMockPublicConfigBuilder, + mockPublicConfig, } from "./ensdb-writer-worker.mock"; vi.mock("@ensnode/ensnode-sdk", async () => { @@ -61,11 +61,9 @@ describe("EnsDbWriterWorker", () => { // assert - verify initial upserts happened expect(ensDbClient.upsertEnsDbVersion).toHaveBeenCalledWith( - ensDbClientMock.publicConfig.versionInfo.ensDb, - ); - expect(ensDbClient.upsertEnsIndexerPublicConfig).toHaveBeenCalledWith( - ensDbClientMock.publicConfig, + mockPublicConfig.versionInfo.ensDb, ); + expect(ensDbClient.upsertEnsIndexerPublicConfig).toHaveBeenCalledWith(mockPublicConfig); // advance time to trigger interval await vi.advanceTimersByTimeAsync(1000); @@ -89,9 +87,9 @@ describe("EnsDbWriterWorker", () => { }); const ensDbClient = createMockEnsDbClient({ - getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(ensDbClientMock.publicConfig), + getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(mockPublicConfig), }); - const publicConfigBuilder = createMockPublicConfigBuilder(ensDbClientMock.publicConfig); + const publicConfigBuilder = createMockPublicConfigBuilder(mockPublicConfig); const indexingStatusBuilder = createMockIndexingStatusBuilder(); const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); @@ -159,9 +157,9 @@ describe("EnsDbWriterWorker", () => { }); const ensDbClient = createMockEnsDbClient({ - getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(ensDbClientMock.publicConfig), + getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(mockPublicConfig), }); - const publicConfigBuilder = createMockPublicConfigBuilder(ensDbClientMock.publicConfig); + const publicConfigBuilder = createMockPublicConfigBuilder(mockPublicConfig); const indexingStatusBuilder = createMockIndexingStatusBuilder(); const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); @@ -193,9 +191,7 @@ describe("EnsDbWriterWorker", () => { // assert - config should be called once (pRetry is mocked) expect(publicConfigBuilder.getPublicConfig).toHaveBeenCalledTimes(1); - expect(ensDbClient.upsertEnsIndexerPublicConfig).toHaveBeenCalledWith( - ensDbClientMock.publicConfig, - ); + expect(ensDbClient.upsertEnsIndexerPublicConfig).toHaveBeenCalledWith(mockPublicConfig); // cleanup worker.stop(); From b66c443f0cee08fb63096a1785b3d3d0e0470885 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 13:49:29 +0100 Subject: [PATCH 09/34] docs(changeset): Moved `ensdb` module from ENSNode SDK into ENSDb SDK. --- .changeset/quick-years-divide.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/quick-years-divide.md diff --git a/.changeset/quick-years-divide.md b/.changeset/quick-years-divide.md new file mode 100644 index 0000000000..db04346d4a --- /dev/null +++ b/.changeset/quick-years-divide.md @@ -0,0 +1,6 @@ +--- +"@ensnode/ensnode-sdk": minor +"@ensnode/ensdb-sdk": minor +--- + +Moved `ensdb` module from ENSNode SDK into ENSDb SDK. From 187a3b50ef4bbc30a95f9600d36d1a624db656d7 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 13:50:11 +0100 Subject: [PATCH 10/34] docs(changeset): Introduced two client implementations for ENSDb: `EnsDbReader` and `EnsDbWriter`. --- .changeset/thin-humans-punch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thin-humans-punch.md diff --git a/.changeset/thin-humans-punch.md b/.changeset/thin-humans-punch.md new file mode 100644 index 0000000000..ed9858ca85 --- /dev/null +++ b/.changeset/thin-humans-punch.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensdb-sdk": minor +--- + +Introduced two client implementations for ENSDb: `EnsDbReader` and `EnsDbWriter`. From bcc17842b32e209d63a144e307a334bd21d3cfce Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 13:51:43 +0100 Subject: [PATCH 11/34] docs(changeset): Replaced a bespoke `EnsDbClient` implementation with `EnsDbWriter` from ENSDb SDK. --- .changeset/beige-brooms-prove.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/beige-brooms-prove.md diff --git a/.changeset/beige-brooms-prove.md b/.changeset/beige-brooms-prove.md new file mode 100644 index 0000000000..7156207f5c --- /dev/null +++ b/.changeset/beige-brooms-prove.md @@ -0,0 +1,5 @@ +--- +"ensindexer": minor +--- + +Replaced a bespoke `EnsDbClient` implementation with `EnsDbWriter` from ENSDb SDK. From 6c82714fb29cb1334f94e8848cc1791c692a614c Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 13:52:48 +0100 Subject: [PATCH 12/34] docs(changeset): Added running database migrations for ENSDb as a responsibility for ENSIndexer. --- .changeset/common-mangos-sort.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/common-mangos-sort.md diff --git a/.changeset/common-mangos-sort.md b/.changeset/common-mangos-sort.md new file mode 100644 index 0000000000..6985dfdfe2 --- /dev/null +++ b/.changeset/common-mangos-sort.md @@ -0,0 +1,5 @@ +--- +"ensindexer": minor +--- + +Added running database migrations for ENSDb as a responsibility for ENSIndexer. From 8edabdda6ce6b145942f50c60e0df2d23dd83f65 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 14:01:12 +0100 Subject: [PATCH 13/34] Code cleanup Remove unused code --- apps/ensindexer/src/lib/ensindexer-client/singleton.ts | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 apps/ensindexer/src/lib/ensindexer-client/singleton.ts diff --git a/apps/ensindexer/src/lib/ensindexer-client/singleton.ts b/apps/ensindexer/src/lib/ensindexer-client/singleton.ts deleted file mode 100644 index 47c09b9ffc..0000000000 --- a/apps/ensindexer/src/lib/ensindexer-client/singleton.ts +++ /dev/null @@ -1,5 +0,0 @@ -import config from "@/config"; - -import { EnsIndexerClient } from "@ensnode/ensnode-sdk"; - -export const ensIndexerClient = new EnsIndexerClient({ url: config.ensIndexerUrl }); From 24f2d1b9a4ba8273f40d342341a9916a23bf2131 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 16:15:35 +0100 Subject: [PATCH 14/34] Introduce testing suite to ENSDb SDK Covers `EnsDbReader` and `EnsDbWriter`. --- .../ensdb-writer-worker.mock.ts | 2 +- .../public-config-builder.test.ts | 6 +- packages/ensdb-sdk/package.json | 2 + .../ensdb-sdk/src/client/ensdb-client.mock.ts | 72 ++++++++ .../ensdb-sdk/src/client/ensdb-reader.test.ts | 137 +++++++++++++++ .../ensdb-sdk/src/client/ensdb-writer.test.ts | 157 ++++++++++++++++++ packages/ensdb-sdk/vitest.config.ts | 15 ++ pnpm-lock.yaml | 3 + 8 files changed, 390 insertions(+), 4 deletions(-) create mode 100644 packages/ensdb-sdk/src/client/ensdb-client.mock.ts create mode 100644 packages/ensdb-sdk/src/client/ensdb-reader.test.ts create mode 100644 packages/ensdb-sdk/src/client/ensdb-writer.test.ts create mode 100644 packages/ensdb-sdk/vitest.config.ts diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts index 4f0b422888..9f4347b59e 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts @@ -28,7 +28,7 @@ export const mockEnsRainbowPublicConfig: EnsRainbowPublicConfig = { // Test fixture for EnsIndexerVersionInfo export const mockVersionInfo: EnsIndexerVersionInfo = { - nodejs: "v20.0.0", + nodejs: "20.0.0", ponder: "0.9.0", ensDb: "1.0.0", ensIndexer: "1.0.0", diff --git a/apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts b/apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts index a1e8b7ac24..ea087b349d 100644 --- a/apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts +++ b/apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts @@ -58,7 +58,7 @@ const mockEnsRainbowConfig: EnsRainbowPublicConfig = { }; const mockVersionInfo: EnsIndexerVersionInfo = { - nodejs: "v20.0.0", + nodejs: "20.0.0", ponder: "0.9.0", ensDb: "1.0.0", ensIndexer: "1.0.0", @@ -83,7 +83,7 @@ function createMockPublicConfig(overrides: Partial = {}) // Helper to setup standard mocks function setupStandardMocks() { vi.mocked(getEnsIndexerVersion).mockReturnValue("1.0.0"); - vi.mocked(getNodeJsVersion).mockReturnValue("v20.0.0"); + vi.mocked(getNodeJsVersion).mockReturnValue("20.0.0"); vi.mocked(getPackageVersion).mockReturnValue("0.9.0"); vi.mocked(validateEnsIndexerVersionInfo).mockReturnValue(mockVersionInfo); } @@ -117,7 +117,7 @@ describe("PublicConfigBuilder", () => { expect(getPackageVersion).toHaveBeenCalledWith("@adraffy/ens-normalize"); expect(validateEnsIndexerVersionInfo).toHaveBeenCalledWith({ - nodejs: "v20.0.0", + nodejs: "20.0.0", ponder: "0.9.0", ensDb: "1.0.0", ensIndexer: "1.0.0", diff --git a/packages/ensdb-sdk/package.json b/packages/ensdb-sdk/package.json index 035301c792..916da3a57c 100644 --- a/packages/ensdb-sdk/package.json +++ b/packages/ensdb-sdk/package.json @@ -54,6 +54,8 @@ "prepublish": "tsup", "lint": "biome check --write .", "lint:ci": "biome ci", + "test": "vitest", + "typecheck": "tsgo --noEmit", "drizzle-kit:generate": "drizzle-kit generate" }, "peerDependencies": { diff --git a/packages/ensdb-sdk/src/client/ensdb-client.mock.ts b/packages/ensdb-sdk/src/client/ensdb-client.mock.ts new file mode 100644 index 0000000000..7cb2c59d0b --- /dev/null +++ b/packages/ensdb-sdk/src/client/ensdb-client.mock.ts @@ -0,0 +1,72 @@ +import { + type BlockRef, + ChainIndexingStatusIds, + CrossChainIndexingStrategyIds, + type EnsIndexerPublicConfig, + OmnichainIndexingStatusIds, + PluginName, + RangeTypeIds, + type SerializedCrossChainIndexingStatusSnapshot, +} from "@ensnode/ensnode-sdk"; + +export const earlierBlockRef = { + timestamp: 1672531199, + number: 1024, +} as const satisfies BlockRef; + +export const laterBlockRef = { + timestamp: 1672531200, + number: 1025, +} as const satisfies BlockRef; + +export const ensDbUrl = "postgres://user:pass@localhost:5432/ensdb"; + +export const ensIndexerSchemaName = "ensindexer_0"; + +export const publicConfig = { + databaseSchemaName: ensIndexerSchemaName, + ensRainbowPublicConfig: { + version: "0.32.0", + labelSet: { + labelSetId: "subgraph", + highestLabelSetVersion: 0, + }, + recordsCount: 100, + }, + labelSet: { + labelSetId: "subgraph", + labelSetVersion: 0, + }, + indexedChainIds: new Set([1]), + isSubgraphCompatible: true, + namespace: "mainnet", + plugins: [PluginName.Subgraph], + versionInfo: { + nodejs: "22.10.12", + ponder: "0.11.25", + ensDb: "0.32.0", + ensIndexer: "0.32.0", + ensNormalize: "1.11.1", + }, +} satisfies EnsIndexerPublicConfig; + +export const serializedSnapshot = { + strategy: CrossChainIndexingStrategyIds.Omnichain, + slowestChainIndexingCursor: earlierBlockRef.timestamp, + snapshotTime: earlierBlockRef.timestamp + 20, + omnichainSnapshot: { + omnichainStatus: OmnichainIndexingStatusIds.Following, + chains: { + "1": { + chainStatus: ChainIndexingStatusIds.Following, + config: { + rangeType: RangeTypeIds.LeftBounded, + startBlock: earlierBlockRef, + }, + latestIndexedBlock: earlierBlockRef, + latestKnownBlock: laterBlockRef, + }, + }, + omnichainIndexingCursor: earlierBlockRef.timestamp, + }, +} satisfies SerializedCrossChainIndexingStatusSnapshot; diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts new file mode 100644 index 0000000000..8946986275 --- /dev/null +++ b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts @@ -0,0 +1,137 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + deserializeCrossChainIndexingStatusSnapshot, + serializeEnsIndexerPublicConfig, +} from "@ensnode/ensnode-sdk"; + +import * as ensNodeSchema from "../ensnode"; +import { buildEnsDbDrizzleClient } from "../lib/drizzle"; +import * as ensDbClientMock from "./ensdb-client.mock"; +import { EnsDbReader } from "./ensdb-reader"; + +// Mock the buildEnsDbDrizzleClient function to return a mock database instance +vi.mock("../lib/drizzle", () => ({ buildEnsDbDrizzleClient: vi.fn() })); + +describe("EnsDbReader", () => { + // Mock database query results and methods + const selectResult = { current: [] as Array<{ value: unknown }> }; + const whereMock = vi.fn(async () => selectResult.current); + const fromMock = vi.fn(() => ({ where: whereMock })); + const selectMock = vi.fn(() => ({ from: fromMock })); + const onConflictDoUpdateMock = vi.fn(async () => undefined); + const valuesMock = vi.fn(() => ({ onConflictDoUpdate: onConflictDoUpdateMock })); + const insertMock = vi.fn(() => ({ values: valuesMock })); + const executeMock = vi.fn(async () => undefined); + const txMock = { insert: insertMock, execute: executeMock }; + const transactionMock = vi.fn(async (callback: (tx: typeof txMock) => Promise) => + callback(txMock), + ); + const dbMock = { select: selectMock, insert: insertMock, transaction: transactionMock }; + + beforeEach(() => { + selectResult.current = []; + whereMock.mockClear(); + fromMock.mockClear(); + selectMock.mockClear(); + onConflictDoUpdateMock.mockClear(); + valuesMock.mockClear(); + insertMock.mockClear(); + executeMock.mockClear(); + transactionMock.mockClear(); + vi.mocked(buildEnsDbDrizzleClient).mockReturnValue( + dbMock as unknown as ReturnType, + ); + }); + + describe("getEnsDbVersion", () => { + it("returns undefined when no record exists", async () => { + // arrange + const client = new EnsDbReader( + ensDbClientMock.ensDbUrl, + ensDbClientMock.ensIndexerSchemaName, + ); + + // act & assert + await expect(client.getEnsDbVersion()).resolves.toBeUndefined(); + + expect(selectMock).toHaveBeenCalledTimes(1); + expect(fromMock).toHaveBeenCalledWith(ensNodeSchema.metadata); + }); + + it("returns value when one record exists", async () => { + // arrange + selectResult.current = [{ value: "0.1.0" }]; + + const client = new EnsDbReader( + ensDbClientMock.ensDbUrl, + ensDbClientMock.ensIndexerSchemaName, + ); + + // act & assert + await expect(client.getEnsDbVersion()).resolves.toBe("0.1.0"); + }); + + // This scenario should be impossible due to the primary key constraint on + // the 'key' column of 'ensnode_metadata' table. + it("throws when multiple records exist", async () => { + // arrange + selectResult.current = [{ value: "0.1.0" }, { value: "0.1.1" }]; + + const client = new EnsDbReader( + ensDbClientMock.ensDbUrl, + ensDbClientMock.ensIndexerSchemaName, + ); + + // act & assert + await expect(client.getEnsDbVersion()).rejects.toThrowError(/ensdb_version/i); + }); + }); + + describe("getEnsIndexerPublicConfig", () => { + it("returns undefined when no record exists", async () => { + // arrange + const client = new EnsDbReader( + ensDbClientMock.ensDbUrl, + ensDbClientMock.ensIndexerSchemaName, + ); + + // act & assert + await expect(client.getEnsIndexerPublicConfig()).resolves.toBeUndefined(); + }); + + it("deserializes the stored config", async () => { + // arrange + const serializedConfig = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig); + selectResult.current = [{ value: serializedConfig }]; + + const client = new EnsDbReader( + ensDbClientMock.ensDbUrl, + ensDbClientMock.ensIndexerSchemaName, + ); + + // act & assert + await expect(client.getEnsIndexerPublicConfig()).resolves.toStrictEqual( + ensDbClientMock.publicConfig, + ); + }); + }); + + describe("getIndexingStatusSnapshot", () => { + it("deserializes the stored indexing status snapshot", async () => { + // arrange + selectResult.current = [{ value: ensDbClientMock.serializedSnapshot }]; + + const client = new EnsDbReader( + ensDbClientMock.ensDbUrl, + ensDbClientMock.ensIndexerSchemaName, + ); + const expected = deserializeCrossChainIndexingStatusSnapshot( + ensDbClientMock.serializedSnapshot, + ); + + // act & assert + await expect(client.getIndexingStatusSnapshot()).resolves.toStrictEqual(expected); + }); + }); +}); diff --git a/packages/ensdb-sdk/src/client/ensdb-writer.test.ts b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts new file mode 100644 index 0000000000..d1ba033db8 --- /dev/null +++ b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts @@ -0,0 +1,157 @@ +import { migrate } from "drizzle-orm/node-postgres/migrator"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + deserializeCrossChainIndexingStatusSnapshot, + serializeCrossChainIndexingStatusSnapshot, + serializeEnsIndexerPublicConfig, +} from "@ensnode/ensnode-sdk"; + +import * as ensNodeSchema from "../ensnode"; +import { buildEnsDbDrizzleClient } from "../lib/drizzle"; +import * as ensDbClientMock from "./ensdb-client.mock"; +import { EnsDbWriter } from "./ensdb-writer"; +import { EnsNodeMetadataKeys } from "./ensnode-metadata"; + +// Mock the buildEnsDbDrizzleClient function to return a mock database instance +vi.mock("../lib/drizzle", () => ({ buildEnsDbDrizzleClient: vi.fn() })); + +// Mock the drizzle-orm migrator +vi.mock("drizzle-orm/node-postgres/migrator", () => ({ migrate: vi.fn() })); + +describe("EnsDbWriter", () => { + // Mock database query results and methods + const selectResult = { current: [] as Array<{ value: unknown }> }; + const whereMock = vi.fn(async () => selectResult.current); + const fromMock = vi.fn(() => ({ where: whereMock })); + const selectMock = vi.fn(() => ({ from: fromMock })); + const onConflictDoUpdateMock = vi.fn(async () => undefined); + const valuesMock = vi.fn(() => ({ onConflictDoUpdate: onConflictDoUpdateMock })); + const insertMock = vi.fn(() => ({ values: valuesMock })); + const executeMock = vi.fn(async () => undefined); + const txMock = { insert: insertMock, execute: executeMock }; + const transactionMock = vi.fn(async (callback: (tx: typeof txMock) => Promise) => + callback(txMock), + ); + const dbMock = { select: selectMock, insert: insertMock, transaction: transactionMock }; + + beforeEach(() => { + selectResult.current = []; + whereMock.mockClear(); + fromMock.mockClear(); + selectMock.mockClear(); + onConflictDoUpdateMock.mockClear(); + valuesMock.mockClear(); + insertMock.mockClear(); + executeMock.mockClear(); + transactionMock.mockClear(); + vi.mocked(migrate).mockClear(); + vi.mocked(buildEnsDbDrizzleClient).mockReturnValue( + dbMock as unknown as ReturnType, + ); + }); + + describe("upsertEnsDbVersion", () => { + it("writes the database version metadata", async () => { + // arrange + const client = new EnsDbWriter( + ensDbClientMock.ensDbUrl, + ensDbClientMock.ensIndexerSchemaName, + ); + + // act + await client.upsertEnsDbVersion("0.2.0"); + + // assert + expect(insertMock).toHaveBeenCalledWith(ensNodeSchema.metadata); + expect(valuesMock).toHaveBeenCalledWith({ + ensIndexerSchemaName: ensDbClientMock.ensIndexerSchemaName, + key: EnsNodeMetadataKeys.EnsDbVersion, + value: "0.2.0", + }); + expect(onConflictDoUpdateMock).toHaveBeenCalledWith({ + target: [ensNodeSchema.metadata.ensIndexerSchemaName, ensNodeSchema.metadata.key], + set: { value: "0.2.0" }, + }); + }); + }); + + describe("upsertEnsIndexerPublicConfig", () => { + it("serializes and writes the public config", async () => { + // arrange + const client = new EnsDbWriter( + ensDbClientMock.ensDbUrl, + ensDbClientMock.ensIndexerSchemaName, + ); + const expectedValue = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig); + + // act + await client.upsertEnsIndexerPublicConfig(ensDbClientMock.publicConfig); + + // assert + expect(valuesMock).toHaveBeenCalledWith({ + ensIndexerSchemaName: ensDbClientMock.ensIndexerSchemaName, + key: EnsNodeMetadataKeys.EnsIndexerPublicConfig, + value: expectedValue, + }); + }); + }); + + describe("upsertIndexingStatusSnapshot", () => { + it("serializes and writes the indexing status snapshot", async () => { + // arrange + const client = new EnsDbWriter( + ensDbClientMock.ensDbUrl, + ensDbClientMock.ensIndexerSchemaName, + ); + const snapshot = deserializeCrossChainIndexingStatusSnapshot( + ensDbClientMock.serializedSnapshot, + ); + const expectedValue = serializeCrossChainIndexingStatusSnapshot(snapshot); + + // act + await client.upsertIndexingStatusSnapshot(snapshot); + + // assert + expect(valuesMock).toHaveBeenCalledWith({ + ensIndexerSchemaName: ensDbClientMock.ensIndexerSchemaName, + key: EnsNodeMetadataKeys.EnsIndexerIndexingStatus, + value: expectedValue, + }); + }); + }); + + describe("migrate", () => { + it("calls drizzle-orm migrate with the correct parameters", async () => { + // arrange + const client = new EnsDbWriter( + ensDbClientMock.ensDbUrl, + ensDbClientMock.ensIndexerSchemaName, + ); + const migrationsDirPath = "/path/to/migrations"; + + // act + await client.migrate(migrationsDirPath); + + // assert + expect(vi.mocked(migrate)).toHaveBeenCalledWith(dbMock, { + migrationsFolder: migrationsDirPath, + migrationsSchema: "ensnode", + }); + }); + + it("propagates errors from the migrate function", async () => { + // arrange + const client = new EnsDbWriter( + ensDbClientMock.ensDbUrl, + ensDbClientMock.ensIndexerSchemaName, + ); + const migrationsDirPath = "/path/to/migrations"; + const error = new Error("Migration failed"); + vi.mocked(migrate).mockRejectedValueOnce(error); + + // act & assert + await expect(client.migrate(migrationsDirPath)).rejects.toThrow("Migration failed"); + }); + }); +}); diff --git a/packages/ensdb-sdk/vitest.config.ts b/packages/ensdb-sdk/vitest.config.ts new file mode 100644 index 0000000000..8e3d64e885 --- /dev/null +++ b/packages/ensdb-sdk/vitest.config.ts @@ -0,0 +1,15 @@ +import { resolve } from "node:path"; + +import { configDefaults, defineProject } from "vitest/config"; + +export default defineProject({ + resolve: { + alias: { + "@": resolve(__dirname, "./src"), + }, + }, + test: { + environment: "node", + exclude: [...configDefaults.exclude, "**/*.integration.test.ts"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9eb2ff248f..0faedd59e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -857,6 +857,9 @@ importers: viem: specifier: 'catalog:' version: 2.38.5(typescript@5.9.3)(zod@3.25.76) + vitest: + specifier: 'catalog:' + 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.21.0)(yaml@2.8.1) packages/ensnode-react: dependencies: From 4b20c506810e49ab3e60871dfd921ebe7af147a1 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 18:27:36 +0100 Subject: [PATCH 15/34] Fix typo --- packages/ensdb-sdk/src/client/ensdb-reader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.ts b/packages/ensdb-sdk/src/client/ensdb-reader.ts index 34ff175c55..92d2349e48 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.ts @@ -61,7 +61,7 @@ export class EnsDbReader implements EnsNodeDbQueries { } /** - * Bind a ENSIndexer Schema definition with a specific instance of + * Bind an ENSIndexer Schema definition with a specific instance of * ENSIndexer Schema in ENSDb. * * ENSIndexer Schema definition does not have a fixed database schema name, From 4e216f4ce88a797b91d10a25520d0b4a41ef7708 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 19 Mar 2026 19:34:10 +0100 Subject: [PATCH 16/34] Apply AI PR feedback --- .../public-config-builder.test.ts | 4 ++-- .../ensdb-sdk/src/client/ensdb-reader.test.ts | 15 +-------------- packages/ensdb-sdk/src/client/ensdb-reader.ts | 4 +++- .../ensdb-sdk/src/client/ensdb-writer.test.ts | 9 +-------- 4 files changed, 7 insertions(+), 25 deletions(-) diff --git a/apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts b/apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts index ea087b349d..57c8b9f225 100644 --- a/apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts +++ b/apps/ensindexer/src/lib/public-config-builder/public-config-builder.test.ts @@ -183,11 +183,11 @@ describe("PublicConfigBuilder", () => { } as unknown as EnsRainbowApiClient; vi.mocked(getEnsIndexerVersion).mockReturnValue("2.0.0"); - vi.mocked(getNodeJsVersion).mockReturnValue("v22.0.0"); + vi.mocked(getNodeJsVersion).mockReturnValue("22.0.0"); vi.mocked(getPackageVersion).mockReturnValue("1.0.0"); const customVersionInfo: EnsIndexerVersionInfo = { - nodejs: "v22.0.0", + nodejs: "22.0.0", ponder: "1.0.0", ensDb: "2.0.0", ensIndexer: "2.0.0", diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts index 8946986275..84aaf7be4d 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts @@ -19,26 +19,13 @@ describe("EnsDbReader", () => { const whereMock = vi.fn(async () => selectResult.current); const fromMock = vi.fn(() => ({ where: whereMock })); const selectMock = vi.fn(() => ({ from: fromMock })); - const onConflictDoUpdateMock = vi.fn(async () => undefined); - const valuesMock = vi.fn(() => ({ onConflictDoUpdate: onConflictDoUpdateMock })); - const insertMock = vi.fn(() => ({ values: valuesMock })); - const executeMock = vi.fn(async () => undefined); - const txMock = { insert: insertMock, execute: executeMock }; - const transactionMock = vi.fn(async (callback: (tx: typeof txMock) => Promise) => - callback(txMock), - ); - const dbMock = { select: selectMock, insert: insertMock, transaction: transactionMock }; + const dbMock = { select: selectMock }; beforeEach(() => { selectResult.current = []; whereMock.mockClear(); fromMock.mockClear(); selectMock.mockClear(); - onConflictDoUpdateMock.mockClear(); - valuesMock.mockClear(); - insertMock.mockClear(); - executeMock.mockClear(); - transactionMock.mockClear(); vi.mocked(buildEnsDbDrizzleClient).mockReturnValue( dbMock as unknown as ReturnType, ); diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.ts b/packages/ensdb-sdk/src/client/ensdb-reader.ts index 92d2349e48..2cc5d370cc 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.ts @@ -163,6 +163,8 @@ export class EnsDbReader implements EnsNodeDbQueries { return result[0].value as EnsNodeMetadataType["value"]; } - throw new Error(`There must be exactly one ENSNodeMetadata record for '${metadata.key}' key`); + throw new Error( + `There must be exactly one ENSNodeMetadata record for ('${this.ensIndexerSchemaName}', '${metadata.key}') composite key`, + ); } } diff --git a/packages/ensdb-sdk/src/client/ensdb-writer.test.ts b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts index d1ba033db8..e07dd035e5 100644 --- a/packages/ensdb-sdk/src/client/ensdb-writer.test.ts +++ b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts @@ -28,12 +28,7 @@ describe("EnsDbWriter", () => { const onConflictDoUpdateMock = vi.fn(async () => undefined); const valuesMock = vi.fn(() => ({ onConflictDoUpdate: onConflictDoUpdateMock })); const insertMock = vi.fn(() => ({ values: valuesMock })); - const executeMock = vi.fn(async () => undefined); - const txMock = { insert: insertMock, execute: executeMock }; - const transactionMock = vi.fn(async (callback: (tx: typeof txMock) => Promise) => - callback(txMock), - ); - const dbMock = { select: selectMock, insert: insertMock, transaction: transactionMock }; + const dbMock = { select: selectMock, insert: insertMock }; beforeEach(() => { selectResult.current = []; @@ -43,8 +38,6 @@ describe("EnsDbWriter", () => { onConflictDoUpdateMock.mockClear(); valuesMock.mockClear(); insertMock.mockClear(); - executeMock.mockClear(); - transactionMock.mockClear(); vi.mocked(migrate).mockClear(); vi.mocked(buildEnsDbDrizzleClient).mockReturnValue( dbMock as unknown as ReturnType, From 83cf6cf3efe07ae2dc4ef64e2cd6384108536ac1 Mon Sep 17 00:00:00 2001 From: Tomek Kopacki Date: Fri, 20 Mar 2026 10:53:41 +0100 Subject: [PATCH 17/34] Apply suggestions from code review Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- packages/ensdb-sdk/src/client/ensdb-reader.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.ts b/packages/ensdb-sdk/src/client/ensdb-reader.ts index 2cc5d370cc..5b7e376008 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.ts @@ -24,8 +24,7 @@ import type { /** * ENSDb Reader * - * Allows querying an ENSDb instance, including ENSNode Metadata records, - * as well as complex queries across multiple database schemas in ENSDb. + * Enables read-only querying of an ENSDb instance, including data spanning the ENSNode Schema and the specified ENSIndexer Schema. */ export class EnsDbReader implements EnsNodeDbQueries { /** @@ -34,16 +33,16 @@ export class EnsDbReader implements EnsNodeDbQueries { protected drizzleClient: EnsDbDrizzle; /** - * References the ENSIndexer instance's database schema in ENSDb. - * - * It is required to scope the ENSNode metadata records to - * a specific ENSIndexer instance in ENSDb. + * The name of the ENSIndexer schema to read from in ENSDb. + * + * This also identifies which ENSNode metadata records to read from the ENSNode Schema + * as the ENSNode Schema is multi-tenant across ENSIndexer instances / ENSIndexer Schemas in an ENSDb. */ protected ensIndexerSchemaName: string; /** * @param ensDbConnectionString connection string for ENSDb Postgres database - * @param ensIndexerSchemaName reference string for ENSIndexer instance + * @param ensIndexerSchemaName the name of the ENSIndexer Schema to read from in ENSDb */ constructor(ensDbConnectionString: string, ensIndexerSchemaName: string) { EnsDbReader.bindEnsIndexerSchemaWithName(ensIndexerSchemaName); From f03192e569933bc35640017cf0fefb4cc73e4381 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Mar 2026 11:12:09 +0100 Subject: [PATCH 18/34] Apply PR feedback Removed ENSDb SDK interfaces. Renamed `ensDbClient` to `ensDbWriter`. --- .../ponder/src/api/handlers/ensnode-api.ts | 6 +- apps/ensindexer/ponder/src/api/index.ts | 5 +- .../ensdb-writer-worker.mock.ts | 17 ++--- .../ensdb-writer-worker.test.ts | 72 +++++++++---------- .../ensdb-writer-worker.ts | 22 +++--- .../src/lib/ensdb-writer-worker/singleton.ts | 4 +- ...nsnode-db.ts => migrate-ensnode-schema.ts} | 9 ++- apps/ensindexer/src/lib/ensdb/singleton.ts | 2 +- packages/ensdb-sdk/src/client/ensdb-reader.ts | 27 ++++--- .../ensdb-sdk/src/client/ensdb-writer.test.ts | 12 ++-- packages/ensdb-sdk/src/client/ensdb-writer.ts | 24 ++++--- .../src/client/ensnode-db-migrations.ts | 15 ---- .../src/client/ensnode-db-mutations.ts | 32 --------- .../src/client/ensnode-db-queries.ts | 32 --------- packages/ensdb-sdk/src/client/index.ts | 3 - 15 files changed, 103 insertions(+), 179 deletions(-) rename apps/ensindexer/src/lib/ensdb/{migrate-ensnode-db.ts => migrate-ensnode-schema.ts} (55%) delete mode 100644 packages/ensdb-sdk/src/client/ensnode-db-migrations.ts delete mode 100644 packages/ensdb-sdk/src/client/ensnode-db-mutations.ts delete mode 100644 packages/ensdb-sdk/src/client/ensnode-db-queries.ts diff --git a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts index fc6a46e796..5e563c77db 100644 --- a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts +++ b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts @@ -10,13 +10,13 @@ import { serializeIndexingStatusResponse, } from "@ensnode/ensnode-sdk"; -import { ensDbClient } from "@/lib/ensdb/singleton"; +import { ensDbWriter } from "@/lib/ensdb/singleton"; const app = new Hono(); // include ENSIndexer Public Config endpoint app.get("/config", async (c) => { - const publicConfig = await ensDbClient.getEnsIndexerPublicConfig(); + const publicConfig = await ensDbWriter.getEnsIndexerPublicConfig(); // Invariant: the public config is guaranteed to be available in ENSDb after // application startup. @@ -30,7 +30,7 @@ app.get("/config", async (c) => { app.get("/indexing-status", async (c) => { try { - const crossChainSnapshot = await ensDbClient.getIndexingStatusSnapshot(); + const crossChainSnapshot = await ensDbWriter.getIndexingStatusSnapshot(); // Invariant: the Indexing Status Snapshot is expected to be available in // ENSDb shortly after application startup. There is a possibility that diff --git a/apps/ensindexer/ponder/src/api/index.ts b/apps/ensindexer/ponder/src/api/index.ts index 37b2692cc7..bfb67eb2bb 100644 --- a/apps/ensindexer/ponder/src/api/index.ts +++ b/apps/ensindexer/ponder/src/api/index.ts @@ -5,15 +5,14 @@ import { cors } from "hono/cors"; import type { ErrorResponse } from "@ensnode/ensnode-sdk"; -import { migrateEnsNodeDb } from "@/lib/ensdb/migrate-ensnode-db"; -import { ensDbClient } from "@/lib/ensdb/singleton"; +import { migrateEnsNodeSchema } from "@/lib/ensdb/migrate-ensnode-schema"; import { startEnsDbWriterWorker } from "@/lib/ensdb-writer-worker/singleton"; import ensNodeApi from "./handlers/ensnode-api"; // Before starting the ENSDb Writer Worker, we need to ensure that // the ENSNode Schema in ENSDb is up to date by running any pending migrations. -migrateEnsNodeDb(ensDbClient).then(() => { +migrateEnsNodeSchema().then(() => { // The entry point for the ENSDb Writer Worker. It must be placed inside // the `api` directory of the Ponder app to avoid the following build issue: // Error: Invalid dependency graph. Config, schema, and indexing function files diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts index 9f4347b59e..232428743c 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts @@ -1,6 +1,6 @@ import { vi } from "vitest"; -import type { EnsNodeDbMutations, EnsNodeDbQueries } from "@ensnode/ensdb-sdk"; +import type { EnsDbWriter } from "@ensnode/ensdb-sdk"; import { type CrossChainIndexingStatusSnapshot, CrossChainIndexingStrategyIds, @@ -16,9 +16,6 @@ import { import type { IndexingStatusBuilder } from "@/lib/indexing-status-builder"; import type { PublicConfigBuilder } from "@/lib/public-config-builder"; -// Helper type for the combined client interface used by EnsDbWriterWorker -type EnsDbClientForWorker = EnsNodeDbMutations & EnsNodeDbQueries; - // Test fixture for EnsRainbowPublicConfig export const mockEnsRainbowPublicConfig: EnsRainbowPublicConfig = { version: "1.0.0", @@ -48,16 +45,16 @@ export const mockPublicConfig: EnsIndexerPublicConfig = { }; // Helper to create mock objects with consistent typing -export function createMockEnsDbClient( - overrides: Partial> = {}, -): EnsDbClientForWorker { +export function createMockEnsDbWriter( + overrides: Partial> = {}, +): EnsDbWriter { return { - ...baseEnsDbClient(), + ...baseEnsDbWriter(), ...overrides, - } as unknown as EnsDbClientForWorker; + } as unknown as EnsDbWriter; } -export function baseEnsDbClient() { +export function baseEnsDbWriter() { return { getEnsDbVersion: vi.fn().mockResolvedValue(undefined), getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(undefined), diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts index 2c015d5059..6e5619c14a 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts @@ -12,7 +12,7 @@ import type { PublicConfigBuilder } from "@/lib/public-config-builder/public-con import { createMockCrossChainSnapshot, - createMockEnsDbClient, + createMockEnsDbWriter, createMockIndexingStatusBuilder, createMockOmnichainSnapshot, createMockPublicConfigBuilder, @@ -50,26 +50,26 @@ describe("EnsDbWriterWorker", () => { const snapshot = createMockCrossChainSnapshot({ omnichainSnapshot }); vi.mocked(buildCrossChainIndexingStatusSnapshotOmnichain).mockReturnValue(snapshot); - const ensDbClient = createMockEnsDbClient(); + const ensDbWriter = createMockEnsDbWriter(); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = createMockIndexingStatusBuilder(omnichainSnapshot); - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); // act await worker.run(); // assert - verify initial upserts happened - expect(ensDbClient.upsertEnsDbVersion).toHaveBeenCalledWith( + expect(ensDbWriter.upsertEnsDbVersion).toHaveBeenCalledWith( mockPublicConfig.versionInfo.ensDb, ); - expect(ensDbClient.upsertEnsIndexerPublicConfig).toHaveBeenCalledWith(mockPublicConfig); + expect(ensDbWriter.upsertEnsIndexerPublicConfig).toHaveBeenCalledWith(mockPublicConfig); // advance time to trigger interval await vi.advanceTimersByTimeAsync(1000); // assert - snapshot should be upserted - expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenCalledWith(snapshot); + expect(ensDbWriter.upsertIndexingStatusSnapshot).toHaveBeenCalledWith(snapshot); expect(buildCrossChainIndexingStatusSnapshotOmnichain).toHaveBeenCalledWith( omnichainSnapshot, expect.any(Number), @@ -86,26 +86,26 @@ describe("EnsDbWriterWorker", () => { throw incompatibleError; }); - const ensDbClient = createMockEnsDbClient({ + const ensDbWriter = createMockEnsDbWriter({ getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(mockPublicConfig), }); const publicConfigBuilder = createMockPublicConfigBuilder(mockPublicConfig); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); // act & assert await expect(worker.run()).rejects.toThrow("incompatible"); - expect(ensDbClient.upsertEnsDbVersion).not.toHaveBeenCalled(); + expect(ensDbWriter.upsertEnsDbVersion).not.toHaveBeenCalled(); }); it("throws error when worker is already running", async () => { // arrange - const ensDbClient = createMockEnsDbClient(); + const ensDbWriter = createMockEnsDbWriter(); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); // act - first run await worker.run(); @@ -120,34 +120,34 @@ describe("EnsDbWriterWorker", () => { it("throws error when config fetch fails", async () => { // arrange const networkError = new Error("Network failure"); - const ensDbClient = createMockEnsDbClient(); + const ensDbWriter = createMockEnsDbWriter(); const publicConfigBuilder = { getPublicConfig: vi.fn().mockRejectedValue(networkError), } as unknown as PublicConfigBuilder; const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); // act & assert await expect(worker.run()).rejects.toThrow("Network failure"); expect(publicConfigBuilder.getPublicConfig).toHaveBeenCalledTimes(1); - expect(ensDbClient.upsertEnsDbVersion).not.toHaveBeenCalled(); + expect(ensDbWriter.upsertEnsDbVersion).not.toHaveBeenCalled(); }); it("throws error when stored config fetch fails", async () => { // arrange const dbError = new Error("Database connection lost"); - const ensDbClient = createMockEnsDbClient({ + const ensDbWriter = createMockEnsDbWriter({ getEnsIndexerPublicConfig: vi.fn().mockRejectedValue(dbError), }); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); // act & assert await expect(worker.run()).rejects.toThrow("Database connection lost"); - expect(ensDbClient.upsertEnsDbVersion).not.toHaveBeenCalled(); + expect(ensDbWriter.upsertEnsDbVersion).not.toHaveBeenCalled(); }); it("fetches stored and in-memory configs concurrently", async () => { @@ -156,19 +156,19 @@ describe("EnsDbWriterWorker", () => { // validation passes }); - const ensDbClient = createMockEnsDbClient({ + const ensDbWriter = createMockEnsDbWriter({ getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(mockPublicConfig), }); const publicConfigBuilder = createMockPublicConfigBuilder(mockPublicConfig); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); // act await worker.run(); // assert - both should have been called (concurrent execution via Promise.all) - expect(ensDbClient.getEnsIndexerPublicConfig).toHaveBeenCalledTimes(1); + expect(ensDbWriter.getEnsIndexerPublicConfig).toHaveBeenCalledTimes(1); expect(publicConfigBuilder.getPublicConfig).toHaveBeenCalledTimes(1); // cleanup @@ -180,18 +180,18 @@ describe("EnsDbWriterWorker", () => { const snapshot = createMockCrossChainSnapshot(); vi.mocked(buildCrossChainIndexingStatusSnapshotOmnichain).mockReturnValue(snapshot); - const ensDbClient = createMockEnsDbClient(); + const ensDbWriter = createMockEnsDbWriter(); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); // act await worker.run(); // assert - config should be called once (pRetry is mocked) expect(publicConfigBuilder.getPublicConfig).toHaveBeenCalledTimes(1); - expect(ensDbClient.upsertEnsIndexerPublicConfig).toHaveBeenCalledWith(mockPublicConfig); + expect(ensDbWriter.upsertEnsIndexerPublicConfig).toHaveBeenCalledWith(mockPublicConfig); // cleanup worker.stop(); @@ -202,11 +202,11 @@ describe("EnsDbWriterWorker", () => { it("stops the interval when stop() is called", async () => { // arrange const upsertIndexingStatusSnapshot = vi.fn().mockResolvedValue(undefined); - const ensDbClient = createMockEnsDbClient({ upsertIndexingStatusSnapshot }); + const ensDbWriter = createMockEnsDbWriter({ upsertIndexingStatusSnapshot }); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); // act await worker.run(); @@ -227,11 +227,11 @@ describe("EnsDbWriterWorker", () => { describe("isRunning - worker state", () => { it("indicates isRunning status correctly", async () => { // arrange - const ensDbClient = createMockEnsDbClient(); + const ensDbWriter = createMockEnsDbWriter(); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); // assert - not running initially expect(worker.isRunning).toBe(false); @@ -267,7 +267,7 @@ describe("EnsDbWriterWorker", () => { vi.mocked(buildCrossChainIndexingStatusSnapshotOmnichain).mockReturnValue(crossChainSnapshot); - const ensDbClient = createMockEnsDbClient(); + const ensDbWriter = createMockEnsDbWriter(); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = { getOmnichainIndexingStatusSnapshot: vi @@ -276,7 +276,7 @@ describe("EnsDbWriterWorker", () => { .mockResolvedValueOnce(validSnapshot), } as unknown as IndexingStatusBuilder; - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); // act - run returns immediately await worker.run(); @@ -289,8 +289,8 @@ describe("EnsDbWriterWorker", () => { // assert expect(indexingStatusBuilder.getOmnichainIndexingStatusSnapshot).toHaveBeenCalledTimes(2); - expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenCalledTimes(1); - expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenCalledWith(crossChainSnapshot); + expect(ensDbWriter.upsertIndexingStatusSnapshot).toHaveBeenCalledTimes(1); + expect(ensDbWriter.upsertIndexingStatusSnapshot).toHaveBeenCalledWith(crossChainSnapshot); // cleanup worker.stop(); @@ -317,7 +317,7 @@ describe("EnsDbWriterWorker", () => { .mockReturnValueOnce(crossChainSnapshot2) .mockReturnValueOnce(crossChainSnapshot2); - const ensDbClient = createMockEnsDbClient({ + const ensDbWriter = createMockEnsDbWriter({ upsertIndexingStatusSnapshot: vi .fn() .mockResolvedValueOnce(undefined) @@ -333,24 +333,24 @@ describe("EnsDbWriterWorker", () => { .mockResolvedValueOnce(snapshot2), } as unknown as IndexingStatusBuilder; - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); // act await worker.run(); // first tick - succeeds await vi.advanceTimersByTimeAsync(1000); - expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenCalledWith(crossChainSnapshot1); + expect(ensDbWriter.upsertIndexingStatusSnapshot).toHaveBeenCalledWith(crossChainSnapshot1); // second tick - fails with DB error, but continues await vi.advanceTimersByTimeAsync(1000); - expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenLastCalledWith( + expect(ensDbWriter.upsertIndexingStatusSnapshot).toHaveBeenLastCalledWith( crossChainSnapshot2, ); // third tick - succeeds again await vi.advanceTimersByTimeAsync(1000); - expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenCalledTimes(3); + expect(ensDbWriter.upsertIndexingStatusSnapshot).toHaveBeenCalledTimes(3); // cleanup worker.stop(); diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts index e9fd38dbcc..8dc66b2ae2 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts @@ -1,7 +1,7 @@ import { getUnixTime, secondsToMilliseconds } from "date-fns"; import pRetry from "p-retry"; -import type { EnsNodeDbMutations, EnsNodeDbQueries } from "@ensnode/ensdb-sdk"; +import type { EnsDbWriter } from "@ensnode/ensdb-sdk"; import { buildCrossChainIndexingStatusSnapshotOmnichain, type CrossChainIndexingStatusSnapshot, @@ -21,10 +21,6 @@ import type { PublicConfigBuilder } from "@/lib/public-config-builder/public-con */ const INDEXING_STATUS_RECORD_UPDATE_INTERVAL: Duration = 1; -// Helper type to precisely define the shape of the ENSDb Client -// used by the ENSDb Writer Worker. -type EnsDbClientForEnsDbWriterWorker = EnsNodeDbMutations & EnsNodeDbQueries; - /** * ENSDb Writer Worker * @@ -42,7 +38,7 @@ export class EnsDbWriterWorker { /** * ENSDb Client instance used by the worker to interact with ENSDb. */ - private ensDbClient: EnsDbClientForEnsDbWriterWorker; + private ensDbWriter: EnsDbWriter; /** * Indexing Status Builder instance used by the worker to read ENSIndexer Indexing Status. @@ -55,16 +51,16 @@ export class EnsDbWriterWorker { private publicConfigBuilder: PublicConfigBuilder; /** - * @param ensDbClient ENSDb Client instance used by the worker to interact with ENSDb. + * @param ensDbWriter ENSDb Writer instance used by the worker to interact with ENSDb. * @param publicConfigBuilder ENSIndexer Public Config Builder instance used by the worker to read ENSIndexer Public Config. * @param indexingStatusBuilder Indexing Status Builder instance used by the worker to read ENSIndexer Indexing Status. */ constructor( - ensDbClient: EnsDbClientForEnsDbWriterWorker, + ensDbWriter: EnsDbWriter, publicConfigBuilder: PublicConfigBuilder, indexingStatusBuilder: IndexingStatusBuilder, ) { - this.ensDbClient = ensDbClient; + this.ensDbWriter = ensDbWriter; this.publicConfigBuilder = publicConfigBuilder; this.indexingStatusBuilder = indexingStatusBuilder; } @@ -94,14 +90,14 @@ export class EnsDbWriterWorker { // Task 1: upsert ENSDb version into ENSDb. console.log(`[EnsDbWriterWorker]: Upserting ENSDb version into ENSDb...`); - await this.ensDbClient.upsertEnsDbVersion(inMemoryConfig.versionInfo.ensDb); + await this.ensDbWriter.upsertEnsDbVersion(inMemoryConfig.versionInfo.ensDb); console.log( `[EnsDbWriterWorker]: ENSDb version upserted successfully: ${inMemoryConfig.versionInfo.ensDb}`, ); // Task 2: upsert of EnsIndexerPublicConfig into ENSDb. console.log(`[EnsDbWriterWorker]: Upserting ENSIndexer Public Config into ENSDb...`); - await this.ensDbClient.upsertEnsIndexerPublicConfig(inMemoryConfig); + await this.ensDbWriter.upsertEnsIndexerPublicConfig(inMemoryConfig); console.log(`[EnsDbWriterWorker]: ENSIndexer Public Config upserted successfully`); // Task 3: recurring upsert of Indexing Status Snapshot into ENSDb. @@ -168,7 +164,7 @@ export class EnsDbWriterWorker { try { [storedConfig, inMemoryConfig] = await Promise.all([ - this.ensDbClient.getEnsIndexerPublicConfig(), + this.ensDbWriter.getEnsIndexerPublicConfig(), inMemoryConfigPromise, ]); } catch (error) { @@ -225,7 +221,7 @@ export class EnsDbWriterWorker { snapshotTime, ); - await this.ensDbClient.upsertIndexingStatusSnapshot(crossChainSnapshot); + await this.ensDbWriter.upsertIndexingStatusSnapshot(crossChainSnapshot); } catch (error) { console.error( `[EnsDbWriterWorker]: Error retrieving or validating Indexing Status Snapshot:`, diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts index 48ef90dffe..32d7968bda 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts @@ -1,4 +1,4 @@ -import { ensDbClient } from "@/lib/ensdb/singleton"; +import { ensDbWriter } from "@/lib/ensdb/singleton"; import { indexingStatusBuilder } from "@/lib/indexing-status-builder/singleton"; import { publicConfigBuilder } from "@/lib/public-config-builder/singleton"; @@ -21,7 +21,7 @@ export function startEnsDbWriterWorker() { } ensDbWriterWorker = new EnsDbWriterWorker( - ensDbClient, + ensDbWriter, publicConfigBuilder, indexingStatusBuilder, ); diff --git a/apps/ensindexer/src/lib/ensdb/migrate-ensnode-db.ts b/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts similarity index 55% rename from apps/ensindexer/src/lib/ensdb/migrate-ensnode-db.ts rename to apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts index c96d8056b0..1362c699af 100644 --- a/apps/ensindexer/src/lib/ensdb/migrate-ensnode-db.ts +++ b/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts @@ -1,7 +1,7 @@ import { createRequire } from "node:module"; import { join } from "node:path"; -import type { EnsNodeDbMigrations } from "@ensnode/ensdb-sdk"; +import { ensDbWriter } from "./singleton"; // Resolve the path to the migrations directory within the ENSDb SDK package const migrationsDirPath = join( @@ -10,11 +10,10 @@ const migrationsDirPath = join( ); /** - * Execute database migrations for ENSNode Schema in ENSDb, using the given ENSDb client. - * @param ensDbClient - The ENSDb client to use for executing migrations. + * Execute database migrations for ENSNode Schema in ENSDb. */ -export async function migrateEnsNodeDb(ensDbClient: EnsNodeDbMigrations): Promise { +export async function migrateEnsNodeSchema(): Promise { console.log(`Running database migrations for ENSNode Schema in ENSDb.`); - await ensDbClient.migrate(migrationsDirPath); + await ensDbWriter.migrateEnsNodeSchema(migrationsDirPath); console.log(`Database migrations for ENSNode Schema in ENSDb completed successfully.`); } diff --git a/apps/ensindexer/src/lib/ensdb/singleton.ts b/apps/ensindexer/src/lib/ensdb/singleton.ts index 1250e4f410..677998d6b1 100644 --- a/apps/ensindexer/src/lib/ensdb/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb/singleton.ts @@ -7,4 +7,4 @@ const { databaseUrl: ensDbUrl, databaseSchemaName: ensIndexerSchemaName } = conf /** * Singleton instance of ENSDbWriter for the ENSIndexer application. */ -export const ensDbClient = new EnsDbWriter(ensDbUrl, ensIndexerSchemaName); +export const ensDbWriter = new EnsDbWriter(ensDbUrl, ensIndexerSchemaName); diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.ts b/packages/ensdb-sdk/src/client/ensdb-reader.ts index 5b7e376008..b01df31df4 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.ts @@ -12,7 +12,6 @@ import { import * as ensIndexerSchema from "../ensindexer"; import * as ensNodeSchema from "../ensnode"; import { buildEnsDbDrizzleClient, type EnsDbDrizzle } from "../lib/drizzle"; -import type { EnsNodeDbQueries } from "./ensnode-db-queries"; import { EnsNodeMetadataKeys } from "./ensnode-metadata"; import type { SerializedEnsNodeMetadata, @@ -24,25 +23,25 @@ import type { /** * ENSDb Reader * - * Enables read-only querying of an ENSDb instance, including data spanning the ENSNode Schema and the specified ENSIndexer Schema. + * Enables read-only querying of an ENSDb instance, including data spanning the ENSNode Schema and the specified ENSIndexer Schema. */ -export class EnsDbReader implements EnsNodeDbQueries { +export class EnsDbReader { /** * Drizzle client for ENSDb. */ protected drizzleClient: EnsDbDrizzle; /** - * The name of the ENSIndexer schema to read from in ENSDb. - * - * This also identifies which ENSNode metadata records to read from the ENSNode Schema - * as the ENSNode Schema is multi-tenant across ENSIndexer instances / ENSIndexer Schemas in an ENSDb. + * The name of the ENSIndexer schema to read from in ENSDb. + * + * This also identifies which ENSNode metadata records to read from the ENSNode Schema + * as the ENSNode Schema is multi-tenant across ENSIndexer instances / ENSIndexer Schemas in an ENSDb. */ protected ensIndexerSchemaName: string; /** * @param ensDbConnectionString connection string for ENSDb Postgres database - * @param ensIndexerSchemaName the name of the ENSIndexer Schema to read from in ENSDb + * @param ensIndexerSchemaName the name of the ENSIndexer Schema to read from in ENSDb */ constructor(ensDbConnectionString: string, ensIndexerSchemaName: string) { EnsDbReader.bindEnsIndexerSchemaWithName(ensIndexerSchemaName); @@ -91,7 +90,9 @@ export class EnsDbReader implements EnsNodeDbQueries { } } /** - * @inheritdoc + * Get ENSDb Version + * + * @returns the existing record, or `undefined`. */ async getEnsDbVersion(): Promise { const record = await this.getEnsNodeMetadata({ @@ -102,7 +103,9 @@ export class EnsDbReader implements EnsNodeDbQueries { } /** - * @inheritdoc + * Get ENSIndexer Public Config + * + * @returns the existing record, or `undefined`. */ async getEnsIndexerPublicConfig(): Promise { const record = await this.getEnsNodeMetadata({ @@ -117,7 +120,9 @@ export class EnsDbReader implements EnsNodeDbQueries { } /** - * @inheritdoc + * Get Indexing Status Snapshot + * + * @returns the existing record, or `undefined`. */ async getIndexingStatusSnapshot(): Promise { const record = await this.getEnsNodeMetadata( diff --git a/packages/ensdb-sdk/src/client/ensdb-writer.test.ts b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts index e07dd035e5..1dc8bc99c1 100644 --- a/packages/ensdb-sdk/src/client/ensdb-writer.test.ts +++ b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts @@ -114,8 +114,8 @@ describe("EnsDbWriter", () => { }); }); - describe("migrate", () => { - it("calls drizzle-orm migrate with the correct parameters", async () => { + describe("migrateEnsNodeSchema", () => { + it("calls drizzle-orm migrateEnsNodeSchema with the correct parameters", async () => { // arrange const client = new EnsDbWriter( ensDbClientMock.ensDbUrl, @@ -124,7 +124,7 @@ describe("EnsDbWriter", () => { const migrationsDirPath = "/path/to/migrations"; // act - await client.migrate(migrationsDirPath); + await client.migrateEnsNodeSchema(migrationsDirPath); // assert expect(vi.mocked(migrate)).toHaveBeenCalledWith(dbMock, { @@ -133,7 +133,7 @@ describe("EnsDbWriter", () => { }); }); - it("propagates errors from the migrate function", async () => { + it("propagates errors from the migrateEnsNodeSchema function", async () => { // arrange const client = new EnsDbWriter( ensDbClientMock.ensDbUrl, @@ -144,7 +144,9 @@ describe("EnsDbWriter", () => { vi.mocked(migrate).mockRejectedValueOnce(error); // act & assert - await expect(client.migrate(migrationsDirPath)).rejects.toThrow("Migration failed"); + await expect(client.migrateEnsNodeSchema(migrationsDirPath)).rejects.toThrow( + "Migration failed", + ); }); }); }); diff --git a/packages/ensdb-sdk/src/client/ensdb-writer.ts b/packages/ensdb-sdk/src/client/ensdb-writer.ts index f275f0a269..4f9b19a465 100644 --- a/packages/ensdb-sdk/src/client/ensdb-writer.ts +++ b/packages/ensdb-sdk/src/client/ensdb-writer.ts @@ -9,8 +9,6 @@ import { import * as ensNodeSchema from "../ensnode"; import { EnsDbReader } from "./ensdb-reader"; -import type { EnsNodeDbMigrations } from "./ensnode-db-migrations"; -import type { EnsNodeDbMutations } from "./ensnode-db-mutations"; import { EnsNodeMetadataKeys } from "./ensnode-metadata"; import type { SerializedEnsNodeMetadata } from "./serialize/ensnode-metadata"; @@ -21,11 +19,15 @@ import type { SerializedEnsNodeMetadata } from "./serialize/ensnode-metadata"; * - executing database migrations for ENSNode Schema, * - updating ENSNode Metadata records in ENSDb for the given ENSIndexer instance. */ -export class EnsDbWriter extends EnsDbReader implements EnsNodeDbMutations, EnsNodeDbMigrations { +export class EnsDbWriter extends EnsDbReader { /** - * @inheritdoc + * Execute pending database migrations for ENSNode Schema in ENSDb. + * + * @param migrationsDirPath - The file path to the directory containing + * database migration files for ENSNode Schema. + * @throws error when migration execution fails. */ - async migrate(migrationsDirPath: string): Promise { + async migrateEnsNodeSchema(migrationsDirPath: string): Promise { return migrate(this.drizzleClient, { migrationsFolder: migrationsDirPath, migrationsSchema: "ensnode", @@ -33,7 +35,9 @@ export class EnsDbWriter extends EnsDbReader implements EnsNodeDbMutations, EnsN } /** - * @inheritdoc + * Upsert ENSDb Version + * + * @throws when upsert operation failed. */ async upsertEnsDbVersion(ensDbVersion: string): Promise { await this.upsertEnsNodeMetadata({ @@ -43,7 +47,9 @@ export class EnsDbWriter extends EnsDbReader implements EnsNodeDbMutations, EnsN } /** - * @inheritdoc + * Upsert ENSIndexer Public Config + * + * @throws when upsert operation failed. */ async upsertEnsIndexerPublicConfig( ensIndexerPublicConfig: EnsIndexerPublicConfig, @@ -55,7 +61,9 @@ export class EnsDbWriter extends EnsDbReader implements EnsNodeDbMutations, EnsN } /** - * @inheritdoc + * Upsert Indexing Status Snapshot + * + * @throws when upsert operation failed. */ async upsertIndexingStatusSnapshot( indexingStatus: CrossChainIndexingStatusSnapshot, diff --git a/packages/ensdb-sdk/src/client/ensnode-db-migrations.ts b/packages/ensdb-sdk/src/client/ensnode-db-migrations.ts deleted file mode 100644 index 0a8ef40209..0000000000 --- a/packages/ensdb-sdk/src/client/ensnode-db-migrations.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Migrations for ENSNode Schema in ENSDb - * - * Includes methods for migrating ENSNode Schema in ENSDb. - */ -export interface EnsNodeDbMigrations { - /** - * Execute pending database migrations for ENSNode Schema in ENSDb. - * - * @param migrationsDirPath - The file path to the directory containing - * database migration files for ENSNode Schema. - * @throws error when migration execution fails. - */ - migrate(migrationsDirPath: string): Promise; -} diff --git a/packages/ensdb-sdk/src/client/ensnode-db-mutations.ts b/packages/ensdb-sdk/src/client/ensnode-db-mutations.ts deleted file mode 100644 index 3fd4d01568..0000000000 --- a/packages/ensdb-sdk/src/client/ensnode-db-mutations.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { - CrossChainIndexingStatusSnapshot, - EnsIndexerPublicConfig, -} from "@ensnode/ensnode-sdk"; - -/** - * Mutations for ENSNode Schema in ENSDb - * - * Includes methods for writing into ENSNode Schema in ENSDb. - */ -export interface EnsNodeDbMutations { - /** - * Upsert ENSDb Version - * - * @throws when upsert operation failed. - */ - upsertEnsDbVersion(ensDbVersion: string): Promise; - - /** - * Upsert ENSIndexer Public Config - * - * @throws when upsert operation failed. - */ - upsertEnsIndexerPublicConfig(ensIndexerPublicConfig: EnsIndexerPublicConfig): Promise; - - /** - * Upsert Indexing Status Snapshot - * - * @throws when upsert operation failed. - */ - upsertIndexingStatusSnapshot(indexingStatus: CrossChainIndexingStatusSnapshot): Promise; -} diff --git a/packages/ensdb-sdk/src/client/ensnode-db-queries.ts b/packages/ensdb-sdk/src/client/ensnode-db-queries.ts deleted file mode 100644 index 2e47882920..0000000000 --- a/packages/ensdb-sdk/src/client/ensnode-db-queries.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { - CrossChainIndexingStatusSnapshot, - EnsIndexerPublicConfig, -} from "@ensnode/ensnode-sdk"; - -/** - * Queries for ENSNode Schema in ENSDb - * - * Includes methods for querying ENSNode Schema in ENSDb. - */ -export interface EnsNodeDbQueries { - /** - * Get ENSDb Version - * - * @returns the existing record, or `undefined`. - */ - getEnsDbVersion(): Promise; - - /** - * Get ENSIndexer Public Config - * - * @returns the existing record, or `undefined`. - */ - getEnsIndexerPublicConfig(): Promise; - - /** - * Get Indexing Status Snapshot - * - * @returns the existing record, or `undefined`. - */ - getIndexingStatusSnapshot(): Promise; -} diff --git a/packages/ensdb-sdk/src/client/index.ts b/packages/ensdb-sdk/src/client/index.ts index f10d25f9b6..f9b6c416af 100644 --- a/packages/ensdb-sdk/src/client/index.ts +++ b/packages/ensdb-sdk/src/client/index.ts @@ -1,6 +1,3 @@ export * from "./ensdb-reader"; export * from "./ensdb-writer"; -export * from "./ensnode-db-migrations"; -export * from "./ensnode-db-mutations"; -export * from "./ensnode-db-queries"; export * from "./ensnode-metadata"; From f0c7f1fb398b40e01696a11d4b61ee5b786c8215 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Mar 2026 12:20:30 +0100 Subject: [PATCH 19/34] Improve layers of responsibility in ENSDb Drizzle Client file --- packages/ensdb-sdk/src/index.ts | 1 + packages/ensdb-sdk/src/lib/drizzle.ts | 113 ++++++++++++++++++++++++-- 2 files changed, 105 insertions(+), 9 deletions(-) diff --git a/packages/ensdb-sdk/src/index.ts b/packages/ensdb-sdk/src/index.ts index 4a601090c2..f9e0ac65f8 100644 --- a/packages/ensdb-sdk/src/index.ts +++ b/packages/ensdb-sdk/src/index.ts @@ -1,2 +1,3 @@ export * from "./client"; export * from "./ensindexer"; +export * from "./lib/drizzle"; diff --git a/packages/ensdb-sdk/src/lib/drizzle.ts b/packages/ensdb-sdk/src/lib/drizzle.ts index 0e6976cdea..531088b341 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.ts @@ -3,35 +3,130 @@ */ import type { Logger as DrizzleLogger } from "drizzle-orm/logger"; import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres"; +import { isPgEnum } from "drizzle-orm/pg-core"; +import { isTable, Table } from "drizzle-orm/table"; -import * as ensIndexerSchema from "../ensindexer"; +// Import the "abstract" ENSIndexer Schema. +// It's called "abstract" here because tables defined in this schema do not +// reference the specific ENSIndexer Schema name, and therefore cannot be used +// directly to build a Drizzle client for ENSDb. +import * as abstractEnsIndexerSchema from "../ensindexer"; import * as ensNodeSchema from "../ensnode"; /** - * ENSDb Schema + * Abstract ENSIndexer Schema + * + * Represents the "abstract" ENSIndexer Schema definition, where tables do not reference + * the specific ENSIndexer Schema name. + */ +export type AbstractEnsIndexerSchema = typeof abstractEnsIndexerSchema; + +/** + * Build a "concrete" ENSIndexer Schema definition for ENSDb. + * + * This function uses the "abstract" ENSIndexer Schema definition + * to create a "concrete" ENSIndexer Schema definition referencing the provided + * ENSIndexer Schema name. The "concrete" ENSIndexer Schema definition can then + * be used to build the ENSDb Schema for a Drizzle client for ENSDb. + * + * @param ensIndexerSchemaName - The name of the ENSIndexer Schema instance in ENSDb. + * + * Note: this function is a replacement for `setDatabaseSchema` from `@ponder/client`. + */ +function buildEnsIndexerSchema( + ensIndexerSchemaName: string, +): EnsIndexerSchemaType { + const ensIndexerSchema = {} as EnsIndexerSchemaType; + + for (const [key, abstractSchemaObject] of Object.entries(abstractEnsIndexerSchema)) { + if (isTable(abstractSchemaObject)) { + const concreteSchemaObject = { ...abstractSchemaObject }; + // @ts-expect-error - Drizzle's Table type for the schema symbol is + // not typed in a way that allows us to set it directly, + // but we know it exists and can be set. + concreteSchemaObject[Table.Symbol.Schema] = ensIndexerSchemaName; + (ensIndexerSchema as any)[key] = concreteSchemaObject; + } else if (isPgEnum(abstractSchemaObject)) { + // Enums are functions; clone by copying properties onto a new function. + const concreteSchemaObject = Object.assign( + (...args: any[]) => abstractSchemaObject(...args), + abstractSchemaObject, + ); + // @ts-expect-error - Drizzle's PgEnum type for the schema symbol is + // typed as readonly, but we need to set it here so + // the output schema definition has the correct schema for + // all table and enum objects. + concreteSchemaObject.schema = ensIndexerSchemaName; + (ensIndexerSchema as any)[key] = concreteSchemaObject; + } else { + (ensIndexerSchema as any)[key] = abstractSchemaObject; + } + } + + return ensIndexerSchema; +} + +/** + * ENSNode Schema + * + * Represents the ENSNode Schema definition for ENSDb. + */ +type EnsNodeSchema = typeof ensNodeSchema; + +/** + * ENSDb Schema type * * Represents the combined database schema for ENSDb, - * including both the ENSIndexer Schema and the ENSNode Schema. + * including both the "concrete" ENSIndexer Schema and the ENSNode Schema. + */ +export type EnsDbSchema = + EnsIndexerSchemaType & EnsNodeSchema; + +/** + * Build ENSDb Schema for Drizzle client + * + * Uses the provided ENSIndexer Schema name to build + * the "concrete" ENSIndexer Schema definition within the ENSDb Schema. + * + * @param ensIndexerSchemaName - The name of the ENSIndexer Schema instance + * in ENSDb. + * @returns The ENSDb Schema definition for use in building + * a Drizzle client for ENSDb. */ -export const ensDbSchema = { ...ensIndexerSchema, ...ensNodeSchema }; +export function buildEnsDbSchema( + ensIndexerSchemaName: string, +): EnsDbSchema { + const ensIndexerSchema = buildEnsIndexerSchema(ensIndexerSchemaName); -export type EnsDbSchema = typeof ensDbSchema; + return { + ...ensIndexerSchema, + ...ensNodeSchema, + }; +} /** - * Drizzle client for ENSDb. + * Drizzle client type for ENSDb. + * + * The `EnsIndexerSchemaType` type parameter allows for typing + * the Drizzle client with a "concrete" ENSIndexer Schema definition + * where tables reference the specific ENSIndexer Schema name. */ -export type EnsDbDrizzle = NodePgDatabase; +export type EnsDbDrizzleClient = + NodePgDatabase>; /** * Build a Drizzle client for ENSDb. + * * @param connectionString - The connection string for the ENSDb. + * @param ensDbSchema - The ENSDb Schema definition for the Drizzle client. * @param logger - Optional Drizzle logger for query logging. * @returns A Drizzle client for ENSDb. */ -export function buildEnsDbDrizzleClient( +export function buildEnsDbDrizzleClient( connectionString: string, + ensDbSchema: EnsDbSchema, logger?: DrizzleLogger, -): EnsDbDrizzle { +): EnsDbDrizzleClient { return drizzle({ connection: connectionString, schema: ensDbSchema, From 54301fcb9152d108f9f553be823c5541b652dc62 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Mar 2026 12:48:04 +0100 Subject: [PATCH 20/34] Improve layers of responsibility in ENSDb SDK clients Make `EnsDbReader` and `EnsDbWriter` to take input for ENSDb Drizzle Client and the ENSDb schema it uses. --- apps/ensindexer/src/lib/ensdb/singleton.ts | 12 ++- packages/ensdb-sdk/src/client/ensdb-reader.ts | 86 +++++++++---------- packages/ensdb-sdk/src/client/ensdb-writer.ts | 5 +- 3 files changed, 53 insertions(+), 50 deletions(-) diff --git a/apps/ensindexer/src/lib/ensdb/singleton.ts b/apps/ensindexer/src/lib/ensdb/singleton.ts index 677998d6b1..eeb8d3b648 100644 --- a/apps/ensindexer/src/lib/ensdb/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb/singleton.ts @@ -1,10 +1,16 @@ import config from "@/config"; -import { EnsDbWriter } from "@ensnode/ensdb-sdk"; +import { buildEnsDbDrizzleClient, buildEnsDbSchema, EnsDbWriter } from "@ensnode/ensdb-sdk"; -const { databaseUrl: ensDbUrl, databaseSchemaName: ensIndexerSchemaName } = config; +const { databaseUrl: ensDbConnectionString, databaseSchemaName: ensIndexerSchemaName } = config; + +/** + * Build a ENSDb Schema for Drizzle client using the ENSIndexer Schema name from config. + */ +const ensDbSchema = buildEnsDbSchema(ensIndexerSchemaName); +const ensDbDrizzleClient = buildEnsDbDrizzleClient(ensDbConnectionString, ensDbSchema); /** * Singleton instance of ENSDbWriter for the ENSIndexer application. */ -export const ensDbWriter = new EnsDbWriter(ensDbUrl, ensIndexerSchemaName); +export const ensDbWriter = new EnsDbWriter(ensDbDrizzleClient, ensDbSchema, ensIndexerSchemaName); diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.ts b/packages/ensdb-sdk/src/client/ensdb-reader.ts index b01df31df4..964faacc49 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.ts @@ -1,5 +1,3 @@ -import { isTable, Table } from "drizzle-orm"; -import { isPgEnum } from "drizzle-orm/pg-core"; import { and, eq } from "drizzle-orm/sql"; import { @@ -9,9 +7,7 @@ import { type EnsIndexerPublicConfig, } from "@ensnode/ensnode-sdk"; -import * as ensIndexerSchema from "../ensindexer"; -import * as ensNodeSchema from "../ensnode"; -import { buildEnsDbDrizzleClient, type EnsDbDrizzle } from "../lib/drizzle"; +import type { AbstractEnsIndexerSchema, EnsDbDrizzleClient, EnsDbSchema } from "../lib/drizzle"; import { EnsNodeMetadataKeys } from "./ensnode-metadata"; import type { SerializedEnsNodeMetadata, @@ -23,13 +19,31 @@ import type { /** * ENSDb Reader * - * Enables read-only querying of an ENSDb instance, including data spanning the ENSNode Schema and the specified ENSIndexer Schema. + * Enables read-only querying of an ENSDb instance, including data spanning + * the ENSNode Schema and the specified ENSIndexer Schema. + * + * Note: we use a parameter type `EnsIndexerSchemaType` to represent + * the "concrete" ENSIndexer Schema type within the ENSDb Schema and + * make sure that the Drizzle client used for querying is typed with + * the same "concrete" ENSIndexer Schema type. */ -export class EnsDbReader { +export class EnsDbReader< + EnsIndexerSchemaType extends AbstractEnsIndexerSchema = AbstractEnsIndexerSchema, +> { /** * Drizzle client for ENSDb. + * + * Uses the ENSDb Schema from {@link ensDbSchema}. + */ + protected drizzleClient: EnsDbDrizzleClient; + + /** + * ENSDb Schema definition for ENSDb. + * + * This is the "concrete" ENSDb Schema in which tables reference + * the ENSIndexer Schema name from {@link ensIndexerSchemaName}. */ - protected drizzleClient: EnsDbDrizzle; + protected ensDbSchema: EnsDbSchema; /** * The name of the ENSIndexer schema to read from in ENSDb. @@ -40,12 +54,17 @@ export class EnsDbReader { protected ensIndexerSchemaName: string; /** - * @param ensDbConnectionString connection string for ENSDb Postgres database - * @param ensIndexerSchemaName the name of the ENSIndexer Schema to read from in ENSDb + * @param ensDbDrizzleClient Drizzle client for ENSDb, typed with the "concrete" ENSIndexer Schema type. + * @param ensDbSchema ENSDb Schema definition for ENSDb used by the Drizzle client. + * @param ensIndexerSchemaName The name of the ENSIndexer schema to read from in ENSDb, used to identify which ENSNode metadata records to read. */ - constructor(ensDbConnectionString: string, ensIndexerSchemaName: string) { - EnsDbReader.bindEnsIndexerSchemaWithName(ensIndexerSchemaName); - this.drizzleClient = buildEnsDbDrizzleClient(ensDbConnectionString); + constructor( + ensDbDrizzleClient: EnsDbDrizzleClient, + ensDbSchema: EnsDbSchema, + ensIndexerSchemaName: string, + ) { + this.drizzleClient = ensDbDrizzleClient; + this.ensDbSchema = ensDbSchema; this.ensIndexerSchemaName = ensIndexerSchemaName; } @@ -54,41 +73,20 @@ export class EnsDbReader { * * Useful while working on complex queries for ENSDb. */ - get client(): EnsDbDrizzle { + get client(): EnsDbDrizzleClient { return this.drizzleClient; } /** - * Bind an ENSIndexer Schema definition with a specific instance of - * ENSIndexer Schema in ENSDb. + * Getter for the ENSDb Schema definition used in the Drizzle client + * for ENSDb instance. * - * ENSIndexer Schema definition does not have a fixed database schema name, - * as it is determined by Ponder when starting up the ENSIndexer instance. - * - * This function allows to bind the ENSIndexer Schema definition with - * the correct database schema name for the ENSIndexer instance we want to - * reference when interacting with ENSDb. - * - * @param ensIndexerSchemaName - The name of the ENSIndexer Schema instance in ENSDb. - * - * Note: this function is a replacement for `setDatabaseSchema` from `@ponder/client`. + * Useful while working on complex queries for ENSDb. */ - static bindEnsIndexerSchemaWithName(ensIndexerSchemaName: string): void { - for (const dbObjectDef of Object.values(ensIndexerSchema)) { - if (isTable(dbObjectDef)) { - // @ts-expect-error - Drizzle's Table type for the schema symbol is - // not typed in a way that allows us to set it directly, - // but we know it exists and can be set. - dbObjectDef[Table.Symbol.Schema] = ensIndexerSchemaName; - } else if (isPgEnum(dbObjectDef)) { - // @ts-expect-error - Drizzle's PgEnum type for the schema symbol is - // typed as readonly, but we need to set it here so - // the output schema definition has the correct schema for - // all table and enum objects. - dbObjectDef.schema = ensIndexerSchemaName; - } - } + get schema(): EnsDbSchema { + return this.ensDbSchema; } + /** * Get ENSDb Version * @@ -151,11 +149,11 @@ export class EnsDbReader { ): Promise { const result = await this.drizzleClient .select() - .from(ensNodeSchema.metadata) + .from(this.ensDbSchema.metadata) .where( and( - eq(ensNodeSchema.metadata.ensIndexerSchemaName, this.ensIndexerSchemaName), - eq(ensNodeSchema.metadata.key, metadata.key), + eq(this.ensDbSchema.metadata.ensIndexerSchemaName, this.ensIndexerSchemaName), + eq(this.ensDbSchema.metadata.key, metadata.key), ), ); diff --git a/packages/ensdb-sdk/src/client/ensdb-writer.ts b/packages/ensdb-sdk/src/client/ensdb-writer.ts index 4f9b19a465..4e11ea2321 100644 --- a/packages/ensdb-sdk/src/client/ensdb-writer.ts +++ b/packages/ensdb-sdk/src/client/ensdb-writer.ts @@ -7,7 +7,6 @@ import { serializeEnsIndexerPublicConfig, } from "@ensnode/ensnode-sdk"; -import * as ensNodeSchema from "../ensnode"; import { EnsDbReader } from "./ensdb-reader"; import { EnsNodeMetadataKeys } from "./ensnode-metadata"; import type { SerializedEnsNodeMetadata } from "./serialize/ensnode-metadata"; @@ -81,14 +80,14 @@ export class EnsDbWriter extends EnsDbReader { */ private async upsertEnsNodeMetadata(metadata: SerializedEnsNodeMetadata): Promise { await this.drizzleClient - .insert(ensNodeSchema.metadata) + .insert(this.ensDbSchema.metadata) .values({ ensIndexerSchemaName: this.ensIndexerSchemaName, key: metadata.key, value: metadata.value, }) .onConflictDoUpdate({ - target: [ensNodeSchema.metadata.ensIndexerSchemaName, ensNodeSchema.metadata.key], + target: [this.ensDbSchema.metadata.ensIndexerSchemaName, this.ensDbSchema.metadata.key], set: { value: metadata.value }, }); } From a3dbb6b7102dfc6713cb1e43a22aa080dc761636 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Mar 2026 13:25:41 +0100 Subject: [PATCH 21/34] Update testing suite for ENSDb SDK --- .../ensdb-sdk/src/client/ensdb-reader.test.ts | 71 ++------- .../ensdb-sdk/src/client/ensdb-writer.test.ts | 77 ++-------- packages/ensdb-sdk/src/lib/drizzle.test.ts | 142 ++++++++++++++++++ 3 files changed, 170 insertions(+), 120 deletions(-) create mode 100644 packages/ensdb-sdk/src/lib/drizzle.test.ts diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts index 84aaf7be4d..610129533c 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts @@ -6,99 +6,60 @@ import { } from "@ensnode/ensnode-sdk"; import * as ensNodeSchema from "../ensnode"; -import { buildEnsDbDrizzleClient } from "../lib/drizzle"; import * as ensDbClientMock from "./ensdb-client.mock"; import { EnsDbReader } from "./ensdb-reader"; -// Mock the buildEnsDbDrizzleClient function to return a mock database instance -vi.mock("../lib/drizzle", () => ({ buildEnsDbDrizzleClient: vi.fn() })); - describe("EnsDbReader", () => { - // Mock database query results and methods const selectResult = { current: [] as Array<{ value: unknown }> }; const whereMock = vi.fn(async () => selectResult.current); const fromMock = vi.fn(() => ({ where: whereMock })); const selectMock = vi.fn(() => ({ from: fromMock })); - const dbMock = { select: selectMock }; + const drizzleClientMock = { select: selectMock } as any; + const schemaMock = ensNodeSchema as any; + + const createEnsDbReader = () => + new EnsDbReader(drizzleClientMock, schemaMock, ensDbClientMock.ensIndexerSchemaName); beforeEach(() => { selectResult.current = []; whereMock.mockClear(); fromMock.mockClear(); selectMock.mockClear(); - vi.mocked(buildEnsDbDrizzleClient).mockReturnValue( - dbMock as unknown as ReturnType, - ); }); describe("getEnsDbVersion", () => { it("returns undefined when no record exists", async () => { - // arrange - const client = new EnsDbReader( - ensDbClientMock.ensDbUrl, - ensDbClientMock.ensIndexerSchemaName, - ); - - // act & assert - await expect(client.getEnsDbVersion()).resolves.toBeUndefined(); + await expect(createEnsDbReader().getEnsDbVersion()).resolves.toBeUndefined(); expect(selectMock).toHaveBeenCalledTimes(1); - expect(fromMock).toHaveBeenCalledWith(ensNodeSchema.metadata); + expect(fromMock).toHaveBeenCalledWith(schemaMock.metadata); }); it("returns value when one record exists", async () => { - // arrange selectResult.current = [{ value: "0.1.0" }]; - const client = new EnsDbReader( - ensDbClientMock.ensDbUrl, - ensDbClientMock.ensIndexerSchemaName, - ); - - // act & assert - await expect(client.getEnsDbVersion()).resolves.toBe("0.1.0"); + await expect(createEnsDbReader().getEnsDbVersion()).resolves.toBe("0.1.0"); }); // This scenario should be impossible due to the primary key constraint on // the 'key' column of 'ensnode_metadata' table. it("throws when multiple records exist", async () => { - // arrange selectResult.current = [{ value: "0.1.0" }, { value: "0.1.1" }]; - const client = new EnsDbReader( - ensDbClientMock.ensDbUrl, - ensDbClientMock.ensIndexerSchemaName, - ); - - // act & assert - await expect(client.getEnsDbVersion()).rejects.toThrowError(/ensdb_version/i); + await expect(createEnsDbReader().getEnsDbVersion()).rejects.toThrowError(/ensdb_version/i); }); }); describe("getEnsIndexerPublicConfig", () => { it("returns undefined when no record exists", async () => { - // arrange - const client = new EnsDbReader( - ensDbClientMock.ensDbUrl, - ensDbClientMock.ensIndexerSchemaName, - ); - - // act & assert - await expect(client.getEnsIndexerPublicConfig()).resolves.toBeUndefined(); + await expect(createEnsDbReader().getEnsIndexerPublicConfig()).resolves.toBeUndefined(); }); it("deserializes the stored config", async () => { - // arrange const serializedConfig = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig); selectResult.current = [{ value: serializedConfig }]; - const client = new EnsDbReader( - ensDbClientMock.ensDbUrl, - ensDbClientMock.ensIndexerSchemaName, - ); - - // act & assert - await expect(client.getEnsIndexerPublicConfig()).resolves.toStrictEqual( + await expect(createEnsDbReader().getEnsIndexerPublicConfig()).resolves.toStrictEqual( ensDbClientMock.publicConfig, ); }); @@ -106,19 +67,15 @@ describe("EnsDbReader", () => { describe("getIndexingStatusSnapshot", () => { it("deserializes the stored indexing status snapshot", async () => { - // arrange selectResult.current = [{ value: ensDbClientMock.serializedSnapshot }]; - const client = new EnsDbReader( - ensDbClientMock.ensDbUrl, - ensDbClientMock.ensIndexerSchemaName, - ); const expected = deserializeCrossChainIndexingStatusSnapshot( ensDbClientMock.serializedSnapshot, ); - // act & assert - await expect(client.getIndexingStatusSnapshot()).resolves.toStrictEqual(expected); + await expect(createEnsDbReader().getIndexingStatusSnapshot()).resolves.toStrictEqual( + expected, + ); }); }); }); diff --git a/packages/ensdb-sdk/src/client/ensdb-writer.test.ts b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts index 1dc8bc99c1..56452b6814 100644 --- a/packages/ensdb-sdk/src/client/ensdb-writer.test.ts +++ b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts @@ -8,62 +8,41 @@ import { } from "@ensnode/ensnode-sdk"; import * as ensNodeSchema from "../ensnode"; -import { buildEnsDbDrizzleClient } from "../lib/drizzle"; import * as ensDbClientMock from "./ensdb-client.mock"; import { EnsDbWriter } from "./ensdb-writer"; import { EnsNodeMetadataKeys } from "./ensnode-metadata"; -// Mock the buildEnsDbDrizzleClient function to return a mock database instance -vi.mock("../lib/drizzle", () => ({ buildEnsDbDrizzleClient: vi.fn() })); - -// Mock the drizzle-orm migrator vi.mock("drizzle-orm/node-postgres/migrator", () => ({ migrate: vi.fn() })); describe("EnsDbWriter", () => { - // Mock database query results and methods - const selectResult = { current: [] as Array<{ value: unknown }> }; - const whereMock = vi.fn(async () => selectResult.current); - const fromMock = vi.fn(() => ({ where: whereMock })); - const selectMock = vi.fn(() => ({ from: fromMock })); const onConflictDoUpdateMock = vi.fn(async () => undefined); const valuesMock = vi.fn(() => ({ onConflictDoUpdate: onConflictDoUpdateMock })); const insertMock = vi.fn(() => ({ values: valuesMock })); - const dbMock = { select: selectMock, insert: insertMock }; + const drizzleClientMock = { insert: insertMock } as any; + const schemaMock = ensNodeSchema as any; + + const createEnsDbWriter = () => + new EnsDbWriter(drizzleClientMock, schemaMock, ensDbClientMock.ensIndexerSchemaName); beforeEach(() => { - selectResult.current = []; - whereMock.mockClear(); - fromMock.mockClear(); - selectMock.mockClear(); onConflictDoUpdateMock.mockClear(); valuesMock.mockClear(); insertMock.mockClear(); vi.mocked(migrate).mockClear(); - vi.mocked(buildEnsDbDrizzleClient).mockReturnValue( - dbMock as unknown as ReturnType, - ); }); describe("upsertEnsDbVersion", () => { it("writes the database version metadata", async () => { - // arrange - const client = new EnsDbWriter( - ensDbClientMock.ensDbUrl, - ensDbClientMock.ensIndexerSchemaName, - ); - - // act - await client.upsertEnsDbVersion("0.2.0"); + await createEnsDbWriter().upsertEnsDbVersion("0.2.0"); - // assert - expect(insertMock).toHaveBeenCalledWith(ensNodeSchema.metadata); + expect(insertMock).toHaveBeenCalledWith(schemaMock.metadata); expect(valuesMock).toHaveBeenCalledWith({ ensIndexerSchemaName: ensDbClientMock.ensIndexerSchemaName, key: EnsNodeMetadataKeys.EnsDbVersion, value: "0.2.0", }); expect(onConflictDoUpdateMock).toHaveBeenCalledWith({ - target: [ensNodeSchema.metadata.ensIndexerSchemaName, ensNodeSchema.metadata.key], + target: [schemaMock.metadata.ensIndexerSchemaName, schemaMock.metadata.key], set: { value: "0.2.0" }, }); }); @@ -71,17 +50,10 @@ describe("EnsDbWriter", () => { describe("upsertEnsIndexerPublicConfig", () => { it("serializes and writes the public config", async () => { - // arrange - const client = new EnsDbWriter( - ensDbClientMock.ensDbUrl, - ensDbClientMock.ensIndexerSchemaName, - ); const expectedValue = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig); - // act - await client.upsertEnsIndexerPublicConfig(ensDbClientMock.publicConfig); + await createEnsDbWriter().upsertEnsIndexerPublicConfig(ensDbClientMock.publicConfig); - // assert expect(valuesMock).toHaveBeenCalledWith({ ensIndexerSchemaName: ensDbClientMock.ensIndexerSchemaName, key: EnsNodeMetadataKeys.EnsIndexerPublicConfig, @@ -92,20 +64,13 @@ describe("EnsDbWriter", () => { describe("upsertIndexingStatusSnapshot", () => { it("serializes and writes the indexing status snapshot", async () => { - // arrange - const client = new EnsDbWriter( - ensDbClientMock.ensDbUrl, - ensDbClientMock.ensIndexerSchemaName, - ); const snapshot = deserializeCrossChainIndexingStatusSnapshot( ensDbClientMock.serializedSnapshot, ); const expectedValue = serializeCrossChainIndexingStatusSnapshot(snapshot); - // act - await client.upsertIndexingStatusSnapshot(snapshot); + await createEnsDbWriter().upsertIndexingStatusSnapshot(snapshot); - // assert expect(valuesMock).toHaveBeenCalledWith({ ensIndexerSchemaName: ensDbClientMock.ensIndexerSchemaName, key: EnsNodeMetadataKeys.EnsIndexerIndexingStatus, @@ -116,35 +81,21 @@ describe("EnsDbWriter", () => { describe("migrateEnsNodeSchema", () => { it("calls drizzle-orm migrateEnsNodeSchema with the correct parameters", async () => { - // arrange - const client = new EnsDbWriter( - ensDbClientMock.ensDbUrl, - ensDbClientMock.ensIndexerSchemaName, - ); const migrationsDirPath = "/path/to/migrations"; - // act - await client.migrateEnsNodeSchema(migrationsDirPath); + await createEnsDbWriter().migrateEnsNodeSchema(migrationsDirPath); - // assert - expect(vi.mocked(migrate)).toHaveBeenCalledWith(dbMock, { + expect(vi.mocked(migrate)).toHaveBeenCalledWith(drizzleClientMock, { migrationsFolder: migrationsDirPath, migrationsSchema: "ensnode", }); }); it("propagates errors from the migrateEnsNodeSchema function", async () => { - // arrange - const client = new EnsDbWriter( - ensDbClientMock.ensDbUrl, - ensDbClientMock.ensIndexerSchemaName, - ); const migrationsDirPath = "/path/to/migrations"; - const error = new Error("Migration failed"); - vi.mocked(migrate).mockRejectedValueOnce(error); + vi.mocked(migrate).mockRejectedValueOnce(new Error("Migration failed")); - // act & assert - await expect(client.migrateEnsNodeSchema(migrationsDirPath)).rejects.toThrow( + await expect(createEnsDbWriter().migrateEnsNodeSchema(migrationsDirPath)).rejects.toThrow( "Migration failed", ); }); diff --git a/packages/ensdb-sdk/src/lib/drizzle.test.ts b/packages/ensdb-sdk/src/lib/drizzle.test.ts new file mode 100644 index 0000000000..a2f34a7cd6 --- /dev/null +++ b/packages/ensdb-sdk/src/lib/drizzle.test.ts @@ -0,0 +1,142 @@ +import { isPgEnum } from "drizzle-orm/pg-core"; +import { isTable } from "drizzle-orm/table"; +import { describe, expect, it, vi } from "vitest"; + +import * as abstractEnsIndexerSchema from "../ensindexer"; +import { buildEnsDbDrizzleClient, buildEnsDbSchema } from "./drizzle"; + +vi.mock("drizzle-orm/node-postgres", () => ({ + drizzle: vi.fn(() => "mock-drizzle-client"), +})); + +// Re-import after mock to get the mocked version +const { drizzle } = await import("drizzle-orm/node-postgres"); + +const SCHEMA_NAME = "ensindexer_test"; + +const DrizzleSchemaSymbol = Symbol.for("drizzle:Schema"); + +function getSchemaName(obj: unknown): string | undefined { + return (obj as any)[DrizzleSchemaSymbol]; +} + +describe("buildEnsDbSchema", () => { + it("returns an object containing all ENSNode schema exports", () => { + const schema = buildEnsDbSchema(SCHEMA_NAME); + + expect(schema.metadata).toBeDefined(); + }); + + it("returns an object containing all ENSIndexer schema exports", () => { + const schema = buildEnsDbSchema(SCHEMA_NAME); + + expect(schema.event).toBeDefined(); + expect(schema.v1Domain).toBeDefined(); + expect(schema.registration).toBeDefined(); + expect(schema.registrationType).toBeDefined(); + }); + + it("sets the schema name on all ENSIndexer tables", () => { + const schema = buildEnsDbSchema(SCHEMA_NAME); + + for (const [key] of Object.entries(abstractEnsIndexerSchema)) { + const value = schema[key as keyof typeof schema]; + if (isTable(value)) { + expect(getSchemaName(value)).toBe(SCHEMA_NAME); + } + } + }); + + it("does not mutate the schema name on ENSNode tables", () => { + const schema = buildEnsDbSchema(SCHEMA_NAME); + + expect(getSchemaName(schema.metadata)).toBe("ensnode"); + }); + + it("sets the schema name on all ENSIndexer enums", () => { + const schema = buildEnsDbSchema(SCHEMA_NAME); + + for (const [key] of Object.entries(abstractEnsIndexerSchema)) { + const value = schema[key as keyof typeof schema]; + if (isPgEnum(value)) { + expect((value as any).schema).toBe(SCHEMA_NAME); + } + } + }); + + it("skips relation objects (neither tables nor enums)", () => { + const schema = buildEnsDbSchema(SCHEMA_NAME); + + for (const [key, value] of Object.entries(schema)) { + if (key.endsWith("_relations") || key.endsWith("Relations")) { + expect(isTable(value)).toBe(false); + expect(isPgEnum(value)).toBe(false); + } + } + }); + + it("applies a different schema name to ENSIndexer objects", () => { + const otherSchemaName = "ensindexer_other"; + const schema = buildEnsDbSchema(otherSchemaName); + + for (const [key] of Object.entries(abstractEnsIndexerSchema)) { + const value = schema[key as keyof typeof schema]; + if (isTable(value)) { + expect(getSchemaName(value)).toBe(otherSchemaName); + } + } + }); + + it("builds two concrete schemas with respective names, leaving abstract unaffected", () => { + const schemaNameA = "ensindexer_alpha"; + const schemaNameB = "ensindexer_beta"; + + const concreteA = buildEnsDbSchema(schemaNameA); + const concreteB = buildEnsDbSchema(schemaNameB); + + for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { + const valueA = concreteA[key as keyof typeof concreteA]; + const valueB = concreteB[key as keyof typeof concreteB]; + + if (isTable(abstractValue)) { + expect(getSchemaName(valueA)).toBe(schemaNameA); + expect(getSchemaName(valueB)).toBe(schemaNameB); + expect(getSchemaName(abstractValue)).toBeUndefined(); + } + if (isPgEnum(abstractValue)) { + expect((valueA as any).schema).toBe(schemaNameA); + expect((valueB as any).schema).toBe(schemaNameB); + expect((abstractValue as any).schema).toBeUndefined(); + } + } + + expect(getSchemaName(concreteA.metadata)).toBe("ensnode"); + expect(getSchemaName(concreteB.metadata)).toBe("ensnode"); + }); +}); + +describe("buildEnsDbDrizzleClient", () => { + it("calls drizzle with the correct connection config", () => { + const connectionString = "postgres://user:pass@localhost:5432/ensdb"; + const schema = buildEnsDbSchema(SCHEMA_NAME); + + buildEnsDbDrizzleClient(connectionString, schema); + + expect(drizzle).toHaveBeenCalledWith({ + connection: connectionString, + schema, + casing: "snake_case", + logger: undefined, + }); + }); + + it("passes the logger to drizzle when provided", () => { + const connectionString = "postgres://user:pass@localhost:5432/ensdb"; + const schema = buildEnsDbSchema(SCHEMA_NAME); + const logger = { logQuery: vi.fn() }; + + buildEnsDbDrizzleClient(connectionString, schema, logger); + + expect(drizzle).toHaveBeenCalledWith(expect.objectContaining({ logger })); + }); +}); From 9eeb9354f5fc7100463aec360bab90fa7d10e1ab Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Mar 2026 13:44:26 +0100 Subject: [PATCH 22/34] Update docs in `ponder.schema.ts` file --- apps/ensindexer/ponder/ponder.schema.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/ensindexer/ponder/ponder.schema.ts b/apps/ensindexer/ponder/ponder.schema.ts index 62fe7bf34b..5baf7a0cf0 100644 --- a/apps/ensindexer/ponder/ponder.schema.ts +++ b/apps/ensindexer/ponder/ponder.schema.ts @@ -1,2 +1,10 @@ -// export database schema definition for ENSIndexer +/** + * Export database schema definition for ENSIndexer + * Note: Ponder uses `globalThis.PONDER_NAMESPACE_BUILD.schema` value to + * dynamically build the "concrete" ENSIndexer Schema definition + * from the "abstract" ENSIndexer Schema definition for Ponder app to use. + * + * @see https://github.com/ponder-sh/ponder/blob/c8f6935fb65176c01b40cae9056be704c0e5318e/packages/core/src/build/index.ts#L380-L424 + * @see https://github.com/ponder-sh/ponder/blob/6fcc15d4234e43862cb6e21c05f3c57f4c2f7464/packages/core/src/drizzle/onchain.ts#L280-L281 + **/ export * from "@ensnode/ensdb-sdk/ensindexer"; From 0534ef3b6bf7390ca6ba0b23bd9ad7a34611fad8 Mon Sep 17 00:00:00 2001 From: Tomek Kopacki Date: Fri, 20 Mar 2026 16:37:16 +0100 Subject: [PATCH 23/34] Update packages/ensdb-sdk/src/client/ensdb-reader.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/ensdb-sdk/src/client/ensdb-reader.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts index 610129533c..da432d019e 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts @@ -42,7 +42,7 @@ describe("EnsDbReader", () => { }); // This scenario should be impossible due to the primary key constraint on - // the 'key' column of 'ensnode_metadata' table. + // the ('ensIndexerSchemaName', 'key') columns of the 'ensnode_metadata' table. it("throws when multiple records exist", async () => { selectResult.current = [{ value: "0.1.0" }, { value: "0.1.1" }]; From 132eca85d526f8987894d5f7fcefcf46768c93f8 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Mar 2026 19:13:57 +0100 Subject: [PATCH 24/34] Rename `ensindexer` file to `ensindexer-abstract` in ENSDb SDK --- apps/ensindexer/ponder/ponder.schema.ts | 12 ++++-------- packages/ensdb-sdk/package.json | 12 ++++++------ .../ensnode-metadata.schema.ts | 0 .../ensv2.schema.ts | 0 .../src/{ensindexer => ensindexer-abstract}/index.ts | 4 +++- .../protocol-acceleration.schema.ts | 0 .../registrars.schema.ts | 0 .../subgraph.schema.ts | 0 .../tokenscope.schema.ts | 0 packages/ensdb-sdk/src/index.ts | 2 +- packages/ensdb-sdk/tsup.config.ts | 2 +- pnpm-lock.yaml | 2 +- 12 files changed, 16 insertions(+), 18 deletions(-) rename packages/ensdb-sdk/src/{ensindexer => ensindexer-abstract}/ensnode-metadata.schema.ts (100%) rename packages/ensdb-sdk/src/{ensindexer => ensindexer-abstract}/ensv2.schema.ts (100%) rename packages/ensdb-sdk/src/{ensindexer => ensindexer-abstract}/index.ts (58%) rename packages/ensdb-sdk/src/{ensindexer => ensindexer-abstract}/protocol-acceleration.schema.ts (100%) rename packages/ensdb-sdk/src/{ensindexer => ensindexer-abstract}/registrars.schema.ts (100%) rename packages/ensdb-sdk/src/{ensindexer => ensindexer-abstract}/subgraph.schema.ts (100%) rename packages/ensdb-sdk/src/{ensindexer => ensindexer-abstract}/tokenscope.schema.ts (100%) diff --git a/apps/ensindexer/ponder/ponder.schema.ts b/apps/ensindexer/ponder/ponder.schema.ts index 5baf7a0cf0..a837e1c867 100644 --- a/apps/ensindexer/ponder/ponder.schema.ts +++ b/apps/ensindexer/ponder/ponder.schema.ts @@ -1,10 +1,6 @@ /** - * Export database schema definition for ENSIndexer - * Note: Ponder uses `globalThis.PONDER_NAMESPACE_BUILD.schema` value to - * dynamically build the "concrete" ENSIndexer Schema definition - * from the "abstract" ENSIndexer Schema definition for Ponder app to use. - * - * @see https://github.com/ponder-sh/ponder/blob/c8f6935fb65176c01b40cae9056be704c0e5318e/packages/core/src/build/index.ts#L380-L424 - * @see https://github.com/ponder-sh/ponder/blob/6fcc15d4234e43862cb6e21c05f3c57f4c2f7464/packages/core/src/drizzle/onchain.ts#L280-L281 + * We re-export (just) the "abstract" ENSIndexer Schema from ENSDb for Ponder to manage. + * Ponder will internally build a "concrete" ENSIndexer Schema using + * the "abstract" ENSIndexer Schema and the ENSIndexer Schema name. **/ -export * from "@ensnode/ensdb-sdk/ensindexer"; +export * from "@ensnode/ensdb-sdk/ensindexer-abstract"; diff --git a/packages/ensdb-sdk/package.json b/packages/ensdb-sdk/package.json index 916da3a57c..1d9af5bfe4 100644 --- a/packages/ensdb-sdk/package.json +++ b/packages/ensdb-sdk/package.json @@ -17,7 +17,7 @@ ], "exports": { ".": "./src/index.ts", - "./ensindexer": "./src/ensindexer/index.ts", + "./ensindexer-abstract": "./src/ensindexer-abstract/index.ts", "./ensnode": "./src/ensnode/index.ts" }, "files": [ @@ -33,10 +33,10 @@ "default": "./dist/index.js" } }, - "./ensindexer": { + "./ensindexer-abstract": { "import": { - "types": "./dist/ensindexer/index.d.ts", - "default": "./dist/ensindexer/index.js" + "types": "./dist/ensindexer-abstract/index.d.ts", + "default": "./dist/ensindexer-abstract/index.js" } }, "./ensnode": { @@ -59,13 +59,13 @@ "drizzle-kit:generate": "drizzle-kit generate" }, "peerDependencies": { - "@ensnode/ensnode-sdk": "workspace:", + "@ensnode/ensnode-sdk": "workspace:*", "drizzle-orm": "catalog:", "ponder": "catalog:", "viem": "catalog:" }, "devDependencies": { - "@ensnode/ensnode-sdk": "workspace:", + "@ensnode/ensnode-sdk": "workspace:*", "@ensnode/shared-configs": "workspace:*", "drizzle-kit": "0.31.10", "drizzle-orm": "catalog:", diff --git a/packages/ensdb-sdk/src/ensindexer/ensnode-metadata.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/ensnode-metadata.schema.ts similarity index 100% rename from packages/ensdb-sdk/src/ensindexer/ensnode-metadata.schema.ts rename to packages/ensdb-sdk/src/ensindexer-abstract/ensnode-metadata.schema.ts diff --git a/packages/ensdb-sdk/src/ensindexer/ensv2.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts similarity index 100% rename from packages/ensdb-sdk/src/ensindexer/ensv2.schema.ts rename to packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts diff --git a/packages/ensdb-sdk/src/ensindexer/index.ts b/packages/ensdb-sdk/src/ensindexer-abstract/index.ts similarity index 58% rename from packages/ensdb-sdk/src/ensindexer/index.ts rename to packages/ensdb-sdk/src/ensindexer-abstract/index.ts index b245eab10f..438cbccf97 100644 --- a/packages/ensdb-sdk/src/ensindexer/index.ts +++ b/packages/ensdb-sdk/src/ensindexer-abstract/index.ts @@ -1,5 +1,7 @@ /** - * Merge the various sub-schemas into ENSIndexer Schema. + * Merge the various sub-schemas into an "abstract" ENSIndexer Schema. + * This "abstract" ENSIndexer Schema is used to build the "concrete" ENSIndexer Schema + * for ENSDb, which is then used to build the ENSDb Schema for a Drizzle client for ENSDb. */ // TODO: remove `ensnode-metadata.schema` export when database migrations diff --git a/packages/ensdb-sdk/src/ensindexer/protocol-acceleration.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/protocol-acceleration.schema.ts similarity index 100% rename from packages/ensdb-sdk/src/ensindexer/protocol-acceleration.schema.ts rename to packages/ensdb-sdk/src/ensindexer-abstract/protocol-acceleration.schema.ts diff --git a/packages/ensdb-sdk/src/ensindexer/registrars.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/registrars.schema.ts similarity index 100% rename from packages/ensdb-sdk/src/ensindexer/registrars.schema.ts rename to packages/ensdb-sdk/src/ensindexer-abstract/registrars.schema.ts diff --git a/packages/ensdb-sdk/src/ensindexer/subgraph.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts similarity index 100% rename from packages/ensdb-sdk/src/ensindexer/subgraph.schema.ts rename to packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts diff --git a/packages/ensdb-sdk/src/ensindexer/tokenscope.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/tokenscope.schema.ts similarity index 100% rename from packages/ensdb-sdk/src/ensindexer/tokenscope.schema.ts rename to packages/ensdb-sdk/src/ensindexer-abstract/tokenscope.schema.ts diff --git a/packages/ensdb-sdk/src/index.ts b/packages/ensdb-sdk/src/index.ts index f9e0ac65f8..d55a9f32ef 100644 --- a/packages/ensdb-sdk/src/index.ts +++ b/packages/ensdb-sdk/src/index.ts @@ -1,3 +1,3 @@ export * from "./client"; -export * from "./ensindexer"; +export * from "./ensindexer-abstract"; export * from "./lib/drizzle"; diff --git a/packages/ensdb-sdk/tsup.config.ts b/packages/ensdb-sdk/tsup.config.ts index 82d273ac30..222e1cd9d9 100644 --- a/packages/ensdb-sdk/tsup.config.ts +++ b/packages/ensdb-sdk/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["src/index.ts", "src/ensindexer/index.ts", "src/ensnode/index.ts"], + entry: ["src/index.ts", "src/ensindexer-abstract/index.ts", "src/ensnode/index.ts"], platform: "neutral", format: ["esm"], target: "es2022", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0faedd59e8..636b789f1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -834,7 +834,7 @@ importers: packages/ensdb-sdk: devDependencies: '@ensnode/ensnode-sdk': - specifier: 'workspace:' + specifier: workspace:* version: link:../ensnode-sdk '@ensnode/shared-configs': specifier: workspace:* From e52729227054a299d8de739aede0080e94bf9ce7 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Mar 2026 19:14:27 +0100 Subject: [PATCH 25/34] Improve the method for cloning drizzle table objects --- packages/ensdb-sdk/src/lib/drizzle.test.ts | 107 ++++++++++++++++----- packages/ensdb-sdk/src/lib/drizzle.ts | 44 +++++++-- 2 files changed, 119 insertions(+), 32 deletions(-) diff --git a/packages/ensdb-sdk/src/lib/drizzle.test.ts b/packages/ensdb-sdk/src/lib/drizzle.test.ts index a2f34a7cd6..9d5c193c8c 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.test.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.test.ts @@ -2,7 +2,7 @@ import { isPgEnum } from "drizzle-orm/pg-core"; import { isTable } from "drizzle-orm/table"; import { describe, expect, it, vi } from "vitest"; -import * as abstractEnsIndexerSchema from "../ensindexer"; +import * as abstractEnsIndexerSchema from "../ensindexer-abstract"; import { buildEnsDbDrizzleClient, buildEnsDbSchema } from "./drizzle"; vi.mock("drizzle-orm/node-postgres", () => ({ @@ -12,7 +12,7 @@ vi.mock("drizzle-orm/node-postgres", () => ({ // Re-import after mock to get the mocked version const { drizzle } = await import("drizzle-orm/node-postgres"); -const SCHEMA_NAME = "ensindexer_test"; +const ENSINDEXER_SCHEMA_NAME = "ensindexer_test"; const DrizzleSchemaSymbol = Symbol.for("drizzle:Schema"); @@ -22,13 +22,13 @@ function getSchemaName(obj: unknown): string | undefined { describe("buildEnsDbSchema", () => { it("returns an object containing all ENSNode schema exports", () => { - const schema = buildEnsDbSchema(SCHEMA_NAME); + const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); expect(schema.metadata).toBeDefined(); }); it("returns an object containing all ENSIndexer schema exports", () => { - const schema = buildEnsDbSchema(SCHEMA_NAME); + const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); expect(schema.event).toBeDefined(); expect(schema.v1Domain).toBeDefined(); @@ -36,36 +36,56 @@ describe("buildEnsDbSchema", () => { expect(schema.registrationType).toBeDefined(); }); - it("sets the schema name on all ENSIndexer tables", () => { - const schema = buildEnsDbSchema(SCHEMA_NAME); + it("preserves table/enum classification across abstract → concrete", () => { + const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + + for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { + const concreteValue = schema[key as keyof typeof schema]; + + if (isTable(abstractValue)) { + expect(isTable(concreteValue)).toBe(true); + } else { + expect(isTable(concreteValue)).toBe(false); + } - for (const [key] of Object.entries(abstractEnsIndexerSchema)) { - const value = schema[key as keyof typeof schema]; - if (isTable(value)) { - expect(getSchemaName(value)).toBe(SCHEMA_NAME); + if (isPgEnum(abstractValue)) { + expect(isPgEnum(concreteValue)).toBe(true); + } else { + expect(isPgEnum(concreteValue)).toBe(false); } } }); + it("sets the schema name on all ENSIndexer tables", () => { + const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + + for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { + if (!isTable(abstractValue)) continue; + const concreteValue = schema[key as keyof typeof schema]; + expect(isTable(concreteValue)).toBe(true); + expect(getSchemaName(concreteValue)).toBe(ENSINDEXER_SCHEMA_NAME); + } + }); + it("does not mutate the schema name on ENSNode tables", () => { - const schema = buildEnsDbSchema(SCHEMA_NAME); + const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); expect(getSchemaName(schema.metadata)).toBe("ensnode"); }); it("sets the schema name on all ENSIndexer enums", () => { - const schema = buildEnsDbSchema(SCHEMA_NAME); + const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); - for (const [key] of Object.entries(abstractEnsIndexerSchema)) { - const value = schema[key as keyof typeof schema]; - if (isPgEnum(value)) { - expect((value as any).schema).toBe(SCHEMA_NAME); - } + for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { + if (!isPgEnum(abstractValue)) continue; + const concreteValue = schema[key as keyof typeof schema]; + expect(isPgEnum(concreteValue)).toBe(true); + expect((concreteValue as any).schema).toBe(ENSINDEXER_SCHEMA_NAME); } }); it("skips relation objects (neither tables nor enums)", () => { - const schema = buildEnsDbSchema(SCHEMA_NAME); + const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); for (const [key, value] of Object.entries(schema)) { if (key.endsWith("_relations") || key.endsWith("Relations")) { @@ -79,11 +99,11 @@ describe("buildEnsDbSchema", () => { const otherSchemaName = "ensindexer_other"; const schema = buildEnsDbSchema(otherSchemaName); - for (const [key] of Object.entries(abstractEnsIndexerSchema)) { - const value = schema[key as keyof typeof schema]; - if (isTable(value)) { - expect(getSchemaName(value)).toBe(otherSchemaName); - } + for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { + if (!isTable(abstractValue)) continue; + const concreteValue = schema[key as keyof typeof schema]; + expect(isTable(concreteValue)).toBe(true); + expect(getSchemaName(concreteValue)).toBe(otherSchemaName); } }); @@ -99,11 +119,16 @@ describe("buildEnsDbSchema", () => { const valueB = concreteB[key as keyof typeof concreteB]; if (isTable(abstractValue)) { + expect(isTable(valueA)).toBe(true); + expect(isTable(valueB)).toBe(true); expect(getSchemaName(valueA)).toBe(schemaNameA); expect(getSchemaName(valueB)).toBe(schemaNameB); expect(getSchemaName(abstractValue)).toBeUndefined(); } + if (isPgEnum(abstractValue)) { + expect(isPgEnum(valueA)).toBe(true); + expect(isPgEnum(valueB)).toBe(true); expect((valueA as any).schema).toBe(schemaNameA); expect((valueB as any).schema).toBe(schemaNameB); expect((abstractValue as any).schema).toBeUndefined(); @@ -115,10 +140,42 @@ describe("buildEnsDbSchema", () => { }); }); +describe("buildEnsDbSchema — prototype and Symbol preservation", () => { + const IsDrizzleTable = Symbol.for("drizzle:IsDrizzleTable"); + const Columns = Symbol.for("drizzle:Columns"); + const TableName = Symbol.for("drizzle:Name"); + + it("preserves the Table prototype on cloned tables", () => { + const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const abstractTable = abstractEnsIndexerSchema.v1Domain; + const concreteTable = schema.v1Domain; + + expect(Object.getPrototypeOf(concreteTable)).toBe(Object.getPrototypeOf(abstractTable)); + }); + + it("preserves Symbol-keyed properties (IsDrizzleTable, Columns, TableName) on cloned tables", () => { + const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const abstractTable = abstractEnsIndexerSchema.v1Domain; + const concreteTable = schema.v1Domain; + + expect((concreteTable as any)[IsDrizzleTable]).toBe((abstractTable as any)[IsDrizzleTable]); + expect((concreteTable as any)[Columns]).toBe((abstractTable as any)[Columns]); + expect((concreteTable as any)[TableName]).toBe((abstractTable as any)[TableName]); + }); + + it("isTable() returns true for cloned concrete tables", () => { + const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + + expect(isTable(schema.v1Domain)).toBe(true); + expect(isTable(schema.registration)).toBe(true); + expect(isTable(schema.event)).toBe(true); + }); +}); + describe("buildEnsDbDrizzleClient", () => { it("calls drizzle with the correct connection config", () => { const connectionString = "postgres://user:pass@localhost:5432/ensdb"; - const schema = buildEnsDbSchema(SCHEMA_NAME); + const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); buildEnsDbDrizzleClient(connectionString, schema); @@ -132,7 +189,7 @@ describe("buildEnsDbDrizzleClient", () => { it("passes the logger to drizzle when provided", () => { const connectionString = "postgres://user:pass@localhost:5432/ensdb"; - const schema = buildEnsDbSchema(SCHEMA_NAME); + const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); const logger = { logQuery: vi.fn() }; buildEnsDbDrizzleClient(connectionString, schema, logger); diff --git a/packages/ensdb-sdk/src/lib/drizzle.ts b/packages/ensdb-sdk/src/lib/drizzle.ts index 531088b341..cbd3cc29bb 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.ts @@ -10,7 +10,7 @@ import { isTable, Table } from "drizzle-orm/table"; // It's called "abstract" here because tables defined in this schema do not // reference the specific ENSIndexer Schema name, and therefore cannot be used // directly to build a Drizzle client for ENSDb. -import * as abstractEnsIndexerSchema from "../ensindexer"; +import * as abstractEnsIndexerSchema from "../ensindexer-abstract"; import * as ensNodeSchema from "../ensnode"; /** @@ -21,6 +21,36 @@ import * as ensNodeSchema from "../ensnode"; */ export type AbstractEnsIndexerSchema = typeof abstractEnsIndexerSchema; +/** + * Clone a Drizzle Table object with a new schema name. + * + * Drizzle tables store their identity (name, columns, schema) on + * Symbol-keyed properties. Cloning a table requires creating + * a new object with the same prototype, copying all properties, + * and updating the schema name. + */ +function cloneTableWithSchema( + table: TableType, + schemaName: string, +): TableType { + const clone = Object.create( + Object.getPrototypeOf(table), + Object.getOwnPropertyDescriptors(table), + ) as TableType; + + // @ts-expect-error - Drizzle's Table type for the schema symbol is + // not typed in a way that allows us to set it directly, + // but we know it exists and can be set. + clone[Table.Symbol.Schema] = schemaName; + + // Fail-fast if the clone lost the Drizzle sentinel. + if (!isTable(clone)) { + throw new Error(`Cloned table is no longer a valid Drizzle Table (schema: ${schemaName}).`); + } + + return clone; +} + /** * Build a "concrete" ENSIndexer Schema definition for ENSDb. * @@ -40,14 +70,14 @@ function buildEnsIndexerSchema abstractSchemaObject(...args), abstractSchemaObject, From 121efd5563ac657390982c0ec1835bb5d2c0cf7e Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Mar 2026 19:31:24 +0100 Subject: [PATCH 26/34] Allow using individual ENSDb Schema defintions for Drizzle querires --- apps/ensindexer/src/lib/ensdb/singleton.ts | 17 ++++++-- packages/ensdb-sdk/src/client/ensdb-reader.ts | 39 +++++++++++++------ packages/ensdb-sdk/src/client/ensdb-writer.ts | 4 +- packages/ensdb-sdk/src/lib/drizzle.ts | 31 +++++++++++---- 4 files changed, 66 insertions(+), 25 deletions(-) diff --git a/apps/ensindexer/src/lib/ensdb/singleton.ts b/apps/ensindexer/src/lib/ensdb/singleton.ts index eeb8d3b648..f5ff012fb7 100644 --- a/apps/ensindexer/src/lib/ensdb/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb/singleton.ts @@ -1,16 +1,27 @@ import config from "@/config"; -import { buildEnsDbDrizzleClient, buildEnsDbSchema, EnsDbWriter } from "@ensnode/ensdb-sdk"; +import { + buildEnsDbDrizzleClient, + buildEnsDbSchema, + buildIndividualEnsDbSchemas, + EnsDbWriter, +} from "@ensnode/ensdb-sdk"; const { databaseUrl: ensDbConnectionString, databaseSchemaName: ensIndexerSchemaName } = config; +const { ensIndexerSchema, ensNodeSchema } = buildIndividualEnsDbSchemas(ensIndexerSchemaName); /** * Build a ENSDb Schema for Drizzle client using the ENSIndexer Schema name from config. */ -const ensDbSchema = buildEnsDbSchema(ensIndexerSchemaName); +const ensDbSchema = buildEnsDbSchema(ensIndexerSchema); const ensDbDrizzleClient = buildEnsDbDrizzleClient(ensDbConnectionString, ensDbSchema); /** * Singleton instance of ENSDbWriter for the ENSIndexer application. */ -export const ensDbWriter = new EnsDbWriter(ensDbDrizzleClient, ensDbSchema, ensIndexerSchemaName); +export const ensDbWriter = new EnsDbWriter( + ensDbDrizzleClient, + ensIndexerSchema, + ensIndexerSchemaName, + ensNodeSchema, +); diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.ts b/packages/ensdb-sdk/src/client/ensdb-reader.ts index 964faacc49..5c5efb18e0 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.ts @@ -7,7 +7,7 @@ import { type EnsIndexerPublicConfig, } from "@ensnode/ensnode-sdk"; -import type { AbstractEnsIndexerSchema, EnsDbDrizzleClient, EnsDbSchema } from "../lib/drizzle"; +import type { AbstractEnsIndexerSchema, EnsDbDrizzleClient, EnsNodeSchema } from "../lib/drizzle"; import { EnsNodeMetadataKeys } from "./ensnode-metadata"; import type { SerializedEnsNodeMetadata, @@ -38,12 +38,12 @@ export class EnsDbReader< protected drizzleClient: EnsDbDrizzleClient; /** - * ENSDb Schema definition for ENSDb. + * "Concrete" ENSIndexer Schema definition for ENSDb. * - * This is the "concrete" ENSDb Schema in which tables reference + * This is the "concrete" ENSIndexer Schema in which tables reference * the ENSIndexer Schema name from {@link ensIndexerSchemaName}. */ - protected ensDbSchema: EnsDbSchema; + protected _ensIndexerSchema: EnsIndexerSchemaType; /** * The name of the ENSIndexer schema to read from in ENSDb. @@ -53,19 +53,24 @@ export class EnsDbReader< */ protected ensIndexerSchemaName: string; + protected _ensNodeSchema: EnsNodeSchema; + /** * @param ensDbDrizzleClient Drizzle client for ENSDb, typed with the "concrete" ENSIndexer Schema type. * @param ensDbSchema ENSDb Schema definition for ENSDb used by the Drizzle client. * @param ensIndexerSchemaName The name of the ENSIndexer schema to read from in ENSDb, used to identify which ENSNode metadata records to read. + * @param ensNodeSchema The ENSNode Schema definition for ENSDb used by the Drizzle client. */ constructor( ensDbDrizzleClient: EnsDbDrizzleClient, - ensDbSchema: EnsDbSchema, + ensIndexerSchema: EnsIndexerSchemaType, ensIndexerSchemaName: string, + ensNodeSchema: EnsNodeSchema, ) { this.drizzleClient = ensDbDrizzleClient; - this.ensDbSchema = ensDbSchema; + this._ensIndexerSchema = ensIndexerSchema; this.ensIndexerSchemaName = ensIndexerSchemaName; + this._ensNodeSchema = ensNodeSchema; } /** @@ -78,13 +83,23 @@ export class EnsDbReader< } /** - * Getter for the ENSDb Schema definition used in the Drizzle client + * Getter for the "concrete" ENSIndexer Schema definition used in the Drizzle client + * for ENSDb instance. + * + * Useful while working on complex queries for ENSDb. + */ + get ensIndexerSchema(): EnsIndexerSchemaType { + return this._ensIndexerSchema; + } + + /** + * Getter for the ENSNode Schema definition used in the Drizzle client * for ENSDb instance. * * Useful while working on complex queries for ENSDb. */ - get schema(): EnsDbSchema { - return this.ensDbSchema; + get ensNodeSchema(): EnsNodeSchema { + return this._ensNodeSchema; } /** @@ -149,11 +164,11 @@ export class EnsDbReader< ): Promise { const result = await this.drizzleClient .select() - .from(this.ensDbSchema.metadata) + .from(this.ensNodeSchema.metadata) .where( and( - eq(this.ensDbSchema.metadata.ensIndexerSchemaName, this.ensIndexerSchemaName), - eq(this.ensDbSchema.metadata.key, metadata.key), + eq(this.ensNodeSchema.metadata.ensIndexerSchemaName, this.ensIndexerSchemaName), + eq(this.ensNodeSchema.metadata.key, metadata.key), ), ); diff --git a/packages/ensdb-sdk/src/client/ensdb-writer.ts b/packages/ensdb-sdk/src/client/ensdb-writer.ts index 4e11ea2321..725e9a6190 100644 --- a/packages/ensdb-sdk/src/client/ensdb-writer.ts +++ b/packages/ensdb-sdk/src/client/ensdb-writer.ts @@ -80,14 +80,14 @@ export class EnsDbWriter extends EnsDbReader { */ private async upsertEnsNodeMetadata(metadata: SerializedEnsNodeMetadata): Promise { await this.drizzleClient - .insert(this.ensDbSchema.metadata) + .insert(this.ensNodeSchema.metadata) .values({ ensIndexerSchemaName: this.ensIndexerSchemaName, key: metadata.key, value: metadata.value, }) .onConflictDoUpdate({ - target: [this.ensDbSchema.metadata.ensIndexerSchemaName, this.ensDbSchema.metadata.key], + target: [this.ensNodeSchema.metadata.ensIndexerSchemaName, this.ensNodeSchema.metadata.key], set: { value: metadata.value }, }); } diff --git a/packages/ensdb-sdk/src/lib/drizzle.ts b/packages/ensdb-sdk/src/lib/drizzle.ts index cbd3cc29bb..68b63885cb 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.ts @@ -101,7 +101,25 @@ function buildEnsIndexerSchema( + ensIndexerSchemaName: string, +): { + ensIndexerSchema: EnsIndexerSchemaType; + ensNodeSchema: EnsNodeSchema; +} { + return { + ensIndexerSchema: buildEnsIndexerSchema(ensIndexerSchemaName), + ensNodeSchema, + }; +} /** * ENSDb Schema type @@ -115,19 +133,16 @@ export type EnsDbSchema = /** * Build ENSDb Schema for Drizzle client * - * Uses the provided ENSIndexer Schema name to build - * the "concrete" ENSIndexer Schema definition within the ENSDb Schema. + * Uses the provided "concrete" ENSIndexer Schema definition to build + * the ENSDb Schema. * - * @param ensIndexerSchemaName - The name of the ENSIndexer Schema instance - * in ENSDb. + * @param ensIndexerSchema - The "concrete" ENSIndexer Schema definition. * @returns The ENSDb Schema definition for use in building * a Drizzle client for ENSDb. */ export function buildEnsDbSchema( - ensIndexerSchemaName: string, + ensIndexerSchema: EnsIndexerSchemaType, ): EnsDbSchema { - const ensIndexerSchema = buildEnsIndexerSchema(ensIndexerSchemaName); - return { ...ensIndexerSchema, ...ensNodeSchema, From 6145e504b7c264c193df4686db25f0efac81a585 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Mar 2026 19:31:33 +0100 Subject: [PATCH 27/34] Update test files --- .../ensdb-sdk/src/client/ensdb-reader.test.ts | 14 ++++- .../ensdb-sdk/src/client/ensdb-writer.test.ts | 15 +++-- packages/ensdb-sdk/src/lib/drizzle.test.ts | 56 +++++++++++++------ 3 files changed, 62 insertions(+), 23 deletions(-) diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts index da432d019e..6267329d75 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts @@ -5,20 +5,28 @@ import { serializeEnsIndexerPublicConfig, } from "@ensnode/ensnode-sdk"; +import * as ensIndexerSchema from "../ensindexer-abstract"; import * as ensNodeSchema from "../ensnode"; import * as ensDbClientMock from "./ensdb-client.mock"; import { EnsDbReader } from "./ensdb-reader"; +const ensIndexerSchemaMock = ensIndexerSchema as any; +const ensNodeSchemaMock = ensNodeSchema as any; + describe("EnsDbReader", () => { const selectResult = { current: [] as Array<{ value: unknown }> }; const whereMock = vi.fn(async () => selectResult.current); const fromMock = vi.fn(() => ({ where: whereMock })); const selectMock = vi.fn(() => ({ from: fromMock })); const drizzleClientMock = { select: selectMock } as any; - const schemaMock = ensNodeSchema as any; const createEnsDbReader = () => - new EnsDbReader(drizzleClientMock, schemaMock, ensDbClientMock.ensIndexerSchemaName); + new EnsDbReader( + drizzleClientMock, + ensIndexerSchemaMock, + ensDbClientMock.ensIndexerSchemaName, + ensNodeSchemaMock, + ); beforeEach(() => { selectResult.current = []; @@ -32,7 +40,7 @@ describe("EnsDbReader", () => { await expect(createEnsDbReader().getEnsDbVersion()).resolves.toBeUndefined(); expect(selectMock).toHaveBeenCalledTimes(1); - expect(fromMock).toHaveBeenCalledWith(schemaMock.metadata); + expect(fromMock).toHaveBeenCalledWith(ensNodeSchemaMock.metadata); }); it("returns value when one record exists", async () => { diff --git a/packages/ensdb-sdk/src/client/ensdb-writer.test.ts b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts index 56452b6814..f68071b358 100644 --- a/packages/ensdb-sdk/src/client/ensdb-writer.test.ts +++ b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts @@ -7,6 +7,7 @@ import { serializeEnsIndexerPublicConfig, } from "@ensnode/ensnode-sdk"; +import * as ensIndexerSchema from "../ensindexer-abstract"; import * as ensNodeSchema from "../ensnode"; import * as ensDbClientMock from "./ensdb-client.mock"; import { EnsDbWriter } from "./ensdb-writer"; @@ -19,10 +20,16 @@ describe("EnsDbWriter", () => { const valuesMock = vi.fn(() => ({ onConflictDoUpdate: onConflictDoUpdateMock })); const insertMock = vi.fn(() => ({ values: valuesMock })); const drizzleClientMock = { insert: insertMock } as any; - const schemaMock = ensNodeSchema as any; + const ensIndexerSchemaMock = ensIndexerSchema as any; + const ensNodeSchemaMock = ensNodeSchema as any; const createEnsDbWriter = () => - new EnsDbWriter(drizzleClientMock, schemaMock, ensDbClientMock.ensIndexerSchemaName); + new EnsDbWriter( + drizzleClientMock, + ensIndexerSchemaMock, + ensDbClientMock.ensIndexerSchemaName, + ensNodeSchemaMock, + ); beforeEach(() => { onConflictDoUpdateMock.mockClear(); @@ -35,14 +42,14 @@ describe("EnsDbWriter", () => { it("writes the database version metadata", async () => { await createEnsDbWriter().upsertEnsDbVersion("0.2.0"); - expect(insertMock).toHaveBeenCalledWith(schemaMock.metadata); + expect(insertMock).toHaveBeenCalledWith(ensNodeSchemaMock.metadata); expect(valuesMock).toHaveBeenCalledWith({ ensIndexerSchemaName: ensDbClientMock.ensIndexerSchemaName, key: EnsNodeMetadataKeys.EnsDbVersion, value: "0.2.0", }); expect(onConflictDoUpdateMock).toHaveBeenCalledWith({ - target: [schemaMock.metadata.ensIndexerSchemaName, schemaMock.metadata.key], + target: [ensNodeSchemaMock.metadata.ensIndexerSchemaName, ensNodeSchemaMock.metadata.key], set: { value: "0.2.0" }, }); }); diff --git a/packages/ensdb-sdk/src/lib/drizzle.test.ts b/packages/ensdb-sdk/src/lib/drizzle.test.ts index 9d5c193c8c..0e2fe3fdda 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.test.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.test.ts @@ -3,7 +3,7 @@ import { isTable } from "drizzle-orm/table"; import { describe, expect, it, vi } from "vitest"; import * as abstractEnsIndexerSchema from "../ensindexer-abstract"; -import { buildEnsDbDrizzleClient, buildEnsDbSchema } from "./drizzle"; +import { buildEnsDbDrizzleClient, buildEnsDbSchema, buildIndividualEnsDbSchemas } from "./drizzle"; vi.mock("drizzle-orm/node-postgres", () => ({ drizzle: vi.fn(() => "mock-drizzle-client"), @@ -22,13 +22,17 @@ function getSchemaName(obj: unknown): string | undefined { describe("buildEnsDbSchema", () => { it("returns an object containing all ENSNode schema exports", () => { - const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const schema = buildEnsDbSchema( + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + ); expect(schema.metadata).toBeDefined(); }); it("returns an object containing all ENSIndexer schema exports", () => { - const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const schema = buildEnsDbSchema( + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + ); expect(schema.event).toBeDefined(); expect(schema.v1Domain).toBeDefined(); @@ -37,7 +41,9 @@ describe("buildEnsDbSchema", () => { }); it("preserves table/enum classification across abstract → concrete", () => { - const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const schema = buildEnsDbSchema( + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + ); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { const concreteValue = schema[key as keyof typeof schema]; @@ -57,7 +63,9 @@ describe("buildEnsDbSchema", () => { }); it("sets the schema name on all ENSIndexer tables", () => { - const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const schema = buildEnsDbSchema( + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + ); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { if (!isTable(abstractValue)) continue; @@ -68,13 +76,17 @@ describe("buildEnsDbSchema", () => { }); it("does not mutate the schema name on ENSNode tables", () => { - const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const schema = buildEnsDbSchema( + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + ); expect(getSchemaName(schema.metadata)).toBe("ensnode"); }); it("sets the schema name on all ENSIndexer enums", () => { - const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const schema = buildEnsDbSchema( + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + ); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { if (!isPgEnum(abstractValue)) continue; @@ -85,7 +97,9 @@ describe("buildEnsDbSchema", () => { }); it("skips relation objects (neither tables nor enums)", () => { - const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const schema = buildEnsDbSchema( + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + ); for (const [key, value] of Object.entries(schema)) { if (key.endsWith("_relations") || key.endsWith("Relations")) { @@ -97,7 +111,7 @@ describe("buildEnsDbSchema", () => { it("applies a different schema name to ENSIndexer objects", () => { const otherSchemaName = "ensindexer_other"; - const schema = buildEnsDbSchema(otherSchemaName); + const schema = buildEnsDbSchema(buildIndividualEnsDbSchemas(otherSchemaName).ensIndexerSchema); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { if (!isTable(abstractValue)) continue; @@ -111,8 +125,8 @@ describe("buildEnsDbSchema", () => { const schemaNameA = "ensindexer_alpha"; const schemaNameB = "ensindexer_beta"; - const concreteA = buildEnsDbSchema(schemaNameA); - const concreteB = buildEnsDbSchema(schemaNameB); + const concreteA = buildEnsDbSchema(buildIndividualEnsDbSchemas(schemaNameA).ensIndexerSchema); + const concreteB = buildEnsDbSchema(buildIndividualEnsDbSchemas(schemaNameB).ensIndexerSchema); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { const valueA = concreteA[key as keyof typeof concreteA]; @@ -146,7 +160,9 @@ describe("buildEnsDbSchema — prototype and Symbol preservation", () => { const TableName = Symbol.for("drizzle:Name"); it("preserves the Table prototype on cloned tables", () => { - const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const schema = buildEnsDbSchema( + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + ); const abstractTable = abstractEnsIndexerSchema.v1Domain; const concreteTable = schema.v1Domain; @@ -154,7 +170,9 @@ describe("buildEnsDbSchema — prototype and Symbol preservation", () => { }); it("preserves Symbol-keyed properties (IsDrizzleTable, Columns, TableName) on cloned tables", () => { - const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const schema = buildEnsDbSchema( + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + ); const abstractTable = abstractEnsIndexerSchema.v1Domain; const concreteTable = schema.v1Domain; @@ -164,7 +182,9 @@ describe("buildEnsDbSchema — prototype and Symbol preservation", () => { }); it("isTable() returns true for cloned concrete tables", () => { - const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const schema = buildEnsDbSchema( + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + ); expect(isTable(schema.v1Domain)).toBe(true); expect(isTable(schema.registration)).toBe(true); @@ -175,7 +195,9 @@ describe("buildEnsDbSchema — prototype and Symbol preservation", () => { describe("buildEnsDbDrizzleClient", () => { it("calls drizzle with the correct connection config", () => { const connectionString = "postgres://user:pass@localhost:5432/ensdb"; - const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const schema = buildEnsDbSchema( + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + ); buildEnsDbDrizzleClient(connectionString, schema); @@ -189,7 +211,9 @@ describe("buildEnsDbDrizzleClient", () => { it("passes the logger to drizzle when provided", () => { const connectionString = "postgres://user:pass@localhost:5432/ensdb"; - const schema = buildEnsDbSchema(ENSINDEXER_SCHEMA_NAME); + const schema = buildEnsDbSchema( + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + ); const logger = { logQuery: vi.fn() }; buildEnsDbDrizzleClient(connectionString, schema, logger); From d5f7c3ff6ad29c500dce397f7cd70ee6a0dca97b Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Fri, 20 Mar 2026 19:35:55 +0100 Subject: [PATCH 28/34] Handle database migrations execution failure --- apps/ensindexer/ponder/src/api/index.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/ensindexer/ponder/src/api/index.ts b/apps/ensindexer/ponder/src/api/index.ts index bfb67eb2bb..9b184f1280 100644 --- a/apps/ensindexer/ponder/src/api/index.ts +++ b/apps/ensindexer/ponder/src/api/index.ts @@ -10,15 +10,18 @@ import { startEnsDbWriterWorker } from "@/lib/ensdb-writer-worker/singleton"; import ensNodeApi from "./handlers/ensnode-api"; +// The entry point for the ENSDb Writer Worker. It must be placed inside +// the `api` directory of the Ponder app to avoid the following build issue: +// Error: Invalid dependency graph. Config, schema, and indexing function files +// cannot import objects from the API function file "src/api/index.ts". // Before starting the ENSDb Writer Worker, we need to ensure that // the ENSNode Schema in ENSDb is up to date by running any pending migrations. -migrateEnsNodeSchema().then(() => { - // The entry point for the ENSDb Writer Worker. It must be placed inside - // the `api` directory of the Ponder app to avoid the following build issue: - // Error: Invalid dependency graph. Config, schema, and indexing function files - // cannot import objects from the API function file "src/api/index.ts". - startEnsDbWriterWorker(); -}); +migrateEnsNodeSchema() + .then(startEnsDbWriterWorker) + .catch((error) => { + console.error("Failed to migrate ENSNode Schema — ", error); + process.exit(1); + }); const app = new Hono(); From c5397bcf5a13529dced6d87bb358f1a6c7081346 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sat, 21 Mar 2026 06:39:17 +0100 Subject: [PATCH 29/34] Rename `ensDbWriter` consts to `ensDbClient` --- .../ponder/src/api/handlers/ensnode-api.ts | 6 +- .../ensdb-writer-worker.test.ts | 70 +++++++++---------- .../ensdb-writer-worker.ts | 16 ++--- .../src/lib/ensdb-writer-worker/singleton.ts | 4 +- .../src/lib/ensdb/migrate-ensnode-schema.ts | 4 +- apps/ensindexer/src/lib/ensdb/singleton.ts | 2 +- 6 files changed, 51 insertions(+), 51 deletions(-) diff --git a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts index 5e563c77db..fc6a46e796 100644 --- a/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts +++ b/apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts @@ -10,13 +10,13 @@ import { serializeIndexingStatusResponse, } from "@ensnode/ensnode-sdk"; -import { ensDbWriter } from "@/lib/ensdb/singleton"; +import { ensDbClient } from "@/lib/ensdb/singleton"; const app = new Hono(); // include ENSIndexer Public Config endpoint app.get("/config", async (c) => { - const publicConfig = await ensDbWriter.getEnsIndexerPublicConfig(); + const publicConfig = await ensDbClient.getEnsIndexerPublicConfig(); // Invariant: the public config is guaranteed to be available in ENSDb after // application startup. @@ -30,7 +30,7 @@ app.get("/config", async (c) => { app.get("/indexing-status", async (c) => { try { - const crossChainSnapshot = await ensDbWriter.getIndexingStatusSnapshot(); + const crossChainSnapshot = await ensDbClient.getIndexingStatusSnapshot(); // Invariant: the Indexing Status Snapshot is expected to be available in // ENSDb shortly after application startup. There is a possibility that diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts index 6e5619c14a..0883978cd8 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts @@ -50,26 +50,26 @@ describe("EnsDbWriterWorker", () => { const snapshot = createMockCrossChainSnapshot({ omnichainSnapshot }); vi.mocked(buildCrossChainIndexingStatusSnapshotOmnichain).mockReturnValue(snapshot); - const ensDbWriter = createMockEnsDbWriter(); + const ensDbClient = createMockEnsDbWriter(); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = createMockIndexingStatusBuilder(omnichainSnapshot); - const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); // act await worker.run(); // assert - verify initial upserts happened - expect(ensDbWriter.upsertEnsDbVersion).toHaveBeenCalledWith( + expect(ensDbClient.upsertEnsDbVersion).toHaveBeenCalledWith( mockPublicConfig.versionInfo.ensDb, ); - expect(ensDbWriter.upsertEnsIndexerPublicConfig).toHaveBeenCalledWith(mockPublicConfig); + expect(ensDbClient.upsertEnsIndexerPublicConfig).toHaveBeenCalledWith(mockPublicConfig); // advance time to trigger interval await vi.advanceTimersByTimeAsync(1000); // assert - snapshot should be upserted - expect(ensDbWriter.upsertIndexingStatusSnapshot).toHaveBeenCalledWith(snapshot); + expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenCalledWith(snapshot); expect(buildCrossChainIndexingStatusSnapshotOmnichain).toHaveBeenCalledWith( omnichainSnapshot, expect.any(Number), @@ -86,26 +86,26 @@ describe("EnsDbWriterWorker", () => { throw incompatibleError; }); - const ensDbWriter = createMockEnsDbWriter({ + const ensDbClient = createMockEnsDbWriter({ getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(mockPublicConfig), }); const publicConfigBuilder = createMockPublicConfigBuilder(mockPublicConfig); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); // act & assert await expect(worker.run()).rejects.toThrow("incompatible"); - expect(ensDbWriter.upsertEnsDbVersion).not.toHaveBeenCalled(); + expect(ensDbClient.upsertEnsDbVersion).not.toHaveBeenCalled(); }); it("throws error when worker is already running", async () => { // arrange - const ensDbWriter = createMockEnsDbWriter(); + const ensDbClient = createMockEnsDbWriter(); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); // act - first run await worker.run(); @@ -120,34 +120,34 @@ describe("EnsDbWriterWorker", () => { it("throws error when config fetch fails", async () => { // arrange const networkError = new Error("Network failure"); - const ensDbWriter = createMockEnsDbWriter(); + const ensDbClient = createMockEnsDbWriter(); const publicConfigBuilder = { getPublicConfig: vi.fn().mockRejectedValue(networkError), } as unknown as PublicConfigBuilder; const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); // act & assert await expect(worker.run()).rejects.toThrow("Network failure"); expect(publicConfigBuilder.getPublicConfig).toHaveBeenCalledTimes(1); - expect(ensDbWriter.upsertEnsDbVersion).not.toHaveBeenCalled(); + expect(ensDbClient.upsertEnsDbVersion).not.toHaveBeenCalled(); }); it("throws error when stored config fetch fails", async () => { // arrange const dbError = new Error("Database connection lost"); - const ensDbWriter = createMockEnsDbWriter({ + const ensDbClient = createMockEnsDbWriter({ getEnsIndexerPublicConfig: vi.fn().mockRejectedValue(dbError), }); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); // act & assert await expect(worker.run()).rejects.toThrow("Database connection lost"); - expect(ensDbWriter.upsertEnsDbVersion).not.toHaveBeenCalled(); + expect(ensDbClient.upsertEnsDbVersion).not.toHaveBeenCalled(); }); it("fetches stored and in-memory configs concurrently", async () => { @@ -156,19 +156,19 @@ describe("EnsDbWriterWorker", () => { // validation passes }); - const ensDbWriter = createMockEnsDbWriter({ + const ensDbClient = createMockEnsDbWriter({ getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(mockPublicConfig), }); const publicConfigBuilder = createMockPublicConfigBuilder(mockPublicConfig); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); // act await worker.run(); // assert - both should have been called (concurrent execution via Promise.all) - expect(ensDbWriter.getEnsIndexerPublicConfig).toHaveBeenCalledTimes(1); + expect(ensDbClient.getEnsIndexerPublicConfig).toHaveBeenCalledTimes(1); expect(publicConfigBuilder.getPublicConfig).toHaveBeenCalledTimes(1); // cleanup @@ -180,18 +180,18 @@ describe("EnsDbWriterWorker", () => { const snapshot = createMockCrossChainSnapshot(); vi.mocked(buildCrossChainIndexingStatusSnapshotOmnichain).mockReturnValue(snapshot); - const ensDbWriter = createMockEnsDbWriter(); + const ensDbClient = createMockEnsDbWriter(); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); // act await worker.run(); // assert - config should be called once (pRetry is mocked) expect(publicConfigBuilder.getPublicConfig).toHaveBeenCalledTimes(1); - expect(ensDbWriter.upsertEnsIndexerPublicConfig).toHaveBeenCalledWith(mockPublicConfig); + expect(ensDbClient.upsertEnsIndexerPublicConfig).toHaveBeenCalledWith(mockPublicConfig); // cleanup worker.stop(); @@ -202,11 +202,11 @@ describe("EnsDbWriterWorker", () => { it("stops the interval when stop() is called", async () => { // arrange const upsertIndexingStatusSnapshot = vi.fn().mockResolvedValue(undefined); - const ensDbWriter = createMockEnsDbWriter({ upsertIndexingStatusSnapshot }); + const ensDbClient = createMockEnsDbWriter({ upsertIndexingStatusSnapshot }); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); // act await worker.run(); @@ -227,11 +227,11 @@ describe("EnsDbWriterWorker", () => { describe("isRunning - worker state", () => { it("indicates isRunning status correctly", async () => { // arrange - const ensDbWriter = createMockEnsDbWriter(); + const ensDbClient = createMockEnsDbWriter(); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = createMockIndexingStatusBuilder(); - const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); // assert - not running initially expect(worker.isRunning).toBe(false); @@ -267,7 +267,7 @@ describe("EnsDbWriterWorker", () => { vi.mocked(buildCrossChainIndexingStatusSnapshotOmnichain).mockReturnValue(crossChainSnapshot); - const ensDbWriter = createMockEnsDbWriter(); + const ensDbClient = createMockEnsDbWriter(); const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = { getOmnichainIndexingStatusSnapshot: vi @@ -276,7 +276,7 @@ describe("EnsDbWriterWorker", () => { .mockResolvedValueOnce(validSnapshot), } as unknown as IndexingStatusBuilder; - const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); // act - run returns immediately await worker.run(); @@ -289,8 +289,8 @@ describe("EnsDbWriterWorker", () => { // assert expect(indexingStatusBuilder.getOmnichainIndexingStatusSnapshot).toHaveBeenCalledTimes(2); - expect(ensDbWriter.upsertIndexingStatusSnapshot).toHaveBeenCalledTimes(1); - expect(ensDbWriter.upsertIndexingStatusSnapshot).toHaveBeenCalledWith(crossChainSnapshot); + expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenCalledTimes(1); + expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenCalledWith(crossChainSnapshot); // cleanup worker.stop(); @@ -317,7 +317,7 @@ describe("EnsDbWriterWorker", () => { .mockReturnValueOnce(crossChainSnapshot2) .mockReturnValueOnce(crossChainSnapshot2); - const ensDbWriter = createMockEnsDbWriter({ + const ensDbClient = createMockEnsDbWriter({ upsertIndexingStatusSnapshot: vi .fn() .mockResolvedValueOnce(undefined) @@ -333,24 +333,24 @@ describe("EnsDbWriterWorker", () => { .mockResolvedValueOnce(snapshot2), } as unknown as IndexingStatusBuilder; - const worker = new EnsDbWriterWorker(ensDbWriter, publicConfigBuilder, indexingStatusBuilder); + const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); // act await worker.run(); // first tick - succeeds await vi.advanceTimersByTimeAsync(1000); - expect(ensDbWriter.upsertIndexingStatusSnapshot).toHaveBeenCalledWith(crossChainSnapshot1); + expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenCalledWith(crossChainSnapshot1); // second tick - fails with DB error, but continues await vi.advanceTimersByTimeAsync(1000); - expect(ensDbWriter.upsertIndexingStatusSnapshot).toHaveBeenLastCalledWith( + expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenLastCalledWith( crossChainSnapshot2, ); // third tick - succeeds again await vi.advanceTimersByTimeAsync(1000); - expect(ensDbWriter.upsertIndexingStatusSnapshot).toHaveBeenCalledTimes(3); + expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenCalledTimes(3); // cleanup worker.stop(); diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts index 8dc66b2ae2..5dc5b4b1e8 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts @@ -38,7 +38,7 @@ export class EnsDbWriterWorker { /** * ENSDb Client instance used by the worker to interact with ENSDb. */ - private ensDbWriter: EnsDbWriter; + private ensDbClient: EnsDbWriter; /** * Indexing Status Builder instance used by the worker to read ENSIndexer Indexing Status. @@ -51,16 +51,16 @@ export class EnsDbWriterWorker { private publicConfigBuilder: PublicConfigBuilder; /** - * @param ensDbWriter ENSDb Writer instance used by the worker to interact with ENSDb. + * @param ensDbClient ENSDb Writer instance used by the worker to interact with ENSDb. * @param publicConfigBuilder ENSIndexer Public Config Builder instance used by the worker to read ENSIndexer Public Config. * @param indexingStatusBuilder Indexing Status Builder instance used by the worker to read ENSIndexer Indexing Status. */ constructor( - ensDbWriter: EnsDbWriter, + ensDbClient: EnsDbWriter, publicConfigBuilder: PublicConfigBuilder, indexingStatusBuilder: IndexingStatusBuilder, ) { - this.ensDbWriter = ensDbWriter; + this.ensDbClient = ensDbClient; this.publicConfigBuilder = publicConfigBuilder; this.indexingStatusBuilder = indexingStatusBuilder; } @@ -90,14 +90,14 @@ export class EnsDbWriterWorker { // Task 1: upsert ENSDb version into ENSDb. console.log(`[EnsDbWriterWorker]: Upserting ENSDb version into ENSDb...`); - await this.ensDbWriter.upsertEnsDbVersion(inMemoryConfig.versionInfo.ensDb); + await this.ensDbClient.upsertEnsDbVersion(inMemoryConfig.versionInfo.ensDb); console.log( `[EnsDbWriterWorker]: ENSDb version upserted successfully: ${inMemoryConfig.versionInfo.ensDb}`, ); // Task 2: upsert of EnsIndexerPublicConfig into ENSDb. console.log(`[EnsDbWriterWorker]: Upserting ENSIndexer Public Config into ENSDb...`); - await this.ensDbWriter.upsertEnsIndexerPublicConfig(inMemoryConfig); + await this.ensDbClient.upsertEnsIndexerPublicConfig(inMemoryConfig); console.log(`[EnsDbWriterWorker]: ENSIndexer Public Config upserted successfully`); // Task 3: recurring upsert of Indexing Status Snapshot into ENSDb. @@ -164,7 +164,7 @@ export class EnsDbWriterWorker { try { [storedConfig, inMemoryConfig] = await Promise.all([ - this.ensDbWriter.getEnsIndexerPublicConfig(), + this.ensDbClient.getEnsIndexerPublicConfig(), inMemoryConfigPromise, ]); } catch (error) { @@ -221,7 +221,7 @@ export class EnsDbWriterWorker { snapshotTime, ); - await this.ensDbWriter.upsertIndexingStatusSnapshot(crossChainSnapshot); + await this.ensDbClient.upsertIndexingStatusSnapshot(crossChainSnapshot); } catch (error) { console.error( `[EnsDbWriterWorker]: Error retrieving or validating Indexing Status Snapshot:`, diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts index 32d7968bda..48ef90dffe 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts @@ -1,4 +1,4 @@ -import { ensDbWriter } from "@/lib/ensdb/singleton"; +import { ensDbClient } from "@/lib/ensdb/singleton"; import { indexingStatusBuilder } from "@/lib/indexing-status-builder/singleton"; import { publicConfigBuilder } from "@/lib/public-config-builder/singleton"; @@ -21,7 +21,7 @@ export function startEnsDbWriterWorker() { } ensDbWriterWorker = new EnsDbWriterWorker( - ensDbWriter, + ensDbClient, publicConfigBuilder, indexingStatusBuilder, ); diff --git a/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts b/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts index 1362c699af..6a3efdebb0 100644 --- a/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts +++ b/apps/ensindexer/src/lib/ensdb/migrate-ensnode-schema.ts @@ -1,7 +1,7 @@ import { createRequire } from "node:module"; import { join } from "node:path"; -import { ensDbWriter } from "./singleton"; +import { ensDbClient } from "./singleton"; // Resolve the path to the migrations directory within the ENSDb SDK package const migrationsDirPath = join( @@ -14,6 +14,6 @@ const migrationsDirPath = join( */ export async function migrateEnsNodeSchema(): Promise { console.log(`Running database migrations for ENSNode Schema in ENSDb.`); - await ensDbWriter.migrateEnsNodeSchema(migrationsDirPath); + await ensDbClient.migrateEnsNodeSchema(migrationsDirPath); console.log(`Database migrations for ENSNode Schema in ENSDb completed successfully.`); } diff --git a/apps/ensindexer/src/lib/ensdb/singleton.ts b/apps/ensindexer/src/lib/ensdb/singleton.ts index f5ff012fb7..323d0b36de 100644 --- a/apps/ensindexer/src/lib/ensdb/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb/singleton.ts @@ -19,7 +19,7 @@ const ensDbDrizzleClient = buildEnsDbDrizzleClient(ensDbConnectionString, ensDbS /** * Singleton instance of ENSDbWriter for the ENSIndexer application. */ -export const ensDbWriter = new EnsDbWriter( +export const ensDbClient = new EnsDbWriter( ensDbDrizzleClient, ensIndexerSchema, ensIndexerSchemaName, From 5ac0060db6e74bf7c71d7319966c1f8b9d490808 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sat, 21 Mar 2026 06:54:20 +0100 Subject: [PATCH 30/34] Make usage of ENSIndexer Schema explicitly "concrete" --- apps/ensindexer/src/lib/ensdb/singleton.ts | 7 ++-- .../ensdb-sdk/src/client/ensdb-reader.test.ts | 15 ++++---- packages/ensdb-sdk/src/client/ensdb-reader.ts | 16 ++++----- .../ensdb-sdk/src/client/ensdb-writer.test.ts | 16 ++++----- packages/ensdb-sdk/src/lib/drizzle.test.ts | 36 +++++++++++-------- packages/ensdb-sdk/src/lib/drizzle.ts | 36 ++++++++++--------- 6 files changed, 67 insertions(+), 59 deletions(-) diff --git a/apps/ensindexer/src/lib/ensdb/singleton.ts b/apps/ensindexer/src/lib/ensdb/singleton.ts index 323d0b36de..85dd2e198c 100644 --- a/apps/ensindexer/src/lib/ensdb/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb/singleton.ts @@ -9,11 +9,12 @@ import { const { databaseUrl: ensDbConnectionString, databaseSchemaName: ensIndexerSchemaName } = config; -const { ensIndexerSchema, ensNodeSchema } = buildIndividualEnsDbSchemas(ensIndexerSchemaName); +const { concreteEnsIndexerSchema, ensNodeSchema } = + buildIndividualEnsDbSchemas(ensIndexerSchemaName); /** * Build a ENSDb Schema for Drizzle client using the ENSIndexer Schema name from config. */ -const ensDbSchema = buildEnsDbSchema(ensIndexerSchema); +const ensDbSchema = buildEnsDbSchema(concreteEnsIndexerSchema); const ensDbDrizzleClient = buildEnsDbDrizzleClient(ensDbConnectionString, ensDbSchema); /** @@ -21,7 +22,7 @@ const ensDbDrizzleClient = buildEnsDbDrizzleClient(ensDbConnectionString, ensDbS */ export const ensDbClient = new EnsDbWriter( ensDbDrizzleClient, - ensIndexerSchema, + concreteEnsIndexerSchema, ensIndexerSchemaName, ensNodeSchema, ); diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts index 6267329d75..18c4725e7f 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts @@ -5,27 +5,26 @@ import { serializeEnsIndexerPublicConfig, } from "@ensnode/ensnode-sdk"; -import * as ensIndexerSchema from "../ensindexer-abstract"; -import * as ensNodeSchema from "../ensnode"; +import { buildIndividualEnsDbSchemas } from "../lib/drizzle"; import * as ensDbClientMock from "./ensdb-client.mock"; import { EnsDbReader } from "./ensdb-reader"; -const ensIndexerSchemaMock = ensIndexerSchema as any; -const ensNodeSchemaMock = ensNodeSchema as any; - describe("EnsDbReader", () => { const selectResult = { current: [] as Array<{ value: unknown }> }; const whereMock = vi.fn(async () => selectResult.current); const fromMock = vi.fn(() => ({ where: whereMock })); const selectMock = vi.fn(() => ({ from: fromMock })); const drizzleClientMock = { select: selectMock } as any; + const { concreteEnsIndexerSchema, ensNodeSchema } = buildIndividualEnsDbSchemas( + ensDbClientMock.ensIndexerSchemaName, + ); const createEnsDbReader = () => new EnsDbReader( drizzleClientMock, - ensIndexerSchemaMock, + concreteEnsIndexerSchema, ensDbClientMock.ensIndexerSchemaName, - ensNodeSchemaMock, + ensNodeSchema, ); beforeEach(() => { @@ -40,7 +39,7 @@ describe("EnsDbReader", () => { await expect(createEnsDbReader().getEnsDbVersion()).resolves.toBeUndefined(); expect(selectMock).toHaveBeenCalledTimes(1); - expect(fromMock).toHaveBeenCalledWith(ensNodeSchemaMock.metadata); + expect(fromMock).toHaveBeenCalledWith(ensNodeSchema.metadata); }); it("returns value when one record exists", async () => { diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.ts b/packages/ensdb-sdk/src/client/ensdb-reader.ts index 5c5efb18e0..7a8458ed4f 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.ts @@ -22,20 +22,20 @@ import type { * Enables read-only querying of an ENSDb instance, including data spanning * the ENSNode Schema and the specified ENSIndexer Schema. * - * Note: we use a parameter type `EnsIndexerSchemaType` to represent + * Note: we use a parameter type `ConcreteEnsIndexerSchema` to represent * the "concrete" ENSIndexer Schema type within the ENSDb Schema and * make sure that the Drizzle client used for querying is typed with * the same "concrete" ENSIndexer Schema type. */ export class EnsDbReader< - EnsIndexerSchemaType extends AbstractEnsIndexerSchema = AbstractEnsIndexerSchema, + ConcreteEnsIndexerSchema extends AbstractEnsIndexerSchema = AbstractEnsIndexerSchema, > { /** * Drizzle client for ENSDb. * * Uses the ENSDb Schema from {@link ensDbSchema}. */ - protected drizzleClient: EnsDbDrizzleClient; + protected drizzleClient: EnsDbDrizzleClient; /** * "Concrete" ENSIndexer Schema definition for ENSDb. @@ -43,7 +43,7 @@ export class EnsDbReader< * This is the "concrete" ENSIndexer Schema in which tables reference * the ENSIndexer Schema name from {@link ensIndexerSchemaName}. */ - protected _ensIndexerSchema: EnsIndexerSchemaType; + protected _ensIndexerSchema: ConcreteEnsIndexerSchema; /** * The name of the ENSIndexer schema to read from in ENSDb. @@ -62,8 +62,8 @@ export class EnsDbReader< * @param ensNodeSchema The ENSNode Schema definition for ENSDb used by the Drizzle client. */ constructor( - ensDbDrizzleClient: EnsDbDrizzleClient, - ensIndexerSchema: EnsIndexerSchemaType, + ensDbDrizzleClient: EnsDbDrizzleClient, + ensIndexerSchema: ConcreteEnsIndexerSchema, ensIndexerSchemaName: string, ensNodeSchema: EnsNodeSchema, ) { @@ -78,7 +78,7 @@ export class EnsDbReader< * * Useful while working on complex queries for ENSDb. */ - get client(): EnsDbDrizzleClient { + get client(): EnsDbDrizzleClient { return this.drizzleClient; } @@ -88,7 +88,7 @@ export class EnsDbReader< * * Useful while working on complex queries for ENSDb. */ - get ensIndexerSchema(): EnsIndexerSchemaType { + get ensIndexerSchema(): ConcreteEnsIndexerSchema { return this._ensIndexerSchema; } diff --git a/packages/ensdb-sdk/src/client/ensdb-writer.test.ts b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts index f68071b358..c89a65d461 100644 --- a/packages/ensdb-sdk/src/client/ensdb-writer.test.ts +++ b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts @@ -7,8 +7,7 @@ import { serializeEnsIndexerPublicConfig, } from "@ensnode/ensnode-sdk"; -import * as ensIndexerSchema from "../ensindexer-abstract"; -import * as ensNodeSchema from "../ensnode"; +import { buildIndividualEnsDbSchemas } from "../lib/drizzle"; import * as ensDbClientMock from "./ensdb-client.mock"; import { EnsDbWriter } from "./ensdb-writer"; import { EnsNodeMetadataKeys } from "./ensnode-metadata"; @@ -20,15 +19,16 @@ describe("EnsDbWriter", () => { const valuesMock = vi.fn(() => ({ onConflictDoUpdate: onConflictDoUpdateMock })); const insertMock = vi.fn(() => ({ values: valuesMock })); const drizzleClientMock = { insert: insertMock } as any; - const ensIndexerSchemaMock = ensIndexerSchema as any; - const ensNodeSchemaMock = ensNodeSchema as any; + const { concreteEnsIndexerSchema, ensNodeSchema } = buildIndividualEnsDbSchemas( + ensDbClientMock.ensIndexerSchemaName, + ); const createEnsDbWriter = () => new EnsDbWriter( drizzleClientMock, - ensIndexerSchemaMock, + concreteEnsIndexerSchema, ensDbClientMock.ensIndexerSchemaName, - ensNodeSchemaMock, + ensNodeSchema, ); beforeEach(() => { @@ -42,14 +42,14 @@ describe("EnsDbWriter", () => { it("writes the database version metadata", async () => { await createEnsDbWriter().upsertEnsDbVersion("0.2.0"); - expect(insertMock).toHaveBeenCalledWith(ensNodeSchemaMock.metadata); + expect(insertMock).toHaveBeenCalledWith(ensNodeSchema.metadata); expect(valuesMock).toHaveBeenCalledWith({ ensIndexerSchemaName: ensDbClientMock.ensIndexerSchemaName, key: EnsNodeMetadataKeys.EnsDbVersion, value: "0.2.0", }); expect(onConflictDoUpdateMock).toHaveBeenCalledWith({ - target: [ensNodeSchemaMock.metadata.ensIndexerSchemaName, ensNodeSchemaMock.metadata.key], + target: [ensNodeSchema.metadata.ensIndexerSchemaName, ensNodeSchema.metadata.key], set: { value: "0.2.0" }, }); }); diff --git a/packages/ensdb-sdk/src/lib/drizzle.test.ts b/packages/ensdb-sdk/src/lib/drizzle.test.ts index 0e2fe3fdda..3b1a91290c 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.test.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.test.ts @@ -23,7 +23,7 @@ function getSchemaName(obj: unknown): string | undefined { describe("buildEnsDbSchema", () => { it("returns an object containing all ENSNode schema exports", () => { const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, ); expect(schema.metadata).toBeDefined(); @@ -31,7 +31,7 @@ describe("buildEnsDbSchema", () => { it("returns an object containing all ENSIndexer schema exports", () => { const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, ); expect(schema.event).toBeDefined(); @@ -42,7 +42,7 @@ describe("buildEnsDbSchema", () => { it("preserves table/enum classification across abstract → concrete", () => { const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, ); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { @@ -64,7 +64,7 @@ describe("buildEnsDbSchema", () => { it("sets the schema name on all ENSIndexer tables", () => { const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, ); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { @@ -77,7 +77,7 @@ describe("buildEnsDbSchema", () => { it("does not mutate the schema name on ENSNode tables", () => { const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, ); expect(getSchemaName(schema.metadata)).toBe("ensnode"); @@ -85,7 +85,7 @@ describe("buildEnsDbSchema", () => { it("sets the schema name on all ENSIndexer enums", () => { const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, ); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { @@ -98,7 +98,7 @@ describe("buildEnsDbSchema", () => { it("skips relation objects (neither tables nor enums)", () => { const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, ); for (const [key, value] of Object.entries(schema)) { @@ -111,7 +111,9 @@ describe("buildEnsDbSchema", () => { it("applies a different schema name to ENSIndexer objects", () => { const otherSchemaName = "ensindexer_other"; - const schema = buildEnsDbSchema(buildIndividualEnsDbSchemas(otherSchemaName).ensIndexerSchema); + const schema = buildEnsDbSchema( + buildIndividualEnsDbSchemas(otherSchemaName).concreteEnsIndexerSchema, + ); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { if (!isTable(abstractValue)) continue; @@ -125,8 +127,12 @@ describe("buildEnsDbSchema", () => { const schemaNameA = "ensindexer_alpha"; const schemaNameB = "ensindexer_beta"; - const concreteA = buildEnsDbSchema(buildIndividualEnsDbSchemas(schemaNameA).ensIndexerSchema); - const concreteB = buildEnsDbSchema(buildIndividualEnsDbSchemas(schemaNameB).ensIndexerSchema); + const concreteA = buildEnsDbSchema( + buildIndividualEnsDbSchemas(schemaNameA).concreteEnsIndexerSchema, + ); + const concreteB = buildEnsDbSchema( + buildIndividualEnsDbSchemas(schemaNameB).concreteEnsIndexerSchema, + ); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { const valueA = concreteA[key as keyof typeof concreteA]; @@ -161,7 +167,7 @@ describe("buildEnsDbSchema — prototype and Symbol preservation", () => { it("preserves the Table prototype on cloned tables", () => { const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, ); const abstractTable = abstractEnsIndexerSchema.v1Domain; const concreteTable = schema.v1Domain; @@ -171,7 +177,7 @@ describe("buildEnsDbSchema — prototype and Symbol preservation", () => { it("preserves Symbol-keyed properties (IsDrizzleTable, Columns, TableName) on cloned tables", () => { const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, ); const abstractTable = abstractEnsIndexerSchema.v1Domain; const concreteTable = schema.v1Domain; @@ -183,7 +189,7 @@ describe("buildEnsDbSchema — prototype and Symbol preservation", () => { it("isTable() returns true for cloned concrete tables", () => { const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, ); expect(isTable(schema.v1Domain)).toBe(true); @@ -196,7 +202,7 @@ describe("buildEnsDbDrizzleClient", () => { it("calls drizzle with the correct connection config", () => { const connectionString = "postgres://user:pass@localhost:5432/ensdb"; const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, ); buildEnsDbDrizzleClient(connectionString, schema); @@ -212,7 +218,7 @@ describe("buildEnsDbDrizzleClient", () => { it("passes the logger to drizzle when provided", () => { const connectionString = "postgres://user:pass@localhost:5432/ensdb"; const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).ensIndexerSchema, + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, ); const logger = { logQuery: vi.fn() }; diff --git a/packages/ensdb-sdk/src/lib/drizzle.ts b/packages/ensdb-sdk/src/lib/drizzle.ts index 68b63885cb..062c0cdb3f 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.ts @@ -63,10 +63,10 @@ function cloneTableWithSchema( * * Note: this function is a replacement for `setDatabaseSchema` from `@ponder/client`. */ -function buildEnsIndexerSchema( +function buildConcreteEnsIndexerSchema( ensIndexerSchemaName: string, -): EnsIndexerSchemaType { - const ensIndexerSchema = {} as EnsIndexerSchemaType; +): ConcreteEnsIndexerSchema { + const ensIndexerSchema = {} as ConcreteEnsIndexerSchema; for (const [key, abstractSchemaObject] of Object.entries(abstractEnsIndexerSchema)) { if (isTable(abstractSchemaObject)) { @@ -109,14 +109,16 @@ export type EnsNodeSchema = typeof ensNodeSchema; * @param ensIndexerSchemaName - The name of the ENSIndexer Schema instance in ENSDb. * @returns An object containing the "concrete" ENSIndexer Schema and the ENSNode Schema. */ -export function buildIndividualEnsDbSchemas( +export function buildIndividualEnsDbSchemas< + ConcreteEnsIndexerSchema extends AbstractEnsIndexerSchema, +>( ensIndexerSchemaName: string, ): { - ensIndexerSchema: EnsIndexerSchemaType; + concreteEnsIndexerSchema: ConcreteEnsIndexerSchema; ensNodeSchema: EnsNodeSchema; } { return { - ensIndexerSchema: buildEnsIndexerSchema(ensIndexerSchemaName), + concreteEnsIndexerSchema: buildConcreteEnsIndexerSchema(ensIndexerSchemaName), ensNodeSchema, }; } @@ -127,8 +129,8 @@ export function buildIndividualEnsDbSchemas = - EnsIndexerSchemaType & EnsNodeSchema; +export type EnsDbSchema = + ConcreteEnsIndexerSchema & EnsNodeSchema; /** * Build ENSDb Schema for Drizzle client @@ -140,9 +142,9 @@ export type EnsDbSchema = * @returns The ENSDb Schema definition for use in building * a Drizzle client for ENSDb. */ -export function buildEnsDbSchema( - ensIndexerSchema: EnsIndexerSchemaType, -): EnsDbSchema { +export function buildEnsDbSchema( + ensIndexerSchema: ConcreteEnsIndexerSchema, +): EnsDbSchema { return { ...ensIndexerSchema, ...ensNodeSchema, @@ -152,12 +154,12 @@ export function buildEnsDbSchema = - NodePgDatabase>; +export type EnsDbDrizzleClient = + NodePgDatabase>; /** * Build a Drizzle client for ENSDb. @@ -167,11 +169,11 @@ export type EnsDbDrizzleClient( +export function buildEnsDbDrizzleClient( connectionString: string, - ensDbSchema: EnsDbSchema, + ensDbSchema: EnsDbSchema, logger?: DrizzleLogger, -): EnsDbDrizzleClient { +): EnsDbDrizzleClient { return drizzle({ connection: connectionString, schema: ensDbSchema, From 593cbb885ce5a8b17a792c65809bae82a68c7a85 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sat, 21 Mar 2026 07:12:04 +0100 Subject: [PATCH 31/34] Make `drizzle` module abstractions from ENSDb SDK not to be shared publicly. The module is only required to support implementations of `EnsDbReader` and `EnsDbWriter`. --- packages/ensdb-sdk/src/index.ts | 1 - packages/ensdb-sdk/src/lib/drizzle.test.ts | 163 +++++++++++---------- packages/ensdb-sdk/src/lib/drizzle.ts | 16 +- 3 files changed, 96 insertions(+), 84 deletions(-) diff --git a/packages/ensdb-sdk/src/index.ts b/packages/ensdb-sdk/src/index.ts index d55a9f32ef..0443c6f825 100644 --- a/packages/ensdb-sdk/src/index.ts +++ b/packages/ensdb-sdk/src/index.ts @@ -1,3 +1,2 @@ export * from "./client"; export * from "./ensindexer-abstract"; -export * from "./lib/drizzle"; diff --git a/packages/ensdb-sdk/src/lib/drizzle.test.ts b/packages/ensdb-sdk/src/lib/drizzle.test.ts index 3b1a91290c..eecd357693 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.test.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.test.ts @@ -1,9 +1,9 @@ import { isPgEnum } from "drizzle-orm/pg-core"; import { isTable } from "drizzle-orm/table"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import * as abstractEnsIndexerSchema from "../ensindexer-abstract"; -import { buildEnsDbDrizzleClient, buildEnsDbSchema, buildIndividualEnsDbSchemas } from "./drizzle"; +import { buildEnsDbDrizzleClient, buildIndividualEnsDbSchemas } from "./drizzle"; vi.mock("drizzle-orm/node-postgres", () => ({ drizzle: vi.fn(() => "mock-drizzle-client"), @@ -11,6 +11,7 @@ vi.mock("drizzle-orm/node-postgres", () => ({ // Re-import after mock to get the mocked version const { drizzle } = await import("drizzle-orm/node-postgres"); +const drizzleMock = vi.mocked(drizzle); const ENSINDEXER_SCHEMA_NAME = "ensindexer_test"; @@ -20,33 +21,35 @@ function getSchemaName(obj: unknown): string | undefined { return (obj as any)[DrizzleSchemaSymbol]; } -describe("buildEnsDbSchema", () => { - it("returns an object containing all ENSNode schema exports", () => { - const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, - ); +function getCombinedSchema(schemaName: string) { + drizzleMock.mockClear(); + const { concreteEnsIndexerSchema } = buildIndividualEnsDbSchemas(schemaName); + buildEnsDbDrizzleClient("postgres://localhost/test", concreteEnsIndexerSchema); + const callArgs = drizzleMock.mock.calls[0][0] as { schema: Record }; + return callArgs.schema; +} - expect(schema.metadata).toBeDefined(); +describe("buildIndividualEnsDbSchemas", () => { + it("returns a concreteEnsIndexerSchema containing all ENSIndexer schema exports", () => { + const { concreteEnsIndexerSchema } = buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME); + + expect(concreteEnsIndexerSchema.event).toBeDefined(); + expect(concreteEnsIndexerSchema.v1Domain).toBeDefined(); + expect(concreteEnsIndexerSchema.registration).toBeDefined(); + expect(concreteEnsIndexerSchema.registrationType).toBeDefined(); }); - it("returns an object containing all ENSIndexer schema exports", () => { - const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, - ); + it("returns an ensNodeSchema containing metadata", () => { + const { ensNodeSchema } = buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME); - expect(schema.event).toBeDefined(); - expect(schema.v1Domain).toBeDefined(); - expect(schema.registration).toBeDefined(); - expect(schema.registrationType).toBeDefined(); + expect(ensNodeSchema.metadata).toBeDefined(); }); it("preserves table/enum classification across abstract → concrete", () => { - const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, - ); + const { concreteEnsIndexerSchema } = buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { - const concreteValue = schema[key as keyof typeof schema]; + const concreteValue = concreteEnsIndexerSchema[key as keyof typeof concreteEnsIndexerSchema]; if (isTable(abstractValue)) { expect(isTable(concreteValue)).toBe(true); @@ -63,45 +66,31 @@ describe("buildEnsDbSchema", () => { }); it("sets the schema name on all ENSIndexer tables", () => { - const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, - ); + const { concreteEnsIndexerSchema } = buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { if (!isTable(abstractValue)) continue; - const concreteValue = schema[key as keyof typeof schema]; + const concreteValue = concreteEnsIndexerSchema[key as keyof typeof concreteEnsIndexerSchema]; expect(isTable(concreteValue)).toBe(true); expect(getSchemaName(concreteValue)).toBe(ENSINDEXER_SCHEMA_NAME); } }); - it("does not mutate the schema name on ENSNode tables", () => { - const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, - ); - - expect(getSchemaName(schema.metadata)).toBe("ensnode"); - }); - it("sets the schema name on all ENSIndexer enums", () => { - const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, - ); + const { concreteEnsIndexerSchema } = buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { if (!isPgEnum(abstractValue)) continue; - const concreteValue = schema[key as keyof typeof schema]; + const concreteValue = concreteEnsIndexerSchema[key as keyof typeof concreteEnsIndexerSchema]; expect(isPgEnum(concreteValue)).toBe(true); expect((concreteValue as any).schema).toBe(ENSINDEXER_SCHEMA_NAME); } }); it("skips relation objects (neither tables nor enums)", () => { - const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, - ); + const { concreteEnsIndexerSchema } = buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME); - for (const [key, value] of Object.entries(schema)) { + for (const [key, value] of Object.entries(concreteEnsIndexerSchema)) { if (key.endsWith("_relations") || key.endsWith("Relations")) { expect(isTable(value)).toBe(false); expect(isPgEnum(value)).toBe(false); @@ -111,13 +100,11 @@ describe("buildEnsDbSchema", () => { it("applies a different schema name to ENSIndexer objects", () => { const otherSchemaName = "ensindexer_other"; - const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(otherSchemaName).concreteEnsIndexerSchema, - ); + const { concreteEnsIndexerSchema } = buildIndividualEnsDbSchemas(otherSchemaName); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { if (!isTable(abstractValue)) continue; - const concreteValue = schema[key as keyof typeof schema]; + const concreteValue = concreteEnsIndexerSchema[key as keyof typeof concreteEnsIndexerSchema]; expect(isTable(concreteValue)).toBe(true); expect(getSchemaName(concreteValue)).toBe(otherSchemaName); } @@ -127,12 +114,8 @@ describe("buildEnsDbSchema", () => { const schemaNameA = "ensindexer_alpha"; const schemaNameB = "ensindexer_beta"; - const concreteA = buildEnsDbSchema( - buildIndividualEnsDbSchemas(schemaNameA).concreteEnsIndexerSchema, - ); - const concreteB = buildEnsDbSchema( - buildIndividualEnsDbSchemas(schemaNameB).concreteEnsIndexerSchema, - ); + const { concreteEnsIndexerSchema: concreteA } = buildIndividualEnsDbSchemas(schemaNameA); + const { concreteEnsIndexerSchema: concreteB } = buildIndividualEnsDbSchemas(schemaNameB); for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { const valueA = concreteA[key as keyof typeof concreteA]; @@ -154,33 +137,61 @@ describe("buildEnsDbSchema", () => { expect((abstractValue as any).schema).toBeUndefined(); } } + }); +}); + +describe("combined schema (via buildEnsDbDrizzleClient)", () => { + it("contains all ENSNode schema exports (metadata)", () => { + const schema = getCombinedSchema(ENSINDEXER_SCHEMA_NAME); + + expect(schema.metadata).toBeDefined(); + }); + + it("does not mutate the schema name on ENSNode tables", () => { + const schema = getCombinedSchema(ENSINDEXER_SCHEMA_NAME); + + expect(getSchemaName(schema.metadata)).toBe("ensnode"); + }); + + it("contains all ENSIndexer schema exports", () => { + const schema = getCombinedSchema(ENSINDEXER_SCHEMA_NAME); + + for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { + const concreteValue = schema[key as keyof typeof schema]; + if (isTable(abstractValue)) { + expect(isTable(concreteValue)).toBe(true); + } else if (isPgEnum(abstractValue)) { + expect(isPgEnum(concreteValue)).toBe(true); + } + } + }); + + it("ensures ensnode metadata schema is consistent across multiple concrete schemas", () => { + const schemaA = getCombinedSchema("ensindexer_alpha"); + const schemaB = getCombinedSchema("ensindexer_beta"); - expect(getSchemaName(concreteA.metadata)).toBe("ensnode"); - expect(getSchemaName(concreteB.metadata)).toBe("ensnode"); + expect(getSchemaName(schemaA.metadata)).toBe("ensnode"); + expect(getSchemaName(schemaB.metadata)).toBe("ensnode"); }); }); -describe("buildEnsDbSchema — prototype and Symbol preservation", () => { +describe("concrete tables — prototype and Symbol preservation", () => { const IsDrizzleTable = Symbol.for("drizzle:IsDrizzleTable"); const Columns = Symbol.for("drizzle:Columns"); const TableName = Symbol.for("drizzle:Name"); it("preserves the Table prototype on cloned tables", () => { - const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, - ); + const { concreteEnsIndexerSchema } = buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME); const abstractTable = abstractEnsIndexerSchema.v1Domain; - const concreteTable = schema.v1Domain; + const concreteTable = concreteEnsIndexerSchema.v1Domain; expect(Object.getPrototypeOf(concreteTable)).toBe(Object.getPrototypeOf(abstractTable)); }); it("preserves Symbol-keyed properties (IsDrizzleTable, Columns, TableName) on cloned tables", () => { - const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, - ); + const { concreteEnsIndexerSchema } = buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME); const abstractTable = abstractEnsIndexerSchema.v1Domain; - const concreteTable = schema.v1Domain; + const concreteTable = concreteEnsIndexerSchema.v1Domain; expect((concreteTable as any)[IsDrizzleTable]).toBe((abstractTable as any)[IsDrizzleTable]); expect((concreteTable as any)[Columns]).toBe((abstractTable as any)[Columns]); @@ -188,28 +199,30 @@ describe("buildEnsDbSchema — prototype and Symbol preservation", () => { }); it("isTable() returns true for cloned concrete tables", () => { - const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, - ); + const { concreteEnsIndexerSchema } = buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME); - expect(isTable(schema.v1Domain)).toBe(true); - expect(isTable(schema.registration)).toBe(true); - expect(isTable(schema.event)).toBe(true); + expect(isTable(concreteEnsIndexerSchema.v1Domain)).toBe(true); + expect(isTable(concreteEnsIndexerSchema.registration)).toBe(true); + expect(isTable(concreteEnsIndexerSchema.event)).toBe(true); }); }); describe("buildEnsDbDrizzleClient", () => { + beforeEach(() => { + drizzleMock.mockClear(); + }); + it("calls drizzle with the correct connection config", () => { const connectionString = "postgres://user:pass@localhost:5432/ensdb"; - const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, - ); + const { concreteEnsIndexerSchema } = buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME); - buildEnsDbDrizzleClient(connectionString, schema); + buildEnsDbDrizzleClient(connectionString, concreteEnsIndexerSchema); expect(drizzle).toHaveBeenCalledWith({ connection: connectionString, - schema, + schema: expect.objectContaining({ + metadata: expect.anything(), + }), casing: "snake_case", logger: undefined, }); @@ -217,12 +230,10 @@ describe("buildEnsDbDrizzleClient", () => { it("passes the logger to drizzle when provided", () => { const connectionString = "postgres://user:pass@localhost:5432/ensdb"; - const schema = buildEnsDbSchema( - buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME).concreteEnsIndexerSchema, - ); + const { concreteEnsIndexerSchema } = buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME); const logger = { logQuery: vi.fn() }; - buildEnsDbDrizzleClient(connectionString, schema, logger); + buildEnsDbDrizzleClient(connectionString, concreteEnsIndexerSchema, logger); expect(drizzle).toHaveBeenCalledWith(expect.objectContaining({ logger })); }); diff --git a/packages/ensdb-sdk/src/lib/drizzle.ts b/packages/ensdb-sdk/src/lib/drizzle.ts index 062c0cdb3f..47886d95b1 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.ts @@ -129,7 +129,7 @@ export function buildIndividualEnsDbSchemas< * Represents the combined database schema for ENSDb, * including both the "concrete" ENSIndexer Schema and the ENSNode Schema. */ -export type EnsDbSchema = +type EnsDbSchema = ConcreteEnsIndexerSchema & EnsNodeSchema; /** @@ -138,15 +138,15 @@ export type EnsDbSchema( - ensIndexerSchema: ConcreteEnsIndexerSchema, +function buildEnsDbSchema( + concreteEnsIndexerSchema: ConcreteEnsIndexerSchema, ): EnsDbSchema { return { - ...ensIndexerSchema, + ...concreteEnsIndexerSchema, ...ensNodeSchema, }; } @@ -165,15 +165,17 @@ export type EnsDbDrizzleClient( connectionString: string, - ensDbSchema: EnsDbSchema, + concreteEnsIndexerSchema: ConcreteEnsIndexerSchema, logger?: DrizzleLogger, ): EnsDbDrizzleClient { + const ensDbSchema = buildEnsDbSchema(concreteEnsIndexerSchema); + return drizzle({ connection: connectionString, schema: ensDbSchema, From ba48daa32d5c803b119a900e19ba0036d1a35056 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sat, 21 Mar 2026 07:19:37 +0100 Subject: [PATCH 32/34] Make `EnsDbReader` constructor to build Drizzle client --- apps/ensindexer/src/lib/ensdb/singleton.ts | 22 ++---------- packages/ensdb-sdk/src/client/ensdb-reader.ts | 36 ++++++++++++------- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/apps/ensindexer/src/lib/ensdb/singleton.ts b/apps/ensindexer/src/lib/ensdb/singleton.ts index 85dd2e198c..e667024d9a 100644 --- a/apps/ensindexer/src/lib/ensdb/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb/singleton.ts @@ -1,28 +1,10 @@ import config from "@/config"; -import { - buildEnsDbDrizzleClient, - buildEnsDbSchema, - buildIndividualEnsDbSchemas, - EnsDbWriter, -} from "@ensnode/ensdb-sdk"; +import { EnsDbWriter } from "@ensnode/ensdb-sdk"; const { databaseUrl: ensDbConnectionString, databaseSchemaName: ensIndexerSchemaName } = config; -const { concreteEnsIndexerSchema, ensNodeSchema } = - buildIndividualEnsDbSchemas(ensIndexerSchemaName); -/** - * Build a ENSDb Schema for Drizzle client using the ENSIndexer Schema name from config. - */ -const ensDbSchema = buildEnsDbSchema(concreteEnsIndexerSchema); -const ensDbDrizzleClient = buildEnsDbDrizzleClient(ensDbConnectionString, ensDbSchema); - /** * Singleton instance of ENSDbWriter for the ENSIndexer application. */ -export const ensDbClient = new EnsDbWriter( - ensDbDrizzleClient, - concreteEnsIndexerSchema, - ensIndexerSchemaName, - ensNodeSchema, -); +export const ensDbClient = new EnsDbWriter(ensDbConnectionString, ensIndexerSchemaName); diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.ts b/packages/ensdb-sdk/src/client/ensdb-reader.ts index 7a8458ed4f..0eb3b18221 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.ts @@ -7,7 +7,13 @@ import { type EnsIndexerPublicConfig, } from "@ensnode/ensnode-sdk"; -import type { AbstractEnsIndexerSchema, EnsDbDrizzleClient, EnsNodeSchema } from "../lib/drizzle"; +import { + type AbstractEnsIndexerSchema, + buildEnsDbDrizzleClient, + buildIndividualEnsDbSchemas, + type EnsDbDrizzleClient, + type EnsNodeSchema, +} from "../lib/drizzle"; import { EnsNodeMetadataKeys } from "./ensnode-metadata"; import type { SerializedEnsNodeMetadata, @@ -43,7 +49,7 @@ export class EnsDbReader< * This is the "concrete" ENSIndexer Schema in which tables reference * the ENSIndexer Schema name from {@link ensIndexerSchemaName}. */ - protected _ensIndexerSchema: ConcreteEnsIndexerSchema; + protected _concreteEnsIndexerSchema: ConcreteEnsIndexerSchema; /** * The name of the ENSIndexer schema to read from in ENSDb. @@ -56,19 +62,18 @@ export class EnsDbReader< protected _ensNodeSchema: EnsNodeSchema; /** - * @param ensDbDrizzleClient Drizzle client for ENSDb, typed with the "concrete" ENSIndexer Schema type. - * @param ensDbSchema ENSDb Schema definition for ENSDb used by the Drizzle client. + * @param ensDbConnectionString The connection string for Drizzle to connect to the ENSDb instance. * @param ensIndexerSchemaName The name of the ENSIndexer schema to read from in ENSDb, used to identify which ENSNode metadata records to read. - * @param ensNodeSchema The ENSNode Schema definition for ENSDb used by the Drizzle client. */ - constructor( - ensDbDrizzleClient: EnsDbDrizzleClient, - ensIndexerSchema: ConcreteEnsIndexerSchema, - ensIndexerSchemaName: string, - ensNodeSchema: EnsNodeSchema, - ) { + constructor(ensDbConnectionString: string, ensIndexerSchemaName: string) { + const { concreteEnsIndexerSchema, ensNodeSchema } = + buildIndividualEnsDbSchemas(ensIndexerSchemaName); + const ensDbDrizzleClient = buildEnsDbDrizzleClient( + ensDbConnectionString, + concreteEnsIndexerSchema, + ); this.drizzleClient = ensDbDrizzleClient; - this._ensIndexerSchema = ensIndexerSchema; + this._concreteEnsIndexerSchema = concreteEnsIndexerSchema; this.ensIndexerSchemaName = ensIndexerSchemaName; this._ensNodeSchema = ensNodeSchema; } @@ -87,9 +92,14 @@ export class EnsDbReader< * for ENSDb instance. * * Useful while working on complex queries for ENSDb. + * + * Note: using `ensIndexerSchema` name for this getter to make it read better + * in the context of query building. For example: + * `this.client.select().from(this.ensIndexerSchema.event)` vs. + * `this.client.select().from(this.concreteEnsIndexerSchema.event)`. */ get ensIndexerSchema(): ConcreteEnsIndexerSchema { - return this._ensIndexerSchema; + return this._concreteEnsIndexerSchema; } /** From 0adcd14957e63d73c6504be951307ca1d5bab507 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sat, 21 Mar 2026 07:19:42 +0100 Subject: [PATCH 33/34] Update test files --- .../ensdb-sdk/src/client/ensdb-reader.test.ts | 30 +++++++++---------- .../ensdb-sdk/src/client/ensdb-writer.test.ts | 29 ++++++++---------- 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts index 18c4725e7f..3a488c4a86 100644 --- a/packages/ensdb-sdk/src/client/ensdb-reader.test.ts +++ b/packages/ensdb-sdk/src/client/ensdb-reader.test.ts @@ -5,27 +5,24 @@ import { serializeEnsIndexerPublicConfig, } from "@ensnode/ensnode-sdk"; -import { buildIndividualEnsDbSchemas } from "../lib/drizzle"; import * as ensDbClientMock from "./ensdb-client.mock"; import { EnsDbReader } from "./ensdb-reader"; +const whereMock = vi.fn(async () => [] as Array<{ value: unknown }>); +const fromMock = vi.fn(() => ({ where: whereMock })); +const selectMock = vi.fn(() => ({ from: fromMock })); +const drizzleClientMock = { select: selectMock } as any; + +vi.mock("drizzle-orm/node-postgres", () => ({ + drizzle: vi.fn(() => drizzleClientMock), +})); + describe("EnsDbReader", () => { const selectResult = { current: [] as Array<{ value: unknown }> }; - const whereMock = vi.fn(async () => selectResult.current); - const fromMock = vi.fn(() => ({ where: whereMock })); - const selectMock = vi.fn(() => ({ from: fromMock })); - const drizzleClientMock = { select: selectMock } as any; - const { concreteEnsIndexerSchema, ensNodeSchema } = buildIndividualEnsDbSchemas( - ensDbClientMock.ensIndexerSchemaName, - ); + whereMock.mockImplementation(async () => selectResult.current); const createEnsDbReader = () => - new EnsDbReader( - drizzleClientMock, - concreteEnsIndexerSchema, - ensDbClientMock.ensIndexerSchemaName, - ensNodeSchema, - ); + new EnsDbReader(ensDbClientMock.ensDbUrl, ensDbClientMock.ensIndexerSchemaName); beforeEach(() => { selectResult.current = []; @@ -36,7 +33,10 @@ describe("EnsDbReader", () => { describe("getEnsDbVersion", () => { it("returns undefined when no record exists", async () => { - await expect(createEnsDbReader().getEnsDbVersion()).resolves.toBeUndefined(); + const ensDbClient = createEnsDbReader(); + const { ensNodeSchema } = ensDbClient; + + await expect(ensDbClient.getEnsDbVersion()).resolves.toBeUndefined(); expect(selectMock).toHaveBeenCalledTimes(1); expect(fromMock).toHaveBeenCalledWith(ensNodeSchema.metadata); diff --git a/packages/ensdb-sdk/src/client/ensdb-writer.test.ts b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts index c89a65d461..51e7ccf754 100644 --- a/packages/ensdb-sdk/src/client/ensdb-writer.test.ts +++ b/packages/ensdb-sdk/src/client/ensdb-writer.test.ts @@ -7,29 +7,23 @@ import { serializeEnsIndexerPublicConfig, } from "@ensnode/ensnode-sdk"; -import { buildIndividualEnsDbSchemas } from "../lib/drizzle"; import * as ensDbClientMock from "./ensdb-client.mock"; import { EnsDbWriter } from "./ensdb-writer"; import { EnsNodeMetadataKeys } from "./ensnode-metadata"; +const onConflictDoUpdateMock = vi.fn(async () => undefined); +const valuesMock = vi.fn(() => ({ onConflictDoUpdate: onConflictDoUpdateMock })); +const insertMock = vi.fn(() => ({ values: valuesMock })); +const drizzleClientMock = { insert: insertMock } as any; + +vi.mock("drizzle-orm/node-postgres", () => ({ + drizzle: vi.fn(() => drizzleClientMock), +})); vi.mock("drizzle-orm/node-postgres/migrator", () => ({ migrate: vi.fn() })); describe("EnsDbWriter", () => { - const onConflictDoUpdateMock = vi.fn(async () => undefined); - const valuesMock = vi.fn(() => ({ onConflictDoUpdate: onConflictDoUpdateMock })); - const insertMock = vi.fn(() => ({ values: valuesMock })); - const drizzleClientMock = { insert: insertMock } as any; - const { concreteEnsIndexerSchema, ensNodeSchema } = buildIndividualEnsDbSchemas( - ensDbClientMock.ensIndexerSchemaName, - ); - const createEnsDbWriter = () => - new EnsDbWriter( - drizzleClientMock, - concreteEnsIndexerSchema, - ensDbClientMock.ensIndexerSchemaName, - ensNodeSchema, - ); + new EnsDbWriter(ensDbClientMock.ensDbUrl, ensDbClientMock.ensIndexerSchemaName); beforeEach(() => { onConflictDoUpdateMock.mockClear(); @@ -40,7 +34,10 @@ describe("EnsDbWriter", () => { describe("upsertEnsDbVersion", () => { it("writes the database version metadata", async () => { - await createEnsDbWriter().upsertEnsDbVersion("0.2.0"); + const ensDbClient = createEnsDbWriter(); + const { ensNodeSchema } = ensDbClient; + + await ensDbClient.upsertEnsDbVersion("0.2.0"); expect(insertMock).toHaveBeenCalledWith(ensNodeSchema.metadata); expect(valuesMock).toHaveBeenCalledWith({ From 219b7bdc47d5e0ab20bbaec906754499d6cbf210 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sat, 21 Mar 2026 07:52:47 +0100 Subject: [PATCH 34/34] fix npm audit issues --- package.json | 6 ++--- pnpm-lock.yaml | 70 +++++++++++++++++++++++++------------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index d8f2bfd90e..3ace6b00da 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,9 @@ "undici@>=7.0.0 <7.24.0": "^7.24.0", "undici@>=6.0.0 <6.24.0": "^6.24.0", "yauzl@<3.2.1": "^3.2.1", - "fast-xml-parser@>=5.0.0 <=5.5.5": ">=5.5.6", - "kysely@>=0.26.0 <0.28.12": "^0.28.12", - "h3@<1.15.6": "^1.15.6" + "fast-xml-parser@>=5.0.0 <5.5.7": ">=5.5.7", + "kysely@>=0.26.0 <0.28.14": ">=0.28.14", + "h3@<1.15.9": ">=1.15.9" }, "ignoredBuiltDependencies": [ "bun" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 636b789f1d..803d801846 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,9 +104,9 @@ overrides: undici@>=7.0.0 <7.24.0: ^7.24.0 undici@>=6.0.0 <6.24.0: ^6.24.0 yauzl@<3.2.1: ^3.2.1 - fast-xml-parser@>=5.0.0 <=5.5.5: '>=5.5.6' - kysely@>=0.26.0 <0.28.12: ^0.28.12 - h3@<1.15.6: ^1.15.6 + fast-xml-parser@>=5.0.0 <5.5.7: '>=5.5.7' + kysely@>=0.26.0 <0.28.14: '>=0.28.14' + h3@<1.15.9: '>=1.15.9' patchedDependencies: '@changesets/assemble-release-plan@6.0.9': @@ -366,7 +366,7 @@ importers: version: 1.37.0 '@ponder/client': specifier: 'catalog:' - version: 0.16.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.12)(pg@8.16.3)(typescript@5.9.3) + version: 0.16.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) '@ponder/utils': specifier: 'catalog:' version: 0.2.16(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6)) @@ -390,7 +390,7 @@ importers: version: 4.1.0 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.28.12)(pg@8.16.3) + 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.28.14)(pg@8.16.3) graphql: specifier: ^16.11.0 version: 16.11.0 @@ -478,7 +478,7 @@ importers: version: link:../../packages/ponder-sdk '@ponder/client': specifier: 'catalog:' - version: 0.16.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.12)(pg@8.16.3)(typescript@5.9.3) + version: 0.16.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) caip: specifier: 'catalog:' version: 1.1.1 @@ -493,7 +493,7 @@ importers: version: 5.6.1 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.28.12)(pg@8.16.3) + 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.28.14)(pg@8.16.3) hono: specifier: 'catalog:' version: 4.12.7 @@ -844,7 +844,7 @@ importers: version: 0.31.10 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.28.12)(pg@8.16.3) + 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.28.14)(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) @@ -1113,13 +1113,13 @@ importers: version: 2.5.1 '@ponder/client': specifier: 'catalog:' - version: 0.16.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.12)(pg@8.16.3)(typescript@5.9.3) + version: 0.16.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) dataloader: specifier: ^2.2.3 version: 2.2.3 drizzle-orm: specifier: 0.41.0 - 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.28.12)(pg@8.16.3) + 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.28.14)(pg@8.16.3) graphql: specifier: ^16.10.0 version: 16.11.0 @@ -5622,7 +5622,7 @@ packages: expo-sqlite: '>=14.0.0' gel: '>=2' knex: '*' - kysely: ^0.28.12 + kysely: '>=0.28.14' mysql2: '>=2' pg: '>=8' postgres: '>=3' @@ -5899,8 +5899,8 @@ packages: fast-xml-builder@1.1.4: resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} - fast-xml-parser@5.5.6: - resolution: {integrity: sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==} + fast-xml-parser@5.5.8: + resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} hasBin: true fastq@1.19.1: @@ -6138,8 +6138,8 @@ packages: resolution: {integrity: sha512-cvVIBILwuoSyD54U4cF/UXDh5yAobhNV/tPygI4lZhgOIJQE/WLWC4waBRb4I6bDVYb3OVx3lfHbaQOEoUD5sg==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - h3@1.15.8: - resolution: {integrity: sha512-iOH6Vl8mGd9nNfu9C0IZ+GuOAfJHcyf3VriQxWaSWIB76Fg4BnFuk4cxBxjmQSSxJS664+pgjP6e7VBnUzFfcg==} + h3@1.15.9: + resolution: {integrity: sha512-H7UPnyIupUOYUQu7f2x7ABVeMyF/IbJjqn20WSXpMdnQB260luADUkSgJU7QTWLutq8h3tUayMQ1DdbSYX5LkA==} hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} @@ -6537,8 +6537,8 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - kysely@0.28.12: - resolution: {integrity: sha512-kWiueDWXhbCchgiotwXkwdxZE/6h56IHAeFWg4euUfW0YsmO9sxbAxzx1KLLv2lox15EfuuxHQvgJ1qIfZuHGw==} + kysely@0.28.14: + resolution: {integrity: sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==} engines: {node: '>=20.0.0'} langium@4.2.1: @@ -7306,8 +7306,8 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-expression-matcher@1.1.3: - resolution: {integrity: sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==} + path-expression-matcher@1.2.0: + resolution: {integrity: sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==} engines: {node: '>=14.0.0'} path-is-inside@1.0.2: @@ -9830,7 +9830,7 @@ snapshots: '@aws-sdk/xml-builder@3.972.9': dependencies: '@smithy/types': 4.13.0 - fast-xml-parser: 5.5.6 + fast-xml-parser: 5.5.8 tslib: 2.8.1 '@aws/lambda-invoke-store@0.2.3': {} @@ -11628,9 +11628,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@ponder/client@0.16.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.12)(pg@8.16.3)(typescript@5.9.3)': + '@ponder/client@0.16.3(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3)': dependencies: - drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.12)(pg@8.16.3) + drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) eventsource: 3.0.7 superjson: 2.2.6 optionalDependencies: @@ -14556,12 +14556,12 @@ snapshots: esbuild: 0.25.11 tsx: 4.21.0 - drizzle-orm@0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.12)(pg@8.16.3): + drizzle-orm@0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3): optionalDependencies: '@electric-sql/pglite': 0.2.13 '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) '@types/pg': 8.16.0 - kysely: 0.28.12 + kysely: 0.28.14 pg: 8.16.3 dset@3.1.4: {} @@ -14842,12 +14842,12 @@ snapshots: fast-xml-builder@1.1.4: dependencies: - path-expression-matcher: 1.1.3 + path-expression-matcher: 1.2.0 - fast-xml-parser@5.5.6: + fast-xml-parser@5.5.8: dependencies: fast-xml-builder: 1.1.4 - path-expression-matcher: 1.1.3 + path-expression-matcher: 1.2.0 strnum: 2.2.0 fastq@1.19.1: @@ -15118,7 +15118,7 @@ snapshots: graphql@16.8.2: {} - h3@1.15.8: + h3@1.15.9: dependencies: cookie-es: 1.2.2 crossws: 0.3.5 @@ -15618,7 +15618,7 @@ snapshots: kolorist@1.8.0: {} - kysely@0.28.12: {} + kysely@0.28.14: {} langium@4.2.1: dependencies: @@ -16638,7 +16638,7 @@ snapshots: path-exists@4.0.0: {} - path-expression-matcher@1.1.3: {} + path-expression-matcher@1.2.0: {} path-is-inside@1.0.2: {} @@ -16820,13 +16820,13 @@ snapshots: dataloader: 2.2.3 detect-package-manager: 3.0.2 dotenv: 16.6.1 - drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.12)(pg@8.16.3) + drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) glob: 10.5.0 graphql: 16.8.2 graphql-yoga: 5.17.1(graphql@16.8.2) hono: 4.12.7 http-terminator: 3.2.0 - kysely: 0.28.12 + kysely: 0.28.14 pg: 8.16.3 pg-connection-string: 2.9.1 pg-copy-streams: 6.0.6 @@ -16902,13 +16902,13 @@ snapshots: dataloader: 2.2.3 detect-package-manager: 3.0.2 dotenv: 16.6.1 - drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.12)(pg@8.16.3) + drizzle-orm: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.28.14)(pg@8.16.3) glob: 10.5.0 graphql: 16.8.2 graphql-yoga: 5.17.1(graphql@16.8.2) hono: 4.12.7 http-terminator: 3.2.0 - kysely: 0.28.12 + kysely: 0.28.14 pg: 8.16.3 pg-connection-string: 2.9.1 pg-copy-streams: 6.0.6 @@ -18349,7 +18349,7 @@ snapshots: anymatch: 3.1.3 chokidar: 4.0.3 destr: 2.0.5 - h3: 1.15.8 + h3: 1.15.9 lru-cache: 10.4.3 node-fetch-native: 1.6.7 ofetch: 1.5.0