@reventlessdev/reventless-core 3.0.0-alpha.215 → 3.0.0-alpha.216
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 +8 -0
- package/package.json +6 -6
- package/src/admin/Platform_Admin_Structure.res +4 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +5 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +16 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +24 -1
- package/src/components/Aggregate/Aggregate_Callback.res +14 -2
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +3 -3
- package/src/components/CommandTopic/CommandTopic.res.mjs +12 -0
- package/src/components/CommandTopic/CommandTopic_Helpers.res +88 -2
- package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +48 -2
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +13 -1
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
- package/src/plugin/component/Plugin_Builder.res +1 -0
- package/src/plugin/component/Plugin_Builder.res.mjs +1 -0
- package/src/plugin/component/Plugin_Structure.res +66 -51
- package/src/plugin/component/Plugin_Structure.res.mjs +61 -46
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +21 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +21 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res +258 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +372 -0
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +9 -1
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res.mjs +3 -3
- package/tests/message/MessageTest.res +1 -0
- package/tests/message/MessageTest.res.mjs +1 -0
- package/tests/plugin/PluginStructureTest.res +27 -0
- package/tests/plugin/PluginStructureTest.res.mjs +27 -0
- package/tests/plugin/StateChangeSlice/PsShipOrder.res +3 -1
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -1
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +8 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Stream from "effect/Stream";
|
|
6
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
7
|
+
import * as DcbFixtures$ReventlessCore from "../dcb/DcbFixtures.res.mjs";
|
|
8
|
+
import * as RuntimeExtension$ReventlessCore from "../../src/adapter/RuntimeExtension/RuntimeExtension.res.mjs";
|
|
9
|
+
import * as AggregateFixtures$ReventlessCore from "../aggregate/AggregateFixtures.res.mjs";
|
|
10
|
+
import * as CommandTopic_Helpers$ReventlessCore from "../../src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
11
|
+
import * as DcbEventLog_Operations$ReventlessCore from "../../src/components/DcbEventLog/DcbEventLog_Operations.res.mjs";
|
|
12
|
+
import * as StateChangeSlice_Callback$ReventlessCore from "../../src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs";
|
|
13
|
+
|
|
14
|
+
let dcbMock = DcbFixtures$ReventlessCore.makeMockStorage();
|
|
15
|
+
|
|
16
|
+
let storage = dcbMock.operations;
|
|
17
|
+
|
|
18
|
+
let publishJson = dcbMock.mockPublishJson;
|
|
19
|
+
|
|
20
|
+
let TestDcbOps = {
|
|
21
|
+
name: "TestDcbEventLog",
|
|
22
|
+
serviceName: "TestDcbEventLog",
|
|
23
|
+
storage: storage,
|
|
24
|
+
publishJson: publishJson
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let EventLogOps = DcbEventLog_Operations$ReventlessCore.Make(TestDcbOps);
|
|
28
|
+
|
|
29
|
+
let dcbEventLog_read = EventLogOps.read;
|
|
30
|
+
|
|
31
|
+
let dcbEventLog_append = EventLogOps.append;
|
|
32
|
+
|
|
33
|
+
let dcbEventLog_readStream = EventLogOps.readStream;
|
|
34
|
+
|
|
35
|
+
let dcbEventLog_appendStream = EventLogOps.appendStream;
|
|
36
|
+
|
|
37
|
+
let dcbEventLog = {
|
|
38
|
+
read: dcbEventLog_read,
|
|
39
|
+
append: dcbEventLog_append,
|
|
40
|
+
readStream: dcbEventLog_readStream,
|
|
41
|
+
appendStream: dcbEventLog_appendStream
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let initialState = DcbFixtures$ReventlessCore.TestCommandSpec.initialState;
|
|
45
|
+
|
|
46
|
+
let evolve = DcbFixtures$ReventlessCore.TestCommandSpec.evolve;
|
|
47
|
+
|
|
48
|
+
let decide = DcbFixtures$ReventlessCore.TestCommandSpec.decide;
|
|
49
|
+
|
|
50
|
+
let moduleUrl = DcbFixtures$ReventlessCore.TestCommandSpec.moduleUrl;
|
|
51
|
+
|
|
52
|
+
let SliceBehavior = {
|
|
53
|
+
initialState: initialState,
|
|
54
|
+
evolve: evolve,
|
|
55
|
+
decide: decide,
|
|
56
|
+
moduleUrl: moduleUrl
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
let SliceHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
60
|
+
name: DcbFixtures$ReventlessCore.TestCommandSpec.name,
|
|
61
|
+
moduleUrl: DcbFixtures$ReventlessCore.TestCommandSpec.moduleUrl,
|
|
62
|
+
Id: Id$Reventless.$$String,
|
|
63
|
+
consumedEventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.consumedEventSchema,
|
|
64
|
+
errorSchema: DcbFixtures$ReventlessCore.TestCommandSpec.errorSchema,
|
|
65
|
+
eventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.eventSchema,
|
|
66
|
+
commandSchema: DcbFixtures$ReventlessCore.TestCommandSpec.commandSchema,
|
|
67
|
+
commandAuthorization: DcbFixtures$ReventlessCore.TestCommandSpec.commandAuthorization,
|
|
68
|
+
readConsistency: DcbFixtures$ReventlessCore.TestCommandSpec.readConsistency
|
|
69
|
+
})(SliceBehavior);
|
|
70
|
+
|
|
71
|
+
function sliceItem(reference, command) {
|
|
72
|
+
return {
|
|
73
|
+
command: {
|
|
74
|
+
id: Id$Reventless.$$String.makeFromString("cmd-" + reference),
|
|
75
|
+
meta: DcbFixtures$ReventlessCore.testMeta,
|
|
76
|
+
command: command
|
|
77
|
+
},
|
|
78
|
+
reference: reference
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function runSlice(reference, command) {
|
|
83
|
+
return Effect.runPromise(SliceHandler.handleCommands(undefined, undefined, dcbEventLog, Stream.fromIterable([sliceItem(reference, command)])));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function runAgg(aggIdOpt, reference, command) {
|
|
87
|
+
let aggId = aggIdOpt !== undefined ? aggIdOpt : "agg-1";
|
|
88
|
+
return Effect.runPromise(AggregateFixtures$ReventlessCore.TestHandler.handleCommands(Stream.fromIterable([AggregateFixtures$ReventlessCore.makeTopicItem(aggId, reference, command)])));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let observed = {
|
|
92
|
+
contents: []
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function seen() {
|
|
96
|
+
return observed.contents.map(r => {
|
|
97
|
+
let match = r.outcome;
|
|
98
|
+
if (match.TAG === "OutcomeAccepted") {
|
|
99
|
+
return [
|
|
100
|
+
r.component,
|
|
101
|
+
r.reference,
|
|
102
|
+
"accepted",
|
|
103
|
+
"",
|
|
104
|
+
match.eventCount
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
let tmp;
|
|
108
|
+
tmp = match.cause === "DomainRejection" ? "rejected/domain" : "rejected/infrastructure";
|
|
109
|
+
return [
|
|
110
|
+
r.component,
|
|
111
|
+
r.reference,
|
|
112
|
+
tmp,
|
|
113
|
+
match.errorCode,
|
|
114
|
+
0
|
|
115
|
+
];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function record() {
|
|
120
|
+
CommandTopic_Helpers$ReventlessCore.registerCommandOutcome(r => {
|
|
121
|
+
observed.contents = observed.contents.concat([r]);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
globalThis.beforeEach(() => {
|
|
126
|
+
AggregateFixtures$ReventlessCore.mock.reset();
|
|
127
|
+
AggregateFixtures$ReventlessCore.TestHandler.resetCache();
|
|
128
|
+
dcbMock.reset();
|
|
129
|
+
SliceHandler.resetCache();
|
|
130
|
+
observed.contents = [];
|
|
131
|
+
CommandTopic_Helpers$ReventlessCore.clearCommandOutcome();
|
|
132
|
+
RuntimeExtension$ReventlessCore.reset();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
globalThis.describe("CommandTopic command-outcome hook:", () => {
|
|
136
|
+
globalThis.describe("aggregate", () => {
|
|
137
|
+
globalThis.test("observes an accepted command with no side-channel installed", async () => {
|
|
138
|
+
record();
|
|
139
|
+
let results = await runAgg(undefined, "ref-1", {
|
|
140
|
+
TAG: "Create",
|
|
141
|
+
name: "Widget"
|
|
142
|
+
});
|
|
143
|
+
globalThis.expect(results).toEqual([{
|
|
144
|
+
TAG: "Ok",
|
|
145
|
+
_0: "ref-1"
|
|
146
|
+
}]);
|
|
147
|
+
globalThis.expect(seen()).toEqual([[
|
|
148
|
+
"TestAggregate",
|
|
149
|
+
"ref-1",
|
|
150
|
+
"accepted",
|
|
151
|
+
"",
|
|
152
|
+
1
|
|
153
|
+
]]);
|
|
154
|
+
});
|
|
155
|
+
globalThis.test("carries the entity id of an accepted command", async () => {
|
|
156
|
+
record();
|
|
157
|
+
await runAgg("agg-7", "ref-1", {
|
|
158
|
+
TAG: "Create",
|
|
159
|
+
name: "W"
|
|
160
|
+
});
|
|
161
|
+
let match = observed.contents[0].outcome;
|
|
162
|
+
let entityId;
|
|
163
|
+
entityId = match.TAG === "OutcomeAccepted" ? match.entityId : undefined;
|
|
164
|
+
globalThis.expect(entityId).toEqual("agg-7");
|
|
165
|
+
});
|
|
166
|
+
globalThis.test("reports a decide rejection as domain, with the declared error code", async () => {
|
|
167
|
+
await runAgg(undefined, "ref-seed", {
|
|
168
|
+
TAG: "Create",
|
|
169
|
+
name: "Widget"
|
|
170
|
+
});
|
|
171
|
+
record();
|
|
172
|
+
let results = await runAgg(undefined, "ref-1", {
|
|
173
|
+
TAG: "Create",
|
|
174
|
+
name: "Widget"
|
|
175
|
+
});
|
|
176
|
+
globalThis.expect(results).toEqual([{
|
|
177
|
+
TAG: "Ok",
|
|
178
|
+
_0: "ref-1"
|
|
179
|
+
}]);
|
|
180
|
+
globalThis.expect(seen()).toEqual([[
|
|
181
|
+
"TestAggregate",
|
|
182
|
+
"ref-1",
|
|
183
|
+
"rejected/domain",
|
|
184
|
+
"AlreadyExists",
|
|
185
|
+
0
|
|
186
|
+
]]);
|
|
187
|
+
});
|
|
188
|
+
globalThis.test("reports a failed append as infrastructure, not a domain rejection", async () => {
|
|
189
|
+
record();
|
|
190
|
+
AggregateFixtures$ReventlessCore.mock.failNextAppend.contents = true;
|
|
191
|
+
await runAgg(undefined, "ref-1", {
|
|
192
|
+
TAG: "Create",
|
|
193
|
+
name: "Widget"
|
|
194
|
+
});
|
|
195
|
+
globalThis.expect(seen()).toEqual([[
|
|
196
|
+
"TestAggregate",
|
|
197
|
+
"ref-1",
|
|
198
|
+
"rejected/infrastructure",
|
|
199
|
+
"AppendFailed",
|
|
200
|
+
0
|
|
201
|
+
]]);
|
|
202
|
+
});
|
|
203
|
+
globalThis.test("observes every command of a mixed batch", async () => {
|
|
204
|
+
await runAgg(undefined, "ref-seed", {
|
|
205
|
+
TAG: "Create",
|
|
206
|
+
name: "Widget"
|
|
207
|
+
});
|
|
208
|
+
record();
|
|
209
|
+
await Effect.runPromise(AggregateFixtures$ReventlessCore.TestHandler.handleCommands(Stream.fromIterable([
|
|
210
|
+
AggregateFixtures$ReventlessCore.makeTopicItem(undefined, "ref-1", {
|
|
211
|
+
TAG: "Rename",
|
|
212
|
+
newName: "A"
|
|
213
|
+
}),
|
|
214
|
+
AggregateFixtures$ReventlessCore.makeTopicItem(undefined, "ref-2", {
|
|
215
|
+
TAG: "Create",
|
|
216
|
+
name: "B"
|
|
217
|
+
}),
|
|
218
|
+
AggregateFixtures$ReventlessCore.makeTopicItem(undefined, "ref-3", {
|
|
219
|
+
TAG: "Rename",
|
|
220
|
+
newName: "C"
|
|
221
|
+
})
|
|
222
|
+
])));
|
|
223
|
+
globalThis.expect(seen()).toEqual([
|
|
224
|
+
[
|
|
225
|
+
"TestAggregate",
|
|
226
|
+
"ref-1",
|
|
227
|
+
"accepted",
|
|
228
|
+
"",
|
|
229
|
+
2
|
|
230
|
+
],
|
|
231
|
+
[
|
|
232
|
+
"TestAggregate",
|
|
233
|
+
"ref-2",
|
|
234
|
+
"rejected/domain",
|
|
235
|
+
"AlreadyExists",
|
|
236
|
+
0
|
|
237
|
+
],
|
|
238
|
+
[
|
|
239
|
+
"TestAggregate",
|
|
240
|
+
"ref-3",
|
|
241
|
+
"accepted",
|
|
242
|
+
"",
|
|
243
|
+
2
|
|
244
|
+
]
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
globalThis.describe("state-change slice", () => {
|
|
249
|
+
globalThis.test("observes an accepted command with no side-channel installed", async () => {
|
|
250
|
+
record();
|
|
251
|
+
await runSlice("ref-1", {
|
|
252
|
+
TAG: "CreateItem",
|
|
253
|
+
itemId: "item-1",
|
|
254
|
+
name: "Test"
|
|
255
|
+
});
|
|
256
|
+
globalThis.expect(seen()).toEqual([[
|
|
257
|
+
"TestStateChangeSlice",
|
|
258
|
+
DcbFixtures$ReventlessCore.testMeta.msgId,
|
|
259
|
+
"accepted",
|
|
260
|
+
"",
|
|
261
|
+
1
|
|
262
|
+
]]);
|
|
263
|
+
});
|
|
264
|
+
globalThis.test("reports a decide rejection as domain, with the declared error code", async () => {
|
|
265
|
+
await runSlice("ref-seed", {
|
|
266
|
+
TAG: "CreateItem",
|
|
267
|
+
itemId: "item-1",
|
|
268
|
+
name: "Test"
|
|
269
|
+
});
|
|
270
|
+
record();
|
|
271
|
+
await runSlice("ref-1", {
|
|
272
|
+
TAG: "CreateItem",
|
|
273
|
+
itemId: "item-1",
|
|
274
|
+
name: "Again"
|
|
275
|
+
});
|
|
276
|
+
globalThis.expect(seen()).toEqual([[
|
|
277
|
+
"TestStateChangeSlice",
|
|
278
|
+
DcbFixtures$ReventlessCore.testMeta.msgId,
|
|
279
|
+
"rejected/domain",
|
|
280
|
+
"ItemAlreadyExists",
|
|
281
|
+
0
|
|
282
|
+
]]);
|
|
283
|
+
});
|
|
284
|
+
globalThis.test("reports exhausted append conflicts as infrastructure", async () => {
|
|
285
|
+
record();
|
|
286
|
+
dcbMock.failNextAppends.contents = 4;
|
|
287
|
+
await runSlice("ref-1", {
|
|
288
|
+
TAG: "CreateItem",
|
|
289
|
+
itemId: "item-1",
|
|
290
|
+
name: "Test"
|
|
291
|
+
});
|
|
292
|
+
globalThis.expect(seen()).toEqual([[
|
|
293
|
+
"TestStateChangeSlice",
|
|
294
|
+
DcbFixtures$ReventlessCore.testMeta.msgId,
|
|
295
|
+
"rejected/infrastructure",
|
|
296
|
+
"Conflict",
|
|
297
|
+
0
|
|
298
|
+
]]);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
globalThis.describe("registration", () => {
|
|
302
|
+
globalThis.test("with nothing registered nothing is observed and the command is unaffected", async () => {
|
|
303
|
+
let results = await runAgg(undefined, "ref-1", {
|
|
304
|
+
TAG: "Create",
|
|
305
|
+
name: "Widget"
|
|
306
|
+
});
|
|
307
|
+
globalThis.expect(results).toEqual([{
|
|
308
|
+
TAG: "Ok",
|
|
309
|
+
_0: "ref-1"
|
|
310
|
+
}]);
|
|
311
|
+
globalThis.expect(AggregateFixtures$ReventlessCore.mock.getAll().length).toBe(1);
|
|
312
|
+
globalThis.expect(observed.contents.length).toBe(0);
|
|
313
|
+
});
|
|
314
|
+
globalThis.test("clearCommandOutcome stops observation", async () => {
|
|
315
|
+
record();
|
|
316
|
+
CommandTopic_Helpers$ReventlessCore.clearCommandOutcome();
|
|
317
|
+
await runAgg(undefined, "ref-1", {
|
|
318
|
+
TAG: "Create",
|
|
319
|
+
name: "Widget"
|
|
320
|
+
});
|
|
321
|
+
globalThis.expect(observed.contents.length).toBe(0);
|
|
322
|
+
});
|
|
323
|
+
globalThis.test("an out-of-tree extension can register it from onColdStart", async () => {
|
|
324
|
+
let onColdStart = (param, param$1, param$2, param$3) => record();
|
|
325
|
+
let Accountant = {
|
|
326
|
+
moduleUrl: "file:///pkg/accountant.res.mjs",
|
|
327
|
+
onColdStart: onColdStart
|
|
328
|
+
};
|
|
329
|
+
RuntimeExtension$ReventlessCore.use(Accountant);
|
|
330
|
+
RuntimeExtension$ReventlessCore.notifyColdStart("Aggregate", "AllAggregates", "Catalog", "Shop");
|
|
331
|
+
await runAgg(undefined, "ref-1", {
|
|
332
|
+
TAG: "Create",
|
|
333
|
+
name: "Widget"
|
|
334
|
+
});
|
|
335
|
+
globalThis.expect(seen()).toEqual([[
|
|
336
|
+
"TestAggregate",
|
|
337
|
+
"ref-1",
|
|
338
|
+
"accepted",
|
|
339
|
+
"",
|
|
340
|
+
1
|
|
341
|
+
]]);
|
|
342
|
+
});
|
|
343
|
+
globalThis.test("a throwing hook is swallowed and the command still succeeds", async () => {
|
|
344
|
+
CommandTopic_Helpers$ReventlessCore.registerCommandOutcome(param => Stdlib_JsError.throwWithMessage("hook exploded"));
|
|
345
|
+
let results = await runAgg(undefined, "ref-1", {
|
|
346
|
+
TAG: "Create",
|
|
347
|
+
name: "Widget"
|
|
348
|
+
});
|
|
349
|
+
globalThis.expect(results).toEqual([{
|
|
350
|
+
TAG: "Ok",
|
|
351
|
+
_0: "ref-1"
|
|
352
|
+
}]);
|
|
353
|
+
globalThis.expect(AggregateFixtures$ReventlessCore.mock.getAll().length).toBe(1);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
export {
|
|
359
|
+
dcbMock,
|
|
360
|
+
TestDcbOps,
|
|
361
|
+
EventLogOps,
|
|
362
|
+
dcbEventLog,
|
|
363
|
+
SliceBehavior,
|
|
364
|
+
SliceHandler,
|
|
365
|
+
sliceItem,
|
|
366
|
+
runSlice,
|
|
367
|
+
runAgg,
|
|
368
|
+
observed,
|
|
369
|
+
seen,
|
|
370
|
+
record,
|
|
371
|
+
}
|
|
372
|
+
/* dcbMock Not a pure module */
|
|
@@ -26,8 +26,14 @@ describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
|
|
|
26
26
|
->Effect.map(items => {
|
|
27
27
|
items->Array.forEach(item => {
|
|
28
28
|
// Both channels report for the same reference; rejected wins.
|
|
29
|
-
CommandTopic_Helpers.reportAccepted(
|
|
29
|
+
CommandTopic_Helpers.reportAccepted(
|
|
30
|
+
~component="TestService",
|
|
31
|
+
item.reference,
|
|
32
|
+
{entityId: "agg-1", eventCount: 0},
|
|
33
|
+
)
|
|
30
34
|
CommandTopic_Helpers.reportRejected(
|
|
35
|
+
~component="TestService",
|
|
36
|
+
~cause=DomainRejection,
|
|
31
37
|
item.reference,
|
|
32
38
|
{errorCode: "AlreadyExists", errorDetail: ""},
|
|
33
39
|
)
|
|
@@ -53,6 +59,8 @@ describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
|
|
|
53
59
|
->Effect.map(items => {
|
|
54
60
|
items->Array.forEach(item =>
|
|
55
61
|
CommandTopic_Helpers.reportRejected(
|
|
62
|
+
~component="TestService",
|
|
63
|
+
~cause=DomainRejection,
|
|
56
64
|
item.reference,
|
|
57
65
|
{errorCode: "BusinessRuleViolated", errorDetail: "{\"reason\":\"x\"}"},
|
|
58
66
|
)
|
|
@@ -30,11 +30,11 @@ globalThis.describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
|
|
|
30
30
|
globalThis.test("Rejected from rejectedResultChannel beats Accepted from acceptedResultChannel", async () => {
|
|
31
31
|
let handler = stream => Effect.map(Stream.runCollect(stream), items => {
|
|
32
32
|
items.forEach(item => {
|
|
33
|
-
CommandTopic_Helpers$ReventlessCore.reportAccepted(item.reference, {
|
|
33
|
+
CommandTopic_Helpers$ReventlessCore.reportAccepted("TestService", item.reference, {
|
|
34
34
|
entityId: "agg-1",
|
|
35
35
|
eventCount: 0
|
|
36
36
|
});
|
|
37
|
-
CommandTopic_Helpers$ReventlessCore.reportRejected(item.reference, {
|
|
37
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected("TestService", "DomainRejection", item.reference, {
|
|
38
38
|
errorCode: "AlreadyExists",
|
|
39
39
|
errorDetail: ""
|
|
40
40
|
});
|
|
@@ -61,7 +61,7 @@ globalThis.describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
|
|
|
61
61
|
});
|
|
62
62
|
globalThis.test("Rejected from rejectedResultChannel beats Error result (synthesized Conflict)", async () => {
|
|
63
63
|
let handler = stream => Effect.map(Stream.runCollect(stream), items => {
|
|
64
|
-
items.forEach(item => CommandTopic_Helpers$ReventlessCore.reportRejected(item.reference, {
|
|
64
|
+
items.forEach(item => CommandTopic_Helpers$ReventlessCore.reportRejected("TestService", "DomainRejection", item.reference, {
|
|
65
65
|
errorCode: "BusinessRuleViolated",
|
|
66
66
|
errorDetail: "{\"reason\":\"x\"}"
|
|
67
67
|
}));
|
|
@@ -245,6 +245,33 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
245
245
|
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
246
246
|
expect(shipOrder.producedEventTypes)->toEqual(["TestPlugin.OrderShipped"])
|
|
247
247
|
})
|
|
248
|
+
|
|
249
|
+
// A slice's declared errors are the third family of its contract, beside the
|
|
250
|
+
// commands it accepts and the events it emits — what a caller has to handle
|
|
251
|
+
// when a decision is refused.
|
|
252
|
+
testSync("PlaceOrder: the single payload-less error variant is surfaced", () => {
|
|
253
|
+
let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
|
|
254
|
+
expect(placeOrder.errors->Array.map(e => e.name))->toEqual(["AlreadyPlaced"])
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
testSync("ShipOrder: both error variants surface, in declaration order", () => {
|
|
258
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
259
|
+
expect(shipOrder.errors->Array.map(e => e.name))->toEqual(["OrderNotFound", "NotShippable"])
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
testSync("ShipOrder: a payload-carrying error carries its field schema", () => {
|
|
263
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
264
|
+
let notShippable =
|
|
265
|
+
shipOrder.errors->Array.find(e => e.name == "NotShippable")->Option.getOrThrow
|
|
266
|
+
expect(notShippable.schema->String.includes("\"reason\""))->toBe(true)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
testSync("ShipOrder: a payload-less error carries no fields and no references", () => {
|
|
270
|
+
let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
|
|
271
|
+
let notFound = shipOrder.errors->Array.find(e => e.name == "OrderNotFound")->Option.getOrThrow
|
|
272
|
+
expect((notFound.schema->String.includes("\"reason\""), notFound.references->Array.length))
|
|
273
|
+
->toEqual((false, 0))
|
|
274
|
+
})
|
|
248
275
|
})
|
|
249
276
|
|
|
250
277
|
describe("stateViewSlices", () => {
|
|
@@ -398,6 +398,33 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
398
398
|
let shipOrder = structure.stateChangeSlices[1];
|
|
399
399
|
globalThis.expect(shipOrder.producedEventTypes).toEqual(["TestPlugin.OrderShipped"]);
|
|
400
400
|
});
|
|
401
|
+
globalThis.test("PlaceOrder: the single payload-less error variant is surfaced", () => {
|
|
402
|
+
let placeOrder = structure.stateChangeSlices[0];
|
|
403
|
+
globalThis.expect(placeOrder.errors.map(e => e.name)).toEqual(["AlreadyPlaced"]);
|
|
404
|
+
});
|
|
405
|
+
globalThis.test("ShipOrder: both error variants surface, in declaration order", () => {
|
|
406
|
+
let shipOrder = structure.stateChangeSlices[1];
|
|
407
|
+
globalThis.expect(shipOrder.errors.map(e => e.name)).toEqual([
|
|
408
|
+
"OrderNotFound",
|
|
409
|
+
"NotShippable"
|
|
410
|
+
]);
|
|
411
|
+
});
|
|
412
|
+
globalThis.test("ShipOrder: a payload-carrying error carries its field schema", () => {
|
|
413
|
+
let shipOrder = structure.stateChangeSlices[1];
|
|
414
|
+
let notShippable = Stdlib_Option.getOrThrow(shipOrder.errors.find(e => e.name === "NotShippable"), undefined);
|
|
415
|
+
globalThis.expect(notShippable.schema.includes("\"reason\"")).toBe(true);
|
|
416
|
+
});
|
|
417
|
+
globalThis.test("ShipOrder: a payload-less error carries no fields and no references", () => {
|
|
418
|
+
let shipOrder = structure.stateChangeSlices[1];
|
|
419
|
+
let notFound = Stdlib_Option.getOrThrow(shipOrder.errors.find(e => e.name === "OrderNotFound"), undefined);
|
|
420
|
+
globalThis.expect([
|
|
421
|
+
notFound.schema.includes("\"reason\""),
|
|
422
|
+
notFound.references.length
|
|
423
|
+
]).toEqual([
|
|
424
|
+
false,
|
|
425
|
+
0
|
|
426
|
+
]);
|
|
427
|
+
});
|
|
401
428
|
});
|
|
402
429
|
globalThis.describe("stateViewSlices", () => {
|
|
403
430
|
globalThis.test("produces four SVS entries in declaration order", () => {
|
|
@@ -21,8 +21,10 @@ let evolve = (_state, _event) => true
|
|
|
21
21
|
@schema
|
|
22
22
|
type command = @targetState("Shipped") ShipOrder({orderId: string}) | @noApi CancelShipment
|
|
23
23
|
|
|
24
|
+
// Two variants, one payload-less and one carrying a field — the same two branches
|
|
25
|
+
// `events` exercises, so the structure's `errors` list is pinned on both.
|
|
24
26
|
@schema
|
|
25
|
-
type error = OrderNotFound
|
|
27
|
+
type error = OrderNotFound | NotShippable({reason: string})
|
|
26
28
|
|
|
27
29
|
// ShipmentVoided is payload-less — excluded from producedEventTypes (DCB filter),
|
|
28
30
|
// but still surfaced in `events` (toEventDef's payload-less branch).
|
|
@@ -22,7 +22,13 @@ let commandSchema = S.union([
|
|
|
22
22
|
S.literal("CancelShipment")
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
|
-
let errorSchema = S.
|
|
25
|
+
let errorSchema = S.union([
|
|
26
|
+
S.literal("OrderNotFound"),
|
|
27
|
+
S.schema(s => ({
|
|
28
|
+
TAG: "NotShippable",
|
|
29
|
+
reason: s.m(S.string)
|
|
30
|
+
}))
|
|
31
|
+
]);
|
|
26
32
|
|
|
27
33
|
let eventSchema = S.union([
|
|
28
34
|
S.schema(s => ({
|
|
@@ -43,6 +43,10 @@
|
|
|
43
43
|
.structure.aggregates[].commands[].references[].fieldName: string
|
|
44
44
|
.structure.aggregates[].commands[].schema: string
|
|
45
45
|
.structure.aggregates[].consumedEventTypes[]: string
|
|
46
|
+
.structure.aggregates[].errors[].name: string
|
|
47
|
+
.structure.aggregates[].errors[].references[].entity: string
|
|
48
|
+
.structure.aggregates[].errors[].references[].fieldName: string
|
|
49
|
+
.structure.aggregates[].errors[].schema: string
|
|
46
50
|
.structure.aggregates[].events[].name: string
|
|
47
51
|
.structure.aggregates[].events[].references[].entity: string
|
|
48
52
|
.structure.aggregates[].events[].references[].fieldName: string
|
|
@@ -86,6 +90,10 @@
|
|
|
86
90
|
.structure.stateChangeSlices[].commands[].references[].fieldName: string
|
|
87
91
|
.structure.stateChangeSlices[].commands[].schema: string
|
|
88
92
|
.structure.stateChangeSlices[].consumedEventTypes[]: string
|
|
93
|
+
.structure.stateChangeSlices[].errors[].name: string
|
|
94
|
+
.structure.stateChangeSlices[].errors[].references[].entity: string
|
|
95
|
+
.structure.stateChangeSlices[].errors[].references[].fieldName: string
|
|
96
|
+
.structure.stateChangeSlices[].errors[].schema: string
|
|
89
97
|
.structure.stateChangeSlices[].events[].name: string
|
|
90
98
|
.structure.stateChangeSlices[].events[].references[].entity: string
|
|
91
99
|
.structure.stateChangeSlices[].events[].references[].fieldName: string
|