@reventlessdev/reventless-core 3.0.0-alpha.262 → 3.0.0-alpha.264
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 +23 -0
- package/package.json +9 -9
- package/src/admin/Platform_Admin_Structure.res.mjs +1 -1
- package/src/components/Aggregate/Aggregate_Builder.res.mjs +1 -1
- package/src/components/CommandGenerator/CommandGenerator.res +4 -4
- package/src/components/CommandGenerator/CommandGenerator_Callback.res +13 -20
- package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +3 -24
- package/src/components/Dcb/Dcb_Builder.res +45 -49
- package/src/components/Dcb/Dcb_Builder.res.mjs +31 -40
- package/src/components/StateChangeSlice/StateChangeSlice.res +2 -0
- package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +6 -0
- package/src/components/StateChangeSlice/StateChangeSlice_Builder.res.mjs +4 -4
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +51 -22
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +47 -25
- package/src/plugin/component/Plugin_Structure.res +101 -4
- package/src/plugin/component/Plugin_Structure.res.mjs +82 -9
- package/tests/adapter/RuntimeExtensionHookReachTest.res.mjs +1 -1
- package/tests/commandgenerator/CommandGeneratorCallbackTest.res +1 -1
- package/tests/commandgenerator/CommandGeneratorCallbackTest.res.mjs +1 -1
- package/tests/commandgenerator/CommandGeneratorFixtures.res +2 -0
- package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +17 -3
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +2 -2
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +1 -1
- package/tests/dcb/DcbFixtures.res +18 -1
- package/tests/dcb/DcbFixtures.res.mjs +14 -0
- package/tests/dcb/DcbScopeInferenceTest.res +234 -0
- package/tests/dcb/DcbScopeInferenceTest.res.mjs +425 -13
- package/tests/dcb/DcbStateChangeSliceTest.res.mjs +22 -22
- package/tests/dcb/DcbTagTest.res +81 -68
- package/tests/dcb/DcbTagTest.res.mjs +88 -89
- package/tests/plugin/PluginStructureAccessTest.res +1 -0
- package/tests/plugin/PluginStructureAccessTest.res.mjs +1 -1
- package/tests/plugin/PluginStructureTest.res +47 -0
- package/tests/plugin/PluginStructureTest.res.mjs +50 -10
|
@@ -107,6 +107,7 @@ let singleTagSliceGen = CommandGenerator_Callback.makeGenerateCommand(
|
|
|
107
107
|
~commandSchema=singleTagCommandSchema->S.castToUnknown,
|
|
108
108
|
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
109
109
|
~stripIdFromParams=false,
|
|
110
|
+
~partitionTag=Simple({key: "itemId"}),
|
|
110
111
|
)
|
|
111
112
|
|
|
112
113
|
let compositeTagSliceGen = CommandGenerator_Callback.makeGenerateCommand(
|
|
@@ -115,6 +116,7 @@ let compositeTagSliceGen = CommandGenerator_Callback.makeGenerateCommand(
|
|
|
115
116
|
~commandSchema=compositeTagCommandSchema->S.castToUnknown,
|
|
116
117
|
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
117
118
|
~stripIdFromParams=false,
|
|
119
|
+
~partitionTag=Composite({keys: ["environment", "service"], seps: ["-"]}),
|
|
118
120
|
)
|
|
119
121
|
|
|
120
122
|
// Build a payload for a StateChangeSlice command — args must NOT include `id`.
|
|
@@ -139,11 +139,25 @@ let optionalFieldCommandSchema = Sury.$schema(s => ({
|
|
|
139
139
|
note: s.m(Sury.$option(Sury.string))
|
|
140
140
|
}));
|
|
141
141
|
|
|
142
|
-
let optionalFieldSliceGen = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, undefined, "OptionalFieldSlice", optionalFieldCommandSchema, "StateChangeSlice", false);
|
|
142
|
+
let optionalFieldSliceGen = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, undefined, "OptionalFieldSlice", optionalFieldCommandSchema, "StateChangeSlice", false, undefined);
|
|
143
143
|
|
|
144
|
-
let singleTagSliceGen = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, undefined, "SingleTagSlice", singleTagCommandSchema, "StateChangeSlice", false
|
|
144
|
+
let singleTagSliceGen = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, undefined, "SingleTagSlice", singleTagCommandSchema, "StateChangeSlice", false, {
|
|
145
|
+
TAG: "Simple",
|
|
146
|
+
_0: {
|
|
147
|
+
key: "itemId"
|
|
148
|
+
}
|
|
149
|
+
});
|
|
145
150
|
|
|
146
|
-
let compositeTagSliceGen = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, undefined, "CompositeTagSlice", compositeTagCommandSchema, "StateChangeSlice", false
|
|
151
|
+
let compositeTagSliceGen = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, undefined, "CompositeTagSlice", compositeTagCommandSchema, "StateChangeSlice", false, {
|
|
152
|
+
TAG: "Composite",
|
|
153
|
+
_0: {
|
|
154
|
+
keys: [
|
|
155
|
+
"environment",
|
|
156
|
+
"service"
|
|
157
|
+
],
|
|
158
|
+
seps: ["-"]
|
|
159
|
+
}
|
|
160
|
+
});
|
|
147
161
|
|
|
148
162
|
function makeSlicePayload(command, args) {
|
|
149
163
|
return {
|
|
@@ -32,7 +32,7 @@ let published = {
|
|
|
32
32
|
|
|
33
33
|
let generate = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(async cmds => {
|
|
34
34
|
published.contents = published.contents.concat(cmds);
|
|
35
|
-
}, undefined, "Ordering", commandSchema, "StateChangeSlice", false);
|
|
35
|
+
}, undefined, "Ordering", commandSchema, "StateChangeSlice", false, undefined);
|
|
36
36
|
|
|
37
37
|
function cognito(userId, groups) {
|
|
38
38
|
return {
|
|
@@ -193,7 +193,7 @@ globalThis.describe("@owner stamping on the command path:", () => {
|
|
|
193
193
|
globalThis.describe("the schema a generator is built with must answer for its commands:", () => {
|
|
194
194
|
let permissive = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(async cmds => {
|
|
195
195
|
published.contents = published.contents.concat(cmds);
|
|
196
|
-
}, undefined, "Ordering", Sury.json, "StateChangeSlice", false);
|
|
196
|
+
}, undefined, "Ordering", Sury.json, "StateChangeSlice", false, undefined);
|
|
197
197
|
globalThis.test("a permissive schema silently stamps nothing — the defect, pinned", async () => {
|
|
198
198
|
await Effect.runPromise(permissive(placeOrder("cust-B", cognito("cust-A", ["User"]))));
|
|
199
199
|
globalThis.expect(fieldOf(published.contents[published.contents.length - 1 | 0], "customerId")).toEqual("cust-B");
|
|
@@ -82,7 +82,7 @@ function sliceItem(reference, command) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function runSlice(reference, command) {
|
|
85
|
-
return Effect.runPromise(SliceHandler.handleCommands(undefined, undefined, dcbEventLog, Stream.fromIterable([sliceItem(reference, command)])));
|
|
85
|
+
return Effect.runPromise(SliceHandler.handleCommands(undefined, undefined, undefined, dcbEventLog, Stream.fromIterable([sliceItem(reference, command)])));
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
function runAgg(aggIdOpt, reference, command) {
|
|
@@ -375,7 +375,7 @@ type compositeMultiVariant =
|
|
|
375
375
|
count: int,
|
|
376
376
|
})
|
|
377
377
|
|
|
378
|
-
// Schema with @partitionTag (simple)
|
|
378
|
+
// Schema with @partitionTag (simple)
|
|
379
379
|
@schema
|
|
380
380
|
type simplePartitionEvent =
|
|
381
381
|
| OrderPlaced({
|
|
@@ -383,6 +383,23 @@ type simplePartitionEvent =
|
|
|
383
383
|
customerId: @s.matches(Reventless.DcbTag.string) string,
|
|
384
384
|
})
|
|
385
385
|
|
|
386
|
+
// A second entity in the same boundary, annotated on its own key. The partition
|
|
387
|
+
// key is declared after the reference, so a positional default would pick the wrong one.
|
|
388
|
+
@schema
|
|
389
|
+
type addressLinkedEvent =
|
|
390
|
+
| AddressLinked({
|
|
391
|
+
addressId: @s.matches(Reventless.DcbTag.string) string,
|
|
392
|
+
customerId: @s.matches(Reventless.DcbTag.partition) string,
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// Two ids, no annotation and nothing read: inference cannot choose.
|
|
396
|
+
@schema
|
|
397
|
+
type unannotatedLinkEvent =
|
|
398
|
+
| CustomerAddressLinked({
|
|
399
|
+
customerId: @s.matches(Reventless.DcbTag.string) string,
|
|
400
|
+
addressId: @s.matches(Reventless.DcbTag.string) string,
|
|
401
|
+
})
|
|
402
|
+
|
|
386
403
|
// Schema that mixes composite and simple partition (should throw)
|
|
387
404
|
@schema
|
|
388
405
|
type mixedStrategyEvent =
|
|
@@ -473,6 +473,18 @@ let simplePartitionEventSchema = Sury.$schema(s => ({
|
|
|
473
473
|
customerId: s.m(DcbTag$Reventless.string)
|
|
474
474
|
}));
|
|
475
475
|
|
|
476
|
+
let addressLinkedEventSchema = Sury.$schema(s => ({
|
|
477
|
+
TAG: "AddressLinked",
|
|
478
|
+
addressId: s.m(DcbTag$Reventless.string),
|
|
479
|
+
customerId: s.m(DcbTag$Reventless.partition)
|
|
480
|
+
}));
|
|
481
|
+
|
|
482
|
+
let unannotatedLinkEventSchema = Sury.$schema(s => ({
|
|
483
|
+
TAG: "CustomerAddressLinked",
|
|
484
|
+
customerId: s.m(DcbTag$Reventless.string),
|
|
485
|
+
addressId: s.m(DcbTag$Reventless.string)
|
|
486
|
+
}));
|
|
487
|
+
|
|
476
488
|
let mixedStrategyEventSchema = Sury.$schema(s => ({
|
|
477
489
|
TAG: "BadEvent",
|
|
478
490
|
env: s.m(DcbTag$Reventless.compositePartitionMember(0, undefined)),
|
|
@@ -532,6 +544,8 @@ export {
|
|
|
532
544
|
compositeEventCustomSepSchema,
|
|
533
545
|
compositeMultiVariantSchema,
|
|
534
546
|
simplePartitionEventSchema,
|
|
547
|
+
addressLinkedEventSchema,
|
|
548
|
+
unannotatedLinkEventSchema,
|
|
535
549
|
mixedStrategyEventSchema,
|
|
536
550
|
singleCompositeEventSchema,
|
|
537
551
|
subscriptionEventSchema,
|
|
@@ -12,12 +12,14 @@ let slice = (
|
|
|
12
12
|
~consumed=[],
|
|
13
13
|
~produced=[],
|
|
14
14
|
~partitionHint=?,
|
|
15
|
+
~chapter=?,
|
|
15
16
|
): DSI.sliceShape => {
|
|
16
17
|
sliceName,
|
|
17
18
|
command,
|
|
18
19
|
consumed,
|
|
19
20
|
produced,
|
|
20
21
|
partitionHint,
|
|
22
|
+
?chapter,
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
// The online-shop-hybrid catalog boundary, transcribed structurally (NO tag
|
|
@@ -271,6 +273,238 @@ describe("DcbScopeInference:", () => {
|
|
|
271
273
|
)
|
|
272
274
|
})
|
|
273
275
|
|
|
276
|
+
describe("partition: identity given back only when subtraction leaves nothing", () => {
|
|
277
|
+
let partitionOf = (d: DSI.derived, name) => d.partitionBySlice->Dict.get(name)
|
|
278
|
+
|
|
279
|
+
testSync(
|
|
280
|
+
"AddProduct without a hint stays productId, though its category arms come from a categoryId slice",
|
|
281
|
+
() => {
|
|
282
|
+
let d = DSI.infer(catalog)
|
|
283
|
+
expect(addProduct.partitionHint)->toEqual(None)
|
|
284
|
+
expect(d->partitionOf("AddProduct"))->toEqual(Some("productId"))
|
|
285
|
+
},
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
testSync(
|
|
289
|
+
"a lifecycle arm naming the slice's own id resolves",
|
|
290
|
+
() => {
|
|
291
|
+
let changeName = slice(
|
|
292
|
+
"ChangeProductName",
|
|
293
|
+
~command=[scal("productId")],
|
|
294
|
+
~consumed=[ev("ProductAdded", [scal("productId")])],
|
|
295
|
+
~produced=[ev("ProductNameChanged", [scal("productId")])],
|
|
296
|
+
)
|
|
297
|
+
let d = DSI.infer(catalog->Array.concat([changeName]))
|
|
298
|
+
expect(d->partitionOf("ChangeProductName"))->toEqual(Some("productId"))
|
|
299
|
+
expect(d.ambiguities)->toEqual([])
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
// Every arm a slice reads from the other also names that slice's ids, so
|
|
304
|
+
// subtraction leaves both with nothing and each depends on the other.
|
|
305
|
+
let placeOrder = slice(
|
|
306
|
+
"PlaceOrder",
|
|
307
|
+
~partitionHint="orderId",
|
|
308
|
+
~command=[scal("orderId"), scal("customerId")],
|
|
309
|
+
~produced=[ev("OrderPlaced", [scal("orderId"), scal("customerId"), arr("productIds")])],
|
|
310
|
+
)
|
|
311
|
+
let shipOrder = slice(
|
|
312
|
+
"ShipOrder",
|
|
313
|
+
~command=[scal("orderId")],
|
|
314
|
+
~consumed=[
|
|
315
|
+
ev("OrderPlaced", [scal("orderId"), scal("customerId"), arr("productIds")]),
|
|
316
|
+
ev("OrderCancelled", [scal("orderId"), arr("productIds")]),
|
|
317
|
+
],
|
|
318
|
+
~produced=[ev("OrderShipped", [scal("orderId"), scal("customerId")])],
|
|
319
|
+
)
|
|
320
|
+
let cancelOrder = slice(
|
|
321
|
+
"CancelOrder",
|
|
322
|
+
~command=[scal("orderId")],
|
|
323
|
+
~consumed=[
|
|
324
|
+
ev("OrderPlaced", [scal("orderId"), scal("customerId"), arr("productIds")]),
|
|
325
|
+
ev("OrderShipped", [scal("orderId"), scal("customerId")]),
|
|
326
|
+
],
|
|
327
|
+
~produced=[ev("OrderCancelled", [scal("orderId"), arr("productIds")])],
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
testSync(
|
|
331
|
+
"the ShipOrder ⇄ CancelOrder cycle resolves both to orderId",
|
|
332
|
+
() => {
|
|
333
|
+
let d = DSI.infer([placeOrder, shipOrder, cancelOrder])
|
|
334
|
+
expect(d->partitionOf("ShipOrder"))->toEqual(Some("orderId"))
|
|
335
|
+
expect(d->partitionOf("CancelOrder"))->toEqual(Some("orderId"))
|
|
336
|
+
expect(d.ambiguities)->toEqual([])
|
|
337
|
+
},
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
testSync(
|
|
341
|
+
"seen alone, the same slice names the arms to change",
|
|
342
|
+
() =>
|
|
343
|
+
expect(DSI.partitionBlockers(cancelOrder))->toEqual([
|
|
344
|
+
("orderId", ["OrderPlaced", "OrderShipped"]),
|
|
345
|
+
("productId", ["OrderPlaced"]),
|
|
346
|
+
]),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
let recordDemand = slice(
|
|
350
|
+
"RecordProductDemand",
|
|
351
|
+
~command=[scal("productId"), scal("orderId")],
|
|
352
|
+
~consumed=[ev("ProductDemandRecorded", [scal("orderId")])],
|
|
353
|
+
~produced=[ev("ProductDemandRecorded", [scal("productId"), scal("orderId")])],
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
testSync(
|
|
357
|
+
"a join without a hint stays ambiguous",
|
|
358
|
+
() => {
|
|
359
|
+
let d = DSI.infer([recordDemand])
|
|
360
|
+
expect(d->partitionOf("RecordProductDemand"))->toEqual(None)
|
|
361
|
+
expect(d.ambiguities->Array.map(((n, _)) => n))->toEqual(["RecordProductDemand"])
|
|
362
|
+
},
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
testSync(
|
|
366
|
+
"a produced id another slice is partitioned by is not dropped",
|
|
367
|
+
() => {
|
|
368
|
+
// Were productId dropped for belonging to AddProduct, the join would
|
|
369
|
+
// silently resolve to orderId.
|
|
370
|
+
let d = DSI.infer(catalog->Array.concat([placeOrder, recordDemand]))
|
|
371
|
+
expect(d->partitionOf("RecordProductDemand"))->toEqual(None)
|
|
372
|
+
expect(
|
|
373
|
+
d.ambiguities->Array.some(
|
|
374
|
+
((n, reason)) =>
|
|
375
|
+
n == "RecordProductDemand" && reason->String.includes("orderId, productId"),
|
|
376
|
+
),
|
|
377
|
+
)->toBe(true)
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
testSync(
|
|
382
|
+
"a boundary that never settles is reported, not guessed",
|
|
383
|
+
() => {
|
|
384
|
+
// A resolves only while B is unresolved, and B only while A is: each
|
|
385
|
+
// pass flips both. Z* arms have no producer, so they always block.
|
|
386
|
+
let a = slice(
|
|
387
|
+
"A",
|
|
388
|
+
~consumed=[ev("BHappened", [scal("xId")]), ev("ZHappened", [scal("yId")])],
|
|
389
|
+
~produced=[ev("AHappened", [scal("xId"), scal("yId")])],
|
|
390
|
+
)
|
|
391
|
+
let b = slice(
|
|
392
|
+
"B",
|
|
393
|
+
~consumed=[ev("AHappened", [scal("yId")]), ev("Z2Happened", [scal("xId")])],
|
|
394
|
+
~produced=[ev("BHappened", [scal("xId"), scal("yId")])],
|
|
395
|
+
)
|
|
396
|
+
let d = DSI.infer([a, b])
|
|
397
|
+
expect(d.ambiguities->Array.map(((n, _)) => n))->toEqual(["A", "B"])
|
|
398
|
+
expect(
|
|
399
|
+
d.ambiguities->Array.every(((_, reason)) => reason->String.includes("did not settle")),
|
|
400
|
+
)->toBe(true)
|
|
401
|
+
},
|
|
402
|
+
)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
describe("partition: the chapter breaks a tie", () => {
|
|
406
|
+
let placeOrder = (~chapter=?) =>
|
|
407
|
+
slice(
|
|
408
|
+
"PlaceOrder",
|
|
409
|
+
~command=[scal("orderId"), scal("customerId")],
|
|
410
|
+
~consumed=[ev("OrderPlaced", [scal("orderId")])],
|
|
411
|
+
~produced=[ev("OrderPlaced", [scal("orderId"), scal("customerId")])],
|
|
412
|
+
~chapter?,
|
|
413
|
+
)
|
|
414
|
+
let cancelOrder = slice(
|
|
415
|
+
"CancelOrder",
|
|
416
|
+
~chapter="Order",
|
|
417
|
+
~consumed=[ev("OrderPlaced", [])],
|
|
418
|
+
~produced=[ev("OrderCancelled", [scal("orderId")])],
|
|
419
|
+
)
|
|
420
|
+
let partitionOf = (d: DSI.derived, name) => d.partitionBySlice->Dict.get(name)
|
|
421
|
+
|
|
422
|
+
testSync(
|
|
423
|
+
"an order and its customer resolve to the id every Order event carries",
|
|
424
|
+
() => {
|
|
425
|
+
let d = DSI.infer([placeOrder(~chapter="Order"), cancelOrder])
|
|
426
|
+
expect(d->partitionOf("PlaceOrder"))->toEqual(Some("orderId"))
|
|
427
|
+
expect(d.ambiguities)->toEqual([])
|
|
428
|
+
},
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
testSync(
|
|
432
|
+
"without a chapter the same slice stays ambiguous",
|
|
433
|
+
() => {
|
|
434
|
+
let d = DSI.infer([placeOrder(), cancelOrder])
|
|
435
|
+
expect(d->partitionOf("PlaceOrder"))->toEqual(None)
|
|
436
|
+
},
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
testSync(
|
|
440
|
+
"a join whose chapter carries both ids stays ambiguous, and the reason says so",
|
|
441
|
+
() => {
|
|
442
|
+
let demand = slice(
|
|
443
|
+
"RecordProductDemand",
|
|
444
|
+
~chapter="ProductDemand",
|
|
445
|
+
~produced=[ev("ProductDemandRecorded", [scal("productId"), scal("orderId")])],
|
|
446
|
+
)
|
|
447
|
+
let d = DSI.infer([demand])
|
|
448
|
+
expect(d->partitionOf("RecordProductDemand"))->toEqual(None)
|
|
449
|
+
expect(
|
|
450
|
+
d.ambiguities->Array.some(
|
|
451
|
+
((_, reason)) => reason->String.includes("The ProductDemand chapter does not decide"),
|
|
452
|
+
),
|
|
453
|
+
)->toBe(true)
|
|
454
|
+
},
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
testSync(
|
|
458
|
+
"it never overrides a key the subtraction decided",
|
|
459
|
+
() => {
|
|
460
|
+
// AddProduct filed under a chapter keyed by categoryId still resolves to
|
|
461
|
+
// productId: the tie-breaker only sees slices left with several keys.
|
|
462
|
+
let misfiled = {...addProduct, chapter: "Category"}
|
|
463
|
+
let d = DSI.infer([misfiled, {...addCategory, chapter: "Category"}])
|
|
464
|
+
expect(d->partitionOf("AddProduct"))->toEqual(Some("productId"))
|
|
465
|
+
},
|
|
466
|
+
)
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
describe("validatePartitionHintsVsInference (@partitionTag ⇄ inference)", () => {
|
|
470
|
+
let hinted = (s: DSI.sliceShape, key) => {...s, partitionHint: Some(key)}
|
|
471
|
+
let check = shapes => Reventless.DcbValidation.validatePartitionHintsVsInference(~shapes)
|
|
472
|
+
let names = (issues: array<Reventless.DcbValidation.validationError>) =>
|
|
473
|
+
issues->Array.map(e => e.sliceName)
|
|
474
|
+
|
|
475
|
+
testSync(
|
|
476
|
+
"a hint naming a key the slice only references is a contradiction",
|
|
477
|
+
() => {
|
|
478
|
+
let issues = check([hinted(addProduct, "categoryId"), addCategory])
|
|
479
|
+
expect(issues.contradictions->names)->toEqual(["AddProduct"])
|
|
480
|
+
expect(issues.redundancies)->toEqual([])
|
|
481
|
+
},
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
testSync(
|
|
485
|
+
"a hint inference reaches unaided is redundant",
|
|
486
|
+
() => {
|
|
487
|
+
let issues = check([hinted(addProduct, "productId"), addCategory])
|
|
488
|
+
expect(issues.contradictions)->toEqual([])
|
|
489
|
+
expect(issues.redundancies->names)->toEqual(["AddProduct"])
|
|
490
|
+
},
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
testSync(
|
|
494
|
+
"a hint that decides a join is neither",
|
|
495
|
+
() => {
|
|
496
|
+
let demand = slice(
|
|
497
|
+
"RecordProductDemand",
|
|
498
|
+
~partitionHint="productId",
|
|
499
|
+
~produced=[ev("ProductDemandRecorded", [scal("productId"), scal("orderId")])],
|
|
500
|
+
)
|
|
501
|
+
let issues = check([demand])
|
|
502
|
+
expect(issues.contradictions)->toEqual([])
|
|
503
|
+
expect(issues.redundancies)->toEqual([])
|
|
504
|
+
},
|
|
505
|
+
)
|
|
506
|
+
})
|
|
507
|
+
|
|
274
508
|
describe("ambiguity surfacing", () => {
|
|
275
509
|
// A pure join: produces an event carrying two foreign keys, owns neither.
|
|
276
510
|
let aOwner = slice("A", ~produced=[ev("AHappened", [scal("aId")])])
|