@genesislcap/mock-server 15.52.0 → 15.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -27
- package/dist/db/criteria.d.ts +12 -0
- package/dist/db/criteria.js +39 -29
- package/dist/db/ordering.d.ts +8 -0
- package/dist/db/ordering.js +49 -0
- package/dist/db/schema.d.ts +17 -1
- package/dist/db/schema.js +131 -16
- package/dist/db/store.d.ts +8 -2
- package/dist/db/store.js +120 -44
- package/dist/handlers/commitEvent.js +32 -8
- package/dist/handlers/crudEvents.d.ts +2 -0
- package/dist/handlers/crudEvents.js +138 -0
- package/dist/handlers/dataLogon.js +2 -23
- package/dist/handlers/eventValidation.d.ts +5 -0
- package/dist/handlers/eventValidation.js +29 -0
- package/dist/handlers/jsonSchema.d.ts +3 -2
- package/dist/handlers/jsonSchema.js +23 -17
- package/dist/handlers/meta.js +13 -9
- package/dist/handlers/requestReply.js +518 -40
- package/dist/handlers/resources.js +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/protocol/fieldTypes.d.ts +1 -0
- package/dist/protocol/fieldTypes.js +6 -0
- package/dist/protocol/msgNack.d.ts +2 -0
- package/dist/protocol/msgNack.js +10 -0
- package/dist/protocol/schemaValidation.d.ts +11 -0
- package/dist/protocol/schemaValidation.js +186 -0
- package/dist/server.js +5 -1
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/db/criteria.ts +52 -27
- package/src/db/ordering.ts +56 -0
- package/src/db/schema.ts +164 -15
- package/src/db/store.ts +146 -46
- package/src/handlers/commitEvent.ts +39 -8
- package/src/handlers/crudEvents.ts +181 -0
- package/src/handlers/dataLogon.ts +2 -19
- package/src/handlers/eventValidation.ts +43 -0
- package/src/handlers/jsonSchema.ts +31 -19
- package/src/handlers/meta.ts +14 -9
- package/src/handlers/requestReply.ts +625 -41
- package/src/handlers/resources.ts +3 -0
- package/src/index.ts +1 -0
- package/src/protocol/fieldTypes.ts +7 -0
- package/src/protocol/msgNack.ts +12 -0
- package/src/protocol/schemaValidation.ts +208 -0
- package/src/server.ts +5 -1
- package/src/types.ts +56 -18
|
@@ -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
|
@@ -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'];
|
package/src/protocol/msgNack.ts
CHANGED
|
@@ -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
|
|
70
|
-
// marks it NULLABLE:false, OPTIONAL:true
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
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.
|
|
107
|
-
//
|
|
108
|
-
//
|
|
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
|
|
214
|
-
// both are set, resolver wins. With resolver only, set
|
|
215
|
-
// representative sample reply) or `fieldTypes` so
|
|
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
|
|
224
|
-
// then
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
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
|