@nocobase/plugin-workflow-delay 0.17.0-alpha.4

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,109 @@
1
+ import WorkflowPlugin, {
2
+ Processor,
3
+ Instruction,
4
+ JOB_STATUS,
5
+ JobModel,
6
+ EXECUTION_STATUS,
7
+ } from '@nocobase/plugin-workflow';
8
+
9
+ type ValueOf<T> = T[keyof T];
10
+
11
+ interface DelayConfig {
12
+ endStatus: ValueOf<typeof JOB_STATUS>;
13
+ duration: number;
14
+ }
15
+
16
+ export default class extends Instruction {
17
+ timers: Map<number, NodeJS.Timeout> = new Map();
18
+
19
+ constructor(public plugin: WorkflowPlugin) {
20
+ super(plugin);
21
+
22
+ plugin.app.on('afterStart', this.load);
23
+ plugin.app.on('beforeStop', this.unload);
24
+ }
25
+
26
+ load = async () => {
27
+ const { model } = this.plugin.db.getCollection('jobs');
28
+ const jobs = (await model.findAll({
29
+ where: {
30
+ status: JOB_STATUS.PENDING,
31
+ },
32
+ include: [
33
+ {
34
+ association: 'execution',
35
+ attributes: [],
36
+ where: {
37
+ status: EXECUTION_STATUS.STARTED,
38
+ },
39
+ required: true,
40
+ },
41
+ {
42
+ association: 'node',
43
+ attributes: ['config'],
44
+ where: {
45
+ type: 'delay',
46
+ },
47
+ required: true,
48
+ },
49
+ ],
50
+ })) as JobModel[];
51
+
52
+ jobs.forEach((job) => {
53
+ this.schedule(job);
54
+ });
55
+ };
56
+
57
+ unload = () => {
58
+ for (const timer of this.timers.values()) {
59
+ clearTimeout(timer);
60
+ }
61
+
62
+ this.timers = new Map();
63
+ };
64
+
65
+ schedule(job) {
66
+ const now = new Date();
67
+ const createdAt = Date.parse(job.createdAt);
68
+ const delay = createdAt + job.node.config.duration - now.getTime();
69
+ if (delay > 0) {
70
+ const trigger = this.trigger.bind(this, job);
71
+ this.timers.set(job.id, setTimeout(trigger, delay));
72
+ } else {
73
+ this.trigger(job);
74
+ }
75
+ }
76
+
77
+ async trigger(job) {
78
+ if (!job.execution) {
79
+ job.execution = await job.getExecution();
80
+ }
81
+ if (job.execution.status === EXECUTION_STATUS.STARTED) {
82
+ this.plugin.resume(job);
83
+ }
84
+ if (this.timers.get(job.id)) {
85
+ this.timers.delete(job.id);
86
+ }
87
+ }
88
+
89
+ async run(node, prevJob, processor: Processor) {
90
+ const job = await processor.saveJob({
91
+ status: JOB_STATUS.PENDING,
92
+ result: null,
93
+ nodeId: node.id,
94
+ upstreamId: prevJob?.id ?? null,
95
+ });
96
+ job.node = node;
97
+
98
+ // add to schedule
99
+ this.schedule(job);
100
+
101
+ return processor.exit();
102
+ }
103
+
104
+ async resume(node, prevJob, processor: Processor) {
105
+ const { endStatus } = node.config as DelayConfig;
106
+ prevJob.set('status', endStatus);
107
+ return prevJob;
108
+ }
109
+ }
@@ -0,0 +1,14 @@
1
+ import { Plugin } from '@nocobase/server';
2
+ import WorkflowPlugin from '@nocobase/plugin-workflow';
3
+
4
+ import DelayInstruction from './DelayInstruction';
5
+
6
+ export default class extends Plugin {
7
+ workflow: WorkflowPlugin;
8
+
9
+ async load() {
10
+ const workflowPlugin = this.app.getPlugin('workflow') as WorkflowPlugin;
11
+ this.workflow = workflowPlugin;
12
+ workflowPlugin.instructions.register('delay', new DelayInstruction(workflowPlugin));
13
+ }
14
+ }
@@ -0,0 +1,190 @@
1
+ import path from 'path';
2
+
3
+ import Database from '@nocobase/database';
4
+ import { Application } from '@nocobase/server';
5
+ import { EXECUTION_STATUS, JOB_STATUS } from '@nocobase/plugin-workflow';
6
+ import { getApp, sleep } from '@nocobase/plugin-workflow-test';
7
+
8
+ import Plugin from '..';
9
+
10
+ describe('workflow > instructions > delay', () => {
11
+ let app: Application;
12
+ let db: Database;
13
+ let PostRepo;
14
+ let WorkflowModel;
15
+ let workflow;
16
+ let plugin;
17
+
18
+ beforeEach(async () => {
19
+ app = await getApp({
20
+ plugins: [Plugin],
21
+ collectionPath: path.join(__dirname, './collections'),
22
+ });
23
+ plugin = app.pm.get('workflow');
24
+
25
+ db = app.db;
26
+ WorkflowModel = db.getCollection('workflows').model;
27
+ PostRepo = db.getCollection('posts').repository;
28
+
29
+ workflow = await WorkflowModel.create({
30
+ enabled: true,
31
+ type: 'collection',
32
+ config: {
33
+ mode: 1,
34
+ collection: 'posts',
35
+ },
36
+ });
37
+ });
38
+
39
+ afterEach(() => app.destroy());
40
+
41
+ describe('runtime', () => {
42
+ it('delay to resolved', async () => {
43
+ const n1 = await workflow.createNode({
44
+ type: 'delay',
45
+ config: {
46
+ duration: 2000,
47
+ endStatus: JOB_STATUS.RESOLVED,
48
+ },
49
+ });
50
+
51
+ const post = await PostRepo.create({ values: { title: 't1' } });
52
+
53
+ await sleep(500);
54
+
55
+ const [e1] = await workflow.getExecutions();
56
+ expect(e1.status).toEqual(EXECUTION_STATUS.STARTED);
57
+ const [j1] = await e1.getJobs();
58
+ expect(j1.status).toBe(JOB_STATUS.PENDING);
59
+
60
+ await sleep(2000);
61
+
62
+ const [e2] = await workflow.getExecutions();
63
+ expect(e2.status).toEqual(EXECUTION_STATUS.RESOLVED);
64
+ const [j2] = await e2.getJobs();
65
+ expect(j2.status).toBe(JOB_STATUS.RESOLVED);
66
+ });
67
+
68
+ it('delay to reject', async () => {
69
+ const n1 = await workflow.createNode({
70
+ type: 'delay',
71
+ config: {
72
+ duration: 2000,
73
+ endStatus: JOB_STATUS.FAILED,
74
+ },
75
+ });
76
+
77
+ const post = await PostRepo.create({ values: { title: 't1' } });
78
+
79
+ await sleep(500);
80
+
81
+ const [e1] = await workflow.getExecutions();
82
+ expect(e1.status).toEqual(EXECUTION_STATUS.STARTED);
83
+ const [j1] = await e1.getJobs();
84
+ expect(j1.status).toBe(JOB_STATUS.PENDING);
85
+
86
+ await sleep(2000);
87
+
88
+ const [e2] = await workflow.getExecutions();
89
+ expect(e2.status).toEqual(EXECUTION_STATUS.FAILED);
90
+ const [j2] = await e2.getJobs();
91
+ expect(j2.status).toBe(JOB_STATUS.FAILED);
92
+ });
93
+
94
+ it('delay to resolve and rollback in downstream node', async () => {
95
+ const n1 = await workflow.createNode({
96
+ type: 'delay',
97
+ config: {
98
+ duration: 2000,
99
+ endStatus: JOB_STATUS.RESOLVED,
100
+ },
101
+ });
102
+ const n2 = await workflow.createNode({
103
+ type: 'create',
104
+ config: {
105
+ collection: 'comment',
106
+ params: {
107
+ values: {
108
+ status: 'should be number but use string to raise an error',
109
+ },
110
+ },
111
+ },
112
+ upstreamId: n1.id,
113
+ });
114
+ await n1.setDownstream(n2);
115
+
116
+ const post = await PostRepo.create({ values: { title: 't1' } });
117
+
118
+ await sleep(500);
119
+
120
+ const [e1] = await workflow.getExecutions();
121
+ expect(e1.status).toEqual(EXECUTION_STATUS.STARTED);
122
+ const [j1] = await e1.getJobs();
123
+ expect(j1.status).toBe(JOB_STATUS.PENDING);
124
+
125
+ await sleep(2000);
126
+
127
+ const [e2] = await workflow.getExecutions();
128
+ expect(e2.status).toEqual(EXECUTION_STATUS.ERROR);
129
+ const [j2, j3] = await e2.getJobs({ order: [['id', 'ASC']] });
130
+ expect(j2.status).toBe(JOB_STATUS.RESOLVED);
131
+ expect(j3.status).toBe(JOB_STATUS.ERROR);
132
+ });
133
+ });
134
+
135
+ describe('app lifecycle', () => {
136
+ beforeEach(async () => {
137
+ await workflow.createNode({
138
+ type: 'delay',
139
+ config: {
140
+ duration: 2000,
141
+ endStatus: JOB_STATUS.RESOLVED,
142
+ },
143
+ });
144
+ });
145
+
146
+ it('restart app should trigger delayed job', async () => {
147
+ const post = await PostRepo.create({ values: { title: 't1' } });
148
+
149
+ await sleep(500);
150
+
151
+ const [e1] = await workflow.getExecutions();
152
+ expect(e1.status).toEqual(EXECUTION_STATUS.STARTED);
153
+ const [j1] = await e1.getJobs();
154
+ expect(j1.status).toBe(JOB_STATUS.PENDING);
155
+
156
+ await app.stop();
157
+ await sleep(500);
158
+
159
+ await app.start();
160
+ await sleep(2000);
161
+
162
+ const [e2] = await workflow.getExecutions();
163
+ expect(e2.status).toEqual(EXECUTION_STATUS.RESOLVED);
164
+ const [j2] = await e2.getJobs();
165
+ expect(j2.status).toBe(JOB_STATUS.RESOLVED);
166
+ });
167
+
168
+ it('restart app should trigger missed delayed job', async () => {
169
+ const post = await PostRepo.create({ values: { title: 't1' } });
170
+
171
+ await sleep(500);
172
+
173
+ const [e1] = await workflow.getExecutions();
174
+ expect(e1.status).toEqual(EXECUTION_STATUS.STARTED);
175
+ const [j1] = await e1.getJobs();
176
+ expect(j1.status).toBe(JOB_STATUS.PENDING);
177
+
178
+ await app.stop();
179
+ await sleep(2000);
180
+
181
+ await app.start();
182
+ await sleep(1000);
183
+
184
+ const [e2] = await workflow.getExecutions();
185
+ expect(e2.status).toEqual(EXECUTION_STATUS.RESOLVED);
186
+ const [j2] = await e2.getJobs();
187
+ expect(j2.status).toBe(JOB_STATUS.RESOLVED);
188
+ });
189
+ });
190
+ });
@@ -0,0 +1 @@
1
+ export { default } from './Plugin';