@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/src/server.ts
CHANGED
|
@@ -3,11 +3,11 @@ import { WebSocketServer, type WebSocket } from 'ws';
|
|
|
3
3
|
import { Store } from './db/store.ts';
|
|
4
4
|
import { pendingKey } from './handlers/dataLogon.ts';
|
|
5
5
|
import { createHttpRequestListener } from './handlers/http.ts';
|
|
6
|
-
import {
|
|
6
|
+
import { permittedQueryRow, requestingUser } from './handlers/resourceAuth.ts';
|
|
7
7
|
import { Connection, type Subscription } from './protocol/connection.ts';
|
|
8
8
|
import { isLegacyFidelity } from './protocol/fidelity.ts';
|
|
9
9
|
import { routeMessage } from './protocol/router.ts';
|
|
10
|
-
import { stampRow, type RowOperation } from './protocol/rowUpdate.ts';
|
|
10
|
+
import { dataserverRow, stampRow, type RowOperation } from './protocol/rowUpdate.ts';
|
|
11
11
|
import type {
|
|
12
12
|
BroadcastFn,
|
|
13
13
|
BroadcastTableChangeFn,
|
|
@@ -141,9 +141,10 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
141
141
|
const permitted =
|
|
142
142
|
operation === 'DELETE'
|
|
143
143
|
? undefined
|
|
144
|
-
:
|
|
144
|
+
: permittedQueryRow(row, query?.auth, requestingUser(connection), subscription.fieldNames);
|
|
145
145
|
if (pending.has(key)) {
|
|
146
|
-
if (permitted)
|
|
146
|
+
if (permitted)
|
|
147
|
+
pending.set(key, stampRow(dataserverRow(permitted, row, query?.fields), rowRef, 'INSERT'));
|
|
147
148
|
else pending.delete(key);
|
|
148
149
|
return null;
|
|
149
150
|
}
|
|
@@ -153,11 +154,22 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
153
154
|
return 'DELETE';
|
|
154
155
|
}
|
|
155
156
|
if (!permitted) return null;
|
|
157
|
+
// Under ORDER_BY, a new row that sorts among the waiting ones waits too,
|
|
158
|
+
// so MORE_ROWS pages stay in order.
|
|
159
|
+
if (
|
|
160
|
+
subscription.queueInsert?.(
|
|
161
|
+
key,
|
|
162
|
+
stampRow(dataserverRow(permitted, row, query?.fields), rowRef, 'INSERT'),
|
|
163
|
+
row,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
return null;
|
|
167
|
+
|
|
156
168
|
held.add(key);
|
|
157
169
|
return 'INSERT';
|
|
158
170
|
}
|
|
159
171
|
|
|
160
|
-
|
|
172
|
+
function pushChange(resourceName: string, operation: RowOperation, row: Row): void {
|
|
161
173
|
const query = config.queries?.[resourceName];
|
|
162
174
|
// The query's own filter (where-clause equivalent) applies to live pushes
|
|
163
175
|
// too — a row the snapshot would exclude must not leak in via broadcast.
|
|
@@ -188,19 +200,27 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
188
200
|
? trackClientView(subscription, connection, query, resolved, row, rowRef)
|
|
189
201
|
: resolved;
|
|
190
202
|
if (rowOperation === null) continue;
|
|
203
|
+
const legacy = isLegacyFidelity(config);
|
|
191
204
|
let outRow = row;
|
|
192
205
|
if (rowOperation === 'DELETE') {
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
if (
|
|
206
|
+
// GSF's DELETE row is DETAILS {OPERATION, ROW_REF} and nothing else.
|
|
207
|
+
// Legacy mode sent the pk fields: the stub it was given for a real
|
|
208
|
+
// DELETE, or one rebuilt from the key for a change that left the
|
|
209
|
+
// subscription (the full row may carry columns hideFields strips).
|
|
210
|
+
if (!legacy) outRow = {};
|
|
211
|
+
else if (operation !== 'DELETE') {
|
|
198
212
|
outRow = query ? store.rowStubFromKey(query.source, rowRef as string) : row;
|
|
199
213
|
}
|
|
200
|
-
} else
|
|
201
|
-
|
|
214
|
+
} else {
|
|
215
|
+
// Only the subscription's columns, minus the ones this user can't see.
|
|
216
|
+
const permitted = permittedQueryRow(
|
|
217
|
+
row,
|
|
218
|
+
query?.auth,
|
|
219
|
+
requestingUser(connection),
|
|
220
|
+
subscription.fieldNames,
|
|
221
|
+
);
|
|
202
222
|
if (!permitted) continue;
|
|
203
|
-
outRow = permitted;
|
|
223
|
+
outRow = dataserverRow(permitted, row, query?.fields);
|
|
204
224
|
}
|
|
205
225
|
subscription.sequenceId += 1;
|
|
206
226
|
// A live push carries neither MORE_ROWS nor ROWS_COUNT on the real
|
|
@@ -208,12 +228,33 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
208
228
|
connection.send({
|
|
209
229
|
MESSAGE_TYPE: 'QUERY_UPDATE',
|
|
210
230
|
SOURCE_REF: sourceRef,
|
|
211
|
-
ROW: [stampRow(outRow, rowRef, rowOperation)],
|
|
212
|
-
...(
|
|
231
|
+
ROW: [stampRow(outRow, rowRef, rowOperation, legacy)],
|
|
232
|
+
...(legacy ? { ROWS_COUNT: 1 } : {}),
|
|
213
233
|
SEQUENCE_ID: subscription.sequenceId,
|
|
214
234
|
});
|
|
215
235
|
}
|
|
216
236
|
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Pushes an unsolicited row update to every subscription on the resource
|
|
240
|
+
// (see pushChange). A DELETE given as a pk-only row is resolved to the
|
|
241
|
+
// record(s) removed from that key that no broadcast has announced yet —
|
|
242
|
+
// their RECORD_IDs are the ROW_REFs a client may hold — and those are
|
|
243
|
+
// consumed, so a later delete of a re-inserted key names its own record.
|
|
244
|
+
const broadcast: BroadcastFn = (resourceName, operation, row) => {
|
|
245
|
+
const source = config.queries?.[resourceName]?.source;
|
|
246
|
+
if (operation !== 'DELETE' || !source) {
|
|
247
|
+
pushChange(resourceName, operation, row);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const removed = store.takeRemovedRecordsFor(source, row);
|
|
251
|
+
if (isLegacyFidelity(config)) {
|
|
252
|
+
pushChange(resourceName, operation, row);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const records = row.RECORD_ID === undefined || row.RECORD_ID === null ? removed : [row];
|
|
256
|
+
if (records.length === 0) records.push(row);
|
|
257
|
+
for (const record of records) pushChange(resourceName, 'DELETE', record);
|
|
217
258
|
};
|
|
218
259
|
|
|
219
260
|
// Event handlers mutate one table (e.g. TRADE) but grids may be subscribed to
|
|
@@ -221,8 +262,26 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
221
262
|
// query resource backed by that table — directly or via a view — and pushes
|
|
222
263
|
// the (re-joined, for views) row to each one's subscribers. `pk` is the
|
|
223
264
|
// store key: the single pk field's value, or store.rowKey(...) for
|
|
224
|
-
// composite-keyed tables.
|
|
265
|
+
// composite-keyed tables. ROW_REF is the record's RECORD_ID, so a DELETE is
|
|
266
|
+
// broadcast with every record the key lost that no broadcast announced yet
|
|
267
|
+
// (Store.takeRemovedRows; held-rows tracking drops the ones a client
|
|
268
|
+
// doesn't hold), and a record replaced under the same key (deleted, then
|
|
269
|
+
// re-inserted, before one broadcast) gets a DELETE of its old ROW_REF before
|
|
270
|
+
// the new one goes out. A view row is built from the changed base row
|
|
271
|
+
// alone, so it is found whatever the view's own pkField; a MODIFY that takes
|
|
272
|
+
// it out of the view (an anti-join) is pushed as a DELETE. Legacy ROW_REFs
|
|
273
|
+
// are the key itself, so there a DELETE is the pk stub and a replacement is
|
|
274
|
+
// the one INSERT/MODIFY, as before.
|
|
225
275
|
const broadcastTableChange: BroadcastTableChangeFn = (tableName, operation, pk) => {
|
|
276
|
+
const legacy = isLegacyFidelity(config);
|
|
277
|
+
const current = operation === 'DELETE' ? undefined : store.getRow(tableName, pk);
|
|
278
|
+
let removed = store
|
|
279
|
+
.takeRemovedRows(tableName, pk)
|
|
280
|
+
.filter((record) => record.RECORD_ID !== current?.RECORD_ID);
|
|
281
|
+
// A DELETE broadcast before the row is removed still names it.
|
|
282
|
+
const live = store.getRow(tableName, pk);
|
|
283
|
+
if (operation === 'DELETE' && removed.length === 0 && live) removed = [live];
|
|
284
|
+
if (legacy) removed = [];
|
|
226
285
|
for (const [queryName, query] of Object.entries(config.queries ?? {})) {
|
|
227
286
|
const isDirect = query.source === tableName;
|
|
228
287
|
const view = store.views.get(query.source);
|
|
@@ -242,23 +301,38 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
242
301
|
|
|
243
302
|
if (isViewJoiningTable) {
|
|
244
303
|
for (const viewRow of store.getViewRows(query.source)) {
|
|
245
|
-
|
|
304
|
+
pushChange(queryName, 'MODIFY', viewRow);
|
|
246
305
|
}
|
|
247
306
|
continue;
|
|
248
307
|
}
|
|
249
308
|
|
|
250
|
-
|
|
309
|
+
// The removed base records carry the RECORD_ID a view row's ROW_REF
|
|
310
|
+
// comes from too; the DELETE body is DETAILS only either way.
|
|
311
|
+
for (const record of removed) pushChange(queryName, 'DELETE', record);
|
|
251
312
|
if (operation === 'DELETE') {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
313
|
+
if (legacy) {
|
|
314
|
+
const stub = store.rowStubFromKey(query.source, pk);
|
|
315
|
+
if (Object.keys(stub).length > 0) pushChange(queryName, 'DELETE', stub);
|
|
316
|
+
}
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (!current) continue;
|
|
320
|
+
if (isDirect) {
|
|
321
|
+
pushChange(queryName, operation, current);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const viewRow = store.getViewRows(query.source, { [tableName]: [current] })[0];
|
|
325
|
+
if (viewRow) {
|
|
326
|
+
pushChange(queryName, operation, viewRow);
|
|
327
|
+
} else if (operation === 'MODIFY') {
|
|
328
|
+
pushChange(
|
|
329
|
+
queryName,
|
|
330
|
+
'DELETE',
|
|
331
|
+
legacy
|
|
332
|
+
? store.rowStubFromKey(query.source, store.getRowRef(query.source, current) as string)
|
|
333
|
+
: current,
|
|
334
|
+
);
|
|
260
335
|
}
|
|
261
|
-
if (row) broadcast(queryName, operation, row);
|
|
262
336
|
}
|
|
263
337
|
};
|
|
264
338
|
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
6
6
|
import type { Store } from './db/store.ts';
|
|
7
|
+
import type { NackErrorInput } from './protocol/errors.ts';
|
|
7
8
|
|
|
8
9
|
export type Row = Record<string, any>;
|
|
9
10
|
|
|
@@ -14,6 +15,76 @@ export interface GenesisMessage {
|
|
|
14
15
|
[key: string]: any;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
// The Genesis dictionary field types (global.genesis.dictionary.Field.Type).
|
|
19
|
+
export type GenesisFieldType =
|
|
20
|
+
| 'STRING'
|
|
21
|
+
| 'ENUM'
|
|
22
|
+
| 'INT'
|
|
23
|
+
| 'SHORT'
|
|
24
|
+
| 'LONG'
|
|
25
|
+
| 'DOUBLE'
|
|
26
|
+
| 'BIGDECIMAL'
|
|
27
|
+
| 'BOOLEAN'
|
|
28
|
+
| 'DATE'
|
|
29
|
+
| 'DATETIME'
|
|
30
|
+
| 'NANO_TIMESTAMP'
|
|
31
|
+
| 'RAW';
|
|
32
|
+
|
|
33
|
+
// One column of a table, as the project's tables dictionary declares it
|
|
34
|
+
// (`field("NAME", STRING(100)).notNull()`). META_REQUEST and
|
|
35
|
+
// JSON_SCHEMA_REQUEST are built from these when a table declares them; see the
|
|
36
|
+
// README's "Declared schema".
|
|
37
|
+
export interface FieldDef {
|
|
38
|
+
name: string;
|
|
39
|
+
type: GenesisFieldType;
|
|
40
|
+
// Default true, as in the dictionary (`.notNull()` makes it false). Primary
|
|
41
|
+
// key and generated fields default to false.
|
|
42
|
+
nullable?: boolean;
|
|
43
|
+
// ENUM values, in dictionary order.
|
|
44
|
+
values?: string[];
|
|
45
|
+
// The dictionary `.default(...)`. A non-null field with a default is
|
|
46
|
+
// optional in event schemas.
|
|
47
|
+
default?: unknown;
|
|
48
|
+
// STRING column size (`STRING(100)`): MAX_LENGTH / maxLength.
|
|
49
|
+
maxSize?: number;
|
|
50
|
+
// `metadata { minLength = 0 }`: MIN_LENGTH / minLength.
|
|
51
|
+
minLength?: number;
|
|
52
|
+
// Replaces the DESCRIPTION / description the real server fills with the
|
|
53
|
+
// field's JVM class (kotlin.String, org.joda.time.DateTime, ...). Leave it
|
|
54
|
+
// unset for DATE/DATETIME: foundation-forms recognises dates partly by that
|
|
55
|
+
// class name.
|
|
56
|
+
description?: string;
|
|
57
|
+
// Defaults to the name in title case ('COUNTERPARTY_ID' -> 'Counterparty Id').
|
|
58
|
+
title?: string;
|
|
59
|
+
// Whether an event payload may leave it out (OPTIONAL, and not in the JSON
|
|
60
|
+
// schema's `required`). The real server makes a field optional only when
|
|
61
|
+
// its class property has a default. Unset: a table field is optional when
|
|
62
|
+
// it is nullable, generated or has a default (the generated DAO defaults
|
|
63
|
+
// all of those); a custom DTO field only when it has a `default` — set
|
|
64
|
+
// `optional: true` for a Kotlin default the metadata can't show, such as
|
|
65
|
+
// `val enabled: Boolean? = true`.
|
|
66
|
+
optional?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// A field the database fills on insert. Declares the shape only: metadata
|
|
70
|
+
// marks it NULLABLE:false, OPTIONAL:true and leaves it out of `required`.
|
|
71
|
+
// Value generation still comes from `sequencePrefix` (single-field keys).
|
|
72
|
+
// A table with `sequencePrefix` gets its key marked as a SEQUENCE without
|
|
73
|
+
// declaring it here.
|
|
74
|
+
export interface GeneratedFieldDef {
|
|
75
|
+
field: string;
|
|
76
|
+
kind: 'SEQUENCE' | 'AUTO_INCREMENT';
|
|
77
|
+
prefix?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// A table or dataserver-query index (`indices { unique("A") }`). FIELDS is
|
|
81
|
+
// sent space-separated (`'A B'`).
|
|
82
|
+
export interface IndexDef {
|
|
83
|
+
name: string;
|
|
84
|
+
fields: string[];
|
|
85
|
+
unique?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
17
88
|
export interface TableDef {
|
|
18
89
|
// A single field, or several for composite-keyed tables (e.g. PRICE keyed
|
|
19
90
|
// by [RFQ_ID, SIDE]). Composite rows are identified by the fields' values
|
|
@@ -23,9 +94,28 @@ export interface TableDef {
|
|
|
23
94
|
pkField: string | string[];
|
|
24
95
|
sequencePrefix?: string;
|
|
25
96
|
sequenceWidth?: number;
|
|
97
|
+
// How generated pk values look. See SequenceFormat.
|
|
98
|
+
sequenceFormat?: SequenceFormat;
|
|
26
99
|
rows?: Row[];
|
|
100
|
+
// Declared columns, in dictionary order. When present they are the schema:
|
|
101
|
+
// metadata, JSON schemas and dataserver rows follow them (missing values go
|
|
102
|
+
// out as null), even with no rows at all. When absent, field metadata is
|
|
103
|
+
// inferred from the rows (src/db/metadata.ts).
|
|
104
|
+
fields?: FieldDef[];
|
|
105
|
+
generated?: GeneratedFieldDef[];
|
|
106
|
+
// Table indices. Declared for later items (ORDER_BY, duplicate keys); a
|
|
107
|
+
// dataserver query only reports the indices on its own QueryDef.indexes, as
|
|
108
|
+
// the real dataserver does.
|
|
109
|
+
indexes?: IndexDef[];
|
|
27
110
|
}
|
|
28
111
|
|
|
112
|
+
// 'prefix' (the default): sequencePrefix + a counter padded to sequenceWidth
|
|
113
|
+
// ('TR001'). 'genesis': the platform's id format (genesis-db IdStrategy.java)
|
|
114
|
+
// — the counter padded to 15 digits, the sequence id (sequencePrefix, two
|
|
115
|
+
// characters in a GSF dictionary), then location 'LO' and system '1':
|
|
116
|
+
// '000000000000001TRLO1'.
|
|
117
|
+
export type SequenceFormat = 'prefix' | 'genesis';
|
|
118
|
+
|
|
29
119
|
export interface JoinDef {
|
|
30
120
|
table: string;
|
|
31
121
|
on: [string, string];
|
|
@@ -53,6 +143,16 @@ export interface ViewDef {
|
|
|
53
143
|
joins?: JoinDef[];
|
|
54
144
|
antiJoins?: AntiJoinDef[];
|
|
55
145
|
derived?: Record<string, (row: Row) => any>;
|
|
146
|
+
// Types of the `derived` fields (`derivedField("TOTAL", DOUBLE)`): a type,
|
|
147
|
+
// or a full field definition without its name. Columns from the base table
|
|
148
|
+
// and from `joins[].select` take their declared types from their source
|
|
149
|
+
// tables; a derived field with no entry here is inferred from the rows.
|
|
150
|
+
derivedTypes?: Record<string, GenesisFieldType | Omit<FieldDef, 'name'>>;
|
|
151
|
+
// The view's fields { } block: the only columns it exposes, in this order
|
|
152
|
+
// (base, joined and derived alike). Unset: every base column, then each
|
|
153
|
+
// join's select, then the derived fields. Keep the key fields in it — they
|
|
154
|
+
// are the ROW_REF.
|
|
155
|
+
fields?: string[];
|
|
56
156
|
}
|
|
57
157
|
|
|
58
158
|
// The authenticated identity a resource-level auth hook sees. userName is
|
|
@@ -76,7 +176,17 @@ export interface ResourceAuthDef {
|
|
|
76
176
|
|
|
77
177
|
export interface QueryDef {
|
|
78
178
|
source: string;
|
|
179
|
+
// Overrides the TYPE of named fields, declared or inferred.
|
|
79
180
|
fieldTypes?: Record<string, string>;
|
|
181
|
+
// The query's `fields { }` block: the only columns it exposes, in this
|
|
182
|
+
// order, in rows and in META_REQUEST. Names the source doesn't have are
|
|
183
|
+
// dropped from metadata. RECORD_ID and TIMESTAMP reach dataserver rows only
|
|
184
|
+
// when named here.
|
|
185
|
+
|
|
186
|
+
fields?: string[];
|
|
187
|
+
// The query's `indices { }` block, reported as META_REQUEST INDEXES. With
|
|
188
|
+
// none, INDEXES is the default [{NAME: null, FIELDS: 'RECORD_ID'}].
|
|
189
|
+
indexes?: IndexDef[];
|
|
80
190
|
// Server-side where-clause equivalent, applied before any client
|
|
81
191
|
// CRITERIA_MATCH — to both the DATA_LOGON snapshot and live pushes. Use it
|
|
82
192
|
// for things the real query's filter/where clause does (e.g. keep template
|
|
@@ -110,12 +220,35 @@ export interface RequestReplyDef {
|
|
|
110
220
|
fieldTypes?: Record<string, string>;
|
|
111
221
|
filter?: (row: Row) => boolean;
|
|
112
222
|
auth?: ResourceAuthDef;
|
|
223
|
+
// `criteriaOnlyRequest = true` in the request server's config: META_REQUEST
|
|
224
|
+
// then reports REQUEST_FIELD: [], CRITERIA_ONLY_REQUEST: true,
|
|
225
|
+
// SORTABLE_FIELDS and CRITERIA_FIELDS, and adds RECORD_ID and TIMESTAMP to
|
|
226
|
+
// REPLY_FIELD. Metadata only for now: OFFSET paging is not implemented, and
|
|
227
|
+
// grid-pro's server-side and infinite row models page a criteria-only
|
|
228
|
+
// resource with OFFSET.
|
|
229
|
+
criteriaOnly?: boolean;
|
|
230
|
+
// The request block's fields (REQUEST_FIELD, and the JSON schema's REQUEST).
|
|
231
|
+
// Defaults to the source's primary key, as for a GPAL requestReply(TABLE).
|
|
232
|
+
requestFields?: string[];
|
|
113
233
|
}
|
|
114
234
|
|
|
235
|
+
// How META_REQUEST / JSON_SCHEMA_REQUEST describe an event whose name doesn't
|
|
236
|
+
// follow EVENT_<TABLE>_<VERB>: the table (or view) its DETAILS take, and
|
|
237
|
+
// whether only the key fields are sent (a delete).
|
|
115
238
|
export interface EventSchemaOverride {
|
|
116
239
|
table?: string;
|
|
117
240
|
pkOnly?: boolean;
|
|
118
241
|
fieldTypes?: Record<string, string>;
|
|
242
|
+
// The event's own DETAILS class, for an event that takes a custom DTO
|
|
243
|
+
// (`eventHandler<MyInput>`) rather than a table row. Described as the real
|
|
244
|
+
// server describes a non-table class: no titles, lengths, ranges or
|
|
245
|
+
// genesis types; `nullable`, `default` and `optional` as declared. A
|
|
246
|
+
// nullable field is still mandatory unless it has a default or
|
|
247
|
+
// `optional: true` (Kotlin `val x: T?` must be sent; `val x: T? = null`
|
|
248
|
+
// needn't be). `table` then only names where its ENUM columns come from.
|
|
249
|
+
fields?: FieldDef[];
|
|
250
|
+
// The DTO's class name, reported as the event's DESCRIPTION.
|
|
251
|
+
className?: string;
|
|
119
252
|
}
|
|
120
253
|
|
|
121
254
|
export interface ResourceItem {
|
|
@@ -181,7 +314,11 @@ export interface EventHandlerCtx {
|
|
|
181
314
|
broadcastTableChange: BroadcastTableChangeFn;
|
|
182
315
|
}
|
|
183
316
|
|
|
184
|
-
|
|
317
|
+
// `warnings` turns the reply into an EVENT_NACK carrying them in WARNING
|
|
318
|
+
// (with an empty ERROR), as a GSF handler's warning-only EventNack does.
|
|
319
|
+
// Return them from a handler that has not written anything yet: the mock has
|
|
320
|
+
// no transaction to roll back, and doesn't honour IGNORE_WARNINGS yet.
|
|
321
|
+
export type EventHandlerResult = { generated?: Row[]; warnings?: NackErrorInput[] } | void;
|
|
185
322
|
|
|
186
323
|
export type EventHandler = (details: Row, ctx: EventHandlerCtx) => EventHandlerResult;
|
|
187
324
|
|
|
@@ -245,11 +382,18 @@ export interface MockServerConfig {
|
|
|
245
382
|
httpRoutes?: HttpRouteDef[];
|
|
246
383
|
onMessage?: OnMessageHook;
|
|
247
384
|
productName?: string;
|
|
385
|
+
// The Genesis application name, as in the generated enum classes the real
|
|
386
|
+
// server names in event metadata
|
|
387
|
+
// (global.genesis.gen.dao.enums.<appName>.<table>.<Field>). Unset: the
|
|
388
|
+
// segment is left out. Nothing a client does depends on it.
|
|
389
|
+
appName?: string;
|
|
248
390
|
}
|
|
249
391
|
|
|
392
|
+
// A META_REQUEST FIELD / REQUEST_FIELD / REPLY_FIELD entry.
|
|
250
393
|
export interface FieldMetadata {
|
|
251
394
|
NAME: string;
|
|
252
395
|
TYPE: string;
|
|
396
|
+
VALID_VALUES?: string[];
|
|
253
397
|
}
|
|
254
398
|
|
|
255
399
|
// Minimal shape both `ws`'s WebSocket and Bun's ServerWebSocket satisfy —
|