@genesislcap/mock-server 15.59.0 → 15.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +287 -40
  2. package/dist/db/criteria.d.ts +28 -9
  3. package/dist/db/criteria.js +856 -258
  4. package/dist/db/criteriaDialect.d.ts +6 -0
  5. package/dist/db/criteriaDialect.js +441 -0
  6. package/dist/db/criteriaTokens.d.ts +12 -0
  7. package/dist/db/criteriaTokens.js +53 -0
  8. package/dist/db/groovySyntax.d.ts +26 -0
  9. package/dist/db/groovySyntax.js +481 -0
  10. package/dist/db/groovyValidator.d.ts +11 -0
  11. package/dist/db/groovyValidator.js +523 -0
  12. package/dist/db/gsfExpr.d.ts +21 -0
  13. package/dist/db/gsfExpr.js +333 -0
  14. package/dist/db/identity.d.ts +2 -1
  15. package/dist/db/identity.js +24 -3
  16. package/dist/db/legacyCriteria.d.ts +7 -0
  17. package/dist/db/legacyCriteria.js +280 -0
  18. package/dist/db/ordering.d.ts +2 -0
  19. package/dist/db/ordering.js +24 -3
  20. package/dist/db/schema.d.ts +2 -0
  21. package/dist/db/schema.js +34 -3
  22. package/dist/db/store.d.ts +7 -0
  23. package/dist/db/store.js +55 -7
  24. package/dist/db/values.d.ts +19 -0
  25. package/dist/db/values.js +243 -0
  26. package/dist/handlers/commitEvent.js +3 -2
  27. package/dist/handlers/dataLogon.d.ts +2 -1
  28. package/dist/handlers/dataLogon.js +108 -51
  29. package/dist/handlers/eventValidation.d.ts +2 -1
  30. package/dist/handlers/eventValidation.js +33 -6
  31. package/dist/handlers/requestReply.js +43 -16
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +1 -1
  34. package/dist/protocol/connection.d.ts +1 -1
  35. package/dist/server.js +15 -8
  36. package/dist/types.d.ts +1 -0
  37. package/package.json +1 -1
  38. package/src/db/criteria.ts +979 -290
  39. package/src/db/criteriaDialect.ts +487 -0
  40. package/src/db/criteriaTokens.ts +72 -0
  41. package/src/db/groovySyntax.ts +500 -0
  42. package/src/db/groovyValidator.ts +546 -0
  43. package/src/db/gsfExpr.ts +392 -0
  44. package/src/db/identity.ts +22 -4
  45. package/src/db/legacyCriteria.ts +301 -0
  46. package/src/db/ordering.ts +27 -3
  47. package/src/db/schema.ts +43 -8
  48. package/src/db/store.ts +65 -7
  49. package/src/db/values.ts +247 -0
  50. package/src/handlers/commitEvent.ts +3 -2
  51. package/src/handlers/dataLogon.ts +134 -56
  52. package/src/handlers/eventValidation.ts +40 -13
  53. package/src/handlers/requestReply.ts +47 -14
  54. package/src/index.ts +6 -1
  55. package/src/protocol/connection.ts +3 -4
  56. package/src/server.ts +16 -15
  57. package/src/types.ts +7 -0
@@ -16,13 +16,17 @@
16
16
  // See the README's "Request servers" for every rule and its evidence.
17
17
 
18
18
  import {
19
+ criteriaCompileRefusal,
19
20
  CriteriaSyntaxError,
20
21
  filterByCriteria,
21
22
  parseCriteria,
22
23
  parseGenesisDateTime,
23
24
  UnsupportedCriteriaError,
25
+ type ParsedCriteria,
24
26
  type Predicate,
25
27
  } from '../db/criteria.ts';
28
+ import { strictCriteriaRefusal } from '../db/criteriaDialect.ts';
29
+ import { checkGroovySyntax } from '../db/groovySyntax.ts';
26
30
  import { withoutRecordFields } from '../db/identity.ts';
27
31
  import { compareValues, parseOrderBy, rowComparator, type OrderedField } from '../db/ordering.ts';
28
32
  import {
@@ -34,6 +38,7 @@ import {
34
38
  type ResolvedField,
35
39
  } from '../db/schema.ts';
36
40
  import type { Store } from '../db/store.ts';
41
+ import { bigDecimalFields, decimalText, encodeRow } from '../db/values.ts';
37
42
  import type { Connection } from '../protocol/connection.ts';
38
43
  import { ErrorCode, NackError, nackPayload } from '../protocol/errors.ts';
39
44
  import { isLegacyFidelity } from '../protocol/fidelity.ts';
@@ -92,6 +97,9 @@ interface Source {
92
97
  // Every field the entity has (its columns, RECORD_ID and TIMESTAMP), by name.
93
98
  fields: Map<string, ResolvedField>;
94
99
  pkFields: string[];
100
+ // Each field's Genesis type when the source declares its fields: what GSF
101
+ // compiles a criteria against (db/criteria.ts).
102
+ types?: Map<string, string>;
95
103
  }
96
104
 
97
105
  function sourceOf(requestReply: RequestReplyDef, config: MockServerConfig, store: Store): Source {
@@ -101,6 +109,9 @@ function sourceOf(requestReply: RequestReplyDef, config: MockServerConfig, store
101
109
  name,
102
110
  fields: new Map(schema.fields.map((field) => [field.name, field])),
103
111
  pkFields: store.getPkFields(name),
112
+ ...(schema.declared
113
+ ? { types: new Map(schema.fields.map((field) => [field.name, field.type])) }
114
+ : {}),
104
115
  };
105
116
  }
106
117
 
@@ -117,29 +128,47 @@ function parametricTypeOf(name: string, config: MockServerConfig, store: Store):
117
128
  const byPrimaryKey = (source: Source): OrderedField[] =>
118
129
  source.pkFields.map((field) => ({ field, descending: false }));
119
130
 
131
+ // The REP_ rows as they go on the wire: each BigDecimal a string with its
132
+ // column's scale, as GSF's serializer writes it (db/values.ts).
133
+ function wireRows(rows: Row[], source: Source): Row[] {
134
+ const fields = bigDecimalFields([...source.fields.values()]);
135
+ return fields ? rows.map((row) => encodeRow(row, fields)) : rows;
136
+ }
137
+
120
138
  // --- criteria ----------------------------------------------------------------
121
139
 
122
140
  type CriteriaFilter = { kind: 'all' } | { kind: 'none' } | { kind: 'match'; predicate: Predicate };
123
141
 
124
142
  // CRITERIA_MATCH against the entity. A malformed one fails as `onSyntaxError`
125
- // says (the two server kinds differ). A criteria naming a field the entity
126
- // doesn't have matches nothing: GSF evaluates it outside the database, where
127
- // the unknown variable never compares true (recorded: REPLY: []). A call to a
128
- // function this grammar doesn't model may be valid Groovy, so it still fails
129
- // open (logged), as everywhere else in the engine.
143
+ // says (the two server kinds differ): one Groovy refuses (db/groovySyntax.ts),
144
+ // or one this grammar can't parse that the Groovy check can't vouch for
145
+ // either. A criteria naming a field the entity doesn't have matches nothing:
146
+ // GSF evaluates it outside the database, where the unknown variable never
147
+ // compares true (recorded: REPLY: []). Valid Groovy this grammar doesn't model
148
+ // (a bare boolean field, `in [...]`, a function it doesn't know) fails open
149
+ // (logged), as everywhere else in the engine.
130
150
  function criteriaFilter(
131
151
  criteria: string,
132
152
  source: Source,
133
153
  onSyntaxError: (error: CriteriaSyntaxError) => Error,
134
154
  ): CriteriaFilter {
135
155
  if (criteria.trim() === '') return { kind: 'all' };
156
+ // What GSF can't compile: a syntax error Groovy reports, this engine's
157
+ // dialect (AND, OR, NOT, `<>`, `=`, a call off GSF's whitelist, ...), or a
158
+ // call no overload takes, with GSF's text.
159
+ const refusal =
160
+ strictCriteriaRefusal(criteria, new Set(source.fields.keys()), source.types) ??
161
+ criteriaCompileRefusal(criteria, source.types);
162
+ if (refusal) throw onSyntaxError(new CriteriaSyntaxError(refusal));
163
+ const groovy = checkGroovySyntax(criteria);
164
+ let parsed: ParsedCriteria;
136
165
  try {
137
- const { predicate, fields } = parseCriteria(criteria);
138
- if (fields.some((field) => !source.fields.has(field))) return { kind: 'none' };
139
- return { kind: 'match', predicate };
166
+ parsed = parseCriteria(criteria, { types: source.types });
140
167
  } catch (error) {
141
- if (error instanceof CriteriaSyntaxError) throw onSyntaxError(error);
142
- if (error instanceof UnsupportedCriteriaError) {
168
+ if (error instanceof CriteriaSyntaxError && groovy.kind !== 'valid') {
169
+ throw onSyntaxError(error);
170
+ }
171
+ if (error instanceof CriteriaSyntaxError || error instanceof UnsupportedCriteriaError) {
143
172
  console.warn(
144
173
  `[mock-server] Unsupported CRITERIA_MATCH syntax, ignoring filter: ${criteria}`,
145
174
  error,
@@ -148,6 +177,8 @@ function criteriaFilter(
148
177
  }
149
178
  throw error;
150
179
  }
180
+ if (parsed.fields.some((field) => !source.fields.has(field))) return { kind: 'none' };
181
+ return { kind: 'match', predicate: parsed.predicate };
151
182
  }
152
183
 
153
184
  function applyCriteria(rows: Row[], filter: CriteriaFilter): Row[] {
@@ -338,7 +369,7 @@ function criteriaOnlyReply(
338
369
  }
339
370
 
340
371
  return {
341
- REPLY: page.rows,
372
+ REPLY: wireRows(page.rows, source),
342
373
  PARAMETRIC_TYPE: parametricTypeOf(source.name, config, store),
343
374
  MESSAGE_TYPE: `REP_${resourceName}`,
344
375
  ...(byView && page.rows.length > 0 ? { NEXT_VIEW: viewNumber + 1 } : {}),
@@ -389,9 +420,11 @@ function convertValue(value: unknown, field: ResolvedField): unknown {
389
420
  }
390
421
 
391
422
  // What Kotlin's toString() makes of a row value, for wildcard matching: a
392
- // Double keeps its fraction ("2.0"), a date prints as ISO-8601, null is "null".
423
+ // Double keeps its fraction ("2.0"), a BigDecimal its scale ("15.00000"), a
424
+ // date prints as ISO-8601, null is "null".
393
425
  function kotlinString(value: unknown, field: ResolvedField | undefined): string {
394
426
  if (value === undefined || value === null) return 'null';
427
+ if (field?.type === 'BIGDECIMAL') return decimalText(value, field.scale) ?? String(value);
395
428
  if (typeof value === 'number') {
396
429
  if ((field?.type === 'DATE' || field?.type === 'DATETIME') && Number.isFinite(value)) {
397
430
  return new Date(value).toISOString();
@@ -538,7 +571,7 @@ function plainReply(
538
571
  MESSAGE_TYPE: `REP_${resourceName}`,
539
572
  SOURCE_REF: message.SOURCE_REF,
540
573
  PARAMETRIC_TYPE: parametricTypeOf(source.name, config, store),
541
- REPLY: reply,
574
+ REPLY: wireRows(reply, source),
542
575
  };
543
576
  }
544
577
 
@@ -667,7 +700,7 @@ function legacyRequestReply(
667
700
  rows = rows.filter((row) => matchesLegacyRequestFields(row, request));
668
701
  }
669
702
  const criteria = message.DETAILS?.CRITERIA_MATCH ?? message.CRITERIA_MATCH;
670
- if (criteria) rows = filterByCriteria(rows, criteria);
703
+ if (criteria) rows = filterByCriteria(rows, criteria, { lenient: true });
671
704
  if (requestReply.filter) rows = rows.filter(requestReply.filter);
672
705
  }
673
706
  rows = applyResourceAuth(rows, requestReply.auth, user).map(withoutRecordFields);
package/src/index.ts CHANGED
@@ -12,7 +12,12 @@ export {
12
12
  type NackErrorInput,
13
13
  type NackErrorOptions,
14
14
  } from './protocol/errors.ts';
15
- export { compileCriteria, filterByCriteria, type Predicate } from './db/criteria.ts';
15
+ export {
16
+ compileCriteria,
17
+ type CriteriaOptions,
18
+ filterByCriteria,
19
+ type Predicate,
20
+ } from './db/criteria.ts';
16
21
  export type {
17
22
  AntiJoinDef,
18
23
  AuthConfig,
@@ -31,10 +31,9 @@ export interface Subscription {
31
31
  // The columns this subscription's rows carry (the query's fields block or
32
32
  // declared columns, narrowed by DETAILS.FIELDS); undefined: rows as stored.
33
33
  fieldNames?: string[];
34
- // Set under ORDER_BY: offered every live INSERT (stamped, and the raw
35
- // row). Returns true when the row sorts among the rows still waiting for
36
- // MORE_ROWS, and so was queued there in order instead of being pushed.
37
- queueInsert?: (key: string, stamped: Row, row: Row) => boolean;
34
+ // Turns a row into its wire form (BigDecimals as scaled strings); unset
35
+ // when the query has nothing to encode, and under fidelity: 'legacy'.
36
+ encode?: (row: Row) => Row;
38
37
  }
39
38
 
40
39
  // Wraps one WebSocket client connection: auth state and its live DATA_LOGON
package/src/server.ts CHANGED
@@ -82,7 +82,9 @@ export interface MockServer {
82
82
  }
83
83
 
84
84
  export function createMockServer(config: MockServerConfig): MockServer {
85
- const store = new Store();
85
+ // Declared columns are held by type (db/values.ts), except under
86
+ // fidelity: 'legacy', which keeps rows as given.
87
+ const store = new Store({ normalizeValues: () => !isLegacyFidelity(config) });
86
88
  for (const [tableName, tableDef] of Object.entries(config.tables ?? {})) {
87
89
  store.registerTable(tableName, tableDef);
88
90
  }
@@ -119,6 +121,13 @@ export function createMockServer(config: MockServerConfig): MockServer {
119
121
  }
120
122
  }
121
123
 
124
+ // A row as one subscription sends it: its columns (dataserverRow), each
125
+ // BigDecimal a scaled string (Subscription.encode).
126
+ function wireRow(subscription: Subscription, permitted: Row, row: Row): Row {
127
+ const sent = dataserverRow(permitted, row, config.queries?.[subscription.resourceName]?.fields);
128
+ return subscription.encode ? subscription.encode(sent) : sent;
129
+ }
130
+
122
131
  // The operation to push to one subscription, or null to skip it entirely.
123
132
  // A MODIFY that moved the row out of this subscription's CRITERIA_MATCH
124
133
  // becomes a DELETE so the client drops it rather than keeping a stale ghost
@@ -150,7 +159,10 @@ export function createMockServer(config: MockServerConfig): MockServer {
150
159
  // - A held row gets the MODIFY, or a DELETE when it is gone or out of scope.
151
160
  // - A row the client doesn't hold that is (now) in scope is pushed as an
152
161
  // INSERT and becomes held: a brand-new row, or one a MODIFY brought back
153
- // into the criteria. Out of scope, the client is told nothing.
162
+ // into the criteria. Out of scope, the client is told nothing. The
163
+ // snapshot's order doesn't matter: a new row that sorts among the rows
164
+ // still waiting is pushed now too, as GSF's client index does, and no
165
+ // later page repeats it.
154
166
  // An INSERT is pushed even when the view already holds MAX_VIEW rows; the
155
167
  // real server would also evict the oldest row in the same frame (moving
156
168
  // view), which this engine doesn't, so the view can grow past MAX_VIEW.
@@ -175,7 +187,7 @@ export function createMockServer(config: MockServerConfig): MockServer {
175
187
  : permittedQueryRow(row, query?.auth, requestingUser(connection), subscription.fieldNames);
176
188
  if (pending.has(key)) {
177
189
  if (permitted)
178
- pending.set(key, stampRow(dataserverRow(permitted, row, query?.fields), rowRef, 'INSERT'));
190
+ pending.set(key, stampRow(wireRow(subscription, permitted, row), rowRef, 'INSERT'));
179
191
  else pending.delete(key);
180
192
  return null;
181
193
  }
@@ -185,17 +197,6 @@ export function createMockServer(config: MockServerConfig): MockServer {
185
197
  return 'DELETE';
186
198
  }
187
199
  if (!permitted) return null;
188
- // Under ORDER_BY, a new row that sorts among the waiting ones waits too,
189
- // so MORE_ROWS pages stay in order.
190
- if (
191
- subscription.queueInsert?.(
192
- key,
193
- stampRow(dataserverRow(permitted, row, query?.fields), rowRef, 'INSERT'),
194
- row,
195
- )
196
- )
197
- return null;
198
-
199
200
  held.add(key);
200
201
  return 'INSERT';
201
202
  }
@@ -251,7 +252,7 @@ export function createMockServer(config: MockServerConfig): MockServer {
251
252
  subscription.fieldNames,
252
253
  );
253
254
  if (!permitted) continue;
254
- outRow = dataserverRow(permitted, row, query?.fields);
255
+ outRow = wireRow(subscription, permitted, row);
255
256
  }
256
257
  subscription.sequenceId += 1;
257
258
  // A live push carries neither MORE_ROWS nor ROWS_COUNT on the real
package/src/types.ts CHANGED
@@ -47,6 +47,13 @@ export interface FieldDef {
47
47
  default?: unknown;
48
48
  // STRING column size (`STRING(100)`): MAX_LENGTH / maxLength.
49
49
  maxSize?: number;
50
+ // A BIGDECIMAL column's scale (`BIGDECIMAL(20, 2)` in the dictionary):
51
+ // values are rounded HALF_UP to it when written and sent as strings with
52
+ // exactly that many decimals ("15.00000"). Default 5, GSF's SQL default
53
+ // (BigDecimalSqlPrecisionConfig). A view's derived BIGDECIMAL has no column
54
+ // and goes out with its own decimals unless this says otherwise. See the
55
+ // README's "Value encoding".
56
+ scale?: number;
50
57
  // `metadata { minLength = 0 }`: MIN_LENGTH / minLength.
51
58
  minLength?: number;
52
59
  // Replaces the DESCRIPTION / description the real server fills with the