@genesislcap/mock-server 15.51.1 → 15.53.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 (70) hide show
  1. package/README.md +408 -39
  2. package/dist/db/criteria.d.ts +13 -0
  3. package/dist/db/criteria.js +60 -21
  4. package/dist/db/identity.d.ts +11 -0
  5. package/dist/db/identity.js +46 -0
  6. package/dist/db/ordering.d.ts +8 -0
  7. package/dist/db/ordering.js +49 -0
  8. package/dist/db/schema.d.ts +62 -0
  9. package/dist/db/schema.js +490 -0
  10. package/dist/db/store.d.ts +20 -3
  11. package/dist/db/store.js +300 -50
  12. package/dist/handlers/commitEvent.js +47 -9
  13. package/dist/handlers/crudEvents.d.ts +2 -0
  14. package/dist/handlers/crudEvents.js +138 -0
  15. package/dist/handlers/dataLogon.js +105 -6
  16. package/dist/handlers/eventValidation.d.ts +5 -0
  17. package/dist/handlers/eventValidation.js +29 -0
  18. package/dist/handlers/jsonSchema.d.ts +4 -2
  19. package/dist/handlers/jsonSchema.js +222 -61
  20. package/dist/handlers/meta.d.ts +6 -1
  21. package/dist/handlers/meta.js +168 -21
  22. package/dist/handlers/requestReply.js +519 -30
  23. package/dist/handlers/resourceAuth.d.ts +1 -0
  24. package/dist/handlers/resourceAuth.js +14 -0
  25. package/dist/handlers/resources.js +3 -0
  26. package/dist/index.d.ts +3 -2
  27. package/dist/index.js +2 -1
  28. package/dist/protocol/connection.d.ts +2 -0
  29. package/dist/protocol/errors.d.ts +100 -2
  30. package/dist/protocol/errors.js +300 -12
  31. package/dist/protocol/fieldTypes.d.ts +10 -0
  32. package/dist/protocol/fieldTypes.js +107 -0
  33. package/dist/protocol/gsfSchemas.d.ts +9 -0
  34. package/dist/protocol/gsfSchemas.js +423 -0
  35. package/dist/protocol/messageTypes.d.ts +1 -0
  36. package/dist/protocol/messageTypes.js +1 -0
  37. package/dist/protocol/msgNack.d.ts +2 -0
  38. package/dist/protocol/msgNack.js +10 -0
  39. package/dist/protocol/rowUpdate.d.ts +5 -3
  40. package/dist/protocol/rowUpdate.js +33 -15
  41. package/dist/protocol/schemaValidation.d.ts +11 -0
  42. package/dist/protocol/schemaValidation.js +186 -0
  43. package/dist/server.js +95 -29
  44. package/dist/types.d.ts +45 -0
  45. package/package.json +1 -1
  46. package/src/db/criteria.ts +72 -21
  47. package/src/db/identity.ts +53 -0
  48. package/src/db/ordering.ts +56 -0
  49. package/src/db/schema.ts +672 -0
  50. package/src/db/store.ts +343 -50
  51. package/src/handlers/commitEvent.ts +54 -10
  52. package/src/handlers/crudEvents.ts +181 -0
  53. package/src/handlers/dataLogon.ts +127 -7
  54. package/src/handlers/eventValidation.ts +43 -0
  55. package/src/handlers/jsonSchema.ts +302 -69
  56. package/src/handlers/meta.ts +227 -23
  57. package/src/handlers/requestReply.ts +626 -33
  58. package/src/handlers/resourceAuth.ts +18 -0
  59. package/src/handlers/resources.ts +3 -0
  60. package/src/index.ts +17 -1
  61. package/src/protocol/connection.ts +10 -2
  62. package/src/protocol/errors.ts +361 -13
  63. package/src/protocol/fieldTypes.ts +115 -0
  64. package/src/protocol/gsfSchemas.ts +505 -0
  65. package/src/protocol/messageTypes.ts +1 -0
  66. package/src/protocol/msgNack.ts +12 -0
  67. package/src/protocol/rowUpdate.ts +40 -20
  68. package/src/protocol/schemaValidation.ts +208 -0
  69. package/src/server.ts +106 -28
  70. package/src/types.ts +187 -5
