@genesislcap/mock-server 15.51.0 → 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
@@ -1,26 +1,284 @@
1
- import { deriveFields } from '../db/metadata.ts';
1
+ import {
2
+ eventDefaultOf,
3
+ eventFields,
4
+ inferEventTable,
5
+ knownValues,
6
+ isDeleteEventName,
7
+ querySchema,
8
+ RECORD_IDENTITY_FIELDS,
9
+ requestFieldsOf,
10
+ requestReplySchema,
11
+ resolveEventTarget,
12
+ resolveSourceSchema,
13
+ withTypeOverrides,
14
+ type EventTarget,
15
+ type ResolvedField,
16
+ } from '../db/schema.ts';
2
17
  import type { Store } from '../db/store.ts';
3
18
  import type { Connection } from '../protocol/connection.ts';
4
19
  import { isLegacyFidelity } from '../protocol/fidelity.ts';
20
+ import {
21
+ BIGDECIMAL_PATTERN,
22
+ enumClassOf,
23
+ jsonTypeOf,
24
+ jvmClassOf,
25
+ rangeOf,
26
+ titleCase,
27
+ } from '../protocol/fieldTypes.ts';
28
+ import {
29
+ dataLogonReplySchema,
30
+ dataLogonSchema,
31
+ eventInboundProperties,
32
+ eventReplySchema,
33
+ GENESIS_META_SCHEMA,
34
+ REQUEST_DETAILS_SCHEMA,
35
+ } from '../protocol/gsfSchemas.ts';
5
36
  import { ERROR_CODE, MESSAGE_TYPE } from '../protocol/messageTypes.ts';
6
37
  import { resourceNotMappedNack } from '../protocol/msgNack.ts';
7
- import type { FieldMetadata, GenesisMessage, MockServerConfig, Row } from '../types.ts';
38
+ import type {
39
+ FieldMetadata,
40
+ GenesisMessage,
41
+ MockServerConfig,
42
+ QueryDef,
43
+ RequestReplyDef,
44
+ Row,
45
+ } from '../types.ts';
46
+
47
+ // Kept here for existing importers; the resolver lives in db/schema.ts.
48
+ export { inferEventTable };
8
49
 
9
50
  // JSON_SCHEMA_REQUEST shares its request envelope with META_REQUEST (same
10
51
  // DETAILS.FEATURE resource-name field) but its reply is a completely
11
52
  // different shape: Connect.getJSONSchema doesn't read response.DETAILS at
12
53
  // all — the response *is* the SchemaResponse ({ INBOUND, OUTBOUND }, each a
