feat: scrip engine implementation — real SMTP, webhooks, DB actions
- 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"}
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
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): { success: boolean; message: string };
|
||||
execute(payload: ActionPayload): Promise<{ success: boolean; message: string }>;
|
||||
}
|
||||
|
||||
export interface ActionPayload {
|
||||
@@ -7,6 +13,7 @@ export interface ActionPayload {
|
||||
scripName: string;
|
||||
actionType: string;
|
||||
actionConfig: Record<string, unknown>;
|
||||
ticketId?: string;
|
||||
recipients?: string[];
|
||||
subject?: string;
|
||||
body?: string;
|
||||
@@ -18,45 +25,147 @@ export interface ActionPayload {
|
||||
}
|
||||
|
||||
export class SendEmail implements ActionExecutor {
|
||||
execute(payload: ActionPayload): { success: boolean; message: string } {
|
||||
console.log('[SendEmail] Would send email:', {
|
||||
subject: payload.subject ?? payload.actionConfig['subject'],
|
||||
body: payload.body ?? payload.actionConfig['body'],
|
||||
recipients: payload.recipients ?? payload.actionConfig['recipients'],
|
||||
});
|
||||
return { success: true, message: `Email queued: "${payload.subject ?? 'No subject'}"` };
|
||||
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 {
|
||||
execute(payload: ActionPayload): { success: boolean; message: string } {
|
||||
console.log('[Webhook] Would fire webhook:', {
|
||||
url: payload.url ?? payload.actionConfig['url'],
|
||||
method: payload.method ?? payload.actionConfig['method'] ?? 'POST',
|
||||
headers: payload.headers ?? payload.actionConfig['headers'],
|
||||
body: payload.body ?? payload.actionConfig['body'],
|
||||
});
|
||||
return { success: true, message: `Webhook fired: ${payload.url ?? 'unknown URL'}` };
|
||||
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 {
|
||||
execute(payload: ActionPayload): { success: boolean; message: string } {
|
||||
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'] ?? '');
|
||||
console.log('[SetCustomField] Would set:', { field_id: fieldId, value });
|
||||
return { success: true, message: `Custom field ${fieldId} set to "${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}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const actionRegistry: Record<string, ActionExecutor> = {
|
||||
SendEmail: new SendEmail(),
|
||||
Webhook: new Webhook(),
|
||||
SetCustomField: new SetCustomField(),
|
||||
};
|
||||
export class CreateTransaction implements ActionExecutor {
|
||||
constructor(private db: Db) {}
|
||||
|
||||
export function getActionExecutor(type: string): ActionExecutor | null {
|
||||
return actionRegistry[type] ?? null;
|
||||
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 { actionRegistry };
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user