@reventlessdev/reventless-spec 3.0.0-alpha.61
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 +625 -0
- package/LICENSE +202 -0
- package/README.md +114 -0
- package/package.json +34 -0
- package/rescript.json +25 -0
- package/run-generator.mjs +2 -0
- package/src/AnsiStyle.res +40 -0
- package/src/AnsiStyle.res.mjs +54 -0
- package/src/LogPrefix.res +168 -0
- package/src/LogPrefix.res.mjs +148 -0
- package/src/PackageVersion.res +73 -0
- package/src/PackageVersion.res.mjs +81 -0
- package/src/components/Aggregate.res +64 -0
- package/src/components/Aggregate.res.mjs +2 -0
- package/src/components/AutomationSlice.res +279 -0
- package/src/components/AutomationSlice.res.mjs +30 -0
- package/src/components/Counter.res +24 -0
- package/src/components/Counter.res.mjs +2 -0
- package/src/components/DcbDecode.res +128 -0
- package/src/components/DcbDecode.res.mjs +117 -0
- package/src/components/DcbTag.res +1256 -0
- package/src/components/DcbTag.res.mjs +869 -0
- package/src/components/DcbValidation.res +362 -0
- package/src/components/DcbValidation.res.mjs +360 -0
- package/src/components/DisplayName.res +40 -0
- package/src/components/DisplayName.res.mjs +26 -0
- package/src/components/ExtensionPoint.res +27 -0
- package/src/components/ExtensionPoint.res.mjs +2 -0
- package/src/components/InboundTranslationSlice.res +79 -0
- package/src/components/InboundTranslationSlice.res.mjs +2 -0
- package/src/components/OutboundTranslationSlice.res +108 -0
- package/src/components/OutboundTranslationSlice.res.mjs +2 -0
- package/src/components/Plugin.res +373 -0
- package/src/components/Plugin.res.mjs +249 -0
- package/src/components/ReadModel.res +199 -0
- package/src/components/ReadModel.res.mjs +18 -0
- package/src/components/Reference.res +51 -0
- package/src/components/Reference.res.mjs +37 -0
- package/src/components/StateAnnotations.res +65 -0
- package/src/components/StateAnnotations.res.mjs +15 -0
- package/src/components/StateChangeSlice.res +131 -0
- package/src/components/StateChangeSlice.res.mjs +2 -0
- package/src/components/StateViewSlice.res +99 -0
- package/src/components/StateViewSlice.res.mjs +2 -0
- package/src/components/Task.res +62 -0
- package/src/components/Task.res.mjs +2 -0
- package/src/generator/Codegen.res +703 -0
- package/src/generator/Codegen.res.mjs +485 -0
- package/src/generator/Config.res +85 -0
- package/src/generator/Config.res.mjs +78 -0
- package/src/generator/Discovery.res +204 -0
- package/src/generator/Discovery.res.mjs +213 -0
- package/src/generator/Generator_Node.res +49 -0
- package/src/generator/Generator_Node.res.mjs +18 -0
- package/src/generator/Pairing.res +386 -0
- package/src/generator/Pairing.res.mjs +353 -0
- package/src/generator/PluginGenerator.res +53 -0
- package/src/generator/PluginGenerator.res.mjs +70 -0
- package/src/types/Authorization.res +23 -0
- package/src/types/Authorization.res.mjs +33 -0
- package/src/types/Behavior.res +78 -0
- package/src/types/Behavior.res.mjs +2 -0
- package/src/types/EventMapping.res +100 -0
- package/src/types/EventMapping.res.mjs +15 -0
- package/src/types/Handler.res +30 -0
- package/src/types/Handler.res.mjs +2 -0
- package/src/types/Id.res +75 -0
- package/src/types/Id.res.mjs +37 -0
- package/src/types/Identity.res +46 -0
- package/src/types/Identity.res.mjs +51 -0
- package/src/types/Message.res +197 -0
- package/src/types/Message.res.mjs +108 -0
- package/src/types/Projection.res +220 -0
- package/src/types/Projection.res.mjs +44 -0
- package/src/types/QueryEngine.res +123 -0
- package/src/types/QueryEngine.res.mjs +12 -0
- package/src/types/ReadConsistency.res +38 -0
- package/src/types/ReadConsistency.res.mjs +29 -0
- package/src/types/Schedule.res +65 -0
- package/src/types/Schedule.res.mjs +68 -0
- package/src/types/SideEffect.res +45 -0
- package/src/types/SideEffect.res.mjs +2 -0
- package/src/types/StoredEvent.res +46 -0
- package/src/types/StoredEvent.res.mjs +32 -0
- package/src/types/Visibility.res +24 -0
- package/src/types/Visibility.res.mjs +25 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
|
|
7
|
+
import * as AnsiStyle$Reventless from "./AnsiStyle.res.mjs";
|
|
8
|
+
|
|
9
|
+
let currentPluginName = {
|
|
10
|
+
contents: undefined
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let componentPluginRegistry = {
|
|
14
|
+
contents: {}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function registerComponentPlugin(componentName, pluginName) {
|
|
18
|
+
componentPluginRegistry.contents[componentName] = pluginName;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hashStr(s) {
|
|
22
|
+
let h = 0;
|
|
23
|
+
for (let i = 0, i_finish = s.length; i < i_finish; ++i) {
|
|
24
|
+
let code = Stdlib_Option.getOr(Stdlib_String.charCodeAt(s, i), 0);
|
|
25
|
+
h = h + (code * (i + 1 | 0) | 0) | 0;
|
|
26
|
+
}
|
|
27
|
+
return h;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let pluginColors = [
|
|
31
|
+
"\x1b[32m",
|
|
32
|
+
"\x1b[34m",
|
|
33
|
+
"\x1b[35m",
|
|
34
|
+
"\x1b[92m",
|
|
35
|
+
"\x1b[94m",
|
|
36
|
+
"\x1b[95m"
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function pluginColor(name) {
|
|
40
|
+
return pluginColors[Primitive_int.mod_(hashStr(name), pluginColors.length)];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function extractInnerName(comp) {
|
|
44
|
+
let openIdx = comp.indexOf("(");
|
|
45
|
+
let closeIdx = comp.lastIndexOf(")");
|
|
46
|
+
if (openIdx > 0 && closeIdx > (openIdx + 1 | 0)) {
|
|
47
|
+
return comp.slice(openIdx + 1 | 0, closeIdx);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function stripAtVersion(s) {
|
|
52
|
+
let idx = s.indexOf("@");
|
|
53
|
+
if (idx > 0) {
|
|
54
|
+
return s.slice(0, idx);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stripPluginSuffix(s) {
|
|
59
|
+
let suffix = "Plugin";
|
|
60
|
+
if (s.endsWith(suffix) && s.length > suffix.length) {
|
|
61
|
+
return s.slice(0, s.length - suffix.length | 0);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function firstDotSegment(s) {
|
|
66
|
+
let idx = s.indexOf(".");
|
|
67
|
+
if (idx > 0) {
|
|
68
|
+
return s.slice(0, idx);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function lastDotSegment(s) {
|
|
73
|
+
let idx = s.lastIndexOf(".");
|
|
74
|
+
if (idx > 0 && (idx + 1 | 0) < s.length) {
|
|
75
|
+
return s.slice(idx + 1 | 0, s.length);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function lookup(name) {
|
|
80
|
+
return componentPluginRegistry.contents[name];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolvePlugin(comp, param) {
|
|
84
|
+
if (comp === undefined) {
|
|
85
|
+
return currentPluginName.contents;
|
|
86
|
+
}
|
|
87
|
+
let inner = extractInnerName(comp);
|
|
88
|
+
if (inner === undefined) {
|
|
89
|
+
return currentPluginName.contents;
|
|
90
|
+
}
|
|
91
|
+
let p = componentPluginRegistry.contents[inner];
|
|
92
|
+
if (p !== undefined) {
|
|
93
|
+
return p;
|
|
94
|
+
}
|
|
95
|
+
let p$1 = currentPluginName.contents;
|
|
96
|
+
if (p$1 !== undefined) {
|
|
97
|
+
return p$1;
|
|
98
|
+
}
|
|
99
|
+
let candidates = [
|
|
100
|
+
stripAtVersion(inner),
|
|
101
|
+
stripPluginSuffix(inner),
|
|
102
|
+
lastDotSegment(inner),
|
|
103
|
+
firstDotSegment(inner)
|
|
104
|
+
];
|
|
105
|
+
return Stdlib_Array.reduce(candidates, undefined, (acc, candidate) => {
|
|
106
|
+
if (acc !== undefined) {
|
|
107
|
+
return acc;
|
|
108
|
+
} else {
|
|
109
|
+
return Stdlib_Option.flatMap(candidate, lookup);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function fmtPlainPrefix(comp, param) {
|
|
115
|
+
let p = resolvePlugin(comp, undefined);
|
|
116
|
+
let plugin = p !== undefined ? `[` + p + `]` : "";
|
|
117
|
+
let c = comp !== undefined ? `[` + comp + `] ` : "";
|
|
118
|
+
return plugin + c;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function fmtComp(comp, param) {
|
|
122
|
+
if (!AnsiStyle$Reventless.useAnsi()) {
|
|
123
|
+
return fmtPlainPrefix(comp, undefined);
|
|
124
|
+
}
|
|
125
|
+
let p = resolvePlugin(comp, undefined);
|
|
126
|
+
let plugin = p !== undefined ? pluginColor(p) + AnsiStyle$Reventless.bold(`[` + p + `]`) : "";
|
|
127
|
+
let c = comp !== undefined ? AnsiStyle$Reventless.bold(`[` + comp + `]`) + ` ` : "";
|
|
128
|
+
return plugin + c;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export {
|
|
132
|
+
currentPluginName,
|
|
133
|
+
componentPluginRegistry,
|
|
134
|
+
registerComponentPlugin,
|
|
135
|
+
hashStr,
|
|
136
|
+
pluginColors,
|
|
137
|
+
pluginColor,
|
|
138
|
+
extractInnerName,
|
|
139
|
+
stripAtVersion,
|
|
140
|
+
stripPluginSuffix,
|
|
141
|
+
firstDotSegment,
|
|
142
|
+
lastDotSegment,
|
|
143
|
+
lookup,
|
|
144
|
+
resolvePlugin,
|
|
145
|
+
fmtPlainPrefix,
|
|
146
|
+
fmtComp,
|
|
147
|
+
}
|
|
148
|
+
/* AnsiStyle-Reventless Not a pure module */
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
@module("node:fs") external readFileSync: (string, string) => string = "readFileSync"
|
|
2
|
+
@module("node:fs") external existsSync: string => bool = "existsSync"
|
|
3
|
+
@module("node:url") external fileURLToPath: string => string = "fileURLToPath"
|
|
4
|
+
@module("node:path") external dirname: string => string = "dirname"
|
|
5
|
+
@module("node:path") external join: (string, string) => string = "join"
|
|
6
|
+
|
|
7
|
+
let readVersion = (path: string) =>
|
|
8
|
+
readFileSync(path, "utf8")
|
|
9
|
+
->JSON.parseOrThrow
|
|
10
|
+
->JSON.Decode.object
|
|
11
|
+
->Option.flatMap(o => o->Dict.get("version"))
|
|
12
|
+
->Option.flatMap(JSON.Decode.string)
|
|
13
|
+
->Option.getOr("0.0.0")
|
|
14
|
+
|
|
15
|
+
let fromCwd = () => readVersion("./package.json")
|
|
16
|
+
|
|
17
|
+
let findVersion = dir => {
|
|
18
|
+
let rec find = dir => {
|
|
19
|
+
let candidate = join(dir, "package.json")
|
|
20
|
+
if existsSync(candidate) {
|
|
21
|
+
readVersion(candidate)
|
|
22
|
+
} else {
|
|
23
|
+
let parent = dirname(dir)
|
|
24
|
+
if parent == dir {
|
|
25
|
+
"0.0.0"
|
|
26
|
+
} else {
|
|
27
|
+
find(parent)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
find(dir)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let fromModuleUrl = (url: string) => findVersion(url->fileURLToPath->dirname)
|
|
35
|
+
|
|
36
|
+
/** Detect the caller's file via V8 Structured Stack Trace API and read the
|
|
37
|
+
version from the nearest package.json. Skips frames from framework packages
|
|
38
|
+
(reventless-spec, reventless-core, reventless-infra, reventless-local,
|
|
39
|
+
reventless-aws, reventless-interop). Falls back to CWD's package.json. */
|
|
40
|
+
let callerFile: unit => option<string> = %raw(`
|
|
41
|
+
function() {
|
|
42
|
+
var frameworkMarkers = [
|
|
43
|
+
'/reventless/reventless-',
|
|
44
|
+
'/@reventlessdev/reventless-',
|
|
45
|
+
];
|
|
46
|
+
var oldPrepare = Error.prepareStackTrace;
|
|
47
|
+
Error.prepareStackTrace = function(_, stack) { return stack; };
|
|
48
|
+
var err = new Error();
|
|
49
|
+
var stack = err.stack;
|
|
50
|
+
Error.prepareStackTrace = oldPrepare;
|
|
51
|
+
for (var i = 0; i < stack.length; i++) {
|
|
52
|
+
var file = stack[i].getFileName();
|
|
53
|
+
if (!file) continue;
|
|
54
|
+
var isFramework = false;
|
|
55
|
+
for (var j = 0; j < frameworkMarkers.length; j++) {
|
|
56
|
+
if (file.indexOf(frameworkMarkers[j]) !== -1) { isFramework = true; break; }
|
|
57
|
+
}
|
|
58
|
+
if (!isFramework) return file;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
`)
|
|
63
|
+
|
|
64
|
+
let fromCaller = () =>
|
|
65
|
+
switch callerFile() {
|
|
66
|
+
| Some(file) =>
|
|
67
|
+
if file->String.startsWith("file://") {
|
|
68
|
+
findVersion(file->fileURLToPath->dirname)
|
|
69
|
+
} else {
|
|
70
|
+
findVersion(file->dirname)
|
|
71
|
+
}
|
|
72
|
+
| None => fromCwd()
|
|
73
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
4
|
+
import * as Nodeurl from "node:url";
|
|
5
|
+
import * as Nodepath from "node:path";
|
|
6
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
7
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
|
+
|
|
9
|
+
function readVersion(path) {
|
|
10
|
+
return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(Nodefs.readFileSync(path, "utf8"))), o => o["version"]), Stdlib_JSON.Decode.string), "0.0.0");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function fromCwd() {
|
|
14
|
+
return readVersion("./package.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findVersion(dir) {
|
|
18
|
+
let _dir = dir;
|
|
19
|
+
while (true) {
|
|
20
|
+
let dir$1 = _dir;
|
|
21
|
+
let candidate = Nodepath.join(dir$1, "package.json");
|
|
22
|
+
if (Nodefs.existsSync(candidate)) {
|
|
23
|
+
return readVersion(candidate);
|
|
24
|
+
}
|
|
25
|
+
let parent = Nodepath.dirname(dir$1);
|
|
26
|
+
if (parent === dir$1) {
|
|
27
|
+
return "0.0.0";
|
|
28
|
+
}
|
|
29
|
+
_dir = parent;
|
|
30
|
+
continue;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fromModuleUrl(url) {
|
|
35
|
+
return findVersion(Nodepath.dirname(Nodeurl.fileURLToPath(url)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let callerFile = (function() {
|
|
39
|
+
var frameworkMarkers = [
|
|
40
|
+
'/reventless/reventless-',
|
|
41
|
+
'/@reventlessdev/reventless-',
|
|
42
|
+
];
|
|
43
|
+
var oldPrepare = Error.prepareStackTrace;
|
|
44
|
+
Error.prepareStackTrace = function(_, stack) { return stack; };
|
|
45
|
+
var err = new Error();
|
|
46
|
+
var stack = err.stack;
|
|
47
|
+
Error.prepareStackTrace = oldPrepare;
|
|
48
|
+
for (var i = 0; i < stack.length; i++) {
|
|
49
|
+
var file = stack[i].getFileName();
|
|
50
|
+
if (!file) continue;
|
|
51
|
+
var isFramework = false;
|
|
52
|
+
for (var j = 0; j < frameworkMarkers.length; j++) {
|
|
53
|
+
if (file.indexOf(frameworkMarkers[j]) !== -1) { isFramework = true; break; }
|
|
54
|
+
}
|
|
55
|
+
if (!isFramework) return file;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function fromCaller() {
|
|
61
|
+
let file = callerFile();
|
|
62
|
+
if (file !== undefined) {
|
|
63
|
+
if (file.startsWith("file://")) {
|
|
64
|
+
return findVersion(Nodepath.dirname(Nodeurl.fileURLToPath(file)));
|
|
65
|
+
} else {
|
|
66
|
+
return findVersion(Nodepath.dirname(file));
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
return readVersion("./package.json");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
readVersion,
|
|
75
|
+
fromCwd,
|
|
76
|
+
findVersion,
|
|
77
|
+
fromModuleUrl,
|
|
78
|
+
callerFile,
|
|
79
|
+
fromCaller,
|
|
80
|
+
}
|
|
81
|
+
/* node:fs Not a pure module */
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Module type for an aggregate's identity and schema specification.
|
|
3
|
+
|
|
4
|
+
Every aggregate defines a `Spec` module satisfying this type. The `Spec` is
|
|
5
|
+
used by `Platform.Aggregate.Make` and by `Behavior.T` to fix the aggregate's
|
|
6
|
+
command, event, and error types.
|
|
7
|
+
|
|
8
|
+
@example
|
|
9
|
+
```rescript
|
|
10
|
+
// Category.res
|
|
11
|
+
module Id = Id.String
|
|
12
|
+
let name = "Category"
|
|
13
|
+
|
|
14
|
+
@schema
|
|
15
|
+
type command =
|
|
16
|
+
| AddCategory({categoryId: string, name: string})
|
|
17
|
+
| RenameCategory({categoryId: string, name: string})
|
|
18
|
+
| ArchiveCategory({categoryId: string})
|
|
19
|
+
|
|
20
|
+
@schema
|
|
21
|
+
type event =
|
|
22
|
+
| CategoryAdded({categoryId: string, name: string})
|
|
23
|
+
| CategoryRenamed({categoryId: string, name: string})
|
|
24
|
+
| CategoryArchived({categoryId: string})
|
|
25
|
+
|
|
26
|
+
@schema
|
|
27
|
+
type error =
|
|
28
|
+
| CategoryAlreadyExists
|
|
29
|
+
| CategoryNotFound
|
|
30
|
+
| CategoryAlreadyArchived
|
|
31
|
+
```
|
|
32
|
+
*/
|
|
33
|
+
module type Spec = {
|
|
34
|
+
module Id: Id.T
|
|
35
|
+
|
|
36
|
+
/** Logical aggregate name used as a prefix for infrastructure resource names. */
|
|
37
|
+
let name: string
|
|
38
|
+
|
|
39
|
+
/** Commands this aggregate accepts. Must carry `@schema`. */
|
|
40
|
+
@schema
|
|
41
|
+
type command
|
|
42
|
+
|
|
43
|
+
/** Events this aggregate emits. Must carry `@schema`. */
|
|
44
|
+
@schema
|
|
45
|
+
type event
|
|
46
|
+
|
|
47
|
+
/** Business rule violation errors. Must carry `@schema`. */
|
|
48
|
+
@schema
|
|
49
|
+
type error
|
|
50
|
+
|
|
51
|
+
/** Sury schema for the command type — generated automatically by `@schema`. */
|
|
52
|
+
let commandSchema: S.t<command>
|
|
53
|
+
|
|
54
|
+
/** File URL of this module (`import.meta.url`). Used by AWS builders to derive
|
|
55
|
+
the npm specifier for runtime dynamic imports. */
|
|
56
|
+
let moduleUrl: string
|
|
57
|
+
|
|
58
|
+
/** Authorization rule evaluated at the GraphQL resolver entry before any
|
|
59
|
+
command is dispatched. Auto-injected by `@@reventless.spec` and on
|
|
60
|
+
structurally-detected inline spec modules — defaults to
|
|
61
|
+
`AllowAuthenticated`; override at the file/module level with
|
|
62
|
+
`@@reventless.authorize(<rule>)`. */
|
|
63
|
+
let commandAuthorization: command => Authorization.permission
|
|
64
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Module types for a DCB automation slice (TODO List Pattern).
|
|
3
|
+
|
|
4
|
+
An `AutomationSlice` listens to the shared `DcbEventLog` event topic, accumulates
|
|
5
|
+
pending work items (TODO list), and processes them exactly once by issuing commands.
|
|
6
|
+
Completion events mark TODO items as done, closing the automation loop.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
Event(s) → TODO List (read model) → Processor → Command → Event(s)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Plan 02 splits the merged spec into two module types:
|
|
13
|
+
|
|
14
|
+
- `Spec` — types, identity, schemas, configuration constants. Per D2,
|
|
15
|
+
`todoItem` lives here (the persisted TODO state has a schema queried for
|
|
16
|
+
sweeping pending items — externally-observable, like ReadModel state).
|
|
17
|
+
- `Automation` — the three pure functions `collect`, `resolve`, `process`.
|
|
18
|
+
|
|
19
|
+
@example
|
|
20
|
+
```rescript
|
|
21
|
+
// ShipOrder.res
|
|
22
|
+
let name = "ShipOrder"
|
|
23
|
+
|
|
24
|
+
@schema type todoItem = {orderId: string, shippingAddress: string}
|
|
25
|
+
@schema type command = CreateShipment({orderId: @s.matches(DcbTag.string) string, address: string})
|
|
26
|
+
|
|
27
|
+
let collect = event => switch event {
|
|
28
|
+
| OrderPlaced({orderId, shippingAddress}) =>
|
|
29
|
+
[(orderId, {orderId, shippingAddress})]
|
|
30
|
+
| _ => []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let resolve = event => switch event {
|
|
34
|
+
| ShipmentCreated({orderId}) => Some(orderId)
|
|
35
|
+
| _ => None
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let process = (id, item) =>
|
|
39
|
+
Some((id, CreateShipment({orderId: item.orderId, address: item.shippingAddress})))
|
|
40
|
+
|
|
41
|
+
let maxRetries = 3
|
|
42
|
+
let heartbeatInterval = 60
|
|
43
|
+
```
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
The lean Spec for an AutomationSlice — types, identity, schemas, sweep config.
|
|
48
|
+
|
|
49
|
+
Plan 04 dropped `consumedEvent` from the Spec — the framework now derives the
|
|
50
|
+
consumed-event set from each per-source Mapping's `sourceEventSchema`, so a
|
|
51
|
+
manually-declared union is no longer necessary. Single-source slices have one
|
|
52
|
+
`Mapping` whose `sourceEventSchema` IS the consumed-event schema; multi-source
|
|
53
|
+
slices have several Mappings and the framework walks all of them.
|
|
54
|
+
*/
|
|
55
|
+
module type Spec = {
|
|
56
|
+
/** Logical name of this automation slice (used as a component prefix). */
|
|
57
|
+
let name: string
|
|
58
|
+
let moduleUrl: string
|
|
59
|
+
|
|
60
|
+
/** The TODO item state — what data is accumulated for each pending work item. Must carry `@schema`. */
|
|
61
|
+
@schema
|
|
62
|
+
type todoItem
|
|
63
|
+
|
|
64
|
+
/** The command type produced by the processor. Must carry `@schema`. */
|
|
65
|
+
@schema
|
|
66
|
+
type command
|
|
67
|
+
|
|
68
|
+
/** Maximum number of retries for a failed processing attempt. */
|
|
69
|
+
let maxRetries: int
|
|
70
|
+
|
|
71
|
+
/** Heartbeat interval in seconds for sweeping pending/failed items. */
|
|
72
|
+
let heartbeatInterval: int
|
|
73
|
+
|
|
74
|
+
/** Name of the aggregate or StateChangeSlice that receives the produced command. */
|
|
75
|
+
let targetName: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Plan 04: Mixed-source mappings ───────────────────────────────────────────
|
|
79
|
+
//
|
|
80
|
+
// An AutomationSlice can declare per-source mappings that consume Aggregate
|
|
81
|
+
// events alongside the slice's own DcbEventLog events. Each mapping carries its
|
|
82
|
+
// own `collect`/`resolve` (over the source's `event` type) plus a `toTags`
|
|
83
|
+
// validation step run before each command publish. `process` remains shared in
|
|
84
|
+
// the slice's `Automation` module since it operates on `todoItem` regardless of
|
|
85
|
+
// the originating source.
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
Ambient deployment context plumbed to every mapping function.
|
|
89
|
+
|
|
90
|
+
`Plugin_Builder` constructs this from its existing `~environment`, `~name` and
|
|
91
|
+
related parameters. `collect` and `resolve` use it to complete partial event
|
|
92
|
+
payloads (e.g. when a DCB tag field is supplied by deployment metadata rather
|
|
93
|
+
than the source event); `toTags` uses it to validate that all tag fields on the
|
|
94
|
+
target command will be populated.
|
|
95
|
+
|
|
96
|
+
The record is intentionally narrow — extending it is a deliberate framework
|
|
97
|
+
change, not an open-dict escape hatch. Runtime registry lookups are out of
|
|
98
|
+
scope (see Plan 04 / decision 3).
|
|
99
|
+
*/
|
|
100
|
+
type context = {
|
|
101
|
+
environment: string,
|
|
102
|
+
platformName: string,
|
|
103
|
+
pluginName: string,
|
|
104
|
+
sliceName: string,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
A compiled per-source-to-AutomationSlice mapping.
|
|
109
|
+
|
|
110
|
+
Created by `AutomationSlice.Mapping.Make(Source, Target, Impl)`. The mapping
|
|
111
|
+
binds `sourceEvent` (decode target) plus `collect`/`resolve` to the slice's
|
|
112
|
+
`todoItem` type. `process` remains in the slice's `Automation` module — it is
|
|
113
|
+
source-agnostic.
|
|
114
|
+
|
|
115
|
+
Validation belongs upstream of the mapping: `collect` should return `[]` for
|
|
116
|
+
events that shouldn't enter the TODO list, and the command schema's
|
|
117
|
+
`@compositePartitionTag` / `@s.matches(DcbTag.string)` annotations enforce
|
|
118
|
+
DCB-tag invariants at encode time (failures mark the item Failed → retry).
|
|
119
|
+
*/
|
|
120
|
+
module type Mapping = {
|
|
121
|
+
module SourceId: Id.T
|
|
122
|
+
@schema
|
|
123
|
+
type sourceEvent
|
|
124
|
+
type todoItem
|
|
125
|
+
type command
|
|
126
|
+
let sourceEventSchema: S.t<sourceEvent>
|
|
127
|
+
let sourceName: string
|
|
128
|
+
let collect: (sourceEvent, context) => array<(string, todoItem)>
|
|
129
|
+
let resolve: sourceEvent => option<string>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
The Automation — the source-agnostic processing function.
|
|
134
|
+
|
|
135
|
+
Plan 04 moved `collect` and `resolve` to per-source `Mapping` modules (where
|
|
136
|
+
they switch over the source's own `event` type). `process` stays here because
|
|
137
|
+
it operates on `todoItem`, which is uniform across sources.
|
|
138
|
+
|
|
139
|
+
The "close PPX gaps" plan merged the previous `Mappings` shape into this type:
|
|
140
|
+
the same module that exposes `process` also exposes the per-source `mappings`
|
|
141
|
+
array and the inner `module type Mapping`. This collapses the Plan 04 3-arg
|
|
142
|
+
`AutomationSlice.Make(Spec, Automation, Mappings)` into a 2-arg
|
|
143
|
+
`Make(Spec, Automation)` and lets the merged `_Automation.res` file (process +
|
|
144
|
+
per-source `Mapping.Make` + `let mappings`) live as a single source file.
|
|
145
|
+
*/
|
|
146
|
+
module type Automation = {
|
|
147
|
+
module Spec: Spec
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
Process: given a pending TODO item, produce a command.
|
|
151
|
+
The processor calls this for each pending item. Returns the target ID and command.
|
|
152
|
+
May return `None` to skip processing (e.g., wait for more data).
|
|
153
|
+
*/
|
|
154
|
+
let process: (string, Spec.todoItem) => option<(string, Spec.command)>
|
|
155
|
+
|
|
156
|
+
/** File URL of this Automation module (`import.meta.url`). */
|
|
157
|
+
let moduleUrl: string
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
The per-source `Mapping` type — substituted onto the slice's `Spec`. The
|
|
161
|
+
PPX-injected `module M = Reventless.AutomationSlice.Mappings.Make(Spec)`
|
|
162
|
+
+ `module type Mapping = M.Mapping` produces exactly this shape; legacy
|
|
163
|
+
3-file files re-export it via `module type Mapping = <Stem>_Mappings.Mapping`.
|
|
164
|
+
*/
|
|
165
|
+
module type Mapping = Mapping
|
|
166
|
+
with type todoItem = Spec.todoItem
|
|
167
|
+
and type command = Spec.command
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
The per-source mappings registered with this slice. Each entry is a first-class
|
|
171
|
+
module satisfying `Mapping`. The framework walks these to discover source
|
|
172
|
+
topics and dispatch decoded events to the matching `collect`/`resolve`.
|
|
173
|
+
*/
|
|
174
|
+
let mappings: array<module(Mapping)>
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
The implementation passed to `AutomationSlice.Mapping.Make`. Caller-supplied
|
|
179
|
+
`collect`/`resolve` are bound to source/target types via destructive
|
|
180
|
+
substitution in the functor.
|
|
181
|
+
*/
|
|
182
|
+
module type MappingImpl = {
|
|
183
|
+
type sourceEvent
|
|
184
|
+
type todoItem
|
|
185
|
+
type command
|
|
186
|
+
let collect: (sourceEvent, context) => array<(string, todoItem)>
|
|
187
|
+
let resolve: sourceEvent => option<string>
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
A collection of `Mapping` modules for a single AutomationSlice target.
|
|
192
|
+
|
|
193
|
+
Pass a `Mappings` module to the AutomationSlice plugin builder to register all
|
|
194
|
+
source-to-slice mappings. The shape mirrors `Projection.Mappings` for
|
|
195
|
+
ReadModels.
|
|
196
|
+
|
|
197
|
+
@example
|
|
198
|
+
```rescript
|
|
199
|
+
// AutoFulfillment_Mappings.res
|
|
200
|
+
module M = Reventless.AutomationSlice.Mappings.Make(AutoFulfillmentSpec)
|
|
201
|
+
module type Mapping = M.Mapping
|
|
202
|
+
|
|
203
|
+
let mappings: array<module(Mapping)> = [
|
|
204
|
+
module(FromOrderShipped),
|
|
205
|
+
module(FromStockReserved),
|
|
206
|
+
]
|
|
207
|
+
```
|
|
208
|
+
*/
|
|
209
|
+
module type Mappings = {
|
|
210
|
+
module Target: Spec
|
|
211
|
+
module type Mapping = Mapping
|
|
212
|
+
with type todoItem = Target.todoItem
|
|
213
|
+
and type command = Target.command
|
|
214
|
+
let moduleUrl: string
|
|
215
|
+
let mappings: array<module(Mapping)>
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
Builds an `AutomationSlice.Mapping` from a `Source`, the slice's `Spec`, and a
|
|
220
|
+
`MappingImpl`.
|
|
221
|
+
|
|
222
|
+
@example
|
|
223
|
+
```rescript
|
|
224
|
+
// AutoFulfillment/FromOrderShipped.res
|
|
225
|
+
module FromOrderShipped = Reventless.AutomationSlice.Mapping.Make(
|
|
226
|
+
OrderSpec, // Source: Aggregate spec module
|
|
227
|
+
AutoFulfillmentSpec, // Target: this slice's spec
|
|
228
|
+
{
|
|
229
|
+
let collect = (event, _ctx) =>
|
|
230
|
+
switch event {
|
|
231
|
+
| OrderSpec.OrderShipped({orderId, productId}) =>
|
|
232
|
+
[(orderId ++ ":" ++ productId, {AutoFulfillmentSpec.orderId, productId})]
|
|
233
|
+
| _ => []
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let resolve = event =>
|
|
237
|
+
switch event {
|
|
238
|
+
| OrderSpec.OrderRefunded({orderId, productId}) =>
|
|
239
|
+
Some(orderId ++ ":" ++ productId)
|
|
240
|
+
| _ => None
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
```
|
|
245
|
+
*/
|
|
246
|
+
module Mapping = {
|
|
247
|
+
module Make = (
|
|
248
|
+
Source: Projection.Source,
|
|
249
|
+
Target: Spec,
|
|
250
|
+
Impl: MappingImpl
|
|
251
|
+
with type sourceEvent := Source.event
|
|
252
|
+
and type todoItem := Target.todoItem
|
|
253
|
+
and type command := Target.command,
|
|
254
|
+
): (
|
|
255
|
+
Mapping
|
|
256
|
+
with type sourceEvent = Source.event
|
|
257
|
+
and type todoItem = Target.todoItem
|
|
258
|
+
and type command = Target.command
|
|
259
|
+
and module SourceId = Source.Id
|
|
260
|
+
) => {
|
|
261
|
+
module SourceId = Source.Id
|
|
262
|
+
@schema
|
|
263
|
+
type sourceEvent = Source.event
|
|
264
|
+
type todoItem = Target.todoItem
|
|
265
|
+
type command = Target.command
|
|
266
|
+
let sourceName = Source.name
|
|
267
|
+
let collect = Impl.collect
|
|
268
|
+
let resolve = Impl.resolve
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module Mappings = {
|
|
273
|
+
module Make = (Target: Spec) => {
|
|
274
|
+
module type Mapping = Mapping
|
|
275
|
+
with type todoItem = Target.todoItem
|
|
276
|
+
and type command = Target.command
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
function Make(Source) {
|
|
5
|
+
return Target => (Impl => ({
|
|
6
|
+
SourceId: Source.Id,
|
|
7
|
+
sourceEventSchema: Source.eventSchema,
|
|
8
|
+
sourceName: Source.name,
|
|
9
|
+
collect: Impl.collect,
|
|
10
|
+
resolve: Impl.resolve
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let Mapping = {
|
|
15
|
+
Make: Make
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function Make$1(Target) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let Mappings = {
|
|
23
|
+
Make: Make$1
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
Mapping,
|
|
28
|
+
Mappings,
|
|
29
|
+
}
|
|
30
|
+
/* No side effect */
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
A named counter (e.g. `"ProductCount"`). Used as a key in the counts database.
|
|
3
|
+
Multiple counters can exist within the same `Counter` component.
|
|
4
|
+
*/
|
|
5
|
+
type counterId = string
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
An opaque reference string that de-duplicates counter increments.
|
|
9
|
+
|
|
10
|
+
Each unique `reference` is counted at most once per `counterId`.
|
|
11
|
+
Typically set to the event's message ID to make increments idempotent.
|
|
12
|
+
*/
|
|
13
|
+
type reference = string
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
A counter threshold target.
|
|
17
|
+
|
|
18
|
+
When the count for `counterId` reaches `target`, the event mapping
|
|
19
|
+
framework fires the associated command.
|
|
20
|
+
*/
|
|
21
|
+
type counterTarget = {
|
|
22
|
+
counterId: counterId,
|
|
23
|
+
target: int,
|
|
24
|
+
}
|