@safeaccess/inline 0.1.1 → 0.1.3
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 +23 -5
- package/LICENSE +1 -1
- package/README.md +79 -21
- package/dist/accessors/abstract-accessor.d.ts +24 -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 +26 -56
- package/dist/inline.js +43 -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 +4 -1
- package/dist/security/security-guard.js +7 -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 +25 -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 +46 -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 +10 -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 +379 -0
- package/tests/parser/xml-parser.test.ts +17 -330
- 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 +8 -479
- 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,142 @@
|
|
|
1
|
+
import type { FilterEvaluatorInterface } from '../contracts/filter-evaluator-interface.js';
|
|
2
|
+
import type { SecurityGuardInterface } from '../contracts/security-guard-interface.js';
|
|
3
|
+
import type { FilterExpression } from './segment-type.js';
|
|
4
|
+
/**
|
|
5
|
+
* Parse and evaluate filter predicate expressions for path queries.
|
|
6
|
+
*
|
|
7
|
+
* Handles `[?expression]` syntax with comparison operators (==, !=, >, <, >=, <=),
|
|
8
|
+
* logical operators (&& and ||), and built-in functions (starts_with, contains, values).
|
|
9
|
+
* Supports arithmetic expressions and nested field access via dot-notation.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*
|
|
13
|
+
* @see FilterEvaluatorInterface Contract this class implements.
|
|
14
|
+
* @see SegmentParser Uses this for filter segment parsing.
|
|
15
|
+
* @see SegmentPathResolver Uses this for filter evaluation during resolution.
|
|
16
|
+
* @see SecurityGuardInterface Guards field access against forbidden keys.
|
|
17
|
+
*/
|
|
18
|
+
export declare class SegmentFilterParser implements FilterEvaluatorInterface {
|
|
19
|
+
private readonly guard;
|
|
20
|
+
/**
|
|
21
|
+
* Create a filter parser with security guard for field validation.
|
|
22
|
+
*
|
|
23
|
+
* @param guard - Key validator for field access.
|
|
24
|
+
*/
|
|
25
|
+
constructor(guard: SecurityGuardInterface);
|
|
26
|
+
/**
|
|
27
|
+
* Parse a filter expression into structured conditions and logical operators.
|
|
28
|
+
*
|
|
29
|
+
* @param expression - Raw filter string (e.g. "age>18 && active==true").
|
|
30
|
+
* @returns Parsed structure with conditions and logical operators.
|
|
31
|
+
*
|
|
32
|
+
* @throws {InvalidFormatException} When the expression syntax is invalid.
|
|
33
|
+
*/
|
|
34
|
+
parse(expression: string): FilterExpression;
|
|
35
|
+
/**
|
|
36
|
+
* Evaluate a parsed filter expression against a data item.
|
|
37
|
+
*
|
|
38
|
+
* @param item - Data item to test.
|
|
39
|
+
* @param expr - Parsed expression.
|
|
40
|
+
* @returns True if the item satisfies all conditions.
|
|
41
|
+
*/
|
|
42
|
+
evaluate(item: Record<string, unknown>, expr: FilterExpression): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Split expression by logical operators (&& and ||), respecting quotes.
|
|
45
|
+
*
|
|
46
|
+
* @param expression - Raw expression string.
|
|
47
|
+
* @returns Tokens and their joining operators.
|
|
48
|
+
*/
|
|
49
|
+
private splitLogical;
|
|
50
|
+
/**
|
|
51
|
+
* Parse a single condition token into a structured object.
|
|
52
|
+
*
|
|
53
|
+
* @param token - Single condition (e.g. "age>18", "starts_with(@.name, 'J')").
|
|
54
|
+
* @returns Parsed condition.
|
|
55
|
+
*
|
|
56
|
+
* @throws {InvalidFormatException} When the condition syntax is invalid.
|
|
57
|
+
*/
|
|
58
|
+
private parseCondition;
|
|
59
|
+
/**
|
|
60
|
+
* Parse a raw value string to its native type.
|
|
61
|
+
*
|
|
62
|
+
* @param raw - Raw value (e.g. "true", "'hello'", "42").
|
|
63
|
+
* @returns Native value.
|
|
64
|
+
*/
|
|
65
|
+
private parseValue;
|
|
66
|
+
/**
|
|
67
|
+
* Parse a non-keyword raw value to number or string.
|
|
68
|
+
*
|
|
69
|
+
* @param raw - Raw value string.
|
|
70
|
+
* @returns Typed value.
|
|
71
|
+
*/
|
|
72
|
+
private parseValueDefault;
|
|
73
|
+
/**
|
|
74
|
+
* Evaluate a single parsed condition against a data item.
|
|
75
|
+
*
|
|
76
|
+
* @param item - Data item.
|
|
77
|
+
* @param condition - Parsed condition.
|
|
78
|
+
* @returns True if the condition is satisfied.
|
|
79
|
+
*/
|
|
80
|
+
private evaluateCondition;
|
|
81
|
+
/**
|
|
82
|
+
* Dispatch and evaluate a built-in filter function.
|
|
83
|
+
*
|
|
84
|
+
* @param item - Data item.
|
|
85
|
+
* @param func - Function name.
|
|
86
|
+
* @param funcArgs - Function arguments.
|
|
87
|
+
* @returns Function result.
|
|
88
|
+
*
|
|
89
|
+
* @throws {InvalidFormatException} When the function name is unknown.
|
|
90
|
+
*/
|
|
91
|
+
private evaluateFunction;
|
|
92
|
+
/**
|
|
93
|
+
* Evaluate the starts_with() filter function.
|
|
94
|
+
*
|
|
95
|
+
* @param item - Data item.
|
|
96
|
+
* @param funcArgs - [field, prefix].
|
|
97
|
+
* @returns True if the field value starts with the prefix.
|
|
98
|
+
*/
|
|
99
|
+
private evalStartsWith;
|
|
100
|
+
/**
|
|
101
|
+
* Evaluate the contains() filter function.
|
|
102
|
+
*
|
|
103
|
+
* @param item - Data item.
|
|
104
|
+
* @param funcArgs - [field, needle].
|
|
105
|
+
* @returns True if the field value contains the needle.
|
|
106
|
+
*/
|
|
107
|
+
private evalContains;
|
|
108
|
+
/**
|
|
109
|
+
* Evaluate the values() filter function (returns count).
|
|
110
|
+
*
|
|
111
|
+
* @param item - Data item.
|
|
112
|
+
* @param funcArgs - [field].
|
|
113
|
+
* @returns Number of elements in the field array, or 0.
|
|
114
|
+
*/
|
|
115
|
+
private evalValues;
|
|
116
|
+
/**
|
|
117
|
+
* Resolve an arithmetic expression from a filter predicate.
|
|
118
|
+
*
|
|
119
|
+
* @param item - Data item for field resolution.
|
|
120
|
+
* @param expr - Arithmetic expression (e.g. "@.price * @.qty").
|
|
121
|
+
* @returns Computed result, or null on failure.
|
|
122
|
+
*/
|
|
123
|
+
private resolveArithmetic;
|
|
124
|
+
/**
|
|
125
|
+
* Resolve a filter argument to its value from the data item.
|
|
126
|
+
*
|
|
127
|
+
* @param item - Data item.
|
|
128
|
+
* @param arg - Argument ("@", "@.field", or "field").
|
|
129
|
+
* @returns Resolved value.
|
|
130
|
+
*/
|
|
131
|
+
private resolveFilterArg;
|
|
132
|
+
/**
|
|
133
|
+
* Resolve a dot-separated field path from a data item.
|
|
134
|
+
*
|
|
135
|
+
* @param item - Data item.
|
|
136
|
+
* @param field - Dot-separated field path.
|
|
137
|
+
* @returns Resolved value, or null if not found.
|
|
138
|
+
*
|
|
139
|
+
* @throws {SecurityException} When a field key is forbidden.
|
|
140
|
+
*/
|
|
141
|
+
private resolveField;
|
|
142
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { InvalidFormatException } from '../exceptions/invalid-format-exception.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse and evaluate filter predicate expressions for path queries.
|
|
4
|
+
*
|
|
5
|
+
* Handles `[?expression]` syntax with comparison operators (==, !=, >, <, >=, <=),
|
|
6
|
+
* logical operators (&& and ||), and built-in functions (starts_with, contains, values).
|
|
7
|
+
* Supports arithmetic expressions and nested field access via dot-notation.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*
|
|
11
|
+
* @see FilterEvaluatorInterface Contract this class implements.
|
|
12
|
+
* @see SegmentParser Uses this for filter segment parsing.
|
|
13
|
+
* @see SegmentPathResolver Uses this for filter evaluation during resolution.
|
|
14
|
+
* @see SecurityGuardInterface Guards field access against forbidden keys.
|
|
15
|
+
*/
|
|
16
|
+
export class SegmentFilterParser {
|
|
17
|
+
guard;
|
|
18
|
+
/**
|
|
19
|
+
* Create a filter parser with security guard for field validation.
|
|
20
|
+
*
|
|
21
|
+
* @param guard - Key validator for field access.
|
|
22
|
+
*/
|
|
23
|
+
constructor(guard) {
|
|
24
|
+
this.guard = guard;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse a filter expression into structured conditions and logical operators.
|
|
28
|
+
*
|
|
29
|
+
* @param expression - Raw filter string (e.g. "age>18 && active==true").
|
|
30
|
+
* @returns Parsed structure with conditions and logical operators.
|
|
31
|
+
*
|
|
32
|
+
* @throws {InvalidFormatException} When the expression syntax is invalid.
|
|
33
|
+
*/
|
|
34
|
+
parse(expression) {
|
|
35
|
+
const conditions = [];
|
|
36
|
+
const parts = this.splitLogical(expression);
|
|
37
|
+
for (const token of parts.tokens) {
|
|
38
|
+
conditions.push(this.parseCondition(token.trim()));
|
|
39
|
+
}
|
|
40
|
+
return { conditions, logicals: parts.operators };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Evaluate a parsed filter expression against a data item.
|
|
44
|
+
*
|
|
45
|
+
* @param item - Data item to test.
|
|
46
|
+
* @param expr - Parsed expression.
|
|
47
|
+
* @returns True if the item satisfies all conditions.
|
|
48
|
+
*/
|
|
49
|
+
evaluate(item, expr) {
|
|
50
|
+
if (expr.conditions.length === 0) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
let result = this.evaluateCondition(item, expr.conditions[0]);
|
|
54
|
+
for (let i = 0; i < expr.logicals.length; i++) {
|
|
55
|
+
const nextResult = this.evaluateCondition(item, expr.conditions[i + 1]);
|
|
56
|
+
if (expr.logicals[i] === '&&') {
|
|
57
|
+
result = result && nextResult;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
result = result || nextResult;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Split expression by logical operators (&& and ||), respecting quotes.
|
|
67
|
+
*
|
|
68
|
+
* @param expression - Raw expression string.
|
|
69
|
+
* @returns Tokens and their joining operators.
|
|
70
|
+
*/
|
|
71
|
+
splitLogical(expression) {
|
|
72
|
+
const tokens = [];
|
|
73
|
+
const operators = [];
|
|
74
|
+
let current = '';
|
|
75
|
+
let inString = false;
|
|
76
|
+
let stringChar = '';
|
|
77
|
+
for (let i = 0; i < expression.length; i++) {
|
|
78
|
+
const ch = expression[i];
|
|
79
|
+
if (inString) {
|
|
80
|
+
current += ch;
|
|
81
|
+
if (ch === stringChar) {
|
|
82
|
+
inString = false;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (ch === "'" || ch === '"') {
|
|
87
|
+
inString = true;
|
|
88
|
+
stringChar = ch;
|
|
89
|
+
current += ch;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (ch === '&' && expression[i + 1] === '&') {
|
|
93
|
+
tokens.push(current);
|
|
94
|
+
operators.push('&&');
|
|
95
|
+
current = '';
|
|
96
|
+
i++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (ch === '|' && expression[i + 1] === '|') {
|
|
100
|
+
tokens.push(current);
|
|
101
|
+
operators.push('||');
|
|
102
|
+
current = '';
|
|
103
|
+
i++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
current += ch;
|
|
107
|
+
}
|
|
108
|
+
tokens.push(current);
|
|
109
|
+
return { tokens, operators };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Parse a single condition token into a structured object.
|
|
113
|
+
*
|
|
114
|
+
* @param token - Single condition (e.g. "age>18", "starts_with(@.name, 'J')").
|
|
115
|
+
* @returns Parsed condition.
|
|
116
|
+
*
|
|
117
|
+
* @throws {InvalidFormatException} When the condition syntax is invalid.
|
|
118
|
+
*/
|
|
119
|
+
parseCondition(token) {
|
|
120
|
+
const operators = ['>=', '<=', '!=', '==', '>', '<'];
|
|
121
|
+
const funcCompareMatch = token.match(/^(\w+)\(([^)]*)\)\s*(>=|<=|!=|==|>|<)\s*(.+)$/);
|
|
122
|
+
if (funcCompareMatch) {
|
|
123
|
+
const func = funcCompareMatch[1];
|
|
124
|
+
const argsRaw = funcCompareMatch[2];
|
|
125
|
+
const operator = funcCompareMatch[3];
|
|
126
|
+
const rawValue = funcCompareMatch[4].trim();
|
|
127
|
+
const funcArgs = argsRaw.split(',').map((a) => a.trim());
|
|
128
|
+
return {
|
|
129
|
+
field: funcArgs[0],
|
|
130
|
+
operator,
|
|
131
|
+
value: this.parseValue(rawValue),
|
|
132
|
+
func,
|
|
133
|
+
funcArgs,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const funcBoolMatch = token.match(/^(\w+)\(([^)]*)\)$/);
|
|
137
|
+
if (funcBoolMatch) {
|
|
138
|
+
const func = funcBoolMatch[1];
|
|
139
|
+
const argsRaw = funcBoolMatch[2];
|
|
140
|
+
const funcArgs = argsRaw.split(',').map((a) => a.trim());
|
|
141
|
+
return {
|
|
142
|
+
field: funcArgs[0],
|
|
143
|
+
operator: '==',
|
|
144
|
+
value: true,
|
|
145
|
+
func,
|
|
146
|
+
funcArgs,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
for (const op of operators) {
|
|
150
|
+
const pos = token.indexOf(op);
|
|
151
|
+
if (pos !== -1) {
|
|
152
|
+
const field = token.substring(0, pos).trim();
|
|
153
|
+
const rawValue = token.substring(pos + op.length).trim();
|
|
154
|
+
return {
|
|
155
|
+
field,
|
|
156
|
+
operator: op,
|
|
157
|
+
value: this.parseValue(rawValue),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
throw new InvalidFormatException(`Invalid filter condition: "${token}"`);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Parse a raw value string to its native type.
|
|
165
|
+
*
|
|
166
|
+
* @param raw - Raw value (e.g. "true", "'hello'", "42").
|
|
167
|
+
* @returns Native value.
|
|
168
|
+
*/
|
|
169
|
+
parseValue(raw) {
|
|
170
|
+
if (raw === 'true')
|
|
171
|
+
return true;
|
|
172
|
+
if (raw === 'false')
|
|
173
|
+
return false;
|
|
174
|
+
if (raw === 'null')
|
|
175
|
+
return null;
|
|
176
|
+
return this.parseValueDefault(raw);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Parse a non-keyword raw value to number or string.
|
|
180
|
+
*
|
|
181
|
+
* @param raw - Raw value string.
|
|
182
|
+
* @returns Typed value.
|
|
183
|
+
*/
|
|
184
|
+
parseValueDefault(raw) {
|
|
185
|
+
if ((raw.startsWith("'") && raw.endsWith("'")) ||
|
|
186
|
+
(raw.startsWith('"') && raw.endsWith('"'))) {
|
|
187
|
+
return raw.substring(1, raw.length - 1);
|
|
188
|
+
}
|
|
189
|
+
if (!isNaN(Number(raw)) && raw !== '') {
|
|
190
|
+
return raw.includes('.') ? parseFloat(raw) : parseInt(raw, 10);
|
|
191
|
+
}
|
|
192
|
+
return raw;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Evaluate a single parsed condition against a data item.
|
|
196
|
+
*
|
|
197
|
+
* @param item - Data item.
|
|
198
|
+
* @param condition - Parsed condition.
|
|
199
|
+
* @returns True if the condition is satisfied.
|
|
200
|
+
*/
|
|
201
|
+
evaluateCondition(item, condition) {
|
|
202
|
+
let fieldValue;
|
|
203
|
+
if (condition.func !== undefined) {
|
|
204
|
+
fieldValue = this.evaluateFunction(item, condition.func, condition.funcArgs ?? []);
|
|
205
|
+
}
|
|
206
|
+
else if (/[@\w.]+\s*[+\-*/]\s*[@\w.]+/.test(condition.field)) {
|
|
207
|
+
fieldValue = this.resolveArithmetic(item, condition.field);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
fieldValue = this.resolveField(item, condition.field);
|
|
211
|
+
}
|
|
212
|
+
const expected = condition.value;
|
|
213
|
+
switch (condition.operator) {
|
|
214
|
+
case '==':
|
|
215
|
+
return fieldValue === expected;
|
|
216
|
+
case '!=':
|
|
217
|
+
return fieldValue !== expected;
|
|
218
|
+
case '>':
|
|
219
|
+
return fieldValue > expected;
|
|
220
|
+
case '<':
|
|
221
|
+
return fieldValue < expected;
|
|
222
|
+
case '>=':
|
|
223
|
+
return fieldValue >= expected;
|
|
224
|
+
case '<=':
|
|
225
|
+
return fieldValue <= expected;
|
|
226
|
+
default:
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Dispatch and evaluate a built-in filter function.
|
|
232
|
+
*
|
|
233
|
+
* @param item - Data item.
|
|
234
|
+
* @param func - Function name.
|
|
235
|
+
* @param funcArgs - Function arguments.
|
|
236
|
+
* @returns Function result.
|
|
237
|
+
*
|
|
238
|
+
* @throws {InvalidFormatException} When the function name is unknown.
|
|
239
|
+
*/
|
|
240
|
+
evaluateFunction(item, func, funcArgs) {
|
|
241
|
+
switch (func) {
|
|
242
|
+
case 'starts_with':
|
|
243
|
+
return this.evalStartsWith(item, funcArgs);
|
|
244
|
+
case 'contains':
|
|
245
|
+
return this.evalContains(item, funcArgs);
|
|
246
|
+
case 'values':
|
|
247
|
+
return this.evalValues(item, funcArgs);
|
|
248
|
+
default:
|
|
249
|
+
throw new InvalidFormatException(`Unknown filter function: "${func}"`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Evaluate the starts_with() filter function.
|
|
254
|
+
*
|
|
255
|
+
* @param item - Data item.
|
|
256
|
+
* @param funcArgs - [field, prefix].
|
|
257
|
+
* @returns True if the field value starts with the prefix.
|
|
258
|
+
*/
|
|
259
|
+
evalStartsWith(item, funcArgs) {
|
|
260
|
+
const val = this.resolveFilterArg(item, funcArgs[0] ?? '@');
|
|
261
|
+
if (typeof val !== 'string') {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
const prefix = String(this.parseValue((funcArgs[1] ?? '').trim()));
|
|
265
|
+
return val.startsWith(prefix);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Evaluate the contains() filter function.
|
|
269
|
+
*
|
|
270
|
+
* @param item - Data item.
|
|
271
|
+
* @param funcArgs - [field, needle].
|
|
272
|
+
* @returns True if the field value contains the needle.
|
|
273
|
+
*/
|
|
274
|
+
evalContains(item, funcArgs) {
|
|
275
|
+
const val = this.resolveFilterArg(item, funcArgs[0] ?? '@');
|
|
276
|
+
const needle = String(this.parseValue((funcArgs[1] ?? '').trim()));
|
|
277
|
+
if (typeof val === 'string') {
|
|
278
|
+
return val.includes(needle);
|
|
279
|
+
}
|
|
280
|
+
if (Array.isArray(val)) {
|
|
281
|
+
return val.includes(needle);
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Evaluate the values() filter function (returns count).
|
|
287
|
+
*
|
|
288
|
+
* @param item - Data item.
|
|
289
|
+
* @param funcArgs - [field].
|
|
290
|
+
* @returns Number of elements in the field array, or 0.
|
|
291
|
+
*/
|
|
292
|
+
evalValues(item, funcArgs) {
|
|
293
|
+
const val = this.resolveFilterArg(item, funcArgs[0] ?? '@');
|
|
294
|
+
if (Array.isArray(val)) {
|
|
295
|
+
return val.length;
|
|
296
|
+
}
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Resolve an arithmetic expression from a filter predicate.
|
|
301
|
+
*
|
|
302
|
+
* @param item - Data item for field resolution.
|
|
303
|
+
* @param expr - Arithmetic expression (e.g. "@.price * @.qty").
|
|
304
|
+
* @returns Computed result, or null on failure.
|
|
305
|
+
*/
|
|
306
|
+
resolveArithmetic(item, expr) {
|
|
307
|
+
const m = expr.match(/^([@\w.]+)\s*([+\-*/])\s*([@\w.]+|\d+(?:\.\d+)?)$/);
|
|
308
|
+
if (!m) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const toNumber = (token) => {
|
|
312
|
+
if (!token.startsWith('@') && !isNaN(Number(token)) && token !== '') {
|
|
313
|
+
return token.includes('.') ? parseFloat(token) : parseInt(token, 10);
|
|
314
|
+
}
|
|
315
|
+
const val = this.resolveFilterArg(item, token);
|
|
316
|
+
if (typeof val === 'number') {
|
|
317
|
+
return val;
|
|
318
|
+
}
|
|
319
|
+
if (typeof val === 'string' && !isNaN(Number(val)) && val !== '') {
|
|
320
|
+
return val.includes('.') ? parseFloat(val) : parseInt(val, 10);
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
};
|
|
324
|
+
const left = toNumber(m[1]);
|
|
325
|
+
const right = toNumber(m[3]);
|
|
326
|
+
if (left === null || right === null) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
switch (m[2]) {
|
|
330
|
+
case '+':
|
|
331
|
+
return left + right;
|
|
332
|
+
case '-':
|
|
333
|
+
return left - right;
|
|
334
|
+
case '*':
|
|
335
|
+
return left * right;
|
|
336
|
+
default:
|
|
337
|
+
return right !== 0 ? left / right : null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Resolve a filter argument to its value from the data item.
|
|
342
|
+
*
|
|
343
|
+
* @param item - Data item.
|
|
344
|
+
* @param arg - Argument ("@", "@.field", or "field").
|
|
345
|
+
* @returns Resolved value.
|
|
346
|
+
*/
|
|
347
|
+
resolveFilterArg(item, arg) {
|
|
348
|
+
if (arg === '' || arg === '@') {
|
|
349
|
+
return item;
|
|
350
|
+
}
|
|
351
|
+
if (arg.startsWith('@.')) {
|
|
352
|
+
return this.resolveField(item, arg.substring(2));
|
|
353
|
+
}
|
|
354
|
+
return this.resolveField(item, arg);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Resolve a dot-separated field path from a data item.
|
|
358
|
+
*
|
|
359
|
+
* @param item - Data item.
|
|
360
|
+
* @param field - Dot-separated field path.
|
|
361
|
+
* @returns Resolved value, or null if not found.
|
|
362
|
+
*
|
|
363
|
+
* @throws {SecurityException} When a field key is forbidden.
|
|
364
|
+
*/
|
|
365
|
+
resolveField(item, field) {
|
|
366
|
+
if (field.includes('.')) {
|
|
367
|
+
let current = item;
|
|
368
|
+
for (const key of field.split('.')) {
|
|
369
|
+
this.guard.assertSafeKey(key);
|
|
370
|
+
if (typeof current === 'object' &&
|
|
371
|
+
current !== null &&
|
|
372
|
+
Object.prototype.hasOwnProperty.call(current, key)) {
|
|
373
|
+
current = current[key];
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return current;
|
|
380
|
+
}
|
|
381
|
+
this.guard.assertSafeKey(field);
|
|
382
|
+
return item[field] ?? null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { FilterEvaluatorInterface } from '../contracts/filter-evaluator-interface.js';
|
|
2
|
+
import type { Segment } from './segment-type.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parse dot-notation path strings into typed segment arrays.
|
|
5
|
+
*
|
|
6
|
+
* Converts path expressions (e.g. "users[0].address..city") into ordered
|
|
7
|
+
* segment arrays with {@link SegmentType} metadata for resolution by
|
|
8
|
+
* {@link SegmentPathResolver}. Supports key, index, wildcard, descent,
|
|
9
|
+
* multi-key/index, filter, slice, and projection segment types.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*
|
|
13
|
+
* @see SegmentType Enum of all segment types produced.
|
|
14
|
+
* @see SegmentPathResolver Consumer that resolves segments against data.
|
|
15
|
+
* @see FilterEvaluatorInterface Delegate for filter expression parsing.
|
|
16
|
+
*/
|
|
17
|
+
export declare class SegmentParser {
|
|
18
|
+
private readonly segmentFilterParser;
|
|
19
|
+
/**
|
|
20
|
+
* Create a segment parser with a filter evaluator.
|
|
21
|
+
*
|
|
22
|
+
* @param segmentFilterParser - Delegate for [?filter] parsing.
|
|
23
|
+
*/
|
|
24
|
+
constructor(segmentFilterParser: FilterEvaluatorInterface);
|
|
25
|
+
/**
|
|
26
|
+
* Parse a dot-notation path into an ordered array of typed segments.
|
|
27
|
+
*
|
|
28
|
+
* @param path - Dot-notation path expression.
|
|
29
|
+
* @returns Typed segment array.
|
|
30
|
+
*
|
|
31
|
+
* @throws {InvalidFormatException} When slice step is zero.
|
|
32
|
+
*/
|
|
33
|
+
parseSegments(path: string): Segment[];
|
|
34
|
+
/**
|
|
35
|
+
* Parse a recursive descent segment (`..key` or `..[...]`).
|
|
36
|
+
*
|
|
37
|
+
* @param path - Full path string.
|
|
38
|
+
* @param pos - Current position (mutated in place).
|
|
39
|
+
* @param len - Total path length.
|
|
40
|
+
* @returns Parsed descent segment.
|
|
41
|
+
*/
|
|
42
|
+
private parseDescent;
|
|
43
|
+
/**
|
|
44
|
+
* Parse a projection segment (`.{field1, field2}` or `.{alias: field}`).
|
|
45
|
+
*
|
|
46
|
+
* @param path - Full path string.
|
|
47
|
+
* @param pos - Current position (mutated in place).
|
|
48
|
+
* @param len - Total path length.
|
|
49
|
+
* @returns Parsed projection segment, or null if not a projection.
|
|
50
|
+
*/
|
|
51
|
+
private parseProjection;
|
|
52
|
+
/**
|
|
53
|
+
* Parse a filter segment (`[?expression]`).
|
|
54
|
+
*
|
|
55
|
+
* @param path - Full path string.
|
|
56
|
+
* @param pos - Current position (mutated in place).
|
|
57
|
+
* @param len - Total path length.
|
|
58
|
+
* @returns Parsed filter segment.
|
|
59
|
+
*/
|
|
60
|
+
private parseFilter;
|
|
61
|
+
/**
|
|
62
|
+
* Parse a bracket segment (`[0]`, `[0,1,2]`, `[0:5]`, `['key']`, `[*]`).
|
|
63
|
+
*
|
|
64
|
+
* @param path - Full path string.
|
|
65
|
+
* @param pos - Current position (mutated in place).
|
|
66
|
+
* @param len - Total path length.
|
|
67
|
+
* @returns Parsed bracket segment.
|
|
68
|
+
*
|
|
69
|
+
* @throws {InvalidFormatException} When slice step is zero.
|
|
70
|
+
*/
|
|
71
|
+
private parseBracket;
|
|
72
|
+
/**
|
|
73
|
+
* Parse a regular dot-separated key with escaped-dot support.
|
|
74
|
+
*
|
|
75
|
+
* @param path - Full path string.
|
|
76
|
+
* @param pos - Current position (mutated in place).
|
|
77
|
+
* @param len - Total path length.
|
|
78
|
+
* @returns Parsed key segment.
|
|
79
|
+
*/
|
|
80
|
+
private parseKey;
|
|
81
|
+
/**
|
|
82
|
+
* Check if all parts in a comma-separated list are quoted strings.
|
|
83
|
+
*
|
|
84
|
+
* @param parts - Raw parts from split.
|
|
85
|
+
* @returns True if every part is single- or double-quoted.
|
|
86
|
+
*/
|
|
87
|
+
private allQuoted;
|
|
88
|
+
/**
|
|
89
|
+
* Parse a simple dot-notation path into plain string keys.
|
|
90
|
+
*
|
|
91
|
+
* Handles bracket notation and escaped dots. Does not produce typed
|
|
92
|
+
* segments - used for set/remove operations via {@link DotNotationParser}.
|
|
93
|
+
*
|
|
94
|
+
* @param path - Simple dot-notation path.
|
|
95
|
+
* @returns Ordered list of key strings.
|
|
96
|
+
*/
|
|
97
|
+
parseKeys(path: string): string[];
|
|
98
|
+
}
|