@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.
- package/dist/client/618.c91be3f6e4d73501.js +10 -0
- package/dist/client/{67.452743ce8ec30617.js → 67.61e70b8b777f8638.js} +1 -1
- package/dist/client/964.5e11e1820fece37b.js +10 -0
- package/dist/client/Branch.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/nodes/output.d.ts +1 -1
- package/dist/client/nodes/query.d.ts +1 -1
- package/dist/client/style.d.ts +1 -0
- package/dist/client/triggers/collection.d.ts +29 -2
- package/dist/externalVersion.js +13 -12
- package/dist/locale/de-DE.json +1 -0
- package/dist/locale/en-US.json +5 -1
- package/dist/locale/es-ES.json +1 -0
- package/dist/locale/fr-FR.json +1 -0
- package/dist/locale/hu-HU.json +1 -0
- package/dist/locale/id-ID.json +1 -0
- package/dist/locale/it-IT.json +1 -0
- package/dist/locale/ja-JP.json +1 -0
- package/dist/locale/ko-KR.json +1 -0
- package/dist/locale/nl-NL.json +1 -0
- package/dist/locale/pt-BR.json +1 -0
- package/dist/locale/ru-RU.json +1 -0
- package/dist/locale/tr-TR.json +1 -0
- package/dist/locale/uk-UA.json +1 -0
- package/dist/locale/vi-VN.json +1 -0
- package/dist/locale/zh-CN.json +5 -1
- package/dist/locale/zh-TW.json +1 -0
- package/dist/node_modules/cron-parser/package.json +1 -1
- package/dist/node_modules/joi/package.json +1 -1
- package/dist/node_modules/lru-cache/package.json +1 -1
- package/dist/node_modules/nodejs-snowflake/package.json +1 -1
- package/dist/server/Dispatcher.d.ts +4 -2
- package/dist/server/Dispatcher.js +103 -67
- package/dist/server/ExecutionTimeoutManager.js +2 -2
- package/dist/server/Plugin.d.ts +1 -1
- package/dist/server/Processor.d.ts +16 -0
- package/dist/server/Processor.js +112 -0
- package/dist/server/actions/nodes.js +36 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/instructions/CreateInstruction.js +6 -2
- package/dist/server/instructions/DestroyInstruction.js +2 -1
- package/dist/server/instructions/QueryInstruction.js +2 -1
- package/dist/server/instructions/UpdateInstruction.js +2 -1
- package/dist/server/triggers/CollectionTrigger.d.ts +5 -0
- package/dist/server/triggers/CollectionTrigger.js +26 -1
- package/dist/server/triggers/ScheduleTrigger/StaticScheduleTrigger.d.ts +1 -1
- package/dist/server/utils.js +27 -27
- package/package.json +2 -2
- package/dist/client/618.19af7f84261c815d.js +0 -10
- 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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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 (
|
|
208
|
-
|
|
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
|
-
}
|
|
211
|
-
this.plugin.getLogger("dispatcher").
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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.
|
|
386
|
+
isolationLevel: this.plugin.db.options.dialect === "sqlite" ? void 0 : import_sequelize.Transaction.ISOLATION_LEVELS.READ_COMMITTED
|
|
350
387
|
});
|
|
351
388
|
try {
|
|
352
|
-
const
|
|
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
|
-
|
|
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
|
|
448
|
+
return null;
|
|
407
449
|
}
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
-
|
|
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 =
|
|
84
|
+
isExpired(execution, now = new Date(Date.now())) {
|
|
85
85
|
return !!execution.expiresAt && execution.expiresAt.getTime() <= now.getTime();
|
|
86
86
|
}
|
|
87
|
-
getRemainingMs(execution, now =
|
|
87
|
+
getRemainingMs(execution, now = new Date(Date.now())) {
|
|
88
88
|
if (!execution.expiresAt) {
|
|
89
89
|
return null;
|
|
90
90
|
}
|
package/dist/server/Plugin.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/server/Processor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
90
|
+
transaction
|
|
90
91
|
});
|
|
91
92
|
if (failOnEmpty && (multiple ? !result.length : !result)) {
|
|
92
93
|
return {
|