Skip to content
Closed
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/dry-trams-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensapi": minor
---

Updated response data model for `/api/config` route to include `ensDbPublicConfig` field.
5 changes: 5 additions & 0 deletions .changeset/heavy-laws-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensnode-sdk": minor
---

Introduced the `EnsDbPublicConfig` data model.
5 changes: 5 additions & 0 deletions .changeset/social-dryers-find.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensdb-sdk": minor
---

Added `getEnsDbPublicConfig` method to `EnsDbReader` class.
17 changes: 16 additions & 1 deletion apps/ensapi/src/config/config.schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import packageJson from "@/../package.json" with { type: "json" };

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { type ENSIndexerPublicConfig, PluginName } from "@ensnode/ensnode-sdk";
import {
type ENSIndexerPublicConfig,
type EnsDbPublicConfig,
PluginName,
} from "@ensnode/ensnode-sdk";
import type { RpcConfig } from "@ensnode/ensnode-sdk/internal";

vi.mock("@/lib/ensdb/singleton", () => ({
ensDbClient: {
getEnsDbPublicConfig: vi.fn(async () => ENSDB_PUBLIC_CONFIG),
getEnsIndexerPublicConfig: vi.fn(async () => ENSINDEXER_PUBLIC_CONFIG),
},
}));
Expand All @@ -30,6 +35,11 @@ const BASE_ENV = {
RPC_URL_1: VALID_RPC_URL,
} satisfies EnsApiEnvironment;

const ENSDB_PUBLIC_CONFIG = {
postgresVersion: "17.4",
rootSchemaVersion: "1.0.0",
} satisfies EnsDbPublicConfig;

