@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.
Files changed (117) hide show
  1. package/.github/workflows/publish.yml +3 -3
  2. package/CHANGELOG.md +20 -0
  3. package/azure-pipelines.yml +1 -1
  4. package/lib/cjs/OData.types.d.js +1 -0
  5. package/lib/cjs/getAliasedProperty.js +36 -0
  6. package/lib/cjs/getExpandFromParser.js +122 -0
  7. package/lib/cjs/getFetchXmlFromParser.js +52 -0
  8. package/lib/cjs/getFilterFromParser.js +257 -0
  9. package/lib/cjs/getOrderByFromParser.js +78 -0
  10. package/lib/cjs/getSelectFromParser.js +27 -0
  11. package/lib/cjs/getTopFromParser.js +40 -0
  12. package/lib/cjs/getXQueryFromParser.js +25 -0
  13. package/lib/cjs/index.js +56 -0
  14. package/lib/cjs/parseOData.js +25 -0
  15. package/lib/cjs/validators/atMostOnce.js +24 -0
  16. package/lib/cjs/validators/differentFromEmptyString.js +23 -0
  17. package/lib/cjs/validators/hasContent.js +24 -0
  18. package/lib/cjs/validators/isGuid.js +25 -0
  19. package/lib/cjs/validators/recognizedGuid.js +23 -0
  20. package/lib/esm/OData.types.d.js +0 -0
  21. package/lib/esm/getAliasedProperty.js +29 -0
  22. package/lib/esm/getExpandFromParser.js +115 -0
  23. package/lib/esm/getFetchXmlFromParser.js +45 -0
  24. package/lib/esm/getFilterFromParser.js +250 -0
  25. package/lib/esm/getOrderByFromParser.js +71 -0
  26. package/lib/esm/getSelectFromParser.js +20 -0
  27. package/lib/esm/getTopFromParser.js +33 -0
  28. package/lib/esm/getXQueryFromParser.js +19 -0
  29. package/lib/esm/index.js +9 -0
  30. package/lib/esm/parseOData.js +19 -0
  31. package/lib/esm/validators/atMostOnce.js +17 -0
  32. package/lib/esm/validators/differentFromEmptyString.js +16 -0
  33. package/lib/esm/validators/hasContent.js +17 -0
  34. package/lib/esm/validators/isGuid.js +18 -0
  35. package/lib/esm/validators/recognizedGuid.js +16 -0
  36. package/lib/modern/OData.types.d.js +0 -0
  37. package/lib/modern/getAliasedProperty.js +29 -0
  38. package/lib/modern/getExpandFromParser.js +115 -0
  39. package/lib/modern/getFetchXmlFromParser.js +45 -0
  40. package/lib/modern/getFilterFromParser.js +255 -0
  41. package/lib/modern/getOrderByFromParser.js +72 -0
  42. package/lib/modern/getSelectFromParser.js +20 -0
  43. package/lib/modern/getTopFromParser.js +33 -0
  44. package/lib/modern/getXQueryFromParser.js +19 -0
  45. package/lib/modern/index.js +9 -0
  46. package/lib/modern/parseOData.js +19 -0
  47. package/lib/modern/validators/atMostOnce.js +17 -0
  48. package/lib/modern/validators/differentFromEmptyString.js +16 -0
  49. package/lib/modern/validators/hasContent.js +17 -0
  50. package/lib/modern/validators/isGuid.js +18 -0
  51. package/lib/modern/validators/recognizedGuid.js +16 -0
  52. package/lib/ts3.4/OData.types.d.ts +172 -0
  53. package/lib/ts3.4/getAliasedProperty.d.ts +10 -0
  54. package/lib/ts3.4/getExpandFromParser.d.ts +7 -0
  55. package/lib/ts3.4/getFetchXmlFromParser.d.ts +7 -0
  56. package/lib/ts3.4/getFilterFromParser.d.ts +7 -0
  57. package/lib/ts3.4/getOrderByFromParser.d.ts +7 -0
  58. package/lib/ts3.4/getSelectFromParser.d.ts +7 -0
  59. package/lib/ts3.4/getTopFromParser.d.ts +7 -0
  60. package/lib/ts3.4/getXQueryFromParser.d.ts +8 -0
  61. package/lib/ts3.4/index.d.ts +11 -0
  62. package/lib/ts3.4/parseOData.d.ts +8 -0
  63. package/lib/ts3.4/validators/atMostOnce.d.ts +10 -0
  64. package/lib/ts3.4/validators/differentFromEmptyString.d.ts +9 -0
  65. package/lib/ts3.4/validators/hasContent.d.ts +10 -0
  66. package/lib/ts3.4/validators/isGuid.d.ts +9 -0
  67. package/lib/ts3.4/validators/recognizedGuid.d.ts +9 -0
  68. package/lib/ts3.9/OData.types.d.ts +172 -0
  69. package/lib/ts3.9/getAliasedProperty.d.ts +10 -0
  70. package/lib/ts3.9/getExpandFromParser.d.ts +7 -0
  71. package/lib/ts3.9/getFetchXmlFromParser.d.ts +7 -0
  72. package/lib/ts3.9/getFilterFromParser.d.ts +7 -0
  73. package/lib/ts3.9/getOrderByFromParser.d.ts +7 -0
  74. package/lib/ts3.9/getSelectFromParser.d.ts +7 -0
  75. package/lib/ts3.9/getTopFromParser.d.ts +7 -0
  76. package/lib/ts3.9/getXQueryFromParser.d.ts +8 -0
  77. package/lib/ts3.9/index.d.ts +11 -0
  78. package/lib/ts3.9/parseOData.d.ts +8 -0
  79. package/lib/ts3.9/validators/atMostOnce.d.ts +10 -0
  80. package/lib/ts3.9/validators/differentFromEmptyString.d.ts +9 -0
  81. package/lib/ts3.9/validators/hasContent.d.ts +10 -0
  82. package/lib/ts3.9/validators/isGuid.d.ts +9 -0
  83. package/lib/ts3.9/validators/recognizedGuid.d.ts +9 -0
  84. package/lib/ts4.2/OData.types.d.ts +226 -0
  85. package/lib/ts4.2/getAliasedProperty.d.ts +10 -0
  86. package/lib/ts4.2/getAliasedProperty.d.ts.map +1 -0
  87. package/lib/ts4.2/getExpandFromParser.d.ts +7 -0
  88. package/lib/ts4.2/getExpandFromParser.d.ts.map +1 -0
  89. package/lib/ts4.2/getFetchXmlFromParser.d.ts +7 -0
  90. package/lib/ts4.2/getFetchXmlFromParser.d.ts.map +1 -0
  91. package/lib/ts4.2/getFilterFromParser.d.ts +7 -0
  92. package/lib/ts4.2/getFilterFromParser.d.ts.map +1 -0
  93. package/lib/ts4.2/getOrderByFromParser.d.ts +7 -0
  94. package/lib/ts4.2/getOrderByFromParser.d.ts.map +1 -0
  95. package/lib/ts4.2/getSelectFromParser.d.ts +7 -0
  96. package/lib/ts4.2/getSelectFromParser.d.ts.map +1 -0
  97. package/lib/ts4.2/getTopFromParser.d.ts +7 -0
  98. package/lib/ts4.2/getTopFromParser.d.ts.map +1 -0
  99. package/lib/ts4.2/getXQueryFromParser.d.ts +8 -0
  100. package/lib/ts4.2/getXQueryFromParser.d.ts.map +1 -0
  101. package/lib/ts4.2/index.d.ts +11 -0
  102. package/lib/ts4.2/index.d.ts.map +1 -0
  103. package/lib/ts4.2/parseOData.d.ts +8 -0
  104. package/lib/ts4.2/parseOData.d.ts.map +1 -0
  105. package/lib/ts4.2/validators/atMostOnce.d.ts +10 -0
  106. package/lib/ts4.2/validators/atMostOnce.d.ts.map +1 -0
  107. package/lib/ts4.2/validators/differentFromEmptyString.d.ts +9 -0
  108. package/lib/ts4.2/validators/differentFromEmptyString.d.ts.map +1 -0
  109. package/lib/ts4.2/validators/hasContent.d.ts +10 -0
  110. package/lib/ts4.2/validators/hasContent.d.ts.map +1 -0
  111. package/lib/ts4.2/validators/isGuid.d.ts +9 -0
  112. package/lib/ts4.2/validators/isGuid.d.ts.map +1 -0
  113. package/lib/ts4.2/validators/recognizedGuid.d.ts +9 -0
  114. package/lib/ts4.2/validators/recognizedGuid.d.ts.map +1 -0
  115. package/package.json +1 -1
  116. package/src/OData.types.d.ts +48 -5
  117. package/src/getFilterFromParser.ts +206 -5
@@ -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
  };