@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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { filterByCriteria } from "../db/criteria.js";
|
|
2
2
|
import { NackError } from "../protocol/errors.js";
|
|
3
|
+
import { isLegacyFidelity } from "../protocol/fidelity.js";
|
|
3
4
|
import { ERROR_CODE } from "../protocol/messageTypes.js";
|
|
5
|
+
import { resourceNotMappedNack } from "../protocol/msgNack.js";
|
|
4
6
|
import { applyResourceAuth, requestingUser } from "./resourceAuth.js";
|
|
5
7
|
// Keyed lookups: foundation-comms' createRequestMessage spreads RequestParams
|
|
6
8
|
// onto the message, so REQUEST is a *top-level* field ({FIELD: value} pairs
|
|
@@ -20,15 +22,17 @@ export function handleRequestReply(message, connection, config, store) {
|
|
|
20
22
|
const resourceName = message.MESSAGE_TYPE.replace(/^REQ_/, '');
|
|
21
23
|
const requestReply = config.requestReplies?.[resourceName];
|
|
22
24
|
if (!requestReply || (!requestReply.resolver && !requestReply.source)) {
|
|
23
|
-
connection.reply(message,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
connection.reply(message, isLegacyFidelity(config)
|
|
26
|
+
? {
|
|
27
|
+
MESSAGE_TYPE: `REP_${resourceName}`,
|
|
28
|
+
ERROR: [
|
|
29
|
+
{
|
|
30
|
+
CODE: ERROR_CODE.UNKNOWN_RESOURCE,
|
|
31
|
+
TEXT: `Unknown request-reply resource: ${resourceName}`,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
}
|
|
35
|
+
: resourceNotMappedNack(resourceName));
|
|
32
36
|
return;
|
|
33
37
|
}
|
|
34
38
|
const user = requestingUser(connection);
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export { launchStack, type LaunchStackOptions, type LaunchedStack } from './laun
|
|
|
3
3
|
export { Store } from './db/store.ts';
|
|
4
4
|
export { NackError, type NackErrorInput, type NackErrorEntry } from './protocol/errors.ts';
|
|
5
5
|
export { compileCriteria, filterByCriteria, type Predicate } from './db/criteria.ts';
|
|
6
|
-
export type { AntiJoinDef, AuthConfig, AuthUser, BroadcastFn, BroadcastTableChangeFn, EventHandler, EventHandlerCtx, EventHandlerResult, EventSchemaOverride, FieldMetadata, GenesisMessage, HttpRouteDef, JoinDef, MockServerConfig, OnMessageCtx, OnMessageHook, QueryDef, RequestingUser, RequestReplyDef, RequestReplyResolverCtx, ResourceAuthDef, ResourceItem, Row, SocketLike, SsoIdentityProvider, TableDef, ViewDef, } from './types.ts';
|
|
6
|
+
export type { AntiJoinDef, AuthConfig, AuthUser, BroadcastFn, BroadcastTableChangeFn, EventHandler, EventHandlerCtx, EventHandlerResult, EventSchemaOverride, FidelityMode, FieldMetadata, GenesisMessage, HttpRouteDef, JoinDef, MockServerConfig, OnMessageCtx, OnMessageHook, QueryDef, RequestingUser, RequestReplyDef, RequestReplyResolverCtx, ResourceAuthDef, ResourceItem, Row, SocketLike, SsoIdentityProvider, TableDef, ViewDef, } from './types.ts';
|
|
@@ -5,6 +5,10 @@ export interface Subscription {
|
|
|
5
5
|
getRowRef: (row: Row) => unknown;
|
|
6
6
|
predicate: Predicate;
|
|
7
7
|
sequenceId: number;
|
|
8
|
+
pending?: Map<string, Row>;
|
|
9
|
+
held?: Set<string>;
|
|
10
|
+
maxRows?: number;
|
|
11
|
+
maxView?: number;
|
|
8
12
|
}
|
|
9
13
|
export declare class Connection {
|
|
10
14
|
readonly ws: SocketLike;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// True when the config opts back into the envelopes this engine sent up to
|
|
2
|
+
// 15.47 (see FidelityMode in types.ts). Read per message, like every other
|
|
3
|
+
// config field, so flipping it on a running server takes effect immediately.
|
|
4
|
+
export function isLegacyFidelity(config) {
|
|
5
|
+
return config.fidelity === 'legacy';
|
|
6
|
+
}
|
|
@@ -10,10 +10,17 @@ export declare const MESSAGE_TYPE: {
|
|
|
10
10
|
readonly HEARTBEAT_PING: "HEARTBEAT_PING";
|
|
11
11
|
readonly HEARTBEAT_PONG: "HEARTBEAT_PONG";
|
|
12
12
|
readonly DATA_LOGON: "DATA_LOGON";
|
|
13
|
+
readonly LOGON_ACK: "LOGON_ACK";
|
|
13
14
|
readonly DATA_LOGOFF: "DATA_LOGOFF";
|
|
15
|
+
readonly LOGOFF_ACK: "LOGOFF_ACK";
|
|
14
16
|
readonly QUERY_UPDATE: "QUERY_UPDATE";
|
|
17
|
+
readonly MORE_ROWS: "MORE_ROWS";
|
|
18
|
+
readonly MORE_ROWS_ACK: "MORE_ROWS_ACK";
|
|
15
19
|
readonly META_REQUEST: "META_REQUEST";
|
|
20
|
+
readonly FEATURE_ACK: "FEATURE_ACK";
|
|
16
21
|
readonly JSON_SCHEMA_REQUEST: "JSON_SCHEMA_REQUEST";
|
|
22
|
+
readonly JSON_SCHEMA_FEATURE_ACK: "JSON_SCHEMA_FEATURE_ACK";
|
|
23
|
+
readonly MSG_NACK: "MSG_NACK";
|
|
17
24
|
readonly RESOURCES_REQUEST: "RESOURCES_REQUEST";
|
|
18
25
|
readonly RESOURCES_REQUEST_ACK: "RESOURCES_REQUEST_ACK";
|
|
19
26
|
readonly RESOURCES_REQUEST_NACK: "RESOURCES_REQUEST_NACK";
|
|
@@ -17,10 +17,19 @@ export const MESSAGE_TYPE = {
|
|
|
17
17
|
HEARTBEAT_PING: 'HEARTBEAT_PING',
|
|
18
18
|
HEARTBEAT_PONG: 'HEARTBEAT_PONG',
|
|
19
19
|
DATA_LOGON: 'DATA_LOGON',
|
|
20
|
+
LOGON_ACK: 'LOGON_ACK',
|
|
20
21
|
DATA_LOGOFF: 'DATA_LOGOFF',
|
|
22
|
+
LOGOFF_ACK: 'LOGOFF_ACK',
|
|
21
23
|
QUERY_UPDATE: 'QUERY_UPDATE',
|
|
24
|
+
MORE_ROWS: 'MORE_ROWS',
|
|
25
|
+
MORE_ROWS_ACK: 'MORE_ROWS_ACK',
|
|
22
26
|
META_REQUEST: 'META_REQUEST',
|
|
27
|
+
FEATURE_ACK: 'FEATURE_ACK',
|
|
23
28
|
JSON_SCHEMA_REQUEST: 'JSON_SCHEMA_REQUEST',
|
|
29
|
+
JSON_SCHEMA_FEATURE_ACK: 'JSON_SCHEMA_FEATURE_ACK',
|
|
30
|
+
// The router's generic rejection (unknown resource, missing or duplicate
|
|
31
|
+
// dataserver subscription) — see protocol/msgNack.ts.
|
|
32
|
+
MSG_NACK: 'MSG_NACK',
|
|
24
33
|
RESOURCES_REQUEST: 'RESOURCES_REQUEST',
|
|
25
34
|
RESOURCES_REQUEST_ACK: 'RESOURCES_REQUEST_ACK',
|
|
26
35
|
RESOURCES_REQUEST_NACK: 'RESOURCES_REQUEST_NACK',
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { MESSAGE_TYPE } from "./messageTypes.js";
|
|
2
|
+
const HTTP_NOT_FOUND = 404;
|
|
3
|
+
const HTTP_CONFLICT = 409;
|
|
4
|
+
// The real router's rejection frame (genesis-router MsgType.Outbound.Error,
|
|
5
|
+
// GSF 8.15.x): an integer HTTP status CODE, and the status line as
|
|
6
|
+
// STATUS_CODE, repeated under DETAILS. It is a different family from the
|
|
7
|
+
// handler-level EVENT_NACK/REP_ errors (string CODEs such as
|
|
8
|
+
// VALIDATION_ERROR) — the router sends it before a request ever reaches a
|
|
9
|
+
// service. foundation-comms' socket REJECTS the pending promise on MSG_NACK
|
|
10
|
+
// (connect/socket.ts), where a typed reply carrying ERROR resolves it.
|
|
11
|
+
function routerNack(code, reason, text) {
|
|
12
|
+
const STATUS_CODE = `${code} ${reason}`;
|
|
13
|
+
return {
|
|
14
|
+
MESSAGE_TYPE: MESSAGE_TYPE.MSG_NACK,
|
|
15
|
+
ERROR: [{ CODE: code, TEXT: text, STATUS_CODE }],
|
|
16
|
+
DETAILS: { ERROR: text, STATUS_CODE },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// The router resolves every message to a resource name first and answers this
|
|
20
|
+
// when no service serves it: DETAILS.DATASOURCE_NAME for DATA_LOGON, the name
|
|
21
|
+
// after REQ_ for request servers, DETAILS.FEATURE for META_REQUEST /
|
|
22
|
+
// JSON_SCHEMA_REQUEST, and the MESSAGE_TYPE itself for commit events. A
|
|
23
|
+
// missing name is the empty string, as on the real router.
|
|
24
|
+
export function resourceNotMappedNack(resourceName) {
|
|
25
|
+
return routerNack(HTTP_NOT_FOUND, 'Not Found', `Resource ${resourceName ?? ''} is not mapped to a service`);
|
|
26
|
+
}
|
|
27
|
+
// MORE_ROWS or DATA_LOGOFF naming a SOURCE_REF this connection has no live
|
|
28
|
+
// subscription on (never logged on, already logged off, or another socket's).
|
|
29
|
+
export function nonExistentSubscriptionNack() {
|
|
30
|
+
return routerNack(HTTP_NOT_FOUND, 'Not Found', 'Dataserver subscription does not exist');
|
|
31
|
+
}
|
|
32
|
+
// DATA_LOGON on a SOURCE_REF that already has a live subscription on this
|
|
33
|
+
// connection. The existing subscription is left untouched.
|
|
34
|
+
export function subscriptionAlreadyExistsNack() {
|
|
35
|
+
return routerNack(HTTP_CONFLICT, 'Conflict', 'Subscription exists for source ref. If using HTTP, try HTTP GET instead of HTTP POST');
|
|
36
|
+
}
|
package/dist/protocol/router.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { handleLogin, handleLogout } from "../handlers/auth.js";
|
|
2
2
|
import { handleCommitEvent } from "../handlers/commitEvent.js";
|
|
3
|
-
import { handleDataLogon, handleDataLogoff } from "../handlers/dataLogon.js";
|
|
3
|
+
import { handleDataLogon, handleDataLogoff, handleMoreRows } from "../handlers/dataLogon.js";
|
|
4
4
|
import { handleHeartbeat } from "../handlers/heartbeat.js";
|
|
5
5
|
import { handleJsonSchemaRequest } from "../handlers/jsonSchema.js";
|
|
6
6
|
import { handleMetaRequest } from "../handlers/meta.js";
|
|
7
7
|
import { handleRequestReply } from "../handlers/requestReply.js";
|
|
8
8
|
import { handleResourcesRequest } from "../handlers/resources.js";
|
|
9
|
+
import { isLegacyFidelity } from "./fidelity.js";
|
|
9
10
|
import { MESSAGE_TYPE, ERROR_CODE, isCommitEvent, isRequestReply } from "./messageTypes.js";
|
|
10
11
|
// Message types a connection may send before authenticating when the OPT-IN
|
|
11
12
|
// auth gate (config.auth.requireAuth = true) is enabled. Everything else
|
|
@@ -13,7 +14,11 @@ import { MESSAGE_TYPE, ERROR_CODE, isCommitEvent, isRequestReply } from "./messa
|
|
|
13
14
|
// requires the connection to have logged in over WS — or to present a
|
|
14
15
|
// SESSION_AUTH_TOKEN this server issued (over either transport).
|
|
15
16
|
// RESOURCES_REQUEST is allowed pre-auth so resource discovery can't deadlock
|
|
16
|
-
// a client that fetches it before (or while) logging in.
|
|
17
|
+
// a client that fetches it before (or while) logging in. MORE_ROWS, like
|
|
18
|
+
// DATA_LOGOFF, can only act on a subscription the connection already opened
|
|
19
|
+
// through the gate (logout clears them), so it answers with its own precise
|
|
20
|
+
// 404 rather than a NOT_LOGGED_ON echo — except under fidelity: 'legacy',
|
|
21
|
+
// which keeps the 15.47 gate (see isPreAuth).
|
|
17
22
|
//
|
|
18
23
|
// The gate is OFF by default because real clients can't be assumed to
|
|
19
24
|
// satisfy it: browser frame captures show that after logging in over
|
|
@@ -29,6 +34,11 @@ const PRE_AUTH_MESSAGE_TYPES = new Set([
|
|
|
29
34
|
MESSAGE_TYPE.DATA_LOGOFF,
|
|
30
35
|
MESSAGE_TYPE.RESOURCES_REQUEST,
|
|
31
36
|
]);
|
|
37
|
+
function isPreAuth(messageType, config) {
|
|
38
|
+
if (messageType === MESSAGE_TYPE.MORE_ROWS)
|
|
39
|
+
return !isLegacyFidelity(config);
|
|
40
|
+
return PRE_AUTH_MESSAGE_TYPES.has(messageType);
|
|
41
|
+
}
|
|
32
42
|
function ensureAuthenticated(message, connection, tokens) {
|
|
33
43
|
if (connection.authenticated)
|
|
34
44
|
return true;
|
|
@@ -69,9 +79,7 @@ export function routeMessage(message, connection, config, store, ctx, tokens = n
|
|
|
69
79
|
// away; the gate itself only applies to data traffic.
|
|
70
80
|
ensureAuthenticated(message, connection, tokens);
|
|
71
81
|
const requireAuth = config.auth?.requireAuth ?? false;
|
|
72
|
-
if (requireAuth &&
|
|
73
|
-
!PRE_AUTH_MESSAGE_TYPES.has(message.MESSAGE_TYPE) &&
|
|
74
|
-
!connection.authenticated) {
|
|
82
|
+
if (requireAuth && !isPreAuth(message.MESSAGE_TYPE, config) && !connection.authenticated) {
|
|
75
83
|
return replyNotLoggedOn(message, connection);
|
|
76
84
|
}
|
|
77
85
|
switch (message.MESSAGE_TYPE) {
|
|
@@ -84,7 +92,9 @@ export function routeMessage(message, connection, config, store, ctx, tokens = n
|
|
|
84
92
|
case MESSAGE_TYPE.DATA_LOGON:
|
|
85
93
|
return handleDataLogon(message, connection, config, store);
|
|
86
94
|
case MESSAGE_TYPE.DATA_LOGOFF:
|
|
87
|
-
return handleDataLogoff(message, connection);
|
|
95
|
+
return handleDataLogoff(message, connection, config);
|
|
96
|
+
case MESSAGE_TYPE.MORE_ROWS:
|
|
97
|
+
return handleMoreRows(message, connection, config);
|
|
88
98
|
case MESSAGE_TYPE.META_REQUEST:
|
|
89
99
|
return handleMetaRequest(message, connection, config, store);
|
|
90
100
|
case MESSAGE_TYPE.JSON_SCHEMA_REQUEST:
|
package/dist/server.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { WebSocketServer } from 'ws';
|
|
3
3
|
import { Store } from "./db/store.js";
|
|
4
|
+
import { pendingKey } from "./handlers/dataLogon.js";
|
|
4
5
|
import { createHttpRequestListener } from "./handlers/http.js";
|
|
5
6
|
import { applyResourceAuth, requestingUser } from "./handlers/resourceAuth.js";
|
|
6
7
|
import { Connection } from "./protocol/connection.js";
|
|
8
|
+
import { isLegacyFidelity } from "./protocol/fidelity.js";
|
|
7
9
|
import { routeMessage } from "./protocol/router.js";
|
|
8
10
|
import { stampRow } from "./protocol/rowUpdate.js";
|
|
9
11
|
export function createMockServer(config) {
|
|
@@ -21,8 +23,9 @@ export function createMockServer(config) {
|
|
|
21
23
|
// grids reflect inserts/amends/deletes without the client re-querying.
|
|
22
24
|
// DELETE rows are PK-only stubs (see broadcastTableChange), so a criteria
|
|
23
25
|
// predicate would evaluate against missing fields and drop the push —
|
|
24
|
-
// leaving the client holding a ghost row.
|
|
25
|
-
//
|
|
26
|
+
// leaving the client holding a ghost row. A DELETE therefore skips the
|
|
27
|
+
// predicate; which subscriptions it reaches is decided by what each client
|
|
28
|
+
// holds (trackClientView), or, in legacy mode, it reaches all of them.
|
|
26
29
|
// 'error' is kept distinct from 'no-match' because only a clean no-match may
|
|
27
30
|
// be turned into a DELETE below; a broken predicate must not evict rows.
|
|
28
31
|
function evaluateSubscription(subscription, sourceRef, row) {
|
|
@@ -49,6 +52,53 @@ export function createMockServer(config) {
|
|
|
49
52
|
return null;
|
|
50
53
|
return operation === 'MODIFY' ? 'DELETE' : null;
|
|
51
54
|
}
|
|
55
|
+
// Decides what one 'gsf' subscription's client gets for a live change, from
|
|
56
|
+
// what that client holds — as the real dataserver does with its client
|
|
57
|
+
// index — and keeps the subscription's pending/held sets up to date.
|
|
58
|
+
// `operation` is the change after the query filter and CRITERIA_MATCH
|
|
59
|
+
// (a MODIFY that left them arrives as a DELETE); the user's rowFilter is
|
|
60
|
+
// applied here too, so leaving it counts as leaving the subscription.
|
|
61
|
+
// - A row still waiting for MORE_ROWS (past the page, or past MAX_VIEW) is
|
|
62
|
+
// never pushed: the client has never seen it. A change replaces the
|
|
63
|
+
// waiting copy in place, so the page later delivers current values; a
|
|
64
|
+
// DELETE drops it.
|
|
65
|
+
// - A held row gets the MODIFY, or a DELETE when it is gone or out of scope.
|
|
66
|
+
// - A row the client doesn't hold that is (now) in scope is pushed as an
|
|
67
|
+
// INSERT and becomes held: a brand-new row, or one a MODIFY brought back
|
|
68
|
+
// into the criteria. Out of scope, the client is told nothing.
|
|
69
|
+
// An INSERT is pushed even when the view already holds MAX_VIEW rows; the
|
|
70
|
+
// real server would also evict the oldest row in the same frame (moving
|
|
71
|
+
// view), which this engine doesn't, so the view can grow past MAX_VIEW.
|
|
72
|
+
// A row without a ROW_REF can't be matched to anything the client holds,
|
|
73
|
+
// so changes to it aren't pushed.
|
|
74
|
+
// Returns the operation to push, or null to push nothing.
|
|
75
|
+
function trackClientView(subscription, connection, query, operation, row, rowRef) {
|
|
76
|
+
const key = pendingKey(rowRef);
|
|
77
|
+
const pending = subscription.pending;
|
|
78
|
+
const held = subscription.held;
|
|
79
|
+
if (key === undefined || !pending || !held)
|
|
80
|
+
return null;
|
|
81
|
+
const permitted = operation === 'DELETE'
|
|
82
|
+
? undefined
|
|
83
|
+
: applyResourceAuth([row], query?.auth, requestingUser(connection))[0];
|
|
84
|
+
if (pending.has(key)) {
|
|
85
|
+
if (permitted)
|
|
86
|
+
pending.set(key, stampRow(permitted, rowRef, 'INSERT'));
|
|
87
|
+
else
|
|
88
|
+
pending.delete(key);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (held.has(key)) {
|
|
92
|
+
if (permitted)
|
|
93
|
+
return 'MODIFY';
|
|
94
|
+
held.delete(key);
|
|
95
|
+
return 'DELETE';
|
|
96
|
+
}
|
|
97
|
+
if (!permitted)
|
|
98
|
+
return null;
|
|
99
|
+
held.add(key);
|
|
100
|
+
return 'INSERT';
|
|
101
|
+
}
|
|
52
102
|
const broadcast = (resourceName, operation, row) => {
|
|
53
103
|
const query = config.queries?.[resourceName];
|
|
54
104
|
// The query's own filter (where-clause equivalent) applies to live pushes
|
|
@@ -57,7 +107,6 @@ export function createMockServer(config) {
|
|
|
57
107
|
// rather than dropped: a swallowed removal leaves the client rendering the
|
|
58
108
|
// row with its stale pre-modify values until a full reload. An INSERT
|
|
59
109
|
// outside the filter is simply dropped (the client never held it).
|
|
60
|
-
// DELETEs always go through: dropping an unknown ROW_REF is a no-op.
|
|
61
110
|
let effectiveOperation = operation;
|
|
62
111
|
if (operation !== 'DELETE' && query?.filter && !query.filter(row)) {
|
|
63
112
|
if (operation !== 'MODIFY')
|
|
@@ -68,32 +117,42 @@ export function createMockServer(config) {
|
|
|
68
117
|
for (const [sourceRef, subscription] of connection.subscriptions) {
|
|
69
118
|
if (subscription.resourceName !== resourceName)
|
|
70
119
|
continue;
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
120
|
+
const resolved = resolveSubscriptionOperation(subscription, sourceRef, effectiveOperation, row);
|
|
121
|
+
if (resolved === null)
|
|
73
122
|
continue;
|
|
74
123
|
// Per-user permissioning (auth maps / hideFields equivalent): the
|
|
75
124
|
// ROW_REF is computed from the unstripped row, since pk fields may
|
|
76
125
|
// themselves be hidden from this user.
|
|
77
126
|
const rowRef = subscription.getRowRef(row);
|
|
127
|
+
const rowOperation = subscription.held
|
|
128
|
+
? trackClientView(subscription, connection, query, resolved, row, rowRef)
|
|
129
|
+
: resolved;
|
|
130
|
+
if (rowOperation === null)
|
|
131
|
+
continue;
|
|
78
132
|
let outRow = row;
|
|
79
|
-
if (rowOperation
|
|
80
|
-
//
|
|
81
|
-
// client needs to drop the row, and the full row may carry
|
|
82
|
-
// this user's hideFields strips.
|
|
83
|
-
|
|
133
|
+
if (rowOperation === 'DELETE') {
|
|
134
|
+
// A change turned into a DELETE: send the row identity only — it is
|
|
135
|
+
// all the client needs to drop the row, and the full row may carry
|
|
136
|
+
// columns this user's hideFields strips. (A real DELETE's row is a
|
|
137
|
+
// stub already.)
|
|
138
|
+
if (operation !== 'DELETE') {
|
|
139
|
+
outRow = query ? store.rowStubFromKey(query.source, rowRef) : row;
|
|
140
|
+
}
|
|
84
141
|
}
|
|
85
|
-
else if (
|
|
142
|
+
else if (query?.auth) {
|
|
86
143
|
const [permitted] = applyResourceAuth([row], query.auth, requestingUser(connection));
|
|
87
144
|
if (!permitted)
|
|
88
145
|
continue;
|
|
89
146
|
outRow = permitted;
|
|
90
147
|
}
|
|
91
148
|
subscription.sequenceId += 1;
|
|
149
|
+
// A live push carries neither MORE_ROWS nor ROWS_COUNT on the real
|
|
150
|
+
// server — those describe the snapshot. Legacy mode sent ROWS_COUNT: 1.
|
|
92
151
|
connection.send({
|
|
93
152
|
MESSAGE_TYPE: 'QUERY_UPDATE',
|
|
94
153
|
SOURCE_REF: sourceRef,
|
|
95
154
|
ROW: [stampRow(outRow, rowRef, rowOperation)],
|
|
96
|
-
ROWS_COUNT: 1,
|
|
155
|
+
...(isLegacyFidelity(config) ? { ROWS_COUNT: 1 } : {}),
|
|
97
156
|
SEQUENCE_ID: subscription.sequenceId,
|
|
98
157
|
});
|
|
99
158
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -118,7 +118,9 @@ export interface OnMessageCtx {
|
|
|
118
118
|
broadcastTableChange: BroadcastTableChangeFn;
|
|
119
119
|
}
|
|
120
120
|
export type OnMessageHook = (message: GenesisMessage, ctx: OnMessageCtx) => GenesisMessage | false | void;
|
|
121
|
+
export type FidelityMode = 'gsf' | 'legacy';
|
|
121
122
|
export interface MockServerConfig {
|
|
123
|
+
fidelity?: FidelityMode;
|
|
122
124
|
tables?: Record<string, TableDef>;
|
|
123
125
|
views?: Record<string, ViewDef>;
|
|
124
126
|
queries?: Record<string, QueryDef>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genesislcap/mock-server",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.50.0-BACE-0.1",
|
|
4
4
|
"description": "Reusable in-memory Genesis WebSocket protocol mock server for client automation/CI, without a JVM or database.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"build:watch": "tsc -p tsconfig.build.json --watch",
|
|
23
23
|
"lint": "echo \"@genesislcap/mock-server: no lint step\"",
|
|
24
24
|
"typecheck": "tsc --noEmit",
|
|
25
|
-
"test": "node --experimental-strip-types --test"
|
|
25
|
+
"test": "node --experimental-strip-types --test \"test/**/*.test.ts\"",
|
|
26
|
+
"test:conformance": "node --experimental-strip-types --test \"test/conformance/**/*.test.ts\"",
|
|
27
|
+
"conformance:record": "node --experimental-strip-types test/conformance/record.ts"
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
28
30
|
"ws": "^8.18.0"
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Store } from '../db/store.ts';
|
|
2
2
|
import type { Connection } from '../protocol/connection.ts';
|
|
3
3
|
import { NackError } from '../protocol/errors.ts';
|
|
4
|
+
import { isLegacyFidelity } from '../protocol/fidelity.ts';
|
|
4
5
|
import { ERROR_CODE, MESSAGE_TYPE } from '../protocol/messageTypes.ts';
|
|
6
|
+
import { resourceNotMappedNack } from '../protocol/msgNack.ts';
|
|
5
7
|
import type {
|
|
6
8
|
BroadcastFn,
|
|
7
9
|
BroadcastTableChangeFn,
|
|
@@ -36,13 +38,23 @@ export function handleCommitEvent(
|
|
|
36
38
|
// real consumers (foundation-comms' kv.service, entity-management) match
|
|
37
39
|
// these names exactly, so a name-suffixed <EVENT>_ACK reads as failure and
|
|
38
40
|
// a <EVENT>_NACK passes as silent success.
|
|
41
|
+
// An event nothing handles never reaches an event handler on the real
|
|
42
|
+
// server: the router rejects it by name with MSG_NACK 404.
|
|
39
43
|
if (!handler) {
|
|
40
|
-
connection.reply(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
connection.reply(
|
|
45
|
+
message,
|
|
46
|
+
isLegacyFidelity(config)
|
|
47
|
+
? {
|
|
48
|
+
MESSAGE_TYPE: MESSAGE_TYPE.EVENT_NACK,
|
|
49
|
+
ERROR: [
|
|
50
|
+
{
|
|
51
|
+
CODE: ERROR_CODE.UNKNOWN_RESOURCE,
|
|
52
|
+
TEXT: `No event handler registered for ${eventName}`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
: resourceNotMappedNack(eventName),
|
|
57
|
+
);
|
|
46
58
|
return;
|
|
47
59
|
}
|
|
48
60
|
|