-
Notifications
You must be signed in to change notification settings - Fork 0
feat(COMPT-50): implement Zod-based env schema validation engine #2
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
31ed737
feat(COMPT-50): implement Zod-based env schema validation engine
y-aithnini 1311529
chore: bump version to 0.1.0
y-aithnini 0c92aa3
style: apply prettier formatting across all files
y-aithnini 5c945a3
fix(lint): use import type for z in define-config.ts
y-aithnini File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,20 @@ | ||
| version: 2 | ||
| updates: | ||
| - package-ecosystem: npm | ||
| directory: '/' | ||
| directory: "/" | ||
| schedule: | ||
| interval: monthly | ||
| open-pull-requests-limit: 1 | ||
| groups: | ||
| npm-dependencies: | ||
| patterns: | ||
| - '*' | ||
| - "*" | ||
| assignees: | ||
| - CISCODE-MA/devops | ||
| labels: | ||
| - 'dependencies' | ||
| - 'npm' | ||
| - "dependencies" | ||
| - "npm" | ||
| commit-message: | ||
| prefix: 'chore(deps)' | ||
| include: 'scope' | ||
| prefix: "chore(deps)" | ||
| include: "scope" | ||
| rebase-strategy: auto |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| /** | ||
| * @file define-config.ts | ||
| * @description | ||
| * Core validation engine for ConfigKit. | ||
| * | ||
| * Consumers call `defineConfig(zodSchema)` to declare the shape of their | ||
| * environment variables. The returned `ConfigDefinition` is passed to | ||
| * `ConfigModule.register()` which calls `.parse()` synchronously during | ||
| * NestJS module initialization — before any provider resolves. | ||
| * | ||
| * Contents: | ||
| * - ConfigDefinition<T> — opaque wrapper that holds the schema + parse logic | ||
| * - defineConfig() — factory function; the primary public API | ||
| */ | ||
|
|
||
| // Only type references to `z` are used in this file (ZodObject, ZodRawShape, z.output<T>) | ||
| // so `import type` satisfies the @typescript-eslint/consistent-type-imports rule | ||
| import type { z } from "zod"; | ||
|
|
||
| import { ConfigValidationError } from "@/errors/config-validation.error"; | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────────── | ||
| // Types | ||
| // ───────────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /** | ||
| * Constraint for the Zod schema accepted by defineConfig. | ||
| * | ||
| * Must be a ZodObject so we can key into its shape for typed `.get()` calls | ||
| * inside ConfigService. | ||
| */ | ||
| type AnyZodObject = z.ZodObject<z.ZodRawShape>; | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────────── | ||
| // ConfigDefinition | ||
| // ───────────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /** | ||
| * Opaque wrapper returned by `defineConfig()`. | ||
| * | ||
| * Holds the Zod schema and exposes a `parse()` method that ConfigModule calls | ||
| * at startup to validate `process.env`. Do not instantiate this class | ||
| * directly — always use `defineConfig()`. | ||
| * | ||
| * The generic parameter `T` carries the full Zod schema type so that | ||
| * `ConfigService` can later infer the exact output type of each key. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const appConfig = defineConfig(z.object({ PORT: z.string().default('3000') })); | ||
| * // typeof appConfig → ConfigDefinition<ZodObject<{ PORT: ZodDefault<ZodString> }>> | ||
| * ``` | ||
| */ | ||
| export class ConfigDefinition<T extends AnyZodObject> { | ||
| /** | ||
| * The Zod schema provided by the consumer. | ||
| * ConfigModule stores this to produce injection tokens with correct types. | ||
| */ | ||
| public readonly schema: T; | ||
|
|
||
| constructor(schema: T) { | ||
| // Store the schema for later use by ConfigModule | ||
| this.schema = schema; | ||
| } | ||
|
|
||
| /** | ||
| * Validates the given environment record against `this.schema`. | ||
| * | ||
| * Called **synchronously** by ConfigModule during NestJS module | ||
| * initialization. If validation fails the app never finishes booting. | ||
| * | ||
| * @param env - The environment record to validate. | ||
| * Defaults to `process.env` so consumers never need to pass it. | ||
| * @returns A deep-frozen, fully-typed config object with no `string | undefined` values. | ||
| * Zod coerces types and fills in `.default()` values automatically. | ||
| * @throws {ConfigValidationError} When one or more fields fail validation. | ||
| * The error lists every failing ZodIssue so developers can fix all | ||
| * problems in a single restart cycle. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Called internally by ConfigModule — you rarely call this yourself | ||
| * const config = appConfig.parse(process.env); | ||
| * config.PORT; // string (never undefined) | ||
| * config.DATABASE_URL; // string | ||
| * ``` | ||
| */ | ||
| parse(env: Record<string, string | undefined> = process.env): Readonly<z.output<T>> { | ||
| // Run a safe (non-throwing) parse so we can collect ALL issues at once | ||
| const result = this.schema.safeParse(env); | ||
|
|
||
| // If validation failed, throw with the full ZodIssue list | ||
| if (!result.success) { | ||
| throw new ConfigValidationError(result.error.issues); | ||
| } | ||
|
|
||
y-aithnini marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Freeze the resulting object so consumers cannot mutate config at runtime | ||
y-aithnini marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return Object.freeze(result.data) as Readonly<z.output<T>>; | ||
| } | ||
| } | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────────── | ||
| // defineConfig | ||
| // ───────────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /** | ||
| * Declares the shape of your application's environment variables using a Zod schema. | ||
| * | ||
| * Returns a `ConfigDefinition` that you pass to `ConfigModule.register()` or | ||
| * `ConfigModule.forRoot()`. ConfigModule will call `.parse(process.env)` at | ||
| * startup and make the result available through `ConfigService`. | ||
| * | ||
| * **No validation happens at call time** — validation is deferred to module | ||
| * initialization so that tests can import and compose schemas without a | ||
| * fully-populated environment. | ||
| * | ||
| * @param schema - A `z.object(...)` describing every env variable the module needs. | ||
| * Use `.default()` for optional variables and `.transform()` for | ||
| * type coercion (e.g. `z.coerce.number()` for port numbers). | ||
| * @returns `ConfigDefinition<T>` — pass this to `ConfigModule.register(definition)`. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // config/app.config.ts | ||
| * import { defineConfig } from '@ciscode/config-kit'; | ||
| * import { z } from 'zod'; | ||
| * | ||
| * export const appConfig = defineConfig( | ||
| * z.object({ | ||
| * PORT: z.coerce.number().default(3000), | ||
| * DATABASE_URL: z.string().url(), | ||
| * JWT_SECRET: z.string().min(32), | ||
| * NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), | ||
| * }), | ||
| * ); | ||
| * | ||
| * // app.module.ts | ||
| * @Module({ imports: [ConfigModule.forRoot(appConfig)] }) | ||
| * export class AppModule {} | ||
| * | ||
| * // some.service.ts | ||
| * constructor(private config: ConfigService<typeof appConfig>) {} | ||
| * // config.get('PORT') → number (not string | undefined) | ||
| * ``` | ||
| */ | ||
| export function defineConfig<T extends AnyZodObject>(schema: T): ConfigDefinition<T> { | ||
| // Wrap the schema in a ConfigDefinition; no side effects at definition time | ||
| return new ConfigDefinition(schema); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| /** | ||
| * @file config-validation.error.ts | ||
| * @description | ||
| * Custom error class thrown when process.env fails validation against a | ||
| * consumer-provided Zod schema. | ||
| * | ||
| * Thrown synchronously during ConfigModule initialization so the NestJS app | ||
| * never boots in a misconfigured state. | ||
| * | ||
| * Contents: | ||
| * - ConfigValidationError — extends Error, carries the full ZodIssue list | ||
| */ | ||
|
|
||
| import type { ZodIssue } from "zod"; | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────────── | ||
| // Error class | ||
| // ───────────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /** | ||
| * Thrown when one or more environment variables fail Zod schema validation. | ||
| * | ||
| * The `fields` array contains every ZodIssue that failed — callers can inspect | ||
| * it programmatically or let the built-in message surface them all at once. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * try { | ||
| * defineConfig(schema).parse(process.env); | ||
| * } catch (err) { | ||
| * if (err instanceof ConfigValidationError) { | ||
| * console.error(err.fields); // ZodIssue[] | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| export class ConfigValidationError extends Error { | ||
| /** | ||
| * All ZodIssue objects that caused validation to fail. | ||
| * Each issue contains the field path, the failing code, and a human-readable message. | ||
| */ | ||
| public readonly fields: ZodIssue[]; | ||
|
|
||
| constructor(fields: ZodIssue[]) { | ||
| // Build a multi-line message so developers immediately see what's wrong in logs | ||
| const lines = fields.map( | ||
| (issue) => | ||
| // Format each issue as " • FIELD_PATH: error message" | ||
| ` • ${issue.path.join(".") || "(root)"}: ${issue.message}`, | ||
| ); | ||
|
|
||
| super(`Config validation failed:\n${lines.join("\n")}`); | ||
|
|
||
| // Set the prototype explicitly so `instanceof ConfigValidationError` works | ||
| // correctly after TypeScript compiles to ES5 / CommonJS targets | ||
| Object.setPrototypeOf(this, new.target.prototype); | ||
|
|
||
| // Give the error a recognisable name in stack traces | ||
| this.name = "ConfigValidationError"; | ||
|
|
||
| // Store the raw ZodIssue list for programmatic inspection | ||
| this.fields = fields; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.