@genesislcap/mock-server 15.51.1 → 15.52.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 +217 -31
- package/dist/db/criteria.d.ts +1 -0
- package/dist/db/criteria.js +29 -0
- package/dist/db/identity.d.ts +11 -0
- package/dist/db/identity.js +46 -0
- package/dist/db/schema.d.ts +46 -0
- package/dist/db/schema.js +375 -0
- package/dist/db/store.d.ts +13 -2
- package/dist/db/store.js +208 -34
- package/dist/handlers/commitEvent.js +20 -6
- package/dist/handlers/dataLogon.js +126 -6
- package/dist/handlers/jsonSchema.d.ts +2 -1
- package/dist/handlers/jsonSchema.js +216 -61
- package/dist/handlers/meta.d.ts +6 -1
- package/dist/handlers/meta.js +164 -21
- package/dist/handlers/requestReply.js +16 -5
- package/dist/handlers/resourceAuth.d.ts +1 -0
- package/dist/handlers/resourceAuth.js +14 -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 +9 -0
- package/dist/protocol/fieldTypes.js +101 -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/rowUpdate.d.ts +5 -3
- package/dist/protocol/rowUpdate.js +33 -15
- package/dist/server.js +90 -28
- package/dist/types.d.ts +40 -0
- package/package.json +1 -1
- package/src/db/criteria.ts +26 -0
- package/src/db/identity.ts +53 -0
- package/src/db/schema.ts +523 -0
- package/src/db/store.ts +225 -32
- package/src/handlers/commitEvent.ts +20 -7
- package/src/handlers/dataLogon.ts +144 -7
- package/src/handlers/jsonSchema.ts +290 -69
- package/src/handlers/meta.ts +222 -23
- package/src/handlers/requestReply.ts +15 -6
- package/src/handlers/resourceAuth.ts +18 -0
- package/src/index.ts +16 -1
- package/src/protocol/connection.ts +10 -2
- package/src/protocol/errors.ts +361 -13
- package/src/protocol/fieldTypes.ts +108 -0
- package/src/protocol/gsfSchemas.ts +505 -0
- package/src/protocol/messageTypes.ts +1 -0
- package/src/protocol/rowUpdate.ts +40 -20
- package/src/server.ts +101 -27
- package/src/types.ts +145 -1
package/README.md
CHANGED
|
@@ -152,12 +152,90 @@ covers the server-side behaviors real projects lean on:
|
|
|
152
152
|
```
|
|
153
153
|
|
|
154
154
|
- **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
|
-
|
|
155
|
+
(`pkField: ['RFQ_ID', 'SIDE']`). Rows are stored under the fields' values
|
|
156
|
+
joined with `'|'` (so values must not contain `'|'`) — no synthetic key
|
|
157
|
+
column, nothing extra in grids. Event handlers on composite tables pass
|
|
158
|
+
`store.rowKey(table, row)` to `broadcastTableChange`; sequence generation
|
|
159
|
+
still requires a single `pkField`.
|
|
160
|
+
|
|
161
|
+
## Record identity, ROW_REF and sequences
|
|
162
|
+
|
|
163
|
+
Every stored row carries `RECORD_ID` and `TIMESTAMP`, as every Genesis table
|
|
164
|
+
row does (genesis-db `InsertOperation.kt`):
|
|
165
|
+
|
|
166
|
+
- Seeding and `insertRow` stamp `RECORD_ID === TIMESTAMP` from one counter per
|
|
167
|
+
server that only goes up (`src/db/identity.ts`). A seed row's own
|
|
168
|
+
`RECORD_ID` is kept when it is a safe integer or a string not used by an
|
|
169
|
+
earlier seed row; an unsafe one (above 2^53, already rounded by
|
|
170
|
+
`JSON.parse`) or a repeat is re-stamped, with one warning per table, and a
|
|
171
|
+
missing `TIMESTAMP` becomes the `RECORD_ID`. An insert always gets fresh ones. `updateRow` moves `TIMESTAMP` on and
|
|
172
|
+
keeps `RECORD_ID`, even if the patch names either field.
|
|
173
|
+
- **Deviation:** GSF's values are about 7.5e18 (epoch millis shifted left 22
|
|
174
|
+
bits, plus a counter). That is above 2^53, so `JSON.parse` rounds
|
|
175
|
+
neighbouring rows onto one number. The mock's stamps are epoch millis, or the
|
|
176
|
+
last stamp plus one: exact, strictly increasing safe integers, in the same
|
|
177
|
+
order, that still read as a write time.
|
|
178
|
+
- **Dataserver rows** never carry `RECORD_ID`/`TIMESTAMP` (the real dataserver
|
|
179
|
+
identifies rows by `DETAILS.ROW_REF` only), unless the query's `fields` block
|
|
180
|
+
names them. Field metadata (`META_REQUEST`, `JSON_SCHEMA_REQUEST`) leaves them out.
|
|
181
|
+
- **`REP_` rows** from a `source` include them. **Deviation:** GSF sends them
|
|
182
|
+
only from criteria-only request servers (`criteriaOnly: true`); here every
|
|
183
|
+
source-backed request server does, because grid-pro's client-side
|
|
184
|
+
REQUEST_SERVER datasource keys rows by `RECORD_ID` and gives up without it,
|
|
185
|
+
and the showcase's servers are not marked `criteriaOnly` until `OFFSET`
|
|
186
|
+
paging lands (plan item 1.9), which makes this exact. A `resolver`'s rows
|
|
187
|
+
are sent as returned.
|
|
188
|
+
- **`DETAILS.ROW_REF`** is the record's `RECORD_ID` as a string
|
|
189
|
+
(`GenesisSetMaskingJsonSerializer`: `value.toString()`); a view row's is its
|
|
190
|
+
base record's. Rows no longer carry a top-level `ROW_REF` — clients copy it
|
|
191
|
+
from `DETAILS` themselves (`row.DETAILS[rowId] ?? row[rowId]`). Lookups stay
|
|
192
|
+
by pk: handlers still pass the pk (or `store.rowKey`) to
|
|
193
|
+
`broadcastTableChange`, and a pk-only row passed to `broadcast()` is matched
|
|
194
|
+
to its record, a just-deleted one included. A view row is rebuilt from the
|
|
195
|
+
changed base row, so a view's own `pkField` doesn't matter, and a MODIFY that
|
|
196
|
+
takes the row out of the view (an anti-join) is pushed as a DELETE.
|
|
197
|
+
- **Live DELETE** is `ROW: [{DETAILS: {OPERATION: 'DELETE', ROW_REF}}]` and
|
|
198
|
+
nothing else. The store remembers every record a key lost until a broadcast
|
|
199
|
+
announces it (`broadcastTableChange`, or `broadcast()` with a DELETE), so a
|
|
200
|
+
DELETE names each of them — a client gets the ones it holds — and a record
|
|
201
|
+
replaced under the same key before one broadcast goes out as a DELETE of the
|
|
202
|
+
old `ROW_REF` followed by the new INSERT. Deletes nobody broadcasts are kept
|
|
203
|
+
for the next one (bounded: 16 per key, 10,000 keys per table).
|
|
204
|
+
- **Sequences:** `TableDef.sequenceFormat` is `'prefix'` by default
|
|
205
|
+
(`sequencePrefix` + a counter padded to `sequenceWidth`: `TR001`), so
|
|
206
|
+
existing seeds, tests and snapshots keep their ids. `'genesis'` mints
|
|
207
|
+
`IdStrategy.java`'s format — 15-digit counter, sequence id, `LO`, `1`:
|
|
208
|
+
`000000000000001TRLO1`. Seeds in the table's format advance the counter. A
|
|
209
|
+
supplied value is kept; only a missing (`undefined`/`null`) one is generated.
|
|
210
|
+
|
|
211
|
+
`fidelity: 'legacy'` restores the pk-based `ROW_REF` (the pk value, or the
|
|
212
|
+
composite key), the top-level `ROW_REF`, pk-stub DELETE rows and `REP_` rows
|
|
213
|
+
without `RECORD_ID`/`TIMESTAMP`.
|
|
214
|
+
|
|
215
|
+
## NACK vocabulary
|
|
216
|
+
|
|
217
|
+
Service NACKs (`EVENT_NACK`, `REP_*` errors) follow GSF's `GenesisError` family
|
|
218
|
+
(`src/protocol/errors.ts`, pinned against recorded frames in
|
|
219
|
+
`test/nack.test.ts`): every `ERROR` item has `@type`, a string `CODE`, `TEXT`
|
|
220
|
+
and a `"<code> <reason>"` `STATUS_CODE`, and the envelope carries `WARNING`:
|
|
221
|
+
|
|
222
|
+
| `@type` | Extra fields | Built by |
|
|
223
|
+
|---|---|---|
|
|
224
|
+
| `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?)` |
|
|
225
|
+
| `FieldError` | `FIELD`, `PATH` (`null` for a handler's own check) | `NackError.field(field, text, path?)`, or any input with `FIELD` (`VALIDATION_ERROR`) |
|
|
226
|
+
| `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) |
|
|
227
|
+
| `LoginError` | `DETAILS: null` | `{'@type': 'LoginError', ...}` |
|
|
228
|
+
|
|
229
|
+
`ErrorCode` mirrors GSF's `ErrorCode.kt` (names, default texts, statuses;
|
|
230
|
+
`statusCodeFor(code)`); unknown codes get `500 Internal Server Error`.
|
|
231
|
+
`new NackError([...])` reports several errors at once. Anything else a handler
|
|
232
|
+
throws is `StandardError INTERNAL_ERROR` with its message. A handler can
|
|
233
|
+
return `{ warnings: [...] }` for a NACK with an empty `ERROR` and those
|
|
234
|
+
warnings, or pass `{ warnings }` to `NackError`; `VALIDATE`/`IGNORE_WARNINGS`
|
|
235
|
+
are not modelled yet. The router's `MSG_NACK` keeps its own shape (no `@type`,
|
|
236
|
+
an integer `CODE`, `DETAILS {ERROR, STATUS_CODE}`). `fidelity: 'legacy'` sends
|
|
237
|
+
the old `{CODE, TEXT, FIELD?}` items (`VALIDATION_ERROR` by default) and no
|
|
238
|
+
`WARNING`: warnings go in `ERROR` there.
|
|
161
239
|
|
|
162
240
|
## Running several servers in parallel (and one-command dev stacks)
|
|
163
241
|
|
|
@@ -317,8 +395,8 @@ the envelopes this engine sent up to 15.47 — see
|
|
|
317
395
|
| (live push) | `QUERY_UPDATE {ROW, SEQUENCE_ID}` | One row per push; `DETAILS.OPERATION` is `INSERT`/`MODIFY`/`DELETE`. No `MORE_ROWS`, no `ROWS_COUNT` |
|
|
318
396
|
| `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
397
|
| `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}` |
|
|
398
|
+
| `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) |
|
|
399
|
+
| `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
400
|
| `RESOURCES_REQUEST` | `RESOURCES_REQUEST_ACK` | |
|
|
323
401
|
| `REQ_<NAME>` | `REP_<NAME> {REPLY}` | Incl. keyed `REQUEST` field lookups (`'*'` = any) and `CRITERIA_MATCH` |
|
|
324
402
|
| `EVENT_<ENTITY>_<VERB>` commit events | `EVENT_ACK` / `EVENT_NACK` | Via `config.eventHandlers` or `defaultEventHandler` |
|
|
@@ -377,12 +455,109 @@ createMockServer({ ...config, fidelity: 'legacy' });
|
|
|
377
455
|
| `MORE_ROWS` | The next page, plus `MORE_ROWS_ACK` | Unanswered (logged as unhandled) |
|
|
378
456
|
| `DATA_LOGOFF` | `LOGOFF_ACK`, or `MSG_NACK` 404 | Unanswered |
|
|
379
457
|
| `META_REQUEST` / `JSON_SCHEMA_REQUEST` success | `FEATURE_ACK` / `JSON_SCHEMA_FEATURE_ACK` | Echoes the request's `MESSAGE_TYPE` |
|
|
458
|
+
| `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 |
|
|
459
|
+
| `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 |
|
|
460
|
+
| Declared schemas (`TableDef.fields`, query and view `fields`, `derivedTypes`) | Used for metadata and rows | Ignored: metadata is inferred, rows go out as stored |
|
|
461
|
+
| `DATA_LOGON` `ORDER_BY` / `REVERSE` / `FIELDS`, criteria on unexposed fields | Honoured / `LOGON_NACK` | Ignored |
|
|
380
462
|
| Unknown resource | `MSG_NACK` 404 (the client's promise rejects) | A typed reply with `ERROR: [{CODE: 'UNKNOWN_RESOURCE'}]` (the promise resolves) |
|
|
381
463
|
|
|
464
|
+
`test/legacyParity.test.ts` pins `'legacy'` to replies recorded from the 15.47.0 engine
|
|
465
|
+
itself, for a config with no declarations and for the showcase config.
|
|
466
|
+
|
|
382
467
|
`'legacy'` is a migration aid, not a second supported protocol. Plan to remove
|
|
383
468
|
it from your config; it will then be dropped from the engine. It is read on
|
|
384
469
|
every message, so changing it on a running server takes effect straight away.
|
|
385
470
|
|
|
471
|
+
## Declared schema (`META_REQUEST` and `JSON_SCHEMA_REQUEST`)
|
|
472
|
+
|
|
473
|
+
Metadata comes from the tables dictionary when a table declares one, and is
|
|
474
|
+
inferred from the rows (`src/db/metadata.ts`) when it doesn't. **Declared
|
|
475
|
+
wins.** A config that declares nothing keeps working, but its types are
|
|
476
|
+
guesses (`LONG`/`DOUBLE`/`BOOLEAN`/`STRING`), it has no enum values,
|
|
477
|
+
defaults or not-null, and a table with no rows has no fields.
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
tables: {
|
|
481
|
+
TRADE: {
|
|
482
|
+
pkField: 'TRADE_ID',
|
|
483
|
+
sequencePrefix: 'TR', // the key is a SEQUENCE: NULLABLE false, OPTIONAL true
|
|
484
|
+
fields: [
|
|
485
|
+
{ name: 'TRADE_ID', type: 'STRING' },
|
|
486
|
+
{ name: 'QUANTITY', type: 'INT', nullable: false },
|
|
487
|
+
{ name: 'SIDE', type: 'ENUM', values: ['SELL', 'BUY'], default: 'BUY', nullable: false },
|
|
488
|
+
{ name: 'COMMISSION', type: 'BIGDECIMAL' },
|
|
489
|
+
{ name: 'NAME', type: 'STRING', maxSize: 100, minLength: 0, title: 'Name' },
|
|
490
|
+
],
|
|
491
|
+
generated: [{ field: 'TRADE_NO', kind: 'AUTO_INCREMENT' }], // shape only
|
|
492
|
+
indexes: [{ name: 'TRADE_BY_SIDE', fields: ['SIDE'] }], // for later items
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
views: { TRADE_VIEW: { base: 'TRADE', /* joins, derived */ derivedTypes: { TOTAL: 'DOUBLE' } } },
|
|
496
|
+
queries: { ALL_TRADES: { source: 'TRADE_VIEW', fields: ['TRADE_ID', 'SIDE', 'TOTAL'],
|
|
497
|
+
indexes: [{ name: 'BY_SIDE', fields: ['SIDE'] }] } },
|
|
498
|
+
requestReplies: { TRADE: { source: 'TRADE', criteriaOnly: true } },
|
|
499
|
+
eventSchemas: { EVENT_TRADE_BOOK: { className: 'com.acme.BookInput', fields: [/* FieldDef */] } },
|
|
500
|
+
appName: 'showcase', // enum class names: global.genesis.gen.dao.enums.<appName>.<table>.<Field>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
- **`FieldDef`** `{name, type, nullable?, values?, default?, maxSize?, minLength?, description?, title?, optional?}`.
|
|
504
|
+
`type` is a dictionary type (`STRING ENUM INT SHORT LONG DOUBLE BIGDECIMAL BOOLEAN DATE
|
|
505
|
+
DATETIME NANO_TIMESTAMP RAW`). Fields are nullable unless declared otherwise; key and
|
|
506
|
+
generated fields default to not null. `description` replaces the JVM class name the real
|
|
507
|
+
server puts there (leave it unset on dates: foundation-forms spots a date by
|
|
508
|
+
`org.joda.time.DateTime`). `optional` decides whether an event payload may leave the field
|
|
509
|
+
out (see events below); an ENUM with no `values` sends no `VALID_VALUES` and no `enum`.
|
|
510
|
+
- **Views** take their base table's fields, each join's `select` columns typed from the joined
|
|
511
|
+
table (nullable), and `derivedTypes` for derived fields (inferred from rows when missing).
|
|
512
|
+
`ViewDef.fields` is the view's `fields { }` block: the view then exposes only those, in that
|
|
513
|
+
order (keep the key in it).
|
|
514
|
+
- **`QueryDef.fields`** is the query's `fields { }` block: rows **and** metadata carry only those
|
|
515
|
+
columns, in that order. A declared source's rows always carry every exposed column, `null`
|
|
516
|
+
when unset, as the real dataserver sends them. `rowFilter` still sees the whole row.
|
|
517
|
+
- `fieldTypes` (queries, request servers, `eventSchemas`) still overrides a TYPE either way.
|
|
518
|
+
|
|
519
|
+
What is sent (GSF 8.15.29; `test/recordedSchemas.test.ts` pins every shape to recorded frames,
|
|
520
|
+
ignoring only `SOURCE_REF`):
|
|
521
|
+
|
|
522
|
+
| Resource | `META_REQUEST` `DETAILS` | `JSON_SCHEMA_REQUEST` |
|
|
523
|
+
|---|---|---|
|
|
524
|
+
| 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`) |
|
|
525
|
+
| 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}` with `RECORD_ID: LONG` and `TIMESTAMP: NANO_TIMESTAMP` added | INBOUND: top-level `REQUEST` (object or array of the request fields) required, `DETAILS {VIEW_NUMBER, MAX_ROWS, CRITERIA_MATCH}`. OUTBOUND: `properties.REPLY.items` (nullable fields as `oneOf [null, type]`) |
|
|
526
|
+
| 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` |
|
|
527
|
+
|
|
528
|
+
Events resolve as `JSON_SCHEMA_REQUEST` always did: `config.eventSchemas`, then
|
|
529
|
+
`EVENT_<TABLE>_<VERB>` (longest table name wins) — but only for a registered event (an
|
|
530
|
+
`eventHandlers` or `eventSchemas` entry); any other `EVENT_*` name gets the router's 404, as on
|
|
531
|
+
the real server. `OPTIONAL` is true only when the class property has a default
|
|
532
|
+
(PALMetadataUtils.kt): a table's generated DAO defaults every nullable and generated column, so
|
|
533
|
+
those are optional; a custom DTO field is optional only with a `default` or `optional: true`
|
|
534
|
+
(a nullable DTO field with no default must still be sent). An optional ENUM with no default
|
|
535
|
+
reports its first value as `DEFAULT`. Three classes, as the real server reports them:
|
|
536
|
+
|
|
537
|
+
- **Table events** (a generated DAO): every field sorted by Kotlin property name (`DATE_FIELD`
|
|
538
|
+
before `DATETIME_FIELD`), with titles, lengths, ranges and `GENESIS_TYPE`. `TYPE` is the JVM
|
|
539
|
+
one: a DATE column is `TYPE: 'DATETIME', GENESIS_TYPE: 'DATE'`.
|
|
540
|
+
- **Deletes** (`.../_DELETE`, or `pkOnly`): `<Table>.ById`, the key fields only, all mandatory,
|
|
541
|
+
no annotations (`GENESIS_TYPE: null`).
|
|
542
|
+
- **Custom DTOs** (`eventSchemas[name].fields`): the declared fields, no annotations,
|
|
543
|
+
`className` as DESCRIPTION.
|
|
544
|
+
|
|
545
|
+
The AI assistant (`ai-assistant/src/genesis/event-fields.ts`) builds its write tools from event
|
|
546
|
+
META: `NAME`, `GENESIS_TYPE`/`TYPE`, `JSON_TYPE`, `OPTIONAL`, `READ_ONLY`, `TITLE`,
|
|
547
|
+
`VALID_VALUES`.
|
|
548
|
+
|
|
549
|
+
**Behaviour changes for existing configs** (none under `fidelity: 'legacy'`):
|
|
550
|
+
`JSON_SCHEMA_REQUEST` by a query or request-server name now returns that resource's own
|
|
551
|
+
schema (the `DATA_LOGON` message, or `REQUEST` / `REPLY`), as GSF does, not its source table's
|
|
552
|
+
event-shaped one. Event names sort their fields, and `EVENT_*` names with no handler are a 404.
|
|
553
|
+
A declared source's rows carry exactly its exposed columns, `null` when unset.
|
|
554
|
+
|
|
555
|
+
Deviations: a bare table name with no request server of that name still gets the table's
|
|
556
|
+
insert-shaped event schema from `JSON_SCHEMA_REQUEST` (this engine's fallback; the real server
|
|
557
|
+
answers 404). `criteriaOnly` changes metadata only — `REQ_` doesn't page by `OFFSET` yet, so the
|
|
558
|
+
showcase mock leaves it off (grid-pro's server-side and infinite models page a criteria-only
|
|
559
|
+
resource by `OFFSET`). Dataserver rows still carry neither RECORD_ID nor TIMESTAMP.
|
|
560
|
+
|
|
386
561
|
## Dataserver paging
|
|
387
562
|
|
|
388
563
|
`DATA_LOGON` pages the way the real dataserver does (genesis-pal-dataserver's
|
|
@@ -451,11 +626,9 @@ real server would also evict the oldest row in the same frame (the moving
|
|
|
451
626
|
view, a later item), so here the view can grow past `MAX_VIEW`; the next
|
|
452
627
|
`MORE_ROWS` then delivers nothing until DELETEs bring it back under the cap.
|
|
453
628
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
without one is never pushed, and rows sharing one are a single row to live
|
|
458
|
-
changes.
|
|
629
|
+
Every row's `ROW_REF` is its own record's `RECORD_ID` (a view row's, its base
|
|
630
|
+
record's), so live changes are matched to exactly the row the client holds,
|
|
631
|
+
whatever a view's `pkField` says.
|
|
459
632
|
|
|
460
633
|
`VIEW_NUMBER` is accepted and ignored. It selects the real server's paginated
|
|
461
634
|
mode, where each page replaces the last; here paging is always sequential.
|
|
@@ -479,21 +652,22 @@ zero config for the common case: `EVENT_<TABLE>_<VERB...>` is matched against
|
|
|
479
652
|
`config.tables` (longest match wins), and any event name matching
|
|
480
653
|
`.../_DELETE(/...)?/` gets a PK-only schema (matching what
|
|
481
654
|
`EntityManagement.confirmDelete` actually needs) while everything else gets
|
|
482
|
-
every field on that table
|
|
655
|
+
every field on that table (see [Declared schema](#declared-schema-meta_request-and-json_schema_request)
|
|
656
|
+
for what each property carries). This covers naming variants like
|
|
483
657
|
`EVENT_COUNTERPARTY_INSERT_WITH_APPROVAL`, `EVENT_TRADE_MODIFY_PRICE`, or
|
|
484
658
|
`EVENT_TRADE_INSERT_FAIL` without per-event config. Override via
|
|
485
659
|
`config.eventSchemas['<eventName>'] = { table, pkOnly, fieldTypes }` for
|
|
486
660
|
names that don't follow the convention.
|
|
487
661
|
|
|
488
|
-
The `FEATURE` doesn't have to be an event name. It
|
|
489
|
-
|
|
490
|
-
`
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
662
|
+
The `FEATURE` doesn't have to be an event name. It resolves, in order: an
|
|
663
|
+
entry in `config.eventSchemas`, a **dataserver** name (`ALL_TRADES`: the
|
|
664
|
+
`DATA_LOGON` schema, its rows in `OUTBOUND.$defs[...QueryRow]` — what
|
|
665
|
+
`<foundation-filters>` reads), a **request server** name (`TRADE`, without
|
|
666
|
+
`REQ_`: its rows in `OUTBOUND.properties.REPLY.items`), an
|
|
667
|
+
`EVENT_<TABLE>_<VERB>` event, and finally a bare **table name** (the table's
|
|
668
|
+
insert-shaped schema — a fallback the real server doesn't have). Under
|
|
669
|
+
`fidelity: 'legacy'` a query or request-server name resolves to its source
|
|
670
|
+
table's fields instead, as up to 15.47.
|
|
497
671
|
|
|
498
672
|
Everything else `<entity-management>` needs — resource discovery
|
|
499
673
|
(`RESOURCES_REQUEST`), grid metadata (`META_REQUEST`), row data (`DATA_LOGON`
|
|
@@ -568,8 +742,8 @@ extracted from for how each was actually found:
|
|
|
568
742
|
(`src/protocol/connection.ts`) does this automatically — never build a raw
|
|
569
743
|
response object yourself without going through it. A response missing
|
|
570
744
|
`SOURCE_REF` leaves the client's pending promise unresolved forever.
|
|
571
|
-
2. **`EVENT_NACK`/`REP_*` errors are always `ERROR
|
|
572
|
-
|
|
745
|
+
2. **`EVENT_NACK`/`REP_*` errors are always an `ERROR` array of items** —
|
|
746
|
+
uppercase keys (see [NACK vocabulary](#nack-vocabulary)). Throw `NackError` from an event handler or
|
|
573
747
|
request-reply resolver rather than hand-rolling this shape; a lowercase
|
|
574
748
|
`{error, details}` object crashes the real client's `.forEach`. (The
|
|
575
749
|
router's `MSG_NACK` uses the same array, with an integer `CODE` and a
|
|
@@ -593,7 +767,9 @@ extracted from for how each was actually found:
|
|
|
593
767
|
directly.
|
|
594
768
|
5. **Datasource `init()` needs `TYPE: 'DATASERVER'` plus a non-empty `FIELD`
|
|
595
769
|
array** or the client silently reports "no fields returned" and gives up.
|
|
596
|
-
|
|
770
|
+
Declare the table's `fields` and it never is (see
|
|
771
|
+
[Declared schema](#declared-schema-meta_request-and-json_schema_request)).
|
|
772
|
+
Without them, `deriveFields()` (`src/db/metadata.ts`) infers this from the rows' own JS
|
|
597
773
|
types — falling back to the registration-time seed rows when every live
|
|
598
774
|
row has been deleted, so a runtime delete can't collapse a resource's
|
|
599
775
|
schema to zero fields. Numeric inference scans every row (a column is
|
|
@@ -643,8 +819,7 @@ in is pushed as an **INSERT** (`resolveSubscriptionOperation()` and
|
|
|
643
819
|
`trackClientView()` in `src/server.ts`; see
|
|
644
820
|
[Dataserver paging](#dataserver-paging)). If the predicate throws, the push
|
|
645
821
|
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
|
|
822
|
+
the criteria predicate; they reach every client that holds the row. (Under
|
|
648
823
|
`fidelity: 'legacy'`, every DELETE reaches every subscription, and a row
|
|
649
824
|
entering the criteria is pushed as a MODIFY.)
|
|
650
825
|
|
|
@@ -656,9 +831,20 @@ Where this engine still differs from the real server:
|
|
|
656
831
|
only the rows whose values changed.
|
|
657
832
|
- There is no **moving view**. Once the view holds `MAX_VIEW` rows, live
|
|
658
833
|
INSERTs are still pushed instead of also evicting the oldest row.
|
|
659
|
-
-
|
|
660
|
-
by `RECORD_ID`. `ORDER_BY
|
|
661
|
-
|
|
834
|
+
- Without `ORDER_BY`, snapshots are in **insertion order** (with or without
|
|
835
|
+
`REVERSE`), where the real default is newest first by `RECORD_ID`. `ORDER_BY` names one of
|
|
836
|
+
the query's `indexes` and sorts ascending by its fields, ties by `RECORD_ID`; `REVERSE`
|
|
837
|
+
|
|
838
|
+
flips that; an unknown index is `LOGON_NACK INVALID_INDEX`. Under `ORDER_BY`, a live INSERT
|
|
839
|
+
that sorts among the rows still waiting for `MORE_ROWS` waits with them, in order (a MODIFY
|
|
840
|
+
of a waiting row keeps its place). `DETAILS.FIELDS` narrows the rows to those columns.
|
|
841
|
+
- A `CRITERIA_MATCH` naming a field the query doesn't expose is `LOGON_NACK
|
|
842
|
+
INVALID_CRITERIA`, as on GSF — when the query's fields are known (declared, or a `fields`
|
|
843
|
+
block). An inferred query can't tell, and evaluates the criteria on the stored row.
|
|
844
|
+
`REQ_` criteria aren't checked (the real server answers an unknown field with `REPLY: []`).
|
|
845
|
+
- `RECORD_ID`/`TIMESTAMP` are safe integers, not GSF's ~7.5e18 values (see
|
|
846
|
+
[Record identity](#record-identity-row_ref-and-sequences)).
|
|
847
|
+
|
|
662
848
|
- The opt-in auth gate answers with typed `NOT_LOGGED_ON` replies, not the
|
|
663
849
|
router's `MSG_NACK` 401.
|
|
664
850
|
|
package/dist/db/criteria.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ import type { Row } from '../types.ts';
|
|
|
2
2
|
export type Predicate = (record: Row) => boolean;
|
|
3
3
|
/** Compiles a CRITERIA_MATCH string into a predicate function(record) => boolean. */
|
|
4
4
|
export declare function compileCriteria(criteria?: string | null): Predicate;
|
|
5
|
+
export declare function criteriaFields(criteria?: string | null): string[] | undefined;
|
|
5
6
|
export declare function filterByCriteria(rows: Row[], criteria?: string | null): Row[];
|
package/dist/db/criteria.js
CHANGED
|
@@ -154,6 +154,9 @@ function compareByTime(record, args, resolve, cmp) {
|
|
|
154
154
|
class Parser {
|
|
155
155
|
tokens;
|
|
156
156
|
pos = 0;
|
|
157
|
+
// Every field the expression reads: bare identifiers in comparisons, and
|
|
158
|
+
// function-call arguments and receivers.
|
|
159
|
+
fields = new Set();
|
|
157
160
|
constructor(tokens) {
|
|
158
161
|
this.tokens = tokens;
|
|
159
162
|
}
|
|
@@ -227,6 +230,8 @@ class Parser {
|
|
|
227
230
|
if (rightToken === undefined) {
|
|
228
231
|
throw new Error('Expected operand on the right side of comparison');
|
|
229
232
|
}
|
|
233
|
+
this.noteField(leftToken);
|
|
234
|
+
this.noteField(rightToken);
|
|
230
235
|
const resolveLeft = (record) => this.resolveValue(leftToken, record);
|
|
231
236
|
const resolveRight = (record) => this.resolveValue(rightToken, record);
|
|
232
237
|
return (record) => {
|
|
@@ -252,6 +257,10 @@ class Parser {
|
|
|
252
257
|
}
|
|
253
258
|
};
|
|
254
259
|
}
|
|
260
|
+
noteField(token) {
|
|
261
|
+
if (parseLiteral(token) === undefined)
|
|
262
|
+
this.fields.add(token);
|
|
263
|
+
}
|
|
255
264
|
resolveValue(token, record) {
|
|
256
265
|
const literal = parseLiteral(token);
|
|
257
266
|
if (literal !== undefined)
|
|
@@ -286,6 +295,8 @@ class Parser {
|
|
|
286
295
|
if (!handler)
|
|
287
296
|
throw new Error(`Unsupported criteria function: ${name}`);
|
|
288
297
|
const allArgs = receiver === 'Expr' ? args : [receiver, ...args];
|
|
298
|
+
for (const arg of allArgs)
|
|
299
|
+
this.noteField(arg);
|
|
289
300
|
const resolve = (token, record) => this.resolveValue(token, record);
|
|
290
301
|
return (record) => handler(record, allArgs, resolve);
|
|
291
302
|
}
|
|
@@ -314,6 +325,24 @@ export function compileCriteria(criteria) {
|
|
|
314
325
|
return () => true;
|
|
315
326
|
}
|
|
316
327
|
}
|
|
328
|
+
// The fields a CRITERIA_MATCH reads, or undefined when it doesn't parse
|
|
329
|
+
// (compileCriteria then fails open). The real dataserver refuses a criteria
|
|
330
|
+
// naming a field its query doesn't expose (INVALID_CRITERIA).
|
|
331
|
+
export function criteriaFields(criteria) {
|
|
332
|
+
if (!criteria || typeof criteria !== 'string' || criteria.trim() === '')
|
|
333
|
+
return [];
|
|
334
|
+
try {
|
|
335
|
+
const tokens = tokenize(criteria.trim());
|
|
336
|
+
const parser = new Parser(tokens);
|
|
337
|
+
parser.parseExpression();
|
|
338
|
+
if (parser.position !== tokens.length)
|
|
339
|
+
return undefined;
|
|
340
|
+
return [...parser.fields];
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
317
346
|
export function filterByCriteria(rows, criteria) {
|
|
318
347
|
const predicate = compileCriteria(criteria);
|
|
319
348
|
return rows.filter((row) => {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Row } from '../types.ts';
|
|
2
|
+
export declare const RECORD_ID = "RECORD_ID";
|
|
3
|
+
export declare const TIMESTAMP = "TIMESTAMP";
|
|
4
|
+
export declare const RECORD_FIELDS: readonly string[];
|
|
5
|
+
export declare class RecordClock {
|
|
6
|
+
private last;
|
|
7
|
+
next(): number;
|
|
8
|
+
observe(value: unknown): void;
|
|
9
|
+
}
|
|
10
|
+
export declare function withoutRecordFields(row: Row): Row;
|
|
11
|
+
export declare function recordKey(row: Row | undefined | null): string | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Record identity: the RECORD_ID and TIMESTAMP every Genesis table row carries.
|
|
2
|
+
//
|
|
3
|
+
// GSF sets TIMESTAMP from a high-resolution clock on every write and
|
|
4
|
+
// RECORD_ID = TIMESTAMP on insert (genesis-db InsertOperation.kt); a modify
|
|
5
|
+
// moves TIMESTAMP on and keeps RECORD_ID. The real values are around 7.5e18:
|
|
6
|
+
// epoch millis shifted left 22 bits, plus a counter. That is far above 2^53,
|
|
7
|
+
// so a JavaScript client's JSON.parse rounds neighbouring rows onto the same
|
|
8
|
+
// number. The mock deliberately stays below that: its stamps are a counter
|
|
9
|
+
// seeded from epoch millis that only goes up, so they stay exact, strictly
|
|
10
|
+
// increasing safe integers, and a client that reads TIMESTAMP as a date (the
|
|
11
|
+
// AI assistant shows a NANO_TIMESTAMP as epoch millis) still gets roughly the
|
|
12
|
+
// write time. The same order, a different magnitude.
|
|
13
|
+
export const RECORD_ID = 'RECORD_ID';
|
|
14
|
+
export const TIMESTAMP = 'TIMESTAMP';
|
|
15
|
+
export const RECORD_FIELDS = [RECORD_ID, TIMESTAMP];
|
|
16
|
+
// Hands out RECORD_ID/TIMESTAMP values: the current epoch millis, or one more
|
|
17
|
+
// than the last value when that is not ahead of it (seeding a thousand rows
|
|
18
|
+
// runs the counter a second or so ahead of the clock).
|
|
19
|
+
export class RecordClock {
|
|
20
|
+
last = 0;
|
|
21
|
+
next() {
|
|
22
|
+
this.last = Math.max(Date.now(), this.last + 1);
|
|
23
|
+
return this.last;
|
|
24
|
+
}
|
|
25
|
+
// Keeps later stamps above a value a seed row brought with it.
|
|
26
|
+
observe(value) {
|
|
27
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value > this.last) {
|
|
28
|
+
this.last = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// A row without its record identity — what a dataserver sends (the real
|
|
33
|
+
// dataserver never puts RECORD_ID or TIMESTAMP in a row, only
|
|
34
|
+
// DETAILS.ROW_REF) and what field metadata is derived from.
|
|
35
|
+
export function withoutRecordFields(row) {
|
|
36
|
+
if (!(RECORD_ID in row) && !(TIMESTAMP in row))
|
|
37
|
+
return row;
|
|
38
|
+
const { [RECORD_ID]: _recordId, [TIMESTAMP]: _timestamp, ...rest } = row;
|
|
39
|
+
return rest;
|
|
40
|
+
}
|
|
41
|
+
// The Map key a record is tracked under. RECORD_IDs are numbers here, but a
|
|
42
|
+
// seed may bring a recorded string, so they are compared as strings.
|
|
43
|
+
export function recordKey(row) {
|
|
44
|
+
const id = row?.[RECORD_ID];
|
|
45
|
+
return id === undefined || id === null ? undefined : String(id);
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { FieldDef, MockServerConfig, QueryDef, RequestReplyDef, Row, TableDef } from '../types.ts';
|
|
2
|
+
import type { Store } from './store.ts';
|
|
3
|
+
export interface ResolvedField {
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
nullable: boolean;
|
|
7
|
+
generated: boolean;
|
|
8
|
+
values?: string[];
|
|
9
|
+
default?: unknown;
|
|
10
|
+
maxSize?: number;
|
|
11
|
+
minLength?: number;
|
|
12
|
+
description?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
optional?: boolean;
|
|
15
|
+
table?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ResolvedSchema {
|
|
18
|
+
fields: ResolvedField[];
|
|
19
|
+
declared: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function generatedFieldsOf(def: TableDef): Set<string>;
|
|
22
|
+
export declare function resolveSourceSchema(source: string, config: MockServerConfig, store: Store): ResolvedSchema;
|
|
23
|
+
export declare function withTypeOverrides(schema: ResolvedSchema, fieldTypes: Record<string, string> | undefined): ResolvedSchema;
|
|
24
|
+
export declare function projectSchema(schema: ResolvedSchema, names: string[] | undefined): ResolvedSchema;
|
|
25
|
+
export declare function querySchema(query: QueryDef, config: MockServerConfig, store: Store): ResolvedSchema;
|
|
26
|
+
export declare function requestReplySchema(requestReply: RequestReplyDef, config: MockServerConfig, store: Store): ResolvedSchema;
|
|
27
|
+
export declare const RECORD_IDENTITY_FIELDS: ResolvedField[];
|
|
28
|
+
export declare function requestFieldsOf(requestReply: RequestReplyDef, store: Store): string[];
|
|
29
|
+
export declare function knownValues(field: ResolvedField): string[] | undefined;
|
|
30
|
+
export declare function eventDefaultOf(field: ResolvedField): unknown;
|
|
31
|
+
export declare function queryFieldNames(query: QueryDef | undefined, config: MockServerConfig, store: Store): string[] | undefined;
|
|
32
|
+
export declare function projectRow(row: Row, names: string[] | undefined): Row;
|
|
33
|
+
export declare function isDeleteEventName(name: string): boolean;
|
|
34
|
+
export declare function inferEventTable(eventName: string | undefined, tableNames: string[]): string | undefined;
|
|
35
|
+
export interface EventTarget {
|
|
36
|
+
kind: 'table' | 'byId' | 'dto';
|
|
37
|
+
table?: string;
|
|
38
|
+
className: string;
|
|
39
|
+
title?: string;
|
|
40
|
+
fieldTypes?: Record<string, string>;
|
|
41
|
+
fields?: FieldDef[];
|
|
42
|
+
}
|
|
43
|
+
export declare function resolveEventTarget(name: string | undefined, config: MockServerConfig, { bareTable }?: {
|
|
44
|
+
bareTable?: boolean;
|
|
45
|
+
}): EventTarget | undefined;
|
|
46
|
+
export declare function eventFields(target: EventTarget, config: MockServerConfig, store: Store): ResolvedField[];
|