@shko.online/dataverse-odata 0.1.5 → 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/.github/workflows/publish.yml +3 -3
- package/CHANGELOG.md +20 -0
- package/azure-pipelines.yml +1 -1
- package/lib/cjs/OData.types.d.js +1 -0
- package/lib/cjs/getAliasedProperty.js +36 -0
- package/lib/cjs/getExpandFromParser.js +122 -0
- package/lib/cjs/getFetchXmlFromParser.js +52 -0
- package/lib/cjs/getFilterFromParser.js +257 -0
- package/lib/cjs/getOrderByFromParser.js +78 -0
- package/lib/cjs/getSelectFromParser.js +27 -0
- package/lib/cjs/getTopFromParser.js +40 -0
- package/lib/cjs/getXQueryFromParser.js +25 -0
- package/lib/cjs/index.js +56 -0
- package/lib/cjs/parseOData.js +25 -0
- package/lib/cjs/validators/atMostOnce.js +24 -0
- package/lib/cjs/validators/differentFromEmptyString.js +23 -0
- package/lib/cjs/validators/hasContent.js +24 -0
- package/lib/cjs/validators/isGuid.js +25 -0
- package/lib/cjs/validators/recognizedGuid.js +23 -0
- package/lib/esm/OData.types.d.js +0 -0
- package/lib/esm/getAliasedProperty.js +29 -0
- package/lib/esm/getExpandFromParser.js +115 -0
- package/lib/esm/getFetchXmlFromParser.js +45 -0
- package/lib/esm/getFilterFromParser.js +250 -0
- package/lib/esm/getOrderByFromParser.js +71 -0
- package/lib/esm/getSelectFromParser.js +20 -0
- package/lib/esm/getTopFromParser.js +33 -0
- package/lib/esm/getXQueryFromParser.js +19 -0
- package/lib/esm/index.js +9 -0
- package/lib/esm/parseOData.js +19 -0
- package/lib/esm/validators/atMostOnce.js +17 -0
- package/lib/esm/validators/differentFromEmptyString.js +16 -0
- package/lib/esm/validators/hasContent.js +17 -0
- package/lib/esm/validators/isGuid.js +18 -0
- package/lib/esm/validators/recognizedGuid.js +16 -0
- package/lib/modern/OData.types.d.js +0 -0
- package/lib/modern/getAliasedProperty.js +29 -0
- package/lib/modern/getExpandFromParser.js +115 -0
- package/lib/modern/getFetchXmlFromParser.js +45 -0
- package/lib/modern/getFilterFromParser.js +255 -0
- package/lib/modern/getOrderByFromParser.js +72 -0
- package/lib/modern/getSelectFromParser.js +20 -0
- package/lib/modern/getTopFromParser.js +33 -0
- package/lib/modern/getXQueryFromParser.js +19 -0
- package/lib/modern/index.js +9 -0
- package/lib/modern/parseOData.js +19 -0
- package/lib/modern/validators/atMostOnce.js +17 -0
- package/lib/modern/validators/differentFromEmptyString.js +16 -0
- package/lib/modern/validators/hasContent.js +17 -0
- package/lib/modern/validators/isGuid.js +18 -0
- package/lib/modern/validators/recognizedGuid.js +16 -0
- package/lib/ts3.4/OData.types.d.ts +172 -0
- package/lib/ts3.4/getAliasedProperty.d.ts +10 -0
- package/lib/ts3.4/getExpandFromParser.d.ts +7 -0
- package/lib/ts3.4/getFetchXmlFromParser.d.ts +7 -0
- package/lib/ts3.4/getFilterFromParser.d.ts +7 -0
- package/lib/ts3.4/getOrderByFromParser.d.ts +7 -0
- package/lib/ts3.4/getSelectFromParser.d.ts +7 -0
- package/lib/ts3.4/getTopFromParser.d.ts +7 -0
- package/lib/ts3.4/getXQueryFromParser.d.ts +8 -0
- package/lib/ts3.4/index.d.ts +11 -0
- package/lib/ts3.4/parseOData.d.ts +8 -0
- package/lib/ts3.4/validators/atMostOnce.d.ts +10 -0
- package/lib/ts3.4/validators/differentFromEmptyString.d.ts +9 -0
- package/lib/ts3.4/validators/hasContent.d.ts +10 -0
- package/lib/ts3.4/validators/isGuid.d.ts +9 -0
- package/lib/ts3.4/validators/recognizedGuid.d.ts +9 -0
- package/lib/ts3.9/OData.types.d.ts +172 -0
- package/lib/ts3.9/getAliasedProperty.d.ts +10 -0
- package/lib/ts3.9/getExpandFromParser.d.ts +7 -0
- package/lib/ts3.9/getFetchXmlFromParser.d.ts +7 -0
- package/lib/ts3.9/getFilterFromParser.d.ts +7 -0
- package/lib/ts3.9/getOrderByFromParser.d.ts +7 -0
- package/lib/ts3.9/getSelectFromParser.d.ts +7 -0
- package/lib/ts3.9/getTopFromParser.d.ts +7 -0
- package/lib/ts3.9/getXQueryFromParser.d.ts +8 -0
- package/lib/ts3.9/index.d.ts +11 -0
- package/lib/ts3.9/parseOData.d.ts +8 -0
- package/lib/ts3.9/validators/atMostOnce.d.ts +10 -0
- package/lib/ts3.9/validators/differentFromEmptyString.d.ts +9 -0
- package/lib/ts3.9/validators/hasContent.d.ts +10 -0
- package/lib/ts3.9/validators/isGuid.d.ts +9 -0
- package/lib/ts3.9/validators/recognizedGuid.d.ts +9 -0
- package/lib/ts4.2/OData.types.d.ts +226 -0
- package/lib/ts4.2/getAliasedProperty.d.ts +10 -0
- package/lib/ts4.2/getAliasedProperty.d.ts.map +1 -0
- package/lib/ts4.2/getExpandFromParser.d.ts +7 -0
- package/lib/ts4.2/getExpandFromParser.d.ts.map +1 -0
- package/lib/ts4.2/getFetchXmlFromParser.d.ts +7 -0
- package/lib/ts4.2/getFetchXmlFromParser.d.ts.map +1 -0
- package/lib/ts4.2/getFilterFromParser.d.ts +7 -0
- package/lib/ts4.2/getFilterFromParser.d.ts.map +1 -0
- package/lib/ts4.2/getOrderByFromParser.d.ts +7 -0
- package/lib/ts4.2/getOrderByFromParser.d.ts.map +1 -0
- package/lib/ts4.2/getSelectFromParser.d.ts +7 -0
- package/lib/ts4.2/getSelectFromParser.d.ts.map +1 -0
- package/lib/ts4.2/getTopFromParser.d.ts +7 -0
- package/lib/ts4.2/getTopFromParser.d.ts.map +1 -0
- package/lib/ts4.2/getXQueryFromParser.d.ts +8 -0
- package/lib/ts4.2/getXQueryFromParser.d.ts.map +1 -0
- package/lib/ts4.2/index.d.ts +11 -0
- package/lib/ts4.2/index.d.ts.map +1 -0
- package/lib/ts4.2/parseOData.d.ts +8 -0
- package/lib/ts4.2/parseOData.d.ts.map +1 -0
- package/lib/ts4.2/validators/atMostOnce.d.ts +10 -0
- package/lib/ts4.2/validators/atMostOnce.d.ts.map +1 -0
- package/lib/ts4.2/validators/differentFromEmptyString.d.ts +9 -0
- package/lib/ts4.2/validators/differentFromEmptyString.d.ts.map +1 -0
- package/lib/ts4.2/validators/hasContent.d.ts +10 -0
- package/lib/ts4.2/validators/hasContent.d.ts.map +1 -0
- package/lib/ts4.2/validators/isGuid.d.ts +9 -0
- package/lib/ts4.2/validators/isGuid.d.ts.map +1 -0
- package/lib/ts4.2/validators/recognizedGuid.d.ts +9 -0
- package/lib/ts4.2/validators/recognizedGuid.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/OData.types.d.ts +48 -5
- package/src/getFilterFromParser.ts +206 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
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
|
+
|
|
14
|
+
## [0.1.6](https://github.com/Shko-Online/dataverse-odata/compare/v0.1.5...v0.1.6) (2026-03-15)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* build before publish ([abf325d](https://github.com/Shko-Online/dataverse-odata/commit/abf325da1f8f7ee2d1ba44f35f0b182de9d9cb6a))
|
|
20
|
+
|
|
1
21
|
## [0.1.5](https://github.com/Shko-Online/dataverse-odata/compare/v0.1.4...v0.1.5) (2026-03-15)
|
|
2
22
|
|
|
3
23
|
|
package/azure-pipelines.yml
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getAliasedProperty = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Recursively gets the value of an aliased property. For example, if the query is `$orderby=@p1` and `@p1=name`, this function will return `name`
|
|
9
|
+
* @param parser The URLSearchParams object containing the query parameters
|
|
10
|
+
* @param result Will contain the error details in case there is any
|
|
11
|
+
* @param property The property to expand
|
|
12
|
+
* @returns The expanded property or null when there is an error
|
|
13
|
+
*/
|
|
14
|
+
const getAliasedProperty = (parser, result, property) => {
|
|
15
|
+
let propertyName = parser.get(property);
|
|
16
|
+
if (!propertyName) {
|
|
17
|
+
result.error = {
|
|
18
|
+
code: '0x80060888',
|
|
19
|
+
message: 'Order By Property must be of type EdmProperty'
|
|
20
|
+
};
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (!/^[@a-zA-Z]\w+/gi.test(propertyName)) {
|
|
24
|
+
const position = propertyName.length;
|
|
25
|
+
result.error = {
|
|
26
|
+
code: '0x80060888',
|
|
27
|
+
message: `Syntax error at position ${position} in '${propertyName}'.`
|
|
28
|
+
};
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (propertyName.startsWith('@')) {
|
|
32
|
+
return getAliasedProperty(parser, result, propertyName);
|
|
33
|
+
}
|
|
34
|
+
return propertyName;
|
|
35
|
+
};
|
|
36
|
+
exports.getAliasedProperty = getAliasedProperty;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getExpandFromParser = void 0;
|
|
7
|
+
var _getSelectFromParser = require("./getSelectFromParser");
|
|
8
|
+
var _atMostOnce = require("./validators/atMostOnce");
|
|
9
|
+
const option = '$expand';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parses the {@link ODataExpand.$expand $expand} query
|
|
13
|
+
* @returns Returns `false` when the parse has an error
|
|
14
|
+
*/
|
|
15
|
+
const getExpandFromParser = (parser, result) => {
|
|
16
|
+
const value = parser.getAll(option);
|
|
17
|
+
if (value.length === 0) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (!(0, _atMostOnce.atMostOnce)(option, value, result)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
result.$expand = {};
|
|
24
|
+
if (!extractExpand(value[0], result)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
};
|
|
29
|
+
exports.getExpandFromParser = getExpandFromParser;
|
|
30
|
+
const extractExpand = (value, $expand) => {
|
|
31
|
+
const match = value.match(/^\s*(\w(\w|\d|_)*)\s*(,|\(|\))?\s*/);
|
|
32
|
+
if (match === null || match[0].length < value.length && match[3] === null || match[0].length === value.length && match[3] !== undefined) {
|
|
33
|
+
$expand.error = {
|
|
34
|
+
code: '0x0',
|
|
35
|
+
message: `Term '${value}' is not valid in a $select or $expand expression.`
|
|
36
|
+
};
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
let matchSeparator = match[3];
|
|
40
|
+
let matchLength = match[0].length;
|
|
41
|
+
if (matchSeparator !== '(') {
|
|
42
|
+
if ($expand.$expand !== undefined) {
|
|
43
|
+
$expand.$expand[match[1]] = {
|
|
44
|
+
$select: []
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
const {
|
|
49
|
+
index,
|
|
50
|
+
error
|
|
51
|
+
} = getClosingBracket(value.substring(matchLength));
|
|
52
|
+
if (error) {
|
|
53
|
+
$expand.error = {
|
|
54
|
+
code: '0x0',
|
|
55
|
+
message: error
|
|
56
|
+
};
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if ($expand.$expand !== undefined) {
|
|
60
|
+
const innerExpand = {};
|
|
61
|
+
const parser = new URLSearchParams('?' + value.substring(matchLength, matchLength + index));
|
|
62
|
+
if (!(0, _getSelectFromParser.getSelectFromParser)(parser, innerExpand)) {
|
|
63
|
+
$expand.error = innerExpand.error;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (!getExpandFromParser(parser, innerExpand)) {
|
|
67
|
+
$expand.error = innerExpand.error;
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (innerExpand.$expand === undefined && innerExpand.$select === undefined) {
|
|
71
|
+
$expand.error = {
|
|
72
|
+
code: '0x0',
|
|
73
|
+
message: `Missing expand option on navigation property '${match[1]}'. If a parenthesis expression follows an expanded navigation property, then at least one expand option must be provided.`
|
|
74
|
+
};
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
$expand.$expand[match[1]] = innerExpand;
|
|
78
|
+
}
|
|
79
|
+
matchLength = matchLength + index;
|
|
80
|
+
const secondMatch = new RegExp(/\s*(,?)\s*d/).exec(value.substring(matchLength + 1));
|
|
81
|
+
if (secondMatch !== null) {
|
|
82
|
+
matchLength = matchLength + secondMatch[0].length;
|
|
83
|
+
if (secondMatch[1] !== null) {
|
|
84
|
+
matchSeparator = ',';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (matchSeparator === ',') {
|
|
89
|
+
if (!extractExpand(value.substring(matchLength), $expand)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
};
|
|
95
|
+
const getClosingBracket = value => {
|
|
96
|
+
let depth = 1;
|
|
97
|
+
let startAt = 0;
|
|
98
|
+
while (depth > 0) {
|
|
99
|
+
const match = value.substring(startAt).match(/\(|\)/);
|
|
100
|
+
if (match === null) {
|
|
101
|
+
return {
|
|
102
|
+
error: 'Found an unbalanced bracket expression.',
|
|
103
|
+
index: -1
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (match[0] === ')') {
|
|
107
|
+
depth -= 1;
|
|
108
|
+
if (depth === 0) {
|
|
109
|
+
return {
|
|
110
|
+
index: match.index || 0
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
depth += 1;
|
|
115
|
+
}
|
|
116
|
+
startAt += (match.index || 0) + 1;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
error: 'Found an unbalanced bracket expression.',
|
|
120
|
+
index: -1
|
|
121
|
+
};
|
|
122
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getFetchXmlFromParser = void 0;
|
|
7
|
+
var _atMostOnce = require("./validators/atMostOnce");
|
|
8
|
+
var _differentFromEmptyString = require("./validators/differentFromEmptyString");
|
|
9
|
+
const option = 'fetchXml';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parses the {@link ODataFetch.fetchXml fetchXml} query
|
|
13
|
+
* @returns Returns `false` when the parse has an error
|
|
14
|
+
*/
|
|
15
|
+
const getFetchXmlFromParser = (parser, result) => {
|
|
16
|
+
const value = parser.getAll(option);
|
|
17
|
+
if (value.length === 0) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (!(0, _atMostOnce.atMostOnce)(option, value, result) || !(0, _differentFromEmptyString.differentFromEmptyString)(value, result)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const fetchXml = value[0];
|
|
24
|
+
const serializer = new DOMParser();
|
|
25
|
+
const fetchXmlDocument = serializer.parseFromString(fetchXml, 'text/xml');
|
|
26
|
+
if (fetchXmlDocument.documentElement.tagName === 'parsererror') {
|
|
27
|
+
result.error = {
|
|
28
|
+
code: '0x80040201',
|
|
29
|
+
message: 'Invalid XML.'
|
|
30
|
+
};
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
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')) {
|
|
35
|
+
result.error = {
|
|
36
|
+
code: '0x80041102',
|
|
37
|
+
message: 'Entity Name was not specified in FetchXml String.'
|
|
38
|
+
};
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const invalidAttribute = fetchXmlDocument.evaluate('fetch/entity/*[not(self::filter or self::order or self::link-entity or self::attribute or self::all-attributes or self::no-attrs)]', fetchXmlDocument, null, XPathResult.ANY_TYPE, null).iterateNext();
|
|
42
|
+
if (invalidAttribute) {
|
|
43
|
+
result.error = {
|
|
44
|
+
code: '0x8004111c',
|
|
45
|
+
message: `Invalid Child Node, valid nodes are filter, order, link-entity, attribute, all-attributes, no-attrs. NodeName = ${invalidAttribute.tagName} NodeXml = ${invalidAttribute.outerHTML}`
|
|
46
|
+
};
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
result.fetchXml = fetchXmlDocument;
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
52
|
+
exports.getFetchXmlFromParser = getFetchXmlFromParser;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getFilterFromParser = void 0;
|
|
7
|
+
var _atMostOnce = require("./validators/atMostOnce");
|
|
8
|
+
var _hasContent = require("./validators/hasContent");
|
|
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
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Parses the {@link ODataFilter.$filter $filter} query
|
|
234
|
+
* @returns {boolean} Returns `false` when the parse has an error
|
|
235
|
+
*/
|
|
236
|
+
const getFilterFromParser = (parser, result) => {
|
|
237
|
+
const value = parser.getAll(option);
|
|
238
|
+
if (value.length === 0) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
if (!(0, _atMostOnce.atMostOnce)(option, value, result) || !(0, _hasContent.hasContent)(option, value, result)) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
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}`
|
|
252
|
+
};
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
return true;
|
|
256
|
+
};
|
|
257
|
+
exports.getFilterFromParser = getFilterFromParser;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getOrderByFromParser = void 0;
|
|
7
|
+
var _getAliasedProperty = require("./getAliasedProperty");
|
|
8
|
+
var _atMostOnce = require("./validators/atMostOnce");
|
|
9
|
+
var _hasContent = require("./validators/hasContent");
|
|
10
|
+
const option = '$orderby';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parses the {@link ODataOrderBy.$orderby $orderby} query
|
|
14
|
+
* @returns Returns `false` when the parse has an error
|
|
15
|
+
*/
|
|
16
|
+
const getOrderByFromParser = (parser, result) => {
|
|
17
|
+
let value = parser.getAll(option);
|
|
18
|
+
if (value.length === 0) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (!(0, _atMostOnce.atMostOnce)(option, value, result) || !(0, _hasContent.hasContent)(option, value, result)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
let $orderby = value[0].trimEnd();
|
|
25
|
+
let $orderbyParts = $orderby.split(',');
|
|
26
|
+
const orderByArray = [];
|
|
27
|
+
let position = 0;
|
|
28
|
+
for (const element of $orderbyParts) {
|
|
29
|
+
const parts = Array.from(element.matchAll(/\s*(\S+)/gi));
|
|
30
|
+
if (parts.length > 2) {
|
|
31
|
+
position = position + parts[0][0].length + parts[1][0].length + parts[2][0].length;
|
|
32
|
+
result.error = {
|
|
33
|
+
code: '0x80060888',
|
|
34
|
+
message: `Syntax error at position ${position} in '${$orderby}'.`
|
|
35
|
+
};
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (!/^[@a-zA-Z]\w+/gi.test(parts[0][1])) {
|
|
39
|
+
position = position + parts[0][0].length;
|
|
40
|
+
result.error = {
|
|
41
|
+
code: '0x80060888',
|
|
42
|
+
message: `Syntax error at position ${position} in '${$orderby}'.`
|
|
43
|
+
};
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const orderBy = {
|
|
47
|
+
column: parts[0][1],
|
|
48
|
+
asc: true // default is ascending
|
|
49
|
+
};
|
|
50
|
+
if (parts[0][1].startsWith('@')) {
|
|
51
|
+
orderBy.column = (0, _getAliasedProperty.getAliasedProperty)(parser, result, parts[0][1]);
|
|
52
|
+
if (!orderBy.column) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (parts.length === 1) {
|
|
57
|
+
orderByArray.push(orderBy);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (parts[1][1].toLowerCase() === 'asc') {
|
|
61
|
+
orderBy.asc = true;
|
|
62
|
+
orderByArray.push(orderBy);
|
|
63
|
+
} else if (parts[1][1].toLowerCase() === 'desc') {
|
|
64
|
+
orderBy.asc = false;
|
|
65
|
+
orderByArray.push(orderBy);
|
|
66
|
+
} else {
|
|
67
|
+
position = position + parts[0][0].length + parts[1][0].length;
|
|
68
|
+
result.error = {
|
|
69
|
+
code: '0x80060888',
|
|
70
|
+
message: `Syntax error at position ${position} in '${$orderby}'.`
|
|
71
|
+
};
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
result.$orderby = orderByArray;
|
|
76
|
+
return true;
|
|
77
|
+
};
|
|
78
|
+
exports.getOrderByFromParser = getOrderByFromParser;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getSelectFromParser = void 0;
|
|
7
|
+
var _atMostOnce = require("./validators/atMostOnce");
|
|
8
|
+
const option = '$select';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parses the {@link ODataSelect.$select $select} query
|
|
12
|
+
* @returns {boolean} Returns `false` when the parse has an error
|
|
13
|
+
*/
|
|
14
|
+
const getSelectFromParser = (parser, result) => {
|
|
15
|
+
const value = parser.getAll(option);
|
|
16
|
+
if (value.length === 0) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (!(0, _atMostOnce.atMostOnce)(option, value, result)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (value.length > 0) {
|
|
23
|
+
result.$select = value[0].split(',');
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
};
|
|
27
|
+
exports.getSelectFromParser = getSelectFromParser;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getTopFromParser = void 0;
|
|
7
|
+
var _atMostOnce = require("./validators/atMostOnce");
|
|
8
|
+
var _hasContent = require("./validators/hasContent");
|
|
9
|
+
const option = '$top';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parses the {@link ODataTop.$top $top} query
|
|
13
|
+
* @returns Returns `false` when the parse has an error
|
|
14
|
+
*/
|
|
15
|
+
const getTopFromParser = (parser, result) => {
|
|
16
|
+
const value = parser.getAll(option);
|
|
17
|
+
if (value.length === 0) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (!(0, _atMostOnce.atMostOnce)(option, value, result) || !(0, _hasContent.hasContent)(option, value, result)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
let $top;
|
|
24
|
+
if (!value[0].match(/^\d+$/) || ($top = parseInt(value[0])) < 0) {
|
|
25
|
+
result.error = {
|
|
26
|
+
code: '0x0',
|
|
27
|
+
message: `Invalid value '${value}' for $top query option found. The $top query option requires a non-negative integer value.`
|
|
28
|
+
};
|
|
29
|
+
return false;
|
|
30
|
+
} else if ($top === 0) {
|
|
31
|
+
result.error = {
|
|
32
|
+
code: '0x0',
|
|
33
|
+
message: `Invalid value for $top query option.`
|
|
34
|
+
};
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
result.$top = $top;
|
|
38
|
+
return true;
|
|
39
|
+
};
|
|
40
|
+
exports.getTopFromParser = getTopFromParser;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getXQueryFromParser = void 0;
|
|
7
|
+
var _isGuid = require("./validators/isGuid");
|
|
8
|
+
var _recognizedGuid = require("./validators/recognizedGuid");
|
|
9
|
+
/**
|
|
10
|
+
* Parses the {@link ODataSavedQuery.savedQuery savedQuery} or
|
|
11
|
+
* {@link ODataUserQuery.userQuery userQuery} query
|
|
12
|
+
* @returns Returns `false` when the parse has an error
|
|
13
|
+
*/
|
|
14
|
+
const getXQueryFromParser = (X, parser, result) => {
|
|
15
|
+
const value = parser.getAll(X);
|
|
16
|
+
if (value.length === 0) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (!(0, _recognizedGuid.recognizedGuid)(value, result) || !(0, _isGuid.isGuid)(value, result)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
result[X] = value[0];
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
25
|
+
exports.getXQueryFromParser = getXQueryFromParser;
|