@genesislcap/mock-server 15.51.1 → 15.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +408 -39
  2. package/dist/db/criteria.d.ts +13 -0
  3. package/dist/db/criteria.js +60 -21
  4. package/dist/db/identity.d.ts +11 -0
  5. package/dist/db/identity.js +46 -0
  6. package/dist/db/ordering.d.ts +8 -0
  7. package/dist/db/ordering.js +49 -0
  8. package/dist/db/schema.d.ts +62 -0
  9. package/dist/db/schema.js +490 -0
  10. package/dist/db/store.d.ts +20 -3
  11. package/dist/db/store.js +300 -50
  12. package/dist/handlers/commitEvent.js +47 -9
  13. package/dist/handlers/crudEvents.d.ts +2 -0
  14. package/dist/handlers/crudEvents.js +138 -0
  15. package/dist/handlers/dataLogon.js +105 -6
  16. package/dist/handlers/eventValidation.d.ts +5 -0
  17. package/dist/handlers/eventValidation.js +29 -0
  18. package/dist/handlers/jsonSchema.d.ts +4 -2
  19. package/dist/handlers/jsonSchema.js +222 -61
  20. package/dist/handlers/meta.d.ts +6 -1
  21. package/dist/handlers/meta.js +168 -21
  22. package/dist/handlers/requestReply.js +519 -30
  23. package/dist/handlers/resourceAuth.d.ts +1 -0
  24. package/dist/handlers/resourceAuth.js +14 -0
  25. package/dist/handlers/resources.js +3 -0
  26. package/dist/index.d.ts +3 -2
  27. package/dist/index.js +2 -1
  28. package/dist/protocol/connection.d.ts +2 -0
  29. package/dist/protocol/errors.d.ts +100 -2
  30. package/dist/protocol/errors.js +300 -12
  31. package/dist/protocol/fieldTypes.d.ts +10 -0
  32. package/dist/protocol/fieldTypes.js +107 -0
  33. package/dist/protocol/gsfSchemas.d.ts +9 -0
  34. package/dist/protocol/gsfSchemas.js +423 -0
  35. package/dist/protocol/messageTypes.d.ts +1 -0
  36. package/dist/protocol/messageTypes.js +1 -0
  37. package/dist/protocol/msgNack.d.ts +2 -0
  38. package/dist/protocol/msgNack.js +10 -0
  39. package/dist/protocol/rowUpdate.d.ts +5 -3
  40. package/dist/protocol/rowUpdate.js +33 -15
  41. package/dist/protocol/schemaValidation.d.ts +11 -0
  42. package/dist/protocol/schemaValidation.js +186 -0
  43. package/dist/server.js +95 -29
  44. package/dist/types.d.ts +45 -0
  45. package/package.json +1 -1
  46. package/src/db/criteria.ts +72 -21
  47. package/src/db/identity.ts +53 -0
  48. package/src/db/ordering.ts +56 -0
  49. package/src/db/schema.ts +672 -0
  50. package/src/db/store.ts +343 -50
  51. package/src/handlers/commitEvent.ts +54 -10
  52. package/src/handlers/crudEvents.ts +181 -0
  53. package/src/handlers/dataLogon.ts +127 -7
  54. package/src/handlers/eventValidation.ts +43 -0
  55. package/src/handlers/jsonSchema.ts +302 -69
  56. package/src/handlers/meta.ts +227 -23
  57. package/src/handlers/requestReply.ts +626 -33
  58. package/src/handlers/resourceAuth.ts +18 -0
  59. package/src/handlers/resources.ts +3 -0
  60. package/src/index.ts +17 -1
  61. package/src/protocol/connection.ts +10 -2
  62. package/src/protocol/errors.ts +361 -13
  63. package/src/protocol/fieldTypes.ts +115 -0
  64. package/src/protocol/gsfSchemas.ts +505 -0
  65. package/src/protocol/messageTypes.ts +1 -0
  66. package/src/protocol/msgNack.ts +12 -0
  67. package/src/protocol/rowUpdate.ts +40 -20
  68. package/src/protocol/schemaValidation.ts +208 -0
  69. package/src/server.ts +106 -28
  70. package/src/types.ts +187 -5
