@fincity/kirun-js 2.16.0 → 2.16.2

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,317 @@
1
+ import {
2
+ FunctionExecutionParameters,
3
+ KIRunFunctionRepository,
4
+ KIRunSchemaRepository,
5
+ } from '../../../../src';
6
+ import { Make } from '../../../../src/engine/function/system/Make';
7
+
8
+ const make: Make = new Make();
9
+
10
+ function createOutputMap(data: Record<string, any>): Map<string, Map<string, Map<string, any>>> {
11
+ return new Map([
12
+ [
13
+ 'step1',
14
+ new Map([
15
+ [
16
+ 'output',
17
+ new Map(Object.entries(data)),
18
+ ],
19
+ ]),
20
+ ],
21
+ ]);
22
+ }
23
+
24
+ test('test simple object with expression', async () => {
25
+ const source = { name: 'John', age: 30 };
26
+
27
+ const resultShape = {
28
+ fullName: '{{Steps.step1.output.source.name}}',
29
+ userAge: '{{Steps.step1.output.source.age}}',
30
+ };
31
+
32
+ const fep: FunctionExecutionParameters = new FunctionExecutionParameters(
33
+ new KIRunFunctionRepository(),
34
+ new KIRunSchemaRepository(),
35
+ );
36
+
37
+ fep.setArguments(new Map<string, any>([['resultShape', resultShape]]));
38
+ fep.setContext(new Map());
39
+ fep.setSteps(createOutputMap({ source }));
40
+
41
+ const result = await make.execute(fep);
42
+ const value = result.allResults()[0]?.getResult()?.get('value');
43
+
44
+ expect(value).toStrictEqual({
45
+ fullName: 'John',
46
+ userAge: 30,
47
+ });
48
+ });
49
+
50
+ test('test nested object with expressions', async () => {
51
+ const source = {
52
+ user: { firstName: 'John', lastName: 'Doe' },
53
+ address: { city: 'NYC', zip: '10001' },
54
+ };
55
+
56
+ const resultShape = {
57
+ person: {
58
+ name: '{{Steps.step1.output.source.user.firstName}}',
59
+ surname: '{{Steps.step1.output.source.user.lastName}}',
60
+ },
61
+ location: {
62
+ cityName: '{{Steps.step1.output.source.address.city}}',
63
+ postalCode: '{{Steps.step1.output.source.address.zip}}',
64
+ },
65
+ };
66
+
67
+ const fep: FunctionExecutionParameters = new FunctionExecutionParameters(
68
+ new KIRunFunctionRepository(),
69
+ new KIRunSchemaRepository(),
70
+ );
71
+
72
+ fep.setArguments(new Map<string, any>([['resultShape', resultShape]]));
73
+ fep.setContext(new Map());
74
+ fep.setSteps(createOutputMap({ source }));
75
+
76
+ const result = await make.execute(fep);
77
+ const value = result.allResults()[0]?.getResult()?.get('value');
78
+
79
+ expect(value).toStrictEqual({
80
+ person: {
81
+ name: 'John',
82
+ surname: 'Doe',
83
+ },
84
+ location: {
85
+ cityName: 'NYC',
86
+ postalCode: '10001',
87
+ },
88
+ });
89
+ });
90
+
91
+ test('test array with expressions', async () => {
92
+ const source = {
93
+ items: ['apple', 'banana', 'cherry'],
94
+ };
95
+
96
+ const resultShape = {
97
+ fruits: [
98
+ '{{Steps.step1.output.source.items[0]}}',
99
+ '{{Steps.step1.output.source.items[1]}}',
100
+ '{{Steps.step1.output.source.items[2]}}',
101
+ ],
102
+ };
103
+
104
+ const fep: FunctionExecutionParameters = new FunctionExecutionParameters(
105
+ new KIRunFunctionRepository(),
106
+ new KIRunSchemaRepository(),
107
+ );
108
+
109
+ fep.setArguments(new Map<string, any>([['resultShape', resultShape]]));
110
+ fep.setContext(new Map());
111
+ fep.setSteps(createOutputMap({ source }));
112
+
113
+ const result = await make.execute(fep);
114
+ const value = result.allResults()[0]?.getResult()?.get('value');
115
+
116
+ expect(value).toStrictEqual({
117
+ fruits: ['apple', 'banana', 'cherry'],
118
+ });
119
+ });
120
+
121
+ test('test deeply nested structure with arrays', async () => {
122
+ const source = {
123
+ data: {
124
+ users: [
125
+ { id: 1, name: 'Alice' },
126
+ { id: 2, name: 'Bob' },
127
+ ],
128
+ },
129
+ };
130
+
131
+ const resultShape = {
132
+ level1: {
133
+ level2: {
134
+ level3: {
135
+ userList: [
136
+ {
137
+ userId: '{{Steps.step1.output.source.data.users[0].id}}',
138
+ userName: '{{Steps.step1.output.source.data.users[0].name}}',
139
+ },
140
+ {
141
+ userId: '{{Steps.step1.output.source.data.users[1].id}}',
142
+ userName: '{{Steps.step1.output.source.data.users[1].name}}',
143
+ },
144
+ ],
145
+ },
146
+ },
147
+ },
148
+ };
149
+
150
+ const fep: FunctionExecutionParameters = new FunctionExecutionParameters(
151
+ new KIRunFunctionRepository(),
152
+ new KIRunSchemaRepository(),
153
+ );
154
+
155
+ fep.setArguments(new Map<string, any>([['resultShape', resultShape]]));
156
+ fep.setContext(new Map());
157
+ fep.setSteps(createOutputMap({ source }));
158
+
159
+ const result = await make.execute(fep);
160
+ const value = result.allResults()[0]?.getResult()?.get('value');
161
+
162
+ expect(value).toStrictEqual({
163
+ level1: {
164
+ level2: {
165
+ level3: {
166
+ userList: [
167
+ { userId: 1, userName: 'Alice' },
168
+ { userId: 2, userName: 'Bob' },
169
+ ],
170
+ },
171
+ },
172
+ },
173
+ });
174
+ });
175
+
176
+ test('test mixed static and dynamic values', async () => {
177
+ const source = { dynamicValue: 'from source' };
178
+
179
+ const resultShape = {
180
+ static: 'static string',
181
+ dynamic: '{{Steps.step1.output.source.dynamicValue}}',
182
+ nested: {
183
+ staticNum: 42,
184
+ dynamicNum: '{{Steps.step1.output.source.dynamicValue}}',
185
+ },
186
+ array: ['static', '{{Steps.step1.output.source.dynamicValue}}', 123],
187
+ };
188
+
189
+ const fep: FunctionExecutionParameters = new FunctionExecutionParameters(
190
+ new KIRunFunctionRepository(),
191
+ new KIRunSchemaRepository(),
192
+ );
193
+
194
+ fep.setArguments(new Map<string, any>([['resultShape', resultShape]]));
195
+ fep.setContext(new Map());
196
+ fep.setSteps(createOutputMap({ source }));
197
+
198
+ const result = await make.execute(fep);
199
+ const value = result.allResults()[0]?.getResult()?.get('value');
200
+
201
+ expect(value).toStrictEqual({
202
+ static: 'static string',
203
+ dynamic: 'from source',
204
+ nested: {
205
+ staticNum: 42,
206
+ dynamicNum: 'from source',
207
+ },
208
+ array: ['static', 'from source', 123],
209
+ });
210
+ });
211
+
212
+ test('test null and undefined handling', async () => {
213
+ const resultShape = {
214
+ nullValue: null,
215
+ nested: {
216
+ inner: null,
217
+ },
218
+ };
219
+
220
+ const fep: FunctionExecutionParameters = new FunctionExecutionParameters(
221
+ new KIRunFunctionRepository(),
222
+ new KIRunSchemaRepository(),
223
+ );
224
+
225
+ fep.setArguments(new Map<string, any>([['resultShape', resultShape]]));
226
+ fep.setContext(new Map());
227
+ fep.setSteps(new Map());
228
+
229
+ const result = await make.execute(fep);
230
+ const value = result.allResults()[0]?.getResult()?.get('value');
231
+
232
+ expect(value).toStrictEqual({
233
+ nullValue: null,
234
+ nested: {
235
+ inner: null,
236
+ },
237
+ });
238
+ });
239
+
240
+ test('test array of arrays', async () => {
241
+ const source = {
242
+ matrix: [
243
+ [1, 2],
244
+ [3, 4],
245
+ ],
246
+ };
247
+
248
+ const resultShape = {
249
+ grid: [
250
+ ['{{Steps.step1.output.source.matrix[0][0]}}', '{{Steps.step1.output.source.matrix[0][1]}}'],
251
+ ['{{Steps.step1.output.source.matrix[1][0]}}', '{{Steps.step1.output.source.matrix[1][1]}}'],
252
+ ],
253
+ };
254
+
255
+ const fep: FunctionExecutionParameters = new FunctionExecutionParameters(
256
+ new KIRunFunctionRepository(),
257
+ new KIRunSchemaRepository(),
258
+ );
259
+
260
+ fep.setArguments(new Map<string, any>([['resultShape', resultShape]]));
261
+ fep.setContext(new Map());
262
+ fep.setSteps(createOutputMap({ source }));
263
+
264
+ const result = await make.execute(fep);
265
+ const value = result.allResults()[0]?.getResult()?.get('value');
266
+
267
+ expect(value).toStrictEqual({
268
+ grid: [
269
+ [1, 2],
270
+ [3, 4],
271
+ ],
272
+ });
273
+ });
274
+
275
+ test('test primitive result shape', async () => {
276
+ const source = { value: 'hello' };
277
+
278
+ const resultShape = '{{Steps.step1.output.source.value}}';
279
+
280
+ const fep: FunctionExecutionParameters = new FunctionExecutionParameters(
281
+ new KIRunFunctionRepository(),
282
+ new KIRunSchemaRepository(),
283
+ );
284
+
285
+ fep.setArguments(new Map<string, any>([['resultShape', resultShape]]));
286
+ fep.setContext(new Map());
287
+ fep.setSteps(createOutputMap({ source }));
288
+
289
+ const result = await make.execute(fep);
290
+ const value = result.allResults()[0]?.getResult()?.get('value');
291
+
292
+ expect(value).toBe('hello');
293
+ });
294
+
295
+ test('test array as root result shape', async () => {
296
+ const source = { a: 1, b: 2, c: 3 };
297
+
298
+ const resultShape = [
299
+ '{{Steps.step1.output.source.a}}',
300
+ '{{Steps.step1.output.source.b}}',
301
+ '{{Steps.step1.output.source.c}}',
302
+ ];
303
+
304
+ const fep: FunctionExecutionParameters = new FunctionExecutionParameters(
305
+ new KIRunFunctionRepository(),
306
+ new KIRunSchemaRepository(),
307
+ );
308
+
309
+ fep.setArguments(new Map<string, any>([['resultShape', resultShape]]));
310
+ fep.setContext(new Map());
311
+ fep.setSteps(createOutputMap({ source }));
312
+
313
+ const result = await make.execute(fep);
314
+ const value = result.allResults()[0]?.getResult()?.get('value');
315
+
316
+ expect(value).toStrictEqual([1, 2, 3]);
317
+ });
@@ -0,0 +1,282 @@
1
+ import { ExpressionEvaluator } from '../../../../src/engine/runtime/expression/ExpressionEvaluator';
2
+ import { TokenValueExtractor } from '../../../../src/engine/runtime/expression/tokenextractor/TokenValueExtractor';
3
+
4
+ class TestExtractor extends TokenValueExtractor {
5
+ private data: any;
6
+
7
+ constructor(data: any) {
8
+ super();
9
+ this.data = data;
10
+ }
11
+
12
+ protected getValueInternal(token: string): any {
13
+ const prefix = this.getPrefix();
14
+ const path = token.substring(prefix.length);
15
+ const parts = TokenValueExtractor['splitPath'](path);
16
+ return this.retrieveElementFrom(token, parts, 0, this.data);
17
+ }
18
+
19
+ public getPrefix(): string {
20
+ return 'Context.';
21
+ }
22
+
23
+ public getStore(): any {
24
+ return this.data;
25
+ }
26
+
27
+ // Expose retrieveElementFrom for testing
28
+ public retrieveElementFrom(
29
+ token: string,
30
+ parts: string[],
31
+ partNumber: number,
32
+ jsonElement: any,
33
+ ): any {
34
+ return super.retrieveElementFrom(token, parts, partNumber, jsonElement);
35
+ }
36
+ }
37
+
38
+ describe('ExpressionEvaluator with Bracket Notation', () => {
39
+ let testData: any;
40
+ let extractor: TestExtractor;
41
+ let evaluatorMap: Map<string, TokenValueExtractor>;
42
+
43
+ beforeEach(() => {
44
+ testData = {
45
+ obj: {
46
+ 'mail.props.port': 587,
47
+ 'mail.props.host': 'smtp.example.com',
48
+ 'api.key.secret': 'secret123',
49
+ simple: 'value',
50
+ count: 100,
51
+ },
52
+ arr: [10, 20, 30, 40, 50],
53
+ nested: {
54
+ 'field.with.dots': 'nestedValue',
55
+ regular: 'regularValue',
56
+ },
57
+ };
58
+ extractor = new TestExtractor(testData);
59
+ evaluatorMap = new Map([['Context.', extractor]]);
60
+ });
61
+
62
+ describe('Basic bracket notation with dotted keys', () => {
63
+ test('should access property with double quotes', () => {
64
+ const result = new ExpressionEvaluator('Context.obj["mail.props.port"]').evaluate(
65
+ evaluatorMap,
66
+ );
67
+ expect(result).toBe(587);
68
+ });
69
+
70
+ test('should access property with single quotes', () => {
71
+ const result = new ExpressionEvaluator("Context.obj['mail.props.host']").evaluate(
72
+ evaluatorMap,
73
+ );
74
+ expect(result).toBe('smtp.example.com');
75
+ });
76
+
77
+ test('should access nested property with dotted key', () => {
78
+ const result = new ExpressionEvaluator(
79
+ "Context.nested['field.with.dots']",
80
+ ).evaluate(evaluatorMap);
81
+ expect(result).toBe('nestedValue');
82
+ });
83
+ });
84
+
85
+ describe('Comparison operators with bracket notation', () => {
86
+ test('should work with equality operator (=)', () => {
87
+ const result = new ExpressionEvaluator(
88
+ 'Context.obj["mail.props.port"] = 587',
89
+ ).evaluate(evaluatorMap);
90
+ expect(result).toBe(true);
91
+ });
92
+
93
+ test('should work with not equal operator (!=)', () => {
94
+ const result = new ExpressionEvaluator(
95
+ 'Context.obj["mail.props.port"] != 500',
96
+ ).evaluate(evaluatorMap);
97
+ expect(result).toBe(true);
98
+ });
99
+
100
+ test('should work with greater than operator (>)', () => {
101
+ const result = new ExpressionEvaluator(
102
+ 'Context.obj["mail.props.port"] > 500',
103
+ ).evaluate(evaluatorMap);
104
+ expect(result).toBe(true);
105
+ });
106
+
107
+ test('should work with greater than or equal operator (>=)', () => {
108
+ const result = new ExpressionEvaluator(
109
+ 'Context.obj["mail.props.port"] >= 587',
110
+ ).evaluate(evaluatorMap);
111
+ expect(result).toBe(true);
112
+ });
113
+
114
+ test('should work with less than operator (<)', () => {
115
+ const result = new ExpressionEvaluator(
116
+ 'Context.obj["mail.props.port"] < 600',
117
+ ).evaluate(evaluatorMap);
118
+ expect(result).toBe(true);
119
+ });
120
+
121
+ test('should work with less than or equal operator (<=)', () => {
122
+ const result = new ExpressionEvaluator(
123
+ 'Context.obj["mail.props.port"] <= 587',
124
+ ).evaluate(evaluatorMap);
125
+ expect(result).toBe(true);
126
+ });
127
+ });
128
+
129
+ describe('Arithmetic operators with bracket notation', () => {
130
+ test('should work with addition', () => {
131
+ const result = new ExpressionEvaluator(
132
+ 'Context.obj["mail.props.port"] + 13',
133
+ ).evaluate(evaluatorMap);
134
+ expect(result).toBe(600);
135
+ });
136
+
137
+ test('should work with subtraction', () => {
138
+ const result = new ExpressionEvaluator(
139
+ 'Context.obj["mail.props.port"] - 87',
140
+ ).evaluate(evaluatorMap);
141
+ expect(result).toBe(500);
142
+ });
143
+
144
+ test('should work with multiplication', () => {
145
+ const result = new ExpressionEvaluator('Context.obj["count"] * 2').evaluate(
146
+ evaluatorMap,
147
+ );
148
+ expect(result).toBe(200);
149
+ });
150
+
151
+ test('should work with division', () => {
152
+ const result = new ExpressionEvaluator('Context.obj["count"] / 4').evaluate(
153
+ evaluatorMap,
154
+ );
155
+ expect(result).toBe(25);
156
+ });
157
+ });
158
+
159
+ describe('Ternary operator with bracket notation', () => {
160
+ test('should work with ternary operator', () => {
161
+ const result = new ExpressionEvaluator(
162
+ 'Context.obj["mail.props.port"] > 500 ? "high" : "low"',
163
+ ).evaluate(evaluatorMap);
164
+ expect(result).toBe('high');
165
+ });
166
+
167
+ test('should return false branch of ternary', () => {
168
+ const result = new ExpressionEvaluator(
169
+ 'Context.obj["mail.props.port"] < 500 ? "high" : "low"',
170
+ ).evaluate(evaluatorMap);
171
+ expect(result).toBe('low');
172
+ });
173
+ });
174
+
175
+ describe('Logical operators with bracket notation', () => {
176
+ test('should work with logical AND', () => {
177
+ const result = new ExpressionEvaluator(
178
+ 'Context.obj["mail.props.port"] > 500 and Context.obj["count"] = 100',
179
+ ).evaluate(evaluatorMap);
180
+ expect(result).toBe(true);
181
+ });
182
+
183
+ test('should work with logical OR', () => {
184
+ const result = new ExpressionEvaluator(
185
+ 'Context.obj["mail.props.port"] < 500 or Context.obj["count"] = 100',
186
+ ).evaluate(evaluatorMap);
187
+ expect(result).toBe(true);
188
+ });
189
+
190
+ test('should work with logical NOT', () => {
191
+ const result = new ExpressionEvaluator(
192
+ 'not Context.obj["mail.props.port"] < 500',
193
+ ).evaluate(evaluatorMap);
194
+ expect(result).toBe(true);
195
+ });
196
+ });
197
+
198
+ describe('Mixed bracket and dot notation', () => {
199
+ test('should work with bracket notation followed by dot notation', () => {
200
+ testData.obj['mail.props.port'] = { subfield: 'subvalue' };
201
+ const result = new ExpressionEvaluator(
202
+ 'Context.obj["mail.props.port"].subfield',
203
+ ).evaluate(evaluatorMap);
204
+ expect(result).toBe('subvalue');
205
+ });
206
+
207
+ test('should work with dot notation followed by bracket notation', () => {
208
+ const result = new ExpressionEvaluator(
209
+ "Context.nested['field.with.dots']",
210
+ ).evaluate(evaluatorMap);
211
+ expect(result).toBe('nestedValue');
212
+ });
213
+ });
214
+
215
+ describe('Array bracket notation (pre-existing functionality)', () => {
216
+ test('should work with array index access', () => {
217
+ const result = new ExpressionEvaluator('Context.arr[0]').evaluate(evaluatorMap);
218
+ expect(result).toBe(10);
219
+ });
220
+
221
+ test('should work with array index and comparison', () => {
222
+ const result = new ExpressionEvaluator('Context.arr[0] = 10').evaluate(
223
+ evaluatorMap,
224
+ );
225
+ expect(result).toBe(true);
226
+ });
227
+
228
+ test('should work with array index and arithmetic', () => {
229
+ const result = new ExpressionEvaluator('Context.arr[1] + Context.arr[2]').evaluate(
230
+ evaluatorMap,
231
+ );
232
+ expect(result).toBe(50);
233
+ });
234
+ });
235
+
236
+ describe('Complex expressions', () => {
237
+ test('should work with multiple bracket notations in one expression', () => {
238
+ const result = new ExpressionEvaluator(
239
+ 'Context.obj["mail.props.port"] + Context.obj["count"]',
240
+ ).evaluate(evaluatorMap);
241
+ expect(result).toBe(687);
242
+ });
243
+
244
+ test('should work with bracket notation in nested expression', () => {
245
+ const result = new ExpressionEvaluator(
246
+ '(Context.obj["mail.props.port"] > 500) and (Context.obj["count"] < 200)',
247
+ ).evaluate(evaluatorMap);
248
+ expect(result).toBe(true);
249
+ });
250
+
251
+ test('should work with string concatenation', () => {
252
+ const result = new ExpressionEvaluator(
253
+ 'Context.obj["mail.props.host"] + ":587"',
254
+ ).evaluate(evaluatorMap);
255
+ expect(result).toBe('smtp.example.com:587');
256
+ });
257
+ });
258
+
259
+ describe('Edge cases', () => {
260
+ test('should handle property with multiple dots', () => {
261
+ testData.obj['a.b.c.d'] = 'deepValue';
262
+ const result = new ExpressionEvaluator('Context.obj["a.b.c.d"]').evaluate(
263
+ evaluatorMap,
264
+ );
265
+ expect(result).toBe('deepValue');
266
+ });
267
+
268
+ test('should handle empty string key (though unusual)', () => {
269
+ testData.obj[''] = 'emptyKey';
270
+ const result = new ExpressionEvaluator('Context.obj[""]').evaluate(evaluatorMap);
271
+ expect(result).toBe('emptyKey');
272
+ });
273
+
274
+ test('should handle key with special characters', () => {
275
+ testData.obj['key@#$%'] = 'specialValue';
276
+ const result = new ExpressionEvaluator('Context.obj["key@#$%"]').evaluate(
277
+ evaluatorMap,
278
+ );
279
+ expect(result).toBe('specialValue');
280
+ });
281
+ });
282
+ });
@@ -37,3 +37,31 @@ test('Expression Test', () => {
37
37
  opInTheName = new Expression('Store.a.b.corStore.c.d.x');
38
38
  expect(opInTheName.toString()).toBe('(Store.(a.(b.(corStore.(c.(d.x))))))');
39
39
  });
