@genesislcap/mock-server 15.60.0 → 15.62.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 +287 -40
- package/dist/db/criteria.d.ts +28 -9
- package/dist/db/criteria.js +856 -258
- package/dist/db/criteriaDialect.d.ts +6 -0
- package/dist/db/criteriaDialect.js +441 -0
- package/dist/db/criteriaTokens.d.ts +12 -0
- package/dist/db/criteriaTokens.js +53 -0
- package/dist/db/groovySyntax.d.ts +26 -0
- package/dist/db/groovySyntax.js +481 -0
- package/dist/db/groovyValidator.d.ts +11 -0
- package/dist/db/groovyValidator.js +523 -0
- package/dist/db/gsfExpr.d.ts +21 -0
- package/dist/db/gsfExpr.js +333 -0
- package/dist/db/identity.d.ts +2 -1
- package/dist/db/identity.js +24 -3
- package/dist/db/legacyCriteria.d.ts +7 -0
- package/dist/db/legacyCriteria.js +280 -0
- package/dist/db/ordering.d.ts +2 -0
- package/dist/db/ordering.js +24 -3
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +34 -3
- package/dist/db/store.d.ts +7 -0
- package/dist/db/store.js +55 -7
- package/dist/db/values.d.ts +19 -0
- package/dist/db/values.js +243 -0
- package/dist/handlers/commitEvent.js +3 -2
- package/dist/handlers/dataLogon.d.ts +2 -1
- package/dist/handlers/dataLogon.js +108 -51
- package/dist/handlers/eventValidation.d.ts +2 -1
- package/dist/handlers/eventValidation.js +33 -6
- package/dist/handlers/requestReply.js +43 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/protocol/connection.d.ts +1 -1
- package/dist/server.js +15 -8
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/db/criteria.ts +979 -290
- package/src/db/criteriaDialect.ts +487 -0
- package/src/db/criteriaTokens.ts +72 -0
- package/src/db/groovySyntax.ts +500 -0
- package/src/db/groovyValidator.ts +546 -0
- package/src/db/gsfExpr.ts +392 -0
- package/src/db/identity.ts +22 -4
- package/src/db/legacyCriteria.ts +301 -0
- package/src/db/ordering.ts +27 -3
- package/src/db/schema.ts +43 -8
- package/src/db/store.ts +65 -7
- package/src/db/values.ts +247 -0
- package/src/handlers/commitEvent.ts +3 -2
- package/src/handlers/dataLogon.ts +134 -56
- package/src/handlers/eventValidation.ts +40 -13
- package/src/handlers/requestReply.ts +47 -14
- package/src/index.ts +6 -1
- package/src/protocol/connection.ts +3 -4
- package/src/server.ts +16 -15
- package/src/types.ts +7 -0
|
@@ -16,13 +16,17 @@
|
|
|
16
16
|
// See the README's "Request servers" for every rule and its evidence.
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
|
+
criteriaCompileRefusal,
|
|
19
20
|
CriteriaSyntaxError,
|
|
20
21
|
filterByCriteria,
|
|
21
22
|
parseCriteria,
|
|
22
23
|
parseGenesisDateTime,
|
|
23
24
|
UnsupportedCriteriaError,
|
|
25
|
+
type ParsedCriteria,
|
|
24
26
|
type Predicate,
|
|
25
27
|
} from '../db/criteria.ts';
|
|
28
|
+
import { strictCriteriaRefusal } from '../db/criteriaDialect.ts';
|
|
29
|
+
import { checkGroovySyntax } from '../db/groovySyntax.ts';
|
|
26
30
|
import { withoutRecordFields } from '../db/identity.ts';
|
|
27
31
|
import { compareValues, parseOrderBy, rowComparator, type OrderedField } from '../db/ordering.ts';
|
|
28
32
|
import {
|
|
@@ -34,6 +38,7 @@ import {
|
|
|
34
38
|
type ResolvedField,
|
|
35
39
|
} from '../db/schema.ts';
|
|
36
40
|
import type { Store } from '../db/store.ts';
|
|
41
|
+
import { bigDecimalFields, decimalText, encodeRow } from '../db/values.ts';
|
|
37
42
|
import type { Connection } from '../protocol/connection.ts';
|
|
38
43
|
import { ErrorCode, NackError, nackPayload } from '../protocol/errors.ts';
|
|
39
44
|
import { isLegacyFidelity } from '../protocol/fidelity.ts';
|
|
@@ -92,6 +97,9 @@ interface Source {
|
|
|
92
97
|
// Every field the entity has (its columns, RECORD_ID and TIMESTAMP), by name.
|
|
93
98
|
fields: Map<string, ResolvedField>;
|
|
94
99
|
pkFields: string[];
|
|
100
|
+
// Each field's Genesis type when the source declares its fields: what GSF
|
|
101
|
+
// compiles a criteria against (db/criteria.ts).
|
|
102
|
+
types?: Map<string, string>;
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
function sourceOf(requestReply: RequestReplyDef, config: MockServerConfig, store: Store): Source {
|
|
@@ -101,6 +109,9 @@ function sourceOf(requestReply: RequestReplyDef, config: MockServerConfig, store
|
|
|
101
109
|
name,
|
|
102
110
|
fields: new Map(schema.fields.map((field) => [field.name, field])),
|
|
103
111
|
pkFields: store.getPkFields(name),
|
|
112
|
+
...(schema.declared
|
|
113
|
+
? { types: new Map(schema.fields.map((field) => [field.name, field.type])) }
|
|
114
|
+
: {}),
|
|
104
115
|
};
|
|
105
116
|
}
|
|
106
117
|
|
|
@@ -117,29 +128,47 @@ function parametricTypeOf(name: string, config: MockServerConfig, store: Store):
|
|
|
117
128
|
const byPrimaryKey = (source: Source): OrderedField[] =>
|
|
118
129
|
source.pkFields.map((field) => ({ field, descending: false }));
|
|
119
130
|
|
|
131
|
+
// The REP_ rows as they go on the wire: each BigDecimal a string with its
|
|
132
|
+
// column's scale, as GSF's serializer writes it (db/values.ts).
|
|
133
|
+
function wireRows(rows: Row[], source: Source): Row[] {
|
|
134
|
+
const fields = bigDecimalFields([...source.fields.values()]);
|
|
135
|
+
return fields ? rows.map((row) => encodeRow(row, fields)) : rows;
|
|
136
|
+
}
|
|
137
|
+
|
|
120
138
|
// --- criteria ----------------------------------------------------------------
|
|
121
139
|
|
|
122
140
|
type CriteriaFilter = { kind: 'all' } | { kind: 'none' } | { kind: 'match'; predicate: Predicate };
|
|
123
141
|
|
|
124
142
|
// CRITERIA_MATCH against the entity. A malformed one fails as `onSyntaxError`
|
|
125
|
-
// says (the two server kinds differ)
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
143
|
+
// says (the two server kinds differ): one Groovy refuses (db/groovySyntax.ts),
|
|
144
|
+
// or one this grammar can't parse that the Groovy check can't vouch for
|
|
145
|
+
// either. A criteria naming a field the entity doesn't have matches nothing:
|
|
146
|
+
// GSF evaluates it outside the database, where the unknown variable never
|
|
147
|
+
// compares true (recorded: REPLY: []). Valid Groovy this grammar doesn't model
|
|
148
|
+
// (a bare boolean field, `in [...]`, a function it doesn't know) fails open
|
|
149
|
+
// (logged), as everywhere else in the engine.
|
|
130
150
|
function criteriaFilter(
|
|
131
151
|
criteria: string,
|
|
132
152
|
source: Source,
|
|
133
153
|
onSyntaxError: (error: CriteriaSyntaxError) => Error,
|
|
134
154
|
): CriteriaFilter {
|
|
135
155
|
if (criteria.trim() === '') return { kind: 'all' };
|
|
156
|
+
// What GSF can't compile: a syntax error Groovy reports, this engine's
|
|
157
|
+
// dialect (AND, OR, NOT, `<>`, `=`, a call off GSF's whitelist, ...), or a
|
|
158
|
+
// call no overload takes, with GSF's text.
|
|
159
|
+
const refusal =
|
|
160
|
+
strictCriteriaRefusal(criteria, new Set(source.fields.keys()), source.types) ??
|
|
161
|
+
criteriaCompileRefusal(criteria, source.types);
|
|
162
|
+
if (refusal) throw onSyntaxError(new CriteriaSyntaxError(refusal));
|
|
163
|
+
const groovy = checkGroovySyntax(criteria);
|
|
164
|
+
let parsed: ParsedCriteria;
|
|
136
165
|
try {
|
|
137
|
-
|
|
138
|
-
if (fields.some((field) => !source.fields.has(field))) return { kind: 'none' };
|
|
139
|
-
return { kind: 'match', predicate };
|
|
166
|
+
parsed = parseCriteria(criteria, { types: source.types });
|
|
140
167
|
} catch (error) {
|
|
141
|
-
if (error instanceof CriteriaSyntaxError
|
|
142
|
-
|
|
168
|
+
if (error instanceof CriteriaSyntaxError && groovy.kind !== 'valid') {
|
|
169
|
+
throw onSyntaxError(error);
|
|
170
|
+
}
|
|
171
|
+
if (error instanceof CriteriaSyntaxError || error instanceof UnsupportedCriteriaError) {
|
|
143
172
|
console.warn(
|
|
144
173
|
`[mock-server] Unsupported CRITERIA_MATCH syntax, ignoring filter: ${criteria}`,
|
|
145
174
|
error,
|
|
@@ -148,6 +177,8 @@ function criteriaFilter(
|
|
|
148
177
|
}
|
|
149
178
|
throw error;
|
|
150
179
|
}
|
|
180
|
+
if (parsed.fields.some((field) => !source.fields.has(field))) return { kind: 'none' };
|
|
181
|
+
return { kind: 'match', predicate: parsed.predicate };
|
|
151
182
|
}
|
|
152
183
|
|
|
153
184
|
function applyCriteria(rows: Row[], filter: CriteriaFilter): Row[] {
|
|
@@ -338,7 +369,7 @@ function criteriaOnlyReply(
|
|
|
338
369
|
}
|
|
339
370
|
|
|
340
371
|
return {
|
|
341
|
-
REPLY: page.rows,
|
|
372
|
+
REPLY: wireRows(page.rows, source),
|
|
342
373
|
PARAMETRIC_TYPE: parametricTypeOf(source.name, config, store),
|
|
343
374
|
MESSAGE_TYPE: `REP_${resourceName}`,
|
|
344
375
|
...(byView && page.rows.length > 0 ? { NEXT_VIEW: viewNumber + 1 } : {}),
|
|
@@ -389,9 +420,11 @@ function convertValue(value: unknown, field: ResolvedField): unknown {
|
|
|
389
420
|
}
|
|
390
421
|
|
|
391
422
|
// What Kotlin's toString() makes of a row value, for wildcard matching: a
|
|
392
|
-
// Double keeps its fraction ("2.0"), a
|
|
423
|
+
// Double keeps its fraction ("2.0"), a BigDecimal its scale ("15.00000"), a
|
|
424
|
+
// date prints as ISO-8601, null is "null".
|
|
393
425
|
function kotlinString(value: unknown, field: ResolvedField | undefined): string {
|
|
394
426
|
if (value === undefined || value === null) return 'null';
|
|
427
|
+
if (field?.type === 'BIGDECIMAL') return decimalText(value, field.scale) ?? String(value);
|
|
395
428
|
if (typeof value === 'number') {
|
|
396
429
|
if ((field?.type === 'DATE' || field?.type === 'DATETIME') && Number.isFinite(value)) {
|
|
397
430
|
return new Date(value).toISOString();
|
|
@@ -538,7 +571,7 @@ function plainReply(
|
|
|
538
571
|
MESSAGE_TYPE: `REP_${resourceName}`,
|
|
539
572
|
SOURCE_REF: message.SOURCE_REF,
|
|
540
573
|
PARAMETRIC_TYPE: parametricTypeOf(source.name, config, store),
|
|
541
|
-
REPLY: reply,
|
|
574
|
+
REPLY: wireRows(reply, source),
|
|
542
575
|
};
|
|
543
576
|
}
|
|
544
577
|
|
|
@@ -667,7 +700,7 @@ function legacyRequestReply(
|
|
|
667
700
|
rows = rows.filter((row) => matchesLegacyRequestFields(row, request));
|
|
668
701
|
}
|
|
669
702
|
const criteria = message.DETAILS?.CRITERIA_MATCH ?? message.CRITERIA_MATCH;
|
|
670
|
-
if (criteria) rows = filterByCriteria(rows, criteria);
|
|
703
|
+
if (criteria) rows = filterByCriteria(rows, criteria, { lenient: true });
|
|
671
704
|
if (requestReply.filter) rows = rows.filter(requestReply.filter);
|
|
672
705
|
}
|
|
673
706
|
rows = applyResourceAuth(rows, requestReply.auth, user).map(withoutRecordFields);
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,12 @@ export {
|
|
|
12
12
|
type NackErrorInput,
|
|
13
13
|
type NackErrorOptions,
|
|
14
14
|
} from './protocol/errors.ts';
|
|
15
|
-
export {
|
|
15
|
+
export {
|
|
16
|
+
compileCriteria,
|
|
17
|
+
type CriteriaOptions,
|
|
18
|
+
filterByCriteria,
|
|
19
|
+
type Predicate,
|
|
20
|
+
} from './db/criteria.ts';
|
|
16
21
|
export type {
|
|
17
22
|
AntiJoinDef,
|
|
18
23
|
AuthConfig,
|
|
@@ -31,10 +31,9 @@ export interface Subscription {
|
|
|
31
31
|
// The columns this subscription's rows carry (the query's fields block or
|
|
32
32
|
// declared columns, narrowed by DETAILS.FIELDS); undefined: rows as stored.
|
|
33
33
|
fieldNames?: string[];
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
queueInsert?: (key: string, stamped: Row, row: Row) => boolean;
|
|
34
|
+
// Turns a row into its wire form (BigDecimals as scaled strings); unset
|
|
35
|
+
// when the query has nothing to encode, and under fidelity: 'legacy'.
|
|
36
|
+
encode?: (row: Row) => Row;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
// Wraps one WebSocket client connection: auth state and its live DATA_LOGON
|
package/src/server.ts
CHANGED
|
@@ -82,7 +82,9 @@ export interface MockServer {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
export function createMockServer(config: MockServerConfig): MockServer {
|
|
85
|
-
|
|
85
|
+
// Declared columns are held by type (db/values.ts), except under
|
|
86
|
+
// fidelity: 'legacy', which keeps rows as given.
|
|
87
|
+
const store = new Store({ normalizeValues: () => !isLegacyFidelity(config) });
|
|
86
88
|
for (const [tableName, tableDef] of Object.entries(config.tables ?? {})) {
|
|
87
89
|
store.registerTable(tableName, tableDef);
|
|
88
90
|
}
|
|
@@ -119,6 +121,13 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
// A row as one subscription sends it: its columns (dataserverRow), each
|
|
125
|
+
// BigDecimal a scaled string (Subscription.encode).
|
|
126
|
+
function wireRow(subscription: Subscription, permitted: Row, row: Row): Row {
|
|
127
|
+
const sent = dataserverRow(permitted, row, config.queries?.[subscription.resourceName]?.fields);
|
|
128
|
+
return subscription.encode ? subscription.encode(sent) : sent;
|
|
129
|
+
}
|
|
130
|
+
|
|
122
131
|
// The operation to push to one subscription, or null to skip it entirely.
|
|
123
132
|
// A MODIFY that moved the row out of this subscription's CRITERIA_MATCH
|
|
124
133
|
// becomes a DELETE so the client drops it rather than keeping a stale ghost
|
|
@@ -150,7 +159,10 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
150
159
|
// - A held row gets the MODIFY, or a DELETE when it is gone or out of scope.
|
|
151
160
|
// - A row the client doesn't hold that is (now) in scope is pushed as an
|
|
152
161
|
// INSERT and becomes held: a brand-new row, or one a MODIFY brought back
|
|
153
|
-
// into the criteria. Out of scope, the client is told nothing.
|
|
162
|
+
// into the criteria. Out of scope, the client is told nothing. The
|
|
163
|
+
// snapshot's order doesn't matter: a new row that sorts among the rows
|
|
164
|
+
// still waiting is pushed now too, as GSF's client index does, and no
|
|
165
|
+
// later page repeats it.
|
|
154
166
|
// An INSERT is pushed even when the view already holds MAX_VIEW rows; the
|
|
155
167
|
// real server would also evict the oldest row in the same frame (moving
|
|
156
168
|
// view), which this engine doesn't, so the view can grow past MAX_VIEW.
|
|
@@ -175,7 +187,7 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
175
187
|
: permittedQueryRow(row, query?.auth, requestingUser(connection), subscription.fieldNames);
|
|
176
188
|
if (pending.has(key)) {
|
|
177
189
|
if (permitted)
|
|
178
|
-
pending.set(key, stampRow(
|
|
190
|
+
pending.set(key, stampRow(wireRow(subscription, permitted, row), rowRef, 'INSERT'));
|
|
179
191
|
else pending.delete(key);
|
|
180
192
|
return null;
|
|
181
193
|
}
|
|
@@ -185,17 +197,6 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
185
197
|
return 'DELETE';
|
|
186
198
|
}
|
|
187
199
|
if (!permitted) return null;
|
|
188
|
-
// Under ORDER_BY, a new row that sorts among the waiting ones waits too,
|
|
189
|
-
// so MORE_ROWS pages stay in order.
|
|
190
|
-
if (
|
|
191
|
-
subscription.queueInsert?.(
|
|
192
|
-
key,
|
|
193
|
-
stampRow(dataserverRow(permitted, row, query?.fields), rowRef, 'INSERT'),
|
|
194
|
-
row,
|
|
195
|
-
)
|
|
196
|
-
)
|
|
197
|
-
return null;
|
|
198
|
-
|
|
199
200
|
held.add(key);
|
|
200
201
|
return 'INSERT';
|
|
201
202
|
}
|
|
@@ -251,7 +252,7 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
251
252
|
subscription.fieldNames,
|
|
252
253
|
);
|
|
253
254
|
if (!permitted) continue;
|
|
254
|
-
outRow =
|
|
255
|
+
outRow = wireRow(subscription, permitted, row);
|
|
255
256
|
}
|
|
256
257
|
subscription.sequenceId += 1;
|
|
257
258
|
// A live push carries neither MORE_ROWS nor ROWS_COUNT on the real
|
package/src/types.ts
CHANGED
|
@@ -47,6 +47,13 @@ export interface FieldDef {
|
|
|
47
47
|
default?: unknown;
|
|
48
48
|
// STRING column size (`STRING(100)`): MAX_LENGTH / maxLength.
|
|
49
49
|
maxSize?: number;
|
|
50
|
+
// A BIGDECIMAL column's scale (`BIGDECIMAL(20, 2)` in the dictionary):
|
|
51
|
+
// values are rounded HALF_UP to it when written and sent as strings with
|
|
52
|
+
// exactly that many decimals ("15.00000"). Default 5, GSF's SQL default
|
|
53
|
+
// (BigDecimalSqlPrecisionConfig). A view's derived BIGDECIMAL has no column
|
|
54
|
+
// and goes out with its own decimals unless this says otherwise. See the
|
|
55
|
+
// README's "Value encoding".
|
|
56
|
+
scale?: number;
|
|
50
57
|
// `metadata { minLength = 0 }`: MIN_LENGTH / minLength.
|
|
51
58
|
minLength?: number;
|
|
52
59
|
// Replaces the DESCRIPTION / description the real server fills with the
|