@@ -0,0 +1,186 @@
1
+ // Inbound schema validation, as GSF runs it on every event before any handler
2
+ // sees it: GenesisMessageDecoder validates the whole message against the
3
+ // event's JSON schema (the INBOUND that JSON_SCHEMA_REQUEST serves) and
4
+ // answers each failure with a FieldError VALIDATION_ERROR. GSF 8.15.29 uses
5
+ // networknt json-schema-validator 2.0.1, configured by
6
+ // GenesisJsonSchemaFactory (draft 2019-09, PathType.LEGACY,
7
+ // losslessNarrowing). This is the subset of that validator the engine's own
8
+ // event schemas need: type, const, enum, oneOf, properties, required,
9
+ // additionalProperties, minLength / maxLength, minimum / maximum and pattern.
10
+ // Texts, locations and their order follow networknt's output, checked by
11
+ // running the real validator on the recorded showcase schemas
12
+ // (test/schemaValidation.test.ts pins the cases).
13
+ //
14
+ // Order: at each schema, `type` and then `properties` are checked first and
15
+ // every other keyword after them in the schema's key order; `properties` in
16
+ // their key order; unknown properties in the instance's order. A failed
17
+ // `type` doesn't stop the rest (an enum still reports its own error).
18
+ // `oneOf` with no valid branch reports itself, then every branch's errors.
19
+ // networknt's JsonType names. With losslessNarrowing a number with no
20
+ // fraction is an integer (1.0 included, which JSON.parse makes 1 anyway).
21
+ function jsonTypeOf(value) {
22
+ if (value === null)
23
+ return 'null';
24
+ if (Array.isArray(value))
25
+ return 'array';
26
+ if (typeof value === 'number')
27
+ return Number.isInteger(value) ? 'integer' : 'number';
28
+ return typeof value;
29
+ }
30
+ function matchesType(value, type) {
31
+ const actual = jsonTypeOf(value);
32
+ return actual === type || (type === 'number' && actual === 'integer');
33
+ }
34
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
35
+ function deepEqual(a, b) {
36
+ if (a === b)
37
+ return true;
38
+ if (Array.isArray(a) && Array.isArray(b)) {
39
+ return a.length === b.length && a.every((item, index) => deepEqual(item, b[index]));
40
+ }
41
+ if (isObject(a) && isObject(b)) {
42
+ const keys = Object.keys(a);
43
+ return (keys.length === Object.keys(b).length &&
44
+ keys.every((key) => Object.hasOwn(b, key) && deepEqual(a[key], b[key])));
45
+ }
46
+ return false;
47
+ }
48
+ // networknt evaluates these before a schema's other keywords, whatever their
49
+ // position; the rest go in key order.
50
+ const FIRST_KEYWORDS = ['type', 'properties'];
51
+ // String lengths count code points, as Java's codePointCount does.
52
+ const codePoints = (text) => [...text].length;
53
+ const NOT_DEFINED = 'is not defined in the schema and the schema does not allow additional properties';
54
+ // One check per keyword, each a no-op on values it doesn't apply to (string
55
+ // keywords on non-strings, object keywords on non-objects, ...).
56
+ const KEYWORDS = {
57
+ type({ arg, value, fail }) {
58
+ if (typeof arg === 'string' && !matchesType(value, arg)) {
59
+ fail(`${jsonTypeOf(value)} found, ${arg} expected`);
60
+ }
61
+ },
62
+ const({ arg, value, fail }) {
63
+ if (!deepEqual(value, arg))
64
+ fail(`must be the constant value '${String(arg)}'`);
65
+ },
66
+ enum({ arg, value, fail }) {
67
+ if (Array.isArray(arg) && !arg.some((candidate) => deepEqual(value, candidate))) {
68
+ const values = arg.map((item) => JSON.stringify(item)).join(', ');
69
+ fail(`does not have a value in the enumeration [${values}]`);
70
+ }
71
+ },
72
+ minLength({ arg, value, fail }) {
73
+ if (typeof value === 'string' && codePoints(value) < arg) {
74
+ fail(`must be at least ${arg} characters long`);
75
+ }
76
+ },
77
+ maxLength({ arg, value, fail }) {
78
+ if (typeof value === 'string' && codePoints(value) > arg) {
79
+ fail(`must be at most ${arg} characters long`);
80
+ }
81
+ },
82
+ minimum({ arg, value, fail }) {
83
+ if (typeof value === 'number' && value < arg)
84
+ fail(`must have a minimum value of ${arg}`);
85
+ },
86
+ maximum({ arg, value, fail }) {
87
+ if (typeof value === 'number' && value > arg)
88
+ fail(`must have a maximum value of ${arg}`);
89
+ },
90
+ // JSON Schema patterns are unanchored: '[0-9]+' accepts 'x1y'.
91
+ pattern({ arg, value, fail }) {
92
+ if (typeof value === 'string' && !new RegExp(arg, 'u').test(value)) {
93
+ fail(`does not match the regex pattern ${arg}`);
94
+ }
95
+ },
96
+ oneOf({ arg, value, location, errors, fail }) {
97
+ if (!Array.isArray(arg))
98
+ return;
99
+ const results = arg.map((branch) => validateSchema(branch, value, location));
100
+ const valid = results.flatMap((result, index) => (result.length === 0 ? [index] : []));
101
+ if (valid.length === 1)
102
+ return;
103
+ if (valid.length > 1) {
104
+ fail(`must be valid to one and only one schema, but ${valid.length} are valid with indexes '${valid.join(', ')}'`);
105
+ return;
106
+ }
107
+ fail('must be valid to one and only one schema, but 0 are valid');
108
+ errors.push(...results.flat());
109
+ },
110
+ properties({ arg, value, location, errors }) {
111
+ if (!isObject(value) || !isObject(arg))
112
+ return;
113
+ for (const [name, propertySchema] of Object.entries(arg)) {
114
+ if (!Object.hasOwn(value, name))
115
+ continue;
116
+ errors.push(...validateSchema(propertySchema, value[name], `${location}.${name}`));
117
+ }
118
+ },
119
+ required({ arg, value, fail }) {
120
+ if (!isObject(value) || !Array.isArray(arg))
121
+ return;
122
+ for (const name of arg) {
123
+ if (!Object.hasOwn(value, name))
124
+ fail(`required property '${name}' not found`, name);
125
+ }
126
+ },
127
+ additionalProperties({ arg, schema, value, location, errors, fail }) {
128
+ if (!isObject(value) || (arg !== false && !isObject(arg)))
129
+ return;
130
+ const known = isObject(schema.properties) ? schema.properties : {};
131
+ for (const name of Object.keys(value)) {
132
+ if (Object.hasOwn(known, name))
133
+ continue;
134
+ if (arg === false)
135
+ fail(`property '${name}' ${NOT_DEFINED}`, name);
136
+ else
137
+ errors.push(...validateSchema(arg, value[name], `${location}.${name}`));
138
+ }
139
+ },
140
+ // Anything else is an annotation (description, title, default, readOnly,
141
+ // genesisType, $schema, $defs, ...) and validates nothing; networknt
142
+ // doesn't enforce readOnly unless asked to.
143
+ };
144
+ export function validateSchema(schema, value, location = '$') {
145
+ const errors = [];
146
+ if (!isObject(schema))
147
+ return errors;
148
+ const keywords = Object.keys(schema);
149
+ const ordered = [
150
+ ...FIRST_KEYWORDS.filter((keyword) => keywords.includes(keyword)),
151
+ ...keywords.filter((keyword) => !FIRST_KEYWORDS.includes(keyword)),
152
+ ];
153
+ for (const keyword of ordered) {
154
+ if (!Object.hasOwn(KEYWORDS, keyword))
155
+ continue;
156
+ KEYWORDS[keyword]({
157
+ arg: schema[keyword],
158
+ schema,
159
+ value,
160
+ location,
161
+ errors,
162
+ fail: (message, property) => errors.push({
163
+ keyword,
164
+ instanceLocation: location,
165
+ ...(property !== undefined ? { property } : {}),
166
+ message,
167
+ }),
168
+ });
169
+ }
170
+ return errors;
171
+ }
172
+ // GenesisMessageDecoder's FieldError for each failure: TEXT is the error's
173
+ // toString() ("<location>: <message>"), FIELD the missing property for
174
+ // `required` and otherwise the last segment of the location (so an unknown
175
+ // DETAILS property names 'DETAILS'), PATH the location.
176
+ export function schemaFieldErrors(errors) {
177
+ return errors.map((error) => ({
178
+ '@type': 'FieldError',
179
+ CODE: 'VALIDATION_ERROR',
180
+ TEXT: `${error.instanceLocation}: ${error.message}`,
181
+ FIELD: error.keyword === 'required'
182
+ ? error.property
183
+ : error.instanceLocation.slice(error.instanceLocation.lastIndexOf('.') + 1),
184
+ PATH: error.instanceLocation,
185
+ }));
186
+ }
package/dist/server.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import http from 'node:http';
2
2
  import { WebSocketServer } from 'ws';
