@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +904 -0
  2. package/jest.config.ts +10 -0
  3. package/package.json +7 -20
  4. package/project.json +41 -0
  5. package/src/index.ts +15 -0
  6. package/src/lib/ContextManager.ts +111 -0
  7. package/src/lib/FlowApplication.ts +659 -0
  8. package/src/lib/FlowElement.ts +220 -0
  9. package/src/lib/FlowEvent.ts +73 -0
  10. package/src/lib/FlowLogger.ts +131 -0
  11. package/src/lib/FlowModule.ts +18 -0
  12. package/src/lib/RpcClient.ts +99 -0
  13. package/src/lib/TestModule.ts +14 -0
  14. package/src/lib/__pycache__/rpc_server.cpython-310.pyc +0 -0
  15. package/src/lib/amqp.ts +32 -0
  16. package/src/lib/extra-validators.ts +62 -0
  17. package/src/lib/flow.interface.ts +56 -0
  18. package/{dist/index.d.ts → src/lib/index.ts} +3 -0
  19. package/src/lib/nats.ts +140 -0
  20. package/src/lib/unit-decorators.ts +156 -0
  21. package/src/lib/unit-utils.ts +163 -0
  22. package/src/lib/units.ts +587 -0
  23. package/src/lib/utils.ts +176 -0
  24. package/test/context-manager-purpose.spec.ts +248 -0
  25. package/test/context-manager.spec.ts +55 -0
  26. package/test/context.spec.ts +180 -0
  27. package/test/event.spec.ts +155 -0
  28. package/test/extra-validators.spec.ts +84 -0
  29. package/test/flow-logger.spec.ts +104 -0
  30. package/test/flow.spec.ts +508 -0
  31. package/test/input-stream.decorator.spec.ts +379 -0
  32. package/test/long-rpc.test.py +14 -0
  33. package/test/long-running-rpc.spec.ts +60 -0
  34. package/test/message.spec.ts +57 -0
  35. package/test/mocks/logger.mock.ts +7 -0
  36. package/test/mocks/nats-connection.mock.ts +135 -0
  37. package/test/mocks/nats-prepare.reals-nats.ts +15 -0
  38. package/test/rpc.spec.ts +198 -0
  39. package/test/rpc.test.py +45 -0
  40. package/test/rx.spec.ts +92 -0
  41. package/test/unit-decorator.spec.ts +57 -0
  42. package/test/utils.spec.ts +210 -0
  43. package/test/validation.spec.ts +174 -0
  44. package/tsconfig.json +13 -0
  45. package/tsconfig.lib.json +22 -0
  46. package/tsconfig.spec.json +8 -0
  47. package/LICENSE +0 -21
  48. package/dist/ContextManager.d.ts +0 -40
  49. package/dist/ContextManager.js +0 -77
  50. package/dist/FlowApplication.d.ts +0 -85
  51. package/dist/FlowApplication.js +0 -500
  52. package/dist/FlowElement.d.ts +0 -67
  53. package/dist/FlowElement.js +0 -163
  54. package/dist/FlowEvent.d.ts +0 -25
  55. package/dist/FlowEvent.js +0 -71
  56. package/dist/FlowLogger.d.ts +0 -44
  57. package/dist/FlowLogger.js +0 -94
  58. package/dist/FlowModule.d.ts +0 -7
  59. package/dist/FlowModule.js +0 -13
  60. package/dist/RpcClient.d.ts +0 -13
  61. package/dist/RpcClient.js +0 -84
  62. package/dist/TestModule.d.ts +0 -2
  63. package/dist/TestModule.js +0 -27
  64. package/dist/amqp.d.ts +0 -14
  65. package/dist/amqp.js +0 -12
  66. package/dist/extra-validators.d.ts +0 -1
  67. package/dist/extra-validators.js +0 -51
  68. package/dist/flow.interface.d.ts +0 -48
  69. package/dist/flow.interface.js +0 -9
  70. package/dist/index.js +0 -18
  71. package/dist/nats.d.ts +0 -12
  72. package/dist/nats.js +0 -109
  73. package/dist/unit-decorators.d.ts +0 -39
  74. package/dist/unit-decorators.js +0 -156
  75. package/dist/unit-utils.d.ts +0 -8
  76. package/dist/unit-utils.js +0 -143
  77. package/dist/units.d.ts +0 -31
  78. package/dist/units.js +0 -570
  79. package/dist/utils.d.ts +0 -51
  80. package/dist/utils.js +0 -137
  81. /package/{dist → src/lib}/rpc_server.py +0 -0
@@ -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 {}