Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c34c0bd
feat(config): add configuration schema and environment handling for E…
djstrong Dec 22, 2025
bbe487e
chore: update dependencies and clean up imports in ENSRainbow configu…
djstrong Dec 22, 2025
6acc196
Merge branch 'main' into 1407-build-ensrainbow-config
djstrong Jan 21, 2026
22a81f0
feat(config): build and export ENSRainbowConfig from environment vari…
djstrong Jan 21, 2026
03722ac
refactor(tests): update CLI tests to use vi.stubEnv and async imports…
djstrong Jan 21, 2026
ec75cfe
fix(cli): improve port validation logic to use configured port instea…
djstrong Jan 21, 2026
887aecc
fix lint
djstrong Jan 21, 2026
bd1dd1c
refactor(cli): update CLI to use configured port and improve test imp…
djstrong Jan 23, 2026
1ddfc33
refactor(config): enhance path resolution and improve error handling …
djstrong Jan 23, 2026
3b5c7cc
Merge branch 'main' into 1407-build-ensrainbow-config
djstrong Jan 23, 2026
782e329
test(config): add comprehensive tests for buildConfigFromEnvironment …
djstrong Jan 23, 2026
2e1f9d9
feat(api): add public configuration endpoint and enhance ENSRainbowAp…
djstrong Jan 23, 2026
ddaaf29
test(config): remove redundant test for DB_SCHEMA_VERSION handling in…
djstrong Jan 23, 2026
a43b125
refactor(config): improve environment configuration validation and er…
djstrong Jan 23, 2026
c57e7f6
Create young-carrots-cheer.md
djstrong Jan 23, 2026
e3a6c90
refactor(config): remove unused imports from config schema files to s…
djstrong Jan 23, 2026
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
7 changes: 7 additions & 0 deletions .changeset/young-carrots-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"ensrainbow": patch
"@ensnode/ensnode-sdk": patch
"@ensnode/ensrainbow-sdk": patch
---

Build ENSRainbow config
3 changes: 2 additions & 1 deletion apps/ensrainbow/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"protobufjs": "^7.4.0",
"viem": "catalog:",
"yargs": "^17.7.2",
"@fast-csv/parse": "^5.0.0"
"@fast-csv/parse": "^5.0.0",
"zod": "catalog:"
},
"devDependencies": {
"@ensnode/shared-configs": "workspace:*",
Expand Down
78 changes: 51 additions & 27 deletions apps/ensrainbow/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join } from "node:path";

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

import { DEFAULT_PORT, getEnvPort } from "@/lib/env";
import { ENSRAINBOW_DEFAULT_PORT } from "@/config/defaults";

import { createCLI, validatePortConfiguration } from "./cli";

Expand Down Expand Up @@ -38,42 +38,60 @@ describe("CLI", () => {
});

