@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.
Files changed (48) hide show
  1. package/README.md +210 -27
  2. package/dist/db/criteria.d.ts +12 -0
  3. package/dist/db/criteria.js +39 -29
  4. package/dist/db/ordering.d.ts +8 -0
  5. package/dist/db/ordering.js +49 -0
  6. package/dist/db/schema.d.ts +17 -1
  7. package/dist/db/schema.js +131 -16
  8. package/dist/db/store.d.ts +8 -2
  9. package/dist/db/store.js +120 -44
  10. package/dist/handlers/commitEvent.js +32 -8
  11. package/dist/handlers/crudEvents.d.ts +2 -0
  12. package/dist/handlers/crudEvents.js +138 -0
  13. package/dist/handlers/dataLogon.js +2 -23
  14. package/dist/handlers/eventValidation.d.ts +5 -0
  15. package/dist/handlers/eventValidation.js +29 -0
  16. package/dist/handlers/jsonSchema.d.ts +3 -2
  17. package/dist/handlers/jsonSchema.js +23 -17
  18. package/dist/handlers/meta.js +13 -9
  19. package/dist/handlers/requestReply.js +518 -40
  20. package/dist/handlers/resources.js +3 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/protocol/fieldTypes.d.ts +1 -0
  23. package/dist/protocol/fieldTypes.js +6 -0
  24. package/dist/protocol/msgNack.d.ts +2 -0
  25. package/dist/protocol/msgNack.js +10 -0
  26. package/dist/protocol/schemaValidation.d.ts +11 -0
  27. package/dist/protocol/schemaValidation.js +186 -0
  28. package/dist/server.js +5 -1
  29. package/dist/types.d.ts +5 -0
  30. package/package.json +1 -1
  31. package/src/db/criteria.ts +52 -27
  32. package/src/db/ordering.ts +56 -0
  33. package/src/db/schema.ts +164 -15
  34. package/src/db/store.ts +146 -46
  35. package/src/handlers/commitEvent.ts +39 -8
  36. package/src/handlers/crudEvents.ts +181 -0
  37. package/src/handlers/dataLogon.ts +2 -19
  38. package/src/handlers/eventValidation.ts +43 -0
  39. package/src/handlers/jsonSchema.ts +31 -19
  40. package/src/handlers/meta.ts +14 -9
  41. package/src/handlers/requestReply.ts +625 -41
  42. package/src/handlers/resources.ts +3 -0
  43. package/src/index.ts +1 -0
  44. package/src/protocol/fieldTypes.ts +7 -0
  45. package/src/protocol/msgNack.ts +12 -0
  46. package/src/protocol/schemaValidation.ts +208 -0
  47. package/src/server.ts +5 -1
  48. package/src/types.ts +56 -18
@@ -1,3 +1,4 @@
1
+ import { crudEventNames } from '../db/schema.ts';
1
2
  import type { Connection } from '../protocol/connection.ts';
2
3
  import { MESSAGE_TYPE } from '../protocol/messageTypes.ts';
3
4
  import type { GenesisMessage, MockServerConfig, ResourceItem } from '../types.ts';
@@ -19,6 +20,8 @@ function deriveResources(config: MockServerConfig): ResourceItem[] {
19
20
  }
