@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
|
@@ -4,6 +4,8 @@ module type T = {
|
|
|
4
4
|
let handleCommands: (
|
|
5
5
|
~tagKeysByEventType: Dict.t<array<string>>=?,
|
|
6
6
|
~crossPartitionTagKeys: array<string>=?,
|
|
7
|
+
/** This slice's partition within its boundary; inferred from the slice alone when absent. */
|
|
8
|
+
~partitionTag: Reventless.DcbTag.derivedPartitionTag=?,
|
|
7
9
|
DcbEventLog.operations,
|
|
8
10
|
Stream.t<
|
|
9
11
|
CommandTopic.topicItem<Message.command'<Reventless.Id.String.t, Spec.command>>,
|
|
@@ -74,18 +76,32 @@ module Make = (
|
|
|
74
76
|
{eventType, data: JSON.Object(data), tags, meta}
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// The partition inferred from this slice alone, for callers that do not thread
|
|
80
|
+
// the boundary's. Lazy and non-throwing: it only names ids in logs and outcomes,
|
|
81
|
+
// and a slice that needs its siblings to resolve must not fail to load for that.
|
|
82
|
+
let slicePartitionTag = Lazy.make(() =>
|
|
83
|
+
try Some(
|
|
84
|
+
Reventless.DcbTag.deriveSlicePartition({
|
|
85
|
+
name: Spec.name,
|
|
86
|
+
commandSchema: Spec.commandSchema->S.castToUnknown,
|
|
87
|
+
consumedEventSchema: Spec.consumedEventSchema->S.castToUnknown,
|
|
88
|
+
eventSchema: Spec.eventSchema->S.castToUnknown,
|
|
89
|
+
moduleUrl: Spec.moduleUrl,
|
|
90
|
+
}),
|
|
91
|
+
) catch {
|
|
92
|
+
| _ => None
|
|
93
|
+
}
|
|
94
|
+
)
|
|
81
95
|
|
|
82
96
|
// Extracts the entity id of a read event from its own tags, for logging
|
|
83
97
|
// which events the decision model was built from. Prefers this slice's
|
|
84
|
-
// partition key
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
// partition key, but consumed events from other sources are tagged by their
|
|
99
|
+
// own key (e.g. CatalogProductSynced → productId, not this slice's orderId),
|
|
100
|
+
// so fall back to the read event's own tag value(s) instead of logging no id.
|
|
101
|
+
let readEventId = (
|
|
102
|
+
~partitionTag: option<Reventless.DcbTag.derivedPartitionTag>,
|
|
103
|
+
tags: array<Reventless.DcbTag.tag>,
|
|
104
|
+
): option<string> => {
|
|
89
105
|
let ownTagValues = () => {
|
|
90
106
|
let vals =
|
|
91
107
|
tags->Array.reduce([], (acc, t: Reventless.DcbTag.tag) =>
|
|
@@ -93,15 +109,14 @@ module Make = (
|
|
|
93
109
|
)
|
|
94
110
|
vals->Array.length == 0 ? None : Some(vals->Array.join(","))
|
|
95
111
|
}
|
|
96
|
-
switch
|
|
97
|
-
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
|
|
|
102
|
-
| None => ownTagValues()
|
|
112
|
+
switch partitionTag {
|
|
113
|
+
| Some(Composite(spec)) => Some(Reventless.DcbTag.getCompositePartitionKeyValue(tags, spec))
|
|
114
|
+
| Some(pt) =>
|
|
115
|
+
switch tags->Reventless.DcbTag.partitionValueOfTags(pt) {
|
|
116
|
+
| "" => ownTagValues()
|
|
117
|
+
| v => Some(v)
|
|
103
118
|
}
|
|
104
|
-
|
|
|
119
|
+
| None => ownTagValues()
|
|
105
120
|
}
|
|
106
121
|
}
|
|
107
122
|
|
|
@@ -147,6 +162,7 @@ module Make = (
|
|
|
147
162
|
let handleSingleCommand = (
|
|
148
163
|
~tagKeysByEventType,
|
|
149
164
|
~crossPartitionTagKeys,
|
|
165
|
+
~partitionTag,
|
|
150
166
|
dcbEventLog: DcbEventLog.operations,
|
|
151
167
|
command': Message.command'<Reventless.Id.String.t, Spec.command>,
|
|
152
168
|
) => {
|
|
@@ -192,11 +208,12 @@ module Make = (
|
|
|
192
208
|
EffectLogger.logInfo(~comp, `query: ${queryDetail}`)->Effect.runSync
|
|
193
209
|
|
|
194
210
|
// Extract the entity ID from the command for use in Accepted outcomes.
|
|
195
|
-
let entityId = switch
|
|
196
|
-
|
|
|
197
|
-
| Composite(spec) =>
|
|
211
|
+
let entityId = switch partitionTag {
|
|
212
|
+
| Some(Reventless.DcbTag.Composite(spec)) =>
|
|
198
213
|
let tags = Reventless.DcbTag.extractTags(Spec.commandSchema, command'.command)
|
|
199
214
|
Some(Reventless.DcbTag.getCompositePartitionKeyValue(tags, spec))
|
|
215
|
+
| Some(Simple(pt)) => Reventless.DcbTag.getPartitionTagValue(query, pt)
|
|
216
|
+
| Some(ByEventType(_)) | None => None
|
|
200
217
|
}
|
|
201
218
|
|
|
202
219
|
// Fail closed: a query that can't be serialized yields `None`, not the empty
|
|
@@ -267,7 +284,12 @@ module Make = (
|
|
|
267
284
|
~eventType=raw.eventType,
|
|
268
285
|
~data=raw.data->JSON.Decode.object->Option.getOr(Dict.make()),
|
|
269
286
|
)
|
|
270
|
-
decoded->Option.map(event => (
|
|
287
|
+
decoded->Option.map(event => (
|
|
288
|
+
event,
|
|
289
|
+
raw.position,
|
|
290
|
+
raw.eventType,
|
|
291
|
+
readEventId(~partitionTag, raw.tags),
|
|
292
|
+
))
|
|
271
293
|
})
|
|
272
294
|
->Stream.flatMap(opt =>
|
|
273
295
|
switch opt {
|
|
@@ -474,14 +496,20 @@ module Make = (
|
|
|
474
496
|
let handleCommands = (
|
|
475
497
|
~tagKeysByEventType=Dict.make(),
|
|
476
498
|
~crossPartitionTagKeys=[],
|
|
499
|
+
~partitionTag=?,
|
|
477
500
|
dcbEventLog,
|
|
478
501
|
stream,
|
|
479
|
-
) =>
|
|
502
|
+
) => {
|
|
503
|
+
let partitionTag = switch partitionTag {
|
|
504
|
+
| Some(_) => partitionTag
|
|
505
|
+
| None => slicePartitionTag->Lazy.get
|
|
506
|
+
}
|
|
480
507
|
stream
|
|
481
508
|
->Stream.mapEffect(({ReventlessInfra.CommandTopic.reference: reference, command}) =>
|
|
482
509
|
handleSingleCommand(
|
|
483
510
|
~tagKeysByEventType,
|
|
484
511
|
~crossPartitionTagKeys,
|
|
512
|
+
~partitionTag,
|
|
485
513
|
dcbEventLog,
|
|
486
514
|
command,
|
|
487
515
|
)->Effect.map(result =>
|
|
@@ -492,4 +520,5 @@ module Make = (
|
|
|
492
520
|
)
|
|
493
521
|
)
|
|
494
522
|
->Stream.runCollect
|
|
523
|
+
}
|
|
495
524
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
|
|
4
4
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Stdlib_Lazy from "@rescript/runtime/lib/es6/Stdlib_Lazy.js";
|
|
5
6
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
7
|
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
7
8
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
@@ -22,12 +23,20 @@ function Make(Spec) {
|
|
|
22
23
|
let comp = `StateChangeSlice(` + Spec.name + `)`;
|
|
23
24
|
let decoder = DcbDecode$Reventless.makeDecoder(Spec.consumedEventSchema);
|
|
24
25
|
let queryEventTypes = decoder.eventTypes;
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
let slicePartitionTag = Stdlib_Lazy.make(() => {
|
|
27
|
+
try {
|
|
28
|
+
return DcbTag$Reventless.deriveSlicePartition({
|
|
29
|
+
name: Spec.name,
|
|
30
|
+
commandSchema: Spec.commandSchema,
|
|
31
|
+
consumedEventSchema: Spec.consumedEventSchema,
|
|
32
|
+
eventSchema: Spec.eventSchema,
|
|
33
|
+
moduleUrl: Spec.moduleUrl
|
|
34
|
+
});
|
|
35
|
+
} catch (exn) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
let readEventId = (partitionTag, tags) => {
|
|
31
40
|
let ownTagValues = () => {
|
|
32
41
|
let vals = Stdlib_Array.reduce(tags, [], (acc, t) => {
|
|
33
42
|
if (acc.includes(t.value)) {
|
|
@@ -42,24 +51,26 @@ function Make(Spec) {
|
|
|
42
51
|
return vals.join(",");
|
|
43
52
|
}
|
|
44
53
|
};
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
54
|
+
if (partitionTag === undefined) {
|
|
55
|
+
return ownTagValues();
|
|
47
56
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
switch (partitionTag.TAG) {
|
|
58
|
+
case "Composite" :
|
|
59
|
+
return DcbTag$Reventless.getCompositePartitionKeyValue(tags, partitionTag._0);
|
|
60
|
+
case "Simple" :
|
|
61
|
+
case "ByEventType" :
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
let v = DcbTag$Reventless.partitionValueOfTags(tags, partitionTag);
|
|
65
|
+
if (v === "") {
|
|
57
66
|
return ownTagValues();
|
|
67
|
+
} else {
|
|
68
|
+
return v;
|
|
58
69
|
}
|
|
59
70
|
};
|
|
60
71
|
let projectionCache = Lru$ReventlessCore.make(100);
|
|
61
72
|
let resetCache = () => Lru$ReventlessCore.clear(projectionCache);
|
|
62
|
-
let handleSingleCommand = (tagKeysByEventType, crossPartitionTagKeys, dcbEventLog, command$p) => {
|
|
73
|
+
let handleSingleCommand = (tagKeysByEventType, crossPartitionTagKeys, partitionTag, dcbEventLog, command$p) => {
|
|
63
74
|
let cmdJson = Message$ReventlessCore.commandJsonOfCommand$p(Id$Reventless.$$String.toString, Spec.commandSchema, command$p);
|
|
64
75
|
Effect.runSync(EffectLogger$ReventlessCore.logInfo(comp, cmdJson.commandJson, `handling command: ` + LogFormat$ReventlessCore.cmdDetailNoId(cmdJson)));
|
|
65
76
|
let query = DcbTag$Reventless.buildQueryFromCommand(queryEventTypes, Spec.commandSchema, command$p.command, tagKeysByEventType, crossPartitionTagKeys);
|
|
@@ -76,11 +87,21 @@ function Make(Spec) {
|
|
|
76
87
|
}).join(" OR ");
|
|
77
88
|
Effect.runSync(EffectLogger$ReventlessCore.logInfo(comp, undefined, `query: ` + queryDetail));
|
|
78
89
|
let entityId;
|
|
79
|
-
if (
|
|
80
|
-
|
|
90
|
+
if (partitionTag !== undefined) {
|
|
91
|
+
switch (partitionTag.TAG) {
|
|
92
|
+
case "Simple" :
|
|
93
|
+
entityId = DcbTag$Reventless.getPartitionTagValue(query, partitionTag._0);
|
|
94
|
+
break;
|
|
95
|
+
case "Composite" :
|
|
96
|
+
let tags = DcbTag$Reventless.extractTags(Spec.commandSchema, command$p.command);
|
|
97
|
+
entityId = DcbTag$Reventless.getCompositePartitionKeyValue(tags, partitionTag._0);
|
|
98
|
+
break;
|
|
99
|
+
case "ByEventType" :
|
|
100
|
+
entityId = undefined;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
81
103
|
} else {
|
|
82
|
-
|
|
83
|
-
entityId = DcbTag$Reventless.getCompositePartitionKeyValue(tags, derivedPartitionTag._0);
|
|
104
|
+
entityId = undefined;
|
|
84
105
|
}
|
|
85
106
|
let cacheKey = JSON.stringify(query);
|
|
86
107
|
let cacheGet = () => {
|
|
@@ -131,7 +152,7 @@ function Make(Spec) {
|
|
|
131
152
|
event,
|
|
132
153
|
raw.position,
|
|
133
154
|
raw.eventType,
|
|
134
|
-
readEventId(raw.tags)
|
|
155
|
+
readEventId(partitionTag, raw.tags)
|
|
135
156
|
]);
|
|
136
157
|
}), opt => {
|
|
137
158
|
if (opt !== undefined) {
|
|
@@ -290,12 +311,13 @@ function Make(Spec) {
|
|
|
290
311
|
};
|
|
291
312
|
return attempt(3);
|
|
292
313
|
};
|
|
293
|
-
let handleCommands = (tagKeysByEventTypeOpt, crossPartitionTagKeysOpt, dcbEventLog, stream) => {
|
|
314
|
+
let handleCommands = (tagKeysByEventTypeOpt, crossPartitionTagKeysOpt, partitionTag, dcbEventLog, stream) => {
|
|
294
315
|
let tagKeysByEventType = tagKeysByEventTypeOpt !== undefined ? tagKeysByEventTypeOpt : ({});
|
|
295
316
|
let crossPartitionTagKeys = crossPartitionTagKeysOpt !== undefined ? crossPartitionTagKeysOpt : [];
|
|
317
|
+
let partitionTag$1 = partitionTag !== undefined ? partitionTag : Stdlib_Lazy.get(slicePartitionTag);
|
|
296
318
|
return Stream.runCollect(Stream$1.mapEffect(stream, param => {
|
|
297
319
|
let reference = param.reference;
|
|
298
|
-
return Effect.map(handleSingleCommand(tagKeysByEventType, crossPartitionTagKeys, dcbEventLog, param.command), result => {
|
|
320
|
+
return Effect.map(handleSingleCommand(tagKeysByEventType, crossPartitionTagKeys, partitionTag$1, dcbEventLog, param.command), result => {
|
|
299
321
|
if (result.TAG === "Ok") {
|
|
300
322
|
return {
|
|
301
323
|
TAG: "Ok",
|
|
@@ -345,6 +345,40 @@ let reportTranslationTables = (
|
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
let describePartition = (pt: Reventless.DcbTag.derivedPartitionTag) =>
|
|
349
|
+
switch pt {
|
|
350
|
+
| Simple({key}) => key
|
|
351
|
+
| Composite({keys}) => keys->Array.join(" + ")
|
|
352
|
+
| ByEventType(_) => "a key per event type"
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// An extension groups the commands it sends a DCB slice by the slice's partition
|
|
356
|
+
// key, but it infers that key from the slice alone: the command and the events,
|
|
357
|
+
// not what the slice reads. Where the plugin needs those reads — or the chapter —
|
|
358
|
+
// to decide, the two can differ, and the extension would fail or group commands
|
|
359
|
+
// by the wrong id at run time. `expected` is `None` when the plugin itself cannot
|
|
360
|
+
// derive a partition; that failure is the builder's to report.
|
|
361
|
+
let extensionPartitionFailure = (
|
|
362
|
+
~label: string,
|
|
363
|
+
~delegate: string,
|
|
364
|
+
~expected: option<Reventless.DcbTag.derivedPartitionTag>,
|
|
365
|
+
~actual: result<Reventless.DcbTag.derivedPartitionTag, string>,
|
|
366
|
+
): option<string> =>
|
|
367
|
+
switch (expected, actual) {
|
|
368
|
+
| (None, _) => None
|
|
369
|
+
| (Some(e), Ok(a)) if e == a => None
|
|
370
|
+
| (Some(e), outcome) =>
|
|
371
|
+
let seen = switch outcome {
|
|
372
|
+
| Ok(a) => `groups its commands by ${a->describePartition}`
|
|
373
|
+
| Error(reason) => `cannot tell which key to group its commands by (${reason})`
|
|
374
|
+
}
|
|
375
|
+
Some(
|
|
376
|
+
`${label}: the extension ${seen}, but the plugin partitions ${delegate} by ${e->describePartition}. ` ++
|
|
377
|
+
`An extension sees only the slice's command and events, not what it reads or its chapter — ` ++
|
|
378
|
+
`add @partitionTag ${e->describePartition} to ${delegate}'s event.`,
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
348
382
|
// A command's declared edge names states belonging to another component's enum,
|
|
349
383
|
// and nothing forces a spec to pick the one its linked view declares. Both sides
|
|
350
384
|
// are in hand here. A state the linked views do not declare raises (this runs at assembly,
|
|
@@ -636,7 +670,15 @@ let isCreateCommandName = name =>
|
|
|
636
670
|
name->String.startsWith(p)
|
|
637
671
|
)
|
|
638
672
|
|
|
639
|
-
|
|
673
|
+
// A slice command's id field is the one carrying its slice's partition key, as the
|
|
674
|
+
// boundary infers it; a `@partitionTag` or the first tagged field stands in only
|
|
675
|
+
// where no key was inferred.
|
|
676
|
+
let commandLevelAndId = (
|
|
677
|
+
~isAggregate,
|
|
678
|
+
~partitionKey: option<string>=?,
|
|
679
|
+
~variantName,
|
|
680
|
+
properties: dict<S.t<unknown>>,
|
|
681
|
+
) =>
|
|
640
682
|
if isAggregate {
|
|
641
683
|
if isCreateCommandName(variantName) {
|
|
642
684
|
(Reventless.Plugin.Collection, None)
|
|
@@ -651,9 +693,18 @@ let commandLevelAndId = (~isAggregate, ~variantName, properties: dict<S.t<unknow
|
|
|
651
693
|
fieldName != "TAG" &&
|
|
652
694
|
(Reventless.DcbTag.isTagged(fieldSchema) || Reventless.DcbTag.isTaggedArray(fieldSchema))
|
|
653
695
|
)
|
|
696
|
+
let keyOf = ((fieldName, fieldSchema)) =>
|
|
697
|
+
Reventless.DcbTag.isTaggedArray(fieldSchema)
|
|
698
|
+
? Reventless.DcbTag.resolveArrayTagKey(fieldName, fieldSchema)
|
|
699
|
+
: Reventless.DcbTag.resolveTagKey(fieldName, fieldSchema)
|
|
654
700
|
let taggedField =
|
|
655
|
-
|
|
656
|
-
->Array.find(
|
|
701
|
+
partitionKey
|
|
702
|
+
->Option.flatMap(key => taggedFields->Array.find(field => keyOf(field) == key))
|
|
703
|
+
->Option.orElse(
|
|
704
|
+
taggedFields->Array.find(((_, fieldSchema)) =>
|
|
705
|
+
Reventless.DcbTag.isPartitionTag(fieldSchema)
|
|
706
|
+
),
|
|
707
|
+
)
|
|
657
708
|
->Option.orElse(taggedFields->Array.get(0))
|
|
658
709
|
switch taggedField {
|
|
659
710
|
| Some((fieldName, _)) =>
|
|
@@ -700,6 +751,7 @@ let annotateArgTypes = (schema: JSON.t, argTypes: dict<string>): JSON.t => {
|
|
|
700
751
|
|
|
701
752
|
let toCommandDef = (
|
|
702
753
|
~isAggregate,
|
|
754
|
+
~partitionKey: option<string>=?,
|
|
703
755
|
~mutationFieldFor: string => string,
|
|
704
756
|
~parentSchema: S.t<unknown>,
|
|
705
757
|
// The PPX-generated `command => permission`. Per VARIANT, not per component:
|
|
@@ -727,7 +779,12 @@ let toCommandDef = (
|
|
|
727
779
|
// empty for a payload-less variant (e.g. `| Archive`), which compiles to a bare
|
|
728
780
|
// `S.literal("Archive")` string rather than an `{TAG, ...}` object.
|
|
729
781
|
let mkDef = (~variantName, ~properties) => {
|
|
730
|
-
let (guessedLevel, aggregateIdField) = commandLevelAndId(
|
|
782
|
+
let (guessedLevel, aggregateIdField) = commandLevelAndId(
|
|
783
|
+
~isAggregate,
|
|
784
|
+
~partitionKey?,
|
|
785
|
+
~variantName,
|
|
786
|
+
properties,
|
|
787
|
+
)
|
|
731
788
|
let references = extractReferences(properties)
|
|
732
789
|
// Evaluated against a synthetic value per constructor, the same shape the
|
|
733
790
|
// resolver builds at call time: a payload-bearing variant compiles to
|
|
@@ -867,6 +924,7 @@ let toCommandDef = (
|
|
|
867
924
|
|
|
868
925
|
let extractCommandDefs = (
|
|
869
926
|
~isAggregate,
|
|
927
|
+
~partitionKey: option<string>=?,
|
|
870
928
|
~mutationFieldFor: string => string,
|
|
871
929
|
~commandAuthorization: unknown => Reventless.Authorization.permission,
|
|
872
930
|
~commandTransition: unknown => Reventless.Transition.t<string>,
|
|
@@ -878,6 +936,7 @@ let extractCommandDefs = (
|
|
|
878
936
|
anyOf->Array.filterMap(v =>
|
|
879
937
|
toCommandDef(
|
|
880
938
|
~isAggregate,
|
|
939
|
+
~partitionKey?,
|
|
881
940
|
~mutationFieldFor,
|
|
882
941
|
~parentSchema=commandSchema,
|
|
883
942
|
~commandAuthorization,
|
|
@@ -890,6 +949,7 @@ let extractCommandDefs = (
|
|
|
890
949
|
// Single-variant command types compile to a bare Object schema, not a Union.
|
|
891
950
|
toCommandDef(
|
|
892
951
|
~isAggregate,
|
|
952
|
+
~partitionKey?,
|
|
893
953
|
~mutationFieldFor,
|
|
894
954
|
~parentSchema=commandSchema,
|
|
895
955
|
~commandAuthorization,
|
|
@@ -1445,6 +1505,30 @@ let make = (
|
|
|
1445
1505
|
|
|
1446
1506
|
// ── Build writable defs ────────────────────────────────────────────────────
|
|
1447
1507
|
|
|
1508
|
+
// Non-throwing: this is a description of the plugin, and the builder is what
|
|
1509
|
+
// refuses a boundary whose partitions do not resolve.
|
|
1510
|
+
let dcbSliceSchemas =
|
|
1511
|
+
stateChangeSlices->Array.map((module(SCS: ReventlessInfra.StateChangeSlice.T)) => {
|
|
1512
|
+
Reventless.DcbTag.name: SCS.Spec.name,
|
|
1513
|
+
commandSchema: SCS.Spec.commandSchema->S.castToUnknown,
|
|
1514
|
+
consumedEventSchema: SCS.Spec.consumedEventSchema->S.castToUnknown,
|
|
1515
|
+
eventSchema: SCS.Spec.eventSchema->S.castToUnknown,
|
|
1516
|
+
moduleUrl: SCS.Spec.moduleUrl,
|
|
1517
|
+
})
|
|
1518
|
+
let {partitionBySlice}: Reventless.DcbScopeInference.partitionResolution =
|
|
1519
|
+
dcbSliceSchemas
|
|
1520
|
+
->Array.map(Reventless.DcbTag.sliceShape)
|
|
1521
|
+
->Reventless.DcbScopeInference.resolvePartitions
|
|
1522
|
+
let slicePartitionOf = switch Reventless.DcbTag.deriveBoundaryPartition(dcbSliceSchemas) {
|
|
1523
|
+
| boundary =>
|
|
1524
|
+
delegate =>
|
|
1525
|
+
dcbSliceSchemas->Array.some(s => s.name == delegate)
|
|
1526
|
+
? boundary->Reventless.DcbTag.slicePartitionTag(delegate)
|
|
1527
|
+
: None
|
|
1528
|
+
| exception JsExn(_) => _ => None
|
|
1529
|
+
}
|
|
1530
|
+
let partitionFailures = []
|
|
1531
|
+
|
|
1448
1532
|
let stateChangeDefs =
|
|
1449
1533
|
stateChangeSlices->Array.mapWithIndex((module(SCS: ReventlessInfra.StateChangeSlice.T), i) => {
|
|
1450
1534
|
let (_, produced) = scsProduced->Array.getUnsafe(i)
|
|
@@ -1455,6 +1539,7 @@ let make = (
|
|
|
1455
1539
|
Reventless.Plugin.name: SCS.Spec.name,
|
|
1456
1540
|
commands: extractCommandDefs(
|
|
1457
1541
|
~isAggregate=false,
|
|
1542
|
+
~partitionKey=?partitionBySlice->Dict.get(SCS.Spec.name),
|
|
1458
1543
|
~mutationFieldFor=variantName =>
|
|
1459
1544
|
Api_Naming.sliceMutationFieldFor(
|
|
1460
1545
|
~plugin=name,
|
|
@@ -1592,6 +1677,12 @@ let make = (
|
|
|
1592
1677
|
let eventsByCommand: Dict.t<array<string>> = Dict.make()
|
|
1593
1678
|
E.mappings->Array.forEach((module(M: E.Mapping)) => {
|
|
1594
1679
|
let label = `${E.Spec.name} → ${M.delegateName}`
|
|
1680
|
+
extensionPartitionFailure(
|
|
1681
|
+
~label,
|
|
1682
|
+
~delegate=M.delegateName,
|
|
1683
|
+
~expected=slicePartitionOf(M.delegateName),
|
|
1684
|
+
~actual=M.delegatePartition(),
|
|
1685
|
+
)->Option.forEach(f => partitionFailures->Array.push(f))
|
|
1595
1686
|
pushAll(
|
|
1596
1687
|
tableFailures,
|
|
1597
1688
|
handledTableFailures(
|
|
@@ -1965,6 +2056,12 @@ let make = (
|
|
|
1965
2056
|
})
|
|
1966
2057
|
|
|
1967
2058
|
reportTranslationTables(~pluginName=name, ~failures=tableFailures, ~warnings=tableWarnings)
|
|
2059
|
+
if partitionFailures->Array.length > 0 {
|
|
2060
|
+
JsError.throwWithMessage(
|
|
2061
|
+
`${name}: an extension cannot group its commands by the target slice's partition key.\n` ++
|
|
2062
|
+
partitionFailures->Array.join("\n"),
|
|
2063
|
+
)
|
|
2064
|
+
}
|
|
1968
2065
|
|
|
1969
2066
|
// Second pass, on purpose: commands are built well before `linkedViews` is
|
|
1970
2067
|
// assembled, so the check cannot run inline where the defs are made.
|
|
@@ -13,6 +13,7 @@ import * as Trait$Reventless from "@reventlessdev/reventless-spec/src/types/Trai
|
|
|
13
13
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
14
14
|
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
15
15
|
import * as Lifecycle$Reventless from "@reventlessdev/reventless-spec/src/types/Lifecycle.res.mjs";
|
|
16
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
16
17
|
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
17
18
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
18
19
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
@@ -23,6 +24,7 @@ import * as Api_Naming$ReventlessCore from "../../components/Api/Api_Naming.res.
|
|
|
23
24
|
import * as CapabilityNeed$Reventless from "@reventlessdev/reventless-spec/src/semantic/CapabilityNeed.res.mjs";
|
|
24
25
|
import * as SchemaType$ReventlessCore from "../../components/Api/SchemaType.res.mjs";
|
|
25
26
|
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
27
|
+
import * as DcbScopeInference$Reventless from "@reventlessdev/reventless-spec/src/components/DcbScopeInference.res.mjs";
|
|
26
28
|
import * as ApiNoApiHelpers$ReventlessCore from "../../components/Api/ApiNoApiHelpers.res.mjs";
|
|
27
29
|
import * as SuryToJsonSchema$ReventlessCore from "../../components/Api/SuryToJsonSchema.res.mjs";
|
|
28
30
|
import * as Capability_Inference$ReventlessCore from "./Capability_Inference.res.mjs";
|
|
@@ -289,6 +291,29 @@ function reportTranslationTables(pluginName, failures, warnings) {
|
|
|
289
291
|
}
|
|
290
292
|
}
|
|
291
293
|
|
|
294
|
+
function describePartition(pt) {
|
|
295
|
+
switch (pt.TAG) {
|
|
296
|
+
case "Simple" :
|
|
297
|
+
return pt._0.key;
|
|
298
|
+
case "Composite" :
|
|
299
|
+
return pt._0.keys.join(" + ");
|
|
300
|
+
case "ByEventType" :
|
|
301
|
+
return "a key per event type";
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function extensionPartitionFailure(label, delegate, expected, actual) {
|
|
306
|
+
if (expected === undefined) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (actual.TAG === "Ok" && Primitive_object.equal(expected, actual._0)) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
let seen;
|
|
313
|
+
seen = actual.TAG === "Ok" ? `groups its commands by ` + describePartition(actual._0) : `cannot tell which key to group its commands by (` + actual._0 + `)`;
|
|
314
|
+
return label + `: the extension ` + seen + `, but the plugin partitions ` + delegate + ` by ` + describePartition(expected) + `. An extension sees only the slice's command and events, not what it reads or its chapter — ` + (`add @partitionTag ` + describePartition(expected) + ` to ` + delegate + `'s event.`);
|
|
315
|
+
}
|
|
316
|
+
|
|
292
317
|
function checkDeclaredTransitions(pluginName, writables, lifecycleStatesByView) {
|
|
293
318
|
let unvalidated = {
|
|
294
319
|
contents: 0
|
|
@@ -492,7 +517,7 @@ function isCreateCommandName(name) {
|
|
|
492
517
|
].some(p => name.startsWith(p));
|
|
493
518
|
}
|
|
494
519
|
|
|
495
|
-
function commandLevelAndId(isAggregate, variantName, properties) {
|
|
520
|
+
function commandLevelAndId(isAggregate, partitionKey, variantName, properties) {
|
|
496
521
|
if (isAggregate) {
|
|
497
522
|
if (isCreateCommandName(variantName)) {
|
|
498
523
|
return [
|
|
@@ -518,7 +543,16 @@ function commandLevelAndId(isAggregate, variantName, properties) {
|
|
|
518
543
|
return false;
|
|
519
544
|
}
|
|
520
545
|
});
|
|
521
|
-
let
|
|
546
|
+
let keyOf = param => {
|
|
547
|
+
let fieldSchema = param[1];
|
|
548
|
+
let fieldName = param[0];
|
|
549
|
+
if (DcbTag$Reventless.isTaggedArray(fieldSchema)) {
|
|
550
|
+
return DcbTag$Reventless.resolveArrayTagKey(fieldName, fieldSchema);
|
|
551
|
+
} else {
|
|
552
|
+
return DcbTag$Reventless.resolveTagKey(fieldName, fieldSchema);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
let taggedField = Stdlib_Option.orElse(Stdlib_Option.orElse(Stdlib_Option.flatMap(partitionKey, key => taggedFields.find(field => keyOf(field) === key)), taggedFields.find(param => DcbTag$Reventless.isPartitionTag(param[1]))), taggedFields[0]);
|
|
522
556
|
if (taggedField === undefined) {
|
|
523
557
|
return [
|
|
524
558
|
"Collection",
|
|
@@ -563,9 +597,9 @@ function annotateArgTypes(schema, argTypes) {
|
|
|
563
597
|
return schema;
|
|
564
598
|
}
|
|
565
599
|
|
|
566
|
-
function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthorization, commandTransition, derivedEdgeFor, v) {
|
|
600
|
+
function toCommandDef(isAggregate, partitionKey, mutationFieldFor, parentSchema, commandAuthorization, commandTransition, derivedEdgeFor, v) {
|
|
567
601
|
let mkDef = (variantName, properties) => {
|
|
568
|
-
let match = commandLevelAndId(isAggregate, variantName, properties);
|
|
602
|
+
let match = commandLevelAndId(isAggregate, partitionKey, variantName, properties);
|
|
569
603
|
let references = extractReferences(properties);
|
|
570
604
|
let syntheticCommand = DcbTag$Reventless.isVariantPayloadBearing(parentSchema, variantName) ? ({
|
|
571
605
|
TAG: variantName
|
|
@@ -649,12 +683,12 @@ function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthor
|
|
|
649
683
|
}
|
|
650
684
|
}
|
|
651
685
|
|
|
652
|
-
function extractCommandDefs(isAggregate, mutationFieldFor, commandAuthorization, commandTransition, derivedEdgeForOpt, commandSchema) {
|
|
686
|
+
function extractCommandDefs(isAggregate, partitionKey, mutationFieldFor, commandAuthorization, commandTransition, derivedEdgeForOpt, commandSchema) {
|
|
653
687
|
let derivedEdgeFor = derivedEdgeForOpt !== undefined ? derivedEdgeForOpt : param => {};
|
|
654
688
|
if (commandSchema.type === "anyOf") {
|
|
655
|
-
return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, v));
|
|
689
|
+
return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, partitionKey, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, v));
|
|
656
690
|
} else {
|
|
657
|
-
return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, commandSchema), [], def => [def]);
|
|
691
|
+
return Stdlib_Option.mapOr(toCommandDef(isAggregate, partitionKey, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, derivedEdgeFor, commandSchema), [], def => [def]);
|
|
658
692
|
}
|
|
659
693
|
}
|
|
660
694
|
|
|
@@ -1036,6 +1070,37 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1036
1070
|
requiredAccess: accessKeysFor(SVS.Spec.authorization)
|
|
1037
1071
|
};
|
|
1038
1072
|
});
|
|
1073
|
+
let dcbSliceSchemas = stateChangeSlices.map(SCS => ({
|
|
1074
|
+
name: SCS.Spec.name,
|
|
1075
|
+
commandSchema: SCS.Spec.commandSchema,
|
|
1076
|
+
consumedEventSchema: SCS.Spec.consumedEventSchema,
|
|
1077
|
+
eventSchema: SCS.Spec.eventSchema,
|
|
1078
|
+
moduleUrl: SCS.Spec.moduleUrl
|
|
1079
|
+
}));
|
|
1080
|
+
let match = DcbScopeInference$Reventless.resolvePartitions(dcbSliceSchemas.map(DcbTag$Reventless.sliceShape));
|
|
1081
|
+
let partitionBySlice = match.partitionBySlice;
|
|
1082
|
+
let slicePartitionOf;
|
|
1083
|
+
let exit = 0;
|
|
1084
|
+
let boundary;
|
|
1085
|
+
try {
|
|
1086
|
+
boundary = DcbTag$Reventless.deriveBoundaryPartition(dcbSliceSchemas);
|
|
1087
|
+
exit = 1;
|
|
1088
|
+
} catch (raw_exn) {
|
|
1089
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
1090
|
+
if (exn.RE_EXN_ID === "JsExn") {
|
|
1091
|
+
slicePartitionOf = param => {};
|
|
1092
|
+
} else {
|
|
1093
|
+
throw exn;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (exit === 1) {
|
|
1097
|
+
slicePartitionOf = delegate => {
|
|
1098
|
+
if (dcbSliceSchemas.some(s => s.name === delegate)) {
|
|
1099
|
+
return DcbTag$Reventless.slicePartitionTag(boundary, delegate);
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
let partitionFailures = [];
|
|
1039
1104
|
let stateChangeDefs = stateChangeSlices.map((SCS, i) => {
|
|
1040
1105
|
let match = scsProduced[i];
|
|
1041
1106
|
let produced = match[1];
|
|
@@ -1043,7 +1108,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1043
1108
|
let consumed = match$1[1];
|
|
1044
1109
|
return {
|
|
1045
1110
|
name: SCS.Spec.name,
|
|
1046
|
-
commands: extractCommandDefs(false, variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandTransition, extra => derivedEdgeFor(SCS.Spec.name, extra), SCS.Spec.commandSchema),
|
|
1111
|
+
commands: extractCommandDefs(false, partitionBySlice[SCS.Spec.name], variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandTransition, extra => derivedEdgeFor(SCS.Spec.name, extra), SCS.Spec.commandSchema),
|
|
1047
1112
|
producedEventTypes: produced,
|
|
1048
1113
|
consumedEventTypes: consumed,
|
|
1049
1114
|
linkedViews: linkedSvsFor(produced),
|
|
@@ -1058,7 +1123,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1058
1123
|
let produced = match[1];
|
|
1059
1124
|
return {
|
|
1060
1125
|
name: A.Spec.name,
|
|
1061
|
-
commands: extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandTransition, extra => derivedEdgeFor(A.Spec.name, extra), A.Spec.commandSchema),
|
|
1126
|
+
commands: extractCommandDefs(true, undefined, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandTransition, extra => derivedEdgeFor(A.Spec.name, extra), A.Spec.commandSchema),
|
|
1062
1127
|
producedEventTypes: produced,
|
|
1063
1128
|
consumedEventTypes: [],
|
|
1064
1129
|
linkedViews: linkedSvsFor(produced).concat(linkedReadModelsFor(A.Spec.name)),
|
|
@@ -1109,6 +1174,9 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1109
1174
|
let eventsByCommand = {};
|
|
1110
1175
|
E.mappings.forEach(M => {
|
|
1111
1176
|
let label = E.Spec.name + ` → ` + M.delegateName;
|
|
1177
|
+
Stdlib_Option.forEach(extensionPartitionFailure(label, M.delegateName, slicePartitionOf(M.delegateName), M.delegatePartition()), f => {
|
|
1178
|
+
partitionFailures.push(f);
|
|
1179
|
+
});
|
|
1112
1180
|
pushAll(tableFailures, handledTableFailures(label, M.handledEvents, epEventNames, M.delegateCommandNames.concat(epCommandNames)));
|
|
1113
1181
|
pushAll(tableFailures, commandTableFailures(label, "issuedCommands", "comes from", M.issuedCommands.map(param => [
|
|
1114
1182
|
param.name,
|
|
@@ -1330,6 +1398,9 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
1330
1398
|
});
|
|
1331
1399
|
});
|
|
1332
1400
|
reportTranslationTables(name, tableFailures, tableWarnings);
|
|
1401
|
+
if (partitionFailures.length !== 0) {
|
|
1402
|
+
Stdlib_JsError.throwWithMessage(name + `: an extension cannot group its commands by the target slice's partition key.\n` + partitionFailures.join("\n"));
|
|
1403
|
+
}
|
|
1333
1404
|
checkDeclaredTransitions(name, stateChangeDefs.concat(aggregateDefs), lifecycleStatesByView);
|
|
1334
1405
|
checkLifecycleTopology(name, stateChangeDefs.concat(aggregateDefs), lifecycleStatesByView);
|
|
1335
1406
|
reportRetiredStates(name, retiredFailures, retiredUnchecked);
|
|
@@ -1374,6 +1445,8 @@ export {
|
|
|
1374
1445
|
probeMeta,
|
|
1375
1446
|
probeQueryEngine,
|
|
1376
1447
|
reportTranslationTables,
|
|
1448
|
+
describePartition,
|
|
1449
|
+
extensionPartitionFailure,
|
|
1377
1450
|
checkDeclaredTransitions,
|
|
1378
1451
|
lifecycleTopologyFindings,
|
|
1379
1452
|
checkLifecycleTopology,
|
|
@@ -22,7 +22,7 @@ async function publishJsons(cmds) {
|
|
|
22
22
|
published.contents = published.contents.concat(cmds);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, undefined, "Product", commandSchema, "StateChangeSlice", false);
|
|
25
|
+
let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, undefined, "Product", commandSchema, "StateChangeSlice", false, undefined);
|
|
26
26
|
|
|
27
27
|
function addChair() {
|
|
28
28
|
return {
|
|
@@ -91,7 +91,7 @@ describe("CommandGenerator_Callback.generateCommand:", () => {
|
|
|
91
91
|
|
|
92
92
|
describe("StateChangeSlice envelope id derivation", () => {
|
|
93
93
|
testPromise(
|
|
94
|
-
"
|
|
94
|
+
"the slice's partition key: id derived from its field when args have no id",
|
|
95
95
|
async () => {
|
|
96
96
|
let payload = makeSlicePayload(
|
|
97
97
|
~command="CreateItem",
|
|
@@ -85,7 +85,7 @@ globalThis.describe("CommandGenerator_Callback.generateCommand:", () => {
|
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
globalThis.describe("StateChangeSlice envelope id derivation", () => {
|
|
88
|
-
globalThis.test("
|
|
88
|
+
globalThis.test("the slice's partition key: id derived from its field when args have no id", async () => {
|
|
89
89
|
let payload = CommandGeneratorFixtures$ReventlessCore.makeSlicePayload("CreateItem", Object.fromEntries([
|
|
90
90
|
[
|
|
91
91
|
"itemId",
|