describe("getEnvPort", () => {
it("should return DEFAULT_PORT when PORT is not set", () => {
expect(getEnvPort()).toBe(DEFAULT_PORT);
it("should return ENSRAINBOW_DEFAULT_PORT when PORT is not set", async () => {
vi.resetModules();
const { getEnvPort: getEnvPortFresh } = await import("@/lib/env");
expect(getEnvPortFresh()).toBe(ENSRAINBOW_DEFAULT_PORT);
});

it("should return port from environment variable", () => {
it("should return port from environment variable", async () => {
const customPort = 4000;
process.env.PORT = customPort.toString();
expect(getEnvPort()).toBe(customPort);
vi.stubEnv("PORT", customPort.toString());
vi.resetModules();
const { getEnvPort: getEnvPortFresh } = await import("@/lib/env");
expect(getEnvPortFresh()).toBe(customPort);
});

it("should throw error for invalid port number", () => {
process.env.PORT = "invalid";
expect(() => getEnvPort()).toThrow(
'Invalid PORT value "invalid": must be a non-negative integer',
);
it("should throw error for invalid port number", async () => {
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {
throw new Error("process.exit called");
}) as never);
vi.stubEnv("PORT", "invalid");
vi.resetModules();
await expect(import("@/lib/env")).rejects.toThrow("process.exit called");
expect(exitSpy).toHaveBeenCalledWith(1);
});

it("should throw error for negative port number", () => {
process.env.PORT = "-1";
expect(() => getEnvPort()).toThrow('Invalid PORT value "-1": must be a non-negative integer');
it("should throw error for negative port number", async () => {
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {
throw new Error("process.exit called");
}) as never);
vi.stubEnv("PORT", "-1");
vi.resetModules();
await expect(import("@/lib/env")).rejects.toThrow("process.exit called");
expect(exitSpy).toHaveBeenCalledWith(1);
});
});

describe("validatePortConfiguration", () => {
it("should not throw when PORT env var is not set", () => {
expect(() => validatePortConfiguration(3000)).not.toThrow();
it("should not throw when PORT env var is not set", async () => {
vi.resetModules();
const { validatePortConfiguration: validatePortConfigurationFresh } = await import("./cli");
expect(() => validatePortConfigurationFresh(3000)).not.toThrow();
});

it("should not throw when PORT matches CLI port", () => {
process.env.PORT = "3000";
expect(() => validatePortConfiguration(3000)).not.toThrow();
it("should not throw when PORT matches CLI port", async () => {
vi.stubEnv("PORT", "3000");
vi.resetModules();
const { validatePortConfiguration: validatePortConfigurationFresh } = await import("./cli");
expect(() => validatePortConfigurationFresh(3000)).not.toThrow();
});

it("should throw when PORT conflicts with CLI port", () => {
process.env.PORT = "3000";
expect(() => validatePortConfiguration(4000)).toThrow("Port conflict");
it("should throw when PORT conflicts with CLI port", async () => {
vi.stubEnv("PORT", "3000");
vi.resetModules();
const { validatePortConfiguration: validatePortConfigurationFresh } = await import("./cli");
expect(() => validatePortConfigurationFresh(4000)).toThrow("Port conflict");
});
});

Expand Down Expand Up @@ -526,11 +544,14 @@ describe("CLI", () => {

it("should respect PORT environment variable", async () => {
const customPort = 5115;
process.env.PORT = customPort.toString();
vi.stubEnv("PORT", customPort.toString());
vi.resetModules();
const { createCLI: createCLIFresh } = await import("./cli");
const cliWithCustomPort = createCLIFresh({ exitProcess: false });

// First ingest some test data
const ensrainbowOutputFile = join(TEST_FIXTURES_DIR, "test_ens_names_0.ensrainbow");
await cli.parse([
await cliWithCustomPort.parse([
"ingest-ensrainbow",
"--input-file",
ensrainbowOutputFile,
Expand All @@ -539,7 +560,7 @@ describe("CLI", () => {
]);

// Start server
const serverPromise = cli.parse(["serve", "--data-dir", testDataDir]);
const serverPromise = cliWithCustomPort.parse(["serve", "--data-dir", testDataDir]);

// Give server time to start
await new Promise((resolve) => setTimeout(resolve, 100));
Expand Down Expand Up @@ -587,9 +608,12 @@ describe("CLI", () => {
});

it("should throw on port conflict", async () => {
process.env.PORT = "5000";
vi.stubEnv("PORT", "5000");
vi.resetModules();
const { createCLI: createCLIFresh } = await import("./cli");
const cliWithPort = createCLIFresh({ exitProcess: false });
await expect(
cli.parse(["serve", "--port", "4000", "--data-dir", testDataDir]),
cliWithPort.parse(["serve", "--port", "4000", "--data-dir", testDataDir]),
).rejects.toThrow("Port conflict");
});
});
Expand Down
22 changes: 12 additions & 10 deletions apps/ensrainbow/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import config, { getDefaultDataDir } from "@/config";

import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

Expand All @@ -14,13 +16,13 @@ import { ingestProtobufCommand } from "@/commands/ingest-protobuf-command";
import { purgeCommand } from "@/commands/purge-command";
import { serverCommand } from "@/commands/server-command";
import { validateCommand } from "@/commands/validate-command";
import { getDefaultDataSubDir, getEnvPort } from "@/lib/env";

export function validatePortConfiguration(cliPort: number): void {
const envPort = process.env.PORT;
if (envPort !== undefined && cliPort !== getEnvPort()) {
// Only validate if PORT was explicitly set in the environment
// If PORT is not set, CLI port can override the default
if (process.env.PORT !== undefined && cliPort !== config.port) {
throw new Error(
`Port conflict: Command line argument (${cliPort}) differs from PORT environment variable (${envPort}). ` +
`Port conflict: Command line argument (${cliPort}) differs from configured port (${config.port}). ` +
`Please use only one method to specify the port.`,
);
}
Expand Down Expand Up @@ -89,7 +91,7 @@ export function createCLI(options: CLIOptions = {}) {
// .option("data-dir", {
// type: "string",
// description: "Directory to store LevelDB data",
// default: getDefaultDataSubDir(),
// default: getDefaultDataDir(),
// });
// },
// async (argv: ArgumentsCamelCase<IngestArgs>) => {
Expand All @@ -112,7 +114,7 @@ export function createCLI(options: CLIOptions = {}) {
.option("data-dir", {
type: "string",
description: "Directory to store LevelDB data",
default: getDefaultDataSubDir(),
default: getDefaultDataDir(),
});
},
async (argv: ArgumentsCamelCase<IngestProtobufArgs>) => {
Expand All @@ -130,12 +132,12 @@ export function createCLI(options: CLIOptions = {}) {
.option("port", {
type: "number",
description: "Port to listen on",
default: getEnvPort(),
default: config.port,
})
.option("data-dir", {
type: "string",
description: "Directory containing LevelDB data",
default: getDefaultDataSubDir(),
default: getDefaultDataDir(),
});
},
async (argv: ArgumentsCamelCase<ServeArgs>) => {
Expand All @@ -154,7 +156,7 @@ export function createCLI(options: CLIOptions = {}) {
.option("data-dir", {
type: "string",
description: "Directory containing LevelDB data",
default: getDefaultDataSubDir(),
default: getDefaultDataDir(),
})
.option("lite", {
type: "boolean",
Expand All @@ -177,7 +179,7 @@ export function createCLI(options: CLIOptions = {}) {
return yargs.option("data-dir", {
type: "string",
description: "Directory containing LevelDB data",
default: getDefaultDataSubDir(),
default: getDefaultDataDir(),
});
},
async (argv: ArgumentsCamelCase<PurgeArgs>) => {
Expand Down
Loading
Loading