Skip to content

feat(ensrainbow): refine handling of env vars #367

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
50 changes: 34 additions & 16 deletions apps/ensrainbow/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { tmpdir } from "os";
import { join } from "path";
import { mkdtemp, rm } from "fs/promises";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { DEFAULT_PORT, getEnvPort } from "@/lib/env";
import { createCLI, validatePortConfiguration } from "./cli";
import { createCLI } from "./cli";
import { DEFAULT_PORT } from "./utils/config";
import { getPort, validatePortConfiguration } from "./utils/env-utils";
import * as envUtils from "./utils/env-utils";

// Path to test fixtures
const TEST_FIXTURES_DIR = join(__dirname, "..", "test", "fixtures");
Expand All @@ -27,6 +28,15 @@ describe("CLI", () => {

// Create CLI instance with process.exit disabled
cli = createCLI({ exitProcess: false });

// Mock getInputFile
vi.mock("./utils/env-utils", async () => {
const actual = await vi.importActual("./utils/env-utils");
return {
...actual,
getInputFile: vi.fn().mockImplementation((actual as any).getInputFile),
};
});
});

afterEach(async () => {
Expand All @@ -47,25 +57,27 @@ describe("CLI", () => {

describe("getEnvPort", () => {
it("should return DEFAULT_PORT when PORT is not set", () => {
expect(getEnvPort()).toBe(DEFAULT_PORT);
expect(getPort()).toBe(DEFAULT_PORT);
});

it("should return port from environment variable", () => {
const customPort = 4000;
process.env.PORT = customPort.toString();
expect(getEnvPort()).toBe(customPort);
expect(getPort()).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',
expect(() => getPort()).toThrow(
'Environment variable error: (PORT): "invalid" is not a valid number',
);
});

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');
expect(() => getPort()).toThrow(
'Environment variable error: (PORT): "-1" is not a non-negative integer',
);
});
});

Expand Down Expand Up @@ -118,17 +130,23 @@ describe("CLI", () => {
// Verify database was created by trying to validate it
await expect(cli.parse(["validate", "--data-dir", testDataDir])).resolves.not.toThrow();
});
});

describe("ingest command with environment-specific data", () => {
it("should successfully ingest environment-specific test data", async () => {
// Use ens-test-env test data for specialized testing
const customInputFile = join(TEST_FIXTURES_DIR, "ens_test_env_names.sql.gz");
it("should execute ingest command with default input file path", async () => {
// Mock getInputFile to return a test file path
const originalGetInputFile = envUtils.getInputFile;
vi.mocked(envUtils.getInputFile).mockReturnValue(
join(TEST_FIXTURES_DIR, "test_ens_names.sql.gz"),
);

await cli.parse(["ingest", "--input-file", customInputFile, "--data-dir", testDataDir]);
try {
await cli.parse(["ingest", "--data-dir", testDataDir]);

// Verify database was created and can be validated
await expect(cli.parse(["validate", "--data-dir", testDataDir])).resolves.not.toThrow();
// Verify database was created by trying to validate it
await expect(cli.parse(["validate", "--data-dir", testDataDir])).resolves.not.toThrow();
} finally {
// Restore the original function
vi.mocked(envUtils.getInputFile).mockImplementation(originalGetInputFile);
}
});
});

Expand Down
87 changes: 44 additions & 43 deletions apps/ensrainbow/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,31 @@ import { fileURLToPath } from "url";
import type { ArgumentsCamelCase, Argv } from "yargs";
import { hideBin } from "yargs/helpers";
import yargs from "yargs/yargs";

import { ingestCommand } from "@/commands/ingest-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()) {
throw new Error(
`Port conflict: Command line argument (${cliPort}) differs from PORT environment variable (${envPort}). ` +
`Please use only one method to specify the port.`,
);
}
}
import { ingestCommand } from "./commands/ingest-command";
import { purgeCommand } from "./commands/purge-command";
import { serverCommand } from "./commands/server-command";
import { validateCommand } from "./commands/validate-command";
import { resolveDirPath, resolveFilePath, resolvePort } from "./utils/command-utils";
import { DEFAULT_PORT } from "./utils/config";
import { getDataDir, getInputFile, validatePortConfiguration } from "./utils/env-utils";

interface IngestArgs {
"input-file": string;
"data-dir": string;
"input-file": string | undefined;
"data-dir": string | undefined;
}

interface ServeArgs {
port: number;
"data-dir": string;
port: number | undefined;
"data-dir": string | undefined;
}

interface ValidateArgs {
"data-dir": string;
"data-dir": string | undefined;
lite: boolean;
}

