@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
@@ -1,21 +1,211 @@
1
- import { deriveFields } from "../db/metadata.js";
1
+ import { eventDefaultOf, eventFields, inferEventTable, knownValues, isDeleteEventName, querySchema, RECORD_IDENTITY_FIELDS, requestFieldsOf, requestReplySchema, resolveEventTarget, resolveSourceSchema, withTypeOverrides, } from "../db/schema.js";
2
2
  import { isLegacyFidelity } from "../protocol/fidelity.js";
3
+ import { BIGDECIMAL_PATTERN, enumClassOf, jsonTypeOf, jvmClassOf, rangeOf, titleCase, } from "../protocol/fieldTypes.js";
4
+ import { dataLogonReplySchema, dataLogonSchema, eventInboundProperties, eventReplySchema, GENESIS_META_SCHEMA, REQUEST_DETAILS_SCHEMA, } from "../protocol/gsfSchemas.js";
3
5
  import { ERROR_CODE, MESSAGE_TYPE } from "../protocol/messageTypes.js";
4
6
  import { resourceNotMappedNack } from "../protocol/msgNack.js";
7
+ // Kept here for existing importers; the resolver lives in db/schema.ts.
8
+ export { inferEventTable };
5
9
  // JSON_SCHEMA_REQUEST shares its request envelope with META_REQUEST (same
6
10
  // DETAILS.FEATURE resource-name field) but its reply is a completely
7
11
  // different shape: Connect.getJSONSchema doesn't read response.DETAILS at
8
12
  // all — the response *is* the SchemaResponse ({ INBOUND, OUTBOUND }, each a
