Skip to content

Commit c336c79

Browse files
authored
Extend ensdb-sdk with EnsDbConfig data model (#1868)
1 parent 065ecbe commit c336c79

24 files changed

Lines changed: 291 additions & 301 deletions

File tree

.changeset/fluffy-forks-film.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ensnode/ensdb-sdk": minor
3+
---
4+
5+
Added `validateEnsDbConfig` function to support validation for the `EnsDbConfig` data model.

apps/ensapi/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
"hono": "catalog:",
5959
"p-memoize": "^8.0.0",
6060
"p-retry": "catalog:",
61-
"pg-connection-string": "catalog:",
6261
"pino": "catalog:",
6362
"ponder-enrich-gql-docs-middleware": "^0.1.3",
6463
"superjson": "^2.2.6",

apps/ensapi/src/config/config.schema.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ vi.mock("@/lib/ensdb/singleton", () => ({
1111
},
1212
}));
1313

14+
vi.mock("@/config/ensdb-config", () => ({
15+
default: {
16+
ensDbUrl: "postgresql://user:password@localhost:5432/mydb",
17+
ensIndexerSchemaName: "ensindexer_0",
18+
},
19+
}));
20+
1421
import { buildConfigFromEnvironment, buildEnsApiPublicConfig } from "@/config/config.schema";
1522
import { ENSApi_DEFAULT_PORT } from "@/config/defaults";
1623
import type { EnsApiEnvironment } from "@/config/environment";
@@ -97,9 +104,7 @@ describe("buildConfigFromEnvironment", () => {
97104
mockExit.mockClear();
98105
});
99106

100-
const TEST_ENV: EnsApiEnvironment = {
101-
ENSDB_URL: BASE_ENV.ENSDB_URL,
102-
};
107+
const TEST_ENV: EnsApiEnvironment = structuredClone(BASE_ENV);
103108

