@shko.online/dataverse-odata 0.1.6 → 0.2.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ # [0.2.0](https://github.com/Shko-Online/dataverse-odata/compare/v0.1.6...v0.2.0) (2026-04-11)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * column comparison uses no keyword prefix, e.g. firstname eq lastname ([34727d5](https://github.com/Shko-Online/dataverse-odata/commit/34727d5ce8698b4d0ec822ee226637169c881028))
7
+ * distinguish between null, boolean and column operators ([62511ef](https://github.com/Shko-Online/dataverse-odata/commit/62511ef624ebbd06aa6a7732b9693e7f50dbbbce))
8
+
9
+
10
+ ### Features
11
+
12
+ * implement $filter parser and add sample tests for each operator ([e2f5387](https://github.com/Shko-Online/dataverse-odata/commit/e2f538740422ba51c13ff13ecb9222606a19ce17))
13
+
1
14
  ## [0.1.6](https://github.com/Shko-Online/dataverse-odata/compare/v0.1.5...v0.1.6) (2026-03-15)
2
15
 
3
16
 
@@ -2,7 +2,7 @@ pool:
2
2
  vmImage: ubuntu-latest
3
3
 
4
4
  steps:
5
- - task: NodeTool@0
5
+ - task: NodeTool@1
6
6
  inputs:
7
7
  versionSpec: '24.x'
8
8
  displayName: 'Install Node.js'
@@ -5,7 +5,229 @@ 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 (/[0-9]/.test(input[i]) || input[i] === '-' && /[0-9]/.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 && /[a-zA-Z0-9_]/.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
+ if (!token) throw new Error(`Unexpected end of filter expression${expected ? `, expected ${expected}` : ''}`);
105
+ return token;
106
+ }
107
+ expect(type, value) {
108
+ const token = this.consume(`${type}${value ? ` '${value}'` : ''}`);
109
+ if (token.type !== type || value !== undefined && token.value !== value) {
110
+ throw new Error(`Expected ${type}${value ? ` '${value}'` : ''} but got ${token.type} '${token.value}'`);
111
+ }
112
+ return token;
113
+ }
114
+ parse() {
115
+ const expr = this.parseOr();
116
+ if (this.pos < this.tokens.length) {
117
+ throw new Error(`Unexpected token '${this.tokens[this.pos].value}'`);
118
+ }
119
+ return expr;
120
+ }
121
+ parseOr() {
122
+ let left = this.parseAnd();
123
+ while (this.peek()?.value === 'or') {
124
+ this.consume();
125
+ const right = this.parseAnd();
126
+ left = {
127
+ operator: 'or',
128
+ left,
129
+ right
130
+ };
131
+ }
132
+ return left;
133
+ }
134
+ parseAnd() {
135
+ let left = this.parseNot();
136
+ while (this.peek()?.value === 'and') {
137
+ this.consume();
138
+ const right = this.parseNot();
139
+ left = {
140
+ operator: 'and',
141
+ left,
142
+ right
143
+ };
144
+ }
145
+ return left;
146
+ }
147
+ parseNot() {
148
+ if (this.peek()?.value === 'not') {
149
+ this.consume();
150
+ const right = this.parseNot();
151
+ return {
152
+ operator: 'not',
153
+ right
154
+ };
155
+ }
156
+ return this.parsePrimary();
157
+ }
158
+ parsePrimary() {
159
+ const token = this.peek();
160
+ if (!token) throw new Error('Unexpected end of filter expression');
161
+ if (token.type === 'lparen') {
162
+ this.consume();
163
+ const expr = this.parseOr();
164
+ this.expect('rparen');
165
+ return expr;
166
+ }
167
+ if (token.type === 'word' && isQueryFunctionOperator(token.value.toLowerCase())) {
168
+ const func = this.consume().value.toLowerCase();
169
+ this.expect('lparen');
170
+ const left = this.expect('word').value;
171
+ this.expect('comma');
172
+ const right = this.expect('string').value;
173
+ this.expect('rparen');
174
+ return {
175
+ operator: func,
176
+ left,
177
+ right
178
+ };
179
+ }
180
+ if (token.type === 'word') {
181
+ const left = this.consume().value;
182
+ const opToken = this.consume(`a comparison operator`);
183
+ if (!isStandardOperator(opToken.value.toLowerCase())) {
184
+ throw new Error(`Invalid operator '${opToken.value}'`);
185
+ }
186
+ const operator = opToken.value.toLowerCase();
187
+ const right = this.consume(`a value or column name`);
188
+ if (right.type === 'string') {
189
+ return {
190
+ operator,
191
+ left,
192
+ right: right.value
193
+ };
194
+ } else if (right.type === 'number') {
195
+ return {
196
+ operator,
197
+ left,
198
+ right: Number(right.value)
199
+ };
200
+ } else if (right.type === 'word') {
201
+ // Constant keywords stay as StandardOperator; bare identifiers are column comparisons
202
+ const BOOL_CONSTANTS = ['true', 'false'];
203
+ if (BOOL_CONSTANTS.includes(right.value.toLowerCase())) {
204
+ return {
205
+ operator,
206
+ left,
207
+ isBooleanOperation: true,
208
+ right: right.value === 'true'
209
+ };
210
+ } else if (right.value.toLowerCase() === 'null') {
211
+ return {
212
+ operator,
213
+ left,
214
+ isNullOperation: true,
215
+ right: null
216
+ };
217
+ }
218
+ return {
219
+ left,
220
+ operator,
221
+ isColumnOperation: true,
222
+ right: right.value
223
+ };
224
+ } else {
225
+ throw new Error(`Invalid right-hand side value of type '${right.type}' in filter expression`);
226
+ }
227
+ }
228
+ throw new Error(`Unexpected token '${token.value}'`);
229
+ }
230
+ }
9
231
 
10
232
  /**
11
233
  * Parses the {@link ODataFilter.$filter $filter} query
@@ -16,15 +238,19 @@ const getFilterFromParser = (parser, result) => {
16
238
  if (value.length === 0) {
17
239
  return true;
18
240
  }
19
- if (!(0, _atMostOnce.atMostOnce)(option, value, result)) {
241
+ if (!(0, _atMostOnce.atMostOnce)(option, value, result) || !(0, _hasContent.hasContent)(option, value, result)) {
20
242
  return false;
21
243
  }
22
- if (value.length > 0) {
23
- result.$filter = {
24
- operator: 'eq',
25
- left: '',
26
- right: ''
244
+ try {
245
+ const tokens = tokenize(value[0]);
246
+ const p = new FilterParser(tokens);
247
+ result.$filter = p.parse();
248
+ } catch (e) {
249
+ result.error = {
250
+ code: '0x80060888',
251
+ message: `Syntax error in '$filter': ${e.message}`
27
252
  };
253
+ return false;
28
254
  }
29
255
  return true;
30
256
  };
@@ -1,5 +1,227 @@
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 (/[0-9]/.test(input[i]) || input[i] === '-' && /[0-9]/.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 && /[a-zA-Z0-9_]/.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
+ if (!token) throw new Error(`Unexpected end of filter expression${expected ? `, expected ${expected}` : ''}`);
99
+ return token;
100
+ }
101
+ expect(type, value) {
102
+ const token = this.consume(`${type}${value ? ` '${value}'` : ''}`);
103
+ if (token.type !== type || value !== undefined && token.value !== value) {
104
+ throw new Error(`Expected ${type}${value ? ` '${value}'` : ''} but got ${token.type} '${token.value}'`);
105
+ }
106
+ return token;
107
+ }
108
+ parse() {
109
+ const expr = this.parseOr();
110
+ if (this.pos < this.tokens.length) {
111
+ throw new Error(`Unexpected token '${this.tokens[this.pos].value}'`);
112
+ }
113
+ return expr;
114
+ }
115
+ parseOr() {
116
+ let left = this.parseAnd();
117
+ while (this.peek()?.value === 'or') {
118
+ this.consume();
119
+ const right = this.parseAnd();
120
+ left = {
121
+ operator: 'or',
122
+ left,
123
+ right
124
+ };
125
+ }
126
+ return left;
127
+ }
128
+ parseAnd() {
129
+ let left = this.parseNot();
130
+ while (this.peek()?.value === 'and') {
131
+ this.consume();
132
+ const right = this.parseNot();
133
+ left = {
134
+ operator: 'and',
135
+ left,
136
+ right
137
+ };
138
+ }
139
+ return left;
140
+ }
141
+ parseNot() {
142
+ if (this.peek()?.value === 'not') {
143
+ this.consume();
144
+ const right = this.parseNot();
145
+ return {
146
+ operator: 'not',
147
+ right
148
+ };
149
+ }
150
+ return this.parsePrimary();
151
+ }
152
+ parsePrimary() {
153
+ const token = this.peek();
154
+ if (!token) throw new Error('Unexpected end of filter expression');
155
+ if (token.type === 'lparen') {
156
+ this.consume();
157
+ const expr = this.parseOr();
158
+ this.expect('rparen');
159
+ return expr;
160
+ }
161
+ if (token.type === 'word' && isQueryFunctionOperator(token.value.toLowerCase())) {
162
+ const func = this.consume().value.toLowerCase();
163
+ this.expect('lparen');
164
+ const left = this.expect('word').value;
165
+ this.expect('comma');
166
+ const right = this.expect('string').value;
167
+ this.expect('rparen');
168
+ return {
169
+ operator: func,
170
+ left,
171
+ right
172
+ };
173
+ }
174
+ if (token.type === 'word') {
175
+ const left = this.consume().value;
176
+ const opToken = this.consume(`a comparison operator`);
177
+ if (!isStandardOperator(opToken.value.toLowerCase())) {
178
+ throw new Error(`Invalid operator '${opToken.value}'`);
179
+ }
180
+ const operator = opToken.value.toLowerCase();
181
+ const right = this.consume(`a value or column name`);
182
+ if (right.type === 'string') {
183
+ return {
184
+ operator,
185
+ left,
186
+ right: right.value
187
+ };
188
+ } else if (right.type === 'number') {
189
+ return {
190
+ operator,
191
+ left,
192
+ right: Number(right.value)
193
+ };
194
+ } else if (right.type === 'word') {
195
+ // Constant keywords stay as StandardOperator; bare identifiers are column comparisons
196
+ const BOOL_CONSTANTS = ['true', 'false'];
197
+ if (BOOL_CONSTANTS.includes(right.value.toLowerCase())) {
198
+ return {
199
+ operator,
200
+ left,
201
+ isBooleanOperation: true,
202
+ right: right.value === 'true'
203
+ };
204
+ } else if (right.value.toLowerCase() === 'null') {
205
+ return {
206
+ operator,
207
+ left,
208
+ isNullOperation: true,
209
+ right: null
210
+ };
211
+ }
212
+ return {
213
+ left,
214
+ operator,
215
+ isColumnOperation: true,
216
+ right: right.value
217
+ };
218
+ } else {
219
+ throw new Error(`Invalid right-hand side value of type '${right.type}' in filter expression`);
220
+ }
221
+ }
222
+ throw new Error(`Unexpected token '${token.value}'`);
223
+ }
224
+ }
3
225
 
4
226
  /**
5
227
  * Parses the {@link ODataFilter.$filter $filter} query
@@ -10,15 +232,19 @@ export const getFilterFromParser = (parser, result) => {
10
232
  if (value.length === 0) {
11
233
  return true;
12
234
  }
13
- if (!atMostOnce(option, value, result)) {
235
+ if (!atMostOnce(option, value, result) || !hasContent(option, value, result)) {
14
236
  return false;
15
237
  }
16
- if (value.length > 0) {
17
- result.$filter = {
18
- operator: 'eq',
19
- left: '',
20
- right: ''
238
+ try {
239
+ const tokens = tokenize(value[0]);
240
+ const p = new FilterParser(tokens);
241
+ result.$filter = p.parse();
242
+ } catch (e) {
243
+ result.error = {
244
+ code: '0x80060888',
245
+ message: `Syntax error in '$filter': ${e.message}`
21
246
  };
247
+ return false;
22
248
  }
23
249
  return true;
24
250
  };
@@ -1,5 +1,232 @@
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
+ var _input;
18
+ if (/\s/.test(input[i])) {
19
+ i++;
20
+ continue;
21
+ }
22
+ if (input[i] === '(') {
23
+ tokens.push({
24
+ type: 'lparen',
25
+ value: '('
26
+ });
27
+ i++;
28
+ } else if (input[i] === ')') {
29
+ tokens.push({
30
+ type: 'rparen',
31
+ value: ')'
32
+ });
33
+ i++;
34
+ } else if (input[i] === ',') {
35
+ tokens.push({
36
+ type: 'comma',
37
+ value: ','
38
+ });
39
+ i++;
40
+ } else if (input[i] === "'") {
41
+ let j = i + 1;
42
+ let str = '';
43
+ while (j < input.length) {
44
+ if (input[j] === "'" && j + 1 < input.length && input[j + 1] === "'") {
45
+ str += "'";
46
+ j += 2;
47
+ } else if (input[j] === "'") {
48
+ break;
49
+ } else {
50
+ str += input[j];
51
+ j++;
52
+ }
53
+ }
54
+ tokens.push({
55
+ type: 'string',
56
+ value: str
57
+ });
58
+ i = j + 1;
59
+ } else if (/[0-9a-fA-F]/.test(input[i]) && GUID_REGEX.test(input.slice(i))) {
60
+ tokens.push({
61
+ type: 'string',
62
+ value: input.slice(i, i + 36)
63
+ });
64
+ i += 36;
65
+ } else if (/[0-9]/.test(input[i]) || input[i] === '-' && /[0-9]/.test((_input = input[i + 1]) !== null && _input !== void 0 ? _input : '')) {
66
+ let j = i;
67
+ if (input[j] === '-') j++;
68
+ while (j < input.length && /[0-9.]/.test(input[j])) j++;
69
+ tokens.push({
70
+ type: 'number',
71
+ value: input.slice(i, j)
72
+ });
73
+ i = j;
74
+ } else if (/[a-zA-Z_]/.test(input[i])) {
75
+ let j = i;
76
+ while (j < input.length && /[a-zA-Z0-9_]/.test(input[j])) j++;
77
+ tokens.push({
78
+ type: 'word',
79
+ value: input.slice(i, j)
80
+ });
81
+ i = j;
82
+ } else {
83
+ i++;
84
+ }
85
+ }
86
+ return tokens;
87
+ }
88
+ class FilterParser {
89
+ constructor(tokens) {
90
+ this.tokens = void 0;
91
+ this.pos = 0;
92
+ this.tokens = tokens;
93
+ }
94
+ peek() {
95
+ var _this$tokens$this$pos;
96
+ return (_this$tokens$this$pos = this.tokens[this.pos]) !== null && _this$tokens$this$pos !== void 0 ? _this$tokens$this$pos : null;
97
+ }
98
+ consume(expected) {
99
+ const token = this.tokens[this.pos++];
100
+ if (!token) throw new Error(`Unexpected end of filter expression${expected ? `, expected ${expected}` : ''}`);
101
+ return token;
102
+ }
103
+ expect(type, value) {
104
+ const token = this.consume(`${type}${value ? ` '${value}'` : ''}`);
105
+ if (token.type !== type || value !== undefined && token.value !== value) {
106
+ throw new Error(`Expected ${type}${value ? ` '${value}'` : ''} 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 = this.peek()) === null || _this$peek === void 0 ? void 0 : _this$peek.value) === 'or') {
120
+ var _this$peek;
121
+ this.consume();
122
+ const right = this.parseAnd();
123
+ left = {
124
+ operator: 'or',
125
+ left,
126
+ right
127
+ };
128
+ }
129
+ return left;
130
+ }
131
+ parseAnd() {
132
+ let left = this.parseNot();
133
+ while (((_this$peek2 = this.peek()) === null || _this$peek2 === void 0 ? void 0 : _this$peek2.value) === 'and') {
134
+ var _this$peek2;
135
+ this.consume();
136
+ const right = this.parseNot();
137
+ left = {
138
+ operator: 'and',
139
+ left,
140
+ right
141
+ };
142
+ }
143
+ return left;
144
+ }
145
+ parseNot() {
146
+ var _this$peek3;
147
+ if (((_this$peek3 = this.peek()) === null || _this$peek3 === void 0 ? void 0 : _this$peek3.value) === 'not') {
148
+ this.consume();
149
+ const right = this.parseNot();
150
+ return {
151
+ operator: 'not',
152
+ right
153
+ };
154
+ }
155
+ return this.parsePrimary();
156
+ }
157
+ parsePrimary() {
158
+ const token = this.peek();
159
+ if (!token) throw new Error('Unexpected end of filter expression');
160
+ if (token.type === 'lparen') {
161
+ this.consume();
162
+ const expr = this.parseOr();
163
+ this.expect('rparen');
164
+ return expr;
165
+ }
166
+ if (token.type === 'word' && isQueryFunctionOperator(token.value.toLowerCase())) {
167
+ const func = this.consume().value.toLowerCase();
168
+ this.expect('lparen');
169
+ const left = this.expect('word').value;
170
+ this.expect('comma');
171
+ const right = this.expect('string').value;
172
+ this.expect('rparen');
173
+ return {
174
+ operator: func,
175
+ left,
176
+ right
177
+ };
178
+ }
179
+ if (token.type === 'word') {
180
+ const left = this.consume().value;
181
+ const opToken = this.consume(`a comparison operator`);
182
+ if (!isStandardOperator(opToken.value.toLowerCase())) {
183
+ throw new Error(`Invalid operator '${opToken.value}'`);
184
+ }
185
+ const operator = opToken.value.toLowerCase();
186
+ const right = this.consume(`a value or column name`);
187
+ if (right.type === 'string') {
188
+ return {
189
+ operator,
190
+ left,
191
+ right: right.value
192
+ };
193
+ } else if (right.type === 'number') {
194
+ return {
195
+ operator,
196
+ left,
197
+ right: Number(right.value)
198
+ };
199
+ } else if (right.type === 'word') {
200
+ // Constant keywords stay as StandardOperator; bare identifiers are column comparisons
201
+ const BOOL_CONSTANTS = ['true', 'false'];
202
+ if (BOOL_CONSTANTS.includes(right.value.toLowerCase())) {
203
+ return {
204
+ operator,
205
+ left,
206
+ isBooleanOperation: true,
207
+ right: right.value === 'true'
208
+ };
209
+ } else if (right.value.toLowerCase() === 'null') {
210
+ return {
211
+ operator,
212
+ left,
213
+ isNullOperation: true,
214
+ right: null
215
+ };
216
+ }
217
+ return {
218
+ left,
219
+ operator,
220
+ isColumnOperation: true,
221
+ right: right.value
222
+ };
223
+ } else {
224
+ throw new Error(`Invalid right-hand side value of type '${right.type}' in filter expression`);
225
+ }
226
+ }
227
+ throw new Error(`Unexpected token '${token.value}'`);
228
+ }
229
+ }
3
230
 
4
231
  /**
5
232
  * Parses the {@link ODataFilter.$filter $filter} query
@@ -10,15 +237,19 @@ export const getFilterFromParser = (parser, result) => {
10
237
  if (value.length === 0) {
11
238
  return true;
12
239
  }
13
- if (!atMostOnce(option, value, result)) {
240
+ if (!atMostOnce(option, value, result) || !hasContent(option, value, result)) {
14
241
  return false;
15
242
  }
16
- if (value.length > 0) {
17
- result.$filter = {
18
- operator: 'eq',
19
- left: '',
20
- right: ''
243
+ try {
244
+ const tokens = tokenize(value[0]);
245
+ const p = new FilterParser(tokens);
246
+ result.$filter = p.parse();
247
+ } catch (e) {
248
+ result.error = {
249
+ code: '0x80060888',
250
+ message: `Syntax error in '$filter': ${e.message}`
21
251
  };
252
+ return false;
22
253
  }
23
254
  return true;
24
255
  };
@@ -33,7 +33,7 @@ interface ODataFilter {
33
33
  */
34
34
  $filter?: FilterOperator;
35
35
  }
36
- type FilterOperator = StandardOperator | ColumnOperator | UnaryOperator | BinaryOperator | QueryFunctionOperator;
36
+ type FilterOperator = StandardOperator | BooleanColumnOperator | NullOperator | ColumnOperator | UnaryOperator | BinaryOperator | QueryFunctionOperator;
37
37
  interface ODataFetch {
38
38
  /**
39
39
  * You can compose a FetchXML query for a specific table.
@@ -101,9 +101,40 @@ type StandardOperators = StandardEqualityOperators | 'gt' | 'ge' | 'lt' | 'le';
101
101
  * * Microsoft Docs: {@link https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query/filter-rows#column-comparison Column comparison }
102
102
  */
103
103
  interface ColumnOperator {
104
- column: string;
104
+ /**
105
+ * The left side of the 'X' operator must be a property of the entity.
106
+ */
107
+ left: string;
105
108
  operator: StandardOperators;
106
- otherColumn: string;
109
+ isColumnOperation: true;
110
+ /**
111
+ * The right side of the 'X' operator must be a property of the entity.
112
+ */
113
+ right: string;
114
+ }
115
+ interface BooleanColumnOperator {
116
+ /**
117
+ * The left side of the 'X' operator must be a property of the entity.
118
+ */
119
+ left: string;
120
+ operator: StandardOperators;
121
+ isBooleanOperation: true;
122
+ /**
123
+ * The right side of the 'X' operator must be a property of the entity.
124
+ */
125
+ right: boolean;
126
+ }
127
+ interface NullOperator {
128
+ /**
129
+ * The left side of the 'X' operator must be a property of the entity.
130
+ */
131
+ left: string;
132
+ operator: StandardOperators;
133
+ isNullOperation: true;
134
+ /**
135
+ * The right side of the 'X' operator must be null.
136
+ */
137
+ right: null;
107
138
  }
108
139
  interface StandardOperator {
109
140
  operator: StandardOperators;
@@ -138,4 +169,4 @@ interface BinaryOperator {
138
169
  right: FilterOperator;
139
170
  }
140
171
  type ODataQuery = ODataError & ODataExpand & ODataFetch & ODataFilter & ODataOrderBy & ODataSavedQuery & ODataSelect & ODataTop & ODataUserQuery;
141
- export { BinaryOperator, ODataError, ODataExpand, ODataExpandQuery, ODataFetch, ODataFilter, ODataOrderBy, ODataQuery, ODataSavedQuery, ODataSelect, ODataTop, ODataUserQuery, StandardOperator, StandardOperators, UnaryOperator, };
172
+ export { BinaryOperator, ColumnOperator, FilterOperator, ODataError, ODataExpand, ODataExpandQuery, ODataFetch, ODataFilter, ODataOrderBy, ODataQuery, ODataSavedQuery, ODataSelect, ODataTop, ODataUserQuery, QueryFunctionOperator, QueryFunctionOperators, StandardOperator, StandardOperators, UnaryOperator, };
@@ -33,7 +33,7 @@ interface ODataFilter {
33
33
  */
34
34
  $filter?: FilterOperator;
35
35
  }
36
- type FilterOperator = StandardOperator | ColumnOperator | UnaryOperator | BinaryOperator | QueryFunctionOperator;
36
+ type FilterOperator = StandardOperator | BooleanColumnOperator | NullOperator | ColumnOperator | UnaryOperator | BinaryOperator | QueryFunctionOperator;
37
37
  interface ODataFetch {
38
38
  /**
39
39
  * You can compose a FetchXML query for a specific table.
@@ -101,9 +101,40 @@ type StandardOperators = StandardEqualityOperators | 'gt' | 'ge' | 'lt' | 'le';
101
101
  * * Microsoft Docs: {@link https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query/filter-rows#column-comparison Column comparison }
102
102
  */
103
103
  interface ColumnOperator {
104
- column: string;
104
+ /**
105
+ * The left side of the 'X' operator must be a property of the entity.
106
+ */
107
+ left: string;
105
108
  operator: StandardOperators;
106
- otherColumn: string;
109
+ isColumnOperation: true;
110
+ /**
111
+ * The right side of the 'X' operator must be a property of the entity.
112
+ */
113
+ right: string;
114
+ }
115
+ interface BooleanColumnOperator {
116
+ /**
117
+ * The left side of the 'X' operator must be a property of the entity.
118
+ */
119
+ left: string;
120
+ operator: StandardOperators;
121
+ isBooleanOperation: true;
122
+ /**
123
+ * The right side of the 'X' operator must be a property of the entity.
124
+ */
125
+ right: boolean;
126
+ }
127
+ interface NullOperator {
128
+ /**
129
+ * The left side of the 'X' operator must be a property of the entity.
130
+ */
131
+ left: string;
132
+ operator: StandardOperators;
133
+ isNullOperation: true;
134
+ /**
135
+ * The right side of the 'X' operator must be null.
136
+ */
137
+ right: null;
107
138
  }
108
139
  interface StandardOperator {
109
140
  operator: StandardOperators;
@@ -138,4 +169,4 @@ interface BinaryOperator {
138
169
  right: FilterOperator;
139
170
  }
140
171
  type ODataQuery = ODataError & ODataExpand & ODataFetch & ODataFilter & ODataOrderBy & ODataSavedQuery & ODataSelect & ODataTop & ODataUserQuery;
141
- export type { BinaryOperator, ODataError, ODataExpand, ODataExpandQuery, ODataFetch, ODataFilter, ODataOrderBy, ODataQuery, ODataSavedQuery, ODataSelect, ODataTop, ODataUserQuery, StandardOperator, StandardOperators, UnaryOperator, };
172
+ export type { BinaryOperator, ColumnOperator, FilterOperator, ODataError, ODataExpand, ODataExpandQuery, ODataFetch, ODataFilter, ODataOrderBy, ODataQuery, ODataSavedQuery, ODataSelect, ODataTop, ODataUserQuery, QueryFunctionOperator, QueryFunctionOperators, StandardOperator, StandardOperators, UnaryOperator, };
@@ -37,7 +37,14 @@ interface ODataFilter {
37
37
  $filter?: FilterOperator;
38
38
  }
39
39
 
40
- type FilterOperator = StandardOperator | ColumnOperator | UnaryOperator | BinaryOperator | QueryFunctionOperator;
40
+ type FilterOperator =
41
+ | StandardOperator
42
+ | BooleanColumnOperator
43
+ | NullOperator
44
+ | ColumnOperator
45
+ | UnaryOperator
46
+ | BinaryOperator
47
+ | QueryFunctionOperator;
41
48
 
42
49
  interface ODataFetch {
43
50
  /**
@@ -107,15 +114,47 @@ type StandardEqualityOperators = 'eq' | 'ne';
107
114
  type StandardOperators = StandardEqualityOperators | 'gt' | 'ge' | 'lt' | 'le';
108
115
 
109
116
  /**
110
- *
117
+ *
111
118
  * * Microsoft Docs: {@link https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query/filter-rows#column-comparison Column comparison }
112
119
  */
113
120
  interface ColumnOperator {
114
- column: string;
121
+ /**
122
+ * The left side of the 'X' operator must be a property of the entity.
123
+ */
124
+ left: string;
125
+ operator: StandardOperators;
126
+ isColumnOperation: true;
127
+ /**
128
+ * The right side of the 'X' operator must be a property of the entity.
129
+ */
130
+ right: string;
131
+ }
132
+
133
+ interface BooleanColumnOperator {
134
+ /**
135
+ * The left side of the 'X' operator must be a property of the entity.
136
+ */
137
+ left: string;
115
138
  operator: StandardOperators;
116
- otherColumn: string;
139
+ isBooleanOperation: true;
140
+ /**
141
+ * The right side of the 'X' operator must be a property of the entity.
142
+ */
143
+ right: boolean;
117
144
  }
118
145
 
146
+ interface NullOperator {
147
+ /**
148
+ * The left side of the 'X' operator must be a property of the entity.
149
+ */
150
+ left: string;
151
+ operator: StandardOperators;
152
+ isNullOperation: true;
153
+ /**
154
+ * The right side of the 'X' operator must be null.
155
+ */
156
+ right: null;
157
+ }
119
158
 
120
159
  interface StandardOperator {
121
160
  operator: StandardOperators;
@@ -166,6 +205,8 @@ type ODataQuery = ODataError &
166
205
 
167
206
  export type {
168
207
  BinaryOperator,
208
+ ColumnOperator,
209
+ FilterOperator,
169
210
  ODataError,
170
211
  ODataExpand,
171
212
  ODataExpandQuery,
@@ -177,7 +218,9 @@ export type {
177
218
  ODataSelect,
178
219
  ODataTop,
179
220
  ODataUserQuery,
221
+ QueryFunctionOperator,
222
+ QueryFunctionOperators,
180
223
  StandardOperator,
181
224
  StandardOperators,
182
- UnaryOperator,
225
+ UnaryOperator,
183
226
  };
@@ -1 +1 @@
1
- {"version":3,"file":"getFilterFromParser.d.ts","sourceRoot":"","sources":["../../src/getFilterFromParser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,eAAe,CAAC;AAK7D;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ,eAAe,EAAE,QAAQ,UAAU,KAAG,OAajF,CAAC"}
1
+ {"version":3,"file":"getFilterFromParser.d.ts","sourceRoot":"","sources":["../../src/getFilterFromParser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,eAAe,CAAC;AAuMhE;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ,eAAe,EAAE,QAAQ,UAAU,KAAG,OAoBjF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shko.online/dataverse-odata",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "This package will help parse OData strings (only the Microsoft Dataverse subset). It can be used as a validator, or you can build some javascript library which consumes the output of this library.",
5
5
  "scripts": {
6
6
  "build": "npm run lint && build-npm-package",
@@ -37,7 +37,14 @@ interface ODataFilter {
37
37
  $filter?: FilterOperator;
38
38
  }
39
39
 
40
- type FilterOperator = StandardOperator | ColumnOperator | UnaryOperator | BinaryOperator | QueryFunctionOperator;
40
+ type FilterOperator =
41
+ | StandardOperator
42
+ | BooleanColumnOperator
43
+ | NullOperator
44
+ | ColumnOperator
45
+ | UnaryOperator
46
+ | BinaryOperator
47
+ | QueryFunctionOperator;
41
48
 
42
49
  interface ODataFetch {
43
50
  /**
@@ -107,15 +114,47 @@ type StandardEqualityOperators = 'eq' | 'ne';
107
114
  type StandardOperators = StandardEqualityOperators | 'gt' | 'ge' | 'lt' | 'le';
108
115
 
109
116
  /**
110
- *
117
+ *
111
118
  * * Microsoft Docs: {@link https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query/filter-rows#column-comparison Column comparison }
112
119
  */
113
120
  interface ColumnOperator {
114
- column: string;
121
+ /**
122
+ * The left side of the 'X' operator must be a property of the entity.
123
+ */
124
+ left: string;
125
+ operator: StandardOperators;
126
+ isColumnOperation: true;
127
+ /**
128
+ * The right side of the 'X' operator must be a property of the entity.
129
+ */
130
+ right: string;
131
+ }
132
+
133
+ interface BooleanColumnOperator {
134
+ /**
135
+ * The left side of the 'X' operator must be a property of the entity.
136
+ */
137
+ left: string;
115
138
  operator: StandardOperators;
116
- otherColumn: string;
139
+ isBooleanOperation: true;
140
+ /**
141
+ * The right side of the 'X' operator must be a property of the entity.
142
+ */
143
+ right: boolean;
117
144
  }
118
145
 
146
+ interface NullOperator {
147
+ /**
148
+ * The left side of the 'X' operator must be a property of the entity.
149
+ */
150
+ left: string;
151
+ operator: StandardOperators;
152
+ isNullOperation: true;
153
+ /**
154
+ * The right side of the 'X' operator must be null.
155
+ */
156
+ right: null;
157
+ }
119
158
 
120
159
  interface StandardOperator {
121
160
  operator: StandardOperators;
@@ -166,6 +205,8 @@ type ODataQuery = ODataError &
166
205
 
167
206
  export type {
168
207
  BinaryOperator,
208
+ ColumnOperator,
209
+ FilterOperator,
169
210
  ODataError,
170
211
  ODataExpand,
171
212
  ODataExpandQuery,
@@ -177,7 +218,9 @@ export type {
177
218
  ODataSelect,
178
219
  ODataTop,
179
220
  ODataUserQuery,
221
+ QueryFunctionOperator,
222
+ QueryFunctionOperators,
180
223
  StandardOperator,
181
224
  StandardOperators,
182
- UnaryOperator,
225
+ UnaryOperator,
183
226
  };
@@ -1,8 +1,202 @@
1
- import type { ODataQuery, ODataFilter } from './OData.types';
1
+ import type { ODataQuery, FilterOperator } from './OData.types';
2
2
  import { atMostOnce } from './validators/atMostOnce';
3
+ import { hasContent } from './validators/hasContent';
3
4
 
4
5
  const option = '$filter';
5
6
 
7
+ const STANDARD_OPERATORS = ['eq', 'ne', 'gt', 'ge', 'lt', 'le'] as const;
8
+ const QUERY_FUNCTION_OPERATORS = ['contains', 'endswith', 'startswith'] as const;
9
+
10
+ type StandardOp = (typeof STANDARD_OPERATORS)[number];
11
+ type QueryFunctionOp = (typeof QUERY_FUNCTION_OPERATORS)[number];
12
+
13
+ function isStandardOperator(s: string): s is StandardOp {
14
+ return (STANDARD_OPERATORS as readonly string[]).includes(s);
15
+ }
16
+
17
+ function isQueryFunctionOperator(s: string): s is QueryFunctionOp {
18
+ return (QUERY_FUNCTION_OPERATORS as readonly string[]).includes(s);
19
+ }
20
+
21
+ type TokenType = 'lparen' | 'rparen' | 'comma' | 'word' | 'string' | 'number';
22
+
23
+ interface Token {
24
+ type: TokenType;
25
+ value: string;
26
+ }
27
+
28
+ 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}/;
29
+
30
+ function tokenize(input: string): Token[] {
31
+ const tokens: Token[] = [];
32
+ let i = 0;
33
+ while (i < input.length) {
34
+ if (/\s/.test(input[i])) {
35
+ i++;
36
+ continue;
37
+ }
38
+ if (input[i] === '(') {
39
+ tokens.push({ type: 'lparen', value: '(' });
40
+ i++;
41
+ } else if (input[i] === ')') {
42
+ tokens.push({ type: 'rparen', value: ')' });
43
+ i++;
44
+ } else if (input[i] === ',') {
45
+ tokens.push({ type: 'comma', value: ',' });
46
+ i++;
47
+ } else if (input[i] === "'") {
48
+ let j = i + 1;
49
+ let str = '';
50
+ while (j < input.length) {
51
+ if (input[j] === "'" && j + 1 < input.length && input[j + 1] === "'") {
52
+ str += "'";
53
+ j += 2;
54
+ } else if (input[j] === "'") {
55
+ break;
56
+ } else {
57
+ str += input[j];
58
+ j++;
59
+ }
60
+ }
61
+ tokens.push({ type: 'string', value: str });
62
+ i = j + 1;
63
+ } else if (/[0-9a-fA-F]/.test(input[i]) && GUID_REGEX.test(input.slice(i))) {
64
+ tokens.push({ type: 'string', value: input.slice(i, i + 36) });
65
+ i += 36;
66
+ } else if (/[0-9]/.test(input[i]) || (input[i] === '-' && /[0-9]/.test(input[i + 1] ?? ''))) {
67
+ let j = i;
68
+ if (input[j] === '-') j++;
69
+ while (j < input.length && /[0-9.]/.test(input[j])) j++;
70
+ tokens.push({ type: 'number', value: input.slice(i, j) });
71
+ i = j;
72
+ } else if (/[a-zA-Z_]/.test(input[i])) {
73
+ let j = i;
74
+ while (j < input.length && /[a-zA-Z0-9_]/.test(input[j])) j++;
75
+ tokens.push({ type: 'word', value: input.slice(i, j) });
76
+ i = j;
77
+ } else {
78
+ i++;
79
+ }
80
+ }
81
+ return tokens;
82
+ }
83
+
84
+ class FilterParser {
85
+ private tokens: Token[];
86
+ private pos = 0;
87
+
88
+ constructor(tokens: Token[]) {
89
+ this.tokens = tokens;
90
+ }
91
+
92
+ private peek(): Token | null {
93
+ return this.tokens[this.pos] ?? null;
94
+ }
95
+
96
+ private consume(expected?: string): Token {
97
+ const token = this.tokens[this.pos++];
98
+ if (!token) throw new Error(`Unexpected end of filter expression${expected ? `, expected ${expected}` : ''}`);
99
+ return token;
100
+ }
101
+
102
+ private expect(type: TokenType, value?: string): Token {
103
+ const token = this.consume(`${type}${value ? ` '${value}'` : ''}`);
104
+ if (token.type !== type || (value !== undefined && token.value !== value)) {
105
+ throw new Error(
106
+ `Expected ${type}${value ? ` '${value}'` : ''} but got ${token.type} '${token.value}'`,
107
+ );
108
+ }
109
+ return token;
110
+ }
111
+
112
+ parse(): FilterOperator {
113
+ const expr = this.parseOr();
114
+ if (this.pos < this.tokens.length) {
115
+ throw new Error(`Unexpected token '${this.tokens[this.pos].value}'`);
116
+ }
117
+ return expr;
118
+ }
119
+
120
+ private parseOr(): FilterOperator {
121
+ let left = this.parseAnd();
122
+ while (this.peek()?.value === 'or') {
123
+ this.consume();
124
+ const right = this.parseAnd();
125
+ left = { operator: 'or', left, right };
126
+ }
127
+ return left;
128
+ }
129
+
130
+ private parseAnd(): FilterOperator {
131
+ let left = this.parseNot();
132
+ while (this.peek()?.value === 'and') {
133
+ this.consume();
134
+ const right = this.parseNot();
135
+ left = { operator: 'and', left, right };
136
+ }
137
+ return left;
138
+ }
139
+
140
+ private parseNot(): FilterOperator {
141
+ if (this.peek()?.value === 'not') {
142
+ this.consume();
143
+ const right = this.parseNot();
144
+ return { operator: 'not', right };
145
+ }
146
+ return this.parsePrimary();
147
+ }
148
+
149
+ private parsePrimary(): FilterOperator {
150
+ const token = this.peek();
151
+ if (!token) throw new Error('Unexpected end of filter expression');
152
+
153
+ if (token.type === 'lparen') {
154
+ this.consume();
155
+ const expr = this.parseOr();
156
+ this.expect('rparen');
157
+ return expr;
158
+ }
159
+
160
+ if (token.type === 'word' && isQueryFunctionOperator(token.value.toLowerCase())) {
161
+ const func = this.consume().value.toLowerCase() as QueryFunctionOp;
162
+ this.expect('lparen');
163
+ const left = this.expect('word').value;
164
+ this.expect('comma');
165
+ const right = this.expect('string').value;
166
+ this.expect('rparen');
167
+ return { operator: func, left, right };
168
+ }
169
+
170
+ if (token.type === 'word') {
171
+ const left = this.consume().value;
172
+ const opToken = this.consume(`a comparison operator`);
173
+ if (!isStandardOperator(opToken.value.toLowerCase())) {
174
+ throw new Error(`Invalid operator '${opToken.value}'`);
175
+ }
176
+ const operator = opToken.value.toLowerCase() as StandardOp;
177
+ const right = this.consume(`a value or column name`);
178
+ if (right.type === 'string') {
179
+ return { operator, left, right: right.value };
180
+ } else if (right.type === 'number') {
181
+ return { operator, left, right: Number(right.value) };
182
+ } else if (right.type === 'word') {
183
+ // Constant keywords stay as StandardOperator; bare identifiers are column comparisons
184
+ const BOOL_CONSTANTS = ['true', 'false'];
185
+ if (BOOL_CONSTANTS.includes(right.value.toLowerCase())) {
186
+ return { operator, left, isBooleanOperation: true, right: right.value === 'true' };
187
+ }else if (right.value.toLowerCase() === 'null') {
188
+ return { operator, left, isNullOperation: true, right: null };
189
+ }
190
+ return { left, operator, isColumnOperation: true, right: right.value };
191
+ } else {
192
+ throw new Error(`Invalid right-hand side value of type '${right.type}' in filter expression`);
193
+ }
194
+ }
195
+
196
+ throw new Error(`Unexpected token '${token.value}'`);
197
+ }
198
+ }
199
+
6
200
  /**
7
201
  * Parses the {@link ODataFilter.$filter $filter} query
8
202
  * @returns {boolean} Returns `false` when the parse has an error
@@ -12,12 +206,19 @@ export const getFilterFromParser = (parser: URLSearchParams, result: ODataQuery)
12
206
  if (value.length === 0) {
13
207
  return true;
14
208
  }
15
- if (!atMostOnce(option, value, result)) {
209
+ if (!atMostOnce(option, value, result) || !hasContent(option, value, result)) {
16
210
  return false;
17
211
  }
18
- if (value.length > 0) {
19
-
20
- result.$filter = {operator: 'eq', left: '', right: ''};
212
+ try {
213
+ const tokens = tokenize(value[0]);
214
+ const p = new FilterParser(tokens);
215
+ result.$filter = p.parse();
216
+ } catch (e) {
217
+ result.error = {
218
+ code: '0x80060888',
219
+ message: `Syntax error in '$filter': ${(e as Error).message}`,
220
+ };
221
+ return false;
21
222
  }
22
223
  return true;
23
224
  };