import { dirname } from "path"; import { registerAs } from "@nestjs/config"; import { TypeOrmModuleOptions } from "@nestjs/typeorm"; import { DataSourceOptions } from "typeorm"; /** * Finance migrations are recorded in `finance.migrations`, alongside * `iam.typeorm_migrations`, `freight.migrations` and `hr.migrations` in the same * database. Each app owns exactly one history table; Finance never writes * another app's. */ export const FINANCE_MIGRATIONS = { schema: "finance", table: "migrations", } as const; /** * IAM entity registration. * * `IamModule` is embedded in this app (the platform pattern: every service takes * the IAM package as a dependency and IAM gates each action by the caller's * position), so its entities must be on this DataSource. * * Registered as GLOBS over the two package dists rather than as a hand-written * class list — copied from `apps/edr-hr-api/src/config/database.config.ts`, * which documents why: freight-api's hand-listed array goes stale on every * package bump, and globs cannot. * * BOTH dists are required. Some IAM entities relate to notification entities * that physically live in `@tria-plc/api-common` — the IAM barrel only * re-exports them — so registering only the IAM dist throws * `Entity metadata for User#sessions was not found` at boot. */ function resolvePackageDist(pkg: string): string { // Node honours each package's `exports` map at runtime even though TypeScript's // resolution does not, so require.resolve on the barrel lands in dist/. return dirname(require.resolve(pkg)).replace(/\\/g, "/"); } const IAM_ENTITY_GLOBS = [ `${resolvePackageDist("@tria-plc/iamapi-common")}/entities/**/*.entity.{ts,js}`, `${resolvePackageDist("@tria-plc/api-common")}/entities/**/*.entity.{ts,js}`, ]; function buildConnectionOptions() { return { type: "postgres" as const, host: process.env.DB_HOST ?? "localhost", port: parseInt(process.env.DB_PORT ?? "5432", 10), username: process.env.DB_USER ?? "edr", password: process.env.DB_PASSWORD ?? "edr_secret", database: process.env.DB_NAME ?? "edr_database", // Do NOT pass `extra.options: '-c search_path=...'` — the connection pooler // fronting the dev database rejects the Postgres `options` startup parameter // with `08P01`. search_path is applied per physical connection in a pool // `connect` handler instead (see app.module.ts). synchronize: false, logging: process.env.TYPEORM_LOGGING === "true" ? true : (["error", "warn"] as DataSourceOptions["logging"]), }; } /** * Runtime options for the API. Carries no migrations — see `migration:run`. * * One connection serves both schemas: Finance entities are * `@Entity({schema:"finance"})` and IAM's are `schema:"iam"`, so they cannot * collide. `autoLoadEntities` is OFF because everything is listed explicitly * here — leaving it on lets IamModule's `forFeature` registrations add a PARTIAL * entity set, which is the exact configuration that failed at boot in HR. */ export function buildDataSourceOptions(): DataSourceOptions { return { ...buildConnectionOptions(), schema: FINANCE_MIGRATIONS.schema, entities: [__dirname + "/../**/*.entity.{ts,js}", ...IAM_ENTITY_GLOBS], migrations: [], }; } /** * Finance migrations only, recorded in `finance.migrations`. * * Deliberately no entities: this DataSource exists to run Finance's own DDL. * Finance never migrates `iam`, `freight`, `passenger`, `hr` or `edr_payment` — * it only ever READS those, and each belongs to its own owner. */ export function buildFinanceMigrationDataSourceOptions(): DataSourceOptions { return { ...buildConnectionOptions(), schema: FINANCE_MIGRATIONS.schema, entities: [], migrations: [__dirname + "/../migrations/*.js"], migrationsTableName: FINANCE_MIGRATIONS.table, migrationsTransactionMode: "each", }; } export default registerAs( "database", (): TypeOrmModuleOptions => ({ ...buildDataSourceOptions(), autoLoadEntities: false, // Migrations run as a separate one-shot step, never on API boot (house rule). migrationsRun: false, }), );