mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 01:48:12 +00:00
- Implemented utility to calculate wagon usage metrics for train schedules. - Created for sending wagons to maintenance with optional notes. - Added unit tests for train builder maintenance functionalities, including formatting train run labels and building maintenance notes. - Developed component for merging train schedules with detailed previews and reasons for merging. - Introduced component for selecting wagons with search functionality and selection limits. - Created for displaying and filtering audit logs, including detailed views of individual log entries. - Added for handling API interactions related to audit logs, including fetching logs and entity types.
239 lines
7.4 KiB
TypeScript
239 lines
7.4 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import {
|
|
Application,
|
|
Organization,
|
|
Permission,
|
|
Position,
|
|
PositionPermission,
|
|
Unit,
|
|
} from '@tria-plc/iamapi-common';
|
|
import { DataSource, EntityManager, In } from 'typeorm';
|
|
|
|
import { EDR_FREIGHT_APPLICATION, EDR_FREIGHT_POSITIONS } from './edr-freight.seed';
|
|
|
|
/**
|
|
* Turn a permission key into a readable fallback name for a row this seeder has
|
|
* to mint itself: `edr_freight_app:train_scheduling:edit_train_number` becomes
|
|
* "Train scheduling: edit train number". Only used for keys absent from the
|
|
* catalog — a key that IS catalogued keeps its curated Amharic/English name.
|
|
*/
|
|
const nameForKey = (key: string): { en: string } => {
|
|
const [, ...rest] = key.split(':');
|
|
const [resource, ...action] = rest;
|
|
const humanize = (s: string) => s.replace(/_/g, ' ');
|
|
const label = action.length
|
|
? `${humanize(resource)}: ${humanize(action.join(' '))}`
|
|
: humanize(resource);
|
|
return { en: label.charAt(0).toUpperCase() + label.slice(1) };
|
|
};
|
|
|
|
const SEED_FLAG = 'SEED_EDR_ORG';
|
|
const EDR_ORG_KEY = 'edr_freight';
|
|
const EDR_UNIT_KEY = 'edr_freight_app';
|
|
|
|
/**
|
|
* Seeds the operational freight positions (CEO, Chief, Director, Marketer,
|
|
* Operation, Ethiopian GL, Djibouti GL) as Position + PositionPermission rows
|
|
* on the `edr_freight_app` unit. Positions-as-roles: users get their freight
|
|
* access by being assigned to a Position (via EmployeePosition), and the
|
|
* position's PositionPermission grants come from EDR_FREIGHT_POSITIONS.
|
|
*
|
|
* Gated behind the same SEED_EDR_ORG flag as EdrOrgSeeder and depends on the
|
|
* org/unit/permission catalog it seeds, so it must run AFTER EdrOrgSeeder.
|
|
* Idempotent: positions upsert by (key, unitId); grants insert only the
|
|
* permission ids a position is still missing.
|
|
*/
|
|
@Injectable()
|
|
export class FreightPositionsSeeder {
|
|
private readonly logger = new Logger(FreightPositionsSeeder.name);
|
|
|
|
constructor(private readonly dataSource: DataSource) {}
|
|
|
|
async run() {
|
|
if (process.env[SEED_FLAG]?.trim().toLowerCase() !== 'true') {
|
|
this.logger.log(
|
|
`Skipping freight positions seed because ${SEED_FLAG} is not enabled`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
await this.dataSource.transaction(async (manager) => {
|
|
const organization = await manager.getRepository(Organization).findOne({
|
|
where: { key: EDR_ORG_KEY },
|
|
select: { id: true },
|
|
});
|
|
|
|
if (!organization) {
|
|
throw new Error(`missing_organization:${EDR_ORG_KEY}`);
|
|
}
|
|
|
|
const unit = await manager.getRepository(Unit).findOne({
|
|
where: { key: EDR_UNIT_KEY, organizationId: organization.id },
|
|
select: { id: true },
|
|
});
|
|
|
|
if (!unit) {
|
|
throw new Error(`missing_unit:${EDR_UNIT_KEY}`);
|
|
}
|
|
|
|
const permissionKeyToId = await this.loadPermissionIds(manager);
|
|
|
|
for (const seed of EDR_FREIGHT_POSITIONS) {
|
|
const positionId = await this.ensurePosition(
|
|
manager,
|
|
seed,
|
|
unit.id as string,
|
|
organization.id as string,
|
|
);
|
|
|
|
await this.ensurePositionPermissions(
|
|
manager,
|
|
positionId,
|
|
seed,
|
|
permissionKeyToId,
|
|
);
|
|
}
|
|
});
|
|
|
|
this.logger.log(
|
|
`Ensured ${EDR_FREIGHT_POSITIONS.length} freight positions on unit '${EDR_UNIT_KEY}'`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolve every permission key referenced by any position to its id, minting
|
|
* the rows that do not exist yet.
|
|
*
|
|
* The position presets draw from FREIGHT_PERMS (the registry), which is
|
|
* broader than the EDR_FREIGHT_PERMISSIONS catalog EdrOrgSeeder inserts —
|
|
* module keys like `train_scheduling:*` live only in the registry. So every
|
|
* newly-added preset key would otherwise abort boot with
|
|
* `missing_permissions:<key>` until someone hand-inserted it. Ensuring them
|
|
* here keeps this seeder self-sufficient: it declares the keys it needs, so
|
|
* it is the one that guarantees they exist. Same approach, and the same
|
|
* id-less insert reasoning, as FreightNotificationPermissionsSeeder.
|
|
*/
|
|
private async loadPermissionIds(
|
|
manager: EntityManager,
|
|
): Promise<Map<string, string>> {
|
|
const keys = [
|
|
...new Set(EDR_FREIGHT_POSITIONS.flatMap((p) => p.permissionKeys)),
|
|
];
|
|
|
|
const read = async () => {
|
|
const rows = await manager.getRepository(Permission).find({
|
|
where: { key: In(keys) },
|
|
select: { id: true, key: true },
|
|
});
|
|
return new Map(rows.map((p) => [p.key, p.id as string]));
|
|
};
|
|
|
|
let map = await read();
|
|
const missing = keys.filter((key) => !map.has(key));
|
|
if (missing.length === 0) {
|
|
return map;
|
|
}
|
|
|
|
const application = await manager.getRepository(Application).findOne({
|
|
where: { key: EDR_FREIGHT_APPLICATION.key },
|
|
select: { id: true },
|
|
});
|
|
if (!application?.id) {
|
|
throw new Error(`missing_application:${EDR_FREIGHT_APPLICATION.key}`);
|
|
}
|
|
|
|
// Ids are left to the column default and never sent: iam.permissions has
|
|
// two unique columns (PK id, UQ key) and ON CONFLICT can only target one,
|
|
// so a hand-minted id already owned by a retired key would slip past
|
|
// ON CONFLICT (key) and die on the PK.
|
|
await manager
|
|
.createQueryBuilder()
|
|
.insert()
|
|
.into(Permission)
|
|
.values(
|
|
missing.map((key) => ({
|
|
key,
|
|
name: nameForKey(key),
|
|
applicationId: application.id as string,
|
|
})),
|
|
)
|
|
.orIgnore()
|
|
.execute();
|
|
|
|
this.logger.log(
|
|
`Seeded ${missing.length} permission(s) referenced by positions but absent from the catalog: ${missing.join(', ')}`,
|
|
);
|
|
|
|
map = await read();
|
|
const stillMissing = keys.filter((key) => !map.has(key));
|
|
if (stillMissing.length > 0) {
|
|
throw new Error(`missing_permissions:${stillMissing.join(',')}`);
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
private async ensurePosition(
|
|
manager: EntityManager,
|
|
seed: (typeof EDR_FREIGHT_POSITIONS)[number],
|
|
unitId: string,
|
|
organizationId: string,
|
|
): Promise<string> {
|
|
const positionRepository = manager.getRepository(Position);
|
|
|
|
const existing = await positionRepository.findOne({
|
|
where: { key: seed.key, unitId },
|
|
select: { id: true },
|
|
});
|
|
|
|
if (existing) {
|
|
return existing.id as string;
|
|
}
|
|
|
|
const inserted = await positionRepository.insert({
|
|
key: seed.key,
|
|
name: { ...seed.name },
|
|
rank: seed.rank,
|
|
unitId,
|
|
organizationId,
|
|
});
|
|
|
|
this.logger.log(`Seeded freight position '${seed.key}'`);
|
|
|
|
return inserted.identifiers[0]?.id as string;
|
|
}
|
|
|
|
private async ensurePositionPermissions(
|
|
manager: EntityManager,
|
|
positionId: string,
|
|
seed: (typeof EDR_FREIGHT_POSITIONS)[number],
|
|
permissionKeyToId: Map<string, string>,
|
|
) {
|
|
const positionPermissionRepository =
|
|
manager.getRepository(PositionPermission);
|
|
|
|
const existing = await positionPermissionRepository.find({
|
|
where: { positionId },
|
|
select: { permissionId: true },
|
|
});
|
|
const existingPermissionIds = new Set(
|
|
existing.map((row) => row.permissionId),
|
|
);
|
|
|
|
const rowsToInsert = seed.permissionKeys
|
|
.map((key) => permissionKeyToId.get(key) as string)
|
|
.filter((permissionId) => !existingPermissionIds.has(permissionId))
|
|
.map((permissionId) => ({ positionId, permissionId }));
|
|
|
|
if (rowsToInsert.length === 0) {
|
|
return;
|
|
}
|
|
|
|
await positionPermissionRepository.insert(rowsToInsert);
|
|
|
|
this.logger.log(
|
|
`Granted ${rowsToInsert.length} permissions to position '${seed.key}'`,
|
|
);
|
|
}
|
|
}
|