Files
tessera/src/lifecycle/validator.ts
Gjermund Høsøien Wiggen 70f0924d4b feat: auth system, scrip scheduler, UI widgets, and new API routes
- Add session-based authentication (login page, middleware, auth context)
- Add cron-like scrip scheduler for time-based conditions
- Add layout builder, scrip wizard, searchable select components
- Add trend chart widget for dashboards
- Add notifications, attachments, queue-permissions API routes
- Add seed-users script
- Update schema with 10 new migrations (0008-0017)
- Apply redesign: Linear-inspired dark theme, conversation-centric UI
- Gitignore runtime data directory

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:42:17 +02:00

107 lines
3.0 KiB
TypeScript

export interface LifecycleDefinition {
statuses: {
initial: string[];
active: string[];
inactive: string[];
};
transitions: Record<string, string[]>;
transition_rights?: Record<string, string>; // "from→to" → rightName
}
export interface ValidationResult {
valid: boolean;
error?: string;
requiredRight?: string; // Named right required for this transition, if any
}
const FALLBACK_RIGHT = 'ticket.modify';
export class LifecycleValidator {
validateTransition(
lifecycleDef: LifecycleDefinition,
fromStatus: string,
toStatus: string,
): ValidationResult {
const allStatuses = [
...lifecycleDef.statuses.initial,
...lifecycleDef.statuses.active,
...lifecycleDef.statuses.inactive,
];
if (!allStatuses.includes(toStatus)) {
return {
valid: false,
error: `Status "${toStatus}" is not defined in the lifecycle`,
};
}
// Check for allowed transitions
const allowedTransitions = this.getAllowedTransitions(lifecycleDef, fromStatus);
if (allowedTransitions.includes(toStatus)) {
const right = this.getRequiredRight(lifecycleDef, fromStatus, toStatus);
return { valid: true, requiredRight: right ?? FALLBACK_RIGHT };
}
// Also handle wildcard "*" -> any transition
const wildcardTransitions = this.getAllowedTransitions(lifecycleDef, '*');
if (wildcardTransitions.includes(toStatus)) {
const right = this.getRequiredRight(lifecycleDef, fromStatus, toStatus);
return { valid: true, requiredRight: right ?? FALLBACK_RIGHT };
}
return {
valid: false,
error: `Transition from "${fromStatus}" to "${toStatus}" is not allowed`,
};
}
/**
* Get the required right for a transition using RT's 4-level priority:
* 1. exact "from→to"
* 2. wildcard from "*→to"
* 3. wildcard to "from→*"
* 4. full wildcard "*→*"
* 5. fallback: ticket.modify
*/
getRequiredRight(
lifecycleDef: LifecycleDefinition,
fromStatus: string,
toStatus: string,
): string | null {
const rights = lifecycleDef.transition_rights ?? {};
// Priority 1: exact match
if (rights[`${fromStatus}${toStatus}`]) return rights[`${fromStatus}${toStatus}`];
// Priority 2: wildcard from
if (rights[`*→${toStatus}`]) return rights[`*→${toStatus}`];
// Priority 3: wildcard to
if (rights[`${fromStatus}→*`]) return rights[`${fromStatus}→*`];
// Priority 4: full wildcard
if (rights['*→*']) return rights['*→*'];
// Priority 5: fallback
return null;
}
isResolvedStatus(lifecycleDef: LifecycleDefinition, status: string): boolean {
return lifecycleDef.statuses.inactive.includes(status);
}
private getAllowedTransitions(
lifecycleDef: LifecycleDefinition,
fromStatus: string,
): string[] {
if (lifecycleDef.transitions[fromStatus]) {
return lifecycleDef.transitions[fromStatus]!;
}
if (lifecycleDef.transitions['*']) {
return lifecycleDef.transitions['*']!;
}
return [];
}
}