@hahnpro/flow-sdk 9.6.4 → 2025.2.0-beta.1
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/CHANGELOG.md +904 -0
- package/jest.config.ts +10 -0
- package/package.json +7 -20
- package/project.json +41 -0
- package/src/index.ts +15 -0
- package/src/lib/ContextManager.ts +111 -0
- package/src/lib/FlowApplication.ts +659 -0
- package/src/lib/FlowElement.ts +220 -0
- package/src/lib/FlowEvent.ts +73 -0
- package/src/lib/FlowLogger.ts +131 -0
- package/src/lib/FlowModule.ts +18 -0
- package/src/lib/RpcClient.ts +99 -0
- package/src/lib/TestModule.ts +14 -0
- package/src/lib/__pycache__/rpc_server.cpython-310.pyc +0 -0
- package/src/lib/amqp.ts +32 -0
- package/src/lib/extra-validators.ts +62 -0
- package/src/lib/flow.interface.ts +56 -0
- package/{dist/index.d.ts → src/lib/index.ts} +3 -0
- package/src/lib/nats.ts +140 -0
- package/src/lib/unit-decorators.ts +156 -0
- package/src/lib/unit-utils.ts +163 -0
- package/src/lib/units.ts +587 -0
- package/src/lib/utils.ts +176 -0
- package/test/context-manager-purpose.spec.ts +248 -0
- package/test/context-manager.spec.ts +55 -0
- package/test/context.spec.ts +180 -0
- package/test/event.spec.ts +155 -0
- package/test/extra-validators.spec.ts +84 -0
- package/test/flow-logger.spec.ts +104 -0
- package/test/flow.spec.ts +508 -0
- package/test/input-stream.decorator.spec.ts +379 -0
- package/test/long-rpc.test.py +14 -0
- package/test/long-running-rpc.spec.ts +60 -0
- package/test/message.spec.ts +57 -0
- package/test/mocks/logger.mock.ts +7 -0
- package/test/mocks/nats-connection.mock.ts +135 -0
- package/test/mocks/nats-prepare.reals-nats.ts +15 -0
- package/test/rpc.spec.ts +198 -0
- package/test/rpc.test.py +45 -0
- package/test/rx.spec.ts +92 -0
- package/test/unit-decorator.spec.ts +57 -0
- package/test/utils.spec.ts +210 -0
- package/test/validation.spec.ts +174 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +22 -0
- package/tsconfig.spec.json +8 -0
- package/LICENSE +0 -21
- package/dist/ContextManager.d.ts +0 -40
- package/dist/ContextManager.js +0 -77
- package/dist/FlowApplication.d.ts +0 -85
- package/dist/FlowApplication.js +0 -500
- package/dist/FlowElement.d.ts +0 -67
- package/dist/FlowElement.js +0 -163
- package/dist/FlowEvent.d.ts +0 -25
- package/dist/FlowEvent.js +0 -71
- package/dist/FlowLogger.d.ts +0 -44
- package/dist/FlowLogger.js +0 -94
- package/dist/FlowModule.d.ts +0 -7
- package/dist/FlowModule.js +0 -13
- package/dist/RpcClient.d.ts +0 -13
- package/dist/RpcClient.js +0 -84
- package/dist/TestModule.d.ts +0 -2
- package/dist/TestModule.js +0 -27
- package/dist/amqp.d.ts +0 -14
- package/dist/amqp.js +0 -12
- package/dist/extra-validators.d.ts +0 -1
- package/dist/extra-validators.js +0 -51
- package/dist/flow.interface.d.ts +0 -48
- package/dist/flow.interface.js +0 -9
- package/dist/index.js +0 -18
- package/dist/nats.d.ts +0 -12
- package/dist/nats.js +0 -109
- package/dist/unit-decorators.d.ts +0 -39
- package/dist/unit-decorators.js +0 -156
- package/dist/unit-utils.d.ts +0 -8
- package/dist/unit-utils.js +0 -143
- package/dist/units.d.ts +0 -31
- package/dist/units.js +0 -570
- package/dist/utils.d.ts +0 -51
- package/dist/utils.js +0 -137
- /package/{dist → src/lib}/rpc_server.py +0 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { setTimeout } from 'timers/promises';
|
|
2
|
+
|
|
3
|
+
import { MockAPI } from '@hahnpro/hpc-api';
|
|
4
|
+
import { Type } from 'class-transformer';
|
|
5
|
+
import { IsArray, IsNumber, IsString, ValidateNested } from 'class-validator';
|
|
6
|
+
|
|
7
|
+
import { delayWithAbort, FlowApplication, FlowEvent, FlowFunction, FlowModule, FlowResource, FlowTask, InputStream } from '../src';
|
|
8
|
+
import { loggerMock } from './mocks/logger.mock';
|
|
9
|
+
import { NatsConnectionMock } from './mocks/nats-connection.mock';
|
|
10
|
+
|
|
11
|
+
describe('Flow Application', () => {
|
|
12
|
+
let nc: NatsConnectionMock;
|
|
13
|
+
let flowApplication: FlowApplication;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
nc = new NatsConnectionMock();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await flowApplication?.destroy();
|
|
21
|
+
await nc?.close();
|
|
22
|
+
|
|
23
|
+
loggerMock.log.mockReset();
|
|
24
|
+
loggerMock.warn.mockReset();
|
|
25
|
+
loggerMock.error.mockReset();
|
|
26
|
+
jest.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('FLOW.FA.1 should run simple flow app with a long running task', async () => {
|
|
30
|
+
const size = 8;
|
|
31
|
+
const flow = {
|
|
32
|
+
elements: [
|
|
33
|
+
{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.TestResource' },
|
|
34
|
+
{ id: 'testResource', module: 'test-module', functionFqn: 'test.resource.TestResource' },
|
|
35
|
+
{
|
|
36
|
+
id: 'longRunningTask',
|
|
37
|
+
module: 'test-module',
|
|
38
|
+
functionFqn: 'test.task.LongRunningTask',
|
|
39
|
+
properties: { delay: 500 },
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
connections: [
|
|
43
|
+
{ id: 'testConnection1', source: 'testTrigger', target: 'testResource' },
|
|
44
|
+
{ id: 'testConnection2', source: 'testResource', target: 'longRunningTask' },
|
|
45
|
+
],
|
|
46
|
+
context: {
|
|
47
|
+
flowId: 'testFlow',
|
|
48
|
+
deploymentId: 'testDeployment',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
flowApplication = new FlowApplication([TestModule], flow, {
|
|
52
|
+
natsConnection: nc as any,
|
|
53
|
+
logger: loggerMock,
|
|
54
|
+
skipApi: true,
|
|
55
|
+
explicitInit: true,
|
|
56
|
+
});
|
|
57
|
+
await flowApplication.init();
|
|
58
|
+
|
|
59
|
+
flowApplication.subscribe('testResource.default', {
|
|
60
|
+
next: (event: FlowEvent) => {
|
|
61
|
+
expect(event.getData()).toEqual({ hello: 'world' });
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const done = new Promise<void>((resolve, reject) => {
|
|
66
|
+
let count = 0;
|
|
67
|
+
flowApplication.subscribe('longRunningTask.default', {
|
|
68
|
+
next: (event: FlowEvent) => {
|
|
69
|
+
try {
|
|
70
|
+
expect(event.getData()).toEqual({ foo: 'bar' });
|
|
71
|
+
expect(event.getDataContentType()).toBe('application/json');
|
|
72
|
+
expect(event.getSource()).toBe('flows/testFlow/deployments/testDeployment/elements/longRunningTask');
|
|
73
|
+
expect(event.getSubject()).toBe('test.task.LongRunningTask');
|
|
74
|
+
expect(event.getType()).toBe('default');
|
|
75
|
+
expect(event.getTime()).toBeDefined();
|
|
76
|
+
|
|
77
|
+
if (++count === size) {
|
|
78
|
+
resolve();
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
reject(e);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < size; i++) {
|
|
88
|
+
flowApplication.emit(new FlowEvent({ id: 'testTrigger' }, {}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
expect(loggerMock.log).toHaveBeenCalledWith('Flow Deployment is running', expect.objectContaining(flow.context));
|
|
92
|
+
return done;
|
|
93
|
+
}, 10000);
|
|
94
|
+
|
|
95
|
+
it('FLOW.FA.2 should handle invalid stream handlers', async () => {
|
|
96
|
+
const flow = {
|
|
97
|
+
elements: [
|
|
98
|
+
{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.TestResource' },
|
|
99
|
+
{ id: 'testResource', module: 'test-module', functionFqn: 'test.resource.TestResource' },
|
|
100
|
+
],
|
|
101
|
+
connections: [{ id: 'testConnection1', source: 'testTrigger', target: 'testResource', targetStream: 'does-not-exist' }],
|
|
102
|
+
context: {
|
|
103
|
+
flowId: 'testFlow',
|
|
104
|
+
deploymentId: 'testDeployment',
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
flowApplication = new FlowApplication([TestModule], flow, { logger: loggerMock, skipApi: true, explicitInit: true });
|
|
108
|
+
await flowApplication.init();
|
|
109
|
+
|
|
110
|
+
expect(loggerMock.error).toHaveBeenNthCalledWith(
|
|
111
|
+
1,
|
|
112
|
+
expect.stringContaining('testResource does not implement a handler for does-not-exist\nat logErrorAndExit'),
|
|
113
|
+
{
|
|
114
|
+
...flow.context,
|
|
115
|
+
functionFqn: 'FlowApplication',
|
|
116
|
+
id: 'none',
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('FLOW.FA.3 should handle invalid function FQNs', async () => {
|
|
122
|
+
const flow = {
|
|
123
|
+
elements: [{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.Test123' }],
|
|
124
|
+
connections: [],
|
|
125
|
+
context: {
|
|
126
|
+
flowId: 'testFlow',
|
|
127
|
+
deploymentId: 'testDeployment',
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
flowApplication = new FlowApplication([TestModule], flow, { logger: loggerMock, skipApi: true, explicitInit: true });
|
|
131
|
+
await flowApplication.init();
|
|
132
|
+
|
|
133
|
+
expect(loggerMock.warn).toHaveBeenCalledTimes(0);
|
|
134
|
+
expect(loggerMock.error).toHaveBeenCalledTimes(1);
|
|
135
|
+
expect(loggerMock.error).toHaveBeenLastCalledWith(
|
|
136
|
+
expect.stringContaining('Could not create FlowElement for test-module.test.resource.Test123\nat logErrorAndExit'),
|
|
137
|
+
expect.objectContaining(flow.context),
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('FLOW.FA.4 should handle invalid connection targets', async () => {
|
|
142
|
+
const flow = {
|
|
143
|
+
elements: [{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.TestResource' }],
|
|
144
|
+
connections: [{ id: 'testConnection1', source: 'testTrigger', target: 'testResource' }],
|
|
145
|
+
context: {
|
|
146
|
+
flowId: 'testFlow',
|
|
147
|
+
deploymentId: 'testDeployment',
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
flowApplication = new FlowApplication([TestModule], flow, { logger: loggerMock, skipApi: true, explicitInit: true });
|
|
151
|
+
await flowApplication.init();
|
|
152
|
+
|
|
153
|
+
expect(loggerMock.warn).toHaveBeenCalledTimes(0);
|
|
154
|
+
expect(loggerMock.error).toHaveBeenCalledTimes(1);
|
|
155
|
+
expect(loggerMock.error).toHaveBeenLastCalledWith(
|
|
156
|
+
expect.stringContaining('testResource has not been initialized\nat logErrorAndExit'),
|
|
157
|
+
{
|
|
158
|
+
...flow.context,
|
|
159
|
+
functionFqn: 'FlowApplication',
|
|
160
|
+
id: 'none',
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('FLOW.FA.5 should handle invalid flow modules', async () => {
|
|
166
|
+
const flow = {
|
|
167
|
+
elements: [{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.TestResource' }],
|
|
168
|
+
connections: [{ id: 'testConnection1', source: 'testTrigger', target: 'testResource' }],
|
|
169
|
+
context: {
|
|
170
|
+
flowId: 'testFlow',
|
|
171
|
+
deploymentId: 'testDeployment',
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
flowApplication = new FlowApplication([FakeModule], flow, { logger: loggerMock, skipApi: true, explicitInit: true });
|
|
175
|
+
await flowApplication.init();
|
|
176
|
+
|
|
177
|
+
expect(loggerMock.warn).toHaveBeenCalledTimes(0);
|
|
178
|
+
expect(loggerMock.error).toHaveBeenCalledTimes(1);
|
|
179
|
+
expect(loggerMock.error).toHaveBeenLastCalledWith(
|
|
180
|
+
expect.stringContaining('FlowModule (FakeModule) metadata is missing or invalid\nat logErrorAndExit'),
|
|
181
|
+
{
|
|
182
|
+
...flow.context,
|
|
183
|
+
functionFqn: 'FlowApplication',
|
|
184
|
+
id: 'none',
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('FLOW.FA.6 should handle invalid flow functions', async () => {
|
|
190
|
+
const flow = {
|
|
191
|
+
elements: [{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.TestResource' }],
|
|
192
|
+
connections: [{ id: 'testConnection1', source: 'testTrigger', target: 'testResource' }],
|
|
193
|
+
context: {
|
|
194
|
+
flowId: 'testFlow',
|
|
195
|
+
deploymentId: 'testDeployment',
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
flowApplication = new FlowApplication([TestModule2], flow, { logger: loggerMock, skipApi: true, explicitInit: true });
|
|
199
|
+
await flowApplication.init();
|
|
200
|
+
|
|
201
|
+
expect(loggerMock.warn).toHaveBeenCalledTimes(0);
|
|
202
|
+
expect(loggerMock.error).toHaveBeenCalledTimes(1);
|
|
203
|
+
expect(loggerMock.error).toHaveBeenLastCalledWith(
|
|
204
|
+
expect.stringContaining('FlowFunction (FakeTask) metadata is missing or invalid\nat logErrorAndExit'),
|
|
205
|
+
{
|
|
206
|
+
...flow.context,
|
|
207
|
+
functionFqn: 'FlowApplication',
|
|
208
|
+
id: 'none',
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('FLOW.FA.7 should warn if high event loop utilization is detected', (done) => {
|
|
214
|
+
const flow = {
|
|
215
|
+
elements: [
|
|
216
|
+
{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.TestResource' },
|
|
217
|
+
{ id: 'highEluTask', module: 'test-module', functionFqn: 'test.task.HighEluTask', properties: { n: 1_000_000_000 } },
|
|
218
|
+
],
|
|
219
|
+
connections: [{ id: 'testConnection1', source: 'testTrigger', target: 'highEluTask' }],
|
|
220
|
+
context: { flowId: 'testFlow', deploymentId: 'testDeployment' },
|
|
221
|
+
};
|
|
222
|
+
flowApplication = new FlowApplication([TestModule], flow, loggerMock, null, null, true);
|
|
223
|
+
|
|
224
|
+
flowApplication.subscribe('highEluTask.default', {
|
|
225
|
+
next: async (event: FlowEvent) => {
|
|
226
|
+
expect(event.getData()).toEqual({ foo: 'bar' });
|
|
227
|
+
await setTimeout(200);
|
|
228
|
+
try {
|
|
229
|
+
expect(loggerMock.warn).toHaveBeenCalledTimes(1);
|
|
230
|
+
expect(loggerMock.warn).toHaveBeenCalledWith(
|
|
231
|
+
expect.stringContaining('High event loop utilization detected for highEluTask.default with event'),
|
|
232
|
+
{ ...flow.context, functionFqn: 'FlowApplication', id: 'none' },
|
|
233
|
+
);
|
|
234
|
+
done();
|
|
235
|
+
} catch (err) {
|
|
236
|
+
done(err);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
flowApplication.emit(new FlowEvent({ id: 'testTrigger' }, {}));
|
|
242
|
+
expect(loggerMock.log).toHaveBeenCalledWith('Flow Deployment is running', expect.objectContaining(flow.context));
|
|
243
|
+
}, 20000);
|
|
244
|
+
|
|
245
|
+
it('FLOW.FA.8 should warn if event queue size is above threshold', (done) => {
|
|
246
|
+
const flow = {
|
|
247
|
+
elements: [
|
|
248
|
+
{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.TestResource' },
|
|
249
|
+
{ id: 'longRunningTask', module: 'test-module', functionFqn: 'test.task.LongRunningTask', properties: { delay: 300 } },
|
|
250
|
+
],
|
|
251
|
+
connections: [{ id: 'testConnection', source: 'testTrigger', target: 'longRunningTask' }],
|
|
252
|
+
context: { flowId: 'testFlow', deploymentId: 'testDeployment' },
|
|
253
|
+
};
|
|
254
|
+
flowApplication = new FlowApplication([TestModule], flow, { logger: loggerMock, skipApi: true });
|
|
255
|
+
|
|
256
|
+
let count = 0;
|
|
257
|
+
flowApplication.subscribe('longRunningTask.default', {
|
|
258
|
+
next: (event: FlowEvent) => {
|
|
259
|
+
try {
|
|
260
|
+
expect(event.getData()).toEqual({ foo: 'bar' });
|
|
261
|
+
if (++count === 10) {
|
|
262
|
+
expect(loggerMock.warn).toHaveBeenCalledTimes(2);
|
|
263
|
+
expect(loggerMock.warn).toHaveBeenCalledWith(
|
|
264
|
+
expect.stringContaining('Input stream "longRunningTask.default" has 100 queued events\nat FlowApplication.setQueueMetrics'),
|
|
265
|
+
{
|
|
266
|
+
deploymentId: 'testDeployment',
|
|
267
|
+
flowId: 'testFlow',
|
|
268
|
+
functionFqn: 'FlowApplication',
|
|
269
|
+
id: 'none',
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
expect(loggerMock.warn).toHaveBeenLastCalledWith(
|
|
273
|
+
expect.stringContaining('Input stream "longRunningTask.default" has 200 queued events\nat FlowApplication.setQueueMetrics'),
|
|
274
|
+
expect.anything(),
|
|
275
|
+
);
|
|
276
|
+
flowApplication.destroy();
|
|
277
|
+
done();
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
flowApplication.destroy();
|
|
281
|
+
done(err);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
for (let i = 0; i < 210; i++) {
|
|
287
|
+
flowApplication.emit(new FlowEvent({ id: 'testTrigger' }, {}));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
expect(loggerMock.log).toHaveBeenCalledWith('Flow Deployment is running', expect.objectContaining(flow.context));
|
|
291
|
+
}, 10000);
|
|
292
|
+
|
|
293
|
+
it('FLOW.FA.9 test complex properties', (done) => {
|
|
294
|
+
const flow = {
|
|
295
|
+
elements: [
|
|
296
|
+
{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.TestResource' },
|
|
297
|
+
{
|
|
298
|
+
id: 'complex',
|
|
299
|
+
module: 'test-module',
|
|
300
|
+
functionFqn: 'test.resource.ComplexProperties',
|
|
301
|
+
properties: {
|
|
302
|
+
variables: [
|
|
303
|
+
{
|
|
304
|
+
name: 'testVar1',
|
|
305
|
+
function: 'rndSin',
|
|
306
|
+
min: 0,
|
|
307
|
+
max: 20,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: 'testVar2',
|
|
311
|
+
function: 'rndCos',
|
|
312
|
+
min: 10,
|
|
313
|
+
max: 100,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'testVar3',
|
|
317
|
+
function: 'rndInt',
|
|
318
|
+
min: 20,
|
|
319
|
+
max: 20,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
connections: [{ id: 'testConnection', source: 'testTrigger', target: 'complex' }],
|
|
326
|
+
context: { flowId: 'testFlow', deploymentId: 'testDeployment' },
|
|
327
|
+
};
|
|
328
|
+
flowApplication = new FlowApplication([TestModule], flow, { logger: loggerMock, skipApi: true });
|
|
329
|
+
|
|
330
|
+
flowApplication.subscribe('complex.default', {
|
|
331
|
+
next: (event: FlowEvent) => {
|
|
332
|
+
const data = event.getData();
|
|
333
|
+
expect(data.variables.every((v) => v instanceof Options)).toBe(true);
|
|
334
|
+
done();
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
flowApplication.emit(new FlowEvent({ id: 'testTrigger' }, {}));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('FLOW.FA.10 should take a Mock-API as a parameter', () => {
|
|
341
|
+
const flow = {
|
|
342
|
+
elements: [
|
|
343
|
+
{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.TestResource' },
|
|
344
|
+
{ id: 'testTask', module: 'test-module', functionFqn: 'test.task.TestTask' },
|
|
345
|
+
],
|
|
346
|
+
connections: [{ id: 'testConnection', source: 'testTrigger', target: 'testTask' }],
|
|
347
|
+
context: { flowId: 'testFlow', deploymentId: 'testDeployment' },
|
|
348
|
+
};
|
|
349
|
+
flowApplication = new FlowApplication([TestModule], flow, { logger: loggerMock, skipApi: true, mockApi: new MockAPI({}) });
|
|
350
|
+
|
|
351
|
+
expect(flowApplication.api).toBeInstanceOf(MockAPI);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('FLOW.FA.11 should take the standard order of parameters', async () => {
|
|
355
|
+
const flow = {
|
|
356
|
+
elements: [{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.TestResource' }],
|
|
357
|
+
connections: [],
|
|
358
|
+
context: { flowId: 'testFlow', deploymentId: 'testDeployment' },
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const flowApp1 = new FlowApplication([TestModule], flow, { logger: loggerMock, skipApi: true });
|
|
362
|
+
expect(flowApp1.api).toBeNull();
|
|
363
|
+
expect(flowApp1.natsConnection).toBeUndefined();
|
|
364
|
+
await flowApp1.destroy(0);
|
|
365
|
+
|
|
366
|
+
const flowApp2 = new FlowApplication([TestModule], flow, {
|
|
367
|
+
skipApi: true,
|
|
368
|
+
amqpConfig: {},
|
|
369
|
+
natsConnection: nc as any,
|
|
370
|
+
explicitInit: true,
|
|
371
|
+
logger: loggerMock,
|
|
372
|
+
});
|
|
373
|
+
expect(flowApp2.natsConnection).toBeDefined();
|
|
374
|
+
await flowApp2.destroy(0);
|
|
375
|
+
}, 60000);
|
|
376
|
+
|
|
377
|
+
it('FLOW.FA.12 should take positional parameters', async () => {
|
|
378
|
+
const flow = {
|
|
379
|
+
elements: [{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.TestResource' }],
|
|
380
|
+
connections: [],
|
|
381
|
+
context: { flowId: 'testFlow', deploymentId: 'testDeployment' },
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const flowApp1 = new FlowApplication([TestModule], flow, loggerMock, null, null, true, true);
|
|
385
|
+
expect(flowApp1.api).toBeNull();
|
|
386
|
+
expect(flowApp1.natsConnection).toBeNull();
|
|
387
|
+
await flowApp1.destroy(0);
|
|
388
|
+
|
|
389
|
+
const flowApp2 = new FlowApplication([TestModule], flow, loggerMock, null, nc as any, true, true);
|
|
390
|
+
expect(flowApp2.natsConnection).toBeDefined();
|
|
391
|
+
await flowApp2.destroy(0);
|
|
392
|
+
}, 60000);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
@FlowFunction('test.resource.TestResource')
|
|
396
|
+
class TestResource extends FlowResource {
|
|
397
|
+
@InputStream('default', { concurrent: 5 })
|
|
398
|
+
public async onDefault(event) {
|
|
399
|
+
return this.emitEvent({ hello: 'world' }, null);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
class Options {
|
|
404
|
+
@IsString()
|
|
405
|
+
name: string;
|
|
406
|
+
|
|
407
|
+
@IsString()
|
|
408
|
+
function: string;
|
|
409
|
+
|
|
410
|
+
@IsNumber()
|
|
411
|
+
min: number;
|
|
412
|
+
|
|
413
|
+
@IsNumber()
|
|
414
|
+
max: number;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
class ComplexProperties {
|
|
418
|
+
@IsArray()
|
|
419
|
+
@Type(() => Options)
|
|
420
|
+
@ValidateNested({ each: true })
|
|
421
|
+
variables: Options[];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
@FlowFunction('test.resource.ComplexProperties')
|
|
425
|
+
class TestComplexProperties extends FlowResource {
|
|
426
|
+
constructor(context, properties) {
|
|
427
|
+
super(context, properties, ComplexProperties, true);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
@InputStream()
|
|
431
|
+
public async onDefault(event) {
|
|
432
|
+
return this.emitEvent(this.properties, event);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
@FlowFunction('test.task.LongRunningTask')
|
|
437
|
+
class LongRunningTask extends FlowTask<Properties> {
|
|
438
|
+
private readonly abortController = new AbortController();
|
|
439
|
+
|
|
440
|
+
constructor(context, properties) {
|
|
441
|
+
super(context, properties, Properties);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
@InputStream()
|
|
445
|
+
public async loveMeLongTime(event) {
|
|
446
|
+
try {
|
|
447
|
+
await delayWithAbort(this.properties.delay, { signal: this.abortController.signal });
|
|
448
|
+
return this.emitEvent({ foo: 'bar' }, null);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
if (err.message === 'AbortError') {
|
|
451
|
+
return null; // Task was aborted
|
|
452
|
+
}
|
|
453
|
+
throw err;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
public onDestroy = () => {
|
|
458
|
+
this.abortController.abort();
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
class Properties {
|
|
463
|
+
@IsNumber()
|
|
464
|
+
delay: number;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
@FlowFunction('test.task.HighEluTask')
|
|
468
|
+
class HighEluTask extends FlowTask<HighEluProperties> {
|
|
469
|
+
private readonly abortController = new AbortController();
|
|
470
|
+
constructor(context, properties) {
|
|
471
|
+
super(context, properties, HighEluProperties);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
@InputStream()
|
|
475
|
+
public async onDefault(event) {
|
|
476
|
+
for (let i = 0; i < this.properties.n; i++) {
|
|
477
|
+
if (i % (this.properties.n / 10) === 0) {
|
|
478
|
+
await delayWithAbort(10, { signal: this.abortController.signal });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return this.emitEvent({ foo: 'bar' }, null);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
public onDestroy = () => {
|
|
485
|
+
this.abortController.abort();
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
class HighEluProperties {
|
|
490
|
+
@IsNumber()
|
|
491
|
+
n: number;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
@FlowModule({
|
|
495
|
+
name: 'test-module',
|
|
496
|
+
declarations: [HighEluTask, LongRunningTask, TestResource, TestComplexProperties],
|
|
497
|
+
})
|
|
498
|
+
class TestModule {}
|
|
499
|
+
|
|
500
|
+
class FakeTask {}
|
|
501
|
+
|
|
502
|
+
@FlowModule({
|
|
503
|
+
name: 'test-module2',
|
|
504
|
+
declarations: [FakeTask as any],
|
|
505
|
+
})
|
|
506
|
+
class TestModule2 {}
|
|
507
|
+
|
|
508
|
+
class FakeModule {}
|