@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.
Files changed (53) hide show
  1. package/README.md +217 -31
  2. package/dist/db/criteria.d.ts +1 -0
  3. package/dist/db/criteria.js +29 -0
  4. package/dist/db/identity.d.ts +11 -0
  5. package/dist/db/identity.js +46 -0
  6. package/dist/db/schema.d.ts +46 -0
  7. package/dist/db/schema.js +375 -0
  8. package/dist/db/store.d.ts +13 -2
  9. package/dist/db/store.js +208 -34
  10. package/dist/handlers/commitEvent.js +20 -6
  11. package/dist/handlers/dataLogon.js +126 -6
  12. package/dist/handlers/jsonSchema.d.ts +2 -1
  13. package/dist/handlers/jsonSchema.js +216 -61
  14. package/dist/handlers/meta.d.ts +6 -1
  15. package/dist/handlers/meta.js +164 -21
  16. package/dist/handlers/requestReply.js +16 -5
  17. package/dist/handlers/resourceAuth.d.ts +1 -0
  18. package/dist/handlers/resourceAuth.js +14 -0
  19. package/dist/index.d.ts +3 -2
  20. package/dist/index.js +2 -1
  21. package/dist/protocol/connection.d.ts +2 -0
  22. package/dist/protocol/errors.d.ts +100 -2
  23. package/dist/protocol/errors.js +300 -12
  24. package/dist/protocol/fieldTypes.d.ts +9 -0
  25. package/dist/protocol/fieldTypes.js +101 -0
  26. package/dist/protocol/gsfSchemas.d.ts +9 -0
  27. package/dist/protocol/gsfSchemas.js +423 -0
  28. package/dist/protocol/messageTypes.d.ts +1 -0
  29. package/dist/protocol/messageTypes.js +1 -0
  30. package/dist/protocol/rowUpdate.d.ts +5 -3
  31. package/dist/protocol/rowUpdate.js +33 -15
  32. package/dist/server.js +90 -28
  33. package/dist/types.d.ts +40 -0
  34. package/package.json +1 -1
  35. package/src/db/criteria.ts +26 -0
  36. package/src/db/identity.ts +53 -0
  37. package/src/db/schema.ts +523 -0
  38. package/src/db/store.ts +225 -32
  39. package/src/handlers/commitEvent.ts +20 -7
  40. package/src/handlers/dataLogon.ts +144 -7
  41. package/src/handlers/jsonSchema.ts +290 -69
  42. package/src/handlers/meta.ts +222 -23
  43. package/src/handlers/requestReply.ts +15 -6
  44. package/src/handlers/resourceAuth.ts +18 -0
  45. package/src/index.ts +16 -1
  46. package/src/protocol/connection.ts +10 -2
  47. package/src/protocol/errors.ts +361 -13
  48. package/src/protocol/fieldTypes.ts +108 -0
  49. package/src/protocol/gsfSchemas.ts +505 -0
  50. package/src/protocol/messageTypes.ts +1 -0
  51. package/src/protocol/rowUpdate.ts +40 -20
  52. package/src/server.ts +101 -27
  53. package/src/types.ts +145 -1
