@genesislcap/mock-server 15.48.0 → 15.50.0-BACE-0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -36
- package/dist/handlers/commitEvent.js +15 -6
- package/dist/handlers/dataLogon.d.ts +8 -2
- package/dist/handlers/dataLogon.js +168 -17
- package/dist/handlers/jsonSchema.js +14 -7
- package/dist/handlers/meta.js +15 -6
- package/dist/handlers/requestReply.js +13 -9
- package/dist/index.d.ts +1 -1
- package/dist/protocol/connection.d.ts +4 -0
- package/dist/protocol/fidelity.d.ts +2 -0
- package/dist/protocol/fidelity.js +6 -0
- package/dist/protocol/messageTypes.d.ts +7 -0
- package/dist/protocol/messageTypes.js +9 -0
- package/dist/protocol/msgNack.d.ts +4 -0
- package/dist/protocol/msgNack.js +36 -0
- package/dist/protocol/router.js +16 -6
- package/dist/server.js +71 -12
- package/dist/types.d.ts +2 -0
- package/package.json +4 -2
- package/src/handlers/commitEvent.ts +18 -6
- package/src/handlers/dataLogon.ts +204 -18
- package/src/handlers/jsonSchema.ts +17 -7
- package/src/handlers/meta.ts +18 -6
- package/src/handlers/requestReply.ts +16 -9
- package/src/index.ts +1 -0
- package/src/protocol/connection.ts +16 -0
- package/src/protocol/fidelity.ts +8 -0
- package/src/protocol/messageTypes.ts +9 -0
- package/src/protocol/msgNack.ts +50 -0
- package/src/protocol/router.ts +16 -8
- package/src/server.ts +74 -12
- package/src/types.ts +15 -1
package/README.md
CHANGED
|
@@ -250,7 +250,8 @@ having to monkey-patch the engine:
|
|
|
250
250
|
registered handler, so a project with dozens of events can adopt
|
|
251
251
|
incrementally (`defaultEventHandler: () => ({})` acks everything
|
|
252
252
|
unhandled) instead of enumerating ack-only handlers. Unregistered events
|
|
253
|
-
|
|
253
|
+
get the router's `MSG_NACK` 404 when unset (see
|
|
254
|
+
[Protocol coverage](#protocol-coverage)). Note `RESOURCES_REQUEST` only
|
|
254
255
|
lists *named* events (`eventHandlers` + `eventSchemas` keys) — if the
|
|
255
256
|
client's resource gate blocks an event served only by the default handler,
|
|
256
257
|
name it in `config.eventSchemas` or `config.resources`.
|
|
@@ -301,18 +302,163 @@ a client opens. Enable it only for clients that authenticate over WS.
|
|
|
301
302
|
|
|
302
303
|
## Protocol coverage
|
|
303
304
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
|
310
|
-
|
|
311
|
-
| `
|
|
312
|
-
| `
|
|
313
|
-
| `
|
|
314
|
-
| `
|
|
315
|
-
| `
|
|
305
|
+
Envelopes follow the real GSF 8.15.x server (router, dataserver and event
|
|
306
|
+
handler Kotlin, checked against tag `v8.15.14`). `fidelity: 'legacy'` restores
|
|
307
|
+
the envelopes this engine sent up to 15.47 — see
|
|
308
|
+
[Wire fidelity](#wire-fidelity-and-the-legacy-escape-hatch).
|
|
309
|
+
|
|
310
|
+
| Inbound | Reply | Notes |
|
|
311
|
+
|---|---|---|
|
|
312
|
+
| `EVENT_LOGIN_AUTH` (WS) | `EVENT_LOGIN_AUTH_ACK` / `_NACK` | |
|
|
313
|
+
| `gwf/event-login-auth`, `gwf/event-login-details` (HTTP) | `EVENT_LOGIN_AUTH_ACK` / `EVENT_LOGIN_DETAILS_ACK` | |
|
|
314
|
+
| `EVENT_LOGOUT` | `LOGOUT_ACK` | |
|
|
315
|
+
| `HEARTBEAT_PING` | `HEARTBEAT_PONG` | |
|
|
316
|
+
| `DATA_LOGON` | `LOGON_ACK {SEQUENCE_ID: 0}`, then `QUERY_UPDATE {ROW, MORE_ROWS, ROWS_COUNT, SEQUENCE_ID: 1}` | The first page only — see [Dataserver paging](#dataserver-paging). No matching rows: `{QUERY_UPDATE, MORE_ROWS: false, SEQUENCE_ID: 1}`, with no `ROW` and no `ROWS_COUNT`. A second logon on a live `SOURCE_REF` gets `MSG_NACK` 409 |
|
|
317
|
+
| (live push) | `QUERY_UPDATE {ROW, SEQUENCE_ID}` | One row per push; `DETAILS.OPERATION` is `INSERT`/`MODIFY`/`DELETE`. No `MORE_ROWS`, no `ROWS_COUNT` |
|
|
318
|
+
| `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
|
+
| `DATA_LOGOFF` | `LOGOFF_ACK` | `MSG_NACK` 404 when the connection has no such subscription |
|
|
320
|
+
| `META_REQUEST` | `FEATURE_ACK {DETAILS}` | Auto-derived from seed data |
|
|
321
|
+
| `JSON_SCHEMA_REQUEST` | `JSON_SCHEMA_FEATURE_ACK {INBOUND, OUTBOUND}` | Auto-derived per event — see below |
|
|
322
|
+
| `RESOURCES_REQUEST` | `RESOURCES_REQUEST_ACK` | |
|
|
323
|
+
| `REQ_<NAME>` | `REP_<NAME> {REPLY}` | Incl. keyed `REQUEST` field lookups (`'*'` = any) and `CRITERIA_MATCH` |
|
|
324
|
+
| `EVENT_<ENTITY>_<VERB>` commit events | `EVENT_ACK` / `EVENT_NACK` | Via `config.eventHandlers` or `defaultEventHandler` |
|
|
325
|
+
| Anything naming an unknown resource | `MSG_NACK` 404 | See below |
|
|
326
|
+
|
|
327
|
+
Each `SEQUENCE_ID` on a subscription is the previous one plus 1. The snapshot
|
|
328
|
+
is 1, and each later `MORE_ROWS` page or live push takes the next number.
|
|
329
|
+
`LOGON_ACK` is 0, and `MORE_ROWS_ACK` and `LOGOFF_ACK` carry none.
|
|
330
|
+
|
|
331
|
+
**Unknown resources** are rejected the way the real router rejects them,
|
|
332
|
+
before any handler runs:
|
|
333
|
+
|
|
334
|
+
```json
|
|
335
|
+
{
|
|
336
|
+
"MESSAGE_TYPE": "MSG_NACK",
|
|
337
|
+
"SOURCE_REF": "<the request's>",
|
|
338
|
+
"ERROR": [{ "CODE": 404, "TEXT": "Resource X is not mapped to a service", "STATUS_CODE": "404 Not Found" }],
|
|
339
|
+
"DETAILS": { "ERROR": "Resource X is not mapped to a service", "STATUS_CODE": "404 Not Found" }
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`CODE` is the integer status. The resource name `X` is `DETAILS.DATASOURCE_NAME`
|
|
344
|
+
for `DATA_LOGON`, the name after `REQ_` for request servers, `DETAILS.FEATURE`
|
|
345
|
+
for `META_REQUEST` and `JSON_SCHEMA_REQUEST`, and the `MESSAGE_TYPE` for a
|
|
346
|
+
commit event with no handler (and no `defaultEventHandler`). A missing name is
|
|
347
|
+
the empty string. The same frame answers two subscription errors:
|
|
348
|
+
|
|
349
|
+
- `MORE_ROWS` or `DATA_LOGOFF` for a subscription the connection doesn't
|
|
350
|
+
have gets `TEXT` "Dataserver subscription does not exist".
|
|
351
|
+
- A repeated `DATA_LOGON` gets `CODE` 409 and `"409 Conflict"`.
|
|
352
|
+
|
|
353
|
+
This is the change consumers notice most. foundation-comms' socket
|
|
354
|
+
**rejects** the pending promise on `MSG_NACK`
|
|
355
|
+
(`foundation-comms/src/connect/socket.ts`). The old typed replies resolved it:
|
|
356
|
+
`QUERY_UPDATE`, `REP_*`, `EVENT_NACK` or an echoed `META_REQUEST`, each carrying
|
|
357
|
+
`ERROR: [{CODE: 'UNKNOWN_RESOURCE'}]`.
|
|
358
|
+
|
|
359
|
+
## Wire fidelity and the legacy escape hatch
|
|
360
|
+
|
|
361
|
+
Behaviour that matches the real server is the default. `config.fidelity` is one
|
|
362
|
+
switch for the whole server. Its type is `FidelityMode` (`'gsf' | 'legacy'`)
|
|
363
|
+
and it defaults to `'gsf'`. `'legacy'` restores what this engine sent up to
|
|
364
|
+
15.47, for a consumer built against those envelopes that can't move yet:
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
createMockServer({ ...config, fidelity: 'legacy' });
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
| | `'gsf'` (default) | `'legacy'` |
|
|
371
|
+
|---|---|---|
|
|
372
|
+
| `DATA_LOGON` | `LOGON_ACK`, then the first `MAX_ROWS` rows | Every row in one `QUERY_UPDATE`, no `LOGON_ACK`; `MAX_ROWS` and `MAX_VIEW` are ignored |
|
|
373
|
+
| No matching rows | No `ROW`, no `ROWS_COUNT` | `ROW: []`, `ROWS_COUNT: 0` |
|
|
374
|
+
| Repeated `DATA_LOGON` on a live `SOURCE_REF` | `MSG_NACK` 409; the first subscription keeps running | Replaces the subscription and sends a new snapshot |
|
|
375
|
+
| `ROWS_COUNT` | Rows after the query `filter`, before `CRITERIA_MATCH` and the `rowFilter` | Rows after all three |
|
|
376
|
+
| Live push | No `ROWS_COUNT`; only for rows the client holds, or as an `INSERT` for a row that is new to it | `ROWS_COUNT: 1`; to every subscription whose criteria match (every DELETE goes to all of them) |
|
|
377
|
+
| `MORE_ROWS` | The next page, plus `MORE_ROWS_ACK` | Unanswered (logged as unhandled) |
|
|
378
|
+
| `DATA_LOGOFF` | `LOGOFF_ACK`, or `MSG_NACK` 404 | Unanswered |
|
|
379
|
+
| `META_REQUEST` / `JSON_SCHEMA_REQUEST` success | `FEATURE_ACK` / `JSON_SCHEMA_FEATURE_ACK` | Echoes the request's `MESSAGE_TYPE` |
|
|
380
|
+
| Unknown resource | `MSG_NACK` 404 (the client's promise rejects) | A typed reply with `ERROR: [{CODE: 'UNKNOWN_RESOURCE'}]` (the promise resolves) |
|
|
381
|
+
|
|
382
|
+
`'legacy'` is a migration aid, not a second supported protocol. Plan to remove
|
|
383
|
+
it from your config; it will then be dropped from the engine. It is read on
|
|
384
|
+
every message, so changing it on a running server takes effect straight away.
|
|
385
|
+
|
|
386
|
+
## Dataserver paging
|
|
387
|
+
|
|
388
|
+
`DATA_LOGON` pages the way the real dataserver does (genesis-pal-dataserver's
|
|
389
|
+
`ClientOptions` and `QueryClient`):
|
|
390
|
+
|
|
391
|
+
- **`DETAILS.MAX_ROWS`** (default 250) is the page size. **`DETAILS.MAX_VIEW`**
|
|
392
|
+
(default 1000) is the most rows the view holds across all pages. `MAX_ROWS`
|
|
393
|
+
is clamped to `MAX_VIEW`, and `0` means "no limit" for either. These are
|
|
394
|
+
also foundation-comms' `DatasourceDefaults`, so a grid that sets neither gets
|
|
395
|
+
250 rows and then pages.
|
|
396
|
+
- The snapshot is the first page: at most `MAX_ROWS` rows, and `MORE_ROWS: true`
|
|
397
|
+
when more are waiting. **`ROWS_COUNT`** is on the first page only, and counts
|
|
398
|
+
like the real dataserver's index count: it **ignores `CRITERIA_MATCH` and the
|
|
399
|
+
user's `rowFilter`** (recorded: `ROWS_COUNT: 10` with 6 rows matching), so it
|
|
400
|
+
can be larger than what the client can ever page to. grid-pro and the AI
|
|
401
|
+
assistant already treat it that way. It is taken after the query's own
|
|
402
|
+
`filter`, the where clause the real index is built over; no recording covers
|
|
403
|
+
that case. With no matching rows it is left out, as GSF does.
|
|
404
|
+
- The other rows wait in the subscription. **`MORE_ROWS`** sends the next
|
|
405
|
+
`MAX_ROWS` of them as a `QUERY_UPDATE` on the logon's `SOURCE_REF`, with
|
|
406
|
+
`ROW` and `MORE_ROWS` and without `ROWS_COUNT`. The request names the
|
|
407
|
+
subscription in `DETAILS.SOURCE_REF`, or is sent on the logon's own
|
|
408
|
+
`SOURCE_REF`. foundation-comms' `createMoreRowsMessage` sends
|
|
409
|
+
`{MESSAGE_TYPE: 'MORE_ROWS', SOURCE_REF: <new>, DETAILS: {SOURCE_REF: <stream>, VIEW_NUMBER}}`
|
|
410
|
+
and doesn't wait for a reply.
|
|
411
|
+
- When `DETAILS.SOURCE_REF` is given, `MORE_ROWS_ACK` also answers the
|
|
412
|
+
request's own `SOURCE_REF`. It is sent before the page: the real router
|
|
413
|
+
acks while its dataserver is still reading.
|
|
414
|
+
- After the last page, `MORE_ROWS` still gets an answer:
|
|
415
|
+
`{QUERY_UPDATE, MORE_ROWS: false}` with no `ROW`. The real server sends the
|
|
416
|
+
same when a read finds nothing.
|
|
417
|
+
- **A full view is not the end of the data.** Once the client holds `MAX_VIEW`
|
|
418
|
+
rows, matching rows that don't fit keep `MORE_ROWS: true` — on the page that
|
|
419
|
+
filled the view, and on the empty `{QUERY_UPDATE, MORE_ROWS: true}` page that
|
|
420
|
+
answers every later `MORE_ROWS` (recorded: `MAX_ROWS` 10, `MAX_VIEW` 3 and 10
|
|
421
|
+
rows give 3 rows, then empty pages, all `MORE_ROWS: true`). A client that
|
|
422
|
+
keeps asking while `MORE_ROWS` is true loops. When a live `DELETE` frees a
|
|
423
|
+
slot, the next `MORE_ROWS` fills it with the next row waiting, as the real
|
|
424
|
+
server's read does.
|
|
425
|
+
- Subscriptions belong to their connection. Another socket's `MORE_ROWS` or
|
|
426
|
+
`DATA_LOGOFF` for the same `SOURCE_REF` gets `MSG_NACK` 404 and no rows, and
|
|
427
|
+
the owner's next page is untouched. Two sockets may use the same
|
|
428
|
+
`SOURCE_REF`; closing one leaves the other's subscription alone.
|
|
429
|
+
|
|
430
|
+
**Live changes follow what each client holds.** Like the real dataserver's
|
|
431
|
+
client index, every subscription knows which rows it has sent (`held`) and
|
|
432
|
+
which matched rows are still waiting (`pending`: past the current page, or
|
|
433
|
+
past `MAX_VIEW`). A row is in scope while it passes the query `filter`,
|
|
434
|
+
`CRITERIA_MATCH` and the user's `rowFilter`. For each live change:
|
|
435
|
+
|
|
436
|
+
| The row is… | In scope after the change | Out of scope, or deleted |
|
|
437
|
+
|---|---|---|
|
|
438
|
+
| Held by the client | `MODIFY` pushed | `DELETE` pushed (row identity only) |
|
|
439
|
+
| Waiting for `MORE_ROWS` | Waiting copy updated in place, nothing pushed | Dropped from the waiting rows, nothing pushed |
|
|
440
|
+
| Not known to the client | `INSERT` pushed with the full row; now held | Nothing pushed |
|
|
441
|
+
|
|
442
|
+
So a new row arrives as an `INSERT`, a row a MODIFY brings back into the
|
|
443
|
+
criteria arrives as an `INSERT` (recorded: `DELETE` on leaving, `INSERT` on
|
|
444
|
+
re-entering, same `ROW_REF`), and rows the client never got — including rows
|
|
445
|
+
cut off by `MAX_VIEW`, and the hundreds of view rows a change to a joined table
|
|
446
|
+
re-materializes — get no MODIFYs or DELETEs at all. A waiting row's page later
|
|
447
|
+
delivers its current values. Nothing absorbed uses a `SEQUENCE_ID`.
|
|
448
|
+
|
|
449
|
+
An `INSERT` is pushed even when the view already holds `MAX_VIEW` rows. The
|
|
450
|
+
real server would also evict the oldest row in the same frame (the moving
|
|
451
|
+
view, a later item), so here the view can grow past `MAX_VIEW`; the next
|
|
452
|
+
`MORE_ROWS` then delivers nothing until DELETEs bring it back under the cap.
|
|
453
|
+
|
|
454
|
+
Rows with no `ROW_REF`, or one another row already has (a view `pkField` that
|
|
455
|
+
isn't unique), are still delivered by the snapshot and `MORE_ROWS`. Live
|
|
456
|
+
changes can only be matched by `ROW_REF`, as on the client: a change to a row
|
|
457
|
+
without one is never pushed, and rows sharing one are a single row to live
|
|
458
|
+
changes.
|
|
459
|
+
|
|
460
|
+
`VIEW_NUMBER` is accepted and ignored. It selects the real server's paginated
|
|
461
|
+
mode, where each page replaces the last; here paging is always sequential.
|
|
316
462
|
|
|
317
463
|
## Entity-management (`@genesislcap/foundation-entity-management`) support
|
|
318
464
|
|
|
@@ -346,7 +492,7 @@ an entry in `config.eventSchemas`, a **resource name** (a `config.queries` or
|
|
|
346
492
|
`<entity-management resourceName="COUNTERPARTY">` asks by table and the
|
|
347
493
|
showcase's react-client Admin page does that, while the FAST-Element client's
|
|
348
494
|
grids ask by query name (`ALL_USERS`, `ALL_PROFILES`, `ALL_TRADES`). Resolve
|
|
349
|
-
only event names and those requests
|
|
495
|
+
only event names and those requests are rejected with `MSG_NACK` 404 and the form
|
|
350
496
|
renders no fields — the same dead button as above, from the other direction.
|
|
351
497
|
|
|
352
498
|
Everything else `<entity-management>` needs — resource discovery
|
|
@@ -425,7 +571,9 @@ extracted from for how each was actually found:
|
|
|
425
571
|
2. **`EVENT_NACK`/`REP_*` errors are always `ERROR: [{CODE, TEXT}]`** — an
|
|
426
572
|
array, uppercase keys. Throw `NackError` from an event handler or
|
|
427
573
|
request-reply resolver rather than hand-rolling this shape; a lowercase
|
|
428
|
-
`{error, details}` object crashes the real client's `.forEach`.
|
|
574
|
+
`{error, details}` object crashes the real client's `.forEach`. (The
|
|
575
|
+
router's `MSG_NACK` uses the same array, with an integer `CODE` and a
|
|
576
|
+
`STATUS_CODE` — `src/protocol/msgNack.ts` builds it.)
|
|
429
577
|
3. **Commit-event acks are the GENERIC `EVENT_ACK`/`EVENT_NACK`** — never
|
|
430
578
|
`<EVENT_NAME>_ACK`/`_NACK`. The reply is correlated purely by
|
|
431
579
|
`SOURCE_REF`; real consumers match the MESSAGE_TYPE string exactly
|
|
@@ -434,9 +582,10 @@ extracted from for how each was actually found:
|
|
|
434
582
|
`=== 'EVENT_NACK'`). Get this wrong and every successful commit renders
|
|
435
583
|
as a failure toast while every NACK passes as silent success.
|
|
436
584
|
`handleCommitEvent()` (`src/handlers/commitEvent.ts`) does this for you.
|
|
437
|
-
4. **`DATA_LOGON` snapshot and later live pushes
|
|
438
|
-
(`MESSAGE_TYPE: 'QUERY_UPDATE'`,
|
|
439
|
-
|
|
585
|
+
4. **`DATA_LOGON` snapshot, `MORE_ROWS` pages and later live pushes share one
|
|
586
|
+
envelope** (`MESSAGE_TYPE: 'QUERY_UPDATE'`, the logon's `SOURCE_REF`; only
|
|
587
|
+
the snapshot carries `ROWS_COUNT`, and live pushes carry no `MORE_ROWS`) —
|
|
588
|
+
the client distinguishes insert/amend/delete purely via each row's
|
|
440
589
|
`DETAILS.OPERATION` (`'INSERT' | 'MODIFY' | 'DELETE'`, no other vocabulary
|
|
441
590
|
is recognized) plus `DETAILS.ROW_REF` for row identity. `stampRow()`
|
|
442
591
|
(`src/protocol/rowUpdate.ts`) does this for you — always route pushes
|
|
@@ -485,30 +634,43 @@ extracted from for how each was actually found:
|
|
|
485
634
|
combination of provided fields — needed for tables where no single field
|
|
486
635
|
is unique on its own.
|
|
487
636
|
|
|
488
|
-
##
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
client
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
637
|
+
## Live criteria membership, and known deviations
|
|
638
|
+
|
|
639
|
+
A live **MODIFY** that moves a row out of a subscription's `CRITERIA_MATCH`, or
|
|
640
|
+
out of the query's `filter`, is pushed as a **DELETE** carrying only the row's
|
|
641
|
+
identity to a client that holds the row, and a MODIFY that brings a row back
|
|
642
|
+
in is pushed as an **INSERT** (`resolveSubscriptionOperation()` and
|
|
643
|
+
`trackClientView()` in `src/server.ts`; see
|
|
644
|
+
[Dataserver paging](#dataserver-paging)). If the predicate throws, the push
|
|
645
|
+
is skipped instead: a broken filter must never evict rows. **Deletes** bypass
|
|
646
|
+
the criteria predicate, because the pushed row is a PK-only stub that couldn't
|
|
647
|
+
satisfy one; they reach every client that holds the row. (Under
|
|
648
|
+
`fidelity: 'legacy'`, every DELETE reaches every subscription, and a row
|
|
649
|
+
entering the criteria is pushed as a MODIFY.)
|
|
650
|
+
|
|
651
|
+
Where this engine still differs from the real server:
|
|
652
|
+
|
|
653
|
+
- Live MODIFYs carry the **full row**. The real server sends only the changed
|
|
654
|
+
fields; a full row is a superset, which clients tolerate.
|
|
655
|
+
- A change to a **joined table** pushes a MODIFY for every held view row, not
|
|
656
|
+
only the rows whose values changed.
|
|
657
|
+
- There is no **moving view**. Once the view holds `MAX_VIEW` rows, live
|
|
658
|
+
INSERTs are still pushed instead of also evicting the oldest row.
|
|
659
|
+
- Snapshots are in **insertion order**, where the real default is newest first
|
|
660
|
+
by `RECORD_ID`. `ORDER_BY`, `REVERSE` and `FIELDS` are ignored.
|
|
661
|
+
- Rows carry `ROW_REF` at the top level as well as in `DETAILS.ROW_REF`.
|
|
662
|
+
- The opt-in auth gate answers with typed `NOT_LOGGED_ON` replies, not the
|
|
663
|
+
router's `MSG_NACK` 401.
|
|
504
664
|
|
|
505
665
|
## Tests
|
|
506
666
|
|
|
507
667
|
`pnpm --filter @genesislcap/mock-server test` runs the unit tests for
|
|
508
668
|
the criteria grammar, store, metadata inference, and event-table inference
|
|
509
669
|
plus end-to-end WS/HTTP tests (auth gating, filtered-subscription DELETE
|
|
510
|
-
delivery, keyed `REQ_*` lookups, session reuse, clean shutdown
|
|
511
|
-
`
|
|
670
|
+
delivery, keyed `REQ_*` lookups, session reuse, clean shutdown — and, in
|
|
671
|
+
`test/dataserver.test.ts`, the GSF envelopes, paging and the `'legacy'`
|
|
672
|
+
fidelity mode) via `node --test`, no build step. Only `test/**/*.test.ts`
|
|
673
|
+
files are run, so shared helpers (`test/support.ts`) can sit beside them.
|
|
512
674
|
|
|
513
675
|
## What's per-project vs. generic
|
|
514
676
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { NackError } from "../protocol/errors.js";
|
|
2
|
+
import { isLegacyFidelity } from "../protocol/fidelity.js";
|
|
2
3
|
import { ERROR_CODE, MESSAGE_TYPE } from "../protocol/messageTypes.js";
|
|
4
|
+
import { resourceNotMappedNack } from "../protocol/msgNack.js";
|
|
3
5
|
// Commit events (EVENT_<ENTITY>_INSERT/_AMEND/_DELETE, or whatever suffix
|
|
4
6
|
// convention your project uses) are looked up by exact MESSAGE_TYPE against
|
|
5
7
|
// config.eventHandlers. A handler receives (details, ctx) and either returns
|
|
@@ -15,13 +17,20 @@ export function handleCommitEvent(message, connection, config, store, ctx) {
|
|
|
15
17
|
// real consumers (foundation-comms' kv.service, entity-management) match
|
|
16
18
|
// these names exactly, so a name-suffixed <EVENT>_ACK reads as failure and
|
|
17
19
|
// a <EVENT>_NACK passes as silent success.
|
|
20
|
+
// An event nothing handles never reaches an event handler on the real
|
|
21
|
+
// server: the router rejects it by name with MSG_NACK 404.
|
|
18
22
|
if (!handler) {
|
|
19
|
-
connection.reply(message,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
connection.reply(message, isLegacyFidelity(config)
|
|
24
|
+
? {
|
|
25
|
+
MESSAGE_TYPE: MESSAGE_TYPE.EVENT_NACK,
|
|
26
|
+
ERROR: [
|
|
27
|
+
{
|
|
28
|
+
CODE: ERROR_CODE.UNKNOWN_RESOURCE,
|
|
29
|
+
TEXT: `No event handler registered for ${eventName}`,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
}
|
|
33
|
+
: resourceNotMappedNack(eventName));
|
|
25
34
|
return;
|
|
26
35
|
}
|
|
27
36
|
try {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { Store } from '../db/store.ts';
|
|
2
2
|
import type { Connection } from '../protocol/connection.ts';
|
|
3
|
-
import type { GenesisMessage, MockServerConfig } from '../types.ts';
|
|
3
|
+
import type { GenesisMessage, MockServerConfig, Row } from '../types.ts';
|
|
4
|
+
export declare function pagingOptions(details: Row): {
|
|
5
|
+
maxRows: number;
|
|
6
|
+
maxView: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function pendingKey(rowRef: unknown): string | undefined;
|
|
4
9
|
export declare function handleDataLogon(message: GenesisMessage, connection: Connection, config: MockServerConfig, store: Store): void;
|
|
5
|
-
export declare function
|
|
10
|
+
export declare function handleMoreRows(message: GenesisMessage, connection: Connection, config: MockServerConfig): void;
|
|
11
|
+
export declare function handleDataLogoff(message: GenesisMessage, connection: Connection, config: MockServerConfig): void;
|
|
@@ -1,20 +1,88 @@
|
|
|
1
1
|
import { compileCriteria, filterByCriteria } from "../db/criteria.js";
|
|
2
|
+
import { isLegacyFidelity } from "../protocol/fidelity.js";
|
|
2
3
|
import { MESSAGE_TYPE, ERROR_CODE } from "../protocol/messageTypes.js";
|
|
4
|
+
import { nonExistentSubscriptionNack, resourceNotMappedNack, subscriptionAlreadyExistsNack, } from "../protocol/msgNack.js";
|
|
3
5
|
import { stampRow } from "../protocol/rowUpdate.js";
|
|
4
6
|
import { applyResourceAuth, requestingUser } from "./resourceAuth.js";
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// The real dataserver's defaults (genesis-pal-dataserver ClientOptions,
|
|
8
|
+
// GSF 8.15.x) — also what foundation-comms' DatasourceDefaults send.
|
|
9
|
+
const DEFAULT_MAX_ROWS = 250;
|
|
10
|
+
const DEFAULT_MAX_VIEW = 1000;
|
|
11
|
+
function readCount(value, fallback) {
|
|
12
|
+
const count = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
13
|
+
return typeof count === 'number' && Number.isInteger(count) && count >= 0 ? count : fallback;
|
|
14
|
+
}
|
|
15
|
+
// MAX_ROWS is the page size, MAX_VIEW the most rows the client's view may
|
|
16
|
+
// hold across all pages. 0 means "no limit" for either, and MAX_ROWS is
|
|
17
|
+
// clamped to MAX_VIEW — both exactly as ClientOptions.parse does.
|
|
18
|
+
export function pagingOptions(details) {
|
|
19
|
+
const maxView = readCount(details.MAX_VIEW, DEFAULT_MAX_VIEW) || Infinity;
|
|
20
|
+
const maxRows = readCount(details.MAX_ROWS, DEFAULT_MAX_ROWS) || Infinity;
|
|
21
|
+
return { maxRows: Math.min(maxRows, maxView), maxView };
|
|
22
|
+
}
|
|
23
|
+
// The key a row is tracked under in Subscription.pending / held — undefined
|
|
24
|
+
// for a row without a ROW_REF, which no live change could ever be matched
|
|
25
|
+
// against (and which the client can't identify either).
|
|
26
|
+
export function pendingKey(rowRef) {
|
|
27
|
+
return rowRef === undefined || rowRef === null ? undefined : String(rowRef);
|
|
28
|
+
}
|
|
29
|
+
function buildPending(rows) {
|
|
30
|
+
const pending = new Map();
|
|
31
|
+
rows.forEach((row, index) => {
|
|
32
|
+
const key = pendingKey(row.DETAILS?.ROW_REF);
|
|
33
|
+
// A row with no ROW_REF (or a duplicate one) still has to be delivered;
|
|
34
|
+
// park it under a key no ROW_REF can produce rather than dropping it.
|
|
35
|
+
// Live changes can't reach it there, which is as much as the client
|
|
36
|
+
// could make of them: to it, rows sharing a ROW_REF are one row.
|
|
37
|
+
pending.set(key === undefined || pending.has(key) ? `\u0000${index}` : key, row);
|
|
38
|
+
});
|
|
39
|
+
return pending;
|
|
40
|
+
}
|
|
41
|
+
// The next page: up to MAX_ROWS waiting rows, but never more than the view
|
|
42
|
+
// has room for. The real NextRowsStrategy reads while the client index is
|
|
43
|
+
// below MAX_VIEW, so a full view gets an empty page, and a slot freed by a
|
|
44
|
+
// DELETE is filled by the next row waiting. Delivered rows move to `held`.
|
|
45
|
+
function takePage(subscription) {
|
|
46
|
+
const { pending, held } = subscription;
|
|
47
|
+
if (!pending)
|
|
48
|
+
return [];
|
|
49
|
+
const room = Math.max(0, Math.min(subscription.maxRows ?? Infinity, (subscription.maxView ?? Infinity) - (held?.size ?? 0)));
|
|
50
|
+
const page = [];
|
|
51
|
+
for (const [key, row] of pending) {
|
|
52
|
+
if (page.length >= room)
|
|
53
|
+
break;
|
|
54
|
+
page.push(row);
|
|
55
|
+
pending.delete(key);
|
|
56
|
+
held?.add(key);
|
|
57
|
+
}
|
|
58
|
+
return page;
|
|
59
|
+
}
|
|
60
|
+
// DATA_LOGON opens a persistent subscription keyed by SOURCE_REF: reply with
|
|
61
|
+
// LOGON_ACK and the first page of the snapshot, then keep the SOURCE_REF
|
|
62
|
+
// registered so MORE_ROWS can page the rest and later inserts/amends/deletes
|
|
63
|
+
// on this resource are pushed on the same channel (see server.ts broadcast())
|
|
64
|
+
// until the client sends DATA_LOGOFF.
|
|
9
65
|
export function handleDataLogon(message, connection, config, store) {
|
|
66
|
+
const legacy = isLegacyFidelity(config);
|
|
10
67
|
const details = message.DETAILS ?? {};
|
|
11
68
|
const resourceName = details.DATASOURCE_NAME;
|
|
12
69
|
const query = config.queries?.[resourceName];
|
|
13
70
|
if (!query) {
|
|
14
|
-
connection.reply(message,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
71
|
+
connection.reply(message, legacy
|
|
72
|
+
? {
|
|
73
|
+
MESSAGE_TYPE: MESSAGE_TYPE.QUERY_UPDATE,
|
|
74
|
+
ERROR: [
|
|
75
|
+
{ CODE: ERROR_CODE.UNKNOWN_RESOURCE, TEXT: `Unknown datasource: ${resourceName}` },
|
|
76
|
+
],
|
|
77
|
+
}
|
|
78
|
+
: resourceNotMappedNack(resourceName));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const sourceRef = message.SOURCE_REF;
|
|
82
|
+
// The router refuses a second logon on a live SOURCE_REF and leaves the
|
|
83
|
+
// first subscription running. (Legacy mode silently replaced it.)
|
|
84
|
+
if (!legacy && connection.subscriptions.has(sourceRef)) {
|
|
85
|
+
connection.reply(message, subscriptionAlreadyExistsNack());
|
|
18
86
|
return;
|
|
19
87
|
}
|
|
20
88
|
const criteria = details.CRITERIA_MATCH;
|
|
@@ -22,9 +90,16 @@ export function handleDataLogon(message, connection, config, store) {
|
|
|
22
90
|
// Order matters: the query's own filter (the where-clause equivalent) and
|
|
23
91
|
// the user's row permissioning narrow the set BEFORE the client's
|
|
24
92
|
// CRITERIA_MATCH; hidden fields are stripped last, off the final rows.
|
|
93
|
+
// Paging applies after all of them.
|
|
25
94
|
let rows = store.getResourceRows(query.source);
|
|
26
95
|
if (query.filter)
|
|
27
96
|
rows = rows.filter(query.filter);
|
|
97
|
+
// ROWS_COUNT is the real dataserver's index count: it ignores
|
|
98
|
+
// CRITERIA_MATCH and the user's rowFilter (recorded: ROWS_COUNT 10 with 6
|
|
99
|
+
// rows matching), which grid-pro and the AI assistant already allow for.
|
|
100
|
+
// The query's own filter is the where clause the index is built over, so
|
|
101
|
+
// the count is taken after it — an assumption, as no recording covers it.
|
|
102
|
+
const rowsCount = rows.length;
|
|
28
103
|
rows = filterByCriteria(rows, criteria);
|
|
29
104
|
// Permissioning is applied per row so the ROW_REF can still be computed from
|
|
30
105
|
// the UNSTRIPPED row — pk fields may themselves be hidden from this user,
|
|
@@ -37,20 +112,96 @@ export function handleDataLogon(message, connection, config, store) {
|
|
|
37
112
|
continue;
|
|
38
113
|
stampedRows.push(stampRow(permitted, store.getRowRef(query.source, row), 'INSERT'));
|
|
39
114
|
}
|
|
40
|
-
|
|
115
|
+
const subscription = {
|
|
41
116
|
resourceName,
|
|
42
117
|
getRowRef: (row) => store.getRowRef(query.source, row),
|
|
43
118
|
predicate: compileCriteria(criteria),
|
|
44
119
|
sequenceId: 1,
|
|
45
|
-
}
|
|
46
|
-
|
|
120
|
+
};
|
|
121
|
+
if (legacy) {
|
|
122
|
+
connection.subscribe(sourceRef, subscription);
|
|
123
|
+
connection.reply(message, {
|
|
124
|
+
MESSAGE_TYPE: MESSAGE_TYPE.QUERY_UPDATE,
|
|
125
|
+
ROW: stampedRows,
|
|
126
|
+
ROWS_COUNT: stampedRows.length,
|
|
127
|
+
SEQUENCE_ID: 1,
|
|
128
|
+
MORE_ROWS: false,
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Every matched row starts out waiting; the first page moves up to
|
|
133
|
+
// MAX_ROWS of them into `held`. Rows the view has no room for (past
|
|
134
|
+
// MAX_VIEW) keep waiting, so MORE_ROWS stays true — as on the real server,
|
|
135
|
+
// which then answers every MORE_ROWS with an empty {MORE_ROWS: true} page.
|
|
136
|
+
const { maxRows, maxView } = pagingOptions(details);
|
|
137
|
+
const tracked = {
|
|
138
|
+
...subscription,
|
|
139
|
+
pending: buildPending(stampedRows),
|
|
140
|
+
held: new Set(),
|
|
141
|
+
maxRows,
|
|
142
|
+
maxView,
|
|
143
|
+
};
|
|
144
|
+
const firstPage = takePage(tracked);
|
|
145
|
+
const pending = tracked.pending;
|
|
146
|
+
connection.subscribe(sourceRef, tracked);
|
|
147
|
+
connection.reply(message, { MESSAGE_TYPE: MESSAGE_TYPE.LOGON_ACK, SEQUENCE_ID: 0 });
|
|
148
|
+
// No matching rows: the real dataserver sends neither ROW nor ROWS_COUNT.
|
|
149
|
+
connection.reply(message, firstPage.length === 0
|
|
150
|
+
? { MESSAGE_TYPE: MESSAGE_TYPE.QUERY_UPDATE, MORE_ROWS: false, SEQUENCE_ID: 1 }
|
|
151
|
+
: {
|
|
152
|
+
MESSAGE_TYPE: MESSAGE_TYPE.QUERY_UPDATE,
|
|
153
|
+
ROW: firstPage,
|
|
154
|
+
MORE_ROWS: pending.size > 0,
|
|
155
|
+
// Only on this first page (see rowsCount above).
|
|
156
|
+
ROWS_COUNT: rowsCount,
|
|
157
|
+
SEQUENCE_ID: 1,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// MORE_ROWS names its subscription in DETAILS.SOURCE_REF (foundation-comms'
|
|
161
|
+
// createMoreRowsMessage gives the request a SOURCE_REF of its own) or, when
|
|
162
|
+
// that is absent, in its own SOURCE_REF. The page goes out as a QUERY_UPDATE
|
|
163
|
+
// on the subscription's SOURCE_REF without ROWS_COUNT; MORE_ROWS_ACK answers
|
|
164
|
+
// the request's own SOURCE_REF, and only when DETAILS.SOURCE_REF was given —
|
|
165
|
+
// both as the real ConnectionManager.processMoreRows does. The ack goes first
|
|
166
|
+
// because the real server writes it while its dataserver is still reading the
|
|
167
|
+
// page. Subscriptions are per connection, so another socket can never page
|
|
168
|
+
// this one: its lookup misses and it gets the 404.
|
|
169
|
+
export function handleMoreRows(message, connection, config) {
|
|
170
|
+
if (isLegacyFidelity(config)) {
|
|
171
|
+
// Up to 15.47 MORE_ROWS was unrouted: logged, never answered.
|
|
172
|
+
console.warn(`[mock-server] Unhandled MESSAGE_TYPE: ${message.MESSAGE_TYPE}`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const detailsSourceRef = message.DETAILS?.SOURCE_REF;
|
|
176
|
+
const streamSourceRef = detailsSourceRef ?? message.SOURCE_REF;
|
|
177
|
+
const subscription = streamSourceRef === undefined ? undefined : connection.subscriptions.get(streamSourceRef);
|
|
178
|
+
if (!subscription) {
|
|
179
|
+
connection.reply(message, nonExistentSubscriptionNack());
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (detailsSourceRef !== undefined) {
|
|
183
|
+
connection.reply(message, { MESSAGE_TYPE: MESSAGE_TYPE.MORE_ROWS_ACK });
|
|
184
|
+
}
|
|
185
|
+
const page = takePage(subscription);
|
|
186
|
+
subscription.sequenceId += 1;
|
|
187
|
+
// No rows to send still gets an answer — an empty QUERY_UPDATE, as the real
|
|
188
|
+
// QueryClient sends when a read finds nothing: MORE_ROWS: false at the end
|
|
189
|
+
// of the data, true while rows are waiting for room in a full view.
|
|
190
|
+
connection.send({
|
|
47
191
|
MESSAGE_TYPE: MESSAGE_TYPE.QUERY_UPDATE,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
192
|
+
SOURCE_REF: streamSourceRef,
|
|
193
|
+
...(page.length > 0 ? { ROW: page } : {}),
|
|
194
|
+
MORE_ROWS: (subscription.pending?.size ?? 0) > 0,
|
|
195
|
+
SEQUENCE_ID: subscription.sequenceId,
|
|
52
196
|
});
|
|
53
197
|
}
|
|
54
|
-
export function handleDataLogoff(message, connection) {
|
|
55
|
-
|
|
198
|
+
export function handleDataLogoff(message, connection, config) {
|
|
199
|
+
const sourceRef = message.SOURCE_REF;
|
|
200
|
+
const subscribed = sourceRef !== undefined && connection.subscriptions.has(sourceRef);
|
|
201
|
+
if (subscribed)
|
|
202
|
+
connection.unsubscribe(sourceRef);
|
|
203
|
+
// Legacy mode never answered a logoff.
|
|
204
|
+
if (isLegacyFidelity(config))
|
|
205
|
+
return;
|
|
206
|
+
connection.reply(message, subscribed ? { MESSAGE_TYPE: MESSAGE_TYPE.LOGOFF_ACK } : nonExistentSubscriptionNack());
|
|
56
207
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { deriveFields } from "../db/metadata.js";
|
|
2
|
-
import {
|
|
2
|
+
import { isLegacyFidelity } from "../protocol/fidelity.js";
|
|
3
|
+
import { ERROR_CODE, MESSAGE_TYPE } from "../protocol/messageTypes.js";
|
|
4
|
+
import { resourceNotMappedNack } from "../protocol/msgNack.js";
|
|
3
5
|
// JSON_SCHEMA_REQUEST shares its request envelope with META_REQUEST (same
|
|
4
6
|
// DETAILS.FEATURE resource-name field) but its reply is a completely
|
|
5
7
|
// different shape: Connect.getJSONSchema doesn't read response.DETAILS at
|
|
@@ -10,7 +12,9 @@ import { ERROR_CODE } from "../protocol/messageTypes.js";
|
|
|
10
12
|
// (it reads Object.keys(...properties) to know which fields to copy from the
|
|
11
13
|
// selected row into the commit DETAILS). Route this to handleMetaRequest by
|
|
12
14
|
// mistake and forms silently render with zero fields, and delete can never
|
|
13
|
-
// build a payload — no error, just a dead button.
|
|
15
|
+
// build a payload — no error, just a dead button. The real success reply is
|
|
16
|
+
// JSON_SCHEMA_FEATURE_ACK with INBOUND/OUTBOUND at the top level (legacy mode
|
|
17
|
+
// echoed JSON_SCHEMA_REQUEST).
|
|
14
18
|
function jsonSchemaType(fieldType) {
|
|
15
19
|
switch (fieldType) {
|
|
16
20
|
case 'LONG':
|
|
@@ -73,11 +77,14 @@ export function handleJsonSchemaRequest(message, connection, config, store) {
|
|
|
73
77
|
const table = override?.table ??
|
|
74
78
|
resourceSource ??
|
|
75
79
|
inferEventTable(eventName, Object.keys(config.tables ?? {}));
|
|
80
|
+
const legacy = isLegacyFidelity(config);
|
|
76
81
|
if (!table) {
|
|
77
|
-
connection.reply(message,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
connection.reply(message, legacy
|
|
83
|
+
? {
|
|
84
|
+
MESSAGE_TYPE: message.MESSAGE_TYPE,
|
|
85
|
+
ERROR: [{ CODE: ERROR_CODE.UNKNOWN_RESOURCE, TEXT: `Unknown resource: ${eventName}` }],
|
|
86
|
+
}
|
|
87
|
+
: resourceNotMappedNack(eventName));
|
|
81
88
|
return;
|
|
82
89
|
}
|
|
83
90
|
const pkOnly = override?.pkOnly ?? DELETE_EVENT_RE.test(eventName);
|
|
@@ -87,7 +94,7 @@ export function handleJsonSchemaRequest(message, connection, config, store) {
|
|
|
87
94
|
fields = fields.filter((field) => pkFields.includes(field.NAME));
|
|
88
95
|
const schema = buildSchema(fields, pkOnly ? pkFields : []);
|
|
89
96
|
connection.reply(message, {
|
|
90
|
-
MESSAGE_TYPE: message.MESSAGE_TYPE,
|
|
97
|
+
MESSAGE_TYPE: legacy ? message.MESSAGE_TYPE : MESSAGE_TYPE.JSON_SCHEMA_FEATURE_ACK,
|
|
91
98
|
INBOUND: schema,
|
|
92
99
|
OUTBOUND: schema,
|
|
93
100
|
});
|
package/dist/handlers/meta.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { deriveFields } from "../db/metadata.js";
|
|
2
|
-
import {
|
|
2
|
+
import { isLegacyFidelity } from "../protocol/fidelity.js";
|
|
3
|
+
import { ERROR_CODE, MESSAGE_TYPE } from "../protocol/messageTypes.js";
|
|
4
|
+
import { resourceNotMappedNack } from "../protocol/msgNack.js";
|
|
3
5
|
import { hiddenFields, requestingUser } from "./resourceAuth.js";
|
|
4
6
|
// createMetaRequestMessage (foundation-comms/src/connect/message.ts) puts the
|
|
5
7
|
// resource name in DETAILS.FEATURE — not DATASOURCE_NAME/RESOURCE_NAME despite
|
|
@@ -12,15 +14,22 @@ function resolveResourceName(message) {
|
|
|
12
14
|
// DATASERVER resources need DETAILS.FIELD, REQUEST_SERVER ones need
|
|
13
15
|
// DETAILS.REPLY_FIELD. Get either wrong and every grid silently reports
|
|
14
16
|
// "Metadata Fetch failed" / "Unsupported resource type: undefined".
|
|
17
|
+
// The real reply is FEATURE_ACK (getMetadata reads only DETAILS, so the type
|
|
18
|
+
// is for fidelity; legacy mode echoed META_REQUEST).
|
|
15
19
|
export function handleMetaRequest(message, connection, config, store) {
|
|
16
20
|
const resourceName = resolveResourceName(message);
|
|
17
21
|
const query = config.queries?.[resourceName];
|
|
18
22
|
const requestReply = config.requestReplies?.[resourceName];
|
|
23
|
+
const legacy = isLegacyFidelity(config);
|
|
19
24
|
if (!query && !requestReply) {
|
|
20
|
-
connection.reply(message,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
connection.reply(message, legacy
|
|
26
|
+
? {
|
|
27
|
+
MESSAGE_TYPE: message.MESSAGE_TYPE,
|
|
28
|
+
ERROR: [
|
|
29
|
+
{ CODE: ERROR_CODE.UNKNOWN_RESOURCE, TEXT: `Unknown resource: ${resourceName}` },
|
|
30
|
+
],
|
|
31
|
+
}
|
|
32
|
+
: resourceNotMappedNack(resourceName));
|
|
24
33
|
return;
|
|
25
34
|
}
|
|
26
35
|
const resolved = (query ?? requestReply);
|
|
@@ -34,7 +43,7 @@ export function handleMetaRequest(message, connection, config, store) {
|
|
|
34
43
|
const hidden = hiddenFields(resolved.auth, requestingUser(connection));
|
|
35
44
|
const fields = deriveFields(rows, resolved.fieldTypes).filter((field) => !hidden.has(field.NAME));
|
|
36
45
|
connection.reply(message, {
|
|
37
|
-
MESSAGE_TYPE: message.MESSAGE_TYPE,
|
|
46
|
+
MESSAGE_TYPE: legacy ? message.MESSAGE_TYPE : MESSAGE_TYPE.FEATURE_ACK,
|
|
38
47
|
DETAILS: query
|
|
39
48
|
? { NAME: resourceName, TYPE: 'DATASERVER', FIELD: fields, KEY: [] }
|
|
40
49
|
: { NAME: resourceName, TYPE: 'REQUEST_SERVER', REPLY_FIELD: fields },
|