@genesislcap/mock-server 15.51.1 → 15.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +408 -39
- package/dist/db/criteria.d.ts +13 -0
- package/dist/db/criteria.js +60 -21
- package/dist/db/identity.d.ts +11 -0
- package/dist/db/identity.js +46 -0
- package/dist/db/ordering.d.ts +8 -0
- package/dist/db/ordering.js +49 -0
- package/dist/db/schema.d.ts +62 -0
- package/dist/db/schema.js +490 -0
- package/dist/db/store.d.ts +20 -3
- package/dist/db/store.js +300 -50
- package/dist/handlers/commitEvent.js +47 -9
- package/dist/handlers/crudEvents.d.ts +2 -0
- package/dist/handlers/crudEvents.js +138 -0
- package/dist/handlers/dataLogon.js +105 -6
- package/dist/handlers/eventValidation.d.ts +5 -0
- package/dist/handlers/eventValidation.js +29 -0
- package/dist/handlers/jsonSchema.d.ts +4 -2
- package/dist/handlers/jsonSchema.js +222 -61
- package/dist/handlers/meta.d.ts +6 -1
- package/dist/handlers/meta.js +168 -21
- package/dist/handlers/requestReply.js +519 -30
- package/dist/handlers/resourceAuth.d.ts +1 -0
- package/dist/handlers/resourceAuth.js +14 -0
- package/dist/handlers/resources.js +3 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/protocol/connection.d.ts +2 -0
- package/dist/protocol/errors.d.ts +100 -2
- package/dist/protocol/errors.js +300 -12
- package/dist/protocol/fieldTypes.d.ts +10 -0
- package/dist/protocol/fieldTypes.js +107 -0
- package/dist/protocol/gsfSchemas.d.ts +9 -0
- package/dist/protocol/gsfSchemas.js +423 -0
- package/dist/protocol/messageTypes.d.ts +1 -0
- package/dist/protocol/messageTypes.js +1 -0
- package/dist/protocol/msgNack.d.ts +2 -0
- package/dist/protocol/msgNack.js +10 -0
- package/dist/protocol/rowUpdate.d.ts +5 -3
- package/dist/protocol/rowUpdate.js +33 -15
- package/dist/protocol/schemaValidation.d.ts +11 -0
- package/dist/protocol/schemaValidation.js +186 -0
- package/dist/server.js +95 -29
- package/dist/types.d.ts +45 -0
- package/package.json +1 -1
- package/src/db/criteria.ts +72 -21
- package/src/db/identity.ts +53 -0
- package/src/db/ordering.ts +56 -0
- package/src/db/schema.ts +672 -0
- package/src/db/store.ts +343 -50
- package/src/handlers/commitEvent.ts +54 -10
- package/src/handlers/crudEvents.ts +181 -0
- package/src/handlers/dataLogon.ts +127 -7
- package/src/handlers/eventValidation.ts +43 -0
- package/src/handlers/jsonSchema.ts +302 -69
- package/src/handlers/meta.ts +227 -23
- package/src/handlers/requestReply.ts +626 -33
- package/src/handlers/resourceAuth.ts +18 -0
- package/src/handlers/resources.ts +3 -0
- package/src/index.ts +17 -1
- package/src/protocol/connection.ts +10 -2
- package/src/protocol/errors.ts +361 -13
- package/src/protocol/fieldTypes.ts +115 -0
- package/src/protocol/gsfSchemas.ts +505 -0
- package/src/protocol/messageTypes.ts +1 -0
- package/src/protocol/msgNack.ts +12 -0
- package/src/protocol/rowUpdate.ts +40 -20
- package/src/protocol/schemaValidation.ts +208 -0
- package/src/server.ts +106 -28
- package/src/types.ts +187 -5
package/README.md
CHANGED
|
@@ -42,7 +42,9 @@ import { createMockServer, NackError, type MockServerConfig } from '@genesislcap
|
|
|
42
42
|
|
|
43
43
|
const config: MockServerConfig = {
|
|
44
44
|
tables: {
|
|
45
|
-
|
|
45
|
+
// events: generic EVENT_TRADE_MODIFY / _DELETE handlers
|
|
46
|
+
// (see "Generic CRUD events"); EVENT_TRADE_INSERT is written out below.
|
|
47
|
+
TRADE: { pkField: 'TRADE_ID', sequencePrefix: 'TR', rows: [...], events: ['MODIFY', 'DELETE'] },
|
|
46
48
|
},
|
|
47
49
|
views: {
|
|
48
50
|
TRADE_VIEW: {
|
|
@@ -88,8 +90,10 @@ covers the server-side behaviors real projects lean on:
|
|
|
88
90
|
of a `source`. It receives `{ request, message, user, store }` (the keyed
|
|
89
91
|
`REQUEST` fields, the raw message, the requesting user, the live store) and
|
|
90
92
|
returns the `REPLY` rows verbatim; throw `NackError` for a
|
|
91
|
-
protocol-correct error
|
|
92
|
-
|
|
93
|
+
protocol-correct error (a `MSG_NACK`; see
|
|
94
|
+
[Request servers](#request-servers-req_)). Set `metadataRows` (a
|
|
95
|
+
representative sample reply) so `META_REQUEST` can still derive
|
|
96
|
+
`REPLY_FIELD`:
|
|
93
97
|
|
|
94
98
|
```ts
|
|
95
99
|
requestReplies: {
|
|
@@ -152,12 +156,187 @@ covers the server-side behaviors real projects lean on:
|
|
|
152
156
|
```
|
|
153
157
|
|
|
154
158
|
- **Composite primary keys** — `pkField` accepts an array
|
|
155
|
-
(`pkField: ['RFQ_ID', 'SIDE']`). Rows are
|
|
156
|
-
joined with `'|'` (so values must not contain `'|'`)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
(`pkField: ['RFQ_ID', 'SIDE']`). Rows are stored under the fields' values
|
|
160
|
+
joined with `'|'` (so values must not contain `'|'`) — no synthetic key
|
|
161
|
+
column, nothing extra in grids. Event handlers on composite tables pass
|
|
162
|
+
`store.rowKey(table, row)` to `broadcastTableChange`; sequence generation
|
|
163
|
+
still requires a single `pkField`.
|
|
164
|
+
|
|
165
|
+
## Record identity, ROW_REF and sequences
|
|
166
|
+
|
|
167
|
+
Every stored row carries `RECORD_ID` and `TIMESTAMP`, as every Genesis table
|
|
168
|
+
row does (genesis-db `InsertOperation.kt`):
|
|
169
|
+
|
|
170
|
+
- Seeding and `insertRow` stamp `RECORD_ID === TIMESTAMP` from one counter per
|
|
171
|
+
server that only goes up (`src/db/identity.ts`). A seed row's own
|
|
172
|
+
`RECORD_ID` is kept when it is a safe integer or a string not used by an
|
|
173
|
+
earlier seed row; an unsafe one (above 2^53, already rounded by
|
|
174
|
+
`JSON.parse`) or a repeat is re-stamped, with one warning per table, and a
|
|
175
|
+
missing `TIMESTAMP` becomes the `RECORD_ID`. An insert always gets fresh ones. `updateRow` moves `TIMESTAMP` on and
|
|
176
|
+
keeps `RECORD_ID`, even if the patch names either field.
|
|
177
|
+
- **Deviation:** GSF's values are about 7.5e18 (epoch millis shifted left 22
|
|
178
|
+
bits, plus a counter). That is above 2^53, so `JSON.parse` rounds
|
|
179
|
+
neighbouring rows onto one number. The mock's stamps are epoch millis, or the
|
|
180
|
+
last stamp plus one: exact, strictly increasing safe integers, in the same
|
|
181
|
+
order, that still read as a write time.
|
|
182
|
+
- **Dataserver rows** never carry `RECORD_ID`/`TIMESTAMP` (the real dataserver
|
|
183
|
+
identifies rows by `DETAILS.ROW_REF` only), unless the query's `fields` block
|
|
184
|
+
names them. Field metadata (`META_REQUEST`, `JSON_SCHEMA_REQUEST`) leaves them out.
|
|
185
|
+
- **`REP_` rows** from a `source` include them, last, unless the request
|
|
186
|
+
server's reply block (`RequestReplyDef.fields`) leaves them out — as GSF
|
|
187
|
+
sends a table entity, criteria-only or not. Auth's `RIGHT` sends none only
|
|
188
|
+
because its `reply { CODE DESCRIPTION }` block names neither (see
|
|
189
|
+
[Request servers](#request-servers-req_)). A `resolver`'s rows are sent as
|
|
190
|
+
returned.
|
|
191
|
+
- **`DETAILS.ROW_REF`** is the record's `RECORD_ID` as a string
|
|
192
|
+
(`GenesisSetMaskingJsonSerializer`: `value.toString()`); a view row's is its
|
|
193
|
+
base record's. Rows no longer carry a top-level `ROW_REF` — clients copy it
|
|
194
|
+
from `DETAILS` themselves (`row.DETAILS[rowId] ?? row[rowId]`). Lookups stay
|
|
195
|
+
by pk: handlers still pass the pk (or `store.rowKey`) to
|
|
196
|
+
`broadcastTableChange`, and a pk-only row passed to `broadcast()` is matched
|
|
197
|
+
to its record, a just-deleted one included. A view row is rebuilt from the
|
|
198
|
+
changed base row, so a view's own `pkField` doesn't matter, and a MODIFY that
|
|
199
|
+
takes the row out of the view (an anti-join) is pushed as a DELETE.
|
|
200
|
+
- **Live DELETE** is `ROW: [{DETAILS: {OPERATION: 'DELETE', ROW_REF}}]` and
|
|
201
|
+
nothing else. The store remembers every record a key lost until a broadcast
|
|
202
|
+
announces it (`broadcastTableChange`, or `broadcast()` with a DELETE), so a
|
|
203
|
+
DELETE names each of them — a client gets the ones it holds — and a record
|
|
204
|
+
replaced under the same key before one broadcast goes out as a DELETE of the
|
|
205
|
+
old `ROW_REF` followed by the new INSERT. Deletes nobody broadcasts are kept
|
|
206
|
+
for the next one (bounded: 16 per key, 10,000 keys per table).
|
|
207
|
+
- **Sequences:** `TableDef.sequenceFormat` is `'prefix'` by default
|
|
208
|
+
(`sequencePrefix` + a counter padded to `sequenceWidth`: `TR001`), so
|
|
209
|
+
existing seeds, tests and snapshots keep their ids. `'genesis'` mints
|
|
210
|
+
`IdStrategy.java`'s format — 15-digit counter, sequence id, `LO`, `1`:
|
|
211
|
+
`000000000000001TRLO1`. Seeds in the table's format advance the counter. A
|
|
212
|
+
supplied value is kept; only a missing (`undefined`/`null`) one is generated.
|
|
213
|
+
- **Generated fields:** `insertRow` also fills the `TableDef.generated` fields
|
|
214
|
+
a record leaves out: a `SEQUENCE` in the same format as the key's (its own
|
|
215
|
+
`prefix`, else the table's `sequencePrefix`; with neither it stays shape
|
|
216
|
+
only), an `AUTO_INCREMENT` from the highest seeded value plus one (1 with no
|
|
217
|
+
seeds). Supplied values are kept and move the counters on.
|
|
218
|
+
|
|
219
|
+
`fidelity: 'legacy'` restores the pk-based `ROW_REF` (the pk value, or the
|
|
220
|
+
composite key), the top-level `ROW_REF`, pk-stub DELETE rows and `REP_` rows
|
|
221
|
+
without `RECORD_ID`/`TIMESTAMP`.
|
|
222
|
+
|
|
223
|
+
## NACK vocabulary
|
|
224
|
+
|
|
225
|
+
Service NACKs (`EVENT_NACK`, and the `MSG_NACK` a request server fails with) follow GSF's `GenesisError` family
|
|
226
|
+
(`src/protocol/errors.ts`, pinned against recorded frames in
|
|
227
|
+
`test/nack.test.ts`): every `ERROR` item has `@type`, a string `CODE`, `TEXT`
|
|
228
|
+
and a `"<code> <reason>"` `STATUS_CODE`, and the envelope carries `WARNING`:
|
|
229
|
+
|
|
230
|
+
| `@type` | Extra fields | Built by |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| `StandardError` | — | `new NackError('text')` (`REQUEST_FAILED`, 400: a failed GPAL `require()`), `NackError.nack(text)` (`INTERNAL_ERROR`, 500: GPAL `nack("text")`, which GSF wraps in an Exception), `NackError.standard(code, text?)` |
|
|
233
|
+
| `FieldError` | `FIELD`, `PATH` (`null` for a handler's own check) | `NackError.field(field, text, path?)`, or any input with `FIELD` (`VALIDATION_ERROR`) |
|
|
234
|
+
| `TableStandardError` | `TABLE`, `FIELD` | `NackError.duplicateKey(table, index, value)` (`DUPLICATE_KEY`, 500, `FIELD: '<INDEX> -> <value>'`), `NackError.recordNotFound(table, index?)` (404; the index for a modify, `''` for a delete) |
|
|
235
|
+
| `LoginError` | `DETAILS: null` | `{'@type': 'LoginError', ...}` |
|
|
236
|
+
|
|
237
|
+
`ErrorCode` mirrors GSF's `ErrorCode.kt` (names, default texts, statuses;
|
|
238
|
+
`statusCodeFor(code)`); unknown codes get `500 Internal Server Error`.
|
|
239
|
+
`new NackError([...])` reports several errors at once. Anything else a handler
|
|
240
|
+
throws is `StandardError INTERNAL_ERROR` with its message. A handler can
|
|
241
|
+
return `{ warnings: [...] }` for a NACK with an empty `ERROR` and those
|
|
242
|
+
warnings, or pass `{ warnings }` to `NackError`; `IGNORE_WARNINGS` is not
|
|
243
|
+
modelled yet. `VALIDATE: true` reaches a handler as `ctx.validate` (see
|
|
244
|
+
[Generic CRUD events](#generic-crud-events-tabledefevents)). A request server
|
|
245
|
+
that fails answers a service `MSG_NACK` carrying the same items:
|
|
246
|
+
`{WARNING, ERROR, MESSAGE_TYPE: 'MSG_NACK'}` (genesis-messages `MsgNack`). The
|
|
247
|
+
router's `MSG_NACK` keeps its own shape (no `@type`,
|
|
248
|
+
an integer `CODE`, `DETAILS {ERROR, STATUS_CODE}`). `fidelity: 'legacy'` sends
|
|
249
|
+
the old `{CODE, TEXT, FIELD?}` items (`VALIDATION_ERROR` by default) and no
|
|
250
|
+
`WARNING`: warnings go in `ERROR` there.
|
|
251
|
+
|
|
252
|
+
## Generic CRUD events (`TableDef.events`)
|
|
253
|
+
|
|
254
|
+
A table that only needs plain insert, modify and delete gets them without a
|
|
255
|
+
hand-written handler:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
tables: {
|
|
259
|
+
COUNTERPARTY: {
|
|
260
|
+
pkField: 'COUNTERPARTY_ID',
|
|
261
|
+
sequencePrefix: 'CP',
|
|
262
|
+
fields: [/* FieldDef */],
|
|
263
|
+
indexes: [{ name: 'COUNTERPARTY_BY_LEI', fields: ['LEI'], unique: true }],
|
|
264
|
+
events: ['INSERT', 'MODIFY', 'DELETE'],
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Each verb registers `EVENT_<TABLE>_<VERB>` as the handler Genesis Create
|
|
270
|
+
generates for a table's `crud` event handlers (codegen-service
|
|
271
|
+
`JsonToKtsEventHandlersGenerator`): `eventHandler<Table>("<TABLE>_INSERT")`
|
|
272
|
+
and `_MODIFY` calling `entityDb.insert` / `modify`, and
|
|
273
|
+
`eventHandler<Table.ById>("<TABLE>_DELETE")` calling `entityDb.delete`, with no
|
|
274
|
+
`onValidate`. Those are what the showcase server runs — the recordings come
|
|
275
|
+
from them — and what Create's real tier deploys. The events are listed in
|
|
276
|
+
`RESOURCES_REQUEST` and described by `META_REQUEST` and `JSON_SCHEMA_REQUEST`
|
|
277
|
+
like any registered event (the table's DAO; a delete's `<Table>.ById`). Verbs a
|
|
278
|
+
table doesn't list are the router's 404.
|
|
279
|
+
|
|
280
|
+
| Event | Does | Replies |
|
|
281
|
+
|---|---|---|
|
|
282
|
+
| `INSERT` | Inserts DETAILS as the table's record: fields left out take their declared `default`, else `null`; sequence and auto-increment fields left out are generated | `EVENT_ACK` with `GENERATED: [{…}]`: every generated field, sorted by name, with its stored value — supplied or generated (`ack(listOf(mapOf("TRADE_ID" to insertedRow.record.tradeId)))`) — or `[]` for a table with no generated fields. A taken primary key or other unique index: `DUPLICATE_KEY` |
|
|
283
|
+
| `MODIFY` | Finds the record by primary key and writes DETAILS over it. DETAILS are the **whole record**: a field left out is reset to its default or `null`, as the deserialized DAO overwrites the stored row (recorded: a TRADE modify without `CREATED_AT` nulls it). Generated fields left out keep their values; `RECORD_ID` stays, `TIMESTAMP` moves on | `EVENT_ACK GENERATED: []`. No record: `RECORD_NOT_FOUND`, `FIELD` the key's index. A unique value another record holds: `DUPLICATE_KEY` |
|
|
284
|
+
| `DELETE` | Deletes by primary key | `EVENT_ACK GENERATED: []`. No record: `RECORD_NOT_FOUND`, `FIELD: ''` |
|
|
285
|
+
|
|
286
|
+
Index names follow the dictionary: a unique `indexes` entry over exactly the
|
|
287
|
+
key fields names the primary key, else it is `<TABLE>_BY_<fields>` without the
|
|
288
|
+
`<TABLE>_` prefix (`COUNTERPARTY_ID` → `COUNTERPARTY_BY_ID`).
|
|
289
|
+
`DUPLICATE_KEY`'s `FIELD` is `'<INDEX> -> <values>'`, values joined with
|
|
290
|
+
`' | '`. Every change is pushed live (`broadcastTableChange`), to queries on
|
|
291
|
+
the table and on views over it.
|
|
292
|
+
|
|
293
|
+
**`VALIDATE: true`** reaches every handler as `ctx.validate`, and nothing is
|
|
294
|
+
written. The generic events have no `onValidate`, so they ack `GENERATED: []`
|
|
295
|
+
straight away (recorded for `COUNTERPARTY_INSERT`): no duplicate check, no
|
|
296
|
+
sequence value used, no lookup for a modify or delete. A hand-written handler
|
|
297
|
+
should run its checks and return before writing when `ctx.validate` is set
|
|
298
|
+
(recorded: `TRADE_INSERT`'s `onValidate` NACK still comes back).
|
|
299
|
+
`IGNORE_WARNINGS` is not modelled.
|
|
300
|
+
|
|
301
|
+
**An `eventHandlers` entry of the same name wins**, so a table can list all
|
|
302
|
+
three verbs and still write out the one with business rules.
|
|
303
|
+
`getEventHandler(name)` returns the generic handler when there is no entry, so
|
|
304
|
+
`extendEventHandler` can wrap it.
|
|
305
|
+
|
|
306
|
+
**Not genesis-eventhandler's typed handlers.** GSF also ships
|
|
307
|
+
`GenericInsertEventHandler`, `GenericModifyEventHandler` and
|
|
308
|
+
`GenericDeleteEventHandler` (`EventHandlerBuilder.registerCUDEvents`), which
|
|
309
|
+
Create doesn't use. They differ in three places: their insert's `GENERATED`
|
|
310
|
+
lists only the values that differ from DETAILS (`[{}]` when all were sent),
|
|
311
|
+
their modify and delete look the record up under `VALIDATE`, and their modify
|
|
312
|
+
makes the key fields mandatory in metadata.
|
|
313
|
+
|
|
314
|
+
### Inbound schema check
|
|
315
|
+
|
|
316
|
+
Before any handler runs, the real server checks the whole message against the
|
|
317
|
+
event's `INBOUND` JSON schema (`GenesisMessageDecoder`), and so does this
|
|
318
|
+
engine — for the generic events and hand-written ones alike — whenever the
|
|
319
|
+
DETAILS class is declared: a table event on a table with `fields`, or an
|
|
320
|
+
`eventSchemas` entry with `fields`. An inferred schema is a guess from the
|
|
321
|
+
rows and is never checked against. Every failure is one `FieldError
|
|
322
|
+
VALIDATION_ERROR` in a single `EVENT_NACK`, worded as GSF's validator (networknt
|
|
323
|
+
2.0.1) words it:
|
|
324
|
+
|
|
325
|
+
| Failure | `TEXT` | `FIELD` | `PATH` |
|
|
326
|
+
|---|---|---|---|
|
|
327
|
+
| Missing required field | `$.DETAILS: required property 'NAME' not found` | `NAME` | `$.DETAILS` |
|
|
328
|
+
| Unknown field (`additionalProperties: false`) | `$.DETAILS: property 'X' is not defined in the schema and the schema does not allow additional properties` | `DETAILS` | `$.DETAILS` |
|
|
329
|
+
| Wrong JSON type | `$.DETAILS.ENABLED: string found, boolean expected` | `ENABLED` | `$.DETAILS.ENABLED` |
|
|
330
|
+
| ENUM value not allowed | `$.DETAILS.TIER: does not have a value in the enumeration ["STANDARD", "PREMIUM", "TRIAL"]` | `TIER` | `$.DETAILS.TIER` |
|
|
331
|
+
| Nullable field invalid | `…: must be valid to one and only one schema, but 0 are valid`, then each branch's error | the field | its path |
|
|
332
|
+
|
|
333
|
+
Lengths (`maxLength`, in code points), ranges (`minimum`/`maximum`: an INT
|
|
334
|
+
must fit 32 bits) and BigDecimal's pattern are checked too, and so is the
|
|
335
|
+
envelope (`VALIDATE` must be a boolean). The order is the validator's: each
|
|
336
|
+
field's errors in field order, then missing fields, then unknown ones.
|
|
337
|
+
`test/schemaValidation.test.ts` pins all of it to the real validator's output
|
|
338
|
+
on the recorded schemas. A modify therefore needs every required field, as on
|
|
339
|
+
the real server. `fidelity: 'legacy'` checks nothing.
|
|
161
340
|
|
|
162
341
|
## Running several servers in parallel (and one-command dev stacks)
|
|
163
342
|
|
|
@@ -252,7 +431,8 @@ having to monkey-patch the engine:
|
|
|
252
431
|
unhandled) instead of enumerating ack-only handlers. Unregistered events
|
|
253
432
|
get the router's `MSG_NACK` 404 when unset (see
|
|
254
433
|
[Protocol coverage](#protocol-coverage)). Note `RESOURCES_REQUEST` only
|
|
255
|
-
lists *named* events (`eventHandlers`
|
|
434
|
+
lists *named* events (`eventHandlers` and `eventSchemas` keys, and the
|
|
435
|
+
tables' `events`) — if the
|
|
256
436
|
client's resource gate blocks an event served only by the default handler,
|
|
257
437
|
name it in `config.eventSchemas` or `config.resources`.
|
|
258
438
|
|
|
@@ -317,11 +497,11 @@ the envelopes this engine sent up to 15.47 — see
|
|
|
317
497
|
| (live push) | `QUERY_UPDATE {ROW, SEQUENCE_ID}` | One row per push; `DETAILS.OPERATION` is `INSERT`/`MODIFY`/`DELETE`. No `MORE_ROWS`, no `ROWS_COUNT` |
|
|
318
498
|
| `MORE_ROWS` | `QUERY_UPDATE {ROW?, MORE_ROWS, SEQUENCE_ID}` on the logon's `SOURCE_REF`, plus `MORE_ROWS_ACK` | See [Dataserver paging](#dataserver-paging) |
|
|
319
499
|
| `DATA_LOGOFF` | `LOGOFF_ACK` | `MSG_NACK` 404 when the connection has no such subscription |
|
|
320
|
-
| `META_REQUEST` | `FEATURE_ACK {DETAILS}` |
|
|
321
|
-
| `JSON_SCHEMA_REQUEST` | `JSON_SCHEMA_FEATURE_ACK {INBOUND, OUTBOUND}` |
|
|
500
|
+
| `META_REQUEST` | `FEATURE_ACK {DETAILS}` | Dataservers, request servers (by name, without `REQ_`) and events — see [Declared schema](#declared-schema-meta_request-and-json_schema_request) |
|
|
501
|
+
| `JSON_SCHEMA_REQUEST` | `JSON_SCHEMA_FEATURE_ACK {INBOUND, OUTBOUND}` | Events, dataservers and request servers — see [Declared schema](#declared-schema-meta_request-and-json_schema_request) |
|
|
322
502
|
| `RESOURCES_REQUEST` | `RESOURCES_REQUEST_ACK` | |
|
|
323
|
-
| `REQ_<NAME>` | `REP_<NAME> {REPLY}` |
|
|
324
|
-
| `EVENT_<ENTITY>_<VERB>` commit events | `EVENT_ACK` / `EVENT_NACK` | Via `config.eventHandlers` or `defaultEventHandler
|
|
503
|
+
| `REQ_<NAME>` | `REP_<NAME> {REPLY, PARAMETRIC_TYPE}`, plus `MORE_ROWS` / `NEXT_OFFSET` / `NEXT_VIEW` on criteria-only servers; a failure is a service `MSG_NACK` | Plain servers select by `REQUEST` fields (exact, `*` wildcards, ranges, arrays) and keyed lookups (`'*'` = any); criteria-only ones page by `OFFSET` and sort by `ORDER_BY` — see [Request servers](#request-servers-req_) |
|
|
504
|
+
| `EVENT_<ENTITY>_<VERB>` commit events | `EVENT_ACK` / `EVENT_NACK` | Via `config.eventHandlers`, a table's generic `events`, or `defaultEventHandler`; checked against a declared schema first — see [Generic CRUD events](#generic-crud-events-tabledefevents) |
|
|
325
505
|
| Anything naming an unknown resource | `MSG_NACK` 404 | See below |
|
|
326
506
|
|
|
327
507
|
Each `SEQUENCE_ID` on a subscription is the previous one plus 1. The snapshot
|
|
@@ -377,12 +557,109 @@ createMockServer({ ...config, fidelity: 'legacy' });
|
|
|
377
557
|
| `MORE_ROWS` | The next page, plus `MORE_ROWS_ACK` | Unanswered (logged as unhandled) |
|
|
378
558
|
| `DATA_LOGOFF` | `LOGOFF_ACK`, or `MSG_NACK` 404 | Unanswered |
|
|
379
559
|
| `META_REQUEST` / `JSON_SCHEMA_REQUEST` success | `FEATURE_ACK` / `JSON_SCHEMA_FEATURE_ACK` | Echoes the request's `MESSAGE_TYPE` |
|
|
560
|
+
| `META_REQUEST` content | The GSF shapes below; event META too | `{NAME, TYPE}` fields, `KEY: []` on dataservers, `REPLY_FIELD` alone on request servers; events are unknown |
|
|
561
|
+
| `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 |
|
|
562
|
+
| Declared schemas (`TableDef.fields`, query and view `fields`, `derivedTypes`) | Used for metadata and rows | Ignored: metadata is inferred, rows go out as stored |
|
|
563
|
+
| `DATA_LOGON` `ORDER_BY` / `REVERSE` / `FIELDS`, criteria on unexposed fields | Honoured / `LOGON_NACK` | Ignored |
|
|
564
|
+
| `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` |
|
|
380
565
|
| Unknown resource | `MSG_NACK` 404 (the client's promise rejects) | A typed reply with `ERROR: [{CODE: 'UNKNOWN_RESOURCE'}]` (the promise resolves) |
|
|
381
566
|
|
|
567
|
+
`test/legacyParity.test.ts` pins `'legacy'` to replies recorded from the 15.47.0 engine
|
|
568
|
+
itself, for a config with no declarations and for the showcase config.
|
|
569
|
+
|
|
382
570
|
`'legacy'` is a migration aid, not a second supported protocol. Plan to remove
|
|
383
571
|
it from your config; it will then be dropped from the engine. It is read on
|
|
384
572
|
every message, so changing it on a running server takes effect straight away.
|
|
385
573
|
|
|
574
|
+
## Declared schema (`META_REQUEST` and `JSON_SCHEMA_REQUEST`)
|
|
575
|
+
|
|
576
|
+
Metadata comes from the tables dictionary when a table declares one, and is
|
|
577
|
+
inferred from the rows (`src/db/metadata.ts`) when it doesn't. **Declared
|
|
578
|
+
wins.** A config that declares nothing keeps working, but its types are
|
|
579
|
+
guesses (`LONG`/`DOUBLE`/`BOOLEAN`/`STRING`), it has no enum values,
|
|
580
|
+
defaults or not-null, and a table with no rows has no fields.
|
|
581
|
+
|
|
582
|
+
```ts
|
|
583
|
+
tables: {
|
|
584
|
+
TRADE: {
|
|
585
|
+
pkField: 'TRADE_ID',
|
|
586
|
+
sequencePrefix: 'TR', // the key is a SEQUENCE: NULLABLE false, OPTIONAL true
|
|
587
|
+
fields: [
|
|
588
|
+
{ name: 'TRADE_ID', type: 'STRING' },
|
|
589
|
+
{ name: 'QUANTITY', type: 'INT', nullable: false },
|
|
590
|
+
{ name: 'SIDE', type: 'ENUM', values: ['SELL', 'BUY'], default: 'BUY', nullable: false },
|
|
591
|
+
{ name: 'COMMISSION', type: 'BIGDECIMAL' },
|
|
592
|
+
{ name: 'NAME', type: 'STRING', maxSize: 100, minLength: 0, title: 'Name' },
|
|
593
|
+
],
|
|
594
|
+
generated: [{ field: 'TRADE_NO', kind: 'AUTO_INCREMENT' }], // filled in on insert
|
|
595
|
+
indexes: [{ name: 'TRADE_BY_SIDE', fields: ['SIDE'] }], // unique ones: DUPLICATE_KEY
|
|
596
|
+
events: ['INSERT', 'MODIFY', 'DELETE'], // see Generic CRUD events
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
views: { TRADE_VIEW: { base: 'TRADE', /* joins, derived */ derivedTypes: { TOTAL: 'DOUBLE' } } },
|
|
600
|
+
queries: { ALL_TRADES: { source: 'TRADE_VIEW', fields: ['TRADE_ID', 'SIDE', 'TOTAL'],
|
|
601
|
+
indexes: [{ name: 'BY_SIDE', fields: ['SIDE'] }] } },
|
|
602
|
+
requestReplies: { TRADE: { source: 'TRADE', criteriaOnly: true } },
|
|
603
|
+
eventSchemas: { EVENT_TRADE_BOOK: { className: 'com.acme.BookInput', fields: [/* FieldDef */] } },
|
|
604
|
+
appName: 'showcase', // enum class names: global.genesis.gen.dao.enums.<appName>.<table>.<Field>
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
- **`FieldDef`** `{name, type, nullable?, values?, default?, maxSize?, minLength?, description?, title?, optional?}`.
|
|
608
|
+
`type` is a dictionary type (`STRING ENUM INT SHORT LONG DOUBLE BIGDECIMAL BOOLEAN DATE
|
|
609
|
+
DATETIME NANO_TIMESTAMP RAW`). Fields are nullable unless declared otherwise; key and
|
|
610
|
+
generated fields default to not null. `description` replaces the JVM class name the real
|
|
611
|
+
server puts there (leave it unset on dates: foundation-forms spots a date by
|
|
612
|
+
`org.joda.time.DateTime`). `optional` decides whether an event payload may leave the field
|
|
613
|
+
out (see events below); an ENUM with no `values` sends no `VALID_VALUES` and no `enum`.
|
|
614
|
+
- **Views** take their base table's fields, each join's `select` columns typed from the joined
|
|
615
|
+
table (nullable), and `derivedTypes` for derived fields (inferred from rows when missing).
|
|
616
|
+
`ViewDef.fields` is the view's `fields { }` block: the view then exposes only those, in that
|
|
617
|
+
order (keep the key in it).
|
|
618
|
+
- **`QueryDef.fields`** is the query's `fields { }` block: rows **and** metadata carry only those
|
|
619
|
+
columns, in that order. A declared source's rows always carry every exposed column, `null`
|
|
620
|
+
when unset, as the real dataserver sends them. `rowFilter` still sees the whole row.
|
|
621
|
+
- `fieldTypes` (queries, request servers, `eventSchemas`) still overrides a TYPE either way.
|
|
622
|
+
|
|
623
|
+
What is sent (GSF 8.15.29; `test/recordedSchemas.test.ts` pins every shape to recorded frames,
|
|
624
|
+
ignoring only `SOURCE_REF`):
|
|
625
|
+
|
|
626
|
+
| Resource | `META_REQUEST` `DETAILS` | `JSON_SCHEMA_REQUEST` |
|
|
627
|
+
|---|---|---|
|
|
628
|
+
| Dataserver | `{TYPE: 'DATASERVER', NAME, FIELD: [{NAME, TYPE, VALID_VALUES?}], INDEXES: [{NAME, FIELDS: 'A B'}]}`. `VALID_VALUES` on ENUMs only; no indexes → `[{NAME: null, FIELDS: 'RECORD_ID'}]` | INBOUND: the `DATA_LOGON` message. OUTBOUND: the `DataLogonReply` `oneOf`, the fields in `$defs['global.genesis.message.core.dataserver.QueryRow']` (all `oneOf [null, type]`, `genesisType`) |
|
|
629
|
+
| Request server | Plain: `{REQUEST_FIELD, INDICES: [], CRITERIA_ONLY_REQUEST: false, REPLY_FIELD}`; `REQUEST_FIELD` is `requestFields`, else the key. `criteriaOnly`: `{REQUEST_FIELD: [], CRITERIA_ONLY_REQUEST: true, SORTABLE_FIELDS, CRITERIA_FIELDS, REPLY_FIELD}`, the sortable and criteria fields being the source's fields then `RECORD_ID` and `TIMESTAMP`. `REPLY_FIELD` is the reply block (`fields`), else the source's fields then `RECORD_ID: LONG` and `TIMESTAMP: NANO_TIMESTAMP`, for either kind | INBOUND: top-level `REQUEST` (object or array of the request fields) required, `DETAILS {VIEW_NUMBER, MAX_ROWS, CRITERIA_MATCH}`. OUTBOUND: `properties.REPLY.items` (as `REPLY_FIELD`; nullable fields as `oneOf [null, type]`) |
|
|
630
|
+
| Event | `{NAME, TITLE?, DESCRIPTION, FIELD, DEFINITIONS: {}, REPLY, REQUIRES_APPROVAL: false, SCHEDULABLE: false, TYPE: 'EVENT_HANDLER'}`. FIELD items: `NAME, TITLE?, DESCRIPTION (JVM class), NULLABLE, TYPE, JSON_TYPE, OPTIONAL, READ_ONLY, GENESIS_TYPE` plus `MIN/MAX`, `MIN_LENGTH/MAX_LENGTH`, `VALID_VALUES`, `DEFAULT_VALUE`, `ENUM_CLASS`, `DEFAULT` where they apply. `REPLY` describes EventAck/EventApprovableAck/EventNack, copied from the recording | INBOUND: the event envelope, `DETAILS` with `additionalProperties: false` and `required` = the fields that aren't `OPTIONAL`. Nullable fields are `oneOf [schema, null]`; dates are `integer`; BigDecimal is a `string` with a pattern and no `genesisType`. OUTBOUND: the `EventReply` `oneOf` |
|
|
631
|
+
|
|
632
|
+
Events resolve as `JSON_SCHEMA_REQUEST` always did: `config.eventSchemas`, then
|
|
633
|
+
`EVENT_<TABLE>_<VERB>` (longest table name wins) — but only for a registered event (an
|
|
634
|
+
`eventHandlers` or `eventSchemas` entry, or a table's `events`); any other `EVENT_*` name gets the router's 404, as on
|
|
635
|
+
the real server. `OPTIONAL` is true only when the class property has a default
|
|
636
|
+
(PALMetadataUtils.kt): a table's generated DAO defaults every nullable and generated column, so
|
|
637
|
+
those are optional; a custom DTO field is optional only with a `default` or `optional: true`
|
|
638
|
+
(a nullable DTO field with no default must still be sent). An optional ENUM with no default
|
|
639
|
+
reports its first value as `DEFAULT`. Three classes, as the real server reports them:
|
|
640
|
+
|
|
641
|
+
- **Table events** (a generated DAO): every field sorted by Kotlin property name (`DATE_FIELD`
|
|
642
|
+
before `DATETIME_FIELD`), with titles, lengths, ranges and `GENESIS_TYPE`. `TYPE` is the JVM
|
|
643
|
+
one: a DATE column is `TYPE: 'DATETIME', GENESIS_TYPE: 'DATE'`.
|
|
644
|
+
- **Deletes** (`.../_DELETE`, or `pkOnly`): `<Table>.ById`, the key fields only, all mandatory,
|
|
645
|
+
no annotations (`GENESIS_TYPE: null`).
|
|
646
|
+
- **Custom DTOs** (`eventSchemas[name].fields`): the declared fields, no annotations,
|
|
647
|
+
`className` as DESCRIPTION.
|
|
648
|
+
|
|
649
|
+
The AI assistant (`ai-assistant/src/genesis/event-fields.ts`) builds its write tools from event
|
|
650
|
+
META: `NAME`, `GENESIS_TYPE`/`TYPE`, `JSON_TYPE`, `OPTIONAL`, `READ_ONLY`, `TITLE`,
|
|
651
|
+
`VALID_VALUES`.
|
|
652
|
+
|
|
653
|
+
**Behaviour changes for existing configs** (none under `fidelity: 'legacy'`):
|
|
654
|
+
`JSON_SCHEMA_REQUEST` by a query or request-server name now returns that resource's own
|
|
655
|
+
schema (the `DATA_LOGON` message, or `REQUEST` / `REPLY`), as GSF does, not its source table's
|
|
656
|
+
event-shaped one. Event names sort their fields, and `EVENT_*` names with no handler are a 404.
|
|
657
|
+
A declared source's rows carry exactly its exposed columns, `null` when unset.
|
|
658
|
+
|
|
659
|
+
Deviations: a bare table name with no request server of that name still gets the table's
|
|
660
|
+
insert-shaped event schema from `JSON_SCHEMA_REQUEST` (this engine's fallback; the real server
|
|
661
|
+
answers 404). Dataserver rows still carry neither RECORD_ID nor TIMESTAMP.
|
|
662
|
+
|
|
386
663
|
## Dataserver paging
|
|
387
664
|
|
|
388
665
|
`DATA_LOGON` pages the way the real dataserver does (genesis-pal-dataserver's
|
|
@@ -451,15 +728,89 @@ real server would also evict the oldest row in the same frame (the moving
|
|
|
451
728
|
view, a later item), so here the view can grow past `MAX_VIEW`; the next
|
|
452
729
|
`MORE_ROWS` then delivers nothing until DELETEs bring it back under the cap.
|
|
453
730
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
without one is never pushed, and rows sharing one are a single row to live
|
|
458
|
-
changes.
|
|
731
|
+
Every row's `ROW_REF` is its own record's `RECORD_ID` (a view row's, its base
|
|
732
|
+
record's), so live changes are matched to exactly the row the client holds,
|
|
733
|
+
whatever a view's `pkField` says.
|
|
459
734
|
|
|
460
735
|
`VIEW_NUMBER` is accepted and ignored. It selects the real server's paginated
|
|
461
736
|
mode, where each page replaces the last; here paging is always sequential.
|
|
462
737
|
|
|
738
|
+
## Request servers (`REQ_`)
|
|
739
|
+
|
|
740
|
+
A request server is one of two kinds on GSF 8.15.29, and they read a request
|
|
741
|
+
differently. `RequestReplyDef.criteriaOnly` picks the kind, as
|
|
742
|
+
`criteriaOnlyRequest = true` does in the GPAL config (genesis-pal-requestserver:
|
|
743
|
+
`EntityDbRequestReply.kt` for criteria-only, `RequestReply.kt` otherwise).
|
|
744
|
+
`test/requestReply.test.ts` pins the rules below, and the `reqrep-*` conformance
|
|
745
|
+
recordings replay against the showcase mock.
|
|
746
|
+
|
|
747
|
+
```ts
|
|
748
|
+
requestReplies: {
|
|
749
|
+
// showcase-reqrep.kts: config { criteriaOnlyRequest = true }
|
|
750
|
+
COUNTERPARTY: { source: 'COUNTERPARTY', criteriaOnly: true },
|
|
751
|
+
// auth-reqrep.kts: request { CODE } reply { CODE DESCRIPTION }
|
|
752
|
+
RIGHT: { source: 'RIGHT', fields: ['CODE', 'DESCRIPTION'] },
|
|
753
|
+
// config { maxRows = 500 }
|
|
754
|
+
BIG: { source: 'BIG', criteriaOnly: true, rowLimit: 500 },
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
| | Criteria-only (`criteriaOnly: true`) | Plain |
|
|
759
|
+
|---|---|---|
|
|
760
|
+
| `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) |
|
|
761
|
+
| `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 |
|
|
762
|
+
| `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 |
|
|
763
|
+
| `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 |
|
|
764
|
+
| `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) |
|
|
765
|
+
| Reply | `{REPLY, PARAMETRIC_TYPE, MESSAGE_TYPE, NEXT_VIEW?, NEXT_OFFSET?, MORE_ROWS}`: `MORE_ROWS` always, `NEXT_OFFSET` only while it is `true`; past the end `REPLY: []` (recorded: `reqrep-paging`, `reqrep-max-rows`) | `{MESSAGE_TYPE, SOURCE_REF, PARAMETRIC_TYPE, REPLY}` |
|
|
766
|
+
|
|
767
|
+
- **Wildcards** (`WildcardFilter.kt`): any string value containing `*`. With `*`
|
|
768
|
+
at both ends it is "contains", at the start "ends with", at the end "starts
|
|
769
|
+
with", after trimming every leading and trailing `*`; `*` alone matches
|
|
770
|
+
everything. Case-sensitive. A `*` only in the middle matches nothing. The
|
|
771
|
+
row value is compared as Kotlin's `toString()`: `null` reads `"null"`, a
|
|
772
|
+
whole `DOUBLE` `"2.0"`, a date its ISO-8601 form.
|
|
773
|
+
- **`PARAMETRIC_TYPE`** is the source's generated class:
|
|
774
|
+
`global.genesis.gen.dao.<Table>` (`global.genesis.gen.dao.Counterparty`), or
|
|
775
|
+
`global.genesis.gen.view.entity.<View>` for a view.
|
|
776
|
+
- **Rows** carry the reply block (`fields`) in its order, `null` where unset;
|
|
777
|
+
without one, the source's columns then `RECORD_ID` and `TIMESTAMP`.
|
|
778
|
+
`REPLY_FIELD` and the JSON schema's `REPLY` say the same. An undeclared
|
|
779
|
+
source's rows go out as stored.
|
|
780
|
+
- **`filter` and `auth`** are applied to the rows the read returns, so a
|
|
781
|
+
criteria-only server reads on until one row past the page has passed them,
|
|
782
|
+
and `NEXT_OFFSET` is the request's `OFFSET` plus the rows that took, minus
|
|
783
|
+
one — not `OFFSET + MAX_ROWS`. A `VIEW_NUMBER` page read this way keeps
|
|
784
|
+
what passes of one page and sends no `MORE_ROWS` (GSF can't tell).
|
|
785
|
+
- **Failures** are a service `MSG_NACK`
|
|
786
|
+
(`{WARNING: [], ERROR: [{'@type': 'StandardError', CODE, TEXT, STATUS_CODE}], MESSAGE_TYPE: 'MSG_NACK'}`),
|
|
787
|
+
which foundation-comms rejects the request's promise with. A `resolver`'s
|
|
788
|
+
`NackError` keeps its items; anything else it throws is `GENERIC_ERROR`
|
|
789
|
+
with the message (`AbstractCustomReqRep.kt`).
|
|
790
|
+
- A `CRITERIA_MATCH` calling a function this engine's grammar doesn't model
|
|
791
|
+
(it may be valid Groovy) still fails open, logged, as on the dataserver.
|
|
792
|
+
|
|
793
|
+
What clients see:
|
|
794
|
+
|
|
795
|
+
- **grid-pro's server-side and infinite row models** need a criteria-only
|
|
796
|
+
server (they refuse a resource whose `META_REQUEST` says
|
|
797
|
+
`CRITERIA_ONLY_REQUEST: false`), and page it by `OFFSET` with
|
|
798
|
+
`ORDER_BY: '<rowId> ASC'` by default. The showcase mock's request servers
|
|
799
|
+
are criteria-only, as the real showcase's are.
|
|
800
|
+
- **grid-pro's client-side datasource** keys `REP_` rows by `RECORD_ID`
|
|
801
|
+
unless `row-id` is set. A server whose reply block leaves `RECORD_ID` out
|
|
802
|
+
(like `RIGHT`) can't feed it without a `row-id`: that is GSF's behaviour too.
|
|
803
|
+
- **The AI assistant** reads with `REQUEST: {}` and
|
|
804
|
+
`DETAILS: {MAX_ROWS, CRITERIA_MATCH}`, and takes `MORE_ROWS` when sent,
|
|
805
|
+
else a full page, as "there may be more".
|
|
806
|
+
|
|
807
|
+
Deviations: `VIEW_NUMBER` and `OFFSET` are as GSF's SQL engine answers them
|
|
808
|
+
(non-SQL engines send no `MORE_ROWS` or `NEXT_OFFSET`); request field aliases,
|
|
809
|
+
`criteriaTemplate` and timeouts are not modelled; the plain kind always reads
|
|
810
|
+
by primary key, where GSF reads through the index a `REQUEST` names (the same
|
|
811
|
+
rows, possibly in that index's order); a resolver's rows are not filtered by
|
|
812
|
+
`CRITERIA_MATCH` or capped by `MAX_ROWS`, which a custom request server's are.
|
|
813
|
+
|
|
463
814
|
## Entity-management (`@genesislcap/foundation-entity-management`) support
|
|
464
815
|
|
|
465
816
|
`<entity-management>`'s create/update forms and delete confirmation don't use
|
|
@@ -479,21 +830,22 @@ zero config for the common case: `EVENT_<TABLE>_<VERB...>` is matched against
|
|
|
479
830
|
`config.tables` (longest match wins), and any event name matching
|
|
480
831
|
`.../_DELETE(/...)?/` gets a PK-only schema (matching what
|
|
481
832
|
`EntityManagement.confirmDelete` actually needs) while everything else gets
|
|
482
|
-
every field on that table
|
|
833
|
+
every field on that table (see [Declared schema](#declared-schema-meta_request-and-json_schema_request)
|
|
834
|
+
for what each property carries). This covers naming variants like
|
|
483
835
|
`EVENT_COUNTERPARTY_INSERT_WITH_APPROVAL`, `EVENT_TRADE_MODIFY_PRICE`, or
|
|
484
836
|
`EVENT_TRADE_INSERT_FAIL` without per-event config. Override via
|
|
485
837
|
`config.eventSchemas['<eventName>'] = { table, pkOnly, fieldTypes }` for
|
|
486
838
|
names that don't follow the convention.
|
|
487
839
|
|
|
488
|
-
The `FEATURE` doesn't have to be an event name. It
|
|
489
|
-
|
|
490
|
-
`
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
840
|
+
The `FEATURE` doesn't have to be an event name. It resolves, in order: an
|
|
841
|
+
entry in `config.eventSchemas`, a **dataserver** name (`ALL_TRADES`: the
|
|
842
|
+
`DATA_LOGON` schema, its rows in `OUTBOUND.$defs[...QueryRow]` — what
|
|
843
|
+
`<foundation-filters>` reads), a **request server** name (`TRADE`, without
|
|
844
|
+
`REQ_`: its rows in `OUTBOUND.properties.REPLY.items`), an
|
|
845
|
+
`EVENT_<TABLE>_<VERB>` event, and finally a bare **table name** (the table's
|
|
846
|
+
insert-shaped schema — a fallback the real server doesn't have). Under
|
|
847
|
+
`fidelity: 'legacy'` a query or request-server name resolves to its source
|
|
848
|
+
table's fields instead, as up to 15.47.
|
|
497
849
|
|
|
498
850
|
Everything else `<entity-management>` needs — resource discovery
|
|
499
851
|
(`RESOURCES_REQUEST`), grid metadata (`META_REQUEST`), row data (`DATA_LOGON`
|
|
@@ -568,8 +920,8 @@ extracted from for how each was actually found:
|
|
|
568
920
|
(`src/protocol/connection.ts`) does this automatically — never build a raw
|
|
569
921
|
response object yourself without going through it. A response missing
|
|
570
922
|
`SOURCE_REF` leaves the client's pending promise unresolved forever.
|
|
571
|
-
2. **`EVENT_NACK
|
|
572
|
-
|
|
923
|
+
2. **`EVENT_NACK` and request-server `MSG_NACK` errors are always an `ERROR` array of items** —
|
|
924
|
+
uppercase keys (see [NACK vocabulary](#nack-vocabulary)). Throw `NackError` from an event handler or
|
|
573
925
|
request-reply resolver rather than hand-rolling this shape; a lowercase
|
|
574
926
|
`{error, details}` object crashes the real client's `.forEach`. (The
|
|
575
927
|
router's `MSG_NACK` uses the same array, with an integer `CODE` and a
|
|
@@ -593,7 +945,9 @@ extracted from for how each was actually found:
|
|
|
593
945
|
directly.
|
|
594
946
|
5. **Datasource `init()` needs `TYPE: 'DATASERVER'` plus a non-empty `FIELD`
|
|
595
947
|
array** or the client silently reports "no fields returned" and gives up.
|
|
596
|
-
|
|
948
|
+
Declare the table's `fields` and it never is (see
|
|
949
|
+
[Declared schema](#declared-schema-meta_request-and-json_schema_request)).
|
|
950
|
+
Without them, `deriveFields()` (`src/db/metadata.ts`) infers this from the rows' own JS
|
|
597
951
|
types — falling back to the registration-time seed rows when every live
|
|
598
952
|
row has been deleted, so a runtime delete can't collapse a resource's
|
|
599
953
|
schema to zero fields. Numeric inference scans every row (a column is
|
|
@@ -626,7 +980,10 @@ extracted from for how each was actually found:
|
|
|
626
980
|
warning. Any criteria this grammar still doesn't cover fails open instead
|
|
627
981
|
of crashing the handler: `compileCriteria()` logs a warning and returns a
|
|
628
982
|
match-all predicate, so `DATA_LOGON` still responds (unfiltered) rather
|
|
629
|
-
than leaving the client's grid stuck on "Loading...".
|
|
983
|
+
than leaving the client's grid stuck on "Loading...". (A request server
|
|
984
|
+
answers a malformed criteria with a `MSG_NACK`, as GSF does, and fails open
|
|
985
|
+
only on a function it doesn't model; see
|
|
986
|
+
[Request servers](#request-servers-req_).) Add new entries to
|
|
630
987
|
`EXPR_FUNCTIONS` in `criteria.ts` if you hit one that isn't covered yet.
|
|
631
988
|
7. **Composite-key AMEND/DELETE matching needs a fallback beyond the PK.**
|
|
632
989
|
`Store.findRow()` (`src/db/store.ts`) tries the configured PK field, then
|
|
@@ -643,8 +1000,7 @@ in is pushed as an **INSERT** (`resolveSubscriptionOperation()` and
|
|
|
643
1000
|
`trackClientView()` in `src/server.ts`; see
|
|
644
1001
|
[Dataserver paging](#dataserver-paging)). If the predicate throws, the push
|
|
645
1002
|
is skipped instead: a broken filter must never evict rows. **Deletes** bypass
|
|
646
|
-
the criteria predicate
|
|
647
|
-
satisfy one; they reach every client that holds the row. (Under
|
|
1003
|
+
the criteria predicate; they reach every client that holds the row. (Under
|
|
648
1004
|
`fidelity: 'legacy'`, every DELETE reaches every subscription, and a row
|
|
649
1005
|
entering the criteria is pushed as a MODIFY.)
|
|
650
1006
|
|
|
@@ -656,9 +1012,21 @@ Where this engine still differs from the real server:
|
|
|
656
1012
|
only the rows whose values changed.
|
|
657
1013
|
- There is no **moving view**. Once the view holds `MAX_VIEW` rows, live
|
|
658
1014
|
INSERTs are still pushed instead of also evicting the oldest row.
|
|
659
|
-
-
|
|
660
|
-
by `RECORD_ID`. `ORDER_BY
|
|
661
|
-
|
|
1015
|
+
- Without `ORDER_BY`, snapshots are in **insertion order** (with or without
|
|
1016
|
+
`REVERSE`), where the real default is newest first by `RECORD_ID`. `ORDER_BY` names one of
|
|
1017
|
+
the query's `indexes` and sorts ascending by its fields, ties by `RECORD_ID`; `REVERSE`
|
|
1018
|
+
|
|
1019
|
+
flips that; an unknown index is `LOGON_NACK INVALID_INDEX`. Under `ORDER_BY`, a live INSERT
|
|
1020
|
+
that sorts among the rows still waiting for `MORE_ROWS` waits with them, in order (a MODIFY
|
|
1021
|
+
of a waiting row keeps its place). `DETAILS.FIELDS` narrows the rows to those columns.
|
|
1022
|
+
- A `CRITERIA_MATCH` naming a field the query doesn't expose is `LOGON_NACK
|
|
1023
|
+
INVALID_CRITERIA`, as on GSF — when the query's fields are known (declared, or a `fields`
|
|
1024
|
+
block). An inferred query can't tell, and evaluates the criteria on the stored row.
|
|
1025
|
+
(`REQ_` answers an unknown field with `REPLY: []`, as GSF does — see
|
|
1026
|
+
[Request servers](#request-servers-req_).)
|
|
1027
|
+
- `RECORD_ID`/`TIMESTAMP` are safe integers, not GSF's ~7.5e18 values (see
|
|
1028
|
+
[Record identity](#record-identity-row_ref-and-sequences)).
|
|
1029
|
+
|
|
662
1030
|
- The opt-in auth gate answers with typed `NOT_LOGGED_ON` replies, not the
|
|
663
1031
|
router's `MSG_NACK` 401.
|
|
664
1032
|
|
|
@@ -669,7 +1037,8 @@ the criteria grammar, store, metadata inference, and event-table inference
|
|
|
669
1037
|
plus end-to-end WS/HTTP tests (auth gating, filtered-subscription DELETE
|
|
670
1038
|
delivery, keyed `REQ_*` lookups, session reuse, clean shutdown — and, in
|
|
671
1039
|
`test/dataserver.test.ts`, the GSF envelopes, paging and the `'legacy'`
|
|
672
|
-
fidelity mode
|
|
1040
|
+
fidelity mode; `test/requestReply.test.ts` covers both kinds of request server)
|
|
1041
|
+
via `node --test`, no build step. Only `test/**/*.test.ts`
|
|
673
1042
|
files are run, so shared helpers (`test/support.ts`) can sit beside them.
|
|
674
1043
|
|
|
675
1044
|
## What's per-project vs. generic
|
package/dist/db/criteria.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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;
|
|
5
|
+
}
|
|
6
|
+
export declare class UnsupportedCriteriaError extends Error {
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseGenesisDateTime(value: unknown): number | null;
|
|
10
|
+
export interface ParsedCriteria {
|
|
11
|
+
predicate: Predicate;
|
|
12
|
+
fields: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function parseCriteria(criteria?: string | null): ParsedCriteria;
|
|
3
15
|
/** Compiles a CRITERIA_MATCH string into a predicate function(record) => boolean. */
|
|
4
16
|
export declare function compileCriteria(criteria?: string | null): Predicate;
|
|
17
|
+
export declare function criteriaFields(criteria?: string | null): string[] | undefined;
|
|
5
18
|
export declare function filterByCriteria(rows: Row[], criteria?: string | null): Row[];
|