@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,490 @@
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 { RECORD_FIELDS, RECORD_ID, TIMESTAMP } from "./identity.js";
9
+ import { deriveFields } from "./metadata.js";
10
+ export function pkFieldsOf(def) {
11
+ return Array.isArray(def.pkField) ? def.pkField : [def.pkField];
12
+ }
13
+ // The declared generated fields, plus the key of a table that mints its own
14
+ // keys through sequencePrefix.
15
+ export function generatedFieldsOf(def) {
16
+ return new Set(generatedFieldList(def));
17
+ }
18
+ // The same fields in the order GSF lists a table's generated fields
19
+ // (EventHandlerUtils.kt generatedFields: the id sequences, then the
20
+ // auto-increment fields): the sequencePrefix key first, then the declared
21
+ // SEQUENCE fields, then the AUTO_INCREMENT ones.
22
+ export function generatedFieldList(def) {
23
+ const pkFields = pkFieldsOf(def);
24
+ const declared = def.generated ?? [];
25
+ const fields = [
26
+ ...(def.sequencePrefix && pkFields.length === 1 ? [pkFields[0]] : []),
27
+ ...declared.filter((entry) => entry.kind === 'SEQUENCE').map((entry) => entry.field),
28
+ ...declared.filter((entry) => entry.kind === 'AUTO_INCREMENT').map((entry) => entry.field),
29
+ ];
30
+ return [...new Set(fields)];
31
+ }
32
+ function fromFieldDef(field, table, notNullByDefault, generated) {
33
+ return {
34
+ name: field.name,
35
+ type: field.type,
36
+ nullable: field.nullable ?? !notNullByDefault,
37
+ generated,
38
+ ...(field.values ? { values: field.values } : {}),
39
+ ...(field.default !== undefined ? { default: field.default } : {}),
40
+ ...(field.maxSize !== undefined ? { maxSize: field.maxSize } : {}),
41
+ ...(field.minLength !== undefined ? { minLength: field.minLength } : {}),
42
+ ...(field.description !== undefined ? { description: field.description } : {}),
43
+ ...(field.title !== undefined ? { title: field.title } : {}),
44
+ ...(field.optional !== undefined ? { optional: field.optional } : {}),
45
+ ...(table ? { table } : {}),
46
+ };
47
+ }
48
+ function declaredTableFields(tableName, def) {
49
+ const pkFields = pkFieldsOf(def);
50
+ const generated = generatedFieldsOf(def);
51
+ return (def.fields ?? []).map((field) => fromFieldDef(field, tableName, pkFields.includes(field.name) || generated.has(field.name), generated.has(field.name)));
52
+ }
53
+ // Inference from rows: a type per column (metadata.ts), nothing nullable but
54
+ // the key, no defaults or enum values (rows can't say).
55
+ function inferredFields(rows, pkFields, generated, table) {
56
+ return deriveFields(rows).map(({ NAME, TYPE }) => ({
57
+ name: NAME,
58
+ type: TYPE,
59
+ nullable: !(pkFields.includes(NAME) || generated.has(NAME)),
60
+ generated: generated.has(NAME),
61
+ ...(table ? { table } : {}),
62
+ }));
63
+ }
64
+ function derivedTypeDef(name, entry) {
65
+ return typeof entry === 'string' ? { name, type: entry } : { ...entry, name };
66
+ }
67
+ function viewOf(name, config, store) {
68
+ return store.views.get(name) ?? config.views?.[name];
69
+ }
70
+ // A view over a declared base table: the base table's columns, each join's
71
+ // selected columns typed from the joined table (nullable, since a base row
72
+ // may have no match), and the derived fields typed from derivedTypes (or
73
+ // inferred from the view's rows when not given) — narrowed to the view's
74
+ // fields block when it has one.
75
+ function declaredViewFields(viewName, view, config, store) {
76
+ const fields = new Map();
77
+ for (const field of resolveSourceSchema(view.base, config, store).fields) {
78
+ fields.set(field.name, field);
79
+ }
80
+ for (const join of view.joins ?? []) {
81
+ const joined = resolveSourceSchema(join.table, config, store).fields;
82
+ for (const [alias, sourceField] of Object.entries(join.select ?? {})) {
83
+ const source = joined.find((field) => field.name === sourceField);
84
+ const { title: _title, ...rest } = source ?? {
85
+ name: alias,
86
+ type: 'STRING',
87
+ nullable: true,
88
+ generated: false,
89
+ };
90
+ fields.set(alias, { ...rest, name: alias, nullable: true, generated: false });
91
+ }
92
+ }
93
+ const derivedNames = Object.keys(view.derived ?? {});
94
+ const untyped = derivedNames.filter((name) => !view.derivedTypes?.[name]);
95
+ const inferred = untyped.length > 0
96
+ ? deriveFields(store
97
+ .getMetadataRows(viewName)
98
+ .map((row) => Object.fromEntries(untyped.map((name) => [name, row[name]]))))
99
+ : [];
100
+ for (const name of derivedNames) {
101
+ const typed = view.derivedTypes?.[name];
102
+ fields.set(name, typed
103
+ ? fromFieldDef(derivedTypeDef(name, typed), undefined, false, false)
104
+ : {
105
+ name,
106
+ type: inferred.find((field) => field.NAME === name)?.TYPE ?? 'STRING',
107
+ nullable: true,
108
+ generated: false,
109
+ });
110
+ }
111
+ return view.fields ? pick(fields, view.fields) : [...fields.values()];
112
+ }
113
+ function pick(fields, names) {
114
+ return names.flatMap((name) => {
115
+ const field = fields.get(name);
116
+ return field ? [field] : [];
117
+ });
118
+ }
119
+ // Whether a table (or a view, through its base table) declares its fields.
120
+ // Checked without resolving the schema, so it costs nothing for a big
121
+ // undeclared table.
122
+ export function isDeclared(source, config, store) {
123
+ const table = config.tables?.[source];
124
+ if (table)
125
+ return !!table.fields;
126
+ const view = viewOf(source, config, store);
127
+ return view ? isDeclared(view.base, config, store) : false;
128
+ }
129
+ // The fields of a table or view. Unknown names resolve to no fields. Under
130
+ // fidelity: 'legacy' everything is inferred, as up to 15.47: declarations,
131
+ // fields blocks and derivedTypes are ignored.
132
+ export function resolveSourceSchema(source, config, store) {
133
+ const legacy = isLegacyFidelity(config);
134
+ const table = config.tables?.[source];
135
+ if (!legacy && table?.fields) {
136
+ return { fields: declaredTableFields(source, table), declared: true };
137
+ }
138
+ const view = viewOf(source, config, store);
139
+ if (!legacy && view && isDeclared(view.base, config, store)) {
140
+ return { fields: declaredViewFields(source, view, config, store), declared: true };
141
+ }
142
+ const generated = table ? generatedFieldsOf(table) : new Set();
143
+ let fields = inferredFields(store.getMetadataRows(source), store.getPkFields(source), generated, table ? source : view?.base);
144
+ if (legacy)
145
+ return { fields, declared: false };
146
+ // derivedTypes and the fields block describe a view even when the rest of
147
+ // it is inferred.
148
+ if (view?.derivedTypes) {
149
+ fields = fields.map((field) => {
150
+ const typed = view.derivedTypes?.[field.name];
151
+ return typed
152
+ ? fromFieldDef(derivedTypeDef(field.name, typed), undefined, false, false)
153
+ : field;
154
+ });
155
+ }
156
+ if (view?.fields)
157
+ fields = pick(new Map(fields.map((field) => [field.name, field])), view.fields);
158
+ return { fields, declared: false };
159
+ }
160
+ // fieldTypes overrides the TYPE of the named fields, declared or inferred.
161
+ export function withTypeOverrides(schema, fieldTypes) {
162
+ if (!fieldTypes)
163
+ return schema;
164
+ return {
165
+ ...schema,
166
+ fields: schema.fields.map((field) => fieldTypes[field.name] ? { ...field, type: fieldTypes[field.name] } : field),
167
+ };
168
+ }
169
+ // A query's fields block: those fields only, in its order. Names the source
170
+ // doesn't have are dropped, as the real dataserver drops them.
171
+ export function projectSchema(schema, names) {
172
+ if (!names)
173
+ return schema;
174
+ const byName = new Map(schema.fields.map((field) => [field.name, field]));
175
+ return {
176
+ ...schema,
177
+ fields: names.flatMap((name) => {
178
+ const field = byName.get(name);
179
+ return field ? [field] : [];
180
+ }),
181
+ };
182
+ }
183
+ export function querySchema(query, config, store) {
184
+ return projectSchema(withTypeOverrides(resolveSourceSchema(query.source, config, store), query.fieldTypes), isLegacyFidelity(config) ? undefined : query.fields);
185
+ }
186
+ // A resolver-only request server has no table: its fields come from the
187
+ // def's metadataRows sample.
188
+ export function requestReplySchema(requestReply, config, store) {
189
+ const schema = requestReply.source
190
+ ? resolveSourceSchema(requestReply.source, config, store)
191
+ : {
192
+ fields: inferredFields(requestReply.metadataRows ?? [], [], new Set(), undefined),
193
+ declared: false,
194
+ };
195
+ return withTypeOverrides(schema, requestReply.fieldTypes);
196
+ }
197
+ // The record identity every table entity carries after its own columns: what
198
+ // a request server with no reply block sends in each row and lists in
199
+ // REPLY_FIELD, and what a criteria-only one can sort and filter by.
200
+ export const RECORD_IDENTITY_FIELDS = [
201
+ { name: RECORD_ID, type: 'LONG', nullable: false, generated: true },
202
+ { name: TIMESTAMP, type: 'NANO_TIMESTAMP', nullable: false, generated: true },
203
+ ];
204
+ const withIdentity = (fields) => [
205
+ ...fields.filter((field) => !RECORD_FIELDS.includes(field.name)),
206
+ ...RECORD_IDENTITY_FIELDS,
207
+ ];
208
+ // What a request server's database entity has: its source's fields, then
209
+ // RECORD_ID and TIMESTAMP (a resolver's: its metadataRows). A criteria-only
210
+ // server sorts and filters by these (SORTABLE_FIELDS, CRITERIA_FIELDS), and
211
+ // REQUEST may name any of them, whatever the reply block says.
212
+ export function requestReplySourceSchema(requestReply, config, store) {
213
+ const schema = requestReplySchema(requestReply, config, store);
214
+ return requestReply.source && !requestReply.resolver
215
+ ? { ...schema, fields: withIdentity(schema.fields) }
216
+ : schema;
217
+ }
218
+ // What a request server's REP_ rows carry (getReplyFields in
219
+ // RequestReplyServer.kt, GSF v8.15.29): the reply block (`fields`), else the
220
+ // source's fields followed by RECORD_ID and TIMESTAMP — a table entity's own
221
+ // field list, which the real server sends whether or not the server is
222
+ // criteria-only (auth's RIGHT sends no RECORD_ID only because its reply block
223
+ // names CODE and DESCRIPTION). REPLY_FIELD and the JSON schema's REPLY come
224
+ // from here. A resolver's fields are its metadataRows'; under fidelity:
225
+ // 'legacy' everything is the source schema, as up to 15.47.
226
+ export function requestReplyReplySchema(requestReply, config, store) {
227
+ if (isLegacyFidelity(config))
228
+ return requestReplySchema(requestReply, config, store);
229
+ const schema = requestReplySourceSchema(requestReply, config, store);
230
+ if (!requestReply.fields || !requestReply.source || requestReply.resolver)
231
+ return schema;
232
+ return projectSchema(schema, requestReply.fields);
233
+ }
234
+ // The columns a source-backed request server's REP_ rows carry, in order (see
235
+ // requestReplyReplySchema). undefined — rows as stored, which already end
236
+ // with their RECORD_ID and TIMESTAMP — for an undeclared source with no reply
237
+ // block, for a resolver, and under fidelity: 'legacy'.
238
+ export function requestReplyFieldNames(requestReply, config, store) {
239
+ if (isLegacyFidelity(config) || !requestReply.source || requestReply.resolver)
240
+ return undefined;
241
+ const declared = sourceFieldNames(requestReply.source, config, store);
242
+ const names = declared && [
243
+ ...declared.filter((name) => !RECORD_FIELDS.includes(name)),
244
+ ...RECORD_FIELDS,
245
+ ];
246
+ if (!requestReply.fields)
247
+ return names;
248
+ return names ? requestReply.fields.filter((name) => names.includes(name)) : requestReply.fields;
249
+ }
250
+ // The request block's fields: requestFields, else the source's key.
251
+ export function requestFieldsOf(requestReply, store) {
252
+ if (requestReply.requestFields)
253
+ return requestReply.requestFields;
254
+ return requestReply.source ? store.getPkFields(requestReply.source) : [];
255
+ }
256
+ // An ENUM's values, or undefined when none are known: a client must then be
257
+ // told nothing rather than an empty list (grid-pro builds an empty select from
258
+ // VALID_VALUES: [], and a JSON schema `enum: []` accepts no value at all).
259
+ export function knownValues(field) {
260
+ return field.values?.length ? field.values : undefined;
261
+ }
262
+ // The DEFAULT an event field reports (META DEFAULT, JSON schema `default`).
263
+ // Only an optional property has one, and it is its type's default
264
+ // (PALMetadataUtils.kt:159, getPropertyDefaultValue, GSF v8.15.29): the
265
+ // dictionary default, or for an ENUM with none the enum's first value
266
+ // (PALMetadataUtils.kt:205, EnumType defaultValue = ... ?: jsonValues.firstOrNull()).
267
+ export function eventDefaultOf(field) {
268
+ if (!field.optional)
269
+ return undefined;
270
+ if (field.default !== undefined)
271
+ return field.default;
272
+ return field.type === 'ENUM' ? knownValues(field)?.[0] : undefined;
273
+ }
274
+ // --- query rows --------------------------------------------------------------
275
+ function sourceFieldNames(source, config, store) {
276
+ const table = config.tables?.[source];
277
+ if (table)
278
+ return table.fields?.map((field) => field.name);
279
+ const view = viewOf(source, config, store);
280
+ if (!view)
281
+ return undefined;
282
+ const base = sourceFieldNames(view.base, config, store);
283
+ // An undeclared base: the view's own fields block is all that is known.
284
+ if (!base)
285
+ return view.fields;
286
+ const names = new Set(base);
287
+ for (const join of view.joins ?? []) {
288
+ for (const alias of Object.keys(join.select ?? {}))
289
+ names.add(alias);
290
+ }
291
+ for (const name of Object.keys(view.derived ?? {}))
292
+ names.add(name);
293
+ return view.fields ? view.fields.filter((name) => names.has(name)) : [...names];
294
+ }
295
+ // The columns a query's rows carry, in order: its fields block, else every
296
+ // declared column of its source (a view's own fields block included).
297
+ // undefined when neither is known, and always under fidelity: 'legacy': rows
298
+ // go out as stored.
299
+ export function queryFieldNames(query, config, store) {
300
+ if (!query || isLegacyFidelity(config))
301
+ return undefined;
302
+ const names = sourceFieldNames(query.source, config, store);
303
+ if (!query.fields)
304
+ return names;
305
+ return names ? query.fields.filter((name) => names.includes(name)) : query.fields;
306
+ }
307
+ // A row with exactly these columns. The real dataserver sends every column it
308
+ // exposes, null when unset, so a missing value goes out as null.
309
+ export function projectRow(row, names) {
310
+ if (!names)
311
+ return row;
312
+ return Object.fromEntries(names.map((name) => [name, row[name] ?? null]));
313
+ }
314
+ // --- indexes -----------------------------------------------------------------
315
+ // The name the dictionary gives an index declared without one
316
+ // (DslFieldContext.kt generateIndexName, GSF v8.15.29): '<TABLE>_BY_' and the
317
+ // fields, each without a leading '<TABLE>_', joined with '_' — so
318
+ // primaryKey("COUNTERPARTY_ID") is COUNTERPARTY_BY_ID and
319
+ // unique("INSTRUMENT_ID") on POSITION is POSITION_BY_INSTRUMENT_ID.
320
+ export function defaultIndexName(table, fields) {
321
+ const prefix = `${table}_`;
322
+ const names = fields.map((field) => (field.startsWith(prefix) && field.slice(prefix.length)) || field);
323
+ return `${prefix}BY_${names.join('_')}`;
324
+ }
325
+ const sameFields = (a, b) => a.length === b.length && a.every((field, index) => field === b[index]);
326
+ // The primary key's index name: a declared unique index over exactly the key
327
+ // fields names it, else the dictionary default. NACKs name it in FIELD.
328
+ export function primaryKeyIndexName(table, def) {
329
+ const pkFields = pkFieldsOf(def);
330
+ const declared = def.indexes?.find((index) => index.unique && sameFields(index.fields, pkFields));
331
+ return declared?.name ?? defaultIndexName(table, pkFields);
332
+ }
333
+ // The table's other unique indexes, each a constraint an insert or modify
334
+ // must not break.
335
+ export function secondaryUniqueIndexes(def) {
336
+ const pkFields = pkFieldsOf(def);
337
+ return (def.indexes ?? []).filter((index) => index.unique && !sameFields(index.fields, pkFields));
338
+ }
339
+ // --- events ------------------------------------------------------------------
340
+ export const CRUD_EVENT_VERBS = ['INSERT', 'MODIFY', 'DELETE'];
341
+ // A generic event's name: Create names the handler '<TABLE>_<VERB>'
342
+ // (as GSF's GenericInsertEventHandler.messageType() does too) and the event
343
+ // handler serves it as EVENT_<TABLE>_<VERB>.
344
+ export function crudEventName(table, verb) {
345
+ return `EVENT_${table}_${verb}`;
346
+ }
347
+ // The table and verb of a generic CRUD event a table registers through
348
+ // TableDef.events, or undefined.
349
+ export function crudEventOf(name, config) {
350
+ if (!name?.startsWith('EVENT_'))
351
+ return undefined;
352
+ for (const [table, def] of Object.entries(config.tables ?? {})) {
353
+ for (const verb of def.events ?? []) {
354
+ if (crudEventName(table, verb) === name)
355
+ return { table, verb };
356
+ }
357
+ }
358
+ return undefined;
359
+ }
360
+ // Every generic CRUD event the tables register, in table then verb order.
361
+ export function crudEventNames(config) {
362
+ return Object.entries(config.tables ?? {}).flatMap(([table, def]) => CRUD_EVENT_VERBS.filter((verb) => def.events?.includes(verb)).map((verb) => crudEventName(table, verb)));
363
+ }
364
+ // Any event named .../_DELETE(_...)? takes only the key fields, which is what
365
+ // entity-management's delete confirmation copies from the selected row.
366
+ export function isDeleteEventName(name) {
367
+ return /_DELETE(_|$)/.test(name);
368
+ }
369
+ // Zero-config default: EVENT_<TABLE>_<VERB...> maps to <TABLE> if it's a
370
+ // registered table name (longest match wins, so e.g. a hypothetical
371
+ // COUNTERPARTY_AUTH table can't shadow COUNTERPARTY). Covers every event
372
+ // naming pattern in this codebase (INSERT, MODIFY, DELETE, plus suffixed
373
+ // variants like INSERT_WITH_APPROVAL, DEMO_INSERT, MODIFY_PRICE, INSERT_FAIL)
374
+ // without hand-maintaining a table per event. Override via
375
+ // config.eventSchemas for names that don't follow this convention.
376
+ export function inferEventTable(eventName, tableNames) {
377
+ if (!eventName)
378
+ return undefined;
379
+ // Not every caller asks by event name: entity-management resolves a grid's
380
+ // schema from the bare resource/table name (FEATURE: 'TRADE'), so accept an
381
+ // exact table match before requiring the EVENT_ prefix. Without this the
382
+ // request errors with UNKNOWN_RESOURCE and the form renders no fields.
383
+ if (tableNames.includes(eventName))
384
+ return eventName;
385
+ if (!eventName.startsWith('EVENT_'))
386
+ return undefined;
387
+ const rest = eventName.slice('EVENT_'.length);
388
+ const candidates = tableNames.filter((name) => rest === name || rest.startsWith(`${name}_`));
389
+ candidates.sort((a, b) => b.length - a.length);
390
+ return candidates[0];
391
+ }
392
+ // The table an event writes: config.eventSchemas first, then the table that
393
+ // registers it as a generic CRUD event, then the EVENT_<TABLE>_<VERB>
394
+ // convention. Only a registered event (an eventHandlers or eventSchemas
395
+ // entry, or a TableDef.events verb) has metadata, as only a deployed event
396
+ // handler has on the real server; anything else is the router's 404.
397
+ // `bareTable` also accepts a plain table name, which only JSON_SCHEMA_REQUEST
398
+ // does (see the README).
399
+ export function resolveEventTarget(name, config, { bareTable = false } = {}) {
400
+ if (!name)
401
+ return undefined;
402
+ const override = config.eventSchemas?.[name];
403
+ const crud = crudEventOf(name, config);
404
+ if (name.startsWith('EVENT_') && !override && !config.eventHandlers?.[name] && !crud) {
405
+ return undefined;
406
+ }
407
+ const tableNames = Object.keys(config.tables ?? {});
408
+ const table = override?.table ??
409
+ crud?.table ??
410
+ (bareTable || name.startsWith('EVENT_') ? inferEventTable(name, tableNames) : undefined);
411
+ const fieldTypes = override?.fieldTypes ? { fieldTypes: override.fieldTypes } : {};
412
+ if (override?.fields) {
413
+ return {
414
+ kind: 'dto',
415
+ ...(table ? { table } : {}),
416
+ className: override.className ?? daoClassOf(table ?? name.replace(/^EVENT_/, '')),
417
+ fields: override.fields,
418
+ ...fieldTypes,
419
+ };
420
+ }
421
+ if (!table)
422
+ return undefined;
423
+ const byId = override?.pkOnly ?? isDeleteEventName(name);
424
+ return byId
425
+ ? {
426
+ kind: 'byId',
427
+ table,
428
+ className: override?.className ?? `${daoClassOf(table)}.ById`,
429
+ ...fieldTypes,
430
+ }
431
+ : {
432
+ kind: 'table',
433
+ table,
434
+ className: override?.className ?? daoClassOf(table),
435
+ title: titleCase(table),
436
+ ...fieldTypes,
437
+ };
438
+ }
439
+ // Kotlin reflection lists a class's properties sorted by their (camelCase)
440
+ // names, and the real server only then turns them into UPPER_UNDERSCORE —
441
+ // so DATE_FIELD (dateField) sorts before DATETIME_FIELD (datetimeField),
442
+ // which a plain sort of the field names would reverse.
443
+ function propertyName(field) {
444
+ return field.name
445
+ .toLowerCase()
446
+ .replace(/_+([a-z0-9])/g, (_match, char) => char.toUpperCase());
447
+ }
448
+ const byName = (a, b) => {
449
+ const [left, right] = [propertyName(a), propertyName(b)];
450
+ return left < right ? -1 : left > right ? 1 : 0;
451
+ };
452
+ // The fields an event's DETAILS take, sorted by name as the real server
453
+ // lists a class's properties — except a delete's, which are the key fields
454
+ // in key order, all mandatory. A key field the schema doesn't know (an
455
+ // inferred table with no rows) is still listed, as a STRING. `optional` is
456
+ // always resolved: FieldDef.optional when given, else whether the class
457
+ // property has a default (PALMetadataUtils.kt mapMemberProperty: optional =
458
+ // propertyType != Mandatory). A generated DAO gives every nullable and every
459
+ // generated column a default; a custom DTO's defaults are invisible, so a
460
+ // DTO field is optional only when it declares a default or `optional: true`.
461
+ export function eventFields(target, config, store) {
462
+ if (target.kind === 'dto') {
463
+ const resolved = (target.fields ?? []).map((field) => fromFieldDef(field, target.table, false, false));
464
+ return withTypeOverrides({ fields: resolved, declared: true }, target.fieldTypes)
465
+ .fields.map((field) => ({
466
+ ...field,
467
+ optional: field.optional ?? field.default !== undefined,
468
+ }))
469
+ .sort(byName);
470
+ }
471
+ const table = target.table;
472
+ const { fields } = withTypeOverrides(resolveSourceSchema(table, config, store), target.fieldTypes);
473
+ if (target.kind === 'byId') {
474
+ return store.getPkFields(table).map((name) => {
475
+ const { default: _default, ...field } = fields.find((candidate) => candidate.name === name) ?? {
476
+ name,
477
+ type: 'STRING',
478
+ nullable: false,
479
+ generated: false,
480
+ };
481
+ return { ...field, nullable: false, generated: false, optional: false };
482
+ });
483
+ }
484
+ return fields
485
+ .map((field) => ({
486
+ ...field,
487
+ optional: field.optional ?? (field.generated || field.nullable || field.default !== undefined),
488
+ }))
489
+ .sort(byName);
490
+ }
@@ -4,25 +4,42 @@ export declare class Store {
4
4
  private tables;
5
5
  private tableConfig;
6
6
  private sequenceCounters;
7
+ private autoIncrementCounters;
8
+ private clock;
7
9
  private seedRows;
10
+ private removedRows;
8
11
  readonly views: Map<string, ViewDef>;
9
- registerTable(tableName: string, { pkField, sequencePrefix, sequenceWidth, rows }: TableDef): this;
12
+ registerTable(tableName: string, definition: TableDef): this;
13
+ private counterOf;
14
+ private countersFor;
15
+ private generatedNumber;
10
16
  registerView(viewName: string, definition: ViewDef): this;
11
- private observeSequenceValue;
17
+ private stampSeedRow;
18
+ private stampNewRow;
19
+ private noteRemoved;
20
+ private observeGeneratedValues;
12
21
  nextSequenceValue(sequencePrefix: string, width: number): string;
22
+ private generateSequenceValue;
23
+ private generateValues;
13
24
  getAllRows(tableName: string): Row[];
14
25
  getRow(tableName: string, pk: Pk): Row | undefined;
15
26
  rowKey(tableName: string, row: Row): Pk;
16
27
  findRow(tableName: string, criteria: Row): Row | undefined;
28
+ withGeneratedValues(tableName: string, record: Row): Row;
17
29
  insertRow(tableName: string, record: Row): Row;
18
30
  updateRow(tableName: string, pk: Pk, patch: Row): Row | undefined;
19
31
  deleteRow(tableName: string, pk: Pk): boolean;
20
- getViewRows(viewName: string): Row[];
32
+ takeRemovedRows(tableName: string, key: Pk): Row[];
33
+ takeRemovedRecordsFor(resourceName: string, row: Row): Row[];
34
+ getViewRows(viewName: string, tables?: Record<string, Row[]>): Row[];
21
35
  private materializeView;
22
36
  getResourceRows(resourceName: string): Row[];
23
37
  getMetadataRows(resourceName: string): Row[];
24
38
  getPkFields(resourceName: string): string[];
25
39
  getRowRef(resourceName: string, row: Row): unknown;
40
+ getRecordRef(resourceName: string, row: Row): unknown;
41
+ private locateRecord;
42
+ private findStoredRecord;
26
43
  rowStubFromKey(resourceName: string, key: Pk): Row;
27
44
  }
28
45
  export {};