feat: implement full scrip action engine with real executors
- SendEmail: real nodemailer transport with SMTP config, dynamic recipient resolution (static + ticket creator/owner lookup), Handlebars template support - Webhook: HTTP POST/any method with configurable headers and JSON body - FetchMetadata: external HTTP fetch, Handlebars URL/body templating, auto-adds result as comment/correspondence on ticket - RunScript: arbitrary async JS execution with helpers (addComment, createTransaction, updateTicket, touchTicket), ticket context, and Drizzle ORM access - SetCustomField: lookup by id/key/name, clear+insert value, record CustomFieldChange transaction - CreateTransaction: insert arbitrary transaction record - Add OnCustomFieldChange condition - Pass condition_config to evaluator in engine Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,29 @@ export interface ConditionEvaluateContext {
|
||||
lifecycleDef?: LifecycleDefinition;
|
||||
}
|
||||
|
||||
export interface ConditionConfig {
|
||||
from_status?: unknown;
|
||||
to_status?: unknown;
|
||||
field_key?: unknown;
|
||||
field_id?: unknown;
|
||||
field?: unknown;
|
||||
old_value?: unknown;
|
||||
new_value?: unknown;
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
export interface ConditionEvaluator {
|
||||
evaluate(ticket: Ticket, transactions: Transaction[], context?: ConditionEvaluateContext): boolean;
|
||||
evaluate(ticket: Ticket, transactions: Transaction[], context?: ConditionEvaluateContext, config?: ConditionConfig): boolean;
|
||||
}
|
||||
|
||||
function matchesStatusFilter(value: string | null, filter: unknown): boolean {
|
||||
if (filter === undefined || filter === null || filter === '') return true;
|
||||
if (value === null) return false;
|
||||
const normalizedValue = value.toLowerCase();
|
||||
if (Array.isArray(filter)) {
|
||||
return filter.map((item) => String(item).toLowerCase()).includes(normalizedValue);
|
||||
}
|
||||
return normalizedValue === String(filter).toLowerCase();
|
||||
}
|
||||
|
||||
export class OnCreate implements ConditionEvaluator {
|
||||
@@ -18,19 +39,25 @@ export class OnCreate implements ConditionEvaluator {
|
||||
}
|
||||
|
||||
export class OnStatusChange implements ConditionEvaluator {
|
||||
evaluate(_ticket: Ticket, transactions: Transaction[]): boolean {
|
||||
return transactions.some((tx) => tx.transaction_type === 'StatusChange');
|
||||
evaluate(_ticket: Ticket, transactions: Transaction[], _context?: ConditionEvaluateContext, config?: ConditionConfig): boolean {
|
||||
return transactions.some((tx) =>
|
||||
tx.transaction_type === 'StatusChange' &&
|
||||
matchesStatusFilter(tx.old_value, config?.from_status) &&
|
||||
matchesStatusFilter(tx.new_value, config?.to_status)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class OnResolve implements ConditionEvaluator {
|
||||
private lifecycleValidator = new LifecycleValidator();
|
||||
|
||||
evaluate(_ticket: Ticket, transactions: Transaction[], context?: ConditionEvaluateContext): boolean {
|
||||
evaluate(_ticket: Ticket, transactions: Transaction[], context?: ConditionEvaluateContext, config?: ConditionConfig): boolean {
|
||||
const lifecycleDef = context?.lifecycleDef;
|
||||
|
||||
return transactions.some((tx) => {
|
||||
if (tx.transaction_type !== 'StatusChange' || tx.new_value === null) return false;
|
||||
if (!matchesStatusFilter(tx.old_value, config?.from_status)) return false;
|
||||
if (!matchesStatusFilter(tx.new_value, config?.to_status)) return false;
|
||||
|
||||
if (lifecycleDef) {
|
||||
return this.lifecycleValidator.isResolvedStatus(lifecycleDef, tx.new_value);
|
||||
@@ -41,10 +68,25 @@ export class OnResolve implements ConditionEvaluator {
|
||||
}
|
||||
}
|
||||
|
||||
export class OnCustomFieldChange implements ConditionEvaluator {
|
||||
evaluate(_ticket: Ticket, transactions: Transaction[], _context?: ConditionEvaluateContext, config?: ConditionConfig): boolean {
|
||||
const fieldFilter = config?.field_key ?? config?.field_id ?? config?.field;
|
||||
const newValueFilter = config?.new_value ?? config?.value;
|
||||
|
||||
return transactions.some((tx) =>
|
||||
tx.transaction_type === 'CustomFieldChange' &&
|
||||
matchesStatusFilter(tx.field, fieldFilter) &&
|
||||
matchesStatusFilter(tx.old_value, config?.old_value) &&
|
||||
matchesStatusFilter(tx.new_value, newValueFilter)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const conditionRegistry: Record<string, ConditionEvaluator> = {
|
||||
OnCreate: new OnCreate(),
|
||||
OnStatusChange: new OnStatusChange(),
|
||||
OnResolve: new OnResolve(),
|
||||
OnCustomFieldChange: new OnCustomFieldChange(),
|
||||
};
|
||||
|
||||
export function getConditionEvaluator(type: string): ConditionEvaluator | null {
|
||||
|
||||
Reference in New Issue
Block a user