-
Notifications
You must be signed in to change notification settings - Fork 15
Build ENSRainbow config #1425
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
base: main
Are you sure you want to change the base?
Build ENSRainbow config #1425
Changes from 7 commits
c34c0bd
bbe487e
6acc196
22a81f0
03722ac
ec75cfe
887aecc
bd1dd1c
1ddfc33
3b5c7cc
782e329
2e1f9d9
ddaaf29
a43b125
c57e7f6
e3a6c90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,84 @@ | ||||||
| import { join } from "node:path"; | ||||||
|
|
||||||
| import { prettifyError, ZodError, z } from "zod/v4"; | ||||||
github-code-quality[bot] marked this conversation as resolved.
Fixed
Show fixed
Hide fixed
|
||||||
|
|
||||||
| import { makeFullyPinnedLabelSetSchema, PortSchema } from "@ensnode/ensnode-sdk/internal"; | ||||||
|
|
||||||
| import { ENSRAINBOW_DEFAULT_PORT, getDefaultDataDir } from "@/config/defaults"; | ||||||
| import type { ENSRainbowEnvironment } from "@/config/environment"; | ||||||
| import { invariant_dbSchemaVersionMatch } from "@/config/validations"; | ||||||
| import { logger } from "@/utils/logger"; | ||||||
github-code-quality[bot] marked this conversation as resolved.
Fixed
Show fixed
Hide fixed
|
||||||
|
|
||||||
| const DataDirSchema = z | ||||||
| .string() | ||||||
| .trim() | ||||||
| .min(1, { | ||||||
| error: "DATA_DIR must be a non-empty string.", | ||||||
| }) | ||||||
| .transform((path: string) => { | ||||||
| // Resolve relative paths to absolute paths | ||||||
| if (path.startsWith("/")) { | ||||||
| return path; | ||||||
| } | ||||||
| return join(process.cwd(), path); | ||||||
djstrong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| }); | ||||||
djstrong marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| const DbSchemaVersionSchema = z.coerce | ||||||
| .number({ error: "DB_SCHEMA_VERSION must be a number." }) | ||||||
| .int({ error: "DB_SCHEMA_VERSION must be an integer." }) | ||||||
| .optional(); | ||||||
|
|
||||||
| const LabelSetSchema = makeFullyPinnedLabelSetSchema("LABEL_SET"); | ||||||
|
|
||||||
| const ENSRainbowConfigSchema = z | ||||||
| .object({ | ||||||
| port: PortSchema.default(ENSRAINBOW_DEFAULT_PORT), | ||||||
| dataDir: DataDirSchema.default(getDefaultDataDir()), | ||||||
|
Comment on lines
+36
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Default data directory is evaluated at module load time.
Consider using a getter function for lazy evaluation: ♻️ Suggested lazy evaluation const ENSRainbowConfigSchema = z
.object({
port: PortSchema.default(ENSRAINBOW_DEFAULT_PORT),
- dataDir: DataDirSchema.default(getDefaultDataDir()),
+ dataDir: DataDirSchema.optional(),
dbSchemaVersion: DbSchemaVersionSchema,
labelSet: LabelSetSchema.optional(),
})Then handle the default in return ENSRainbowConfigSchema.parse({
port: env.PORT,
dataDir: env.DATA_DIR ?? getDefaultDataDir(),
// ...
});🤖 Prompt for AI Agents |
||||||
| dbSchemaVersion: DbSchemaVersionSchema, | ||||||
| labelSet: LabelSetSchema.optional(), | ||||||
| }) | ||||||
| /** | ||||||
| * Invariant enforcement | ||||||
| * | ||||||
| * We enforce invariants across multiple values parsed with `ENSRainbowConfigSchema` | ||||||
| * by calling `.check()` function with relevant invariant-enforcing logic. | ||||||
| * Each such function has access to config values that were already parsed. | ||||||
| */ | ||||||
| .check(invariant_dbSchemaVersionMatch); | ||||||
|
|
||||||
| export type ENSRainbowConfig = z.infer<typeof ENSRainbowConfigSchema>; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To stay aligned with rest of the codebase, I suggest replacing
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see that most of types use |
||||||
|
|
||||||
| /** | ||||||
| * Builds the ENSRainbow configuration object from an ENSRainbowEnvironment object. | ||||||
| * | ||||||
| * Validates and parses the complete environment configuration using ENSRainbowConfigSchema. | ||||||
| * | ||||||
| * @returns A validated ENSRainbowConfig object | ||||||
| * @throws Error with formatted validation messages if environment parsing fails | ||||||
| */ | ||||||
| export function buildConfigFromEnvironment(env: ENSRainbowEnvironment): ENSRainbowConfig { | ||||||
| try { | ||||||
| return ENSRainbowConfigSchema.parse({ | ||||||
| port: env.PORT, | ||||||
| dataDir: env.DATA_DIR, | ||||||
djstrong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| dbSchemaVersion: env.DB_SCHEMA_VERSION, | ||||||
| labelSet: | ||||||
| env.LABEL_SET_ID || env.LABEL_SET_VERSION | ||||||
| ? { | ||||||
| labelSetId: env.LABEL_SET_ID, | ||||||
| labelSetVersion: env.LABEL_SET_VERSION, | ||||||
| } | ||||||
djstrong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| : undefined, | ||||||
djstrong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
djstrong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| }); | ||||||
| } catch (error) { | ||||||
| if (error instanceof ZodError) { | ||||||
| logger.error(`Failed to parse environment configuration: \n${prettifyError(error)}\n`); | ||||||
| } else if (error instanceof Error) { | ||||||
| logger.error(error, `Failed to build ENSRainbowConfig`); | ||||||
| } else { | ||||||
| logger.error(`Unknown Error`); | ||||||
| } | ||||||
|
|
||||||
| process.exit(1); | ||||||
| } | ||||||
|
Comment on lines
107
to
114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Calling 🔧 Suggested refactor for better error handling+export class ConfigurationError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "ConfigurationError";
+ }
+}
+
export function buildConfigFromEnvironment(env: ENSRainbowEnvironment): ENSRainbowConfig {
try {
return ENSRainbowConfigSchema.parse({
// ... parsing logic
});
} catch (error) {
if (error instanceof ZodError) {
logger.error(`Failed to parse environment configuration: \n${prettifyError(error)}\n`);
+ throw new ConfigurationError(`Invalid configuration: ${prettifyError(error)}`);
} else if (error instanceof Error) {
logger.error(error, `Failed to build ENSRainbowConfig`);
+ throw error;
} else {
logger.error(`Unknown Error`);
+ throw new ConfigurationError("Unknown configuration error");
}
-
- process.exit(1);
}
}Then handle the exit at the call site (e.g., in CLI entry points): try {
const config = buildConfigFromEnvironment(process.env as ENSRainbowEnvironment);
} catch (error) {
process.exit(1);
}🤖 Prompt for AI Agents |
||||||
| } | ||||||
djstrong marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { join } from "node:path"; | ||
|
|
||
| export const ENSRAINBOW_DEFAULT_PORT = 3223; | ||
|
|
||
| export const getDefaultDataDir = () => join(process.cwd(), "data"); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import type { LogLevelEnvironment, PortEnvironment } from "@ensnode/ensnode-sdk/internal"; | ||
|
|
||
| /** | ||
| * Represents the raw, unvalidated environment variables for the ENSRainbow application. | ||
| * | ||
| * Keys correspond to the environment variable names, and all values are optional strings, reflecting | ||
| * their state in `process.env`. This interface is intended to be the source type which then gets | ||
| * mapped/parsed into a structured configuration object like `ENSRainbowConfig`. | ||
| */ | ||
| export type ENSRainbowEnvironment = PortEnvironment & | ||
| LogLevelEnvironment & { | ||
| /** | ||
| * Directory path where the LevelDB database is stored. | ||
| */ | ||
| DATA_DIR?: string; | ||
|
|
||
| /** | ||
| * Expected Database Schema Version. | ||
| */ | ||
| DB_SCHEMA_VERSION?: string; | ||
|
|
||
| /** | ||
| * Expected Label Set ID. | ||
| */ | ||
| LABEL_SET_ID?: string; | ||
|
|
||
| /** | ||
| * Expected Label Set Version. | ||
| */ | ||
| LABEL_SET_VERSION?: string; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { buildConfigFromEnvironment } from "./config.schema"; | ||
| import type { ENSRainbowEnvironment } from "./environment"; | ||
|
|
||
| export type { ENSRainbowConfig } from "./config.schema"; | ||
| export { buildConfigFromEnvironment } from "./config.schema"; | ||
| export { ENSRAINBOW_DEFAULT_PORT, getDefaultDataDir } from "./defaults"; | ||
| export type { ENSRainbowEnvironment } from "./environment"; | ||
|
|
||
| // build, validate, and export the ENSRainbowConfig from process.env | ||
| export default buildConfigFromEnvironment(process.env as ENSRainbowEnvironment); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export type { ENSRainbowConfig } from "./config.schema"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import type { z } from "zod/v4"; | ||
|
|
||
| import { DB_SCHEMA_VERSION } from "@/lib/database"; | ||
|
|
||
| import type { ENSRainbowConfig } from "./config.schema"; | ||
|
|
||
| /** | ||
| * Zod `.check()` function input. | ||
| */ | ||
| type ZodCheckFnInput<T> = z.core.ParsePayload<T>; | ||
djstrong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
djstrong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
djstrong marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * Invariant: dbSchemaVersion must match the version expected by the code. | ||
| */ | ||
| export function invariant_dbSchemaVersionMatch( | ||
| ctx: ZodCheckFnInput<Pick<ENSRainbowConfig, "dbSchemaVersion">>, | ||
| ): void { | ||
| const { value: config } = ctx; | ||
|
|
||
| if (config.dbSchemaVersion !== undefined && config.dbSchemaVersion !== DB_SCHEMA_VERSION) { | ||
| throw new Error( | ||
| `DB_SCHEMA_VERSION mismatch! Expected version ${DB_SCHEMA_VERSION} from code, but found ${config.dbSchemaVersion} in environment variables.`, | ||
| ); | ||
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,8 @@ | ||
| import { join } from "node:path"; | ||
| import config from "@/config"; | ||
|
|
||
| import { parseNonNegativeInteger } from "@ensnode/ensnode-sdk"; | ||
|
|
||
| import { logger } from "@/utils/logger"; | ||
|
|
||
| export const getDefaultDataSubDir = () => join(process.cwd(), "data"); | ||
|
|
||
| export const DEFAULT_PORT = 3223; | ||
| /** | ||
| * Gets the port from environment variables. | ||
| */ | ||
| export function getEnvPort(): number { | ||
| const envPort = process.env.PORT; | ||
| if (!envPort) { | ||
| return DEFAULT_PORT; | ||
| } | ||
|
|
||
| try { | ||
| const port = parseNonNegativeInteger(envPort); | ||
| return port; | ||
| } catch (_error: unknown) { | ||
| const errorMessage = `Invalid PORT value "${envPort}": must be a non-negative integer`; | ||
| logger.error(errorMessage); | ||
| throw new Error(errorMessage); | ||
| } | ||
| return config.port; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.