@flusys/nestjs-form-builder 6.0.2 → 6.1.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.
@@ -0,0 +1,1205 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _computedfieldutils = require("./computed-field.utils");
6
+ function directField(overrides = {}) {
7
+ return {
8
+ id: 'cf-1',
9
+ name: 'Computed',
10
+ key: 'total',
11
+ valueType: 'number',
12
+ rules: [
13
+ {
14
+ id: 'rule-1',
15
+ computation: {
16
+ type: 'direct',
17
+ config: {
18
+ type: 'direct',
19
+ value: 42
20
+ }
21
+ }
22
+ }
23
+ ],
24
+ ...overrides
25
+ };
26
+ }
27
+ describe('calculateComputedFields', ()=>{
28
+ it('returns an empty object when there are no computed fields', ()=>{
29
+ expect((0, _computedfieldutils.calculateComputedFields)({}, undefined)).toEqual({});
30
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [])).toEqual({});
31
+ });
32
+ it('keys the result by the field.key property, not field.id', ()=>{
33
+ const field = directField({
34
+ id: 'cf-id',
35
+ key: 'my_key'
36
+ });
37
+ const result = (0, _computedfieldutils.calculateComputedFields)({}, [
38
+ field
39
+ ]);
40
+ expect(result).toEqual({
41
+ my_key: 42
42
+ });
43
+ });
44
+ it('evaluates multiple computed fields independently', ()=>{
45
+ const fields = [
46
+ directField({
47
+ key: 'a'
48
+ }),
49
+ directField({
50
+ key: 'b',
51
+ rules: [
52
+ {
53
+ id: 'r2',
54
+ computation: {
55
+ type: 'direct',
56
+ config: {
57
+ type: 'direct',
58
+ value: 'hello'
59
+ }
60
+ }
61
+ }
62
+ ],
63
+ valueType: 'string'
64
+ })
65
+ ];
66
+ const result = (0, _computedfieldutils.calculateComputedFields)({}, fields);
67
+ expect(result).toEqual({
68
+ a: 42,
69
+ b: 'hello'
70
+ });
71
+ });
72
+ // ==========================================================================
73
+ // Rule matching / condition evaluation
74
+ // ==========================================================================
75
+ describe('condition evaluation', ()=>{
76
+ it('uses the first matching rule and ignores subsequent rules', ()=>{
77
+ const field = {
78
+ id: 'cf-1',
79
+ name: 'Total',
80
+ key: 'total',
81
+ valueType: 'string',
82
+ rules: [
83
+ {
84
+ id: 'rule-1',
85
+ condition: {
86
+ operator: 'AND',
87
+ conditions: [
88
+ {
89
+ fieldId: 'x',
90
+ comparison: 'equals',
91
+ value: 1
92
+ }
93
+ ]
94
+ },
95
+ computation: {
96
+ type: 'direct',
97
+ config: {
98
+ type: 'direct',
99
+ value: 'first'
100
+ }
101
+ }
102
+ },
103
+ {
104
+ id: 'rule-2',
105
+ computation: {
106
+ type: 'direct',
107
+ config: {
108
+ type: 'direct',
109
+ value: 'second'
110
+ }
111
+ }
112
+ }
113
+ ]
114
+ };
115
+ const result = (0, _computedfieldutils.calculateComputedFields)({
116
+ x: 1
117
+ }, [
118
+ field
119
+ ]);
120
+ expect(result.total).toBe('first');
121
+ });
122
+ it('falls through to the default value when no rule matches', ()=>{
123
+ const field = {
124
+ id: 'cf-1',
125
+ name: 'Total',
126
+ key: 'total',
127
+ valueType: 'string',
128
+ defaultValue: 'fallback',
129
+ rules: [
130
+ {
131
+ id: 'rule-1',
132
+ condition: {
133
+ operator: 'AND',
134
+ conditions: [
135
+ {
136
+ fieldId: 'x',
137
+ comparison: 'equals',
138
+ value: 1
139
+ }
140
+ ]
141
+ },
142
+ computation: {
143
+ type: 'direct',
144
+ config: {
145
+ type: 'direct',
146
+ value: 'matched'
147
+ }
148
+ }
149
+ }
150
+ ]
151
+ };
152
+ const result = (0, _computedfieldutils.calculateComputedFields)({
153
+ x: 2
154
+ }, [
155
+ field
156
+ ]);
157
+ expect(result.total).toBe('fallback');
158
+ });
159
+ it('returns null when no rule matches and no default value is set', ()=>{
160
+ const field = {
161
+ id: 'cf-1',
162
+ name: 'Total',
163
+ key: 'total',
164
+ valueType: 'string',
165
+ rules: [
166
+ {
167
+ id: 'rule-1',
168
+ condition: {
169
+ operator: 'AND',
170
+ conditions: [
171
+ {
172
+ fieldId: 'x',
173
+ comparison: 'equals',
174
+ value: 1
175
+ }
176
+ ]
177
+ },
178
+ computation: {
179
+ type: 'direct',
180
+ config: {
181
+ type: 'direct',
182
+ value: 'matched'
183
+ }
184
+ }
185
+ }
186
+ ]
187
+ };
188
+ const result = (0, _computedfieldutils.calculateComputedFields)({
189
+ x: 2
190
+ }, [
191
+ field
192
+ ]);
193
+ expect(result.total).toBeNull();
194
+ });
195
+ it('treats a missing/empty condition group as always-true', ()=>{
196
+ const field = directField({
197
+ key: 'total'
198
+ });
199
+ delete field.rules[0].condition;
200
+ const result = (0, _computedfieldutils.calculateComputedFields)({}, [
201
+ field
202
+ ]);
203
+ expect(result.total).toBe(42);
204
+ });
205
+ it('AND operator requires all conditions to match', ()=>{
206
+ const field = {
207
+ id: 'cf-1',
208
+ name: 'Total',
209
+ key: 'total',
210
+ valueType: 'string',
211
+ defaultValue: 'no-match',
212
+ rules: [
213
+ {
214
+ id: 'rule-1',
215
+ condition: {
216
+ operator: 'AND',
217
+ conditions: [
218
+ {
219
+ fieldId: 'a',
220
+ comparison: 'equals',
221
+ value: 1
222
+ },
223
+ {
224
+ fieldId: 'b',
225
+ comparison: 'equals',
226
+ value: 2
227
+ }
228
+ ]
229
+ },
230
+ computation: {
231
+ type: 'direct',
232
+ config: {
233
+ type: 'direct',
234
+ value: 'matched'
235
+ }
236
+ }
237
+ }
238
+ ]
239
+ };
240
+ expect((0, _computedfieldutils.calculateComputedFields)({
241
+ a: 1,
242
+ b: 2
243
+ }, [
244
+ field
245
+ ]).total).toBe('matched');
246
+ expect((0, _computedfieldutils.calculateComputedFields)({
247
+ a: 1,
248
+ b: 99
249
+ }, [
250
+ field
251
+ ]).total).toBe('no-match');
252
+ });
253
+ it('OR operator matches when at least one condition matches', ()=>{
254
+ const field = {
255
+ id: 'cf-1',
256
+ name: 'Total',
257
+ key: 'total',
258
+ valueType: 'string',
259
+ defaultValue: 'no-match',
260
+ rules: [
261
+ {
262
+ id: 'rule-1',
263
+ condition: {
264
+ operator: 'OR',
265
+ conditions: [
266
+ {
267
+ fieldId: 'a',
268
+ comparison: 'equals',
269
+ value: 1
270
+ },
271
+ {
272
+ fieldId: 'b',
273
+ comparison: 'equals',
274
+ value: 2
275
+ }
276
+ ]
277
+ },
278
+ computation: {
279
+ type: 'direct',
280
+ config: {
281
+ type: 'direct',
282
+ value: 'matched'
283
+ }
284
+ }
285
+ }
286
+ ]
287
+ };
288
+ expect((0, _computedfieldutils.calculateComputedFields)({
289
+ a: 1,
290
+ b: 99
291
+ }, [
292
+ field
293
+ ]).total).toBe('matched');
294
+ expect((0, _computedfieldutils.calculateComputedFields)({
295
+ a: 99,
296
+ b: 99
297
+ }, [
298
+ field
299
+ ]).total).toBe('no-match');
300
+ });
301
+ it('normalizes lower-case / unknown operator strings to AND by default', ()=>{
302
+ const field = {
303
+ id: 'cf-1',
304
+ name: 'Total',
305
+ key: 'total',
306
+ valueType: 'string',
307
+ defaultValue: 'no-match',
308
+ rules: [
309
+ {
310
+ id: 'rule-1',
311
+ condition: {
312
+ operator: 'unknown-operator',
313
+ conditions: [
314
+ {
315
+ fieldId: 'a',
316
+ comparison: 'equals',
317
+ value: 1
318
+ },
319
+ {
320
+ fieldId: 'b',
321
+ comparison: 'equals',
322
+ value: 2
323
+ }
324
+ ]
325
+ },
326
+ computation: {
327
+ type: 'direct',
328
+ config: {
329
+ type: 'direct',
330
+ value: 'matched'
331
+ }
332
+ }
333
+ }
334
+ ]
335
+ };
336
+ // Falls back to AND semantics, so a partial match should NOT trigger the rule.
337
+ expect((0, _computedfieldutils.calculateComputedFields)({
338
+ a: 1,
339
+ b: 99
340
+ }, [
341
+ field
342
+ ]).total).toBe('no-match');
343
+ });
344
+ it('normalizes lower-case "or" to OR semantics', ()=>{
345
+ const field = {
346
+ id: 'cf-1',
347
+ name: 'Total',
348
+ key: 'total',
349
+ valueType: 'string',
350
+ defaultValue: 'no-match',
351
+ rules: [
352
+ {
353
+ id: 'rule-1',
354
+ condition: {
355
+ operator: 'or',
356
+ conditions: [
357
+ {
358
+ fieldId: 'a',
359
+ comparison: 'equals',
360
+ value: 1
361
+ }
362
+ ]
363
+ },
364
+ computation: {
365
+ type: 'direct',
366
+ config: {
367
+ type: 'direct',
368
+ value: 'matched'
369
+ }
370
+ }
371
+ }
372
+ ]
373
+ };
374
+ expect((0, _computedfieldutils.calculateComputedFields)({
375
+ a: 1
376
+ }, [
377
+ field
378
+ ]).total).toBe('matched');
379
+ });
380
+ });
381
+ // ==========================================================================
382
+ // Comparison operators
383
+ // ==========================================================================
384
+ describe('comparison operators', ()=>{
385
+ function withComparison(comparison, value) {
386
+ const field = {
387
+ id: 'cf-1',
388
+ name: 'Total',
389
+ key: 'total',
390
+ valueType: 'string',
391
+ defaultValue: 'no-match',
392
+ rules: [
393
+ {
394
+ id: 'rule-1',
395
+ condition: {
396
+ operator: 'AND',
397
+ conditions: [
398
+ {
399
+ fieldId: 'x',
400
+ comparison,
401
+ value
402
+ }
403
+ ]
404
+ },
405
+ computation: {
406
+ type: 'direct',
407
+ config: {
408
+ type: 'direct',
409
+ value: 'matched'
410
+ }
411
+ }
412
+ }
413
+ ]
414
+ };
415
+ return field;
416
+ }
417
+ it('equals matches by strict or stringified equality', ()=>{
418
+ const field = withComparison('equals', '5');
419
+ expect((0, _computedfieldutils.calculateComputedFields)({
420
+ x: 5
421
+ }, [
422
+ field
423
+ ]).total).toBe('matched');
424
+ expect((0, _computedfieldutils.calculateComputedFields)({
425
+ x: 6
426
+ }, [
427
+ field
428
+ ]).total).toBe('no-match');
429
+ });
430
+ it('not_equals is the inverse of equals', ()=>{
431
+ const field = withComparison('not_equals', '5');
432
+ expect((0, _computedfieldutils.calculateComputedFields)({
433
+ x: 6
434
+ }, [
435
+ field
436
+ ]).total).toBe('matched');
437
+ expect((0, _computedfieldutils.calculateComputedFields)({
438
+ x: 5
439
+ }, [
440
+ field
441
+ ]).total).toBe('no-match');
442
+ expect((0, _computedfieldutils.calculateComputedFields)({
443
+ x: '5'
444
+ }, [
445
+ field
446
+ ]).total).toBe('no-match');
447
+ });
448
+ it('is_empty treats null, undefined, empty string and empty array as empty', ()=>{
449
+ const field = withComparison('is_empty', null);
450
+ expect((0, _computedfieldutils.calculateComputedFields)({
451
+ x: null
452
+ }, [
453
+ field
454
+ ]).total).toBe('matched');
455
+ expect((0, _computedfieldutils.calculateComputedFields)({
456
+ x: undefined
457
+ }, [
458
+ field
459
+ ]).total).toBe('matched');
460
+ expect((0, _computedfieldutils.calculateComputedFields)({
461
+ x: ''
462
+ }, [
463
+ field
464
+ ]).total).toBe('matched');
465
+ expect((0, _computedfieldutils.calculateComputedFields)({
466
+ x: []
467
+ }, [
468
+ field
469
+ ]).total).toBe('matched');
470
+ expect((0, _computedfieldutils.calculateComputedFields)({
471
+ x: 0
472
+ }, [
473
+ field
474
+ ]).total).toBe('no-match');
475
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
476
+ field
477
+ ]).total).toBe('matched');
478
+ });
479
+ it('is_not_empty is the inverse of is_empty', ()=>{
480
+ const field = withComparison('is_not_empty', null);
481
+ expect((0, _computedfieldutils.calculateComputedFields)({
482
+ x: 'value'
483
+ }, [
484
+ field
485
+ ]).total).toBe('matched');
486
+ expect((0, _computedfieldutils.calculateComputedFields)({
487
+ x: ''
488
+ }, [
489
+ field
490
+ ]).total).toBe('no-match');
491
+ });
492
+ it('contains / not_contains perform substring checks', ()=>{
493
+ const contains = withComparison('contains', 'ell');
494
+ expect((0, _computedfieldutils.calculateComputedFields)({
495
+ x: 'hello'
496
+ }, [
497
+ contains
498
+ ]).total).toBe('matched');
499
+ expect((0, _computedfieldutils.calculateComputedFields)({
500
+ x: 'world'
501
+ }, [
502
+ contains
503
+ ]).total).toBe('no-match');
504
+ const notContains = withComparison('not_contains', 'ell');
505
+ expect((0, _computedfieldutils.calculateComputedFields)({
506
+ x: 'world'
507
+ }, [
508
+ notContains
509
+ ]).total).toBe('matched');
510
+ expect((0, _computedfieldutils.calculateComputedFields)({
511
+ x: 'hello'
512
+ }, [
513
+ notContains
514
+ ]).total).toBe('no-match');
515
+ });
516
+ it('starts_with / ends_with perform prefix/suffix checks', ()=>{
517
+ const startsWith = withComparison('starts_with', 'hel');
518
+ expect((0, _computedfieldutils.calculateComputedFields)({
519
+ x: 'hello'
520
+ }, [
521
+ startsWith
522
+ ]).total).toBe('matched');
523
+ expect((0, _computedfieldutils.calculateComputedFields)({
524
+ x: 'shell'
525
+ }, [
526
+ startsWith
527
+ ]).total).toBe('no-match');
528
+ const endsWith = withComparison('ends_with', 'llo');
529
+ expect((0, _computedfieldutils.calculateComputedFields)({
530
+ x: 'hello'
531
+ }, [
532
+ endsWith
533
+ ]).total).toBe('matched');
534
+ expect((0, _computedfieldutils.calculateComputedFields)({
535
+ x: 'llothing'
536
+ }, [
537
+ endsWith
538
+ ]).total).toBe('no-match');
539
+ });
540
+ it('numeric comparisons coerce operands with Number()', ()=>{
541
+ expect((0, _computedfieldutils.calculateComputedFields)({
542
+ x: 10
543
+ }, [
544
+ withComparison('greater_than', 5)
545
+ ]).total).toBe('matched');
546
+ expect((0, _computedfieldutils.calculateComputedFields)({
547
+ x: 3
548
+ }, [
549
+ withComparison('greater_than', 5)
550
+ ]).total).toBe('no-match');
551
+ expect((0, _computedfieldutils.calculateComputedFields)({
552
+ x: 3
553
+ }, [
554
+ withComparison('less_than', 5)
555
+ ]).total).toBe('matched');
556
+ expect((0, _computedfieldutils.calculateComputedFields)({
557
+ x: '5'
558
+ }, [
559
+ withComparison('greater_or_equal', 5)
560
+ ]).total).toBe('matched');
561
+ expect((0, _computedfieldutils.calculateComputedFields)({
562
+ x: 5
563
+ }, [
564
+ withComparison('less_or_equal', 5)
565
+ ]).total).toBe('matched');
566
+ expect((0, _computedfieldutils.calculateComputedFields)({
567
+ x: 6
568
+ }, [
569
+ withComparison('less_or_equal', 5)
570
+ ]).total).toBe('no-match');
571
+ });
572
+ it('is_before / is_after compare Date values', ()=>{
573
+ const before = withComparison('is_before', '2024-06-01');
574
+ expect((0, _computedfieldutils.calculateComputedFields)({
575
+ x: '2024-01-01'
576
+ }, [
577
+ before
578
+ ]).total).toBe('matched');
579
+ expect((0, _computedfieldutils.calculateComputedFields)({
580
+ x: '2024-12-01'
581
+ }, [
582
+ before
583
+ ]).total).toBe('no-match');
584
+ const after = withComparison('is_after', '2024-06-01');
585
+ expect((0, _computedfieldutils.calculateComputedFields)({
586
+ x: '2024-12-01'
587
+ }, [
588
+ after
589
+ ]).total).toBe('matched');
590
+ expect((0, _computedfieldutils.calculateComputedFields)({
591
+ x: '2024-01-01'
592
+ }, [
593
+ after
594
+ ]).total).toBe('no-match');
595
+ });
596
+ it('is_checked treats true, "true" and 1 as checked', ()=>{
597
+ const field = withComparison('is_checked', null);
598
+ expect((0, _computedfieldutils.calculateComputedFields)({
599
+ x: true
600
+ }, [
601
+ field
602
+ ]).total).toBe('matched');
603
+ expect((0, _computedfieldutils.calculateComputedFields)({
604
+ x: 'true'
605
+ }, [
606
+ field
607
+ ]).total).toBe('matched');
608
+ expect((0, _computedfieldutils.calculateComputedFields)({
609
+ x: 1
610
+ }, [
611
+ field
612
+ ]).total).toBe('matched');
613
+ expect((0, _computedfieldutils.calculateComputedFields)({
614
+ x: false
615
+ }, [
616
+ field
617
+ ]).total).toBe('no-match');
618
+ expect((0, _computedfieldutils.calculateComputedFields)({
619
+ x: 0
620
+ }, [
621
+ field
622
+ ]).total).toBe('no-match');
623
+ });
624
+ it('is_not_checked treats false, "false", 0 and falsy as unchecked', ()=>{
625
+ const field = withComparison('is_not_checked', null);
626
+ expect((0, _computedfieldutils.calculateComputedFields)({
627
+ x: false
628
+ }, [
629
+ field
630
+ ]).total).toBe('matched');
631
+ expect((0, _computedfieldutils.calculateComputedFields)({
632
+ x: 'false'
633
+ }, [
634
+ field
635
+ ]).total).toBe('matched');
636
+ expect((0, _computedfieldutils.calculateComputedFields)({
637
+ x: 0
638
+ }, [
639
+ field
640
+ ]).total).toBe('matched');
641
+ expect((0, _computedfieldutils.calculateComputedFields)({
642
+ x: undefined
643
+ }, [
644
+ field
645
+ ]).total).toBe('matched');
646
+ expect((0, _computedfieldutils.calculateComputedFields)({
647
+ x: true
648
+ }, [
649
+ field
650
+ ]).total).toBe('no-match');
651
+ });
652
+ it('is_any_of / in match against an array of allowed values', ()=>{
653
+ const field = withComparison('is_any_of', [
654
+ 'a',
655
+ 'b',
656
+ 'c'
657
+ ]);
658
+ expect((0, _computedfieldutils.calculateComputedFields)({
659
+ x: 'b'
660
+ }, [
661
+ field
662
+ ]).total).toBe('matched');
663
+ expect((0, _computedfieldutils.calculateComputedFields)({
664
+ x: 'z'
665
+ }, [
666
+ field
667
+ ]).total).toBe('no-match');
668
+ const inField = withComparison('in', [
669
+ 'a',
670
+ 'b',
671
+ 'c'
672
+ ]);
673
+ expect((0, _computedfieldutils.calculateComputedFields)({
674
+ x: 'a'
675
+ }, [
676
+ inField
677
+ ]).total).toBe('matched');
678
+ });
679
+ it('is_any_of returns false (no match) when compareValue is not an array', ()=>{
680
+ const field = withComparison('is_any_of', 'not-an-array');
681
+ expect((0, _computedfieldutils.calculateComputedFields)({
682
+ x: 'not-an-array'
683
+ }, [
684
+ field
685
+ ]).total).toBe('no-match');
686
+ });
687
+ it('is_none_of / not_in match when value is absent from the array', ()=>{
688
+ const field = withComparison('is_none_of', [
689
+ 'a',
690
+ 'b',
691
+ 'c'
692
+ ]);
693
+ expect((0, _computedfieldutils.calculateComputedFields)({
694
+ x: 'z'
695
+ }, [
696
+ field
697
+ ]).total).toBe('matched');
698
+ expect((0, _computedfieldutils.calculateComputedFields)({
699
+ x: 'a'
700
+ }, [
701
+ field
702
+ ]).total).toBe('no-match');
703
+ const notIn = withComparison('not_in', [
704
+ 'a',
705
+ 'b',
706
+ 'c'
707
+ ]);
708
+ expect((0, _computedfieldutils.calculateComputedFields)({
709
+ x: 'z'
710
+ }, [
711
+ notIn
712
+ ]).total).toBe('matched');
713
+ });
714
+ it('is_none_of treats a non-array compareValue as always matching', ()=>{
715
+ const field = withComparison('is_none_of', 'not-an-array');
716
+ expect((0, _computedfieldutils.calculateComputedFields)({
717
+ x: 'anything'
718
+ }, [
719
+ field
720
+ ]).total).toBe('matched');
721
+ });
722
+ it('returns false for unknown comparison operators', ()=>{
723
+ const field = withComparison('some_unknown_operator', 1);
724
+ expect((0, _computedfieldutils.calculateComputedFields)({
725
+ x: 1
726
+ }, [
727
+ field
728
+ ]).total).toBe('no-match');
729
+ });
730
+ });
731
+ // ==========================================================================
732
+ // Computation types
733
+ // ==========================================================================
734
+ describe('direct computation', ()=>{
735
+ it('coerces the direct value to a number when valueType is number', ()=>{
736
+ const field = directField({
737
+ valueType: 'number'
738
+ });
739
+ field.rules[0].computation = {
740
+ type: 'direct',
741
+ config: {
742
+ type: 'direct',
743
+ value: '42'
744
+ }
745
+ };
746
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
747
+ field
748
+ ]).total).toBe(42);
749
+ });
750
+ it('coerces the direct value to a string when valueType is string', ()=>{
751
+ const field = directField({
752
+ valueType: 'string'
753
+ });
754
+ field.rules[0].computation = {
755
+ type: 'direct',
756
+ config: {
757
+ type: 'direct',
758
+ value: 42
759
+ }
760
+ };
761
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
762
+ field
763
+ ]).total).toBe('42');
764
+ });
765
+ });
766
+ describe('field_reference computation', ()=>{
767
+ it('reads the referenced field value and coerces per valueType', ()=>{
768
+ const field = directField({
769
+ valueType: 'number'
770
+ });
771
+ field.rules[0].computation = {
772
+ type: 'field_reference',
773
+ config: {
774
+ type: 'field_reference',
775
+ fieldId: 'age'
776
+ }
777
+ };
778
+ expect((0, _computedfieldutils.calculateComputedFields)({
779
+ age: '30'
780
+ }, [
781
+ field
782
+ ]).total).toBe(30);
783
+ });
784
+ it('returns null when the referenced field is null or undefined', ()=>{
785
+ const field = directField({
786
+ valueType: 'number'
787
+ });
788
+ field.rules[0].computation = {
789
+ type: 'field_reference',
790
+ config: {
791
+ type: 'field_reference',
792
+ fieldId: 'age'
793
+ }
794
+ };
795
+ expect((0, _computedfieldutils.calculateComputedFields)({
796
+ age: null
797
+ }, [
798
+ field
799
+ ]).total).toBeNull();
800
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
801
+ field
802
+ ]).total).toBeNull();
803
+ });
804
+ it('coerces the referenced field to a string when valueType is string', ()=>{
805
+ const field = directField({
806
+ valueType: 'string'
807
+ });
808
+ field.rules[0].computation = {
809
+ type: 'field_reference',
810
+ config: {
811
+ type: 'field_reference',
812
+ fieldId: 'age'
813
+ }
814
+ };
815
+ expect((0, _computedfieldutils.calculateComputedFields)({
816
+ age: 30
817
+ }, [
818
+ field
819
+ ]).total).toBe('30');
820
+ });
821
+ });
822
+ describe('unknown / malformed computation configs', ()=>{
823
+ it('returns null for an unrecognized computation type', ()=>{
824
+ const field = directField();
825
+ field.rules[0].computation = {
826
+ type: 'not-a-real-type',
827
+ config: {}
828
+ };
829
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
830
+ field
831
+ ]).total).toBeNull();
832
+ });
833
+ it('returns null when computation.type does not match its config shape', ()=>{
834
+ const field = directField();
835
+ // type says "direct" but config is missing the "value" property expected by the type guard
836
+ field.rules[0].computation = {
837
+ type: 'direct',
838
+ config: {
839
+ type: 'field_reference',
840
+ fieldId: 'x'
841
+ }
842
+ };
843
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
844
+ field
845
+ ]).total).toBeNull();
846
+ });
847
+ });
848
+ // ==========================================================================
849
+ // Arithmetic computation
850
+ // ==========================================================================
851
+ describe('arithmetic computation', ()=>{
852
+ function arithmeticField(config) {
853
+ return {
854
+ id: 'cf-1',
855
+ name: 'Total',
856
+ key: 'total',
857
+ valueType: 'number',
858
+ rules: [
859
+ {
860
+ id: 'rule-1',
861
+ computation: {
862
+ type: 'arithmetic',
863
+ config
864
+ }
865
+ }
866
+ ]
867
+ };
868
+ }
869
+ it('sum adds all operand values (fields + constants)', ()=>{
870
+ const field = arithmeticField({
871
+ type: 'arithmetic',
872
+ operation: 'sum',
873
+ operands: [
874
+ {
875
+ type: 'field',
876
+ fieldId: 'a'
877
+ },
878
+ {
879
+ type: 'field',
880
+ fieldId: 'b'
881
+ },
882
+ {
883
+ type: 'constant',
884
+ value: 10
885
+ }
886
+ ]
887
+ });
888
+ expect((0, _computedfieldutils.calculateComputedFields)({
889
+ a: 5,
890
+ b: 3
891
+ }, [
892
+ field
893
+ ]).total).toBe(18);
894
+ });
895
+ it('increment behaves the same as sum', ()=>{
896
+ const field = arithmeticField({
897
+ type: 'arithmetic',
898
+ operation: 'increment',
899
+ operands: [
900
+ {
901
+ type: 'constant',
902
+ value: 1
903
+ },
904
+ {
905
+ type: 'constant',
906
+ value: 2
907
+ }
908
+ ]
909
+ });
910
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
911
+ field
912
+ ]).total).toBe(3);
913
+ });
914
+ it('subtract subtracts subsequent operands from the first', ()=>{
915
+ const field = arithmeticField({
916
+ type: 'arithmetic',
917
+ operation: 'subtract',
918
+ operands: [
919
+ {
920
+ type: 'constant',
921
+ value: 10
922
+ },
923
+ {
924
+ type: 'constant',
925
+ value: 3
926
+ },
927
+ {
928
+ type: 'constant',
929
+ value: 2
930
+ }
931
+ ]
932
+ });
933
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
934
+ field
935
+ ]).total).toBe(5);
936
+ });
937
+ it('decrement behaves the same as subtract', ()=>{
938
+ const field = arithmeticField({
939
+ type: 'arithmetic',
940
+ operation: 'decrement',
941
+ operands: [
942
+ {
943
+ type: 'constant',
944
+ value: 10
945
+ },
946
+ {
947
+ type: 'constant',
948
+ value: 4
949
+ }
950
+ ]
951
+ });
952
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
953
+ field
954
+ ]).total).toBe(6);
955
+ });
956
+ it('multiply multiplies all operand values', ()=>{
957
+ const field = arithmeticField({
958
+ type: 'arithmetic',
959
+ operation: 'multiply',
960
+ operands: [
961
+ {
962
+ type: 'constant',
963
+ value: 2
964
+ },
965
+ {
966
+ type: 'constant',
967
+ value: 3
968
+ },
969
+ {
970
+ type: 'constant',
971
+ value: 4
972
+ }
973
+ ]
974
+ });
975
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
976
+ field
977
+ ]).total).toBe(24);
978
+ });
979
+ it('divide divides the first operand by each subsequent operand in order', ()=>{
980
+ const field = arithmeticField({
981
+ type: 'arithmetic',
982
+ operation: 'divide',
983
+ operands: [
984
+ {
985
+ type: 'constant',
986
+ value: 100
987
+ },
988
+ {
989
+ type: 'constant',
990
+ value: 2
991
+ },
992
+ {
993
+ type: 'constant',
994
+ value: 5
995
+ }
996
+ ]
997
+ });
998
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
999
+ field
1000
+ ]).total).toBe(10);
1001
+ });
1002
+ it('divide safely skips division by zero operands (guards against Infinity/NaN)', ()=>{
1003
+ const field = arithmeticField({
1004
+ type: 'arithmetic',
1005
+ operation: 'divide',
1006
+ operands: [
1007
+ {
1008
+ type: 'constant',
1009
+ value: 100
1010
+ },
1011
+ {
1012
+ type: 'constant',
1013
+ value: 0
1014
+ }
1015
+ ]
1016
+ });
1017
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
1018
+ field
1019
+ ]).total).toBe(100);
1020
+ });
1021
+ it('average computes the mean of all operand values', ()=>{
1022
+ const field = arithmeticField({
1023
+ type: 'arithmetic',
1024
+ operation: 'average',
1025
+ operands: [
1026
+ {
1027
+ type: 'constant',
1028
+ value: 2
1029
+ },
1030
+ {
1031
+ type: 'constant',
1032
+ value: 4
1033
+ },
1034
+ {
1035
+ type: 'constant',
1036
+ value: 6
1037
+ }
1038
+ ]
1039
+ });
1040
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
1041
+ field
1042
+ ]).total).toBe(4);
1043
+ });
1044
+ it('min / max compute the extremes among operand values', ()=>{
1045
+ const operands = [
1046
+ {
1047
+ type: 'constant',
1048
+ value: 5
1049
+ },
1050
+ {
1051
+ type: 'constant',
1052
+ value: 1
1053
+ },
1054
+ {
1055
+ type: 'constant',
1056
+ value: 9
1057
+ }
1058
+ ];
1059
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
1060
+ arithmeticField({
1061
+ type: 'arithmetic',
1062
+ operation: 'min',
1063
+ operands
1064
+ })
1065
+ ]).total).toBe(1);
1066
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
1067
+ arithmeticField({
1068
+ type: 'arithmetic',
1069
+ operation: 'max',
1070
+ operands
1071
+ })
1072
+ ]).total).toBe(9);
1073
+ });
1074
+ it('treats missing field operand values as 0', ()=>{
1075
+ const field = arithmeticField({
1076
+ type: 'arithmetic',
1077
+ operation: 'sum',
1078
+ operands: [
1079
+ {
1080
+ type: 'field',
1081
+ fieldId: 'missing'
1082
+ },
1083
+ {
1084
+ type: 'constant',
1085
+ value: 5
1086
+ }
1087
+ ]
1088
+ });
1089
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
1090
+ field
1091
+ ]).total).toBe(5);
1092
+ });
1093
+ it('treats a constant operand with an undefined value as 0', ()=>{
1094
+ const field = arithmeticField({
1095
+ type: 'arithmetic',
1096
+ operation: 'sum',
1097
+ operands: [
1098
+ {
1099
+ type: 'constant'
1100
+ },
1101
+ {
1102
+ type: 'constant',
1103
+ value: 5
1104
+ }
1105
+ ]
1106
+ });
1107
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
1108
+ field
1109
+ ]).total).toBe(5);
1110
+ });
1111
+ it('returns null for an unknown arithmetic operation', ()=>{
1112
+ const field = arithmeticField({
1113
+ type: 'arithmetic',
1114
+ operation: 'unknown-op',
1115
+ operands: [
1116
+ {
1117
+ type: 'constant',
1118
+ value: 1
1119
+ }
1120
+ ]
1121
+ });
1122
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
1123
+ field
1124
+ ]).total).toBeNull();
1125
+ });
1126
+ it('returns null when operands array is empty', ()=>{
1127
+ const field = arithmeticField({
1128
+ type: 'arithmetic',
1129
+ operation: 'sum',
1130
+ operands: []
1131
+ });
1132
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
1133
+ field
1134
+ ]).total).toBeNull();
1135
+ });
1136
+ it('does not treat an arithmetic config as valid when operands is not an array', ()=>{
1137
+ const field = directField();
1138
+ field.rules[0].computation = {
1139
+ type: 'arithmetic',
1140
+ config: {
1141
+ type: 'arithmetic',
1142
+ operation: 'sum',
1143
+ operands: 'not-an-array'
1144
+ }
1145
+ };
1146
+ // Fails the isArithmeticConfig type guard -> falls through to "unknown computation type" -> null
1147
+ expect((0, _computedfieldutils.calculateComputedFields)({}, [
1148
+ field
1149
+ ]).total).toBeNull();
1150
+ });
1151
+ });
1152
+ // ==========================================================================
1153
+ // Error handling / malformed field defensive fallback
1154
+ // ==========================================================================
1155
+ describe('defensive error handling', ()=>{
1156
+ it('falls back to defaultValue when field.rules is malformed and calculation throws', ()=>{
1157
+ const field = {
1158
+ id: 'cf-1',
1159
+ name: 'Total',
1160
+ key: 'total',
1161
+ valueType: 'number',
1162
+ defaultValue: -1,
1163
+ rules: undefined
1164
+ };
1165
+ const result = (0, _computedfieldutils.calculateComputedFields)({}, [
1166
+ field
1167
+ ]);
1168
+ expect(result.total).toBe(-1);
1169
+ });
1170
+ it('falls back to null when field.rules is malformed and no defaultValue is set', ()=>{
1171
+ const field = {
1172
+ id: 'cf-1',
1173
+ name: 'Total',
1174
+ key: 'total',
1175
+ valueType: 'number',
1176
+ rules: undefined
1177
+ };
1178
+ const result = (0, _computedfieldutils.calculateComputedFields)({}, [
1179
+ field
1180
+ ]);
1181
+ expect(result.total).toBeNull();
1182
+ });
1183
+ it('isolates failures per field — one bad field does not break the others', ()=>{
1184
+ const badField = {
1185
+ id: 'cf-bad',
1186
+ name: 'Bad',
1187
+ key: 'bad',
1188
+ valueType: 'number',
1189
+ defaultValue: 0,
1190
+ rules: undefined
1191
+ };
1192
+ const goodField = directField({
1193
+ key: 'good'
1194
+ });
1195
+ const result = (0, _computedfieldutils.calculateComputedFields)({}, [
1196
+ badField,
1197
+ goodField
1198
+ ]);
1199
+ expect(result).toEqual({
1200
+ bad: 0,
1201
+ good: 42
1202
+ });
1203
+ });
1204
+ });
1205
+ });