@genesislcap/mock-server 15.52.0 → 15.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -27
- package/dist/db/criteria.d.ts +12 -0
- package/dist/db/criteria.js +39 -29
- package/dist/db/ordering.d.ts +8 -0
- package/dist/db/ordering.js +49 -0
- package/dist/db/schema.d.ts +17 -1
- package/dist/db/schema.js +131 -16
- package/dist/db/store.d.ts +8 -2
- package/dist/db/store.js +120 -44
- package/dist/handlers/commitEvent.js +32 -8
- package/dist/handlers/crudEvents.d.ts +2 -0
- package/dist/handlers/crudEvents.js +138 -0
- package/dist/handlers/dataLogon.js +2 -23
- package/dist/handlers/eventValidation.d.ts +5 -0
- package/dist/handlers/eventValidation.js +29 -0
- package/dist/handlers/jsonSchema.d.ts +3 -2
- package/dist/handlers/jsonSchema.js +23 -17
- package/dist/handlers/meta.js +13 -9
- package/dist/handlers/requestReply.js +518 -40
- package/dist/handlers/resources.js +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/protocol/fieldTypes.d.ts +1 -0
- package/dist/protocol/fieldTypes.js +6 -0
- package/dist/protocol/msgNack.d.ts +2 -0
- package/dist/protocol/msgNack.js +10 -0
- package/dist/protocol/schemaValidation.d.ts +11 -0
- package/dist/protocol/schemaValidation.js +186 -0
- package/dist/server.js +5 -1
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/db/criteria.ts +52 -27
- package/src/db/ordering.ts +56 -0
- package/src/db/schema.ts +164 -15
- package/src/db/store.ts +146 -46
- package/src/handlers/commitEvent.ts +39 -8
- package/src/handlers/crudEvents.ts +181 -0
- package/src/handlers/dataLogon.ts +2 -19
- package/src/handlers/eventValidation.ts +43 -0
- package/src/handlers/jsonSchema.ts +31 -19
- package/src/handlers/meta.ts +14 -9
- package/src/handlers/requestReply.ts +625 -41
- package/src/handlers/resources.ts +3 -0
- package/src/index.ts +1 -0
- package/src/protocol/fieldTypes.ts +7 -0
- package/src/protocol/msgNack.ts +12 -0
- package/src/protocol/schemaValidation.ts +208 -0
- package/src/server.ts +5 -1
- package/src/types.ts +56 -18
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// The CRUD event handlers a table registers through TableDef.events: the
|
|
2
|
+
// ones Genesis Create generates for a table's `crud` event handlers
|
|
3
|
+
// (codegen-service JsonToKtsEventHandlersGenerator) — eventHandler<Table>
|
|
4
|
+
// "<TABLE>_INSERT" / "_MODIFY" calling entityDb.insert / modify, and
|
|
5
|
+
// eventHandler<Table.ById> "<TABLE>_DELETE" calling entityDb.delete, with no
|
|
6
|
+
// onValidate. They are what the showcase server runs (its recordings come
|
|
7
|
+
// from them) and what Create's real tier deploys. genesis-eventhandler's own
|
|
8
|
+
// GenericInsert/Modify/DeleteEventHandler (EventHandlerBuilder
|
|
9
|
+
// .registerCUDEvents) differ in GENERATED and VALIDATE; see the README. The
|
|
10
|
+
// inbound schema check runs before them, as for any event
|
|
11
|
+
// (eventValidation.ts).
|
|
12
|
+
|
|
13
|
+
import { RECORD_ID, TIMESTAMP } from '../db/identity.ts';
|
|
14
|
+
import {
|
|
15
|
+
crudEventOf,
|
|
16
|
+
generatedFieldList,
|
|
17
|
+
pkFieldsOf,
|
|
18
|
+
primaryKeyIndexName,
|
|
19
|
+
secondaryUniqueIndexes,
|
|
20
|
+
} from '../db/schema.ts';
|
|
21
|
+
import type { Store } from '../db/store.ts';
|
|
22
|
+
import { NackError } from '../protocol/errors.ts';
|
|
23
|
+
import type {
|
|
24
|
+
CrudEventVerb,
|
|
25
|
+
EventHandler,
|
|
26
|
+
EventHandlerCtx,
|
|
27
|
+
EventHandlerResult,
|
|
28
|
+
MockServerConfig,
|
|
29
|
+
Row,
|
|
30
|
+
TableDef,
|
|
31
|
+
} from '../types.ts';
|
|
32
|
+
|
|
33
|
+
const isMissing = (value: unknown) => value === undefined || value === null;
|
|
34
|
+
|
|
35
|
+
// What DUPLICATE_KEY's FIELD shows after '<INDEX> -> ': the index's values,
|
|
36
|
+
// nulls left out, joined with ' | ' (DbUtil.CREATE_DUPLICATE_KEY_ERROR_MESSAGE).
|
|
37
|
+
function indexValueText(fields: string[], row: Row): string {
|
|
38
|
+
return fields
|
|
39
|
+
.map((field) => row[field])
|
|
40
|
+
.filter((value) => !isMissing(value))
|
|
41
|
+
.map(String)
|
|
42
|
+
.join(' | ');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The database's unique constraints: the primary key (unless `self` is the
|
|
46
|
+
// record being modified, whose key can't change) and every other unique
|
|
47
|
+
// index. A null in an index's fields never clashes, as in SQL.
|
|
48
|
+
function checkUnique(table: string, def: TableDef, row: Row, store: Store, self?: string): void {
|
|
49
|
+
const pkFields = pkFieldsOf(def);
|
|
50
|
+
if (self === undefined && pkFields.every((field) => !isMissing(row[field]))) {
|
|
51
|
+
if (store.getRow(table, store.rowKey(table, row))) {
|
|
52
|
+
throw NackError.duplicateKey(
|
|
53
|
+
table,
|
|
54
|
+
primaryKeyIndexName(table, def),
|
|
55
|
+
indexValueText(pkFields, row),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const index of secondaryUniqueIndexes(def)) {
|
|
60
|
+
if (index.fields.some((field) => isMissing(row[field]))) continue;
|
|
61
|
+
const clash = store
|
|
62
|
+
.getAllRows(table)
|
|
63
|
+
.some(
|
|
64
|
+
(other) =>
|
|
65
|
+
store.rowKey(table, other) !== self &&
|
|
66
|
+
index.fields.every((field) => other[field] === row[field]),
|
|
67
|
+
);
|
|
68
|
+
if (clash) throw NackError.duplicateKey(table, index.name, indexValueText(index.fields, row));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const declaredNames = (def: TableDef) => def.fields?.map((field) => field.name);
|
|
73
|
+
|
|
74
|
+
// The record an event's DETAILS deserialize into: a declared table's
|
|
75
|
+
// generated DAO gives every field the DETAILS leave out its default, or null
|
|
76
|
+
// (a missing not-null field without one never gets here: the schema check
|
|
77
|
+
// NACKs it). Generated fields stay unset for the database to fill — or, on a
|
|
78
|
+
// modify, to keep. An undeclared table has no DAO to go by: an insert takes
|
|
79
|
+
// DETAILS as they are, and a modify resets the record's other stored columns.
|
|
80
|
+
function defaultsFor(def: TableDef, details: Row, fieldNames: string[]): Row {
|
|
81
|
+
const generated = new Set(generatedFieldList(def));
|
|
82
|
+
const pkFields = pkFieldsOf(def);
|
|
83
|
+
const record: Row = { ...details };
|
|
84
|
+
for (const name of fieldNames) {
|
|
85
|
+
if (Object.hasOwn(details, name) || generated.has(name) || pkFields.includes(name)) continue;
|
|
86
|
+
if (name === RECORD_ID || name === TIMESTAMP) continue;
|
|
87
|
+
const declared = def.fields?.find((field) => field.name === name);
|
|
88
|
+
record[name] = declared?.default !== undefined ? declared.default : null;
|
|
89
|
+
}
|
|
90
|
+
return record;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// entityDb.insert(details), then ack(listOf(mapOf(<each generated field,
|
|
94
|
+
// sorted by name> to insertedRow.record.<field>))): every generated field's
|
|
95
|
+
// stored value, supplied or generated — [] for a table with none. With no
|
|
96
|
+
// onValidate, VALIDATE acks without touching the database.
|
|
97
|
+
function insert(
|
|
98
|
+
table: string,
|
|
99
|
+
def: TableDef,
|
|
100
|
+
details: Row,
|
|
101
|
+
ctx: EventHandlerCtx,
|
|
102
|
+
): EventHandlerResult {
|
|
103
|
+
if (ctx.validate) return {};
|
|
104
|
+
const { store } = ctx;
|
|
105
|
+
const record = defaultsFor(def, details, declaredNames(def) ?? []);
|
|
106
|
+
const row = store.withGeneratedValues(table, record);
|
|
107
|
+
checkUnique(table, def, row, store);
|
|
108
|
+
const inserted = store.insertRow(table, row);
|
|
109
|
+
ctx.broadcastTableChange(table, 'INSERT', store.rowKey(table, inserted));
|
|
110
|
+
const generatedFields = generatedFieldList(def).sort();
|
|
111
|
+
if (generatedFields.length === 0) return {};
|
|
112
|
+
return {
|
|
113
|
+
generated: [
|
|
114
|
+
Object.fromEntries(generatedFields.map((field) => [field, inserted[field] ?? null])),
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// entityDb.modify(details): the record is found by its primary key
|
|
120
|
+
// (RECORD_NOT_FOUND naming the key's index, recorded) and DETAILS are written
|
|
121
|
+
// over it. DETAILS are the whole record: a field they leave out is reset to
|
|
122
|
+
// its default or null, as the deserialized DAO's value overwrites the stored
|
|
123
|
+
// one (recorded: a TRADE modify without CREATED_AT, ENTERED_BY and
|
|
124
|
+
// TRADE_STATUS nulls them). Generated fields left out keep their values;
|
|
125
|
+
// RECORD_ID stays, TIMESTAMP moves on. VALIDATE acks without a lookup.
|
|
126
|
+
function modify(
|
|
127
|
+
table: string,
|
|
128
|
+
def: TableDef,
|
|
129
|
+
details: Row,
|
|
130
|
+
ctx: EventHandlerCtx,
|
|
131
|
+
): EventHandlerResult {
|
|
132
|
+
if (ctx.validate) return {};
|
|
133
|
+
const { store } = ctx;
|
|
134
|
+
const key = store.rowKey(table, details);
|
|
135
|
+
const found = store.getRow(table, key);
|
|
136
|
+
if (!found) throw NackError.recordNotFound(table, primaryKeyIndexName(table, def));
|
|
137
|
+
const patch = defaultsFor(def, details, declaredNames(def) ?? Object.keys(found));
|
|
138
|
+
checkUnique(table, def, { ...found, ...patch }, store, key);
|
|
139
|
+
store.updateRow(table, key, patch);
|
|
140
|
+
ctx.broadcastTableChange(table, 'MODIFY', key);
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// entityDb.delete(<Table>.ById): a missing record is RECORD_NOT_FOUND with
|
|
145
|
+
// FIELD '' (recorded). VALIDATE acks without a lookup.
|
|
146
|
+
function remove(
|
|
147
|
+
table: string,
|
|
148
|
+
_def: TableDef,
|
|
149
|
+
details: Row,
|
|
150
|
+
ctx: EventHandlerCtx,
|
|
151
|
+
): EventHandlerResult {
|
|
152
|
+
if (ctx.validate) return {};
|
|
153
|
+
const { store } = ctx;
|
|
154
|
+
const key = store.rowKey(table, details);
|
|
155
|
+
if (!store.getRow(table, key)) throw NackError.recordNotFound(table);
|
|
156
|
+
store.deleteRow(table, key);
|
|
157
|
+
ctx.broadcastTableChange(table, 'DELETE', key);
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const VERBS: Record<
|
|
162
|
+
CrudEventVerb,
|
|
163
|
+
(table: string, def: TableDef, details: Row, ctx: EventHandlerCtx) => EventHandlerResult
|
|
164
|
+
> = { INSERT: insert, MODIFY: modify, DELETE: remove };
|
|
165
|
+
|
|
166
|
+
// The generic handler for an event a table registers through
|
|
167
|
+
// TableDef.events, or undefined. It reads the table's definition on every
|
|
168
|
+
// call, so setTable() takes effect straight away.
|
|
169
|
+
export function crudEventHandler(
|
|
170
|
+
eventName: string,
|
|
171
|
+
config: MockServerConfig,
|
|
172
|
+
): EventHandler | undefined {
|
|
173
|
+
const crud = crudEventOf(eventName, config);
|
|
174
|
+
if (!crud) return undefined;
|
|
175
|
+
const { table, verb } = crud;
|
|
176
|
+
return (details, ctx) => {
|
|
177
|
+
const def = config.tables?.[table];
|
|
178
|
+
if (!def) throw new Error(`Unknown table: ${table}`);
|
|
179
|
+
return VERBS[verb](table, def, details, ctx);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { compileCriteria, criteriaFields, filterByCriteria } from '../db/criteria.ts';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { rowComparator } from '../db/ordering.ts';
|
|
4
3
|
import { queryFieldNames, querySchema } from '../db/schema.ts';
|
|
5
4
|
import type { Store } from '../db/store.ts';
|
|
6
5
|
import type { Connection, Subscription } from '../protocol/connection.ts';
|
|
@@ -112,24 +111,8 @@ function requestedFieldNames(
|
|
|
112
111
|
// Ascending by the index's fields, nulls first. Ties are broken by RECORD_ID,
|
|
113
112
|
// as the real index breaks them, so REVERSE, which flips the whole order,
|
|
114
113
|
// puts the newest of equal rows first.
|
|
115
|
-
|
|
116
114
|
function indexComparator(index: IndexDef): (a: Row, b: Row) => number {
|
|
117
|
-
|
|
118
|
-
if (a === b) return 0;
|
|
119
|
-
if (a === undefined || a === null) return -1;
|
|
120
|
-
if (b === undefined || b === null) return 1;
|
|
121
|
-
if (typeof a === 'number' && typeof b === 'number') return a - b;
|
|
122
|
-
if (typeof a === 'boolean' && typeof b === 'boolean') return a ? 1 : -1;
|
|
123
|
-
const [left, right] = [String(a), String(b)];
|
|
124
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
125
|
-
};
|
|
126
|
-
return (a, b) => {
|
|
127
|
-
for (const field of index.fields) {
|
|
128
|
-
const result = compareValues(a[field], b[field]);
|
|
129
|
-
if (result !== 0) return result;
|
|
130
|
-
}
|
|
131
|
-
return compareValues(a[RECORD_ID], b[RECORD_ID]);
|
|
132
|
-
};
|
|
115
|
+
return rowComparator(index.fields.map((field) => ({ field, descending: false })));
|
|
133
116
|
}
|
|
134
117
|
|
|
135
118
|
// DATA_LOGON opens a persistent subscription keyed by SOURCE_REF: reply with
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { eventFields, isDeclared, resolveEventTarget } from '../db/schema.ts';
|
|
2
|
+
import type { Store } from '../db/store.ts';
|
|
3
|
+
import type { Connection } from '../protocol/connection.ts';
|
|
4
|
+
import type { NackErrorInput } from '../protocol/errors.ts';
|
|
5
|
+
import { schemaFieldErrors, validateSchema } from '../protocol/schemaValidation.ts';
|
|
6
|
+
import type { GenesisMessage, MockServerConfig } from '../types.ts';
|
|
7
|
+
import { eventInboundSchema } from './jsonSchema.ts';
|
|
8
|
+
|
|
9
|
+
// The schema check every event goes through on the real server before its
|
|
10
|
+
// handler runs (GenesisMessageDecoder, GSF v8.15.29): the whole message
|
|
11
|
+
// against the event's INBOUND JSON schema — the one JSON_SCHEMA_REQUEST
|
|
12
|
+
// serves — each failure a FieldError VALIDATION_ERROR (see
|
|
13
|
+
// protocol/schemaValidation.ts). An empty list means it passed.
|
|
14
|
+
//
|
|
15
|
+
// Only an event whose DETAILS class is declared is checked: a table event on
|
|
16
|
+
// a table with TableDef.fields, or an eventSchemas entry with `fields`. An
|
|
17
|
+
// inferred schema is a guess from the seed rows (a column every row leaves
|
|
18
|
+
// empty isn't in it at all), and checking against it would NACK payloads the
|
|
19
|
+
// real server accepts.
|
|
20
|
+
export function validateEventMessage(
|
|
21
|
+
message: GenesisMessage,
|
|
22
|
+
connection: Connection,
|
|
23
|
+
config: MockServerConfig,
|
|
24
|
+
store: Store,
|
|
25
|
+
): NackErrorInput[] {
|
|
26
|
+
const name = message.MESSAGE_TYPE;
|
|
27
|
+
const target = resolveEventTarget(name, config);
|
|
28
|
+
if (!target) return [];
|
|
29
|
+
const declared =
|
|
30
|
+
target.kind === 'dto' ||
|
|
31
|
+
(target.table !== undefined && isDeclared(target.table, config, store));
|
|
32
|
+
if (!declared) return [];
|
|
33
|
+
const inbound = eventInboundSchema(
|
|
34
|
+
name,
|
|
35
|
+
target,
|
|
36
|
+
eventFields(target, config, store),
|
|
37
|
+
config.appName,
|
|
38
|
+
);
|
|
39
|
+
// The router stamps the session's user on everything it forwards, so the
|
|
40
|
+
// schema's required USER_NAME is always there by the time it is checked.
|
|
41
|
+
const forwarded = { ...message, USER_NAME: connection.userName ?? '' };
|
|
42
|
+
return schemaFieldErrors(validateSchema(inbound, forwarded));
|
|
43
|
+
}
|
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
knownValues,
|
|
6
6
|
isDeleteEventName,
|
|
7
7
|
querySchema,
|
|
8
|
-
RECORD_IDENTITY_FIELDS,
|
|
9
8
|
requestFieldsOf,
|
|
10
|
-
|
|
9
|
+
requestReplyReplySchema,
|
|
10
|
+
requestReplySourceSchema,
|
|
11
11
|
resolveEventTarget,
|
|
12
12
|
resolveSourceSchema,
|
|
13
13
|
withTypeOverrides,
|
|
@@ -101,12 +101,15 @@ function isRequired(field: ResolvedField): boolean {
|
|
|
101
101
|
return !field.optional;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
// An event's INBOUND schema: the message, with DETAILS as the event's class.
|
|
105
|
+
// It is also what an inbound event is validated against
|
|
106
|
+
// (handlers/eventValidation.ts), as on the real server.
|
|
107
|
+
export function eventInboundSchema(
|
|
105
108
|
eventName: string,
|
|
106
109
|
target: EventTarget,
|
|
107
110
|
fields: ResolvedField[],
|
|
108
111
|
appName: string | undefined,
|
|
109
|
-
):
|
|
112
|
+
): Row {
|
|
110
113
|
const annotated = target.kind === 'table';
|
|
111
114
|
const details = {
|
|
112
115
|
properties: Object.fromEntries(
|
|
@@ -117,16 +120,25 @@ function eventSchema(
|
|
|
117
120
|
type: 'object',
|
|
118
121
|
};
|
|
119
122
|
return {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
properties: eventInboundProperties(eventName, details),
|
|
124
|
+
required: ['MESSAGE_TYPE', 'SOURCE_REF', 'USER_NAME', 'DETAILS'],
|
|
125
|
+
additionalProperties: true,
|
|
126
|
+
description: target.className,
|
|
127
|
+
...(target.title ? { title: target.title } : {}),
|
|
128
|
+
$defs: {},
|
|
129
|
+
$schema: GENESIS_META_SCHEMA,
|
|
130
|
+
type: 'object',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function eventSchema(
|
|
135
|
+
eventName: string,
|
|
136
|
+
target: EventTarget,
|
|
137
|
+
fields: ResolvedField[],
|
|
138
|
+
appName: string | undefined,
|
|
139
|
+
): { INBOUND: Row; OUTBOUND: Row } {
|
|
140
|
+
return {
|
|
141
|
+
INBOUND: eventInboundSchema(eventName, target, fields, appName),
|
|
130
142
|
OUTBOUND: eventReplySchema(),
|
|
131
143
|
};
|
|
132
144
|
}
|
|
@@ -171,10 +183,10 @@ function requestServerSchema(
|
|
|
171
183
|
config: MockServerConfig,
|
|
172
184
|
store: Store,
|
|
173
185
|
): { INBOUND: Row; OUTBOUND: Row } {
|
|
174
|
-
const
|
|
186
|
+
const entityFields = requestReplySourceSchema(requestReply, config, store).fields;
|
|
175
187
|
const requestProperties = Object.fromEntries(
|
|
176
188
|
requestFieldsOf(requestReply, store).map((fieldName) => {
|
|
177
|
-
const field =
|
|
189
|
+
const field = entityFields.find((candidate) => candidate.name === fieldName);
|
|
178
190
|
const type = field?.type ?? 'STRING';
|
|
179
191
|
return [
|
|
180
192
|
fieldName,
|
|
@@ -189,9 +201,9 @@ function requestServerSchema(
|
|
|
189
201
|
}),
|
|
190
202
|
);
|
|
191
203
|
const request = { properties: requestProperties, additionalProperties: false, type: 'object' };
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
204
|
+
// The reply block, else the source's fields then RECORD_ID and TIMESTAMP,
|
|
205
|
+
// as REPLY_FIELD (see requestReplyReplySchema).
|
|
206
|
+
const fields = requestReplyReplySchema(requestReply, config, store).fields;
|
|
195
207
|
return {
|
|
196
208
|
INBOUND: {
|
|
197
209
|
properties: {
|
package/src/handlers/meta.ts
CHANGED
|
@@ -3,9 +3,10 @@ import {
|
|
|
3
3
|
eventFields,
|
|
4
4
|
knownValues,
|
|
5
5
|
querySchema,
|
|
6
|
-
RECORD_IDENTITY_FIELDS,
|
|
7
6
|
requestFieldsOf,
|
|
7
|
+
requestReplyReplySchema,
|
|
8
8
|
requestReplySchema,
|
|
9
|
+
requestReplySourceSchema,
|
|
9
10
|
resolveEventTarget,
|
|
10
11
|
type EventTarget,
|
|
11
12
|
type ResolvedField,
|
|
@@ -78,9 +79,13 @@ function dataserverMeta(
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
// The two request-server shapes of ReqRepMetaData.kt: criteria-only
|
|
81
|
-
// (REQUEST_FIELD: [], SORTABLE_FIELDS, CRITERIA_FIELDS,
|
|
82
|
-
//
|
|
83
|
-
//
|
|
82
|
+
// (REQUEST_FIELD: [], SORTABLE_FIELDS, CRITERIA_FIELDS, no INDICES) and plain
|
|
83
|
+
// (REQUEST_FIELD from the request block, INDICES: [],
|
|
84
|
+
// CRITERIA_ONLY_REQUEST: false). REPLY_FIELD is the reply block, else the
|
|
85
|
+
// source's fields then RECORD_ID and TIMESTAMP, for either
|
|
86
|
+
// (requestReplyReplySchema). SORTABLE_FIELDS and CRITERIA_FIELDS are the
|
|
87
|
+
// entity's fields whatever the reply block says (the dictionary entry's
|
|
88
|
+
// nativeQueryFields, RequestReplyServer.kt).
|
|
84
89
|
function requestServerMeta(
|
|
85
90
|
name: string,
|
|
86
91
|
requestReply: RequestReplyDef,
|
|
@@ -89,23 +94,23 @@ function requestServerMeta(
|
|
|
89
94
|
hidden: Set<string>,
|
|
90
95
|
): Row {
|
|
91
96
|
const visible = (field: ResolvedField) => !hidden.has(field.name);
|
|
92
|
-
const replyFields =
|
|
97
|
+
const replyFields = requestReplyReplySchema(requestReply, config, store).fields.filter(visible);
|
|
98
|
+
const entityFields = requestReplySourceSchema(requestReply, config, store).fields;
|
|
93
99
|
if (requestReply.criteriaOnly) {
|
|
94
|
-
const
|
|
95
|
-
const names = fields.map((field) => field.name);
|
|
100
|
+
const names = entityFields.filter(visible).map((field) => field.name);
|
|
96
101
|
return {
|
|
97
102
|
TYPE: 'REQUEST_SERVER',
|
|
98
103
|
REQUEST_FIELD: [],
|
|
99
104
|
CRITERIA_ONLY_REQUEST: true,
|
|
100
105
|
SORTABLE_FIELDS: names,
|
|
101
106
|
CRITERIA_FIELDS: names,
|
|
102
|
-
REPLY_FIELD:
|
|
107
|
+
REPLY_FIELD: replyFields.map(fieldMetadata),
|
|
103
108
|
NAME: name,
|
|
104
109
|
};
|
|
105
110
|
}
|
|
106
111
|
const requestFields = requestFieldsOf(requestReply, store).map(
|
|
107
112
|
(fieldName) =>
|
|
108
|
-
|
|
113
|
+
entityFields.find((field) => field.name === fieldName) ?? {
|
|
109
114
|
name: fieldName,
|
|
110
115
|
type: 'STRING',
|
|
111
116
|
nullable: false,
|