@nocobase/plugin-workflow 2.2.0-beta.1 → 2.2.0-beta.11

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.
Files changed (50) hide show
  1. package/dist/client/618.c91be3f6e4d73501.js +10 -0
  2. package/dist/client/{67.452743ce8ec30617.js → 67.61e70b8b777f8638.js} +1 -1
  3. package/dist/client/964.5e11e1820fece37b.js +10 -0
  4. package/dist/client/Branch.d.ts +1 -1
  5. package/dist/client/index.js +1 -1
  6. package/dist/client/nodes/output.d.ts +1 -1
  7. package/dist/client/nodes/query.d.ts +1 -1
  8. package/dist/client/style.d.ts +1 -0
  9. package/dist/client/triggers/collection.d.ts +29 -2
  10. package/dist/externalVersion.js +13 -12
  11. package/dist/locale/de-DE.json +1 -0
  12. package/dist/locale/en-US.json +5 -1
  13. package/dist/locale/es-ES.json +1 -0
  14. package/dist/locale/fr-FR.json +1 -0
  15. package/dist/locale/hu-HU.json +1 -0
  16. package/dist/locale/id-ID.json +1 -0
  17. package/dist/locale/it-IT.json +1 -0
  18. package/dist/locale/ja-JP.json +1 -0
  19. package/dist/locale/ko-KR.json +1 -0
  20. package/dist/locale/nl-NL.json +1 -0
  21. package/dist/locale/pt-BR.json +1 -0
  22. package/dist/locale/ru-RU.json +1 -0
  23. package/dist/locale/tr-TR.json +1 -0
  24. package/dist/locale/uk-UA.json +1 -0
  25. package/dist/locale/vi-VN.json +1 -0
  26. package/dist/locale/zh-CN.json +5 -1
  27. package/dist/locale/zh-TW.json +1 -0
  28. package/dist/node_modules/cron-parser/package.json +1 -1
  29. package/dist/node_modules/joi/package.json +1 -1
  30. package/dist/node_modules/lru-cache/package.json +1 -1
  31. package/dist/node_modules/nodejs-snowflake/package.json +1 -1
  32. package/dist/server/Dispatcher.d.ts +4 -2
  33. package/dist/server/Dispatcher.js +103 -67
  34. package/dist/server/ExecutionTimeoutManager.js +2 -2
  35. package/dist/server/Plugin.d.ts +1 -1
  36. package/dist/server/Processor.d.ts +16 -0
  37. package/dist/server/Processor.js +112 -0
  38. package/dist/server/actions/nodes.js +36 -1
  39. package/dist/server/index.d.ts +1 -1
  40. package/dist/server/instructions/CreateInstruction.js +6 -2
  41. package/dist/server/instructions/DestroyInstruction.js +2 -1
  42. package/dist/server/instructions/QueryInstruction.js +2 -1
  43. package/dist/server/instructions/UpdateInstruction.js +2 -1
  44. package/dist/server/triggers/CollectionTrigger.d.ts +5 -0
  45. package/dist/server/triggers/CollectionTrigger.js +26 -1
  46. package/dist/server/triggers/ScheduleTrigger/StaticScheduleTrigger.d.ts +1 -1
  47. package/dist/server/utils.js +27 -27
  48. package/package.json +2 -2
  49. package/dist/client/618.19af7f84261c815d.js +0 -10
  50. package/dist/client/964.ffbf5b47ed12bbdc.js +0 -10
@@ -35,6 +35,7 @@ var import_constants = require("./constants");
35
35
  var import_Plugin = require("./Plugin");
36
36
  var import_utils = require("./utils");
37
37
  const EXECUTION_ACQUIRE_MAX_ATTEMPTS = 5;
