@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,186 @@
|
|
|
1
|
+
// Inbound schema validation, as GSF runs it on every event before any handler
|
|
2
|
+
// sees it: GenesisMessageDecoder validates the whole message against the
|
|
3
|
+
// event's JSON schema (the INBOUND that JSON_SCHEMA_REQUEST serves) and
|
|
4
|
+
// answers each failure with a FieldError VALIDATION_ERROR. GSF 8.15.29 uses
|
|
5
|
+
// networknt json-schema-validator 2.0.1, configured by
|
|
6
|
+
// GenesisJsonSchemaFactory (draft 2019-09, PathType.LEGACY,
|
|
7
|
+
// losslessNarrowing). This is the subset of that validator the engine's own
|
|
8
|
+
// event schemas need: type, const, enum, oneOf, properties, required,
|
|
9
|
+
// additionalProperties, minLength / maxLength, minimum / maximum and pattern.
|
|
10
|
+
// Texts, locations and their order follow networknt's output, checked by
|
|
11
|
+
// running the real validator on the recorded showcase schemas
|
|
12
|
+
// (test/schemaValidation.test.ts pins the cases).
|
|
13
|
+
//
|
|
14
|
+
// Order: at each schema, `type` and then `properties` are checked first and
|
|
15
|
+
// every other keyword after them in the schema's key order; `properties` in
|
|
16
|
+
// their key order; unknown properties in the instance's order. A failed
|
|
17
|
+
// `type` doesn't stop the rest (an enum still reports its own error).
|
|
18
|
+
// `oneOf` with no valid branch reports itself, then every branch's errors.
|
|
19
|
+
// networknt's JsonType names. With losslessNarrowing a number with no
|
|
20
|
+
// fraction is an integer (1.0 included, which JSON.parse makes 1 anyway).
|
|
21
|
+
function jsonTypeOf(value) {
|
|
22
|
+
if (value === null)
|
|
23
|
+
return 'null';
|
|
24
|
+
if (Array.isArray(value))
|
|
25
|
+
return 'array';
|
|
26
|
+
if (typeof value === 'number')
|
|
27
|
+
return Number.isInteger(value) ? 'integer' : 'number';
|
|
28
|
+
return typeof value;
|
|
29
|
+
}
|
|
30
|
+
function matchesType(value, type) {
|
|
31
|
+
const actual = jsonTypeOf(value);
|
|
32
|
+
return actual === type || (type === 'number' && actual === 'integer');
|
|
33
|
+
}
|
|
34
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
35
|
+
function deepEqual(a, b) {
|
|
36
|
+
if (a === b)
|
|
37
|
+
return true;
|
|
38
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
39
|
+
return a.length === b.length && a.every((item, index) => deepEqual(item, b[index]));
|
|
40
|
+
}
|
|
41
|
+
if (isObject(a) && isObject(b)) {
|
|
42
|
+
const keys = Object.keys(a);
|
|
43
|
+
return (keys.length === Object.keys(b).length &&
|
|
44
|
+
keys.every((key) => Object.hasOwn(b, key) && deepEqual(a[key], b[key])));
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// networknt evaluates these before a schema's other keywords, whatever their
|
|
49
|
+
// position; the rest go in key order.
|
|
50
|
+
const FIRST_KEYWORDS = ['type', 'properties'];
|
|
51
|
+
// String lengths count code points, as Java's codePointCount does.
|
|
52
|
+
const codePoints = (text) => [...text].length;
|
|
53
|
+
const NOT_DEFINED = 'is not defined in the schema and the schema does not allow additional properties';
|
|
54
|
+
// One check per keyword, each a no-op on values it doesn't apply to (string
|
|
55
|
+
// keywords on non-strings, object keywords on non-objects, ...).
|
|
56
|
+
const KEYWORDS = {
|
|
57
|
+
type({ arg, value, fail }) {
|
|
58
|
+
if (typeof arg === 'string' && !matchesType(value, arg)) {
|
|
59
|
+
fail(`${jsonTypeOf(value)} found, ${arg} expected`);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
const({ arg, value, fail }) {
|
|
63
|
+
if (!deepEqual(value, arg))
|
|
64
|
+
fail(`must be the constant value '${String(arg)}'`);
|
|
65
|
+
},
|
|
66
|
+
enum({ arg, value, fail }) {
|
|
67
|
+
if (Array.isArray(arg) && !arg.some((candidate) => deepEqual(value, candidate))) {
|
|
68
|
+
const values = arg.map((item) => JSON.stringify(item)).join(', ');
|
|
69
|
+
fail(`does not have a value in the enumeration [${values}]`);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
minLength({ arg, value, fail }) {
|
|
73
|
+
if (typeof value === 'string' && codePoints(value) < arg) {
|
|
74
|
+
fail(`must be at least ${arg} characters long`);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
maxLength({ arg, value, fail }) {
|
|
78
|
+
if (typeof value === 'string' && codePoints(value) > arg) {
|
|
79
|
+
fail(`must be at most ${arg} characters long`);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
minimum({ arg, value, fail }) {
|
|
83
|
+
if (typeof value === 'number' && value < arg)
|
|
84
|
+
fail(`must have a minimum value of ${arg}`);
|
|
85
|
+
},
|
|
86
|
+
maximum({ arg, value, fail }) {
|
|
87
|
+
if (typeof value === 'number' && value > arg)
|
|
88
|
+
fail(`must have a maximum value of ${arg}`);
|
|
89
|
+
},
|
|
90
|
+
// JSON Schema patterns are unanchored: '[0-9]+' accepts 'x1y'.
|
|
91
|
+
pattern({ arg, value, fail }) {
|
|
92
|
+
if (typeof value === 'string' && !new RegExp(arg, 'u').test(value)) {
|
|
93
|
+
fail(`does not match the regex pattern ${arg}`);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
oneOf({ arg, value, location, errors, fail }) {
|
|
97
|
+
if (!Array.isArray(arg))
|
|
98
|
+
return;
|
|
99
|
+
const results = arg.map((branch) => validateSchema(branch, value, location));
|
|
100
|
+
const valid = results.flatMap((result, index) => (result.length === 0 ? [index] : []));
|
|
101
|
+
if (valid.length === 1)
|
|
102
|
+
return;
|
|
103
|
+
if (valid.length > 1) {
|
|
104
|
+
fail(`must be valid to one and only one schema, but ${valid.length} are valid with indexes '${valid.join(', ')}'`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
fail('must be valid to one and only one schema, but 0 are valid');
|
|
108
|
+
errors.push(...results.flat());
|
|
109
|
+
},
|
|
110
|
+
properties({ arg, value, location, errors }) {
|
|
111
|
+
if (!isObject(value) || !isObject(arg))
|
|
112
|
+
return;
|
|
113
|
+
for (const [name, propertySchema] of Object.entries(arg)) {
|
|
114
|
+
if (!Object.hasOwn(value, name))
|
|
115
|
+
continue;
|
|
116
|
+
errors.push(...validateSchema(propertySchema, value[name], `${location}.${name}`));
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
required({ arg, value, fail }) {
|
|
120
|
+
if (!isObject(value) || !Array.isArray(arg))
|
|
121
|
+
return;
|
|
122
|
+
for (const name of arg) {
|
|
123
|
+
if (!Object.hasOwn(value, name))
|
|
124
|
+
fail(`required property '${name}' not found`, name);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
additionalProperties({ arg, schema, value, location, errors, fail }) {
|
|
128
|
+
if (!isObject(value) || (arg !== false && !isObject(arg)))
|
|
129
|
+
return;
|
|
130
|
+
const known = isObject(schema.properties) ? schema.properties : {};
|
|
131
|
+
for (const name of Object.keys(value)) {
|
|
132
|
+
if (Object.hasOwn(known, name))
|
|
133
|
+
continue;
|
|
134
|
+
if (arg === false)
|
|
135
|
+
fail(`property '${name}' ${NOT_DEFINED}`, name);
|
|
136
|
+
else
|
|
137
|
+
errors.push(...validateSchema(arg, value[name], `${location}.${name}`));
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
// Anything else is an annotation (description, title, default, readOnly,
|
|
141
|
+
// genesisType, $schema, $defs, ...) and validates nothing; networknt
|
|
142
|
+
// doesn't enforce readOnly unless asked to.
|
|
143
|
+
};
|
|
144
|
+
export function validateSchema(schema, value, location = '$') {
|
|
145
|
+
const errors = [];
|
|
146
|
+
if (!isObject(schema))
|
|
147
|
+
return errors;
|
|
148
|
+
const keywords = Object.keys(schema);
|
|
149
|
+
const ordered = [
|
|
150
|
+
...FIRST_KEYWORDS.filter((keyword) => keywords.includes(keyword)),
|
|
151
|
+
...keywords.filter((keyword) => !FIRST_KEYWORDS.includes(keyword)),
|
|
152
|
+
];
|
|
153
|
+
for (const keyword of ordered) {
|
|
154
|
+
if (!Object.hasOwn(KEYWORDS, keyword))
|
|
155
|
+
continue;
|
|
156
|
+
KEYWORDS[keyword]({
|
|
157
|
+
arg: schema[keyword],
|
|
158
|
+
schema,
|
|
159
|
+
value,
|
|
160
|
+
location,
|
|
161
|
+
errors,
|
|
162
|
+
fail: (message, property) => errors.push({
|
|
163
|
+
keyword,
|
|
164
|
+
instanceLocation: location,
|
|
165
|
+
...(property !== undefined ? { property } : {}),
|
|
166
|
+
message,
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return errors;
|
|
171
|
+
}
|
|
172
|
+
// GenesisMessageDecoder's FieldError for each failure: TEXT is the error's
|
|
173
|
+
// toString() ("<location>: <message>"), FIELD the missing property for
|
|
174
|
+
// `required` and otherwise the last segment of the location (so an unknown
|
|
175
|
+
// DETAILS property names 'DETAILS'), PATH the location.
|
|
176
|
+
export function schemaFieldErrors(errors) {
|
|
177
|
+
return errors.map((error) => ({
|
|
178
|
+
'@type': 'FieldError',
|
|
179
|
+
CODE: 'VALIDATION_ERROR',
|
|
180
|
+
TEXT: `${error.instanceLocation}: ${error.message}`,
|
|
181
|
+
FIELD: error.keyword === 'required'
|
|
182
|
+
? error.property
|
|
183
|
+
: error.instanceLocation.slice(error.instanceLocation.lastIndexOf('.') + 1),
|
|
184
|
+
PATH: error.instanceLocation,
|
|
185
|
+
}));
|
|
186
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { WebSocketServer } from 'ws';
|
|
3
3
|
import { Store } from "./db/store.js";
|
|
4
|
+
import { crudEventHandler } from "./handlers/crudEvents.js";
|
|
4
5
|
import { pendingKey } from "./handlers/dataLogon.js";
|
|
5
6
|
import { createHttpRequestListener } from "./handlers/http.js";
|
|
6
|
-
import {
|
|
7
|
+
import { permittedQueryRow, requestingUser } from "./handlers/resourceAuth.js";
|
|
7
8
|
import { Connection } from "./protocol/connection.js";
|
|
8
9
|
import { isLegacyFidelity } from "./protocol/fidelity.js";
|
|
9
10
|
import { routeMessage } from "./protocol/router.js";
|
|
10
|
-
import { stampRow } from "./protocol/rowUpdate.js";
|
|
11
|
+
import { dataserverRow, stampRow } from "./protocol/rowUpdate.js";
|
|
11
12
|
export function createMockServer(config) {
|
|
12
13
|
const store = new Store();
|
|
13
14
|
for (const [tableName, tableDef] of Object.entries(config.tables ?? {})) {
|
|
@@ -80,10 +81,10 @@ export function createMockServer(config) {
|
|
|
80
81
|
return null;
|
|
81
82
|
const permitted = operation === 'DELETE'
|
|
82
83
|
? undefined
|
|
83
|
-
:
|
|
84
|
+
: permittedQueryRow(row, query?.auth, requestingUser(connection), subscription.fieldNames);
|
|
84
85
|
if (pending.has(key)) {
|
|
85
86
|
if (permitted)
|
|
86
|
-
pending.set(key, stampRow(permitted, rowRef, 'INSERT'));
|
|
87
|
+
pending.set(key, stampRow(dataserverRow(permitted, row, query?.fields), rowRef, 'INSERT'));
|
|
87
88
|
else
|
|
88
89
|
pending.delete(key);
|
|
89
90
|
return null;
|
|
@@ -96,10 +97,14 @@ export function createMockServer(config) {
|
|
|
96
97
|
}
|
|
97
98
|
if (!permitted)
|
|
98
99
|
return null;
|
|
100
|
+
// Under ORDER_BY, a new row that sorts among the waiting ones waits too,
|
|
101
|
+
// so MORE_ROWS pages stay in order.
|
|
102
|
+
if (subscription.queueInsert?.(key, stampRow(dataserverRow(permitted, row, query?.fields), rowRef, 'INSERT'), row))
|
|
103
|
+
return null;
|
|
99
104
|
held.add(key);
|
|
100
105
|
return 'INSERT';
|
|
101
106
|
}
|
|
102
|
-
|
|
107
|
+
function pushChange(resourceName, operation, row) {
|
|
103
108
|
const query = config.queries?.[resourceName];
|
|
104
109
|
// The query's own filter (where-clause equivalent) applies to live pushes
|
|
105
110
|
// too — a row the snapshot would exclude must not leak in via broadcast.
|
|
@@ -129,21 +134,25 @@ export function createMockServer(config) {
|
|
|
129
134
|
: resolved;
|
|
130
135
|
if (rowOperation === null)
|
|
131
136
|
continue;
|
|
137
|
+
const legacy = isLegacyFidelity(config);
|
|
132
138
|
let outRow = row;
|
|
133
139
|
if (rowOperation === 'DELETE') {
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
if (
|
|
140
|
+
// GSF's DELETE row is DETAILS {OPERATION, ROW_REF} and nothing else.
|
|
141
|
+
// Legacy mode sent the pk fields: the stub it was given for a real
|
|
142
|
+
// DELETE, or one rebuilt from the key for a change that left the
|
|
143
|
+
// subscription (the full row may carry columns hideFields strips).
|
|
144
|
+
if (!legacy)
|
|
145
|
+
outRow = {};
|
|
146
|
+
else if (operation !== 'DELETE') {
|
|
139
147
|
outRow = query ? store.rowStubFromKey(query.source, rowRef) : row;
|
|
140
148
|
}
|
|
141
149
|
}
|
|
142
|
-
else
|
|
143
|
-
|
|
150
|
+
else {
|
|
151
|
+
// Only the subscription's columns, minus the ones this user can't see.
|
|
152
|
+
const permitted = permittedQueryRow(row, query?.auth, requestingUser(connection), subscription.fieldNames);
|
|
144
153
|
if (!permitted)
|
|
145
154
|
continue;
|
|
146
|
-
outRow = permitted;
|
|
155
|
+
outRow = dataserverRow(permitted, row, query?.fields);
|
|
147
156
|
}
|
|
148
157
|
subscription.sequenceId += 1;
|
|
149
158
|
// A live push carries neither MORE_ROWS nor ROWS_COUNT on the real
|
|
@@ -151,20 +160,62 @@ export function createMockServer(config) {
|
|
|
151
160
|
connection.send({
|
|
152
161
|
MESSAGE_TYPE: 'QUERY_UPDATE',
|
|
153
162
|
SOURCE_REF: sourceRef,
|
|
154
|
-
ROW: [stampRow(outRow, rowRef, rowOperation)],
|
|
155
|
-
...(
|
|
163
|
+
ROW: [stampRow(outRow, rowRef, rowOperation, legacy)],
|
|
164
|
+
...(legacy ? { ROWS_COUNT: 1 } : {}),
|
|
156
165
|
SEQUENCE_ID: subscription.sequenceId,
|
|
157
166
|
});
|
|
158
167
|
}
|
|
159
168
|
}
|
|
169
|
+
}
|
|
170
|
+
// Pushes an unsolicited row update to every subscription on the resource
|
|
171
|
+
// (see pushChange). A DELETE given as a pk-only row is resolved to the
|
|
172
|
+
// record(s) removed from that key that no broadcast has announced yet —
|
|
173
|
+
// their RECORD_IDs are the ROW_REFs a client may hold — and those are
|
|
174
|
+
// consumed, so a later delete of a re-inserted key names its own record.
|
|
175
|
+
const broadcast = (resourceName, operation, row) => {
|
|
176
|
+
const source = config.queries?.[resourceName]?.source;
|
|
177
|
+
if (operation !== 'DELETE' || !source) {
|
|
178
|
+
pushChange(resourceName, operation, row);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const removed = store.takeRemovedRecordsFor(source, row);
|
|
182
|
+
if (isLegacyFidelity(config)) {
|
|
183
|
+
pushChange(resourceName, operation, row);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const records = row.RECORD_ID === undefined || row.RECORD_ID === null ? removed : [row];
|
|
187
|
+
if (records.length === 0)
|
|
188
|
+
records.push(row);
|
|
189
|
+
for (const record of records)
|
|
190
|
+
pushChange(resourceName, 'DELETE', record);
|
|
160
191
|
};
|
|
161
192
|
// Event handlers mutate one table (e.g. TRADE) but grids may be subscribed to
|
|
162
193
|
// a view built on top of it (e.g. ALL_TRADES -> TRADE_VIEW). This finds every
|
|
163
194
|
// query resource backed by that table — directly or via a view — and pushes
|
|
164
195
|
// the (re-joined, for views) row to each one's subscribers. `pk` is the
|
|
165
196
|
// store key: the single pk field's value, or store.rowKey(...) for
|
|
166
|
-
// composite-keyed tables.
|
|
197
|
+
// composite-keyed tables. ROW_REF is the record's RECORD_ID, so a DELETE is
|
|
198
|
+
// broadcast with every record the key lost that no broadcast announced yet
|
|
199
|
+
// (Store.takeRemovedRows; held-rows tracking drops the ones a client
|
|
200
|
+
// doesn't hold), and a record replaced under the same key (deleted, then
|
|
201
|
+
// re-inserted, before one broadcast) gets a DELETE of its old ROW_REF before
|
|
202
|
+
// the new one goes out. A view row is built from the changed base row
|
|
203
|
+
// alone, so it is found whatever the view's own pkField; a MODIFY that takes
|
|
204
|
+
// it out of the view (an anti-join) is pushed as a DELETE. Legacy ROW_REFs
|
|
205
|
+
// are the key itself, so there a DELETE is the pk stub and a replacement is
|
|
206
|
+
// the one INSERT/MODIFY, as before.
|
|
167
207
|
const broadcastTableChange = (tableName, operation, pk) => {
|
|
208
|
+
const legacy = isLegacyFidelity(config);
|
|
209
|
+
const current = operation === 'DELETE' ? undefined : store.getRow(tableName, pk);
|
|
210
|
+
let removed = store
|
|
211
|
+
.takeRemovedRows(tableName, pk)
|
|
212
|
+
.filter((record) => record.RECORD_ID !== current?.RECORD_ID);
|
|
213
|
+
// A DELETE broadcast before the row is removed still names it.
|
|
214
|
+
const live = store.getRow(tableName, pk);
|
|
215
|
+
if (operation === 'DELETE' && removed.length === 0 && live)
|
|
216
|
+
removed = [live];
|
|
217
|
+
if (legacy)
|
|
218
|
+
removed = [];
|
|
168
219
|
for (const [queryName, query] of Object.entries(config.queries ?? {})) {
|
|
169
220
|
const isDirect = query.source === tableName;
|
|
170
221
|
const view = store.views.get(query.source);
|
|
@@ -183,25 +234,37 @@ export function createMockServer(config) {
|
|
|
183
234
|
continue;
|
|
184
235
|
if (isViewJoiningTable) {
|
|
185
236
|
for (const viewRow of store.getViewRows(query.source)) {
|
|
186
|
-
|
|
237
|
+
pushChange(queryName, 'MODIFY', viewRow);
|
|
187
238
|
}
|
|
188
239
|
continue;
|
|
189
240
|
}
|
|
190
|
-
|
|
241
|
+
// The removed base records carry the RECORD_ID a view row's ROW_REF
|
|
242
|
+
// comes from too; the DELETE body is DETAILS only either way.
|
|
243
|
+
for (const record of removed)
|
|
244
|
+
pushChange(queryName, 'DELETE', record);
|
|
191
245
|
if (operation === 'DELETE') {
|
|
192
|
-
|
|
193
|
-
|
|
246
|
+
if (legacy) {
|
|
247
|
+
const stub = store.rowStubFromKey(query.source, pk);
|
|
248
|
+
if (Object.keys(stub).length > 0)
|
|
249
|
+
pushChange(queryName, 'DELETE', stub);
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (!current)
|
|
254
|
+
continue;
|
|
255
|
+
if (isDirect) {
|
|
256
|
+
pushChange(queryName, operation, current);
|
|
257
|
+
continue;
|
|
194
258
|
}
|
|
195
|
-
|
|
196
|
-
|
|
259
|
+
const viewRow = store.getViewRows(query.source, { [tableName]: [current] })[0];
|
|
260
|
+
if (viewRow) {
|
|
261
|
+
pushChange(queryName, operation, viewRow);
|
|
197
262
|
}
|
|
198
|
-
else {
|
|
199
|
-
|
|
200
|
-
.
|
|
201
|
-
|
|
263
|
+
else if (operation === 'MODIFY') {
|
|
264
|
+
pushChange(queryName, 'DELETE', legacy
|
|
265
|
+
? store.rowStubFromKey(query.source, store.getRowRef(query.source, current))
|
|
266
|
+
: current);
|
|
202
267
|
}
|
|
203
|
-
if (row)
|
|
204
|
-
broadcast(queryName, operation, row);
|
|
205
268
|
}
|
|
206
269
|
};
|
|
207
270
|
const httpSessions = new Map(); // sessionId -> username, for the HTTP login transport (see handlers/http.ts)
|
|
@@ -244,8 +307,11 @@ export function createMockServer(config) {
|
|
|
244
307
|
function getRequestReply(resourceName) {
|
|
245
308
|
return config.requestReplies?.[resourceName];
|
|
246
309
|
}
|
|
310
|
+
// An eventHandlers entry, else the generic CRUD handler a table registers
|
|
311
|
+
// (TableDef.events) — so extendEventHandler can wrap a generic event; the
|
|
312
|
+
// wrapper is then an explicit handler, which wins.
|
|
247
313
|
function getEventHandler(eventName) {
|
|
248
|
-
return config.eventHandlers?.[eventName];
|
|
314
|
+
return config.eventHandlers?.[eventName] ?? crudEventHandler(eventName, config);
|
|
249
315
|
}
|
|
250
316
|
// augment(current) => replacement. `current` is undefined if nothing was
|
|
251
317
|
// registered under that name yet, so augment can also be used to define a
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import type { Store } from './db/store.ts';
|
|
3
|
+
import type { NackErrorInput } from './protocol/errors.ts';
|
|
3
4
|
export type Row = Record<string, any>;
|
|
4
5
|
export interface GenesisMessage {
|
|
5
6
|
MESSAGE_TYPE: string;
|
|
@@ -7,12 +8,42 @@ export interface GenesisMessage {
|
|
|
7
8
|
DETAILS?: Row;
|
|
8
9
|
[key: string]: any;
|
|
9
10
|
}
|
|
11
|
+
export type GenesisFieldType = 'STRING' | 'ENUM' | 'INT' | 'SHORT' | 'LONG' | 'DOUBLE' | 'BIGDECIMAL' | 'BOOLEAN' | 'DATE' | 'DATETIME' | 'NANO_TIMESTAMP' | 'RAW';
|
|
12
|
+
export interface FieldDef {
|
|
13
|
+
name: string;
|
|
14
|
+
type: GenesisFieldType;
|
|
15
|
+
nullable?: boolean;
|
|
16
|
+
values?: string[];
|
|
17
|
+
default?: unknown;
|
|
18
|
+
maxSize?: number;
|
|
19
|
+
minLength?: number;
|
|
20
|
+
description?: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
optional?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface GeneratedFieldDef {
|
|
25
|
+
field: string;
|
|
26
|
+
kind: 'SEQUENCE' | 'AUTO_INCREMENT';
|
|
27
|
+
prefix?: string;
|
|
28
|
+
}
|
|
29
|
+
export type CrudEventVerb = 'INSERT' | 'MODIFY' | 'DELETE';
|
|
30
|
+
export interface IndexDef {
|
|
31
|
+
name: string;
|
|
32
|
+
fields: string[];
|
|
33
|
+
unique?: boolean;
|
|
34
|
+
}
|
|
10
35
|
export interface TableDef {
|
|
11
36
|
pkField: string | string[];
|
|
12
37
|
sequencePrefix?: string;
|
|
13
38
|
sequenceWidth?: number;
|
|
39
|
+
sequenceFormat?: SequenceFormat;
|
|
14
40
|
rows?: Row[];
|
|
41
|
+
fields?: FieldDef[];
|
|
42
|
+
generated?: GeneratedFieldDef[];
|
|
43
|
+
indexes?: IndexDef[];
|
|
44
|
+
events?: CrudEventVerb[];
|
|
15
45
|
}
|
|
46
|
+
export type SequenceFormat = 'prefix' | 'genesis';
|
|
16
47
|
export interface JoinDef {
|
|
17
48
|
table: string;
|
|
18
49
|
on: [string, string];
|
|
@@ -30,6 +61,8 @@ export interface ViewDef {
|
|
|
30
61
|
joins?: JoinDef[];
|
|
31
62
|
antiJoins?: AntiJoinDef[];
|
|
32
63
|
derived?: Record<string, (row: Row) => any>;
|
|
64
|
+
derivedTypes?: Record<string, GenesisFieldType | Omit<FieldDef, 'name'>>;
|
|
65
|
+
fields?: string[];
|
|
33
66
|
}
|
|
34
67
|
export interface RequestingUser {
|
|
35
68
|
userName?: string;
|
|
@@ -43,6 +76,8 @@ export interface ResourceAuthDef {
|
|
|
43
76
|
export interface QueryDef {
|
|
44
77
|
source: string;
|
|
45
78
|
fieldTypes?: Record<string, string>;
|
|
79
|
+
fields?: string[];
|
|
80
|
+
indexes?: IndexDef[];
|
|
46
81
|
filter?: (row: Row) => boolean;
|
|
47
82
|
auth?: ResourceAuthDef;
|
|
48
83
|
}
|
|
@@ -59,11 +94,17 @@ export interface RequestReplyDef {
|
|
|
59
94
|
fieldTypes?: Record<string, string>;
|
|
60
95
|
filter?: (row: Row) => boolean;
|
|
61
96
|
auth?: ResourceAuthDef;
|
|
97
|
+
criteriaOnly?: boolean;
|
|
98
|
+
requestFields?: string[];
|
|
99
|
+
fields?: string[];
|
|
100
|
+
rowLimit?: number;
|
|
62
101
|
}
|
|
63
102
|
export interface EventSchemaOverride {
|
|
64
103
|
table?: string;
|
|
65
104
|
pkOnly?: boolean;
|
|
66
105
|
fieldTypes?: Record<string, string>;
|
|
106
|
+
fields?: FieldDef[];
|
|
107
|
+
className?: string;
|
|
67
108
|
}
|
|
68
109
|
export interface ResourceItem {
|
|
69
110
|
RESOURCE_NAME: string;
|
|
@@ -101,9 +142,11 @@ export interface EventHandlerCtx {
|
|
|
101
142
|
message: GenesisMessage;
|
|
102
143
|
broadcast: BroadcastFn;
|
|
103
144
|
broadcastTableChange: BroadcastTableChangeFn;
|
|
145
|
+
validate: boolean;
|
|
104
146
|
}
|
|
105
147
|
export type EventHandlerResult = {
|
|
106
148
|
generated?: Row[];
|
|
149
|
+
warnings?: NackErrorInput[];
|
|
107
150
|
} | void;
|
|
108
151
|
export type EventHandler = (details: Row, ctx: EventHandlerCtx) => EventHandlerResult;
|
|
109
152
|
export interface HttpRouteDef {
|
|
@@ -133,10 +176,12 @@ export interface MockServerConfig {
|
|
|
133
176
|
httpRoutes?: HttpRouteDef[];
|
|
134
177
|
onMessage?: OnMessageHook;
|
|
135
178
|
productName?: string;
|
|
179
|
+
appName?: string;
|
|
136
180
|
}
|
|
137
181
|
export interface FieldMetadata {
|
|
138
182
|
NAME: string;
|
|
139
183
|
TYPE: string;
|
|
184
|
+
VALID_VALUES?: string[];
|
|
140
185
|
}
|
|
141
186
|
export interface SocketLike {
|
|
142
187
|
readyState: number;
|
package/package.json
CHANGED