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