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