@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
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// What GSF's criteria functions compute (genesis-commons v8.15.29: Expr.java,
|
|
2
|
+
// TimeUtils.java, ExtraOperators.kt, TimeRange.kt, TimeContext.kt), for the
|
|
3
|
+
// evaluator in criteria.ts. GSF runs criteria in UTC (Joda's default zone,
|
|
4
|
+
// set by RxDb.kt), so every date here is a UTC one.
|
|
5
|
+
//
|
|
6
|
+
// A Java exception inside a compiled criteria (a date that doesn't parse, a
|
|
7
|
+
// null field, an unknown TimeRange) makes the row not match, however the
|
|
8
|
+
// expression goes on: `!Expr.dateIsEqual(F, 'bad')` matches nothing. These
|
|
9
|
+
// functions throw CriteriaEvaluationError for those, and the evaluator's
|
|
10
|
+
// predicate answers false.
|
|
11
|
+
export class CriteriaEvaluationError extends Error {
|
|
12
|
+
name = 'CriteriaEvaluationError';
|
|
13
|
+
}
|
|
14
|
+
export const evaluationError = (message) => {
|
|
15
|
+
throw new CriteriaEvaluationError(message);
|
|
16
|
+
};
|
|
17
|
+
const HOURS_PER_DAY = 24;
|
|
18
|
+
const MINUTES_PER_HOUR = 60;
|
|
19
|
+
const SECONDS_PER_MINUTE = 60;
|
|
20
|
+
const MS_PER_SECOND = 1000;
|
|
21
|
+
const MS_PER_MINUTE = SECONDS_PER_MINUTE * MS_PER_SECOND;
|
|
22
|
+
const MS_PER_HOUR = MINUTES_PER_HOUR * MS_PER_MINUTE;
|
|
23
|
+
export const MS_PER_DAY = HOURS_PER_DAY * MS_PER_HOUR;
|
|
24
|
+
const DAYS_PER_WEEK = 7;
|
|
25
|
+
const MONTHS_PER_YEAR = 12;
|
|
26
|
+
// --- the calendar (proleptic Gregorian, as Joda's ISO chronology) ------------
|
|
27
|
+
const DAYS_PER_ERA = 146_097;
|
|
28
|
+
const YEARS_PER_ERA = 400;
|
|
29
|
+
const MARCH_BASED_SHIFT = 3; // months before March belong to the year before
|
|
30
|
+
const DAYS_IN_FIVE_MONTHS = 153;
|
|
31
|
+
const FIVE_MONTHS = 5;
|
|
32
|
+
const DAYS_TO_1970 = 719_468;
|
|
33
|
+
const LEAP_EVERY = 4;
|
|
34
|
+
const CENTURY = 100;
|
|
35
|
+
const DAYS_PER_YEAR = 365;
|
|
36
|
+
const DAYS_PER_4_YEARS = 1460;
|
|
37
|
+
const DAYS_PER_100_YEARS = 36_524;
|
|
38
|
+
const DAYS_PER_400_YEARS_MINUS_ONE = 146_096;
|
|
39
|
+
const FEBRUARY = 2;
|
|
40
|
+
const LONG_FEBRUARY = 29;
|
|
41
|
+
const SHORT_FEBRUARY = 28;
|
|
42
|
+
const APRIL = 4;
|
|
43
|
+
const JUNE = 6;
|
|
44
|
+
const SEPTEMBER = 9;
|
|
45
|
+
const NOVEMBER = 11;
|
|
46
|
+
const THIRTY_DAY_MONTHS = new Set([APRIL, JUNE, SEPTEMBER, NOVEMBER]);
|
|
47
|
+
const SHORT_MONTH = 30;
|
|
48
|
+
const LONG_MONTH = 31;
|
|
49
|
+
// Days since 1970-01-01 of a date (month 1-12), any year.
|
|
50
|
+
export function daysFromCivil(year, month, day) {
|
|
51
|
+
const y = month <= 2 ? year - 1 : year;
|
|
52
|
+
const era = Math.floor(y / YEARS_PER_ERA);
|
|
53
|
+
const yearOfEra = y - era * YEARS_PER_ERA;
|
|
54
|
+
const monthFromMarch = (month + MONTHS_PER_YEAR - MARCH_BASED_SHIFT) % MONTHS_PER_YEAR;
|
|
55
|
+
const dayOfYear = Math.floor((DAYS_IN_FIVE_MONTHS * monthFromMarch + 2) / FIVE_MONTHS) + day - 1;
|
|
56
|
+
const dayOfEra = yearOfEra * DAYS_PER_YEAR +
|
|
57
|
+
Math.floor(yearOfEra / LEAP_EVERY) -
|
|
58
|
+
Math.floor(yearOfEra / CENTURY) +
|
|
59
|
+
dayOfYear;
|
|
60
|
+
return era * DAYS_PER_ERA + dayOfEra - DAYS_TO_1970;
|
|
61
|
+
}
|
|
62
|
+
// The date of a day since 1970-01-01.
|
|
63
|
+
export function civilFromDays(days) {
|
|
64
|
+
const z = days + DAYS_TO_1970;
|
|
65
|
+
const era = Math.floor(z / DAYS_PER_ERA);
|
|
66
|
+
const dayOfEra = z - era * DAYS_PER_ERA;
|
|
67
|
+
const yearOfEra = Math.floor((dayOfEra -
|
|
68
|
+
Math.floor(dayOfEra / DAYS_PER_4_YEARS) +
|
|
69
|
+
Math.floor(dayOfEra / DAYS_PER_100_YEARS) -
|
|
70
|
+
Math.floor(dayOfEra / DAYS_PER_400_YEARS_MINUS_ONE)) /
|
|
71
|
+
DAYS_PER_YEAR);
|
|
72
|
+
const dayOfYear = dayOfEra -
|
|
73
|
+
(DAYS_PER_YEAR * yearOfEra +
|
|
74
|
+
Math.floor(yearOfEra / LEAP_EVERY) -
|
|
75
|
+
Math.floor(yearOfEra / CENTURY));
|
|
76
|
+
const monthFromMarch = Math.floor((FIVE_MONTHS * dayOfYear + 2) / DAYS_IN_FIVE_MONTHS);
|
|
77
|
+
const day = dayOfYear - Math.floor((DAYS_IN_FIVE_MONTHS * monthFromMarch + 2) / FIVE_MONTHS) + 1;
|
|
78
|
+
const month = monthFromMarch < MONTHS_PER_YEAR - 2
|
|
79
|
+
? monthFromMarch + MARCH_BASED_SHIFT
|
|
80
|
+
: monthFromMarch - (MONTHS_PER_YEAR - MARCH_BASED_SHIFT);
|
|
81
|
+
const year = yearOfEra + era * YEARS_PER_ERA + (month <= 2 ? 1 : 0);
|
|
82
|
+
return { year, month, day };
|
|
83
|
+
}
|
|
84
|
+
const isLeapYear = (year) => year % LEAP_EVERY === 0 && (year % CENTURY !== 0 || year % YEARS_PER_ERA === 0);
|
|
85
|
+
function daysInMonth(year, month) {
|
|
86
|
+
if (month === FEBRUARY)
|
|
87
|
+
return isLeapYear(year) ? LONG_FEBRUARY : SHORT_FEBRUARY;
|
|
88
|
+
return THIRTY_DAY_MONTHS.has(month) ? SHORT_MONTH : LONG_MONTH;
|
|
89
|
+
}
|
|
90
|
+
export const epochDay = (millis) => Math.floor(millis / MS_PER_DAY);
|
|
91
|
+
// `yyyy` reads at most four digits when another number follows it, and nine
|
|
92
|
+
// otherwise (DateTimeFormat.parsePatternTo); MM, dd, HH, mm and ss two;
|
|
93
|
+
// SSS three.
|
|
94
|
+
const YEAR = { field: 'year', max: 4 };
|
|
95
|
+
const LONG_YEAR = { field: 'year', max: 9 };
|
|
96
|
+
const MONTH = { field: 'month', max: 2 };
|
|
97
|
+
const DAY = { field: 'day', max: 2 };
|
|
98
|
+
const HOUR = { field: 'hour', max: 2 };
|
|
99
|
+
const MINUTE = { field: 'minute', max: 2 };
|
|
100
|
+
const SECOND = { field: 'second', max: 2 };
|
|
101
|
+
const FRACTION = { field: 'fraction', max: 3 };
|
|
102
|
+
// ProcessConstants: DATE_FORMAT_FULL, _TO_SECONDS, _TO_MINUTES, _TO_DATE
|
|
103
|
+
// and DATE_FORMAT_ISO8601_TO_DATE, in ALLOWED_DATE_PARSER's order.
|
|
104
|
+
const TO_DATE = [YEAR, MONTH, DAY];
|
|
105
|
+
const TO_MINUTES = [...TO_DATE, '-', HOUR, ':', MINUTE];
|
|
106
|
+
const TO_SECONDS = [...TO_MINUTES, ':', SECOND];
|
|
107
|
+
const FULL = [...TO_SECONDS, '.', FRACTION];
|
|
108
|
+
const ISO8601_TO_DATE = [LONG_YEAR, '-', MONTH, '-', DAY];
|
|
109
|
+
const ALLOWED_PATTERNS = [FULL, TO_SECONDS, TO_MINUTES, TO_DATE, ISO8601_TO_DATE];
|
|
110
|
+
const DIGIT = /\d/;
|
|
111
|
+
const FRACTION_DIGITS = 3;
|
|
112
|
+
// The fields of `text` when the pattern reads all of it, else undefined.
|
|
113
|
+
function readPattern(text, pattern) {
|
|
114
|
+
const fields = {
|
|
115
|
+
year: 0,
|
|
116
|
+
month: 1,
|
|
117
|
+
day: 1,
|
|
118
|
+
hour: 0,
|
|
119
|
+
minute: 0,
|
|
120
|
+
second: 0,
|
|
121
|
+
millis: 0,
|
|
122
|
+
};
|
|
123
|
+
let at = 0;
|
|
124
|
+
for (const part of pattern) {
|
|
125
|
+
if (typeof part === 'string') {
|
|
126
|
+
if (text[at] !== part)
|
|
127
|
+
return undefined;
|
|
128
|
+
at += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
let sign = 1;
|
|
132
|
+
if (part.field === 'year' && (text[at] === '-' || text[at] === '+')) {
|
|
133
|
+
if (!DIGIT.test(text[at + 1] ?? ''))
|
|
134
|
+
return undefined;
|
|
135
|
+
sign = text[at] === '-' ? -1 : 1;
|
|
136
|
+
at += 1;
|
|
137
|
+
}
|
|
138
|
+
let digits = '';
|
|
139
|
+
while (digits.length < part.max && DIGIT.test(text[at] ?? '')) {
|
|
140
|
+
digits += text[at];
|
|
141
|
+
at += 1;
|
|
142
|
+
}
|
|
143
|
+
if (digits === '')
|
|
144
|
+
return undefined;
|
|
145
|
+
if (part.field === 'fraction') {
|
|
146
|
+
fields.millis = Number(digits.padEnd(FRACTION_DIGITS, '0'));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
fields[part.field] = sign * Number(digits);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return at === text.length ? fields : undefined;
|
|
153
|
+
}
|
|
154
|
+
// Joda's ISO chronology years.
|
|
155
|
+
const MIN_YEAR = -292_275_054;
|
|
156
|
+
const MAX_YEAR = 292_278_993;
|
|
157
|
+
const MAX_HOUR = 23;
|
|
158
|
+
const MAX_MINUTE = 59;
|
|
159
|
+
const MAX_SECOND = 59;
|
|
160
|
+
// The fields' instant, or a Joda IllegalFieldValueException.
|
|
161
|
+
function instantOf(fields, text) {
|
|
162
|
+
const { year, month, day, hour, minute, second, millis } = fields;
|
|
163
|
+
const valid = year >= MIN_YEAR &&
|
|
164
|
+
year <= MAX_YEAR &&
|
|
165
|
+
month >= 1 &&
|
|
166
|
+
month <= MONTHS_PER_YEAR &&
|
|
167
|
+
day >= 1 &&
|
|
168
|
+
day <= daysInMonth(year, month) &&
|
|
169
|
+
hour <= MAX_HOUR &&
|
|
170
|
+
minute <= MAX_MINUTE &&
|
|
171
|
+
second <= MAX_SECOND;
|
|
172
|
+
if (!valid)
|
|
173
|
+
evaluationError(`Cannot parse "${text}": a field value is out of range`);
|
|
174
|
+
return (daysFromCivil(year, month, day) * MS_PER_DAY +
|
|
175
|
+
hour * MS_PER_HOUR +
|
|
176
|
+
minute * MS_PER_MINUTE +
|
|
177
|
+
second * MS_PER_SECOND +
|
|
178
|
+
millis);
|
|
179
|
+
}
|
|
180
|
+
function parseWith(text, patterns) {
|
|
181
|
+
for (const pattern of patterns) {
|
|
182
|
+
const fields = readPattern(text, pattern);
|
|
183
|
+
if (fields)
|
|
184
|
+
return instantOf(fields, text);
|
|
185
|
+
}
|
|
186
|
+
return evaluationError(`Invalid format: "${text}"`);
|
|
187
|
+
}
|
|
188
|
+
// Expr.stringDateTime / TimeUtils.parseDateTime: the instant a criteria date
|
|
189
|
+
// names — yyyyMMdd-HH:mm:ss.SSS, yyyyMMdd-HH:mm:ss, yyyyMMdd-HH:mm, yyyyMMdd
|
|
190
|
+
// or yyyy-MM-dd, in UTC. Joda reads fewer digits than the pattern shows
|
|
191
|
+
// ('2024-1-1', '20240101-9:5') and a fraction of a second ('.5' is 500 ms),
|
|
192
|
+
// nothing else: no 'T', no offset, no spaces.
|
|
193
|
+
export const stringDateTime = (text) => parseWith(text, ALLOWED_PATTERNS);
|
|
194
|
+
// Expr.stringLocalDate / TimeUtils.parseLocalDate: the same formats, as a day
|
|
195
|
+
// (days since 1970-01-01).
|
|
196
|
+
export const stringLocalDate = (text) => epochDay(stringDateTime(text));
|
|
197
|
+
// Expr.singlePatternLocalDate: the day a yyyyMMdd date names — the only form
|
|
198
|
+
// the Long and DateTime overloads of dateIs* take for the date compared to.
|
|
199
|
+
export const singlePatternLocalDate = (text) => epochDay(parseWith(text, [TO_DATE]));
|
|
200
|
+
// DateTime.now().toLocalDate() in UTC.
|
|
201
|
+
export const today = () => epochDay(Date.now());
|
|
202
|
+
// --- strings -----------------------------------------------------------------
|
|
203
|
+
// Character.isWhitespace: the Unicode space separators except the no-break
|
|
204
|
+
// ones, and the ASCII controls \t \n \v \f \r and \u001C-\u001F.
|
|
205
|
+
const JAVA_WHITESPACE = /^[\t\n\v\f\r\p{Zl}\p{Zp}\p{Zs}]$/u;
|
|
206
|
+
const NO_BREAK_SPACE = /^[\u00A0\u2007\u202F]$/u;
|
|
207
|
+
const FILE_SEPARATOR = 0x1c;
|
|
208
|
+
const UNIT_SEPARATOR = 0x1f;
|
|
209
|
+
const isJavaWhitespace = (char) => {
|
|
210
|
+
const code = char.charCodeAt(0);
|
|
211
|
+
if (code >= FILE_SEPARATOR && code <= UNIT_SEPARATOR)
|
|
212
|
+
return true;
|
|
213
|
+
return JAVA_WHITESPACE.test(char) && !NO_BREAK_SPACE.test(char);
|
|
214
|
+
};
|
|
215
|
+
// String.isBlank.
|
|
216
|
+
export const isBlank = (text) => [...text].every(isJavaWhitespace);
|
|
217
|
+
const upper = (char) => {
|
|
218
|
+
const result = char.toUpperCase();
|
|
219
|
+
return result.length === 1 ? result : char;
|
|
220
|
+
};
|
|
221
|
+
const lower = (char) => {
|
|
222
|
+
const result = char.toLowerCase();
|
|
223
|
+
return result.length === 1 ? result : char;
|
|
224
|
+
};
|
|
225
|
+
// String.regionMatches(true, ...): char by char, equal as they are, upper
|
|
226
|
+
// cased, or upper then lower cased.
|
|
227
|
+
function regionMatchesIgnoreCase(text, offset, other, length) {
|
|
228
|
+
for (let index = 0; index < length; index += 1) {
|
|
229
|
+
const a = text[offset + index];
|
|
230
|
+
const b = other[index];
|
|
231
|
+
if (a === b)
|
|
232
|
+
continue;
|
|
233
|
+
const [ua, ub] = [upper(a), upper(b)];
|
|
234
|
+
if (ua === ub || lower(ua) === lower(ub))
|
|
235
|
+
continue;
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
// Expr.containsIgnoreCase and the String?.containsIgnoreCase extension.
|
|
241
|
+
export function containsIgnoreCase(text, search) {
|
|
242
|
+
if (text === null || search === null)
|
|
243
|
+
return false;
|
|
244
|
+
if (search.length === 0)
|
|
245
|
+
return true;
|
|
246
|
+
for (let at = text.length - search.length; at >= 0; at -= 1) {
|
|
247
|
+
if (regionMatchesIgnoreCase(text, at, search, search.length))
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
// Expr.containsWordsStartingWithIgnoreCase and its String? extension: whether
|
|
253
|
+
// a word of `text` — split on the delimiter, ' ' by default — starts with
|
|
254
|
+
// `search`, ignoring case. A word is never shorter than the search.
|
|
255
|
+
export function containsWordsStartingWithIgnoreCase(text, search, delimiter = ' ') {
|
|
256
|
+
if (text === null || search === null)
|
|
257
|
+
return false;
|
|
258
|
+
const length = search.length;
|
|
259
|
+
if (length === 0)
|
|
260
|
+
return true;
|
|
261
|
+
let start = 0;
|
|
262
|
+
let end;
|
|
263
|
+
do {
|
|
264
|
+
end = text.indexOf(delimiter, start);
|
|
265
|
+
if (end === -1)
|
|
266
|
+
end = text.length;
|
|
267
|
+
if (end - start >= length && regionMatchesIgnoreCase(text, start, search, length)) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
start = end + 1;
|
|
271
|
+
} while (end !== text.length);
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
// --- Expr.dateTimeIsInRange --------------------------------------------------
|
|
275
|
+
const TIME_CONTEXT_OFFSETS = { PREVIOUS: 1, CURRENT: 0 };
|
|
276
|
+
const SATURDAY = 6;
|
|
277
|
+
const SUNDAY = 7;
|
|
278
|
+
const THURSDAY_OFFSET = 3; // 1970-01-01 was a Thursday
|
|
279
|
+
// ISO day of week, Monday 1 to Sunday 7.
|
|
280
|
+
const dayOfWeek = (day) => ((((day + THURSDAY_OFFSET) % DAYS_PER_WEEK) + DAYS_PER_WEEK) % DAYS_PER_WEEK) + 1;
|
|
281
|
+
const startOfMonth = (year, month) => {
|
|
282
|
+
const months = year * MONTHS_PER_YEAR + (month - 1);
|
|
283
|
+
const y = Math.floor(months / MONTHS_PER_YEAR);
|
|
284
|
+
return daysFromCivil(y, months - y * MONTHS_PER_YEAR + 1, 1) * MS_PER_DAY;
|
|
285
|
+
};
|
|
286
|
+
// TimeRange.getTimeBounds: [beginning, end) in epoch millis. An enum name
|
|
287
|
+
// TimeRange.valueOf / TimeContext.valueOf doesn't know (they are case
|
|
288
|
+
// sensitive) throws.
|
|
289
|
+
export function timeBounds(range, context, now) {
|
|
290
|
+
const offset = Object.hasOwn(TIME_CONTEXT_OFFSETS, context)
|
|
291
|
+
? TIME_CONTEXT_OFFSETS[context]
|
|
292
|
+
: evaluationError(`No enum constant TimeContext.${context}`);
|
|
293
|
+
const day = epochDay(now);
|
|
294
|
+
const { year, month } = civilFromDays(day);
|
|
295
|
+
switch (range) {
|
|
296
|
+
case 'HOUR': {
|
|
297
|
+
const beginning = Math.floor(now / MS_PER_HOUR) * MS_PER_HOUR - offset * MS_PER_HOUR;
|
|
298
|
+
return [beginning, beginning + MS_PER_HOUR];
|
|
299
|
+
}
|
|
300
|
+
case 'DAY': {
|
|
301
|
+
const beginning = (day - offset) * MS_PER_DAY;
|
|
302
|
+
return [beginning, beginning + MS_PER_DAY];
|
|
303
|
+
}
|
|
304
|
+
case 'WEEK': {
|
|
305
|
+
const shifted = day - offset * DAYS_PER_WEEK;
|
|
306
|
+
const monday = shifted - (dayOfWeek(shifted) - 1);
|
|
307
|
+
return [monday * MS_PER_DAY, (monday + DAYS_PER_WEEK) * MS_PER_DAY];
|
|
308
|
+
}
|
|
309
|
+
case 'WEEK_DAY': {
|
|
310
|
+
const base = day - offset;
|
|
311
|
+
if (offset === 0) {
|
|
312
|
+
return dayOfWeek(base) >= SATURDAY ? [0, 0] : [base * MS_PER_DAY, (base + 1) * MS_PER_DAY];
|
|
313
|
+
}
|
|
314
|
+
const weekday = dayOfWeek(base);
|
|
315
|
+
const friday = weekday === SATURDAY ? base - 1 : weekday === SUNDAY ? base - 2 : base;
|
|
316
|
+
return [friday * MS_PER_DAY, (friday + 1) * MS_PER_DAY];
|
|
317
|
+
}
|
|
318
|
+
case 'MONTH':
|
|
319
|
+
return [startOfMonth(year, month - offset), startOfMonth(year, month - offset + 1)];
|
|
320
|
+
case 'YEAR':
|
|
321
|
+
return [
|
|
322
|
+
daysFromCivil(year - offset, 1, 1) * MS_PER_DAY,
|
|
323
|
+
daysFromCivil(year - offset + 1, 1, 1) * MS_PER_DAY,
|
|
324
|
+
];
|
|
325
|
+
default:
|
|
326
|
+
return evaluationError(`No enum constant TimeRange.${range}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// TimeRange.valueOf(range).isInTimeRange(dateTime, TimeContext.valueOf(context)).
|
|
330
|
+
export function dateTimeIsInRange(millis, range, context) {
|
|
331
|
+
const [beginning, end] = timeBounds(range, context, Date.now());
|
|
332
|
+
return millis >= beginning && millis < end;
|
|
333
|
+
}
|
package/dist/db/identity.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export declare const TIMESTAMP = "TIMESTAMP";
|
|
|
4
4
|
export declare const RECORD_FIELDS: readonly string[];
|
|
5
5
|
export declare class RecordClock {
|
|
6
6
|
private last;
|
|
7
|
-
|
|
7
|
+
private beyondSafe;
|
|
8
|
+
next(): number | string;
|
|
8
9
|
observe(value: unknown): void;
|
|
9
10
|
}
|
|
10
11
|
export declare function withoutRecordFields(row: Row): Row;
|
package/dist/db/identity.js
CHANGED
|
@@ -13,19 +13,40 @@
|
|
|
13
13
|
export const RECORD_ID = 'RECORD_ID';
|
|
14
14
|
export const TIMESTAMP = 'TIMESTAMP';
|
|
15
15
|
export const RECORD_FIELDS = [RECORD_ID, TIMESTAMP];
|
|
16
|
+
const INTEGER_TEXT = /^-?\d+$/;
|
|
16
17
|
// Hands out RECORD_ID/TIMESTAMP values: the current epoch millis, or one more
|
|
17
18
|
// than the last value when that is not ahead of it (seeding a thousand rows
|
|
18
|
-
// runs the counter a second or so ahead of the clock).
|
|
19
|
+
// runs the counter a second or so ahead of the clock). Once a seed brings a
|
|
20
|
+
// real server's id above 2^53 (as a string, which JSON can't carry exactly as
|
|
21
|
+
// a number), later stamps continue from it as strings, so a new row is still
|
|
22
|
+
// the newest one.
|
|
19
23
|
export class RecordClock {
|
|
20
24
|
last = 0;
|
|
25
|
+
beyondSafe;
|
|
21
26
|
next() {
|
|
27
|
+
if (this.beyondSafe !== undefined) {
|
|
28
|
+
this.beyondSafe += 1n;
|
|
29
|
+
return String(this.beyondSafe);
|
|
30
|
+
}
|
|
22
31
|
this.last = Math.max(Date.now(), this.last + 1);
|
|
23
32
|
return this.last;
|
|
24
33
|
}
|
|
25
34
|
// Keeps later stamps above a value a seed row brought with it.
|
|
26
35
|
observe(value) {
|
|
27
|
-
if (typeof value === 'number' && Number.isSafeInteger(value)
|
|
28
|
-
this.last
|
|
36
|
+
if (typeof value === 'number' && Number.isSafeInteger(value)) {
|
|
37
|
+
if (value > this.last)
|
|
38
|
+
this.last = value;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value !== 'string' || !INTEGER_TEXT.test(value))
|
|
42
|
+
return;
|
|
43
|
+
const big = BigInt(value);
|
|
44
|
+
if (big <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
45
|
+
if (Number(big) > this.last)
|
|
46
|
+
this.last = Number(big);
|
|
47
|
+
}
|
|
48
|
+
else if (this.beyondSafe === undefined || big > this.beyondSafe) {
|
|
49
|
+
this.beyondSafe = big;
|
|
29
50
|
}
|
|
30
51
|
}
|
|
31
52
|
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// The criteria evaluator of fidelity: 'legacy' (CriteriaOptions.lenient):
|
|
2
|
+
// this engine's own semantics up to 15.47, kept as they were — JavaScript
|
|
3
|
+
// comparisons (`QTY > -1` holds for a null QTY, `QTY == null` misses a
|
|
4
|
+
// missing field), functions that answer false rather than throw (so
|
|
5
|
+
// `!STATUS.equals('NEW')` keeps a row with no STATUS), inclusive
|
|
6
|
+
// dateTimeIsAfter / dateTimeIsBefore, dates read by Date.parse too, and the
|
|
7
|
+
// AND / OR / NOT, `<>` and `=` dialect. GSF's semantics are criteria.ts's.
|
|
8
|
+
//
|
|
9
|
+
// Only addition: the GSF functions 15.47 didn't have and so failed open on —
|
|
10
|
+
// containsWordsStartingWithIgnoreCase, isNullOrBlank, notNullOrBlank,
|
|
11
|
+
// dateIsAfter, dateIsBefore, dateTimeIsInRange — in the same false-not-throw
|
|
12
|
+
// style.
|
|
13
|
+
import { CriteriaSyntaxError, parseGenesisDateTime, tokenizeCriteria, UnsupportedCriteriaError, } from "./criteriaTokens.js";
|
|
14
|
+
import * as gsf from "./gsfExpr.js";
|
|
15
|
+
function parseLiteral(token) {
|
|
16
|
+
if (/^['"]/.test(token))
|
|
17
|
+
return token.slice(1, -1).replace(/\\(.)/g, '$1');
|
|
18
|
+
if (/^-?\d+(?:\.\d+)?$/.test(token))
|
|
19
|
+
return Number(token);
|
|
20
|
+
if (token === 'true')
|
|
21
|
+
return true;
|
|
22
|
+
if (token === 'false')
|
|
23
|
+
return false;
|
|
24
|
+
if (token === 'null')
|
|
25
|
+
return null;
|
|
26
|
+
return undefined; // not a literal -> treat as a field reference
|
|
27
|
+
}
|
|
28
|
+
const COMPARATORS = new Set(['==', '=', '!=', '<>', '<', '<=', '>', '>=']);
|
|
29
|
+
function toEpochDay(value) {
|
|
30
|
+
const epoch = parseGenesisDateTime(value);
|
|
31
|
+
return epoch === null ? null : gsf.epochDay(epoch);
|
|
32
|
+
}
|
|
33
|
+
function compareByDay(cmp) {
|
|
34
|
+
return (record, args, resolve) => {
|
|
35
|
+
const a = toEpochDay(resolve(args[0], record));
|
|
36
|
+
const b = toEpochDay(resolve(args[1], record));
|
|
37
|
+
return a !== null && b !== null && cmp(a, b);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function compareByTime(cmp) {
|
|
41
|
+
return (record, args, resolve) => {
|
|
42
|
+
const a = parseGenesisDateTime(resolve(args[0], record));
|
|
43
|
+
const b = parseGenesisDateTime(resolve(args[1], record));
|
|
44
|
+
return a !== null && b !== null && cmp(a, b);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const isBlankValue = (value) => value === null || value === undefined || (typeof value === 'string' && gsf.isBlank(value));
|
|
48
|
+
// Expr.<fn>(field, value), and the same functions as <FIELD>.<fn>(value)
|
|
49
|
+
// with the receiver as the first argument.
|
|
50
|
+
const LEGACY_FUNCTIONS = {
|
|
51
|
+
// Multi-value fields (e.g. DEAL_CURRENCIES) may hold an array — treat
|
|
52
|
+
// "contains" as membership there, substring match on strings.
|
|
53
|
+
contains: (record, args, resolve) => {
|
|
54
|
+
const value = resolve(args[0], record);
|
|
55
|
+
const needle = resolve(args[1], record);
|
|
56
|
+
if (typeof needle !== 'string')
|
|
57
|
+
return false;
|
|
58
|
+
if (Array.isArray(value))
|
|
59
|
+
return value.some((v) => typeof v === 'string' && v === needle);
|
|
60
|
+
return typeof value === 'string' && value.includes(needle);
|
|
61
|
+
},
|
|
62
|
+
containsIgnoreCase: (record, args, resolve) => {
|
|
63
|
+
const value = resolve(args[0], record);
|
|
64
|
+
const needle = resolve(args[1], record);
|
|
65
|
+
if (typeof needle !== 'string')
|
|
66
|
+
return false;
|
|
67
|
+
const lowered = needle.toLowerCase();
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return value.some((v) => typeof v === 'string' && v.toLowerCase() === lowered);
|
|
70
|
+
}
|
|
71
|
+
return typeof value === 'string' && value.toLowerCase().includes(lowered);
|
|
72
|
+
},
|
|
73
|
+
equals: (record, args, resolve) => resolve(args[0], record) === resolve(args[1], record),
|
|
74
|
+
equalsIgnoreCase: (record, args, resolve) => {
|
|
75
|
+
const value = resolve(args[0], record);
|
|
76
|
+
const other = resolve(args[1], record);
|
|
77
|
+
if (typeof value !== 'string' || typeof other !== 'string')
|
|
78
|
+
return value === other;
|
|
79
|
+
return value.toLowerCase() === other.toLowerCase();
|
|
80
|
+
},
|
|
81
|
+
startsWith: (record, args, resolve) => {
|
|
82
|
+
const value = resolve(args[0], record);
|
|
83
|
+
const prefix = resolve(args[1], record);
|
|
84
|
+
return typeof value === 'string' && typeof prefix === 'string' && value.startsWith(prefix);
|
|
85
|
+
},
|
|
86
|
+
endsWith: (record, args, resolve) => {
|
|
87
|
+
const value = resolve(args[0], record);
|
|
88
|
+
const suffix = resolve(args[1], record);
|
|
89
|
+
return typeof value === 'string' && typeof suffix === 'string' && value.endsWith(suffix);
|
|
90
|
+
},
|
|
91
|
+
dateIsToday: (record, args, resolve) => {
|
|
92
|
+
const day = toEpochDay(resolve(args[0], record));
|
|
93
|
+
return day !== null && day === toEpochDay(Date.now());
|
|
94
|
+
},
|
|
95
|
+
dateIsEqual: compareByDay((a, b) => a === b),
|
|
96
|
+
dateIsGreaterEqual: compareByDay((a, b) => a >= b),
|
|
97
|
+
dateIsLessEqual: compareByDay((a, b) => a <= b),
|
|
98
|
+
dateTimeIsEqual: compareByTime((a, b) => a === b),
|
|
99
|
+
dateTimeIsGreaterEqual: compareByTime((a, b) => a >= b),
|
|
100
|
+
dateTimeIsLessEqual: compareByTime((a, b) => a <= b),
|
|
101
|
+
dateTimeIsAfter: compareByTime((a, b) => a >= b),
|
|
102
|
+
dateTimeIsBefore: compareByTime((a, b) => a <= b),
|
|
103
|
+
// GSF's, which 15.47 didn't have.
|
|
104
|
+
dateIsAfter: compareByDay((a, b) => a > b),
|
|
105
|
+
dateIsBefore: compareByDay((a, b) => a < b),
|
|
106
|
+
isNullOrBlank: (record, args, resolve) => isBlankValue(resolve(args[0], record)),
|
|
107
|
+
notNullOrBlank: (record, args, resolve) => !isBlankValue(resolve(args[0], record)),
|
|
108
|
+
containsWordsStartingWithIgnoreCase: (record, args, resolve) => {
|
|
109
|
+
const value = resolve(args[0], record);
|
|
110
|
+
const search = resolve(args[1], record);
|
|
111
|
+
const delimiter = args.length > 2 ? resolve(args[2], record) : ' ';
|
|
112
|
+
if (typeof value !== 'string' || typeof search !== 'string')
|
|
113
|
+
return false;
|
|
114
|
+
if (typeof delimiter !== 'string' || delimiter === '')
|
|
115
|
+
return false;
|
|
116
|
+
return gsf.containsWordsStartingWithIgnoreCase(value, search, delimiter[0]);
|
|
117
|
+
},
|
|
118
|
+
dateTimeIsInRange: (record, args, resolve) => {
|
|
119
|
+
const millis = parseGenesisDateTime(resolve(args[0], record));
|
|
120
|
+
const [range, context] = [resolve(args[1], record), resolve(args[2], record)];
|
|
121
|
+
if (millis === null || typeof range !== 'string' || typeof context !== 'string')
|
|
122
|
+
return false;
|
|
123
|
+
try {
|
|
124
|
+
return gsf.dateTimeIsInRange(millis, range, context);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
class Parser {
|
|
132
|
+
tokens;
|
|
133
|
+
pos = 0;
|
|
134
|
+
fields = new Set();
|
|
135
|
+
constructor(tokens) {
|
|
136
|
+
this.tokens = tokens;
|
|
137
|
+
}
|
|
138
|
+
get position() {
|
|
139
|
+
return this.pos;
|
|
140
|
+
}
|
|
141
|
+
peek() {
|
|
142
|
+
return this.tokens[this.pos];
|
|
143
|
+
}
|
|
144
|
+
next() {
|
|
145
|
+
const token = this.tokens[this.pos];
|
|
146
|
+
this.pos += 1;
|
|
147
|
+
return token;
|
|
148
|
+
}
|
|
149
|
+
parseOr() {
|
|
150
|
+
let left = this.parseAnd();
|
|
151
|
+
while (this.peek() === '||' || this.peek()?.toUpperCase() === 'OR') {
|
|
152
|
+
this.next();
|
|
153
|
+
const right = this.parseAnd();
|
|
154
|
+
const prev = left;
|
|
155
|
+
left = (record) => prev(record) || right(record);
|
|
156
|
+
}
|
|
157
|
+
return left;
|
|
158
|
+
}
|
|
159
|
+
parseAnd() {
|
|
160
|
+
let left = this.parseNot();
|
|
161
|
+
while (this.peek() === '&&' || this.peek()?.toUpperCase() === 'AND') {
|
|
162
|
+
this.next();
|
|
163
|
+
const right = this.parseNot();
|
|
164
|
+
const prev = left;
|
|
165
|
+
left = (record) => prev(record) && right(record);
|
|
166
|
+
}
|
|
167
|
+
return left;
|
|
168
|
+
}
|
|
169
|
+
parseNot() {
|
|
170
|
+
const next = this.peek();
|
|
171
|
+
if (next === '!' || next?.toUpperCase() === 'NOT') {
|
|
172
|
+
this.next();
|
|
173
|
+
const operand = this.parseNot();
|
|
174
|
+
return (record) => !operand(record);
|
|
175
|
+
}
|
|
176
|
+
return this.parseComparison();
|
|
177
|
+
}
|
|
178
|
+
parseComparison() {
|
|
179
|
+
if (this.peek() === '(') {
|
|
180
|
+
this.next();
|
|
181
|
+
const inner = this.parseOr();
|
|
182
|
+
if (this.next() !== ')')
|
|
183
|
+
throw new CriteriaSyntaxError('Missing closing parenthesis in criteria');
|
|
184
|
+
return inner;
|
|
185
|
+
}
|
|
186
|
+
const leftToken = this.next();
|
|
187
|
+
if (leftToken === undefined) {
|
|
188
|
+
throw new CriteriaSyntaxError('Expected operand on the left side of comparison');
|
|
189
|
+
}
|
|
190
|
+
if (leftToken.includes('.') && this.peek() === '(')
|
|
191
|
+
return this.parseFunctionCall(leftToken);
|
|
192
|
+
const op = this.peek();
|
|
193
|
+
if (!op || !COMPARATORS.has(op))
|
|
194
|
+
throw new CriteriaSyntaxError(`Expected comparator, got: ${op}`);
|
|
195
|
+
this.next();
|
|
196
|
+
const rightToken = this.next();
|
|
197
|
+
if (rightToken === undefined) {
|
|
198
|
+
throw new CriteriaSyntaxError('Expected operand on the right side of comparison');
|
|
199
|
+
}
|
|
200
|
+
this.noteField(leftToken);
|
|
201
|
+
this.noteField(rightToken);
|
|
202
|
+
const resolveLeft = (record) => this.resolveValue(leftToken, record);
|
|
203
|
+
const resolveRight = (record) => this.resolveValue(rightToken, record);
|
|
204
|
+
return (record) => {
|
|
205
|
+
const a = resolveLeft(record);
|
|
206
|
+
const b = resolveRight(record);
|
|
207
|
+
switch (op) {
|
|
208
|
+
case '==':
|
|
209
|
+
case '=':
|
|
210
|
+
return a === b;
|
|
211
|
+
case '!=':
|
|
212
|
+
case '<>':
|
|
213
|
+
return a !== b;
|
|
214
|
+
case '>':
|
|
215
|
+
return a > b;
|
|
216
|
+
case '>=':
|
|
217
|
+
return a >= b;
|
|
218
|
+
case '<':
|
|
219
|
+
return a < b;
|
|
220
|
+
case '<=':
|
|
221
|
+
return a <= b;
|
|
222
|
+
default:
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
noteField(token) {
|
|
228
|
+
if (parseLiteral(token) === undefined)
|
|
229
|
+
this.fields.add(token);
|
|
230
|
+
}
|
|
231
|
+
resolveValue(token, record) {
|
|
232
|
+
const literal = parseLiteral(token);
|
|
233
|
+
if (literal !== undefined)
|
|
234
|
+
return literal;
|
|
235
|
+
return record[token];
|
|
236
|
+
}
|
|
237
|
+
parseFunctionCall(name) {
|
|
238
|
+
this.next(); // consume '('
|
|
239
|
+
const args = [];
|
|
240
|
+
if (this.peek() !== ')') {
|
|
241
|
+
const arg = this.next();
|
|
242
|
+
if (arg === undefined)
|
|
243
|
+
throw new CriteriaSyntaxError(`Unterminated argument list in ${name}(...)`);
|
|
244
|
+
args.push(arg);
|
|
245
|
+
while (this.peek() === ',') {
|
|
246
|
+
this.next();
|
|
247
|
+
const nextArg = this.next();
|
|
248
|
+
if (nextArg === undefined) {
|
|
249
|
+
throw new CriteriaSyntaxError(`Unterminated argument list in ${name}(...)`);
|
|
250
|
+
}
|
|
251
|
+
args.push(nextArg);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (this.next() !== ')')
|
|
255
|
+
throw new CriteriaSyntaxError(`Missing closing parenthesis in ${name}(...)`);
|
|
256
|
+
const dotIndex = name.lastIndexOf('.');
|
|
257
|
+
const receiver = name.slice(0, dotIndex);
|
|
258
|
+
const fnName = name.slice(dotIndex + 1);
|
|
259
|
+
const handler = Object.hasOwn(LEGACY_FUNCTIONS, fnName) ? LEGACY_FUNCTIONS[fnName] : undefined;
|
|
260
|
+
if (!handler)
|
|
261
|
+
throw new UnsupportedCriteriaError(`Unsupported criteria function: ${name}`);
|
|
262
|
+
const allArgs = receiver === 'Expr' ? args : [receiver, ...args];
|
|
263
|
+
for (const arg of allArgs)
|
|
264
|
+
this.noteField(arg);
|
|
265
|
+
const resolve = (token, record) => this.resolveValue(token, record);
|
|
266
|
+
return (record) => handler(record, allArgs, resolve);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// A criteria as fidelity: 'legacy' evaluates it: its predicate and the fields
|
|
270
|
+
// it reads. Throws CriteriaSyntaxError for what this grammar can't parse
|
|
271
|
+
// and UnsupportedCriteriaError for a function it doesn't have.
|
|
272
|
+
export function parseLegacyCriteria(criteria) {
|
|
273
|
+
const tokens = tokenizeCriteria(criteria.trim()).map((token) => token.text);
|
|
274
|
+
const parser = new Parser(tokens);
|
|
275
|
+
const predicate = parser.parseOr();
|
|
276
|
+
if (parser.position !== tokens.length) {
|
|
277
|
+
throw new CriteriaSyntaxError(`Unexpected trailing tokens in criteria: ${criteria}`);
|
|
278
|
+
}
|
|
279
|
+
return { predicate, fields: [...parser.fields] };
|
|
280
|
+
}
|
package/dist/db/ordering.d.ts
CHANGED
|
@@ -4,5 +4,7 @@ export interface OrderedField {
|
|
|
4
4
|
field: string;
|
|
5
5
|
descending: boolean;
|
|
6
6
|
}
|
|
7
|
+
export declare function compareRecordIds(a: unknown, b: unknown): number;
|
|
8
|
+
export declare function byRecordId(a: Row, b: Row): number;
|
|
7
9
|
export declare function rowComparator(fields: OrderedField[]): (a: Row, b: Row) => number;
|
|
8
10
|
export declare function parseOrderBy(orderBy: string): OrderedField[];
|