@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
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { inspect } from 'util';
|
|
4
|
+
|
|
5
|
+
import { isAxiosError } from 'axios';
|
|
6
|
+
import isPlainObject from 'lodash/isPlainObject';
|
|
7
|
+
import { PythonShell } from 'python-shell';
|
|
8
|
+
import interp from 'string-interp';
|
|
9
|
+
|
|
10
|
+
import { FlowLogger } from './FlowLogger';
|
|
11
|
+
|
|
12
|
+
export function fillTemplate(value: any, ...templateVariables: any): any {
|
|
13
|
+
if (isPlainObject(value)) {
|
|
14
|
+
for (const key of Object.keys(value)) {
|
|
15
|
+
value[key] = fillTemplate(value[key], ...templateVariables);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
} else if (Array.isArray(value) && value.length > 0) {
|
|
19
|
+
value.forEach(function (v, index) {
|
|
20
|
+
this[index] = fillTemplate(v, ...templateVariables);
|
|
21
|
+
}, value);
|
|
22
|
+
return value;
|
|
23
|
+
} else if (value != null && typeof value === 'string' && value.includes('${')) {
|
|
24
|
+
for (const variables of templateVariables) {
|
|
25
|
+
try {
|
|
26
|
+
const result = interp(value, variables || {});
|
|
27
|
+
if (result) {
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
} catch (err) {
|
|
31
|
+
// ignore
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getCircularReplacer() {
|
|
40
|
+
const seen = new WeakSet();
|
|
41
|
+
return (key, value) => {
|
|
42
|
+
if (typeof value === 'object' && value !== null) {
|
|
43
|
+
if (seen.has(value)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
seen.add(value);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function toArray(value: string | string[] = []): string[] {
|
|
53
|
+
return Array.isArray(value) ? value : value.split(',').map((v) => v.trim());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function delay(ms: number): Promise<void> {
|
|
57
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Creates a promise that resolves after a specified delay, with support for cancellation via an AbortSignal.
|
|
62
|
+
*
|
|
63
|
+
* @param {number} ms - The delay duration in milliseconds.
|
|
64
|
+
* @param {Object} [options] - Optional configuration.
|
|
65
|
+
* @param {AbortSignal} [options.signal] - An AbortSignal to allow cancellation of the delay.
|
|
66
|
+
*
|
|
67
|
+
* @returns {Promise<void>} A promise that resolves after the specified delay or rejects if aborted.
|
|
68
|
+
*
|
|
69
|
+
* @throws {Error} If the AbortSignal is already aborted or gets aborted during the delay, the promise rejects with an "AbortError".
|
|
70
|
+
*
|
|
71
|
+
* @details Usage:
|
|
72
|
+
* ```typescript
|
|
73
|
+
* @FlowFunction('test.task.LongRunningTask')
|
|
74
|
+
* class LongRunningTask extends FlowTask<Properties> {
|
|
75
|
+
* private readonly abortController = new AbortController();
|
|
76
|
+
*
|
|
77
|
+
* constructor(...) {...}
|
|
78
|
+
*
|
|
79
|
+
* @InputStream()
|
|
80
|
+
* public async loveMeLongTime(event) {
|
|
81
|
+
* try {
|
|
82
|
+
* await delayWithAbort(this.properties.delay, { signal: this.abortController.signal });
|
|
83
|
+
* return this.emitEvent({ foo: 'bar' }, null);
|
|
84
|
+
* } catch (err) {
|
|
85
|
+
* if (err.message === 'AbortError') {
|
|
86
|
+
* return; // Task was aborted
|
|
87
|
+
* }
|
|
88
|
+
* throw err;
|
|
89
|
+
* }
|
|
90
|
+
* }
|
|
91
|
+
*
|
|
92
|
+
* public onDestroy = () => { this.abortController.abort(); };
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export function delayWithAbort(ms: number, options?: { signal?: AbortSignal }): Promise<void> {
|
|
97
|
+
return new Promise<void>((resolve, reject) => {
|
|
98
|
+
if (options?.signal?.aborted) {
|
|
99
|
+
reject(new Error('AbortError'));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const timeout = setTimeout(() => resolve(), ms);
|
|
104
|
+
|
|
105
|
+
options?.signal?.addEventListener('abort', () => {
|
|
106
|
+
clearTimeout(timeout);
|
|
107
|
+
reject(new Error('AbortError'));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function deleteFiles(dir: string, ...filenames: string[]) {
|
|
113
|
+
for (const filename of filenames) {
|
|
114
|
+
await fs.unlink(join(dir, filename)).catch((err) => {
|
|
115
|
+
/*ignore*/
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function handleApiError(error: any, logger: FlowLogger) {
|
|
121
|
+
if (isAxiosError(error)) {
|
|
122
|
+
const status = error.response?.status ?? error.code ?? '';
|
|
123
|
+
const statusText = error.response?.statusText ?? '';
|
|
124
|
+
const url = error.config?.url ?? '[Unknown URL]';
|
|
125
|
+
const method = error.config?.method?.toUpperCase() ?? '';
|
|
126
|
+
let errorText = error.response?.data ?? '';
|
|
127
|
+
|
|
128
|
+
if (typeof errorText !== 'string') {
|
|
129
|
+
try {
|
|
130
|
+
errorText = JSON.stringify(errorText, null, 2);
|
|
131
|
+
} catch (_) {
|
|
132
|
+
errorText = '[Unserializable error body]';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
logger.error(`${status} ${statusText}: ${method} request to ${url} failed\n${errorText}`);
|
|
136
|
+
|
|
137
|
+
if (error.stack) {
|
|
138
|
+
logger.error(error.stack);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
logger.error(error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function runPyScript(scriptPath: string, data: any) {
|
|
146
|
+
return new Promise<any>((resolve, reject) => {
|
|
147
|
+
let pyData: any;
|
|
148
|
+
|
|
149
|
+
const pyshell = new PythonShell(scriptPath, { mode: 'text', pythonOptions: ['-u'] });
|
|
150
|
+
pyshell.send(JSON.stringify(data));
|
|
151
|
+
pyshell.on('message', (message) => {
|
|
152
|
+
try {
|
|
153
|
+
pyData = JSON.parse(message);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
pyData = message;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
pyshell.end((err, code, signal) => {
|
|
159
|
+
if (err) {
|
|
160
|
+
return reject(err);
|
|
161
|
+
}
|
|
162
|
+
return resolve(pyData);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Truncates an object or string to the specified max length and depth
|
|
169
|
+
*/
|
|
170
|
+
export function truncate(msg: any, depth = 4, maxStringLength = 1000): string {
|
|
171
|
+
let truncated = inspect(msg, { depth, maxStringLength });
|
|
172
|
+
if (truncated.startsWith("'") && truncated.endsWith("'")) {
|
|
173
|
+
truncated = truncated.substring(1, truncated.length - 1);
|
|
174
|
+
}
|
|
175
|
+
return truncated;
|
|
176
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Type } from 'class-transformer';
|
|
2
|
+
import { IsArray, IsBoolean, IsIn, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator';
|
|
3
|
+
import { CloudEvent } from 'cloudevents';
|
|
4
|
+
|
|
5
|
+
import { FlowApplication, FlowEvent, FlowFunction, FlowModule, FlowResource, FlowTask, InputStream } from '../src';
|
|
6
|
+
import { loggerMock } from './mocks/logger.mock';
|
|
7
|
+
|
|
8
|
+
class Properties {}
|
|
9
|
+
|
|
10
|
+
@FlowFunction('test.default.trigger')
|
|
11
|
+
class TestTrigger extends FlowResource {
|
|
12
|
+
constructor(context, properties: unknown) {
|
|
13
|
+
super(context, properties, Properties);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@InputStream()
|
|
17
|
+
public async onDefault(event) {
|
|
18
|
+
return this.emitEvent({ empty: 'empty' }, event);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type ValueDataType = 'string' | 'number' | 'boolean';
|
|
23
|
+
|
|
24
|
+
@FlowFunction('test.default.Inject')
|
|
25
|
+
export class TestInject extends FlowTask<InjectionProperties> {
|
|
26
|
+
constructor(context, properties: unknown) {
|
|
27
|
+
super(context, properties, InjectionProperties, true);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@InputStream()
|
|
31
|
+
public async inject(event: FlowEvent) {
|
|
32
|
+
const injectedData = event.getData();
|
|
33
|
+
for (const injection of this.properties.injections || []) {
|
|
34
|
+
injectedData[injection.key] = this.resolveValue(injection.value, injection.valueDatatype);
|
|
35
|
+
}
|
|
36
|
+
return this.emitEvent(injectedData, event);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private resolveValue(value: string, valueDatatype: ValueDataType) {
|
|
40
|
+
let resultValue;
|
|
41
|
+
switch (valueDatatype) {
|
|
42
|
+
case 'boolean':
|
|
43
|
+
resultValue = value === 'true';
|
|
44
|
+
break;
|
|
45
|
+
case 'number':
|
|
46
|
+
resultValue = parseFloat(value);
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
resultValue = value;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
return resultValue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class Injection {
|
|
57
|
+
@IsString()
|
|
58
|
+
key: string;
|
|
59
|
+
|
|
60
|
+
@IsString()
|
|
61
|
+
value: string;
|
|
62
|
+
|
|
63
|
+
@IsIn(['string', 'number', 'boolean'])
|
|
64
|
+
@IsNotEmpty()
|
|
65
|
+
@IsString()
|
|
66
|
+
valueDatatype?: 'string' | 'number' | 'boolean';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class InjectionProperties {
|
|
70
|
+
@IsArray()
|
|
71
|
+
@Type(() => Injection)
|
|
72
|
+
@ValidateNested({ each: true })
|
|
73
|
+
injections: Injection[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@FlowFunction('test.default.Noop')
|
|
77
|
+
export class TestNoop extends FlowTask<TestProperties> {
|
|
78
|
+
constructor(context, properties: unknown) {
|
|
79
|
+
super(context, properties, TestProperties, true);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@InputStream()
|
|
83
|
+
public async noop(event: FlowEvent) {
|
|
84
|
+
const data = event.getData();
|
|
85
|
+
if (this.properties.logData === true) {
|
|
86
|
+
this.logger.log(data, { truncate: false });
|
|
87
|
+
}
|
|
88
|
+
return this.emitEvent(data, event);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class TestProperties {
|
|
93
|
+
@IsBoolean()
|
|
94
|
+
@IsOptional()
|
|
95
|
+
logData?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@FlowModule({
|
|
99
|
+
name: 'test-module',
|
|
100
|
+
declarations: [TestTrigger, TestInject, TestNoop],
|
|
101
|
+
})
|
|
102
|
+
class TestModule {}
|
|
103
|
+
|
|
104
|
+
describe('CMTP.1: ContextManager purpose test', () => {
|
|
105
|
+
const flow = {
|
|
106
|
+
elements: [
|
|
107
|
+
{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.default.trigger', properties: {} },
|
|
108
|
+
{
|
|
109
|
+
id: 'testInject',
|
|
110
|
+
module: 'test-module',
|
|
111
|
+
functionFqn: 'test.default.Inject',
|
|
112
|
+
properties: {
|
|
113
|
+
injections: [
|
|
114
|
+
{
|
|
115
|
+
key: 'value',
|
|
116
|
+
value: '${flow.value}',
|
|
117
|
+
valueDatatype: 'number',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
key: 'test',
|
|
121
|
+
value: '${test}',
|
|
122
|
+
valueDatatype: 'string',
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{ id: 'testNoop', module: 'test-module', functionFqn: 'test.default.Noop', properties: { logData: true } },
|
|
128
|
+
],
|
|
129
|
+
connections: [
|
|
130
|
+
{ id: 'testConnection1', source: 'testTrigger', target: 'testInject' },
|
|
131
|
+
{ id: 'testConnection2', source: 'testInject', target: 'testNoop' },
|
|
132
|
+
],
|
|
133
|
+
context: {
|
|
134
|
+
flowId: 'testFlow',
|
|
135
|
+
deploymentId: 'testDeployment',
|
|
136
|
+
},
|
|
137
|
+
properties: { value: '123' },
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
let flowApp: FlowApplication;
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
flowApp = new FlowApplication([TestModule], flow, { logger: loggerMock, skipApi: true });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
afterEach(async () => {
|
|
146
|
+
await flowApp.destroy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('CMTP.1.1: Should overwrite all placeholder properties starting with flow. at init', async () => {
|
|
150
|
+
await flowApp.init();
|
|
151
|
+
expect(Array.isArray(((flowApp as any).elements['testInject'] as any).properties.injections)).toBe(true);
|
|
152
|
+
expect(((flowApp as any).elements['testInject'] as any).getPropertiesWithPlaceholders().injections[0].value).toBe('${flow.value}');
|
|
153
|
+
expect(((flowApp as any).elements['testInject'] as any).properties.injections[0].value).toBe('123');
|
|
154
|
+
expect(((flowApp as any).elements['testInject'] as any).getPropertiesWithPlaceholders().injections[1].value).toBe('${test}');
|
|
155
|
+
expect(((flowApp as any).elements['testInject'] as any).properties.injections[1].value).toBe('${test}');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('CMTP.1.2: Should overwrite all placeholder properties starting with flow. also at update', (done) => {
|
|
159
|
+
let iteration = 0;
|
|
160
|
+
flowApp.subscribe('testNoop.default', {
|
|
161
|
+
next: (flowEvent: FlowEvent) => {
|
|
162
|
+
expect(((flowApp as any).elements['testInject'] as any).getPropertiesWithPlaceholders().injections[0].value).toBe('${flow.value}');
|
|
163
|
+
expect(((flowApp as any).elements['testInject'] as any).getPropertiesWithPlaceholders().injections[1].value).toBe('${test}');
|
|
164
|
+
|
|
165
|
+
const data = flowEvent.getData();
|
|
166
|
+
expect(data.test).toEqual('${test}');
|
|
167
|
+
|
|
168
|
+
iteration++;
|
|
169
|
+
if (iteration === 1) {
|
|
170
|
+
expect(data.value).toEqual(123);
|
|
171
|
+
} else if (iteration === 2) {
|
|
172
|
+
expect(data.value).toEqual(456);
|
|
173
|
+
} else if (iteration === 3) {
|
|
174
|
+
expect(data.value).toEqual(1);
|
|
175
|
+
done();
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Check the initial values in first iteration
|
|
181
|
+
flowApp.emit(new FlowEvent({ id: 'testTrigger' }, {}));
|
|
182
|
+
|
|
183
|
+
const cloudEvent = new CloudEvent({
|
|
184
|
+
source: 'flowstudio/deployments',
|
|
185
|
+
type: 'com.flowstudio.deployment',
|
|
186
|
+
subject: 'deploymentId.update',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
flowApp
|
|
190
|
+
.onMessage({ ...cloudEvent, data: { properties: { value: 456 } } } as any)
|
|
191
|
+
.then(() => {
|
|
192
|
+
// Check the updated values in second iteration
|
|
193
|
+
return flowApp.emit(new FlowEvent({ id: 'testTrigger' }, {}));
|
|
194
|
+
})
|
|
195
|
+
.then(() => {
|
|
196
|
+
return flowApp.onMessage({ ...cloudEvent, data: { properties: { value: 1 } } } as any);
|
|
197
|
+
})
|
|
198
|
+
.then(() => {
|
|
199
|
+
// Check the updated values in fourth iteration
|
|
200
|
+
return flowApp.emit(new FlowEvent({ id: 'testTrigger' }, {}));
|
|
201
|
+
});
|
|
202
|
+
}, 60000);
|
|
203
|
+
|
|
204
|
+
test('CMTP.1.3: Should overwrite all placeholder properties starting with flow. also at update even when the element properties update', (done) => {
|
|
205
|
+
let iteration = 0;
|
|
206
|
+
flowApp.subscribe('testNoop.default', {
|
|
207
|
+
next: (flowEvent: FlowEvent) => {
|
|
208
|
+
const data = flowEvent.getData();
|
|
209
|
+
iteration++;
|
|
210
|
+
if (iteration === 1) {
|
|
211
|
+
expect((flowApp as any).elements['testInject'].getPropertiesWithPlaceholders().injections.length).toBe(2);
|
|
212
|
+
expect((flowApp as any).elements['testInject'].getPropertiesWithPlaceholders().injections[0].value).toBe('${flow.value}');
|
|
213
|
+
expect(data.value).toEqual(123);
|
|
214
|
+
} else if (iteration === 2) {
|
|
215
|
+
expect((flowApp as any).elements['testInject'].getPropertiesWithPlaceholders().injections.length).toBe(1);
|
|
216
|
+
expect((flowApp as any).elements['testInject'].getPropertiesWithPlaceholders().injections[0].value).toBe('987');
|
|
217
|
+
expect(data.value).toEqual(987);
|
|
218
|
+
done();
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Check the initial values in first iteration
|
|
224
|
+
flowApp.emit(new FlowEvent({ id: 'testTrigger' }, {}));
|
|
225
|
+
|
|
226
|
+
flowApp
|
|
227
|
+
.onMessage({
|
|
228
|
+
source: 'flowstudio/deployments',
|
|
229
|
+
type: 'com.flowstudio.deployment',
|
|
230
|
+
subject: 'deploymentId.update',
|
|
231
|
+
data: {
|
|
232
|
+
properties: { flow: { value: 456 } },
|
|
233
|
+
elements: [
|
|
234
|
+
{
|
|
235
|
+
id: 'testInject',
|
|
236
|
+
properties: {
|
|
237
|
+
injections: [{ key: 'value', value: '987', valueDatatype: 'number' }],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
} as any,
|
|
242
|
+
} as any)
|
|
243
|
+
.then(() => {
|
|
244
|
+
// Check the updated values in second iteration
|
|
245
|
+
return flowApp.emit(new FlowEvent({ id: 'testTrigger' }, {}));
|
|
246
|
+
});
|
|
247
|
+
}, 60000);
|
|
248
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ContextManager } from '../src';
|
|
2
|
+
import { loggerMock } from './mocks/logger.mock';
|
|
3
|
+
|
|
4
|
+
describe('The ContextManage-Test spec', () => {
|
|
5
|
+
let contextManager: ContextManager;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
contextManager = new ContextManager(loggerMock, {});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('CMT.1: Should be created', () => {
|
|
12
|
+
expect(contextManager).toBeDefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('CMT.2: The ContextManager set method', () => {
|
|
16
|
+
test('CMT.2.1: Should set a property', () => {
|
|
17
|
+
contextManager.set('test', 'value');
|
|
18
|
+
expect(contextManager.get('test')).toBe('value');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('CMT.2.2: Should not set a property with flow.', () => {
|
|
22
|
+
contextManager.set('flow.test', 'value');
|
|
23
|
+
expect(loggerMock.error).toHaveBeenCalled();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('CMT.2.3: Should overwrite a property', () => {
|
|
27
|
+
contextManager.set('test', 'value');
|
|
28
|
+
expect(contextManager.get('test')).toBe('value');
|
|
29
|
+
contextManager.set('test', 'value2');
|
|
30
|
+
expect(contextManager.get('test')).toBe('value2');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('CMT.3: The ContextManager get method', () => {
|
|
35
|
+
test('CMT.3.1: Should get a property', () => {
|
|
36
|
+
contextManager.set('test', 'value');
|
|
37
|
+
expect(contextManager.get('test')).toBe('value');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('CMT.3.2: Should get undefined for not set property', () => {
|
|
41
|
+
expect(contextManager.get('test')).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('CMT.4: The ContextManager update method', () => {
|
|
46
|
+
test('CMT.4.1: Should set the property to the value property', () => {
|
|
47
|
+
contextManager.overwriteAllProperties({ test: 'bar' });
|
|
48
|
+
contextManager.set('not-flow', 'foo');
|
|
49
|
+
contextManager.updateFlowProperties({ test: 'baz' });
|
|
50
|
+
|
|
51
|
+
expect(contextManager.get('not-flow')).toBe('foo');
|
|
52
|
+
expect(contextManager.get('flow.test')).toBe('baz');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { IsString } from 'class-validator';
|
|
2
|
+
import { CloudEvent } from 'cloudevents';
|
|
3
|
+
|
|
4
|
+
import { FlowApplication, FlowEvent, FlowFunction, FlowModule, FlowResource, InputStream } from '../src';
|
|
5
|
+
import { loggerMock } from './mocks/logger.mock';
|
|
6
|
+
|
|
7
|
+
describe('Flow Application', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.resetAllMocks();
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('FLOW.CON.1 Simple Flow Application with Long Running Task', (done) => {
|
|
14
|
+
const flow = {
|
|
15
|
+
elements: [
|
|
16
|
+
{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.TestResource', properties: { assetId: '' } },
|
|
17
|
+
{ id: 'testResource', module: 'test-module', functionFqn: 'test.resource.TestResource', properties: { assetId: 'abc' } },
|
|
18
|
+
],
|
|
19
|
+
connections: [{ id: 'testConnection1', source: 'testTrigger', target: 'testResource' }],
|
|
20
|
+
context: {
|
|
21
|
+
flowId: 'testFlow',
|
|
22
|
+
deploymentId: 'testDeployment',
|
|
23
|
+
},
|
|
24
|
+
properties: { test: '123abcd' },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const flowApp = new FlowApplication([TestModule], flow, { logger: loggerMock, skipApi: true });
|
|
28
|
+
|
|
29
|
+
let iteration = 0;
|
|
30
|
+
flowApp.subscribe('testResource.default', {
|
|
31
|
+
next: (event1: FlowEvent) => {
|
|
32
|
+
const data = event1.getData();
|
|
33
|
+
iteration++;
|
|
34
|
+
if (iteration === 1) {
|
|
35
|
+
expect(data.assetId).toEqual('abc');
|
|
36
|
+
expect(data.event).toEqual({});
|
|
37
|
+
expect(data.elementProps).toEqual({ assetId: 'abc' });
|
|
38
|
+
expect(data.flowProps).toEqual({ flow: { test: '123abcd' } });
|
|
39
|
+
} else if (iteration === 2) {
|
|
40
|
+
expect(data.assetId).toEqual('xyz');
|
|
41
|
+
expect(data.event).toEqual({});
|
|
42
|
+
expect(data.elementProps).toEqual({ assetId: 'xyz' });
|
|
43
|
+
expect(data.flowProps).toEqual({ flow: { test: '123abcd' } });
|
|
44
|
+
} else if (iteration === 3) {
|
|
45
|
+
expect(data.assetId).toEqual('123');
|
|
46
|
+
expect(data.event).toEqual({});
|
|
47
|
+
expect(data.elementProps).toEqual({ assetId: '123' });
|
|
48
|
+
expect(data.flowProps).toEqual({ flow: { test: 42 } });
|
|
49
|
+
done();
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
flowApp.emit(new FlowEvent({ id: 'testTrigger' }, {}));
|
|
55
|
+
|
|
56
|
+
flowApp
|
|
57
|
+
.onMessage(
|
|
58
|
+
new CloudEvent<any>({
|
|
59
|
+
source: 'flowstudio/deployments',
|
|
60
|
+
type: 'com.flowstudio.deployment',
|
|
61
|
+
data: { elements: [{ id: 'testResource', properties: { assetId: 'xyz' } }] },
|
|
62
|
+
subject: 'deploymentId.update',
|
|
63
|
+
}),
|
|
64
|
+
)
|
|
65
|
+
.then(() => {
|
|
66
|
+
return flowApp.emit(new FlowEvent({ id: 'testTrigger' }, {}));
|
|
67
|
+
})
|
|
68
|
+
.then(() => {
|
|
69
|
+
return flowApp.onMessage(
|
|
70
|
+
new CloudEvent<any>({
|
|
71
|
+
subject: 'deploymentId.update',
|
|
72
|
+
source: 'flowstudio/deployments',
|
|
73
|
+
type: 'com.flowstudio.deployment',
|
|
74
|
+
data: {
|
|
75
|
+
elements: [{ id: 'testResource', properties: { assetId: '123' } }],
|
|
76
|
+
properties: { test: 42 },
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
})
|
|
81
|
+
.then(() => {
|
|
82
|
+
return flowApp.emit(new FlowEvent({ id: 'testTrigger' }, {}));
|
|
83
|
+
});
|
|
84
|
+
}, 10000);
|
|
85
|
+
|
|
86
|
+
test('FLOW.CON.2 string interpolation with event data', async () => {
|
|
87
|
+
let tr = new TestResource({ id: 'testResource', logger: loggerMock }, { assetId: '${test}' });
|
|
88
|
+
let event = await tr.onDefault(new FlowEvent({ id: 'tr' }, { test: 'xyz' }));
|
|
89
|
+
let data = event.getData();
|
|
90
|
+
expect(data).toBeDefined();
|
|
91
|
+
expect(data.assetId).toBe('xyz');
|
|
92
|
+
|
|
93
|
+
tr = new TestResource({ id: 'testResource', logger: loggerMock }, { assetId: '${test}' });
|
|
94
|
+
event = await tr.onDefault(new FlowEvent({ id: 'tr' }, { nottest: 'xyz' }));
|
|
95
|
+
data = event.getData();
|
|
96
|
+
expect(data).toBeDefined();
|
|
97
|
+
expect(data.assetId).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('FLOW.CON.3 string interpolation with flow context properties', (done) => {
|
|
101
|
+
const flow = {
|
|
102
|
+
elements: [
|
|
103
|
+
{ id: 'testTrigger', module: 'test-module', functionFqn: 'test.resource.TestResource', properties: { assetId: '' } },
|
|
104
|
+
{ id: 'testResource', module: 'test-module', functionFqn: 'test.resource.TestResource', properties: { assetId: '${test}' } },
|
|
105
|
+
],
|
|
106
|
+
connections: [{ id: 'testConnection1', source: 'testTrigger', target: 'testResource' }],
|
|
107
|
+
context: { flowId: 'testFlow', deploymentId: 'testDeployment' },
|
|
108
|
+
properties: { test: '123abcd' },
|
|
109
|
+
};
|
|
110
|
+
const flowApp = new FlowApplication([TestModule], flow, { logger: loggerMock, skipApi: true });
|
|
111
|
+
|
|
112
|
+
let count = 0;
|
|
113
|
+
flowApp.subscribe('testResource.default', {
|
|
114
|
+
next: (event: FlowEvent) => {
|
|
115
|
+
const data = event.getData();
|
|
116
|
+
expect(data).toBeDefined();
|
|
117
|
+
if (count === 1) {
|
|
118
|
+
expect(data.assetId).toBe('987zyx');
|
|
119
|
+
done();
|
|
120
|
+
} else {
|
|
121
|
+
expect(data.assetId).toBe(undefined);
|
|
122
|
+
count++;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
flowApp.emit(new FlowEvent({ id: 'testTrigger' }, { x: 'y' }));
|
|
127
|
+
flowApp.emit(new FlowEvent({ id: 'testTrigger' }, { test: '987zyx' }));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('FLOW.CON.4 untruncated logging', async () => {
|
|
131
|
+
const tr = new TestResource({ id: 'testResource', logger: loggerMock }, { assetId: '1234' });
|
|
132
|
+
await tr.onDefault(new FlowEvent({ id: 'tr' }, { test: 'tyz' }));
|
|
133
|
+
expect(loggerMock.verbose).toHaveBeenCalledTimes(1);
|
|
134
|
+
expect(loggerMock.verbose).toHaveBeenCalledWith(expect.stringContaining('test'), expect.objectContaining({ truncate: false }));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('Flow.CON.5 creation of element without app', () => {
|
|
138
|
+
const elem = new TestResource(
|
|
139
|
+
{
|
|
140
|
+
id: 'testResource',
|
|
141
|
+
logger: loggerMock,
|
|
142
|
+
app: {
|
|
143
|
+
emit: jest.fn(),
|
|
144
|
+
emitPartial: jest.fn(),
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{ assetId: '1234' },
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
elem.onDefault(new FlowEvent({ id: 'tr' }, { test: 'tyz' }));
|
|
151
|
+
expect(loggerMock.verbose).toHaveBeenCalledTimes(1);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
@FlowFunction('test.resource.TestResource')
|
|
156
|
+
class TestResource extends FlowResource {
|
|
157
|
+
constructor(context, properties: unknown) {
|
|
158
|
+
super(context, properties, Properties);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@InputStream()
|
|
162
|
+
public async onDefault(event) {
|
|
163
|
+
const assetId = this.interpolate(this.properties.assetId, event.getData(), this.flowProperties);
|
|
164
|
+
const data = { assetId, event: {}, elementProps: this.properties, flowProps: this.flowProperties };
|
|
165
|
+
|
|
166
|
+
this.logger.verbose('test', { truncate: false });
|
|
167
|
+
return this.emitEvent(data, event);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
class Properties {
|
|
172
|
+
@IsString()
|
|
173
|
+
assetId: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@FlowModule({
|
|
177
|
+
name: 'test-module',
|
|
178
|
+
declarations: [TestResource],
|
|
179
|
+
})
|
|
180
|
+
class TestModule {}
|