151 lines
4.9 KiB
TypeScript
151 lines
4.9 KiB
TypeScript
/**
|
|
* Triage Validator
|
|
* Validates classification output against DB picklists.
|
|
* Ensures issueType/subIssueType parent-child relationships are correct,
|
|
* priority exists, queue exists, etc.
|
|
*/
|
|
|
|
import { postgresClient } from './postgres-client';
|
|
import {
|
|
ClassificationResult,
|
|
ValidationResult,
|
|
ValidationError,
|
|
} from '../types/workflow';
|
|
|
|
// Cache TTL for picklist data (10 minutes)
|
|
const PICKLIST_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
|
|
interface PicklistCache {
|
|
issueTypes: Set<number>;
|
|
subIssueTypes: Map<number, number>; // value → parent_value
|
|
priorities: Set<number>;
|
|
queues: Set<number>;
|
|
ticketCategories: Set<number>;
|
|
loadedAt: number;
|
|
}
|
|
|
|
export class TriageValidator {
|
|
private cache: PicklistCache | null = null;
|
|
|
|
/**
|
|
* Load picklist data from DB into cache.
|
|
*/
|
|
private async loadPicklists(forceRefresh = false): Promise<PicklistCache> {
|
|
const now = Date.now();
|
|
if (!forceRefresh && this.cache && now - this.cache.loadedAt < PICKLIST_CACHE_TTL_MS) {
|
|
return this.cache;
|
|
}
|
|
|
|
const [issueTypes, subIssueTypes, priorities, queues, ticketCategories] = await Promise.all([
|
|
postgresClient.query(`SELECT value FROM issue_types WHERE is_deleted = false`),
|
|
postgresClient.query(`SELECT value, parent_value FROM sub_issue_types WHERE is_deleted = false`),
|
|
postgresClient.query(`SELECT value FROM priorities WHERE is_active = true`),
|
|
postgresClient.query(`SELECT value FROM queues WHERE is_active = true`),
|
|
postgresClient.query(`SELECT value FROM ticket_categories WHERE is_active = true`),
|
|
]);
|
|
|
|
this.cache = {
|
|
issueTypes: new Set(issueTypes.rows.map((r: any) => Number(r.value))),
|
|
subIssueTypes: new Map(
|
|
subIssueTypes.rows.map((r: any) => [Number(r.value), Number(r.parent_value)])
|
|
),
|
|
priorities: new Set(priorities.rows.map((r: any) => Number(r.value))),
|
|
queues: new Set(queues.rows.map((r: any) => Number(r.value))),
|
|
ticketCategories: new Set(ticketCategories.rows.map((r: any) => Number(r.value))),
|
|
loadedAt: now,
|
|
};
|
|
|
|
console.log(
|
|
`[VALIDATOR] Loaded picklists: ${this.cache.issueTypes.size} issueTypes, ` +
|
|
`${this.cache.subIssueTypes.size} subIssueTypes, ${this.cache.priorities.size} priorities, ` +
|
|
`${this.cache.queues.size} queues, ${this.cache.ticketCategories.size} ticketCategories`
|
|
);
|
|
|
|
return this.cache;
|
|
}
|
|
|
|
/**
|
|
* Validate a classification result against DB picklists.
|
|
*/
|
|
async validate(classification: ClassificationResult): Promise<ValidationResult> {
|
|
const picklists = await this.loadPicklists();
|
|
const errors: ValidationError[] = [];
|
|
|
|
// Validate issue type
|
|
if (classification.issue_classification) {
|
|
const issueTypeValue = Number(classification.issue_classification.value);
|
|
if (!picklists.issueTypes.has(issueTypeValue)) {
|
|
errors.push({
|
|
field: 'issue_type',
|
|
message: `Issue type ${issueTypeValue} not found in picklist`,
|
|
value: issueTypeValue,
|
|
});
|
|
}
|
|
|
|
// Validate sub-issue type
|
|
if (classification.issue_classification.value_2 != null) {
|
|
const subIssueTypeValue = Number(classification.issue_classification.value_2);
|
|
if (!picklists.subIssueTypes.has(subIssueTypeValue)) {
|
|
errors.push({
|
|
field: 'sub_issue_type',
|
|
message: `Sub-issue type ${subIssueTypeValue} not found in picklist`,
|
|
value: subIssueTypeValue,
|
|
});
|
|
} else {
|
|
// Validate parent-child relationship
|
|
const parentValue = picklists.subIssueTypes.get(subIssueTypeValue);
|
|
if (parentValue !== issueTypeValue) {
|
|
errors.push({
|
|
field: 'sub_issue_type',
|
|
message: `Sub-issue type ${subIssueTypeValue} has parent ${parentValue}, but issue type is ${issueTypeValue}`,
|
|
value: subIssueTypeValue,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate priority
|
|
if (classification.priority) {
|
|
const priorityValue = Number(classification.priority.value);
|
|
if (!picklists.priorities.has(priorityValue)) {
|
|
errors.push({
|
|
field: 'priority',
|
|
message: `Priority ${priorityValue} not found in picklist`,
|
|
value: priorityValue,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Validate queue
|
|
if (classification.queue) {
|
|
const queueValue = Number(classification.queue.value);
|
|
if (!picklists.queues.has(queueValue)) {
|
|
errors.push({
|
|
field: 'queue_id',
|
|
message: `Queue ${queueValue} not found in picklist`,
|
|
value: queueValue,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
console.warn(`[VALIDATOR] Validation failed with ${errors.length} error(s):`, errors);
|
|
}
|
|
|
|
return {
|
|
is_valid: errors.length === 0,
|
|
errors,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Force refresh the picklist cache.
|
|
*/
|
|
async refreshCache(): Promise<void> {
|
|
await this.loadPicklists(true);
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const triageValidator = new TriageValidator();
|