@reventlessdev/reventless-local 3.0.0-alpha.301 → 3.0.0-alpha.303
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/CHANGELOG.md +46 -0
- package/package.json +9 -9
- package/src/EmitCapabilities.res +12 -5
- package/src/EmitCapabilities.res.mjs +13 -1
- package/src/Platform.res +1 -2
- package/src/Platform.res.mjs +6 -10
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +2 -2
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +0 -2
- package/src/adapter/LocalCapabilities.res +1 -1
- package/src/adapter/LocalCapabilities.res.mjs +2 -1
- package/src/adapter/LocalFingerprintKey.res +66 -0
- package/src/adapter/LocalFingerprintKey.res.mjs +75 -0
- package/src/adapter/LocalUploadResolvers.res +7 -7
- package/src/adapter/LocalUploadResolvers.res.mjs +5 -4
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +0 -1
- package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +0 -1
- package/tests/EmitCapabilitiesTest.res +6 -0
- package/tests/EmitCapabilitiesTest.res.mjs +6 -0
- package/tests/PluginEventDecodeTest.res.mjs +2 -2
- package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +1 -1
- package/tests/adapter/DcbEventLogStorageTest.res.mjs +1 -1
- package/tests/adapter/ObjectStorePersistenceTest.res +6 -3
- package/tests/adapter/ObjectStorePersistenceTest.res.mjs +3 -3
- package/tests/adapter/ServedBucketHttpTest.res +3 -1
- package/tests/adapter/ServedBucketHttpTest.res.mjs +2 -1
- package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +1 -1
- package/tests/components/dcb/DcbEventLogAppendStreamTest.res.mjs +1 -1
- package/tests/components/dcb/DcbRepeatTest.res +291 -0
- package/tests/components/dcb/DcbRepeatTest.res.mjs +420 -0
- package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res +71 -15
- package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res.mjs +119 -14
- package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res.mjs +1 -1
- package/tests/components/stateviewslice/StateViewSliceFixtures.res.mjs +1 -1
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res.mjs +1 -1
- package/tests/plugin/Plugin_Fixtures.res +1 -3
- package/tests/plugin/Plugin_Fixtures.res.mjs +1 -2
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
5
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Stream from "effect/Stream";
|
|
8
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
9
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
10
|
+
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
11
|
+
import * as LocalBus$ReventlessLocal from "../../../src/adapter/LocalBus.res.mjs";
|
|
12
|
+
import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
|
|
13
|
+
import * as BackendState$ReventlessLocal from "../../../src/adapter/BackendState.res.mjs";
|
|
14
|
+
import * as SqliteDriver$ReventlessLocal from "../../../src/adapter/SqliteDriver.res.mjs";
|
|
15
|
+
import * as ExtensionMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionMapping.res.mjs";
|
|
16
|
+
import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
17
|
+
import * as DcbEventLog_Operations$ReventlessCore from "@reventlessdev/reventless-core/src/components/DcbEventLog/DcbEventLog_Operations.res.mjs";
|
|
18
|
+
import * as LocalDcbEventLogStorage$ReventlessLocal from "../../../src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs";
|
|
19
|
+
import * as StateChangeSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs";
|
|
20
|
+
|
|
21
|
+
TestRunner$ReventlessLocal.setup();
|
|
22
|
+
|
|
23
|
+
let opts = {};
|
|
24
|
+
|
|
25
|
+
let name = "Ordering.Orders";
|
|
26
|
+
|
|
27
|
+
let commandSchema = Sury.literal("Noop");
|
|
28
|
+
|
|
29
|
+
let eventSchema = Sury.$schema(s => ({
|
|
30
|
+
TAG: "ItemOrdered",
|
|
31
|
+
productId: s.m(Sury.string),
|
|
32
|
+
orderId: s.m(Sury.string)
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
let directiveSchema = Sury.literal("NoDirective");
|
|
36
|
+
|
|
37
|
+
let moduleUrl = import.meta.url;
|
|
38
|
+
|
|
39
|
+
function authorizationOf(param) {
|
|
40
|
+
return "AllowAuthenticated";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function commandTransition(param) {
|
|
44
|
+
return "Undeclared";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let traits = [];
|
|
48
|
+
|
|
49
|
+
let Port = {
|
|
50
|
+
name: name,
|
|
51
|
+
commandSchema: commandSchema,
|
|
52
|
+
eventSchema: eventSchema,
|
|
53
|
+
directiveSchema: directiveSchema,
|
|
54
|
+
moduleUrl: moduleUrl,
|
|
55
|
+
authorizationOf: authorizationOf,
|
|
56
|
+
commandTransition: commandTransition,
|
|
57
|
+
traits: traits
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let name$1 = "RecordDemandNoCheck";
|
|
61
|
+
|
|
62
|
+
let moduleUrl$1 = import.meta.url;
|
|
63
|
+
|
|
64
|
+
let eventSchema$1 = Sury.$schema(s => ({
|
|
65
|
+
TAG: "DemandRecorded",
|
|
66
|
+
productId: s.m(DcbTag$Reventless.partition),
|
|
67
|
+
orderId: s.m(Sury.string)
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
let consumedEventSchema = Sury.$schema(s => ({
|
|
71
|
+
TAG: "DemandRecorded",
|
|
72
|
+
productId: s.m(DcbTag$Reventless.string),
|
|
73
|
+
orderId: s.m(Sury.string)
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
let commandSchema$1 = Sury.$schema(s => ({
|
|
77
|
+
TAG: "RecordDemand",
|
|
78
|
+
productId: s.m(DcbTag$Reventless.string),
|
|
79
|
+
orderId: s.m(Sury.string)
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
let errorSchema = Sury.literal("Never");
|
|
83
|
+
|
|
84
|
+
function authorizationOf$1(param) {
|
|
85
|
+
return "AllowAuthenticated";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function commandTransition$1(param) {
|
|
89
|
+
return "Unrestricted";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let traits$1 = [];
|
|
93
|
+
|
|
94
|
+
let DemandSlice = {
|
|
95
|
+
name: name$1,
|
|
96
|
+
Id: undefined,
|
|
97
|
+
moduleUrl: moduleUrl$1,
|
|
98
|
+
eventSchema: eventSchema$1,
|
|
99
|
+
consumedEventSchema: consumedEventSchema,
|
|
100
|
+
commandSchema: commandSchema$1,
|
|
101
|
+
errorSchema: errorSchema,
|
|
102
|
+
authorizationOf: authorizationOf$1,
|
|
103
|
+
commandTransition: commandTransition$1,
|
|
104
|
+
traits: traits$1,
|
|
105
|
+
readConsistency: "EscalateOnRetry"
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let initialState = {
|
|
109
|
+
orders: []
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function evolve(state, event) {
|
|
113
|
+
return {
|
|
114
|
+
orders: state.orders.concat([event.orderId])
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function decide(_state, command) {
|
|
119
|
+
return {
|
|
120
|
+
TAG: "Ok",
|
|
121
|
+
_0: [{
|
|
122
|
+
TAG: "DemandRecorded",
|
|
123
|
+
productId: command.productId,
|
|
124
|
+
orderId: command.orderId
|
|
125
|
+
}]
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let DemandDecision = {
|
|
130
|
+
initialState: initialState,
|
|
131
|
+
moduleUrl: moduleUrl$1,
|
|
132
|
+
evolve: evolve,
|
|
133
|
+
decide: decide
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
let DemandHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
137
|
+
name: name$1,
|
|
138
|
+
moduleUrl: moduleUrl$1,
|
|
139
|
+
consumedEventSchema: consumedEventSchema,
|
|
140
|
+
errorSchema: errorSchema,
|
|
141
|
+
eventSchema: eventSchema$1,
|
|
142
|
+
commandSchema: commandSchema$1,
|
|
143
|
+
authorizationOf: authorizationOf$1,
|
|
144
|
+
commandTransition: commandTransition$1,
|
|
145
|
+
traits: traits$1,
|
|
146
|
+
readConsistency: "EscalateOnRetry"
|
|
147
|
+
})({
|
|
148
|
+
initialState: initialState,
|
|
149
|
+
evolve: evolve,
|
|
150
|
+
decide: decide,
|
|
151
|
+
moduleUrl: moduleUrl$1
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
let partitionTag = {
|
|
155
|
+
TAG: "Simple",
|
|
156
|
+
_0: {
|
|
157
|
+
key: "productId"
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
let moduleUrl$2 = import.meta.url;
|
|
162
|
+
|
|
163
|
+
function mapIncomingEvent(_id, event, _meta, _pluginDef, _queryEngine) {
|
|
164
|
+
return [{
|
|
165
|
+
TAG: "PublishStateChangeSliceCommand",
|
|
166
|
+
_0: {
|
|
167
|
+
TAG: "RecordDemand",
|
|
168
|
+
productId: event.productId,
|
|
169
|
+
orderId: event.orderId
|
|
170
|
+
}
|
|
171
|
+
}];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let handledEvents = [];
|
|
175
|
+
|
|
176
|
+
let issuedCommands = [];
|
|
177
|
+
|
|
178
|
+
let DemandExtension = ExtensionMapping$ReventlessInfra.Make({
|
|
179
|
+
ExtensionPoint: {
|
|
180
|
+
name: name,
|
|
181
|
+
moduleUrl: moduleUrl,
|
|
182
|
+
commandSchema: commandSchema,
|
|
183
|
+
eventSchema: eventSchema,
|
|
184
|
+
directiveSchema: directiveSchema
|
|
185
|
+
},
|
|
186
|
+
Delegate: {
|
|
187
|
+
Id: Id$Reventless.$$String,
|
|
188
|
+
name: name$1,
|
|
189
|
+
eventSchema: eventSchema$1,
|
|
190
|
+
errorSchema: errorSchema,
|
|
191
|
+
commandSchema: commandSchema$1,
|
|
192
|
+
moduleUrl: moduleUrl$1,
|
|
193
|
+
authorizationOf: authorizationOf$1,
|
|
194
|
+
commandTransition: commandTransition$1,
|
|
195
|
+
traits: traits$1
|
|
196
|
+
},
|
|
197
|
+
moduleUrl: moduleUrl$2,
|
|
198
|
+
delegateModuleUrl: moduleUrl$1,
|
|
199
|
+
mapIncomingEvent: mapIncomingEvent,
|
|
200
|
+
mapOutgoingEvent: undefined,
|
|
201
|
+
handledEvents: handledEvents,
|
|
202
|
+
issuedCommands: issuedCommands
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
async function queryEngine_scan(param, param$1, param$2) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function queryEngine_query(param, param$1, param$2, param$3, param$4, param$5, param$6) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let queryEngine = {
|
|
214
|
+
scan: queryEngine_scan,
|
|
215
|
+
query: queryEngine_query
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
let pluginDefinition = {
|
|
219
|
+
id: "Catalog@1",
|
|
220
|
+
name: "Catalog",
|
|
221
|
+
version: "1",
|
|
222
|
+
extensionPoints: [],
|
|
223
|
+
extensions: [{
|
|
224
|
+
name: "Catalog",
|
|
225
|
+
extensionPointName: name
|
|
226
|
+
}],
|
|
227
|
+
eventCollector: "eventCollector",
|
|
228
|
+
extensionProtocols: [],
|
|
229
|
+
apiSchemaFragment: undefined,
|
|
230
|
+
structure: undefined,
|
|
231
|
+
kind: "Domain"
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
let published_meta = {
|
|
235
|
+
service: name,
|
|
236
|
+
time: "2024-01-01T00:00:00Z",
|
|
237
|
+
msgId: "item-ordered-msg",
|
|
238
|
+
correlationId: "place-order-cmd",
|
|
239
|
+
causationId: "order-placed-event"
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
let published_event = {
|
|
243
|
+
TAG: "ItemOrdered",
|
|
244
|
+
productId: "product-1",
|
|
245
|
+
orderId: "order-1"
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
let published = {
|
|
249
|
+
id: "order-1",
|
|
250
|
+
meta: published_meta,
|
|
251
|
+
event: published_event
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
function commandOf(event$p) {
|
|
255
|
+
let match = DemandExtension.mapIncomingEvent(event$p, pluginDefinition, queryEngine);
|
|
256
|
+
if (match.length !== 1) {
|
|
257
|
+
return Stdlib_JsError.throwWithMessage("expected one slice command");
|
|
258
|
+
}
|
|
259
|
+
let match$1 = match[0];
|
|
260
|
+
if (match$1.TAG === "AbstractPublishAggregateCommand") {
|
|
261
|
+
return match$1._1;
|
|
262
|
+
} else {
|
|
263
|
+
return Stdlib_JsError.throwWithMessage("expected one slice command");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function eventLogOn(name) {
|
|
268
|
+
let Bus = LocalBus$ReventlessLocal.Make({});
|
|
269
|
+
let Storage = LocalDcbEventLogStorage$ReventlessLocal.Make(Bus);
|
|
270
|
+
let s = Storage.make(name, [], partitionTag, undefined, opts);
|
|
271
|
+
let storage = await TestRunner$ReventlessLocal.resolve(s.operations);
|
|
272
|
+
let publishJson = async (param, param$1, param$2) => {};
|
|
273
|
+
let Log = DcbEventLog_Operations$ReventlessCore.Make({
|
|
274
|
+
name: name,
|
|
275
|
+
serviceName: name,
|
|
276
|
+
storage: storage,
|
|
277
|
+
publishJson: publishJson
|
|
278
|
+
});
|
|
279
|
+
return {
|
|
280
|
+
read: Log.read,
|
|
281
|
+
append: Log.append,
|
|
282
|
+
readStream: Log.readStream,
|
|
283
|
+
appendStream: Log.appendStream
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function runUnderMemory(fn) {
|
|
288
|
+
BackendState$ReventlessLocal.setMemory();
|
|
289
|
+
return await fn();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function runUnderSqlite(fn) {
|
|
293
|
+
let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
|
|
294
|
+
BackendState$ReventlessLocal.setSqlite(db, ":memory:");
|
|
295
|
+
await fn();
|
|
296
|
+
BackendState$ReventlessLocal.setMemory();
|
|
297
|
+
return SqliteDriver$ReventlessLocal.close(db);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function deliver(eventLog, cmd) {
|
|
301
|
+
return Effect.runPromise(DemandHandler.handleCommands(undefined, undefined, partitionTag, eventLog, Stream.fromIterable([{
|
|
302
|
+
command: Message$ReventlessCore.decodeCommand$p(CommandTopic_Helpers$ReventlessCore.encodeCommandJson(cmd), Id$Reventless.$$String.schema, commandSchema$1),
|
|
303
|
+
reference: cmd.meta.msgId
|
|
304
|
+
}])));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function recordedDemands(eventLog) {
|
|
308
|
+
return (await eventLog.read([{
|
|
309
|
+
eventTypes: ["DemandRecorded"]
|
|
310
|
+
}], undefined)).events;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let counter = {
|
|
314
|
+
contents: 0
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
function fresh(prefix) {
|
|
318
|
+
counter.contents = counter.contents + 1 | 0;
|
|
319
|
+
return prefix + `-` + counter.contents.toString();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
globalThis.beforeEach(() => DemandHandler.resetCache());
|
|
323
|
+
|
|
324
|
+
let backends = [
|
|
325
|
+
[
|
|
326
|
+
"in-memory",
|
|
327
|
+
runUnderMemory
|
|
328
|
+
],
|
|
329
|
+
[
|
|
330
|
+
"SQLite",
|
|
331
|
+
runUnderSqlite
|
|
332
|
+
]
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
backends.forEach(param => {
|
|
336
|
+
let runUnder = param[1];
|
|
337
|
+
globalThis.describe(`a redelivered fact is recorded once (` + param[0] + ` backend)`, () => {
|
|
338
|
+
globalThis.test("the same published event delivered twice appends one event", async () => await runUnder(async () => {
|
|
339
|
+
let log = await eventLogOn(fresh("RepeatOnce"));
|
|
340
|
+
let first = await deliver(log, commandOf(published));
|
|
341
|
+
let again = await deliver(log, commandOf(published));
|
|
342
|
+
globalThis.expect(first.length).toBe(1);
|
|
343
|
+
globalThis.expect(again).toEqual(first);
|
|
344
|
+
let events = await recordedDemands(log);
|
|
345
|
+
globalThis.expect(events.length).toBe(1);
|
|
346
|
+
let cmd = commandOf(published);
|
|
347
|
+
globalThis.expect(events[0].meta.causationId).toEqual(cmd.meta.msgId);
|
|
348
|
+
globalThis.expect(cmd.meta.causationId).toEqual(published_meta.msgId);
|
|
349
|
+
}));
|
|
350
|
+
globalThis.test("a different order for the same product still counts", async () => await runUnder(async () => {
|
|
351
|
+
let log = await eventLogOn(fresh("RepeatOther"));
|
|
352
|
+
await deliver(log, commandOf(published));
|
|
353
|
+
let newrecord = {...published_meta};
|
|
354
|
+
let second_meta = (newrecord.msgId = "item-ordered-msg-2", newrecord);
|
|
355
|
+
let second_event = {
|
|
356
|
+
TAG: "ItemOrdered",
|
|
357
|
+
productId: "product-1",
|
|
358
|
+
orderId: "order-2"
|
|
359
|
+
};
|
|
360
|
+
let second = {
|
|
361
|
+
id: "order-2",
|
|
362
|
+
meta: second_meta,
|
|
363
|
+
event: second_event
|
|
364
|
+
};
|
|
365
|
+
await deliver(log, commandOf(second));
|
|
366
|
+
await deliver(log, commandOf(published));
|
|
367
|
+
await deliver(log, commandOf(second));
|
|
368
|
+
globalThis.expect((await recordedDemands(log)).length).toBe(2);
|
|
369
|
+
}));
|
|
370
|
+
globalThis.test("a command with a random id is decided every time", async () => await runUnder(async () => {
|
|
371
|
+
let log = await eventLogOn(fresh("RepeatRandom"));
|
|
372
|
+
let cmd = commandOf(published);
|
|
373
|
+
let newrecord = {...cmd};
|
|
374
|
+
newrecord.meta = Message$ReventlessCore.deriveMeta(cmd.meta, undefined);
|
|
375
|
+
await deliver(log, newrecord);
|
|
376
|
+
await deliver(log, newrecord);
|
|
377
|
+
globalThis.expect((await recordedDemands(log)).length).toBe(2);
|
|
378
|
+
}));
|
|
379
|
+
globalThis.test("two concurrent copies append one event", async () => await runUnder(async () => {
|
|
380
|
+
let log = await eventLogOn(fresh("RepeatConcurrent"));
|
|
381
|
+
let cmd = commandOf(published);
|
|
382
|
+
let match = await Promise.all([
|
|
383
|
+
deliver(log, cmd),
|
|
384
|
+
deliver(log, cmd)
|
|
385
|
+
]);
|
|
386
|
+
let b = match[1];
|
|
387
|
+
let a = match[0];
|
|
388
|
+
globalThis.expect(a.length).toBe(1);
|
|
389
|
+
globalThis.expect(b.length).toBe(1);
|
|
390
|
+
globalThis.expect(Stdlib_Result.isOk(a[0]) && Stdlib_Result.isOk(b[0])).toBe(true);
|
|
391
|
+
globalThis.expect((await recordedDemands(log)).length).toBe(1);
|
|
392
|
+
}));
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
let Message;
|
|
397
|
+
|
|
398
|
+
export {
|
|
399
|
+
opts,
|
|
400
|
+
Message,
|
|
401
|
+
Port,
|
|
402
|
+
DemandSlice,
|
|
403
|
+
DemandDecision,
|
|
404
|
+
DemandHandler,
|
|
405
|
+
partitionTag,
|
|
406
|
+
DemandExtension,
|
|
407
|
+
queryEngine,
|
|
408
|
+
pluginDefinition,
|
|
409
|
+
published,
|
|
410
|
+
commandOf,
|
|
411
|
+
eventLogOn,
|
|
412
|
+
runUnderMemory,
|
|
413
|
+
runUnderSqlite,
|
|
414
|
+
deliver,
|
|
415
|
+
recordedDemands,
|
|
416
|
+
counter,
|
|
417
|
+
fresh,
|
|
418
|
+
backends,
|
|
419
|
+
}
|
|
420
|
+
/* Not a pure module */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// A ReadModel whose `Mapping.sourceName` doesn't match any key in `allEventTopics`
|
|
2
|
+
// fails plugin assembly (here: ReadModel construction) with a clear error, instead
|
|
3
|
+
// of silently producing zero events. A dotted name is an extension point's, and
|
|
4
|
+
// the error then also says how another plugin's facts do reach a read model.
|
|
4
5
|
|
|
5
6
|
open JestGlobals
|
|
6
7
|
open Reventless
|
|
@@ -68,22 +69,77 @@ let allEventTopics: ReventlessInfra.EventTopic.allOutputs = Dict.fromArray([
|
|
|
68
69
|
("ProductCatalogDcbEventLog", {ReventlessInfra.EventTopic.resources: [topicResource]}),
|
|
69
70
|
])
|
|
70
71
|
|
|
72
|
+
// A Source named like an extension point ("Ordering.Orders"): a read model
|
|
73
|
+
// cannot subscribe to one, so the message must say where to go instead.
|
|
74
|
+
module ExtensionPointSource = {
|
|
75
|
+
module Id = Reventless.Id.String
|
|
76
|
+
let name = "Ordering.Orders"
|
|
77
|
+
|
|
78
|
+
@schema
|
|
79
|
+
type event = ItemOrdered({productId: string, orderId: string})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module EpRmSpec = {
|
|
83
|
+
module Id = Reventless.Id.String
|
|
84
|
+
let name = "EpCheckReadModel"
|
|
85
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
86
|
+
|
|
87
|
+
@schema
|
|
88
|
+
type state = {productId: string, orderCount: int}
|
|
89
|
+
|
|
90
|
+
let config = ReadModel.config()
|
|
91
|
+
let subIdConfig = None
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module EpMapping = Mapping.Make(
|
|
95
|
+
ExtensionPointSource,
|
|
96
|
+
EpRmSpec,
|
|
97
|
+
{
|
|
98
|
+
let project = (msg: Message.event'<ExtensionPointSource.Id.t, ExtensionPointSource.event>) =>
|
|
99
|
+
switch msg.event {
|
|
100
|
+
| ItemOrdered({productId}) =>
|
|
101
|
+
Set(EpRmSpec.Id.makeFromString(productId), ({productId, orderCount: 1}: EpRmSpec.state))
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
module EpMappings: Mappings with module Target := EpRmSpec = {
|
|
107
|
+
module Mappings = Mappings.Make(EpRmSpec)
|
|
108
|
+
module type Mapping = Mappings.Mapping
|
|
109
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
110
|
+
let mappings: array<module(Mapping)> = [module(EpMapping)]
|
|
111
|
+
}
|
|
112
|
+
|
|
71
113
|
module ReadModelMaker = ReadModel_Builder.Make(Bus)
|
|
72
114
|
module RmMaker = ReadModelMaker.Make(RmSpec, TypoMappings)
|
|
115
|
+
module EpRmMaker = ReadModelMaker.Make(EpRmSpec, EpMappings)
|
|
116
|
+
|
|
117
|
+
let messageOf = make => {
|
|
118
|
+
let msg = ref(None)
|
|
119
|
+
try {
|
|
120
|
+
let _ = make()
|
|
121
|
+
} catch {
|
|
122
|
+
| JsExn(err) => msg := Some(err->JsExn.message->Option.getOr(""))
|
|
123
|
+
}
|
|
124
|
+
msg.contents
|
|
125
|
+
}
|
|
73
126
|
|
|
74
127
|
describe("ReadModel source-name fail-fast:", () => {
|
|
75
128
|
testSync("constructing a ReadModel whose Mapping.sourceName is missing throws", () => {
|
|
76
|
-
let
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
expect(msg
|
|
129
|
+
let msg =
|
|
130
|
+
messageOf(() => RmMaker.make(~api=(), ~apiRole=(), ~allEventTopics))->Option.getOrThrow
|
|
131
|
+
expect(msg->String.includes("MissingDcb"))->toBe(true)
|
|
132
|
+
expect(msg->String.includes("ProductCatalogDcbEventLog"))->toBe(true)
|
|
133
|
+
// A typo keeps the plain message: no extension point is being named.
|
|
134
|
+
expect(msg->String.includes("extension point"))->toBe(false)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
testSync("a dotted sourceName names the Extension → slice route instead", () => {
|
|
138
|
+
let msg =
|
|
139
|
+
messageOf(() => EpRmMaker.make(~api=(), ~apiRole=(), ~allEventTopics))->Option.getOrThrow
|
|
140
|
+
expect(msg->String.includes("Ordering.Orders"))->toBe(true)
|
|
141
|
+
expect(msg->String.includes("looks like an extension point name"))->toBe(true)
|
|
142
|
+
expect(msg->String.includes("Declare an Extension"))->toBe(true)
|
|
143
|
+
expect(msg->String.includes("Targeting a StateChangeSlice instead of an Aggregate"))->toBe(true)
|
|
88
144
|
})
|
|
89
145
|
})
|
|
@@ -104,6 +104,84 @@ let allEventTopics = Object.fromEntries([[
|
|
|
104
104
|
}
|
|
105
105
|
]]);
|
|
106
106
|
|
|
107
|
+
let name$2 = "Ordering.Orders";
|
|
108
|
+
|
|
109
|
+
let eventSchema$1 = Sury.$schema(s => ({
|
|
110
|
+
TAG: "ItemOrdered",
|
|
111
|
+
productId: s.m(Sury.string),
|
|
112
|
+
orderId: s.m(Sury.string)
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
let ExtensionPointSource = {
|
|
116
|
+
Id: undefined,
|
|
117
|
+
name: name$2,
|
|
118
|
+
eventSchema: eventSchema$1
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
let name$3 = "EpCheckReadModel";
|
|
122
|
+
|
|
123
|
+
let moduleUrl$2 = import.meta.url;
|
|
124
|
+
|
|
125
|
+
let stateSchema$1 = Sury.$schema(s => ({
|
|
126
|
+
productId: s.m(Sury.string),
|
|
127
|
+
orderCount: s.m(Sury.int32)
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
let config$1 = ReadModel$Reventless.config(undefined, undefined, undefined);
|
|
131
|
+
|
|
132
|
+
let EpRmSpec = {
|
|
133
|
+
Id: undefined,
|
|
134
|
+
name: name$3,
|
|
135
|
+
moduleUrl: moduleUrl$2,
|
|
136
|
+
stateSchema: stateSchema$1,
|
|
137
|
+
config: config$1,
|
|
138
|
+
subIdConfig: undefined,
|
|
139
|
+
authorization: "AllowAuthenticated",
|
|
140
|
+
visibility: "Public"
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
function project$1(msg) {
|
|
144
|
+
let match = msg.event;
|
|
145
|
+
let productId = match.productId;
|
|
146
|
+
return {
|
|
147
|
+
TAG: "Set",
|
|
148
|
+
_0: Id$Reventless.$$String.makeFromString(productId),
|
|
149
|
+
_1: {
|
|
150
|
+
productId: productId,
|
|
151
|
+
orderCount: 1
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let EpMapping = Projection$Reventless.Mapping.Make({
|
|
157
|
+
Id: Id$Reventless.$$String,
|
|
158
|
+
name: name$2,
|
|
159
|
+
eventSchema: eventSchema$1
|
|
160
|
+
})({
|
|
161
|
+
Id: Id$Reventless.$$String,
|
|
162
|
+
name: name$3,
|
|
163
|
+
stateSchema: stateSchema$1,
|
|
164
|
+
subIdConfig: undefined
|
|
165
|
+
})({
|
|
166
|
+
project: project$1
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
Projection$Reventless.Mappings.Make({
|
|
170
|
+
Id: Id$Reventless.$$String,
|
|
171
|
+
name: name$3,
|
|
172
|
+
stateSchema: stateSchema$1,
|
|
173
|
+
subIdConfig: undefined
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
let moduleUrl$3 = import.meta.url;
|
|
177
|
+
|
|
178
|
+
let mappings$1 = [EpMapping];
|
|
179
|
+
|
|
180
|
+
let EpMappings = {
|
|
181
|
+
moduleUrl: moduleUrl$3,
|
|
182
|
+
mappings: mappings$1
|
|
183
|
+
};
|
|
184
|
+
|
|
107
185
|
let ReadModelMaker = ReadModel_Builder$ReventlessLocal.Make(Bus);
|
|
108
186
|
|
|
109
187
|
let RmMaker = ReadModelMaker.Make({
|
|
@@ -117,24 +195,45 @@ let RmMaker = ReadModelMaker.Make({
|
|
|
117
195
|
visibility: "Public"
|
|
118
196
|
})(TypoMappings);
|
|
119
197
|
|
|
198
|
+
let EpRmMaker = ReadModelMaker.Make({
|
|
199
|
+
Id: Id$Reventless.$$String,
|
|
200
|
+
name: name$3,
|
|
201
|
+
moduleUrl: moduleUrl$2,
|
|
202
|
+
stateSchema: stateSchema$1,
|
|
203
|
+
config: config$1,
|
|
204
|
+
subIdConfig: undefined,
|
|
205
|
+
authorization: "AllowAuthenticated",
|
|
206
|
+
visibility: "Public"
|
|
207
|
+
})(EpMappings);
|
|
208
|
+
|
|
209
|
+
function messageOf(make) {
|
|
210
|
+
let msg;
|
|
211
|
+
try {
|
|
212
|
+
make();
|
|
213
|
+
} catch (raw_err) {
|
|
214
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
215
|
+
if (err.RE_EXN_ID === "JsExn") {
|
|
216
|
+
msg = Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "");
|
|
217
|
+
} else {
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return msg;
|
|
222
|
+
}
|
|
223
|
+
|
|
120
224
|
globalThis.describe("ReadModel source-name fail-fast:", () => {
|
|
121
225
|
globalThis.test("constructing a ReadModel whose Mapping.sourceName is missing throws", () => {
|
|
122
|
-
let
|
|
123
|
-
let msg = "";
|
|
124
|
-
try {
|
|
125
|
-
RmMaker.make(undefined, undefined, allEventTopics, undefined, undefined);
|
|
126
|
-
} catch (raw_err) {
|
|
127
|
-
let err = Primitive_exceptions.internalToException(raw_err);
|
|
128
|
-
if (err.RE_EXN_ID === "JsExn") {
|
|
129
|
-
threw = true;
|
|
130
|
-
msg = Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "");
|
|
131
|
-
} else {
|
|
132
|
-
throw err;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
globalThis.expect(threw).toBe(true);
|
|
226
|
+
let msg = Stdlib_Option.getOrThrow(messageOf(() => RmMaker.make(undefined, undefined, allEventTopics, undefined, undefined)), undefined);
|
|
136
227
|
globalThis.expect(msg.includes("MissingDcb")).toBe(true);
|
|
137
228
|
globalThis.expect(msg.includes("ProductCatalogDcbEventLog")).toBe(true);
|
|
229
|
+
globalThis.expect(msg.includes("extension point")).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
globalThis.test("a dotted sourceName names the Extension → slice route instead", () => {
|
|
232
|
+
let msg = Stdlib_Option.getOrThrow(messageOf(() => EpRmMaker.make(undefined, undefined, allEventTopics, undefined, undefined)), undefined);
|
|
233
|
+
globalThis.expect(msg.includes("Ordering.Orders")).toBe(true);
|
|
234
|
+
globalThis.expect(msg.includes("looks like an extension point name")).toBe(true);
|
|
235
|
+
globalThis.expect(msg.includes("Declare an Extension")).toBe(true);
|
|
236
|
+
globalThis.expect(msg.includes("Targeting a StateChangeSlice instead of an Aggregate")).toBe(true);
|
|
138
237
|
});
|
|
139
238
|
});
|
|
140
239
|
|
|
@@ -146,7 +245,13 @@ export {
|
|
|
146
245
|
TypoMappings,
|
|
147
246
|
topicResource,
|
|
148
247
|
allEventTopics,
|
|
248
|
+
ExtensionPointSource,
|
|
249
|
+
EpRmSpec,
|
|
250
|
+
EpMapping,
|
|
251
|
+
EpMappings,
|
|
149
252
|
ReadModelMaker,
|
|
150
253
|
RmMaker,
|
|
254
|
+
EpRmMaker,
|
|
255
|
+
messageOf,
|
|
151
256
|
}
|
|
152
257
|
/* Bus Not a pure module */
|
|
@@ -155,7 +155,7 @@ function encodeEvent(event) {
|
|
|
155
155
|
let json = Util_Sury$Reventless.toJson(event, eventSchema);
|
|
156
156
|
let match = Message$ReventlessCore.splitMessage(json);
|
|
157
157
|
let tags = DcbTag$Reventless.extractTags(eventSchema, event);
|
|
158
|
-
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined
|
|
158
|
+
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined);
|
|
159
159
|
return {
|
|
160
160
|
eventType: match[0],
|
|
161
161
|
data: match[1],
|
|
@@ -158,7 +158,7 @@ function encodeEvent(event) {
|
|
|
158
158
|
let json = Util_Sury$Reventless.toJson(event, eventSchema);
|
|
159
159
|
let match = Message$ReventlessCore.splitMessage(json);
|
|
160
160
|
let tags = DcbTag$Reventless.extractTags(eventSchema, event);
|
|
161
|
-
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined
|
|
161
|
+
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined);
|
|
162
162
|
return {
|
|
163
163
|
eventType: match[0],
|
|
164
164
|
data: match[1],
|
|
@@ -153,7 +153,7 @@ function encodeEvent(event) {
|
|
|
153
153
|
let json = Util_Sury$Reventless.toJson(event, eventSchema);
|
|
154
154
|
let match = Message$ReventlessCore.splitMessage(json);
|
|
155
155
|
let tags = DcbTag$Reventless.extractTags(eventSchema, event);
|
|
156
|
-
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined
|
|
156
|
+
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined);
|
|
157
157
|
return {
|
|
158
158
|
eventType: match[0],
|
|
159
159
|
data: match[1],
|