@@ -15,6 +15,17 @@
15
15
  // Deliberately NOT eval()/new Function() — this parses a small typed grammar
16
16
  // so a malformed or hostile criteria string can only fail to parse, never
17
17
  // execute arbitrary JS.
18
+ // A criteria this grammar can't parse: unbalanced parentheses, a missing
19
+ // operand, text it can't tokenize. GSF fails to compile those too.
20
+ export class CriteriaSyntaxError extends Error {
21
+ name = 'CriteriaSyntaxError';
22
+ }
23
+ // A well-formed call to a function EXPR_FUNCTIONS doesn't have. It may well be
24
+ // valid Groovy, so callers that refuse malformed criteria still fail open on
25
+ // this one.
26
+ export class UnsupportedCriteriaError extends Error {
27
+ name = 'UnsupportedCriteriaError';
28
+ }
18
29
  const TOKEN_RE = /\s*(&&|\|\||<=|>=|!=|<>|==|=|<|>|!|\(|\)|,|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?|[A-Za-z_][A-Za-z0-9_.]*)/g;
19
30
  function tokenize(input) {
20
31
  const tokens = [];
@@ -23,13 +34,13 @@ function tokenize(input) {
23
34
  TOKEN_RE.lastIndex = 0;
24
35
  while ((match = TOKEN_RE.exec(input))) {
25
36
  if (match.index !== lastIndex) {
26
- throw new Error(`Unable to tokenize criteria near: ${input.slice(lastIndex)}`);
37
+ throw new CriteriaSyntaxError(`Unable to tokenize criteria near: ${input.slice(lastIndex)}`);
27
38
  }
28
39
  tokens.push(match[1]);
29
40
  lastIndex = TOKEN_RE.lastIndex;
30
41
  }
31
42
  if (lastIndex !== input.length) {
32
- throw new Error(`Unable to tokenize criteria near: ${input.slice(lastIndex)}`);
43
+ throw new CriteriaSyntaxError(`Unable to tokenize criteria near: ${input.slice(lastIndex)}`);
33
44
  }
34
45
  return tokens;
35
46
  }
@@ -53,7 +64,7 @@ const COMPARATORS = new Set(['==', '=', '!=', '<>', '<', '<=', '>', '>=']);
53
64
  // them makes every Expr.dateTimeIs* comparison silently false (date-filtered
54
65
  // grids render zero rows with no warning). Row values may already be epoch
55
66
  // millis (number) or an ISO string. Returns null if unparseable.
56
- function parseGenesisDateTime(value) {
67
+ export function parseGenesisDateTime(value) {
57
68
  if (typeof value === 'number')
58
69
  return value;
59
70
  if (typeof value !== 'string')
@@ -154,6 +165,9 @@ function compareByTime(record, args, resolve, cmp) {
154
165
  class Parser {
155
166
  tokens;
156
167
  pos = 0;
168
+ // Every field the expression reads: bare identifiers in comparisons, and
169
+ // function-call arguments and receivers.
170
+ fields = new Set();
157
171
  constructor(tokens) {
158
172
  this.tokens = tokens;
159
173
  }
@@ -205,12 +219,12 @@ class Parser {
205
219
  this.next();
206
220
  const inner = this.parseExpression();
207
221
  if (this.next() !== ')')
208
- throw new Error('Missing closing parenthesis in criteria');
222
+ throw new CriteriaSyntaxError('Missing closing parenthesis in criteria');
209
223
  return inner;
210
224
  }
211
225
  const leftToken = this.next();
212
226
  if (leftToken === undefined) {
213
- throw new Error('Expected operand on the left side of comparison');
227
+ throw new CriteriaSyntaxError('Expected operand on the left side of comparison');
214
228
  }
215
229
  // Both function-call dialects arrive as one dotted token followed by '(':
216
230
  // 'Expr.containsIgnoreCase' (foundation-criteria) or
@@ -220,13 +234,15 @@ class Parser {
220
234
  }
221
235
  const op = this.peek();
222
236
  if (!op || !COMPARATORS.has(op)) {
223
- throw new Error(`Expected comparator, got: ${op}`);
237
+ throw new CriteriaSyntaxError(`Expected comparator, got: ${op}`);
224
238
  }
225
239
  this.next();
226
240
  const rightToken = this.next();
227
241
  if (rightToken === undefined) {
228
- throw new Error('Expected operand on the right side of comparison');
242
+ throw new CriteriaSyntaxError('Expected operand on the right side of comparison');
229
243
  }
244
+ this.noteField(leftToken);
245
+ this.noteField(rightToken);
230
246
  const resolveLeft = (record) => this.resolveValue(leftToken, record);
231
247
  const resolveRight = (record) => this.resolveValue(rightToken, record);
232
248
  return (record) => {
@@ -252,6 +268,10 @@ class Parser {
252
268
  }
253
269
  };
254
270
  }
271
+ noteField(token) {
272
+ if (parseLiteral(token) === undefined)
273
+ this.fields.add(token);
274
+ }
255
275
  resolveValue(token, record) {
256
276
  const literal = parseLiteral(token);
257
277
  if (literal !== undefined)
@@ -264,18 +284,18 @@ class Parser {
264
284
  if (this.peek() !== ')') {
265
285
  const arg = this.next();
266
286
  if (arg === undefined)
267
- throw new Error(`Unterminated argument list in ${name}(...)`);
287
+ throw new CriteriaSyntaxError(`Unterminated argument list in ${name}(...)`);
268
288
  args.push(arg);
269
289
  while (this.peek() === ',') {
270
290
  this.next();
271
291
  const nextArg = this.next();
272
292
  if (nextArg === undefined)
273
- throw new Error(`Unterminated argument list in ${name}(...)`);
293
+ throw new CriteriaSyntaxError(`Unterminated argument list in ${name}(...)`);
274
294
  args.push(nextArg);
275
295
  }
276
296
  }
277
297
  if (this.next() !== ')')
278
- throw new Error(`Missing closing parenthesis in ${name}(...)`);
298
+ throw new CriteriaSyntaxError(`Missing closing parenthesis in ${name}(...)`);
279
299
  // 'Expr.<fn>(field, value)' passes args through as-is; the method-call
280
300
  // form '<FIELD>.<fn>(value)' makes the receiver field the implicit first
281
301
  // argument, so both dialects share EXPR_FUNCTIONS.
@@ -284,36 +304,55 @@ class Parser {
284
304
  const fnName = name.slice(dotIndex + 1);
285
305
  const handler = EXPR_FUNCTIONS[fnName];
286
306
  if (!handler)
287
- throw new Error(`Unsupported criteria function: ${name}`);
307
+ throw new UnsupportedCriteriaError(`Unsupported criteria function: ${name}`);
288
308
  const allArgs = receiver === 'Expr' ? args : [receiver, ...args];
309
+ for (const arg of allArgs)
310
+ this.noteField(arg);
289
311
  const resolve = (token, record) => this.resolveValue(token, record);
290
312
  return (record) => handler(record, allArgs, resolve);
291
313
  }
292
314
  }
293
- /** Compiles a CRITERIA_MATCH string into a predicate function(record) => boolean. */
294
- export function compileCriteria(criteria) {
315
+ // Parses a CRITERIA_MATCH without failing open: throws CriteriaSyntaxError for
316
+ // a malformed one and UnsupportedCriteriaError for a call to a function the
317
+ // grammar doesn't have. A blank criteria matches everything.
318
+ export function parseCriteria(criteria) {
295
319
  if (!criteria || typeof criteria !== 'string' || criteria.trim() === '') {
296
- return () => true;
320
+ return { predicate: () => true, fields: [] };
297
321
  }
322
+ const tokens = tokenize(criteria.trim());
323
+ const parser = new Parser(tokens);
324
+ const predicate = parser.parseExpression();
325
+ if (parser.position !== tokens.length) {
326
+ throw new CriteriaSyntaxError(`Unexpected trailing tokens in criteria: ${criteria}`);
327
+ }
328
+ return { predicate, fields: [...parser.fields] };
329
+ }
330
+ /** Compiles a CRITERIA_MATCH string into a predicate function(record) => boolean. */
331
+ export function compileCriteria(criteria) {
298
332
  // Real Genesis clients can emit CRITERIA_MATCH syntax this grammar doesn't
299
333
  // cover yet (e.g. function-call criteria like Expr.dateTimeIsGreaterEqual(...)
300
334
  // from date-range filters). Failing open — matching everything and logging —
301
335
  // keeps DATA_LOGON/broadcast working (unfiltered) instead of crashing the
302
336
  // handler and leaving the client's grid stuck on "Loading...".
303
337
  try {
304
- const tokens = tokenize(criteria.trim());
305
- const parser = new Parser(tokens);
306
- const predicate = parser.parseExpression();
307
- if (parser.position !== tokens.length) {
308
- throw new Error(`Unexpected trailing tokens in criteria: ${criteria}`);
309
- }
310
- return predicate;
338
+ return parseCriteria(criteria).predicate;
311
339
  }
312
340
  catch (error) {
313
341
  console.warn(`[mock-server] Unsupported CRITERIA_MATCH syntax, ignoring filter: ${criteria}`, error);
314
342
  return () => true;
315
343
  }
316
344
  }
345
+ // The fields a CRITERIA_MATCH reads, or undefined when it doesn't parse
346
+ // (compileCriteria then fails open). The real dataserver refuses a criteria
347
+ // naming a field its query doesn't expose (INVALID_CRITERIA).
348
+ export function criteriaFields(criteria) {
349
+ try {
350
+ return parseCriteria(criteria).fields;
351
+ }
352
+ catch {
353
+ return undefined;
354
+ }
355
+ }
317
356
  export function filterByCriteria(rows, criteria) {
318
357
  const predicate = compileCriteria(criteria);
319
358
  return rows.filter((row) => {
@@ -0,0 +1,11 @@
1
+ import type { Row } from '../types.ts';
2
+ export declare const RECORD_ID = "RECORD_ID";
3
+ export declare const TIMESTAMP = "TIMESTAMP";
4
+ export declare const RECORD_FIELDS: readonly string[];
5
+ export declare class RecordClock {
6
+ private last;
7
+ next(): number;
8
+ observe(value: unknown): void;
9
+ }
10
+ export declare function withoutRecordFields(row: Row): Row;
11
+ export declare function recordKey(row: Row | undefined | null): string | undefined;
@@ -0,0 +1,46 @@
1
+ // Record identity: the RECORD_ID and TIMESTAMP every Genesis table row carries.
2
+ //
3
+ // GSF sets TIMESTAMP from a high-resolution clock on every write and
4
+ // RECORD_ID = TIMESTAMP on insert (genesis-db InsertOperation.kt); a modify
5
+ // moves TIMESTAMP on and keeps RECORD_ID. The real values are around 7.5e18:
6
+ // epoch millis shifted left 22 bits, plus a counter. That is far above 2^53,
7
+ // so a JavaScript client's JSON.parse rounds neighbouring rows onto the same
8
+ // number. The mock deliberately stays below that: its stamps are a counter
9
+ // seeded from epoch millis that only goes up, so they stay exact, strictly
10
+ // increasing safe integers, and a client that reads TIMESTAMP as a date (the
11
+ // AI assistant shows a NANO_TIMESTAMP as epoch millis) still gets roughly the
12
+ // write time. The same order, a different magnitude.
13
+ export const RECORD_ID = 'RECORD_ID';
14
+ export const TIMESTAMP = 'TIMESTAMP';
15
+ export const RECORD_FIELDS = [RECORD_ID, TIMESTAMP];
16
+ // Hands out RECORD_ID/TIMESTAMP values: the current epoch millis, or one more
17
+ // 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
+ export class RecordClock {
20
+ last = 0;
21
+ next() {
22
+ this.last = Math.max(Date.now(), this.last + 1);
23
+ return this.last;
24
+ }
25
+ // Keeps later stamps above a value a seed row brought with it.
26
+ observe(value) {
27
+ if (typeof value === 'number' && Number.isSafeInteger(value) && value > this.last) {
28
+ this.last = value;
29
+ }
30
+ }
31
+ }
32
+ // A row without its record identity — what a dataserver sends (the real
33
+ // dataserver never puts RECORD_ID or TIMESTAMP in a row, only
34
+ // DETAILS.ROW_REF) and what field metadata is derived from.
35
+ export function withoutRecordFields(row) {
36
+ if (!(RECORD_ID in row) && !(TIMESTAMP in row))
37
+ return row;
38
+ const { [RECORD_ID]: _recordId, [TIMESTAMP]: _timestamp, ...rest } = row;
39
+ return rest;
40
+ }
41
+ // The Map key a record is tracked under. RECORD_IDs are numbers here, but a
42
+ // seed may bring a recorded string, so they are compared as strings.
43
+ export function recordKey(row) {
44
+ const id = row?.[RECORD_ID];
45
+ return id === undefined || id === null ? undefined : String(id);
46
+ }
@@ -0,0 +1,8 @@
1
+ import type { Row } from '../types.ts';
2
+ export declare function compareValues(a: unknown, b: unknown): number;
3
+ export interface OrderedField {
4
+ field: string;
5
+ descending: boolean;
6
+ }
7
+ export declare function rowComparator(fields: OrderedField[]): (a: Row, b: Row) => number;
8
+ export declare function parseOrderBy(orderBy: string): OrderedField[];
@@ -0,0 +1,49 @@
1
+ // Row ordering shared by the dataserver (DATA_LOGON ORDER_BY, an index name)
2
+ // and request servers (REQ_ DETAILS.ORDER_BY, fields with a direction).
3
+ import { RECORD_ID } from "./identity.js";
4
+ // Nulls sort low (first ascending, last descending), as H2's default null
5
+ // ordering does; numbers numerically, booleans false before true, anything
6
+ // else by its string form (UTF-16 order, as Java's String.compareTo).
7
+ export function compareValues(a, b) {
8
+ if (a === b)
9
+ return 0;
10
+ if (a === undefined || a === null)
11
+ return -1;
12
+ if (b === undefined || b === null)
13
+ return 1;
14
+ if (typeof a === 'number' && typeof b === 'number')
15
+ return a - b;
16
+ if (typeof a === 'boolean' && typeof b === 'boolean')
17
+ return a ? 1 : -1;
18
+ const [left, right] = [String(a), String(b)];
19
+ return left < right ? -1 : left > right ? 1 : 0;
20
+ }
21
+ // Sorts by the fields in turn, then by RECORD_ID ascending: the tie-breaker
22
+ // column GSF's SQL ordering always appends (GetByCriteriaOperation.kt,
23
+ // tieBreakerColumn = "RECORD_ID", v8.15.29), whatever the direction.
24
+ export function rowComparator(fields) {
25
+ return (a, b) => {
26
+ for (const { field, descending } of fields) {
27
+ const result = compareValues(a[field], b[field]);
28
+ if (result !== 0)
29
+ return descending ? -result : result;
30
+ }
31
+ return compareValues(a[RECORD_ID], b[RECORD_ID]);
32
+ };
33
+ }
34
+ const ORDER_SEPARATOR = /\s*,\s*/;
35
+ const ORDER_FIELD = /^([A-Za-z]\w+)(?:\s+(ASC|DESC))?$/i;
36
+ // DETAILS.ORDER_BY as a request server reads it (OrderSpec.kt OrderedString,
37
+ // GSF v8.15.29): comma-separated fields, each with an optional ASC or DESC in
38
+ // any case. Throws GSF's IllegalArgumentException text for anything else,
39
+ // which the request server answers as REQUEST_FAILED.
40
+ export function parseOrderBy(orderBy) {
41
+ return orderBy.split(ORDER_SEPARATOR).map((part) => {
42
+ const match = ORDER_FIELD.exec(part);
43
+ if (!match) {
44
+ throw new Error(`${orderBy} is not a valid ORDER string, Fields should be comma separated, with followed ` +
45
+ "by an optional ASC or DESC. E.g. 'TRADER, TRADE_DATE DESC'");
46
+ }
47
+ return { field: match[1], descending: match[2]?.toUpperCase() === 'DESC' };
48
+ });
49
+ }
@@ -0,0 +1,62 @@
1
+ import type { CrudEventVerb, FieldDef, IndexDef, MockServerConfig, QueryDef, RequestReplyDef, Row, TableDef } from '../types.ts';
2
+ import type { Store } from './store.ts';
3
+ export interface ResolvedField {
4
+ name: string;
5
+ type: string;
6
+ nullable: boolean;
7
+ generated: boolean;
8
+ values?: string[];
9
+ default?: unknown;
10
+ maxSize?: number;
11
+ minLength?: number;
12
+ description?: string;
13
+ title?: string;
14
+ optional?: boolean;
15
+ table?: string;
16
+ }
17
+ export interface ResolvedSchema {
18
+ fields: ResolvedField[];
19
+ declared: boolean;
20
+ }
21
+ export declare function pkFieldsOf(def: TableDef): string[];
22
+ export declare function generatedFieldsOf(def: TableDef): Set<string>;
23
+ export declare function generatedFieldList(def: TableDef): string[];
24
+ export declare function isDeclared(source: string, config: MockServerConfig, store: Store): boolean;
25
+ export declare function resolveSourceSchema(source: string, config: MockServerConfig, store: Store): ResolvedSchema;
26
+ export declare function withTypeOverrides(schema: ResolvedSchema, fieldTypes: Record<string, string> | undefined): ResolvedSchema;
27
+ export declare function projectSchema(schema: ResolvedSchema, names: string[] | undefined): ResolvedSchema;
28
+ export declare function querySchema(query: QueryDef, config: MockServerConfig, store: Store): ResolvedSchema;
29
+ export declare function requestReplySchema(requestReply: RequestReplyDef, config: MockServerConfig, store: Store): ResolvedSchema;
30
+ export declare const RECORD_IDENTITY_FIELDS: ResolvedField[];
31
+ export declare function requestReplySourceSchema(requestReply: RequestReplyDef, config: MockServerConfig, store: Store): ResolvedSchema;
32
+ export declare function requestReplyReplySchema(requestReply: RequestReplyDef, config: MockServerConfig, store: Store): ResolvedSchema;
33
+ export declare function requestReplyFieldNames(requestReply: RequestReplyDef, config: MockServerConfig, store: Store): string[] | undefined;
34
+ export declare function requestFieldsOf(requestReply: RequestReplyDef, store: Store): string[];
35
+ export declare function knownValues(field: ResolvedField): string[] | undefined;
36
+ export declare function eventDefaultOf(field: ResolvedField): unknown;
37
+ export declare function queryFieldNames(query: QueryDef | undefined, config: MockServerConfig, store: Store): string[] | undefined;
38
+ export declare function projectRow(row: Row, names: string[] | undefined): Row;
39
+ export declare function defaultIndexName(table: string, fields: string[]): string;
40
+ export declare function primaryKeyIndexName(table: string, def: TableDef): string;
41
+ export declare function secondaryUniqueIndexes(def: TableDef): IndexDef[];
42
+ export declare const CRUD_EVENT_VERBS: readonly CrudEventVerb[];
43
+ export declare function crudEventName(table: string, verb: CrudEventVerb): string;
44
+ export declare function crudEventOf(name: string | undefined, config: MockServerConfig): {
45
+ table: string;
46
+ verb: CrudEventVerb;
47
+ } | undefined;
48
+ export declare function crudEventNames(config: MockServerConfig): string[];
49
+ export declare function isDeleteEventName(name: string): boolean;
50
+ export declare function inferEventTable(eventName: string | undefined, tableNames: string[]): string | undefined;
51
+ export interface EventTarget {
52
+ kind: 'table' | 'byId' | 'dto';
53
+ table?: string;
54
+ className: string;
55
+ title?: string;
56
+ fieldTypes?: Record<string, string>;
57
+ fields?: FieldDef[];
58
+ }
59
+ export declare function resolveEventTarget(name: string | undefined, config: MockServerConfig, { bareTable }?: {
60
+ bareTable?: boolean;
61
+ }): EventTarget | undefined;
62
+ export declare function eventFields(target: EventTarget, config: MockServerConfig, store: Store): ResolvedField[];