Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/moody-aliens-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensindexer": minor
---

Made `ponder.schema.ts` to explicitly import just ENSIndexer Schema.
Comment on lines +1 to +5
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Minor grammar suggestion in changeset description.

The description is technically correct but could read more naturally:

✏️ Suggested improvement
-Made `ponder.schema.ts` to explicitly import just ENSIndexer Schema.
+Updated `ponder.schema.ts` to explicitly import only the ENSIndexer Schema.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
---
"ensindexer": minor
---
Made `ponder.schema.ts` to explicitly import just ENSIndexer Schema.
---
"ensindexer": minor
---
Updated `ponder.schema.ts` to explicitly import only the ENSIndexer Schema.
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 5-5: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/moody-aliens-begin.md around lines 1 - 5, The changeset
description is grammatically awkward; update the text to read naturally (e.g.,
"Made ponder.schema.ts explicitly import only the ENSIndexer schema." or
"Updated ponder.schema.ts to explicitly import the ENSIndexer schema.") by
editing the .changeset/moody-aliens-begin.md entry so it uses clear phrasing
referencing ponder.schema.ts and ENSIndexer schema.

5 changes: 5 additions & 0 deletions .changeset/strict-shrimps-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensdb-sdk": minor
---

Created isolated database schema definitions: ENSIndexer Schema and ENSNode Schema.
2 changes: 1 addition & 1 deletion apps/ensindexer/ponder/ponder.schema.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// export database schema definition for ENSIndexer
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have this /ponder directory or ponder.schema.ts file at all anymore?

Is there a special reason why whatever is importing this ponder.schema.ts file currently couldn't just import the ensindexer schema instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a requirement from Ponder app:

Each table must be a named export from the ponder.schema.ts file. The build step ignores tables that are not exported.

Exports from the ponder.schema.ts file are later used to derive type system for Ponder app.

export * from "@ensnode/ensdb-sdk";
export * from "@ensnode/ensdb-sdk/ensindexer";
34 changes: 28 additions & 6 deletions packages/ensdb-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,56 @@
"ENSDb"
],
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./ensindexer": "./src/ensindexer/index.ts",
"./ensnode": "./src/ensnode/index.ts"
},
"files": [
"dist"
],
"publishConfig": {
"access": "public",
"exports": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./ensindexer": {
"import": {
"types": "./dist/ensindexer/index.d.ts",
"default": "./dist/ensindexer/index.js"
}
},
"./ensnode": {
"import": {
"types": "./dist/ensnode/index.d.ts",
"default": "./dist/ensnode/index.js"
}
}
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"scripts": {
"prepublish": "tsup",
"lint": "biome check --write .",
"lint:ci": "biome ci"
},
"dependencies": {
"peerDependencies": {
"drizzle-orm": "catalog:",
"ponder": "catalog:",
"viem": "catalog:"
},
"devDependencies": {
"@ensnode/ensnode-sdk": "workspace:",
"@ensnode/shared-configs": "workspace:*",
"drizzle-orm": "catalog:",
"ponder": "catalog:",
"tsup": "catalog:",
"typescript": "catalog:"
"typescript": "catalog:",
"viem": "catalog:"
}
}
12 changes: 12 additions & 0 deletions packages/ensdb-sdk/src/ensindexer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Merge the various sub-schemas into ENSIndexer Schema.
*/

// TODO: remove `ensnode-metadata.schema` export when database migrations
// for ENSNode Schema are executable.
Comment on lines +5 to +6
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Track this TODO with an issue to avoid stale compatibility exports.

Linking the removal condition to an issue/milestone will make cleanup auditable.

