diff --git a/.changeset/chatty-monkeys-run.md b/.changeset/chatty-monkeys-run.md new file mode 100644 index 0000000000..5a0ed3e88c --- /dev/null +++ b/.changeset/chatty-monkeys-run.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensdb-sdk": minor +--- + +Hotfixed the `buildConcreteEnsIndexerSchema` function by replacing the cloning approach with working mutation approach. diff --git a/packages/ensdb-sdk/src/ensindexer-abstract/ensnode-metadata.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/ensnode-metadata.schema.ts deleted file mode 100644 index bac75fd626..0000000000 --- a/packages/ensdb-sdk/src/ensindexer-abstract/ensnode-metadata.schema.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Schema Definitions that hold metadata about the ENSNode instance. - */ - -import { onchainTable } from "ponder"; - -/** - * ENSNode Metadata - * - * Possible key value pairs are defined by 'EnsNodeMetadata' type: - * - `EnsNodeMetadataEnsDbVersion` - * - `EnsNodeMetadataEnsIndexerPublicConfig` - * - `EnsNodeMetadataEnsIndexerIndexingStatus` - */ -export const ensNodeMetadata = onchainTable("ensnode_metadata", (t) => ({ - /** - * Key - * - * Allowed keys: - * - `EnsNodeMetadataEnsDbVersion['key']` - * - `EnsNodeMetadataEnsIndexerPublicConfig['key']` - * - `EnsNodeMetadataEnsIndexerIndexingStatus['key']` - */ - key: t.text().primaryKey(), - - /** - * Value - * - * Allowed values: - * - `EnsNodeMetadataEnsDbVersion['value']` - * - `EnsNodeMetadataEnsIndexerPublicConfig['value']` - * - `EnsNodeMetadataEnsIndexerIndexingStatus['value']` - * - * Guaranteed to be a serialized representation of JSON object. - */ - value: t.jsonb().notNull(), -})); diff --git a/packages/ensdb-sdk/src/ensindexer-abstract/index.ts b/packages/ensdb-sdk/src/ensindexer-abstract/index.ts index 438cbccf97..be985db946 100644 --- a/packages/ensdb-sdk/src/ensindexer-abstract/index.ts +++ b/packages/ensdb-sdk/src/ensindexer-abstract/index.ts @@ -4,9 +4,6 @@ * 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 -// for ENSNode Schema are executable. -export * from "./ensnode-metadata.schema"; export * from "./ensv2.schema"; export * from "./protocol-acceleration.schema"; export * from "./registrars.schema"; diff --git a/packages/ensdb-sdk/src/lib/drizzle.test.ts b/packages/ensdb-sdk/src/lib/drizzle.test.ts index eecd357693..9ee6316e31 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.test.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.test.ts @@ -98,45 +98,12 @@ describe("buildIndividualEnsDbSchemas", () => { } }); - it("applies a different schema name to ENSIndexer objects", () => { - const otherSchemaName = "ensindexer_other"; - const { concreteEnsIndexerSchema } = buildIndividualEnsDbSchemas(otherSchemaName); + it("throws an error when called a second time with a different schema name", () => { + buildIndividualEnsDbSchemas(ENSINDEXER_SCHEMA_NAME); - for (const [key, abstractValue] of Object.entries(abstractEnsIndexerSchema)) { - if (!isTable(abstractValue)) continue; - const concreteValue = concreteEnsIndexerSchema[key as keyof typeof concreteEnsIndexerSchema]; - expect(isTable(concreteValue)).toBe(true); - expect(getSchemaName(concreteValue)).toBe(otherSchemaName); - } - }); - - it("builds two concrete schemas with respective names, leaving abstract unaffected", () => { - const schemaNameA = "ensindexer_alpha"; - const schemaNameB = "ensindexer_beta"; - - 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]; - 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(); - } - } + expect(() => buildIndividualEnsDbSchemas("ensindexer_other")).toThrow( + /buildConcreteEnsIndexerSchema was already called with schema/, + ); }); }); @@ -165,14 +132,6 @@ describe("combined schema (via buildEnsDbDrizzleClient)", () => { } } }); - - it("ensures ensnode metadata schema is consistent across multiple concrete schemas", () => { - const schemaA = getCombinedSchema("ensindexer_alpha"); - const schemaB = getCombinedSchema("ensindexer_beta"); - - expect(getSchemaName(schemaA.metadata)).toBe("ensnode"); - expect(getSchemaName(schemaB.metadata)).toBe("ensnode"); - }); }); describe("concrete tables — prototype and Symbol preservation", () => { diff --git a/packages/ensdb-sdk/src/lib/drizzle.ts b/packages/ensdb-sdk/src/lib/drizzle.ts index 47886d95b1..0519558467 100644 --- a/packages/ensdb-sdk/src/lib/drizzle.ts +++ b/packages/ensdb-sdk/src/lib/drizzle.ts @@ -21,35 +21,16 @@ import * as ensNodeSchema from "../ensnode"; */ export type AbstractEnsIndexerSchema = typeof abstractEnsIndexerSchema; +// TODO: remove the `appliedNameForConcreteEnsIndexerSchema` variable and +// related logic when the `buildConcreteEnsIndexerSchema` function is +// refactored to avoid mutating the "abstract" ENSIndexer Schema definition. /** - * Clone a Drizzle Table object with a new schema name. + * Applied name for the "concrete" ENSIndexer Schema. * - * 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. + * This is needed to prevent multiple calls to `buildConcreteEnsIndexerSchema` with different schema names, + * which would mutate the same "abstract" ENSIndexer Schema and cause schema corruption. */ -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; -} +let appliedNameForConcreteEnsIndexerSchema: string | undefined; /** * Build a "concrete" ENSIndexer Schema definition for ENSDb. @@ -66,34 +47,39 @@ function cloneTableWithSchema( function buildConcreteEnsIndexerSchema( ensIndexerSchemaName: string, ): ConcreteEnsIndexerSchema { - const ensIndexerSchema = {} as ConcreteEnsIndexerSchema; - - for (const [key, abstractSchemaObject] of Object.entries(abstractEnsIndexerSchema)) { - if (isTable(abstractSchemaObject)) { - (ensIndexerSchema as any)[key] = cloneTableWithSchema( - abstractSchemaObject, - ensIndexerSchemaName, - ); - } else if (isPgEnum(abstractSchemaObject)) { - // Enums are functions; clone by copying properties onto a new function. - // Unlike tables, enums don't rely on prototype identity, so - // Object.assign is sufficient here. - 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; + // TODO: Refactor this function to avoid mutating the "abstract" ENSIndexer Schema definition. + // https://github.com/namehash/ensnode/issues/1830 + + if ( + appliedNameForConcreteEnsIndexerSchema !== undefined && + appliedNameForConcreteEnsIndexerSchema !== ensIndexerSchemaName + ) { + throw new Error( + `buildConcreteEnsIndexerSchema was already called with schema "${appliedNameForConcreteEnsIndexerSchema}". ` + + `Calling it again with "${ensIndexerSchemaName}" would corrupt the previously built schema.`, + ); + } + appliedNameForConcreteEnsIndexerSchema = ensIndexerSchemaName; + + const concreteEnsIndexerSchema = abstractEnsIndexerSchema as ConcreteEnsIndexerSchema; + + for (const dbObject of Object.values(abstractEnsIndexerSchema)) { + if (isTable(dbObject)) { + // Update Drizzle table definition to reference + // the specific `ensIndexerSchemaName` name of the ENSIndexer Schema. + // @ts-expect-error - Drizzle types don't define `Table.Symbol.Schema` type, + // but it's present at runtime. + dbObject[Table.Symbol.Schema] = ensIndexerSchemaName; + } else if (isPgEnum(dbObject)) { + // Update Drizzle enum definition to reference + // the specific `ensIndexerSchemaName` name of the ENSIndexer Schema. + // @ts-expect-error - Drizzle types consider `schema` to be + // a readonly property. + dbObject.schema = ensIndexerSchemaName; } } - return ensIndexerSchema; + return concreteEnsIndexerSchema; } /** diff --git a/packages/integration-test-env/src/orchestrator.ts b/packages/integration-test-env/src/orchestrator.ts index e46ac7f3b4..29bd5a9ada 100644 --- a/packages/integration-test-env/src/orchestrator.ts +++ b/packages/integration-test-env/src/orchestrator.ts @@ -189,35 +189,41 @@ async function pollIndexingStatus( ensIndexerSchemaName: string, timeoutMs: number, ): Promise { - const client = new (await import("@ensnode/ensdb-sdk")).EnsDbReader( - ensDbUrl, - ensIndexerSchemaName, - ); + const { EnsDbReader } = await import("@ensnode/ensdb-sdk"); + const ensDbClient = new EnsDbReader(ensDbUrl, ensIndexerSchemaName); const start = Date.now(); log("Polling indexing status..."); - while (Date.now() - start < timeoutMs) { - checkAborted(); - try { - const snapshot = await client.getIndexingStatusSnapshot(); - if (snapshot !== undefined) { - const omnichainStatus = snapshot.omnichainSnapshot.omnichainStatus; - log(`Omnichain status: ${omnichainStatus}`); - if ( - omnichainStatus === OmnichainIndexingStatusIds.Following || - omnichainStatus === OmnichainIndexingStatusIds.Completed - ) { - log("Indexing reached target status"); - return; + try { + while (Date.now() - start < timeoutMs) { + checkAborted(); + try { + const snapshot = await ensDbClient.getIndexingStatusSnapshot(); + if (snapshot !== undefined) { + const omnichainStatus = snapshot.omnichainSnapshot.omnichainStatus; + log(`Omnichain status: ${omnichainStatus}`); + if ( + omnichainStatus === OmnichainIndexingStatusIds.Following || + omnichainStatus === OmnichainIndexingStatusIds.Completed + ) { + log("Indexing reached target status"); + return; + } } + } catch { + // indexer may not be ready yet } - } catch { - // indexer may not be ready yet + await new Promise((r) => setTimeout(r, 3000)); } - await new Promise((r) => setTimeout(r, 3000)); + throw new Error(`Indexing did not complete within ${timeoutMs / 1000}s`); + } finally { + console.log("Closing ENSDb client..."); + // @ts-expect-error - DrizzleClient.$client is not typed to have an `end` method, + // but in practice it does (e.g. pg's Client does). + await ensDbClient.ensDb.$client.end(); + console.log("ENSDb client closed"); } - throw new Error(`Indexing did not complete within ${timeoutMs / 1000}s`); } function logVersions() { @@ -312,7 +318,7 @@ async function main() { // Phase 3: Start ENSIndexer const ENSINDEXER_URL = `http://localhost:${ENSINDEXER_PORT}`; - const ENSINDEXER_SCHEMA_NAME = "public"; + const ENSINDEXER_SCHEMA_NAME = "ensindexer_0"; log("Starting ENSIndexer..."); spawnService(