104109
it("logs error and exits when CUSTOM_REFERRAL_PROGRAM_EDITIONS is not a valid URL", async () => {
105110
await buildConfigFromEnvironment({

apps/ensapi/src/config/config.schema.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
} from "@ensnode/ensnode-sdk/internal";
1515

1616
import { ENSApi_DEFAULT_PORT } from "@/config/defaults";
17-
import { EnsDbConfigSchema } from "@/config/ensdb-config.schema";
17+
import ensDbConfig from "@/config/ensdb-config";
1818
import type { EnsApiEnvironment } from "@/config/environment";
1919
import { invariant_ensIndexerPublicConfigVersionInfo } from "@/config/validations";
2020
import { ensDbClient } from "@/lib/ensdb/singleton";
@@ -47,8 +47,11 @@ const EnsApiConfigSchema = z
4747
rpcConfigs: RpcConfigsSchema,
4848
ensIndexerPublicConfig: makeENSIndexerPublicConfigSchema("ensIndexerPublicConfig"),
4949
customReferralProgramEditionConfigSetUrl: CustomReferralProgramEditionConfigSetUrlSchema,
50+
51+
// include the ENSDbConfig params in the EnsApiConfigSchema
52+
ensDbUrl: z.string(),
53+
ensIndexerSchemaName: z.string(),
5054
})
51-
.extend(EnsDbConfigSchema.shape)
5255
.check(invariant_rpcConfigsSpecifiedForRootChain)
5356
.check(invariant_ensIndexerPublicConfigVersionInfo);
5457

@@ -89,13 +92,15 @@ export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promis
8992

9093
return EnsApiConfigSchema.parse({
9194
port: env.PORT,
92-
ensDbUrl: env.ENSDB_URL,
9395
theGraphApiKey: env.THEGRAPH_API_KEY,
9496
ensIndexerPublicConfig,
9597
namespace: ensIndexerPublicConfig.namespace,
96-
ensIndexerSchemaName: ensIndexerPublicConfig.ensIndexerSchemaName,
9798
rpcConfigs,
9899
customReferralProgramEditionConfigSetUrl: env.CUSTOM_REFERRAL_PROGRAM_EDITIONS,
100+
101+
// include the validated ENSDb config values in the parsed EnsApiConfig
102+
ensDbUrl: ensDbConfig.ensDbUrl,
103+
ensIndexerSchemaName: ensDbConfig.ensIndexerSchemaName,
99104
});
100105
} catch (error) {
101106
if (error instanceof ZodError) {

apps/ensapi/src/config/ensdb-config.schema.ts

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1-
import { buildEnsDbConfigFromEnvironment } from "./ensdb-config.schema";
1+
import { type EnsDbConfig, validateEnsDbConfig } from "@ensnode/ensdb-sdk";
2+
import type { Unvalidated } from "@ensnode/ensnode-sdk";
23

3-
export default buildEnsDbConfigFromEnvironment(process.env);
4+
import { lazyProxy } from "@/lib/lazy";
5+
import logger from "@/lib/logger";
6+
7+
/**
8+
* Build ENSDb config from environment variables for ENSApi app.
9+
*
10+
* Exits the process if the configuration is invalid, logging the error details.
11+
*/
12+
export function buildEnsDbConfigFromEnvironment(env: NodeJS.ProcessEnv): EnsDbConfig {
13+
const unvalidatedConfig = {
14+
ensDbUrl: env.ENSDB_URL,
15+
ensIndexerSchemaName: env.ENSINDEXER_SCHEMA_NAME,
16+
} satisfies Unvalidated<EnsDbConfig>;
17+
18+
try {
19+
return validateEnsDbConfig(unvalidatedConfig);
20+
} catch (error) {
21+
const errorMessage = error instanceof Error ? error.message : String(error);
22+
logger.error(`Failed to validate ENSDb config from environment: ${errorMessage}`);
23+
process.exit(1);
24+
}
25+
}
26+
27+
// lazyProxy defers construction until first use so that this module can be
28+
// imported without env vars being present (e.g. during OpenAPI generation).
29+
const ensDbConfig = lazyProxy<EnsDbConfig>(() => buildEnsDbConfigFromEnvironment(process.env));
30+
31+
export default ensDbConfig;

apps/ensapi/src/lib/ensdb/singleton.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { EnsDbReader } from "@ensnode/ensdb-sdk";
22

3-
import { buildEnsDbConfigFromEnvironment } from "@/config/ensdb-config.schema";
3+
import { buildEnsDbConfigFromEnvironment } from "@/config/ensdb-config";
44
import { lazyProxy } from "@/lib/lazy";
55

66
// lazyProxy defers construction until first use so that this module can be

apps/ensindexer/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
"deepmerge-ts": "^7.1.5",
3535
"dns-packet": "^5.6.1",
3636
"drizzle-orm": "catalog:",
37-
"pg-connection-string": "catalog:",
3837
"p-retry": "catalog:",
3938
"hono": "catalog:",
4039
"ponder": "catalog:",

apps/ensindexer/src/config/config.schema.ts

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import { parse as parseConnectionString } from "pg-connection-string";
21
import { prettifyError, ZodError, z } from "zod/v4";
32

43
import { buildBlockNumberRange, PluginName, uniq } from "@ensnode/ensnode-sdk";
54
import {
65
buildRpcConfigsFromEnv,
76
ENSNamespaceSchema,
8-
EnsIndexerSchemaNameSchema,
97
invariant_isSubgraphCompatibleRequirements,
108
invariant_rpcConfigsSpecifiedForRootChain,
119
makeFullyPinnedLabelSetSchema,
@@ -14,6 +12,7 @@ import {
1412
} from "@ensnode/ensnode-sdk/internal";
1513

1614
import { DEFAULT_SUBGRAPH_COMPAT } from "@/config/defaults";
15+
import ensDbConfig from "@/config/ensdb-config";
1716
import type { ENSIndexerEnvironment } from "@/config/environment";
1817
import { applyDefaults, EnvironmentDefaults } from "@/config/environment-defaults";
1918

@@ -27,24 +26,6 @@ import {
2726
invariant_validContractConfigs,
2827
} from "./validations";
2928

30-
export const EnsDbUrlSchema = z.string().refine(
31-
(url) => {
32-
try {
33-
if (!url.startsWith("postgresql://") && !url.startsWith("postgres://")) {
34-
return false;
35-
}
36-
const config = parseConnectionString(url);
37-
return !!(config.host && config.port && config.database);
38-
} catch {
39-
return false;
40-
}
41-
},
42-
{
43-
error:
44-
"Invalid PostgreSQL connection string for ENSDb. Expected format: postgresql://username:password@host:port/database",
45-
},
46-
);
47-
4829
// parses an env string bool with strict requirement of 'true' or 'false'
4930
const makeEnvStringBoolSchema = (envVarKey: string) =>
5031
z
@@ -106,8 +87,6 @@ const IsSubgraphCompatibleSchema =
10687

10788
const ENSIndexerConfigSchema = z
10889
.object({
109-
ensDbUrl: EnsDbUrlSchema,
110-
ensIndexerSchemaName: EnsIndexerSchemaNameSchema,
11190
rpcConfigs: RpcConfigsSchema,
11291

11392
namespace: ENSNamespaceSchema,
@@ -116,6 +95,10 @@ const ENSIndexerConfigSchema = z
11695
globalBlockrange: BlockrangeSchema,
11796
ensRainbowUrl: EnsRainbowUrlSchema,
11897
labelSet: LabelSetSchema,
98+
99+
// include the ENSDbConfig params in the ENSIndexerConfigSchema
100+
ensDbUrl: z.string(),
101+
ensIndexerSchemaName: z.string(),
119102
})
120103
/**
121104
* Derived configuration
@@ -184,8 +167,6 @@ export function buildConfigFromEnvironment(_env: ENSIndexerEnvironment): EnsInde
184167

185168
// parse/validate with ENSIndexerConfigSchema
186169
return ENSIndexerConfigSchema.parse({
187-
ensDbUrl: env.ENSDB_URL,
188-
ensIndexerSchemaName: env.ENSINDEXER_SCHEMA_NAME,
189170
namespace: env.NAMESPACE,
190171
rpcConfigs,
191172

@@ -200,6 +181,10 @@ export function buildConfigFromEnvironment(_env: ENSIndexerEnvironment): EnsInde
200181
labelSetId: env.LABEL_SET_ID,
201182
labelSetVersion: env.LABEL_SET_VERSION,
202183
},
184+
185+
// include the validated ENSDb config values in the parsed EnsIndexerConfig
186+
ensDbUrl: ensDbConfig.ensDbUrl,
187+
ensIndexerSchemaName: ensDbConfig.ensIndexerSchemaName,
203188
});
204189
} catch (error) {
205190
if (error instanceof ZodError) {

apps/ensindexer/src/config/config.test.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22

3+
// For config.test.ts, we need a mock that validates env vars without providing defaults,
4+
// because this test file specifically tests validation failures.
5+
vi.mock("@/config/ensdb-config", async () => {
6+
const { validateEnsDbConfig } =
7+
await vi.importActual<typeof import("@ensnode/ensdb-sdk")>("@ensnode/ensdb-sdk");
8+
return {
9+
default: {
10+
get ensDbUrl() {
11+
const url = process.env.ENSDB_URL;
12+
const schema = process.env.ENSINDEXER_SCHEMA_NAME;
13+
validateEnsDbConfig({ ensDbUrl: url, ensIndexerSchemaName: schema });
14+
return url;
15+
},
16+
get ensIndexerSchemaName() {
17+
const url = process.env.ENSDB_URL;
18+
const schema = process.env.ENSINDEXER_SCHEMA_NAME;
19+
validateEnsDbConfig({ ensDbUrl: url, ensIndexerSchemaName: schema });
20+
return schema;
21+
},
22+
},
23+
};
24+
});
25+
326
import {
427
type ENSNamespaceId,
528
ensTestEnvChain,
@@ -170,21 +193,17 @@ describe("config (with base env)", () => {
170193

171194
it("throws an error when ENSINDEXER_SCHEMA_NAME is not set", async () => {
172195
vi.stubEnv("ENSINDEXER_SCHEMA_NAME", undefined);
173-
await expect(getConfig()).rejects.toThrow(/ENSINDEXER_SCHEMA_NAME is required/);
196+
await expect(getConfig()).rejects.toThrow(/ENSIndexer Schema Name is required/);
174197
});
175198

176199
it("throws an error when ENSINDEXER_SCHEMA_NAME is empty", async () => {
177200
vi.stubEnv("ENSINDEXER_SCHEMA_NAME", "");
178-
await expect(getConfig()).rejects.toThrow(
179-
/ENSINDEXER_SCHEMA_NAME is required and cannot be an empty string/,
180-
);
201+
await expect(getConfig()).rejects.toThrow(/ENSIndexer Schema Name cannot be an empty string/);
181202
});
182203

183204
it("throws an error when ENSINDEXER_SCHEMA_NAME is only whitespace", async () => {
184205
vi.stubEnv("ENSINDEXER_SCHEMA_NAME", " ");
185-
await expect(getConfig()).rejects.toThrow(
186-
/ENSINDEXER_SCHEMA_NAME is required and cannot be an empty string/,
187-
);
206+
await expect(getConfig()).rejects.toThrow(/ENSIndexer Schema Name cannot be an empty string/);
188207
});
189208
});
190209

0 commit comments

Comments
 (0)