const ENSINDEXER_PUBLIC_CONFIG = {
namespace: "mainnet",
databaseSchemaName: "ensapi",
Expand Down Expand Up @@ -58,6 +68,7 @@ describe("buildConfigFromEnvironment", () => {
databaseUrl: BASE_ENV.DATABASE_URL,
theGraphApiKey: undefined,

ensDbPublicConfig: ENSDB_PUBLIC_CONFIG,
ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG,
namespace: ENSINDEXER_PUBLIC_CONFIG.namespace,
ensIndexerSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName,
Expand Down Expand Up @@ -150,6 +161,7 @@ describe("buildEnsApiPublicConfig", () => {
const mockConfig = {
port: ENSApi_DEFAULT_PORT,
databaseUrl: BASE_ENV.DATABASE_URL,
ensDbPublicConfig: ENSDB_PUBLIC_CONFIG,
ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG,
namespace: ENSINDEXER_PUBLIC_CONFIG.namespace,
ensIndexerSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName,
Expand All @@ -173,6 +185,7 @@ describe("buildEnsApiPublicConfig", () => {
canFallback: false,
reason: "not-subgraph-compatible",
},
ensDbPublicConfig: ENSDB_PUBLIC_CONFIG,
ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG,
});
});
Expand All @@ -181,6 +194,7 @@ describe("buildEnsApiPublicConfig", () => {
const mockConfig = {
port: ENSApi_DEFAULT_PORT,
databaseUrl: BASE_ENV.DATABASE_URL,
ensDbPublicConfig: ENSDB_PUBLIC_CONFIG,
ensIndexerPublicConfig: ENSINDEXER_PUBLIC_CONFIG,
namespace: ENSINDEXER_PUBLIC_CONFIG.namespace,
ensIndexerSchemaName: ENSINDEXER_PUBLIC_CONFIG.databaseSchemaName,
Expand Down Expand Up @@ -210,6 +224,7 @@ describe("buildEnsApiPublicConfig", () => {
const mockConfig = {
port: ENSApi_DEFAULT_PORT,
databaseUrl: BASE_ENV.DATABASE_URL,
ensDbPublicConfig: ENSDB_PUBLIC_CONFIG,
ensIndexerPublicConfig: {
...ENSINDEXER_PUBLIC_CONFIG,
plugins: ["subgraph"],
Expand Down
9 changes: 9 additions & 0 deletions apps/ensapi/src/config/config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
makeENSIndexerPublicConfigSchema,
OptionalPortNumberSchema,
RpcConfigsSchema,
schemaEnsDbPublicConfig,
TheGraphApiKeySchema,
} from "@ensnode/ensnode-sdk/internal";

Expand Down Expand Up @@ -47,6 +48,7 @@ const EnsApiConfigSchema = z
namespace: ENSNamespaceSchema,
rpcConfigs: RpcConfigsSchema,
ensIndexerPublicConfig: makeENSIndexerPublicConfigSchema("ensIndexerPublicConfig"),
ensDbPublicConfig: schemaEnsDbPublicConfig,
customReferralProgramEditionConfigSetUrl: CustomReferralProgramEditionConfigSetUrlSchema,
})
.extend(EnsDbConfigSchema.shape)
Expand Down Expand Up @@ -86,12 +88,18 @@ export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promis
},
);

// TODO: transfer the responsibility of fetching
// the ENSDb Public Config to a middleware layer as well,
// similar to the ENSIndexer Public Config, as per:
// https://github.com/namehash/ensnode/issues/1806
const ensDbPublicConfig = await ensDbClient.getEnsDbPublicConfig();
const rpcConfigs = buildRpcConfigsFromEnv(env, ensIndexerPublicConfig.namespace);

return EnsApiConfigSchema.parse({
port: env.PORT,
databaseUrl: env.DATABASE_URL,
theGraphApiKey: env.THEGRAPH_API_KEY,
ensDbPublicConfig,
ensIndexerPublicConfig,
namespace: ensIndexerPublicConfig.namespace,
ensIndexerSchemaName: ensIndexerPublicConfig.databaseSchemaName,
Expand Down Expand Up @@ -128,6 +136,7 @@ export function buildEnsApiPublicConfig(config: EnsApiConfig): EnsApiPublicConfi
theGraphApiKey: config.theGraphApiKey ? "<API_KEY>" : undefined,
isSubgraphCompatible: config.ensIndexerPublicConfig.isSubgraphCompatible,
}),
ensDbPublicConfig: config.ensDbPublicConfig,
ensIndexerPublicConfig: config.ensIndexerPublicConfig,
};
}
15 changes: 14 additions & 1 deletion docs/docs.ensnode.io/ensapi-openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@
}
]
},
"ensDbPublicConfig": {
"type": "object",
"properties": {
"postgresVersion": { "type": "string", "minLength": 1 },
"rootSchemaVersion": { "type": "string", "minLength": 1 }
},
"required": ["postgresVersion", "rootSchemaVersion"]
},
"ensIndexerPublicConfig": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -146,7 +154,12 @@
]
}
},
"required": ["version", "theGraphFallback", "ensIndexerPublicConfig"]
"required": [
"version",
"theGraphFallback",
"ensDbPublicConfig",
"ensIndexerPublicConfig"
]
}
}
}
Expand Down
68 changes: 67 additions & 1 deletion packages/ensdb-sdk/src/client/ensdb-reader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import {
serializeEnsIndexerPublicConfig,
} from "@ensnode/ensnode-sdk";

import { ENSDB_ROOT_SCHEMA_VERSION } from "../config";
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;
const executeMock = vi.fn(async () => ({
rows: [] as Array<{ setting_server_version: string | number }>,
}));
const drizzleClientMock = { select: selectMock, execute: executeMock } as any;

vi.mock("drizzle-orm/node-postgres", () => ({
drizzle: vi.fn(() => drizzleClientMock),
Expand Down Expand Up @@ -85,4 +89,66 @@ describe("EnsDbReader", () => {
);
});
});

describe("getEnsDbPublicConfig", () => {
beforeEach(() => {
executeMock.mockClear();
});

it("returns public config with postgres version and root schema version", async () => {
executeMock.mockImplementation(async () => ({
rows: [{ setting_server_version: "17.4 (Debian 17.4-1.pgdg120+2)" }],
}));

const result = await createEnsDbReader().getEnsDbPublicConfig();

expect(result).toStrictEqual({
postgresVersion: "17.4",
rootSchemaVersion: ENSDB_ROOT_SCHEMA_VERSION,
});
expect(executeMock).toHaveBeenCalledTimes(1);
});

it("throws when server version setting is not a string", async () => {
executeMock.mockImplementation(async () => ({
rows: [{ setting_server_version: 17.4 }],
}));

await expect(createEnsDbReader().getEnsDbPublicConfig()).rejects.toThrowError(
"Unexpected type for server_version setting: number",
);
});

it("throws when postgres version is empty", async () => {
executeMock.mockImplementation(async () => ({
rows: [{ setting_server_version: "" }],
}));

await expect(createEnsDbReader().getEnsDbPublicConfig()).rejects.toThrowError(
"PostgreSQL version must be a non-empty string.",
);
});
});

