diff --git a/apps/edr-freight-api/src/seed/edr-org.seeder.ts b/apps/edr-freight-api/src/seed/edr-org.seeder.ts index 6b8529225..9f10c3e0e 100644 --- a/apps/edr-freight-api/src/seed/edr-org.seeder.ts +++ b/apps/edr-freight-api/src/seed/edr-org.seeder.ts @@ -164,20 +164,25 @@ export class EdrOrgSeeder { manager: EntityManager, applicationId: string, ) { - const permissionRepository = manager.getRepository(Permission); - - // Upsert by key so reruns are idempotent; applicationId ties every - // permission to the EDR Freight application (also backfills rows that - // were previously seeded without the relation). - await permissionRepository.upsert( - EDR_FREIGHT_PERMISSIONS.map((permission) => ({ - id: permission.id, - key: permission.key, - name: { ...permission.name }, - applicationId, - })), - { conflictPaths: { key: true } }, - ); + // iam.permissions has TWO unique columns (PK id, UQ key) but ON CONFLICT + // can only target one. Seeding a hand-minted id that some older/retired key + // already owns in an environment slips past ON CONFLICT (key) and dies on + // the PK. The key is the identity every consumer resolves by (positions + // seeder maps key -> id at runtime), so ids are left to the column default + // and never sent — no id can collide. + await manager + .createQueryBuilder() + .insert() + .into(Permission) + .values( + EDR_FREIGHT_PERMISSIONS.map((permission) => ({ + key: permission.key, + name: { ...permission.name }, + applicationId, + })), + ) + .orUpdate(["name", "application_id"], ["key"]) + .execute(); this.logger.log( `Ensured ${EDR_FREIGHT_PERMISSIONS.length} permissions on application '${EDR_FREIGHT_APPLICATION.key}'`, diff --git a/apps/edr-freight-api/src/seed/freight-permissions.registry.spec.ts b/apps/edr-freight-api/src/seed/freight-permissions.registry.spec.ts new file mode 100644 index 000000000..caab85c3e --- /dev/null +++ b/apps/edr-freight-api/src/seed/freight-permissions.registry.spec.ts @@ -0,0 +1,13 @@ +import { EDR_FREIGHT_PERMISSIONS } from './edr-freight.seed'; + +describe('EDR_FREIGHT_PERMISSIONS', () => { + // The seeder inserts the whole catalog in one ON CONFLICT (key) DO UPDATE + // statement — a duplicated key there is a Postgres 21000 at boot, not a + // silent no-op. + it('has no duplicate keys', () => { + const keys = EDR_FREIGHT_PERMISSIONS.map((permission) => permission.key); + const duplicates = [...new Set(keys.filter((key, i) => keys.indexOf(key) !== i))]; + + expect(duplicates).toEqual([]); + }); +}); diff --git a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts index 788479329..38e1f9104 100644 --- a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts +++ b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts @@ -109,8 +109,12 @@ export const CONTRACT_PERMISSIONS: FreightPermissionSeed[] = [ perm('a3000001-0001-4000-8000-00000000001b', 'edr_freight_app:contracts:suspend', 'Suspend / resume a signed contract'), ]; -// Existing per-slug view ids are kept as-is: position-type grants reference -// them by id, so re-minting would orphan those rows. +// Historical ids. EdrOrgSeeder no longer sends them — it upserts on `key` and +// lets the column default mint the uuid — so these are kept only as a record of +// which ids each environment already holds. A hand-picked id must still never +// be recycled from a retired key: `edr_freight_app:rule_engine:truck_types:manage` +// owned …001b, and reusing it for transit-agents crashed boot with a PK 23505 +// on every environment that still had the retired row. const RULE_ENGINE_VIEW_IDS: Record = { 'cargo-types': 'b2000001-0001-4000-8000-000000000001', 'container-types': 'b2000001-0001-4000-8000-000000000003', @@ -124,7 +128,7 @@ const RULE_ENGINE_VIEW_IDS: Record = { rates: 'b2000001-0001-4000-8000-000000000011', 'approval-rules': 'b2000001-0001-4000-8000-000000000013', 'yard-distances': 'b2000001-0001-4000-8000-000000000018', - 'transit-agents': 'b2000001-0001-4000-8000-00000000001b', + 'transit-agents': 'b2000003-0001-4000-8000-000000000001', }; // CRUD replaces the retired coarse `:manage`. New ids live in a fresh block