- 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>
107 lines
3.0 KiB
TypeScript
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 [];
|
|
}
|
|
}
|