diff --git a/packages/config/jest.config.js b/packages/config/jest.config.js new file mode 100644 index 0000000..50c59e4 --- /dev/null +++ b/packages/config/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/*.test.ts'], +}; diff --git a/packages/config/package.json b/packages/config/package.json new file mode 100644 index 0000000..63c712d --- /dev/null +++ b/packages/config/package.json @@ -0,0 +1,26 @@ +{ + "name": "@tower/config", + "version": "0.0.1", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "test": "jest" + }, + "dependencies": { + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/jest": "^29.0.0", + "jest": "^29.0.0", + "ts-jest": "^29.0.0", + "typescript": "^5.7.0" + } +} diff --git a/packages/config/src/index.test.ts b/packages/config/src/index.test.ts new file mode 100644 index 0000000..9ecd785 --- /dev/null +++ b/packages/config/src/index.test.ts @@ -0,0 +1,40 @@ +import { validateEnv } from './index'; + +const validEnv = { + NODE_ENV: 'development', + DATABASE_URL: 'postgresql://tower:tower_dev@localhost:5432/tower_dev', + REDIS_URL: 'redis://localhost:6379', + JWT_SECRET: 'a_super_secret_key_that_is_at_least_32_chars_long', +} as unknown as NodeJS.ProcessEnv; + +describe('validateEnv', () => { + it('returns parsed config for valid env', () => { + const result = validateEnv(validEnv); + expect(result.NODE_ENV).toBe('development'); + expect(result.API_PORT).toBe(3001); + }); + + it('applies default API_PORT of 3001 when not set', () => { + const result = validateEnv(validEnv); + expect(result.API_PORT).toBe(3001); + }); + + it('throws when DATABASE_URL is missing', () => { + const { DATABASE_URL, ...withoutDb } = validEnv as Record; + expect(() => + validateEnv(withoutDb as unknown as NodeJS.ProcessEnv), + ).toThrow('Invalid environment variables'); + }); + + it('throws when JWT_SECRET is shorter than 32 chars', () => { + expect(() => + validateEnv({ ...validEnv, JWT_SECRET: 'tooshort' } as unknown as NodeJS.ProcessEnv), + ).toThrow('Invalid environment variables'); + }); + + it('throws when DATABASE_URL is not a valid URL', () => { + expect(() => + validateEnv({ ...validEnv, DATABASE_URL: 'not-a-url' } as unknown as NodeJS.ProcessEnv), + ).toThrow('Invalid environment variables'); + }); +}); diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts new file mode 100644 index 0000000..d3f6e4b --- /dev/null +++ b/packages/config/src/index.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +const envSchema = z.object({ + NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), + DATABASE_URL: z.string().url(), + REDIS_URL: z.string().url(), + API_PORT: z.coerce.number().default(3001), + JWT_SECRET: z.string().min(32), + MEILI_URL: z.string().url().default('http://localhost:7700'), + MEILI_MASTER_KEY: z.string().default('tower_meili_dev_key'), + LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error']).default('info'), +}); + +export type Env = z.infer; + +export function validateEnv(env: NodeJS.ProcessEnv = process.env): Env { + const result = envSchema.safeParse(env); + if (!result.success) { + console.error('Invalid environment variables:', result.error.format()); + throw new Error('Invalid environment variables'); + } + return result.data; +} diff --git a/packages/config/tsconfig.json b/packages/config/tsconfig.json new file mode 100644 index 0000000..792172f --- /dev/null +++ b/packages/config/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"] +}