@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.
- package/README.md +217 -31
- package/dist/db/criteria.d.ts +1 -0
- package/dist/db/criteria.js +29 -0
- package/dist/db/identity.d.ts +11 -0
- package/dist/db/identity.js +46 -0
- package/dist/db/schema.d.ts +46 -0
- package/dist/db/schema.js +375 -0
- package/dist/db/store.d.ts +13 -2
- package/dist/db/store.js +208 -34
- package/dist/handlers/commitEvent.js +20 -6
- package/dist/handlers/dataLogon.js +126 -6
- package/dist/handlers/jsonSchema.d.ts +2 -1
- package/dist/handlers/jsonSchema.js +216 -61
- package/dist/handlers/meta.d.ts +6 -1
- package/dist/handlers/meta.js +164 -21
- package/dist/handlers/requestReply.js +16 -5
- package/dist/handlers/resourceAuth.d.ts +1 -0
- package/dist/handlers/resourceAuth.js +14 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/protocol/connection.d.ts +2 -0
- package/dist/protocol/errors.d.ts +100 -2
- package/dist/protocol/errors.js +300 -12
- package/dist/protocol/fieldTypes.d.ts +9 -0
- package/dist/protocol/fieldTypes.js +101 -0
- package/dist/protocol/gsfSchemas.d.ts +9 -0
- package/dist/protocol/gsfSchemas.js +423 -0
- package/dist/protocol/messageTypes.d.ts +1 -0
- package/dist/protocol/messageTypes.js +1 -0
- package/dist/protocol/rowUpdate.d.ts +5 -3
- package/dist/protocol/rowUpdate.js +33 -15
- package/dist/server.js +90 -28
- package/dist/types.d.ts +40 -0
- package/package.json +1 -1
- package/src/db/criteria.ts +26 -0
- package/src/db/identity.ts +53 -0
- package/src/db/schema.ts +523 -0
- package/src/db/store.ts +225 -32
- package/src/handlers/commitEvent.ts +20 -7
- package/src/handlers/dataLogon.ts +144 -7
- package/src/handlers/jsonSchema.ts +290 -69
- package/src/handlers/meta.ts +222 -23
- package/src/handlers/requestReply.ts +15 -6
- package/src/handlers/resourceAuth.ts +18 -0
- package/src/index.ts +16 -1
- package/src/protocol/connection.ts +10 -2
- package/src/protocol/errors.ts +361 -13
- package/src/protocol/fieldTypes.ts +108 -0
- package/src/protocol/gsfSchemas.ts +505 -0
- package/src/protocol/messageTypes.ts +1 -0
- package/src/protocol/rowUpdate.ts +40 -20
- package/src/server.ts +101 -27
- package/src/types.ts +145 -1
|
@@ -1,21 +1,211 @@
|
|
|
1
|
-
import {
|
|
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
|
-
//
|
|
10
|
-
// <
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
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
|
|
220
|
+
function legacySchema(fields, required) {
|
|
31
221
|
const properties = {};
|
|
32
222
|
for (const field of fields) {
|
|
33
|
-
properties[field.NAME] = { 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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 ??
|
|
245
|
+
const pkOnly = override?.pkOnly ?? isDeleteEventName(feature);
|
|
91
246
|
const pkFields = store.getPkFields(table);
|
|
92
|
-
let fields =
|
|
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 =
|
|
250
|
+
const schema = legacySchema(fields, pkOnly ? pkFields : []);
|
|
96
251
|
connection.reply(message, {
|
|
97
|
-
MESSAGE_TYPE:
|
|
252
|
+
MESSAGE_TYPE: message.MESSAGE_TYPE,
|
|
98
253
|
INBOUND: schema,
|
|
99
254
|
OUTBOUND: schema,
|
|
100
255
|
});
|
package/dist/handlers/meta.d.ts
CHANGED
|
@@ -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;
|
package/dist/handlers/meta.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
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
|
-
//
|
|
18
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 {
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 {
|
|
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 {
|
|
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";
|