interface PurgeArgs {
"data-dir": string;
"data-dir": string | undefined;
}

export interface CLIOptions {
Expand All @@ -57,19 +48,22 @@ export function createCLI(options: CLIOptions = {}) {
return yargs
.option("input-file", {
type: "string",
description: "Path to the gzipped SQL dump file",
default: join(process.cwd(), "ens_names.sql.gz"),
description:
"Path to the gzipped SQL dump file (default: from INPUT_FILE env var or config)",
})
.option("data-dir", {
type: "string",
description: "Directory to store LevelDB data",
default: getDefaultDataSubDir(),
description:
"Directory to store LevelDB data (default: from DATA_DIR env var or config)",
});
},
async (argv: ArgumentsCamelCase<IngestArgs>) => {
const inputFile = resolveFilePath(argv["input-file"], "INPUT_FILE", getInputFile());
const dataDir = resolveDirPath(argv["data-dir"], "DATA_DIR", getDataDir(), true);

await ingestCommand({
inputFile: argv["input-file"],
dataDir: argv["data-dir"],
inputFile,
dataDir,
});
},
)
Expand All @@ -80,20 +74,26 @@ export function createCLI(options: CLIOptions = {}) {
return yargs
.option("port", {
type: "number",
description: "Port to listen on",
default: getEnvPort(),
description: "Port to listen on (default: from PORT env var or config)",
})
.option("data-dir", {
type: "string",
description: "Directory containing LevelDB data",
default: getDefaultDataSubDir(),
description:
"Directory containing LevelDB data (default: from DATA_DIR env var or config)",
});
},
async (argv: ArgumentsCamelCase<ServeArgs>) => {
validatePortConfiguration(argv.port);
// validate port configuration if CLI argument is provided
if (argv.port !== undefined) {
validatePortConfiguration(argv.port);
}

const port = resolvePort(argv.port, "PORT", DEFAULT_PORT);
const dataDir = resolveDirPath(argv["data-dir"], "DATA_DIR", getDataDir());

await serverCommand({
port: argv.port,
dataDir: argv["data-dir"],
port,
dataDir,
});
},
)
Expand All @@ -104,8 +104,8 @@ export function createCLI(options: CLIOptions = {}) {
return yargs
.option("data-dir", {
type: "string",
description: "Directory containing LevelDB data",
default: getDefaultDataSubDir(),
description:
"Directory containing LevelDB data (default: from DATA_DIR env var or config)",
})
.option("lite", {
type: "boolean",
Expand All @@ -115,8 +115,10 @@ export function createCLI(options: CLIOptions = {}) {
});
},
async (argv: ArgumentsCamelCase<ValidateArgs>) => {
const dataDir = resolveDirPath(argv["data-dir"], "DATA_DIR", getDataDir());

await validateCommand({
dataDir: argv["data-dir"],
dataDir,
lite: argv.lite,
});
},
Expand All @@ -127,14 +129,13 @@ export function createCLI(options: CLIOptions = {}) {
(yargs: Argv) => {
return yargs.option("data-dir", {
type: "string",
description: "Directory containing LevelDB data",
default: getDefaultDataSubDir(),
description:
"Directory containing LevelDB data (default: from DATA_DIR env var or config)",
});
},
async (argv: ArgumentsCamelCase<PurgeArgs>) => {
await purgeCommand({
dataDir: argv["data-dir"],
});
const dataDir = resolveDirPath(argv["data-dir"], "DATA_DIR", getDataDir(), true);
await purgeCommand({ dataDir });
},
)
.demandCommand(1, "You must specify a command")
Expand Down
13 changes: 10 additions & 3 deletions apps/ensrainbow/src/commands/purge-command.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { existsSync } from "fs";
import { rm } from "fs/promises";

import { logger } from "@/utils/logger";
Expand All @@ -9,10 +10,16 @@ export interface PurgeCommandOptions {
export async function purgeCommand(options: PurgeCommandOptions): Promise<void> {
const { dataDir } = options;

const dirExists = existsSync(dataDir);

try {
logger.info(`Removing database directory at ${dataDir}...`);
await rm(dataDir, { recursive: true, force: true });
logger.info("Database directory removed successfully.");
if (dirExists) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice one 👍

logger.info(`Removing database directory at ${dataDir}...`);
await rm(dataDir, { recursive: true, force: true });
logger.info("Database directory removed successfully.");
} else {
logger.info(`Directory ${dataDir} does not exist, nothing to remove.`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";

Expand Down
23 changes: 0 additions & 23 deletions apps/ensrainbow/src/lib/env.ts

This file was deleted.

Loading