@safeaccess/inline 0.1.1 → 0.1.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.
- package/.gitattributes +1 -1
- package/CHANGELOG.md +10 -5
- package/LICENSE +1 -1
- package/README.md +56 -14
- package/dist/accessors/abstract-accessor.d.ts +22 -10
- package/dist/accessors/abstract-accessor.js +21 -8
- package/dist/accessors/abstract-integration-accessor.d.ts +22 -0
- package/dist/accessors/abstract-integration-accessor.js +23 -0
- package/dist/accessors/formats/any-accessor.d.ts +10 -8
- package/dist/accessors/formats/any-accessor.js +9 -8
- package/dist/accessors/formats/array-accessor.d.ts +2 -0
- package/dist/accessors/formats/array-accessor.js +2 -0
- package/dist/accessors/formats/env-accessor.d.ts +2 -0
- package/dist/accessors/formats/env-accessor.js +2 -0
- package/dist/accessors/formats/ini-accessor.d.ts +2 -0
- package/dist/accessors/formats/ini-accessor.js +2 -0
- package/dist/accessors/formats/json-accessor.d.ts +2 -0
- package/dist/accessors/formats/json-accessor.js +2 -0
- package/dist/accessors/formats/ndjson-accessor.d.ts +2 -0
- package/dist/accessors/formats/ndjson-accessor.js +2 -0
- package/dist/accessors/formats/object-accessor.d.ts +2 -0
- package/dist/accessors/formats/object-accessor.js +2 -0
- package/dist/accessors/formats/xml-accessor.d.ts +2 -0
- package/dist/accessors/formats/xml-accessor.js +2 -0
- package/dist/accessors/formats/yaml-accessor.d.ts +3 -1
- package/dist/accessors/formats/yaml-accessor.js +4 -2
- package/dist/cache/simple-path-cache.d.ts +51 -0
- package/dist/cache/simple-path-cache.js +72 -0
- package/dist/contracts/accessors-interface.d.ts +2 -0
- package/dist/contracts/factory-accessors-interface.d.ts +2 -0
- package/dist/contracts/filter-evaluator-interface.d.ts +28 -0
- package/dist/contracts/filter-evaluator-interface.js +1 -0
- package/dist/contracts/parse-integration-interface.d.ts +2 -0
- package/dist/contracts/parser-interface.d.ts +92 -0
- package/dist/contracts/parser-interface.js +1 -0
- package/dist/contracts/path-cache-interface.d.ts +7 -6
- package/dist/contracts/readable-accessors-interface.d.ts +11 -6
- package/dist/contracts/security-guard-interface.d.ts +2 -0
- package/dist/contracts/security-parser-interface.d.ts +2 -0
- package/dist/contracts/validatable-parser-interface.d.ts +59 -0
- package/dist/contracts/validatable-parser-interface.js +1 -0
- package/dist/contracts/writable-accessors-interface.d.ts +5 -0
- package/dist/core/accessor-factory.d.ts +124 -0
- package/dist/core/accessor-factory.js +157 -0
- package/dist/core/dot-notation-parser.d.ts +34 -5
- package/dist/core/dot-notation-parser.js +51 -10
- package/dist/core/inline-builder-accessor.d.ts +82 -0
- package/dist/core/inline-builder-accessor.js +107 -0
- package/dist/exceptions/accessor-exception.d.ts +9 -0
- package/dist/exceptions/accessor-exception.js +9 -0
- package/dist/exceptions/invalid-format-exception.d.ts +5 -0
- package/dist/exceptions/invalid-format-exception.js +5 -0
- package/dist/exceptions/parser-exception.d.ts +4 -0
- package/dist/exceptions/parser-exception.js +4 -0
- package/dist/exceptions/path-not-found-exception.d.ts +4 -0
- package/dist/exceptions/path-not-found-exception.js +4 -0
- package/dist/exceptions/readonly-violation-exception.d.ts +4 -0
- package/dist/exceptions/readonly-violation-exception.js +4 -0
- package/dist/exceptions/security-exception.d.ts +6 -0
- package/dist/exceptions/security-exception.js +6 -0
- package/dist/exceptions/unsupported-type-exception.d.ts +4 -0
- package/dist/exceptions/unsupported-type-exception.js +4 -0
- package/dist/exceptions/yaml-parse-exception.d.ts +4 -0
- package/dist/exceptions/yaml-parse-exception.js +4 -0
- package/dist/index.js +2 -1
- package/dist/inline.d.ts +22 -56
- package/dist/inline.js +39 -111
- package/dist/parser/xml-parser.js +23 -10
- package/dist/parser/yaml-parser.d.ts +54 -7
- package/dist/parser/yaml-parser.js +268 -51
- package/dist/path-query/segment-filter-parser.d.ts +142 -0
- package/dist/path-query/segment-filter-parser.js +384 -0
- package/dist/path-query/segment-parser.d.ts +98 -0
- package/dist/path-query/segment-parser.js +283 -0
- package/dist/path-query/segment-path-resolver.d.ts +149 -0
- package/dist/path-query/segment-path-resolver.js +351 -0
- package/dist/path-query/segment-type.d.ts +85 -0
- package/dist/path-query/segment-type.js +35 -0
- package/dist/security/forbidden-keys.d.ts +2 -2
- package/dist/security/forbidden-keys.js +5 -5
- package/dist/security/security-guard.d.ts +3 -1
- package/dist/security/security-guard.js +5 -2
- package/dist/security/security-parser.d.ts +10 -1
- package/dist/security/security-parser.js +10 -1
- package/dist/type-format.d.ts +2 -0
- package/dist/type-format.js +2 -0
- package/package.json +11 -3
- package/src/accessors/abstract-accessor.ts +23 -19
- package/src/accessors/abstract-integration-accessor.ts +27 -0
- package/src/accessors/formats/any-accessor.ts +11 -11
- package/src/accessors/formats/array-accessor.ts +2 -0
- package/src/accessors/formats/env-accessor.ts +2 -0
- package/src/accessors/formats/ini-accessor.ts +2 -0
- package/src/accessors/formats/json-accessor.ts +2 -0
- package/src/accessors/formats/ndjson-accessor.ts +2 -0
- package/src/accessors/formats/object-accessor.ts +2 -0
- package/src/accessors/formats/xml-accessor.ts +2 -0
- package/src/accessors/formats/yaml-accessor.ts +4 -2
- package/src/cache/simple-path-cache.ts +77 -0
- package/src/contracts/accessors-interface.ts +2 -0
- package/src/contracts/factory-accessors-interface.ts +2 -0
- package/src/contracts/filter-evaluator-interface.ts +30 -0
- package/src/contracts/parse-integration-interface.ts +2 -0
- package/src/contracts/parser-interface.ts +114 -0
- package/src/contracts/path-cache-interface.ts +8 -6
- package/src/contracts/readable-accessors-interface.ts +11 -6
- package/src/contracts/security-guard-interface.ts +2 -0
- package/src/contracts/security-parser-interface.ts +2 -0
- package/src/contracts/validatable-parser-interface.ts +64 -0
- package/src/contracts/writable-accessors-interface.ts +5 -0
- package/src/core/accessor-factory.ts +173 -0
- package/src/core/dot-notation-parser.ts +74 -11
- package/src/core/inline-builder-accessor.ts +163 -0
- package/src/exceptions/accessor-exception.ts +9 -0
- package/src/exceptions/invalid-format-exception.ts +5 -0
- package/src/exceptions/parser-exception.ts +4 -0
- package/src/exceptions/path-not-found-exception.ts +4 -0
- package/src/exceptions/readonly-violation-exception.ts +4 -0
- package/src/exceptions/security-exception.ts +6 -0
- package/src/exceptions/unsupported-type-exception.ts +4 -0
- package/src/exceptions/yaml-parse-exception.ts +4 -0
- package/src/index.ts +3 -1
- package/src/inline.ts +42 -120
- package/src/parser/xml-parser.ts +31 -10
- package/src/parser/yaml-parser.ts +310 -45
- package/src/path-query/segment-filter-parser.ts +444 -0
- package/src/path-query/segment-parser.ts +321 -0
- package/src/path-query/segment-path-resolver.ts +521 -0
- package/src/path-query/segment-type.ts +82 -0
- package/src/security/forbidden-keys.ts +5 -5
- package/src/security/security-guard.ts +7 -2
- package/src/security/security-parser.ts +18 -3
- package/src/type-format.ts +2 -0
- package/stryker.config.json +8 -10
- package/tests/accessors/abstract-accessor.test.ts +217 -0
- package/tests/accessors/abstract-integration-accessor.test.ts +37 -0
- package/tests/accessors/formats/any-accessor.test.ts +57 -0
- package/tests/accessors/formats/array-accessor.test.ts +42 -0
- package/tests/accessors/formats/env-accessor.test.ts +103 -0
- package/tests/accessors/formats/ini-accessor.test.ts +186 -0
- package/tests/accessors/{json-accessor.test.ts → formats/json-accessor.test.ts} +6 -6
- package/tests/accessors/formats/ndjson-accessor.test.ts +49 -0
- package/tests/accessors/formats/object-accessor.test.ts +172 -0
- package/tests/accessors/formats/xml-accessor.test.ts +162 -0
- package/tests/accessors/formats/yaml-accessor.test.ts +36 -0
- package/tests/cache/simple-path-cache.test.ts +168 -0
- package/tests/core/accessor-factory.test.ts +157 -0
- package/tests/core/dot-notation-parser-edge-cases.test.ts +415 -0
- package/tests/core/dot-notation-parser.test.ts +0 -288
- package/tests/core/inline-builder-accessor.test.ts +114 -0
- package/tests/exceptions/accessor-exception.test.ts +28 -0
- package/tests/exceptions/invalid-format-exception.test.ts +31 -0
- package/tests/exceptions/path-not-found-exception.test.ts +33 -0
- package/tests/exceptions/readonly-violation-exception.test.ts +35 -0
- package/tests/exceptions/security-exception.test.ts +33 -0
- package/tests/exceptions/unsupported-type-exception.test.ts +33 -0
- package/tests/exceptions/yaml-parse-exception.test.ts +38 -0
- package/tests/mocks/fake-path-cache.ts +4 -3
- package/tests/parity-from.test.ts +118 -0
- package/tests/parity.test.ts +227 -10
- package/tests/parser/xml-parser-mutations.test.ts +579 -0
- package/tests/parser/xml-parser-scanner.test.ts +332 -0
- package/tests/parser/xml-parser.test.ts +10 -334
- package/tests/parser/yaml-parser-mutations.test.ts +750 -0
- package/tests/parser/yaml-parser.test.ts +844 -18
- package/tests/path-query/segment-filter-parser-mutations.test.ts +735 -0
- package/tests/path-query/segment-filter-parser.test.ts +1091 -0
- package/tests/path-query/segment-parser-mutations.test.ts +539 -0
- package/tests/path-query/segment-parser.test.ts +606 -0
- package/tests/path-query/segment-path-resolver-mutations.test.ts +626 -0
- package/tests/path-query/segment-path-resolver.test.ts +1009 -0
- package/tests/security/security-guard-advanced.test.ts +413 -0
- package/tests/security/security-guard-forbidden-keys.test.ts +87 -0
- package/tests/security/security-guard.test.ts +3 -484
- package/tests/security/security-parser.test.ts +18 -14
- package/vitest.config.ts +3 -3
- package/benchmarks/get.bench.ts +0 -26
- package/benchmarks/parse.bench.ts +0 -41
- package/tests/accessors/accessors.test.ts +0 -1017
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { SegmentFilterParser } from '../../src/path-query/segment-filter-parser.js';
|
|
3
|
+
import { SecurityGuard } from '../../src/security/security-guard.js';
|
|
4
|
+
import { InvalidFormatException } from '../../src/exceptions/invalid-format-exception.js';
|
|
5
|
+
import { SecurityException } from '../../src/exceptions/security-exception.js';
|
|
6
|
+
|
|
7
|
+
describe(SegmentFilterParser.name, () => {
|
|
8
|
+
describe(`${SegmentFilterParser.name} > parse`, () => {
|
|
9
|
+
it('parses a simple greater-than condition', () => {
|
|
10
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
11
|
+
|
|
12
|
+
const result = parser.parse('age>18');
|
|
13
|
+
|
|
14
|
+
expect(result.conditions).toHaveLength(1);
|
|
15
|
+
expect(result.conditions[0].field).toBe('age');
|
|
16
|
+
expect(result.conditions[0].operator).toBe('>');
|
|
17
|
+
expect(result.conditions[0].value).toBe(18);
|
|
18
|
+
expect(result.logicals).toHaveLength(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('parses an equality condition with a string value', () => {
|
|
22
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
23
|
+
|
|
24
|
+
const result = parser.parse("name=='Alice'");
|
|
25
|
+
|
|
26
|
+
expect(result.conditions[0].field).toBe('name');
|
|
27
|
+
expect(result.conditions[0].operator).toBe('==');
|
|
28
|
+
expect(result.conditions[0].value).toBe('Alice');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('parses a boolean value (true)', () => {
|
|
32
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
33
|
+
|
|
34
|
+
const result = parser.parse('active==true');
|
|
35
|
+
|
|
36
|
+
expect(result.conditions[0].value).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('parses a boolean value (false)', () => {
|
|
40
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
41
|
+
|
|
42
|
+
const result = parser.parse('active==false');
|
|
43
|
+
|
|
44
|
+
expect(result.conditions[0].value).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('parses an equality condition with a double-quoted string value', () => {
|
|
48
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
49
|
+
|
|
50
|
+
const result = parser.parse('name=="Alice"');
|
|
51
|
+
|
|
52
|
+
expect(result.conditions[0].value).toBe('Alice');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('parses a null value', () => {
|
|
56
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
57
|
+
|
|
58
|
+
const result = parser.parse('field==null');
|
|
59
|
+
|
|
60
|
+
expect(result.conditions[0].value).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('parses a float value', () => {
|
|
64
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
65
|
+
|
|
66
|
+
const result = parser.parse('score>=9.5');
|
|
67
|
+
|
|
68
|
+
expect(result.conditions[0].value).toBe(9.5);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('parses two conditions joined by logical AND (&&)', () => {
|
|
72
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
73
|
+
|
|
74
|
+
const result = parser.parse('age>18 && active==true');
|
|
75
|
+
|
|
76
|
+
expect(result.conditions).toHaveLength(2);
|
|
77
|
+
expect(result.logicals).toEqual(['&&']);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('parses two conditions joined by logical OR (||)', () => {
|
|
81
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
82
|
+
|
|
83
|
+
const result = parser.parse("role=='admin' || role=='moderator'");
|
|
84
|
+
|
|
85
|
+
expect(result.logicals).toEqual(['||']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('parses a starts_with() function call', () => {
|
|
89
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
90
|
+
|
|
91
|
+
const result = parser.parse("starts_with(@.name, 'J')");
|
|
92
|
+
|
|
93
|
+
expect(result.conditions[0].func).toBe('starts_with');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('parses a contains() function call', () => {
|
|
97
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
98
|
+
|
|
99
|
+
const result = parser.parse("contains(@.tags, 'php')");
|
|
100
|
+
|
|
101
|
+
expect(result.conditions[0].func).toBe('contains');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('parses a values() boolean function call', () => {
|
|
105
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
106
|
+
|
|
107
|
+
const result = parser.parse('values(@.tags)');
|
|
108
|
+
|
|
109
|
+
expect(result.conditions[0].func).toBe('values');
|
|
110
|
+
expect(result.conditions[0].operator).toBe('==');
|
|
111
|
+
expect(result.conditions[0].value).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('parses a function with a comparison operator', () => {
|
|
115
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
116
|
+
|
|
117
|
+
const result = parser.parse('values(@.tags)>2');
|
|
118
|
+
|
|
119
|
+
expect(result.conditions[0].func).toBe('values');
|
|
120
|
+
expect(result.conditions[0].operator).toBe('>');
|
|
121
|
+
expect(result.conditions[0].value).toBe(2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('throws InvalidFormatException for a condition without an operator', () => {
|
|
125
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
126
|
+
|
|
127
|
+
expect(() => parser.parse('invalid_no_operator')).toThrow(InvalidFormatException);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe(`${SegmentFilterParser.name} > evaluate`, () => {
|
|
132
|
+
it('returns false when there are no conditions', () => {
|
|
133
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
134
|
+
|
|
135
|
+
const result = parser.evaluate({ age: 30 }, { conditions: [], logicals: [] });
|
|
136
|
+
|
|
137
|
+
expect(result).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns true when the item satisfies a > condition', () => {
|
|
141
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
142
|
+
const expr = parser.parse('age>18');
|
|
143
|
+
|
|
144
|
+
expect(parser.evaluate({ age: 30 }, expr)).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('returns false when the item fails a > condition', () => {
|
|
148
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
149
|
+
const expr = parser.parse('age>18');
|
|
150
|
+
|
|
151
|
+
expect(parser.evaluate({ age: 10 }, expr)).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('returns true for an == condition with string match', () => {
|
|
155
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
156
|
+
const expr = parser.parse("name=='Alice'");
|
|
157
|
+
|
|
158
|
+
expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('returns false for a != condition when values are equal', () => {
|
|
162
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
163
|
+
const expr = parser.parse("status!='open'");
|
|
164
|
+
|
|
165
|
+
expect(parser.evaluate({ status: 'open' }, expr)).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('returns true for a != condition when values differ', () => {
|
|
169
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
170
|
+
const expr = parser.parse("status!='open'");
|
|
171
|
+
|
|
172
|
+
expect(parser.evaluate({ status: 'closed' }, expr)).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('evaluates logical AND as true only when both conditions pass', () => {
|
|
176
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
177
|
+
const expr = parser.parse('age>18 && active==true');
|
|
178
|
+
|
|
179
|
+
expect(parser.evaluate({ age: 30, active: true }, expr)).toBe(true);
|
|
180
|
+
expect(parser.evaluate({ age: 30, active: false }, expr)).toBe(false);
|
|
181
|
+
expect(parser.evaluate({ age: 10, active: true }, expr)).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('evaluates logical OR as true when at least one condition passes', () => {
|
|
185
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
186
|
+
const expr = parser.parse("role=='admin' || role=='moderator'");
|
|
187
|
+
|
|
188
|
+
expect(parser.evaluate({ role: 'admin' }, expr)).toBe(true);
|
|
189
|
+
expect(parser.evaluate({ role: 'moderator' }, expr)).toBe(true);
|
|
190
|
+
expect(parser.evaluate({ role: 'guest' }, expr)).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('evaluates starts_with() returning true when the field starts with prefix', () => {
|
|
194
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
195
|
+
const expr = parser.parse("starts_with(@.name, 'Al')");
|
|
196
|
+
|
|
197
|
+
expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(true);
|
|
198
|
+
expect(parser.evaluate({ name: 'Bob' }, expr)).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('evaluates starts_with() as false for non-string field values', () => {
|
|
202
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
203
|
+
const expr = parser.parse("starts_with(@.score, 'A')");
|
|
204
|
+
|
|
205
|
+
expect(parser.evaluate({ score: 42 }, expr)).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('evaluates contains() returning true when string field contains needle', () => {
|
|
209
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
210
|
+
const expr = parser.parse("contains(@.bio, 'engineer')");
|
|
211
|
+
|
|
212
|
+
expect(parser.evaluate({ bio: 'senior engineer' }, expr)).toBe(true);
|
|
213
|
+
expect(parser.evaluate({ bio: 'designer' }, expr)).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('evaluates contains() on an array field', () => {
|
|
217
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
218
|
+
const expr = parser.parse("contains(@.tags, 'php')");
|
|
219
|
+
|
|
220
|
+
expect(parser.evaluate({ tags: ['php', 'python'] }, expr)).toBe(true);
|
|
221
|
+
expect(parser.evaluate({ tags: ['ruby'] }, expr)).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('evaluates contains() as false for non-string non-array values', () => {
|
|
225
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
226
|
+
const expr = parser.parse("contains(@.count, 'x')");
|
|
227
|
+
|
|
228
|
+
expect(parser.evaluate({ count: 42 }, expr)).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('evaluates values() returning count of array field', () => {
|
|
232
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
233
|
+
const expr = parser.parse('values(@.tags)>1');
|
|
234
|
+
|
|
235
|
+
expect(parser.evaluate({ tags: ['php', 'js'] }, expr)).toBe(true);
|
|
236
|
+
expect(parser.evaluate({ tags: ['php'] }, expr)).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('evaluates values() returning 0 for non-array fields', () => {
|
|
240
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
241
|
+
const expr = parser.parse('values(@.name)>0');
|
|
242
|
+
|
|
243
|
+
expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('evaluates an arithmetic expression in the field', () => {
|
|
247
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
248
|
+
const expr = parser.parse('@.price * @.qty > 100');
|
|
249
|
+
|
|
250
|
+
expect(parser.evaluate({ price: 25, qty: 5 }, expr)).toBe(true);
|
|
251
|
+
expect(parser.evaluate({ price: 10, qty: 5 }, expr)).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('evaluates <= and >= boundary conditions', () => {
|
|
255
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
256
|
+
|
|
257
|
+
const exprLe = parser.parse('age<=30');
|
|
258
|
+
expect(parser.evaluate({ age: 30 }, exprLe)).toBe(true);
|
|
259
|
+
expect(parser.evaluate({ age: 31 }, exprLe)).toBe(false);
|
|
260
|
+
|
|
261
|
+
const exprGe = parser.parse('age>=18');
|
|
262
|
+
expect(parser.evaluate({ age: 18 }, exprGe)).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('throws InvalidFormatException for an unknown function', () => {
|
|
266
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
267
|
+
const expr = parser.parse('unknown_func(@.name)');
|
|
268
|
+
|
|
269
|
+
expect(() => parser.evaluate({ name: 'Alice' }, expr)).toThrow(InvalidFormatException);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('throws SecurityException when a field key is forbidden', () => {
|
|
273
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
274
|
+
const expr = parser.parse('__proto__==true');
|
|
275
|
+
|
|
276
|
+
expect(() =>
|
|
277
|
+
parser.evaluate({ __proto__: true } as Record<string, unknown>, expr),
|
|
278
|
+
).toThrow(SecurityException);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('parses an unquoted non-numeric value as a bare string', () => {
|
|
282
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
283
|
+
const expr = parser.parse('status == active');
|
|
284
|
+
|
|
285
|
+
expect(expr.conditions[0].value).toBe('active');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('evaluates a strict less-than condition', () => {
|
|
289
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
290
|
+
const expr = parser.parse('age < 18');
|
|
291
|
+
|
|
292
|
+
expect(parser.evaluate({ age: 15 }, expr)).toBe(true);
|
|
293
|
+
expect(parser.evaluate({ age: 18 }, expr)).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('returns null from resolveArithmetic for a multi-operator field expression', () => {
|
|
297
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
298
|
+
const expr = parser.parse('@.a + @.b + @.c > 0');
|
|
299
|
+
|
|
300
|
+
expect(parser.evaluate({ a: 1, b: 2, c: 3 }, expr)).toBe(false);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('evaluates arithmetic with a float literal on the left side', () => {
|
|
304
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
305
|
+
const expr = parser.parse('5.5 * @.qty > 2');
|
|
306
|
+
|
|
307
|
+
expect(parser.evaluate({ qty: 1 }, expr)).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('converts a numeric-string field value to a number in arithmetic', () => {
|
|
311
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
312
|
+
const expr = parser.parse('@.price * @.qty > 40');
|
|
313
|
+
|
|
314
|
+
expect(parser.evaluate({ price: '10', qty: '5' }, expr)).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('returns null and evaluates false when an arithmetic field is missing from data', () => {
|
|
318
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
319
|
+
const expr = parser.parse('@.missing * @.qty > 0');
|
|
320
|
+
|
|
321
|
+
expect(parser.evaluate({ qty: 5 }, expr)).toBe(false);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('evaluates arithmetic addition between two field values', () => {
|
|
325
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
326
|
+
const expr = parser.parse('@.a + @.b > 5');
|
|
327
|
+
|
|
328
|
+
expect(parser.evaluate({ a: 3, b: 4 }, expr)).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('evaluates arithmetic subtraction between two field values', () => {
|
|
332
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
333
|
+
const expr = parser.parse('@.a - @.b > 0');
|
|
334
|
+
|
|
335
|
+
expect(parser.evaluate({ a: 5, b: 3 }, expr)).toBe(true);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('evaluates contains() with @ referring to the whole item array', () => {
|
|
339
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
340
|
+
const expr = parser.parse("contains(@, 'x')");
|
|
341
|
+
|
|
342
|
+
expect(parser.evaluate(['x', 'y'] as unknown as Record<string, unknown>, expr)).toBe(
|
|
343
|
+
true,
|
|
344
|
+
);
|
|
345
|
+
expect(parser.evaluate(['a', 'b'] as unknown as Record<string, unknown>, expr)).toBe(
|
|
346
|
+
false,
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('resolves a plain field name (no @. prefix) in an arithmetic expression', () => {
|
|
351
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
352
|
+
const expr = parser.parse('price * 2 > 10');
|
|
353
|
+
|
|
354
|
+
expect(parser.evaluate({ price: 6 }, expr)).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('resolves a dot-separated field path in a filter condition', () => {
|
|
358
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
359
|
+
const expr = parser.parse('user.age >= 18');
|
|
360
|
+
|
|
361
|
+
expect(parser.evaluate({ user: { age: 20 } }, expr)).toBe(true);
|
|
362
|
+
expect(parser.evaluate({ user: { age: 15 } }, expr)).toBe(false);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('returns null for a missing nested field in a dot-separated filter path', () => {
|
|
366
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
367
|
+
const expr = parser.parse('user.age >= 18');
|
|
368
|
+
|
|
369
|
+
expect(parser.evaluate({ other: 'data' }, expr)).toBe(false);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('evaluates arithmetic division between two field values', () => {
|
|
373
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
374
|
+
const expr = parser.parse('@.a / @.b > 2');
|
|
375
|
+
|
|
376
|
+
expect(parser.evaluate({ a: 10, b: 3 }, expr)).toBe(true);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('returns null for arithmetic division by zero', () => {
|
|
380
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
381
|
+
const expr = parser.parse('@.a / @.b > 0');
|
|
382
|
+
|
|
383
|
+
expect(parser.evaluate({ a: 10, b: 0 }, expr)).toBe(false);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('returns false for an unsupported operator in evaluateCondition', () => {
|
|
387
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
388
|
+
const expr = {
|
|
389
|
+
conditions: [{ field: 'x', operator: '~', value: 1 }],
|
|
390
|
+
logicals: [],
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
expect(parser.evaluate({ x: 1 }, expr)).toBe(false);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('evaluates arithmetic with an integer literal on the right side', () => {
|
|
397
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
398
|
+
const expr = parser.parse('@.qty * 2 > 4');
|
|
399
|
+
|
|
400
|
+
expect(parser.evaluate({ qty: 3 }, expr)).toBe(true);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('evaluates a numeric-string field with a decimal in arithmetic', () => {
|
|
404
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
405
|
+
const expr = parser.parse('@.price * @.rate > 10');
|
|
406
|
+
|
|
407
|
+
expect(parser.evaluate({ price: '3.5', rate: '4.0' }, expr)).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('falls back to @ when starts_with funcArgs is empty', () => {
|
|
411
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
412
|
+
const expr = {
|
|
413
|
+
conditions: [
|
|
414
|
+
{
|
|
415
|
+
field: '@',
|
|
416
|
+
operator: '==',
|
|
417
|
+
value: true,
|
|
418
|
+
func: 'starts_with',
|
|
419
|
+
funcArgs: [] as string[],
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
logicals: [],
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
expect(parser.evaluate({ x: 1 }, expr)).toBe(false);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('falls back to @ when contains funcArgs is empty', () => {
|
|
429
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
430
|
+
const expr = {
|
|
431
|
+
conditions: [
|
|
432
|
+
{
|
|
433
|
+
field: '@',
|
|
434
|
+
operator: '==',
|
|
435
|
+
value: true,
|
|
436
|
+
func: 'contains',
|
|
437
|
+
funcArgs: [] as string[],
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
logicals: [],
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
expect(parser.evaluate({ x: 1 }, expr)).toBe(false);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('falls back to @ when values funcArgs is empty', () => {
|
|
447
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
448
|
+
const expr = {
|
|
449
|
+
conditions: [
|
|
450
|
+
{
|
|
451
|
+
field: '@',
|
|
452
|
+
operator: '==',
|
|
453
|
+
value: true,
|
|
454
|
+
func: 'values',
|
|
455
|
+
funcArgs: [] as string[],
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
logicals: [],
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
expect(parser.evaluate({ x: 1 }, expr)).toBe(false);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('falls back to empty funcArgs when funcArgs is undefined', () => {
|
|
465
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
466
|
+
const expr = {
|
|
467
|
+
conditions: [{ field: '@', operator: '==', value: true, func: 'values' }],
|
|
468
|
+
logicals: [],
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
expect(parser.evaluate({ x: 1 }, expr)).toBe(false);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('evaluates starts_with with only one funcArg (no prefix)', () => {
|
|
475
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
476
|
+
const expr = {
|
|
477
|
+
conditions: [
|
|
478
|
+
{
|
|
479
|
+
field: '@.name',
|
|
480
|
+
operator: '==',
|
|
481
|
+
value: true,
|
|
482
|
+
func: 'starts_with',
|
|
483
|
+
funcArgs: ['@.name'],
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
logicals: [],
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(true);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('parses strings inside logical expressions respecting quotes', () => {
|
|
493
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
494
|
+
const expr = parser.parse("name=='a && b'");
|
|
495
|
+
|
|
496
|
+
expect(expr.conditions).toHaveLength(1);
|
|
497
|
+
expect(expr.conditions[0].value).toBe('a && b');
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
describe(`${SegmentFilterParser.name} > mutation boundary tests`, () => {
|
|
502
|
+
it('trims whitespace from tokens in splitLogical before parsing condition', () => {
|
|
503
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
504
|
+
const expr = parser.parse(' age > 18 && active == true ');
|
|
505
|
+
|
|
506
|
+
expect(expr.conditions).toHaveLength(2);
|
|
507
|
+
expect(expr.conditions[0].field).toBe('age');
|
|
508
|
+
expect(expr.conditions[1].field).toBe('active');
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('does not treat single & as logical AND', () => {
|
|
512
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
513
|
+
const expr = parser.parse('x>1 & y>2');
|
|
514
|
+
|
|
515
|
+
expect(expr.conditions).toHaveLength(1);
|
|
516
|
+
expect(expr.conditions[0].field).toBe('x');
|
|
517
|
+
expect(expr.conditions[0].value).toBe('1 & y>2');
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('does not treat single | as logical OR', () => {
|
|
521
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
522
|
+
const expr = parser.parse('x>1 | y>2');
|
|
523
|
+
|
|
524
|
+
expect(expr.conditions).toHaveLength(1);
|
|
525
|
+
expect(expr.conditions[0].field).toBe('x');
|
|
526
|
+
expect(expr.conditions[0].value).toBe('1 | y>2');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('treats & at end of expression as part of token', () => {
|
|
530
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
531
|
+
const expr = parser.parse('x>1&');
|
|
532
|
+
|
|
533
|
+
expect(expr.conditions).toHaveLength(1);
|
|
534
|
+
expect(expr.conditions[0].value).toBe('1&');
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('treats | at end of expression as part of token', () => {
|
|
538
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
539
|
+
const expr = parser.parse('x>1|');
|
|
540
|
+
|
|
541
|
+
expect(expr.conditions).toHaveLength(1);
|
|
542
|
+
expect(expr.conditions[0].value).toBe('1|');
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('handles && where the second & is the last character of the expression', () => {
|
|
546
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
547
|
+
const expr = parser.parse('a>1&&b>2');
|
|
548
|
+
|
|
549
|
+
expect(expr.conditions).toHaveLength(2);
|
|
550
|
+
expect(expr.logicals).toEqual(['&&']);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('handles || where the second | is the last character of the expression', () => {
|
|
554
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
555
|
+
const expr = parser.parse('a>1||b>2');
|
|
556
|
+
|
|
557
|
+
expect(expr.conditions).toHaveLength(2);
|
|
558
|
+
expect(expr.logicals).toEqual(['||']);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('handles & not followed by & (single ampersand in quoted value)', () => {
|
|
562
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
563
|
+
const expr = parser.parse("name=='a&b'");
|
|
564
|
+
|
|
565
|
+
expect(expr.conditions).toHaveLength(1);
|
|
566
|
+
expect(expr.conditions[0].value).toBe('a&b');
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('handles | not followed by | (single pipe in quoted value)', () => {
|
|
570
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
571
|
+
const expr = parser.parse("name=='a|b'");
|
|
572
|
+
|
|
573
|
+
expect(expr.conditions).toHaveLength(1);
|
|
574
|
+
expect(expr.conditions[0].value).toBe('a|b');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('funcCompareMatch regex requires ^ anchor', () => {
|
|
578
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
579
|
+
const expr = parser.parse("starts_with(@.name, 'J') > 0");
|
|
580
|
+
|
|
581
|
+
expect(expr.conditions[0].func).toBe('starts_with');
|
|
582
|
+
expect(expr.conditions[0].operator).toBe('>');
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('funcCompareMatch regex requires $ anchor (rejects trailing text)', () => {
|
|
586
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
587
|
+
const expr = parser.parse("starts_with(@.name, 'J') > 0");
|
|
588
|
+
|
|
589
|
+
expect(expr.conditions[0].func).toBe('starts_with');
|
|
590
|
+
expect(expr.conditions[0].value).toBe(0);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('funcCompareMatch \\s* requires whitespace not \\S* for operator spacing', () => {
|
|
594
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
595
|
+
const expr = parser.parse('values(@.items) >= 3');
|
|
596
|
+
|
|
597
|
+
expect(expr.conditions[0].func).toBe('values');
|
|
598
|
+
expect(expr.conditions[0].operator).toBe('>=');
|
|
599
|
+
expect(expr.conditions[0].value).toBe(3);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('trims rawValue from funcCompareMatch capture group', () => {
|
|
603
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
604
|
+
const expr = parser.parse("values(@.items) > ' hello '");
|
|
605
|
+
|
|
606
|
+
expect(expr.conditions[0].value).toBe(' hello ');
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('trims funcArgs from argsRaw split in funcCompare', () => {
|
|
610
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
611
|
+
const expr = parser.parse("starts_with( @.name , 'J' ) > 0");
|
|
612
|
+
|
|
613
|
+
expect(expr.conditions[0].funcArgs).toEqual(['@.name', "'J'"]);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('funcBoolMatch regex requires ^ anchor', () => {
|
|
617
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
618
|
+
const expr = parser.parse('values(@.tags)');
|
|
619
|
+
|
|
620
|
+
expect(expr.conditions[0].func).toBe('values');
|
|
621
|
+
expect(expr.conditions[0].operator).toBe('==');
|
|
622
|
+
expect(expr.conditions[0].value).toBe(true);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('funcBoolMatch regex requires $ anchor (does not match suffix text)', () => {
|
|
626
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
627
|
+
|
|
628
|
+
expect(() => parser.parse('values(@.tags)x')).toThrow(InvalidFormatException);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('parseValue returns integer for numbers without a decimal', () => {
|
|
632
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
633
|
+
const expr = parser.parse('count==42');
|
|
634
|
+
|
|
635
|
+
expect(expr.conditions[0].value).toBe(42);
|
|
636
|
+
expect(Number.isInteger(expr.conditions[0].value)).toBe(true);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('parseValue returns float for numbers with a decimal', () => {
|
|
640
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
641
|
+
const expr = parser.parse('score==9.5');
|
|
642
|
+
|
|
643
|
+
expect(expr.conditions[0].value).toBe(9.5);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('parseValue uses parseInt for integer strings not parseFloat', () => {
|
|
647
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
648
|
+
const expr = parser.parse('count==007');
|
|
649
|
+
|
|
650
|
+
expect(expr.conditions[0].value).toBe(7);
|
|
651
|
+
expect(Number.isInteger(expr.conditions[0].value)).toBe(true);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('evaluates starts_with correctly, not falling through to contains', () => {
|
|
655
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
656
|
+
const expr = parser.parse("starts_with(@.name, 'A')");
|
|
657
|
+
|
|
658
|
+
expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(true);
|
|
659
|
+
expect(parser.evaluate({ name: 'Bob' }, expr)).toBe(false);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('evaluates starts_with false for value not starting with prefix', () => {
|
|
663
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
664
|
+
const expr = parser.parse("starts_with(@.name, 'Z')");
|
|
665
|
+
|
|
666
|
+
expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(false);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('falls back to @ when funcArgs[0] is undefined in starts_with', () => {
|
|
670
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
671
|
+
const expr = {
|
|
672
|
+
conditions: [
|
|
673
|
+
{
|
|
674
|
+
field: '@',
|
|
675
|
+
operator: '==',
|
|
676
|
+
value: true,
|
|
677
|
+
func: 'starts_with',
|
|
678
|
+
funcArgs: [] as string[],
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
logicals: [],
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
expect(parser.evaluate('hello' as unknown as Record<string, unknown>, expr)).toBe(true);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('falls back to @ when funcArgs[0] is undefined in contains', () => {
|
|
688
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
689
|
+
const expr = {
|
|
690
|
+
conditions: [
|
|
691
|
+
{
|
|
692
|
+
field: '@',
|
|
693
|
+
operator: '==',
|
|
694
|
+
value: true,
|
|
695
|
+
func: 'contains',
|
|
696
|
+
funcArgs: [] as string[],
|
|
697
|
+
},
|
|
698
|
+
],
|
|
699
|
+
logicals: [],
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
expect(parser.evaluate('hello' as unknown as Record<string, unknown>, expr)).toBe(true);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('falls back to @ when funcArgs[0] is undefined in values', () => {
|
|
706
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
707
|
+
const expr = {
|
|
708
|
+
conditions: [
|
|
709
|
+
{
|
|
710
|
+
field: '@',
|
|
711
|
+
operator: '==',
|
|
712
|
+
value: 3,
|
|
713
|
+
func: 'values',
|
|
714
|
+
funcArgs: [] as string[],
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
logicals: [],
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
expect(parser.evaluate([1, 2, 3] as unknown as Record<string, unknown>, expr)).toBe(
|
|
721
|
+
true,
|
|
722
|
+
);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it('trims whitespace from the second funcArg in starts_with', () => {
|
|
726
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
727
|
+
const expr = parser.parse("starts_with(@.name, ' Al ')");
|
|
728
|
+
|
|
729
|
+
expect(parser.evaluate({ name: ' Al ice' }, expr)).toBe(true);
|
|
730
|
+
expect(parser.evaluate({ name: 'Al' }, expr)).toBe(false);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('trims whitespace from the second funcArg in contains', () => {
|
|
734
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
735
|
+
const expr = parser.parse("contains(@.bio, ' eng ')");
|
|
736
|
+
|
|
737
|
+
expect(parser.evaluate({ bio: 'senior eng ineer' }, expr)).toBe(true);
|
|
738
|
+
expect(parser.evaluate({ bio: 'senior engineer' }, expr)).toBe(false);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('contains with missing second funcArg falls back to empty string', () => {
|
|
742
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
743
|
+
const expr = {
|
|
744
|
+
conditions: [
|
|
745
|
+
{
|
|
746
|
+
field: '@.name',
|
|
747
|
+
operator: '==',
|
|
748
|
+
value: true,
|
|
749
|
+
func: 'contains',
|
|
750
|
+
funcArgs: ['@.name'],
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
logicals: [],
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
expect(parser.evaluate({ name: 'hello' }, expr)).toBe(true);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('starts_with with missing second funcArg falls back to empty string', () => {
|
|
760
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
761
|
+
const expr = {
|
|
762
|
+
conditions: [
|
|
763
|
+
{
|
|
764
|
+
field: '@.name',
|
|
765
|
+
operator: '==',
|
|
766
|
+
value: true,
|
|
767
|
+
func: 'starts_with',
|
|
768
|
+
funcArgs: ['@.name'],
|
|
769
|
+
},
|
|
770
|
+
],
|
|
771
|
+
logicals: [],
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(true);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it('starts_with returns false when val is not a string', () => {
|
|
778
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
779
|
+
const expr = {
|
|
780
|
+
conditions: [
|
|
781
|
+
{
|
|
782
|
+
field: '@',
|
|
783
|
+
operator: '==',
|
|
784
|
+
value: true,
|
|
785
|
+
func: 'starts_with',
|
|
786
|
+
funcArgs: ['@', "'x'"],
|
|
787
|
+
},
|
|
788
|
+
],
|
|
789
|
+
logicals: [],
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
expect(parser.evaluate({ a: 1 } as Record<string, unknown>, expr)).toBe(false);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('resolveFilterArg returns item when arg is empty string', () => {
|
|
796
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
797
|
+
const expr = {
|
|
798
|
+
conditions: [
|
|
799
|
+
{ field: '', operator: '==', value: 3, func: 'values', funcArgs: [''] },
|
|
800
|
+
],
|
|
801
|
+
logicals: [],
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
expect(parser.evaluate([1, 2, 3] as unknown as Record<string, unknown>, expr)).toBe(
|
|
805
|
+
true,
|
|
806
|
+
);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('resolveFilterArg returns item when arg is @', () => {
|
|
810
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
811
|
+
const expr = {
|
|
812
|
+
conditions: [
|
|
813
|
+
{ field: '@', operator: '==', value: 3, func: 'values', funcArgs: ['@'] },
|
|
814
|
+
],
|
|
815
|
+
logicals: [],
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
expect(parser.evaluate([1, 2, 3] as unknown as Record<string, unknown>, expr)).toBe(
|
|
819
|
+
true,
|
|
820
|
+
);
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('resolveFilterArg resolves @.field path', () => {
|
|
824
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
825
|
+
const expr = {
|
|
826
|
+
conditions: [
|
|
827
|
+
{
|
|
828
|
+
field: '@.items',
|
|
829
|
+
operator: '==',
|
|
830
|
+
value: 2,
|
|
831
|
+
func: 'values',
|
|
832
|
+
funcArgs: ['@.items'],
|
|
833
|
+
},
|
|
834
|
+
],
|
|
835
|
+
logicals: [],
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
expect(parser.evaluate({ items: [1, 2] }, expr)).toBe(true);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('arithmetic regex requires \\d+ not \\d for multi-digit integers', () => {
|
|
842
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
843
|
+
const expr = parser.parse('@.val + 10 > 5');
|
|
844
|
+
|
|
845
|
+
expect(parser.evaluate({ val: 0 }, expr)).toBe(true);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it('arithmetic regex requires \\d not \\D for digit matching', () => {
|
|
849
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
850
|
+
|
|
851
|
+
const expr = parser.parse('@.val + 5 > 4');
|
|
852
|
+
expect(parser.evaluate({ val: 0 }, expr)).toBe(true);
|
|
853
|
+
|
|
854
|
+
const expr2 = parser.parse('@.val * abc > 0');
|
|
855
|
+
expect(parser.evaluate({ val: 5 }, expr2)).toBe(false);
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it('arithmetic regex treats \\.\\d+ as optional via (?:)?', () => {
|
|
859
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
860
|
+
const expr = parser.parse('@.val + 10 > 9');
|
|
861
|
+
|
|
862
|
+
expect(parser.evaluate({ val: 0 }, expr)).toBe(true);
|
|
863
|
+
|
|
864
|
+
const exprFloat = parser.parse('@.val + 10.5 > 10');
|
|
865
|
+
|
|
866
|
+
expect(parser.evaluate({ val: 0 }, exprFloat)).toBe(true);
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it('arithmetic regex requires \\. not \\D for decimal separator', () => {
|
|
870
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
871
|
+
const expr = parser.parse('@.val + 1.5 > 1');
|
|
872
|
+
|
|
873
|
+
expect(parser.evaluate({ val: 0 }, expr)).toBe(true);
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it('arithmetic regex requires \\d+ after decimal point', () => {
|
|
877
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
878
|
+
const expr = parser.parse('@.val + 10.55 > 10');
|
|
879
|
+
|
|
880
|
+
expect(parser.evaluate({ val: 0 }, expr)).toBe(true);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it("toNumber checks startsWith('@') not endsWith('@') for field tokens", () => {
|
|
884
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
885
|
+
const expr = parser.parse('@.val + 5 > 0');
|
|
886
|
+
|
|
887
|
+
expect(parser.evaluate({ val: 1 }, expr)).toBe(true);
|
|
888
|
+
|
|
889
|
+
const expr2 = parser.parse('abc + 5 > 0');
|
|
890
|
+
expect(parser.evaluate({ abc: 1 }, expr2)).toBe(true);
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it('toNumber uses parseInt for integer tokens not parseFloat', () => {
|
|
894
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
895
|
+
const expr = parser.parse('@.val + 007 > 6');
|
|
896
|
+
|
|
897
|
+
expect(parser.evaluate({ val: 0 }, expr)).toBe(true);
|
|
898
|
+
expect(Number.isInteger(7)).toBe(true);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it('toNumber uses parseInt for integer string values not parseFloat', () => {
|
|
902
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
903
|
+
const expr = parser.parse('@.val * 1 == 42');
|
|
904
|
+
|
|
905
|
+
expect(parser.evaluate({ val: '42' }, expr)).toBe(true);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('toNumber uses parseFloat for decimal string values', () => {
|
|
909
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
910
|
+
const expr = parser.parse('@.val * 1 == 3.14');
|
|
911
|
+
|
|
912
|
+
expect(parser.evaluate({ val: '3.14' }, expr)).toBe(true);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it('evaluates condition when funcArgs is undefined and falls back to empty array', () => {
|
|
916
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
917
|
+
const expr = {
|
|
918
|
+
conditions: [{ field: '@', operator: '==', value: 0, func: 'values' }],
|
|
919
|
+
logicals: [],
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
expect(parser.evaluate({ items: [1, 2] }, expr)).toBe(true);
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it('funcArgs fallback to empty array does not inject a string value', () => {
|
|
926
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
927
|
+
const expr = {
|
|
928
|
+
conditions: [{ field: '@', operator: '==', value: true, func: 'starts_with' }],
|
|
929
|
+
logicals: [],
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
expect(parser.evaluate('hello' as unknown as Record<string, unknown>, expr)).toBe(true);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it('arithmetic regex + quantifier on left side matches multi-char fields', () => {
|
|
936
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
937
|
+
const expr = parser.parse('@.total + 1 > 5');
|
|
938
|
+
|
|
939
|
+
expect(parser.evaluate({ total: 10 }, expr)).toBe(true);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
it('arithmetic regex + quantifier on right side matches multi-char tokens', () => {
|
|
943
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
944
|
+
const expr = parser.parse('@.x + @.total > 5');
|
|
945
|
+
|
|
946
|
+
expect(parser.evaluate({ x: 1, total: 10 }, expr)).toBe(true);
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
it('detects arithmetic even with \\W mutation by testing non-@ token resolution', () => {
|
|
950
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
951
|
+
|
|
952
|
+
const expr = parser.parse('count + 1 > 5');
|
|
953
|
+
expect(parser.evaluate({ count: 10 }, expr)).toBe(true);
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it('resolveField with dot-separated path traverses nested objects', () => {
|
|
957
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
958
|
+
const expr = parser.parse('a.b.c > 1');
|
|
959
|
+
|
|
960
|
+
expect(parser.evaluate({ a: { b: { c: 5 } } }, expr)).toBe(true);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
it('resolveField returns null when intermediate key leads to non-object', () => {
|
|
964
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
965
|
+
const expr = parser.parse('a.b.c > 1');
|
|
966
|
+
|
|
967
|
+
expect(parser.evaluate({ a: { b: 'string' } }, expr)).toBe(false);
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
it('resolveField returns null when intermediate key leads to null', () => {
|
|
971
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
972
|
+
const expr = parser.parse('a.b.c > 1');
|
|
973
|
+
|
|
974
|
+
expect(parser.evaluate({ a: { b: null } }, expr)).toBe(false);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
it('resolveField returns null when intermediate key is missing', () => {
|
|
978
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
979
|
+
const expr = parser.parse('a.b.c > 1');
|
|
980
|
+
|
|
981
|
+
expect(parser.evaluate({ a: {} }, expr)).toBe(false);
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it('resolveField handles single key (no dot)', () => {
|
|
985
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
986
|
+
const expr = parser.parse("name == 'Alice'");
|
|
987
|
+
|
|
988
|
+
expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(true);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
it('resolveField with dot returns null when intermediate value is non-object', () => {
|
|
992
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
993
|
+
const expr = parser.parse('a.b > 0');
|
|
994
|
+
|
|
995
|
+
expect(parser.evaluate({ a: 42 }, expr)).toBe(false);
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
it('resolveField with dot segment includes branch requires typeof check', () => {
|
|
999
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
1000
|
+
const expr = parser.parse('a.b > 0');
|
|
1001
|
+
|
|
1002
|
+
expect(parser.evaluate({ a: 'string' }, expr)).toBe(false);
|
|
1003
|
+
expect(parser.evaluate({ a: { b: 5 } }, expr)).toBe(true);
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
it('parses strings inside logical expressions respecting quotes', () => {
|
|
1007
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
1008
|
+
const expr = parser.parse("name=='a && b'");
|
|
1009
|
+
|
|
1010
|
+
expect(expr.conditions).toHaveLength(1);
|
|
1011
|
+
expect(expr.conditions[0].value).toBe('a && b');
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('trim on token in parse() is needed for function expressions with leading whitespace', () => {
|
|
1015
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
1016
|
+
const expr = parser.parse(" starts_with(@.name, 'J') && contains(@.name, 'o') ");
|
|
1017
|
+
|
|
1018
|
+
expect(expr.conditions).toHaveLength(2);
|
|
1019
|
+
expect(expr.conditions[0].func).toBe('starts_with');
|
|
1020
|
+
expect(expr.conditions[1].func).toBe('contains');
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
it('trim on token in parse() is needed for funcCompare with leading whitespace', () => {
|
|
1024
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
1025
|
+
const expr = parser.parse(' values(@.items) > 2 ');
|
|
1026
|
+
|
|
1027
|
+
expect(expr.conditions).toHaveLength(1);
|
|
1028
|
+
expect(expr.conditions[0].func).toBe('values');
|
|
1029
|
+
expect(expr.conditions[0].operator).toBe('>');
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
it('trim on rawValue in funcCompare is needed for trailing whitespace', () => {
|
|
1033
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
1034
|
+
const expr = parser.parse('values(@.items) > 3 ');
|
|
1035
|
+
|
|
1036
|
+
expect(expr.conditions[0].value).toBe(3);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
it('integer vs float distinction in toNumber via includes(.)', () => {
|
|
1040
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
1041
|
+
const expr = parser.parse('@.count * 2 > 5');
|
|
1042
|
+
|
|
1043
|
+
expect(parser.evaluate({ count: 3 }, expr)).toBe(true);
|
|
1044
|
+
expect(parser.evaluate({ count: 2 }, expr)).toBe(false);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
it('toNumber uses parseInt for integer tokens producing exact int', () => {
|
|
1048
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
1049
|
+
const expr = parser.parse('@.price * 100 == 500');
|
|
1050
|
+
|
|
1051
|
+
expect(parser.evaluate({ price: 5 }, expr)).toBe(true);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it('toNumber converts numeric string val to number for arithmetic', () => {
|
|
1055
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
1056
|
+
const expr = parser.parse('@.a + @.b == 10');
|
|
1057
|
+
|
|
1058
|
+
expect(parser.evaluate({ a: '3', b: '7' }, expr)).toBe(true);
|
|
1059
|
+
expect(parser.evaluate({ a: '3.5', b: '6.5' }, expr)).toBe(true);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
it('toNumber returns null for empty string field value in arithmetic', () => {
|
|
1063
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
1064
|
+
const expr = parser.parse('@.a + @.b == 10');
|
|
1065
|
+
|
|
1066
|
+
expect(parser.evaluate({ a: '', b: '7' }, expr)).toBe(false);
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
it('toNumber val !== empty string check prevents empty string becoming 0', () => {
|
|
1070
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
1071
|
+
const expr = parser.parse('@.val + 1 == 1');
|
|
1072
|
+
|
|
1073
|
+
expect(parser.evaluate({ val: '' }, expr)).toBe(false);
|
|
1074
|
+
expect(parser.evaluate({ val: 0 }, expr)).toBe(true);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it('resolveField dot path typeof check prevents crash on null intermediate', () => {
|
|
1078
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
1079
|
+
const expr = parser.parse('a.b.c == 1');
|
|
1080
|
+
|
|
1081
|
+
expect(parser.evaluate({ a: { b: null } }, expr)).toBe(false);
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
it('resolveField dot path typeof check prevents crash on primitive intermediate', () => {
|
|
1085
|
+
const parser = new SegmentFilterParser(new SecurityGuard());
|
|
1086
|
+
const expr = parser.parse('a.b.c == 1');
|
|
1087
|
+
|
|
1088
|
+
expect(parser.evaluate({ a: { b: 42 } }, expr)).toBe(false);
|
|
1089
|
+
});
|
|
1090
|
+
});
|
|
1091
|
+
});
|