biz-a-cli 2.3.73 → 2.3.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1136 @@
1
+ import { queryData, save as dbSave, deleteMD } from "../../db/db.js";
2
+ import vm from "node:vm";
3
+
4
+ // ================
5
+ // INTERNAL HELPERS
6
+ // ================
7
+
8
+ function buildStateEntryDetails(targetStateDef, isTerminal, instanceId) {
9
+ const details = { tasks: [], standaloneTimers: [] };
10
+ if (!targetStateDef || isTerminal) return details;
11
+
12
+ const taskEnabled = Number(targetStateDef.task_enabled) === 1;
13
+ const slaMinutes = Number(targetStateDef.sla_duration_minutes) || null;
14
+
15
+ if (taskEnabled) {
16
+ let assignedUser = null;
17
+ if (
18
+ targetStateDef.assignment_type === "USER" &&
19
+ targetStateDef.assignment_value
20
+ ) {
21
+ assignedUser = parseInt(targetStateDef.assignment_value, 10);
22
+ }
23
+
24
+ const newTask = {
25
+ id: null,
26
+ state_code: targetStateDef.state_code,
27
+ name: targetStateDef.task_name,
28
+ status: "OPEN",
29
+ assignment_type: targetStateDef.assignment_type,
30
+ assignment_value: targetStateDef.assignment_value,
31
+ assigned_user_id: assignedUser,
32
+ sla_minutes: slaMinutes,
33
+ };
34
+
35
+ if (slaMinutes && slaMinutes > 0) {
36
+ newTask.SYS$BPM_TIMER_JOB = [
37
+ {
38
+ id: null,
39
+ sys$bpm_wf_instance_id: instanceId
40
+ ? Number(instanceId)
41
+ : null,
42
+ state_code: targetStateDef.state_code,
43
+ timer_type: "SLA_TIMEOUT",
44
+ sla_minutes: slaMinutes,
45
+ status: "OPEN",
46
+ },
47
+ ];
48
+ }
49
+ details.tasks.push(newTask);
50
+ } else if (slaMinutes && slaMinutes > 0) {
51
+ details.standaloneTimers.push({
52
+ id: null,
53
+ sys$bpm_wf_instance_id: instanceId ? Number(instanceId) : null,
54
+ sys$bpm_task_id: null,
55
+ state_code: targetStateDef.state_code,
56
+ timer_type: "SLA_TIMEOUT",
57
+ sla_minutes: slaMinutes,
58
+ status: "OPEN",
59
+ });
60
+ }
61
+
62
+ return details;
63
+ }
64
+
65
+ async function evaluateGuardRule(guardType, guardRuleKey, entityData) {
66
+ if (guardType !== "BUSINESS_RULE" || !guardRuleKey) {
67
+ return { isPassed: true, errorMsg: "" };
68
+ }
69
+ try {
70
+ // Create a secure, isolated sandbox object containing ONLY entityData as 'm'
71
+ const sandbox = { m: entityData };
72
+ vm.createContext(sandbox);
73
+
74
+ // Execute the expression securely, wrap it in () to evaluate as boolean expression
75
+ // `return (${guardRuleKey});`
76
+ const result = vm.runInContext(`(${guardRuleKey})`, sandbox, {
77
+ timeout: 1000,
78
+ });
79
+ const isPassed = Boolean(result);
80
+
81
+ return {
82
+ isPassed,
83
+ errorMsg: isPassed ? "" : `Requirement not met: ${guardRuleKey}`,
84
+ };
85
+ } catch (error) {
86
+ // console.error("Biz-A Guard Error:", error);
87
+ return {
88
+ isPassed: false,
89
+ errorMsg: `Invalid guard expression syntax.`,
90
+ };
91
+ }
92
+ }
93
+
94
+ // ===============
95
+ // ENGINE ENDPOINT
96
+ // ===============
97
+
98
+ export async function initiate(payload) {
99
+ const { entityType, entityId, workflowDefId, apiConfig, userId } = payload;
100
+
101
+ let wfParam;
102
+
103
+ if (workflowDefId) {
104
+ wfParam = {
105
+ length: 1,
106
+ columns: [
107
+ { data: "SYS$BPM_WORKFLOW_DEF.ID", key: "id" },
108
+ {
109
+ data: "SYS$BPM_WORKFLOW_DEF.INITIAL_STATE_CODE",
110
+ key: "initial_state_code",
111
+ },
112
+ { data: "SYS$BPM_WORKFLOW_DEF.STATUS", key: "status" },
113
+ { data: "SYS$BPM_WORKFLOW_DEF.VERSION", key: "version" },
114
+ ],
115
+ filter: [
116
+ {
117
+ junction: "",
118
+ column: "SYS$BPM_WORKFLOW_DEF.ID",
119
+ operator: "=",
120
+ value1: workflowDefId,
121
+ },
122
+ {
123
+ junction: "AND",
124
+ column: "SYS$BPM_WORKFLOW_DEF.STATUS",
125
+ operator: "=",
126
+ value1: "'PUBLISHED'",
127
+ },
128
+ ],
129
+ };
130
+ } else {
131
+ if (!entityType) {
132
+ throw new Error(
133
+ "Must provide either 'workflowDefId' or 'entityType' to start a workflow.",
134
+ );
135
+ }
136
+ wfParam = {
137
+ length: 1,
138
+ columns: [
139
+ { data: "SYS$BPM_WORKFLOW_DEF.ID", key: "id" },
140
+ {
141
+ data: "SYS$BPM_WORKFLOW_DEF.INITIAL_STATE_CODE",
142
+ key: "initial_state_code",
143
+ },
144
+ { data: "SYS$BPM_WORKFLOW_DEF.STATUS", key: "status" },
145
+ { data: "SYS$BPM_WORKFLOW_DEF.VERSION", key: "version" },
146
+ ],
147
+ filter: [
148
+ {
149
+ junction: "",
150
+ column: "SYS$BPM_WORKFLOW_DEF.ENTITY_TYPE",
151
+ operator: "=",
152
+ value1: `'${entityType}'`,
153
+ },
154
+ {
155
+ junction: "AND",
156
+ column: "SYS$BPM_WORKFLOW_DEF.STATUS",
157
+ operator: "=",
158
+ value1: "'PUBLISHED'",
159
+ },
160
+ ],
161
+ order: [{ column: "VERSION", dir: "desc" }],
162
+ };
163
+ }
164
+
165
+ const wfDefs = await queryData(wfParam, apiConfig);
166
+
167
+ if (!wfDefs || wfDefs.length === 0) {
168
+ throw new Error(
169
+ `Action Denied: No published workflow found for ${entityType || "ID: " + workflowDefId}.`,
170
+ );
171
+ }
172
+
173
+ const def = wfDefs[0];
174
+ const actualDefId = workflowDefId || def.id;
175
+
176
+ const stateParam = {
177
+ length: 1,
178
+ columns: [
179
+ { data: "SYS$BPM_STATE_DEF.STATE_CODE", key: "state_code" },
180
+ { data: "SYS$BPM_STATE_DEF.TASK_ENABLED", key: "task_enabled" },
181
+ { data: "SYS$BPM_STATE_DEF.TASK_NAME", key: "task_name" },
182
+ {
183
+ data: "SYS$BPM_STATE_DEF.ASSIGNMENT_TYPE",
184
+ key: "assignment_type",
185
+ },
186
+ {
187
+ data: "SYS$BPM_STATE_DEF.ASSIGNMENT_VALUE",
188
+ key: "assignment_value",
189
+ },
190
+ {
191
+ data: "SYS$BPM_STATE_DEF.SLA_DURATION_MINUTES",
192
+ key: "sla_duration_minutes",
193
+ },
194
+ ],
195
+ filter: [
196
+ {
197
+ junction: "",
198
+ column: "SYS$BPM_STATE_DEF.SYS$BPM_WORKFLOW_DEF_ID",
199
+ operator: "=",
200
+ value1: actualDefId,
201
+ },
202
+ {
203
+ junction: "AND",
204
+ column: "SYS$BPM_STATE_DEF.STATE_CODE",
205
+ operator: "=",
206
+ value1: `'${def.initial_state_code}'`,
207
+ },
208
+ ],
209
+ };
210
+
211
+ const states = await queryData(stateParam, apiConfig);
212
+ const initialStateDef = states.length > 0 ? states[0] : null;
213
+ const entryDetails = buildStateEntryDetails(initialStateDef, false, null);
214
+
215
+ const instanceModel = {
216
+ id: null,
217
+ sys$bpm_workflow_def_id: actualDefId,
218
+ entity_type: entityType,
219
+ entity_id: String(entityId),
220
+ current_state_code: def.initial_state_code,
221
+ status: "ACTIVE",
222
+ version_no: def.version,
223
+ created_by: userId || null,
224
+ SYS$BPM_STATE_HISTORY: [
225
+ {
226
+ from_state_code: null,
227
+ to_state_code: def.initial_state_code,
228
+ trigger_type: "SYSTEM",
229
+ actor_type: "USER",
230
+ actor_id: userId || null,
231
+ request_id: `START_${entityType}_${entityId}_${new Date().getTime()}`,
232
+ },
233
+ ],
234
+ SYS$BPM_TASK: entryDetails.tasks,
235
+ SYS$BPM_TIMER_JOB: entryDetails.standaloneTimers,
236
+ };
237
+
238
+ const result = await dbSave(
239
+ { SYS$BPM_WF_INSTANCE: instanceModel },
240
+ apiConfig,
241
+ );
242
+ return result.data;
243
+ }
244
+
245
+ export async function availableTransitions(payload) {
246
+ const { instanceId, userRoles = [], entityData = {}, apiConfig } = payload;
247
+
248
+ const instParam = {
249
+ length: 1,
250
+ columns: [
251
+ {
252
+ data: "SYS$BPM_WF_INSTANCE.SYS$BPM_WORKFLOW_DEF_ID",
253
+ key: "def_id",
254
+ },
255
+ {
256
+ data: "SYS$BPM_WF_INSTANCE.CURRENT_STATE_CODE",
257
+ key: "current_state",
258
+ },
259
+ { data: "SYS$BPM_WF_INSTANCE.STATUS", key: "status" },
260
+ ],
261
+ filter: [
262
+ {
263
+ junction: "",
264
+ column: "SYS$BPM_WF_INSTANCE.ID",
265
+ operator: "=",
266
+ value1: String(instanceId),
267
+ },
268
+ ],
269
+ };
270
+
271
+ const instRes = await queryData(instParam, apiConfig);
272
+ if (!instRes || instRes.length === 0)
273
+ throw new Error(`Instance ${instanceId} not found.`);
274
+
275
+ const instance = instRes[0];
276
+ const currentStatus = String(instance.status).toUpperCase();
277
+
278
+ if (currentStatus !== "ACTIVE") {
279
+ throw new Error(
280
+ `Action Denied: Cannot view transitions. Instance is currently ${currentStatus}.`,
281
+ );
282
+ }
283
+
284
+ const transParam = {
285
+ length: -1,
286
+ columns: [
287
+ { data: "SYS$BPM_TRANS_DEF.ID", key: "id" },
288
+ {
289
+ data: "SYS$BPM_TRANS_DEF.TRANSITION_CODE",
290
+ key: "transition_code",
291
+ },
292
+ { data: "SYS$BPM_TRANS_DEF.LABEL", key: "label" },
293
+ {
294
+ data: "SYS$BPM_TRANS_DEF.FROM_STATE_CODE",
295
+ key: "from_state_code",
296
+ },
297
+ { data: "SYS$BPM_TRANS_DEF.TO_STATE_CODE", key: "to_state_code" },
298
+ { data: "SYS$BPM_TRANS_DEF.GUARD_TYPE", key: "guard_type" },
299
+ { data: "SYS$BPM_TRANS_DEF.GUARD_RULE_KEY", key: "guard_rule_key" },
300
+ ],
301
+ filter: [
302
+ {
303
+ junction: "",
304
+ column: "SYS$BPM_TRANS_DEF.SYS$BPM_WORKFLOW_DEF_ID",
305
+ operator: "=",
306
+ value1: instance.def_id,
307
+ },
308
+ {
309
+ junction: "AND",
310
+ column: "SYS$BPM_TRANS_DEF.FROM_STATE_CODE",
311
+ operator: "=",
312
+ value1: `'${instance.current_state}'`,
313
+ },
314
+ ],
315
+ };
316
+
317
+ const rawTransitions = await queryData(transParam, apiConfig);
318
+ if (!rawTransitions || rawTransitions.length === 0) return [];
319
+
320
+ const permittedTransitions = [];
321
+ for (const t of rawTransitions) {
322
+ const roleQuery = {
323
+ length: -1,
324
+ columns: [
325
+ { data: "SYS$BPM_TRANS_ROLE.ROLE_CODE", key: "role_code" },
326
+ ],
327
+ filter: [
328
+ {
329
+ junction: "",
330
+ column: "SYS$BPM_TRANS_ROLE.SYS$BPM_TRANS_DEF_ID",
331
+ operator: "=",
332
+ value1: t.id,
333
+ },
334
+ ],
335
+ };
336
+ const requiredRoles = (await queryData(roleQuery, apiConfig)) || [];
337
+
338
+ if (requiredRoles.length === 0) {
339
+ permittedTransitions.push(t);
340
+ } else {
341
+ const hasRole = requiredRoles.some((rr) =>
342
+ userRoles.includes(String(rr.role_code).toUpperCase()),
343
+ );
344
+ if (hasRole) permittedTransitions.push(t);
345
+ }
346
+ }
347
+
348
+ const evaluatedTransitions = [];
349
+ for (const t of permittedTransitions) {
350
+ const guard = await evaluateGuardRule(
351
+ t.guard_type,
352
+ t.guard_rule_key,
353
+ entityData,
354
+ );
355
+ evaluatedTransitions.push({
356
+ ...t,
357
+ isGuardPassed: guard.isPassed,
358
+ guardHint: guard.isPassed ? "" : ` [DISABLED: ${guard.errorMsg}]`,
359
+ });
360
+ }
361
+
362
+ return evaluatedTransitions;
363
+ }
364
+
365
+ export async function executeTransition(payload) {
366
+ const {
367
+ instanceId,
368
+ transition,
369
+ userRoles = [],
370
+ entityData = {},
371
+ apiConfig,
372
+ userId,
373
+ } = payload;
374
+
375
+ const instParam = {
376
+ length: 1,
377
+ columns: [
378
+ {
379
+ data: "SYS$BPM_WF_INSTANCE.SYS$BPM_WORKFLOW_DEF_ID",
380
+ key: "wf_def_id",
381
+ },
382
+ { data: "SYS$BPM_WF_INSTANCE.STATUS", key: "status" },
383
+ {
384
+ data: "SYS$BPM_WF_INSTANCE.CURRENT_STATE_CODE",
385
+ key: "current_state_code",
386
+ },
387
+ ],
388
+ filter: [
389
+ {
390
+ junction: "",
391
+ column: "SYS$BPM_WF_INSTANCE.ID",
392
+ operator: "=",
393
+ value1: String(instanceId),
394
+ },
395
+ ],
396
+ };
397
+ const instances = await queryData(instParam, apiConfig);
398
+ if (!instances || instances.length === 0)
399
+ throw new Error(`Instance ID ${instanceId} not found.`);
400
+ const instanceRecord = instances[0];
401
+
402
+ const currentStatus = String(instanceRecord.status).toUpperCase();
403
+ if (currentStatus !== "ACTIVE")
404
+ throw new Error(`Action Denied: Instance is ${currentStatus}.`);
405
+ if (instanceRecord.current_state_code !== transition.from_state_code)
406
+ throw new Error(
407
+ `Action Denied: State has changed. Expected '${transition.from_state_code}'.`,
408
+ );
409
+
410
+ const guard = await evaluateGuardRule(
411
+ transition.guard_type,
412
+ transition.guard_rule_key,
413
+ entityData,
414
+ );
415
+ if (!guard.isPassed) throw new Error(`Action Denied: ${guard.errorMsg}`);
416
+
417
+ const wfDefId = Number(instanceRecord.wf_def_id);
418
+
419
+ const stateParam = {
420
+ length: 1,
421
+ columns: [
422
+ { data: "SYS$BPM_STATE_DEF.STATE_CODE", key: "state_code" },
423
+ { data: "SYS$BPM_STATE_DEF.TASK_ENABLED", key: "task_enabled" },
424
+ { data: "SYS$BPM_STATE_DEF.TASK_NAME", key: "task_name" },
425
+ {
426
+ data: "SYS$BPM_STATE_DEF.ASSIGNMENT_TYPE",
427
+ key: "assignment_type",
428
+ },
429
+ {
430
+ data: "SYS$BPM_STATE_DEF.ASSIGNMENT_VALUE",
431
+ key: "assignment_value",
432
+ },
433
+ {
434
+ data: "SYS$BPM_STATE_DEF.SLA_DURATION_MINUTES",
435
+ key: "sla_duration_minutes",
436
+ },
437
+ ],
438
+ filter: [
439
+ {
440
+ junction: "",
441
+ column: "SYS$BPM_STATE_DEF.SYS$BPM_WORKFLOW_DEF_ID",
442
+ operator: "=",
443
+ value1: String(wfDefId),
444
+ },
445
+ {
446
+ junction: "AND",
447
+ column: "SYS$BPM_STATE_DEF.STATE_CODE",
448
+ operator: "=",
449
+ value1: `'${transition.to_state_code}'`,
450
+ },
451
+ ],
452
+ };
453
+ const states = await queryData(stateParam, apiConfig);
454
+ const targetStateDef = states.length > 0 ? states[0] : null;
455
+
456
+ const effectsQuery = {
457
+ length: -1,
458
+ columns: [
459
+ {
460
+ data: "SYS$BPM_TRANS_EFFECT_DEF.EFFECT_TYPE",
461
+ key: "effect_type",
462
+ },
463
+ { data: "SYS$BPM_TRANS_EFFECT_DEF.EVENT_NAME", key: "event_name" },
464
+ ],
465
+ filter: [
466
+ {
467
+ junction: "",
468
+ column: "SYS$BPM_TRANS_EFFECT_DEF.SYS$BPM_TRANS_DEF_ID",
469
+ operator: "=",
470
+ value1: transition.id,
471
+ },
472
+ ],
473
+ };
474
+ const effects = (await queryData(effectsQuery, apiConfig)) || [];
475
+
476
+ const outgoingTransQuery = {
477
+ length: 1,
478
+ columns: [{ data: "SYS$BPM_TRANS_DEF.ID", key: "id" }],
479
+ filter: [
480
+ {
481
+ junction: "",
482
+ column: "SYS$BPM_TRANS_DEF.SYS$BPM_WORKFLOW_DEF_ID",
483
+ operator: "=",
484
+ value1: String(wfDefId),
485
+ },
486
+ {
487
+ junction: "AND",
488
+ column: "SYS$BPM_TRANS_DEF.FROM_STATE_CODE",
489
+ operator: "=",
490
+ value1: `'${transition.to_state_code}'`,
491
+ },
492
+ ],
493
+ };
494
+ const outgoingTrans =
495
+ (await queryData(outgoingTransQuery, apiConfig)) || [];
496
+ const isTerminal = outgoingTrans.length === 0;
497
+
498
+ const rolesQuery = {
499
+ length: -1,
500
+ columns: [{ data: "SYS$BPM_TRANS_ROLE.ROLE_CODE", key: "role_code" }],
501
+ filter: [
502
+ {
503
+ junction: "",
504
+ column: "SYS$BPM_TRANS_ROLE.SYS$BPM_TRANS_DEF_ID",
505
+ operator: "=",
506
+ value1: transition.id,
507
+ },
508
+ ],
509
+ };
510
+ const requiredRoles = (await queryData(rolesQuery, apiConfig)) || [];
511
+
512
+ if (requiredRoles.length > 0) {
513
+ const hasRole = requiredRoles.some((rr) =>
514
+ userRoles.includes(String(rr.role_code).toUpperCase()),
515
+ );
516
+ if (!hasRole)
517
+ throw new Error(
518
+ `Action Denied: You lack the required authorization role to execute this transition.`,
519
+ );
520
+ }
521
+
522
+ const tasksTimers = await fetchInstanceTasksAndTimers({
523
+ instanceId,
524
+ apiConfig,
525
+ });
526
+
527
+ const rawTasks = tasksTimers[0] || [];
528
+ const rawTimers = tasksTimers[1] || [];
529
+
530
+ const openTask = rawTasks.find(
531
+ (t) => t.status === "OPEN" || t.STATUS === "OPEN",
532
+ );
533
+ if (openTask) {
534
+ openTask.status = "DONE";
535
+ openTask.completed_by = userId;
536
+ openTask.closed_reason = "TRANSITIONED";
537
+ }
538
+
539
+ rawTimers.forEach((t) => {
540
+ if (t.status === "OPEN" || t.STATUS === "OPEN") t.status = "CANCELLED";
541
+ });
542
+
543
+ const oldStandaloneTimers = rawTimers.filter(
544
+ (t) => !t.sys$bpm_task_id && !t.SYS$BPM_TASK_ID,
545
+ );
546
+
547
+ const oldTaskTimers = rawTimers.filter(
548
+ (t) => t.sys$bpm_task_id || t.SYS$BPM_TASK_ID,
549
+ );
550
+
551
+ const processedTasks = rawTasks.map((t) => {
552
+ const myOldTimers = oldTaskTimers.filter(
553
+ (tmr) =>
554
+ String(tmr.sys$bpm_task_id || tmr.SYS$BPM_TASK_ID) ===
555
+ String(t.id || t.ID),
556
+ );
557
+ if (myOldTimers.length > 0) {
558
+ t.SYS$BPM_TIMER_JOB = myOldTimers;
559
+ }
560
+ return t;
561
+ });
562
+
563
+ const entryDetails = buildStateEntryDetails(
564
+ targetStateDef,
565
+ isTerminal,
566
+ instanceId,
567
+ );
568
+
569
+ const transitionPayload = {
570
+ SYS$BPM_WF_INSTANCE: {
571
+ id: Number(instanceId),
572
+ current_state_code: transition.to_state_code,
573
+ status: isTerminal ? "COMPLETED" : "ACTIVE",
574
+ SYS$BPM_TIMER_JOB: [
575
+ ...oldStandaloneTimers,
576
+ ...entryDetails.standaloneTimers,
577
+ ],
578
+ SYS$BPM_TASK: [...processedTasks, ...entryDetails.tasks],
579
+ },
580
+ SYS$BPM_STATE_HISTORY: {
581
+ id: null,
582
+ sys$bpm_wf_instance_id: Number(instanceId),
583
+ from_state_code: transition.from_state_code,
584
+ to_state_code: transition.to_state_code,
585
+ sys$bpm_trans_def_id: transition.id,
586
+ trigger_type: "MANUAL",
587
+ actor_type: "USER",
588
+ actor_id: userId,
589
+ request_id: `TRANS_${transition.transition_code}_${instanceId}_${new Date().getTime()}`,
590
+ },
591
+ };
592
+
593
+ if (effects && effects.length > 0) {
594
+ transitionPayload.SYS$BPM_EFFECT_LOG = effects.map((eff) => ({
595
+ id: null,
596
+ sys$bpm_wf_instance_id: Number(instanceId),
597
+ sys$bpm_trans_def_id: transition.id,
598
+ effect_type: eff.effect_type,
599
+ event_name: eff.event_name,
600
+ status: "PENDING",
601
+ retry_count: 0,
602
+ }));
603
+ }
604
+
605
+ const result = await dbSave(transitionPayload, apiConfig);
606
+ return result.data;
607
+ }
608
+
609
+ export async function terminate(payload) {
610
+ const { instanceId, apiConfig, userId } = payload;
611
+
612
+ const queryParam = {
613
+ length: 1,
614
+ columns: [
615
+ { data: "SYS$BPM_WF_INSTANCE.STATUS", key: "status" },
616
+ {
617
+ data: "SYS$BPM_WF_INSTANCE.CURRENT_STATE_CODE",
618
+ key: "current_state_code",
619
+ },
620
+ ],
621
+ filter: [
622
+ {
623
+ junction: "",
624
+ column: "SYS$BPM_WF_INSTANCE.ID",
625
+ operator: "=",
626
+ value1: String(instanceId),
627
+ },
628
+ ],
629
+ };
630
+
631
+ const records = (await queryData(queryParam, apiConfig)) || [];
632
+ if (records.length === 0)
633
+ throw new Error(`Instance ID ${instanceId} not found.`);
634
+
635
+ const currentStatus = String(records[0].status).toUpperCase();
636
+ const currentStateCode = records[0].current_state_code;
637
+
638
+ if (currentStatus === "TERMINATED" || currentStatus === "COMPLETED") {
639
+ throw new Error(`Action Denied: Instance is already ${currentStatus}.`);
640
+ }
641
+ if (currentStatus !== "ACTIVE") {
642
+ throw new Error(
643
+ `Action Denied: Cannot terminate instance with status ${currentStatus}.`,
644
+ );
645
+ }
646
+
647
+ const tasksTimers = await fetchInstanceTasksAndTimers({
648
+ instanceId,
649
+ apiConfig,
650
+ });
651
+ const rawTasks = tasksTimers[0] || [];
652
+ const rawTimers = tasksTimers[1] || [];
653
+
654
+ rawTasks.forEach((t) => {
655
+ if (t.status === "OPEN" || t.STATUS === "OPEN") {
656
+ t.status = "CANCELLED";
657
+ t.closed_reason = "TERMINATED";
658
+ }
659
+ });
660
+
661
+ rawTimers.forEach((t) => {
662
+ if (t.status === "OPEN" || t.STATUS === "OPEN") t.status = "CANCELLED";
663
+ });
664
+
665
+ const standaloneTimers = rawTimers.filter(
666
+ (t) => !t.sys$bpm_task_id && !t.SYS$BPM_TASK_ID,
667
+ );
668
+ const taskTimers = rawTimers.filter(
669
+ (t) => t.sys$bpm_task_id || t.SYS$BPM_TASK_ID,
670
+ );
671
+
672
+ const processedTasks = rawTasks.map((t) => {
673
+ const myOldTimers = taskTimers.filter(
674
+ (tmr) =>
675
+ String(tmr.sys$bpm_task_id || tmr.SYS$BPM_TASK_ID) ===
676
+ String(t.id || t.ID),
677
+ );
678
+ if (myOldTimers.length > 0) t.SYS$BPM_TIMER_JOB = myOldTimers;
679
+ return t;
680
+ });
681
+
682
+ const ormPayload = {
683
+ SYS$BPM_WF_INSTANCE: {
684
+ id: Number(instanceId),
685
+ status: "TERMINATED",
686
+ SYS$BPM_TASK: processedTasks,
687
+ SYS$BPM_TIMER_JOB: standaloneTimers,
688
+ },
689
+ SYS$BPM_STATE_HISTORY: {
690
+ id: null,
691
+ sys$bpm_wf_instance_id: Number(instanceId),
692
+ from_state_code: currentStateCode,
693
+ to_state_code: currentStateCode,
694
+ trigger_type: "MANUAL",
695
+ actor_type: "USER",
696
+ actor_id: userId || null,
697
+ request_id: `TERMINATE_${instanceId}_${new Date().getTime()}`,
698
+ },
699
+ };
700
+
701
+ const result = await dbSave(ormPayload, apiConfig);
702
+ return result.data;
703
+ }
704
+
705
+ export async function remove(payload) {
706
+ const { instanceId, apiConfig } = payload;
707
+ const detailTables = [
708
+ "SYS$BPM_STATE_HISTORY",
709
+ "SYS$BPM_TASK",
710
+ "SYS$BPM_TIMER_JOB",
711
+ "SYS$BPM_EFFECT_LOG",
712
+ ];
713
+
714
+ const result = await deleteMD(
715
+ Number(instanceId),
716
+ "SYS$BPM_WF_INSTANCE",
717
+ detailTables,
718
+ apiConfig,
719
+ );
720
+ return result.data;
721
+ }
722
+
723
+ export async function myTasks(payload) {
724
+ const { userRoles = [], apiConfig } = payload;
725
+
726
+ if (userRoles.length === 0) return [];
727
+
728
+ const roleSqlArray = userRoles.map((r) => `'${r}'`).join(",");
729
+
730
+ const param = {
731
+ length: -1,
732
+ columns: [
733
+ { data: "SYS$BPM_TASK.ID", key: "id" },
734
+ { data: "SYS$BPM_TASK.SYS$BPM_WF_INSTANCE_ID", key: "instance_id" },
735
+ { data: "SYS$BPM_TASK.NAME", key: "name", title: "Task Name" },
736
+ {
737
+ data: "(CAST(SYS$BPM_TASK.CREATED_AT AS VARCHAR(24)))",
738
+ key: "created_at",
739
+ title: "Assigned At",
740
+ alias: "created_at",
741
+ },
742
+ {
743
+ data: "(CAST(SYS$BPM_TASK.DUE_AT AS VARCHAR(24)))",
744
+ key: "due_date",
745
+ title: "Due At",
746
+ alias: "due_date",
747
+ },
748
+ ],
749
+ filter: [
750
+ {
751
+ junction: "",
752
+ column: "SYS$BPM_TASK.STATUS",
753
+ operator: "=",
754
+ value1: "'OPEN'",
755
+ },
756
+ {
757
+ junction: "and",
758
+ column: "SYS$BPM_TASK.ASSIGNMENT_TYPE",
759
+ operator: "=",
760
+ value1: "'ROLE'",
761
+ },
762
+ {
763
+ junction: "and",
764
+ column: "SYS$BPM_TASK.ASSIGNMENT_VALUE",
765
+ operator: "in",
766
+ value1: `(${roleSqlArray})`,
767
+ },
768
+ ],
769
+ };
770
+
771
+ const res = await queryData(param, apiConfig);
772
+ return res || [];
773
+ }
774
+
775
+ async function fetchInstanceTasksAndTimers(payload) {
776
+ const { instanceId, apiConfig } = payload;
777
+
778
+ const taskQuery = {
779
+ length: -1,
780
+ columns: [
781
+ { data: "SYS$BPM_TASK.ID", key: "id" },
782
+ {
783
+ data: "SYS$BPM_TASK.SYS$BPM_WF_INSTANCE_ID",
784
+ key: "sys$bpm_wf_instance_id",
785
+ },
786
+ { data: "SYS$BPM_TASK.STATE_CODE", key: "state_code" },
787
+ { data: "SYS$BPM_TASK.NAME", key: "name" },
788
+ { data: "SYS$BPM_TASK.STATUS", key: "status" },
789
+ { data: "SYS$BPM_TASK.ASSIGNMENT_TYPE", key: "assignment_type" },
790
+ { data: "SYS$BPM_TASK.ASSIGNMENT_VALUE", key: "assignment_value" },
791
+ { data: "SYS$BPM_TASK.ASSIGNED_USER_ID", key: "assigned_user_id" },
792
+ { data: "SYS$BPM_TASK.SLA_MINUTES", key: "sla_minutes" },
793
+ { data: "SYS$BPM_TASK.COMPLETED_BY", key: "completed_by" },
794
+ { data: "SYS$BPM_TASK.CLOSED_REASON", key: "closed_reason" },
795
+ {
796
+ data: "(CAST(SYS$BPM_TASK.CREATED_AT AS VARCHAR(24)))",
797
+ key: "created_at",
798
+ alias: "CREATED_AT",
799
+ },
800
+ {
801
+ data: "(CAST(SYS$BPM_TASK.DUE_AT AS VARCHAR(24)))",
802
+ key: "due_at",
803
+ alias: "DUE_AT",
804
+ },
805
+ {
806
+ data: "(CAST(SYS$BPM_TASK.COMPLETED_AT AS VARCHAR(24)))",
807
+ key: "completed_at",
808
+ alias: "COMPLETED_AT",
809
+ },
810
+ ],
811
+ filter: [
812
+ {
813
+ junction: "",
814
+ column: "SYS$BPM_TASK.SYS$BPM_WF_INSTANCE_ID",
815
+ operator: "=",
816
+ value1: String(instanceId),
817
+ },
818
+ ],
819
+ };
820
+
821
+ const timerQuery = {
822
+ length: -1,
823
+ columns: [
824
+ { data: "SYS$BPM_TIMER_JOB.ID", key: "id" },
825
+ {
826
+ data: "SYS$BPM_TIMER_JOB.SYS$BPM_WF_INSTANCE_ID",
827
+ key: "sys$bpm_wf_instance_id",
828
+ },
829
+ {
830
+ data: "SYS$BPM_TIMER_JOB.SYS$BPM_TASK_ID",
831
+ key: "sys$bpm_task_id",
832
+ },
833
+ { data: "SYS$BPM_TIMER_JOB.STATE_CODE", key: "state_code" },
834
+ { data: "SYS$BPM_TIMER_JOB.TIMER_TYPE", key: "timer_type" },
835
+ { data: "SYS$BPM_TIMER_JOB.STATUS", key: "status" },
836
+ { data: "SYS$BPM_TIMER_JOB.SLA_MINUTES", key: "sla_minutes" },
837
+ {
838
+ data: "(CAST(SYS$BPM_TIMER_JOB.DUE_AT AS VARCHAR(24)))",
839
+ key: "due_at",
840
+ alias: "DUE_AT",
841
+ },
842
+ {
843
+ data: "(CAST(SYS$BPM_TIMER_JOB.FIRED_AT AS VARCHAR(24)))",
844
+ key: "fired_at",
845
+ alias: "FIRED_AT",
846
+ },
847
+ ],
848
+ filter: [
849
+ {
850
+ junction: "",
851
+ column: "SYS$BPM_TIMER_JOB.SYS$BPM_WF_INSTANCE_ID",
852
+ operator: "=",
853
+ value1: String(instanceId),
854
+ },
855
+ ],
856
+ };
857
+
858
+ return await Promise.all([
859
+ queryData(taskQuery, apiConfig),
860
+ queryData(timerQuery, apiConfig),
861
+ ]);
862
+ }
863
+
864
+ export async function instanceState(payload) {
865
+ const { instanceId, apiConfig } = payload;
866
+ const instanceQuery = {
867
+ length: -1,
868
+ columns: [
869
+ { data: "SYS$BPM_WF_INSTANCE.ID", key: "id" },
870
+ {
871
+ data: "SYS$BPM_WF_INSTANCE.SYS$BPM_WORKFLOW_DEF_ID",
872
+ key: "SYS$BPM_WORKFLOW_DEF_ID",
873
+ },
874
+ { data: "SYS$BPM_WF_INSTANCE.ENTITY_TYPE", key: "entity_type" },
875
+ { data: "SYS$BPM_WF_INSTANCE.ENTITY_ID", key: "entity_id" },
876
+ {
877
+ data: "SYS$BPM_WF_INSTANCE.CURRENT_STATE_CODE",
878
+ key: "current_state_code",
879
+ },
880
+ { data: "SYS$BPM_WF_INSTANCE.STATUS", key: "status" },
881
+ { data: "SYS$BPM_WF_INSTANCE.VERSION_NO", key: "version_no" },
882
+ {
883
+ data: "(CAST(SYS$BPM_WF_INSTANCE.STARTED_AT AS VARCHAR(24)))",
884
+ key: "started_at",
885
+ alias: "started_at",
886
+ },
887
+ {
888
+ data: "(CAST(SYS$BPM_WF_INSTANCE.ENDED_AT AS VARCHAR(24)))",
889
+ key: "ended_at",
890
+ alias: "ended_at",
891
+ },
892
+ { data: "SYS$BPM_WF_INSTANCE.CREATED_BY", key: "created_by" },
893
+ {
894
+ data: "(CAST(SYS$BPM_WF_INSTANCE.UPDATED_AT AS VARCHAR(24)))",
895
+ key: "updated_at",
896
+ alias: "updated_at",
897
+ },
898
+ ],
899
+ filter: [
900
+ {
901
+ junction: "",
902
+ column: "SYS$BPM_WF_INSTANCE.ID",
903
+ operator: "=",
904
+ value1: String(instanceId),
905
+ },
906
+ ],
907
+ };
908
+ return await queryData(instanceQuery, apiConfig);
909
+ }
910
+
911
+ export async function instanceLogs(payload) {
912
+ const { instanceId, apiConfig } = payload;
913
+ const historyQuery = {
914
+ length: -1,
915
+ columns: [
916
+ { data: "SYS$BPM_STATE_HISTORY.ID", key: "id" },
917
+ {
918
+ data: "SYS$BPM_STATE_HISTORY.SYS$BPM_TRANS_DEF_ID",
919
+ key: "sys$bpm_trans_def_id",
920
+ },
921
+ {
922
+ data: "SYS$BPM_STATE_HISTORY.FROM_STATE_CODE",
923
+ key: "from_state_code",
924
+ },
925
+ {
926
+ data: "SYS$BPM_STATE_HISTORY.TO_STATE_CODE",
927
+ key: "to_state_code",
928
+ },
929
+ {
930
+ data: "SYS$BPM_STATE_HISTORY.TRIGGER_TYPE",
931
+ key: "trigger_type",
932
+ },
933
+ {
934
+ data: "SYS$BPM_STATE_HISTORY.ACTOR_TYPE",
935
+ key: "actor_type",
936
+ },
937
+ { data: "SYS$BPM_STATE_HISTORY.ACTOR_ID", key: "actor_id" },
938
+ { data: "SYS$BPM_STATE_HISTORY.REQUEST_ID", key: "request_id" },
939
+ {
940
+ data: "(CAST(SYS$BPM_STATE_HISTORY.CREATED_AT AS VARCHAR(24)))",
941
+ key: "created_at",
942
+ alias: "created_at",
943
+ },
944
+ ],
945
+ filter: [
946
+ {
947
+ junction: "",
948
+ column: "SYS$BPM_STATE_HISTORY.SYS$BPM_WF_INSTANCE_ID",
949
+ operator: "=",
950
+ value1: String(instanceId),
951
+ },
952
+ ],
953
+ orders: [{ column: "SYS$BPM_STATE_HISTORY.CREATED_AT", dir: "DESC" }],
954
+ };
955
+ return await queryData(historyQuery, apiConfig);
956
+ }
957
+
958
+ export async function instanceTasks(payload) {
959
+ const { instanceId, openOnly, apiConfig } = payload;
960
+ const filters = [
961
+ {
962
+ junction: "",
963
+ column: "SYS$BPM_TASK.SYS$BPM_WF_INSTANCE_ID",
964
+ operator: "=",
965
+ value1: String(instanceId),
966
+ },
967
+ ];
968
+
969
+ if (openOnly) {
970
+ filters.push({
971
+ junction: "AND",
972
+ column: "SYS$BPM_TASK.STATUS",
973
+ operator: "=",
974
+ value1: "'OPEN'",
975
+ });
976
+ }
977
+
978
+ const taskQuery = {
979
+ length: -1,
980
+ columns: [
981
+ { data: "SYS$BPM_TASK.ID", key: "id" },
982
+ {
983
+ data: "SYS$BPM_TASK.SYS$BPM_WF_INSTANCE_ID",
984
+ key: "sys$bpm_wf_instance_id",
985
+ },
986
+ { data: "SYS$BPM_TASK.STATE_CODE", key: "state_code" },
987
+ { data: "SYS$BPM_TASK.NAME", key: "name" },
988
+ { data: "SYS$BPM_TASK.STATUS", key: "status" },
989
+ { data: "SYS$BPM_TASK.ASSIGNMENT_TYPE", key: "assignment_type" },
990
+ { data: "SYS$BPM_TASK.ASSIGNMENT_VALUE", key: "assignment_value" },
991
+ { data: "SYS$BPM_TASK.ASSIGNED_USER_ID", key: "assigned_user_id" },
992
+ {
993
+ data: "(CAST(SYS$BPM_TASK.CREATED_AT AS VARCHAR(24)))",
994
+ key: "created_at",
995
+ alias: "created_at",
996
+ },
997
+ {
998
+ data: "(CAST(SYS$BPM_TASK.DUE_AT AS VARCHAR(24)))",
999
+ key: "due_at",
1000
+ alias: "due_at",
1001
+ },
1002
+ { data: "SYS$BPM_TASK.SLA_MINUTES", key: "sla_minutes" },
1003
+ {
1004
+ data: "(CAST(SYS$BPM_TASK.COMPLETED_AT AS VARCHAR(24)))",
1005
+ key: "completed_at",
1006
+ alias: "completed_at",
1007
+ },
1008
+ { data: "SYS$BPM_TASK.COMPLETED_BY", key: "completed_by" },
1009
+ { data: "SYS$BPM_TASK.CLOSED_REASON", key: "closed_reason" },
1010
+ ],
1011
+ filter: filters,
1012
+ orders: [{ column: "SYS$BPM_TASK.CREATED_AT", dir: "DESC" }],
1013
+ };
1014
+ return await queryData(taskQuery, apiConfig);
1015
+ }
1016
+
1017
+ export async function instanceTimers(payload) {
1018
+ const { instanceId, apiConfig } = payload;
1019
+ const timerQuery = {
1020
+ length: -1,
1021
+ columns: [
1022
+ { data: "SYS$BPM_TIMER_JOB.ID", key: "id" },
1023
+ {
1024
+ data: "SYS$BPM_TIMER_JOB.SYS$BPM_WF_INSTANCE_ID",
1025
+ key: "sys$bpm_wf_instance_id",
1026
+ },
1027
+ {
1028
+ data: "SYS$BPM_TIMER_JOB.SYS$BPM_TASK_ID",
1029
+ key: "sys$bpm_task_id",
1030
+ },
1031
+ { data: "SYS$BPM_TIMER_JOB.STATE_CODE", key: "state_code" },
1032
+ { data: "SYS$BPM_TIMER_JOB.TIMER_TYPE", key: "timer_type" },
1033
+ {
1034
+ data: "(CAST(SYS$BPM_TIMER_JOB.DUE_AT AS VARCHAR(24)))",
1035
+ key: "due_at",
1036
+ alias: "due_at",
1037
+ },
1038
+ { data: "SYS$BPM_TIMER_JOB.SLA_MINUTES", key: "sla_minutes" },
1039
+ { data: "SYS$BPM_TIMER_JOB.STATUS", key: "status" },
1040
+ {
1041
+ data: "(CAST(SYS$BPM_TIMER_JOB.FIRED_AT AS VARCHAR(24)))",
1042
+ key: "fired_at",
1043
+ alias: "fired_at",
1044
+ },
1045
+ ],
1046
+ filter: [
1047
+ {
1048
+ junction: "",
1049
+ column: "SYS$BPM_TIMER_JOB.SYS$BPM_WF_INSTANCE_ID",
1050
+ operator: "=",
1051
+ value1: String(instanceId),
1052
+ },
1053
+ ],
1054
+ };
1055
+ return await queryData(timerQuery, apiConfig);
1056
+ }
1057
+
1058
+ export async function instanceEffectLogs(payload) {
1059
+ const { instanceId, apiConfig } = payload;
1060
+ const effectQuery = {
1061
+ length: -1,
1062
+ columns: [
1063
+ { data: "SYS$BPM_EFFECT_LOG.ID", key: "id" },
1064
+ {
1065
+ data: "SYS$BPM_EFFECT_LOG.SYS$BPM_WF_INSTANCE_ID",
1066
+ key: "sys$bpm_wf_instance_id",
1067
+ },
1068
+ { data: "SYS$BPM_EFFECT_LOG.EFFECT_TYPE", key: "effect_type" },
1069
+ { data: "SYS$BPM_EFFECT_LOG.EVENT_NAME", key: "event_name" },
1070
+ {
1071
+ data: "SYS$BPM_EFFECT_LOG.PAYLOAD_EVALUATED",
1072
+ key: "payload_evaluated",
1073
+ },
1074
+ { data: "SYS$BPM_EFFECT_LOG.STATUS", key: "status" },
1075
+ { data: "SYS$BPM_EFFECT_LOG.RETRY_COUNT", key: "retry_count" },
1076
+ {
1077
+ data: "(CAST(SYS$BPM_EFFECT_LOG.CREATED_AT AS VARCHAR(24)))",
1078
+ key: "created_at",
1079
+ alias: "created_at",
1080
+ },
1081
+ {
1082
+ data: "(CAST(SYS$BPM_EFFECT_LOG.EXECUTED_AT AS VARCHAR(24)))",
1083
+ key: "executed_at",
1084
+ alias: "executed_at",
1085
+ },
1086
+ { data: "SYS$BPM_EFFECT_LOG.ERROR_MESSAGE", key: "error_message" },
1087
+ ],
1088
+ filter: [
1089
+ {
1090
+ junction: "",
1091
+ column: "SYS$BPM_EFFECT_LOG.SYS$BPM_WF_INSTANCE_ID",
1092
+ operator: "=",
1093
+ value1: String(instanceId),
1094
+ },
1095
+ ],
1096
+ orders: [{ column: "SYS$BPM_EFFECT_LOG.CREATED_AT", dir: "DESC" }],
1097
+ };
1098
+ return await queryData(effectQuery, apiConfig);
1099
+ }
1100
+
1101
+ export async function retryEffect(payload) {
1102
+ const { effectId, apiConfig } = payload;
1103
+
1104
+ const effectQuery = {
1105
+ length: -1,
1106
+ columns: [
1107
+ { data: "SYS$BPM_EFFECT_LOG.ID", key: "id" },
1108
+ { data: "SYS$BPM_EFFECT_LOG.RETRY_COUNT", key: "retry_count" },
1109
+ ],
1110
+ filter: [
1111
+ {
1112
+ junction: "",
1113
+ column: "SYS$BPM_EFFECT_LOG.ID",
1114
+ operator: "=",
1115
+ value1: String(effectId),
1116
+ },
1117
+ ],
1118
+ };
1119
+ const effectRes = await queryData(effectQuery, apiConfig);
1120
+
1121
+ if (!effectRes.data || effectRes.data.length === 0) {
1122
+ throw new Error("Effect not found.");
1123
+ }
1124
+
1125
+ const currentRetry = parseInt(effectRes.data[0].retry_count, 10) || 0;
1126
+
1127
+ const dbModel = {
1128
+ id: effectId,
1129
+ status: "PENDING",
1130
+ retry_count: currentRetry + 1,
1131
+ error_message: "",
1132
+ };
1133
+
1134
+ const result = await dbSave({ SYS$BPM_EFFECT_LOG: dbModel });
1135
+ return result.data;
1136
+ }