13
- // draft-07 JSON Schema). @genesislcap/foundation-entity-management's
14
- // <entity-management> calls this for every create/update form (fields come
15
- // from INBOUND.properties.DETAILS.properties) and for delete confirmation
16
- // (it reads Object.keys(...properties) to know which fields to copy from the
17
- // selected row into the commit DETAILS). Route this to handleMetaRequest by
18
- // mistake and forms silently render with zero fields, and delete can never
19
- // build a payload — no error, just a dead button. The real success reply is
20
- // JSON_SCHEMA_FEATURE_ACK with INBOUND/OUTBOUND at the top level (legacy mode
21
- // echoed JSON_SCHEMA_REQUEST).
22
-
23
- function jsonSchemaType(fieldType: string): string {
54
+ // JSON Schema). Who reads what:
55
+ // - foundation-forms' <foundation-form> (and so entity-management's create
56
+ // and update forms) renders INBOUND.properties.DETAILS of an EVENT: its
57
+ // properties, required, enum, default, title, and genesisType / the
58
+ // org.joda.time.DateTime description for date pickers.
59
+ // - entity-management's delete copies Object.keys(INBOUND...DETAILS
60
+ // .properties) from the selected row into the commit DETAILS.
61
+ // - foundation-forms' <foundation-filters> renders a QUERY's or REQUEST
62
+ // SERVER's rows: OUTBOUND.properties.REPLY.items, else
63
+ // OUTBOUND.$defs[...QueryRow].
64
+ // Route this to handleMetaRequest by mistake and forms silently render with
65
+ // zero fields, and delete can never build a payload — no error, just a dead
66
+ // button.
67
+
68
+ // --- events -----------------------------------------------------------------
69
+
70
+ // One DETAILS property of an event's INBOUND schema. Nullable fields are
71
+ // oneOf [schema, null]; BigDecimal is a string with a pattern and no
72
+ // genesisType; dates are integers (epoch millis) described as DateTime. Only
73
+ // a table event's DAO class has titles, lengths, ranges and genesisTypes
74
+ // (see meta.ts eventMetaField).
75
+ function eventProperty(field: ResolvedField, annotated: boolean, appName?: string): Row {
76
+ const type = field.type;
77
+ const property: Row = {};
78
+ if (annotated && type === 'STRING') {
79
+ if (field.minLength !== undefined) property.minLength = field.minLength;
80
+ if (field.maxSize !== undefined) property.maxLength = field.maxSize;
81
+ }
82
+ if (type === 'BIGDECIMAL') property.pattern = BIGDECIMAL_PATTERN;
83
+ const range = annotated ? rangeOf(type) : undefined;
84
+ if (range) [property.minimum, property.maximum] = range;
85
+ property.readOnly = false;
86
+ if (type === 'ENUM' && knownValues(field)) property.enum = knownValues(field);
87
+ property.description =
88
+ field.description ?? jvmClassOf(type, () => enumClassOf(field.table, field.name, appName));
89
+ const fieldDefault = eventDefaultOf(field);
90
+ if (fieldDefault !== undefined) property.default = fieldDefault;
91
+ if (annotated) property.title = field.title ?? titleCase(field.name);
92
+ if (annotated && type !== 'BIGDECIMAL') property.genesisType = type;
93
+ property.type = jsonTypeOf(type);
94
+ return field.nullable ? { oneOf: [property, { type: 'null' }] } : property;
95
+ }
96
+
97
+ // required: the fields that aren't optional (JsonSchema.kt:478), as
98
+ // eventFields resolved them. A sequence key is optional: the database fills
99
+ // it in; a nullable DTO field with no default is still required.
100
+ function isRequired(field: ResolvedField): boolean {
101
+ return !field.optional;
102
+ }
103
+
104
+ function eventSchema(
105
+ eventName: string,
106
+ target: EventTarget,
107
+ fields: ResolvedField[],
108
+ appName: string | undefined,
109
+ ): { INBOUND: Row; OUTBOUND: Row } {
110
+ const annotated = target.kind === 'table';
111
+ const details = {
112
+ properties: Object.fromEntries(
113
+ fields.map((field) => [field.name, eventProperty(field, annotated, appName)]),
114
+ ),
115
+ required: fields.filter(isRequired).map((field) => field.name),
116
+ additionalProperties: false,
117
+ type: 'object',
118
+ };
119
+ return {
120
+ INBOUND: {
121
+ properties: eventInboundProperties(eventName, details),
122
+ required: ['MESSAGE_TYPE', 'SOURCE_REF', 'USER_NAME', 'DETAILS'],
123
+ additionalProperties: true,
124
+ description: target.className,
125
+ ...(target.title ? { title: target.title } : {}),
126
+ $defs: {},
127
+ $schema: GENESIS_META_SCHEMA,
128
+ type: 'object',
129
+ },
130
+ OUTBOUND: eventReplySchema(),
131
+ };
132
+ }
133
+
134
+ // --- dataservers and request servers ---------------------------------------
135
+
136
+ function typeSchema(field: ResolvedField): Row {
137
+ return {
138
+ type: jsonTypeOf(field.type),
139
+ ...(field.type === 'ENUM' && knownValues(field) ? { enum: knownValues(field) } : {}),
140
+ };
141
+ }
142
+
143
+ // A row property in a reply schema: nullable as oneOf [null, schema], with
144
+ // an empty description and the Genesis type (BigDecimal included).
145
+ function rowProperty(field: ResolvedField, nullable: boolean): Row {
146
+ return nullable
147
+ ? { oneOf: [{ type: 'null' }, typeSchema(field)], description: '', genesisType: field.type }
148
+ : { ...typeSchema(field), description: '', genesisType: field.type };
149
+ }
150
+
151
+ // Every QueryRow property is nullable: a live MODIFY carries only the fields
152
+ // that changed.
153
+ function dataserverSchema(
154
+ queryName: string,
155
+ query: QueryDef,
156
+ config: MockServerConfig,
157
+ store: Store,
158
+ ): { INBOUND: Row; OUTBOUND: Row } {
159
+ const fields = querySchema(query, config, store).fields;
160
+ return {
161
+ INBOUND: dataLogonSchema(queryName),
162
+ OUTBOUND: dataLogonReplySchema(
163
+ Object.fromEntries(fields.map((field) => [field.name, rowProperty(field, true)])),
164
+ ),
165
+ };
166
+ }
167
+
168
+ function requestServerSchema(
169
+ name: string,
170
+ requestReply: RequestReplyDef,
171
+ config: MockServerConfig,
172
+ store: Store,
173
+ ): { INBOUND: Row; OUTBOUND: Row } {
174
+ const replyFields = requestReplySchema(requestReply, config, store).fields;
175
+ const requestProperties = Object.fromEntries(
176
+ requestFieldsOf(requestReply, store).map((fieldName) => {
177
+ const field = replyFields.find((candidate) => candidate.name === fieldName);
178
+ const type = field?.type ?? 'STRING';
179
+ return [
180
+ fieldName,
181
+ {
182
+ readOnly: false,
183
+ ...(type === 'ENUM' && field && knownValues(field) ? { enum: knownValues(field) } : {}),
184
+ description: '',
185
+ genesisType: type,
186
+ type: jsonTypeOf(type),
187
+ },
188
+ ];
189
+ }),
190
+ );
191
+ const request = { properties: requestProperties, additionalProperties: false, type: 'object' };
192
+ const fields = requestReply.criteriaOnly
193
+ ? [...replyFields, ...RECORD_IDENTITY_FIELDS]
194
+ : replyFields;
195
+ return {
196
+ INBOUND: {
197
+ properties: {
198
+ MESSAGE_TYPE: { const: `REQ_${name}`, description: 'Message type', type: 'string' },
199
+ SOURCE_REF: { description: 'Source reference', type: 'string' },
200
+ USER_NAME: { description: 'User name', type: 'string' },
201
+ REQUEST: { oneOf: [request, { items: request, type: 'array' }] },
202
+ DETAILS: REQUEST_DETAILS_SCHEMA,
203
+ },
204
+ required: ['MESSAGE_TYPE', 'SOURCE_REF', 'USER_NAME', 'REQUEST'],
205
+ additionalProperties: false,
206
+ description: `GPAL Request definition for ${name}`,
207
+ $defs: {},
208
+ type: 'object',
209
+ },
210
+ OUTBOUND: {
211
+ description: `GPAL Reply definition for ${name}`,
212
+ type: 'object',
213
+ properties: {
214
+ MESSAGE_TYPE: { const: `REP_${name}` },
215
+ SOURCE_REF: { type: 'string' },
216
+ REPLY: {
217
+ type: 'array',
218
+ items: {
219
+ type: 'object',
220
+ properties: Object.fromEntries(
221
+ fields.map((field) => [field.name, rowProperty(field, field.nullable)]),
222
+ ),
223
+ additionalProperties: false,
224
+ required: [],
225
+ },
226
+ },
227
+ NEXT_VIEW: { type: 'number' },
228
+ },
229
+ required: ['MESSAGE_TYPE', 'SOURCE_REF'],
230
+ additionalProperties: false,
231
+ },
232
+ };
233
+ }
234
+
235
+ // The FEATURE resolves, in order, as: an entry in config.eventSchemas, a
236
+ // dataserver query, a request server (by resource name, without REQ_), an
237
+ // EVENT_<TABLE>_<VERB> event, and finally a bare table name — the last is
238
+ // this engine's own fallback for clients that ask by table (the real server
239
+ // answers 404 there). Anything else gets the router's 404.
240
+ export function handleJsonSchemaRequest(
241
+ message: GenesisMessage,
242
+ connection: Connection,
243
+ config: MockServerConfig,
244
+ store: Store,
245
+ ): void {
246
+ const feature: string | undefined = message.DETAILS?.FEATURE;
247
+ if (isLegacyFidelity(config)) {
248
+ handleLegacyJsonSchemaRequest(message, connection, config, store, feature);
249
+ return;
250
+ }
251
+
252
+ const overridden = feature !== undefined && !!config.eventSchemas?.[feature];
253
+ const query = !overridden && feature ? config.queries?.[feature] : undefined;
254
+ const requestReply = !overridden && feature ? config.requestReplies?.[feature] : undefined;
255
+ let schema: { INBOUND: Row; OUTBOUND: Row } | undefined;
256
+ if (query) {
257
+ schema = dataserverSchema(feature!, query, config, store);
258
+ } else if (requestReply) {
259
+ schema = requestServerSchema(feature!, requestReply, config, store);
260
+ } else {
261
+ const event = resolveEventTarget(feature, config, { bareTable: true });
262
+ if (event) {
263
+ schema = eventSchema(feature!, event, eventFields(event, config, store), config.appName);
264
+ }
265
+ }
266
+
267
+ if (!schema) {
268
+ connection.reply(message, resourceNotMappedNack(feature));
269
+ return;
270
+ }
271
+ connection.reply(message, { MESSAGE_TYPE: MESSAGE_TYPE.JSON_SCHEMA_FEATURE_ACK, ...schema });
272
+ }
273
+
274
+ // --- fidelity: 'legacy' -----------------------------------------------------
275
+ // Up to 15.47 every FEATURE resolved to a table (or view) and got the same
276
+ // minimal schema as INBOUND and OUTBOUND: {type, properties: {DETAILS:
277
+ // {type, properties: {<field>: {type}}, required}}}, `required` being the key
278
+ // on deletes and empty otherwise. A query or request-server name resolved
279
+ // through its `source`. The reply echoed JSON_SCHEMA_REQUEST.
280
+
281
+ function legacyJsonSchemaType(fieldType: string): string {
24
282
  switch (fieldType) {
25
283
  case 'LONG':
26
284
  return 'integer';
@@ -33,10 +291,10 @@ function jsonSchemaType(fieldType: string): string {
33
291
  }
34
292
  }
35
293
 
36
- function buildSchema(fields: FieldMetadata[], required: string[]): Row {
294
+ function legacySchema(fields: FieldMetadata[], required: string[]): Row {
37
295
  const properties: Row = {};
38
296
  for (const field of fields) {
39
- properties[field.NAME] = { type: jsonSchemaType(field.TYPE) };
297
+ properties[field.NAME] = { type: legacyJsonSchemaType(field.TYPE) };
40
298
  }
41
299
  return {
42
300
  type: 'object',
@@ -46,76 +304,39 @@ function buildSchema(fields: FieldMetadata[], required: string[]): Row {
46
304
  };
47
305
  }
48
306
 
49
- const DELETE_EVENT_RE = /_DELETE(_|$)/;
50
-
51
- // Zero-config default: EVENT_<TABLE>_<VERB...> maps to <TABLE> if it's a
52
- // registered table name (longest match wins, so e.g. a hypothetical
53
- // COUNTERPARTY_AUTH table can't shadow COUNTERPARTY). Covers every event
54
- // naming pattern in this codebase (INSERT, MODIFY, DELETE, plus suffixed
55
- // variants like INSERT_WITH_APPROVAL, DEMO_INSERT, MODIFY_PRICE, INSERT_FAIL)
56
- // without hand-maintaining a table per event. Override via
57
- // config.eventSchemas for names that don't follow this convention.
58
- // Exported for unit tests only.
59
- export function inferEventTable(
60
- eventName: string | undefined,
61
- tableNames: string[],
62
- ): string | undefined {
63
- if (!eventName) return undefined;
64
- // Not every caller asks by event name: entity-management resolves a grid's
65
- // schema from the bare resource/table name (FEATURE: 'TRADE'), so accept an
66
- // exact table match before requiring the EVENT_ prefix. Without this the
67
- // request errors with UNKNOWN_RESOURCE and the form renders no fields.
68
- if (tableNames.includes(eventName)) return eventName;
69
- if (!eventName.startsWith('EVENT_')) return undefined;
70
- const rest = eventName.slice('EVENT_'.length);
71
- const candidates = tableNames.filter((name) => rest === name || rest.startsWith(`${name}_`));
72
- candidates.sort((a, b) => b.length - a.length);
73
- return candidates[0];
74
- }
75
-
76
- export function handleJsonSchemaRequest(
307
+ function handleLegacyJsonSchemaRequest(
77
308
  message: GenesisMessage,
78
309
  connection: Connection,
79
310
  config: MockServerConfig,
80
311
  store: Store,
312
+ feature: string | undefined,
81
313
  ): void {
82
- const eventName: string | undefined = message.DETAILS?.FEATURE;
83
- const override = eventName ? config.eventSchemas?.[eventName] : undefined;
84
- // A DATASERVER/REQUEST_SERVER resource name (e.g. 'ALL_USERS') resolves
85
- // through its own definition to the backing table/view. The FAST-Element
86
- // showcase's entity-management grids ask by query name, not by table or
87
- // event name, so without this their create/update forms render empty.
88
- const resourceSource = eventName
89
- ? (config.queries?.[eventName]?.source ?? config.requestReplies?.[eventName]?.source)
314
+ const override = feature ? config.eventSchemas?.[feature] : undefined;
315
+ const resourceSource = feature
316
+ ? (config.queries?.[feature]?.source ?? config.requestReplies?.[feature]?.source)
90
317
  : undefined;
91
318
  const table =
92
- override?.table ??
93
- resourceSource ??
94
- inferEventTable(eventName, Object.keys(config.tables ?? {}));
319
+ override?.table ?? resourceSource ?? inferEventTable(feature, Object.keys(config.tables ?? {}));
95
320
 
96
- const legacy = isLegacyFidelity(config);
97
321
  if (!table) {
98
- connection.reply(
99
- message,
100
- legacy
101
- ? {
102
- MESSAGE_TYPE: message.MESSAGE_TYPE,
103
- ERROR: [{ CODE: ERROR_CODE.UNKNOWN_RESOURCE, TEXT: `Unknown resource: ${eventName}` }],
104
- }
105
- : resourceNotMappedNack(eventName),
106
- );
322
+ connection.reply(message, {
323
+ MESSAGE_TYPE: message.MESSAGE_TYPE,
324
+ ERROR: [{ CODE: ERROR_CODE.UNKNOWN_RESOURCE, TEXT: `Unknown resource: ${feature}` }],
325
+ });
107
326
  return;
108
327
  }
109
328
 
110
- const pkOnly = override?.pkOnly ?? DELETE_EVENT_RE.test(eventName!);
329
+ const pkOnly = override?.pkOnly ?? isDeleteEventName(feature!);
111
330
  const pkFields = store.getPkFields(table);
112
- let fields = deriveFields(store.getMetadataRows(table), override?.fieldTypes);
331
+ let fields: FieldMetadata[] = withTypeOverrides(
332
+ resolveSourceSchema(table, config, store),
333
+ override?.fieldTypes,
334
+ ).fields.map((field) => ({ NAME: field.name, TYPE: field.type }));
113
335
  if (pkOnly) fields = fields.filter((field) => pkFields.includes(field.NAME));
114
336
 
115
- const schema = buildSchema(fields, pkOnly ? pkFields : []);
116
-
337
+ const schema = legacySchema(fields, pkOnly ? pkFields : []);
117
338
  connection.reply(message, {
118
- MESSAGE_TYPE: legacy ? message.MESSAGE_TYPE : MESSAGE_TYPE.JSON_SCHEMA_FEATURE_ACK,
339
+ MESSAGE_TYPE: message.MESSAGE_TYPE,
119
340
  INBOUND: schema,
120
341
  OUTBOUND: schema,
121
342
  });
@@ -1,10 +1,37 @@
1
- import { deriveFields } from '../db/metadata.ts';
1
+ import {
2
+ eventDefaultOf,
3
+ eventFields,
4
+ knownValues,
5
+ querySchema,
6
+ RECORD_IDENTITY_FIELDS,
7
+ requestFieldsOf,
8
+ requestReplySchema,
9
+ resolveEventTarget,
10
+ type EventTarget,
11
+ type ResolvedField,
12
+ } from '../db/schema.ts';
2
13
  import type { Store } from '../db/store.ts';
3
14
  import type { Connection } from '../protocol/connection.ts';
4
15
  import { isLegacyFidelity } from '../protocol/fidelity.ts';
16
+ import {
17
+ enumClassOf,
18
+ eventMetaTypeOf,
19
+ jsonTypeOf,
20
+ jvmClassOf,
21
+ rangeOf,
22
+ titleCase,
23
+ } from '../protocol/fieldTypes.ts';
24
+ import { eventReplyMeta } from '../protocol/gsfSchemas.ts';
5
25
  import { ERROR_CODE, MESSAGE_TYPE } from '../protocol/messageTypes.ts';
6
26
  import { resourceNotMappedNack } from '../protocol/msgNack.ts';
7
- import type { GenesisMessage, MockServerConfig, Row } from '../types.ts';
27
+ import type {
28
+ FieldMetadata,
29
+ GenesisMessage,
30
+ MockServerConfig,
31
+ QueryDef,
32
+ RequestReplyDef,
33
+ Row,
34
+ } from '../types.ts';
8
35
  import { hiddenFields, requestingUser } from './resourceAuth.ts';
9
36
 
10
37
  // createMetaRequestMessage (foundation-comms/src/connect/message.ts) puts the
@@ -16,13 +43,157 @@ function resolveResourceName(message: GenesisMessage): string {
16
43
  );
17
44
  }
18
45
 
46
+ // A FIELD / REQUEST_FIELD / REPLY_FIELD entry: VALID_VALUES on ENUM fields
47
+ // only (DataServerMetaData.kt, ReqRepMetaData.kt).
48
+ function fieldMetadata(field: ResolvedField): FieldMetadata {
49
+ return {
50
+ NAME: field.name,
51
+ TYPE: field.type,
52
+ ...(field.type === 'ENUM' && knownValues(field) ? { VALID_VALUES: knownValues(field) } : {}),
53
+ };
54
+ }
55
+
56
+ // Index-less queries report the unnamed RECORD_ID index the real dataserver
57
+ // adds (QueryDefinition.DEFAULT_RECORD_ID_INDEX).
58
+ const DEFAULT_INDEXES = [{ NAME: null, FIELDS: 'RECORD_ID' }];
59
+
60
+ function dataserverMeta(
61
+ name: string,
62
+ query: QueryDef,
63
+ config: MockServerConfig,
64
+ store: Store,
65
+ hidden: Set<string>,
66
+ ): Row {
67
+ const fields = querySchema(query, config, store).fields.filter(
68
+ (field) => !hidden.has(field.name),
69
+ );
70
+ return {
71
+ TYPE: 'DATASERVER',
72
+ NAME: name,
73
+ FIELD: fields.map(fieldMetadata),
74
+ INDEXES: query.indexes?.length
75
+ ? query.indexes.map((index) => ({ NAME: index.name, FIELDS: index.fields.join(' ') }))
76
+ : DEFAULT_INDEXES,
77
+ };
78
+ }
79
+
80
+ // The two request-server shapes of ReqRepMetaData.kt: criteria-only
81
+ // (REQUEST_FIELD: [], SORTABLE_FIELDS, CRITERIA_FIELDS, RECORD_ID and
82
+ // TIMESTAMP in REPLY_FIELD, no INDICES) and plain (REQUEST_FIELD from the
83
+ // request block, INDICES: [], CRITERIA_ONLY_REQUEST: false).
84
+ function requestServerMeta(
85
+ name: string,
86
+ requestReply: RequestReplyDef,
87
+ config: MockServerConfig,
88
+ store: Store,
89
+ hidden: Set<string>,
90
+ ): Row {
91
+ const visible = (field: ResolvedField) => !hidden.has(field.name);
92
+ const replyFields = requestReplySchema(requestReply, config, store).fields.filter(visible);
93
+ if (requestReply.criteriaOnly) {
94
+ const fields = [...replyFields, ...RECORD_IDENTITY_FIELDS.filter(visible)];
95
+ const names = fields.map((field) => field.name);
96
+ return {
97
+ TYPE: 'REQUEST_SERVER',
98
+ REQUEST_FIELD: [],
99
+ CRITERIA_ONLY_REQUEST: true,
100
+ SORTABLE_FIELDS: names,
101
+ CRITERIA_FIELDS: names,
102
+ REPLY_FIELD: fields.map(fieldMetadata),
103
+ NAME: name,
104
+ };
105
+ }
106
+ const requestFields = requestFieldsOf(requestReply, store).map(
107
+ (fieldName) =>
108
+ replyFields.find((field) => field.name === fieldName) ?? {
109
+ name: fieldName,
110
+ type: 'STRING',
111
+ nullable: false,
112
+ generated: false,
113
+ },
114
+ );
115
+ return {
116
+ TYPE: 'REQUEST_SERVER',
117
+ REQUEST_FIELD: requestFields.map(fieldMetadata),
118
+ INDICES: [],
119
+ CRITERIA_ONLY_REQUEST: false,
120
+ REPLY_FIELD: replyFields.map(fieldMetadata),
121
+ NAME: name,
122
+ };
123
+ }
124
+
125
+ // One FIELD entry of an event's META (EventMeta's MetaField, flattened as the
126
+ // real server serializes it). Only a table event's class (a generated DAO)
127
+ // carries the dictionary's annotations: TITLE, MIN_LENGTH / MAX_LENGTH,
128
+ // MIN / MAX and GENESIS_TYPE. A delete's <Table>.ById and a custom DTO have
129
+ // none of them (GENESIS_TYPE null). An ENUM always names its values, its
130
+ // class and a DEFAULT_VALUE (the class's first value when the field has no
131
+ // default); DEFAULT appears only when the field has a default.
132
+ export function eventMetaField(
133
+ field: ResolvedField,
134
+ { annotated, appName }: { annotated: boolean; appName?: string },
135
+ ): Row {
136
+ const type = field.type;
137
+ const enumClass = () => enumClassOf(field.table, field.name, appName);
138
+ const values = knownValues(field);
139
+ const fieldDefault = eventDefaultOf(field);
140
+ const meta: Row = { NAME: field.name };
141
+ if (annotated) meta.TITLE = field.title ?? titleCase(field.name);
142
+ meta.DESCRIPTION = field.description ?? jvmClassOf(type, enumClass);
143
+ meta.NULLABLE = field.nullable;
144
+ if (annotated && type === 'STRING') {
145
+ if (field.minLength !== undefined) meta.MIN_LENGTH = field.minLength;
146
+ if (field.maxSize !== undefined) meta.MAX_LENGTH = field.maxSize;
147
+ }
148
+ const range = annotated ? rangeOf(type) : undefined;
149
+ if (range) [meta.MIN, meta.MAX] = range;
150
+ if (type === 'ENUM') {
151
+ // The enum class's own default: the declared one, else its first value.
152
+ if (values) meta.VALID_VALUES = values;
153
+ const typeDefault = field.default ?? values?.[0];
154
+ if (typeDefault !== undefined) meta.DEFAULT_VALUE = typeDefault;
155
+ meta.ENUM_CLASS = enumClass();
156
+ }
157
+ meta.TYPE = eventMetaTypeOf(type);
158
+ meta.JSON_TYPE = jsonTypeOf(type);
159
+ if (type !== 'ENUM' && field.default !== undefined) meta.DEFAULT_VALUE = field.default;
160
+ // OPTIONAL is about the payload, resolved by eventFields (schema.ts).
161
+ meta.OPTIONAL = field.optional ?? false;
162
+ meta.READ_ONLY = false;
163
+ if (fieldDefault !== undefined) meta.DEFAULT = fieldDefault;
164
+ meta.GENESIS_TYPE = annotated ? type : null;
165
+ return meta;
166
+ }
167
+
168
+ function eventMeta(
169
+ name: string,
170
+ target: EventTarget,
171
+ fields: ResolvedField[],
172
+ appName: string | undefined,
173
+ ): Row {
174
+ const annotated = target.kind === 'table';
175
+ return {
176
+ NAME: name,
177
+ ...(target.title ? { TITLE: target.title } : {}),
178
+ DESCRIPTION: target.className,
179
+ FIELD: fields.map((field) => eventMetaField(field, { annotated, appName })),
180
+ DEFINITIONS: {},
181
+ REPLY: eventReplyMeta(),
182
+ REQUIRES_APPROVAL: false,
183
+ SCHEDULABLE: false,
184
+ TYPE: 'EVENT_HANDLER',
185
+ };
186
+ }
187
+
19
188
  // Connect.getMetadata reads metaResponse.DETAILS (not the top-level message)
20
189
  // and extractFieldDefinitions (metadata.utils.ts) branches on DETAILS.TYPE:
21
190
  // DATASERVER resources need DETAILS.FIELD, REQUEST_SERVER ones need
22
191
  // DETAILS.REPLY_FIELD. Get either wrong and every grid silently reports
23
- // "Metadata Fetch failed" / "Unsupported resource type: undefined".
24
- // The real reply is FEATURE_ACK (getMetadata reads only DETAILS, so the type
25
- // is for fidelity; legacy mode echoed META_REQUEST).
192
+ // "Metadata Fetch failed" / "Unsupported resource type: undefined". Event
193
+ // META is what the AI assistant builds its write tools from
194
+ // (ai-assistant/src/genesis/event-fields.ts): no event META, no write tools.
195
+ // Request servers answer by resource name (TRADE); REQ_TRADE is not a
196
+ // resource name and gets the router's 404, as on the real server.
26
197
  export function handleMetaRequest(
27
198
  message: GenesisMessage,
28
199
  connection: Connection,
@@ -32,9 +203,11 @@ export function handleMetaRequest(
32
203
  const resourceName = resolveResourceName(message);
33
204
  const query = config.queries?.[resourceName];
34
205
  const requestReply = config.requestReplies?.[resourceName];
35
-
36
206
  const legacy = isLegacyFidelity(config);
37
- if (!query && !requestReply) {
207
+ const event =
208
+ query || requestReply || legacy ? undefined : resolveEventTarget(resourceName, config);
209
+
210
+ if (!query && !requestReply && !event) {
38
211
  connection.reply(
39
212
  message,
40
213
  legacy
@@ -49,21 +222,47 @@ export function handleMetaRequest(
49
222
  return;
50
223
  }
51
224
 
52
- const resolved = (query ?? requestReply)!;
53
- // Resolver-only request-replies have no backing table/view — their field
54
- // metadata comes from the def's own metadataRows sample (plus fieldTypes).
55
- const rows: Row[] = resolved.source
56
- ? store.getMetadataRows(resolved.source)
57
- : (requestReply?.metadataRows ?? []);
58
- // Hidden fields disappear from the metadata too — a column the user can't
59
- // see must not be offered to the grid at all.
60
- const hidden = hiddenFields(resolved.auth, requestingUser(connection));
61
- const fields = deriveFields(rows, resolved.fieldTypes).filter((field) => !hidden.has(field.NAME));
225
+ if (legacy) {
226
+ connection.reply(message, {
227
+ MESSAGE_TYPE: message.MESSAGE_TYPE,
228
+ DETAILS: legacyMeta(resourceName, query, requestReply, connection, config, store),
229
+ });
230
+ return;
231
+ }
232
+
233
+ let details: Row;
234
+ if (event) {
235
+ details = eventMeta(resourceName, event, eventFields(event, config, store), config.appName);
236
+ } else {
237
+ // Hidden fields disappear from the metadata too — a column the user can't
238
+ // see must not be offered to the grid at all.
239
+ const hidden = hiddenFields((query ?? requestReply)!.auth, requestingUser(connection));
240
+ details = query
241
+ ? dataserverMeta(resourceName, query, config, store, hidden)
242
+ : requestServerMeta(resourceName, requestReply!, config, store, hidden);
243
+ }
244
+ connection.reply(message, { MESSAGE_TYPE: MESSAGE_TYPE.FEATURE_ACK, DETAILS: details });
245
+ }
62
246
 
63
- connection.reply(message, {
64
- MESSAGE_TYPE: legacy ? message.MESSAGE_TYPE : MESSAGE_TYPE.FEATURE_ACK,
65
- DETAILS: query
66
- ? { NAME: resourceName, TYPE: 'DATASERVER', FIELD: fields, KEY: [] }
67
- : { NAME: resourceName, TYPE: 'REQUEST_SERVER', REPLY_FIELD: fields },
68
- });
247
+ // What this engine answered up to 15.47: {NAME, TYPE} fields only, KEY: []
248
+ // on dataservers, REPLY_FIELD alone on request servers, and no event META.
249
+ // Declared types (and query fields blocks) still apply.
250
+ function legacyMeta(
251
+ resourceName: string,
252
+ query: QueryDef | undefined,
253
+ requestReply: RequestReplyDef | undefined,
254
+ connection: Connection,
255
+ config: MockServerConfig,
256
+ store: Store,
257
+ ): Row {
258
+ const hidden = hiddenFields((query ?? requestReply)!.auth, requestingUser(connection));
259
+ const schema = query
260
+ ? querySchema(query, config, store)
261
+ : requestReplySchema(requestReply!, config, store);
262
+ const fields = schema.fields
263
+ .filter((field) => !hidden.has(field.name))
264
+ .map((field) => ({ NAME: field.name, TYPE: field.type }));
265
+ return query
266
+ ? { NAME: resourceName, TYPE: 'DATASERVER', FIELD: fields, KEY: [] }
267
+ : { NAME: resourceName, TYPE: 'REQUEST_SERVER', REPLY_FIELD: fields };
69
268
  }