@reventlessdev/reventless-core 3.0.0-alpha.266 → 3.0.0-alpha.267
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 +19 -0
- package/package.json +6 -6
- 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 +131 -15
- package/src/plugin/component/Plugin_Structure.res.mjs +108 -24
- 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 +99 -0
- package/tests/plugin/IdentityReferenceTest.res.mjs +169 -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,25 @@
|
|
|
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.267 (2026-09-21)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **core:** an event mapping that commands its own source no longer hangs ([d30ce6b](https://github.com/ReventlessDev/reventless-core/commit/d30ce6b360707d933732fa2c90aba49c03dda006))
|
|
11
|
+
* feat(spec)!: typed projection keys ([fc6a018](https://github.com/ReventlessDev/reventless-core/commit/fc6a01811ba788f3289e84a435a09f77fa627e1a))
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **core:** references derived from identities, and identity checks ([7cb1376](https://github.com/ReventlessDev/reventless-core/commit/7cb1376e56cdb4e4d5ab5954cd97fd37fd3ba258))
|
|
15
|
+
|
|
16
|
+
### BREAKING CHANGES
|
|
17
|
+
|
|
18
|
+
* a projection that keys a row by a payload `string`, or uses
|
|
19
|
+
the envelope id as a `string`, converts explicitly with
|
|
20
|
+
`Target.Id.makeFromString` / `Source.Id.toString`. `StateChangeSlice.Spec` no
|
|
21
|
+
longer requires `module Id`.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
# 3.0.0-alpha.266 (2026-09-21)
|
|
7
26
|
|
|
8
27
|
### 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.267",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -29,21 +29,21 @@
|
|
|
29
29
|
"sury": "11.0.0-rc.2",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.33",
|
|
32
|
-
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
33
32
|
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.12",
|
|
34
|
-
"@reventlessdev/rescript-
|
|
33
|
+
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
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
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.168",
|
|
39
40
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.37",
|
|
40
|
-
"@reventlessdev/reventless-
|
|
41
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.139"
|
|
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,92 @@ 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
|
+
properties: dict<S.t<unknown>>,
|
|
652
|
+
): array<Reventless.Plugin.fieldReference> =>
|
|
594
653
|
properties
|
|
595
654
|
->Dict.toArray
|
|
596
|
-
->Array.flatMap(((fieldName, fieldSchema)) =>
|
|
655
|
+
->Array.flatMap(((fieldName, fieldSchema)) => {
|
|
597
656
|
// `collectFieldTargets`, not `getFieldTarget`: a reference declared on a field
|
|
598
657
|
// of a record the field holds (an order line's `productId`) is one this
|
|
599
658
|
// command declares, and it arrives named by its own path. Commands and events
|
|
600
659
|
// both come through here, which is what stops one of them learning about
|
|
601
660
|
// nesting and the other not.
|
|
602
|
-
Reventless.Reference.collectFieldTargets(fieldName, fieldSchema)
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
661
|
+
let declared = Reventless.Reference.collectFieldTargets(fieldName, fieldSchema)
|
|
662
|
+
let targets = switch identityViews {
|
|
663
|
+
| Some(views) => identityReference(views, ~fieldName, fieldSchema, declared)
|
|
664
|
+
| None => declared
|
|
665
|
+
}
|
|
666
|
+
targets->Array.map(((path, target)): Reventless.Plugin.fieldReference => {
|
|
606
667
|
Reventless.Plugin.fieldName: path,
|
|
607
668
|
entity: target.entity,
|
|
608
669
|
plugin: target.plugin,
|
|
609
670
|
})
|
|
610
|
-
)
|
|
671
|
+
})
|
|
611
672
|
|
|
612
|
-
let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
|
|
673
|
+
let toEventDef = (~identityViews=?, v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
|
|
613
674
|
let mkDef = (~variantName, ~properties) => {
|
|
614
|
-
let references = extractReferences(properties)
|
|
675
|
+
let references = extractReferences(~identityViews?, properties)
|
|
615
676
|
|
|
616
677
|
(
|
|
617
678
|
{
|
|
@@ -640,10 +701,12 @@ let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
|
|
|
640
701
|
}
|
|
641
702
|
}
|
|
642
703
|
|
|
643
|
-
let extractEventDefs = (eventSchema: S.t<unknown>): array<
|
|
704
|
+
let extractEventDefs = (~identityViews=?, eventSchema: S.t<unknown>): array<
|
|
705
|
+
Reventless.Plugin.eventDef,
|
|
706
|
+
> =>
|
|
644
707
|
switch eventSchema {
|
|
645
|
-
| AnyOf({anyOf}) => anyOf->Array.filterMap(toEventDef)
|
|
646
|
-
| _ => toEventDef(eventSchema)->Option.mapOr([], def => [def])
|
|
708
|
+
| AnyOf({anyOf}) => anyOf->Array.filterMap(v => toEventDef(~identityViews?, v))
|
|
709
|
+
| _ => toEventDef(~identityViews?, eventSchema)->Option.mapOr([], def => [def])
|
|
647
710
|
}
|
|
648
711
|
|
|
649
712
|
// Errors walk identically to events; only the def type differs, so a consumer
|
|
@@ -750,6 +813,7 @@ let annotateArgTypes = (schema: JSON.t, argTypes: dict<string>): JSON.t => {
|
|
|
750
813
|
}
|
|
751
814
|
|
|
752
815
|
let toCommandDef = (
|
|
816
|
+
~identityViews: option<identityViews>=?,
|
|
753
817
|
~isAggregate,
|
|
754
818
|
~partitionKey: option<string>=?,
|
|
755
819
|
~mutationFieldFor: string => string,
|
|
@@ -785,7 +849,7 @@ let toCommandDef = (
|
|
|
785
849
|
~variantName,
|
|
786
850
|
properties,
|
|
787
851
|
)
|
|
788
|
-
let references = extractReferences(properties)
|
|
852
|
+
let references = extractReferences(~identityViews?, properties)
|
|
789
853
|
// Evaluated against a synthetic value per constructor, the same shape the
|
|
790
854
|
// resolver builds at call time: a payload-bearing variant compiles to
|
|
791
855
|
// `{TAG, ...}`, a payload-less one to a bare string.
|
|
@@ -923,6 +987,7 @@ let toCommandDef = (
|
|
|
923
987
|
}
|
|
924
988
|
|
|
925
989
|
let extractCommandDefs = (
|
|
990
|
+
~identityViews: option<identityViews>=?,
|
|
926
991
|
~isAggregate,
|
|
927
992
|
~partitionKey: option<string>=?,
|
|
928
993
|
~mutationFieldFor: string => string,
|
|
@@ -935,6 +1000,7 @@ let extractCommandDefs = (
|
|
|
935
1000
|
| AnyOf({anyOf}) =>
|
|
936
1001
|
anyOf->Array.filterMap(v =>
|
|
937
1002
|
toCommandDef(
|
|
1003
|
+
~identityViews?,
|
|
938
1004
|
~isAggregate,
|
|
939
1005
|
~partitionKey?,
|
|
940
1006
|
~mutationFieldFor,
|
|
@@ -948,6 +1014,7 @@ let extractCommandDefs = (
|
|
|
948
1014
|
| _ =>
|
|
949
1015
|
// Single-variant command types compile to a bare Object schema, not a Union.
|
|
950
1016
|
toCommandDef(
|
|
1017
|
+
~identityViews?,
|
|
951
1018
|
~isAggregate,
|
|
952
1019
|
~partitionKey?,
|
|
953
1020
|
~mutationFieldFor,
|
|
@@ -1536,6 +1603,53 @@ let make = (
|
|
|
1536
1603
|
}
|
|
1537
1604
|
let partitionFailures = []
|
|
1538
1605
|
|
|
1606
|
+
// Each view's key as an identity: a read model's `Spec.Id` when it is one, else
|
|
1607
|
+
// the key field's type, else the key field's name, so a view keyed by an untyped
|
|
1608
|
+
// `productId` still lists the `productId` identity.
|
|
1609
|
+
let keyIdentityOf = (stateSchema: S.t<unknown>, idField: option<string>) =>
|
|
1610
|
+
idField->Option.map(field =>
|
|
1611
|
+
switch stateSchema {
|
|
1612
|
+
| Object({properties}) =>
|
|
1613
|
+
properties->Dict.get(field)->Option.flatMap(Reventless.Semantic.identityKey)
|
|
1614
|
+
| _ => None
|
|
1615
|
+
}->Option.getOr(field)
|
|
1616
|
+
)
|
|
1617
|
+
let viewKeys = Array.concat(
|
|
1618
|
+
readModels->Array.mapWithIndex((
|
|
1619
|
+
module(R: ReventlessInfra.ReadModel.T with type api = api and type role = role),
|
|
1620
|
+
i,
|
|
1621
|
+
) => {
|
|
1622
|
+
let def = readModelDefs->Array.getUnsafe(i)
|
|
1623
|
+
let key = switch R.Spec.Id.schema->Reventless.Semantic.identityKey {
|
|
1624
|
+
| Some(_) as declared => declared
|
|
1625
|
+
| None => keyIdentityOf(R.Spec.stateSchema->S.castToUnknown, def.idField)
|
|
1626
|
+
}
|
|
1627
|
+
(R.Spec.name, key)
|
|
1628
|
+
}),
|
|
1629
|
+
stateViewSlices->Array.mapWithIndex((module(SVS: ReventlessInfra.StateViewSlice.T), i) => {
|
|
1630
|
+
let def = stateViewDefs->Array.getUnsafe(i)
|
|
1631
|
+
(SVS.Spec.name, keyIdentityOf(SVS.Spec.stateSchema->S.castToUnknown, def.idField))
|
|
1632
|
+
}),
|
|
1633
|
+
)
|
|
1634
|
+
let viewsByKey: dict<array<string>> = Dict.make()
|
|
1635
|
+
viewKeys->Array.forEach(((view, key)) =>
|
|
1636
|
+
key->Option.forEach(k =>
|
|
1637
|
+
viewsByKey->Dict.set(k, viewsByKey->Dict.get(k)->Option.getOr([])->Array.concat([view]))
|
|
1638
|
+
)
|
|
1639
|
+
)
|
|
1640
|
+
// Warned rather than refused, like the key-field gap: the field still decodes,
|
|
1641
|
+
// it just links nowhere. Once per message, since a field recurs across variants.
|
|
1642
|
+
let reported: Set.t<string> = Set.make()
|
|
1643
|
+
let identityViews = {
|
|
1644
|
+
viewsByKey,
|
|
1645
|
+
viewNames: viewKeys->Array.map(((view, _)) => view),
|
|
1646
|
+
report: message =>
|
|
1647
|
+
if !(reported->Set.has(message)) {
|
|
1648
|
+
reported->Set.add(message)
|
|
1649
|
+
log.warn(~comp="Plugin_Structure", `${name}: ${message}`)
|
|
1650
|
+
},
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1539
1653
|
let stateChangeDefs =
|
|
1540
1654
|
stateChangeSlices->Array.mapWithIndex((module(SCS: ReventlessInfra.StateChangeSlice.T), i) => {
|
|
1541
1655
|
let (_, produced) = scsProduced->Array.getUnsafe(i)
|
|
@@ -1545,6 +1659,7 @@ let make = (
|
|
|
1545
1659
|
{
|
|
1546
1660
|
Reventless.Plugin.name: SCS.Spec.name,
|
|
1547
1661
|
commands: extractCommandDefs(
|
|
1662
|
+
~identityViews,
|
|
1548
1663
|
~isAggregate=false,
|
|
1549
1664
|
~partitionKey=?partitionBySlice->Dict.get(SCS.Spec.name),
|
|
1550
1665
|
~mutationFieldFor=variantName =>
|
|
@@ -1563,7 +1678,7 @@ let make = (
|
|
|
1563
1678
|
consumedEventTypes: consumed,
|
|
1564
1679
|
linkedViews: linkedSvsFor(produced),
|
|
1565
1680
|
consistencyRead: consistencyReadFor(consumed),
|
|
1566
|
-
events: extractEventDefs(SCS.Spec.eventSchema->S.castToUnknown),
|
|
1681
|
+
events: extractEventDefs(~identityViews, SCS.Spec.eventSchema->S.castToUnknown),
|
|
1567
1682
|
errors: extractErrorDefs(SCS.Spec.errorSchema->S.castToUnknown),
|
|
1568
1683
|
chapter: chapterOf(SCS.Spec.name),
|
|
1569
1684
|
}: Reventless.Plugin.writableDef
|
|
@@ -1580,6 +1695,7 @@ let make = (
|
|
|
1580
1695
|
{
|
|
1581
1696
|
Reventless.Plugin.name: A.Spec.name,
|
|
1582
1697
|
commands: extractCommandDefs(
|
|
1698
|
+
~identityViews,
|
|
1583
1699
|
~isAggregate=true,
|
|
1584
1700
|
~mutationFieldFor=variantName =>
|
|
1585
1701
|
Api_Naming.aggregateMutationField(
|
|
@@ -1596,7 +1712,7 @@ let make = (
|
|
|
1596
1712
|
consumedEventTypes: [],
|
|
1597
1713
|
linkedViews: Array.concat(linkedSvsFor(produced), linkedReadModelsFor(A.Spec.name)),
|
|
1598
1714
|
consistencyRead: None,
|
|
1599
|
-
events: extractEventDefs(A.Spec.eventSchema->S.castToUnknown),
|
|
1715
|
+
events: extractEventDefs(~identityViews, A.Spec.eventSchema->S.castToUnknown),
|
|
1600
1716
|
errors: extractErrorDefs(A.Spec.errorSchema->S.castToUnknown),
|
|
1601
1717
|
chapter: chapterOf(A.Spec.name),
|
|
1602
1718
|
}: Reventless.Plugin.writableDef
|