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 3 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
45 changes: 38 additions & 7 deletions apps/ensrainbow/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +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 { createCLI, validatePortConfiguration } from "./cli";
import { DEFAULT_PORT, getEnvPort } from "./lib/env";
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 @@ -26,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 @@ -46,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 value for environment variable '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): Invalid value for environment variable 'PORT': \"-1\" is not a non-negative integer",
);
});
});

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

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"),
);

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

// 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);
}
});
});

describe("serve command", () => {
Expand Down
79 changes: 40 additions & 39 deletions apps/ensrainbow/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,31 @@
import { join } from "path";
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 { 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 @@ -54,19 +45,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 @@ -77,20 +71,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 @@ -101,8 +101,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 @@ -112,8 +112,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 @@ -124,14 +126,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 @@ -8,10 +9,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
22 changes: 0 additions & 22 deletions apps/ensrainbow/src/lib/env.ts

This file was deleted.

Loading
Loading