Would you like me to draft an issue template with explicit removal criteria?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ensdb-sdk/src/ensindexer/index.ts` around lines 5 - 6, The TODO
about removing the `ensnode-metadata.schema` export should be tracked with a
formal issue: create an issue titled e.g. "Remove ensnode-metadata.schema export
after ENSNode schema migrations" that includes clear removal criteria (migration
runnable, tests green, migration PR merged), milestone and acceptance steps,
then update the TODO comment next to the `ensnode-metadata.schema` export in
`packages/ensdb-sdk/src/ensindexer/index.ts` to reference the created issue
number/URL and milestone so the cleanup is auditable and discoverable.

export * from "./ensnode-metadata.schema";
export * from "./ensv2.schema";
export * from "./protocol-acceleration.schema";
export * from "./registrars.schema";
export * from "./subgraph.schema";
export * from "./tokenscope.schema";
69 changes: 69 additions & 0 deletions packages/ensdb-sdk/src/ensnode/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { pgSchema, primaryKey } from "drizzle-orm/pg-core";

/**
* ENSNode Schema Name
*
* The name of the ENSNode schema in an ENSDb.
*/
const ENSNODE_SCHEMA_NAME = "ensnode";

/**
* ENSNode Schema
*
* Defines database objects within the ENSNode Schema in ENSDb.
*/
const ENSNODE_SCHEMA = pgSchema(ENSNODE_SCHEMA_NAME);

/**
* ENSNode Metadata
*
* Possible key value pairs are defined by 'EnsNodeMetadata' type:
* - `EnsNodeMetadataEnsDbVersion`
* - `EnsNodeMetadataEnsIndexerPublicConfig`
* - `EnsNodeMetadataEnsIndexerIndexingStatus`
*/
export const metadata = ENSNODE_SCHEMA.table(
"metadata",
(t) => ({
/**
* ENSIndexer Schema Name
*
* References the name of the ENSIndexer Schema that the metadata record
* belongs to. This allows multi-tenancy where multiple ENSIndexer
* instances can write to the same ENSNode Metadata table.
*/
ensIndexerSchemaName: t.text().notNull(),

/**
* Key
*
* Allowed keys:
* - `EnsNodeMetadataEnsDbVersion['key']`
* - `EnsNodeMetadataEnsIndexerPublicConfig['key']`
* - `EnsNodeMetadataEnsIndexerIndexingStatus['key']`
*/
key: t.text().notNull(),

/**
* Value
*
* Allowed values:
* - `EnsNodeMetadataEnsDbVersion['value']`
* - `EnsNodeMetadataEnsIndexerPublicConfig['value']`
* - `EnsNodeMetadataEnsIndexerIndexingStatus['value']`
*
* Guaranteed to be a serialized representation of JSON object.
*/
value: t.jsonb().notNull(),
}),
(table) => [
/**
* Primary key constraint on 'ensIndexerSchemaName' and 'key' columns,
* to ensure that there is only one record for each key per ENSIndexer instance.
*/
primaryKey({
name: "metadata_pkey",
columns: [table.ensIndexerSchemaName, table.key],
}),
Comment on lines +35 to +67
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify ENSNode metadata write paths include ensIndexerSchemaName and composite conflict targets.

# 1) Find imports/usages of ensdb-sdk ensnode schema.
rg -nP --type=ts -C3 '(`@ensnode/ensdb-sdk/ensnode`|ensdb-sdk/src/ensnode)'

# 2) Inspect metadata insert callsites and written fields.
rg -nP --type=ts -C6 '\.insert\(\s*(metadata|ensNodeMetadata)\s*\)'
rg -nP --type=ts -C8 '\.values\(\s*\{'

# 3) Inspect upsert conflict targets for composite key usage.
rg -nP --type=ts -C8 'onConflictDoUpdate\('
rg -nP --type=ts -C8 'target:\s*\[[^\]]*ensIndexerSchemaName[^\]]*key'

Repository: namehash/ensnode

Length of output: 50373


🏁 Script executed:

# Get more context around the metadata insert in ensdb-client.ts
sed -n '150,230p' apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts

Repository: namehash/ensnode

Length of output: 1777


🏁 Script executed:

# Check imports and confirm what ensNodeMetadata is
head -50 apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts

Repository: namehash/ensnode

Length of output: 1633


🏁 Script executed:

# Search more specifically for ensNodeMetadata inserts/upserts to ensure we found all paths
rg -n "ensNodeMetadata" apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts

Repository: namehash/ensnode

Length of output: 307


🏁 Script executed:

# Find all calls to upsertEnsNodeMetadata
rg -n "upsertEnsNodeMetadata" --type ts

Repository: namehash/ensnode

Length of output: 443


🏁 Script executed:

# Check the method signatures in EnsDbClient to understand the class structure
sed -n '80,160p' apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts

Repository: namehash/ensnode

Length of output: 2307


🏁 Script executed:

# Get the constructor and class setup of EnsDbClient
sed -n '60,95p' apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts

Repository: namehash/ensnode

Length of output: 902


🏁 Script executed:

# Look for any ensIndexerSchemaName usage in this file
rg -n "ensIndexerSchemaName" apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts

Repository: namehash/ensnode

Length of output: 42


🏁 Script executed:

# Check the schema definition to understand what ensIndexerSchemaName is
sed -n '1,100p' packages/ensdb-sdk/src/ensnode/index.ts

Repository: namehash/ensnode

Length of output: 1889


🏁 Script executed:

# Look for where EnsDbClient is instantiated to see if ensIndexerSchemaName is passed
rg -n "new EnsDbClient\|EnsDbClient(" apps/ensindexer --type ts -A 3

Repository: namehash/ensnode

Length of output: 168


🏁 Script executed:

# Fix the regex - search for EnsDbClient constructor calls
rg -n "new EnsDbClient" apps/ensindexer --type ts -A 3

Repository: namehash/ensnode

Length of output: 3598


🏁 Script executed:

# Also check how databaseSchemaName is passed
rg -n "databaseSchemaName" apps/ensindexer --type ts -B 2 -A 2

Repository: namehash/ensnode

Length of output: 13812


🏁 Script executed:

# Check if databaseSchemaName is stored in the EnsDbClient instance
sed -n '45,70p' apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts

Repository: namehash/ensnode

Length of output: 754


🏁 Script executed:

# Look for any private fields in EnsDbClient
sed -n '45,210p' apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts | head -60

Repository: namehash/ensnode

Length of output: 1690


🏁 Script executed:

# Check the entire EnsDbClient class to see all private/public fields
cat apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts

Repository: namehash/ensnode

Length of output: 5837


Both metadata readers and writers must include ensIndexerSchemaName in all operations.

The EnsDbClient constructor receives databaseSchemaName but never stores it. This causes two critical issues:

  1. Metadata writes are incomplete (upsertEnsNodeMetadata, lines 189-195):

    • .values() omits ensIndexerSchemaName — the write will fail with the composite key schema
    • onConflictDoUpdate.target only specifies key instead of the composite [ensIndexerSchemaName, key]
  2. Metadata reads have data isolation bugs (getEnsNodeMetadata, lines 157-158):

    • Query filters only by key, returning records from any ENSIndexer instance with that key
    • Should filter by both ensIndexerSchemaName and key to isolate per-instance data

Store databaseSchemaName as a private field in the constructor, then pass it to .values({ ensIndexerSchemaName, key, value }) and update the conflict target and WHERE clauses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ensdb-sdk/src/ensnode/index.ts` around lines 35 - 67, The
constructor for EnsDbClient must store the incoming databaseSchemaName as a
private field (e.g., this.ensIndexerSchemaName) so metadata operations can scope
to the instance; update the constructor to assign the passed databaseSchemaName
to that private member, then modify upsertEnsNodeMetadata to include
ensIndexerSchemaName in .values({ ensIndexerSchemaName, key, value }) and set
onConflictDoUpdate.target to the composite [table.ensIndexerSchemaName,
table.key]; similarly update getEnsNodeMetadata to add a WHERE filter for both
ensIndexerSchemaName and key (not just key) so reads are isolated per ENSIndexer
instance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix that in another PR that will overhaul EnsDbClient implementation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tk-o, understood! Since this will be addressed in the upcoming EnsDbClient overhaul PR, would you like me to open a GitHub issue to track these specific fixes?

