@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.
- 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
package/dist/db/criteria.js
CHANGED
|
@@ -1,172 +1,125 @@
|
|
|
1
1
|
// Evaluator for Genesis CRITERIA_MATCH expressions, e.g.:
|
|
2
|
-
// TRADE_STATUS == 'CONFIRMED'
|
|
3
|
-
//
|
|
2
|
+
// TRADE_STATUS == 'CONFIRMED' && QUANTITY > 100
|
|
3
|
+
// !(SIDE == 'SELL') || PRICE >= 100.5
|
|
4
4
|
// Expr.dateTimeIsGreaterEqual(TRADE_DATE, '20240101-00:00:00') && COUNTERPARTY_ID == 'CPTY001'
|
|
5
5
|
//
|
|
6
|
-
// Supports: ==
|
|
7
|
-
//
|
|
8
|
-
// function
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
6
|
+
// Supports: == != > >= < <=, && || !, parentheses, string/number/boolean/null
|
|
7
|
+
// literals, field names, and calls in both forms GSF takes:
|
|
8
|
+
// - `Expr.<fn>(args)` — every function of GSF's Expr whitelist
|
|
9
|
+
// (CriteriaValidatorVisitor, v8.15.29), with Expr.java's overloads;
|
|
10
|
+
// - `<FIELD>.<method>(args)` — the field methods GSF allows: String's
|
|
11
|
+
// startsWith / equals / equalsIgnoreCase / contains and the
|
|
12
|
+
// ExtraOperators.kt extensions containsIgnoreCase,
|
|
13
|
+
// containsWordsStartingWithIgnoreCase and dateIsToday.
|
|
14
|
+
// A call may be an operand (`Expr.longLocalDate(D) == Expr.stringLocalDate('20240101')`)
|
|
15
|
+
// or an argument of another. What the functions compute, and when they throw
|
|
16
|
+
// (making the row not match), is GSF's: see gsfExpr.ts.
|
|
17
|
+
//
|
|
18
|
+
// It also reads what GSF refuses — = <>, AND OR NOT, and the functions this
|
|
19
|
+
// engine used to offer (Expr.contains, Expr.equals, Expr.equalsIgnoreCase,
|
|
20
|
+
// Expr.startsWith, Expr.endsWith, Expr.dateTimeIsEqual, FIELD.endsWith, and
|
|
21
|
+
// any Expr function called as a field method). Strict criteria refuse those
|
|
22
|
+
// before they get here (criteriaDialect.ts); only fidelity: 'legacy'
|
|
23
|
+
// evaluates them.
|
|
24
|
+
//
|
|
25
|
+
// Types: GSF compiles a criteria against the entity's Java types — a STRING
|
|
26
|
+
// or ENUM field is a String, DATE and DATETIME a Joda DateTime, LONG a Long —
|
|
27
|
+
// which picks each function's overload and lets ExtraOperators compare a
|
|
28
|
+
// DateTime with a date string (`TRADE_DATE > '20240101'`). With
|
|
29
|
+
// CriteriaOptions.types the evaluator does the same; a field without a known
|
|
30
|
+
// type is read by its value (a number as a Long, a string as a String).
|
|
31
|
+
// criteriaCompileRefusal reports what that compilation refuses.
|
|
32
|
+
//
|
|
15
33
|
// Deliberately NOT eval()/new Function() — this parses a small typed grammar
|
|
16
34
|
// so a malformed or hostile criteria string can only fail to parse, never
|
|
17
35
|
// execute arbitrary JS.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
import { CriteriaSyntaxError, tokenizeCriteria, UnsupportedCriteriaError, } from "./criteriaTokens.js";
|
|
37
|
+
import * as gsf from "./gsfExpr.js";
|
|
38
|
+
import { CriteriaEvaluationError, evaluationError } from "./gsfExpr.js";
|
|
39
|
+
import { parseLegacyCriteria } from "./legacyCriteria.js";
|
|
40
|
+
import { epochMillis } from "./values.js";
|
|
41
|
+
export { CriteriaSyntaxError, parseGenesisDateTime, tokenizeCriteria, UnsupportedCriteriaError, } from "./criteriaTokens.js";
|
|
42
|
+
// A generated entity's getter types (ENUM getters are read with toString()).
|
|
43
|
+
const ENTITY_TYPES = {
|
|
44
|
+
STRING: 'String',
|
|
45
|
+
ENUM: 'String',
|
|
46
|
+
INT: 'Integer',
|
|
47
|
+
SHORT: 'Short',
|
|
48
|
+
LONG: 'Long',
|
|
49
|
+
NANO_TIMESTAMP: 'Long',
|
|
50
|
+
DOUBLE: 'Double',
|
|
51
|
+
BIGDECIMAL: 'BigDecimal',
|
|
52
|
+
BOOLEAN: 'Boolean',
|
|
53
|
+
DATE: 'DateTime',
|
|
54
|
+
DATETIME: 'DateTime',
|
|
55
|
+
};
|
|
56
|
+
// A Joda DateTime: an instant, in epoch millis.
|
|
57
|
+
class DateTimeValue {
|
|
58
|
+
millis;
|
|
59
|
+
constructor(millis) {
|
|
60
|
+
this.millis = millis;
|
|
41
61
|
}
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
}
|
|
63
|
+
// A Joda LocalDate: a day, in days since 1970-01-01.
|
|
64
|
+
class LocalDateValue {
|
|
65
|
+
day;
|
|
66
|
+
constructor(day) {
|
|
67
|
+
this.day = day;
|
|
44
68
|
}
|
|
45
|
-
return tokens;
|
|
46
69
|
}
|
|
70
|
+
const INT_MIN = -2_147_483_648;
|
|
71
|
+
const INT_MAX = 2_147_483_647;
|
|
72
|
+
const NULL_POINTER = 'java.lang.NullPointerException';
|
|
47
73
|
function parseLiteral(token) {
|
|
48
|
-
if (/^['"]/.test(token))
|
|
49
|
-
return token.slice(1, -1).replace(/\\(.)/g, '$1');
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
if (/^['"]/.test(token)) {
|
|
75
|
+
return { value: token.slice(1, -1).replace(/\\(.)/g, '$1'), type: 'String' };
|
|
76
|
+
}
|
|
77
|
+
if (/^-?\d+$/.test(token)) {
|
|
78
|
+
const value = Number(token);
|
|
79
|
+
const type = value >= INT_MIN && value <= INT_MAX ? 'Integer' : 'Long';
|
|
80
|
+
return { value, type };
|
|
81
|
+
}
|
|
82
|
+
if (/^-?\d+\.\d+$/.test(token))
|
|
83
|
+
return { value: Number(token), type: 'BigDecimal' };
|
|
84
|
+
if (token === 'true' || token === 'false')
|
|
85
|
+
return { value: token === 'true', type: 'Boolean' };
|
|
56
86
|
if (token === 'null')
|
|
57
|
-
return null;
|
|
58
|
-
return undefined; // not a literal ->
|
|
87
|
+
return { value: null, type: 'null' };
|
|
88
|
+
return undefined; // not a literal -> a field reference
|
|
59
89
|
}
|
|
60
|
-
const
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// millis (number) or an ISO string. Returns null if unparseable.
|
|
67
|
-
export function parseGenesisDateTime(value) {
|
|
68
|
-
if (typeof value === 'number')
|
|
90
|
+
const isNullish = (value) => value === null || value === undefined;
|
|
91
|
+
// Groovy truth: false for null, false, '', 0; true for anything else.
|
|
92
|
+
function groovyTruth(value) {
|
|
93
|
+
if (isNullish(value))
|
|
94
|
+
return false;
|
|
95
|
+
if (typeof value === 'boolean')
|
|
69
96
|
return value;
|
|
70
|
-
if (typeof value
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const [, y, mo, d, h, mi, s, ms] = match;
|
|
76
|
-
return Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s ?? '0'), Number((ms ?? '0').padEnd(MS_DIGITS, '0')));
|
|
77
|
-
}
|
|
78
|
-
const parsed = Date.parse(value);
|
|
79
|
-
return Number.isNaN(parsed) ? null : parsed;
|
|
80
|
-
}
|
|
81
|
-
const HOURS_PER_DAY = 24;
|
|
82
|
-
const MINUTES_PER_HOUR = 60;
|
|
83
|
-
const SECONDS_PER_MINUTE = 60;
|
|
84
|
-
const MS_PER_SECOND = 1000;
|
|
85
|
-
const MS_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
|
|
86
|
-
function toEpochDay(value) {
|
|
87
|
-
const epoch = parseGenesisDateTime(value);
|
|
88
|
-
return epoch === null ? null : Math.floor(epoch / MS_PER_DAY);
|
|
89
|
-
}
|
|
90
|
-
// Expr.<fn>(field, value) function-call criteria, as emitted by
|
|
91
|
-
// foundation-criteria's serialisers.ts (contains/containsIgnoreCase/dateIs*)
|
|
92
|
-
// and foundation-ui's date-range filter.ts (dateTimeIsAfter/dateTimeIsBefore,
|
|
93
|
-
// which operator-map.ts maps onto the same >=/<= semantics as
|
|
94
|
-
// dateTimeIsGreaterEqual/dateTimeIsLessEqual). `dateIs*` compares by calendar
|
|
95
|
-
// day (UTC), `dateTimeIs*` compares full timestamps.
|
|
96
|
-
const EXPR_FUNCTIONS = {
|
|
97
|
-
// Multi-value fields (e.g. DEAL_CURRENCIES) may hold an array — treat
|
|
98
|
-
// "contains" as membership there, substring match on strings.
|
|
99
|
-
contains: (record, args, resolve) => {
|
|
100
|
-
const value = resolve(args[0], record);
|
|
101
|
-
const needle = resolve(args[1], record);
|
|
102
|
-
if (typeof needle !== 'string')
|
|
103
|
-
return false;
|
|
104
|
-
if (Array.isArray(value))
|
|
105
|
-
return value.some((v) => typeof v === 'string' && v === needle);
|
|
106
|
-
return typeof value === 'string' && value.includes(needle);
|
|
107
|
-
},
|
|
108
|
-
containsIgnoreCase: (record, args, resolve) => {
|
|
109
|
-
const value = resolve(args[0], record);
|
|
110
|
-
const needle = resolve(args[1], record);
|
|
111
|
-
if (typeof needle !== 'string')
|
|
112
|
-
return false;
|
|
113
|
-
const lowered = needle.toLowerCase();
|
|
114
|
-
if (Array.isArray(value)) {
|
|
115
|
-
return value.some((v) => typeof v === 'string' && v.toLowerCase() === lowered);
|
|
116
|
-
}
|
|
117
|
-
return typeof value === 'string' && value.toLowerCase().includes(lowered);
|
|
118
|
-
},
|
|
119
|
-
// Groovy/Java String methods the client emits in the FIELD.method(args)
|
|
120
|
-
// form — e.g. pbc-notify-ui's inbox subscribes with
|
|
121
|
-
// `((ALERT_STATUS.equals("NEW")))`. Without these the criteria fails to
|
|
122
|
-
// compile, the filter is dropped, and the grid shows every row instead of
|
|
123
|
-
// the filtered set.
|
|
124
|
-
equals: (record, args, resolve) => resolve(args[0], record) === resolve(args[1], record),
|
|
125
|
-
equalsIgnoreCase: (record, args, resolve) => {
|
|
126
|
-
const value = resolve(args[0], record);
|
|
127
|
-
const other = resolve(args[1], record);
|
|
128
|
-
if (typeof value !== 'string' || typeof other !== 'string')
|
|
129
|
-
return value === other;
|
|
130
|
-
return value.toLowerCase() === other.toLowerCase();
|
|
131
|
-
},
|
|
132
|
-
startsWith: (record, args, resolve) => {
|
|
133
|
-
const value = resolve(args[0], record);
|
|
134
|
-
const prefix = resolve(args[1], record);
|
|
135
|
-
return typeof value === 'string' && typeof prefix === 'string' && value.startsWith(prefix);
|
|
136
|
-
},
|
|
137
|
-
endsWith: (record, args, resolve) => {
|
|
138
|
-
const value = resolve(args[0], record);
|
|
139
|
-
const suffix = resolve(args[1], record);
|
|
140
|
-
return typeof value === 'string' && typeof suffix === 'string' && value.endsWith(suffix);
|
|
141
|
-
},
|
|
142
|
-
dateIsToday: (record, args, resolve) => {
|
|
143
|
-
const day = toEpochDay(resolve(args[0], record));
|
|
144
|
-
return day !== null && day === toEpochDay(Date.now());
|
|
145
|
-
},
|
|
146
|
-
dateIsEqual: (record, args, resolve) => compareByDay(record, args, resolve, (a, b) => a === b),
|
|
147
|
-
dateIsGreaterEqual: (record, args, resolve) => compareByDay(record, args, resolve, (a, b) => a >= b),
|
|
148
|
-
dateIsLessEqual: (record, args, resolve) => compareByDay(record, args, resolve, (a, b) => a <= b),
|
|
149
|
-
dateTimeIsEqual: (record, args, resolve) => compareByTime(record, args, resolve, (a, b) => a === b),
|
|
150
|
-
dateTimeIsGreaterEqual: (record, args, resolve) => compareByTime(record, args, resolve, (a, b) => a >= b),
|
|
151
|
-
dateTimeIsLessEqual: (record, args, resolve) => compareByTime(record, args, resolve, (a, b) => a <= b),
|
|
152
|
-
dateTimeIsAfter: (record, args, resolve) => compareByTime(record, args, resolve, (a, b) => a >= b),
|
|
153
|
-
dateTimeIsBefore: (record, args, resolve) => compareByTime(record, args, resolve, (a, b) => a <= b),
|
|
154
|
-
};
|
|
155
|
-
function compareByDay(record, args, resolve, cmp) {
|
|
156
|
-
const a = toEpochDay(resolve(args[0], record));
|
|
157
|
-
const b = toEpochDay(resolve(args[1], record));
|
|
158
|
-
return a !== null && b !== null && cmp(a, b);
|
|
159
|
-
}
|
|
160
|
-
function compareByTime(record, args, resolve, cmp) {
|
|
161
|
-
const a = parseGenesisDateTime(resolve(args[0], record));
|
|
162
|
-
const b = parseGenesisDateTime(resolve(args[1], record));
|
|
163
|
-
return a !== null && b !== null && cmp(a, b);
|
|
97
|
+
if (typeof value === 'string')
|
|
98
|
+
return value.length > 0;
|
|
99
|
+
if (typeof value === 'number')
|
|
100
|
+
return value !== 0;
|
|
101
|
+
return true;
|
|
164
102
|
}
|
|
103
|
+
const COMPARATORS = new Set(['==', '=', '!=', '<>', '<', '<=', '>', '>=']);
|
|
104
|
+
// CriteriaValidatorVisitor's allowedConstructors (GSF v8.15.29).
|
|
105
|
+
const CONSTRUCTORS = {
|
|
106
|
+
BigDecimal: 'BigDecimal',
|
|
107
|
+
'java.math.BigDecimal': 'BigDecimal',
|
|
108
|
+
Integer: 'Integer',
|
|
109
|
+
'java.lang.Integer': 'Integer',
|
|
110
|
+
Long: 'Long',
|
|
111
|
+
'java.lang.Long': 'Long',
|
|
112
|
+
Double: 'Double',
|
|
113
|
+
'java.lang.Double': 'Double',
|
|
114
|
+
String: 'String',
|
|
115
|
+
'java.lang.String': 'String',
|
|
116
|
+
};
|
|
117
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_.]*$/;
|
|
165
118
|
class Parser {
|
|
166
119
|
tokens;
|
|
167
120
|
pos = 0;
|
|
168
|
-
// Every field the expression reads:
|
|
169
|
-
//
|
|
121
|
+
// Every field the expression reads: field operands, call arguments and
|
|
122
|
+
// call receivers.
|
|
170
123
|
fields = new Set();
|
|
171
124
|
constructor(tokens) {
|
|
172
125
|
this.tokens = tokens;
|
|
@@ -182,16 +135,11 @@ class Parser {
|
|
|
182
135
|
this.pos += 1;
|
|
183
136
|
return token;
|
|
184
137
|
}
|
|
185
|
-
parseExpression() {
|
|
186
|
-
return this.parseOr();
|
|
187
|
-
}
|
|
188
138
|
parseOr() {
|
|
189
139
|
let left = this.parseAnd();
|
|
190
140
|
while (this.peek() === '||' || this.peek()?.toUpperCase() === 'OR') {
|
|
191
141
|
this.next();
|
|
192
|
-
|
|
193
|
-
const prev = left;
|
|
194
|
-
left = (record) => prev(record) || right(record);
|
|
142
|
+
left = { kind: 'or', left, right: this.parseAnd() };
|
|
195
143
|
}
|
|
196
144
|
return left;
|
|
197
145
|
}
|
|
@@ -199,9 +147,7 @@ class Parser {
|
|
|
199
147
|
let left = this.parseNot();
|
|
200
148
|
while (this.peek() === '&&' || this.peek()?.toUpperCase() === 'AND') {
|
|
201
149
|
this.next();
|
|
202
|
-
|
|
203
|
-
const prev = left;
|
|
204
|
-
left = (record) => prev(record) && right(record);
|
|
150
|
+
left = { kind: 'and', left, right: this.parseNot() };
|
|
205
151
|
}
|
|
206
152
|
return left;
|
|
207
153
|
}
|
|
@@ -209,133 +155,556 @@ class Parser {
|
|
|
209
155
|
const next = this.peek();
|
|
210
156
|
if (next === '!' || next?.toUpperCase() === 'NOT') {
|
|
211
157
|
this.next();
|
|
212
|
-
|
|
213
|
-
return (record) => !operand(record);
|
|
158
|
+
return { kind: 'not', operand: this.parseNot() };
|
|
214
159
|
}
|
|
215
|
-
return this.
|
|
160
|
+
return this.parsePrimary();
|
|
216
161
|
}
|
|
217
|
-
|
|
162
|
+
parsePrimary() {
|
|
218
163
|
if (this.peek() === '(') {
|
|
219
164
|
this.next();
|
|
220
|
-
const inner = this.
|
|
165
|
+
const inner = this.parseOr();
|
|
221
166
|
if (this.next() !== ')')
|
|
222
167
|
throw new CriteriaSyntaxError('Missing closing parenthesis in criteria');
|
|
223
168
|
return inner;
|
|
224
169
|
}
|
|
225
|
-
const
|
|
226
|
-
if (leftToken === undefined) {
|
|
227
|
-
throw new CriteriaSyntaxError('Expected operand on the left side of comparison');
|
|
228
|
-
}
|
|
229
|
-
// Both function-call dialects arrive as one dotted token followed by '(':
|
|
230
|
-
// 'Expr.containsIgnoreCase' (foundation-criteria) or
|
|
231
|
-
// 'DEAL_CURRENCIES.containsIgnoreCase' (field method-call form).
|
|
232
|
-
if (leftToken.includes('.') && this.peek() === '(') {
|
|
233
|
-
return this.parseFunctionCall(leftToken);
|
|
234
|
-
}
|
|
170
|
+
const left = this.parseOperand();
|
|
235
171
|
const op = this.peek();
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
this.next();
|
|
240
|
-
const rightToken = this.next();
|
|
241
|
-
if (rightToken === undefined) {
|
|
242
|
-
throw new CriteriaSyntaxError('Expected operand on the right side of comparison');
|
|
172
|
+
if (op !== undefined && COMPARATORS.has(op)) {
|
|
173
|
+
this.next();
|
|
174
|
+
return { kind: 'compare', op, left, right: this.parseOperand() };
|
|
243
175
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const resolveRight = (record) => this.resolveValue(rightToken, record);
|
|
248
|
-
return (record) => {
|
|
249
|
-
const a = resolveLeft(record);
|
|
250
|
-
const b = resolveRight(record);
|
|
251
|
-
switch (op) {
|
|
252
|
-
case '==':
|
|
253
|
-
case '=':
|
|
254
|
-
return a === b;
|
|
255
|
-
case '!=':
|
|
256
|
-
case '<>':
|
|
257
|
-
return a !== b;
|
|
258
|
-
case '>':
|
|
259
|
-
return a > b;
|
|
260
|
-
case '>=':
|
|
261
|
-
return a >= b;
|
|
262
|
-
case '<':
|
|
263
|
-
return a < b;
|
|
264
|
-
case '<=':
|
|
265
|
-
return a <= b;
|
|
266
|
-
default:
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
noteField(token) {
|
|
272
|
-
if (parseLiteral(token) === undefined)
|
|
273
|
-
this.fields.add(token);
|
|
176
|
+
// An operand alone is the condition, by Groovy truth (`ENABLED`,
|
|
177
|
+
// `Expr.isNullOrBlank(NAME)`).
|
|
178
|
+
return left;
|
|
274
179
|
}
|
|
275
|
-
|
|
180
|
+
parseOperand() {
|
|
181
|
+
const token = this.next();
|
|
182
|
+
if (token === undefined)
|
|
183
|
+
throw new CriteriaSyntaxError('Expected an operand');
|
|
184
|
+
if (token === 'new' && this.tokens[this.pos + 1] === '(')
|
|
185
|
+
return this.parseConstructor();
|
|
186
|
+
if (token.includes('.') && this.peek() === '(')
|
|
187
|
+
return this.parseCall(token);
|
|
276
188
|
const literal = parseLiteral(token);
|
|
277
|
-
if (literal
|
|
278
|
-
return literal;
|
|
279
|
-
|
|
189
|
+
if (literal)
|
|
190
|
+
return { kind: 'literal', ...literal };
|
|
191
|
+
if (!NAME.test(token))
|
|
192
|
+
throw new CriteriaSyntaxError(`Unexpected token: ${token}`);
|
|
193
|
+
this.fields.add(token);
|
|
194
|
+
return { kind: 'field', name: token };
|
|
280
195
|
}
|
|
281
|
-
|
|
282
|
-
|
|
196
|
+
// The arguments after a call's or a constructor's '('.
|
|
197
|
+
parseArguments(name) {
|
|
198
|
+
this.next(); // '('
|
|
283
199
|
const args = [];
|
|
284
200
|
if (this.peek() !== ')') {
|
|
285
|
-
|
|
286
|
-
if (arg === undefined)
|
|
287
|
-
throw new CriteriaSyntaxError(`Unterminated argument list in ${name}(...)`);
|
|
288
|
-
args.push(arg);
|
|
201
|
+
args.push(this.parseOperand());
|
|
289
202
|
while (this.peek() === ',') {
|
|
290
203
|
this.next();
|
|
291
|
-
|
|
292
|
-
if (nextArg === undefined)
|
|
293
|
-
throw new CriteriaSyntaxError(`Unterminated argument list in ${name}(...)`);
|
|
294
|
-
args.push(nextArg);
|
|
204
|
+
args.push(this.parseOperand());
|
|
295
205
|
}
|
|
296
206
|
}
|
|
297
207
|
if (this.next() !== ')')
|
|
298
208
|
throw new CriteriaSyntaxError(`Missing closing parenthesis in ${name}(...)`);
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
209
|
+
return args;
|
|
210
|
+
}
|
|
211
|
+
// `new BigDecimal('1.5')`: the constructors GSF's validator allows.
|
|
212
|
+
parseConstructor() {
|
|
213
|
+
const name = this.next();
|
|
214
|
+
const type = Object.hasOwn(CONSTRUCTORS, name) ? CONSTRUCTORS[name] : undefined;
|
|
215
|
+
if (!type)
|
|
216
|
+
throw new UnsupportedCriteriaError(`Unsupported constructor: new ${name}`);
|
|
217
|
+
return { kind: 'ctor', type, args: this.parseArguments(`new ${name}`) };
|
|
218
|
+
}
|
|
219
|
+
parseCall(name) {
|
|
220
|
+
const args = this.parseArguments(name);
|
|
221
|
+
const dot = name.lastIndexOf('.');
|
|
222
|
+
const [receiver, method] = [name.slice(0, dot), name.slice(dot + 1)];
|
|
223
|
+
if (receiver === 'Expr') {
|
|
224
|
+
if (!Object.hasOwn(EXPR_FUNCTIONS, method)) {
|
|
225
|
+
throw new UnsupportedCriteriaError(`Unsupported criteria function: ${name}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
if (!Object.hasOwn(FIELD_METHODS, method) && !Object.hasOwn(EXPR_FUNCTIONS, method)) {
|
|
230
|
+
throw new UnsupportedCriteriaError(`Unsupported criteria function: ${name}`);
|
|
231
|
+
}
|
|
232
|
+
this.fields.add(receiver);
|
|
233
|
+
}
|
|
234
|
+
return { kind: 'call', receiver, method, args };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function parse(criteria) {
|
|
238
|
+
const tokens = tokenizeCriteria(criteria.trim()).map((token) => token.text);
|
|
239
|
+
const parser = new Parser(tokens);
|
|
240
|
+
const root = parser.parseOr();
|
|
241
|
+
if (parser.position !== tokens.length) {
|
|
242
|
+
throw new CriteriaSyntaxError(`Unexpected trailing tokens in criteria: ${criteria}`);
|
|
243
|
+
}
|
|
244
|
+
return { root, fields: [...parser.fields] };
|
|
245
|
+
}
|
|
246
|
+
// --- argument readers --------------------------------------------------------
|
|
247
|
+
// A date in one of GSF's formats; for `untyped` text (a field without a
|
|
248
|
+
// declared type: mock data) ISO-8601 and epoch-millis text as well.
|
|
249
|
+
function instantOfText(text, untyped) {
|
|
250
|
+
if (!untyped)
|
|
251
|
+
return gsf.stringDateTime(text);
|
|
252
|
+
try {
|
|
253
|
+
return gsf.stringDateTime(text);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
const millis = epochMillis(text);
|
|
257
|
+
if (millis === undefined)
|
|
258
|
+
throw error;
|
|
259
|
+
return millis;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const dayOfText = (text, untyped) => gsf.epochDay(instantOfText(text, untyped));
|
|
263
|
+
const isWholeNumber = (value) => typeof value === 'number' || (typeof value === 'string' && /^-?\d+$/.test(value));
|
|
264
|
+
const LONG_TYPES = new Set(['Long', 'Integer', 'Short']);
|
|
265
|
+
// The first argument of a date function, by the overload GSF picks: an
|
|
266
|
+
// instant for a DateTime or a Long (epoch millis), text for a String.
|
|
267
|
+
function dateOperand(arg) {
|
|
268
|
+
if (arg === undefined)
|
|
269
|
+
return evaluationError('Missing argument');
|
|
270
|
+
const { value, type } = arg;
|
|
271
|
+
if (isNullish(value))
|
|
272
|
+
return evaluationError(NULL_POINTER);
|
|
273
|
+
if (value instanceof DateTimeValue)
|
|
274
|
+
return { millis: value.millis };
|
|
275
|
+
if (LONG_TYPES.has(type) || (type === undefined && typeof value === 'number')) {
|
|
276
|
+
return isWholeNumber(value)
|
|
277
|
+
? { millis: Number(value) }
|
|
278
|
+
: evaluationError(`Not a Long: ${String(value)}`);
|
|
279
|
+
}
|
|
280
|
+
if ((type === 'String' || type === undefined) && typeof value === 'string')
|
|
281
|
+
return { text: value };
|
|
282
|
+
return evaluationError(`No date overload for ${String(value)}`);
|
|
283
|
+
}
|
|
284
|
+
// A String argument: null stays null.
|
|
285
|
+
function stringArg(arg) {
|
|
286
|
+
if (arg === undefined)
|
|
287
|
+
return evaluationError('Missing argument');
|
|
288
|
+
if (isNullish(arg.value))
|
|
289
|
+
return null;
|
|
290
|
+
return typeof arg.value === 'string'
|
|
291
|
+
? arg.value
|
|
292
|
+
: evaluationError(`Not a String: ${String(arg.value)}`);
|
|
293
|
+
}
|
|
294
|
+
// A String argument that is dereferenced: null throws.
|
|
295
|
+
function requiredString(arg) {
|
|
296
|
+
return stringArg(arg) ?? evaluationError(NULL_POINTER);
|
|
297
|
+
}
|
|
298
|
+
// The delimiter of containsWordsStartingWithIgnoreCase: its first character.
|
|
299
|
+
function delimiterArg(arg) {
|
|
300
|
+
if (arg === undefined)
|
|
301
|
+
return undefined;
|
|
302
|
+
const text = requiredString(arg);
|
|
303
|
+
return text.length > 0 ? text[0] : evaluationError('String index out of range: 0');
|
|
304
|
+
}
|
|
305
|
+
function instantOf(arg) {
|
|
306
|
+
const operand = dateOperand(arg);
|
|
307
|
+
return 'millis' in operand ? operand.millis : instantOfText(operand.text, arg.untyped === true);
|
|
308
|
+
}
|
|
309
|
+
// dateIs*(date, comparableDate): by day. A DateTime or Long date is compared
|
|
310
|
+
// with a yyyyMMdd date only; a String date and its comparable with any of
|
|
311
|
+
// GSF's formats.
|
|
312
|
+
function compareDays(compare) {
|
|
313
|
+
return ([date, comparable]) => {
|
|
314
|
+
const operand = dateOperand(date);
|
|
315
|
+
const other = requiredString(comparable);
|
|
316
|
+
if ('millis' in operand) {
|
|
317
|
+
return compare(gsf.epochDay(operand.millis), gsf.singlePatternLocalDate(other));
|
|
318
|
+
}
|
|
319
|
+
const day = dayOfText(operand.text, date.untyped === true);
|
|
320
|
+
return compare(day, gsf.stringLocalDate(other));
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
// dateTimeIs*(date, comparableDate): by instant.
|
|
324
|
+
function compareInstants(compare) {
|
|
325
|
+
return ([date, comparable]) => compare(instantOf(date), gsf.stringDateTime(requiredString(comparable)));
|
|
326
|
+
}
|
|
327
|
+
function isNullOrBlank([text]) {
|
|
328
|
+
const value = stringArg(text);
|
|
329
|
+
return value === null || gsf.isBlank(value);
|
|
330
|
+
}
|
|
331
|
+
// A mock row may hold a list in a field (DEAL_CURRENCIES: ['USD', 'GBP']):
|
|
332
|
+
// contains / containsIgnoreCase then test membership. GSF has no such field.
|
|
333
|
+
function listMember(list, needle, ignoreCase) {
|
|
334
|
+
const search = needle === undefined ? null : stringArg(needle);
|
|
335
|
+
if (search === null)
|
|
336
|
+
return false;
|
|
337
|
+
return list.some((item) => typeof item === 'string' &&
|
|
338
|
+
(ignoreCase ? item.toLowerCase() === search.toLowerCase() : item === search));
|
|
339
|
+
}
|
|
340
|
+
function containsIgnoreCase([text, search]) {
|
|
341
|
+
if (Array.isArray(text?.value))
|
|
342
|
+
return listMember(text.value, search, true);
|
|
343
|
+
return gsf.containsIgnoreCase(stringArg(text), stringArg(search));
|
|
344
|
+
}
|
|
345
|
+
function containsWordsStartingWithIgnoreCase([text, search, delimiter]) {
|
|
346
|
+
const separator = delimiterArg(delimiter);
|
|
347
|
+
return gsf.containsWordsStartingWithIgnoreCase(stringArg(text), stringArg(search), separator);
|
|
348
|
+
}
|
|
349
|
+
function dateIsToday([date]) {
|
|
350
|
+
const operand = dateOperand(date);
|
|
351
|
+
const day = 'millis' in operand
|
|
352
|
+
? gsf.epochDay(operand.millis)
|
|
353
|
+
: dayOfText(operand.text, date.untyped === true);
|
|
354
|
+
return day === gsf.today();
|
|
355
|
+
}
|
|
356
|
+
function longArg(arg) {
|
|
357
|
+
if (arg === undefined)
|
|
358
|
+
return evaluationError('Missing argument');
|
|
359
|
+
if (isNullish(arg.value))
|
|
360
|
+
return evaluationError(NULL_POINTER);
|
|
361
|
+
return isWholeNumber(arg.value)
|
|
362
|
+
? Number(arg.value)
|
|
363
|
+
: evaluationError(`Not a Long: ${String(arg.value)}`);
|
|
364
|
+
}
|
|
365
|
+
// What this engine evaluated before GSF's semantics, kept for fidelity:
|
|
366
|
+
// 'legacy' (strict criteria refuse them: GSF's validator doesn't allow them).
|
|
367
|
+
const legacyFunctions = {
|
|
368
|
+
contains: ([text, search]) => {
|
|
369
|
+
if (Array.isArray(text?.value))
|
|
370
|
+
return listMember(text.value, search, false);
|
|
371
|
+
return (typeof text?.value === 'string' &&
|
|
372
|
+
typeof search?.value === 'string' &&
|
|
373
|
+
text.value.includes(search.value));
|
|
374
|
+
},
|
|
375
|
+
equals: ([a, b]) => a?.value === b?.value,
|
|
376
|
+
equalsIgnoreCase: ([a, b]) => {
|
|
377
|
+
if (typeof a?.value !== 'string' || typeof b?.value !== 'string')
|
|
378
|
+
return a?.value === b?.value;
|
|
379
|
+
return a.value.toLowerCase() === b.value.toLowerCase();
|
|
380
|
+
},
|
|
381
|
+
startsWith: ([text, prefix]) => typeof text?.value === 'string' &&
|
|
382
|
+
typeof prefix?.value === 'string' &&
|
|
383
|
+
text.value.startsWith(prefix.value),
|
|
384
|
+
endsWith: ([text, suffix]) => typeof text?.value === 'string' &&
|
|
385
|
+
typeof suffix?.value === 'string' &&
|
|
386
|
+
text.value.endsWith(suffix.value),
|
|
387
|
+
dateTimeIsEqual: compareInstants((a, b) => a === b),
|
|
388
|
+
};
|
|
389
|
+
// Expr.java (GSF v8.15.29), every function its validator allows plus
|
|
390
|
+
// stringDateTime (GSF has it, its validator refuses it).
|
|
391
|
+
const EXPR_FUNCTIONS = {
|
|
392
|
+
...legacyFunctions,
|
|
393
|
+
isNullOrBlank,
|
|
394
|
+
notNullOrBlank: (args) => !isNullOrBlank(args),
|
|
395
|
+
containsIgnoreCase,
|
|
396
|
+
containsWordsStartingWithIgnoreCase,
|
|
397
|
+
dateIsToday,
|
|
398
|
+
dateIsEqual: compareDays((a, b) => a === b),
|
|
399
|
+
dateIsBefore: compareDays((a, b) => a < b),
|
|
400
|
+
dateIsAfter: compareDays((a, b) => a > b),
|
|
401
|
+
dateIsGreaterEqual: compareDays((a, b) => a >= b),
|
|
402
|
+
dateIsLessEqual: compareDays((a, b) => a <= b),
|
|
403
|
+
dateTimeIsAfter: compareInstants((a, b) => a > b),
|
|
404
|
+
dateTimeIsGreaterEqual: compareInstants((a, b) => a >= b),
|
|
405
|
+
dateTimeIsBefore: compareInstants((a, b) => a < b),
|
|
406
|
+
dateTimeIsLessEqual: compareInstants((a, b) => a <= b),
|
|
407
|
+
dateTimeIsInRange: ([date, range, context]) => {
|
|
408
|
+
const [rangeName, contextName] = [requiredString(range), requiredString(context)];
|
|
409
|
+
return gsf.dateTimeIsInRange(instantOf(date), rangeName, contextName);
|
|
410
|
+
},
|
|
411
|
+
longLocalDate: ([date]) => new LocalDateValue(gsf.epochDay(longArg(date))),
|
|
412
|
+
longDateTime: ([date]) => new DateTimeValue(longArg(date)),
|
|
413
|
+
stringLocalDate: ([date]) => new LocalDateValue(dayOfText(requiredString(date), date.untyped === true)),
|
|
414
|
+
stringDateTime: ([date]) => new DateTimeValue(instantOfText(requiredString(date), date.untyped === true)),
|
|
415
|
+
};
|
|
416
|
+
// --- field methods ------------------------------------------------------------
|
|
417
|
+
// Object.equals: the same Java class and value. A field without a known type
|
|
418
|
+
// equals a value of the same kind.
|
|
419
|
+
function javaEquals(receiver, other) {
|
|
420
|
+
const [a, b] = [receiver.value, other.value];
|
|
421
|
+
if (isNullish(b))
|
|
422
|
+
return false;
|
|
423
|
+
if (a instanceof DateTimeValue)
|
|
424
|
+
return b instanceof DateTimeValue && a.millis === b.millis;
|
|
425
|
+
if (a instanceof LocalDateValue)
|
|
426
|
+
return b instanceof LocalDateValue && a.day === b.day;
|
|
427
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
428
|
+
// Long.equals(5) is false: 5 is an Integer.
|
|
429
|
+
if (receiver.type && other.type && receiver.type !== other.type)
|
|
430
|
+
return false;
|
|
431
|
+
return a === b;
|
|
313
432
|
}
|
|
433
|
+
return a === b;
|
|
434
|
+
}
|
|
435
|
+
// The receiver of a String method: null throws (the call dereferences it).
|
|
436
|
+
function stringReceiver(receiver) {
|
|
437
|
+
if (isNullish(receiver.value))
|
|
438
|
+
return evaluationError(NULL_POINTER);
|
|
439
|
+
return typeof receiver.value === 'string'
|
|
440
|
+
? receiver.value
|
|
441
|
+
: evaluationError(`Not a String: ${String(receiver.value)}`);
|
|
442
|
+
}
|
|
443
|
+
// String's own methods, then the ExtraOperators.kt extensions (whose String?
|
|
444
|
+
// receiver may be null: containsIgnoreCase and
|
|
445
|
+
// containsWordsStartingWithIgnoreCase answer false for it).
|
|
446
|
+
const FIELD_METHODS = {
|
|
447
|
+
startsWith: (receiver, [prefix, offset]) => {
|
|
448
|
+
const text = stringReceiver(receiver);
|
|
449
|
+
const start = requiredString(prefix);
|
|
450
|
+
const from = offset === undefined ? 0 : longArg(offset);
|
|
451
|
+
if (from < 0 || from > text.length - start.length)
|
|
452
|
+
return false;
|
|
453
|
+
return text.startsWith(start, from);
|
|
454
|
+
},
|
|
455
|
+
equals: (receiver, [other]) => {
|
|
456
|
+
if (isNullish(receiver.value))
|
|
457
|
+
return evaluationError(NULL_POINTER);
|
|
458
|
+
return other !== undefined && javaEquals(receiver, other);
|
|
459
|
+
},
|
|
460
|
+
equalsIgnoreCase: (receiver, [other]) => {
|
|
461
|
+
const text = stringReceiver(receiver);
|
|
462
|
+
const value = stringArg(other);
|
|
463
|
+
return value !== null && value.length === text.length && gsf.containsIgnoreCase(text, value);
|
|
464
|
+
},
|
|
465
|
+
contains: (receiver, [search]) => {
|
|
466
|
+
if (Array.isArray(receiver.value))
|
|
467
|
+
return listMember(receiver.value, search, false);
|
|
468
|
+
return stringReceiver(receiver).includes(requiredString(search));
|
|
469
|
+
},
|
|
470
|
+
containsIgnoreCase: (receiver, [search]) => containsIgnoreCase([receiver, search]),
|
|
471
|
+
containsWordsStartingWithIgnoreCase: (receiver, [search, delimiter]) => containsWordsStartingWithIgnoreCase([receiver, search, ...(delimiter ? [delimiter] : [])]),
|
|
472
|
+
dateIsToday: (receiver) => dateIsToday([receiver]),
|
|
473
|
+
};
|
|
474
|
+
// --- comparisons ----------------------------------------------------------------
|
|
475
|
+
// Groovy orders null before everything.
|
|
476
|
+
function compareNullable(a, b) {
|
|
477
|
+
if (!isNullish(a) && !isNullish(b))
|
|
478
|
+
return undefined;
|
|
479
|
+
if (isNullish(a) && isNullish(b))
|
|
480
|
+
return 0;
|
|
481
|
+
return isNullish(a) ? -1 : 1;
|
|
482
|
+
}
|
|
483
|
+
function fromOrder(op, order) {
|
|
484
|
+
switch (op) {
|
|
485
|
+
case '==':
|
|
486
|
+
case '=':
|
|
487
|
+
return order === 0;
|
|
488
|
+
case '!=':
|
|
489
|
+
case '<>':
|
|
490
|
+
return order !== 0;
|
|
491
|
+
case '<':
|
|
492
|
+
return order < 0;
|
|
493
|
+
case '<=':
|
|
494
|
+
return order <= 0;
|
|
495
|
+
case '>':
|
|
496
|
+
return order > 0;
|
|
497
|
+
default:
|
|
498
|
+
return order >= 0;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const EQUALITY = new Set(['==', '=', '!=', '<>']);
|
|
502
|
+
// What a DateTime compares with (ExtraOperators.kt compareTo): another
|
|
503
|
+
// DateTime, epoch millis (a Long or an int), a date string. undefined for
|
|
504
|
+
// anything else — a Double, a BigDecimal, a Boolean.
|
|
505
|
+
function instantOperand(operand) {
|
|
506
|
+
const { value, type } = operand;
|
|
507
|
+
if (value instanceof DateTimeValue)
|
|
508
|
+
return value.millis;
|
|
509
|
+
if (typeof value === 'number') {
|
|
510
|
+
return type === 'Double' || type === 'BigDecimal' ? undefined : value;
|
|
511
|
+
}
|
|
512
|
+
if (typeof value === 'string')
|
|
513
|
+
return gsf.stringDateTime(value);
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
// A comparison with a DateTime or a LocalDate on one side. `==` takes a
|
|
517
|
+
// DateTime equal to a date string naming its instant, and never to a
|
|
518
|
+
// one-character string literal (Groovy types that as a char; a field's
|
|
519
|
+
// one-character value is parsed like any other), a Double, a BigDecimal or a
|
|
520
|
+
// Boolean; an unparseable date string throws. A relational
|
|
521
|
+
// operator throws for a pair it can't compare.
|
|
522
|
+
function compareDates(op, a, b) {
|
|
523
|
+
const equality = EQUALITY.has(op);
|
|
524
|
+
const unrelated = () => equality ? op === '!=' || op === '<>' : evaluationError('Cannot compare these values');
|
|
525
|
+
if (a.value instanceof LocalDateValue || b.value instanceof LocalDateValue) {
|
|
526
|
+
if (a.value instanceof LocalDateValue && b.value instanceof LocalDateValue) {
|
|
527
|
+
return fromOrder(op, Math.sign(a.value.day - b.value.day));
|
|
528
|
+
}
|
|
529
|
+
return unrelated();
|
|
530
|
+
}
|
|
531
|
+
const char = (operand) => operand.literal === true && typeof operand.value === 'string' && operand.value.length === 1;
|
|
532
|
+
if (equality && (char(a) || char(b)))
|
|
533
|
+
return unrelated();
|
|
534
|
+
const [x, y] = [instantOperand(a), instantOperand(b)];
|
|
535
|
+
if (x === undefined || y === undefined)
|
|
536
|
+
return unrelated();
|
|
537
|
+
return fromOrder(op, Math.sign(x - y));
|
|
538
|
+
}
|
|
539
|
+
const isDated = (operand) => operand.value instanceof DateTimeValue || operand.value instanceof LocalDateValue;
|
|
540
|
+
function compareValues(op, left, right) {
|
|
541
|
+
const [a, b] = [left.value, right.value];
|
|
542
|
+
const order = compareNullable(a, b);
|
|
543
|
+
if (order !== undefined)
|
|
544
|
+
return fromOrder(op, order);
|
|
545
|
+
if (isDated(left) || isDated(right))
|
|
546
|
+
return compareDates(op, left, right);
|
|
547
|
+
switch (op) {
|
|
548
|
+
case '==':
|
|
549
|
+
case '=':
|
|
550
|
+
return a === b;
|
|
551
|
+
case '!=':
|
|
552
|
+
case '<>':
|
|
553
|
+
return a !== b;
|
|
554
|
+
case '>':
|
|
555
|
+
return a > b;
|
|
556
|
+
case '>=':
|
|
557
|
+
return a >= b;
|
|
558
|
+
case '<':
|
|
559
|
+
return a < b;
|
|
560
|
+
default:
|
|
561
|
+
return a <= b;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// A field as the entity's getter returns it: a DateTime for DATE and
|
|
565
|
+
// DATETIME, a String for STRING and ENUM.
|
|
566
|
+
function readField(name, type, record) {
|
|
567
|
+
const raw = record[name] ?? null;
|
|
568
|
+
if (type === undefined)
|
|
569
|
+
return { value: raw, untyped: true };
|
|
570
|
+
if (raw === null)
|
|
571
|
+
return { value: null, type };
|
|
572
|
+
if (type === 'DateTime') {
|
|
573
|
+
const millis = epochMillis(raw);
|
|
574
|
+
return millis === undefined
|
|
575
|
+
? evaluationError(`Not a date: ${String(raw)}`)
|
|
576
|
+
: { value: new DateTimeValue(millis), type };
|
|
577
|
+
}
|
|
578
|
+
if (type === 'String' && (typeof raw === 'number' || typeof raw === 'boolean')) {
|
|
579
|
+
return { value: String(raw), type };
|
|
580
|
+
}
|
|
581
|
+
return { value: raw, type };
|
|
582
|
+
}
|
|
583
|
+
function compileNode(node, options) {
|
|
584
|
+
switch (node.kind) {
|
|
585
|
+
case 'literal': {
|
|
586
|
+
const value = { value: node.value, type: node.type, literal: true };
|
|
587
|
+
return () => value;
|
|
588
|
+
}
|
|
589
|
+
case 'field': {
|
|
590
|
+
const typeName = options.types?.get(node.name);
|
|
591
|
+
const type = typeName ? ENTITY_TYPES[typeName] : undefined;
|
|
592
|
+
return (record) => readField(node.name, type, record);
|
|
593
|
+
}
|
|
594
|
+
case 'compare': {
|
|
595
|
+
const [left, right] = [compileNode(node.left, options), compileNode(node.right, options)];
|
|
596
|
+
return (record) => ({
|
|
597
|
+
value: compareValues(node.op, left(record), right(record)),
|
|
598
|
+
type: 'Boolean',
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
case 'and':
|
|
602
|
+
case 'or': {
|
|
603
|
+
const [left, right] = [compileNode(node.left, options), compileNode(node.right, options)];
|
|
604
|
+
const and = node.kind === 'and';
|
|
605
|
+
return (record) => {
|
|
606
|
+
const first = groovyTruth(left(record).value);
|
|
607
|
+
if (and ? !first : first)
|
|
608
|
+
return { value: first, type: 'Boolean' };
|
|
609
|
+
return { value: groovyTruth(right(record).value), type: 'Boolean' };
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
case 'not': {
|
|
613
|
+
const operand = compileNode(node.operand, options);
|
|
614
|
+
return (record) => ({ value: !groovyTruth(operand(record).value), type: 'Boolean' });
|
|
615
|
+
}
|
|
616
|
+
case 'call':
|
|
617
|
+
return compileCall(node, options);
|
|
618
|
+
case 'ctor': {
|
|
619
|
+
const args = node.args.map((arg) => compileNode(arg, options));
|
|
620
|
+
return (record) => ({
|
|
621
|
+
value: construct(node.type, args.map((arg) => arg(record))),
|
|
622
|
+
type: node.type,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const JAVA_DECIMAL = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
628
|
+
const JAVA_INTEGER = /^[+-]?\d+$/;
|
|
629
|
+
const JAVA_DOUBLE = /^[+-]?(?:(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?[dDfF]?|Infinity|NaN)$/;
|
|
630
|
+
// `new X(arg)`: the value Java's constructor makes, or the exception it
|
|
631
|
+
// throws (a NumberFormatException for text that isn't the number).
|
|
632
|
+
function construct(type, args) {
|
|
633
|
+
if (type === 'String' && args.length === 0)
|
|
634
|
+
return '';
|
|
635
|
+
if (args.length !== 1)
|
|
636
|
+
return evaluationError('No such constructor');
|
|
637
|
+
const { value } = args[0];
|
|
638
|
+
if (isNullish(value))
|
|
639
|
+
return evaluationError(NULL_POINTER);
|
|
640
|
+
if (type === 'String')
|
|
641
|
+
return typeof value === 'string' ? value : String(value);
|
|
642
|
+
if (typeof value === 'number')
|
|
643
|
+
return value;
|
|
644
|
+
if (typeof value !== 'string')
|
|
645
|
+
return evaluationError('No such constructor');
|
|
646
|
+
const text = type === 'Double' ? value.trim() : value; // only Double.parseDouble trims
|
|
647
|
+
const pattern = type === 'BigDecimal' ? JAVA_DECIMAL : type === 'Double' ? JAVA_DOUBLE : JAVA_INTEGER;
|
|
648
|
+
if (!pattern.test(text))
|
|
649
|
+
return evaluationError(`NumberFormatException: ${value}`);
|
|
650
|
+
const number = Number(text.replace(/[dDfF]$/, ''));
|
|
651
|
+
if (type === 'Integer' && (number < INT_MIN || number > INT_MAX)) {
|
|
652
|
+
return evaluationError(`NumberFormatException: ${value}`);
|
|
653
|
+
}
|
|
654
|
+
return number;
|
|
655
|
+
}
|
|
656
|
+
function compileCall(node, options) {
|
|
657
|
+
const args = node.args.map((arg) => compileNode(arg, options));
|
|
658
|
+
const readArgs = (record) => args.map((arg) => arg(record));
|
|
659
|
+
if (node.receiver === 'Expr') {
|
|
660
|
+
const fn = EXPR_FUNCTIONS[node.method];
|
|
661
|
+
return (record) => ({ value: fn(readArgs(record)) });
|
|
662
|
+
}
|
|
663
|
+
const receiver = compileNode({ kind: 'field', name: node.receiver }, options);
|
|
664
|
+
const method = Object.hasOwn(FIELD_METHODS, node.method) ? FIELD_METHODS[node.method] : undefined;
|
|
665
|
+
if (method) {
|
|
666
|
+
return (record) => ({
|
|
667
|
+
value: method(receiver(record), readArgs(record)),
|
|
668
|
+
type: 'Boolean',
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
// fidelity: 'legacy' — an Expr function called on its first argument.
|
|
672
|
+
const fn = EXPR_FUNCTIONS[node.method];
|
|
673
|
+
return (record) => ({ value: fn([receiver(record), ...readArgs(record)]) });
|
|
314
674
|
}
|
|
315
675
|
// Parses a CRITERIA_MATCH without failing open: throws CriteriaSyntaxError for
|
|
316
676
|
// a malformed one and UnsupportedCriteriaError for a call to a function the
|
|
317
|
-
// grammar doesn't have. A blank criteria matches everything.
|
|
318
|
-
|
|
677
|
+
// grammar doesn't have. A blank criteria matches everything. A row the
|
|
678
|
+
// criteria throws on (gsfExpr.ts) doesn't match.
|
|
679
|
+
export function parseCriteria(criteria, options = {}) {
|
|
319
680
|
if (!criteria || typeof criteria !== 'string' || criteria.trim() === '') {
|
|
320
681
|
return { predicate: () => true, fields: [] };
|
|
321
682
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
683
|
+
if (options.lenient)
|
|
684
|
+
return parseLegacyCriteria(criteria);
|
|
685
|
+
const { root, fields } = parse(criteria);
|
|
686
|
+
const evaluate = compileNode(root, options);
|
|
687
|
+
const predicate = (record) => {
|
|
688
|
+
try {
|
|
689
|
+
return groovyTruth(evaluate(record).value);
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
if (error instanceof CriteriaEvaluationError)
|
|
693
|
+
return false;
|
|
694
|
+
throw error;
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
return { predicate, fields };
|
|
329
698
|
}
|
|
330
699
|
/** Compiles a CRITERIA_MATCH string into a predicate function(record) => boolean. */
|
|
331
|
-
export function compileCriteria(criteria) {
|
|
700
|
+
export function compileCriteria(criteria, options = {}) {
|
|
332
701
|
// Real Genesis clients can emit CRITERIA_MATCH syntax this grammar doesn't
|
|
333
|
-
// cover
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
702
|
+
// cover. Failing open — matching everything and logging — keeps
|
|
703
|
+
// DATA_LOGON/broadcast working (unfiltered) instead of crashing the handler
|
|
704
|
+
// and leaving the client's grid stuck on "Loading...". Strict criteria
|
|
705
|
+
// refuse what GSF refuses before they get here.
|
|
337
706
|
try {
|
|
338
|
-
return parseCriteria(criteria).predicate;
|
|
707
|
+
return parseCriteria(criteria, options).predicate;
|
|
339
708
|
}
|
|
340
709
|
catch (error) {
|
|
341
710
|
console.warn(`[mock-server] Unsupported CRITERIA_MATCH syntax, ignoring filter: ${criteria}`, error);
|
|
@@ -353,8 +722,8 @@ export function criteriaFields(criteria) {
|
|
|
353
722
|
return undefined;
|
|
354
723
|
}
|
|
355
724
|
}
|
|
356
|
-
export function filterByCriteria(rows, criteria) {
|
|
357
|
-
const predicate = compileCriteria(criteria);
|
|
725
|
+
export function filterByCriteria(rows, criteria, options = {}) {
|
|
726
|
+
const predicate = compileCriteria(criteria, options);
|
|
358
727
|
return rows.filter((row) => {
|
|
359
728
|
try {
|
|
360
729
|
return predicate(row);
|
|
@@ -364,3 +733,232 @@ export function filterByCriteria(rows, criteria) {
|
|
|
364
733
|
}
|
|
365
734
|
});
|
|
366
735
|
}
|
|
736
|
+
const S = 'String';
|
|
737
|
+
const DATE_OVERLOADS = [
|
|
738
|
+
[S, S],
|
|
739
|
+
['Long', S],
|
|
740
|
+
['DateTime', S],
|
|
741
|
+
];
|
|
742
|
+
// Expr.java's overloads, and what each function returns.
|
|
743
|
+
const EXPR_SIGNATURES = {
|
|
744
|
+
isNullOrBlank: { overloads: [[S]], returns: 'Boolean' },
|
|
745
|
+
notNullOrBlank: { overloads: [[S]], returns: 'Boolean' },
|
|
746
|
+
containsIgnoreCase: { overloads: [[S, S]], returns: 'Boolean' },
|
|
747
|
+
containsWordsStartingWithIgnoreCase: {
|
|
748
|
+
overloads: [
|
|
749
|
+
[S, S],
|
|
750
|
+
[S, S, S],
|
|
751
|
+
[S, S, 'Character'],
|
|
752
|
+
],
|
|
753
|
+
returns: 'Boolean',
|
|
754
|
+
},
|
|
755
|
+
dateIsToday: { overloads: [['Long'], [S], ['DateTime']], returns: 'Boolean' },
|
|
756
|
+
...Object.fromEntries([
|
|
757
|
+
'dateIsEqual',
|
|
758
|
+
'dateIsBefore',
|
|
759
|
+
'dateIsAfter',
|
|
760
|
+
'dateIsGreaterEqual',
|
|
761
|
+
'dateIsLessEqual',
|
|
762
|
+
'dateTimeIsAfter',
|
|
763
|
+
'dateTimeIsGreaterEqual',
|
|
764
|
+
'dateTimeIsBefore',
|
|
765
|
+
'dateTimeIsLessEqual',
|
|
766
|
+
].map((name) => [name, { overloads: DATE_OVERLOADS, returns: 'Boolean' }])),
|
|
767
|
+
dateTimeIsInRange: {
|
|
768
|
+
overloads: [
|
|
769
|
+
['DateTime', S, S],
|
|
770
|
+
['Long', S, S],
|
|
771
|
+
[S, S, S],
|
|
772
|
+
],
|
|
773
|
+
returns: 'Boolean',
|
|
774
|
+
},
|
|
775
|
+
longLocalDate: { overloads: [['Long']], returns: 'LocalDate' },
|
|
776
|
+
stringLocalDate: { overloads: [[S]], returns: 'LocalDate' },
|
|
777
|
+
longDateTime: { overloads: [['Long']], returns: 'DateTime' },
|
|
778
|
+
stringDateTime: { overloads: [[S]], returns: 'DateTime' },
|
|
779
|
+
};
|
|
780
|
+
const EQUALS = [['Object']];
|
|
781
|
+
const TODAY = [[]];
|
|
782
|
+
// The field methods each receiver type has (String's, Object.equals and the
|
|
783
|
+
// ExtraOperators.kt extensions, whose delimiter is a primitive char).
|
|
784
|
+
const FIELD_SIGNATURES = {
|
|
785
|
+
String: {
|
|
786
|
+
startsWith: [[S], [S, 'int']],
|
|
787
|
+
equals: EQUALS,
|
|
788
|
+
equalsIgnoreCase: [[S]],
|
|
789
|
+
contains: [['CharSequence']],
|
|
790
|
+
containsIgnoreCase: [[S]],
|
|
791
|
+
containsWordsStartingWithIgnoreCase: [[S], [S, S], [S, 'char']],
|
|
792
|
+
dateIsToday: TODAY,
|
|
793
|
+
},
|
|
794
|
+
Long: { equals: EQUALS, dateIsToday: TODAY },
|
|
795
|
+
DateTime: { equals: EQUALS, dateIsToday: TODAY },
|
|
796
|
+
Integer: { equals: EQUALS },
|
|
797
|
+
Short: { equals: EQUALS },
|
|
798
|
+
Double: { equals: EQUALS },
|
|
799
|
+
BigDecimal: { equals: EQUALS },
|
|
800
|
+
Boolean: { equals: EQUALS },
|
|
801
|
+
};
|
|
802
|
+
const PRIMITIVES = new Set(['int', 'char']);
|
|
803
|
+
// Whether Groovy's static type checker passes an argument of type `arg` for a
|
|
804
|
+
// parameter of type `param` (null to any object, Integer and Short widened
|
|
805
|
+
// to Long).
|
|
806
|
+
function assignable(arg, param) {
|
|
807
|
+
if (arg === param || param === 'Object')
|
|
808
|
+
return true;
|
|
809
|
+
if (arg === 'null')
|
|
810
|
+
return !PRIMITIVES.has(param);
|
|
811
|
+
if (param === 'CharSequence')
|
|
812
|
+
return arg === 'String';
|
|
813
|
+
if (param === 'Long')
|
|
814
|
+
return arg === 'Integer' || arg === 'Short';
|
|
815
|
+
if (param === 'int')
|
|
816
|
+
return arg === 'Integer' || arg === 'Short';
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
class CompileFailure extends Error {
|
|
820
|
+
}
|
|
821
|
+
const NUMBERS = new Set([
|
|
822
|
+
'Integer',
|
|
823
|
+
'Short',
|
|
824
|
+
'Long',
|
|
825
|
+
'BigInteger',
|
|
826
|
+
'Double',
|
|
827
|
+
'BigDecimal',
|
|
828
|
+
]);
|
|
829
|
+
// Whether `left < right` compiles: a compareTo for the pair exists — numbers
|
|
830
|
+
// with numbers, a String with a String, a Boolean with a Boolean, and a
|
|
831
|
+
// DateTime with a DateTime, a date string or epoch millis (ExtraOperators.kt).
|
|
832
|
+
// Never with null on the left (Object#compareTo). A LocalDate's comparisons
|
|
833
|
+
// fail when they run, if at all.
|
|
834
|
+
function comparable(left, right) {
|
|
835
|
+
if (left === 'null')
|
|
836
|
+
return false;
|
|
837
|
+
if (right === 'null' || left === 'LocalDate' || right === 'LocalDate')
|
|
838
|
+
return true;
|
|
839
|
+
if (left === 'DateTime')
|
|
840
|
+
return ['DateTime', 'String', 'Long', 'Integer', 'Short'].includes(right);
|
|
841
|
+
if (right === 'DateTime')
|
|
842
|
+
return left === 'String' || left === 'Long';
|
|
843
|
+
if (NUMBERS.has(left))
|
|
844
|
+
return NUMBERS.has(right);
|
|
845
|
+
return left === right;
|
|
846
|
+
}
|
|
847
|
+
// Whether exactly one overload takes these arguments. An argument of unknown
|
|
848
|
+
// type passes any parameter (it can't be checked).
|
|
849
|
+
function checkOverloads(overloads, args) {
|
|
850
|
+
const matching = overloads.filter((params) => params.length === args.length &&
|
|
851
|
+
params.every((param, index) => args[index] === undefined || assignable(args[index], param)));
|
|
852
|
+
if (matching.length === 0)
|
|
853
|
+
throw new CompileFailure();
|
|
854
|
+
// Overloads that all take the arguments (a null) are ambiguous — when the
|
|
855
|
+
// arguments they differ on have known types.
|
|
856
|
+
const differing = args.flatMap((_arg, index) => matching.some((params) => params[index] !== matching[0][index]) ? [index] : []);
|
|
857
|
+
if (matching.length > 1 && differing.every((index) => args[index] !== undefined)) {
|
|
858
|
+
throw new CompileFailure();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// The type GSF's compiler gives a node, checking every call in it; undefined
|
|
862
|
+
// when it can't be known (a field without a declared type).
|
|
863
|
+
function staticType(node, types) {
|
|
864
|
+
switch (node.kind) {
|
|
865
|
+
case 'literal':
|
|
866
|
+
return node.type;
|
|
867
|
+
case 'field': {
|
|
868
|
+
const typeName = types?.get(node.name);
|
|
869
|
+
return typeName ? ENTITY_TYPES[typeName] : undefined;
|
|
870
|
+
}
|
|
871
|
+
case 'compare': {
|
|
872
|
+
const [left, right] = [staticType(node.left, types), staticType(node.right, types)];
|
|
873
|
+
const relational = !EQUALITY.has(node.op);
|
|
874
|
+
if (relational && left && right && !comparable(left, right))
|
|
875
|
+
throw new CompileFailure();
|
|
876
|
+
return 'Boolean';
|
|
877
|
+
}
|
|
878
|
+
case 'and':
|
|
879
|
+
case 'or':
|
|
880
|
+
staticType(node.left, types);
|
|
881
|
+
staticType(node.right, types);
|
|
882
|
+
return 'Boolean';
|
|
883
|
+
case 'not':
|
|
884
|
+
staticType(node.operand, types);
|
|
885
|
+
return 'Boolean';
|
|
886
|
+
case 'ctor':
|
|
887
|
+
return node.type;
|
|
888
|
+
case 'call': {
|
|
889
|
+
const args = node.args.map((arg) => staticType(arg, types));
|
|
890
|
+
if (node.receiver === 'Expr') {
|
|
891
|
+
const signature = EXPR_SIGNATURES[node.method];
|
|
892
|
+
if (!signature)
|
|
893
|
+
return undefined; // not GSF's: the validator refuses it
|
|
894
|
+
checkOverloads(signature.overloads, args);
|
|
895
|
+
return signature.returns;
|
|
896
|
+
}
|
|
897
|
+
const receiver = staticType({ kind: 'field', name: node.receiver }, types);
|
|
898
|
+
if (receiver === undefined || !Object.hasOwn(FIELD_METHODS, node.method))
|
|
899
|
+
return undefined;
|
|
900
|
+
const overloads = FIELD_SIGNATURES[receiver]?.[node.method];
|
|
901
|
+
if (!overloads)
|
|
902
|
+
throw new CompileFailure();
|
|
903
|
+
checkOverloads(overloads, args);
|
|
904
|
+
return 'Boolean';
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
// Guava's UPPER_UNDERSCORE to UPPER_CAMEL / LOWER_CAMEL, as GSF names getters.
|
|
909
|
+
function camelCase(name, upperFirst) {
|
|
910
|
+
const words = name.split('_').map((word, index) => {
|
|
911
|
+
const lowered = word.toLowerCase();
|
|
912
|
+
if (index === 0 && !upperFirst)
|
|
913
|
+
return lowered;
|
|
914
|
+
return lowered.charAt(0).toUpperCase() + lowered.slice(1);
|
|
915
|
+
});
|
|
916
|
+
const joined = words.join('');
|
|
917
|
+
return /^\d/.test(joined) ? `_${joined}` : joined;
|
|
918
|
+
}
|
|
919
|
+
// The getter call CriteriaFilterFactory puts in place of a field.
|
|
920
|
+
function getterOf(name, type) {
|
|
921
|
+
if (type === 'ENUM')
|
|
922
|
+
return `row.get${camelCase(name, true)}()?.toString()`;
|
|
923
|
+
if (type === 'BOOLEAN' && name.startsWith('IS'))
|
|
924
|
+
return `row.${camelCase(name, false)}()`;
|
|
925
|
+
return `row.get${camelCase(name, true)}()`;
|
|
926
|
+
}
|
|
927
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
928
|
+
// The INVALID_CRITERIA TEXT GSF sends when its @CompileStatic compilation
|
|
929
|
+
// of a criteria fails (CriteriaFilterBuilder.getCriteriaFilter, v8.15.29):
|
|
930
|
+
// the criteria as it was compiled — each field the validator saw it read
|
|
931
|
+
// replaced by the entity's getter (CriteriaFilterFactory: longest name
|
|
932
|
+
// first, not where a quote or a word character touches it).
|
|
933
|
+
export function compileFailureText(criteria, fields, types) {
|
|
934
|
+
let replaced = criteria;
|
|
935
|
+
for (const field of [...new Set(fields)].sort((a, b) => b.length - a.length)) {
|
|
936
|
+
const pattern = new RegExp(`(?<!["'\\w])${escapeRegExp(field)}(?!["'\\w])`, 'g');
|
|
937
|
+
replaced = replaced.replace(pattern, () => getterOf(field, types?.get(field)));
|
|
938
|
+
}
|
|
939
|
+
return `Error compiling criteria expression: Failed to compile criteria expression ${replaced}`;
|
|
940
|
+
}
|
|
941
|
+
// What GSF's compilation refuses in a criteria its validator passes: a call
|
|
942
|
+
// no overload takes — the wrong number of arguments, or (where the fields'
|
|
943
|
+
// types are known) an argument of the wrong type, a field method the
|
|
944
|
+
// field's type doesn't have, an ambiguous null — and a relational operator
|
|
945
|
+
// between types that don't compare (`QTY > '5'`). The INVALID_CRITERIA
|
|
946
|
+
// TEXT, or undefined when it compiles or doesn't parse here.
|
|
947
|
+
export function criteriaCompileRefusal(criteria, types) {
|
|
948
|
+
let parsed;
|
|
949
|
+
try {
|
|
950
|
+
parsed = parse(criteria);
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
return undefined;
|
|
954
|
+
}
|
|
955
|
+
try {
|
|
956
|
+
staticType(parsed.root, types);
|
|
957
|
+
return undefined;
|
|
958
|
+
}
|
|
959
|
+
catch (error) {
|
|
960
|
+
if (!(error instanceof CompileFailure))
|
|
961
|
+
throw error;
|
|
962
|
+
}
|
|
963
|
+
return compileFailureText(criteria, parsed.fields, types);
|
|
964
|
+
}
|