- config.ts: add SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM
- engine.ts: prepare() queries real scrips from DB, matches by queue_id + condition_type, loads lifecycle for OnResolve context, renders Handlebars templates, builds PreparedScrip. commit() dispatches to real action executors.
- actions.ts: SendEmail via nodemailer SMTP, Webhook via fetch POST, SetCustomField writes to custom_field_values table
- conditions.ts: OnResolve uses LifecycleValidator.isResolvedStatus()
- tickets.ts: updated to pass lifecycleDef context to scrip engine
- nodemailer installed
- Port changed to 9876 (8080 occupied by Apache)
Verification: bun run src/index.ts starts server, GET /health returns {"status":"ok","version":"0.1.0"}
172 lines
6.1 KiB
TypeScript
172 lines
6.1 KiB
TypeScript
import nodemailer from 'nodemailer';
|
|
import type { Transporter } from 'nodemailer';
|
|
import { config } from '../config.ts';
|
|
import type { Db } from '../db/index.ts';
|
|
import { customFieldValues, transactions } from '../db/schema.ts';
|
|
|
|
export interface ActionExecutor {
|
|
execute(payload: ActionPayload): Promise<{ success: boolean; message: string }>;
|
|
}
|
|
|
|
export interface ActionPayload {
|
|
scripId: string;
|
|
scripName: string;
|
|
actionType: string;
|
|
actionConfig: Record<string, unknown>;
|
|
ticketId?: string;
|
|
recipients?: string[];
|
|
subject?: string;
|
|
body?: string;
|
|
url?: string;
|
|
method?: string;
|
|
headers?: Record<string, string>;
|
|
field_id?: string;
|
|
value?: string;
|
|
}
|
|
|
|
export class SendEmail implements ActionExecutor {
|
|
private transporter: Transporter | null = null;
|
|
|
|
private getTransporter(): Transporter {
|
|
if (!this.transporter) {
|
|
this.transporter = nodemailer.createTransport({
|
|
host: config.SMTP_HOST,
|
|
port: config.SMTP_PORT,
|
|
secure: config.SMTP_PORT === 465,
|
|
...(config.SMTP_USER ? { auth: { user: config.SMTP_USER, pass: config.SMTP_PASS } } : {}),
|
|
});
|
|
}
|
|
return this.transporter;
|
|
}
|
|
|
|
async execute(payload: ActionPayload): Promise<{ success: boolean; message: string }> {
|
|
const recipients = payload.recipients ?? (payload.actionConfig['recipients'] as string[] | undefined) ?? [];
|
|
const subject = payload.subject ?? (payload.actionConfig['subject'] as string | undefined);
|
|
const body = payload.body ?? (payload.actionConfig['body'] as string | undefined);
|
|
|
|
if (recipients.length === 0) {
|
|
return { success: false, message: 'SendEmail: no recipients configured' };
|
|
}
|
|
|
|
try {
|
|
await this.getTransporter().sendMail({
|
|
from: config.SMTP_FROM,
|
|
to: recipients.join(', '),
|
|
subject: subject ?? 'No subject',
|
|
text: body ?? '',
|
|
});
|
|
return { success: true, message: `Email sent to ${recipients.join(', ')}` };
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
return { success: false, message: `SendEmail failed: ${message}` };
|
|
}
|
|
}
|
|
}
|
|
|
|
export class Webhook implements ActionExecutor {
|
|
async execute(payload: ActionPayload): Promise<{ success: boolean; message: string }> {
|
|
const url = payload.url ?? (payload.actionConfig['url'] as string | undefined);
|
|
const method = payload.method ?? (payload.actionConfig['method'] as string | undefined) ?? 'POST';
|
|
const headers = (payload.headers ?? payload.actionConfig['headers'] ?? {}) as Record<string, string>;
|
|
const body = payload.body ?? (payload.actionConfig['body'] as string | undefined);
|
|
|
|
if (!url) {
|
|
return { success: false, message: 'Webhook: no URL configured' };
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
body: body ?? undefined,
|
|
});
|
|
if (!response.ok) {
|
|
return { success: false, message: `Webhook failed: HTTP ${response.status}` };
|
|
}
|
|
return { success: true, message: `Webhook POST to ${url} succeeded` };
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
return { success: false, message: `Webhook failed: ${message}` };
|
|
}
|
|
}
|
|
}
|
|
|
|
export class SetCustomField implements ActionExecutor {
|
|
constructor(private db: Db) {}
|
|
|
|
async execute(payload: ActionPayload): Promise<{ success: boolean; message: string }> {
|
|
const fieldId = payload.field_id ?? String(payload.actionConfig['field_id'] ?? '');
|
|
const value = payload.value ?? String(payload.actionConfig['value'] ?? '');
|
|
const ticketId = payload.ticketId ?? String(payload.actionConfig['ticket_id'] ?? '');
|
|
|
|
if (!fieldId || !value || !ticketId) {
|
|
return { success: false, message: 'SetCustomField: missing field_id, value, or ticket_id' };
|
|
}
|
|
|
|
try {
|
|
await this.db.insert(customFieldValues).values({
|
|
custom_field_id: fieldId,
|
|
ticket_id: ticketId,
|
|
value,
|
|
});
|
|
return { success: true, message: `Custom field ${fieldId} set to "${value}"` };
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
return { success: false, message: `SetCustomField failed: ${message}` };
|
|
}
|
|
}
|
|
}
|
|
|
|
export class CreateTransaction implements ActionExecutor {
|
|
constructor(private db: Db) {}
|
|
|
|
async execute(payload: ActionPayload): Promise<{ success: boolean; message: string }> {
|
|
const ticketId = payload.ticketId ?? String(payload.actionConfig['ticket_id'] ?? '');
|
|
const transactionType = String(payload.actionConfig['transaction_type'] ?? '');
|
|
const field = payload.actionConfig['field'] as string | undefined ?? null;
|
|
const oldValue = payload.actionConfig['old_value'] as string | undefined ?? null;
|
|
const newValue = payload.actionConfig['new_value'] as string | undefined ?? null;
|
|
|
|
if (!ticketId || !transactionType) {
|
|
return { success: false, message: 'CreateTransaction: missing ticket_id or transaction_type' };
|
|
}
|
|
|
|
try {
|
|
await this.db.insert(transactions).values({
|
|
ticket_id: ticketId,
|
|
transaction_type: transactionType,
|
|
field,
|
|
old_value: oldValue,
|
|
new_value: newValue,
|
|
creator_id: '00000000-0000-0000-0000-000000000000',
|
|
});
|
|
return { success: true, message: `Transaction ${transactionType} created for ticket ${ticketId}` };
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
return { success: false, message: `CreateTransaction failed: ${message}` };
|
|
}
|
|
}
|
|
}
|
|
|
|
export function createActionRegistry(db: Db): Record<string, ActionExecutor> {
|
|
return {
|
|
SendEmail: new SendEmail(),
|
|
Webhook: new Webhook(),
|
|
SetCustomField: new SetCustomField(db),
|
|
CreateTransaction: new CreateTransaction(db),
|
|
};
|
|
}
|
|
|
|
let _actionRegistry: Record<string, ActionExecutor> | null = null;
|
|
|
|
export function getActionRegistry(db: Db): Record<string, ActionExecutor> {
|
|
if (!_actionRegistry) {
|
|
_actionRegistry = createActionRegistry(db);
|
|
}
|
|
return _actionRegistry;
|
|
}
|
|
|
|
export function getActionExecutor(db: Db, type: string): ActionExecutor | null {
|
|
return getActionRegistry(db)[type] ?? null;
|
|
}
|