@@ -0,0 +1,375 @@
1
+ // The field schema of a table, view, query, request server or event: what
2
+ // META_REQUEST and JSON_SCHEMA_REQUEST describe, and which columns a query's
3
+ // rows carry. Declared fields (TableDef.fields) win; without them the fields
4
+ // are inferred from the rows (metadata.ts), as before declared schemas
5
+ // existed, so a config that declares nothing keeps working.
6
+ import { isLegacyFidelity } from "../protocol/fidelity.js";
7
+ import { daoClassOf, titleCase } from "../protocol/fieldTypes.js";
8
+ import { deriveFields } from "./metadata.js";
9
+ function pkFieldsOf(def) {
10
+ return Array.isArray(def.pkField) ? def.pkField : [def.pkField];
11
+ }
12
+ // The declared generated fields, plus the key of a table that mints its own
13
+ // keys through sequencePrefix.
14
+ export function generatedFieldsOf(def) {
15
+ const generated = new Set((def.generated ?? []).map((entry) => entry.field));
16
+ const pkFields = pkFieldsOf(def);
17
+ if (def.sequencePrefix && pkFields.length === 1)
18
+ generated.add(pkFields[0]);
19
+ return generated;
20
+ }
21
+ function fromFieldDef(field, table, notNullByDefault, generated) {
22
+ return {
23
+ name: field.name,
24
+ type: field.type,
25
+ nullable: field.nullable ?? !notNullByDefault,
26
+ generated,
27
+ ...(field.values ? { values: field.values } : {}),
28
+ ...(field.default !== undefined ? { default: field.default } : {}),
29
+ ...(field.maxSize !== undefined ? { maxSize: field.maxSize } : {}),
30
+ ...(field.minLength !== undefined ? { minLength: field.minLength } : {}),
31
+ ...(field.description !== undefined ? { description: field.description } : {}),
32
+ ...(field.title !== undefined ? { title: field.title } : {}),
33
+ ...(field.optional !== undefined ? { optional: field.optional } : {}),
34
+ ...(table ? { table } : {}),
35
+ };
36
+ }
37
+ function declaredTableFields(tableName, def) {
38
+ const pkFields = pkFieldsOf(def);
39
+ const generated = generatedFieldsOf(def);
40
+ return (def.fields ?? []).map((field) => fromFieldDef(field, tableName, pkFields.includes(field.name) || generated.has(field.name), generated.has(field.name)));
41
+ }
42
+ // Inference from rows: a type per column (metadata.ts), nothing nullable but
43
+ // the key, no defaults or enum values (rows can't say).
44
+ function inferredFields(rows, pkFields, generated, table) {
45
+ return deriveFields(rows).map(({ NAME, TYPE }) => ({
46
+ name: NAME,
47
+ type: TYPE,
48
+ nullable: !(pkFields.includes(NAME) || generated.has(NAME)),
49
+ generated: generated.has(NAME),
50
+ ...(table ? { table } : {}),
51
+ }));
52
+ }
53
+ function derivedTypeDef(name, entry) {
54
+ return typeof entry === 'string' ? { name, type: entry } : { ...entry, name };
55
+ }
56
+ function viewOf(name, config, store) {
57
+ return store.views.get(name) ?? config.views?.[name];
58
+ }
59
+ // A view over a declared base table: the base table's columns, each join's
60
+ // selected columns typed from the joined table (nullable, since a base row
61
+ // may have no match), and the derived fields typed from derivedTypes (or
62
+ // inferred from the view's rows when not given) — narrowed to the view's
63
+ // fields block when it has one.
64
+ function declaredViewFields(viewName, view, config, store) {
65
+ const fields = new Map();
66
+ for (const field of resolveSourceSchema(view.base, config, store).fields) {
67
+ fields.set(field.name, field);
68
+ }
69
+ for (const join of view.joins ?? []) {
70
+ const joined = resolveSourceSchema(join.table, config, store).fields;
71
+ for (const [alias, sourceField] of Object.entries(join.select ?? {})) {
72
+ const source = joined.find((field) => field.name === sourceField);
73
+ const { title: _title, ...rest } = source ?? {
74
+ name: alias,
75
+ type: 'STRING',
76
+ nullable: true,
77
+ generated: false,
78
+ };
79
+ fields.set(alias, { ...rest, name: alias, nullable: true, generated: false });
80
+ }
81
+ }
82
+ const derivedNames = Object.keys(view.derived ?? {});
83
+ const untyped = derivedNames.filter((name) => !view.derivedTypes?.[name]);
84
+ const inferred = untyped.length > 0
85
+ ? deriveFields(store
86
+ .getMetadataRows(viewName)
87
+ .map((row) => Object.fromEntries(untyped.map((name) => [name, row[name]]))))
88
+ : [];
89
+ for (const name of derivedNames) {
90
+ const typed = view.derivedTypes?.[name];
91
+ fields.set(name, typed
92
+ ? fromFieldDef(derivedTypeDef(name, typed), undefined, false, false)
93
+ : {
94
+ name,
95
+ type: inferred.find((field) => field.NAME === name)?.TYPE ?? 'STRING',
96
+ nullable: true,
97
+ generated: false,
98
+ });
99
+ }
100
+ return view.fields ? pick(fields, view.fields) : [...fields.values()];
101
+ }
102
+ function pick(fields, names) {
103
+ return names.flatMap((name) => {
104
+ const field = fields.get(name);
105
+ return field ? [field] : [];
106
+ });
107
+ }
108
+ function isDeclared(source, config, store) {
109
+ const table = config.tables?.[source];
110
+ if (table)
111
+ return !!table.fields;
112
+ const view = viewOf(source, config, store);
113
+ return view ? isDeclared(view.base, config, store) : false;
114
+ }
115
+ // The fields of a table or view. Unknown names resolve to no fields. Under
116
+ // fidelity: 'legacy' everything is inferred, as up to 15.47: declarations,
117
+ // fields blocks and derivedTypes are ignored.
118
+ export function resolveSourceSchema(source, config, store) {
119
+ const legacy = isLegacyFidelity(config);
120
+ const table = config.tables?.[source];
121
+ if (!legacy && table?.fields) {
122
+ return { fields: declaredTableFields(source, table), declared: true };
123
+ }
124
+ const view = viewOf(source, config, store);
125
+ if (!legacy && view && isDeclared(view.base, config, store)) {
126
+ return { fields: declaredViewFields(source, view, config, store), declared: true };
127
+ }
128
+ const generated = table ? generatedFieldsOf(table) : new Set();
129
+ let fields = inferredFields(store.getMetadataRows(source), store.getPkFields(source), generated, table ? source : view?.base);
130
+ if (legacy)
131
+ return { fields, declared: false };
132
+ // derivedTypes and the fields block describe a view even when the rest of
133
+ // it is inferred.
134
+ if (view?.derivedTypes) {
135
+ fields = fields.map((field) => {
136
+ const typed = view.derivedTypes?.[field.name];
137
+ return typed
138
+ ? fromFieldDef(derivedTypeDef(field.name, typed), undefined, false, false)
139
+ : field;
140
+ });
141
+ }
142
+ if (view?.fields)
143
+ fields = pick(new Map(fields.map((field) => [field.name, field])), view.fields);
144
+ return { fields, declared: false };
145
+ }
146
+ // fieldTypes overrides the TYPE of the named fields, declared or inferred.
147
+ export function withTypeOverrides(schema, fieldTypes) {
148
+ if (!fieldTypes)
149
+ return schema;
150
+ return {
151
+ ...schema,
152
+ fields: schema.fields.map((field) => fieldTypes[field.name] ? { ...field, type: fieldTypes[field.name] } : field),
153
+ };
154
+ }
155
+ // A query's fields block: those fields only, in its order. Names the source
156
+ // doesn't have are dropped, as the real dataserver drops them.
157
+ export function projectSchema(schema, names) {
158
+ if (!names)
159
+ return schema;
160
+ const byName = new Map(schema.fields.map((field) => [field.name, field]));
161
+ return {
162
+ ...schema,
163
+ fields: names.flatMap((name) => {
164
+ const field = byName.get(name);
165
+ return field ? [field] : [];
166
+ }),
167
+ };
168
+ }
169
+ export function querySchema(query, config, store) {
170
+ return projectSchema(withTypeOverrides(resolveSourceSchema(query.source, config, store), query.fieldTypes), isLegacyFidelity(config) ? undefined : query.fields);
171
+ }
172
+ // A resolver-only request server has no table: its fields come from the
173
+ // def's metadataRows sample.
174
+ export function requestReplySchema(requestReply, config, store) {
175
+ const schema = requestReply.source
176
+ ? resolveSourceSchema(requestReply.source, config, store)
177
+ : {
178
+ fields: inferredFields(requestReply.metadataRows ?? [], [], new Set(), undefined),
179
+ declared: false,
180
+ };
181
+ return withTypeOverrides(schema, requestReply.fieldTypes);
182
+ }
183
+ // The record identity a criteria-only request server adds to every reply row.
184
+ export const RECORD_IDENTITY_FIELDS = [
185
+ { name: 'RECORD_ID', type: 'LONG', nullable: false, generated: true },
186
+ { name: 'TIMESTAMP', type: 'NANO_TIMESTAMP', nullable: false, generated: true },
187
+ ];
188
+ // The request block's fields: requestFields, else the source's key.
189
+ export function requestFieldsOf(requestReply, store) {
190
+ if (requestReply.requestFields)
191
+ return requestReply.requestFields;
192
+ return requestReply.source ? store.getPkFields(requestReply.source) : [];
193
+ }
194
+ // An ENUM's values, or undefined when none are known: a client must then be
195
+ // told nothing rather than an empty list (grid-pro builds an empty select from
196
+ // VALID_VALUES: [], and a JSON schema `enum: []` accepts no value at all).
197
+ export function knownValues(field) {
198
+ return field.values?.length ? field.values : undefined;
199
+ }
200
+ // The DEFAULT an event field reports (META DEFAULT, JSON schema `default`).
201
+ // Only an optional property has one, and it is its type's default
202
+ // (PALMetadataUtils.kt:159, getPropertyDefaultValue, GSF v8.15.29): the
203
+ // dictionary default, or for an ENUM with none the enum's first value
204
+ // (PALMetadataUtils.kt:205, EnumType defaultValue = ... ?: jsonValues.firstOrNull()).
205
+ export function eventDefaultOf(field) {
206
+ if (!field.optional)
207
+ return undefined;
208
+ if (field.default !== undefined)
209
+ return field.default;
210
+ return field.type === 'ENUM' ? knownValues(field)?.[0] : undefined;
211
+ }
212
+ // --- query rows --------------------------------------------------------------
213
+ function sourceFieldNames(source, config, store) {
214
+ const table = config.tables?.[source];
215
+ if (table)
216
+ return table.fields?.map((field) => field.name);
217
+ const view = viewOf(source, config, store);
218
+ if (!view)
219
+ return undefined;
220
+ const base = sourceFieldNames(view.base, config, store);
221
+ // An undeclared base: the view's own fields block is all that is known.
222
+ if (!base)
223
+ return view.fields;
224
+ const names = new Set(base);
225
+ for (const join of view.joins ?? []) {
226
+ for (const alias of Object.keys(join.select ?? {}))
227
+ names.add(alias);
228
+ }
229
+ for (const name of Object.keys(view.derived ?? {}))
230
+ names.add(name);
231
+ return view.fields ? view.fields.filter((name) => names.has(name)) : [...names];
232
+ }
233
+ // The columns a query's rows carry, in order: its fields block, else every
234
+ // declared column of its source (a view's own fields block included).
235
+ // undefined when neither is known, and always under fidelity: 'legacy': rows
236
+ // go out as stored.
237
+ export function queryFieldNames(query, config, store) {
238
+ if (!query || isLegacyFidelity(config))
239
+ return undefined;
240
+ const names = sourceFieldNames(query.source, config, store);
241
+ if (!query.fields)
242
+ return names;
243
+ return names ? query.fields.filter((name) => names.includes(name)) : query.fields;
244
+ }
245
+ // A row with exactly these columns. The real dataserver sends every column it
246
+ // exposes, null when unset, so a missing value goes out as null.
247
+ export function projectRow(row, names) {
248
+ if (!names)
249
+ return row;
250
+ return Object.fromEntries(names.map((name) => [name, row[name] ?? null]));
251
+ }
252
+ // --- events ------------------------------------------------------------------
253
+ // Any event named .../_DELETE(_...)? takes only the key fields, which is what
254
+ // entity-management's delete confirmation copies from the selected row.
255
+ export function isDeleteEventName(name) {
256
+ return /_DELETE(_|$)/.test(name);
257
+ }
258
+ // Zero-config default: EVENT_<TABLE>_<VERB...> maps to <TABLE> if it's a
259
+ // registered table name (longest match wins, so e.g. a hypothetical
260
+ // COUNTERPARTY_AUTH table can't shadow COUNTERPARTY). Covers every event
261
+ // naming pattern in this codebase (INSERT, MODIFY, DELETE, plus suffixed
262
+ // variants like INSERT_WITH_APPROVAL, DEMO_INSERT, MODIFY_PRICE, INSERT_FAIL)
263
+ // without hand-maintaining a table per event. Override via
264
+ // config.eventSchemas for names that don't follow this convention.
265
+ export function inferEventTable(eventName, tableNames) {
266
+ if (!eventName)
267
+ return undefined;
268
+ // Not every caller asks by event name: entity-management resolves a grid's
269
+ // schema from the bare resource/table name (FEATURE: 'TRADE'), so accept an
270
+ // exact table match before requiring the EVENT_ prefix. Without this the
271
+ // request errors with UNKNOWN_RESOURCE and the form renders no fields.
272
+ if (tableNames.includes(eventName))
273
+ return eventName;
274
+ if (!eventName.startsWith('EVENT_'))
275
+ return undefined;
276
+ const rest = eventName.slice('EVENT_'.length);
277
+ const candidates = tableNames.filter((name) => rest === name || rest.startsWith(`${name}_`));
278
+ candidates.sort((a, b) => b.length - a.length);
279
+ return candidates[0];
280
+ }
281
+ // The table an event writes: config.eventSchemas first, then the
282
+ // EVENT_<TABLE>_<VERB> convention. Only a registered event (an eventHandlers
283
+ // or eventSchemas entry) has metadata, as only a deployed event handler has
284
+ // on the real server; anything else is the router's 404. `bareTable` also
285
+ // accepts a plain table name, which only JSON_SCHEMA_REQUEST does (see the
286
+ // README).
287
+ export function resolveEventTarget(name, config, { bareTable = false } = {}) {
288
+ if (!name)
289
+ return undefined;
290
+ const override = config.eventSchemas?.[name];
291
+ if (name.startsWith('EVENT_') && !override && !config.eventHandlers?.[name])
292
+ return undefined;
293
+ const tableNames = Object.keys(config.tables ?? {});
294
+ const table = override?.table ??
295
+ (bareTable || name.startsWith('EVENT_') ? inferEventTable(name, tableNames) : undefined);
296
+ const fieldTypes = override?.fieldTypes ? { fieldTypes: override.fieldTypes } : {};
297
+ if (override?.fields) {
298
+ return {
299
+ kind: 'dto',
300
+ ...(table ? { table } : {}),
301
+ className: override.className ?? daoClassOf(table ?? name.replace(/^EVENT_/, '')),
302
+ fields: override.fields,
303
+ ...fieldTypes,
304
+ };
305
+ }
306
+ if (!table)
307
+ return undefined;
308
+ const byId = override?.pkOnly ?? isDeleteEventName(name);
309
+ return byId
310
+ ? {
311
+ kind: 'byId',
312
+ table,
313
+ className: override?.className ?? `${daoClassOf(table)}.ById`,
314
+ ...fieldTypes,
315
+ }
316
+ : {
317
+ kind: 'table',
318
+ table,
319
+ className: override?.className ?? daoClassOf(table),
320
+ title: titleCase(table),
321
+ ...fieldTypes,
322
+ };
323
+ }
324
+ // Kotlin reflection lists a class's properties sorted by their (camelCase)
325
+ // names, and the real server only then turns them into UPPER_UNDERSCORE —
326
+ // so DATE_FIELD (dateField) sorts before DATETIME_FIELD (datetimeField),
327
+ // which a plain sort of the field names would reverse.
328
+ function propertyName(field) {
329
+ return field.name
330
+ .toLowerCase()
331
+ .replace(/_+([a-z0-9])/g, (_match, char) => char.toUpperCase());
332
+ }
333
+ const byName = (a, b) => {
334
+ const [left, right] = [propertyName(a), propertyName(b)];
335
+ return left < right ? -1 : left > right ? 1 : 0;
336
+ };
337
+ // The fields an event's DETAILS take, sorted by name as the real server
338
+ // lists a class's properties — except a delete's, which are the key fields
339
+ // in key order, all mandatory. A key field the schema doesn't know (an
340
+ // inferred table with no rows) is still listed, as a STRING. `optional` is
341
+ // always resolved: FieldDef.optional when given, else whether the class
342
+ // property has a default (PALMetadataUtils.kt mapMemberProperty: optional =
343
+ // propertyType != Mandatory). A generated DAO gives every nullable and every
344
+ // generated column a default; a custom DTO's defaults are invisible, so a
345
+ // DTO field is optional only when it declares a default or `optional: true`.
346
+ export function eventFields(target, config, store) {
347
+ if (target.kind === 'dto') {
348
+ const resolved = (target.fields ?? []).map((field) => fromFieldDef(field, target.table, false, false));
349
+ return withTypeOverrides({ fields: resolved, declared: true }, target.fieldTypes)
350
+ .fields.map((field) => ({
351
+ ...field,
352
+ optional: field.optional ?? field.default !== undefined,
353
+ }))
354
+ .sort(byName);
355
+ }
356
+ const table = target.table;
357
+ const { fields } = withTypeOverrides(resolveSourceSchema(table, config, store), target.fieldTypes);
358
+ if (target.kind === 'byId') {
359
+ return store.getPkFields(table).map((name) => {
360
+ const { default: _default, ...field } = fields.find((candidate) => candidate.name === name) ?? {
361
+ name,
362
+ type: 'STRING',
363
+ nullable: false,
364
+ generated: false,
365
+ };
366
+ return { ...field, nullable: false, generated: false, optional: false };
367
+ });
368
+ }
369
+ return fields
370
+ .map((field) => ({
371
+ ...field,
372
+ optional: field.optional ?? (field.generated || field.nullable || field.default !== undefined),
373
+ }))
374
+ .sort(byName);
375
+ }
@@ -4,12 +4,18 @@ export declare class Store {
4
4
  private tables;
5
5
  private tableConfig;
6
6
  private sequenceCounters;
7
+ private clock;
7
8
  private seedRows;
9
+ private removedRows;
8
10
  readonly views: Map<string, ViewDef>;
9
- registerTable(tableName: string, { pkField, sequencePrefix, sequenceWidth, rows }: TableDef): this;
11
+ registerTable(tableName: string, { pkField, sequencePrefix, sequenceWidth, sequenceFormat, rows }: TableDef): this;
10
12
  registerView(viewName: string, definition: ViewDef): this;
13
+ private stampSeedRow;
14
+ private stampNewRow;
15
+ private noteRemoved;
11
16
  private observeSequenceValue;
12
17
  nextSequenceValue(sequencePrefix: string, width: number): string;
18
+ private generateSequenceValue;
13
19
  getAllRows(tableName: string): Row[];
14
20
  getRow(tableName: string, pk: Pk): Row | undefined;
15
21
  rowKey(tableName: string, row: Row): Pk;
@@ -17,12 +23,17 @@ export declare class Store {
17
23
  insertRow(tableName: string, record: Row): Row;
18
24
  updateRow(tableName: string, pk: Pk, patch: Row): Row | undefined;
19
25
  deleteRow(tableName: string, pk: Pk): boolean;
20
- getViewRows(viewName: string): Row[];
26
+ takeRemovedRows(tableName: string, key: Pk): Row[];
27
+ takeRemovedRecordsFor(resourceName: string, row: Row): Row[];
28
+ getViewRows(viewName: string, tables?: Record<string, Row[]>): Row[];
21
29
  private materializeView;
22
30
  getResourceRows(resourceName: string): Row[];
23
31
  getMetadataRows(resourceName: string): Row[];
24
32
  getPkFields(resourceName: string): string[];
25
33
  getRowRef(resourceName: string, row: Row): unknown;
34
+ getRecordRef(resourceName: string, row: Row): unknown;
35
+ private locateRecord;
36
+ private findStoredRecord;
26
37
  rowStubFromKey(resourceName: string, key: Pk): Row;
27
38
  }
28
39
  export {};