@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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "js/ts.tsdk.path": "node_modules\\typescript\\lib"
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
 
@@ -2,9 +2,9 @@ pool:
2
2
  vmImage: ubuntu-latest
3
3
 
4
4
  steps:
5
- - task: NodeTool@0
5
+ - task: UseNode@1
6
6
  inputs:
7
- versionSpec: '24.x'
7
+ version: '24.x'
8
8
  displayName: 'Install Node.js'
9
9
 
10
10
  - task: Npm@1
@@ -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 || !entity.getAttribute('name')) {
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
- if (value.length > 0) {
23
- result.$filter = {
24
- operator: 'eq',
25
- left: '',
26
- right: ''
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.default = void 0;
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 || !entity.getAttribute('name')) {
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
- if (value.length > 0) {
17
- result.$filter = {
18
- operator: 'eq',
19
- left: '',
20
- right: ''
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 || !entity.getAttribute('name')) {
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.'