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,13 +1,15 @@
|
||||
import type { Db } from '../db/index.ts';
|
||||
import type { Ticket } from '../models/ticket.ts';
|
||||
import type { Transaction } from '../models/transaction.ts';
|
||||
import { tickets, queues, scrips } from '../db/schema.ts';
|
||||
import { eq, asc } from 'drizzle-orm';
|
||||
import { tickets, queues, scrips, lifecycles, customFieldValues, customFields } from '../db/schema.ts';
|
||||
import { eq, asc, inArray } from 'drizzle-orm';
|
||||
import { getConditionEvaluator } from './conditions.ts';
|
||||
import type { ConditionEvaluateContext } from './conditions.ts';
|
||||
import { getActionExecutor } from './actions.ts';
|
||||
import type { ActionPayload } from './actions.ts';
|
||||
import { TemplateRenderer } from './templates.ts';
|
||||
import type { TemplateContext } from './templates.ts';
|
||||
import type { LifecycleDefinition } from '../lifecycle/validator.ts';
|
||||
|
||||
export interface PreparedScrip {
|
||||
scripId: string;
|
||||
@@ -57,6 +59,44 @@ export class ScripEngine {
|
||||
return true;
|
||||
});
|
||||
|
||||
const queue = await this.db.query.queues.findFirst({
|
||||
where: eq(queues.id, ticketRecord.queue_id),
|
||||
});
|
||||
|
||||
let lifecycleDef: LifecycleDefinition | undefined;
|
||||
if (queue?.lifecycle_id) {
|
||||
const lifecycle = await this.db.query.lifecycles.findFirst({
|
||||
where: eq(lifecycles.id, queue.lifecycle_id),
|
||||
});
|
||||
if (lifecycle) {
|
||||
lifecycleDef = lifecycle.definition as LifecycleDefinition;
|
||||
}
|
||||
}
|
||||
|
||||
const conditionContext: ConditionEvaluateContext = {
|
||||
lifecycleDef,
|
||||
};
|
||||
|
||||
const cfValues = await this.db.query.customFieldValues.findMany({
|
||||
where: eq(customFieldValues.ticket_id, ticketId),
|
||||
});
|
||||
|
||||
const cfIds = [...new Set(cfValues.map((v) => v.custom_field_id))];
|
||||
const cfRecords = cfIds.length > 0
|
||||
? await this.db.query.customFields.findMany({
|
||||
where: (t, { inArray }) => inArray(t.id, cfIds),
|
||||
})
|
||||
: [];
|
||||
const cfNameById = new Map(cfRecords.map((cf) => [cf.id, cf.name]));
|
||||
|
||||
const customFieldsMap: Record<string, string> = {};
|
||||
for (const row of cfValues) {
|
||||
const name = cfNameById.get(row.custom_field_id);
|
||||
if (name) {
|
||||
customFieldsMap[name] = row.value;
|
||||
}
|
||||
}
|
||||
|
||||
const prepared: PreparedScrip[] = [];
|
||||
|
||||
for (const scrip of matchingScrips) {
|
||||
@@ -66,7 +106,7 @@ export class ScripEngine {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!evaluator.evaluate(ticketRecord as unknown as Ticket, transactions)) {
|
||||
if (!evaluator.evaluate(ticketRecord as unknown as Ticket, transactions, conditionContext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -79,9 +119,6 @@ export class ScripEngine {
|
||||
});
|
||||
|
||||
if (template) {
|
||||
const queue = await this.db.query.queues.findFirst({
|
||||
where: eq(queues.id, ticketRecord.queue_id),
|
||||
});
|
||||
const latestTx = transactions[transactions.length - 1]!;
|
||||
|
||||
const context: TemplateContext = {
|
||||
@@ -104,7 +141,7 @@ export class ScripEngine {
|
||||
old_value: latestTx.old_value,
|
||||
new_value: latestTx.new_value,
|
||||
},
|
||||
custom_fields: {},
|
||||
custom_fields: customFieldsMap,
|
||||
};
|
||||
|
||||
const rendered = this.templateRenderer.render(
|
||||
@@ -122,6 +159,7 @@ export class ScripEngine {
|
||||
scripName: scrip.name,
|
||||
actionType: scrip.action_type,
|
||||
actionConfig: scrip.action_config as Record<string, unknown>,
|
||||
ticketId: ticketId,
|
||||
subject,
|
||||
body,
|
||||
};
|
||||
@@ -138,7 +176,7 @@ export class ScripEngine {
|
||||
return prepared;
|
||||
}
|
||||
|
||||
commit(prepared: PreparedScrip[]): ScripResult[] {
|
||||
async commit(prepared: PreparedScrip[]): Promise<ScripResult[]> {
|
||||
const results: ScripResult[] = [];
|
||||
|
||||
for (const p of prepared) {
|
||||
@@ -151,7 +189,7 @@ export class ScripEngine {
|
||||
continue;
|
||||
}
|
||||
|
||||
const executor = getActionExecutor(p.actionType);
|
||||
const executor = getActionExecutor(this.db, p.actionType);
|
||||
if (!executor) {
|
||||
results.push({
|
||||
scripId: p.scripId,
|
||||
@@ -161,7 +199,7 @@ export class ScripEngine {
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = executor.execute(p.actionPayload);
|
||||
const result = await executor.execute(p.actionPayload);
|
||||
results.push({
|
||||
scripId: p.scripId,
|
||||
success: result.success,
|
||||
|
||||
Reference in New Issue
Block a user