describe("getters", () => {
it("returns the drizzle client", () => {
const reader = createEnsDbReader();
expect(reader.ensDb).toBe(drizzleClientMock);
});

it("returns the ensIndexerSchema", () => {
const reader = createEnsDbReader();
expect(reader.ensIndexerSchema).toBeDefined();
});

it("returns the ensIndexerSchemaName", () => {
const reader = createEnsDbReader();
expect(reader.ensIndexerSchemaName).toBe(ensDbClientMock.ensIndexerSchemaName);
});

it("returns the ensNodeSchema", () => {
const reader = createEnsDbReader();
expect(reader.ensNodeSchema).toBeDefined();
});
});
});
43 changes: 42 additions & 1 deletion packages/ensdb-sdk/src/client/ensdb-reader.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { and, eq } from "drizzle-orm/sql";
import { and, eq, sql } from "drizzle-orm/sql";

import {
type CrossChainIndexingStatusSnapshot,
deserializeCrossChainIndexingStatusSnapshot,
deserializeEnsIndexerPublicConfig,
type EnsDbPublicConfig,
type EnsIndexerPublicConfig,
validateEnsDbPublicConfig,
} from "@ensnode/ensnode-sdk";

import { ENSDB_ROOT_SCHEMA_VERSION } from "../config";
import {
type AbstractEnsIndexerSchema,
buildEnsDbDrizzleClient,
Expand Down Expand Up @@ -126,6 +129,19 @@ export class EnsDbReader<
return this._ensNodeSchema;
}

/**
* Get ENSDb Public Config
*/
async getEnsDbPublicConfig(): Promise<EnsDbPublicConfig> {
const postgresVersion = await this.getPgVersion();
const rootSchemaVersion = ENSDB_ROOT_SCHEMA_VERSION;
Comment on lines +136 to +137
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

getEnsDbPublicConfig() sets rootSchemaVersion from the SDK constant ENSDB_ROOT_SCHEMA_VERSION, which means the reported value reflects the client library version, not necessarily the connected ENSDb instance’s schema version. If ENSApi and ENSIndexer (or migrations) are out of sync, this can silently misreport the DB’s actual root schema version.

Consider sourcing rootSchemaVersion from the DB (e.g., the existing ensnode.metadata ensdb_version via getEnsDbVersion() or a new dedicated metadata key), and optionally comparing it against ENSDB_ROOT_SCHEMA_VERSION to detect incompatibilities.

Suggested change
const postgresVersion = await this.getPgVersion();
const rootSchemaVersion = ENSDB_ROOT_SCHEMA_VERSION;
const [postgresVersion, dbRootSchemaVersion] = await Promise.all([
this.getPgVersion(),
this.getEnsDbVersion(),
]);
const rootSchemaVersion = dbRootSchemaVersion ?? ENSDB_ROOT_SCHEMA_VERSION;

Copilot uses AI. Check for mistakes.

return validateEnsDbPublicConfig({
postgresVersion,
rootSchemaVersion,
});
}