40
+
41
+ test('Expression with bracket notation and quoted keys', () => {
42
+ // Test bracket notation with quoted keys containing dots
43
+ let expr = new Expression('Context.obj["mail.props.port"]');
44
+ expect(expr.toString()).toBe('(Context.(obj["mail.props.port"]))');
45
+
46
+ // Test with single quotes
47
+ expr = new Expression("Context.obj['api.key.secret']");
48
+ expect(expr.toString()).toBe("(Context.(obj['api.key.secret']))");
49
+
50
+ // Test with comparison operators
51
+ expr = new Expression('Context.obj["mail.props.port"] = 587');
52
+ expect(expr.toString()).toBe('((Context.(obj["mail.props.port"]))=587)');
53
+
54
+ expr = new Expression('Context.obj["mail.props.port"] != 500');
55
+ expect(expr.toString()).toBe('((Context.(obj["mail.props.port"]))!=500)');
56
+
57
+ expr = new Expression('Context.obj["mail.props.port"] > 500');
58
+ expect(expr.toString()).toBe('((Context.(obj["mail.props.port"]))>500)');
59
+
60
+ // Test with ternary operator
61
+ expr = new Expression('Context.obj["mail.props.port"] > 500 ? "high" : "low"');
62
+ expect(expr.toString()).toBe('(((Context.(obj["mail.props.port"]))>500)?"high":"low")');
63
+
64
+ // Test mix of bracket and dot notation
65
+ expr = new Expression('Context.obj["mail.props.port"].value');
66
+ expect(expr.toString()).toBe('(Context.(obj["mail.props.port"].value))');
67
+ });