feat: validate backend runtime configuration
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigurationModule } from '../../../libs/configuration/src';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [ConfigurationModule],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"rootDir": "../../..",
|
"rootDir": "../../..",
|
||||||
"testEnvironment": "node",
|
"testEnvironment": "node",
|
||||||
"testRegex": "apps/api/test/.*\\.e2e-spec\\.ts$",
|
"testRegex": "apps/api/test/.*\\.e2e-spec\\.ts$",
|
||||||
|
"setupFiles": ["<rootDir>/test/setup-env.ts"],
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"declaration": false,
|
"declaration": false
|
||||||
"outDir": "../../dist/apps/api"
|
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist", "test", "**/*.spec.ts"]
|
"exclude": ["node_modules", "dist", "test", "**/*.spec.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigurationModule } from '../../../libs/configuration/src';
|
||||||
|
|
||||||
@Module({})
|
@Module({
|
||||||
|
imports: [ConfigurationModule],
|
||||||
|
})
|
||||||
export class WorkerModule {}
|
export class WorkerModule {}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"declaration": false,
|
"declaration": false
|
||||||
"outDir": "../../dist/apps/worker"
|
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist", "test", "**/*.spec.ts"]
|
"exclude": ["node_modules", "dist", "test", "**/*.spec.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
9
backend/libs/configuration/src/configuration.module.ts
Normal file
9
backend/libs/configuration/src/configuration.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { appEnvironmentProvider } from './environment';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [appEnvironmentProvider],
|
||||||
|
exports: [appEnvironmentProvider],
|
||||||
|
})
|
||||||
|
export class ConfigurationModule {}
|
||||||
25
backend/libs/configuration/src/environment.spec.ts
Normal file
25
backend/libs/configuration/src/environment.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { loadEnvironment } from './environment';
|
||||||
|
|
||||||
|
describe('loadEnvironment', () => {
|
||||||
|
it('requires database and redis URLs', () => {
|
||||||
|
expect(() => loadEnvironment({})).toThrow('DATABASE_URL');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns build metadata without secrets', () => {
|
||||||
|
expect(
|
||||||
|
loadEnvironment({
|
||||||
|
DATABASE_URL: 'postgresql://u:p@postgres:5432/db',
|
||||||
|
REDIS_URL: 'redis://redis:6379',
|
||||||
|
APP_VERSION: '1.2.3',
|
||||||
|
TEAMCITY_BUILD_NUMBER: '42',
|
||||||
|
SOURCE_REVISION: 'abc123',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
databaseUrl: 'postgresql://u:p@postgres:5432/db',
|
||||||
|
redisUrl: 'redis://redis:6379',
|
||||||
|
appVersion: '1.2.3',
|
||||||
|
teamCityBuildNumber: '42',
|
||||||
|
sourceRevision: 'abc123',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
30
backend/libs/configuration/src/environment.ts
Normal file
30
backend/libs/configuration/src/environment.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export interface AppEnvironment {
|
||||||
|
databaseUrl: string;
|
||||||
|
redisUrl: string;
|
||||||
|
appVersion: string;
|
||||||
|
teamCityBuildNumber: string;
|
||||||
|
sourceRevision: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function required(env: NodeJS.ProcessEnv, name: string): string {
|
||||||
|
const value = env[name]?.trim();
|
||||||
|
if (!value) throw new Error(`Missing required environment variable: ${name}`);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadEnvironment(env: NodeJS.ProcessEnv): AppEnvironment {
|
||||||
|
return {
|
||||||
|
databaseUrl: required(env, 'DATABASE_URL'),
|
||||||
|
redisUrl: required(env, 'REDIS_URL'),
|
||||||
|
appVersion: env.APP_VERSION?.trim() || 'dev',
|
||||||
|
teamCityBuildNumber: env.TEAMCITY_BUILD_NUMBER?.trim() || 'local',
|
||||||
|
sourceRevision: env.SOURCE_REVISION?.trim() || 'local',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const APP_ENVIRONMENT = Symbol('APP_ENVIRONMENT');
|
||||||
|
|
||||||
|
export const appEnvironmentProvider = {
|
||||||
|
provide: APP_ENVIRONMENT,
|
||||||
|
useFactory: () => loadEnvironment(process.env),
|
||||||
|
};
|
||||||
2
backend/libs/configuration/src/index.ts
Normal file
2
backend/libs/configuration/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './environment';
|
||||||
|
export * from './configuration.module';
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"root": "apps/api",
|
"root": "apps/api",
|
||||||
"sourceRoot": "apps/api/src",
|
"sourceRoot": "apps/api/src",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"deleteOutDir": true
|
"deleteOutDir": false
|
||||||
},
|
},
|
||||||
"projects": {
|
"projects": {
|
||||||
"api": {
|
"api": {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
"start": "nest start api",
|
"start": "nest start api",
|
||||||
"start:dev": "nest start api --watch",
|
"start:dev": "nest start api --watch",
|
||||||
"start:debug": "nest start api --debug --watch",
|
"start:debug": "nest start api --debug --watch",
|
||||||
"start:api": "node dist/apps/api/main.js",
|
"start:api": "node dist/apps/api/src/main.js",
|
||||||
"start:worker": "node dist/apps/worker/main.js",
|
"start:worker": "node dist/apps/worker/src/main.js",
|
||||||
"lint": "eslint \"{apps,libs}/**/*.ts\" --fix",
|
"lint": "eslint \"{apps,libs}/**/*.ts\" --fix",
|
||||||
"test": "jest --runInBand",
|
"test": "jest --runInBand",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
@@ -63,13 +63,10 @@
|
|||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
"testPathIgnorePatterns": ["<rootDir>/node_modules/", "<rootDir>/dist/"],
|
"testPathIgnorePatterns": ["<rootDir>/node_modules/", "<rootDir>/dist/"],
|
||||||
|
"setupFiles": ["<rootDir>/test/setup-env.ts"],
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
},
|
},
|
||||||
"moduleNameMapper": {
|
|
||||||
"^@travel/configuration$": "<rootDir>/libs/configuration/src",
|
|
||||||
"^@travel/infrastructure$": "<rootDir>/libs/infrastructure/src"
|
|
||||||
},
|
|
||||||
"collectCoverageFrom": [
|
"collectCoverageFrom": [
|
||||||
"apps/**/*.(t|j)s",
|
"apps/**/*.(t|j)s",
|
||||||
"libs/**/*.(t|j)s"
|
"libs/**/*.(t|j)s"
|
||||||
|
|||||||
2
backend/test/setup-env.ts
Normal file
2
backend/test/setup-env.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
process.env.DATABASE_URL ??= 'postgresql://test:test@localhost:5432/test';
|
||||||
|
process.env.REDIS_URL ??= 'redis://localhost:6379';
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"target": "ES2023",
|
"target": "ES2023",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
|
"rootDir": ".",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -20,10 +21,6 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"noFallthroughCasesInSwitch": false,
|
"noFallthroughCasesInSwitch": false
|
||||||
"paths": {
|
|
||||||
"@travel/configuration": ["libs/configuration/src"],
|
|
||||||
"@travel/infrastructure": ["libs/infrastructure/src"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user