@nocobase/plugin-workflow 1.9.0-beta.8 → 2.0.0-alpha.10

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 (42) hide show
  1. package/.env.example +6 -0
  2. package/dist/client/248e211bb2d99aee.js +10 -0
  3. package/dist/client/{9e124936e3877c66.js → 27bd65abee87cafa.js} +1 -1
  4. package/dist/client/RemoveNodeContext.d.ts +11 -0
  5. package/dist/client/components/TriggerWorkflowSelect.d.ts +10 -0
  6. package/dist/client/components/index.d.ts +1 -0
  7. package/dist/client/e7c028a099537ab1.js +10 -0
  8. package/dist/client/{2a8332e23037d42f.js → f68fbc145c3ddec3.js} +1 -1
  9. package/dist/client/flows/triggerWorkflows.d.ts +120 -0
  10. package/dist/client/index.d.ts +1 -0
  11. package/dist/client/index.js +1 -1
  12. package/dist/client/nodes/condition.d.ts +0 -3
  13. package/dist/client/schemas/executions.d.ts +8 -8
  14. package/dist/client/triggers/index.d.ts +1 -1
  15. package/dist/common/collections/executions.d.ts +8 -8
  16. package/dist/common/collections/executions.js +2 -2
  17. package/dist/common/collections/flow_nodes.d.ts +21 -0
  18. package/dist/common/collections/flow_nodes.js +6 -0
  19. package/dist/common/collections/userWorkflowTasks.d.ts +13 -0
  20. package/dist/common/collections/userWorkflowTasks.js +6 -0
  21. package/dist/common/collections/workflowCategories.d.ts +21 -0
  22. package/dist/common/collections/workflowCategories.js +6 -0
  23. package/dist/common/collections/workflows.d.ts +42 -0
  24. package/dist/common/collections/workflows.js +6 -0
  25. package/dist/externalVersion.js +16 -16
  26. package/dist/locale/zh-CN.json +14 -3
  27. package/dist/node_modules/cron-parser/package.json +1 -1
  28. package/dist/node_modules/lru-cache/package.json +1 -1
  29. package/dist/node_modules/nodejs-snowflake/package.json +1 -1
  30. package/dist/server/Dispatcher.d.ts +47 -1
  31. package/dist/server/Dispatcher.js +368 -1
  32. package/dist/server/Plugin.d.ts +4 -24
  33. package/dist/server/Plugin.js +16 -316
  34. package/dist/server/actions/nodes.js +86 -22
  35. package/dist/server/index.d.ts +2 -1
  36. package/dist/server/index.js +0 -2
  37. package/dist/server/triggers/CollectionTrigger.d.ts +1 -1
  38. package/dist/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.d.ts +5 -5
  39. package/dist/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.js +33 -13
  40. package/package.json +16 -16
  41. package/dist/client/3b0762a72796b5f8.js +0 -10
  42. package/dist/client/48fc0fadf459229d.js +0 -10
@@ -6,6 +6,52 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
+ import { Transactionable } from 'sequelize';
10
+ import type { QueueEventOptions } from '@nocobase/server';
11
+ import Processor from './Processor';
12
+ import type { ExecutionModel, JobModel, WorkflowModel } from './types';
13
+ import type PluginWorkflowServer from './Plugin';
14
+ type Pending = {
15
+ execution: ExecutionModel;
16
+ job?: JobModel;
17
+ force?: boolean;
18
+ };
19
+ export type EventOptions = {
20
+ eventKey?: string;
21
+ context?: any;
22
+ deferred?: boolean;
23
+ manually?: boolean;
24
+ force?: boolean;
25
+ stack?: Array<number | string>;
26
+ onTriggerFail?: Function;
27
+ [key: string]: any;
28
+ } & Transactionable;
29
+ export declare const WORKER_JOB_WORKFLOW_PROCESS = "workflow:process";
9
30
  export default class Dispatcher {
10
- constructor();
31
+ private readonly plugin;
32
+ private ready;
33
+ private executing;
34
+ private pending;
35
+ private events;
36
+ private eventsCount;
37
+ get idle(): boolean;
38
+ constructor(plugin: PluginWorkflowServer);
39
+ readonly onQueueExecution: QueueEventOptions['process'];
40
+ setReady(ready: boolean): void;
41
+ isReady(): boolean;
42
+ getEventsCount(): number;
43
+ trigger(workflow: WorkflowModel, context: object, options?: EventOptions): void | Promise<Processor | null>;
44
+ resume(job: any): Promise<void>;
45
+ start(execution: ExecutionModel): Promise<void>;
46
+ beforeStop(): Promise<void>;
47
+ dispatch(): Promise<void>;
48
+ run(pending: Pending): Promise<void>;
49
+ private triggerSync;
50
+ private validateEvent;
51
+ private createExecution;
52
+ private prepare;
53
+ private acquirePendingExecution;
54
+ private acquireQueueingExecution;
55
+ private process;
11
56
  }
