@reventlessdev/reventless-core 3.0.0-alpha.261 → 3.0.0-alpha.263
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 +31 -0
- package/package.json +9 -9
- package/src/adapter/Secrets/Secrets_Node_Backend.res +60 -0
- package/src/adapter/Secrets/Secrets_Node_Backend.res.mjs +69 -0
- 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/Secrets_Node_BackendTest.res +141 -0
- package/tests/Secrets_Node_BackendTest.res.mjs +152 -0
- package/tests/adapter/RuntimeExtensionHookReachTest.res.mjs +1 -1
- package/tests/admin/Platform_Admin_StructureTest.res +12 -3
- package/tests/admin/Platform_Admin_StructureTest.res.mjs +4 -3
- 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
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,37 @@
|
|
|
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.263 (2026-09-16)
|
|
7
|
+
|
|
8
|
+
* feat(spec)!: infer the DCB partition key per slice, and drop @partitionTag where inference agrees ([6c10161](https://github.com/ReventlessDev/reventless-core/commit/6c101611d438beea4bc9175ae8ea0ea1f6f7493b))
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **core:** fail the build when an extension cannot find its slice's partition key ([cf2b093](https://github.com/ReventlessDev/reventless-core/commit/cf2b093dd5dc7cf9425cdac919cd3910fc4aeefd))
|
|
12
|
+
* **spec:** let a slice's chapter break a partition-key tie ([2b9ae77](https://github.com/ReventlessDev/reventless-core/commit/2b9ae77e98eb42177d2bdd4977bc62bb7c7911aa))
|
|
13
|
+
|
|
14
|
+
### BREAKING CHANGES
|
|
15
|
+
|
|
16
|
+
* DcbTag.derivePartitionTag is removed; a boundary whose
|
|
17
|
+
partition cannot be inferred no longer deploys.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# 3.0.0-alpha.262 (2026-09-11)
|
|
22
|
+
|
|
23
|
+
* feat(spec)!: a secret is a capability, so a test can know it in advance ([71507c6](https://github.com/ReventlessDev/reventless-core/commit/71507c6459b49178746f00c5040cb479d7fdade8))
|
|
24
|
+
### Features
|
|
25
|
+
|
|
26
|
+
* **aws:** the first administrator is one command, not the console ([5f1a846](https://github.com/ReventlessDev/reventless-core/commit/5f1a84603b21a9feb691a08a148751c6c6e8eca8))
|
|
27
|
+
|
|
28
|
+
### BREAKING CHANGES
|
|
29
|
+
|
|
30
|
+
* Capabilities.t gains a secrets member. A platform that builds
|
|
31
|
+
the record literally no longer compiles until it supplies one;
|
|
32
|
+
Capabilities.none.secrets is the refusing value. Code that spreads
|
|
33
|
+
Capabilities.none is unaffected.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
6
37
|
# 3.0.0-alpha.261 (2026-09-09)
|
|
7
38
|
|
|
8
39
|
**Note:** Version bump only for package @reventlessdev/reventless-core
|
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.263",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -29,16 +29,16 @@
|
|
|
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-fast-csv": "2.0.0-alpha.
|
|
33
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.11",
|
|
34
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
32
|
+
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.12",
|
|
35
33
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
36
|
-
"@reventlessdev/rescript-
|
|
34
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
35
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.12",
|
|
36
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.21",
|
|
37
|
+
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.12",
|
|
37
38
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
38
|
-
"@reventlessdev/
|
|
39
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
40
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.
|
|
41
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.134"
|
|
39
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.136",
|
|
40
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.164",
|
|
41
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.37"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// The secret source both platforms use: `node:crypto`, which AWS Lambda and the
|
|
2
|
+
// local runtime both have. One implementation rather than two, for the same
|
|
3
|
+
// reason the log messaging transport is shared — a second copy is one edit away
|
|
4
|
+
// from differing on the part that matters.
|
|
5
|
+
|
|
6
|
+
let _bytes = (count: int): array<int> => {
|
|
7
|
+
// Read back as hex and re-split, because the binding treats a Buffer as opaque
|
|
8
|
+
// — it is only ever stringified or fed to an HMAC. Two hex characters is one
|
|
9
|
+
// byte, so this is a decode rather than a reinterpretation.
|
|
10
|
+
let hex = NodeCrypto.randomBytes(count)->NodeCrypto.bufferToString("hex")
|
|
11
|
+
Array.fromInitializer(~length=count, i =>
|
|
12
|
+
hex->String.substring(~start=i * 2, ~end=i * 2 + 2)->Int.fromString(~radix=16)->Option.getOr(0)
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
Whether a random byte may be folded into an alphabet of `size`, or must be drawn
|
|
18
|
+
again.
|
|
19
|
+
|
|
20
|
+
🚨 **`byte % size` is not uniform, and this predicate is the whole defence.** 256
|
|
21
|
+
is not a multiple of 10, so bytes 250–255 fold onto digits 0–5 and make those six
|
|
22
|
+
about four percent likelier each than the other four. An attacker guessing a
|
|
23
|
+
numeric code guesses the likelier half first. Discarding everything at or above
|
|
24
|
+
the largest whole multiple of `size` costs a few extra draws and makes every
|
|
25
|
+
character equally likely.
|
|
26
|
+
|
|
27
|
+
Separate and named because the skew it prevents is far too small to catch by
|
|
28
|
+
sampling the output — it is checked directly, over every byte value.
|
|
29
|
+
*/
|
|
30
|
+
let accepts = (~byte: int, ~size: int): bool => byte < 256 - mod(256, size)
|
|
31
|
+
|
|
32
|
+
let _draw = (~length: int, ~from: string): string => {
|
|
33
|
+
let size = from->String.length
|
|
34
|
+
let out = []
|
|
35
|
+
while out->Array.length < length {
|
|
36
|
+
// Over-draw: most bytes are kept, so one round is nearly always enough, and
|
|
37
|
+
// a short round simply goes again.
|
|
38
|
+
_bytes((length - out->Array.length) * 2 + 8)->Array.forEach(byte =>
|
|
39
|
+
if accepts(~byte, ~size) && out->Array.length < length {
|
|
40
|
+
out->Array.push(from->String.charAt(mod(byte, size)))
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
out->Array.join("")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
The provider both platforms pass.
|
|
49
|
+
|
|
50
|
+
`length` is refused rather than clamped when it is not positive: a caller asking
|
|
51
|
+
for a zero-length token has a bug, and answering `""` would hand back something
|
|
52
|
+
that compares equal to nothing and settles every challenge.
|
|
53
|
+
*/
|
|
54
|
+
let provider: Reventless.Secrets.t = {
|
|
55
|
+
randomToken: (~length, ~alphabet) =>
|
|
56
|
+
length <= 0
|
|
57
|
+
? Error(Unavailable(`a token needs a positive length, not ${length->Int.toString}`))
|
|
58
|
+
: Ok(_draw(~length, ~from=Reventless.Secrets.characters(alphabet))),
|
|
59
|
+
hash: input => Ok(NodeCrypto.sha256Hex(input)),
|
|
60
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as NodeCrypto from "@reventlessdev/rescript-node/src/NodeCrypto.res.mjs";
|
|
4
|
+
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
5
|
+
import * as Nodecrypto from "node:crypto";
|
|
6
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
7
|
+
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
8
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
9
|
+
import * as Secrets$Reventless from "@reventlessdev/reventless-spec/src/semantic/Secrets.res.mjs";
|
|
10
|
+
|
|
11
|
+
function _bytes(count) {
|
|
12
|
+
let hex = Nodecrypto.randomBytes(count).toString("hex");
|
|
13
|
+
return Stdlib_Array.fromInitializer(count, i => Stdlib_Option.getOr(Stdlib_Int.fromString(hex.substring((i << 1), (i << 1) + 2 | 0), 16), 0));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function accepts(byte, size) {
|
|
17
|
+
return byte < (256 - Primitive_int.mod_(256, size) | 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function _draw(length, from) {
|
|
21
|
+
let size = from.length;
|
|
22
|
+
let out = [];
|
|
23
|
+
while (out.length < length) {
|
|
24
|
+
_bytes(((length - out.length | 0) << 1) + 8 | 0).forEach(byte => {
|
|
25
|
+
if (accepts(byte, size) && out.length < length) {
|
|
26
|
+
out.push(from.charAt(Primitive_int.mod_(byte, size)));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
return out.join("");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function provider_randomToken(length, alphabet) {
|
|
35
|
+
if (length <= 0) {
|
|
36
|
+
return {
|
|
37
|
+
TAG: "Error",
|
|
38
|
+
_0: {
|
|
39
|
+
TAG: "Unavailable",
|
|
40
|
+
_0: `a token needs a positive length, not ` + length.toString()
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
} else {
|
|
44
|
+
return {
|
|
45
|
+
TAG: "Ok",
|
|
46
|
+
_0: _draw(length, Secrets$Reventless.characters(alphabet))
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function provider_hash(input) {
|
|
52
|
+
return {
|
|
53
|
+
TAG: "Ok",
|
|
54
|
+
_0: NodeCrypto.sha256Hex(input)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let provider = {
|
|
59
|
+
randomToken: provider_randomToken,
|
|
60
|
+
hash: provider_hash
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
_bytes,
|
|
65
|
+
accepts,
|
|
66
|
+
_draw,
|
|
67
|
+
provider,
|
|
68
|
+
}
|
|
69
|
+
/* NodeCrypto Not a pure module */
|
|
@@ -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, 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(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
|
|
|
@@ -61,7 +61,7 @@ function Make(Spec) {
|
|
|
61
61
|
let fields = registeredFields !== undefined ? (
|
|
62
62
|
registeredFields.length !== 0 ? registeredFields : []
|
|
63
63
|
) : [];
|
|
64
|
-
let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, publishJsonsAndWait, Spec.name, Spec.commandSchema, "Aggregate", undefined);
|
|
64
|
+
let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, publishJsonsAndWait, Spec.name, Spec.commandSchema, "Aggregate", undefined, undefined);
|
|
65
65
|
fields.forEach(field => bindHandler(field, generateCommand));
|
|
66
66
|
} else {
|
|
67
67
|
let resources = Component$ReventlessCore.outputs(commandTopic).resources;
|
|
@@ -7,10 +7,10 @@ The `id` field is the envelope id for the produced command message:
|
|
|
7
7
|
- For **Aggregates**, it is required — the resolver populates it from the
|
|
8
8
|
GraphQL `id: ID!` argument injected into the mutation SDL.
|
|
9
9
|
- For **DCB StateChangeSlices**, it is optional. When the resolver does not
|
|
10
|
-
supply one, `makeGenerateCommand`
|
|
11
|
-
|
|
12
|
-
the
|
|
13
|
-
|
|
10
|
+
supply one, `makeGenerateCommand` takes the envelope id from the command's
|
|
11
|
+
value of the handling slice's partition key — the key `Dcb_Builder` derives
|
|
12
|
+
for storage and the fence — so envelope id and slice partition key share one
|
|
13
|
+
source of truth.
|
|
14
14
|
*/
|
|
15
15
|
type arguments = {id: string}
|
|
16
16
|
type meta = {ip: array<string>, user: string, info: string}
|
|
@@ -112,6 +112,8 @@ let makeGenerateCommand = (
|
|
|
112
112
|
~commandSchema: S.t<unknown>,
|
|
113
113
|
~componentKind: commandComponentKind,
|
|
114
114
|
~stripIdFromParams: bool=true,
|
|
115
|
+
// The handling slice's partition; the envelope id is its value on the command.
|
|
116
|
+
~partitionTag: option<Reventless.DcbTag.derivedPartitionTag>=?,
|
|
115
117
|
): CommandGenerator.commandGenerator => {
|
|
116
118
|
(payload: CommandGenerator.payload) =>
|
|
117
119
|
Effect.sync(() => {
|
|
@@ -157,9 +159,11 @@ let makeGenerateCommand = (
|
|
|
157
159
|
}
|
|
158
160
|
// Resolve the envelope id.
|
|
159
161
|
// - Aggregates always rely on the resolver-supplied id (id: ID! arg in SDL).
|
|
160
|
-
// - DCB StateChangeSlices fall back to
|
|
161
|
-
// partition
|
|
162
|
-
// @compositePartitionTag work end-to-end without per-call-site
|
|
162
|
+
// - DCB StateChangeSlices fall back to the value of the handling slice's
|
|
163
|
+
// partition field on the command when the resolver didn't supply one —
|
|
164
|
+
// what makes @compositePartitionTag work end-to-end without per-call-site
|
|
165
|
+
// id stuffing. A command without that field, or a generator that was not
|
|
166
|
+
// given the partition (the permissive `S.json` fallback), gets `""`.
|
|
163
167
|
let suppliedId = payload.arguments.id
|
|
164
168
|
let id = switch componentKind {
|
|
165
169
|
| Aggregate => suppliedId
|
|
@@ -167,23 +171,12 @@ let makeGenerateCommand = (
|
|
|
167
171
|
switch (suppliedId->Obj.magic: Nullable.t<string>)->Nullable.toOption {
|
|
168
172
|
| Some(idValue) => idValue
|
|
169
173
|
| None =>
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
| Composite(spec) =>
|
|
177
|
-
let tags = Reventless.DcbTag.extractTagsFromJson(commandSchema, commandJson)
|
|
178
|
-
Reventless.DcbTag.getCompositePartitionKeyValue(tags, spec)
|
|
179
|
-
}
|
|
180
|
-
} catch {
|
|
181
|
-
// Permissive schemas (e.g. S.json on the AppSync direct-invocation path)
|
|
182
|
-
// have no tagged fields — derivePartitionTag throws. Fall back to "" so
|
|
183
|
-
// existing transports keep their pre-fix behavior; the slice decoder
|
|
184
|
-
// surfaces the missing id downstream as it did before.
|
|
185
|
-
| _ => ""
|
|
186
|
-
}
|
|
174
|
+
partitionTag->Option.mapOr("", pt =>
|
|
175
|
+
Reventless.DcbTag.extractTagsFromJson(
|
|
176
|
+
commandSchema,
|
|
177
|
+
commandJson,
|
|
178
|
+
)->Reventless.DcbTag.partitionValueOfTags(pt)
|
|
179
|
+
)
|
|
187
180
|
}
|
|
188
181
|
}
|
|
189
182
|
(meta, commandJson, id)
|
|
@@ -59,7 +59,7 @@ function dropNullArguments(obj) {
|
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, commandSchema, componentKind, $staropt$star) {
|
|
62
|
+
function makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, commandSchema, componentKind, $staropt$star, partitionTag) {
|
|
63
63
|
return payload => {
|
|
64
64
|
let stripIdFromParams = $staropt$star !== undefined ? $staropt$star : true;
|
|
65
65
|
return Effect.flatMap(Effect.tap(Effect.sync(() => {
|
|
@@ -85,28 +85,7 @@ function makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, com
|
|
|
85
85
|
]].concat(params)) : commandStr;
|
|
86
86
|
let suppliedId = payload.arguments.id;
|
|
87
87
|
let id;
|
|
88
|
-
|
|
89
|
-
id = suppliedId;
|
|
90
|
-
} else {
|
|
91
|
-
try {
|
|
92
|
-
let derived = DcbTag$Reventless.derivePartitionTag([[
|
|
93
|
-
serviceName,
|
|
94
|
-
"",
|
|
95
|
-
commandSchema
|
|
96
|
-
]]);
|
|
97
|
-
if (derived.TAG === "Simple") {
|
|
98
|
-
let tags = DcbTag$Reventless.extractTagsFromJson(commandSchema, commandJson);
|
|
99
|
-
id = Stdlib_Option.getOr(DcbTag$Reventless.getPartitionTagValue([{
|
|
100
|
-
tags: tags
|
|
101
|
-
}], derived._0), "");
|
|
102
|
-
} else {
|
|
103
|
-
let tags$1 = DcbTag$Reventless.extractTagsFromJson(commandSchema, commandJson);
|
|
104
|
-
id = DcbTag$Reventless.getCompositePartitionKeyValue(tags$1, derived._0);
|
|
105
|
-
}
|
|
106
|
-
} catch (exn) {
|
|
107
|
-
id = "";
|
|
108
|
-
}
|
|
109
|
-
}
|
|
88
|
+
id = componentKind === "Aggregate" || !(suppliedId == null) ? suppliedId : Stdlib_Option.mapOr(partitionTag, "", pt => DcbTag$Reventless.partitionValueOfTags(DcbTag$Reventless.extractTagsFromJson(commandSchema, commandJson), pt));
|
|
110
89
|
return [
|
|
111
90
|
meta,
|
|
112
91
|
commandJson,
|
|
@@ -181,7 +160,7 @@ function makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, com
|
|
|
181
160
|
|
|
182
161
|
function Make(Spec) {
|
|
183
162
|
return AggregateSpec => {
|
|
184
|
-
let generateCommand = makeGenerateCommand(Spec.publishJsons, Spec.publishJsonsAndWait, AggregateSpec.name, AggregateSpec.commandSchema, "Aggregate", undefined);
|
|
163
|
+
let generateCommand = makeGenerateCommand(Spec.publishJsons, Spec.publishJsonsAndWait, AggregateSpec.name, AggregateSpec.commandSchema, "Aggregate", undefined, undefined);
|
|
185
164
|
return {
|
|
186
165
|
generateCommand: generateCommand
|
|
187
166
|
};
|
|
@@ -1,26 +1,5 @@
|
|
|
1
|
-
// Converts a file:// module URL to a path relative to the project root (located by lerna.json),
|
|
2
|
-
// resolving symlinks so workspace packages show their source location rather than node_modules.
|
|
3
|
-
// Falls back to the bare URL string on any error.
|
|
4
1
|
let log = Logger.fromEnv()
|
|
5
2
|
|
|
6
|
-
let toRelativePath: string => string = %raw(`function toRelativePath(moduleUrl) {
|
|
7
|
-
try {
|
|
8
|
-
const fs = process.getBuiltinModule('node:fs');
|
|
9
|
-
const path = process.getBuiltinModule('node:path');
|
|
10
|
-
const { fileURLToPath } = process.getBuiltinModule('node:url');
|
|
11
|
-
const builderReal = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
12
|
-
const sliceReal = fs.realpathSync(fileURLToPath(moduleUrl));
|
|
13
|
-
let dir = path.dirname(builderReal);
|
|
14
|
-
while (dir !== path.dirname(dir)) {
|
|
15
|
-
if (fs.existsSync(path.join(dir, 'lerna.json'))) break;
|
|
16
|
-
dir = path.dirname(dir);
|
|
17
|
-
}
|
|
18
|
-
return path.relative(dir, sliceReal).replace(/\.mjs$/, '.res');
|
|
19
|
-
} catch(e) {
|
|
20
|
-
return moduleUrl.replace('file://', '');
|
|
21
|
-
}
|
|
22
|
-
}`)
|
|
23
|
-
|
|
24
3
|
// The tag of one compiled variant arm: `{TAG: "<Name>", …}` for a payload-bearing
|
|
25
4
|
// constructor, a bare string const for a payload-less one.
|
|
26
5
|
let variantTag = (variant: S.t<unknown>) =>
|
|
@@ -182,12 +161,6 @@ module Make = (
|
|
|
182
161
|
Sc.Spec.name,
|
|
183
162
|
Sc.Spec.eventSchema->S.castToUnknown,
|
|
184
163
|
))
|
|
185
|
-
let producedNamed =
|
|
186
|
-
stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => (
|
|
187
|
-
Sc.Spec.name,
|
|
188
|
-
toRelativePath(Sc.Spec.moduleUrl),
|
|
189
|
-
Sc.Spec.eventSchema->S.castToUnknown,
|
|
190
|
-
))
|
|
191
164
|
let consumed =
|
|
192
165
|
stateChangeSlices
|
|
193
166
|
->Array.map((module(Sc: StateChangeSlice.T)) => (
|
|
@@ -253,7 +226,39 @@ module Make = (
|
|
|
253
226
|
indexes
|
|
254
227
|
}
|
|
255
228
|
|
|
256
|
-
let
|
|
229
|
+
let sliceSchemas = stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => {
|
|
230
|
+
Reventless.DcbTag.name: Sc.Spec.name,
|
|
231
|
+
commandSchema: Sc.Spec.commandSchema->S.castToUnknown,
|
|
232
|
+
consumedEventSchema: Sc.Spec.consumedEventSchema->S.castToUnknown,
|
|
233
|
+
eventSchema: Sc.Spec.eventSchema->S.castToUnknown,
|
|
234
|
+
moduleUrl: Sc.Spec.moduleUrl,
|
|
235
|
+
})
|
|
236
|
+
let inferenceShapes = sliceSchemas->Array.map(Reventless.DcbTag.sliceShape)
|
|
237
|
+
|
|
238
|
+
// A `@partitionTag` inference contradicts would steer storage, fence and read
|
|
239
|
+
// scope to a key the slice only references, so it stops the build. One that
|
|
240
|
+
// inference agrees with is noise, and says so.
|
|
241
|
+
let hintIssues = Reventless.DcbValidation.validatePartitionHintsVsInference(
|
|
242
|
+
~shapes=inferenceShapes,
|
|
243
|
+
)
|
|
244
|
+
hintIssues.redundancies->Array.forEach(e =>
|
|
245
|
+
log.info(~comp="Dcb_Builder", `DCB partition (${e.sliceName}): ${e.message}`)
|
|
246
|
+
)
|
|
247
|
+
if hintIssues.contradictions->Array.length > 0 {
|
|
248
|
+
let detail =
|
|
249
|
+
hintIssues.contradictions
|
|
250
|
+
->Array.map(e => `${e.sliceName}: ${e.message}`)
|
|
251
|
+
->Array.join(" | ")
|
|
252
|
+
log.error(~comp="Dcb_Builder", `DCB partition contradiction (${name}): ${detail}`)
|
|
253
|
+
JsError.throwWithMessage(`DCB partition contradiction (${name}): ${detail}`)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Where each event is stored — and so its fence, and each command's envelope
|
|
257
|
+
// id. Throws, naming the slice, when a partition cannot be inferred.
|
|
258
|
+
let boundaryPartition = Reventless.DcbTag.deriveBoundaryPartition(sliceSchemas)
|
|
259
|
+
let partitionTag = boundaryPartition.partitionTag
|
|
260
|
+
let partitionTagOf = sliceName =>
|
|
261
|
+
boundaryPartition->Reventless.DcbTag.slicePartitionTag(sliceName)
|
|
257
262
|
|
|
258
263
|
// Tag keys declared `@crossPartition` across the produced event schemas.
|
|
259
264
|
// Derived once at build time (the scope is a property of the tag key, and
|
|
@@ -325,15 +330,6 @@ module Make = (
|
|
|
325
330
|
// preserved (generalised rule 3). Safety guard: if any slice's partition is
|
|
326
331
|
// ambiguous we keep the annotated values for the whole boundary rather than
|
|
327
332
|
// thread a partial inference. See docs/plans/done/dcb-tag-scope-inference.md.
|
|
328
|
-
let inferenceShapes =
|
|
329
|
-
stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) =>
|
|
330
|
-
Reventless.DcbTag.sliceShapeFromSchemas(
|
|
331
|
-
~name=Sc.Spec.name,
|
|
332
|
-
~commandSchema=Sc.Spec.commandSchema->S.castToUnknown,
|
|
333
|
-
~consumedEventSchema=Sc.Spec.consumedEventSchema->S.castToUnknown,
|
|
334
|
-
~eventSchema=Sc.Spec.eventSchema->S.castToUnknown,
|
|
335
|
-
)
|
|
336
|
-
)
|
|
337
333
|
let inferred = Reventless.DcbScopeInference.infer(inferenceShapes)
|
|
338
334
|
if inferred.crossPartitionTagKeys != crossPartitionTagKeys {
|
|
339
335
|
log.info(
|
|
@@ -388,14 +384,7 @@ module Make = (
|
|
|
388
384
|
// (DcbCommandTopicEntryPoint.mjs) route through `DcbTag.deriveEffectiveScope`
|
|
389
385
|
// so the runtime decision query cannot drift from this scope — see
|
|
390
386
|
// docs/analysis/dcb-runtime-scope-annotation-drift.md.
|
|
391
|
-
let effective = Reventless.DcbTag.deriveEffectiveScope(
|
|
392
|
-
stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => {
|
|
393
|
-
Reventless.DcbTag.name: Sc.Spec.name,
|
|
394
|
-
commandSchema: Sc.Spec.commandSchema->S.castToUnknown,
|
|
395
|
-
consumedEventSchema: Sc.Spec.consumedEventSchema->S.castToUnknown,
|
|
396
|
-
eventSchema: Sc.Spec.eventSchema->S.castToUnknown,
|
|
397
|
-
}),
|
|
398
|
-
)
|
|
387
|
+
let effective = Reventless.DcbTag.deriveEffectiveScope(sliceSchemas)
|
|
399
388
|
let effectiveCrossPartitionTagKeys = effective.crossPartitionTagKeys
|
|
400
389
|
let effectiveTagKeysByEventType = effective.tagKeysByEventType
|
|
401
390
|
|
|
@@ -487,6 +476,7 @@ module Make = (
|
|
|
487
476
|
~publishJsons,
|
|
488
477
|
~tagKeysByEventType=effectiveTagKeysByEventType,
|
|
489
478
|
~crossPartitionTagKeys=effectiveCrossPartitionTagKeys,
|
|
479
|
+
~partitionTag=?partitionTagOf(StateChangeSlice.Spec.name),
|
|
490
480
|
~runtime=?componentRuntime->Dict.get(StateChangeSlice.Spec.name),
|
|
491
481
|
~opts,
|
|
492
482
|
)
|
|
@@ -507,6 +497,7 @@ module Make = (
|
|
|
507
497
|
~publishJsons=asyncPublishJsons,
|
|
508
498
|
~tagKeysByEventType=effectiveTagKeysByEventType,
|
|
509
499
|
~crossPartitionTagKeys=effectiveCrossPartitionTagKeys,
|
|
500
|
+
~partitionTag=?partitionTagOf(StateChangeSlice.Spec.name),
|
|
510
501
|
~runtime=?componentRuntime->Dict.get(StateChangeSlice.Spec.name),
|
|
511
502
|
~opts,
|
|
512
503
|
)
|
|
@@ -572,6 +563,7 @@ module Make = (
|
|
|
572
563
|
~commandSchema=S.Spec.commandSchema->Obj.magic,
|
|
573
564
|
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
574
565
|
~stripIdFromParams=false,
|
|
566
|
+
~partitionTag=?partitionTagOf(S.Spec.name),
|
|
575
567
|
)
|
|
576
568
|
Api_Naming.sliceMutationFields(
|
|
577
569
|
~plugin=apiNamePrefix,
|
|
@@ -602,6 +594,7 @@ module Make = (
|
|
|
602
594
|
~commandSchema=S.Spec.commandSchema->Obj.magic,
|
|
603
595
|
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
604
596
|
~stripIdFromParams=false,
|
|
597
|
+
~partitionTag=?partitionTagOf(S.Spec.name),
|
|
605
598
|
)
|
|
606
599
|
Api_Naming.sliceMutationFields(
|
|
607
600
|
~plugin=apiNamePrefix,
|
|
@@ -813,7 +806,7 @@ module Make = (
|
|
|
813
806
|
`Two DCB slices in "${name}" both declare the command "${tag}". ` ++
|
|
814
807
|
"Owner stamping resolves a command by that name alone, so it cannot " ++ "tell them apart. Rename one of the constructors.",
|
|
815
808
|
)
|
|
816
|
-
| None => byTag->Dict.set(tag, commandSchema)
|
|
809
|
+
| None => byTag->Dict.set(tag, (commandSchema, partitionTagOf(S.Spec.name)))
|
|
817
810
|
}
|
|
818
811
|
})
|
|
819
812
|
}
|
|
@@ -825,7 +818,7 @@ module Make = (
|
|
|
825
818
|
dcbCommandTopic
|
|
826
819
|
->Component.operations
|
|
827
820
|
->Pulumi.Output.apply(ops => {
|
|
828
|
-
let make = commandSchema =>
|
|
821
|
+
let make = (commandSchema, partitionTag) =>
|
|
829
822
|
CommandGenerator_Callback.makeGenerateCommand(
|
|
830
823
|
~publishJsons=ops.publishJsons,
|
|
831
824
|
~publishJsonsAndWait=?ops.publishJsonsAndWait,
|
|
@@ -833,16 +826,19 @@ module Make = (
|
|
|
833
826
|
~commandSchema,
|
|
834
827
|
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
835
828
|
~stripIdFromParams=false,
|
|
829
|
+
~partitionTag?,
|
|
836
830
|
)
|
|
837
831
|
let byTag = Dict.make()
|
|
838
832
|
dcbCommandSchemasByTag
|
|
839
833
|
->Dict.toArray
|
|
840
|
-
->Array.forEach(((tag, schema
|
|
834
|
+
->Array.forEach(((tag, (schema, partitionTag))) =>
|
|
835
|
+
byTag->Dict.set(tag, make(schema, partitionTag))
|
|
836
|
+
)
|
|
841
837
|
// A command no slice claims keeps the permissive generator — what every
|
|
842
838
|
// command got before this change — so nothing that reaches this handler
|
|
843
839
|
// by another route starts failing. It also cannot stamp, which is why
|
|
844
840
|
// the unknown case is logged rather than left to look like a success.
|
|
845
|
-
(byTag, make(S.json->S.castToUnknown))
|
|
841
|
+
(byTag, make(S.json->S.castToUnknown, None))
|
|
846
842
|
})
|
|
847
843
|
|
|
848
844
|
let dcbHandler =
|