@shko.online/dataverse-odata 0.1.6 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +3 -0
- package/CHANGELOG.md +20 -0
- package/azure-pipelines.yml +2 -2
- package/lib/cjs/getFetchXmlFromParser.js +1 -1
- package/lib/cjs/getFilterFromParser.js +234 -6
- package/lib/cjs/index.js +21 -3
- package/lib/esm/getFetchXmlFromParser.js +1 -1
- package/lib/esm/getFilterFromParser.js +234 -6
- package/lib/esm/index.js +3 -3
- package/lib/modern/getFetchXmlFromParser.js +1 -1
- package/lib/modern/getFilterFromParser.js +239 -6
- package/lib/modern/index.js +3 -3
- package/lib/ts3.4/OData.types.d.ts +35 -4
- package/lib/ts3.4/index.d.ts +4 -4
- package/lib/ts3.9/OData.types.d.ts +35 -4
- package/lib/ts3.9/index.d.ts +4 -4
- package/lib/ts4.2/OData.types.d.ts +48 -5
- package/lib/ts4.2/getFilterFromParser.d.ts.map +1 -1
- package/lib/ts4.2/index.d.ts +4 -4
- package/lib/ts4.2/index.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/OData.types.d.ts +48 -5
- package/src/getFetchXmlFromParser.ts +1 -1
- package/src/getFilterFromParser.ts +208 -5
- package/src/index.ts +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## [0.2.1](https://github.com/Shko-Online/dataverse-odata/compare/v0.2.0...v0.2.1) (2026-05-13)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Fixed exports for index ([d623a40](https://github.com/Shko-Online/dataverse-odata/commit/d623a40515dec7888997634cbf5d043ebbf91461))
|
|
7
|
+
|
|
8
|
+
# [0.2.0](https://github.com/Shko-Online/dataverse-odata/compare/v0.1.6...v0.2.0) (2026-04-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* column comparison uses no keyword prefix, e.g. firstname eq lastname ([34727d5](https://github.com/Shko-Online/dataverse-odata/commit/34727d5ce8698b4d0ec822ee226637169c881028))
|
|
14
|
+
* distinguish between null, boolean and column operators ([62511ef](https://github.com/Shko-Online/dataverse-odata/commit/62511ef624ebbd06aa6a7732b9693e7f50dbbbce))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* implement $filter parser and add sample tests for each operator ([e2f5387](https://github.com/Shko-Online/dataverse-odata/commit/e2f538740422ba51c13ff13ecb9222606a19ce17))
|
|
20
|
+
|
|
1
21
|
## [0.1.6](https://github.com/Shko-Online/dataverse-odata/compare/v0.1.5...v0.1.6) (2026-03-15)
|
|
2
22
|
|
|
3
23
|
|
package/azure-pipelines.yml
CHANGED
|
@@ -31,7 +31,7 @@ const getFetchXmlFromParser = (parser, result) => {
|
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
33
33
|
const entity = fetchXmlDocument.evaluate('fetch/entity', fetchXmlDocument, null, XPathResult.ANY_TYPE, null).iterateNext();
|
|
34
|
-
if (fetchXmlDocument.documentElement.children.length != 1 || !entity
|
|
34
|
+
if (fetchXmlDocument.documentElement.children.length != 1 || !entity?.getAttribute('name')) {
|
|
35
35
|
result.error = {
|
|
36
36
|
code: '0x80041102',
|
|
37
37
|
message: 'Entity Name was not specified in FetchXml String.'
|
|
@@ -5,7 +5,231 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.getFilterFromParser = void 0;
|
|
7
7
|
var _atMostOnce = require("./validators/atMostOnce");
|
|
8
|
+
var _hasContent = require("./validators/hasContent");
|
|
8
9
|
const option = '$filter';
|
|
10
|
+
const STANDARD_OPERATORS = ['eq', 'ne', 'gt', 'ge', 'lt', 'le'];
|
|
11
|
+
const QUERY_FUNCTION_OPERATORS = ['contains', 'endswith', 'startswith'];
|
|
12
|
+
function isStandardOperator(s) {
|
|
13
|
+
return STANDARD_OPERATORS.includes(s);
|
|
14
|
+
}
|
|
15
|
+
function isQueryFunctionOperator(s) {
|
|
16
|
+
return QUERY_FUNCTION_OPERATORS.includes(s);
|
|
17
|
+
}
|
|
18
|
+
const GUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/;
|
|
19
|
+
function tokenize(input) {
|
|
20
|
+
const tokens = [];
|
|
21
|
+
let i = 0;
|
|
22
|
+
while (i < input.length) {
|
|
23
|
+
if (/\s/.test(input[i])) {
|
|
24
|
+
i++;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (input[i] === '(') {
|
|
28
|
+
tokens.push({
|
|
29
|
+
type: 'lparen',
|
|
30
|
+
value: '('
|
|
31
|
+
});
|
|
32
|
+
i++;
|
|
33
|
+
} else if (input[i] === ')') {
|
|
34
|
+
tokens.push({
|
|
35
|
+
type: 'rparen',
|
|
36
|
+
value: ')'
|
|
37
|
+
});
|
|
38
|
+
i++;
|
|
39
|
+
} else if (input[i] === ',') {
|
|
40
|
+
tokens.push({
|
|
41
|
+
type: 'comma',
|
|
42
|
+
value: ','
|
|
43
|
+
});
|
|
44
|
+
i++;
|
|
45
|
+
} else if (input[i] === "'") {
|
|
46
|
+
let j = i + 1;
|
|
47
|
+
let str = '';
|
|
48
|
+
while (j < input.length) {
|
|
49
|
+
if (input[j] === "'" && j + 1 < input.length && input[j + 1] === "'") {
|
|
50
|
+
str += "'";
|
|
51
|
+
j += 2;
|
|
52
|
+
} else if (input[j] === "'") {
|
|
53
|
+
break;
|
|
54
|
+
} else {
|
|
55
|
+
str += input[j];
|
|
56
|
+
j++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
tokens.push({
|
|
60
|
+
type: 'string',
|
|
61
|
+
value: str
|
|
62
|
+
});
|
|
63
|
+
i = j + 1;
|
|
64
|
+
} else if (/[0-9a-fA-F]/.test(input[i]) && GUID_REGEX.test(input.slice(i))) {
|
|
65
|
+
tokens.push({
|
|
66
|
+
type: 'string',
|
|
67
|
+
value: input.slice(i, i + 36)
|
|
68
|
+
});
|
|
69
|
+
i += 36;
|
|
70
|
+
} else if (/\d/.test(input[i]) || input[i] === '-' && /\d/.test(input[i + 1] ?? '')) {
|
|
71
|
+
let j = i;
|
|
72
|
+
if (input[j] === '-') j++;
|
|
73
|
+
while (j < input.length && /[0-9.]/.test(input[j])) j++;
|
|
74
|
+
tokens.push({
|
|
75
|
+
type: 'number',
|
|
76
|
+
value: input.slice(i, j)
|
|
77
|
+
});
|
|
78
|
+
i = j;
|
|
79
|
+
} else if (/[a-zA-Z_]/.test(input[i])) {
|
|
80
|
+
let j = i;
|
|
81
|
+
while (j < input.length && /\w/.test(input[j])) j++;
|
|
82
|
+
tokens.push({
|
|
83
|
+
type: 'word',
|
|
84
|
+
value: input.slice(i, j)
|
|
85
|
+
});
|
|
86
|
+
i = j;
|
|
87
|
+
} else {
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return tokens;
|
|
92
|
+
}
|
|
93
|
+
class FilterParser {
|
|
94
|
+
constructor(tokens) {
|
|
95
|
+
this.tokens = void 0;
|
|
96
|
+
this.pos = 0;
|
|
97
|
+
this.tokens = tokens;
|
|
98
|
+
}
|
|
99
|
+
peek() {
|
|
100
|
+
return this.tokens[this.pos] ?? null;
|
|
101
|
+
}
|
|
102
|
+
consume(expected) {
|
|
103
|
+
const token = this.tokens[this.pos++];
|
|
104
|
+
const expectedString = expected ? `, expected ${expected}` : '';
|
|
105
|
+
if (!token) throw new Error(`Unexpected end of filter expression${expectedString}`);
|
|
106
|
+
return token;
|
|
107
|
+
}
|
|
108
|
+
expect(type, value) {
|
|
109
|
+
const valueString = value ? ` '${value}'` : '';
|
|
110
|
+
const token = this.consume(`${type}${valueString}`);
|
|
111
|
+
if (token.type !== type || value !== undefined && token.value !== value) {
|
|
112
|
+
throw new Error(`Expected ${type}${valueString} but got ${token.type} '${token.value}'`);
|
|
113
|
+
}
|
|
114
|
+
return token;
|
|
115
|
+
}
|
|
116
|
+
parse() {
|
|
117
|
+
const expr = this.parseOr();
|
|
118
|
+
if (this.pos < this.tokens.length) {
|
|
119
|
+
throw new Error(`Unexpected token '${this.tokens[this.pos].value}'`);
|
|
120
|
+
}
|
|
121
|
+
return expr;
|
|
122
|
+
}
|
|
123
|
+
parseOr() {
|
|
124
|
+
let left = this.parseAnd();
|
|
125
|
+
while (this.peek()?.value === 'or') {
|
|
126
|
+
this.consume();
|
|
127
|
+
const right = this.parseAnd();
|
|
128
|
+
left = {
|
|
129
|
+
operator: 'or',
|
|
130
|
+
left,
|
|
131
|
+
right
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return left;
|
|
135
|
+
}
|
|
136
|
+
parseAnd() {
|
|
137
|
+
let left = this.parseNot();
|
|
138
|
+
while (this.peek()?.value === 'and') {
|
|
139
|
+
this.consume();
|
|
140
|
+
const right = this.parseNot();
|
|
141
|
+
left = {
|
|
142
|
+
operator: 'and',
|
|
143
|
+
left,
|
|
144
|
+
right
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return left;
|
|
148
|
+
}
|
|
149
|
+
parseNot() {
|
|
150
|
+
if (this.peek()?.value === 'not') {
|
|
151
|
+
this.consume();
|
|
152
|
+
const right = this.parseNot();
|
|
153
|
+
return {
|
|
154
|
+
operator: 'not',
|
|
155
|
+
right
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return this.parsePrimary();
|
|
159
|
+
}
|
|
160
|
+
parsePrimary() {
|
|
161
|
+
const token = this.peek();
|
|
162
|
+
if (!token) throw new Error('Unexpected end of filter expression');
|
|
163
|
+
if (token.type === 'lparen') {
|
|
164
|
+
this.consume();
|
|
165
|
+
const expr = this.parseOr();
|
|
166
|
+
this.expect('rparen');
|
|
167
|
+
return expr;
|
|
168
|
+
}
|
|
169
|
+
if (token.type === 'word' && isQueryFunctionOperator(token.value.toLowerCase())) {
|
|
170
|
+
const func = this.consume().value.toLowerCase();
|
|
171
|
+
this.expect('lparen');
|
|
172
|
+
const left = this.expect('word').value;
|
|
173
|
+
this.expect('comma');
|
|
174
|
+
const right = this.expect('string').value;
|
|
175
|
+
this.expect('rparen');
|
|
176
|
+
return {
|
|
177
|
+
operator: func,
|
|
178
|
+
left,
|
|
179
|
+
right
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (token.type === 'word') {
|
|
183
|
+
const left = this.consume().value;
|
|
184
|
+
const opToken = this.consume(`a comparison operator`);
|
|
185
|
+
if (!isStandardOperator(opToken.value.toLowerCase())) {
|
|
186
|
+
throw new Error(`Invalid operator '${opToken.value}'`);
|
|
187
|
+
}
|
|
188
|
+
const operator = opToken.value.toLowerCase();
|
|
189
|
+
const right = this.consume(`a value or column name`);
|
|
190
|
+
if (right.type === 'string') {
|
|
191
|
+
return {
|
|
192
|
+
operator,
|
|
193
|
+
left,
|
|
194
|
+
right: right.value
|
|
195
|
+
};
|
|
196
|
+
} else if (right.type === 'number') {
|
|
197
|
+
return {
|
|
198
|
+
operator,
|
|
199
|
+
left,
|
|
200
|
+
right: Number(right.value)
|
|
201
|
+
};
|
|
202
|
+
} else if (right.type === 'word') {
|
|
203
|
+
// Constant keywords stay as StandardOperator; bare identifiers are column comparisons
|
|
204
|
+
const BOOL_CONSTANTS = ['true', 'false'];
|
|
205
|
+
if (BOOL_CONSTANTS.includes(right.value.toLowerCase())) {
|
|
206
|
+
return {
|
|
207
|
+
operator,
|
|
208
|
+
left,
|
|
209
|
+
isBooleanOperation: true,
|
|
210
|
+
right: right.value === 'true'
|
|
211
|
+
};
|
|
212
|
+
} else if (right.value.toLowerCase() === 'null') {
|
|
213
|
+
return {
|
|
214
|
+
operator,
|
|
215
|
+
left,
|
|
216
|
+
isNullOperation: true,
|
|
217
|
+
right: null
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
left,
|
|
222
|
+
operator,
|
|
223
|
+
isColumnOperation: true,
|
|
224
|
+
right: right.value
|
|
225
|
+
};
|
|
226
|
+
} else {
|
|
227
|
+
throw new Error(`Invalid right-hand side value of type '${right.type}' in filter expression`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
throw new Error(`Unexpected token '${token.value}'`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
9
233
|
|
|
10
234
|
/**
|
|
11
235
|
* Parses the {@link ODataFilter.$filter $filter} query
|
|
@@ -16,15 +240,19 @@ const getFilterFromParser = (parser, result) => {
|
|
|
16
240
|
if (value.length === 0) {
|
|
17
241
|
return true;
|
|
18
242
|
}
|
|
19
|
-
if (!(0, _atMostOnce.atMostOnce)(option, value, result)) {
|
|
243
|
+
if (!(0, _atMostOnce.atMostOnce)(option, value, result) || !(0, _hasContent.hasContent)(option, value, result)) {
|
|
20
244
|
return false;
|
|
21
245
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
246
|
+
try {
|
|
247
|
+
const tokens = tokenize(value[0]);
|
|
248
|
+
const p = new FilterParser(tokens);
|
|
249
|
+
result.$filter = p.parse();
|
|
250
|
+
} catch (e) {
|
|
251
|
+
result.error = {
|
|
252
|
+
code: '0x80060888',
|
|
253
|
+
message: `Syntax error in '$filter': ${e.message}`
|
|
27
254
|
};
|
|
255
|
+
return false;
|
|
28
256
|
}
|
|
29
257
|
return true;
|
|
30
258
|
};
|
package/lib/cjs/index.js
CHANGED
|
@@ -3,7 +3,18 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports
|
|
6
|
+
Object.defineProperty(exports, "default", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _parseOData.parseOData;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(exports, "getAliasedProperty", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _getAliasedProperty.getAliasedProperty;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
7
18
|
Object.defineProperty(exports, "getExpandFromParser", {
|
|
8
19
|
enumerable: true,
|
|
9
20
|
get: function () {
|
|
@@ -16,6 +27,12 @@ Object.defineProperty(exports, "getFetchXmlFromParser", {
|
|
|
16
27
|
return _getFetchXmlFromParser.getFetchXmlFromParser;
|
|
17
28
|
}
|
|
18
29
|
});
|
|
30
|
+
Object.defineProperty(exports, "getFilterFromParser", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get: function () {
|
|
33
|
+
return _getFilterFromParser.getFilterFromParser;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
19
36
|
Object.defineProperty(exports, "getOrderByFromParser", {
|
|
20
37
|
enumerable: true,
|
|
21
38
|
get: function () {
|
|
@@ -46,11 +63,12 @@ Object.defineProperty(exports, "parseOData", {
|
|
|
46
63
|
return _parseOData.parseOData;
|
|
47
64
|
}
|
|
48
65
|
});
|
|
66
|
+
var _getAliasedProperty = require("./getAliasedProperty");
|
|
49
67
|
var _getExpandFromParser = require("./getExpandFromParser");
|
|
50
68
|
var _getFetchXmlFromParser = require("./getFetchXmlFromParser");
|
|
69
|
+
var _getFilterFromParser = require("./getFilterFromParser");
|
|
51
70
|
var _getOrderByFromParser = require("./getOrderByFromParser");
|
|
52
71
|
var _getSelectFromParser = require("./getSelectFromParser");
|
|
53
72
|
var _getTopFromParser = require("./getTopFromParser");
|
|
54
73
|
var _getXQueryFromParser = require("./getXQueryFromParser");
|
|
55
|
-
var _parseOData = require("./parseOData");
|
|
56
|
-
var _default = exports.default = _parseOData.parseOData;
|
|
74
|
+
var _parseOData = require("./parseOData");
|
|
@@ -25,7 +25,7 @@ export const getFetchXmlFromParser = (parser, result) => {
|
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
27
|
const entity = fetchXmlDocument.evaluate('fetch/entity', fetchXmlDocument, null, XPathResult.ANY_TYPE, null).iterateNext();
|
|
28
|
-
if (fetchXmlDocument.documentElement.children.length != 1 || !entity
|
|
28
|
+
if (fetchXmlDocument.documentElement.children.length != 1 || !entity?.getAttribute('name')) {
|
|
29
29
|
result.error = {
|
|
30
30
|
code: '0x80041102',
|
|
31
31
|
message: 'Entity Name was not specified in FetchXml String.'
|
|
@@ -1,5 +1,229 @@
|
|
|
1
1
|
import { atMostOnce } from './validators/atMostOnce';
|
|
2
|
+
import { hasContent } from './validators/hasContent';
|
|
2
3
|
const option = '$filter';
|
|
4
|
+
const STANDARD_OPERATORS = ['eq', 'ne', 'gt', 'ge', 'lt', 'le'];
|
|
5
|
+
const QUERY_FUNCTION_OPERATORS = ['contains', 'endswith', 'startswith'];
|
|
6
|
+
function isStandardOperator(s) {
|
|
7
|
+
return STANDARD_OPERATORS.includes(s);
|
|
8
|
+
}
|
|
9
|
+
function isQueryFunctionOperator(s) {
|
|
10
|
+
return QUERY_FUNCTION_OPERATORS.includes(s);
|
|
11
|
+
}
|
|
12
|
+
const GUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/;
|
|
13
|
+
function tokenize(input) {
|
|
14
|
+
const tokens = [];
|
|
15
|
+
let i = 0;
|
|
16
|
+
while (i < input.length) {
|
|
17
|
+
if (/\s/.test(input[i])) {
|
|
18
|
+
i++;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (input[i] === '(') {
|
|
22
|
+
tokens.push({
|
|
23
|
+
type: 'lparen',
|
|
24
|
+
value: '('
|
|
25
|
+
});
|
|
26
|
+
i++;
|
|
27
|
+
} else if (input[i] === ')') {
|
|
28
|
+
tokens.push({
|
|
29
|
+
type: 'rparen',
|
|
30
|
+
value: ')'
|
|
31
|
+
});
|
|
32
|
+
i++;
|
|
33
|
+
} else if (input[i] === ',') {
|
|
34
|
+
tokens.push({
|
|
35
|
+
type: 'comma',
|
|
36
|
+
value: ','
|
|
37
|
+
});
|
|
38
|
+
i++;
|
|
39
|
+
} else if (input[i] === "'") {
|
|
40
|
+
let j = i + 1;
|
|
41
|
+
let str = '';
|
|
42
|
+
while (j < input.length) {
|
|
43
|
+
if (input[j] === "'" && j + 1 < input.length && input[j + 1] === "'") {
|
|
44
|
+
str += "'";
|
|
45
|
+
j += 2;
|
|
46
|
+
} else if (input[j] === "'") {
|
|
47
|
+
break;
|
|
48
|
+
} else {
|
|
49
|
+
str += input[j];
|
|
50
|
+
j++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
tokens.push({
|
|
54
|
+
type: 'string',
|
|
55
|
+
value: str
|
|
56
|
+
});
|
|
57
|
+
i = j + 1;
|
|
58
|
+
} else if (/[0-9a-fA-F]/.test(input[i]) && GUID_REGEX.test(input.slice(i))) {
|
|
59
|
+
tokens.push({
|
|
60
|
+
type: 'string',
|
|
61
|
+
value: input.slice(i, i + 36)
|
|
62
|
+
});
|
|
63
|
+
i += 36;
|
|
64
|
+
} else if (/\d/.test(input[i]) || input[i] === '-' && /\d/.test(input[i + 1] ?? '')) {
|
|
65
|
+
let j = i;
|
|
66
|
+
if (input[j] === '-') j++;
|
|
67
|
+
while (j < input.length && /[0-9.]/.test(input[j])) j++;
|
|
68
|
+
tokens.push({
|
|
69
|
+
type: 'number',
|
|
70
|
+
value: input.slice(i, j)
|
|
71
|
+
});
|
|
72
|
+
i = j;
|
|
73
|
+
} else if (/[a-zA-Z_]/.test(input[i])) {
|
|
74
|
+
let j = i;
|
|
75
|
+
while (j < input.length && /\w/.test(input[j])) j++;
|
|
76
|
+
tokens.push({
|
|
77
|
+
type: 'word',
|
|
78
|
+
value: input.slice(i, j)
|
|
79
|
+
});
|
|
80
|
+
i = j;
|
|
81
|
+
} else {
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return tokens;
|
|
86
|
+
}
|
|
87
|
+
class FilterParser {
|
|
88
|
+
constructor(tokens) {
|
|
89
|
+
this.tokens = void 0;
|
|
90
|
+
this.pos = 0;
|
|
91
|
+
this.tokens = tokens;
|
|
92
|
+
}
|
|
93
|
+
peek() {
|
|
94
|
+
return this.tokens[this.pos] ?? null;
|
|
95
|
+
}
|
|
96
|
+
consume(expected) {
|
|
97
|
+
const token = this.tokens[this.pos++];
|
|
98
|
+
const expectedString = expected ? `, expected ${expected}` : '';
|
|
99
|
+
if (!token) throw new Error(`Unexpected end of filter expression${expectedString}`);
|
|
100
|
+
return token;
|
|
101
|
+
}
|
|
102
|
+
expect(type, value) {
|
|
103
|
+
const valueString = value ? ` '${value}'` : '';
|
|
104
|
+
const token = this.consume(`${type}${valueString}`);
|
|
105
|
+
if (token.type !== type || value !== undefined && token.value !== value) {
|
|
106
|
+
throw new Error(`Expected ${type}${valueString} but got ${token.type} '${token.value}'`);
|
|
107
|
+
}
|
|
108
|
+
return token;
|
|
109
|
+
}
|
|
110
|
+
parse() {
|
|
111
|
+
const expr = this.parseOr();
|
|
112
|
+
if (this.pos < this.tokens.length) {
|
|
113
|
+
throw new Error(`Unexpected token '${this.tokens[this.pos].value}'`);
|
|
114
|
+
}
|
|
115
|
+
return expr;
|
|
116
|
+
}
|
|
117
|
+
parseOr() {
|
|
118
|
+
let left = this.parseAnd();
|
|
119
|
+
while (this.peek()?.value === 'or') {
|
|
120
|
+
this.consume();
|
|
121
|
+
const right = this.parseAnd();
|
|
122
|
+
left = {
|
|
123
|
+
operator: 'or',
|
|
124
|
+
left,
|
|
125
|
+
right
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return left;
|
|
129
|
+
}
|
|
130
|
+
parseAnd() {
|
|
131
|
+
let left = this.parseNot();
|
|
132
|
+
while (this.peek()?.value === 'and') {
|
|
133
|
+
this.consume();
|
|
134
|
+
const right = this.parseNot();
|
|
135
|
+
left = {
|
|
136
|
+
operator: 'and',
|
|
137
|
+
left,
|
|
138
|
+
right
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return left;
|
|
142
|
+
}
|
|
143
|
+
parseNot() {
|
|
144
|
+
if (this.peek()?.value === 'not') {
|
|
145
|
+
this.consume();
|
|
146
|
+
const right = this.parseNot();
|
|
147
|
+
return {
|
|
148
|
+
operator: 'not',
|
|
149
|
+
right
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return this.parsePrimary();
|
|
153
|
+
}
|
|
154
|
+
parsePrimary() {
|
|
155
|
+
const token = this.peek();
|
|
156
|
+
if (!token) throw new Error('Unexpected end of filter expression');
|
|
157
|
+
if (token.type === 'lparen') {
|
|
158
|
+
this.consume();
|
|
159
|
+
const expr = this.parseOr();
|
|
160
|
+
this.expect('rparen');
|
|
161
|
+
return expr;
|
|
162
|
+
}
|
|
163
|
+
if (token.type === 'word' && isQueryFunctionOperator(token.value.toLowerCase())) {
|
|
164
|
+
const func = this.consume().value.toLowerCase();
|
|
165
|
+
this.expect('lparen');
|
|
166
|
+
const left = this.expect('word').value;
|
|
167
|
+
this.expect('comma');
|
|
168
|
+
const right = this.expect('string').value;
|
|
169
|
+
this.expect('rparen');
|
|
170
|
+
return {
|
|
171
|
+
operator: func,
|
|
172
|
+
left,
|
|
173
|
+
right
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (token.type === 'word') {
|
|
177
|
+
const left = this.consume().value;
|
|
178
|
+
const opToken = this.consume(`a comparison operator`);
|
|
179
|
+
if (!isStandardOperator(opToken.value.toLowerCase())) {
|
|
180
|
+
throw new Error(`Invalid operator '${opToken.value}'`);
|
|
181
|
+
}
|
|
182
|
+
const operator = opToken.value.toLowerCase();
|
|
183
|
+
const right = this.consume(`a value or column name`);
|
|
184
|
+
if (right.type === 'string') {
|
|
185
|
+
return {
|
|
186
|
+
operator,
|
|
187
|
+
left,
|
|
188
|
+
right: right.value
|
|
189
|
+
};
|
|
190
|
+
} else if (right.type === 'number') {
|
|
191
|
+
return {
|
|
192
|
+
operator,
|
|
193
|
+
left,
|
|
194
|
+
right: Number(right.value)
|
|
195
|
+
};
|
|
196
|
+
} else if (right.type === 'word') {
|
|
197
|
+
// Constant keywords stay as StandardOperator; bare identifiers are column comparisons
|
|
198
|
+
const BOOL_CONSTANTS = ['true', 'false'];
|
|
199
|
+
if (BOOL_CONSTANTS.includes(right.value.toLowerCase())) {
|
|
200
|
+
return {
|
|
201
|
+
operator,
|
|
202
|
+
left,
|
|
203
|
+
isBooleanOperation: true,
|
|
204
|
+
right: right.value === 'true'
|
|
205
|
+
};
|
|
206
|
+
} else if (right.value.toLowerCase() === 'null') {
|
|
207
|
+
return {
|
|
208
|
+
operator,
|
|
209
|
+
left,
|
|
210
|
+
isNullOperation: true,
|
|
211
|
+
right: null
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
left,
|
|
216
|
+
operator,
|
|
217
|
+
isColumnOperation: true,
|
|
218
|
+
right: right.value
|
|
219
|
+
};
|
|
220
|
+
} else {
|
|
221
|
+
throw new Error(`Invalid right-hand side value of type '${right.type}' in filter expression`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
throw new Error(`Unexpected token '${token.value}'`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
3
227
|
|
|
4
228
|
/**
|
|
5
229
|
* Parses the {@link ODataFilter.$filter $filter} query
|
|
@@ -10,15 +234,19 @@ export const getFilterFromParser = (parser, result) => {
|
|
|
10
234
|
if (value.length === 0) {
|
|
11
235
|
return true;
|
|
12
236
|
}
|
|
13
|
-
if (!atMostOnce(option, value, result)) {
|
|
237
|
+
if (!atMostOnce(option, value, result) || !hasContent(option, value, result)) {
|
|
14
238
|
return false;
|
|
15
239
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
240
|
+
try {
|
|
241
|
+
const tokens = tokenize(value[0]);
|
|
242
|
+
const p = new FilterParser(tokens);
|
|
243
|
+
result.$filter = p.parse();
|
|
244
|
+
} catch (e) {
|
|
245
|
+
result.error = {
|
|
246
|
+
code: '0x80060888',
|
|
247
|
+
message: `Syntax error in '$filter': ${e.message}`
|
|
21
248
|
};
|
|
249
|
+
return false;
|
|
22
250
|
}
|
|
23
251
|
return true;
|
|
24
252
|
};
|
package/lib/esm/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
export { getAliasedProperty } from './getAliasedProperty';
|
|
1
2
|
export { getExpandFromParser } from './getExpandFromParser';
|
|
2
3
|
export { getFetchXmlFromParser } from './getFetchXmlFromParser';
|
|
4
|
+
export { getFilterFromParser } from './getFilterFromParser';
|
|
3
5
|
export { getOrderByFromParser } from './getOrderByFromParser';
|
|
4
6
|
export { getSelectFromParser } from './getSelectFromParser';
|
|
5
7
|
export { getTopFromParser } from './getTopFromParser';
|
|
6
8
|
export { getXQueryFromParser } from './getXQueryFromParser';
|
|
7
|
-
export { parseOData } from './parseOData';
|
|
8
|
-
import { parseOData } from './parseOData';
|
|
9
|
-
export default parseOData;
|
|
9
|
+
export { parseOData, parseOData as default } from './parseOData';
|
|
@@ -25,7 +25,7 @@ export const getFetchXmlFromParser = (parser, result) => {
|
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
27
|
const entity = fetchXmlDocument.evaluate('fetch/entity', fetchXmlDocument, null, XPathResult.ANY_TYPE, null).iterateNext();
|
|
28
|
-
if (fetchXmlDocument.documentElement.children.length != 1 || !entity
|
|
28
|
+
if (fetchXmlDocument.documentElement.children.length != 1 || !(entity !== null && entity !== void 0 && entity.getAttribute('name'))) {
|
|
29
29
|
result.error = {
|
|
30
30
|
code: '0x80041102',
|
|
31
31
|
message: 'Entity Name was not specified in FetchXml String.'
|