38
+ const PENDING_DISPATCH_MAX_ATTEMPTS = 3;
38
39
  class Dispatcher {
39
40
  constructor(plugin) {
40
41
  this.plugin = plugin;
@@ -53,7 +54,7 @@ class Dispatcher {
53
54
  const execution = await ExecutionRepo.findOne({
54
55
  filterByTk: event.executionId
55
56
  });
56
- if (!execution || execution.dispatched) {
57
+ if (!execution || execution.dispatched || execution.status !== import_constants.EXECUTION_STATUS.QUEUEING) {
57
58
  return;
58
59
  }
59
60
  this.plugin.getLogger(execution.workflowId).info(`execution (${execution.id}) received from queue, adding to pending list`);
@@ -70,7 +71,7 @@ class Dispatcher {
70
71
  if (!this.ready) {
71
72
  logger.warn(`app is not ready, event of workflow ${workflow.id} will be ignored`);
72
73
  logger.debug(`ignored event data:`, context);
73
- return;
74
+ return this.handleTriggerFail(workflow, context, options, new Error("app is not ready"));
74
75
  }
75
76
  if (!options.force && !options.manually && !workflow.enabled) {
76
77
  logger.warn(`workflow ${workflow.id} is not enabled, event will be ignored`);
@@ -87,7 +88,7 @@ class Dispatcher {
87
88
  }
88
89
  if (context == null) {
89
90
  logger.warn(`workflow ${workflow.id} event data context is null, event will be ignored`);
90
- return;
91
+ return this.handleTriggerFail(workflow, context, options, new Error("event context is null"));
91
92
  }
92
93
  if (options.manually || this.plugin.isWorkflowSync(workflow)) {
93
94
  return this.triggerSync(workflow, context, options);
@@ -196,39 +197,56 @@ class Dispatcher {
196
197
  }
197
198
  this.executing = (async () => {
198
199
  let next = null;
199
- const pending = this.pending.shift() ?? null;
200
- if (pending || this.ready && this.plugin.serving()) {
201
- const execution = await this.prepare((pending == null ? void 0 : pending.execution) ?? null, {
202
- immediate: pending == null ? void 0 : pending.immediate
203
- });
204
- if (execution) {
205
- next = [execution, pending == null ? void 0 : pending.job, pending == null ? void 0 : pending.rerun];
200
+ let pending = null;
201
+ try {
202
+ pending = this.pending.shift() ?? null;
203
+ if (pending || this.ready && this.plugin.serving()) {
204
+ const execution = await this.prepare((pending == null ? void 0 : pending.execution) ?? null, {
205
+ immediate: pending == null ? void 0 : pending.immediate
206
+ });
207
+ if (execution) {
208
+ next = [execution, pending == null ? void 0 : pending.job, pending == null ? void 0 : pending.rerun];
209
+ }
210
+ if (pending && next) {
211
+ this.plugin.getLogger(next[0].workflowId).info(`pending execution (${next[0].id}) ready to process`);
212
+ }
213
+ } else {
214
+ this.plugin.getLogger("dispatcher").warn(
215
+ `${import_Plugin.WORKER_JOB_WORKFLOW_PROCESS} is not serving on this instance or app not ready, new dispatching will be ignored`
216
+ );
206
217
  }
207
- if (pending && next) {
208
- this.plugin.getLogger(next[0].workflowId).info(`pending execution (${next[0].id}) ready to process`);
218
+ if (next) {
219
+ try {
220
+ await this.process(next[0], next[1], { rerun: next[2] });
221
+ } catch (error) {
222
+ this.plugin.getLogger(next[0].workflowId).error(`execution (${next[0].id}) process failed`, { error });
223
+ if (pending && (0, import_utils.isLockAcquireError)(error)) {
224
+ this.pending.unshift({ ...pending, execution: next[0], immediate: true });
225
+ }
226
+ }
209
227
  }
210
- } else {
211
- this.plugin.getLogger("dispatcher").warn(
212
- `${import_Plugin.WORKER_JOB_WORKFLOW_PROCESS} is not serving on this instance or app not ready, new dispatching will be ignored`
213
- );
214
- }
215
- if (next) {
216
- try {
217
- await this.process(next[0], next[1], { rerun: next[2] });
218
- } catch (error) {
219
- this.plugin.getLogger(next[0].workflowId).error(`execution (${next[0].id}) process failed`, { error });
220
- if (pending && (0, import_utils.isLockAcquireError)(error)) {
221
- this.pending.unshift({ ...pending, execution: next[0], immediate: true });
228
+ } catch (error) {
229
+ this.plugin.getLogger("dispatcher").error(`workflow dispatch failed`, { error });
230
+ if (pending) {
231
+ const dispatchAttempts = (pending.dispatchAttempts ?? 0) + 1;
232
+ if (dispatchAttempts < PENDING_DISPATCH_MAX_ATTEMPTS) {
233
+ this.pending.push({ ...pending, dispatchAttempts });
234
+ } else {
235
+ this.plugin.getLogger(pending.execution.workflowId).error(`pending execution (${pending.execution.id}) dispatch failed, local retry limit reached`, {
236
+ error,
237
+ dispatchAttempts
238
+ });
222
239
  }
223
240
  }
241
+ } finally {
242
+ setImmediate(() => {
243
+ this.executing = null;
244
+ if (next || this.pending.length) {
245
+ this.plugin.getLogger("dispatcher").debug(`last process finished, will do another dispatch`);
246
+ this.dispatch();
247
+ }
248
+ });
224
249
  }
225
- setImmediate(() => {
226
- this.executing = null;
227
- if (next || this.pending.length) {
228
- this.plugin.getLogger("dispatcher").debug(`last process finished, will do another dispatch`);
229
- this.dispatch();
230
- }
231
- });
232
250
  })();
233
251
  }
234
252
  async run(pending) {
@@ -285,8 +303,15 @@ class Dispatcher {
285
303
  }
286
304
  return valid;
287
305
  }
288
- async createExecution(workflow, context, options) {
306
+ async handleTriggerFail(workflow, context, options, error) {
289
307
  var _a;
308
+ try {
309
+ await ((_a = options.onTriggerFail) == null ? void 0 : _a.call(options, workflow, context, options, error));
310
+ } catch (triggerFailError) {
311
+ this.plugin.getLogger(workflow.id).error(`trigger failure callback failed`, { error: triggerFailError });
312
+ }
313
+ }
314
+ async createExecution(workflow, context, options) {
290
315
  const { deferred } = options;
291
316
  let stack = options.stack;
292
317
  if (options.parentExecutionId && !stack) {
@@ -295,21 +320,34 @@ class Dispatcher {
295
320
  });
296
321
  stack = parentExecution ? [...parentExecution.stack ?? [], parentExecution.id] : [];
297
322
  }
298
- const valid = await this.validateEvent(workflow, context, { ...options, stack });
323
+ let valid;
324
+ try {
325
+ valid = await this.validateEvent(workflow, context, { ...options, stack });
326
+ } catch (error) {
327
+ await this.handleTriggerFail(workflow, context, options, error);
328
+ throw error;
329
+ }
299
330
  if (!valid) {
300
- (_a = options.onTriggerFail) == null ? void 0 : _a.call(options, workflow, context, options);
301
- throw new Error("event is not valid");
302
- }
303
- const execution = await workflow.createExecution({
304
- context,
305
- key: workflow.key,
306
- eventKey: options.eventKey ?? (0, import_node_crypto.randomUUID)(),
307
- stack,
308
- parentExecutionId: options.parentExecutionId ?? null,
309
- dispatched: deferred ?? false,
310
- status: deferred ? import_constants.EXECUTION_STATUS.STARTED : import_constants.EXECUTION_STATUS.QUEUEING,
311
- manually: options.manually
312
- });
331
+ const error = new Error("event is not valid");
332
+ await this.handleTriggerFail(workflow, context, options, error);
333
+ throw error;
334
+ }
335
+ let execution;
336
+ try {
337
+ execution = await workflow.createExecution({
338
+ context,
339
+ key: workflow.key,
340
+ eventKey: options.eventKey ?? (0, import_node_crypto.randomUUID)(),
341
+ stack,
342
+ parentExecutionId: options.parentExecutionId ?? null,
343
+ dispatched: deferred ?? false,
344
+ status: deferred ? import_constants.EXECUTION_STATUS.STARTED : import_constants.EXECUTION_STATUS.QUEUEING,
345
+ manually: options.manually
346
+ });
347
+ } catch (error) {
348
+ await this.handleTriggerFail(workflow, context, options, error);
349
+ throw error;
350
+ }
313
351
  this.plugin.getLogger(workflow.id).info(`execution of workflow ${workflow.id} created as ${execution.id}`);
314
352
  if (!workflow.stats) {
315
353
  workflow.stats = await workflow.getStats();
@@ -332,8 +370,7 @@ class Dispatcher {
332
370
  const logger = input ? this.plugin.getLogger(input.workflowId) : this.plugin.getLogger("dispatcher");
333
371
  if (options.transaction) {
334
372
  try {
335
- const { execution } = await this.acquireExecution(input, options, options.transaction);
336
- return execution;
373
+ return await this.acquireExecution(input, options, options.transaction);
337
374
  } catch (error) {
338
375
  if (error instanceof Error) {
339
376
  logger.error(`entering execution failed: ${error.message}`, { error });
@@ -346,13 +383,12 @@ class Dispatcher {
346
383
  await this.acquireWithRetry(
347
384
  async () => {
348
385
  const tx = await this.plugin.db.sequelize.transaction({
349
- isolationLevel: this.plugin.db.options.dialect === "sqlite" ? void 0 : import_sequelize.Transaction.ISOLATION_LEVELS.REPEATABLE_READ
386
+ isolationLevel: this.plugin.db.options.dialect === "sqlite" ? void 0 : import_sequelize.Transaction.ISOLATION_LEVELS.READ_COMMITTED
350
387
  });
351
388
  try {
352
- const { execution, shouldRetry } = await this.acquireExecution(input, options, tx);
389
+ const execution = await this.acquireExecution(input, options, tx);
353
390
  await tx.commit();
354
391
  result = execution;
355
- return shouldRetry;
356
392
  } catch (error) {
357
393
  await tx.rollback();
358
394
  if (this.isConcurrentAcquireError(error)) {
@@ -362,7 +398,6 @@ class Dispatcher {
362
398
  logger.error(`entering execution failed: ${error.message}`, { error });
363
399
  }
364
400
  result = null;
365
- return false;
366
401
  }
367
402
  },
368
403
  {
@@ -386,10 +421,17 @@ class Dispatcher {
386
421
  await execution.reload({ transaction });
387
422
  }
388
423
  } else {
424
+ const workflowIds = [...this.plugin.enabledCache.keys()];
425
+ if (!workflowIds.length) {
426
+ this.plugin.getLogger("dispatcher").debug(`no enabled workflow to process`);
427
+ return null;
428
+ }
389
429
  execution = await this.plugin.db.getRepository("executions").findOne({
390
430
  filter: {
391
431
  dispatched: false,
392
- "workflow.enabled": true
432
+ status: import_constants.EXECUTION_STATUS.QUEUEING,
433
+ startedAt: null,
434
+ workflowId: workflowIds
393
435
  },
394
436
  sort: "id",
395
437
  transaction,
@@ -403,30 +445,24 @@ class Dispatcher {
403
445
  }
404
446
  }
405
447
  if (!execution) {
406
- return { execution: null, shouldRetry: false };
448
+ return null;
407
449
  }
408
- const entered = await this.enter(execution, transaction);
409
- const shouldRetry = !input && !entered;
410
- return { execution: entered, shouldRetry };
450
+ return this.enter(execution, transaction);
411
451
  }
412
452
  async acquireWithRetry(acquire, options) {
413
453
  for (let attempt = 1; attempt <= EXECUTION_ACQUIRE_MAX_ATTEMPTS; attempt++) {
414
- let shouldRetry = false;
415
454
  try {
416
- shouldRetry = await acquire();
455
+ await acquire();
456
+ break;
417
457
  } catch (error) {
418
458
  if (!this.isConcurrentAcquireError(error)) {
419
459
  throw error;
420
460
  }
421
- shouldRetry = true;
422
461
  options.logger.warn(options.conflictMessage, { error });
423
- }
424
- if (!shouldRetry) {
425
- break;
426
- }
427
- if (attempt >= EXECUTION_ACQUIRE_MAX_ATTEMPTS) {
428
- options.logger.warn(options.maxAttemptsMessage);
429
- break;
462
+ if (attempt >= EXECUTION_ACQUIRE_MAX_ATTEMPTS) {
463
+ options.logger.warn(options.maxAttemptsMessage);
464
+ break;
465
+ }
430
466
  }
431
467
  }
432
468
  }
@@ -81,10 +81,10 @@ class ExecutionTimeoutManager {
81
81
  await ((_a = this.scanning) == null ? void 0 : _a.catch(() => {
82
82
  }));
83
83
  }
84
- isExpired(execution, now = /* @__PURE__ */ new Date()) {
84
+ isExpired(execution, now = new Date(Date.now())) {
85
85
  return !!execution.expiresAt && execution.expiresAt.getTime() <= now.getTime();
86
86
  }
87
- getRemainingMs(execution, now = /* @__PURE__ */ new Date()) {
87
+ getRemainingMs(execution, now = new Date(Date.now())) {
88
88
  if (!execution.expiresAt) {
89
89
  return null;
90
90
  }
@@ -74,7 +74,7 @@ export default class PluginWorkflowServer extends Plugin {
74
74
  */
75
75
  load(): Promise<void>;
76
76
  private toggle;
77
- trigger(workflow: WorkflowModel, context: object, options?: EventOptions): void | Promise<Processor | null>;
77
+ trigger(workflow: WorkflowModel, context: object, options?: EventOptions): void | Promise<Processor | null | void>;
78
78
  run(pending: Parameters<Dispatcher['run']>[0]): Promise<void>;
79
79
  dispatch(): void;
80
80
  resume(job: any): Promise<void>;
@@ -29,6 +29,12 @@ export type BackgroundAbortHandle = {
29
29
  dispose: () => void;
30
30
  throwIfAborted: () => void;
31
31
  };
32
+ export type ScopeTransaction = {
33
+ transaction: Transaction;
34
+ dataSource: string;
35
+ isolationLevel?: string;
36
+ closing?: 'commit' | 'rollback';
37
+ };
32
38
  export default class Processor {
33
39
  execution: ExecutionModel;
34
40
  options: ProcessorOptions;
@@ -58,6 +64,7 @@ export default class Processor {
58
64
  private jobsMapByNodeKey;
59
65
  private jobResultsMapByNodeKey;
60
66
  private jobsToSave;
67
+ private scopeTransactions;
61
68
  private rerunContext;
62
69
  /**
63
70
  * @experimental
@@ -111,6 +118,15 @@ export default class Processor {
111
118
  end(node: FlowNodeModel, job: JobModel): Promise<any>;
112
119
  private recall;
113
120
  exit(s?: number | true): Promise<any>;
121
+ setScopeTransaction(key: string, info: ScopeTransaction): void;
122
+ getScopeTransactionByKey(key: string): ScopeTransaction;
123
+ clearScopeTransaction(key: string): void;
124
+ markScopeTransactionClosing(key: string, action: 'commit' | 'rollback'): void;
125
+ getScopeTransaction(node: FlowNodeModel, dataSourceName?: string): Transaction | null;
126
+ isInstructionSync(node: FlowNodeModel): boolean;
127
+ private hasOpenScopeTransactions;
128
+ private markOpenScopePendingJobsAsError;
129
+ private cleanupScopeTransactions;
114
130
  /**
115
131
  * @experimental
116
132
  */
@@ -46,6 +46,7 @@ var import_utils = require("@nocobase/utils");
46
46
  var import_set = __toESM(require("lodash/set"));
47
47
  var import_constants = require("./constants");
48
48
  var import_timeout_errors = require("./timeout-errors");
49
+ const OPEN_SCOPE_PENDING_ERROR = "Pending jobs are not allowed inside an open transaction scope";
49
50
  class Processor {
50
51
  constructor(execution, options) {
51
52
  this.execution = execution;
@@ -79,6 +80,7 @@ class Processor {
79
80
  jobsMapByNodeKey = {};
80
81
  jobResultsMapByNodeKey = {};
81
82
  jobsToSave = /* @__PURE__ */ new Map();
83
+ scopeTransactions = /* @__PURE__ */ new Map();
82
84
  rerunContext = null;
83
85
  /**
84
86
  * @experimental
@@ -447,6 +449,15 @@ class Processor {
447
449
  if (s === true) {
448
450
  return;
449
451
  }
452
+ if ((s == null || s === import_constants.JOB_STATUS.PENDING) && this.hasOpenScopeTransactions()) {
453
+ this.markOpenScopePendingJobsAsError(OPEN_SCOPE_PENDING_ERROR);
454
+ s = import_constants.JOB_STATUS.ERROR;
455
+ }
456
+ const hadScopeTransactions = this.scopeTransactions.size > 0;
457
+ await this.cleanupScopeTransactions({ allowCommit: s === import_constants.JOB_STATUS.RESOLVED });
458
+ if (hadScopeTransactions && s == null && this.execution.status === import_constants.EXECUTION_STATUS.STARTED) {
459
+ s = import_constants.JOB_STATUS.ERROR;
460
+ }
450
461
  if (this.jobsToSave.size) {
451
462
  const newJobs = [];
452
463
  for (const job of this.jobsToSave.values()) {
@@ -520,6 +531,107 @@ class Processor {
520
531
  });
521
532
  return null;
522
533
  }
534
+ setScopeTransaction(key, info) {
535
+ this.scopeTransactions.set(key, info);
536
+ }
537
+ getScopeTransactionByKey(key) {
538
+ return this.scopeTransactions.get(key) ?? null;
539
+ }
540
+ clearScopeTransaction(key) {
541
+ this.scopeTransactions.delete(key);
542
+ }
543
+ markScopeTransactionClosing(key, action) {
544
+ const scopeTransaction = this.scopeTransactions.get(key);
545
+ if (scopeTransaction) {
546
+ scopeTransaction.closing = action;
547
+ }
548
+ }
549
+ getScopeTransaction(node, dataSourceName) {
550
+ for (let current = node; current; current = current.upstream) {
551
+ const scopeTransaction = this.scopeTransactions.get(current.key);
552
+ if (!scopeTransaction) {
553
+ continue;
554
+ }
555
+ if (dataSourceName && scopeTransaction.dataSource !== dataSourceName) {
556
+ continue;
557
+ }
558
+ const branchStartNode = this.findBranchStartNode(node, current);
559
+ if ((branchStartNode == null ? void 0 : branchStartNode.branchIndex) !== 1) {
560
+ continue;
561
+ }
562
+ return scopeTransaction.transaction;
563
+ }
564
+ return null;
565
+ }
566
+ isInstructionSync(node) {
567
+ return this.options.plugin.isWorkflowSync(this.execution.workflow) || Boolean(this.getScopeTransaction(node));
568
+ }
569
+ hasOpenScopeTransactions() {
570
+ for (const { transaction } of this.scopeTransactions.values()) {
571
+ if (!transaction.finished) {
572
+ return true;
573
+ }
574
+ }
575
+ return false;
576
+ }
577
+ markOpenScopePendingJobsAsError(message) {
578
+ const pendingJobs = /* @__PURE__ */ new Set();
579
+ for (const job of this.jobsToSave.values()) {
580
+ if (job.status === import_constants.JOB_STATUS.PENDING) {
581
+ pendingJobs.add(job);
582
+ }
583
+ }
584
+ for (const job of Object.values(this.jobsMapByNodeKey)) {
585
+ if (job.status === import_constants.JOB_STATUS.PENDING) {
586
+ pendingJobs.add(job);
587
+ }
588
+ }
589
+ for (const key of this.scopeTransactions.keys()) {
590
+ const job = this.jobsMapByNodeKey[key];
591
+ if ((job == null ? void 0 : job.status) === import_constants.JOB_STATUS.PENDING) {
592
+ pendingJobs.add(job);
593
+ }
594
+ }
595
+ for (const job of pendingJobs) {
596
+ job.set({
597
+ status: import_constants.JOB_STATUS.ERROR,
598
+ result: {
599
+ message
600
+ }
601
+ });
602
+ this.saveJob(job);
603
+ }
604
+ }
605
+ async cleanupScopeTransactions({ allowCommit = false } = {}) {
606
+ if (!this.scopeTransactions.size) {
607
+ return;
608
+ }
609
+ for (const [key, { transaction, dataSource, closing }] of Array.from(this.scopeTransactions.entries()).reverse()) {
610
+ const tx = transaction;
611
+ if (tx.finished) {
612
+ this.scopeTransactions.delete(key);
613
+ continue;
614
+ }
615
+ const action = closing === "commit" && allowCommit ? "commit" : "rollback";
616
+ const actionText = action === "commit" ? "committing" : "rolling back";
617
+ this.logger.warn(
618
+ `scope transaction (${key}) on data source "${dataSource}" was not closed before exit, ${actionText}`,
619
+ {
620
+ workflowId: this.execution.workflowId
621
+ }
622
+ );
623
+ try {
624
+ await tx[action]();
625
+ } catch (error) {
626
+ this.logger.error(`scope transaction (${key}) fallback ${action} failed`, {
627
+ error,
628
+ workflowId: this.execution.workflowId
629
+ });
630
+ } finally {
631
+ this.scopeTransactions.delete(key);
632
+ }
633
+ }
634
+ }
523
635
  /**
524
636
  * @experimental
525
637
  */
@@ -78,6 +78,23 @@ function validateNode(context, workflow, values) {
78
78
  }
79
79
  }
80
80
  }
81
+ async function touchWorkflow(context, workflowId, transaction) {
82
+ var _a, _b;
83
+ const values = {
84
+ updatedAt: /* @__PURE__ */ new Date()
85
+ };
86
+ const currentUserId = (_b = (_a = context.state) == null ? void 0 : _a.currentUser) == null ? void 0 : _b.id;
87
+ if (currentUserId != null) {
88
+ values.updatedById = currentUserId;
89
+ }
90
+ await context.db.getCollection("workflows").model.update(values, {
91
+ where: {
92
+ id: workflowId
93
+ },
94
+ transaction,
95
+ hooks: false
96
+ });
97
+ }
81
98
  async function create(context, next) {
82
99
  const { db } = context;
83
100
  const repository = import_actions.utils.getRepositoryFromParams(context);
@@ -122,6 +139,7 @@ async function create(context, next) {
122
139
  await instance.setDownstream(previousHead, { transaction });
123
140
  instance.set("downstream", previousHead);
124
141
  }
142
+ await touchWorkflow(context, workflow.id, transaction);
125
143
  return instance;
126
144
  }
127
145
  const upstream = await instance.getUpstream({ transaction });
@@ -162,6 +180,7 @@ async function create(context, next) {
162
180
  }
163
181
  }
164
182
  instance.set("upstream", upstream);
183
+ await touchWorkflow(context, workflow.id, transaction);
165
184
  return instance;
166
185
  });
167
186
  await next();
@@ -233,6 +252,7 @@ async function duplicate(context, next) {
233
252
  await instance.setDownstream(previousHead, { transaction });
234
253
  instance.set("downstream", previousHead);
235
254
  }
255
+ await touchWorkflow(context, origin.workflowId, transaction);
236
256
  return instance;
237
257
  }
238
258
  const upstream = await instance.getUpstream({ transaction });
@@ -273,6 +293,7 @@ async function duplicate(context, next) {
273
293
  }
274
294
  }
275
295
  instance.set("upstream", upstream);
296
+ await touchWorkflow(context, origin.workflowId, transaction);
276
297
  return instance;
277
298
  });
278
299
  await next();
@@ -401,6 +422,7 @@ async function destroy(context, next) {
401
422
  filterByTk: [instance.id, ...branchNodesToDelete.map((item) => item.id)],
402
423
  transaction
403
424
  });
425
+ await touchWorkflow(context, instance.workflowId, transaction);
404
426
  });
405
427
  context.body = instance;
406
428
  await next();
@@ -431,6 +453,7 @@ async function destroyBranch(context, next) {
431
453
  }
432
454
  let deletedBranchHead = null;
433
455
  await db.sequelize.transaction(async (transaction) => {
456
+ let shouldTouchWorkflow = false;
434
457
  const nodes = await repository.find({
435
458
  filter: {
436
459
  workflowId: instance.workflowId
@@ -461,6 +484,7 @@ async function destroyBranch(context, next) {
461
484
  filterByTk: idsToDelete,
462
485
  transaction
463
486
  });
487
+ shouldTouchWorkflow = true;
464
488
  }
465
489
  }
466
490
  if (shift) {
@@ -475,6 +499,12 @@ async function destroyBranch(context, next) {
475
499
  )
476
500
  )
477
501
  );
502
+ if (headsToShift.length) {
503
+ shouldTouchWorkflow = true;
504
+ }
505
+ }
506
+ if (shouldTouchWorkflow) {
507
+ await touchWorkflow(context, instance.workflowId, transaction);
478
508
  }
479
509
  });
480
510
  context.body = deletedBranchHead;
@@ -579,6 +609,7 @@ async function move(context, next) {
579
609
  },
580
610
  { transaction }
581
611
  );
612
+ await touchWorkflow(context, instance.workflowId, transaction);
582
613
  return instance;
583
614
  }
584
615
  if (branchIndex == null) {
@@ -612,6 +643,7 @@ async function move(context, next) {
612
643
  },
613
644
  { transaction }
614
645
  );
646
+ await touchWorkflow(context, instance.workflowId, transaction);
615
647
  return instance;
616
648
  }
617
649
  const branchHead = await repository.findOne({
@@ -640,6 +672,7 @@ async function move(context, next) {
640
672
  },
641
673
  { transaction }
642
674
  );
675
+ await touchWorkflow(context, instance.workflowId, transaction);
643
676
  return instance;
644
677
  });
645
678
  await next();
@@ -660,7 +693,7 @@ async function update(context, next) {
660
693
  }
661
694
  const merged = Object.assign({}, instance.get(), values);
662
695
  validateNode(context, null, merged);
663
- return repository.update({
696
+ const result = await repository.update({
664
697
  filterByTk,
665
698
  values,
666
699
  whitelist,
@@ -670,6 +703,8 @@ async function update(context, next) {
670
703
  context,
671
704
  transaction
672
705
  });
706
+ await touchWorkflow(context, instance.workflowId, transaction);
707
+ return result;
673
708
  });
674
709
  await next();
675
710
  }
@@ -15,6 +15,6 @@ export * from './timeout-errors';
15
15
  export { Trigger } from './triggers';
16
16
  export type { EventOptions } from './Dispatcher';
17
17
  export { default as Processor } from './Processor';
18
- export type { BackgroundAbortHandle, ProcessorOptions } from './Processor';
18
+ export type { BackgroundAbortHandle, ProcessorOptions, ScopeTransaction } from './Processor';
19
19
  export { default } from './Plugin';
20
20
  export * from './types';
@@ -63,9 +63,13 @@ class CreateInstruction extends import__.Instruction {
63
63
  async run(node, input, processor) {
64
64
  const { collection, params: { appends = [], ...params } = {} } = node.config;
65
65
  const [dataSourceName, collectionName] = (0, import_data_source_manager.parseCollectionName)(collection);
66
- const { repository, filterTargetKey } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName).collectionManager.getCollection(collectionName);
66
+ const dataSource = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName);
67
+ if (!dataSource) {
68
+ throw new Error(`Data source ${dataSourceName} not found`);
69
+ }
70
+ const { repository, filterTargetKey } = dataSource.collectionManager.getCollection(collectionName);
67
71
  const options = processor.getParsedValue(params, node.id);
68
- const transaction = this.workflow.useDataSourceTransaction(dataSourceName, processor.transaction);
72
+ const transaction = processor.getScopeTransaction(node, dataSourceName) ?? this.workflow.useDataSourceTransaction(dataSourceName, processor.transaction);
69
73
  const created = await repository.create({
70
74
  ...options,
71
75
  context: {
@@ -64,12 +64,13 @@ class DestroyInstruction extends import__.Instruction {
64
64
  const [dataSourceName, collectionName] = (0, import_data_source_manager.parseCollectionName)(collection);
65
65
  const { repository } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName).collectionManager.getCollection(collectionName);
66
66
  const options = processor.getParsedValue(params, node.id);
67
+ const transaction = processor.getScopeTransaction(node, dataSourceName) ?? this.workflow.useDataSourceTransaction(dataSourceName, processor.transaction);
67
68
  const result = await repository.destroy({
68
69
  ...options,
69
70
  context: {
70
71
  stack: Array.from(new Set((processor.execution.stack ?? []).concat(processor.execution.id)))
71
72
  },
72
- transaction: this.workflow.useDataSourceTransaction(dataSourceName, processor.transaction)
73
+ transaction
73
74
  });
74
75
  return {
75
76
  result,
@@ -78,6 +78,7 @@ class QueryInstruction extends import__.Instruction {
78
78
  return set;
79
79
  }, /* @__PURE__ */ new Set())
80
80
  ) : options.appends;
81
+ const transaction = processor.getScopeTransaction(node, dataSourceName) ?? this.workflow.useDataSourceTransaction(dataSourceName, processor.transaction);
81
82
  const result = await (multiple ? repository.find : repository.findOne).call(repository, {
82
83
  ...options,
83
84
  ...import_actions.utils.pageArgsToLimitArgs(page ?? import_actions.DEFAULT_PAGE, pageSize ?? import_actions.DEFAULT_PER_PAGE),
@@ -86,7 +87,7 @@ class QueryInstruction extends import__.Instruction {
86
87
  return `${((_a = item.direction) == null ? void 0 : _a.toLowerCase()) === "desc" ? "-" : ""}${item.field}`;
87
88
  }),
88
89
  appends,
89
- transaction: this.workflow.useDataSourceTransaction(dataSourceName, processor.transaction)
90
+ transaction
90
91
  });
91
92
  if (failOnEmpty && (multiple ? !result.length : !result)) {
92
93
  return {