@reventlessdev/reventless-core 3.0.0-alpha.215 → 3.0.0-alpha.217
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 +18 -0
- package/package.json +6 -6
- package/src/ResourceAttribution.res +60 -4
- package/src/ResourceAttribution.res.mjs +26 -0
- package/src/adapter/QueryInterception/QueryInterception.res +74 -0
- package/src/adapter/QueryInterception/QueryInterception.res.mjs +26 -0
- package/src/adapter/ResourceAttribution_Deploytime.res +43 -0
- package/src/adapter/ResourceAttribution_Deploytime.res.mjs +20 -0
- package/src/admin/Platform_Admin_Structure.res +4 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +5 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +16 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +24 -1
- package/src/components/Aggregate/Aggregate_Callback.res +14 -2
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +3 -3
- package/src/components/Builder_Helpers.res +21 -3
- package/src/components/Builder_Helpers.res.mjs +4 -3
- package/src/components/CommandTopic/CommandTopic.res.mjs +12 -0
- package/src/components/CommandTopic/CommandTopic_Helpers.res +88 -2
- package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +48 -2
- package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +4 -1
- package/src/components/ExtensionPoint/ExtensionPoint_Builder.res.mjs +2 -1
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +13 -1
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
- package/src/plugin/component/Plugin_Builder.res +6 -1
- package/src/plugin/component/Plugin_Builder.res.mjs +4 -2
- package/src/plugin/component/Plugin_Helpers.res +7 -2
- package/src/plugin/component/Plugin_Helpers.res.mjs +4 -3
- package/src/plugin/component/Plugin_Structure.res +66 -51
- package/src/plugin/component/Plugin_Structure.res.mjs +61 -46
- package/tests/ResourceAttributionTest.res +115 -0
- package/tests/ResourceAttributionTest.res.mjs +106 -0
- package/tests/adapter/QueryInterceptionTest.res +55 -0
- package/tests/adapter/QueryInterceptionTest.res.mjs +49 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +21 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +21 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res +258 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +372 -0
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +9 -1
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res.mjs +3 -3
- package/tests/message/MessageTest.res +1 -0
- package/tests/message/MessageTest.res.mjs +1 -0
- package/tests/plugin/PluginStructureTest.res +27 -0
- package/tests/plugin/PluginStructureTest.res.mjs +27 -0
- package/tests/plugin/StateChangeSlice/PsShipOrder.res +3 -1
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -1
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
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.217 (2026-08-08)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **core:** attribute resources built after a builder's construct returns ([67d6d9c](https://github.com/ReventlessDev/reventless-core/commit/67d6d9c0ff3ff1d38cd9e8ad43a4b86121abf827))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **core:** let an extension switch query interception on, and provision it ([4523b10](https://github.com/ReventlessDev/reventless-core/commit/4523b10597f1a028eedccc42391b250b25a6ddd3))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# 3.0.0-alpha.216 (2026-08-08)
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **core,spec:** surface a write-side component's declared error types ([c9c2505](https://github.com/ReventlessDev/reventless-core/commit/c9c25057c70595fe27d73447c9aef9b451f86168))
|
|
21
|
+
* **core:** add a runtime hook for command outcomes ([f2092a8](https://github.com/ReventlessDev/reventless-core/commit/f2092a8f9a8d8d7feee4f3de19bea7297a250cbd))
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
# 3.0.0-alpha.215 (2026-08-08)
|
|
7
25
|
|
|
8
26
|
### Features
|
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.217",
|
|
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-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
32
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
33
32
|
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.2",
|
|
34
33
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
34
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
35
35
|
"@reventlessdev/rescript-node": "2.0.0-alpha.2",
|
|
36
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
37
36
|
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.2",
|
|
38
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.127",
|
|
39
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
40
37
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
41
|
-
"@reventlessdev/
|
|
38
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
39
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.128",
|
|
40
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.102",
|
|
41
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.30"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -112,10 +112,28 @@ large, purely mechanical churn — so the plugin builder publishes them here for
|
|
|
112
112
|
the duration of its construct, exactly as it already does for
|
|
113
113
|
`Logger.currentPluginName`.
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
The `enter`/`restore` bracket covers what runs **synchronously** inside the
|
|
116
|
+
builder's construct, and everything built there genuinely belongs to that plugin.
|
|
117
|
+
|
|
118
|
+
It does not cover all of construct. Builders defer part of their work until every
|
|
119
|
+
handler has registered, and they defer it by waiting on outputs — so that work
|
|
120
|
+
runs from a `Pulumi.Output.apply` callback, after construct has returned and
|
|
121
|
+
`restore` has already emptied the context. A resource created there would be
|
|
122
|
+
attributed to nobody, and `None` is a *meaningful* value here: it means
|
|
123
|
+
platform-scope substrate. So the miss does not read as a gap, it reads as a
|
|
124
|
+
positive answer, and a consumer cannot tell the two apart.
|
|
125
|
+
|
|
126
|
+
Deferred work therefore carries its own attribution. Capture the context while it
|
|
127
|
+
is still published and reinstate it around the callback: `deferred` for a
|
|
128
|
+
callback registered during construct and invoked later, and
|
|
129
|
+
`ResourceAttribution_Deploytime.applyAttributed` / `flatMapAttributed` for a
|
|
130
|
+
builder's own apply. Both restore afterwards, so several plugins' deferred work
|
|
131
|
+
can run from one apply without leaking into each other.
|
|
132
|
+
|
|
133
|
+
Everything Pulumi-shaped lives in that separate module, so this one stays
|
|
134
|
+
importable from code that ends up in a Lambda bundle.
|
|
135
|
+
|
|
136
|
+
Outside any plugin construct both are `None` — the correct answer for
|
|
119
137
|
platform-scope substrate.
|
|
120
138
|
*/
|
|
121
139
|
type context = {platform: option<string>, plugin: option<string>}
|
|
@@ -131,3 +149,41 @@ let enter = (~platform: string, ~plugin: string) => {
|
|
|
131
149
|
|
|
132
150
|
/** Restore the context captured by `enter`. */
|
|
133
151
|
let restore = (previous: context) => current := previous
|
|
152
|
+
|
|
153
|
+
/** Re-enter a whole captured context — including an empty one, which `enter`
|
|
154
|
+
cannot express since it takes two required strings. Returns the previous
|
|
155
|
+
context, exactly as `enter` does. */
|
|
156
|
+
let enterCaptured = (captured: context) => {
|
|
157
|
+
let previous = current.contents
|
|
158
|
+
current := captured
|
|
159
|
+
previous
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Run `fn` under `captured`, then put the previous context back — including
|
|
163
|
+
when `fn` throws, so a failing deferred callback cannot strand the context
|
|
164
|
+
and mis-attribute everything built after it. */
|
|
165
|
+
let within = (captured: context, fn: unit => 'a): 'a => {
|
|
166
|
+
let previous = enterCaptured(captured)
|
|
167
|
+
try {
|
|
168
|
+
let result = fn()
|
|
169
|
+
restore(previous)
|
|
170
|
+
result
|
|
171
|
+
} catch {
|
|
172
|
+
| e =>
|
|
173
|
+
restore(previous)
|
|
174
|
+
throw(e)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Capture the context now and wrap a callback so it runs under that context
|
|
179
|
+
whenever it is finally called. For work registered during construct but
|
|
180
|
+
executed later from an apply — a builder's deferred `finish`, whose whole
|
|
181
|
+
purpose is to run after every handler has registered.
|
|
182
|
+
|
|
183
|
+
Wrap at **registration**, not at the call site: the registries these
|
|
184
|
+
callbacks land in are module-level and shared by every plugin, so by the time
|
|
185
|
+
they run there is no single context that would be right for all of them. */
|
|
186
|
+
let deferred = (fn: unit => 'a): (unit => 'a) => {
|
|
187
|
+
let captured = current.contents
|
|
188
|
+
() => within(captured, fn)
|
|
189
|
+
}
|
|
@@ -86,11 +86,37 @@ function restore(previous) {
|
|
|
86
86
|
current.contents = previous;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function enterCaptured(captured) {
|
|
90
|
+
let previous = current.contents;
|
|
91
|
+
current.contents = captured;
|
|
92
|
+
return previous;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function within(captured, fn) {
|
|
96
|
+
let previous = enterCaptured(captured);
|
|
97
|
+
try {
|
|
98
|
+
let result = fn();
|
|
99
|
+
current.contents = previous;
|
|
100
|
+
return result;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
current.contents = previous;
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function deferred(fn) {
|
|
108
|
+
let captured = current.contents;
|
|
109
|
+
return () => within(captured, fn);
|
|
110
|
+
}
|
|
111
|
+
|
|
89
112
|
export {
|
|
90
113
|
Scope,
|
|
91
114
|
Role,
|
|
92
115
|
current,
|
|
93
116
|
enter,
|
|
94
117
|
restore,
|
|
118
|
+
enterCaptured,
|
|
119
|
+
within,
|
|
120
|
+
deferred,
|
|
95
121
|
}
|
|
96
122
|
/* No side effect */
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Deploy-time switch: an extension asks for query interception, and the framework
|
|
3
|
+
provisions whatever that costs.
|
|
4
|
+
|
|
5
|
+
`QueryDb_Callback.registerQueryInterceptor` is a runtime hook, and `RuntimeExtension`
|
|
6
|
+
made it reachable inside a deployed runtime. On a DynamoDB-backed read model there
|
|
7
|
+
was still nothing to reach: a top-level Query resolver talks to DynamoDB directly,
|
|
8
|
+
so no code of ours runs on a read at all. Something has to put a runtime in front
|
|
9
|
+
of the read, and that is a provisioning decision the extension cannot make for
|
|
10
|
+
itself — an AppSync Lambda data source needs the api handle and a service role,
|
|
11
|
+
both of which exist only inside the plugin build.
|
|
12
|
+
|
|
13
|
+
So the split follows `Monitoring` and `EventLogProvisioning`: **the extension
|
|
14
|
+
decides, the framework provisions.** An extension says only *that* interception
|
|
15
|
+
should happen; it names no data source, no api and no role, and writes nothing
|
|
16
|
+
provider-shaped. The framework already ships the handler
|
|
17
|
+
(`QueryInterceptor_Lambda` on AWS), so there is nothing left for a caller to
|
|
18
|
+
supply.
|
|
19
|
+
|
|
20
|
+
**Off by default, and silent when off.** No registration means no interceptor
|
|
21
|
+
runtime, no data source, unit resolvers exactly as before, and a byte-identical
|
|
22
|
+
code archive — the same guarantee the cold-start seam makes.
|
|
23
|
+
|
|
24
|
+
**It composes with `RuntimeExtension` without either knowing about the other.**
|
|
25
|
+
The interceptor runtime is built through the standard runtime builder, so
|
|
26
|
+
`Util_Bundle` bundles every registered extension's package into its archive and
|
|
27
|
+
`makeFromCodeAsset` writes `RUNTIME_EXTENSIONS`. An extension that registers its
|
|
28
|
+
interceptor in `onColdStart` is therefore carried into the interceptor runtime
|
|
29
|
+
with no second registration.
|
|
30
|
+
|
|
31
|
+
## What it costs, which is why it is opt-in
|
|
32
|
+
|
|
33
|
+
Interception puts a Lambda invocation in front of **every read** on a
|
|
34
|
+
DynamoDB-backed read model. Reads outrun writes by orders of magnitude, so this
|
|
35
|
+
is the most expensive thing the framework can be asked to switch on. Two
|
|
36
|
+
properties bound it:
|
|
37
|
+
|
|
38
|
+
- **It is a property of the read-model backend, not of the cloud.** A
|
|
39
|
+
Postgres-backed read model already routes through a resolver Lambda, which
|
|
40
|
+
consults the same hook, so interception there costs nothing extra. Only the
|
|
41
|
+
DynamoDB direct-resolver path pays.
|
|
42
|
+
- **It is the only place a read can be refused.** Anything cheaper — a log
|
|
43
|
+
subscription, a stream-fed counter — observes a read after the fact but cannot
|
|
44
|
+
deny it. Where the requirement is enforcement rather than observation, this
|
|
45
|
+
cost is the requirement's price rather than overhead.
|
|
46
|
+
|
|
47
|
+
Granularity is per-deployment, deliberately: an operator asking "what does
|
|
48
|
+
interception cost me" wants one answer, not one per component. Narrowing later is
|
|
49
|
+
additive (a predicate on the registration); widening a per-component switch is
|
|
50
|
+
not.
|
|
51
|
+
|
|
52
|
+
See `docs/plans/done/query-interception-provisioning.md`.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
let enabled = ref(false)
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
Switch query interception on for this deployment. Must run before the
|
|
59
|
+
platform/plugin build in the deploy program (plain statement order — the switch is
|
|
60
|
+
read lazily at each provisioning site), exactly like `Monitoring.use`.
|
|
61
|
+
|
|
62
|
+
Registering the runtime interceptor itself stays separate and is the extension's
|
|
63
|
+
job, through `QueryDb_Callback.registerQueryInterceptor` — typically from a
|
|
64
|
+
`RuntimeExtension` cold-start hook. This call only provisions the path that lets
|
|
65
|
+
that hook be consulted on a read.
|
|
66
|
+
*/
|
|
67
|
+
let use = () => enabled := true
|
|
68
|
+
|
|
69
|
+
/** Whether interception was asked for. Consulted by the provisioning sites. */
|
|
70
|
+
let isEnabled = () => enabled.contents
|
|
71
|
+
|
|
72
|
+
/** Tests only — the switch is process-global, so a test that flips it must put it
|
|
73
|
+
back. */
|
|
74
|
+
let reset = () => enabled := false
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
let enabled = {
|
|
5
|
+
contents: false
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function use() {
|
|
9
|
+
enabled.contents = true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isEnabled() {
|
|
13
|
+
return enabled.contents;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function reset() {
|
|
17
|
+
enabled.contents = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
enabled,
|
|
22
|
+
use,
|
|
23
|
+
isEnabled,
|
|
24
|
+
reset,
|
|
25
|
+
}
|
|
26
|
+
/* No side effect */
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/***
|
|
2
|
+
Carrying `ResourceAttribution` across a `Pulumi.Output` boundary.
|
|
3
|
+
|
|
4
|
+
Separate from `ResourceAttribution` itself on purpose: that module is read by
|
|
5
|
+
code that ends up in a Lambda bundle, and importing the deploy-time engine there
|
|
6
|
+
is a cold-start failure. Everything Pulumi-shaped lives here, where only
|
|
7
|
+
deploy-time builders reach it — the same reason `AdapterDeploytime` sits apart
|
|
8
|
+
from `Adapter`.
|
|
9
|
+
|
|
10
|
+
Both helpers wrap `apply` / `flatMap` rather than the callback alone. Wrapping
|
|
11
|
+
the callback would read more neatly, but it severs the type flow from the Output
|
|
12
|
+
to the callback's parameters, and inference then fails on records the Output was
|
|
13
|
+
the only thing constraining.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** `Pulumi.Output.apply` for a callback that CREATES RESOURCES.
|
|
17
|
+
|
|
18
|
+
An apply callback runs after the enclosing builder's construct has returned,
|
|
19
|
+
so the ambient attribution context — published for the duration of that
|
|
20
|
+
construct and unpublished at the end of it — is already empty by then, and
|
|
21
|
+
anything the callback provisions is attributed to nobody. That is not a gap
|
|
22
|
+
but a wrong answer: an empty plugin means platform-scope substrate.
|
|
23
|
+
|
|
24
|
+
This captures the context where the apply is *registered*, which is
|
|
25
|
+
synchronous inside construct, and reinstates it around the callback. Reach
|
|
26
|
+
for it wherever an apply body reaches a resource; plain `Pulumi.Output.apply`
|
|
27
|
+
stays right for a callback that only shapes data. */
|
|
28
|
+
let applyAttributed = (output, callback) => {
|
|
29
|
+
let captured = ResourceAttribution.current.contents
|
|
30
|
+
output->Pulumi.Output.apply(resolved =>
|
|
31
|
+
captured->ResourceAttribution.within(() => callback(resolved))
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** `Pulumi.Output.flatMap` counterpart, for the same reason — a builder that
|
|
36
|
+
resolves one resource's outputs in order to build the next one defers just as
|
|
37
|
+
far. */
|
|
38
|
+
let flatMapAttributed = (output, callback) => {
|
|
39
|
+
let captured = ResourceAttribution.current.contents
|
|
40
|
+
output->Pulumi.Output.flatMap(resolved =>
|
|
41
|
+
captured->ResourceAttribution.within(() => callback(resolved))
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
4
|
+
import * as ResourceAttribution$ReventlessCore from "../ResourceAttribution.res.mjs";
|
|
5
|
+
|
|
6
|
+
function applyAttributed(output, callback) {
|
|
7
|
+
let captured = ResourceAttribution$ReventlessCore.current.contents;
|
|
8
|
+
return output.apply(resolved => ResourceAttribution$ReventlessCore.within(captured, () => callback(resolved)));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function flatMapAttributed(output, callback) {
|
|
12
|
+
let captured = ResourceAttribution$ReventlessCore.current.contents;
|
|
13
|
+
return Output$Pulumi.flatMap(output, resolved => ResourceAttribution$ReventlessCore.within(captured, () => callback(resolved)));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
applyAttributed,
|
|
18
|
+
flatMapAttributed,
|
|
19
|
+
}
|
|
20
|
+
/* Output-Pulumi Not a pure module */
|
|
@@ -98,6 +98,10 @@ let pluginAggregate: writableDef = {
|
|
|
98
98
|
linkedViews: ["Plugins"],
|
|
99
99
|
consistencyRead: None,
|
|
100
100
|
events: [],
|
|
101
|
+
// Derived rather than left empty like `events` above: the two admin commands can
|
|
102
|
+
// be refused, and `[]` here would read as "this aggregate never rejects". Uses
|
|
103
|
+
// the same walk `Plugin_Structure` applies to every other write side.
|
|
104
|
+
errors: Plugin_Structure.extractErrorDefs(PluginSpec.errorSchema->S.castToUnknown),
|
|
101
105
|
chapter: None,
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -7,6 +7,8 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
7
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
8
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
9
9
|
import * as Api_Naming$ReventlessCore from "../components/Api/Api_Naming.res.mjs";
|
|
10
|
+
import * as PluginSpec$ReventlessCore from "../plugin/lifecycle/PluginSpec.res.mjs";
|
|
11
|
+
import * as Plugin_Structure$ReventlessCore from "../plugin/component/Plugin_Structure.res.mjs";
|
|
10
12
|
import * as SuryToJsonSchema$ReventlessCore from "../components/Api/SuryToJsonSchema.res.mjs";
|
|
11
13
|
import * as PluginBaseFragment$ReventlessCore from "../plugin/api/PluginBaseFragment.res.mjs";
|
|
12
14
|
import * as PluginsReadModelSpec$ReventlessCore from "../plugin/lifecycle/PluginsReadModelSpec.res.mjs";
|
|
@@ -105,6 +107,8 @@ let pluginAggregate_linkedViews = ["Plugins"];
|
|
|
105
107
|
|
|
106
108
|
let pluginAggregate_events = [];
|
|
107
109
|
|
|
110
|
+
let pluginAggregate_errors = Plugin_Structure$ReventlessCore.extractErrorDefs(PluginSpec$ReventlessCore.errorSchema);
|
|
111
|
+
|
|
108
112
|
let pluginAggregate = {
|
|
109
113
|
name: "Plugin",
|
|
110
114
|
commands: pluginAggregate_commands,
|
|
@@ -113,6 +117,7 @@ let pluginAggregate = {
|
|
|
113
117
|
linkedViews: pluginAggregate_linkedViews,
|
|
114
118
|
consistencyRead: undefined,
|
|
115
119
|
events: pluginAggregate_events,
|
|
120
|
+
errors: pluginAggregate_errors,
|
|
116
121
|
chapter: undefined
|
|
117
122
|
};
|
|
118
123
|
|
|
@@ -20,7 +20,11 @@ let sdlTypes: array<string> = [
|
|
|
20
20
|
`type Platform_FieldReference {\n fieldName: String!\n entity: String!\n plugin: String\n}`,
|
|
21
21
|
`type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n}`,
|
|
22
22
|
`type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
23
|
-
|
|
23
|
+
// Same fields as Platform_EventDef, kept a distinct type because a refusal is not
|
|
24
|
+
// a fact: a caller selecting `errors` is asking what a command can be rejected
|
|
25
|
+
// with, and the two lists must stay independently evolvable.
|
|
26
|
+
`type Platform_ErrorDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
27
|
+
`type Platform_WriteSideDef {\n name: String!\n commands: [Platform_CommandDef!]!\n linkedViews: [String!]!\n consistencyRead: String\n producedEventTypes: [String!]!\n consumedEventTypes: [String!]!\n events: [Platform_EventDef!]!\n errors: [Platform_ErrorDef!]!\n chapter: String\n}`,
|
|
24
28
|
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n statusField: String\n visibility: String\n chapter: String\n}`,
|
|
25
29
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n targetName: String\n chapter: String\n}`,
|
|
26
30
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
@@ -102,6 +106,13 @@ let encodeEventDef = (e: eventDef): JSON.t =>
|
|
|
102
106
|
("references", e.references->Array.map(encodeFieldReference)->JSON.Encode.array),
|
|
103
107
|
])->JSON.Encode.object
|
|
104
108
|
|
|
109
|
+
let encodeErrorDef = (e: errorDef): JSON.t =>
|
|
110
|
+
Dict.fromArray([
|
|
111
|
+
("name", JSON.Encode.string(e.name)),
|
|
112
|
+
("schema", JSON.Encode.string(e.schema)),
|
|
113
|
+
("references", e.references->Array.map(encodeFieldReference)->JSON.Encode.array),
|
|
114
|
+
])->JSON.Encode.object
|
|
115
|
+
|
|
105
116
|
let encodeWritableDef = (w: writableDef): JSON.t =>
|
|
106
117
|
Dict.fromArray([
|
|
107
118
|
("name", JSON.Encode.string(w.name)),
|
|
@@ -115,6 +126,10 @@ let encodeWritableDef = (w: writableDef): JSON.t =>
|
|
|
115
126
|
("consumedEventTypes", encodeStrings(w.consumedEventTypes)),
|
|
116
127
|
// Phase 6.3: emitted-event field schemas (None → [] on the wire).
|
|
117
128
|
("events", w.events->Array.map(encodeEventDef)->JSON.Encode.array),
|
|
129
|
+
// Declared errors — the refusals a caller has to handle. `[]` is the honest
|
|
130
|
+
// answer for a component that declares none; the structure is re-derived on
|
|
131
|
+
// every build, so it never stands in for "cannot say".
|
|
132
|
+
("errors", w.errors->Array.map(encodeErrorDef)->JSON.Encode.array),
|
|
118
133
|
("chapter", w.chapter->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
119
134
|
])->JSON.Encode.object
|
|
120
135
|
|
|
@@ -8,7 +8,8 @@ let sdlTypes = [
|
|
|
8
8
|
`type Platform_FieldReference {\n fieldName: String!\n entity: String!\n plugin: String\n}`,
|
|
9
9
|
`type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n}`,
|
|
10
10
|
`type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
11
|
-
`type
|
|
11
|
+
`type Platform_ErrorDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
12
|
+
`type Platform_WriteSideDef {\n name: String!\n commands: [Platform_CommandDef!]!\n linkedViews: [String!]!\n consistencyRead: String\n producedEventTypes: [String!]!\n consumedEventTypes: [String!]!\n events: [Platform_EventDef!]!\n errors: [Platform_ErrorDef!]!\n chapter: String\n}`,
|
|
12
13
|
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n statusField: String\n visibility: String\n chapter: String\n}`,
|
|
13
14
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n targetName: String\n chapter: String\n}`,
|
|
14
15
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
@@ -152,6 +153,23 @@ function encodeEventDef(e) {
|
|
|
152
153
|
]);
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
function encodeErrorDef(e) {
|
|
157
|
+
return Object.fromEntries([
|
|
158
|
+
[
|
|
159
|
+
"name",
|
|
160
|
+
e.name
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
"schema",
|
|
164
|
+
e.schema
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
"references",
|
|
168
|
+
e.references.map(encodeFieldReference)
|
|
169
|
+
]
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
|
|
155
173
|
function encodeWritableDef(w) {
|
|
156
174
|
return Object.fromEntries([
|
|
157
175
|
[
|
|
@@ -182,6 +200,10 @@ function encodeWritableDef(w) {
|
|
|
182
200
|
"events",
|
|
183
201
|
w.events.map(encodeEventDef)
|
|
184
202
|
],
|
|
203
|
+
[
|
|
204
|
+
"errors",
|
|
205
|
+
w.errors.map(encodeErrorDef)
|
|
206
|
+
],
|
|
185
207
|
[
|
|
186
208
|
"chapter",
|
|
187
209
|
Stdlib_Option.mapOr(w.chapter, null, prim => prim)
|
|
@@ -341,6 +363,7 @@ export {
|
|
|
341
363
|
isPublicQueryable,
|
|
342
364
|
encodeQueryableDef,
|
|
343
365
|
encodeEventDef,
|
|
366
|
+
encodeErrorDef,
|
|
344
367
|
encodeWritableDef,
|
|
345
368
|
encodeAutomationSliceDef,
|
|
346
369
|
encodeOutboundTranslationSliceDef,
|
|
@@ -284,13 +284,25 @@ module Make = (
|
|
|
284
284
|
outcomes->Array.map(((reference, outcome, _meta)) =>
|
|
285
285
|
switch outcome {
|
|
286
286
|
| CmdRejected({errorCode, errorDetail}) =>
|
|
287
|
-
CommandTopic_Helpers.reportRejected(
|
|
287
|
+
CommandTopic_Helpers.reportRejected(
|
|
288
|
+
~component=Spec.name,
|
|
289
|
+
~cause=DomainRejection,
|
|
290
|
+
reference,
|
|
291
|
+
{errorCode, errorDetail},
|
|
292
|
+
)
|
|
288
293
|
Ok(reference)
|
|
289
294
|
| CmdOk(_) if appendSucceeded =>
|
|
290
|
-
CommandTopic_Helpers.reportAccepted(
|
|
295
|
+
CommandTopic_Helpers.reportAccepted(
|
|
296
|
+
~component=Spec.name,
|
|
297
|
+
reference,
|
|
298
|
+
{entityId, eventCount: appendedEventCount},
|
|
299
|
+
)
|
|
291
300
|
Ok(reference)
|
|
292
301
|
| CmdOk(_) =>
|
|
302
|
+
// The decision succeeded and the append did not — infrastructure, not the model.
|
|
293
303
|
CommandTopic_Helpers.reportRejected(
|
|
304
|
+
~component=Spec.name,
|
|
305
|
+
~cause=InfrastructureFailure,
|
|
294
306
|
reference,
|
|
295
307
|
{errorCode: "AppendFailed", errorDetail: appendErrorDetail},
|
|
296
308
|
)
|
|
@@ -200,7 +200,7 @@ function Make(Spec) {
|
|
|
200
200
|
let reference = param[0];
|
|
201
201
|
if (outcome.TAG === "CmdOk") {
|
|
202
202
|
if (appendSucceeded) {
|
|
203
|
-
CommandTopic_Helpers$ReventlessCore.reportAccepted(reference, {
|
|
203
|
+
CommandTopic_Helpers$ReventlessCore.reportAccepted(Spec.name, reference, {
|
|
204
204
|
entityId: entityId,
|
|
205
205
|
eventCount: appendedEventCount
|
|
206
206
|
});
|
|
@@ -209,7 +209,7 @@ function Make(Spec) {
|
|
|
209
209
|
_0: reference
|
|
210
210
|
};
|
|
211
211
|
} else {
|
|
212
|
-
CommandTopic_Helpers$ReventlessCore.reportRejected(reference, {
|
|
212
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "InfrastructureFailure", reference, {
|
|
213
213
|
errorCode: "AppendFailed",
|
|
214
214
|
errorDetail: appendErrorDetail
|
|
215
215
|
});
|
|
@@ -219,7 +219,7 @@ function Make(Spec) {
|
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
-
CommandTopic_Helpers$ReventlessCore.reportRejected(reference, {
|
|
222
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "DomainRejection", reference, {
|
|
223
223
|
errorCode: outcome.errorCode,
|
|
224
224
|
errorDetail: outcome.errorDetail
|
|
225
225
|
});
|
|
@@ -42,7 +42,13 @@ let createAggregatesWithoutEventMappers = (
|
|
|
42
42
|
let publishJsons =
|
|
43
43
|
SpecificAggregate.operations(aggregate)->Pulumi.Output.apply(({publishJsons}) => publishJsons)
|
|
44
44
|
publishToAggregates->Dict.set(SpecificAggregate.Spec.name, publishJsons)
|
|
45
|
-
|
|
45
|
+
// `finish` runs from an apply in `finishAggregates`, long after this
|
|
46
|
+
// construct returns — wrap it now, while the plugin is still the ambient
|
|
47
|
+
// one, so whatever it provisions is attributed to that plugin.
|
|
48
|
+
aggregateFinishFns->Dict.set(
|
|
49
|
+
SpecificAggregate.Spec.name,
|
|
50
|
+
ResourceAttribution.deferred(SpecificAggregate.finish),
|
|
51
|
+
)
|
|
46
52
|
aggOutputs
|
|
47
53
|
})
|
|
48
54
|
->Array.map(aggregate => {(aggregate.name, aggregate)})
|
|
@@ -106,7 +112,10 @@ let taskSideEffectFinishFns: array<unit => unit> = []
|
|
|
106
112
|
|
|
107
113
|
let registerTaskSideEffectHandler = (~gate, ~finish) => {
|
|
108
114
|
let _ = taskSideEffectGates->Array.push(gate)
|
|
109
|
-
|
|
115
|
+
// These arrays are module-level and shared by every plugin, so the context has
|
|
116
|
+
// to travel with each entry: by the time `finishTasks` runs them there is no
|
|
117
|
+
// one plugin that would be right for all of them.
|
|
118
|
+
let _ = taskSideEffectFinishFns->Array.push(ResourceAttribution.deferred(finish))
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
// Provision side-effect handler runtimes once every task's handler is ready. Same
|
|
@@ -179,7 +188,16 @@ let createReadModels = (
|
|
|
179
188
|
rmOperations->Pulumi.Output.apply(({enqueueEvent}) => enqueueEvent),
|
|
180
189
|
)
|
|
181
190
|
|
|
182
|
-
|
|
191
|
+
// Deferred to `finishReadModels`, which runs it from an apply — wrap it here
|
|
192
|
+
// so the read model's runtime is attributed to the plugin building it.
|
|
193
|
+
(
|
|
194
|
+
SpecificReadModel.Spec.name,
|
|
195
|
+
{
|
|
196
|
+
outputs: rmOutputs,
|
|
197
|
+
operations: rmOperations,
|
|
198
|
+
finish: ResourceAttribution.deferred(SpecificReadModel.finish),
|
|
199
|
+
},
|
|
200
|
+
)
|
|
183
201
|
})
|
|
184
202
|
readModels->finishReadModels
|
|
185
203
|
readModels->extractReadModelsOutputs
|
|
@@ -4,6 +4,7 @@ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
|
4
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
5
|
import * as Pulumi from "@pulumi/pulumi";
|
|
6
6
|
import * as QueryDb$ReventlessCore from "./QueryDb/QueryDb.res.mjs";
|
|
7
|
+
import * as ResourceAttribution$ReventlessCore from "../ResourceAttribution.res.mjs";
|
|
7
8
|
|
|
8
9
|
let addEventMapperFns = {};
|
|
9
10
|
|
|
@@ -22,7 +23,7 @@ function createAggregatesWithoutEventMappers(aggregates, api, componentRuntime,
|
|
|
22
23
|
aggregateResources[SpecificAggregate.Spec.name] = resources;
|
|
23
24
|
let publishJsons = SpecificAggregate.operations(aggregate).apply(param => param.publishJsons);
|
|
24
25
|
publishToAggregates[SpecificAggregate.Spec.name] = publishJsons;
|
|
25
|
-
aggregateFinishFns[SpecificAggregate.Spec.name] = SpecificAggregate.finish;
|
|
26
|
+
aggregateFinishFns[SpecificAggregate.Spec.name] = ResourceAttribution$ReventlessCore.deferred(SpecificAggregate.finish);
|
|
26
27
|
return aggOutputs;
|
|
27
28
|
}).map(aggregate => [
|
|
28
29
|
aggregate.name,
|
|
@@ -54,7 +55,7 @@ let taskSideEffectFinishFns = [];
|
|
|
54
55
|
|
|
55
56
|
function registerTaskSideEffectHandler(gate, finish) {
|
|
56
57
|
taskSideEffectGates.push(gate);
|
|
57
|
-
taskSideEffectFinishFns.push(finish);
|
|
58
|
+
taskSideEffectFinishFns.push(ResourceAttribution$ReventlessCore.deferred(finish));
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function finishTasks() {
|
|
@@ -102,7 +103,7 @@ function createReadModels(readModels, api, apiRole, componentRuntime, allEventTo
|
|
|
102
103
|
{
|
|
103
104
|
outputs: rmOutputs,
|
|
104
105
|
operations: rmOperations,
|
|
105
|
-
finish: SpecificReadModel.finish
|
|
106
|
+
finish: ResourceAttribution$ReventlessCore.deferred(SpecificReadModel.finish)
|
|
106
107
|
}
|
|
107
108
|
];
|
|
108
109
|
});
|
|
@@ -23,6 +23,14 @@ function filter(allCommandTopics, names) {
|
|
|
23
23
|
|
|
24
24
|
let componentType = "CommandTopic";
|
|
25
25
|
|
|
26
|
+
let commandOutcomeHook = CommandTopic_Helpers$ReventlessCore.commandOutcomeHook;
|
|
27
|
+
|
|
28
|
+
let registerCommandOutcome = CommandTopic_Helpers$ReventlessCore.registerCommandOutcome;
|
|
29
|
+
|
|
30
|
+
let clearCommandOutcome = CommandTopic_Helpers$ReventlessCore.clearCommandOutcome;
|
|
31
|
+
|
|
32
|
+
let fireCommandOutcome = CommandTopic_Helpers$ReventlessCore.fireCommandOutcome;
|
|
33
|
+
|
|
26
34
|
let acceptedResultChannel = CommandTopic_Helpers$ReventlessCore.acceptedResultChannel;
|
|
27
35
|
|
|
28
36
|
let rejectedResultChannel = CommandTopic_Helpers$ReventlessCore.rejectedResultChannel;
|
|
@@ -50,6 +58,10 @@ let getHandlers = CommandTopic_Helpers$ReventlessCore.getHandlers;
|
|
|
50
58
|
export {
|
|
51
59
|
componentType,
|
|
52
60
|
NotPublishedToChannel,
|
|
61
|
+
commandOutcomeHook,
|
|
62
|
+
registerCommandOutcome,
|
|
63
|
+
clearCommandOutcome,
|
|
64
|
+
fireCommandOutcome,
|
|
53
65
|
acceptedResultChannel,
|
|
54
66
|
rejectedResultChannel,
|
|
55
67
|
reportAccepted,
|