@reventlessdev/reventless-core 3.0.0-alpha.266 → 3.0.0-alpha.268
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 +26 -0
- package/package.json +5 -5
- package/src/Projection.res +0 -10
- package/src/Projection.res.mjs +0 -14
- package/src/ProjectionMapper.res +8 -2
- package/src/ProjectionMapper.res.mjs +3 -3
- package/src/admin/Platform_Admin_Structure.res.mjs +1 -1
- package/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs +3 -0
- package/src/components/EventMapper/EventMapper_Builder.res +19 -1
- package/src/components/EventMapper/EventMapper_Builder.res.mjs +10 -1
- package/src/components/StateViewSlice/StateViewSlice_Builder.res +11 -1
- package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +2 -1
- package/src/components/StateViewSlice/StateViewSlice_Callback.res +8 -1
- package/src/components/StateViewSlice/StateViewSlice_Callback.res.mjs +2 -1
- package/src/plugin/component/Plugin_Structure.res +139 -15
- package/src/plugin/component/Plugin_Structure.res.mjs +110 -25
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +0 -1
- package/tests/dcb/DcbStateChangeSliceTest.res.mjs +0 -3
- package/tests/eventmapper/EventMapperFixtures.res +2 -2
- package/tests/eventmapper/EventMapperFixtures.res.mjs +14 -2
- package/tests/plugin/IdentityReferenceTest.res +118 -0
- package/tests/plugin/IdentityReferenceTest.res.mjs +204 -0
- package/tests/plugin/PluginStructureAccessTest.res.mjs +14 -1
- package/tests/plugin/PluginStructureTest.res.mjs +42 -10
- package/tests/plugin/StateView/PsAnnotatedView.res.mjs +3 -0
- package/tests/plugin/StateView/PsAvailableProductsView.res.mjs +3 -0
- package/tests/plugin/StateView/PsCategoriesView.res.mjs +3 -0
- package/tests/plugin/StateView/PsCustomersView.res.mjs +3 -0
- package/tests/plugin/StateView/PsGatedView.res.mjs +3 -0
- package/tests/plugin/StateView/PsOrdersView.res.mjs +3 -0
- package/tests/plugin/StateView/PsShipmentsView.res.mjs +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.268 (2026-09-21)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **core:** a creating command's own id derives no reference ([58a921c](https://github.com/ReventlessDev/reventless-core/commit/58a921c84352f6522e7c175387927e32365aa94a))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.267 (2026-09-21)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **core:** an event mapping that commands its own source no longer hangs ([d30ce6b](https://github.com/ReventlessDev/reventless-core/commit/d30ce6b360707d933732fa2c90aba49c03dda006))
|
|
18
|
+
* feat(spec)!: typed projection keys ([fc6a018](https://github.com/ReventlessDev/reventless-core/commit/fc6a01811ba788f3289e84a435a09f77fa627e1a))
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* **core:** references derived from identities, and identity checks ([7cb1376](https://github.com/ReventlessDev/reventless-core/commit/7cb1376e56cdb4e4d5ab5954cd97fd37fd3ba258))
|
|
22
|
+
|
|
23
|
+
### BREAKING CHANGES
|
|
24
|
+
|
|
25
|
+
* a projection that keys a row by a payload `string`, or uses
|
|
26
|
+
the envelope id as a `string`, converts explicitly with
|
|
27
|
+
`Target.Id.makeFromString` / `Source.Id.toString`. `StateChangeSlice.Spec` no
|
|
28
|
+
longer requires `module Id`.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
6
32
|
# 3.0.0-alpha.266 (2026-09-21)
|
|
7
33
|
|
|
8
34
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.268",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -31,19 +31,19 @@
|
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.33",
|
|
32
32
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
33
33
|
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.12",
|
|
34
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.12",
|
|
35
34
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
35
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.12",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.21",
|
|
37
37
|
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.12",
|
|
38
38
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
39
39
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.37",
|
|
40
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
41
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
40
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.168",
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.140"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
45
45
|
"sury-ppx": "11.0.0-rc.2",
|
|
46
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
46
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.81"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"rescript": "12.3.0"
|
package/src/Projection.res
CHANGED
|
@@ -6,16 +6,6 @@ module Set = Belt.Set.String
|
|
|
6
6
|
|
|
7
7
|
let log = Logger.fromEnv()
|
|
8
8
|
|
|
9
|
-
module Mapping = {
|
|
10
|
-
module MakeGenericSource = (Mapping: Reventless.Projection.Mapping): (
|
|
11
|
-
Mapper.GenericSource with type t = Mapping.sourceEvent
|
|
12
|
-
) => {
|
|
13
|
-
let name = Mapping.sourceName
|
|
14
|
-
type t = Mapping.sourceEvent
|
|
15
|
-
let decode' = json => json->Message.decodeEvent'(S.string, Mapping.sourceEventSchema)
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
9
|
// Lazy: the message thunk (and any `stateToString` it triggers) runs only when
|
|
20
10
|
// debug logging is enabled, so projection actions don't serialize state on the
|
|
21
11
|
// hot path at the default Info level.
|
package/src/Projection.res.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
-
import * as Sury from "sury";
|
|
4
3
|
import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
|
|
5
4
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
6
5
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
@@ -22,18 +21,6 @@ import * as LogFormat$ReventlessCore from "./util/LogFormat.res.mjs";
|
|
|
22
21
|
|
|
23
22
|
let log = Logger$ReventlessCore.fromEnv();
|
|
24
23
|
|
|
25
|
-
function MakeGenericSource(Mapping) {
|
|
26
|
-
let decode$p = json => Message$ReventlessCore.decodeEvent$p(json, Sury.string, Mapping.sourceEventSchema);
|
|
27
|
-
return {
|
|
28
|
-
name: Mapping.sourceName,
|
|
29
|
-
decode$p: decode$p
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let Mapping = {
|
|
34
|
-
MakeGenericSource: MakeGenericSource
|
|
35
|
-
};
|
|
36
|
-
|
|
37
24
|
function logAction(makeStr) {
|
|
38
25
|
log.debugLazy("Projection", makeStr);
|
|
39
26
|
}
|
|
@@ -786,7 +773,6 @@ let $$Set;
|
|
|
786
773
|
export {
|
|
787
774
|
$$Set,
|
|
788
775
|
log,
|
|
789
|
-
Mapping,
|
|
790
776
|
logAction,
|
|
791
777
|
overlaysFor,
|
|
792
778
|
applyOverlays,
|
package/src/ProjectionMapper.res
CHANGED
|
@@ -34,8 +34,14 @@ module Make = (
|
|
|
34
34
|
module GenericMapping = {
|
|
35
35
|
let sourceName = M.sourceName
|
|
36
36
|
let acceptedTags = Reventless.DcbTag.extractAllVariantNames(M.sourceEventSchema)
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// The storage edge: the envelope and the table carry strings, the
|
|
38
|
+
// projection typed ids — the same strings, decoded and encoded here.
|
|
39
|
+
let decode = json => json->Message.decodeEvent'(M.SourceId.schema, M.sourceEventSchema)
|
|
40
|
+
let project = MapperNto1.makeGenericMap(decode, msg =>
|
|
41
|
+
msg
|
|
42
|
+
->M.project
|
|
43
|
+
->Reventless.Projection.mapActionId(~to_=M.targetIdToString, ~from=M.targetIdFromString)
|
|
44
|
+
)
|
|
39
45
|
}
|
|
40
46
|
module(GenericMapping: Mapping)
|
|
41
47
|
})
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
4
|
+
import * as Projection$Reventless from "@reventlessdev/reventless-spec/src/types/Projection.res.mjs";
|
|
4
5
|
import * as Message$ReventlessCore from "./Message.res.mjs";
|
|
5
6
|
import * as MapperNto1$ReventlessCore from "./MapperNto1.res.mjs";
|
|
6
|
-
import * as Projection$ReventlessCore from "./Projection.res.mjs";
|
|
7
7
|
|
|
8
8
|
function MakeGenericTargetFromStateTarget(StateTarget) {
|
|
9
9
|
let decode = json => Message$ReventlessCore.decode(json, StateTarget.stateSchema);
|
|
@@ -27,8 +27,8 @@ function Make(Target) {
|
|
|
27
27
|
};
|
|
28
28
|
let mappings = Mappings.mappings.map(M => {
|
|
29
29
|
let acceptedTags = DcbTag$Reventless.extractAllVariantNames(M.sourceEventSchema);
|
|
30
|
-
let
|
|
31
|
-
let project = MapperNto1$ReventlessCore.makeGenericMap(
|
|
30
|
+
let decode = json => Message$ReventlessCore.decodeEvent$p(json, M.SourceId.schema, M.sourceEventSchema);
|
|
31
|
+
let project = MapperNto1$ReventlessCore.makeGenericMap(decode, msg => Projection$Reventless.mapActionId(M.project(msg), M.targetIdToString, M.targetIdFromString));
|
|
32
32
|
return {
|
|
33
33
|
sourceName: M.sourceName,
|
|
34
34
|
acceptedTags: acceptedTags,
|
|
@@ -7,7 +7,7 @@ import * as PluginsReadModelSpec$ReventlessCore from "../plugin/lifecycle/Plugin
|
|
|
7
7
|
|
|
8
8
|
let pluginId = "Platform";
|
|
9
9
|
|
|
10
|
-
let pluginCommands = Plugin_Structure$ReventlessCore.extractCommandDefs(true, undefined, variantName => Api_Naming$ReventlessCore.adminField(PluginSpec$ReventlessCore.name + "_" + variantName), PluginSpec$ReventlessCore.commandAuthorization, PluginSpec$ReventlessCore.commandTransition, undefined, PluginSpec$ReventlessCore.commandSchema);
|
|
10
|
+
let pluginCommands = Plugin_Structure$ReventlessCore.extractCommandDefs(undefined, true, undefined, variantName => Api_Naming$ReventlessCore.adminField(PluginSpec$ReventlessCore.name + "_" + variantName), PluginSpec$ReventlessCore.commandAuthorization, PluginSpec$ReventlessCore.commandTransition, undefined, PluginSpec$ReventlessCore.commandSchema);
|
|
11
11
|
|
|
12
12
|
let pluginAggregate_producedEventTypes = [];
|
|
13
13
|
|
|
@@ -40,6 +40,8 @@ let name = "UiFragments";
|
|
|
40
40
|
|
|
41
41
|
let Id;
|
|
42
42
|
|
|
43
|
+
let Key;
|
|
44
|
+
|
|
43
45
|
let subIdConfig;
|
|
44
46
|
|
|
45
47
|
let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs";
|
|
@@ -54,6 +56,7 @@ export {
|
|
|
54
56
|
stateSchema,
|
|
55
57
|
consumedEventSchema,
|
|
56
58
|
config,
|
|
59
|
+
Key,
|
|
57
60
|
subIdConfig,
|
|
58
61
|
moduleUrl,
|
|
59
62
|
authorization,
|
|
@@ -79,8 +79,26 @@ module Make = (
|
|
|
79
79
|
~opts,
|
|
80
80
|
)
|
|
81
81
|
|
|
82
|
+
// Not awaited, for the reason AutomationSlice_Builder gives: a mapping that
|
|
83
|
+
// commands its own source (Placed → Ship) appends an event this handler's
|
|
84
|
+
// fiber must dequeue, while the fiber is still waiting on the command. The
|
|
85
|
+
// bus then waits on itself, and so does the request that started it. Only
|
|
86
|
+
// the local runtime runs this handler; a deployed one uses its entry point.
|
|
87
|
+
let publishJsonsDetached = jsons => {
|
|
88
|
+
let _ = publishJsons(jsons)->Promise.catch(exn => {
|
|
89
|
+
log.error(
|
|
90
|
+
~comp="EventMapper",
|
|
91
|
+
`detached publish error: ${exn
|
|
92
|
+
->JsExn.fromException
|
|
93
|
+
->Option.flatMap(JsExn.message)
|
|
94
|
+
->Option.getOr("unknown")}`,
|
|
95
|
+
)
|
|
96
|
+
Promise.resolve()
|
|
97
|
+
})
|
|
98
|
+
Promise.resolve()
|
|
99
|
+
}
|
|
82
100
|
module EventCollectorHandler = EventMapper_Callback.MakeEventCollectorHandler({
|
|
83
|
-
let publishJsons =
|
|
101
|
+
let publishJsons = publishJsonsDetached
|
|
84
102
|
let count = count
|
|
85
103
|
let addToCounterTarget = addToCounterTarget
|
|
86
104
|
let commonEventsHandler = CounterHandler.commonEventsHandler
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
4
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
import * as Pulumi from "@pulumi/pulumi";
|
|
6
7
|
import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
8
|
+
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
7
9
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
8
10
|
import * as Counter$ReventlessCore from "../Counter/Counter.res.mjs";
|
|
9
11
|
import * as Component$ReventlessCore from "../Component.res.mjs";
|
|
@@ -53,8 +55,15 @@ function Make(Target) {
|
|
|
53
55
|
kind: "EventMapper",
|
|
54
56
|
name: name
|
|
55
57
|
}, opts);
|
|
58
|
+
let publishJsonsDetached = jsons => {
|
|
59
|
+
Stdlib_Promise.$$catch(publishJsons(jsons), exn => {
|
|
60
|
+
log.error("EventMapper", undefined, `detached publish error: ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown"));
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
});
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
};
|
|
56
65
|
let EventCollectorHandler = EventMapper_Callback$ReventlessCore.MakeEventCollectorHandler({
|
|
57
|
-
publishJsons:
|
|
66
|
+
publishJsons: publishJsonsDetached,
|
|
58
67
|
count: param.count,
|
|
59
68
|
addToCounterTarget: param.addToCounterTarget,
|
|
60
69
|
commonEventsHandler: CounterHandler.commonEventsHandler
|
|
@@ -124,7 +124,17 @@ module Make = (
|
|
|
124
124
|
let (eventType, dataDict) = rawEvent->Message.splitMessage
|
|
125
125
|
switch decoder.decode(~eventType, ~data=dataDict) {
|
|
126
126
|
| Some(event) =>
|
|
127
|
-
let actions = try Projection.project({
|
|
127
|
+
let actions = try Projection.project({
|
|
128
|
+
event,
|
|
129
|
+
meta,
|
|
130
|
+
recordedAt,
|
|
131
|
+
})->Array.map(
|
|
132
|
+
Reventless.Projection.mapActionId(
|
|
133
|
+
_,
|
|
134
|
+
~to_=Spec.Key.toString,
|
|
135
|
+
~from=Spec.Key.makeFromString,
|
|
136
|
+
),
|
|
137
|
+
) catch {
|
|
128
138
|
| exn =>
|
|
129
139
|
let errMsg =
|
|
130
140
|
exn
|
|
@@ -12,6 +12,7 @@ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js
|
|
|
12
12
|
import * as DcbDecode$Reventless from "@reventlessdev/reventless-spec/src/components/DcbDecode.res.mjs";
|
|
13
13
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
14
14
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
15
|
+
import * as Projection$Reventless from "@reventlessdev/reventless-spec/src/types/Projection.res.mjs";
|
|
15
16
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
16
17
|
import * as Component$ReventlessCore from "../Component.res.mjs";
|
|
17
18
|
import * as LogFormat$ReventlessCore from "../../util/LogFormat.res.mjs";
|
|
@@ -108,7 +109,7 @@ function Make(RuntimeEnvironment) {
|
|
|
108
109
|
event: Primitive_option.valFromOption(event),
|
|
109
110
|
meta: meta,
|
|
110
111
|
recordedAt: recordedAt
|
|
111
|
-
});
|
|
112
|
+
}).map(__x => Projection$Reventless.mapActionId(__x, Spec.Key.toString, Spec.Key.makeFromString));
|
|
112
113
|
} catch (raw_exn) {
|
|
113
114
|
let exn$1 = Primitive_exceptions.internalToException(raw_exn);
|
|
114
115
|
let errMsg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn$1), Stdlib_JsExn.message), "unknown");
|
|
@@ -40,7 +40,14 @@ module Make = (
|
|
|
40
40
|
| Some(event) =>
|
|
41
41
|
idx := idx.contents + 1
|
|
42
42
|
let id = raw.tags->Array.get(0)->Option.map(tag => tag.value)->Option.getOr("?")
|
|
43
|
-
let actions =
|
|
43
|
+
let actions =
|
|
44
|
+
Projection.project({event, meta: raw.meta, recordedAt: raw.recordedAt})->Array.map(
|
|
45
|
+
Reventless.Projection.mapActionId(
|
|
46
|
+
_,
|
|
47
|
+
~to_=Spec.Key.toString,
|
|
48
|
+
~from=Spec.Key.makeFromString,
|
|
49
|
+
),
|
|
50
|
+
)
|
|
44
51
|
let actionsStr = LogFormat.actionNames(actions)
|
|
45
52
|
EffectLogger.logInfo(
|
|
46
53
|
~comp,
|
|
@@ -6,6 +6,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
8
8
|
import * as DcbDecode$Reventless from "@reventlessdev/reventless-spec/src/components/DcbDecode.res.mjs";
|
|
9
|
+
import * as Projection$Reventless from "@reventlessdev/reventless-spec/src/types/Projection.res.mjs";
|
|
9
10
|
import * as LogFormat$ReventlessCore from "../../util/LogFormat.res.mjs";
|
|
10
11
|
import * as Projection$ReventlessCore from "../../Projection.res.mjs";
|
|
11
12
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
@@ -32,7 +33,7 @@ function Make(Spec) {
|
|
|
32
33
|
event: event,
|
|
33
34
|
meta: raw.meta,
|
|
34
35
|
recordedAt: raw.recordedAt
|
|
35
|
-
});
|
|
36
|
+
}).map(__x => Projection$Reventless.mapActionId(__x, Spec.Key.toString, Spec.Key.makeFromString));
|
|
36
37
|
let actionsStr = LogFormat$ReventlessCore.actionNames(actions);
|
|
37
38
|
Effect.runSync(EffectLogger$ReventlessCore.logInfo(comp, raw.data, `handling event ` + idx.contents.toString() + `/` + count + `: ` + LogFormat$ReventlessCore.bold(raw.eventType) + `(` + id + `) ` + actionsStr));
|
|
38
39
|
allActions.push(...actions.map(__x => Projection$ReventlessCore.rewriteAction(__x, raw.meta.time, Spec.stateSchema)));
|
|
@@ -587,31 +587,96 @@ let labelFieldsFromStateSchema = (
|
|
|
587
587
|
// Mirrors the command walk for emitted events. Module-level so Platform_Admin,
|
|
588
588
|
// whose components never pass through `make`, derives its defs the same way.
|
|
589
589
|
|
|
590
|
+
/**
|
|
591
|
+
The plugin's views by the identity each is keyed by, which is what lets a typed
|
|
592
|
+
id find the view that lists it without an `@ref`. `report` says where the type
|
|
593
|
+
cannot decide: several views keyed by the identity, none, or an `@ref` naming a
|
|
594
|
+
view keyed by another.
|
|
595
|
+
*/
|
|
596
|
+
type identityViews = {
|
|
597
|
+
viewsByKey: dict<array<string>>,
|
|
598
|
+
viewNames: array<string>,
|
|
599
|
+
report: string => unit,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// The reference a typed id implies, and the check on one it declares.
|
|
603
|
+
let identityReference = (
|
|
604
|
+
identityViews: identityViews,
|
|
605
|
+
~fieldName,
|
|
606
|
+
fieldSchema: S.t<unknown>,
|
|
607
|
+
declared: array<(string, Reventless.Reference.target)>,
|
|
608
|
+
): array<(string, Reventless.Reference.target)> =>
|
|
609
|
+
switch Reventless.Semantic.fieldIdentityKey(fieldSchema) {
|
|
610
|
+
| None => declared
|
|
611
|
+
| Some(key) =>
|
|
612
|
+
let keyed = identityViews.viewsByKey->Dict.get(key)->Option.getOr([])
|
|
613
|
+
switch declared {
|
|
614
|
+
| [] =>
|
|
615
|
+
switch keyed {
|
|
616
|
+
| [view] => [(fieldName, {Reventless.Semantic.entity: view, plugin: None, identity: key})]
|
|
617
|
+
| [] =>
|
|
618
|
+
identityViews.report(
|
|
619
|
+
`${fieldName} is a ${key}, and no view in this plugin is keyed by it, so it references nothing. List the identity in a view, or reference another plugin's with @ref("Plugin.View").`,
|
|
620
|
+
)
|
|
621
|
+
[]
|
|
622
|
+
| several =>
|
|
623
|
+
identityViews.report(
|
|
624
|
+
`${fieldName} is a ${key}, and ${several->Array.join(
|
|
625
|
+
" and ",
|
|
626
|
+
)} are all keyed by it. Say which it references with @ref.`,
|
|
627
|
+
)
|
|
628
|
+
[]
|
|
629
|
+
}
|
|
630
|
+
| _ =>
|
|
631
|
+
declared->Array.forEach(((_, target)) =>
|
|
632
|
+
if (
|
|
633
|
+
target.plugin->Option.isNone &&
|
|
634
|
+
identityViews.viewNames->Array.includes(target.entity) &&
|
|
635
|
+
!(keyed->Array.includes(target.entity))
|
|
636
|
+
) {
|
|
637
|
+
identityViews.report(
|
|
638
|
+
`${fieldName} is a ${key}, and its @ref names ${target.entity}, which is not keyed by it.`,
|
|
639
|
+
)
|
|
640
|
+
}
|
|
641
|
+
)
|
|
642
|
+
declared
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
590
646
|
// A variant's cross-entity references, shared by the command and event walks.
|
|
591
647
|
// `getFieldTarget`, not `getTarget`: an `array<string>` field declares it on the
|
|
592
648
|
// element schema.
|
|
593
|
-
let extractReferences = (
|
|
649
|
+
let extractReferences = (
|
|
650
|
+
~identityViews: option<identityViews>=?,
|
|
651
|
+
// The id a creating command mints. Its identity names the row the command
|
|
652
|
+
// brings into existence, not one it refers to, so nothing is derived for it.
|
|
653
|
+
~minted: option<string>=?,
|
|
654
|
+
properties: dict<S.t<unknown>>,
|
|
655
|
+
): array<Reventless.Plugin.fieldReference> =>
|
|
594
656
|
properties
|
|
595
657
|
->Dict.toArray
|
|
596
|
-
->Array.flatMap(((fieldName, fieldSchema)) =>
|
|
658
|
+
->Array.flatMap(((fieldName, fieldSchema)) => {
|
|
597
659
|
// `collectFieldTargets`, not `getFieldTarget`: a reference declared on a field
|
|
598
660
|
// of a record the field holds (an order line's `productId`) is one this
|
|
599
661
|
// command declares, and it arrives named by its own path. Commands and events
|
|
600
662
|
// both come through here, which is what stops one of them learning about
|
|
601
663
|
// nesting and the other not.
|
|
602
|
-
Reventless.Reference.collectFieldTargets(fieldName, fieldSchema)
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
664
|
+
let declared = Reventless.Reference.collectFieldTargets(fieldName, fieldSchema)
|
|
665
|
+
let targets = switch identityViews {
|
|
666
|
+
| Some(views) if minted != Some(fieldName) =>
|
|
667
|
+
identityReference(views, ~fieldName, fieldSchema, declared)
|
|
668
|
+
| _ => declared
|
|
669
|
+
}
|
|
670
|
+
targets->Array.map(((path, target)): Reventless.Plugin.fieldReference => {
|
|
606
671
|
Reventless.Plugin.fieldName: path,
|
|
607
672
|
entity: target.entity,
|
|
608
673
|
plugin: target.plugin,
|
|
609
674
|
})
|
|
610
|
-
)
|
|
675
|
+
})
|
|
611
676
|
|
|
612
|
-
let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
|
|
677
|
+
let toEventDef = (~identityViews=?, v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
|
|
613
678
|
let mkDef = (~variantName, ~properties) => {
|
|
614
|
-
let references = extractReferences(properties)
|
|
679
|
+
let references = extractReferences(~identityViews?, properties)
|
|
615
680
|
|
|
616
681
|
(
|
|
617
682
|
{
|
|
@@ -640,10 +705,12 @@ let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
|
|
|
640
705
|
}
|
|
641
706
|
}
|
|
642
707
|
|
|
643
|
-
let extractEventDefs = (eventSchema: S.t<unknown>): array<
|
|
708
|
+
let extractEventDefs = (~identityViews=?, eventSchema: S.t<unknown>): array<
|
|
709
|
+
Reventless.Plugin.eventDef,
|
|
710
|
+
> =>
|
|
644
711
|
switch eventSchema {
|
|
645
|
-
| AnyOf({anyOf}) => anyOf->Array.filterMap(toEventDef)
|
|
646
|
-
| _ => toEventDef(eventSchema)->Option.mapOr([], def => [def])
|
|
712
|
+
| AnyOf({anyOf}) => anyOf->Array.filterMap(v => toEventDef(~identityViews?, v))
|
|
713
|
+
| _ => toEventDef(~identityViews?, eventSchema)->Option.mapOr([], def => [def])
|
|
647
714
|
}
|
|
648
715
|
|
|
649
716
|
// Errors walk identically to events; only the def type differs, so a consumer
|
|
@@ -750,6 +817,7 @@ let annotateArgTypes = (schema: JSON.t, argTypes: dict<string>): JSON.t => {
|
|
|
750
817
|
}
|
|
751
818
|
|
|
752
819
|
let toCommandDef = (
|
|
820
|
+
~identityViews: option<identityViews>=?,
|
|
753
821
|
~isAggregate,
|
|
754
822
|
~partitionKey: option<string>=?,
|
|
755
823
|
~mutationFieldFor: string => string,
|
|
@@ -785,7 +853,6 @@ let toCommandDef = (
|
|
|
785
853
|
~variantName,
|
|
786
854
|
properties,
|
|
787
855
|
)
|
|
788
|
-
let references = extractReferences(properties)
|
|
789
856
|
// Evaluated against a synthetic value per constructor, the same shape the
|
|
790
857
|
// resolver builds at call time: a payload-bearing variant compiles to
|
|
791
858
|
// `{TAG, ...}`, a payload-less one to a bare string.
|
|
@@ -849,6 +916,11 @@ let toCommandDef = (
|
|
|
849
916
|
| None => Reventless.Transition.targetState(declared)
|
|
850
917
|
}
|
|
851
918
|
let level = derived->Option.flatMap(d => d.level)->Option.getOr(guessedLevel)
|
|
919
|
+
let references = extractReferences(
|
|
920
|
+
~identityViews?,
|
|
921
|
+
~minted=?level == Reventless.Plugin.Collection ? aggregateIdField : None,
|
|
922
|
+
properties,
|
|
923
|
+
)
|
|
852
924
|
// API-exposed iff the whole command isn't @noApi and this variant
|
|
853
925
|
// isn't in its @noApi-variants set — mirrors the API-generation filter
|
|
854
926
|
// (Plugin_Helpers / PluginBaseFragment). Drives the event-graph API badge.
|
|
@@ -923,6 +995,7 @@ let toCommandDef = (
|
|
|
923
995
|
}
|
|
924
996
|
|
|
925
997
|
let extractCommandDefs = (
|
|
998
|
+
~identityViews: option<identityViews>=?,
|
|
926
999
|
~isAggregate,
|
|
927
1000
|
~partitionKey: option<string>=?,
|
|
928
1001
|
~mutationFieldFor: string => string,
|
|
@@ -935,6 +1008,7 @@ let extractCommandDefs = (
|
|
|
935
1008
|
| AnyOf({anyOf}) =>
|
|
936
1009
|
anyOf->Array.filterMap(v =>
|
|
937
1010
|
toCommandDef(
|
|
1011
|
+
~identityViews?,
|
|
938
1012
|
~isAggregate,
|
|
939
1013
|
~partitionKey?,
|
|
940
1014
|
~mutationFieldFor,
|
|
@@ -948,6 +1022,7 @@ let extractCommandDefs = (
|
|
|
948
1022
|
| _ =>
|
|
949
1023
|
// Single-variant command types compile to a bare Object schema, not a Union.
|
|
950
1024
|
toCommandDef(
|
|
1025
|
+
~identityViews?,
|
|
951
1026
|
~isAggregate,
|
|
952
1027
|
~partitionKey?,
|
|
953
1028
|
~mutationFieldFor,
|
|
@@ -1536,6 +1611,53 @@ let make = (
|
|
|
1536
1611
|
}
|
|
1537
1612
|
let partitionFailures = []
|
|
1538
1613
|
|
|
1614
|
+
// Each view's key as an identity: a read model's `Spec.Id` when it is one, else
|
|
1615
|
+
// the key field's type, else the key field's name, so a view keyed by an untyped
|
|
1616
|
+
// `productId` still lists the `productId` identity.
|
|
1617
|
+
let keyIdentityOf = (stateSchema: S.t<unknown>, idField: option<string>) =>
|
|
1618
|
+
idField->Option.map(field =>
|
|
1619
|
+
switch stateSchema {
|
|
1620
|
+
| Object({properties}) =>
|
|
1621
|
+
properties->Dict.get(field)->Option.flatMap(Reventless.Semantic.identityKey)
|
|
1622
|
+
| _ => None
|
|
1623
|
+
}->Option.getOr(field)
|
|
1624
|
+
)
|
|
1625
|
+
let viewKeys = Array.concat(
|
|
1626
|
+
readModels->Array.mapWithIndex((
|
|
1627
|
+
module(R: ReventlessInfra.ReadModel.T with type api = api and type role = role),
|
|
1628
|
+
i,
|
|
1629
|
+
) => {
|
|
1630
|
+
let def = readModelDefs->Array.getUnsafe(i)
|
|
1631
|
+
let key = switch R.Spec.Id.schema->Reventless.Semantic.identityKey {
|
|
1632
|
+
| Some(_) as declared => declared
|
|
1633
|
+
| None => keyIdentityOf(R.Spec.stateSchema->S.castToUnknown, def.idField)
|
|
1634
|
+
}
|
|
1635
|
+
(R.Spec.name, key)
|
|
1636
|
+
}),
|
|
1637
|
+
stateViewSlices->Array.mapWithIndex((module(SVS: ReventlessInfra.StateViewSlice.T), i) => {
|
|
1638
|
+
let def = stateViewDefs->Array.getUnsafe(i)
|
|
1639
|
+
(SVS.Spec.name, keyIdentityOf(SVS.Spec.stateSchema->S.castToUnknown, def.idField))
|
|
1640
|
+
}),
|
|
1641
|
+
)
|
|
1642
|
+
let viewsByKey: dict<array<string>> = Dict.make()
|
|
1643
|
+
viewKeys->Array.forEach(((view, key)) =>
|
|
1644
|
+
key->Option.forEach(k =>
|
|
1645
|
+
viewsByKey->Dict.set(k, viewsByKey->Dict.get(k)->Option.getOr([])->Array.concat([view]))
|
|
1646
|
+
)
|
|
1647
|
+
)
|
|
1648
|
+
// Warned rather than refused, like the key-field gap: the field still decodes,
|
|
1649
|
+
// it just links nowhere. Once per message, since a field recurs across variants.
|
|
1650
|
+
let reported: Set.t<string> = Set.make()
|
|
1651
|
+
let identityViews = {
|
|
1652
|
+
viewsByKey,
|
|
1653
|
+
viewNames: viewKeys->Array.map(((view, _)) => view),
|
|
1654
|
+
report: message =>
|
|
1655
|
+
if !(reported->Set.has(message)) {
|
|
1656
|
+
reported->Set.add(message)
|
|
1657
|
+
log.warn(~comp="Plugin_Structure", `${name}: ${message}`)
|
|
1658
|
+
},
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1539
1661
|
let stateChangeDefs =
|
|
1540
1662
|
stateChangeSlices->Array.mapWithIndex((module(SCS: ReventlessInfra.StateChangeSlice.T), i) => {
|
|
1541
1663
|
let (_, produced) = scsProduced->Array.getUnsafe(i)
|
|
@@ -1545,6 +1667,7 @@ let make = (
|
|
|
1545
1667
|
{
|
|
1546
1668
|
Reventless.Plugin.name: SCS.Spec.name,
|
|
1547
1669
|
commands: extractCommandDefs(
|
|
1670
|
+
~identityViews,
|
|
1548
1671
|
~isAggregate=false,
|
|
1549
1672
|
~partitionKey=?partitionBySlice->Dict.get(SCS.Spec.name),
|
|
1550
1673
|
~mutationFieldFor=variantName =>
|
|
@@ -1563,7 +1686,7 @@ let make = (
|
|
|
1563
1686
|
consumedEventTypes: consumed,
|
|
1564
1687
|
linkedViews: linkedSvsFor(produced),
|
|
1565
1688
|
consistencyRead: consistencyReadFor(consumed),
|
|
1566
|
-
events: extractEventDefs(SCS.Spec.eventSchema->S.castToUnknown),
|
|
1689
|
+
events: extractEventDefs(~identityViews, SCS.Spec.eventSchema->S.castToUnknown),
|
|
1567
1690
|
errors: extractErrorDefs(SCS.Spec.errorSchema->S.castToUnknown),
|
|
1568
1691
|
chapter: chapterOf(SCS.Spec.name),
|
|
1569
1692
|
}: Reventless.Plugin.writableDef
|
|
@@ -1580,6 +1703,7 @@ let make = (
|
|
|
1580
1703
|
{
|
|
1581
1704
|
Reventless.Plugin.name: A.Spec.name,
|
|
1582
1705
|
commands: extractCommandDefs(
|
|
1706
|
+
~identityViews,
|
|
1583
1707
|
~isAggregate=true,
|
|
1584
1708
|
~mutationFieldFor=variantName =>
|
|
1585
1709
|
Api_Naming.aggregateMutationField(
|
|
@@ -1596,7 +1720,7 @@ let make = (
|
|
|
1596
1720
|
consumedEventTypes: [],
|
|
1597
1721
|
linkedViews: Array.concat(linkedSvsFor(produced), linkedReadModelsFor(A.Spec.name)),
|
|
1598
1722
|
consistencyRead: None,
|
|
1599
|
-
events: extractEventDefs(A.Spec.eventSchema->S.castToUnknown),
|
|
1723
|
+
events: extractEventDefs(~identityViews, A.Spec.eventSchema->S.castToUnknown),
|
|
1600
1724
|
errors: extractErrorDefs(A.Spec.errorSchema->S.castToUnknown),
|
|
1601
1725
|
chapter: chapterOf(A.Spec.name),
|
|
1602
1726
|
}: Reventless.Plugin.writableDef
|