diff --git a/CLAUDE_NEW.md b/CLAUDE_NEW.md index 11075e9f0..a99f64d0a 100644 --- a/CLAUDE_NEW.md +++ b/CLAUDE_NEW.md @@ -154,13 +154,32 @@ no timeout and will wait forever. Migrations are the most dangerous surface in this repo. Two production-grade incidents have already come from it. -- `migrationsRun: true` — **migrations run automatically on API boot**, with - `migrationsTransactionMode: 'each'`. +- `migrationsRun: false` — **migrations do NOT run on API boot.** They run as a separate + one-shot step, via the Dockerfile's `migration` build target (`docker build --target + migration`), with `migrationsTransactionMode: 'each'`. + - CI: `.github/workflows/deploy.yml` builds the `migration` image and runs it + (`docker run --rm --env-file ...`) *before* building/deploying the app image. + - e2e: `docker-compose.e2e.yaml`'s `freight-migration-e2e` service runs once and + `freight-api-e2e` depends on it (`condition: service_completed_successfully`). + - Local dev (`docker-compose.yaml`) has no equivalent migration service yet — run + migrations yourself before `docker compose up freight-api`, e.g. + `docker build --target migration -f apps/edr-freight-api/Dockerfile -t freight-migration .` + then `docker run --rm --env-file apps/edr-freight-api/.env freight-migration`. Don't + use `pnpm run migrate` for this — it runs via `ts-node`, which never writes compiled + output to `dist/`, and the freight migrations glob only matches `dist/migrations/*.js`. + It silently applies zero freight migrations while exiting 0. - Consequences you must design for: - - Running several `nest start --watch` instances races `migrationsRun`. A non-idempotent - data migration can execute twice. Keep one instance. - A watch-mode hot reload does **not** re-run migrations. If you add a column that new code reads, apply it to the dev database yourself (idempotently) or fully restart. + - `apps/edr-freight-api/src/config/database.config.ts`'s `iamEntities` array is a + hand-maintained list of `@tria-plc/iamapi-common` entity classes. The live app never + notices when it's stale (`autoLoadEntities: true` papers over gaps via IAM's own + `forFeature()` registrations), but the standalone migration `DataSource` + (`data-source.ts`, no `autoLoadEntities`) does not have that fallback — a missing + entity throws `Entity metadata for X#y was not found` at `initialize()`, before a + single migration runs. **Every `@tria-plc/iamapi-common` version bump is a candidate + for this to break again** — diff the package's entity classes against `iamEntities` + when bumping it. - **Give every migration a unique timestamp.** 34 timestamps are currently shared by two or more migrations. TypeORM orders by timestamp and breaks ties non-deterministically. Before adding one, check the filename prefix is unused *and* higher than the newest recorded row. diff --git a/apps/edr-freight-api/package.json b/apps/edr-freight-api/package.json index 603a6b6fc..cd87dd2d3 100644 --- a/apps/edr-freight-api/package.json +++ b/apps/edr-freight-api/package.json @@ -58,8 +58,8 @@ "@nestjs/swagger": "^11.4.2", "@nestjs/typeorm": "^11.0.1", "@nestjs/websockets": "^11.1.27", - "@tria-plc/api-common": "file:../../local-packages/tria-plc-api-common-1.4.3.tgz", - "@tria-plc/iamapi-common": "file:../../local-packages/tria-plc-iamapi-common-0.7.12.tgz", + "@tria-plc/api-common": "file:../../local-packages/tria-plc-api-common-1.6.0.tgz", + "@tria-plc/iamapi-common": "file:../../local-packages/tria-plc-iamapi-common-1.0.0.tgz", "amqp-connection-manager": "^5.0.0", "amqplib": "^2.0.1", "axios": "^1.16.1", diff --git a/apps/edr-freight-api/src/config/database.config.ts b/apps/edr-freight-api/src/config/database.config.ts index 65029819d..0591cd0b1 100644 --- a/apps/edr-freight-api/src/config/database.config.ts +++ b/apps/edr-freight-api/src/config/database.config.ts @@ -45,8 +45,30 @@ import { NotificationTemplate, } from "@tria-plc/iamapi-common"; import { OrganizationSetting } from "@tria-plc/iamapi-common/entities/iam/organization-structure/organization-setting.entity"; +import { UnitSetting } from "@tria-plc/iamapi-common/entities/iam/organization-structure/unit-setting.entity"; +import { UnitDetail } from "@tria-plc/iamapi-common/entities/iam/organization-structure/unit-detail.entity"; +import { OrganizationDetail } from "@tria-plc/iamapi-common/entities/iam/organization-structure/organization-detail.entity"; +import { UnitCluster } from "@tria-plc/iamapi-common/entities/iam/organization-structure/unit-cluster.entity"; +import { Location } from "@tria-plc/iamapi-common/entities/iam/organization-structure/location.entity"; +import { LocationType } from "@tria-plc/iamapi-common/entities/iam/organization-structure/location-type.entity"; +import { DelegationTerminationReason } from "@tria-plc/iamapi-common/entities/iam/organization-structure/delegation-termination-reason.entity"; +import { EmployeePositionActivePeriod } from "@tria-plc/iamapi-common/entities/iam/organization-structure/employee-position-active-period.entity"; +import { UnitConfiguration } from "@tria-plc/iamapi-common/entities/iam/organization-structure/unit-configuration.entity"; +import { Site } from "@tria-plc/iamapi-common/entities/iam/site/site.entity"; +import { SiteSetting } from "@tria-plc/iamapi-common/entities/iam/site/site-setting.entity"; const iamEntities = [ + UnitSetting, + UnitDetail, + OrganizationDetail, + UnitCluster, + Location, + LocationType, + DelegationTerminationReason, + EmployeePositionActivePeriod, + UnitConfiguration, + Site, + SiteSetting, DefaultPosition, DefaultUnit, EmployeePosition, diff --git a/apps/edr-freight-api/src/data-source.ts b/apps/edr-freight-api/src/data-source.ts index 03baa31e2..4b46912b7 100644 --- a/apps/edr-freight-api/src/data-source.ts +++ b/apps/edr-freight-api/src/data-source.ts @@ -1,11 +1,8 @@ // apps/edr-freight-api/src/data-source.ts import "dotenv/config"; -import { DataSource, DataSourceOptions } from "typeorm"; +import { DataSource } from "typeorm"; import { buildDataSourceOptions } from "./config/database.config"; -export const AppDataSource = new DataSource({ - ...buildDataSourceOptions(), - schema: "freight", -} as DataSourceOptions); +export const AppDataSource = new DataSource(buildDataSourceOptions()); export default AppDataSource; diff --git a/docker-compose.e2e.yaml b/docker-compose.e2e.yaml index b00402761..14f1171dc 100644 --- a/docker-compose.e2e.yaml +++ b/docker-compose.e2e.yaml @@ -61,6 +61,27 @@ services: - mc alias set e2e http://minio-e2e:9000 e2e-minio e2e-minio-secret && mc mb --ignore-existing e2e/fhc restart: "no" + # One-shot: runs schema creation + migrations against postgres-freight-e2e, + # then exits. freight-api-e2e no longer migrates itself on boot (migrationsRun + # is false) — this is the CI "migration" stage, run here the same way. + freight-migration-e2e: + build: + context: . + dockerfile: apps/edr-freight-api/Dockerfile + target: migration + secrets: + - npmrc + depends_on: + postgres-freight-e2e: + condition: service_healthy + environment: + DB_HOST: postgres-freight-e2e + DB_PORT: "5432" + DB_USER: edr_e2e + DB_PASSWORD: edr_e2e + DB_NAME: edr_freight_e2e + restart: "no" + freight-api-e2e: build: context: . @@ -74,6 +95,8 @@ services: condition: service_healthy minio-init-e2e: condition: service_completed_successfully + freight-migration-e2e: + condition: service_completed_successfully environment: PORT: "3001" DB_HOST: postgres-freight-e2e @@ -112,7 +135,8 @@ services: ports: - "${E2E_API_PORT:-3101}:3001" healthcheck: - # Boot runs 240+ migrations + seeders on first start — generous start_period. + # Migrations run in freight-migration-e2e before this container even + # starts (depends_on above) — boot here is just Nest bootstrap + seeders. test: [ "CMD", @@ -123,7 +147,7 @@ services: interval: 5s timeout: 5s retries: 12 - start_period: 180s + start_period: 60s freight-portal-e2e: build: diff --git a/local-packages/tria-plc-api-common-1.6.0.tgz b/local-packages/tria-plc-api-common-1.6.0.tgz new file mode 100644 index 000000000..3796d671a Binary files /dev/null and b/local-packages/tria-plc-api-common-1.6.0.tgz differ diff --git a/local-packages/tria-plc-iamapi-common-1.0.0.tgz b/local-packages/tria-plc-iamapi-common-1.0.0.tgz new file mode 100644 index 000000000..a19d462d6 Binary files /dev/null and b/local-packages/tria-plc-iamapi-common-1.0.0.tgz differ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b89d65b6..69fbfe47e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,11 +94,11 @@ importers: specifier: ^11.1.27 version: 11.1.27(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/platform-socket.io@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@tria-plc/api-common': - specifier: file:../../local-packages/tria-plc-api-common-1.4.3.tgz - version: file:local-packages/tria-plc-api-common-1.4.3.tgz(5f5e627b5382f261b5d3edb76e50f0e2) + specifier: file:../../local-packages/tria-plc-api-common-1.6.0.tgz + version: file:local-packages/tria-plc-api-common-1.6.0.tgz(d194f7ef21135288330b07fecd9a2489) '@tria-plc/iamapi-common': - specifier: file:../../local-packages/tria-plc-iamapi-common-0.7.12.tgz - version: file:local-packages/tria-plc-iamapi-common-0.7.12.tgz(578386f46cf99fd4720e3e99f196f69e) + specifier: file:../../local-packages/tria-plc-iamapi-common-1.0.0.tgz + version: file:local-packages/tria-plc-iamapi-common-1.0.0.tgz(2079b56e9fb8fa788c1a142167448388) amqp-connection-manager: specifier: ^5.0.0 version: 5.0.0(amqplib@2.0.1) @@ -4488,9 +4488,25 @@ packages: rxjs: ^7.8.0 typeorm: ^0.3.0 - '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.12.tgz': - resolution: {integrity: sha512-9lZ5t3WzjcRrIEJu0rywzWXlmV+YteYqaYjiYP4T6ETZ9K7LIOXKM1gd2vK5hfrlIh6BPPcsIIhairmJgmpwEQ==, tarball: file:local-packages/tria-plc-iamapi-common-0.7.12.tgz} - version: 0.7.12 + '@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.6.0.tgz': + resolution: {integrity: sha512-SZomla65xesBQZ12n8xH+9eX0TRbXNWToQ3SNURLhP1zlHJWUMTVHoRXTd5zWoe4mqah2Lr83L8ueHERsqCTFw==, tarball: file:local-packages/tria-plc-api-common-1.6.0.tgz} + version: 1.6.0 + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/jwt': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/passport': ^11.0.0 + '@nestjs/swagger': ^11.0.0 + '@nestjs/throttler': ^6.0.0 + '@nestjs/typeorm': ^11.0.0 + reflect-metadata: ^0.2.0 + rxjs: ^7.8.0 + typeorm: ^0.3.0 + + '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.9.tgz': + resolution: {integrity: sha512-Y6SDEJUR4NcwLXrRJFZ+SbknpczybMwp59cR000vjFEu09RClLr5Gzv8TaJbOeDytyhdgjkZriVNPb2Dt6tipA==, tarball: file:local-packages/tria-plc-iamapi-common-0.7.9.tgz} + version: 0.7.9 engines: {node: '>=20'} peerDependencies: '@nestjs/axios': ^4.0.0 @@ -4510,9 +4526,9 @@ packages: rxjs: ^7.8.0 typeorm: ^0.3.0 - '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.9.tgz': - resolution: {integrity: sha512-Y6SDEJUR4NcwLXrRJFZ+SbknpczybMwp59cR000vjFEu09RClLr5Gzv8TaJbOeDytyhdgjkZriVNPb2Dt6tipA==, tarball: file:local-packages/tria-plc-iamapi-common-0.7.9.tgz} - version: 0.7.9 + '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-1.0.0.tgz': + resolution: {integrity: sha512-rfHSXOm/0VUMTj7HrvYysrEAqxItqfPs4Rd35qWJyaAk+snF1wTKFRVz6cRGPoQNj8fhplkVAefnvuKBuHT+xQ==, tarball: file:local-packages/tria-plc-iamapi-common-1.0.0.tgz} + version: 1.0.0 engines: {node: '>=20'} peerDependencies: '@nestjs/axios': ^4.0.0 @@ -16262,7 +16278,7 @@ snapshots: - debug - supports-color - '@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.4.3.tgz(5f5e627b5382f261b5d3edb76e50f0e2)': + '@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.6.0.tgz(d194f7ef21135288330b07fecd9a2489)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2) '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -16273,7 +16289,6 @@ snapshots: '@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))) - '@tria-plc/iamapi-common': file:local-packages/tria-plc-iamapi-common-0.7.12.tgz(578386f46cf99fd4720e3e99f196f69e) argon2: 0.43.1 axios: 1.17.0 change-case: 5.4.4 @@ -16306,7 +16321,7 @@ snapshots: - debug - supports-color - '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.12.tgz(578386f46cf99fd4720e3e99f196f69e)': + '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.9.tgz(c97ba831ddde82920910406ab5262991)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2) '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -16314,10 +16329,10 @@ snapshots: '@nestjs/jwt': 10.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/websockets@11.1.27)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) - '@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))) - '@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(5f5e627b5382f261b5d3edb76e50f0e2) + '@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(59a15a37c5b1c12685ed78e172f27e65) api-common: 1.2.2 argon2: 0.43.1 axios: 1.17.0 @@ -16341,7 +16356,7 @@ snapshots: - '@faker-js/faker' - supports-color - '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.9.tgz(c97ba831ddde82920910406ab5262991)': + '@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-1.0.0.tgz(2079b56e9fb8fa788c1a142167448388)': dependencies: '@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2) '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -16349,11 +16364,10 @@ snapshots: '@nestjs/jwt': 10.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/websockets@11.1.27)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) - '@nestjs/swagger': 7.4.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2) '@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))) - '@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(59a15a37c5b1c12685ed78e172f27e65) - api-common: 1.2.2 + '@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.6.0.tgz(d194f7ef21135288330b07fecd9a2489) argon2: 0.43.1 axios: 1.17.0 class-transformer: 0.5.1