3
3
  import { Store } from "./db/store.js";
4
+ import { crudEventHandler } from "./handlers/crudEvents.js";
4
5
  import { pendingKey } from "./handlers/dataLogon.js";
5
6
  import { createHttpRequestListener } from "./handlers/http.js";
6
- import { applyResourceAuth, requestingUser } from "./handlers/resourceAuth.js";
7
+ import { permittedQueryRow, requestingUser } from "./handlers/resourceAuth.js";
7
8
  import { Connection } from "./protocol/connection.js";
8
9
  import { isLegacyFidelity } from "./protocol/fidelity.js";
9
10
  import { routeMessage } from "./protocol/router.js";
10
- import { stampRow } from "./protocol/rowUpdate.js";
11
+ import { dataserverRow, stampRow } from "./protocol/rowUpdate.js";
11
12
  export function createMockServer(config) {
12
13
  const store = new Store();
13
14
  for (const [tableName, tableDef] of Object.entries(config.tables ?? {})) {
@@ -80,10 +81,10 @@ export function createMockServer(config) {
80
81
  return null;
81
82
  const permitted = operation === 'DELETE'
82
83
  ? undefined
83
- : applyResourceAuth([row], query?.auth, requestingUser(connection))[0];
84
+ : permittedQueryRow(row, query?.auth, requestingUser(connection), subscription.fieldNames);
84
85
  if (pending.has(key)) {
85
86
  if (permitted)
86
- pending.set(key, stampRow(permitted, rowRef, 'INSERT'));
87
+ pending.set(key, stampRow(dataserverRow(permitted, row, query?.fields), rowRef, 'INSERT'));
87
88
  else
88
89
  pending.delete(key);
89
90
  return null;
@@ -96,10 +97,14 @@ export function createMockServer(config) {
96
97
  }
97
98
  if (!permitted)
98
99
  return null;
100
+ // Under ORDER_BY, a new row that sorts among the waiting ones waits too,
101
+ // so MORE_ROWS pages stay in order.
102
+ if (subscription.queueInsert?.(key, stampRow(dataserverRow(permitted, row, query?.fields), rowRef, 'INSERT'), row))
103
+ return null;
99
104
  held.add(key);
100
105
  return 'INSERT';
101
106
  }
102
- const broadcast = (resourceName, operation, row) => {
107
+ function pushChange(resourceName, operation, row) {
103
108
  const query = config.queries?.[resourceName];
104
109
  // The query's own filter (where-clause equivalent) applies to live pushes
105
110
  // too — a row the snapshot would exclude must not leak in via broadcast.
@@ -129,21 +134,25 @@ export function createMockServer(config) {
129
134
  : resolved;
130
135
  if (rowOperation === null)
131
136
  continue;
137
+ const legacy = isLegacyFidelity(config);
132
138
  let outRow = row;
133
139
  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') {
140
+ // GSF's DELETE row is DETAILS {OPERATION, ROW_REF} and nothing else.
141
+ // Legacy mode sent the pk fields: the stub it was given for a real
142
+ // DELETE, or one rebuilt from the key for a change that left the
143
+ // subscription (the full row may carry columns hideFields strips).
144
+ if (!legacy)
145
+ outRow = {};
146
+ else if (operation !== 'DELETE') {
139
147
  outRow = query ? store.rowStubFromKey(query.source, rowRef) : row;
140
148
  }
141
149
  }
142
- else if (query?.auth) {
143
- const [permitted] = applyResourceAuth([row], query.auth, requestingUser(connection));
150
+ else {
151
+ // Only the subscription's columns, minus the ones this user can't see.
152
+ const permitted = permittedQueryRow(row, query?.auth, requestingUser(connection), subscription.fieldNames);
144
153
  if (!permitted)
145
154
  continue;
146
- outRow = permitted;
155
+ outRow = dataserverRow(permitted, row, query?.fields);
147
156
  }
148
157
  subscription.sequenceId += 1;
149
158
  // A live push carries neither MORE_ROWS nor ROWS_COUNT on the real
@@ -151,20 +160,62 @@ export function createMockServer(config) {
151
160
  connection.send({
152
161
  MESSAGE_TYPE: 'QUERY_UPDATE',
153
162
  SOURCE_REF: sourceRef,
154
- ROW: [stampRow(outRow, rowRef, rowOperation)],
155
- ...(isLegacyFidelity(config) ? { ROWS_COUNT: 1 } : {}),
163
+ ROW: [stampRow(outRow, rowRef, rowOperation, legacy)],
164
+ ...(legacy ? { ROWS_COUNT: 1 } : {}),
156
165
  SEQUENCE_ID: subscription.sequenceId,
157
166
  });
158
167
  }
159
168
  }
169
+ }
170
+ // Pushes an unsolicited row update to every subscription on the resource
171
+ // (see pushChange). A DELETE given as a pk-only row is resolved to the
172
+ // record(s) removed from that key that no broadcast has announced yet —
173
+ // their RECORD_IDs are the ROW_REFs a client may hold — and those are
174
+ // consumed, so a later delete of a re-inserted key names its own record.
175
+ const broadcast = (resourceName, operation, row) => {
176
+ const source = config.queries?.[resourceName]?.source;
177
+ if (operation !== 'DELETE' || !source) {
178
+ pushChange(resourceName, operation, row);
179
+ return;
180
+ }
181
+ const removed = store.takeRemovedRecordsFor(source, row);
182
+ if (isLegacyFidelity(config)) {
183
+ pushChange(resourceName, operation, row);
184
+ return;
185
+ }
186
+ const records = row.RECORD_ID === undefined || row.RECORD_ID === null ? removed : [row];
187
+ if (records.length === 0)
188
+ records.push(row);
189
+ for (const record of records)
190
+ pushChange(resourceName, 'DELETE', record);
160
191
  };
161
192
  // Event handlers mutate one table (e.g. TRADE) but grids may be subscribed to
162
193
  // a view built on top of it (e.g. ALL_TRADES -> TRADE_VIEW). This finds every
163
194
  // query resource backed by that table — directly or via a view — and pushes
164
195
  // the (re-joined, for views) row to each one's subscribers. `pk` is the
165
196
  // store key: the single pk field's value, or store.rowKey(...) for
166
- // composite-keyed tables.
197
+ // composite-keyed tables. ROW_REF is the record's RECORD_ID, so a DELETE is
198
+ // broadcast with every record the key lost that no broadcast announced yet
199
+ // (Store.takeRemovedRows; held-rows tracking drops the ones a client
200
+ // doesn't hold), and a record replaced under the same key (deleted, then
201
+ // re-inserted, before one broadcast) gets a DELETE of its old ROW_REF before
202
+ // the new one goes out. A view row is built from the changed base row
203
+ // alone, so it is found whatever the view's own pkField; a MODIFY that takes
204
+ // it out of the view (an anti-join) is pushed as a DELETE. Legacy ROW_REFs
205
+ // are the key itself, so there a DELETE is the pk stub and a replacement is
206
+ // the one INSERT/MODIFY, as before.
167
207
  const broadcastTableChange = (tableName, operation, pk) => {
208
+ const legacy = isLegacyFidelity(config);
209
+ const current = operation === 'DELETE' ? undefined : store.getRow(tableName, pk);
210
+ let removed = store
211
+ .takeRemovedRows(tableName, pk)
212
+ .filter((record) => record.RECORD_ID !== current?.RECORD_ID);
213
+ // A DELETE broadcast before the row is removed still names it.
214
+ const live = store.getRow(tableName, pk);
215
+ if (operation === 'DELETE' && removed.length === 0 && live)
216
+ removed = [live];
217
+ if (legacy)
218
+ removed = [];
168
219
  for (const [queryName, query] of Object.entries(config.queries ?? {})) {
169
220
  const isDirect = query.source === tableName;
170
221
  const view = store.views.get(query.source);
@@ -183,25 +234,37 @@ export function createMockServer(config) {
183
234
  continue;
184
235
  if (isViewJoiningTable) {
185
236
  for (const viewRow of store.getViewRows(query.source)) {
186
- broadcast(queryName, 'MODIFY', viewRow);
237
+ pushChange(queryName, 'MODIFY', viewRow);
187
238
  }
188
239
  continue;
189
240
  }
190
- let row;
241
+ // The removed base records carry the RECORD_ID a view row's ROW_REF
242
+ // comes from too; the DELETE body is DETAILS only either way.
243
+ for (const record of removed)
244
+ pushChange(queryName, 'DELETE', record);
191
245
  if (operation === 'DELETE') {
192
- const stub = store.rowStubFromKey(query.source, pk);
193
- row = Object.keys(stub).length > 0 ? stub : undefined;
246
+ if (legacy) {
247
+ const stub = store.rowStubFromKey(query.source, pk);
248
+ if (Object.keys(stub).length > 0)
249
+ pushChange(queryName, 'DELETE', stub);
250
+ }
251
+ continue;
252
+ }
253
+ if (!current)
254
+ continue;
255
+ if (isDirect) {
256
+ pushChange(queryName, operation, current);
257
+ continue;
194
258
  }
195
- else if (isDirect) {
196
- row = store.getRow(tableName, pk);
259
+ const viewRow = store.getViewRows(query.source, { [tableName]: [current] })[0];
260
+ if (viewRow) {
261
+ pushChange(queryName, operation, viewRow);
197
262
  }
198
- else {
199
- row = store
200
- .getViewRows(query.source)
201
- .find((r) => String(store.getRowRef(query.source, r)) === String(pk));
263
+ else if (operation === 'MODIFY') {
264
+ pushChange(queryName, 'DELETE', legacy
265
+ ? store.rowStubFromKey(query.source, store.getRowRef(query.source, current))
266
+ : current);
202
267
  }
203
- if (row)
204
- broadcast(queryName, operation, row);
205
268
  }
206
269
  };
207
270
  const httpSessions = new Map(); // sessionId -> username, for the HTTP login transport (see handlers/http.ts)
@@ -244,8 +307,11 @@ export function createMockServer(config) {
244
307
  function getRequestReply(resourceName) {
245
308
  return config.requestReplies?.[resourceName];
246
309
  }
310
+ // An eventHandlers entry, else the generic CRUD handler a table registers
311
+ // (TableDef.events) — so extendEventHandler can wrap a generic event; the
312
+ // wrapper is then an explicit handler, which wins.
247
313
  function getEventHandler(eventName) {
248
- return config.eventHandlers?.[eventName];
314
+ return config.eventHandlers?.[eventName] ?? crudEventHandler(eventName, config);
249
315
  }
250
316
  // augment(current) => replacement. `current` is undefined if nothing was
251
317
  // registered under that name yet, so augment can also be used to define a
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import type { Store } from './db/store.ts';
3
+ import type { NackErrorInput } from './protocol/errors.ts';
3
4
  export type Row = Record<string, any>;
4
5
  export interface GenesisMessage {
5
6
  MESSAGE_TYPE: string;
@@ -7,12 +8,42 @@ export interface GenesisMessage {
7
8
  DETAILS?: Row;
8
9
  [key: string]: any;
9
10
  }
11
+ export type GenesisFieldType = 'STRING' | 'ENUM' | 'INT' | 'SHORT' | 'LONG' | 'DOUBLE' | 'BIGDECIMAL' | 'BOOLEAN' | 'DATE' | 'DATETIME' | 'NANO_TIMESTAMP' | 'RAW';
12
+ export interface FieldDef {
13
+ name: string;
14
+ type: GenesisFieldType;
15
+ nullable?: boolean;
16
+ values?: string[];
17
+ default?: unknown;
18
+ maxSize?: number;
19
+ minLength?: number;
20
+ description?: string;
21
+ title?: string;
22
+ optional?: boolean;
23
+ }
24
+ export interface GeneratedFieldDef {
25
+ field: string;
26
+ kind: 'SEQUENCE' | 'AUTO_INCREMENT';
27
+ prefix?: string;
28
+ }
29
+ export type CrudEventVerb = 'INSERT' | 'MODIFY' | 'DELETE';
30
+ export interface IndexDef {
31
+ name: string;
32
+ fields: string[];
33
+ unique?: boolean;
34
+ }
10
35
  export interface TableDef {
11
36
  pkField: string | string[];
12
37
  sequencePrefix?: string;
13
38
  sequenceWidth?: number;
39
+ sequenceFormat?: SequenceFormat;
14
40
  rows?: Row[];
41
+ fields?: FieldDef[];
42
+ generated?: GeneratedFieldDef[];
43
+ indexes?: IndexDef[];
44
+ events?: CrudEventVerb[];
15
45
  }
46
+ export type SequenceFormat = 'prefix' | 'genesis';
16
47
  export interface JoinDef {
17
48
  table: string;
18
49
  on: [string, string];
@@ -30,6 +61,8 @@ export interface ViewDef {
30
61
  joins?: JoinDef[];
31
62
  antiJoins?: AntiJoinDef[];
32
63
  derived?: Record<string, (row: Row) => any>;
64
+ derivedTypes?: Record<string, GenesisFieldType | Omit<FieldDef, 'name'>>;
65
+ fields?: string[];
33
66
  }
34
67
  export interface RequestingUser {
35
68
  userName?: string;
@@ -43,6 +76,8 @@ export interface ResourceAuthDef {
43
76
  export interface QueryDef {
44
77
  source: string;
45
78
  fieldTypes?: Record<string, string>;
79
+ fields?: string[];
80
+ indexes?: IndexDef[];
46
81
  filter?: (row: Row) => boolean;
47
82
  auth?: ResourceAuthDef;
48
83
  }
@@ -59,11 +94,17 @@ export interface RequestReplyDef {
59
94
  fieldTypes?: Record<string, string>;
60
95
  filter?: (row: Row) => boolean;
61
96
  auth?: ResourceAuthDef;
97
+ criteriaOnly?: boolean;
98
+ requestFields?: string[];
99
+ fields?: string[];
100
+ rowLimit?: number;
62
101
  }
63
102
  export interface EventSchemaOverride {
64
103
  table?: string;
65
104
  pkOnly?: boolean;
66
105
  fieldTypes?: Record<string, string>;
106
+ fields?: FieldDef[];
107
+ className?: string;
67
108
  }
68
109
  export interface ResourceItem {
69
110
  RESOURCE_NAME: string;
@@ -101,9 +142,11 @@ export interface EventHandlerCtx {
101
142
  message: GenesisMessage;
102
143
  broadcast: BroadcastFn;
103
144
  broadcastTableChange: BroadcastTableChangeFn;
145
+ validate: boolean;
104
146
  }
105
147
  export type EventHandlerResult = {
106
148
  generated?: Row[];
149
+ warnings?: NackErrorInput[];
107
150
  } | void;
108
151
  export type EventHandler = (details: Row, ctx: EventHandlerCtx) => EventHandlerResult;
109
152
  export interface HttpRouteDef {
@@ -133,10 +176,12 @@ export interface MockServerConfig {
133
176
  httpRoutes?: HttpRouteDef[];
134
177
  onMessage?: OnMessageHook;
135
178
  productName?: string;
179
+ appName?: string;
136
180
  }
137
181
  export interface FieldMetadata {
138
182
  NAME: string;
139
183
  TYPE: string;
184
+ VALID_VALUES?: string[];
140
185
  }
141
186
  export interface SocketLike {
142
187
  readyState: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genesislcap/mock-server",
3
- "version": "15.51.1",
3
+ "version": "15.53.0",
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",