feat(server): add nest AppConfigModule bound to @nestjs/config

This commit is contained in:
jubnl
2026-07-25 15:12:38 +02:00
parent 48ba9092c3
commit e17f165e44
9 changed files with 381 additions and 1 deletions
@@ -0,0 +1,38 @@
/**
* Global binding of @nestjs/config onto TREK's app-config layer.
*
* INVARIANTS — breaking either silently breaks ~60 env-mutating tests:
* - `cache: false`, always. `cache: true` would freeze ConfigService lookups
* for the whole app lifetime; the runtime-toggled vars (DEMO_MODE, NODE_ENV,
* OIDC_*, …) must stay live via RuntimeEnvService/readEnv().
* - No `validate`/`validationSchema` here. Fail-fast validation runs exactly
* once, from the production entrypoint (src/app-config/boot-validate.ts) —
* buildApp() is invoked dozens of times per test run and must stay
* side-effect-free.
* - `ignoreEnvFile: true`, always. dotenv is loaded ONCE in index.ts; tests
* deliberately never load .env, and reading it here would leak a
* developer's local server/.env into every integration test.
*
* The framework's ConfigModule is aliased ONLY inside this file: app.module.ts
* already imports a local `ConfigModule` (the /api/config endpoint module), so
* the alias keeps the two from colliding anywhere else.
*/
import { Global, Module } from '@nestjs/common';
import { ConfigModule as NestConfigModule } from '@nestjs/config';
import { BOOT_STABLE_TOKENS } from './tokens';
import { RuntimeEnvService } from './runtime-env.service';
@Global()
@Module({
imports: [
NestConfigModule.forRoot({
isGlobal: true,
ignoreEnvFile: true,
cache: false,
load: BOOT_STABLE_TOKENS,
}),
],
providers: [RuntimeEnvService],
exports: [RuntimeEnvService],
})
export class AppConfigModule {}
+13
View File
@@ -0,0 +1,13 @@
export { AppConfigModule } from './app-config.module';
export { RuntimeEnvService } from './runtime-env.service';
export {
httpConfig,
sessionConfig,
mcpConfig,
integrationsConfig,
backupConfig,
dbConfig,
pathsConfig,
netConfig,
BOOT_STABLE_TOKENS,
} from './tokens';
@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { readEnv, type AppEnv } from '../../app-config';
/**
* DI-friendly access to the RUNTIME-TOGGLED env values (DEMO_MODE, NODE_ENV,
* APP_VERSION, OIDC_*, …). Every method delegates to readEnv() at call time —
* never snapshot a namespace into a field or the constructor, or the ~60 tests
* that mutate process.env mid-lifetime break.
*
* Boot-stable values are NOT accessed through this service; inject their
* registerAs token instead (see tokens.ts).
*/
@Injectable()
export class RuntimeEnvService {
/** The full live-derived env — prefer the convenience getters where one exists. */
env(): AppEnv {
return readEnv();
}
isDemoMode(): boolean {
return readEnv().demo.enabled;
}
nodeEnv(): string | undefined {
return readEnv().app.nodeEnv;
}
isProduction(): boolean {
return readEnv().app.isProduction;
}
isDevelopment(): boolean {
return readEnv().app.isDevelopment;
}
/** Raw APP_VERSION — call sites keep their own fallback ('0.0.0' vs package.json vs semver). */
appVersion(): string | undefined {
return readEnv().app.appVersion;
}
/** Raw APP_URL — call sites keep their own trailing-slash handling. */
appUrl(): string | undefined {
return readEnv().app.appUrl;
}
}
+50
View File
@@ -0,0 +1,50 @@
/**
* registerAs tokens over the app-config derive functions, for the BOOT-STABLE
* namespaces only (see src/app-config/README.md for the classification).
*
* A token's factory runs during NestFactory.create(), so the injected object is
* a SNAPSHOT per built app — integration tests that mutate env and then rebuild
* buildApp() get fresh values automatically. Runtime-toggled values (DEMO_MODE,
* NODE_ENV, OIDC_*, …) must NOT get tokens here; they go through
* RuntimeEnvService / readEnv() so mid-lifetime env mutations stay visible.
*
* Consumption pattern:
* constructor(@Inject(httpConfig.KEY) private readonly http: ConfigType<typeof httpConfig>) {}
*/
import { registerAs } from '@nestjs/config';
import {
deriveBackup,
deriveDb,
deriveHttp,
deriveIntegrations,
deriveMcp,
deriveNet,
derivePaths,
deriveSession,
} from '../../app-config';
/**
* http is snapshot-consumed only by the pre-init Express layer (bootstrap
* threads it into applyGlobalMiddleware) — apply-time freeze per built app.
* Request-time reads of the same vars (cookie.ts FORCE_HTTPS checks) stay live
* via readEnv().
*/
export const httpConfig = registerAs('http', () => deriveHttp(process.env));
export const sessionConfig = registerAs('session', () => deriveSession(process.env));
export const mcpConfig = registerAs('mcp', () => deriveMcp(process.env));
export const integrationsConfig = registerAs('integrations', () => deriveIntegrations(process.env));
export const backupConfig = registerAs('backup', () => deriveBackup(process.env));
export const dbConfig = registerAs('db', () => deriveDb(process.env));
export const pathsConfig = registerAs('paths', () => derivePaths(process.env));
export const netConfig = registerAs('net', () => deriveNet(process.env));
export const BOOT_STABLE_TOKENS = [
httpConfig,
sessionConfig,
mcpConfig,
integrationsConfig,
backupConfig,
dbConfig,
pathsConfig,
netConfig,
];
+2 -1
View File
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
import { AppConfigModule } from './app-config/app-config.module';
import { DatabaseModule } from './database/database.module';
import { HealthController } from './health/health.controller';
import { HealthService } from './health/health.service';
@@ -52,7 +53,7 @@ import { IdempotencyInterceptor } from './common/idempotency.interceptor';
* migrated.
*/
@Module({
imports: [DatabaseModule, WeatherModule, HelpModule, AirportsModule, ConfigModule, SystemNoticesModule, MapsModule, CategoriesModule, TagsModule, NotificationsModule, AtlasModule, VacayModule, PackingModule, TodoModule, BudgetModule, ReservationsModule, DaysModule, AssignmentsModule, PlacesModule, TripsModule, CollabModule, FilesModule, PhotosModule, MemoriesModule, AirtrailModule, JourneyModule, CollectionsModule, ShareModule, TripInviteModule, TransitModule, FeedsModule, SettingsModule, BackupModule, AuthModule, OidcModule, OauthModule, AdminModule, AddonsModule, PluginsModule, BookingImportModule],
imports: [AppConfigModule, DatabaseModule, WeatherModule, HelpModule, AirportsModule, ConfigModule, SystemNoticesModule, MapsModule, CategoriesModule, TagsModule, NotificationsModule, AtlasModule, VacayModule, PackingModule, TodoModule, BudgetModule, ReservationsModule, DaysModule, AssignmentsModule, PlacesModule, TripsModule, CollabModule, FilesModule, PhotosModule, MemoriesModule, AirtrailModule, JourneyModule, CollectionsModule, ShareModule, TripInviteModule, TransitModule, FeedsModule, SettingsModule, BackupModule, AuthModule, OidcModule, OauthModule, AdminModule, AddonsModule, PluginsModule, BookingImportModule],
controllers: [HealthController],
providers: [
HealthService,