@genesislcap/mock-server 15.59.0 → 15.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +287 -40
- package/dist/db/criteria.d.ts +28 -9
- package/dist/db/criteria.js +856 -258
- package/dist/db/criteriaDialect.d.ts +6 -0
- package/dist/db/criteriaDialect.js +441 -0
- package/dist/db/criteriaTokens.d.ts +12 -0
- package/dist/db/criteriaTokens.js +53 -0
- package/dist/db/groovySyntax.d.ts +26 -0
- package/dist/db/groovySyntax.js +481 -0
- package/dist/db/groovyValidator.d.ts +11 -0
- package/dist/db/groovyValidator.js +523 -0
- package/dist/db/gsfExpr.d.ts +21 -0
- package/dist/db/gsfExpr.js +333 -0
- package/dist/db/identity.d.ts +2 -1
- package/dist/db/identity.js +24 -3
- package/dist/db/legacyCriteria.d.ts +7 -0
- package/dist/db/legacyCriteria.js +280 -0
- package/dist/db/ordering.d.ts +2 -0
- package/dist/db/ordering.js +24 -3
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +34 -3
- package/dist/db/store.d.ts +7 -0
- package/dist/db/store.js +55 -7
- package/dist/db/values.d.ts +19 -0
- package/dist/db/values.js +243 -0
- package/dist/handlers/commitEvent.js +3 -2
- package/dist/handlers/dataLogon.d.ts +2 -1
- package/dist/handlers/dataLogon.js +108 -51
- package/dist/handlers/eventValidation.d.ts +2 -1
- package/dist/handlers/eventValidation.js +33 -6
- package/dist/handlers/requestReply.js +43 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/protocol/connection.d.ts +1 -1
- package/dist/server.js +15 -8
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/db/criteria.ts +979 -290
- package/src/db/criteriaDialect.ts +487 -0
- package/src/db/criteriaTokens.ts +72 -0
- package/src/db/groovySyntax.ts +500 -0
- package/src/db/groovyValidator.ts +546 -0
- package/src/db/gsfExpr.ts +392 -0
- package/src/db/identity.ts +22 -4
- package/src/db/legacyCriteria.ts +301 -0
- package/src/db/ordering.ts +27 -3
- package/src/db/schema.ts +43 -8
- package/src/db/store.ts +65 -7
- package/src/db/values.ts +247 -0
- package/src/handlers/commitEvent.ts +3 -2
- package/src/handlers/dataLogon.ts +134 -56
- package/src/handlers/eventValidation.ts +40 -13
- package/src/handlers/requestReply.ts +47 -14
- package/src/index.ts +6 -1
- package/src/protocol/connection.ts +3 -4
- package/src/server.ts +16 -15
- package/src/types.ts +7 -0
package/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`
|
|
955
|
-
(
|
|
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 `== != > >= < <=
|
|
1124
|
-
|
|
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
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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.
|
package/dist/db/criteria.d.ts
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
import type { Row } from '../types.ts';
|
|
2
2
|
export type Predicate = (record: Row) => boolean;
|
|
3
|
-
export
|
|
4
|
-
|
|
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;
|