From 08661bbd970d3e1f9187ac4dfff3b5aa919f52df Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 20 Mar 2026 07:51:22 -0700 Subject: [PATCH] feat(backend-config): create @bytelyst/backend-config package with shared Zod schema - baseBackendConfigSchema: PORT, HOST, NODE_ENV, CORS_ORIGIN, SERVICE_NAME, DB_PROVIDER, COSMOS_*, JWT_SECRET, PLATFORM_JWKS_URL - parseBackendConfig() helper for env parsing - Products extend via baseBackendConfigSchema.extend({...}) - 8 tests passing --- packages/backend-config/package.json | 30 +++++++++ packages/backend-config/src/index.test.ts | 82 +++++++++++++++++++++++ packages/backend-config/src/index.ts | 39 +++++++++++ packages/backend-config/tsconfig.json | 9 +++ pnpm-lock.yaml | 13 ++++ 5 files changed, 173 insertions(+) create mode 100644 packages/backend-config/package.json create mode 100644 packages/backend-config/src/index.test.ts create mode 100644 packages/backend-config/src/index.ts create mode 100644 packages/backend-config/tsconfig.json diff --git a/packages/backend-config/package.json b/packages/backend-config/package.json new file mode 100644 index 00000000..c03fdcb3 --- /dev/null +++ b/packages/backend-config/package.json @@ -0,0 +1,30 @@ +{ + "name": "@bytelyst/backend-config", + "version": "0.1.0", + "description": "Shared Zod config schema base for Fastify product backends", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "clean": "rm -rf dist" + }, + "dependencies": { + "zod": "^3.24.2" + }, + "devDependencies": { + "typescript": "^5.7.3", + "vitest": "^3.0.5" + }, + "files": [ + "dist" + ] +} diff --git a/packages/backend-config/src/index.test.ts b/packages/backend-config/src/index.test.ts new file mode 100644 index 00000000..61ab6b5b --- /dev/null +++ b/packages/backend-config/src/index.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect } from 'vitest'; +import { baseBackendConfigSchema, parseBackendConfig } from './index.js'; + +describe('baseBackendConfigSchema', () => { + it('parses minimal valid env', () => { + const config = baseBackendConfigSchema.parse({ + JWT_SECRET: 'test-secret', + }); + expect(config.PORT).toBe(3000); + expect(config.HOST).toBe('0.0.0.0'); + expect(config.NODE_ENV).toBe('development'); + expect(config.DB_PROVIDER).toBe('cosmos'); + expect(config.COSMOS_DATABASE).toBe('lysnrai'); + expect(config.JWT_SECRET).toBe('test-secret'); + expect(config.PLATFORM_JWKS_URL).toBeUndefined(); + }); + + it('applies overrides', () => { + const config = baseBackendConfigSchema.parse({ + PORT: '4010', + NODE_ENV: 'production', + DB_PROVIDER: 'memory', + JWT_SECRET: 'prod-secret', + PLATFORM_JWKS_URL: 'https://example.com/.well-known/jwks.json', + }); + expect(config.PORT).toBe(4010); + expect(config.NODE_ENV).toBe('production'); + expect(config.DB_PROVIDER).toBe('memory'); + expect(config.PLATFORM_JWKS_URL).toBe('https://example.com/.well-known/jwks.json'); + }); + + it('rejects missing JWT_SECRET', () => { + expect(() => baseBackendConfigSchema.parse({})).toThrow(); + }); + + it('rejects invalid NODE_ENV', () => { + expect(() => baseBackendConfigSchema.parse({ JWT_SECRET: 's', NODE_ENV: 'staging' })).toThrow(); + }); + + it('rejects invalid DB_PROVIDER', () => { + expect(() => + baseBackendConfigSchema.parse({ JWT_SECRET: 's', DB_PROVIDER: 'postgres' }) + ).toThrow(); + }); +}); + +describe('baseBackendConfigSchema.extend()', () => { + const extendedSchema = baseBackendConfigSchema.extend({ + PLATFORM_SERVICE_URL: baseBackendConfigSchema.shape.HOST.default('http://localhost:4003'), + CUSTOM_FLAG: baseBackendConfigSchema.shape.NODE_ENV.optional(), + }); + + it('parses extended config with product-specific fields', () => { + const config = extendedSchema.parse({ + JWT_SECRET: 'test-secret', + PORT: '4018', + SERVICE_NAME: 'actiontrail-backend', + }); + expect(config.PORT).toBe(4018); + expect(config.SERVICE_NAME).toBe('actiontrail-backend'); + expect(config.PLATFORM_SERVICE_URL).toBe('http://localhost:4003'); + }); +}); + +describe('parseBackendConfig', () => { + it('parses from explicit env object', () => { + const config = parseBackendConfig(baseBackendConfigSchema, { + JWT_SECRET: 'from-env', + PORT: '9999', + }); + expect(config.JWT_SECRET).toBe('from-env'); + expect(config.PORT).toBe(9999); + }); + + it('works with extended schemas', () => { + const schema = baseBackendConfigSchema.extend({ + WEBHOOK_SECRET: baseBackendConfigSchema.shape.HOST.default('dev-webhook'), + }); + const config = parseBackendConfig(schema, { JWT_SECRET: 'x' }); + expect(config.WEBHOOK_SECRET).toBe('dev-webhook'); + }); +}); diff --git a/packages/backend-config/src/index.ts b/packages/backend-config/src/index.ts new file mode 100644 index 00000000..ae8011cd --- /dev/null +++ b/packages/backend-config/src/index.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; + +/** + * Base Zod schema shared by all product backends. + * + * Products extend this with `.extend({...})` to add product-specific fields. + * The base covers: server, CORS, Cosmos DB, JWT auth, DB provider. + */ +export const baseBackendConfigSchema = z.object({ + PORT: z.coerce.number().default(3000), + HOST: z.string().default('0.0.0.0'), + NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), + CORS_ORIGIN: z.string().optional(), + SERVICE_NAME: z.string().default('backend'), + + DB_PROVIDER: z.enum(['cosmos', 'memory']).default('cosmos'), + COSMOS_ENDPOINT: z.string().default(''), + COSMOS_KEY: z.string().default(''), + COSMOS_DATABASE: z.string().default('lysnrai'), + + JWT_SECRET: z.string().min(1, 'JWT_SECRET is required'), + PLATFORM_JWKS_URL: z.string().url().optional(), +}); + +export type BaseBackendConfig = z.infer; + +/** + * Parse and validate backend config from process.env. + * + * @param schema — Zod object schema (typically `baseBackendConfigSchema.extend({...})`) + * @param env — environment object (defaults to process.env) + * @returns Validated, typed config + */ +export function parseBackendConfig( + schema: z.ZodObject, + env: Record = process.env as Record +): z.infer> { + return schema.parse(env); +} diff --git a/packages/backend-config/tsconfig.json b/packages/backend-config/tsconfig.json new file mode 100644 index 00000000..01c4d9a3 --- /dev/null +++ b/packages/backend-config/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4dd6ee1..ace17135 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -350,6 +350,19 @@ importers: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) + packages/backend-config: + dependencies: + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.11)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.31.1)(msw@2.12.10(@types/node@22.19.11)(typescript@5.9.3))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + packages/blob: dependencies: '@bytelyst/storage':