This commit is contained in:
2025-12-26 20:54:20 +01:00
parent 904feb41b8
commit 6389be4f0c
26 changed files with 745 additions and 195 deletions

View File

@@ -17,12 +17,20 @@ import { RaceModule } from './domain/race/RaceModule';
import { SponsorModule } from './domain/sponsor/SponsorModule';
import { TeamModule } from './domain/team/TeamModule';
import { getApiPersistence, getEnableBootstrap } from './env';
const API_PERSISTENCE = getApiPersistence();
const USE_DATABASE = API_PERSISTENCE === 'postgres';
// Keep bootstrap on by default; tests can disable explicitly.
const ENABLE_BOOTSTRAP = getEnableBootstrap();
@Module({
imports: [
HelloModule,
DatabaseModule,
...(USE_DATABASE ? [DatabaseModule] : []),
LoggingModule,
BootstrapModule,
...(ENABLE_BOOTSTRAP ? [BootstrapModule] : []),
AnalyticsModule,
AuthModule,
DashboardModule,

34
apps/api/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,34 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV?: 'development' | 'production' | 'test';
// API runtime toggles
GRIDPILOT_API_PERSISTENCE?: 'postgres' | 'inmemory';
GRIDPILOT_API_BOOTSTRAP?: string;
GENERATE_OPENAPI?: string;
// Database (TypeORM)
DATABASE_URL?: string;
DATABASE_HOST?: string;
DATABASE_PORT?: string;
DATABASE_USER?: string;
DATABASE_PASSWORD?: string;
DATABASE_NAME?: string;
// Policy / operational mode
GRIDPILOT_POLICY_CACHE_MS?: string;
GRIDPILOT_POLICY_PATH?: string;
GRIDPILOT_OPERATIONAL_MODE?: string;
GRIDPILOT_FEATURES_JSON?: string;
GRIDPILOT_MAINTENANCE_ALLOW_VIEW?: string;
GRIDPILOT_MAINTENANCE_ALLOW_MUTATE?: string;
// Authorization
GRIDPILOT_AUTHZ_CACHE_MS?: string;
GRIDPILOT_USER_ROLES_JSON?: string;
}
}
}
export {};

62
apps/api/src/env.ts Normal file
View File

@@ -0,0 +1,62 @@
export type ApiPersistence = 'postgres' | 'inmemory';
function isTruthyEnv(value: string | undefined): boolean {
if (!value) return false;
return value !== '0' && value.toLowerCase() !== 'false';
}
function isSet(value: string | undefined): boolean {
return value !== undefined;
}
function readLower(name: string): string | undefined {
const raw = process.env[name];
if (raw === undefined) return undefined;
return raw.toLowerCase();
}
function requireOneOf<T extends string>(name: string, value: string, allowed: readonly T[]): T {
if ((allowed as readonly string[]).includes(value)) {
return value as T;
}
const valid = allowed.join(', ');
throw new Error(`Invalid ${name}: "${value}". Must be one of: ${valid}`);
}
/**
* Controls whether the API uses Postgres or runs in-memory.
*
* If `GRIDPILOT_API_PERSISTENCE` is set, it must be `postgres|inmemory`.
* Otherwise, it falls back to: `postgres` when `DATABASE_URL` exists, else `inmemory`.
*/
export function getApiPersistence(): ApiPersistence {
const configured = readLower('GRIDPILOT_API_PERSISTENCE');
if (configured) {
return requireOneOf('GRIDPILOT_API_PERSISTENCE', configured, ['postgres', 'inmemory'] as const);
}
return process.env.DATABASE_URL ? 'postgres' : 'inmemory';
}
/**
* Keep bootstrap on by default; tests can disable explicitly.
*
* `GRIDPILOT_API_BOOTSTRAP` uses "truthy" parsing:
* - false when unset / "0" / "false"
* - true otherwise
*/
export function getEnableBootstrap(): boolean {
const raw = process.env.GRIDPILOT_API_BOOTSTRAP;
if (raw === undefined) return true;
return isTruthyEnv(raw);
}
/**
* When set, the API will generate `openapi.json` and optionally reduce logging noise.
*
* Matches previous behavior: any value (even "0") counts as enabled if the var is present.
*/
export function getGenerateOpenapi(): boolean {
return isSet(process.env.GENERATE_OPENAPI);
}

View File

@@ -11,8 +11,11 @@ import { AuthenticationGuard } from './domain/auth/AuthenticationGuard';
import { AuthorizationGuard } from './domain/auth/AuthorizationGuard';
import { FeatureAvailabilityGuard } from './domain/policy/FeatureAvailabilityGuard';
import { getGenerateOpenapi } from './env';
async function bootstrap() {
const app = await NestFactory.create(AppModule, process.env.GENERATE_OPENAPI ? { logger: false } : undefined);
const generateOpenapi = getGenerateOpenapi();
const app = await NestFactory.create(AppModule, generateOpenapi ? { logger: false } : undefined);
// Website runs on a different origin in dev/docker (e.g. http://localhost:3000 -> http://localhost:3001),
// and our website HTTP client uses `credentials: 'include'`, so we must support CORS with credentials.
@@ -64,7 +67,7 @@ async function bootstrap() {
SwaggerModule.setup('api/docs', app as any, document);
// Export OpenAPI spec as JSON file when GENERATE_OPENAPI env var is set
if (process.env.GENERATE_OPENAPI) {
if (generateOpenapi) {
const outputPath = join(__dirname, '../openapi.json');
writeFileSync(outputPath, JSON.stringify(document, null, 2));
console.log(`✅ OpenAPI spec generated at: ${outputPath}`);