The items to track would be:

  1. Store databaseSchemaName as a private field in the EnsDbClient constructor.
  2. Include ensIndexerSchemaName in .values() and set the composite [ensIndexerSchemaName, key] conflict target in upsertEnsNodeMetadata.
  3. Add a WHERE filter on both ensIndexerSchemaName and key in getEnsNodeMetadata to isolate per-instance data.

🧠 Learnings used
Learnt from: tk-o
Repo: namehash/ensnode PR: 1639
File: packages/ensnode-sdk/src/ensapi/api/indexing-status/zod-schemas.ts:21-76
Timestamp: 2026-02-16T17:53:46.139Z
Learning: In the ENSNode SDK (`packages/ensnode-sdk`), schema builder functions exported from `zod-schemas.ts` files (e.g., `makeEnsApiIndexingStatusResponseSchema`) are considered internal API, not public API. These can have breaking changes without requiring deprecated aliases, even when exported via the `internal` entry point.

Learnt from: tk-o
Repo: namehash/ensnode PR: 1671
File: packages/ensnode-sdk/src/ensapi/api/indexing-status/zod-schemas.ts:64-68
Timestamp: 2026-03-02T17:40:32.679Z
Learning: In `packages/ensnode-sdk/src/ensapi/api/indexing-status/zod-schemas.ts`, the serialized Ok response schema (`makeSerializedEnsApiIndexingStatusResponseOkSchema`) intentionally uses `z.object` instead of `z.strictObject` to allow extra fields, while non-serialized schemas use `z.strictObject`. This design choice is deliberate for forward compatibility.

