diff --git a/README.md b/README.md index 8db01c6..814e8f2 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,33 @@ export const handler = async (event: any, context: any) => { }; ``` +### Configuration Example + +```typescript +import { z } from 'zod'; +import { createConfigManager } from '@leanstacks/lambda-utils'; + +// Define your configuration schema +const configSchema = z.object({ + TABLE_NAME: z.string().min(1), + AWS_REGION: z.string().default('us-east-1'), + LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'), +}); + +type Config = z.infer; + +const configManager = createConfigManager(configSchema); +const config = configManager.get(); + +export const handler = async (event: any) => { + console.log(`Using table: ${config.TABLE_NAME}`); + + // Your Lambda handler logic here + + return { statusCode: 200, body: 'Success' }; +}; +``` + ### API Response Example ```typescript @@ -77,12 +104,35 @@ Comprehensive guides and examples are available in the `docs` directory: | Guide | Description | | ------------------------------------------------------------ | ---------------------------------------------------------------------- | +| **[Configuration Guide](./docs/CONFIGURATION.md)** | Validate environment variables with Zod schemas and type safety | | **[Logging Guide](./docs/LOGGING.md)** | Configure and use structured logging with automatic AWS Lambda context | | **[API Gateway Responses](./docs/API_GATEWAY_RESPONSES.md)** | Format responses for API Gateway with standard HTTP patterns | -| **[AWS Clients](./docs/README.md)** | Use pre-configured AWS SDK v3 clients in your handlers | +| **[DynamoDB Client](./docs/DYNAMODB_CLIENT.md)** | Use pre-configured AWS SDK v3 clients in your handlers | ## Usage +### Configuration + +Validate and manage environment variables with type safety: + +```typescript +import { z } from 'zod'; +import { createConfigManager } from '@leanstacks/lambda-utils'; + +const configManager = createConfigManager( + z.object({ + TABLE_NAME: z.string().min(1), + AWS_REGION: z.string().default('us-east-1'), + }), +); + +const config = configManager.get(); +// TypeScript infers type from schema +// Validation errors thrown immediately +``` + +**→ See [Configuration Guide](./docs/CONFIGURATION.md) for detailed validation patterns and best practices** + ### Logging The Logger utility provides structured logging configured specifically for AWS Lambda: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 0000000..7d9ba08 --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,348 @@ +# Configuration Guide + +This guide explains how to use the `createConfigManager` utility to validate and manage environment variables in your Lambda functions with full TypeScript type safety. + +## Overview + +The configuration utility provides: + +- **Schema-based validation** using Zod for environment variables +- **Type-safe access** to your configuration with full TypeScript support +- **Caching** of validated configuration for performance +- **Flexible defaults** for optional environment variables +- **Clear error messages** when validation fails + +## Quick Start + +### Define Your Schema + +Create a Zod schema that describes your environment variables: + +```typescript +import { z } from 'zod'; + +const configSchema = z.object({ + // Required variables + TABLE_NAME: z.string().min(1, 'TABLE_NAME is required'), + + // Optional with defaults + AWS_REGION: z.string().default('us-east-1'), + DEBUG_MODE: z + .enum(['true', 'false']) + .default('false') + .transform((val) => val === 'true'), +}); + +// Infer the TypeScript type from your schema +type Config = z.infer; +``` + +### Create and Use ConfigManager + +```typescript +import { createConfigManager } from '@leanstacks/lambda-utils'; + +// Create the manager +const configManager = createConfigManager(configSchema); + +// Get validated config (cached after first call) +const config = configManager.get(); + +// Use your configuration +console.log(config.TABLE_NAME); // Type-safe access +console.log(config.AWS_REGION); // Automatically defaults to 'us-east-1' +console.log(config.DEBUG_MODE); // Typed as boolean +``` + +## Complete Example + +Here's a realistic Lambda function configuration: + +```typescript +import { z } from 'zod'; +import { createConfigManager } from '@leanstacks/lambda-utils'; + +/** + * Schema for validating environment variables + */ +const envSchema = z.object({ + // Required variables + TASKS_TABLE: z.string().min(1, 'TASKS_TABLE environment variable is required'), + + // Optional variables with defaults + AWS_REGION: z.string().default('us-east-1'), + + // Logging configuration + LOGGING_ENABLED: z + .enum(['true', 'false'] as const) + .default('true') + .transform((val) => val === 'true'), + LOGGING_LEVEL: z.enum(['debug', 'info', 'warn', 'error'] as const).default('debug'), + LOGGING_FORMAT: z.enum(['text', 'json'] as const).default('json'), + + // CORS configuration + CORS_ALLOW_ORIGIN: z.string().default('*'), +}); + +/** + * Type representing our validated config + */ +export type Config = z.infer; + +/** + * Configuration manager instance + */ +const configManager = createConfigManager(envSchema); + +/** + * Validated configuration object. Singleton. + */ +export const config = configManager.get(); + +/** + * Refresh configuration (useful in tests) + */ +export const refreshConfig = () => configManager.refresh(); +``` + +Then use it in your handler: + +```typescript +import { config } from './config'; +import { Logger } from '@leanstacks/lambda-utils'; + +const logger = new Logger({ + level: config.LOGGING_LEVEL, + format: config.LOGGING_FORMAT, +}).instance; + +export const handler = async (event: any) => { + logger.info({ + message: 'Processing request', + table: config.TASKS_TABLE, + region: config.AWS_REGION, + }); + + // Your handler logic here +}; +``` + +## API Reference + +### `createConfigManager(schema: T): ConfigManager>` + +Creates a configuration manager instance. + +**Parameters:** + +- `schema` - A Zod schema defining your environment variables + +**Returns:** A `ConfigManager` instance with two methods + +### `ConfigManager.get(): T` + +Gets the validated configuration (cached after the first call). + +**Throws:** `Error` if validation fails + +**Returns:** The validated configuration object + +```typescript +const config = configManager.get(); +// First call: validates and caches +// Subsequent calls: returns cached value +``` + +### `ConfigManager.refresh(): T` + +Refreshes the configuration by re-validating environment variables against the schema. + +**Throws:** `Error` if validation fails + +**Returns:** The newly validated configuration object + +Use this in tests when you need to change environment variables: + +```typescript +beforeEach(() => { + process.env.TABLE_NAME = 'test-table'; + configManager.refresh(); // Re-validate with new values +}); +``` + +## Best Practices + +### 1. Separate Configuration Module + +Create a dedicated configuration module for your Lambda function: + +```typescript +// src/config.ts +import { z } from 'zod'; +import { createConfigManager } from '@leanstacks/lambda-utils'; + +const schema = z.object({ + TABLE_NAME: z.string().min(1), + AWS_REGION: z.string().default('us-east-1'), +}); + +export type Config = z.infer; + +const configManager = createConfigManager(schema); + +export const config = configManager.get(); +export const refresh = () => configManager.refresh(); +``` + +### 2. Validate Early + +Call `config.get()` during handler initialization to validate configuration before processing requests: + +```typescript +export const handler = async (event: any, context: any) => { + // Validation happens here, fails fast if config is invalid + const config = configManager.get(); + + // Handler logic with validated config +}; +``` + +### 3. Use Enums for Known Values + +Use `z.enum()` for configuration options with limited valid values: + +```typescript +const schema = z.object({ + LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'), + ENVIRONMENT: z.enum(['dev', 'staging', 'prod']), +}); + +// TypeScript autocomplete for config.LOG_LEVEL +``` + +### 4. Transform String to Boolean + +Since environment variables are always strings, use `transform()` to convert them: + +```typescript +const schema = z.object({ + ENABLE_FEATURE: z + .enum(['true', 'false']) + .default('false') + .transform((val) => val === 'true'), +}); + +// config.ENABLE_FEATURE is now a boolean +if (config.ENABLE_FEATURE) { + // Feature is enabled +} +``` + +### 5. Provide Helpful Error Messages + +Use Zod's second parameter to provide context-specific error messages: + +```typescript +const schema = z.object({ + DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'), + API_KEY: z.string().min(32, 'API_KEY must be at least 32 characters'), +}); +``` + +### 6. Test Configuration Validation + +Test that your schema properly validates configuration: + +```typescript +import { config, refresh } from './config'; + +describe('Configuration', () => { + it('should load default values', () => { + delete process.env.AWS_REGION; + refresh(); + expect(config.AWS_REGION).toBe('us-east-1'); + }); + + it('should validate required variables', () => { + delete process.env.TABLE_NAME; + expect(() => refresh()).toThrow(); + }); + + it('should parse boolean values', () => { + process.env.DEBUG_MODE = 'true'; + refresh(); + expect(config.DEBUG_MODE).toBe(true); + }); +}); +``` + +## Common Patterns + +### Database Configuration + +```typescript +const schema = z.object({ + DATABASE_URL: z.string().url(), + DATABASE_POOL_SIZE: z + .string() + .default('10') + .transform((val) => parseInt(val, 10)), + DATABASE_TIMEOUT: z + .string() + .default('5000') + .transform((val) => parseInt(val, 10)), +}); +``` + +### Feature Flags + +```typescript +const schema = z.object({ + FEATURE_NEW_UI: z + .enum(['true', 'false']) + .default('false') + .transform((val) => val === 'true'), + FEATURE_BETA_API: z + .enum(['true', 'false']) + .default('false') + .transform((val) => val === 'true'), +}); +``` + +### Multi-Environment Setup + +```typescript +const schema = z.object({ + ENVIRONMENT: z.enum(['development', 'staging', 'production']), + LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'), + DEBUG_MODE: z + .enum(['true', 'false']) + .refine( + (val) => (val === 'true' ? process.env.ENVIRONMENT === 'development' : true), + 'DEBUG_MODE can only be true in development', + ) + .transform((val) => val === 'true'), +}); +``` + +## Error Handling + +Configuration validation errors include detailed information about what failed: + +```typescript +try { + const config = configManager.get(); +} catch (error) { + if (error instanceof Error) { + console.error(error.message); + // Output: "Configuration validation failed: TABLE_NAME: String must contain at least 1 character" + } +} +``` + +Lambda will automatically fail fast if configuration is invalid, which is the desired behavior for Lambda functions. + +## Related Documentation + +- **[Zod Documentation](https://zod.dev/)** – Learn more about schema validation with Zod +- **[Back to the project documentation](README.md)** diff --git a/docs/README.md b/docs/README.md index 2363aed..50b2567 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,11 +8,10 @@ Lambda Utilities is a collection of pre-configured tools and helpers designed to ## Documentation +- **[Configuration Guide](./CONFIGURATION.md)** – Validate environment variables with Zod schemas and type-safe configuration management - **[Logging Guide](./LOGGING.md)** – Implement structured logging in your Lambda functions with Pino and automatic AWS context enrichment - **[API Gateway Responses](./API_GATEWAY_RESPONSES.md)** – Format Lambda responses for API Gateway with standard HTTP status codes and headers - **[DynamoDB Client](./DYNAMODB_CLIENT.md)** – Reusable singleton DynamoDB client instances with custom configuration -- **[Configuration](./CONFIGURATION.md)** – Validate environment variables and configuration with Zod type safety -- **[Getting Started](./GETTING_STARTED.md)** – Quick setup and installation instructions ## Features diff --git a/package-lock.json b/package-lock.json index 2fc6986..eabb466 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { "name": "@leanstacks/lambda-utils", - "version": "0.3.0-alpha.1", + "version": "0.3.0-alpha.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@leanstacks/lambda-utils", - "version": "0.3.0-alpha.1", + "version": "0.3.0-alpha.4", "license": "MIT", "dependencies": { - "@aws-sdk/client-dynamodb": "^3.955.0", - "@aws-sdk/lib-dynamodb": "^3.955.0", - "pino": "^10.1.0", - "pino-lambda": "^4.4.1" + "@aws-sdk/client-dynamodb": "3.955.0", + "@aws-sdk/lib-dynamodb": "3.955.0", + "pino": "10.1.0", + "pino-lambda": "4.4.1", + "zod": "4.2.1" }, "devDependencies": { "@eslint/js": "9.39.2", @@ -8509,6 +8510,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 8e9e0a3..004ab65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@leanstacks/lambda-utils", - "version": "0.3.0-alpha.2", + "version": "0.3.0-alpha.4", "description": "A collection of utilities and helper functions designed to streamline the development of AWS Lambda functions using TypeScript.", "main": "dist/index.js", "module": "dist/index.esm.js", @@ -63,9 +63,10 @@ "typescript": "5.9.3" }, "dependencies": { - "@aws-sdk/client-dynamodb": "^3.955.0", - "@aws-sdk/lib-dynamodb": "^3.955.0", - "pino": "^10.1.0", - "pino-lambda": "^4.4.1" + "@aws-sdk/client-dynamodb": "3.955.0", + "@aws-sdk/lib-dynamodb": "3.955.0", + "pino": "10.1.0", + "pino-lambda": "4.4.1", + "zod": "4.2.1" } } diff --git a/src/index.ts b/src/index.ts index 972921b..a527bcd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,3 +16,4 @@ export { initializeDynamoDBClients, resetDynamoDBClients, } from './clients/dynamodb-client'; +export { createConfigManager, ConfigManager } from './validation/config'; diff --git a/src/validation/config.test.ts b/src/validation/config.test.ts new file mode 100644 index 0000000..fdb526a --- /dev/null +++ b/src/validation/config.test.ts @@ -0,0 +1,386 @@ +import { z } from 'zod'; +import { createConfigManager, type ConfigManager } from './config'; + +describe('createConfigManager', () => { + // Store original env for restoration + const originalEnv = process.env; + + beforeEach(() => { + // Clear environment variables before each test + process.env = {}; + }); + + afterEach(() => { + // Restore original environment + process.env = originalEnv; + }); + + describe('basic functionality', () => { + it('should create a ConfigManager instance with get and refresh methods', () => { + // Arrange + const schema = z.object({ + TEST_VAR: z.string().default('default'), + }); + + // Act + const manager = createConfigManager(schema); + + // Assert + expect(manager).toHaveProperty('get'); + expect(manager).toHaveProperty('refresh'); + expect(typeof manager.get).toBe('function'); + expect(typeof manager.refresh).toBe('function'); + }); + + it('should validate and return configuration on first get() call', () => { + // Arrange + process.env.TABLE_NAME = 'my-table'; + process.env.AWS_REGION = 'us-west-2'; + + const schema = z.object({ + TABLE_NAME: z.string().min(1), + AWS_REGION: z.string(), + }); + + const manager = createConfigManager(schema); + + // Act + const config = manager.get(); + + // Assert + expect(config).toEqual({ + TABLE_NAME: 'my-table', + AWS_REGION: 'us-west-2', + }); + }); + + it('should apply default values from schema', () => { + // Arrange + process.env.REQUIRED_VAR = 'value'; + + const schema = z.object({ + REQUIRED_VAR: z.string(), + OPTIONAL_VAR: z.string().default('default-value'), + }); + + const manager = createConfigManager(schema); + + // Act + const config = manager.get(); + + // Assert + expect(config.REQUIRED_VAR).toBe('value'); + expect(config.OPTIONAL_VAR).toBe('default-value'); + }); + + it('should apply transformations from schema', () => { + // Arrange + process.env.ENABLED = 'true'; + + const schema = z.object({ + ENABLED: z.enum(['true', 'false'] as const).transform((val) => val === 'true'), + }); + + const manager = createConfigManager(schema); + + // Act + const config = manager.get(); + + // Assert + expect(config.ENABLED).toBe(true); + }); + }); + + describe('caching behavior', () => { + it('should cache configuration after first get() call', () => { + // Arrange + process.env.APP_NAME = 'initial'; + + const schema = z.object({ + APP_NAME: z.string(), + }); + + const manager = createConfigManager(schema); + + // Act - first call + const config1 = manager.get(); + + // Change environment variable + process.env.APP_NAME = 'changed'; + + // Second call should return cached value + const config2 = manager.get(); + + // Assert + expect(config1.APP_NAME).toBe('initial'); + expect(config2.APP_NAME).toBe('initial'); + expect(config1).toBe(config2); // Same reference + }); + + it('should return same cached instance on multiple get() calls', () => { + // Arrange + const schema = z.object({ + VAR: z.string().default('value'), + }); + + const manager = createConfigManager(schema); + + // Act + const config1 = manager.get(); + const config2 = manager.get(); + const config3 = manager.get(); + + // Assert + expect(config1).toBe(config2); + expect(config2).toBe(config3); + }); + }); + + describe('refresh() method', () => { + it('should re-validate and update cache on refresh() call', () => { + // Arrange + process.env.COUNTER = '1'; + + const schema = z.object({ + COUNTER: z.coerce.number(), + }); + + const manager = createConfigManager(schema); + + // Act - initial get + const config1 = manager.get(); + expect(config1.COUNTER).toBe(1); + + // Change environment and refresh + process.env.COUNTER = '2'; + const config2 = manager.refresh(); + + // Assert + expect(config2.COUNTER).toBe(2); + expect(config1).not.toBe(config2); // Different instances + }); + + it('should clear cache and revalidate on refresh()', () => { + // Arrange + process.env.VALUE = 'old'; + + const schema = z.object({ + VALUE: z.string(), + }); + + const manager = createConfigManager(schema); + + // Act - initial + const initial = manager.get(); + expect(initial.VALUE).toBe('old'); + + // Refresh with new value + process.env.VALUE = 'new'; + const refreshed = manager.refresh(); + + // Assert + expect(refreshed.VALUE).toBe('new'); + expect(manager.get().VALUE).toBe('new'); // Cache updated + }); + + it('should return the new config instance from refresh()', () => { + // Arrange + const schema = z.object({ + VAR: z.string().default('default'), + }); + + const manager = createConfigManager(schema); + + // Act + const refreshed = manager.refresh(); + const cached = manager.get(); + + // Assert + expect(refreshed).toBe(cached); + }); + }); + + describe('error handling', () => { + it('should throw error when required variable is missing', () => { + // Arrange + process.env = {}; + + const schema = z.object({ + REQUIRED_VAR: z.string().min(1), + }); + + const manager = createConfigManager(schema); + + // Act & Assert + expect(() => manager.get()).toThrow('Configuration validation failed'); + }); + + it('should throw error when validation fails', () => { + // Arrange + process.env.PORT = 'not-a-number'; + + const schema = z.object({ + PORT: z.coerce.number(), + }); + + const manager = createConfigManager(schema); + + // Act & Assert + expect(() => manager.get()).toThrow('Configuration validation failed'); + }); + + it('should include all validation errors in error message', () => { + // Arrange + process.env.PORT = 'invalid'; + + const schema = z.object({ + DB_HOST: z.string().min(1, 'DB_HOST is required'), + DB_PORT: z.coerce.number().positive('PORT must be positive'), + PORT: z.coerce.number().positive('PORT must be positive'), + }); + + const manager = createConfigManager(schema); + + // Act & Assert + expect(() => manager.get()).toThrow('Configuration validation failed'); + }); + + it('should throw error on refresh() if validation fails', () => { + // Arrange + process.env.STATUS = 'valid'; + + const schema = z.object({ + STATUS: z.enum(['valid', 'invalid'] as const), + }); + + const manager = createConfigManager(schema); + + // Act - initial should succeed + expect(() => manager.get()).not.toThrow(); + + // Change to invalid value and refresh + process.env.STATUS = 'unknown'; + + // Assert + expect(() => manager.refresh()).toThrow('Configuration validation failed'); + }); + }); + + describe('type inference', () => { + it('should maintain type safety with schema', () => { + // Arrange + process.env.NAME = 'test'; + process.env.COUNT = '42'; + process.env.ENABLED = 'true'; + + const schema = z.object({ + NAME: z.string(), + COUNT: z.coerce.number(), + ENABLED: z.enum(['true', 'false'] as const).transform((val) => val === 'true'), + }); + + type TestConfig = z.infer; + + const manager: ConfigManager = createConfigManager(schema); + + // Act + const config = manager.get(); + + // Assert - TypeScript would catch type errors here + expect(typeof config.NAME).toBe('string'); + expect(typeof config.COUNT).toBe('number'); + expect(typeof config.ENABLED).toBe('boolean'); + }); + }); + + describe('complex schemas', () => { + it('should handle nested objects in schema', () => { + // Arrange + process.env.DB_HOST = 'localhost'; + process.env.DB_PORT = '5432'; + process.env.APP_NAME = 'myapp'; + + const schema = z.object({ + APP_NAME: z.string(), + DATABASE: z + .object({ + HOST: z.string(), + PORT: z.coerce.number(), + }) + .default({ + HOST: process.env.DB_HOST || 'localhost', + PORT: parseInt(process.env.DB_PORT || '5432'), + }), + }); + + const manager = createConfigManager(schema); + + // Act + const config = manager.get(); + + // Assert + expect(config.APP_NAME).toBe('myapp'); + expect(config.DATABASE.HOST).toBeDefined(); + }); + + it('should handle array validation in schema', () => { + // Arrange + process.env.ALLOWED_HOSTS = 'host1,host2,host3'; + + const schema = z.object({ + ALLOWED_HOSTS: z + .string() + .transform((val) => val.split(',')) + .default(['localhost']), + }); + + const manager = createConfigManager(schema); + + // Act + const config = manager.get(); + + // Assert + expect(Array.isArray(config.ALLOWED_HOSTS)).toBe(true); + }); + + it('should handle enum values', () => { + // Arrange + process.env.LOG_LEVEL = 'warn'; + + const schema = z.object({ + LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error'] as const).default('info'), + }); + + const manager = createConfigManager(schema); + + // Act + const config = manager.get(); + + // Assert + expect(config.LOG_LEVEL).toBe('warn'); + }); + }); + + describe('multiple instances', () => { + it('should maintain separate caches for different manager instances', () => { + // Arrange + process.env.VALUE = 'instance1'; + + const schema = z.object({ + VALUE: z.string(), + }); + + const manager1 = createConfigManager(schema); + const config1 = manager1.get(); + + process.env.VALUE = 'instance2'; + + const manager2 = createConfigManager(schema); + const config2 = manager2.get(); + + // Act & Assert + expect(config1.VALUE).toBe('instance1'); + expect(config2.VALUE).toBe('instance2'); + }); + }); +}); diff --git a/src/validation/config.ts b/src/validation/config.ts new file mode 100644 index 0000000..bebc3ef --- /dev/null +++ b/src/validation/config.ts @@ -0,0 +1,81 @@ +import { z } from 'zod'; + +/** + * Interface for a configuration manager instance + */ +export interface ConfigManager { + /** + * Get the validated configuration (cached after first call) + * @throws {Error} if validation fails + * @returns The validated configuration object + */ + get: () => T; + + /** + * Refresh the configuration by re-validating environment variables + * Useful in tests when environment variables are changed + * @throws {Error} if validation fails + * @returns The newly validated configuration object + */ + refresh: () => T; +} + +/** + * Creates a reusable configuration manager for any Lambda function + * + * @template T - The configuration type inferred from the provided Zod schema + * @param schema - A Zod schema that defines the structure and validation rules for environment variables + * @returns A ConfigManager instance with get() and refresh() methods + * + * @example + * ```typescript + * // Define your schema + * const configSchema = z.object({ + * TABLE_NAME: z.string().min(1), + * AWS_REGION: z.string().default('us-east-1'), + * }); + * + * // Create config manager + * const configManager = createConfigManager(configSchema); + * + * // Access configuration (cached on first call) + * const config = configManager.get(); + * + * // Type your config + * type Config = z.infer; + * ``` + */ +export const createConfigManager = (schema: T): ConfigManager> => { + let cache: z.infer | null = null; + + const _validateConfig = (): z.infer => { + try { + // Parse and validate environment variables against the schema + return schema.parse(process.env); + } catch (error) { + // Handle Zod validation errors + if (error instanceof z.ZodError) { + const errorMessage = error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join(', '); + + throw new Error(`Configuration validation failed: ${errorMessage}`); + } + + // Re-throw other errors + throw error; + } + }; + + return { + get: (): z.infer => { + if (!cache) { + cache = _validateConfig(); + } + return cache; + }, + + refresh: (): z.infer => { + cache = _validateConfig(); + return cache; + }, + }; +};