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