20
21
  const eventNames = new Set([
21
22
  ...Object.keys(config.eventHandlers ?? {}),
23
+ // The generic CRUD events tables register through TableDef.events.
24
+ ...crudEventNames(config),
22
25
  // Events served only by eventSchemas + defaultEventHandler still need to
23
26
  // be discoverable, or the client-side resource gate blocks them before
24
27
  // the commit is ever sent.
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ export type {
19
19
  AuthUser,
20
20
  BroadcastFn,
21
21
  BroadcastTableChangeFn,
22
+ CrudEventVerb,
22
23
  EventHandler,
23
24
  EventHandlerCtx,
24
25
  EventHandlerResult,
@@ -93,6 +93,13 @@ export function daoClassOf(table: string): string {
93
93
  return `global.genesis.gen.dao.${pascalCase(table)}`;
94
94
  }
95
95
 
96
+ // The generated entity class of a view (ViewEntityGenerator.kt PACKAGE_NAME,
97
+ // GSF v8.15.29): what a request server over the view names in
98
+ // PARAMETRIC_TYPE.
99
+ export function viewEntityClassOf(view: string): string {
100
+ return `global.genesis.gen.view.entity.${pascalCase(view)}`;
101
+ }
102
+
96
103
  // The generated enum class of an ENUM column.
97
104
  export function enumClassOf(table: string | undefined, field: string, appName?: string): string {
98
105
  const segments = ['global.genesis.gen.dao.enums'];
@@ -1,6 +1,18 @@
1
1
  import type { Row } from '../types.ts';
2
+ import type { NackErrorEntry } from './errors.ts';
2
3
  import { MESSAGE_TYPE } from './messageTypes.ts';
3
4
 
5
+ // A service's own MSG_NACK (genesis-messages MsgNack): how a request server
6
+ // answers a request it fails, GSF 8.15.x. Unlike the router's frame below, its
7
+ // items are GenesisErrors (@type, a string CODE, TEXT, STATUS_CODE), there is
8
+ // a WARNING list and no DETAILS. Recorded: a criteria-only REQ_ without
9
+ // REQUEST answers {WARNING: [], ERROR: [{'@type': 'StandardError', CODE:
10
+ // 'INTERNAL_ERROR', TEXT: 'Unexpected server error', STATUS_CODE: '500
11
+ // Internal Server Error'}], MESSAGE_TYPE: 'MSG_NACK'}.
12
+ export function serviceMsgNack(errors: NackErrorEntry[], warnings: NackErrorEntry[] = []): Row {
13
+ return { WARNING: warnings, ERROR: errors, MESSAGE_TYPE: MESSAGE_TYPE.MSG_NACK };
14
+ }
15
+
4
16
  const HTTP_NOT_FOUND = 404;
5
17
  const HTTP_CONFLICT = 409;
6
18
 
@@ -0,0 +1,208 @@
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
+
20
+ import type { NackErrorInput } from './errors.ts';
21
+
22
+ type Schema = Record<string, any>;
23
+
24
+ export interface SchemaError {
25
+ keyword: string;
26
+ // PathType.LEGACY: '$', '$.DETAILS', '$.DETAILS.NAME'.
27
+ instanceLocation: string;
28
+ // The missing property, for `required`; the unknown one, for
29
+ // `additionalProperties`.
30
+ property?: string;
31
+ message: string;
32
+ }
33
+
34
+ // networknt's JsonType names. With losslessNarrowing a number with no
35
+ // fraction is an integer (1.0 included, which JSON.parse makes 1 anyway).
36
+ function jsonTypeOf(value: unknown): string {
37
+ if (value === null) return 'null';
38
+ if (Array.isArray(value)) return 'array';
39
+ if (typeof value === 'number') return Number.isInteger(value) ? 'integer' : 'number';
40
+ return typeof value;
41
+ }
42
+
43
+ function matchesType(value: unknown, type: string): boolean {
44
+ const actual = jsonTypeOf(value);
45
+ return actual === type || (type === 'number' && actual === 'integer');
46
+ }
47
+
48
+ const isObject = (value: unknown): value is Record<string, unknown> =>
49
+ typeof value === 'object' && value !== null && !Array.isArray(value);
50
+
51
+ function deepEqual(a: unknown, b: unknown): boolean {
52
+ if (a === b) return true;
53
+ if (Array.isArray(a) && Array.isArray(b)) {
54
+ return a.length === b.length && a.every((item, index) => deepEqual(item, b[index]));
55
+ }
56
+ if (isObject(a) && isObject(b)) {
57
+ const keys = Object.keys(a);
58
+ return (
59
+ keys.length === Object.keys(b).length &&
60
+ keys.every((key) => Object.hasOwn(b, key) && deepEqual(a[key], b[key]))
61
+ );
62
+ }
63
+ return false;
64
+ }
65
+
66
+ // networknt evaluates these before a schema's other keywords, whatever their
67
+ // position; the rest go in key order.
68
+ const FIRST_KEYWORDS = ['type', 'properties'];
69
+
70
+ // String lengths count code points, as Java's codePointCount does.
71
+ const codePoints = (text: string) => [...text].length;
72
+
73
+ interface KeywordCheck {
74
+ arg: any;
75
+ schema: Schema;
76
+ value: unknown;
77
+ location: string;
78
+ errors: SchemaError[];
79
+ fail(message: string, property?: string): void;
80
+ }
81
+
82
+ const NOT_DEFINED =
83
+ 'is not defined in the schema and the schema does not allow additional properties';
84
+
85
+ // One check per keyword, each a no-op on values it doesn't apply to (string
86
+ // keywords on non-strings, object keywords on non-objects, ...).
87
+ const KEYWORDS: Record<string, (check: KeywordCheck) => void> = {
88
+ type({ arg, value, fail }) {
89
+ if (typeof arg === 'string' && !matchesType(value, arg)) {
90
+ fail(`${jsonTypeOf(value)} found, ${arg} expected`);
91
+ }
92
+ },
93
+ const({ arg, value, fail }) {
94
+ if (!deepEqual(value, arg)) fail(`must be the constant value '${String(arg)}'`);
95
+ },
96
+ enum({ arg, value, fail }) {
97
+ if (Array.isArray(arg) && !arg.some((candidate) => deepEqual(value, candidate))) {
98
+ const values = arg.map((item) => JSON.stringify(item)).join(', ');
99
+ fail(`does not have a value in the enumeration [${values}]`);
100
+ }
101
+ },
102
+ minLength({ arg, value, fail }) {
103
+ if (typeof value === 'string' && codePoints(value) < arg) {
104
+ fail(`must be at least ${arg} characters long`);
105
+ }
106
+ },
107
+ maxLength({ arg, value, fail }) {
108
+ if (typeof value === 'string' && codePoints(value) > arg) {
109
+ fail(`must be at most ${arg} characters long`);
110
+ }
111
+ },
112
+ minimum({ arg, value, fail }) {
113
+ if (typeof value === 'number' && value < arg) fail(`must have a minimum value of ${arg}`);
114
+ },
115
+ maximum({ arg, value, fail }) {
116
+ if (typeof value === 'number' && value > arg) fail(`must have a maximum value of ${arg}`);
117
+ },
118
+ // JSON Schema patterns are unanchored: '[0-9]+' accepts 'x1y'.
119
+ pattern({ arg, value, fail }) {
120
+ if (typeof value === 'string' && !new RegExp(arg, 'u').test(value)) {
121
+ fail(`does not match the regex pattern ${arg}`);
122
+ }
123
+ },
124
+ oneOf({ arg, value, location, errors, fail }) {
125
+ if (!Array.isArray(arg)) return;
126
+ const results = arg.map((branch: Schema) => validateSchema(branch, value, location));
127
+ const valid = results.flatMap((result, index) => (result.length === 0 ? [index] : []));
128
+ if (valid.length === 1) return;
129
+ if (valid.length > 1) {
130
+ fail(
131
+ `must be valid to one and only one schema, but ${valid.length} are valid with indexes '${valid.join(', ')}'`,
132
+ );
133
+ return;
134
+ }
135
+ fail('must be valid to one and only one schema, but 0 are valid');
136
+ errors.push(...results.flat());
137
+ },
138
+ properties({ arg, value, location, errors }) {
139
+ if (!isObject(value) || !isObject(arg)) return;
140
+ for (const [name, propertySchema] of Object.entries(arg)) {
141
+ if (!Object.hasOwn(value, name)) continue;
142
+ errors.push(...validateSchema(propertySchema as Schema, value[name], `${location}.${name}`));
143
+ }
144
+ },
145
+ required({ arg, value, fail }) {
146
+ if (!isObject(value) || !Array.isArray(arg)) return;
147
+ for (const name of arg) {
148
+ if (!Object.hasOwn(value, name)) fail(`required property '${name}' not found`, name);
149
+ }
150
+ },
151
+ additionalProperties({ arg, schema, value, location, errors, fail }) {
152
+ if (!isObject(value) || (arg !== false && !isObject(arg))) return;
153
+ const known = isObject(schema.properties) ? schema.properties : {};
154
+ for (const name of Object.keys(value)) {
155
+ if (Object.hasOwn(known, name)) continue;
156
+ if (arg === false) fail(`property '${name}' ${NOT_DEFINED}`, name);
157
+ else errors.push(...validateSchema(arg, value[name], `${location}.${name}`));
158
+ }
159
+ },
160
+ // Anything else is an annotation (description, title, default, readOnly,
161
+ // genesisType, $schema, $defs, ...) and validates nothing; networknt
162
+ // doesn't enforce readOnly unless asked to.
163
+ };
164
+
165
+ export function validateSchema(schema: Schema, value: unknown, location = '$'): SchemaError[] {
166
+ const errors: SchemaError[] = [];
167
+ if (!isObject(schema)) return errors;
168
+ const keywords = Object.keys(schema);
169
+ const ordered = [
170
+ ...FIRST_KEYWORDS.filter((keyword) => keywords.includes(keyword)),
171
+ ...keywords.filter((keyword) => !FIRST_KEYWORDS.includes(keyword)),
172
+ ];
173
+ for (const keyword of ordered) {
174
+ if (!Object.hasOwn(KEYWORDS, keyword)) continue;
175
+ KEYWORDS[keyword]({
176
+ arg: schema[keyword],
177
+ schema,
178
+ value,
179
+ location,
180
+ errors,
181
+ fail: (message, property) =>
182
+ errors.push({
183
+ keyword,
184
+ instanceLocation: location,
185
+ ...(property !== undefined ? { property } : {}),
186
+ message,
187
+ }),
188
+ });
189
+ }
190
+ return errors;
191
+ }
192
+
193
+ // GenesisMessageDecoder's FieldError for each failure: TEXT is the error's
194
+ // toString() ("<location>: <message>"), FIELD the missing property for
195
+ // `required` and otherwise the last segment of the location (so an unknown
196
+ // DETAILS property names 'DETAILS'), PATH the location.
197
+ export function schemaFieldErrors(errors: SchemaError[]): NackErrorInput[] {
198
+ return errors.map((error) => ({
199
+ '@type': 'FieldError',
200
+ CODE: 'VALIDATION_ERROR',
201
+ TEXT: `${error.instanceLocation}: ${error.message}`,
202
+ FIELD:
203
+ error.keyword === 'required'
204
+ ? error.property
205
+ : error.instanceLocation.slice(error.instanceLocation.lastIndexOf('.') + 1),
206
+ PATH: error.instanceLocation,
207
+ }));
208
+ }
package/src/server.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import http from 'node:http';
2
2
  import { WebSocketServer, type WebSocket } from 'ws';
3
3
  import { Store } from './db/store.ts';
4
+ import { crudEventHandler } from './handlers/crudEvents.ts';
4
5
  import { pendingKey } from './handlers/dataLogon.ts';
5
6
  import { createHttpRequestListener } from './handlers/http.ts';
6
7
  import { permittedQueryRow, requestingUser } from './handlers/resourceAuth.ts';
@@ -384,8 +385,11 @@ export function createMockServer(config: MockServerConfig): MockServer {
384
385
  return config.requestReplies?.[resourceName];
385
386
  }
386
387
 
388
+ // An eventHandlers entry, else the generic CRUD handler a table registers
389
+ // (TableDef.events) — so extendEventHandler can wrap a generic event; the
390
+ // wrapper is then an explicit handler, which wins.
387
391
  function getEventHandler(eventName: string): EventHandler | undefined {
388
- return config.eventHandlers?.[eventName];
392
+ return config.eventHandlers?.[eventName] ?? crudEventHandler(eventName, config);
389
393
  }
390
394
 
391
395
  // augment(current) => replacement. `current` is undefined if nothing was
package/src/types.ts CHANGED
@@ -66,17 +66,22 @@ export interface FieldDef {
66
66
  optional?: boolean;
67
67
  }
68
68
 
69
- // A field the database fills on insert. Declares the shape only: metadata
70
- // marks it NULLABLE:false, OPTIONAL:true and leaves it out of `required`.
71
- // Value generation still comes from `sequencePrefix` (single-field keys).
72
- // A table with `sequencePrefix` gets its key marked as a SEQUENCE without
73
- // declaring it here.
69
+ // A field the database fills on insert when the record leaves it out (a
70
+ // supplied value is kept). Metadata marks it NULLABLE:false, OPTIONAL:true
71
+ // and leaves it out of `required`. A SEQUENCE mints values like the key's
72
+ // sequence (`prefix`, else the table's sequencePrefix, in its
73
+ // sequenceFormat); an AUTO_INCREMENT counts up from the highest seeded value
74
+ // (1 when there is none). A table with `sequencePrefix` gets its key
75
+ // generated as a SEQUENCE without declaring it here.
74
76
  export interface GeneratedFieldDef {
75
77
  field: string;
76
78
  kind: 'SEQUENCE' | 'AUTO_INCREMENT';
77
79
  prefix?: string;
78
80
  }
79
81
 
82
+ // The verbs of the generic CRUD events (see TableDef.events).
83
+ export type CrudEventVerb = 'INSERT' | 'MODIFY' | 'DELETE';
84
+
80
85
  // A table or dataserver-query index (`indices { unique("A") }`). FIELDS is
81
86
  // sent space-separated (`'A B'`).
82
87
  export interface IndexDef {
@@ -103,10 +108,22 @@ export interface TableDef {
103
108
  // inferred from the rows (src/db/metadata.ts).
104
109
  fields?: FieldDef[];
105
110
  generated?: GeneratedFieldDef[];
106
- // Table indices. Declared for later items (ORDER_BY, duplicate keys); a
107
- // dataserver query only reports the indices on its own QueryDef.indexes, as
108
- // the real dataserver does.
111
+ // Table indices. The generic CRUD events NACK DUPLICATE_KEY on a unique
112
+ // one, and a unique index over exactly the key fields names the primary
113
+ // key (else '<TABLE>_BY_<fields>'). A dataserver query only reports the
114
+ // indices on its own QueryDef.indexes, as the real dataserver does.
109
115
  indexes?: IndexDef[];
116
+ // Registers generic CRUD event handlers for this table —
117
+ // EVENT_<TABLE>_INSERT, EVENT_<TABLE>_MODIFY, EVENT_<TABLE>_DELETE — the
118
+ // ones Genesis Create generates (entityDb.insert / modify / delete, no
119
+ // onValidate): insert the record (generating sequence and auto-increment
120
+ // fields, GENERATED listing them all; DUPLICATE_KEY on a taken key), modify
121
+ // or delete it by its primary key (RECORD_NOT_FOUND), ack VALIDATE without
122
+ // writing, push the change live. They are listed in RESOURCES and described
123
+ // by META_REQUEST / JSON_SCHEMA_REQUEST like any registered event. An
124
+ // eventHandlers entry of the same name wins. See the README's "Generic CRUD
125
+ // events".
126
+ events?: CrudEventVerb[];
110
127
  }
111
128
 
112
129
  // 'prefix' (the default): sequencePrefix + a counter padded to sequenceWidth
@@ -210,26 +227,41 @@ export interface RequestReplyDef {
210
227
  // (`resolver`) — for request servers that calculate rather than read (date
211
228
  // calculations, holiday validate/roll, premium schedules, composite
212
229
  // lookups). A resolver receives the REQUEST fields and returns the REPLY
213
- // rows verbatim; throw NackError for a protocol-correct error reply. When
214
- // both are set, resolver wins. With resolver only, set `metadataRows` (a
215
- // representative sample reply) or `fieldTypes` so META_REQUEST can still
216
- // derive REPLY_FIELD metadata.
230
+ // rows verbatim; throw NackError for a protocol-correct error reply (a
231
+ // MSG_NACK). When both are set, resolver wins. With resolver only, set
232
+ // `metadataRows` (a representative sample reply) or `fieldTypes` so
233
+ // META_REQUEST can still derive REPLY_FIELD metadata.
217
234
  source?: string;
218
235
  resolver?: (ctx: RequestReplyResolverCtx) => Row[];
219
236
  metadataRows?: Row[];
220
237
  fieldTypes?: Record<string, string>;
238
+ // The request server's `filter { }` / `where { }` clause. With `filter` or
239
+ // `auth`, a criteria-only server reads past the rows they drop, and
240
+ // NEXT_OFFSET says where it stopped (README: Request servers).
221
241
  filter?: (row: Row) => boolean;
222
242
  auth?: ResourceAuthDef;
223
- // `criteriaOnlyRequest = true` in the request server's config: META_REQUEST
224
- // then reports REQUEST_FIELD: [], CRITERIA_ONLY_REQUEST: true,
225
- // SORTABLE_FIELDS and CRITERIA_FIELDS, and adds RECORD_ID and TIMESTAMP to
226
- // REPLY_FIELD. Metadata only for now: OFFSET paging is not implemented, and
227
- // grid-pro's server-side and infinite row models page a criteria-only
228
- // resource with OFFSET.
243
+ // `criteriaOnlyRequest = true` in the request server's config. REQUEST must
244
+ // then be present (an object; its fields are ignored), DETAILS.OFFSET pages,
245
+ // DETAILS.ORDER_BY sorts by fields, and every REP_ carries MORE_ROWS (plus
246
+ // NEXT_OFFSET while there are more). META_REQUEST reports REQUEST_FIELD: [],
247
+ // CRITERIA_ONLY_REQUEST: true, SORTABLE_FIELDS and CRITERIA_FIELDS. Without
248
+ // it, REQUEST's fields select rows (exact values, `*` wildcards,
249
+ // FIELD_FROM/FIELD_TO ranges; an array runs each) and OFFSET is ignored.
250
+ // See the README's "Request servers".
229
251
  criteriaOnly?: boolean;
230
252
  // The request block's fields (REQUEST_FIELD, and the JSON schema's REQUEST).
231
253
  // Defaults to the source's primary key, as for a GPAL requestReply(TABLE).
232
254
  requestFields?: string[];
255
+ // The reply block (`reply { }` in GPAL): REP_ rows carry exactly these
256
+ // columns, in this order, null when unset, and so do REPLY_FIELD and the JSON
257
+ // schema's REPLY. Names a declared source doesn't have are dropped.
258
+ // RECORD_ID and TIMESTAMP are sent only when named here. Unset: every column
259
+ // of the source followed by RECORD_ID and TIMESTAMP, as GSF sends a table
260
+ // entity. Ignored with a resolver.
261
+ fields?: string[];
262
+ // `config { maxRows = N }`: the most rows a REP_ carries when the request
263
+ // names no DETAILS.MAX_ROWS. Default 10000 (RequestReply.DEFAULT_MAXIMUM_ROWS).
264
+ rowLimit?: number;
233
265
  }
234
266
 
235
267
  // How META_REQUEST / JSON_SCHEMA_REQUEST describe an event whose name doesn't
@@ -312,6 +344,12 @@ export interface EventHandlerCtx {
312
344
  message: GenesisMessage;
313
345
  broadcast: BroadcastFn;
314
346
  broadcastTableChange: BroadcastTableChangeFn;
347
+ // `VALIDATE: true` on the message: the client asks whether the event would
348
+ // succeed. GSF then runs only the handler's onValidate and answers its NACK,
349
+ // or an EVENT_ACK with GENERATED: [] — nothing is written. A handler that
350
+ // writes should run its checks and return before writing when this is set;
351
+ // the generic CRUD events do.
352
+ validate: boolean;
315
353
  }
316
354
 
317
355
  // `warnings` turns the reply into an EVENT_NACK carrying them in WARNING