Learnt from: Goader
Repo: namehash/ensnode PR: 1663
File: packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts:74-96
Timestamp: 2026-02-24T15:53:06.633Z
Learning: In TypeScript code reviews, prefer placing invariants on type aliases only when the invariant is context-independent or reused across multiple fields. If an invariant depends on surrounding rules or object semantics (e.g., field-specific metrics), keep the invariant as a field JSDoc instead. This guideline applies to TS files broadly (e.g., the repo's v1/award-models and similar modules).

Learnt from: CR
Repo: namehash/ensnode PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-16T19:21:37.794Z
Learning: Applies to {apps,packages}/**/src/**/*.{ts,tsx} : Use Drizzle as the ORM for database interactions

Learnt from: tk-o
Repo: namehash/ensnode PR: 1617
File: packages/ensnode-sdk/src/ensindexer/indexing-status/validate/chain-indexing-status-snapshot.ts:9-12
Timestamp: 2026-02-09T10:19:29.575Z
Learning: In ensnode-sdk validation functions (e.g., `validateChainIndexingStatusSnapshot` in `packages/ensnode-sdk/src/ensindexer/indexing-status/validate/chain-indexing-status-snapshot.ts`), the pattern of using `ChainIndexingStatusSnapshot | unknown` (even though it collapses to `unknown` in TypeScript) is intentionally kept for semantic clarity and documentation purposes.

Learnt from: tk-o
Repo: namehash/ensnode PR: 1715
File: apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts:21-25
Timestamp: 2026-03-04T11:40:35.846Z
Learning: In `apps/ensindexer/ponder/src/api/handlers/ensnode-api.ts`, the guard `if (typeof publicConfig === "undefined") { throw new Error("Unreachable: ...") }` in the `/config` handler is intentionally unreachable. The `EnsDbWriterWorker` populates the ENSIndexer public config into ENSDb at startup (fail-fast), so the HTTP layer is guaranteed to only serve requests after the config is available. The `throw` is a defensive invariant, not a real error path.

],
);
11 changes: 1 addition & 10 deletions packages/ensdb-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1 @@
/**
* Merge the various sub-schemas into a single ponder (drizzle) schema.
*/

export * from "./schemas/ensnode-metadata.schema";
export * from "./schemas/ensv2.schema";
export * from "./schemas/protocol-acceleration.schema";
export * from "./schemas/registrars.schema";
export * from "./schemas/subgraph.schema";
export * from "./schemas/tokenscope.schema";
export * from "./ensindexer";
5 changes: 4 additions & 1 deletion packages/ensdb-sdk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"extends": "@ensnode/shared-configs/tsconfig.ponder.json",
"extends": "@ensnode/shared-configs/tsconfig.lib.json",
"compilerOptions": {
"rootDir": "." // necessary for 'The project root is ambiguous'
},
"include": ["./**/*.ts"],
"exclude": ["node_modules"]
}
3 changes: 2 additions & 1 deletion packages/ensdb-sdk/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts"],
entry: ["src/index.ts", "src/ensindexer/index.ts", "src/ensnode/index.ts"],
platform: "neutral",
format: ["esm"],
target: "es2022",
bundle: true,
splitting: false,
sourcemap: true,
dts: true,
clean: true,
external: ["viem", "ponder", "drizzle-orm", "drizzle-orm/pg-core"],
outDir: "./dist",
Expand Down
16 changes: 9 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading