@genesislcap/mock-server 15.60.0 → 15.62.0-GENC-1660.1

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
package/README.md CHANGED
@@ -280,7 +280,9 @@ row does (genesis-db `InsertOperation.kt`):
280
280
  earlier seed row; an unsafe one (above 2^53, already rounded by
281
281
  `JSON.parse`) or a repeat is re-stamped, with one warning per table, and a
282
282
  missing `TIMESTAMP` becomes the `RECORD_ID`. An insert always gets fresh ones. `updateRow` moves `TIMESTAMP` on and
283
- keeps `RECORD_ID`, even if the patch names either field.
283
+ keeps `RECORD_ID`, even if the patch names either field. A seed that brings
284
+ the real server's ids as strings above 2^53 makes later stamps continue from
285
+ the highest of them, as strings, so a new row is still the newest.
284
286
  - **Deviation:** GSF's values are about 7.5e18 (epoch millis shifted left 22
285
287
  bits, plus a counter). That is above 2^53, so `JSON.parse` rounds
286
288
  neighbouring rows onto one number. The mock's stamps are epoch millis, or the
@@ -721,8 +723,12 @@ createMockServer({ ...config, fidelity: 'legacy' });
721
723
  | `JSON_SCHEMA_REQUEST` content | Per resource type, as below | One minimal `{DETAILS: {properties: {F: {type}}, required}}` as both INBOUND and OUTBOUND; a query or request-server name resolves to its source table |
722
724
  | Declared schemas (`TableDef.fields`, query and view `fields`, `derivedTypes`) | Used for metadata and rows | Ignored: metadata is inferred, rows go out as stored |
723
725
  | Commit events | Committed only on `EVENT_ACK`; `IGNORE_WARNINGS` lets returned warnings commit ([Commit semantics](#commit-semantics-validate-warnings-and-ignore_warnings)) | A handler's writes stay whatever it replies; warnings always NACK |
726
+ | Values of declared columns | Held by type; BigDecimals sent as strings with their scale ([Value encoding](#value-encoding)) | As given |
724
727
  | A change to a table a view joins | Pushed only through joins with `backwardsJoin: true`, as the view rows it inserts, deletes or changes ([Views](#views-the-join-model)) | Every view row pushed as a MODIFY, whatever the join |
728
+ | Snapshot row order | Newest first by `RECORD_ID`; `REVERSE` alone gives oldest first; `ORDER_BY` an index ([Dataserver paging](#dataserver-paging)) | The order the table holds the rows in |
725
729
  | `DATA_LOGON` `ORDER_BY` / `REVERSE` / `FIELDS`, criteria on unexposed fields | Honoured / `LOGON_NACK` | Ignored |
730
+ | A `CRITERIA_MATCH` that isn't valid Groovy, is in this engine's `AND`/`OR`/`NOT`/`<>`/`=` dialect, calls a function GSF's validator doesn't allow or one no overload takes | `LOGON_NACK INVALID_CRITERIA` with GSF's message, no subscription ([Strict criteria](#strict-criteria)) | Malformed or unknown: fails open, every row, logged. The dialect and the refused functions: evaluated |
731
+ | The criteria functions | GSF's, typed by the declared fields ([Criteria functions](#criteria-functions)) | The same, a date compared with also read as ISO-8601 |
726
732
  | `REQ_` | As in [Request servers](#request-servers-req_): `PARAMETRIC_TYPE`, criteria-only paging and ordering, `REQUEST` wildcards, ranges and arrays, a reply block, `MSG_NACK` failures | Every matching row in insertion order; `REQUEST` from the top level or `DETAILS`, `'*'`/`null`/`''` match anything, an array value any of its values; `CRITERIA_MATCH` fails open; no `RECORD_ID`/`TIMESTAMP`; errors as a `REP_` with `ERROR` |
727
733
  | Unknown resource | `MSG_NACK` 404 (the client's promise rejects) | A typed reply with `ERROR: [{CODE: 'UNKNOWN_RESOURCE'}]` (the promise resolves) |
728
734
 
@@ -766,13 +772,15 @@ eventSchemas: { EVENT_TRADE_BOOK: { className: 'com.acme.BookInput', fields: [/*
766
772
  appName: 'showcase', // enum class names: global.genesis.gen.dao.enums.<appName>.<table>.<Field>
767
773
  ```
768
774
 
769
- - **`FieldDef`** `{name, type, nullable?, values?, default?, maxSize?, minLength?, description?, title?, optional?}`.
775
+ - **`FieldDef`** `{name, type, nullable?, values?, default?, maxSize?, minLength?, scale?, description?, title?, optional?}`.
770
776
  `type` is a dictionary type (`STRING ENUM INT SHORT LONG DOUBLE BIGDECIMAL BOOLEAN DATE
771
777
  DATETIME NANO_TIMESTAMP RAW`). Fields are nullable unless declared otherwise; key and
772
778
  generated fields default to not null. `description` replaces the JVM class name the real
773
779
  server puts there (leave it unset on dates: foundation-forms spots a date by
774
780
  `org.joda.time.DateTime`). `optional` decides whether an event payload may leave the field
775
781
  out (see events below); an ENUM with no `values` sends no `VALID_VALUES` and no `enum`.
782
+ `scale` is a BIGDECIMAL column's (default 5); values are held and sent by their declared
783
+ type, see [Value encoding](#value-encoding).
776
784
  - **Views** take their base table's fields and `select` aliases, each join's `select` columns
777
785
  typed from the joined table at any depth (nullable unless reached from the base through INNER
778
786
  joins only; see [Views: the join model](#views-the-join-model)), and `derivedTypes` for
@@ -824,6 +832,58 @@ Deviations: a bare table name with no request server of that name still gets the
824
832
  insert-shaped event schema from `JSON_SCHEMA_REQUEST` (this engine's fallback; the real server
825
833
  answers 404). Dataserver rows still carry neither RECORD_ID nor TIMESTAMP.
826
834
 
835
+ ## Value encoding
836
+
837
+ A declared column's values are held by its type, and go on the wire the way
838
+ GSF's serializer writes them (`GenesisSetMaskingJsonSerializer.kt`, GSF
839
+ v8.15.29: a `DateTime` as epoch millis, a `BigDecimal` as
840
+ `value.toPlainString()`):
841
+
842
+ | Type | Held as (seed rows, inserts, update patches) | On the wire |
843
+ |---|---|---|
844
+ | `BIGDECIMAL` | A number rounded HALF_UP to the column's `scale` (a number or a numeric string is accepted) | A string with exactly `scale` decimals: `15` → `"15.00000"`, `1234.56789012` → `"1234.56789"` (recorded) |
845
+ | `DATE` / `DATETIME` | Epoch millis (a number, a `Date`, `yyyyMMdd`, `yyyyMMdd-HH:mm[:ss[.SSS]]` or ISO-8601; ISO without an offset is UTC) | The number |
846
+ | `INT` `SHORT` `LONG` `DOUBLE` `NANO_TIMESTAMP` | A number (a numeric string is read as one) | The number |
847
+ | `BOOLEAN` | `true`/`false` (also the strings, any case) | The boolean |
848
+ | `ENUM` | As given; a value not in `values` is logged once (the real table can't hold it) | The string |
849
+
850
+ - **The scale.** A SQL-backed GSF table stores a BIGDECIMAL as
851
+ `NUMERIC(precision, scale)`, `(20, 5)` unless the dictionary says otherwise
852
+ (`BigDecimalSqlPrecisionConfig`, `SQLBaseTableGenerator.kt`), so every value
853
+ comes back with exactly that many decimals, and SendIt rounds a seed value
854
+ HALF_UP before writing it (`BigDecimalCsvTransformer.kt`). `FieldDef.scale`
855
+ says it (`field("FEE", BIGDECIMAL(20, 2))` is `{type: 'BIGDECIMAL', scale: 2}`).
856
+ A view's joined column keeps its table's scale; a derived BIGDECIMAL has no
857
+ column, so it goes out with its own decimals unless its `derivedTypes` entry
858
+ gives a `scale`.
859
+ - **Numbers inside.** Criteria, `ORDER_BY`, `REQUEST` values and
860
+ derived fields see numbers, so `COMMISSION > 20` and an index on a BIGDECIMAL
861
+ compare numerically, as GSF's typed values do. A `REQ_` wildcard matches the
862
+ scaled text (`'*.50000'`). Precision is a JavaScript number's, about 15
863
+ significant digits; a `NUMERIC(20, 5)` value beyond that loses its last digits.
864
+ - **Events.** An event's schema already demands a BIGDECIMAL as a string and a
865
+ date as an integer (a JSON number for a BIGDECIMAL is `VALIDATION_ERROR`, as on
866
+ GSF, whose validator isn't type-loose). After that check, a declared
867
+ BIGDECIMAL string reaches the handler as a number (unrounded, as GSF hands it
868
+ a deserialized `BigDecimal`); the column scale applies when it is written.
869
+ - **What is left as given.** A key column (the table's `pkField`), so lookups by
870
+ the key a seed or a handler used keep working. An integer beyond 2^53, which
871
+ no JavaScript number holds exactly. A value that isn't its type (a date that
872
+ doesn't exist, `'n/a'` in a BIGDECIMAL): nothing is guessed, `Date.parse`
873
+ included. An undeclared table's values (no types to go by), except that a
874
+ `fieldTypes` override making a column a BIGDECIMAL sends it as a string with
875
+ the default scale. A request-server `resolver`'s rows. A view's joined column
876
+ keeps its table's scale when the view's base table is declared; over an
877
+ undeclared base the view's columns are inferred like the rest.
878
+ - **Fidelity.** Under `fidelity: 'legacy'` nothing is converted. Seed rows are
879
+ converted when their table is registered and writes when they happen, so
880
+ switching `fidelity` on a running server changes how values are sent, not
881
+ how the rows already stored are held.
882
+ - **`ROW_REF`** is the record's `RECORD_ID` as a string in GSF 8.15
883
+ (`GenesisSetMaskingJsonSerializer.kt`: `value.toString()`, recorded) and in
884
+ 10.0 alike (it writes a `Long` ROW_REF as a string, v10.0.0-beta4), so there
885
+ is no option for its type (plan decision D1).
886
+
827
887
  ## Dataserver paging
828
888
 
829
889
  `DATA_LOGON` pages the way the real dataserver does (genesis-pal-dataserver's
@@ -834,6 +894,22 @@ answers 404). Dataserver rows still carry neither RECORD_ID nor TIMESTAMP.
834
894
  is clamped to `MAX_VIEW`, and `0` means "no limit" for either. These are
835
895
  also foundation-comms' `DatasourceDefaults`, so a grid that sets neither gets
836
896
  250 rows and then pages.
897
+ - **Row order.** Without `ORDER_BY` the dataserver reads its primary index,
898
+ `RECORD_ID`, **newest first**, and `REVERSE: true` alone makes it oldest
899
+ first, the order the rows were written in (genesis-pal-dataserver
900
+ `PrimaryKeyDispenser.kt`: `if (reverse) Order.ASC else Order.DESC`;
901
+ recorded in `dataserver-order-by`). `ORDER_BY` names one of the query's
902
+ `indexes` and sorts ascending by its fields, ties by `RECORD_ID`; `REVERSE`
903
+ flips that; an unknown index is `LOGON_NACK INVALID_INDEX` (checked before
904
+ the criteria, as `ConnectionManager.verifyClientOptions` does). Without
905
+ `ORDER_BY`, a `CRITERIA_MATCH` that uses the leading fields of one of the
906
+ query's `indexes` makes the dataserver read through that index instead, so
907
+ the rows come in its order (the index matching the most leading fields, the
908
+ first on a tie: `CriteriaFilterBuilder.findBestMatchingIndex`,
909
+ `QueryClientFactory.build`). `REVERSE` is read as GSF reads a flag: `true`, or
910
+ any text `"true"` in any case. Every page follows the same order. A seed brought with the real server's `RECORD_ID`s
911
+ (as strings, above 2^53) sorts by their integer value. `DETAILS.FIELDS`
912
+ narrows the rows to those columns, dropping names the query doesn't expose.
837
913
  - The snapshot is the first page: at most `MAX_ROWS` rows, and `MORE_ROWS: true`
838
914
  when more are waiting. **`ROWS_COUNT`** is on the first page only, and counts
839
915
  like the real dataserver's index count: it **ignores `CRITERIA_MATCH` and the
@@ -887,6 +963,12 @@ cut off by `MAX_VIEW`, and the view rows a change to a joined table touches —
887
963
  get no MODIFYs or DELETEs at all. A waiting row's page later
888
964
  delivers its current values. Nothing absorbed uses a `SEQUENCE_ID`.
889
965
 
966
+ A new row is pushed as soon as it arrives, whatever the order — also when it
967
+ sorts among the rows still waiting (under `ORDER_BY`, or oldest first) — and
968
+ no later page sends it again: the real `ClientIndex.processInsertUpdate` sends
969
+ it straight away while the view has room (always, with the default moving
970
+ view), and `NextRowsStrategy` skips rows the client already holds.
971
+
890
972
  An `INSERT` is pushed even when the view already holds `MAX_VIEW` rows. The
891
973
  real server would also evict the oldest row in the same frame (the moving
892
974
  view, a later item), so here the view can grow past `MAX_VIEW`; the next
@@ -922,7 +1004,7 @@ requestReplies: {
922
1004
  | | Criteria-only (`criteriaOnly: true`) | Plain |
923
1005
  |---|---|---|
924
1006
  | `REQUEST` | Must be a top-level object (missing, `null`, an array or inside `DETAILS`: `MSG_NACK INTERNAL_ERROR`, as recorded); its fields are ignored | Top level only (`DETAILS.REQUEST` is never read); missing means every row. Each field selects rows: an exact value (converted to the field's type; `null` matches nulls, and nothing on a non-null field), a string with `*` (a wildcard, below), or `FIELD_FROM` + `FIELD_TO` (an inclusive range; one alone matches nothing). A field the source doesn't have matches nothing unless its value is `null`. An array runs each object and concatenates the results (`[]`: no rows) |
925
- | `CRITERIA_MATCH` | Filters the rows. Malformed: `MSG_NACK INTERNAL_ERROR` (recorded). An unknown field: `REPLY: []` (recorded), and `REQUEST_FAILED` "Paging not supported on lazy criteria evaluation" with an `OFFSET` past 0 | Filters the rows. Malformed: `MSG_NACK REQUEST_FAILED` with the parse error. An unknown field matches nothing |
1007
+ | `CRITERIA_MATCH` | Filters the rows. Malformed — refused by the Groovy check, or unparseable and not known to be valid Groovy ([Strict criteria](#strict-criteria)): `MSG_NACK INTERNAL_ERROR` (recorded). An unknown field: `REPLY: []` (recorded), and `REQUEST_FAILED` "Paging not supported on lazy criteria evaluation" with an `OFFSET` past 0 | Filters the rows. Malformed (as for criteria-only): `MSG_NACK REQUEST_FAILED` with the parse error. An unknown field matches nothing |
926
1008
  | `MAX_ROWS` | The page size: `DETAILS.MAX_ROWS`, else `rowLimit`, else 10000. Below 1: `REQUEST_FAILED` "maxRows should be greater than 0" | The most rows each `REQUEST` object returns, same defaults. No signal that more exist |
927
1009
  | `OFFSET` / `VIEW_NUMBER` | `OFFSET` skips that many rows. `VIEW_NUMBER` n (from 1) is the page at `MAX_ROWS × (n − 1)`, with `NEXT_VIEW: n + 1` while the page has rows. Both, or either negative: `REQUEST_FAILED` with GSF's text | Ignored |
928
1010
  | `ORDER_BY` | `'FIELD [ASC\|DESC], …'` (grid-pro sends `'NAME DESC'`, `'RECORD_ID ASC'`); an index name or `PK` stands for its fields; ties go by `RECORD_ID` ascending, nulls sort low. Malformed, or a field the source doesn't have: `REQUEST_FAILED` with GSF's text (`Order fields [Asc(field=X)] not found on table T`). Without it, a first page comes in the table's order (oldest first, recorded) and a later page by primary key | Ignored: rows come in primary-key order (recorded: `RIGHT` in `CODE` order) |
@@ -951,8 +1033,11 @@ requestReplies: {
951
1033
  which foundation-comms rejects the request's promise with. A `resolver`'s
952
1034
  `NackError` keeps its items; anything else it throws is `GENERIC_ERROR`
953
1035
  with the message (`AbstractCustomReqRep.kt`).
954
- - A `CRITERIA_MATCH` calling a function this engine's grammar doesn't model
955
- (it may be valid Groovy) still fails open, logged, as on the dataserver.
1036
+ - A `CRITERIA_MATCH` this engine's grammar doesn't model but that GSF's
1037
+ validator passes (`-QTY < -5`, `QTY == 5L`) still fails open, logged, as on
1038
+ the dataserver. What the validator refuses (`NAME in ['A']`, `QTY * 2 > 1`,
1039
+ a function off its whitelist), or a call no overload takes, is refused as a
1040
+ malformed criteria ([Strict criteria](#strict-criteria)).
956
1041
 
957
1042
  What clients see:
958
1043
 
@@ -1120,41 +1205,187 @@ extracted from for how each was actually found:
1120
1205
  (e.g. a NOTIONAL that happens to be round in the seed data), say so via
1121
1206
  the `fieldTypes` override on the query/request-reply.
1122
1207
  6. **`CRITERIA_MATCH` needs more than `==`.** `compileCriteria()`
1123
- (`src/db/criteria.ts`) supports `== != > >= < <= AND OR NOT` (also
1124
- `&&`/`||`/`!`) and parentheses via a small typed grammar (not
1208
+ (`src/db/criteria.ts`) supports `== != > >= < <= && || !` and
1209
+ parentheses (and, under `fidelity: 'legacy'` only, its own `AND OR NOT`,
1210
+ `<>` and `=`: see [Strict criteria](#strict-criteria)) via a small typed grammar (not
1125
1211
  `eval`/`new Function` — a malformed criteria string can only fail to
1126
- parse, never execute arbitrary JS). It understands both function-call
1127
- dialects real clients emit: the `Expr.<fn>(field, value)` form from
1128
- `foundation-criteria`'s `serialisers.ts` / `foundation-ui`'s date-range
1129
- filter, and the `<FIELD>.<fn>(value)` method-call form project filter
1130
- builders generate (e.g. `DEAL_CURRENCIES.containsIgnoreCase('USD')`) —
1131
- both share the same functions: `contains`, `containsIgnoreCase` (substring
1132
- on strings, membership on array-valued fields), the Java/Groovy string
1133
- methods `equals`, `equalsIgnoreCase`, `startsWith`, `endsWith` (pbc-notify-ui's
1134
- inbox subscribes with `((ALERT_STATUS.equals("NEW")))`, and an unsupported
1135
- method makes the whole criteria fail to compile — the filter is dropped and
1136
- the grid shows every row), `dateIsToday`,
1137
- `dateIsEqual`, `dateIsGreaterEqual`, `dateIsLessEqual`, `dateTimeIsEqual`,
1138
- `dateTimeIsGreaterEqual`, `dateTimeIsLessEqual`, `dateTimeIsAfter`,
1139
- `dateTimeIsBefore` (`dateIs*` compares by calendar day, `dateTimeIs*` by
1140
- full timestamp). Date literals accept `'YYYYMMDD-HH:MM[:SS[.mmm]]'` — the
1141
- milliseconds matter: `getGroovyDateFormat()` emits them, and a parser that
1142
- rejects them makes every `dateTimeIs*` comparison silently false, so a
1143
- grid whose default filter is "Today" renders as an empty screen with no
1144
- warning. Any criteria this grammar still doesn't cover fails open instead
1145
- of crashing the handler: `compileCriteria()` logs a warning and returns a
1146
- match-all predicate, so `DATA_LOGON` still responds (unfiltered) rather
1147
- than leaving the client's grid stuck on "Loading...". (A request server
1148
- answers a malformed criteria with a `MSG_NACK`, as GSF does, and fails open
1149
- only on a function it doesn't model; see
1150
- [Request servers](#request-servers-req_).) Add new entries to
1151
- `EXPR_FUNCTIONS` in `criteria.ts` if you hit one that isn't covered yet.
1212
+ parse, never execute arbitrary JS). It evaluates the calls GSF allows, as
1213
+ GSF computes them (see [Criteria functions](#criteria-functions)): every
1214
+ function on GSF's `Expr` whitelist, and the field methods `startsWith`,
1215
+ `equals`, `equalsIgnoreCase`, `contains`, `containsIgnoreCase`,
1216
+ `containsWordsStartingWithIgnoreCase` and `dateIsToday`
1217
+ (pbc-notify-ui's inbox subscribes with `((ALERT_STATUS.equals("NEW")))`).
1218
+ A criteria GSF refuses — not valid Groovy, a function off its whitelist, a
1219
+ call no overload takes — is refused here too: `DATA_LOGON` gets
1220
+ `LOGON_NACK INVALID_CRITERIA` and a request server a `MSG_NACK` (see
1221
+ [Strict criteria](#strict-criteria)). Any other criteria this grammar
1222
+ doesn't cover — it may be valid Groovy — fails open instead of crashing
1223
+ the handler: `compileCriteria()` logs a warning and returns a match-all
1224
+ predicate, so `DATA_LOGON` still responds (unfiltered) rather than leaving
1225
+ the client's grid stuck on "Loading...". The exported `compileCriteria()`
1226
+ and `filterByCriteria()` evaluate as GSF does by default;
1227
+ `{ types }` (a `ReadonlyMap` of field → Genesis type) types the fields as
1228
+ GSF's entity does, and `{ lenient: true }` evaluates as this engine did up
1229
+ to 15.47 (`fidelity: 'legacy'` uses it — see
1230
+ [Strict criteria](#strict-criteria)).
1152
1231
  7. **Composite-key AMEND/DELETE matching needs a fallback beyond the PK.**
1153
1232
  `Store.findRow()` (`src/db/store.ts`) tries the configured PK field, then
1154
1233
  any single field that uniquely identifies one row, then the full
1155
1234
  combination of provided fields — needed for tables where no single field
1156
1235
  is unique on its own.
1157
1236
 
1237
+ ## Strict criteria
1238
+
1239
+ GSF parses every `CRITERIA_MATCH` as Groovy before it opens a subscription
1240
+ (`CriteriaFilterBuilder.validateAndParseCriteriaOptions`:
1241
+ `AstBuilder().buildFromString(criteria)`, GSF v8.15.29, Groovy 3.0.25). One
1242
+ that doesn't parse gets `LOGON_NACK` alone, no `LOGON_ACK` and no subscription,
1243
+ so a later `DATA_LOGOFF` is the router's 404 (recorded:
1244
+ `dataserver-invalid-criteria`):
1245
+
1246
+ ```jsonc
1247
+ { "MESSAGE_TYPE": "LOGON_NACK", "WARNING": [], "ERROR": [{
1248
+ "@type": "StandardError", "CODE": "INVALID_CRITERIA", "STATUS_CODE": "400 Bad Request",
1249
+ "TEXT": "MultipleCompilationErrorsException : startup failed:\nScript1791….groovy: 1: Unexpected input: '<EOF>' @ line 1, column 19.\n NAME == ((( 'oops'\n ^\n\n1 error\n" }] }
1250
+ ```
1251
+
1252
+ **The engine's own dialect is refused too.** This grammar also takes `AND`,
1253
+ `OR` and `NOT` (any case), `<>` and a single `=`, which Groovy doesn't, and it
1254
+ reads a dotted name as a field and a `$` in a double-quoted string as text.
1255
+ (foundation-criteria puts every string value in double quotes, so a filter
1256
+ value with a `$` in it, such as `US$`, is refused by GSF, and now here.)
1257
+ GSF refuses all of them, at one of three stages
1258
+ (`CriteriaFilterBuilder.getCriteriaFilter`): Groovy's lexer or parser
1259
+ (`A == 1 AND B == 2`, `A <> 1`, `"US$"`), its `CriteriaValidatorVisitor` once
1260
+ Groovy has read them some other way (`NOT x` is a call on an implicit `this`,
1261
+ `A = 1` an assignment, `A.B` a property, `"a$b"` a GString), or the
1262
+ `@CompileStatic` compilation of the filter (`Expr.f(x) AND y` calls `AND` on a
1263
+ Boolean). `src/db/criteriaDialect.ts` gives GSF's text for the shapes clients
1264
+ write, with every other validator error in the order GSF lists them (an
1265
+ unknown field too, when the query's fields are known). It was run side by
1266
+ side with Groovy 3.0.25 and genesis-db 8.15.29's validator on about 1,300
1267
+ probe criteria: every text GSF's parser or validator gives matches. A
1268
+ compilation failure's text is the criteria as GSF compiled it, each field
1269
+ replaced by its getter:
1270
+
1271
+ ```text
1272
+ Error compiling criteria expression: Failed to compile criteria expression Expr.containsIgnoreCase(row.getName(), 'a') AND row.getQty() > 1
1273
+ ```
1274
+
1275
+ **So is a call GSF's validator doesn't allow.** Only the `Expr` functions on
1276
+ its whitelist (see [Criteria functions](#criteria-functions)) and seven field
1277
+ methods pass; anything else — `Expr.foo(X)`, `Expr.stringDateTime`, a call on
1278
+ a class (`Math.abs(QTY)`) or on an implicit `this` (`foo(X)`) — is
1279
+ `LOGON_NACK INVALID_CRITERIA` with the validator's text, every error in
1280
+ source order:
1281
+
1282
+ ```text
1283
+ GenericCriteriaValidationException : Criteria validation failed: Method call Expr.foo is not supported,Variable FOO is not allowed in criteria expression
1284
+ ```
1285
+
1286
+ That includes the functions this engine evaluated before GSF's semantics —
1287
+ `Expr.contains`, `Expr.equals`, `Expr.equalsIgnoreCase`, `Expr.startsWith`,
1288
+ `Expr.endsWith`, `Expr.dateTimeIsEqual`, `FIELD.endsWith`, and an `Expr`
1289
+ function called as a field method (`TRADE_DATE.dateIsEqual(...)`).
1290
+ foundation-criteria's `Serialisers.contains` and `Serialisers.endsWith` emit
1291
+ two of them. **And a call no overload takes**, which GSF's `@CompileStatic`
1292
+ compilation refuses: the wrong number of arguments
1293
+ (`Expr.containsIgnoreCase(NAME)`), an ambiguous `null`
1294
+ (`Expr.dateIsToday(null)`), and — where the fields' types are declared — an
1295
+ argument of the wrong type (`Expr.containsIgnoreCase(QTY, '1')` on a `LONG`),
1296
+ a field method the field's type lacks (`QTY.startsWith('1')`), or a
1297
+ relational operator between types that don't compare (`QTY > '5'`,
1298
+ `TRADE_DATE > 1.5`).
1299
+ Names and strings that merely contain a keyword (`NOTES`, `ORDER_ID`,
1300
+ `'x AND y'`) are not the dialect, and neither is a keyword used where a value
1301
+ goes (`AND == 1`: GSF's unknown-field check answers that). Request servers
1302
+ refuse the dialect as any malformed criteria: `MSG_NACK INTERNAL_ERROR`, or
1303
+ `REQUEST_FAILED` with GSF's text on a plain server.
1304
+
1305
+ **And Groovy the grammar doesn't read, as GSF's validator answers it.**
1306
+ `src/db/groovyValidator.ts` parses such a criteria as Groovy does (Groovy
1307
+ 3.0.25's precedence) and repeats the validator's walk over it: a list
1308
+ (`NAME in ['A']`: "List expressions are not supported in criteria"),
1309
+ arithmetic (`QTY * PRICE > 10`: "Binary operator * not allowed in criteria
1310
+ expression"), a ternary, an Elvis, a range, a map, a cast, a closure, `=~`,
1311
+ `===`, `instanceof`, a subscript, `++`, a property, every error in the order
1312
+ the validator meets it. A constructor call other than `BigDecimal`,
1313
+ `Integer`, `Long`, `Double` and `String` is refused ("Constructor calls for
1314
+ java.util.Date are not allowed in criteria expressions"), one of a class
1315
+ Groovy can't resolve fails to compile ("unable to resolve class Foo"); those
1316
+ five are evaluated (`QTY > new BigDecimal('1.5')`). Checked against Groovy
1317
+ 3.0.25 and genesis-db 8.15.29's validator (`test/groovyValidator.test.ts`).
1318
+
1319
+ This is the default. This engine's criteria grammar is still smaller than
1320
+ Groovy's, so it can't refuse whatever it fails to parse: `-QTY < -5` or
1321
+ `QTY == 5L` are valid Groovy the validator passes and it doesn't evaluate.
1322
+ `src/db/groovySyntax.ts` refuses only the mistakes it can name the way Groovy
1323
+ does: an operator with nothing after it (`QTY > 1 &&`), a parenthesis left open
1324
+ or closing nothing, an operator where an operand must start (`== 'x'`,
1325
+ `A && || B`), an unterminated string (an unescaped `'` in `'O'Brien'`), and a
1326
+ `#`. The text, line and column are Groovy's for those cases (checked by running
1327
+ Groovy 3.0.25 on them; in a few ANTLR names a different token at the same
1328
+ column). Anything else it doesn't understand gets no verdict, and the criteria
1329
+ fails open as before: also a `/` anywhere (a slashy string such as
1330
+ `/O'Brien/`, or a comment), and nesting too deep to check. Request servers
1331
+ refuse what this check refuses, and what their grammar can't parse unless the
1332
+ check found it valid Groovy (`-QTY < -5`), which fails open there too
1333
+ (see [Request servers](#request-servers-req_)).
1334
+
1335
+ Under `fidelity: 'legacy'` criteria are evaluated as up to 15.47
1336
+ (`src/db/legacyCriteria.ts`; `compileCriteria(criteria, { lenient: true })`):
1337
+ a malformed one, a bare field or an unknown function fails open; the
1338
+ dialect is evaluated (`AND` as `&&`, `<>` as `!=`, `=` as `==`); comparisons
1339
+ are JavaScript's (`QTY > -1` holds for a null `QTY`, `QTY == null` misses a
1340
+ row without the field); a function answers `false` rather than throw, so
1341
+ `!STATUS.equals('NEW')` keeps a row with no `STATUS`; `dateTimeIsAfter` and
1342
+ `dateTimeIsBefore` are inclusive; dates are also read by `Date.parse`. The
1343
+ GSF functions 15.47 didn't have (`containsWordsStartingWithIgnoreCase`,
1344
+ `isNullOrBlank`, `notNullOrBlank`, `dateIsAfter`, `dateIsBefore`,
1345
+ `dateTimeIsInRange`) are evaluated in that style instead of failing open.
1346
+ There is no separate option: a `CRITERIA_MATCH` the real server can't compile
1347
+ is a client bug the mock shouldn't hide.
1348
+
1349
+ ## Criteria functions
1350
+
1351
+ What each call computes is GSF's (`Expr.java`, `TimeUtils.java`,
1352
+ `ExtraOperators.kt`, `TimeRange.kt`, genesis-commons v8.15.29;
1353
+ `src/db/gsfExpr.ts`), with every overload: GSF compiles a criteria against
1354
+ the entity's Java types, so a `STRING` or `ENUM` field is a `String`, `DATE`
1355
+ and `DATETIME` a Joda `DateTime`, `LONG` a `Long`, `INT` an `Integer` (which
1356
+ GSF widens where a function takes a `Long`). The
1357
+ evaluator does the same with the declared types; a field without one is read
1358
+ by its value (a number as a `Long` of epoch millis, a string as a `String`,
1359
+ also read as ISO-8601 when it isn't one of GSF's formats, as mock rows often
1360
+ hold). GSF runs in UTC, and so do these.
1361
+
1362
+ | Function | What it does |
1363
+ |---|---|
1364
+ | `Expr.containsIgnoreCase(text, search)`, `FIELD.containsIgnoreCase(search)` | `text` contains `search`, ignoring case. `null` either side: `false`; `''`: `true`. A mock row's list value (`['USD', 'GBP']`): membership |
1365
+ | `Expr.containsWordsStartingWithIgnoreCase(text, search[, delimiter])`, and as a field method | A word of `text` — split on the delimiter's first character, `' '` by default — starts with `search`, ignoring case (grid-pro's "Word starts with": `'Tra'` finds `Global Trade Ltd.`, not `Market Solutions`). `null`: `false` |
1366
+ | `Expr.isNullOrBlank(text)`, `Expr.notNullOrBlank(text)` | `null`, or only whitespace (`String.isBlank`) |
1367
+ | `Expr.dateIsToday(date)`, `FIELD.dateIsToday()` | The date's UTC day is today |
1368
+ | `Expr.dateIsEqual / dateIsBefore / dateIsAfter / dateIsGreaterEqual / dateIsLessEqual(date, comparableDate)` | By day. A `DateTime` or `Long` date is compared with a `yyyyMMdd` date only; a `String` date and its comparable may use any of GSF's formats (below) |
1369
+ | `Expr.dateTimeIsAfter / dateTimeIsGreaterEqual / dateTimeIsBefore / dateTimeIsLessEqual(date, comparableDate)` | By instant. `After` and `Before` are strict: foundation-ui's date-range filter (`dateTimeIsAfter(F, min) && dateTimeIsBefore(F, max)`) leaves out both bounds |
1370
+ | `Expr.dateTimeIsInRange(date, timeRange, timeContext)` | The date is in the `HOUR`, `DAY`, `WEEK` (from Monday), `WEEK_DAY` (a weekend's current one is empty, its previous one Friday), `MONTH` or `YEAR` that is `CURRENT` or `PREVIOUS`. The names are case sensitive |
1371
+ | `Expr.longLocalDate(millis)`, `Expr.stringLocalDate(date)`, `Expr.longDateTime(millis)`, `Expr.stringDateTime(date)` | A day or an instant, to compare (`Expr.longLocalDate(D) == Expr.stringLocalDate('20240101')`). `stringDateTime` exists, but GSF's validator refuses it |
1372
+ | `FIELD.startsWith(prefix[, offset])`, `FIELD.equals(value)`, `FIELD.equalsIgnoreCase(text)`, `FIELD.contains(text)` | Java's. They dereference the field: a `null` one throws. `equals` wants the same class: a `LONG` field never `equals(5)` (an `Integer`) |
1373
+
1374
+ GSF's date formats (`TimeUtils.ALLOWED_DATE_PARSER`) are `yyyyMMdd-HH:mm:ss.SSS`,
1375
+ `yyyyMMdd-HH:mm:ss`, `yyyyMMdd-HH:mm`, `yyyyMMdd` and `yyyy-MM-dd`. Joda reads
1376
+ fewer digits than a pattern shows (`2024-1-1`, `20240101-9:5`) and up to three
1377
+ of a fraction (`.5` is 500 ms); nothing else parses — no `T`, no offset, no
1378
+ spaces. A `DateTime` field also compares with a date string, epoch millis or
1379
+ another `DateTime` (`TRADE_DATE >= '20240101' && TRADE_DATE < '20240102'`,
1380
+ ExtraOperators.kt); `==` takes one naming its instant.
1381
+
1382
+ A call that throws in GSF — an unparseable date, a `null` a function
1383
+ dereferences, an unknown `TimeRange` — makes the row not match, however the
1384
+ criteria goes on: `!Expr.dateIsEqual(TRADE_DATE, '2024-01-01')` matches no
1385
+ row. The evaluator's verdicts were checked against GSF's own: about 570
1386
+ criteria compiled and run by Groovy 3.0.25 with genesis-commons 8.15.29 and
1387
+ Joda-Time 2.14.0 over five rows (`test/gsfCriteriaCases.ts`).
1388
+
1158
1389
  ## Live criteria membership, and known deviations
1159
1390
 
1160
1391
  A live **MODIFY** that moves a row out of a subscription's `CRITERIA_MATCH`, or
@@ -1178,13 +1409,29 @@ Where this engine still differs from the real server:
1178
1409
  nothing then (see [Views](#views-the-join-model)).
1179
1410
  - There is no **moving view**. Once the view holds `MAX_VIEW` rows, live
1180
1411
  INSERTs are still pushed instead of also evicting the oldest row.
1181
- - Without `ORDER_BY`, snapshots are in **insertion order** (with or without
1182
- `REVERSE`), where the real default is newest first by `RECORD_ID`. `ORDER_BY` names one of
1183
- the query's `indexes` and sorts ascending by its fields, ties by `RECORD_ID`; `REVERSE`
1184
-
1185
- flips that; an unknown index is `LOGON_NACK INVALID_INDEX`. Under `ORDER_BY`, a live INSERT
1186
- that sorts among the rows still waiting for `MORE_ROWS` waits with them, in order (a MODIFY
1187
- of a waiting row keeps its place). `DETAILS.FIELDS` narrows the rows to those columns.
1412
+ - **`ROWS_COUNT` under a criteria-chosen index.** When a criteria makes the
1413
+ dataserver read through an index (see [Dataserver paging](#dataserver-paging)),
1414
+ GSF narrows the read to the range the criteria allows on that index
1415
+ (`IndexBuilderVisitor`), and `ROWS_COUNT` counts that range only. The mock
1416
+ counts the whole query, as it does for any other criteria.
1417
+ - A MODIFY of a row still **waiting for `MORE_ROWS`** updates the waiting copy
1418
+ and pushes nothing. GSF's client index doesn't know the row, so it pushes the
1419
+ change as an `INSERT` with the full row (while the view has room, or always
1420
+ with the moving view) and the later page skips it
1421
+ (`ClientIndex.processModifyUpdate` → `putNewRowEntry`, v8.15.29).
1422
+ - **Criteria types** come from declared fields (and `fieldTypes` /
1423
+ `derivedTypes`) only. A field without one is read by its value, and a call
1424
+ or comparison it takes part in isn't type-checked: GSF would refuse
1425
+ `Expr.containsIgnoreCase(QTY, '1')` on any `LONG` column; an inferred
1426
+ `QTY` is just never a String, so no row matches. An `ENUM`'s getter in a
1427
+ compilation error's text needs the declared type too.
1428
+ - `FIELD.equals(1.50)` on a `BIGDECIMAL` compares the values; GSF's
1429
+ `BigDecimal.equals` also compares the scale, which depends on its database.
1430
+ - GSF's `Expr.java` (v8.15.29) caches `singlePatternLocalDate` and
1431
+ `stringLocalDate` in one `STRING_LOCAL_DATE_CACHE`, so for up to 60 s after
1432
+ `stringLocalDate('2024-01-01')` ran, a DateTime's `dateIsEqual(F,
1433
+ '2024-01-01')` gets the cached day instead of throwing. Not modelled: each
1434
+ call parses as its own overload does.
1188
1435
  - A `CRITERIA_MATCH` naming a field the query doesn't expose is `LOGON_NACK
1189
1436
  INVALID_CRITERIA`, as on GSF — when the query's fields are known (declared, or a `fields`
1190
1437
  block). An inferred query can't tell, and evaluates the criteria on the stored row.
@@ -1,18 +1,37 @@
1
1
  import type { Row } from '../types.ts';
2
2
  export type Predicate = (record: Row) => boolean;
3
- export declare class CriteriaSyntaxError extends Error {
4
- name: string;
3
+ export { CriteriaSyntaxError, type CriteriaToken, parseGenesisDateTime, tokenizeCriteria, UnsupportedCriteriaError, } from './criteriaTokens.ts';
4
+ /**
5
+ * How {@link compileCriteria} and {@link filterByCriteria} read a criteria.
6
+ * Without options they evaluate it as GSF 8.15 does, each field read by its
7
+ * value.
8
+ * @public
9
+ */
10
+ export interface CriteriaOptions {
11
+ /**
12
+ * Each field's Genesis type (`STRING`, `DATETIME`, ...), where it is
13
+ * declared: GSF compiles a criteria against these, which picks each
14
+ * function's overload (a `DATETIME` is a Joda DateTime, a `STRING` a
15
+ * String).
16
+ */
17
+ types?: ReadonlyMap<string, string>;
18
+ /**
19
+ * `fidelity: 'legacy'`: evaluate the criteria as this engine did up to
20
+ * 15.47 — its comparisons, its function semantics (inclusive
21
+ * `dateTimeIsAfter`, `false` instead of an exception, dates read by
22
+ * `Date.parse` too), its `AND`/`OR`/`NOT` dialect — plus the GSF functions
23
+ * it didn't have. `types` is ignored.
24
+ */
25
+ lenient?: boolean;
5
26
  }
6
- export declare class UnsupportedCriteriaError extends Error {
7
- name: string;
8
- }
9
- export declare function parseGenesisDateTime(value: unknown): number | null;
10
27
  export interface ParsedCriteria {
11
28
  predicate: Predicate;
12
29
  fields: string[];
13
30
  }
14
- export declare function parseCriteria(criteria?: string | null): ParsedCriteria;
31
+ export declare function parseCriteria(criteria?: string | null, options?: CriteriaOptions): ParsedCriteria;
15
32
  /** Compiles a CRITERIA_MATCH string into a predicate function(record) => boolean. */
16
- export declare function compileCriteria(criteria?: string | null): Predicate;
33
+ export declare function compileCriteria(criteria?: string | null, options?: CriteriaOptions): Predicate;
17
34
  export declare function criteriaFields(criteria?: string | null): string[] | undefined;
18
- export declare function filterByCriteria(rows: Row[], criteria?: string | null): Row[];
35
+ export declare function filterByCriteria(rows: Row[], criteria?: string | null, options?: CriteriaOptions): Row[];
36
+ export declare function compileFailureText(criteria: string, fields: Iterable<string>, types?: ReadonlyMap<string, string>): string;
37
+ export declare function criteriaCompileRefusal(criteria: string, types?: ReadonlyMap<string, string>): string | undefined;