9
- // draft-07 JSON Schema). @genesislcap/foundation-entity-management's
10
- // <entity-management> calls this for every create/update form (fields come
11
- // from INBOUND.properties.DETAILS.properties) and for delete confirmation
12
- // (it reads Object.keys(...properties) to know which fields to copy from the
13
- // selected row into the commit DETAILS). Route this to handleMetaRequest by
14
- // mistake and forms silently render with zero fields, and delete can never
15
- // build a payload — no error, just a dead button. The real success reply is
16
- // JSON_SCHEMA_FEATURE_ACK with INBOUND/OUTBOUND at the top level (legacy mode
17
- // echoed JSON_SCHEMA_REQUEST).
18
- function jsonSchemaType(fieldType) {
13
+ // JSON Schema). Who reads what:
14
+ // - foundation-forms' <foundation-form> (and so entity-management's create
15
+ // and update forms) renders INBOUND.properties.DETAILS of an EVENT: its
16
+ // properties, required, enum, default, title, and genesisType / the
17
+ // org.joda.time.DateTime description for date pickers.
18
+ // - entity-management's delete copies Object.keys(INBOUND...DETAILS
19
+ // .properties) from the selected row into the commit DETAILS.
20
+ // - foundation-forms' <foundation-filters> renders a QUERY's or REQUEST
21
+ // SERVER's rows: OUTBOUND.properties.REPLY.items, else
22
+ // OUTBOUND.$defs[...QueryRow].
23
+ // Route this to handleMetaRequest by mistake and forms silently render with
24
+ // zero fields, and delete can never build a payload — no error, just a dead
25
+ // button.
26
+ // --- events -----------------------------------------------------------------
27
+ // One DETAILS property of an event's INBOUND schema. Nullable fields are
28
+ // oneOf [schema, null]; BigDecimal is a string with a pattern and no
29
+ // genesisType; dates are integers (epoch millis) described as DateTime. Only
30
+ // a table event's DAO class has titles, lengths, ranges and genesisTypes
31
+ // (see meta.ts eventMetaField).
32
+ function eventProperty(field, annotated, appName) {
33
+ const type = field.type;
34
+ const property = {};
35
+ if (annotated && type === 'STRING') {
36
+ if (field.minLength !== undefined)
37
+ property.minLength = field.minLength;
38
+ if (field.maxSize !== undefined)
39
+ property.maxLength = field.maxSize;
40
+ }
41
+ if (type === 'BIGDECIMAL')
42
+ property.pattern = BIGDECIMAL_PATTERN;
43
+ const range = annotated ? rangeOf(type) : undefined;
44
+ if (range)
45
+ [property.minimum, property.maximum] = range;
46
+ property.readOnly = false;
47
+ if (type === 'ENUM' && knownValues(field))
48
+ property.enum = knownValues(field);
49
+ property.description =
50
+ field.description ?? jvmClassOf(type, () => enumClassOf(field.table, field.name, appName));
51
+ const fieldDefault = eventDefaultOf(field);
52
+ if (fieldDefault !== undefined)
53
+ property.default = fieldDefault;
54
+ if (annotated)
55
+ property.title = field.title ?? titleCase(field.name);
56
+ if (annotated && type !== 'BIGDECIMAL')
57
+ property.genesisType = type;
58
+ property.type = jsonTypeOf(type);
59
+ return field.nullable ? { oneOf: [property, { type: 'null' }] } : property;
60
+ }
61
+ // required: the fields that aren't optional (JsonSchema.kt:478), as
62
+ // eventFields resolved them. A sequence key is optional: the database fills
63
+ // it in; a nullable DTO field with no default is still required.
64
+ function isRequired(field) {
65
+ return !field.optional;
66
+ }
67
+ function eventSchema(eventName, target, fields, appName) {
68
+ const annotated = target.kind === 'table';
69
+ const details = {
70
+ properties: Object.fromEntries(fields.map((field) => [field.name, eventProperty(field, annotated, appName)])),
71
+ required: fields.filter(isRequired).map((field) => field.name),
72
+ additionalProperties: false,
73
+ type: 'object',
74
+ };
75
+ return {
76
+ INBOUND: {
77
+ properties: eventInboundProperties(eventName, details),
78
+ required: ['MESSAGE_TYPE', 'SOURCE_REF', 'USER_NAME', 'DETAILS'],
79
+ additionalProperties: true,
80
+ description: target.className,
81
+ ...(target.title ? { title: target.title } : {}),
82
+ $defs: {},
83
+ $schema: GENESIS_META_SCHEMA,
84
+ type: 'object',
85
+ },
86
+ OUTBOUND: eventReplySchema(),
87
+ };
88
+ }
89
+ // --- dataservers and request servers ---------------------------------------
90
+ function typeSchema(field) {
91
+ return {
92
+ type: jsonTypeOf(field.type),
93
+ ...(field.type === 'ENUM' && knownValues(field) ? { enum: knownValues(field) } : {}),
94
+ };
95
+ }
96
+ // A row property in a reply schema: nullable as oneOf [null, schema], with
97
+ // an empty description and the Genesis type (BigDecimal included).
98
+ function rowProperty(field, nullable) {
99
+ return nullable
100
+ ? { oneOf: [{ type: 'null' }, typeSchema(field)], description: '', genesisType: field.type }
101
+ : { ...typeSchema(field), description: '', genesisType: field.type };
102
+ }
103
+ // Every QueryRow property is nullable: a live MODIFY carries only the fields
104
+ // that changed.
105
+ function dataserverSchema(queryName, query, config, store) {
106
+ const fields = querySchema(query, config, store).fields;
107
+ return {
108
+ INBOUND: dataLogonSchema(queryName),
109
+ OUTBOUND: dataLogonReplySchema(Object.fromEntries(fields.map((field) => [field.name, rowProperty(field, true)]))),
110
+ };
111
+ }
112
+ function requestServerSchema(name, requestReply, config, store) {
113
+ const replyFields = requestReplySchema(requestReply, config, store).fields;
114
+ const requestProperties = Object.fromEntries(requestFieldsOf(requestReply, store).map((fieldName) => {
115
+ const field = replyFields.find((candidate) => candidate.name === fieldName);
116
+ const type = field?.type ?? 'STRING';
117
+ return [
118
+ fieldName,
119
+ {
120
+ readOnly: false,
121
+ ...(type === 'ENUM' && field && knownValues(field) ? { enum: knownValues(field) } : {}),
122
+ description: '',
123
+ genesisType: type,
124
+ type: jsonTypeOf(type),
125
+ },
126
+ ];
127
+ }));
128
+ const request = { properties: requestProperties, additionalProperties: false, type: 'object' };
129
+ const fields = requestReply.criteriaOnly
130
+ ? [...replyFields, ...RECORD_IDENTITY_FIELDS]
131
+ : replyFields;
132
+ return {
133
+ INBOUND: {
134
+ properties: {
135
+ MESSAGE_TYPE: { const: `REQ_${name}`, description: 'Message type', type: 'string' },
136
+ SOURCE_REF: { description: 'Source reference', type: 'string' },
137
+ USER_NAME: { description: 'User name', type: 'string' },
138
+ REQUEST: { oneOf: [request, { items: request, type: 'array' }] },
139
+ DETAILS: REQUEST_DETAILS_SCHEMA,
140
+ },
141
+ required: ['MESSAGE_TYPE', 'SOURCE_REF', 'USER_NAME', 'REQUEST'],
142
+ additionalProperties: false,
143
+ description: `GPAL Request definition for ${name}`,
144
+ $defs: {},
145
+ type: 'object',
146
+ },
147
+ OUTBOUND: {
148
+ description: `GPAL Reply definition for ${name}`,
149
+ type: 'object',
150
+ properties: {
151
+ MESSAGE_TYPE: { const: `REP_${name}` },
152
+ SOURCE_REF: { type: 'string' },
153
+ REPLY: {
154
+ type: 'array',
155
+ items: {
156
+ type: 'object',
157
+ properties: Object.fromEntries(fields.map((field) => [field.name, rowProperty(field, field.nullable)])),
158
+ additionalProperties: false,
159
+ required: [],
160
+ },
161
+ },
162
+ NEXT_VIEW: { type: 'number' },
163
+ },
164
+ required: ['MESSAGE_TYPE', 'SOURCE_REF'],
165
+ additionalProperties: false,
166
+ },
167
+ };
168
+ }
169
+ // The FEATURE resolves, in order, as: an entry in config.eventSchemas, a
170
+ // dataserver query, a request server (by resource name, without REQ_), an
171
+ // EVENT_<TABLE>_<VERB> event, and finally a bare table name — the last is
172
+ // this engine's own fallback for clients that ask by table (the real server
173
+ // answers 404 there). Anything else gets the router's 404.
174
+ export function handleJsonSchemaRequest(message, connection, config, store) {
175
+ const feature = message.DETAILS?.FEATURE;
176
+ if (isLegacyFidelity(config)) {
177
+ handleLegacyJsonSchemaRequest(message, connection, config, store, feature);
178
+ return;
179
+ }
180
+ const overridden = feature !== undefined && !!config.eventSchemas?.[feature];
181
+ const query = !overridden && feature ? config.queries?.[feature] : undefined;
182
+ const requestReply = !overridden && feature ? config.requestReplies?.[feature] : undefined;
183
+ let schema;
184
+ if (query) {
185
+ schema = dataserverSchema(feature, query, config, store);
186
+ }
187
+ else if (requestReply) {
188
+ schema = requestServerSchema(feature, requestReply, config, store);
189
+ }
190
+ else {
191
+ const event = resolveEventTarget(feature, config, { bareTable: true });
192
+ if (event) {
193
+ schema = eventSchema(feature, event, eventFields(event, config, store), config.appName);
194
+ }
195
+ }
196
+ if (!schema) {
197
+ connection.reply(message, resourceNotMappedNack(feature));
198
+ return;
199
+ }
200
+ connection.reply(message, { MESSAGE_TYPE: MESSAGE_TYPE.JSON_SCHEMA_FEATURE_ACK, ...schema });
201
+ }
202
+ // --- fidelity: 'legacy' -----------------------------------------------------
203
+ // Up to 15.47 every FEATURE resolved to a table (or view) and got the same
204
+ // minimal schema as INBOUND and OUTBOUND: {type, properties: {DETAILS:
205
+ // {type, properties: {<field>: {type}}, required}}}, `required` being the key
206
+ // on deletes and empty otherwise. A query or request-server name resolved
207
+ // through its `source`. The reply echoed JSON_SCHEMA_REQUEST.
208
+ function legacyJsonSchemaType(fieldType) {
19
209
  switch (fieldType) {
20
210
  case 'LONG':
21
211
  return 'integer';
@@ -27,10 +217,10 @@ function jsonSchemaType(fieldType) {
27
217
  return 'string';
28
218
  }
29
219
  }
30
- function buildSchema(fields, required) {
220
+ function legacySchema(fields, required) {
31
221
  const properties = {};
32
222
  for (const field of fields) {
33
- properties[field.NAME] = { type: jsonSchemaType(field.TYPE) };
223
+ properties[field.NAME] = { type: legacyJsonSchemaType(field.TYPE) };
34
224
  }
35
225
  return {
36
226
  type: 'object',
@@ -39,62 +229,27 @@ function buildSchema(fields, required) {
39
229
  },
40
230
  };
41
231
  }
42
- const DELETE_EVENT_RE = /_DELETE(_|$)/;
43
- // Zero-config default: EVENT_<TABLE>_<VERB...> maps to <TABLE> if it's a
44
- // registered table name (longest match wins, so e.g. a hypothetical
45
- // COUNTERPARTY_AUTH table can't shadow COUNTERPARTY). Covers every event
46
- // naming pattern in this codebase (INSERT, MODIFY, DELETE, plus suffixed
47
- // variants like INSERT_WITH_APPROVAL, DEMO_INSERT, MODIFY_PRICE, INSERT_FAIL)
48
- // without hand-maintaining a table per event. Override via
49
- // config.eventSchemas for names that don't follow this convention.
50
- // Exported for unit tests only.
51
- export function inferEventTable(eventName, tableNames) {
52
- if (!eventName)
53
- return undefined;
54
- // Not every caller asks by event name: entity-management resolves a grid's
55
- // schema from the bare resource/table name (FEATURE: 'TRADE'), so accept an
56
- // exact table match before requiring the EVENT_ prefix. Without this the
57
- // request errors with UNKNOWN_RESOURCE and the form renders no fields.
58
- if (tableNames.includes(eventName))
59
- return eventName;
60
- if (!eventName.startsWith('EVENT_'))
61
- return undefined;
62
- const rest = eventName.slice('EVENT_'.length);
63
- const candidates = tableNames.filter((name) => rest === name || rest.startsWith(`${name}_`));
64
- candidates.sort((a, b) => b.length - a.length);
65
- return candidates[0];
66
- }
67
- export function handleJsonSchemaRequest(message, connection, config, store) {
68
- const eventName = message.DETAILS?.FEATURE;
69
- const override = eventName ? config.eventSchemas?.[eventName] : undefined;
70
- // A DATASERVER/REQUEST_SERVER resource name (e.g. 'ALL_USERS') resolves
71
- // through its own definition to the backing table/view. The FAST-Element
72
- // showcase's entity-management grids ask by query name, not by table or
73
- // event name, so without this their create/update forms render empty.
74
- const resourceSource = eventName
75
- ? (config.queries?.[eventName]?.source ?? config.requestReplies?.[eventName]?.source)
232
+ function handleLegacyJsonSchemaRequest(message, connection, config, store, feature) {
233
+ const override = feature ? config.eventSchemas?.[feature] : undefined;
234
+ const resourceSource = feature
235
+ ? (config.queries?.[feature]?.source ?? config.requestReplies?.[feature]?.source)
76
236
  : undefined;
77
- const table = override?.table ??
78
- resourceSource ??
79
- inferEventTable(eventName, Object.keys(config.tables ?? {}));
80
- const legacy = isLegacyFidelity(config);
237
+ const table = override?.table ?? resourceSource ?? inferEventTable(feature, Object.keys(config.tables ?? {}));
81
238
  if (!table) {
82
- connection.reply(message, legacy
83
- ? {
84
- MESSAGE_TYPE: message.MESSAGE_TYPE,
85
- ERROR: [{ CODE: ERROR_CODE.UNKNOWN_RESOURCE, TEXT: `Unknown resource: ${eventName}` }],
86
- }
87
- : resourceNotMappedNack(eventName));
239
+ connection.reply(message, {
240
+ MESSAGE_TYPE: message.MESSAGE_TYPE,
241
+ ERROR: [{ CODE: ERROR_CODE.UNKNOWN_RESOURCE, TEXT: `Unknown resource: ${feature}` }],
242
+ });
88
243
  return;
89
244
  }
90
- const pkOnly = override?.pkOnly ?? DELETE_EVENT_RE.test(eventName);
245
+ const pkOnly = override?.pkOnly ?? isDeleteEventName(feature);
91
246
  const pkFields = store.getPkFields(table);
92
- let fields = deriveFields(store.getMetadataRows(table), override?.fieldTypes);
247
+ let fields = withTypeOverrides(resolveSourceSchema(table, config, store), override?.fieldTypes).fields.map((field) => ({ NAME: field.name, TYPE: field.type }));
93
248
  if (pkOnly)
94
249
  fields = fields.filter((field) => pkFields.includes(field.NAME));
95
- const schema = buildSchema(fields, pkOnly ? pkFields : []);
250
+ const schema = legacySchema(fields, pkOnly ? pkFields : []);
96
251
  connection.reply(message, {
97
- MESSAGE_TYPE: legacy ? message.MESSAGE_TYPE : MESSAGE_TYPE.JSON_SCHEMA_FEATURE_ACK,
252
+ MESSAGE_TYPE: message.MESSAGE_TYPE,
98
253
  INBOUND: schema,
99
254
  OUTBOUND: schema,
100
255
  });
@@ -1,4 +1,9 @@
1
+ import { type ResolvedField } from '../db/schema.ts';
1
2
  import type { Store } from '../db/store.ts';
2
3
  import type { Connection } from '../protocol/connection.ts';
3
- import type { GenesisMessage, MockServerConfig } from '../types.ts';
4
+ import type { GenesisMessage, MockServerConfig, Row } from '../types.ts';
5
+ export declare function eventMetaField(field: ResolvedField, { annotated, appName }: {
6
+ annotated: boolean;
7
+ appName?: string;
8
+ }): Row;
4
9
  export declare function handleMetaRequest(message: GenesisMessage, connection: Connection, config: MockServerConfig, store: Store): void;
@@ -1,5 +1,7 @@
1
- import { deriveFields } from "../db/metadata.js";
1
+ import { eventDefaultOf, eventFields, knownValues, querySchema, RECORD_IDENTITY_FIELDS, requestFieldsOf, requestReplySchema, resolveEventTarget, } from "../db/schema.js";
2
2
  import { isLegacyFidelity } from "../protocol/fidelity.js";
3
+ import { enumClassOf, eventMetaTypeOf, jsonTypeOf, jvmClassOf, rangeOf, titleCase, } from "../protocol/fieldTypes.js";
4
+ import { eventReplyMeta } from "../protocol/gsfSchemas.js";
3
5
  import { ERROR_CODE, MESSAGE_TYPE } from "../protocol/messageTypes.js";
4
6
  import { resourceNotMappedNack } from "../protocol/msgNack.js";
5
7
  import { hiddenFields, requestingUser } from "./resourceAuth.js";
@@ -9,19 +11,141 @@ import { hiddenFields, requestingUser } from "./resourceAuth.js";
9
11
  function resolveResourceName(message) {
10
12
  return (message.DETAILS?.FEATURE ?? message.DETAILS?.DATASOURCE_NAME ?? message.DETAILS?.RESOURCE_NAME);
11
13
  }
14
+ // A FIELD / REQUEST_FIELD / REPLY_FIELD entry: VALID_VALUES on ENUM fields
15
+ // only (DataServerMetaData.kt, ReqRepMetaData.kt).
16
+ function fieldMetadata(field) {
17
+ return {
18
+ NAME: field.name,
19
+ TYPE: field.type,
20
+ ...(field.type === 'ENUM' && knownValues(field) ? { VALID_VALUES: knownValues(field) } : {}),
21
+ };
22
+ }
23
+ // Index-less queries report the unnamed RECORD_ID index the real dataserver
24
+ // adds (QueryDefinition.DEFAULT_RECORD_ID_INDEX).
25
+ const DEFAULT_INDEXES = [{ NAME: null, FIELDS: 'RECORD_ID' }];
26
+ function dataserverMeta(name, query, config, store, hidden) {
27
+ const fields = querySchema(query, config, store).fields.filter((field) => !hidden.has(field.name));
28
+ return {
29
+ TYPE: 'DATASERVER',
30
+ NAME: name,
31
+ FIELD: fields.map(fieldMetadata),
32
+ INDEXES: query.indexes?.length
33
+ ? query.indexes.map((index) => ({ NAME: index.name, FIELDS: index.fields.join(' ') }))
34
+ : DEFAULT_INDEXES,
35
+ };
36
+ }
37
+ // The two request-server shapes of ReqRepMetaData.kt: criteria-only
38
+ // (REQUEST_FIELD: [], SORTABLE_FIELDS, CRITERIA_FIELDS, RECORD_ID and
39
+ // TIMESTAMP in REPLY_FIELD, no INDICES) and plain (REQUEST_FIELD from the
40
+ // request block, INDICES: [], CRITERIA_ONLY_REQUEST: false).
41
+ function requestServerMeta(name, requestReply, config, store, hidden) {
42
+ const visible = (field) => !hidden.has(field.name);
43
+ const replyFields = requestReplySchema(requestReply, config, store).fields.filter(visible);
44
+ if (requestReply.criteriaOnly) {
45
+ const fields = [...replyFields, ...RECORD_IDENTITY_FIELDS.filter(visible)];
46
+ const names = fields.map((field) => field.name);
47
+ return {
48
+ TYPE: 'REQUEST_SERVER',
49
+ REQUEST_FIELD: [],
50
+ CRITERIA_ONLY_REQUEST: true,
51
+ SORTABLE_FIELDS: names,
52
+ CRITERIA_FIELDS: names,
53
+ REPLY_FIELD: fields.map(fieldMetadata),
54
+ NAME: name,
55
+ };
56
+ }
57
+ const requestFields = requestFieldsOf(requestReply, store).map((fieldName) => replyFields.find((field) => field.name === fieldName) ?? {
58
+ name: fieldName,
59
+ type: 'STRING',
60
+ nullable: false,
61
+ generated: false,
62
+ });
63
+ return {
64
+ TYPE: 'REQUEST_SERVER',
65
+ REQUEST_FIELD: requestFields.map(fieldMetadata),
66
+ INDICES: [],
67
+ CRITERIA_ONLY_REQUEST: false,
68
+ REPLY_FIELD: replyFields.map(fieldMetadata),
69
+ NAME: name,
70
+ };
71
+ }
72
+ // One FIELD entry of an event's META (EventMeta's MetaField, flattened as the
73
+ // real server serializes it). Only a table event's class (a generated DAO)
74
+ // carries the dictionary's annotations: TITLE, MIN_LENGTH / MAX_LENGTH,
75
+ // MIN / MAX and GENESIS_TYPE. A delete's <Table>.ById and a custom DTO have
76
+ // none of them (GENESIS_TYPE null). An ENUM always names its values, its
77
+ // class and a DEFAULT_VALUE (the class's first value when the field has no
78
+ // default); DEFAULT appears only when the field has a default.
79
+ export function eventMetaField(field, { annotated, appName }) {
80
+ const type = field.type;
81
+ const enumClass = () => enumClassOf(field.table, field.name, appName);
82
+ const values = knownValues(field);
83
+ const fieldDefault = eventDefaultOf(field);
84
+ const meta = { NAME: field.name };
85
+ if (annotated)
86
+ meta.TITLE = field.title ?? titleCase(field.name);
87
+ meta.DESCRIPTION = field.description ?? jvmClassOf(type, enumClass);
88
+ meta.NULLABLE = field.nullable;
89
+ if (annotated && type === 'STRING') {
90
+ if (field.minLength !== undefined)
91
+ meta.MIN_LENGTH = field.minLength;
92
+ if (field.maxSize !== undefined)
93
+ meta.MAX_LENGTH = field.maxSize;
94
+ }
95
+ const range = annotated ? rangeOf(type) : undefined;
96
+ if (range)
97
+ [meta.MIN, meta.MAX] = range;
98
+ if (type === 'ENUM') {
99
+ // The enum class's own default: the declared one, else its first value.
100
+ if (values)
101
+ meta.VALID_VALUES = values;
102
+ const typeDefault = field.default ?? values?.[0];
103
+ if (typeDefault !== undefined)
104
+ meta.DEFAULT_VALUE = typeDefault;
105
+ meta.ENUM_CLASS = enumClass();
106
+ }
107
+ meta.TYPE = eventMetaTypeOf(type);
108
+ meta.JSON_TYPE = jsonTypeOf(type);
109
+ if (type !== 'ENUM' && field.default !== undefined)
110
+ meta.DEFAULT_VALUE = field.default;
111
+ // OPTIONAL is about the payload, resolved by eventFields (schema.ts).
112
+ meta.OPTIONAL = field.optional ?? false;
113
+ meta.READ_ONLY = false;
114
+ if (fieldDefault !== undefined)
115
+ meta.DEFAULT = fieldDefault;
116
+ meta.GENESIS_TYPE = annotated ? type : null;
117
+ return meta;
118
+ }
119
+ function eventMeta(name, target, fields, appName) {
120
+ const annotated = target.kind === 'table';
121
+ return {
122
+ NAME: name,
123
+ ...(target.title ? { TITLE: target.title } : {}),
124
+ DESCRIPTION: target.className,
125
+ FIELD: fields.map((field) => eventMetaField(field, { annotated, appName })),
126
+ DEFINITIONS: {},
127
+ REPLY: eventReplyMeta(),
128
+ REQUIRES_APPROVAL: false,
129
+ SCHEDULABLE: false,
130
+ TYPE: 'EVENT_HANDLER',
131
+ };
132
+ }
12
133
  // Connect.getMetadata reads metaResponse.DETAILS (not the top-level message)
13
134
  // and extractFieldDefinitions (metadata.utils.ts) branches on DETAILS.TYPE:
14
135
  // DATASERVER resources need DETAILS.FIELD, REQUEST_SERVER ones need
15
136
  // DETAILS.REPLY_FIELD. Get either wrong and every grid silently reports
16
- // "Metadata Fetch failed" / "Unsupported resource type: undefined".
17
- // The real reply is FEATURE_ACK (getMetadata reads only DETAILS, so the type
18
- // is for fidelity; legacy mode echoed META_REQUEST).
137
+ // "Metadata Fetch failed" / "Unsupported resource type: undefined". Event
138
+ // META is what the AI assistant builds its write tools from
139
+ // (ai-assistant/src/genesis/event-fields.ts): no event META, no write tools.
140
+ // Request servers answer by resource name (TRADE); REQ_TRADE is not a
141
+ // resource name and gets the router's 404, as on the real server.
19
142
  export function handleMetaRequest(message, connection, config, store) {
20
143
  const resourceName = resolveResourceName(message);
21
144
  const query = config.queries?.[resourceName];
22
145
  const requestReply = config.requestReplies?.[resourceName];
23
146
  const legacy = isLegacyFidelity(config);
24
- if (!query && !requestReply) {
147
+ const event = query || requestReply || legacy ? undefined : resolveEventTarget(resourceName, config);
148
+ if (!query && !requestReply && !event) {
25
149
  connection.reply(message, legacy
26
150
  ? {
27
151
  MESSAGE_TYPE: message.MESSAGE_TYPE,
@@ -32,20 +156,39 @@ export function handleMetaRequest(message, connection, config, store) {
32
156
  : resourceNotMappedNack(resourceName));
33
157
  return;
34
158
  }
35
- const resolved = (query ?? requestReply);
36
- // Resolver-only request-replies have no backing table/view — their field
37
- // metadata comes from the def's own metadataRows sample (plus fieldTypes).
38
- const rows = resolved.source
39
- ? store.getMetadataRows(resolved.source)
40
- : (requestReply?.metadataRows ?? []);
41
- // Hidden fields disappear from the metadata too — a column the user can't
42
- // see must not be offered to the grid at all.
43
- const hidden = hiddenFields(resolved.auth, requestingUser(connection));
44
- const fields = deriveFields(rows, resolved.fieldTypes).filter((field) => !hidden.has(field.NAME));
45
- connection.reply(message, {
46
- MESSAGE_TYPE: legacy ? message.MESSAGE_TYPE : MESSAGE_TYPE.FEATURE_ACK,
47
- DETAILS: query
48
- ? { NAME: resourceName, TYPE: 'DATASERVER', FIELD: fields, KEY: [] }
49
- : { NAME: resourceName, TYPE: 'REQUEST_SERVER', REPLY_FIELD: fields },
50
- });
159
+ if (legacy) {
160
+ connection.reply(message, {
161
+ MESSAGE_TYPE: message.MESSAGE_TYPE,
162
+ DETAILS: legacyMeta(resourceName, query, requestReply, connection, config, store),
163
+ });
164
+ return;
165
+ }
166
+ let details;
167
+ if (event) {
168
+ details = eventMeta(resourceName, event, eventFields(event, config, store), config.appName);
169
+ }
170
+ else {
171
+ // Hidden fields disappear from the metadata too — a column the user can't
172
+ // see must not be offered to the grid at all.
173
+ const hidden = hiddenFields((query ?? requestReply).auth, requestingUser(connection));
174
+ details = query
175
+ ? dataserverMeta(resourceName, query, config, store, hidden)
176
+ : requestServerMeta(resourceName, requestReply, config, store, hidden);
177
+ }
178
+ connection.reply(message, { MESSAGE_TYPE: MESSAGE_TYPE.FEATURE_ACK, DETAILS: details });
179
+ }
180
+ // What this engine answered up to 15.47: {NAME, TYPE} fields only, KEY: []
181
+ // on dataservers, REPLY_FIELD alone on request servers, and no event META.
182
+ // Declared types (and query fields blocks) still apply.
183
+ function legacyMeta(resourceName, query, requestReply, connection, config, store) {
184
+ const hidden = hiddenFields((query ?? requestReply).auth, requestingUser(connection));
185
+ const schema = query
186
+ ? querySchema(query, config, store)
187
+ : requestReplySchema(requestReply, config, store);
188
+ const fields = schema.fields
189
+ .filter((field) => !hidden.has(field.name))
190
+ .map((field) => ({ NAME: field.name, TYPE: field.type }));
191
+ return query
192
+ ? { NAME: resourceName, TYPE: 'DATASERVER', FIELD: fields, KEY: [] }
193
+ : { NAME: resourceName, TYPE: 'REQUEST_SERVER', REPLY_FIELD: fields };
51
194
  }
@@ -1,5 +1,6 @@
1
1
  import { filterByCriteria } from "../db/criteria.js";
2
- import { NackError } from "../protocol/errors.js";
2
+ import { withoutRecordFields } from "../db/identity.js";
3
+ import { nackPayload } from "../protocol/errors.js";
3
4
  import { isLegacyFidelity } from "../protocol/fidelity.js";
4
5
  import { ERROR_CODE } from "../protocol/messageTypes.js";
5
6
  import { resourceNotMappedNack } from "../protocol/msgNack.js";
@@ -35,6 +36,7 @@ export function handleRequestReply(message, connection, config, store) {
35
36
  : resourceNotMappedNack(resourceName));
36
37
  return;
37
38
  }
39
+ const legacy = isLegacyFidelity(config);
38
40
  const user = requestingUser(connection);
39
41
  const request = (typeof message.REQUEST === 'object' && message.REQUEST) || message.DETAILS?.REQUEST || {};
40
42
  try {
@@ -58,15 +60,24 @@ export function handleRequestReply(message, connection, config, store) {
58
60
  rows = rows.filter(requestReply.filter);
59
61
  }
60
62
  rows = applyResourceAuth(rows, requestReply.auth, user);
63
+ // Rows read from a table or view carry their RECORD_ID and TIMESTAMP, as
64
+ // a criteria-only request server's do on GSF. Deliberately on every
65
+ // source-backed server, criteriaOnly or not (README: a deviation until
66
+ // OFFSET paging, item 1.9): grid-pro's client-side REQUEST_SERVER
67
+ // datasource keys rows by RECORD_ID and gives up without it. A
68
+ // resolver's rows go out exactly as returned. Legacy mode sent none, a
69
+ // resolver's included (one returning stored rows would now have them).
70
+ if (legacy)
71
+ rows = rows.map(withoutRecordFields);
61
72
  connection.reply(message, {
62
73
  MESSAGE_TYPE: `REP_${resourceName}`,
63
74
  REPLY: rows,
64
75
  });
65
76
  }
66
77
  catch (error) {
67
- const errors = error instanceof NackError
68
- ? error.errors
69
- : [{ CODE: ERROR_CODE.VALIDATION_ERROR, TEXT: error.message }];
70
- connection.reply(message, { MESSAGE_TYPE: `REP_${resourceName}`, ERROR: errors });
78
+ connection.reply(message, {
79
+ MESSAGE_TYPE: `REP_${resourceName}`,
80
+ ...nackPayload(error, legacy),
81
+ });
71
82
  }
72
83
  }
@@ -3,3 +3,4 @@ import type { ResourceAuthDef, RequestingUser, Row } from '../types.ts';
3
3
  export declare function requestingUser(connection: Connection): RequestingUser;
4
4
  export declare function hiddenFields(auth: ResourceAuthDef | undefined, user: RequestingUser): Set<string>;
5
5
  export declare function applyResourceAuth(rows: Row[], auth: ResourceAuthDef | undefined, user: RequestingUser): Row[];
6
+ export declare function permittedQueryRow(row: Row, auth: ResourceAuthDef | undefined, user: RequestingUser, fieldNames: string[] | undefined): Row | undefined;
@@ -1,3 +1,4 @@
1
+ import { projectRow } from "../db/schema.js";
1
2
  // The identity a resource-level auth hook (rowFilter/hideFields) sees for the
2
3
  // requesting connection. userName is undefined for unauthenticated
3
4
  // connections — possible when requireAuth is off, or pre-login.
@@ -25,3 +26,16 @@ export function applyResourceAuth(rows, auth, user) {
25
26
  }
26
27
  return result;
27
28
  }
29
+ // One dataserver row as this user sees it: undefined when rowFilter rejects
30
+ // it, else only the query's columns (projectRow: fieldNames from
31
+ // queryFieldNames) minus the hidden ones. rowFilter sees the whole row, so a
32
+ // filter may read a column the query doesn't expose.
33
+ export function permittedQueryRow(row, auth, user, fieldNames) {
34
+ if (auth?.rowFilter && !auth.rowFilter(row, user))
35
+ return undefined;
36
+ const projected = projectRow(row, fieldNames);
37
+ const hidden = hiddenFields(auth, user);
38
+ if (hidden.size === 0)
39
+ return projected;
40
+ return Object.fromEntries(Object.entries(projected).filter(([field]) => !hidden.has(field)));
41
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createMockServer, type MockServer } from './server.ts';
2
2
  export { launchStack, type LaunchStackOptions, type LaunchedStack } from './launch.ts';
3
3
  export { Store } from './db/store.ts';
4
- export { NackError, type NackErrorInput, type NackErrorEntry } from './protocol/errors.ts';
4
+ export { RECORD_FIELDS, RECORD_ID, TIMESTAMP } from './db/identity.ts';
5
+ export { ErrorCode, NackError, statusCodeFor, type ErrorCodeName, type GenesisErrorType, type NackErrorEntry, type NackErrorInput, type NackErrorOptions, } from './protocol/errors.ts';
5
6
  export { compileCriteria, filterByCriteria, type Predicate } from './db/criteria.ts';
6
- export type { AntiJoinDef, AuthConfig, AuthUser, BroadcastFn, BroadcastTableChangeFn, EventHandler, EventHandlerCtx, EventHandlerResult, EventSchemaOverride, FidelityMode, FieldMetadata, GenesisMessage, HttpRouteDef, JoinDef, MockServerConfig, OnMessageCtx, OnMessageHook, QueryDef, RequestingUser, RequestReplyDef, RequestReplyResolverCtx, ResourceAuthDef, ResourceItem, Row, SocketLike, SsoIdentityProvider, TableDef, ViewDef, } from './types.ts';
7
+ export type { AntiJoinDef, AuthConfig, AuthUser, BroadcastFn, BroadcastTableChangeFn, EventHandler, EventHandlerCtx, EventHandlerResult, EventSchemaOverride, FidelityMode, FieldDef, FieldMetadata, GeneratedFieldDef, GenesisFieldType, GenesisMessage, HttpRouteDef, IndexDef, JoinDef, MockServerConfig, OnMessageCtx, OnMessageHook, QueryDef, RequestingUser, RequestReplyDef, RequestReplyResolverCtx, ResourceAuthDef, ResourceItem, Row, SequenceFormat, SocketLike, SsoIdentityProvider, TableDef, ViewDef, } from './types.ts';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { createMockServer } from "./server.js";
2
2
  export { launchStack } from "./launch.js";
3
3
  export { Store } from "./db/store.js";
4
- export { NackError } from "./protocol/errors.js";
4
+ export { RECORD_FIELDS, RECORD_ID, TIMESTAMP } from "./db/identity.js";
5
+ export { ErrorCode, NackError, statusCodeFor, } from "./protocol/errors.js";
5
6
  export { compileCriteria, filterByCriteria } from "./db/criteria.js";
@@ -9,6 +9,8 @@ export interface Subscription {
9
9
  held?: Set<string>;
10
10
  maxRows?: number;
11
11
  maxView?: number;
12
+ fieldNames?: string[];
13
+ queueInsert?: (key: string, stamped: Row, row: Row) => boolean;
12
14
  }
13
15
  export declare class Connection {
14
16
  readonly ws: SocketLike;