Files
EpicNext-Cms/src/features/housekeeping/foundation/recent/recent-work.ts
T
Simo 5574e601bb
CI / check (pull_request) Successful in 36s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped
feat(housekeeping): personalize command deck
2026-08-30 16:55:10 +02:00

257 lines
6.5 KiB
TypeScript

import "server-only";
import type { AuditEntry } from "@/lib/services/audit";
import { satisfiesCapability } from "../capability-context";
import {
type CanonicalHousekeepingHref,
fail,
type HousekeepingCapabilityContext,
type HousekeepingDomainManifest,
type HousekeepingResult,
ok,
} from "../contracts";
import { createCorrelationId } from "../correlation";
import type { HousekeepingRegistry } from "../registry";
export const RECENT_WORK_POLICY = Object.freeze({
limit: 12,
scanLimit: 200,
dedupe: "routeId",
} as const);
export interface HousekeepingRecentItem {
readonly routeId: string;
readonly canonicalHref: CanonicalHousekeepingHref;
readonly labelKey: string;
readonly occurredAt: string;
readonly source: "route-visit" | "audit";
}
export interface HousekeepingRecentAuditRow {
readonly action: string;
readonly target: string;
readonly domain: string | null;
readonly outcome: string | null;
readonly createdAt: string;
}
interface RegisteredRoute {
readonly domain: HousekeepingDomainManifest;
readonly route: HousekeepingDomainManifest["routes"][number];
}
export interface HousekeepingRecentWorkDependencies {
readonly registry: HousekeepingRegistry;
readonly loadRows: (
userId: number,
limit: number,
) => Promise<readonly HousekeepingRecentAuditRow[]>;
readonly writeAudit: (entry: AuditEntry) => Promise<void>;
readonly createCorrelationId?: () => string;
}
const completedOutcomes = new Set(["success", "partial"]);
const genericActionTokens = new Set([
"add",
"apply",
"change",
"create",
"delete",
"execute",
"remove",
"runtime",
"save",
"set",
"toggle",
"update",
]);
function registeredRoutes(
registry: HousekeepingRegistry,
): readonly RegisteredRoute[] {
return registry.domains.flatMap((domain) =>
domain.routes.map((route) => ({ domain, route })),
);
}
function normalizedTokens(value: string): readonly string[] {
return value
.toLocaleLowerCase()
.split(/[^a-z0-9]+/)
.filter(Boolean)
.map((token) => {
if (token.endsWith("ies") && token.length > 3) {
return `${token.slice(0, -3)}y`;
}
if (token.endsWith("s") && token.length > 3) return token.slice(0, -1);
return token;
})
.filter((token) => !genericActionTokens.has(token));
}
function authorized(
entry: RegisteredRoute,
context: HousekeepingCapabilityContext,
): boolean {
return (
satisfiesCapability(context, entry.domain.capability) &&
satisfiesCapability(context, entry.route.capability)
);
}
function routeForAudit(
row: HousekeepingRecentAuditRow,
routes: readonly RegisteredRoute[],
context: HousekeepingCapabilityContext,
): RegisteredRoute | undefined {
const domainRoutes = routes.filter(
(entry) => entry.domain.id === row.domain && authorized(entry, context),
);
const exact = domainRoutes.find((entry) => entry.route.id === row.target);
if (exact) return exact;
const actionTokens = new Set(
normalizedTokens(row.action).filter((token) => token !== row.domain),
);
let best: { entry: RegisteredRoute; score: number } | undefined;
for (const entry of domainRoutes) {
const score = normalizedTokens(entry.route.id)
.filter((token) => token !== row.domain)
.reduce((total, token) => total + (actionTokens.has(token) ? 1 : 0), 0);
if (score > 0 && (!best || score > best.score)) best = { entry, score };
}
return best?.entry;
}
function recentItem(
row: HousekeepingRecentAuditRow,
route: RegisteredRoute,
): HousekeepingRecentItem {
return {
routeId: route.route.id,
canonicalHref: route.route.href,
labelKey: route.route.labelKey,
occurredAt: row.createdAt,
source: row.action === "housekeeping.route.visit" ? "route-visit" : "audit",
};
}
export function deriveHousekeepingRecentWork(
rows: readonly HousekeepingRecentAuditRow[],
registry: HousekeepingRegistry,
context: HousekeepingCapabilityContext,
): readonly HousekeepingRecentItem[] {
const routes = registeredRoutes(registry);
const sorted = [...rows].sort(
(left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt),
);
const seen = new Set<string>();
const items: HousekeepingRecentItem[] = [];
for (const row of sorted) {
if (!completedOutcomes.has(row.outcome ?? "")) continue;
if (!Number.isFinite(Date.parse(row.createdAt))) continue;
const route =
row.action === "housekeeping.route.visit"
? routes.find(
(entry) =>
entry.route.id === row.target && authorized(entry, context),
)
: routeForAudit(row, routes, context);
if (!route || seen.has(route.route.id)) continue;
seen.add(route.route.id);
items.push(recentItem(row, route));
if (items.length === RECENT_WORK_POLICY.limit) break;
}
return items;
}
export function createHousekeepingRecentWorkService({
registry,
loadRows,
writeAudit,
createCorrelationId: correlationIdFactory = createCorrelationId,
}: HousekeepingRecentWorkDependencies) {
return {
async load(
context: HousekeepingCapabilityContext,
): Promise<HousekeepingResult<readonly HousekeepingRecentItem[]>> {
const correlationId = correlationIdFactory();
try {
const rows = await loadRows(
context.actor.id,
RECENT_WORK_POLICY.scanLimit,
);
return ok(
deriveHousekeepingRecentWork(rows, registry, context),
correlationId,
);
} catch {
return fail(
"INTERNAL",
"errors.housekeeping.recent.read",
correlationId,
);
}
},
async recordVisit(
routeId: string,
context: HousekeepingCapabilityContext,
): Promise<HousekeepingResult<HousekeepingRecentItem>> {
const correlationId = correlationIdFactory();
const entry = registeredRoutes(registry).find(
(candidate) => candidate.route.id === routeId,
);
if (!entry) {
return fail(
"NOT_FOUND",
"errors.housekeeping.recent.routeNotFound",
correlationId,
);
}
if (!authorized(entry, context)) {
return fail(
"FORBIDDEN",
"errors.housekeeping.forbidden",
correlationId,
);
}
const occurredAt = new Date().toISOString();
try {
await writeAudit({
userId: context.actor.id,
action: "housekeeping.route.visit",
target: entry.route.id,
after: {
canonicalHref: entry.route.href,
labelKey: entry.route.labelKey,
},
correlationId,
outcome: "success",
domain: entry.domain.id,
});
return ok(
{
routeId: entry.route.id,
canonicalHref: entry.route.href,
labelKey: entry.route.labelKey,
occurredAt,
source: "route-visit",
},
correlationId,
);
} catch {
return fail(
"INTERNAL",
"errors.housekeeping.recent.record",
correlationId,
);
}
},
};
}