/**
* Get ENSDb Version
*
Expand Down Expand Up @@ -208,4 +224,29 @@ export class EnsDbReader<
`There must be exactly one ENSNodeMetadata record for ('${this.ensIndexerSchemaName}', '${metadata.key}') composite key`,
);
}

/**
* Get PostgreSQL version for ENSDb instance.
*/
private async getPgVersion(): Promise<string> {
const queryResult = await this.ensDb.execute(
sql`SELECT current_setting('server_version') as setting_server_version;`,
);
const serverVersionSetting = queryResult.rows[0]?.setting_server_version;

if (typeof serverVersionSetting !== "string") {
throw new Error(`Unexpected type for server_version setting: ${typeof serverVersionSetting}`);
}

// Extract version number from full version string,
// which is typically in the format "17.4 (Debian 17.4-1.pgdg120+2)".
// We just want the "17.4" part for the PostgreSQL version.
const [pgVersion = ""] = serverVersionSetting.split(" ");

if (pgVersion.length === 0) {
throw new Error(`PostgreSQL version must be a non-empty string.`);
}

return pgVersion;
}
}
10 changes: 10 additions & 0 deletions packages/ensdb-sdk/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Version of the Root Schema definition for ENSDb.
*
* The Root Schema definition is a union of all
* the individual schema definitions in ENSDb:
* - The "abstract" ENSIndexer Schema definition
* - ENSNode Schema definition
* - (future) other schemas related to ENSDb
*/
export const ENSDB_ROOT_SCHEMA_VERSION = "1.0.0";
1 change: 1 addition & 0 deletions packages/ensdb-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./client";
export * from "./config";
12 changes: 6 additions & 6 deletions packages/ensdb-sdk/src/lib/drizzle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,16 @@ type EnsDbSchema<ConcreteEnsIndexerSchema extends AbstractEnsIndexerSchema> =
ConcreteEnsIndexerSchema & EnsNodeSchema;

/**
* Build ENSDb Schema for Drizzle client
* Build ENSDb Root Schema for Drizzle client
*
* Uses the provided "concrete" ENSIndexer Schema definition to build
* the ENSDb Schema.
* the ENSDb Root Schema.
*
* @param concreteEnsIndexerSchema - The "concrete" ENSIndexer Schema definition.
* @returns The ENSDb Schema definition for use in building
* @returns The ENSDb Root Schema definition for use in building
* a Drizzle client for ENSDb.
*/
function buildEnsDbSchema<ConcreteEnsIndexerSchema extends AbstractEnsIndexerSchema>(
function buildEnsDbRootSchema<ConcreteEnsIndexerSchema extends AbstractEnsIndexerSchema>(
concreteEnsIndexerSchema: ConcreteEnsIndexerSchema,
): EnsDbSchema<ConcreteEnsIndexerSchema> {
return {
Expand Down Expand Up @@ -160,11 +160,11 @@ export function buildEnsDbDrizzleClient<ConcreteEnsIndexerSchema extends Abstrac
concreteEnsIndexerSchema: ConcreteEnsIndexerSchema,
logger?: DrizzleLogger,
): EnsDbDrizzleClient<ConcreteEnsIndexerSchema> {
const ensDbSchema = buildEnsDbSchema<ConcreteEnsIndexerSchema>(concreteEnsIndexerSchema);
const ensDbRootSchema = buildEnsDbRootSchema<ConcreteEnsIndexerSchema>(concreteEnsIndexerSchema);

return drizzle({
connection: connectionString,
schema: ensDbSchema,
schema: ensDbRootSchema,
casing: "snake_case",
logger,
});
Expand Down
4 changes: 4 additions & 0 deletions packages/ensnode-sdk/src/ensapi/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ const EXAMPLE_CONFIG_RESPONSE = {
canFallback: false,
reason: "no-api-key",
},
ensDbPublicConfig: {
postgresVersion: "17.4",
rootSchemaVersion: "1.0.0",
},
ensIndexerPublicConfig: {
ensRainbowPublicConfig: {
version: "0.31.0",
Expand Down
8 changes: 8 additions & 0 deletions packages/ensnode-sdk/src/ensapi/config/conversions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const MOCK_ENSAPI_PUBLIC_CONFIG = {
canFallback: false,
reason: "no-api-key",
},
ensDbPublicConfig: {
postgresVersion: "17.4",
rootSchemaVersion: "1.0.0",
},
ensIndexerPublicConfig: {
namespace: ENSNamespaceIds.Mainnet,
databaseSchemaName: "ensapi",
Expand Down Expand Up @@ -49,6 +53,10 @@ describe("ENSApi Config Serialization/Deserialization", () => {
canFallback: false,
reason: "no-api-key",
},
ensDbPublicConfig: {
postgresVersion: "17.4",
rootSchemaVersion: "1.0.0",
},
ensIndexerPublicConfig: {
namespace: ENSNamespaceIds.Mainnet,
databaseSchemaName: "ensapi",
Expand Down
Loading
Loading