@genesislcap/mock-server 15.60.0 → 15.61.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/README.md +287 -40
- package/dist/db/criteria.d.ts +28 -9
- package/dist/db/criteria.js +856 -258
- package/dist/db/criteriaDialect.d.ts +6 -0
- package/dist/db/criteriaDialect.js +441 -0
- package/dist/db/criteriaTokens.d.ts +12 -0
- package/dist/db/criteriaTokens.js +53 -0
- package/dist/db/groovySyntax.d.ts +26 -0
- package/dist/db/groovySyntax.js +481 -0
- package/dist/db/groovyValidator.d.ts +11 -0
- package/dist/db/groovyValidator.js +523 -0
- package/dist/db/gsfExpr.d.ts +21 -0
- package/dist/db/gsfExpr.js +333 -0
- package/dist/db/identity.d.ts +2 -1
- package/dist/db/identity.js +24 -3
- package/dist/db/legacyCriteria.d.ts +7 -0
- package/dist/db/legacyCriteria.js +280 -0
- package/dist/db/ordering.d.ts +2 -0
- package/dist/db/ordering.js +24 -3
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +34 -3
- package/dist/db/store.d.ts +7 -0
- package/dist/db/store.js +55 -7
- package/dist/db/values.d.ts +19 -0
- package/dist/db/values.js +243 -0
- package/dist/handlers/commitEvent.js +3 -2
- package/dist/handlers/dataLogon.d.ts +2 -1
- package/dist/handlers/dataLogon.js +108 -51
- package/dist/handlers/eventValidation.d.ts +2 -1
- package/dist/handlers/eventValidation.js +33 -6
- package/dist/handlers/requestReply.js +43 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/protocol/connection.d.ts +1 -1
- package/dist/server.js +15 -8
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/db/criteria.ts +979 -290
- package/src/db/criteriaDialect.ts +487 -0
- package/src/db/criteriaTokens.ts +72 -0
- package/src/db/groovySyntax.ts +500 -0
- package/src/db/groovyValidator.ts +546 -0
- package/src/db/gsfExpr.ts +392 -0
- package/src/db/identity.ts +22 -4
- package/src/db/legacyCriteria.ts +301 -0
- package/src/db/ordering.ts +27 -3
- package/src/db/schema.ts +43 -8
- package/src/db/store.ts +65 -7
- package/src/db/values.ts +247 -0
- package/src/handlers/commitEvent.ts +3 -2
- package/src/handlers/dataLogon.ts +134 -56
- package/src/handlers/eventValidation.ts +40 -13
- package/src/handlers/requestReply.ts +47 -14
- package/src/index.ts +6 -1
- package/src/protocol/connection.ts +3 -4
- package/src/server.ts +16 -15
- package/src/types.ts +7 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
// What GSF answers a CRITERIA_MATCH written in Groovy this engine's grammar
|
|
2
|
+
// doesn't read — a list (`NAME in ['A']`), arithmetic (`QTY * PRICE > 10`),
|
|
3
|
+
// a ternary, `new X(...)`, `===`, `=~` — which its CriteriaValidatorVisitor
|
|
4
|
+
// (genesis-db v8.15.29) refuses. The criteria is parsed as Groovy reads it
|
|
5
|
+
// (Groovy 3.0.25's precedence) and the validator's walk is repeated over
|
|
6
|
+
// it: every error, in the order the validator meets them, with its text.
|
|
7
|
+
// Anything this parser doesn't follow — the engine's AND / OR / NOT dialect,
|
|
8
|
+
// an assignment, a closure's insides, a GString — gets no verdict here.
|
|
9
|
+
import { groovyCompileErrorText, groovyTokens } from "./groovySyntax.js";
|
|
10
|
+
const VALIDATION_FAILED = 'GenericCriteriaValidationException : Criteria validation failed: ';
|
|
11
|
+
// Classes Groovy resolves in a criteria without an import (java.lang,
|
|
12
|
+
// java.util, java.math, groovy.lang, groovy.util): the ones a criteria might
|
|
13
|
+
// name, with the name the validator prints for a constructor call.
|
|
14
|
+
export const DEFAULT_IMPORT_CLASSES = {
|
|
15
|
+
Arrays: 'java.util.Arrays',
|
|
16
|
+
BigDecimal: 'java.math.BigDecimal',
|
|
17
|
+
BigInteger: 'java.math.BigInteger',
|
|
18
|
+
Boolean: 'java.lang.Boolean',
|
|
19
|
+
Byte: 'java.lang.Byte',
|
|
20
|
+
Calendar: 'java.util.Calendar',
|
|
21
|
+
Character: 'java.lang.Character',
|
|
22
|
+
Class: 'java.lang.Class',
|
|
23
|
+
Collections: 'java.util.Collections',
|
|
24
|
+
Date: 'java.util.Date',
|
|
25
|
+
Double: 'java.lang.Double',
|
|
26
|
+
Eval: 'groovy.util.Eval',
|
|
27
|
+
Float: 'java.lang.Float',
|
|
28
|
+
GString: 'groovy.lang.GString',
|
|
29
|
+
GroovySystem: 'groovy.lang.GroovySystem',
|
|
30
|
+
Integer: 'java.lang.Integer',
|
|
31
|
+
List: 'java.util.List',
|
|
32
|
+
Locale: 'java.util.Locale',
|
|
33
|
+
Long: 'java.lang.Long',
|
|
34
|
+
Map: 'java.util.Map',
|
|
35
|
+
Math: 'java.lang.Math',
|
|
36
|
+
Number: 'java.lang.Number',
|
|
37
|
+
Object: 'java.lang.Object',
|
|
38
|
+
Objects: 'java.util.Objects',
|
|
39
|
+
Optional: 'java.util.Optional',
|
|
40
|
+
Random: 'java.util.Random',
|
|
41
|
+
Runtime: 'java.lang.Runtime',
|
|
42
|
+
Set: 'java.util.Set',
|
|
43
|
+
Short: 'java.lang.Short',
|
|
44
|
+
StrictMath: 'java.lang.StrictMath',
|
|
45
|
+
String: 'java.lang.String',
|
|
46
|
+
StringBuilder: 'java.lang.StringBuilder',
|
|
47
|
+
System: 'java.lang.System',
|
|
48
|
+
Thread: 'java.lang.Thread',
|
|
49
|
+
TimeZone: 'java.util.TimeZone',
|
|
50
|
+
UUID: 'java.util.UUID',
|
|
51
|
+
};
|
|
52
|
+
const QUALIFIED_CLASS = /^(?:java|javax|groovy)\.(?:[a-z]\w*\.)*[A-Z]\w*$/;
|
|
53
|
+
// Whether Groovy resolves this name to a class (a call on it is a class
|
|
54
|
+
// expression).
|
|
55
|
+
export const isClassName = (name) => Object.hasOwn(DEFAULT_IMPORT_CLASSES, name) || QUALIFIED_CLASS.test(name);
|
|
56
|
+
// The validator's whitelists (CriteriaValidatorVisitor).
|
|
57
|
+
export const EXPR_METHODS = new Set([
|
|
58
|
+
'dateIsToday',
|
|
59
|
+
'dateIsEqual',
|
|
60
|
+
'dateIsAfter',
|
|
61
|
+
'dateIsBefore',
|
|
62
|
+
'dateIsGreaterEqual',
|
|
63
|
+
'dateIsLessEqual',
|
|
64
|
+
'dateTimeIsAfter',
|
|
65
|
+
'dateTimeIsGreaterEqual',
|
|
66
|
+
'dateTimeIsBefore',
|
|
67
|
+
'dateTimeIsLessEqual',
|
|
68
|
+
'containsIgnoreCase',
|
|
69
|
+
'containsWordsStartingWithIgnoreCase',
|
|
70
|
+
'isNullOrBlank',
|
|
71
|
+
'notNullOrBlank',
|
|
72
|
+
'longLocalDate',
|
|
73
|
+
'longDateTime',
|
|
74
|
+
'dateTimeIsInRange',
|
|
75
|
+
'stringLocalDate',
|
|
76
|
+
]);
|
|
77
|
+
export const FIELD_METHODS = new Set([
|
|
78
|
+
'startsWith',
|
|
79
|
+
'equals',
|
|
80
|
+
'equalsIgnoreCase',
|
|
81
|
+
'contains',
|
|
82
|
+
'containsIgnoreCase',
|
|
83
|
+
'containsWordsStartingWithIgnoreCase',
|
|
84
|
+
'dateIsToday',
|
|
85
|
+
]);
|
|
86
|
+
const ALLOWED_CONSTRUCTORS = new Set([
|
|
87
|
+
'java.math.BigDecimal',
|
|
88
|
+
'java.lang.Integer',
|
|
89
|
+
'java.lang.Long',
|
|
90
|
+
'java.lang.Double',
|
|
91
|
+
'java.lang.String',
|
|
92
|
+
]);
|
|
93
|
+
// The binary operators the validator visits the operands of.
|
|
94
|
+
const LOGICAL = new Set(['&&', '||']);
|
|
95
|
+
const COMPARISONS = new Set(['==', '!=', '>', '>=', '<', '<=', 'in', '!in']);
|
|
96
|
+
const NOT_SUPPORTED = (what) => `${what} are not supported in criteria`;
|
|
97
|
+
// Thrown for anything this parser doesn't follow: no verdict.
|
|
98
|
+
class GiveUp extends Error {
|
|
99
|
+
}
|
|
100
|
+
// Binding powers, Groovy 3's precedence (higher binds tighter).
|
|
101
|
+
const BINARY_POWER = {
|
|
102
|
+
'||': 2,
|
|
103
|
+
'&&': 3,
|
|
104
|
+
'|': 4,
|
|
105
|
+
'^': 5,
|
|
106
|
+
'&': 6,
|
|
107
|
+
'==': 7,
|
|
108
|
+
'!=': 7,
|
|
109
|
+
'<=>': 7,
|
|
110
|
+
'===': 7,
|
|
111
|
+
'!==': 7,
|
|
112
|
+
'=~': 7,
|
|
113
|
+
'==~': 7,
|
|
114
|
+
'<': 8,
|
|
115
|
+
'<=': 8,
|
|
116
|
+
'>': 8,
|
|
117
|
+
'>=': 8,
|
|
118
|
+
in: 8,
|
|
119
|
+
'!in': 8,
|
|
120
|
+
instanceof: 8,
|
|
121
|
+
'!instanceof': 8,
|
|
122
|
+
as: 8,
|
|
123
|
+
'<<': 9,
|
|
124
|
+
'>>': 9,
|
|
125
|
+
'>>>': 9,
|
|
126
|
+
'..': 9,
|
|
127
|
+
'..<': 9,
|
|
128
|
+
'+': 10,
|
|
129
|
+
'-': 10,
|
|
130
|
+
'*': 11,
|
|
131
|
+
'/': 11,
|
|
132
|
+
'%': 11,
|
|
133
|
+
'**': 13,
|
|
134
|
+
};
|
|
135
|
+
const UNARY_POWER = 12;
|
|
136
|
+
const RIGHT_ASSOCIATIVE = new Set(['**']);
|
|
137
|
+
const KEYWORD_OPERATORS = new Set(['in', 'instanceof', 'as']);
|
|
138
|
+
// This engine's dialect: criteriaDialect.ts answers it.
|
|
139
|
+
const DIALECT_WORDS = new Set(['AND', 'OR', 'NOT']);
|
|
140
|
+
class Parser {
|
|
141
|
+
at = 0;
|
|
142
|
+
tokens;
|
|
143
|
+
constructor(tokens) {
|
|
144
|
+
this.tokens = tokens;
|
|
145
|
+
}
|
|
146
|
+
get done() {
|
|
147
|
+
return this.at >= this.tokens.length;
|
|
148
|
+
}
|
|
149
|
+
peek(ahead = 0) {
|
|
150
|
+
return this.tokens[this.at + ahead];
|
|
151
|
+
}
|
|
152
|
+
next() {
|
|
153
|
+
const token = this.tokens[this.at];
|
|
154
|
+
if (!token)
|
|
155
|
+
throw new GiveUp();
|
|
156
|
+
this.at += 1;
|
|
157
|
+
return token;
|
|
158
|
+
}
|
|
159
|
+
expect(text) {
|
|
160
|
+
if (this.next().text !== text)
|
|
161
|
+
throw new GiveUp();
|
|
162
|
+
}
|
|
163
|
+
// The binary operator at the cursor, and how many tokens it takes.
|
|
164
|
+
operator() {
|
|
165
|
+
const token = this.peek();
|
|
166
|
+
if (!token)
|
|
167
|
+
return undefined;
|
|
168
|
+
if (token.kind === 'operator' && token.text === '!') {
|
|
169
|
+
const word = this.peek(1);
|
|
170
|
+
if (word?.kind === 'name' && (word.text === 'in' || word.text === 'instanceof')) {
|
|
171
|
+
return { op: `!${word.text}`, length: 2 };
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
if (token.kind === 'name' && KEYWORD_OPERATORS.has(token.text)) {
|
|
176
|
+
return { op: token.text, length: 1 };
|
|
177
|
+
}
|
|
178
|
+
if (token.kind === 'name' && DIALECT_WORDS.has(token.text.toUpperCase()))
|
|
179
|
+
throw new GiveUp();
|
|
180
|
+
if (token.kind !== 'operator')
|
|
181
|
+
return undefined;
|
|
182
|
+
if (Object.hasOwn(BINARY_POWER, token.text))
|
|
183
|
+
return { op: token.text, length: 1 };
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
expression(minPower = 0) {
|
|
187
|
+
let left = this.unary();
|
|
188
|
+
for (;;) {
|
|
189
|
+
const token = this.peek();
|
|
190
|
+
if (token?.kind === 'operator' && (token.text === '?' || token.text === '?:')) {
|
|
191
|
+
if (minPower > 1)
|
|
192
|
+
return left;
|
|
193
|
+
this.next();
|
|
194
|
+
if (token.text === '?') {
|
|
195
|
+
this.expression(1);
|
|
196
|
+
this.expect(':');
|
|
197
|
+
}
|
|
198
|
+
this.expression(1);
|
|
199
|
+
left = {
|
|
200
|
+
kind: 'refused',
|
|
201
|
+
message: token.text === '?'
|
|
202
|
+
? NOT_SUPPORTED('Ternary expressions')
|
|
203
|
+
: 'Elvis operator is not supported in criteria',
|
|
204
|
+
};
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (token?.kind === 'operator' && (token.text === '=' || token.text.endsWith('='))) {
|
|
208
|
+
// An assignment (`=`, `+=`, ...) is the dialect's or no criteria.
|
|
209
|
+
if (!Object.hasOwn(BINARY_POWER, token.text))
|
|
210
|
+
throw new GiveUp();
|
|
211
|
+
}
|
|
212
|
+
if (token?.kind === 'operator' && token.text === '<>')
|
|
213
|
+
throw new GiveUp();
|
|
214
|
+
const operator = this.operator();
|
|
215
|
+
if (!operator)
|
|
216
|
+
return left;
|
|
217
|
+
const power = BINARY_POWER[operator.op];
|
|
218
|
+
if (power <= minPower)
|
|
219
|
+
return left;
|
|
220
|
+
this.at += operator.length;
|
|
221
|
+
const right = this.expression(RIGHT_ASSOCIATIVE.has(operator.op) ? power - 1 : power);
|
|
222
|
+
if (operator.op === '..' || operator.op === '..<') {
|
|
223
|
+
left = { kind: 'refused', message: NOT_SUPPORTED('Range expressions') };
|
|
224
|
+
}
|
|
225
|
+
else if (operator.op === 'as') {
|
|
226
|
+
left = { kind: 'refused', message: NOT_SUPPORTED('Cast expressions') };
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
left = { kind: 'binary', op: operator.op, left, right };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
unary() {
|
|
234
|
+
const token = this.peek();
|
|
235
|
+
if (token?.kind === 'operator') {
|
|
236
|
+
if (token.text === '!' || token.text === '-' || token.text === '+') {
|
|
237
|
+
this.next();
|
|
238
|
+
return { kind: 'visitInner', operand: this.expression(UNARY_POWER) };
|
|
239
|
+
}
|
|
240
|
+
if (token.text === '~') {
|
|
241
|
+
this.next();
|
|
242
|
+
this.expression(UNARY_POWER);
|
|
243
|
+
return { kind: 'refused', message: NOT_SUPPORTED('Bitwise negation expressions') };
|
|
244
|
+
}
|
|
245
|
+
if (token.text === '++' || token.text === '--') {
|
|
246
|
+
this.next();
|
|
247
|
+
this.expression(UNARY_POWER);
|
|
248
|
+
return { kind: 'refused', message: NOT_SUPPORTED('Prefix expressions') };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return this.postfix(this.primary());
|
|
252
|
+
}
|
|
253
|
+
postfix(start) {
|
|
254
|
+
let node = start;
|
|
255
|
+
for (;;) {
|
|
256
|
+
const token = this.peek();
|
|
257
|
+
if (token?.kind === 'operator' && (token.text === '.' || token.text === '?.')) {
|
|
258
|
+
this.next();
|
|
259
|
+
const member = this.next();
|
|
260
|
+
if (member.kind !== 'name')
|
|
261
|
+
throw new GiveUp();
|
|
262
|
+
if (this.peek()?.text === '(' && this.peek()?.kind === 'open') {
|
|
263
|
+
node = { kind: 'call', object: node, name: member.text, args: this.arguments() };
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
node = { kind: 'property', object: node, name: member.text };
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (token?.kind === 'open' && token.text === '[') {
|
|
271
|
+
this.next();
|
|
272
|
+
const index = this.expression();
|
|
273
|
+
this.expect(']');
|
|
274
|
+
node = { kind: 'binary', op: '[', left: node, right: index };
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (token?.kind === 'operator' && (token.text === '++' || token.text === '--')) {
|
|
278
|
+
this.next();
|
|
279
|
+
node = { kind: 'refused', message: NOT_SUPPORTED('Postfix expressions') };
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (token?.kind === 'operator' && ['*.', '.&', '.@', '::', '->'].includes(token.text)) {
|
|
283
|
+
throw new GiveUp();
|
|
284
|
+
}
|
|
285
|
+
return node;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// The arguments after a call's '('.
|
|
289
|
+
arguments() {
|
|
290
|
+
this.expect('(');
|
|
291
|
+
const args = [];
|
|
292
|
+
if (this.peek()?.text === ')') {
|
|
293
|
+
this.next();
|
|
294
|
+
return args;
|
|
295
|
+
}
|
|
296
|
+
for (;;) {
|
|
297
|
+
args.push(this.expression());
|
|
298
|
+
const after = this.next();
|
|
299
|
+
if (after.text === ')')
|
|
300
|
+
return args;
|
|
301
|
+
if (after.kind !== 'comma')
|
|
302
|
+
throw new GiveUp();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
primary() {
|
|
306
|
+
const token = this.next();
|
|
307
|
+
if (token.kind === 'literal') {
|
|
308
|
+
// A GString is criteriaDialect.ts's.
|
|
309
|
+
if (token.text.startsWith('"') && token.text.includes('$'))
|
|
310
|
+
throw new GiveUp();
|
|
311
|
+
return { kind: 'constant' };
|
|
312
|
+
}
|
|
313
|
+
if (token.kind === 'name') {
|
|
314
|
+
if (['true', 'false', 'null'].includes(token.text))
|
|
315
|
+
return { kind: 'constant' };
|
|
316
|
+
if (DIALECT_WORDS.has(token.text.toUpperCase()) || KEYWORD_OPERATORS.has(token.text)) {
|
|
317
|
+
throw new GiveUp();
|
|
318
|
+
}
|
|
319
|
+
if (token.text === 'new')
|
|
320
|
+
return this.constructorCall(token);
|
|
321
|
+
// A name called directly is a call on an implicit `this`.
|
|
322
|
+
if (this.peek()?.kind === 'open' && this.peek()?.text === '(') {
|
|
323
|
+
return { kind: 'call', object: undefined, name: token.text, args: this.arguments() };
|
|
324
|
+
}
|
|
325
|
+
return { kind: 'variable', name: token.text };
|
|
326
|
+
}
|
|
327
|
+
if (token.kind === 'open' && token.text === '(') {
|
|
328
|
+
const inner = this.expression();
|
|
329
|
+
this.expect(')');
|
|
330
|
+
return inner;
|
|
331
|
+
}
|
|
332
|
+
if (token.kind === 'open' && token.text === '[')
|
|
333
|
+
return this.listOrMap();
|
|
334
|
+
if (token.kind === 'open' && token.text === '{') {
|
|
335
|
+
let depth = 1;
|
|
336
|
+
while (depth > 0) {
|
|
337
|
+
const inner = this.next();
|
|
338
|
+
if (inner.kind === 'open')
|
|
339
|
+
depth += 1;
|
|
340
|
+
if (inner.kind === 'close')
|
|
341
|
+
depth -= 1;
|
|
342
|
+
}
|
|
343
|
+
return { kind: 'refused', message: 'Closures are not supported in criteria' };
|
|
344
|
+
}
|
|
345
|
+
throw new GiveUp();
|
|
346
|
+
}
|
|
347
|
+
// `[a, b]` or `[k: v]` (`[:]`): refused whole.
|
|
348
|
+
listOrMap() {
|
|
349
|
+
let map = false;
|
|
350
|
+
if (this.peek()?.text === ':') {
|
|
351
|
+
this.next();
|
|
352
|
+
this.expect(']');
|
|
353
|
+
return { kind: 'refused', message: NOT_SUPPORTED('Map expressions') };
|
|
354
|
+
}
|
|
355
|
+
if (this.peek()?.text === ']') {
|
|
356
|
+
this.next();
|
|
357
|
+
return { kind: 'refused', message: NOT_SUPPORTED('List expressions') };
|
|
358
|
+
}
|
|
359
|
+
for (;;) {
|
|
360
|
+
this.expression();
|
|
361
|
+
if (this.peek()?.text === ':') {
|
|
362
|
+
this.next();
|
|
363
|
+
this.expression();
|
|
364
|
+
map = true;
|
|
365
|
+
}
|
|
366
|
+
const after = this.next();
|
|
367
|
+
if (after.text === ']')
|
|
368
|
+
break;
|
|
369
|
+
if (after.kind !== 'comma')
|
|
370
|
+
throw new GiveUp();
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
kind: 'refused',
|
|
374
|
+
message: NOT_SUPPORTED(map ? 'Map expressions' : 'List expressions'),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
// `new X(...)`: the class, possibly qualified; its arguments are never
|
|
378
|
+
// visited.
|
|
379
|
+
constructorCall(keyword) {
|
|
380
|
+
let name = this.next();
|
|
381
|
+
if (name.kind !== 'name')
|
|
382
|
+
throw new GiveUp();
|
|
383
|
+
let qualified = name.text;
|
|
384
|
+
while (this.peek()?.kind === 'operator' && this.peek()?.text === '.') {
|
|
385
|
+
this.next();
|
|
386
|
+
name = this.next();
|
|
387
|
+
if (name.kind !== 'name')
|
|
388
|
+
throw new GiveUp();
|
|
389
|
+
qualified += `.${name.text}`;
|
|
390
|
+
}
|
|
391
|
+
if (this.peek()?.text !== '(')
|
|
392
|
+
throw new GiveUp(); // an array, a closure class
|
|
393
|
+
this.arguments();
|
|
394
|
+
return { kind: 'ctor', name: qualified, offset: keyword.offset };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// The class a qualified name or a name Groovy resolves names, or undefined.
|
|
398
|
+
function className(node) {
|
|
399
|
+
if (node.kind === 'variable')
|
|
400
|
+
return isClassName(node.name) ? node.name : undefined;
|
|
401
|
+
if (node.kind !== 'property')
|
|
402
|
+
return undefined;
|
|
403
|
+
const parts = [node.name];
|
|
404
|
+
let object = node.object;
|
|
405
|
+
while (object.kind === 'property') {
|
|
406
|
+
parts.unshift(object.name);
|
|
407
|
+
object = object.object;
|
|
408
|
+
}
|
|
409
|
+
if (object.kind !== 'variable')
|
|
410
|
+
return undefined;
|
|
411
|
+
const qualified = [object.name, ...parts].join('.');
|
|
412
|
+
return QUALIFIED_CLASS.test(qualified) ? qualified : undefined;
|
|
413
|
+
}
|
|
414
|
+
// CriteriaValidatorVisitor's walk.
|
|
415
|
+
function visit(node, allowed, verdict) {
|
|
416
|
+
const known = (name) => !allowed || allowed.has(name);
|
|
417
|
+
switch (node.kind) {
|
|
418
|
+
case 'binary':
|
|
419
|
+
if (LOGICAL.has(node.op) || COMPARISONS.has(node.op)) {
|
|
420
|
+
visit(node.left, allowed, verdict);
|
|
421
|
+
visit(node.right, allowed, verdict);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
verdict.errors.push(`Binary operator ${node.op} not allowed in criteria expression`);
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
case 'visitInner':
|
|
428
|
+
visit(node.operand, allowed, verdict);
|
|
429
|
+
return;
|
|
430
|
+
case 'refused':
|
|
431
|
+
verdict.errors.push(node.message);
|
|
432
|
+
return;
|
|
433
|
+
case 'constant':
|
|
434
|
+
return;
|
|
435
|
+
case 'variable':
|
|
436
|
+
if (isClassName(node.name))
|
|
437
|
+
verdict.errors.push(NOT_SUPPORTED('Class expressions'));
|
|
438
|
+
else if (!known(node.name)) {
|
|
439
|
+
verdict.errors.push(`Variable ${node.name} is not allowed in criteria expression`);
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
case 'property':
|
|
443
|
+
verdict.errors.push(className(node)
|
|
444
|
+
? NOT_SUPPORTED('Class expressions')
|
|
445
|
+
: NOT_SUPPORTED('Property expressions'));
|
|
446
|
+
return;
|
|
447
|
+
case 'ctor': {
|
|
448
|
+
const resolved = Object.hasOwn(DEFAULT_IMPORT_CLASSES, node.name)
|
|
449
|
+
? DEFAULT_IMPORT_CLASSES[node.name]
|
|
450
|
+
: QUALIFIED_CLASS.test(node.name)
|
|
451
|
+
? node.name
|
|
452
|
+
: undefined;
|
|
453
|
+
if (!resolved) {
|
|
454
|
+
verdict.unresolved ??= { name: node.name, offset: node.offset };
|
|
455
|
+
}
|
|
456
|
+
else if (!ALLOWED_CONSTRUCTORS.has(resolved)) {
|
|
457
|
+
verdict.errors.push(`Constructor calls for ${resolved} are not allowed in criteria expressions`);
|
|
458
|
+
}
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
case 'call': {
|
|
462
|
+
const { object } = node;
|
|
463
|
+
if (object?.kind === 'variable' && object.name === 'Expr') {
|
|
464
|
+
if (!EXPR_METHODS.has(node.name)) {
|
|
465
|
+
verdict.errors.push(`Method call Expr.${node.name} is not supported`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else if (object?.kind === 'variable' && !isClassName(object.name) && known(object.name)) {
|
|
469
|
+
if (!FIELD_METHODS.has(node.name)) {
|
|
470
|
+
verdict.errors.push(`Field method ${object.name}.${node.name} is not supported`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else if (object === undefined) {
|
|
474
|
+
verdict.errors.push('Variable this is not allowed in criteria expression');
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
visit(object, allowed, verdict);
|
|
478
|
+
}
|
|
479
|
+
for (const arg of node.args)
|
|
480
|
+
visit(arg, allowed, verdict);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const SIMPLE_OPERATORS = new Set(['&&', '||', '==', '!=', '<', '<=', '>', '>=', '!', '.']);
|
|
485
|
+
// Whether the criteria uses Groovy this engine's grammar doesn't read: an
|
|
486
|
+
// operator other than its own, a bracket or brace, `new`, `in`,
|
|
487
|
+
// `instanceof`, `as`.
|
|
488
|
+
function usesGroovy(tokens) {
|
|
489
|
+
return tokens.some((token) => (token.kind === 'operator' && !SIMPLE_OPERATORS.has(token.text)) ||
|
|
490
|
+
(token.kind === 'open' && token.text !== '(') ||
|
|
491
|
+
(token.kind === 'name' && ['new', 'in', 'instanceof', 'as'].includes(token.text)));
|
|
492
|
+
}
|
|
493
|
+
// GSF's verdict on a criteria in Groovy this engine's grammar doesn't read;
|
|
494
|
+
// undefined when it reads it all (criteriaDialect.ts answers), or this
|
|
495
|
+
// parser doesn't follow it. `allowed`: the query's fields, when known.
|
|
496
|
+
export function groovyValidation(criteria, allowed) {
|
|
497
|
+
const tokens = groovyTokens(criteria);
|
|
498
|
+
if (!tokens || !usesGroovy(tokens))
|
|
499
|
+
return undefined;
|
|
500
|
+
let root;
|
|
501
|
+
try {
|
|
502
|
+
const parser = new Parser(tokens);
|
|
503
|
+
root = parser.expression();
|
|
504
|
+
if (!parser.done)
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
if (error instanceof GiveUp || error instanceof RangeError)
|
|
509
|
+
return undefined;
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
const verdict = { errors: [] };
|
|
513
|
+
visit(root, allowed, verdict);
|
|
514
|
+
if (verdict.unresolved) {
|
|
515
|
+
const { name, offset } = verdict.unresolved;
|
|
516
|
+
const text = groovyCompileErrorText(criteria, { message: `unable to resolve class ${name}`, line: 1, column: offset + 1 }, { ownLine: true });
|
|
517
|
+
return { kind: 'refused', text };
|
|
518
|
+
}
|
|
519
|
+
if (verdict.errors.length > 0) {
|
|
520
|
+
return { kind: 'refused', text: `${VALIDATION_FAILED}${verdict.errors.join(',')}` };
|
|
521
|
+
}
|
|
522
|
+
return { kind: 'passed' };
|
|
523
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare class CriteriaEvaluationError extends Error {
|
|
2
|
+
name: string;
|
|
3
|
+
}
|
|
4
|
+
export declare const evaluationError: (message: string) => never;
|
|
5
|
+
export declare const MS_PER_DAY: number;
|
|
6
|
+
export declare function daysFromCivil(year: number, month: number, day: number): number;
|
|
7
|
+
export declare function civilFromDays(days: number): {
|
|
8
|
+
year: number;
|
|
9
|
+
month: number;
|
|
10
|
+
day: number;
|
|
11
|
+
};
|
|
12
|
+
export declare const epochDay: (millis: number) => number;
|
|
13
|
+
export declare const stringDateTime: (text: string) => number;
|
|
14
|
+
export declare const stringLocalDate: (text: string) => number;
|
|
15
|
+
export declare const singlePatternLocalDate: (text: string) => number;
|
|
16
|
+
export declare const today: () => number;
|
|
17
|
+
export declare const isBlank: (text: string) => boolean;
|
|
18
|
+
export declare function containsIgnoreCase(text: string | null, search: string | null): boolean;
|
|
19
|
+
export declare function containsWordsStartingWithIgnoreCase(text: string | null, search: string | null, delimiter?: string): boolean;
|
|
20
|
+
export declare function timeBounds(range: string, context: string, now: number): [number, number];
|
|
21
|
+
export declare function dateTimeIsInRange(millis: number, range: string, context: string): boolean;
|