per-user trade-direction access scope

This commit is contained in:
Marshal
2026-08-02 22:29:58 +00:00
parent c055abe8c1
commit f4fd469643
47 changed files with 1451 additions and 107 deletions

View File

@@ -0,0 +1,43 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
/**
* Per-backoffice-user trade-direction scope (import / export / intercity).
* A user with no row (or all three directions) is unrestricted. Admins
* (super_admin / organization_admin) bypass the scope entirely.
*/
export class CreateUserTradeAccess3150000000000 implements MigrationInterface {
name = 'CreateUserTradeAccess3150000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'user_trade_access',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' },
// IAM user id (iam.users) — no FK, iam schema is externally owned.
{ name: 'user_id', type: 'uuid', isUnique: true },
// Comma-separated subset of IMPORT,EXPORT,DOMESTIC (simple-array).
{ name: 'directions', type: 'text', default: "''" },
{ name: 'updated_by_id', type: 'uuid', isNullable: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createIndex(
'freight.user_trade_access',
new TableIndex({
name: 'idx_user_trade_access_user_id',
columnNames: ['user_id'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('freight.user_trade_access', true);
}
}

View File

@@ -0,0 +1,45 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Handling a freight type is not the same as handling it in both directions. A
* facility can be equipped to load containers onto a train but have no yard
* space to receive and stage inbound ones, so the origin and destination sides
* are stated independently per freight type.
*
* Backfilled from `handles_container` / `handles_bulk` so every existing row
* keeps its current behaviour: a facility that handles a type today handles it
* on both sides until someone narrows it in the backoffice. New rows default
* false — an unconfigured facility offers nothing rather than silently
* offering everything.
*/
export class YardFacilityOriginDestination3170000000000 implements MigrationInterface {
name = 'YardFacilityOriginDestination3170000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.yard_facilities
ADD COLUMN IF NOT EXISTS has_container_facility_origin boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS has_bulk_facility_origin boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS has_container_facility_destination boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS has_bulk_facility_destination boolean NOT NULL DEFAULT false
`);
await queryRunner.query(`
UPDATE freight.yard_facilities
SET has_container_facility_origin = handles_container,
has_container_facility_destination = handles_container,
has_bulk_facility_origin = handles_bulk,
has_bulk_facility_destination = handles_bulk
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.yard_facilities
DROP COLUMN IF EXISTS has_container_facility_origin,
DROP COLUMN IF EXISTS has_bulk_facility_origin,
DROP COLUMN IF EXISTS has_container_facility_destination,
DROP COLUMN IF EXISTS has_bulk_facility_destination
`);
}
}