57
+ export {};
@@ -26,10 +26,377 @@ var __copyProps = (to, from, except, desc) => {
26
26
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
27
  var Dispatcher_exports = {};
28
28
  __export(Dispatcher_exports, {
29
+ WORKER_JOB_WORKFLOW_PROCESS: () => WORKER_JOB_WORKFLOW_PROCESS,
29
30
  default: () => Dispatcher
30
31
  });
31
32
  module.exports = __toCommonJS(Dispatcher_exports);
33
+ var import_crypto = require("crypto");
34
+ var import_sequelize = require("sequelize");
35
+ var import_constants = require("./constants");
36
+ const WORKER_JOB_WORKFLOW_PROCESS = "workflow:process";
32
37
  class Dispatcher {
33
- constructor() {
38
+ constructor(plugin) {
39
+ this.plugin = plugin;
40
+ this.prepare = this.prepare.bind(this);
41
+ }
42
+ ready = false;
43
+ executing = null;
44
+ pending = [];
45
+ events = [];
46
+ eventsCount = 0;
47
+ get idle() {
48
+ return !this.executing && !this.pending.length && !this.events.length;
49
+ }
50
+ onQueueExecution = async (event) => {
51
+ const ExecutionRepo = this.plugin.db.getRepository("executions");
52
+ const execution = await ExecutionRepo.findOne({
53
+ filterByTk: event.executionId
54
+ });
55
+ if (!execution || execution.dispatched) {
56
+ this.plugin.getLogger("dispatcher").info(`execution (${event.executionId}) from queue not found or not in queueing status, skip`);
57
+ return;
58
+ }
59
+ this.plugin.getLogger(execution.workflowId).info(`execution (${execution.id}) received from queue, adding to pending list`);
60
+ this.run({ execution });
61
+ };
62
+ setReady(ready) {
63
+ this.ready = ready;
64
+ }
65
+ isReady() {
66
+ return this.ready;
67
+ }
68
+ getEventsCount() {
69
+ return this.eventsCount;
70
+ }
71
+ trigger(workflow, context, options = {}) {
72
+ const logger = this.plugin.getLogger(workflow.id);
73
+ if (!this.ready) {
74
+ logger.warn(`app is not ready, event of workflow ${workflow.id} will be ignored`);
75
+ logger.debug(`ignored event data:`, context);
76
+ return;
77
+ }
78
+ if (!options.force && !options.manually && !workflow.enabled) {
79
+ logger.warn(`workflow ${workflow.id} is not enabled, event will be ignored`);
80
+ return;
81
+ }
82
+ const duplicated = this.events.find(([w, c, { eventKey }]) => {
83
+ if (eventKey && options.eventKey) {
84
+ return eventKey === options.eventKey;
85
+ }
86
+ });
87
+ if (duplicated) {
88
+ logger.warn(`event of workflow ${workflow.id} is duplicated (${options.eventKey}), event will be ignored`);
89
+ return;
90
+ }
91
+ if (context == null) {
92
+ logger.warn(`workflow ${workflow.id} event data context is null, event will be ignored`);
93
+ return;
94
+ }
95
+ if (options.manually || this.plugin.isWorkflowSync(workflow)) {
96
+ return this.triggerSync(workflow, context, options);
97
+ }
98
+ const { transaction, ...rest } = options;
99
+ this.events.push([workflow, context, rest]);
100
+ this.eventsCount = this.events.length;
101
+ logger.info(`new event triggered, now events: ${this.events.length}`);
102
+ logger.debug(`event data:`, { context });
103
+ if (this.events.length > 1) {
104
+ logger.info(`new event is pending to be prepared after previous preparation is finished`);
105
+ return;
106
+ }
107
+ setImmediate(this.prepare);
108
+ }
109
+ async resume(job) {
110
+ let { execution } = job;
111
+ if (!execution) {
112
+ execution = await job.getExecution();
113
+ }
114
+ this.plugin.getLogger(execution.workflowId).info(`execution (${execution.id}) resuming from job (${job.id}) added to pending list`);
115
+ this.run({ execution, job, force: true });
116
+ }
117
+ async start(execution) {
118
+ if (execution.status) {
119
+ return;
120
+ }
121
+ this.plugin.getLogger(execution.workflowId).info(`starting deferred execution (${execution.id})`);
122
+ this.run({ execution, force: true });
123
+ }
124
+ async beforeStop() {
125
+ this.ready = false;
126
+ if (this.events.length) {
127
+ await this.prepare();
128
+ }
129
+ if (this.executing) {
130
+ await this.executing;
131
+ }
132
+ }
133
+ dispatch() {
134
+ if (!this.ready) {
135
+ this.plugin.getLogger("dispatcher").warn(`app is not ready, new dispatching will be ignored`);
136
+ return;
137
+ }
138
+ if (!this.plugin.app.serving(WORKER_JOB_WORKFLOW_PROCESS)) {
139
+ this.plugin.getLogger("dispatcher").warn(`${WORKER_JOB_WORKFLOW_PROCESS} is not serving, new dispatching will be ignored`);
140
+ return;
141
+ }
142
+ if (this.executing) {
143
+ this.plugin.getLogger("dispatcher").warn(`workflow executing is not finished, new dispatching will be ignored`);
144
+ return;
145
+ }
146
+ if (this.events.length) {
147
+ return this.prepare();
148
+ }
149
+ this.executing = (async () => {
150
+ let next = null;
151
+ let execution = null;
152
+ if (this.pending.length) {
153
+ const pending = this.pending.shift();
154
+ execution = pending.force ? pending.execution : await this.acquirePendingExecution(pending.execution);
155
+ if (execution) {
156
+ next = [execution, pending.job];
157
+ this.plugin.getLogger(next[0].workflowId).info(`pending execution (${next[0].id}) ready to process`);
158
+ }
159
+ } else {
160
+ execution = await this.acquireQueueingExecution();
161
+ if (execution) {
162
+ next = [execution];
163
+ }
164
+ }
165
+ if (next) {
166
+ await this.process(...next);
167
+ }
168
+ this.executing = null;
169
+ if (next || this.pending.length) {
170
+ this.plugin.getLogger("dispatcher").debug(`last process finished, will do another dispatch`);
171
+ this.dispatch();
172
+ }
173
+ })();
174
+ }
175
+ async run(pending) {
176
+ this.pending.push(pending);
177
+ this.dispatch();
178
+ }
179
+ async triggerSync(workflow, context, { deferred, ...options } = {}) {
180
+ let execution;
181
+ try {
182
+ execution = await this.createExecution(workflow, context, options);
183
+ } catch (err) {
184
+ this.plugin.getLogger(workflow.id).error(`creating execution failed: ${err.message}`, err);
185
+ return null;
186
+ }
187
+ try {
188
+ return this.process(execution, null, options);
189
+ } catch (err) {
190
+ this.plugin.getLogger(execution.workflowId).error(`execution (${execution.id}) error: ${err.message}`, err);
191
+ }
192
+ return null;
193
+ }
194
+ async validateEvent(workflow, context, options) {
195
+ const trigger = this.plugin.triggers.get(workflow.type);
196
+ const triggerValid = await trigger.validateEvent(workflow, context, options);
197
+ if (!triggerValid) {
198
+ return false;
199
+ }
200
+ const { stack } = options;
201
+ let valid = true;
202
+ if ((stack == null ? void 0 : stack.length) > 0) {
203
+ const existed = await workflow.countExecutions({
204
+ where: {
205
+ id: stack
206
+ },
207
+ transaction: options.transaction
208
+ });
209
+ const limitCount = workflow.options.stackLimit || 1;
210
+ if (existed >= limitCount) {
211
+ this.plugin.getLogger(workflow.id).warn(
212
+ `workflow ${workflow.id} has already been triggered in stacks executions (${stack}), and max call coont is ${limitCount}, newly triggering will be skipped.`
213
+ );
214
+ valid = false;
215
+ }
216
+ }
217
+ return valid;
218
+ }
219
+ async createExecution(workflow, context, options) {
220
+ var _a;
221
+ const { deferred } = options;
222
+ const transaction = await this.plugin.useDataSourceTransaction("main", options.transaction, true);
223
+ const sameTransaction = options.transaction === transaction;
224
+ const valid = await this.validateEvent(workflow, context, { ...options, transaction });
225
+ if (!valid) {
226
+ if (!sameTransaction) {
227
+ await transaction.commit();
228
+ }
229
+ (_a = options.onTriggerFail) == null ? void 0 : _a.call(options, workflow, context, options);
230
+ return Promise.reject(new Error("event is not valid"));
231
+ }
232
+ let execution;
233
+ try {
234
+ execution = await workflow.createExecution(
235
+ {
236
+ context,
237
+ key: workflow.key,
238
+ eventKey: options.eventKey ?? (0, import_crypto.randomUUID)(),
239
+ stack: options.stack,
240
+ dispatched: deferred ?? false
241
+ },
242
+ { transaction }
243
+ );
244
+ } catch (err) {
245
+ if (!sameTransaction) {
246
+ await transaction.rollback();
247
+ }
248
+ throw err;
249
+ }
250
+ this.plugin.getLogger(workflow.id).info(`execution of workflow ${workflow.id} created as ${execution.id}`);
251
+ if (!workflow.stats) {
252
+ workflow.stats = await workflow.getStats({ transaction });
253
+ }
254
+ await workflow.stats.increment("executed", { transaction });
255
+ if (this.plugin.db.options.dialect !== "postgres") {
256
+ await workflow.stats.reload({ transaction });
257
+ }
258
+ if (!workflow.versionStats) {
259
+ workflow.versionStats = await workflow.getVersionStats({ transaction });
260
+ }
261
+ await workflow.versionStats.increment("executed", { transaction });
262
+ if (this.plugin.db.options.dialect !== "postgres") {
263
+ await workflow.versionStats.reload({ transaction });
264
+ }
265
+ if (!sameTransaction) {
266
+ await transaction.commit();
267
+ }
268
+ execution.workflow = workflow;
269
+ return execution;
270
+ }
271
+ prepare = async () => {
272
+ if (this.executing && this.plugin.db.options.dialect === "sqlite") {
273
+ await this.executing;
274
+ }
275
+ const event = this.events.shift();
276
+ this.eventsCount = this.events.length;
277
+ if (!event) {
278
+ this.plugin.getLogger("dispatcher").info(`events queue is empty, no need to prepare`);
279
+ return;
280
+ }
281
+ const logger = this.plugin.getLogger(event[0].id);
282
+ logger.info(`preparing execution for event`);
283
+ try {
284
+ const execution = await this.createExecution(...event);
285
+ if (!(execution == null ? void 0 : execution.dispatched)) {
286
+ if (!this.executing && !this.pending.length) {
287
+ logger.info(`local pending list is empty, adding execution (${execution.id}) to pending list`);
288
+ this.pending.push({ execution });
289
+ } else {
290
+ logger.info(`local pending list is not empty, sending execution (${execution.id}) to queue`);
291
+ if (this.ready) {
292
+ this.plugin.app.backgroundJobManager.publish(`${this.plugin.name}.pendingExecution`, {
293
+ executionId: execution.id
294
+ });
295
+ }
296
+ }
297
+ }
298
+ } catch (error) {
299
+ logger.error(`failed to create execution:`, { error });
300
+ }
301
+ if (this.events.length) {
302
+ await this.prepare();
303
+ } else {
304
+ this.plugin.getLogger("dispatcher").info("no more events need to be prepared, dispatching...");
305
+ if (this.executing) {
306
+ await this.executing;
307
+ }
308
+ this.dispatch();
309
+ }
310
+ };
311
+ async acquirePendingExecution(execution) {
312
+ const logger = this.plugin.getLogger(execution.workflowId);
313
+ const isolationLevel = this.plugin.db.options.dialect === "sqlite" ? [][0] : import_sequelize.Transaction.ISOLATION_LEVELS.REPEATABLE_READ;
314
+ let fetched = execution;
315
+ try {
316
+ await this.plugin.db.sequelize.transaction({ isolationLevel }, async (transaction) => {
317
+ const ExecutionModelClass = this.plugin.db.getModel("executions");
318
+ const [affected] = await ExecutionModelClass.update(
319
+ { dispatched: true, status: import_constants.EXECUTION_STATUS.STARTED },
320
+ {
321
+ where: {
322
+ id: execution.id,
323
+ dispatched: false
324
+ },
325
+ transaction
326
+ }
327
+ );
328
+ if (!affected) {
329
+ fetched = null;
330
+ return;
331
+ }
332
+ await execution.reload({ transaction });
333
+ });
334
+ } catch (error) {
335
+ logger.error(`acquiring pending execution failed: ${error.message}`, { error });
336
+ }
337
+ return fetched;
338
+ }
339
+ async acquireQueueingExecution() {
340
+ const isolationLevel = this.plugin.db.options.dialect === "sqlite" ? [][0] : import_sequelize.Transaction.ISOLATION_LEVELS.REPEATABLE_READ;
341
+ let fetched = null;
342
+ try {
343
+ await this.plugin.db.sequelize.transaction(
344
+ {
345
+ isolationLevel
346
+ },
347
+ async (transaction) => {
348
+ const execution = await this.plugin.db.getRepository("executions").findOne({
349
+ filter: {
350
+ dispatched: false,
351
+ "workflow.enabled": true
352
+ },
353
+ sort: "id",
354
+ transaction
355
+ });
356
+ if (execution) {
357
+ this.plugin.getLogger(execution.workflowId).info(`execution (${execution.id}) fetched from db`);
358
+ await execution.update(
359
+ {
360
+ dispatched: true,
361
+ status: import_constants.EXECUTION_STATUS.STARTED
362
+ },
363
+ { transaction }
364
+ );
365
+ execution.workflow = this.plugin.enabledCache.get(execution.workflowId);
366
+ fetched = execution;
367
+ } else {
368
+ this.plugin.getLogger("dispatcher").debug(`no execution in db queued to process`);
369
+ }
370
+ }
371
+ );
372
+ } catch (error) {
373
+ this.plugin.getLogger("dispatcher").error(`fetching execution from db failed: ${error.message}`, { error });
374
+ }
375
+ return fetched;
376
+ }
377
+ async process(execution, job, options = {}) {
378
+ var _a, _b;
379
+ const logger = this.plugin.getLogger(execution.workflowId);
380
+ if (!execution.dispatched) {
381
+ const transaction = await this.plugin.useDataSourceTransaction("main", options.transaction);
382
+ await execution.update({ dispatched: true, status: import_constants.EXECUTION_STATUS.STARTED }, { transaction });
383
+ logger.info(`execution (${execution.id}) from pending list updated to started`);
384
+ }
385
+ const processor = this.plugin.createProcessor(execution, options);
386
+ logger.info(`execution (${execution.id}) ${job ? "resuming" : "starting"}...`);
387
+ try {
388
+ await (job ? processor.resume(job) : processor.start());
389
+ logger.info(`execution (${execution.id}) finished with status: ${execution.status}`, { execution });
390
+ if (execution.status && ((_b = (_a = execution.workflow.options) == null ? void 0 : _a.deleteExecutionOnStatus) == null ? void 0 : _b.includes(execution.status))) {
391
+ await execution.destroy({ transaction: processor.mainTransaction });
392
+ }
393
+ } catch (err) {
394
+ logger.error(`execution (${execution.id}) error: ${err.message}`, err);
395
+ }
396
+ return processor;
34
397
  }
35
398
  }
399
+ // Annotate the CommonJS export names for ESM import in node:
400
+ 0 && (module.exports = {
401
+ WORKER_JOB_WORKFLOW_PROCESS
402
+ });
@@ -11,37 +11,23 @@ import { Transactionable } from 'sequelize';
11
11
  import { Plugin } from '@nocobase/server';
12
12
  import { Registry } from '@nocobase/utils';
13
13
  import { Logger } from '@nocobase/logger';
14
+ import Dispatcher, { EventOptions } from './Dispatcher';
14
15
  import Processor from './Processor';
15
16
  import { CustomFunction } from './functions';
16
17
  import Trigger from './triggers';
17
18
  import { InstructionInterface } from './instructions';
18
- import type { ExecutionModel, JobModel, WorkflowModel } from './types';
19
+ import type { ExecutionModel, WorkflowModel } from './types';
19
20
  type ID = number | string;
20
- export type EventOptions = {
21
- eventKey?: string;
22
- context?: any;
23
- deferred?: boolean;
24
- manually?: boolean;
25
- force?: boolean;
26
- stack?: Array<ID>;
27
- onTriggerFail?: Function;
28
- [key: string]: any;
29
- } & Transactionable;
30
21
  export default class PluginWorkflowServer extends Plugin {
31
22
  instructions: Registry<InstructionInterface>;
32
23
  triggers: Registry<Trigger>;
33
24
  functions: Registry<CustomFunction>;
34
25
  enabledCache: Map<number, WorkflowModel>;
35
26
  snowflake: Snowflake;
36
- private ready;
37
- private executing;
38
- private pending;
39
- private events;
40
- private eventsCount;
27
+ private dispatcher;
41
28
  private loggerCache;
42
29
  private meter;
43
30
  private checker;
44
- private onQueueExecution;
45
31
  private onBeforeSave;
46
32
  private onAfterCreate;
47
33
  private onAfterUpdate;
@@ -74,20 +60,14 @@ export default class PluginWorkflowServer extends Plugin {
74
60
  load(): Promise<void>;
75
61
  private toggle;
76
62
  trigger(workflow: WorkflowModel, context: object, options?: EventOptions): void | Promise<Processor | null>;
77
- private triggerSync;
78
- run(execution: ExecutionModel, job?: JobModel): Promise<void>;
63
+ run(pending: Parameters<Dispatcher['run']>[0]): Promise<void>;
79
64
  resume(job: any): Promise<void>;
80
65
  /**
81
66
  * Start a deferred execution
82
67
  * @experimental
83
68
  */
84
69
  start(execution: ExecutionModel): Promise<void>;
85
- private validateEvent;
86
- private createExecution;
87
- private prepare;
88
- private dispatch;
89
70
  createProcessor(execution: ExecutionModel, options?: {}): Processor;
90
- private process;
91
71
  execute(workflow: WorkflowModel, values: any, options?: EventOptions): Promise<void | Processor>;
92
72
  /**
93
73
  * @experimental