mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +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.
141 lines
4.6 KiB
TypeScript
141 lines
4.6 KiB
TypeScript
import { AUDIT_ENDPOINTS, type AuditEndpointMeta } from './audit-endpoints';
|
|
|
|
/** What a matched request resolved to. */
|
|
export interface MatchedAuditEndpoint {
|
|
/** Human-readable action, e.g. "Approve contract". */
|
|
title: string;
|
|
/** Primary entity, e.g. "Contract". */
|
|
type: string;
|
|
/** The route template, e.g. `/api/contracts/:id/cancel`. */
|
|
routePath: string;
|
|
/** First path parameter of the template, when the route has one. */
|
|
resourceId: string | null;
|
|
}
|
|
|
|
interface CompiledRoute {
|
|
regex: RegExp;
|
|
/** Param names in capture-group order, e.g. ['id', 'stepId']. */
|
|
paramNames: string[];
|
|
routePath: string;
|
|
meta: AuditEndpointMeta;
|
|
/** Literal (non-parameter) segment count — used to rank specificity. */
|
|
staticSegments: number;
|
|
}
|
|
|
|
const ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g;
|
|
|
|
/**
|
|
* Two keys in AUDIT_ENDPOINTS point at the same path: one route is declared by
|
|
* two different controllers, so the generator suffixed the second with
|
|
* ` [modules/...controller.ts]` to keep both entries. Only the path itself is
|
|
* matchable, so the suffix is stripped here.
|
|
*/
|
|
function stripSourceSuffix(key: string): string {
|
|
const bracket = key.indexOf(' [');
|
|
return bracket === -1 ? key : key.slice(0, bracket);
|
|
}
|
|
|
|
/**
|
|
* Compile one `"/api/contracts/:id/cancel"` template into an anchored regex.
|
|
*
|
|
* A parameter matches a single path segment only (`[^/]+`), so
|
|
* `/api/contracts/:id` cannot swallow `/api/contracts/:id/cancel`.
|
|
*/
|
|
function compileTemplate(path: string): { regex: RegExp; paramNames: string[] } {
|
|
const paramNames: string[] = [];
|
|
const pattern = path
|
|
.split('/')
|
|
.map((segment) => {
|
|
if (!segment.startsWith(':')) {
|
|
return segment.replace(ESCAPE_REGEX, '\\$&');
|
|
}
|
|
paramNames.push(segment.slice(1));
|
|
return '([^/]+)';
|
|
})
|
|
.join('/');
|
|
|
|
return { regex: new RegExp(`^${pattern}$`), paramNames };
|
|
}
|
|
|
|
/**
|
|
* Method-bucketed lookup table for the audited routes.
|
|
*
|
|
* A direct `AUDIT_ENDPOINTS[url]` lookup cannot work: the keys are templates
|
|
* with `:params` while a live request carries real ids and a query string, so
|
|
* every parameterized route — most of the 488 — would miss. Templates are
|
|
* compiled to regexes once at module load and matched per request.
|
|
*
|
|
* Within a method, routes are ordered by literal-segment count descending, so
|
|
* a specific route always wins over a parameterized one that could also match
|
|
* (`/api/routes/:id/permanent` before `/api/routes/:id`).
|
|
*/
|
|
class AuditEndpointMatcher {
|
|
private readonly byMethod = new Map<string, CompiledRoute[]>();
|
|
|
|
constructor() {
|
|
for (const [key, meta] of Object.entries(AUDIT_ENDPOINTS)) {
|
|
const [method, rawPath] = stripSourceSuffix(key).split(' ');
|
|
if (!method || !rawPath) continue;
|
|
|
|
const { regex, paramNames } = compileTemplate(rawPath);
|
|
const bucket = this.byMethod.get(method) ?? [];
|
|
bucket.push({
|
|
regex,
|
|
paramNames,
|
|
routePath: rawPath,
|
|
meta,
|
|
staticSegments: rawPath
|
|
.split('/')
|
|
.filter((s) => s && !s.startsWith(':')).length,
|
|
});
|
|
this.byMethod.set(method, bucket);
|
|
}
|
|
|
|
for (const bucket of this.byMethod.values()) {
|
|
bucket.sort((a, b) => b.staticSegments - a.staticSegments);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve a live request to its audit metadata, or null when the route is
|
|
* not audited (every GET, and anything absent from AUDIT_ENDPOINTS).
|
|
*
|
|
* `url` may include a query string; it is ignored for matching.
|
|
*/
|
|
match(method: string, url: string): MatchedAuditEndpoint | null {
|
|
const bucket = this.byMethod.get(method.toUpperCase());
|
|
if (!bucket) return null;
|
|
|
|
const path = stripQuery(url);
|
|
|
|
for (const route of bucket) {
|
|
const result = route.regex.exec(path);
|
|
if (!result) continue;
|
|
|
|
const [title, , type] = route.meta;
|
|
return {
|
|
title,
|
|
type,
|
|
routePath: route.routePath,
|
|
// The first path parameter is the affected record in this API's
|
|
// conventions (`/api/contracts/:id/...`). Routes with no parameter
|
|
// (a create) legitimately have no resource id yet.
|
|
resourceId: route.paramNames.length > 0 ? result[1] : null,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Strip query string and hash from a URL, leaving the path. */
|
|
export function stripQuery(url: string): string {
|
|
const queryIndex = url.indexOf('?');
|
|
const path = queryIndex === -1 ? url : url.slice(0, queryIndex);
|
|
const hashIndex = path.indexOf('#');
|
|
return hashIndex === -1 ? path : path.slice(0, hashIndex);
|
|
}
|
|
|
|
/** Compiled once at module load and shared by the interceptor. */
|
|
export const auditEndpointMatcher = new AuditEndpointMatcher();
|