@reventlessdev/reventless-core 3.0.0-alpha.191 → 3.0.0-alpha.192
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 +12 -0
- package/package.json +7 -7
- package/src/admin/Platform_Admin.res.mjs +4 -0
- package/src/admin/Platform_Admin_Structure.res +1 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +2 -1
- package/src/components/Aggregate/Aggregate.res +7 -7
- package/src/components/Aggregate/Aggregate.res.mjs +7 -2
- package/src/components/Aggregate/Aggregate_Builder.res +4 -1
- package/src/components/Aggregate/Aggregate_Builder.res.mjs +11 -3
- package/src/components/Builder_Helpers.res +37 -4
- package/src/components/Builder_Helpers.res.mjs +24 -2
- package/src/components/SideEffectHandler/SideEffectHandler.res +11 -0
- package/src/components/SideEffectHandler/SideEffectHandler_Builder.res +2 -0
- package/src/components/SideEffectHandler/SideEffectHandler_Builder.res.mjs +3 -1
- package/src/components/Task/Task_Builder.res +11 -0
- package/src/components/Task/Task_Builder.res.mjs +2 -0
- package/src/plugin/component/Capability_Inference.res +68 -0
- package/src/plugin/component/Capability_Inference.res.mjs +79 -0
- package/src/plugin/component/Plugin_Helpers.res +18 -7
- package/src/plugin/component/Plugin_Helpers.res.mjs +20 -1
- package/src/plugin/component/Plugin_Structure.res +91 -34
- package/src/plugin/component/Plugin_Structure.res.mjs +82 -12
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +2 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +8 -4
- package/tests/message/MessageTest.res +1 -0
- package/tests/message/MessageTest.res.mjs +2 -1
- package/tests/plugin/PluginStructureTest.res +145 -0
- package/tests/plugin/PluginStructureTest.res.mjs +138 -0
- package/tests/plugin/StateChangeSlice/PsChangePhoto.res +33 -0
- package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
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.192 (2026-07-30)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **core,aws:** provision the side-effect handler Lambda, which never existed ([d07311e](https://github.com/ReventlessDev/reventless-core/commit/d07311e0cfc368e9b3d51596e775a58b55a2fc0e))
|
|
11
|
+
* **core:** resolve each eventMapper before collecting, not after ([12060d8](https://github.com/ReventlessDev/reventless-core/commit/12060d84733e1636255be03c432f959139600dbf))
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **spec,core,local:** emit capabilities.json from the plugin build ([57a3276](https://github.com/ReventlessDev/reventless-core/commit/57a3276a6b02af93490a8b460e3aa158b7e0e0f8))
|
|
15
|
+
* **spec,core:** derive the platform capability list — inference and generation (stage 3) ([8ab4d25](https://github.com/ReventlessDev/reventless-core/commit/8ab4d2504bd717487fbc87a0f5a053024c8258f9))
|
|
16
|
+
|
|
17
|
+
|
|
6
18
|
# 3.0.0-alpha.191 (2026-07-29)
|
|
7
19
|
|
|
8
20
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.192",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -28,17 +28,17 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.31",
|
|
32
|
-
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
|
|
33
31
|
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
|
|
34
|
-
"@reventlessdev/rescript-
|
|
32
|
+
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
|
|
33
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.31",
|
|
35
34
|
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
|
|
36
|
-
"@reventlessdev/rescript-
|
|
35
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
37
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
37
|
+
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
|
|
38
38
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
39
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
39
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.110",
|
|
40
40
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
41
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.85"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -247,6 +247,10 @@ function Make(RuntimeEnvironment) {
|
|
|
247
247
|
createAggregatesWithoutEventMappers: Builder_Helpers$ReventlessCore.createAggregatesWithoutEventMappers,
|
|
248
248
|
finishAggregates: Builder_Helpers$ReventlessCore.finishAggregates,
|
|
249
249
|
addEventMappers: Builder_Helpers$ReventlessCore.addEventMappers,
|
|
250
|
+
taskSideEffectGates: Builder_Helpers$ReventlessCore.taskSideEffectGates,
|
|
251
|
+
taskSideEffectFinishFns: Builder_Helpers$ReventlessCore.taskSideEffectFinishFns,
|
|
252
|
+
registerTaskSideEffectHandler: Builder_Helpers$ReventlessCore.registerTaskSideEffectHandler,
|
|
253
|
+
finishTasks: Builder_Helpers$ReventlessCore.finishTasks,
|
|
250
254
|
readModelNamesForSourceName: Builder_Helpers$ReventlessCore.readModelNamesForSourceName,
|
|
251
255
|
publishToReadModels: Builder_Helpers$ReventlessCore.publishToReadModels,
|
|
252
256
|
finishReadModels: Builder_Helpers$ReventlessCore.finishReadModels,
|
|
@@ -170,7 +170,8 @@ let structure = {
|
|
|
170
170
|
inboundTranslationSlices: structure_inboundTranslationSlices,
|
|
171
171
|
extensions: structure_extensions,
|
|
172
172
|
extensionPoints: undefined,
|
|
173
|
-
requiredStores: undefined
|
|
173
|
+
requiredStores: undefined,
|
|
174
|
+
requiredStoreDeclarations: undefined
|
|
174
175
|
};
|
|
175
176
|
|
|
176
177
|
let pluginName = pluginId;
|
|
@@ -55,13 +55,13 @@ let toResolvedOutputs = (
|
|
|
55
55
|
}
|
|
56
56
|
resolved
|
|
57
57
|
})
|
|
58
|
-
let eventMapperResolved =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
let eventMapperResolved =
|
|
59
|
+
outputs.eventMapper->Pulumi.Output.flatMap(em =>
|
|
60
|
+
switch em {
|
|
61
|
+
| Some(em) => em->EventMapper.toResolvedOutputs->Pulumi.Output.apply(r => Some(r))
|
|
62
|
+
| None => Pulumi.Output.make(None)
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
65
|
(commandGeneratorResolved, commandTopicResolved, eventLogResolved, eventMapperResolved)
|
|
66
66
|
->Pulumi.Output.all4
|
|
67
67
|
->Pulumi.Output.apply(((commandGenerator, commandTopic, eventLog, eventMapper)) =>
|
|
@@ -39,8 +39,13 @@ function toResolvedOutputs(outputs) {
|
|
|
39
39
|
resources: param[1]
|
|
40
40
|
}
|
|
41
41
|
}));
|
|
42
|
-
let
|
|
43
|
-
|
|
42
|
+
let eventMapperResolved = Output$Pulumi.flatMap(outputs.eventMapper, em => {
|
|
43
|
+
if (em !== undefined) {
|
|
44
|
+
return EventMapper$ReventlessCore.toResolvedOutputs(em).apply(r => r);
|
|
45
|
+
} else {
|
|
46
|
+
return Pulumi.output(undefined);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
44
49
|
return Pulumi.all([
|
|
45
50
|
commandGeneratorResolved,
|
|
46
51
|
commandTopicResolved,
|
|
@@ -58,7 +58,7 @@ module Make = (
|
|
|
58
58
|
{
|
|
59
59
|
...aggregate->Component.outputs,
|
|
60
60
|
eventMapper: eventMapper->Pulumi.Output.apply(eventMapper =>
|
|
61
|
-
eventMapper->Component.outputs
|
|
61
|
+
Some(eventMapper->Component.outputs)
|
|
62
62
|
),
|
|
63
63
|
}
|
|
64
64
|
} else {
|
|
@@ -175,6 +175,9 @@ module Make = (
|
|
|
175
175
|
commandGenerator: commandGenerator->Component.wrappedOutputs,
|
|
176
176
|
commandTopic: commandTopic->Component.wrappedOutputs,
|
|
177
177
|
eventLog: eventLog->Component.outputs,
|
|
178
|
+
// Resolved-absent rather than an absent field: an aggregate without
|
|
179
|
+
// EventMappings still has to answer the question.
|
|
180
|
+
eventMapper: Pulumi.Output.make(None),
|
|
178
181
|
addEventMapper: self->addEventMapperFn(~opts, ...),
|
|
179
182
|
}
|
|
180
183
|
self->Component.setOutputs(aggOutputs)
|
|
@@ -89,6 +89,7 @@ function Make(Spec) {
|
|
|
89
89
|
let aggOutputs_commandGenerator = Component$ReventlessCore.wrappedOutputs(commandGenerator);
|
|
90
90
|
let aggOutputs_commandTopic = Component$ReventlessCore.wrappedOutputs(commandTopic);
|
|
91
91
|
let aggOutputs_eventLog = Component$ReventlessCore.outputs(eventLog);
|
|
92
|
+
let aggOutputs_eventMapper = Pulumi.output(undefined);
|
|
92
93
|
let aggOutputs_addEventMapper = (none, none$1) => {
|
|
93
94
|
if (EventMappings.mappings.length === 0) {
|
|
94
95
|
return Component$ReventlessCore.outputs(extra);
|
|
@@ -97,15 +98,22 @@ function Make(Spec) {
|
|
|
97
98
|
Component$ReventlessCore.operations(extra),
|
|
98
99
|
Component$ReventlessCore.outputs(extra).commandTopic
|
|
99
100
|
]).apply(param => SpecificEventMapper.make(ComponentType$ReventlessCore.name(Spec.name, Aggregate$ReventlessCore.componentType), none, none$1, param[0].publishJsons, param[1].resources, undefined, undefined, opts));
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
let init = Component$ReventlessCore.outputs(extra);
|
|
102
|
+
return {
|
|
103
|
+
name: init.name,
|
|
104
|
+
commandGenerator: init.commandGenerator,
|
|
105
|
+
commandTopic: init.commandTopic,
|
|
106
|
+
eventLog: init.eventLog,
|
|
107
|
+
eventMapper: eventMapper.apply(eventMapper => Component$ReventlessCore.outputs(eventMapper)),
|
|
108
|
+
addEventMapper: init.addEventMapper
|
|
109
|
+
};
|
|
103
110
|
};
|
|
104
111
|
let aggOutputs = {
|
|
105
112
|
name: aggOutputs_name,
|
|
106
113
|
commandGenerator: aggOutputs_commandGenerator,
|
|
107
114
|
commandTopic: aggOutputs_commandTopic,
|
|
108
115
|
eventLog: aggOutputs_eventLog,
|
|
116
|
+
eventMapper: aggOutputs_eventMapper,
|
|
109
117
|
addEventMapper: aggOutputs_addEventMapper
|
|
110
118
|
};
|
|
111
119
|
return Component$ReventlessCore.setOutputs(extra, aggOutputs);
|
|
@@ -58,16 +58,23 @@ let finishAggregates = (
|
|
|
58
58
|
// to ensure forCommandTopic has registered specs before finish() runs.
|
|
59
59
|
let allCommandTopicOutputs =
|
|
60
60
|
allOutputs->Array.map(aggregateOutputs => aggregateOutputs.commandTopic)
|
|
61
|
-
let eventMapperOutputs =
|
|
62
|
-
allOutputs
|
|
63
|
-
->Array.map(aggregateOutputs => aggregateOutputs.eventMapper)
|
|
64
|
-
->Array.keepSome
|
|
61
|
+
let eventMapperOutputs = allOutputs->Array.map(aggregateOutputs => aggregateOutputs.eventMapper)
|
|
65
62
|
|
|
66
63
|
let _ =
|
|
67
64
|
(eventMapperOutputs->Pulumi.Output.all, allCommandTopicOutputs->Pulumi.Output.all)
|
|
68
65
|
->Pulumi.Output.all2
|
|
69
66
|
->Pulumi.Output.apply(((eventMapperOutputs, _)) =>
|
|
67
|
+
// Filtered after resolution, not before: the absent ones are now `None`
|
|
68
|
+
// inside a resolved array rather than missing fields.
|
|
69
|
+
//
|
|
70
|
+
// `eventCollector` here is a plain record, not an Output: `Output.all` above
|
|
71
|
+
// deeply unwrapped the contents of each element. That is fine because `all`
|
|
72
|
+
// accepts resolved values, but nothing in this block may call
|
|
73
|
+
// `EventMapper.toResolvedOutputs` — it expects unresolved fields and would
|
|
74
|
+
// throw `m.apply is not a function`. Resolve at the depth where the record is
|
|
75
|
+
// still unresolved instead, as `serializeEventMappersOutputs` does.
|
|
70
76
|
eventMapperOutputs
|
|
77
|
+
->Array.keepSome
|
|
71
78
|
->Array.map(eventMapperOutput => eventMapperOutput.eventCollector)
|
|
72
79
|
->Pulumi.Output.all
|
|
73
80
|
->Pulumi.Output.apply(_ =>
|
|
@@ -89,6 +96,32 @@ let addEventMappers = (
|
|
|
89
96
|
aggregatesOutputs
|
|
90
97
|
}
|
|
91
98
|
|
|
99
|
+
// Side-effect handlers registered by Task_Builder, with the Output that tells us the
|
|
100
|
+
// handler has reached its runtime builder. Registration happens inside an
|
|
101
|
+
// `allCommandTopics` apply, so `make` returning is not enough — a synchronous
|
|
102
|
+
// `finish()` would build the shared runtime before any handler had registered, or
|
|
103
|
+
// worse, from only the first one.
|
|
104
|
+
let taskSideEffectGates: array<Pulumi.Output.t<unit>> = []
|
|
105
|
+
let taskSideEffectFinishFns: array<unit => unit> = []
|
|
106
|
+
|
|
107
|
+
let registerTaskSideEffectHandler = (~gate, ~finish) => {
|
|
108
|
+
let _ = taskSideEffectGates->Array.push(gate)
|
|
109
|
+
let _ = taskSideEffectFinishFns->Array.push(finish)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Provision side-effect handler runtimes once every task's handler is ready. Same
|
|
113
|
+
// shape as `finishAggregates`: collect the gates, wait on all of them, then run the
|
|
114
|
+
// finish functions. Adapters guard against repeat calls.
|
|
115
|
+
let finishTasks = () =>
|
|
116
|
+
if taskSideEffectGates->Array.length > 0 {
|
|
117
|
+
let _ =
|
|
118
|
+
taskSideEffectGates
|
|
119
|
+
->Pulumi.Output.all
|
|
120
|
+
->Pulumi.Output.apply(_ =>
|
|
121
|
+
taskSideEffectFinishFns->Array.forEach(finishFn => finishFn())
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
92
125
|
let readModelNamesForSourceName = Dict.make()
|
|
93
126
|
let publishToReadModels = Dict.make()
|
|
94
127
|
|
|
@@ -33,11 +33,11 @@ function createAggregatesWithoutEventMappers(aggregates, api, componentRuntime,
|
|
|
33
33
|
function finishAggregates(aggregatesOutputs) {
|
|
34
34
|
let allOutputs = Object.values(aggregatesOutputs);
|
|
35
35
|
let allCommandTopicOutputs = allOutputs.map(aggregateOutputs => aggregateOutputs.commandTopic);
|
|
36
|
-
let eventMapperOutputs =
|
|
36
|
+
let eventMapperOutputs = allOutputs.map(aggregateOutputs => aggregateOutputs.eventMapper);
|
|
37
37
|
Pulumi.all([
|
|
38
38
|
Pulumi.all(eventMapperOutputs),
|
|
39
39
|
Pulumi.all(allCommandTopicOutputs)
|
|
40
|
-
]).apply(param => Pulumi.all(param[0].map(eventMapperOutput => eventMapperOutput.eventCollector)).apply(param => {
|
|
40
|
+
]).apply(param => Pulumi.all(Stdlib_Array.keepSome(param[0]).map(eventMapperOutput => eventMapperOutput.eventCollector)).apply(param => {
|
|
41
41
|
Object.values(aggregateFinishFns).forEach(finishFn => finishFn());
|
|
42
42
|
}));
|
|
43
43
|
}
|
|
@@ -48,6 +48,24 @@ function addEventMappers(allEventTopics, queryEngine) {
|
|
|
48
48
|
return aggregatesOutputs;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
let taskSideEffectGates = [];
|
|
52
|
+
|
|
53
|
+
let taskSideEffectFinishFns = [];
|
|
54
|
+
|
|
55
|
+
function registerTaskSideEffectHandler(gate, finish) {
|
|
56
|
+
taskSideEffectGates.push(gate);
|
|
57
|
+
taskSideEffectFinishFns.push(finish);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function finishTasks() {
|
|
61
|
+
if (taskSideEffectGates.length !== 0) {
|
|
62
|
+
Pulumi.all(taskSideEffectGates).apply(param => {
|
|
63
|
+
taskSideEffectFinishFns.forEach(finishFn => finishFn());
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
let readModelNamesForSourceName = {};
|
|
52
70
|
|
|
53
71
|
let publishToReadModels = {};
|
|
@@ -131,6 +149,10 @@ export {
|
|
|
131
149
|
createAggregatesWithoutEventMappers,
|
|
132
150
|
finishAggregates,
|
|
133
151
|
addEventMappers,
|
|
152
|
+
taskSideEffectGates,
|
|
153
|
+
taskSideEffectFinishFns,
|
|
154
|
+
registerTaskSideEffectHandler,
|
|
155
|
+
finishTasks,
|
|
134
156
|
readModelNamesForSourceName,
|
|
135
157
|
publishToReadModels,
|
|
136
158
|
finishReadModels,
|
|
@@ -32,4 +32,15 @@ module type T = {
|
|
|
32
32
|
~extraEnvVars: dict<Pulumi.Input.t<string>>=?,
|
|
33
33
|
~opts: Pulumi.ComponentResource.options=?,
|
|
34
34
|
) => component
|
|
35
|
+
|
|
36
|
+
/** Provision the runtime for every handler registered by `make`.
|
|
37
|
+
*
|
|
38
|
+
* Adapters that share one runtime across all side-effect handlers can only build
|
|
39
|
+
* it once every handler has registered, so nothing is provisioned by `make`
|
|
40
|
+
* alone. Every other component type has this seam
|
|
41
|
+
* (`ReadModel_Builder.finish`, `Aggregate_Builder.finish`, …); the side-effect
|
|
42
|
+
* handler went without one, which is why its Lambda was never created. Call it
|
|
43
|
+
* after the last handler is constructed — `Builder_Helpers.finishTasks` gates it
|
|
44
|
+
* on their readiness. Idempotent. */
|
|
45
|
+
let finish: unit => unit
|
|
35
46
|
}
|
|
@@ -93,6 +93,17 @@ module Make = (
|
|
|
93
93
|
)
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
+
// The handler's runtime is provisioned by `SpecificSideEffectHandler.finish()`,
|
|
97
|
+
// not by `make` — see `SideEffectHandler.T.finish`. Register it with the gate that
|
|
98
|
+
// says this handler has reached its runtime builder: `operations` is set at the end
|
|
99
|
+
// of the builder's construct, after `forEventCollector` has run.
|
|
100
|
+
sideEffectHandler->Option.forEach(handler =>
|
|
101
|
+
Builder_Helpers.registerTaskSideEffectHandler(
|
|
102
|
+
~gate=handler->Component.operations->Pulumi.Output.apply(_ => ()),
|
|
103
|
+
~finish=SpecificSideEffectHandler.finish,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
96
107
|
let taskActionsHandler = (taskActions, operations: option<SideEffectHandler.operations>) => {
|
|
97
108
|
taskActions
|
|
98
109
|
->Array.map(async taskAction => {
|
|
@@ -12,6 +12,7 @@ import * as Component$ReventlessCore from "../Component.res.mjs";
|
|
|
12
12
|
import * as Util_Promise$ReventlessCore from "../../util/Util_Promise.res.mjs";
|
|
13
13
|
import * as ComponentType$ReventlessCore from "../../ComponentType.res.mjs";
|
|
14
14
|
import * as RuntimeHints$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/RuntimeHints.res.mjs";
|
|
15
|
+
import * as Builder_Helpers$ReventlessCore from "../Builder_Helpers.res.mjs";
|
|
15
16
|
|
|
16
17
|
let log = Logger$ReventlessCore.fromEnv();
|
|
17
18
|
|
|
@@ -40,6 +41,7 @@ function Make(Spec) {
|
|
|
40
41
|
let publishCommands = (aggregateName, cmdJsons) => Stdlib_Option.getOrThrow(publishToAggregates[aggregateName], undefined)(cmdJsons);
|
|
41
42
|
let config = Spec.setup(queryEngine, queryBucketName, opts);
|
|
42
43
|
let sideEffectHandler = Stdlib_Option.map(config.sideEffects, sideEffects => SpecificSideEffectHandler.make(extra$1, sideEffects, Aggregate$ReventlessCore.allEventTopics(allAggregates), allCommandTopics, undefined, queryEngine, scheduler, resourceNaming, undefined, undefined, undefined, opts));
|
|
44
|
+
Stdlib_Option.forEach(sideEffectHandler, handler => Builder_Helpers$ReventlessCore.registerTaskSideEffectHandler(Component$ReventlessCore.operations(handler).apply(param => {}), SpecificSideEffectHandler.finish));
|
|
43
45
|
let taskActionsHandler = (taskActions, operations) => Util_Promise$ReventlessCore.toUnit(Promise.all(taskActions.map(async taskAction => {
|
|
44
46
|
switch (taskAction.TAG) {
|
|
45
47
|
case "PublishCommands" :
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Heuristic detection of fields that look like stored-object references but
|
|
2
|
+
// carry no `@storageRef` declaration.
|
|
3
|
+
//
|
|
4
|
+
// Declarations are authoritative: `Plugin_Structure` collects them into
|
|
5
|
+
// `requiredStores`, the capability manifest renders them, and the platform
|
|
6
|
+
// provisions from the union. This module is the second rung — the UI's
|
|
7
|
+
// field-name heuristics restated over the same schema walk — and it is a lint,
|
|
8
|
+
// not a mechanism: a heuristic match produces a warning naming the field and
|
|
9
|
+
// the annotation that would settle it, and never provisions anything. Guessing
|
|
10
|
+
// is a poor basis for creating and destroying infrastructure; a field named
|
|
11
|
+
// `imageUrl` is genuinely ambiguous between an uploaded object and an external
|
|
12
|
+
// URL, and only its author can say which.
|
|
13
|
+
|
|
14
|
+
/** One heuristic-only match: a field whose name says "stored object" on a
|
|
15
|
+
component whose schema does not declare a store for it. */
|
|
16
|
+
type warning = {component: string, field: string}
|
|
17
|
+
|
|
18
|
+
// The two name rules the UI's semantic layer applies, restated here so the
|
|
19
|
+
// lint warns about exactly the fields the UI would silently upgrade to an
|
|
20
|
+
// upload input if an endpoint happened to exist. Matched case-insensitively
|
|
21
|
+
// and exactly (suffix rules aside): `customerFile` names a customer's file,
|
|
22
|
+
// not necessarily a stored object.
|
|
23
|
+
let refLikeNames = ["file", "attachment", "upload"]
|
|
24
|
+
let refLikeSuffixes = ["storageref", "fileref", "attachmentref"]
|
|
25
|
+
let imageLikeNames = ["image", "imageurl", "photo", "photourl", "avatar", "avatarurl", "thumbnail"]
|
|
26
|
+
|
|
27
|
+
let nameMatches = (field: string): bool => {
|
|
28
|
+
let lower = field->String.toLowerCase
|
|
29
|
+
refLikeNames->Array.includes(lower) ||
|
|
30
|
+
imageLikeNames->Array.includes(lower) ||
|
|
31
|
+
refLikeSuffixes->Array.some(suffix => lower->String.endsWith(suffix))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let warningsFromProperties = (~component, properties: dict<S.t<unknown>>): array<warning> =>
|
|
35
|
+
properties
|
|
36
|
+
->Dict.toArray
|
|
37
|
+
->Array.filterMap(((field, fieldSchema)) =>
|
|
38
|
+
switch fieldSchema {
|
|
39
|
+
| String(_) if field != "TAG" &&
|
|
40
|
+
nameMatches(field) &&
|
|
41
|
+
Reventless.StorageRef.getStore(fieldSchema)->Option.isNone =>
|
|
42
|
+
Some({component, field})
|
|
43
|
+
| _ => None
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
/** Scan one component schema — a command/event union or a state object — for
|
|
48
|
+
heuristic-only matches. The walk mirrors `Plugin_Structure`'s declared-store
|
|
49
|
+
walk over the same schemas, so a field is either collected there (declared)
|
|
50
|
+
or eligible to warn here, never both. */
|
|
51
|
+
let scanSchema = (~component: string, schema: S.t<unknown>): array<warning> => {
|
|
52
|
+
let fromVariant = v =>
|
|
53
|
+
switch v {
|
|
54
|
+
| S.Object({properties}) => warningsFromProperties(~component, properties)
|
|
55
|
+
| _ => []
|
|
56
|
+
}
|
|
57
|
+
switch schema {
|
|
58
|
+
| Union({anyOf}) => anyOf->Array.flatMap(fromVariant)
|
|
59
|
+
| other => fromVariant(other)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The warning text: name the field, name the annotation that would settle it,
|
|
64
|
+
and say what to do when the heuristic is wrong. */
|
|
65
|
+
let message = (w: warning): string =>
|
|
66
|
+
`${w.component}.${w.field} looks like a stored-object reference but declares no store — ` ++
|
|
67
|
+
`annotate it \`@storageRef("<store>")\` so the deployment provisions the store it needs, ` ++
|
|
68
|
+
`or rename the field if it holds an external URL. Heuristic matches are never provisioned.`
|
|
@@ -0,0 +1,79 @@
|
|
|
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 Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
6
|
+
|
|
7
|
+
let refLikeNames = [
|
|
8
|
+
"file",
|
|
9
|
+
"attachment",
|
|
10
|
+
"upload"
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
let refLikeSuffixes = [
|
|
14
|
+
"storageref",
|
|
15
|
+
"fileref",
|
|
16
|
+
"attachmentref"
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
let imageLikeNames = [
|
|
20
|
+
"image",
|
|
21
|
+
"imageurl",
|
|
22
|
+
"photo",
|
|
23
|
+
"photourl",
|
|
24
|
+
"avatar",
|
|
25
|
+
"avatarurl",
|
|
26
|
+
"thumbnail"
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function nameMatches(field) {
|
|
30
|
+
let lower = field.toLowerCase();
|
|
31
|
+
if (refLikeNames.includes(lower) || imageLikeNames.includes(lower)) {
|
|
32
|
+
return true;
|
|
33
|
+
} else {
|
|
34
|
+
return refLikeSuffixes.some(suffix => lower.endsWith(suffix));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function warningsFromProperties(component, properties) {
|
|
39
|
+
return Stdlib_Array.filterMap(Object.entries(properties), param => {
|
|
40
|
+
let fieldSchema = param[1];
|
|
41
|
+
let field = param[0];
|
|
42
|
+
if (fieldSchema.type === "string" && field !== "TAG" && nameMatches(field) && Stdlib_Option.isNone(StorageRef$Reventless.getStore(fieldSchema))) {
|
|
43
|
+
return {
|
|
44
|
+
component: component,
|
|
45
|
+
field: field
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scanSchema(component, schema) {
|
|
52
|
+
let fromVariant = v => {
|
|
53
|
+
if (v.type === "object") {
|
|
54
|
+
return warningsFromProperties(component, v.properties);
|
|
55
|
+
} else {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
if (schema.type === "union") {
|
|
60
|
+
return schema.anyOf.flatMap(fromVariant);
|
|
61
|
+
} else {
|
|
62
|
+
return fromVariant(schema);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function message(w) {
|
|
67
|
+
return w.component + `.` + w.field + ` looks like a stored-object reference but declares no store — annotate it \`@storageRef("<store>")\` so the deployment provisions the store it needs, or rename the field if it holds an external URL. Heuristic matches are never provisioned.`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
refLikeNames,
|
|
72
|
+
refLikeSuffixes,
|
|
73
|
+
imageLikeNames,
|
|
74
|
+
nameMatches,
|
|
75
|
+
warningsFromProperties,
|
|
76
|
+
scanSchema,
|
|
77
|
+
message,
|
|
78
|
+
}
|
|
79
|
+
/* StorageRef-Reventless Not a pure module */
|
|
@@ -434,6 +434,8 @@ let createTasks = (
|
|
|
434
434
|
~opts=Some(opts),
|
|
435
435
|
))
|
|
436
436
|
)
|
|
437
|
+
// Every task is constructed, so every side-effect handler has registered.
|
|
438
|
+
finishTasks()
|
|
437
439
|
tasksOutputs.contents
|
|
438
440
|
}
|
|
439
441
|
|
|
@@ -1246,23 +1248,32 @@ let serializeTasksOutputs = (pluginOutputs: Plugin.outputs): Pulumi.Output.t<JSO
|
|
|
1246
1248
|
// Serialize event mappers from aggregates to JSON array for the "eventMappers" stack export.
|
|
1247
1249
|
let serializeEventMappersOutputs = (pluginOutputs: Plugin.outputs): Pulumi.Output.t<JSON.t> =>
|
|
1248
1250
|
pluginOutputs.aggregates->Pulumi.Output.flatMap(aggregates =>
|
|
1251
|
+
// Resolve each `eventMapper` through its own Output before collecting, and only
|
|
1252
|
+
// then drop the absent ones. `Output.all` deeply unwraps the *contents* of its
|
|
1253
|
+
// elements, so collecting first would hand `toResolvedOutputs` a record whose
|
|
1254
|
+
// fields Pulumi has already resolved — see `serializePlainDictExport` above for
|
|
1255
|
+
// the same hazard. Absence is carried as `None` through the collection instead.
|
|
1249
1256
|
aggregates
|
|
1250
1257
|
->Dict.valuesToArray
|
|
1251
|
-
->Array.
|
|
1252
|
-
agg.eventMapper->
|
|
1253
|
-
|
|
1258
|
+
->Array.map((agg: Aggregate.outputs) =>
|
|
1259
|
+
agg.eventMapper->Pulumi.Output.flatMap(em =>
|
|
1260
|
+
switch em {
|
|
1261
|
+
| Some(em) =>
|
|
1254
1262
|
em
|
|
1255
1263
|
->EventMapper.toResolvedOutputs
|
|
1256
1264
|
->Pulumi.Output.apply(resolved =>
|
|
1257
|
-
|
|
1258
|
-
|
|
1265
|
+
Some(
|
|
1266
|
+
resolved->S.reverseConvertToJsonOrThrow(
|
|
1267
|
+
ReventlessInterop.EventMapper.resolvedOutputsSchema,
|
|
1268
|
+
),
|
|
1259
1269
|
)
|
|
1260
1270
|
)
|
|
1261
|
-
)
|
|
1271
|
+
| None => Pulumi.Output.make(None)
|
|
1272
|
+
}
|
|
1262
1273
|
)
|
|
1263
1274
|
)
|
|
1264
1275
|
->Pulumi.Output.all
|
|
1265
|
-
->Pulumi.Output.apply(arr => arr->JSON.Encode.array)
|
|
1276
|
+
->Pulumi.Output.apply(arr => arr->Array.keepSome->JSON.Encode.array)
|
|
1266
1277
|
)
|
|
1267
1278
|
|
|
1268
1279
|
// Export platform admin component outputs as individual top-level Pulumi stack exports.
|
|
@@ -248,6 +248,7 @@ function createTasks(tasks, aggregatesOutputs, scheduler, schedulerRoleUrn, publ
|
|
|
248
248
|
let bucketName = bucketNameOpt !== undefined ? bucketNameOpt : "Bucket";
|
|
249
249
|
return ResourceQueryRuntime$ReventlessCore.bucketNameOfTaskExn(tasksOutputs.contents, taskName, bucketName);
|
|
250
250
|
}, scheduler, schedulerRoleUrn, publishToAggregates, queryEngine, resourceNaming, aggregatesOutputs, componentRuntime[SpecificTask.Spec.name], opts)));
|
|
251
|
+
Builder_Helpers$ReventlessCore.finishTasks();
|
|
251
252
|
return tasksOutputs.contents;
|
|
252
253
|
}
|
|
253
254
|
|
|
@@ -627,7 +628,13 @@ function serializeTasksOutputs(pluginOutputs) {
|
|
|
627
628
|
}
|
|
628
629
|
|
|
629
630
|
function serializeEventMappersOutputs(pluginOutputs) {
|
|
630
|
-
return Output$Pulumi.flatMap(pluginOutputs.aggregates, aggregates => Pulumi.all(
|
|
631
|
+
return Output$Pulumi.flatMap(pluginOutputs.aggregates, aggregates => Pulumi.all(Object.values(aggregates).map(agg => Output$Pulumi.flatMap(agg.eventMapper, em => {
|
|
632
|
+
if (em !== undefined) {
|
|
633
|
+
return EventMapper$ReventlessCore.toResolvedOutputs(em).apply(resolved => S.reverseConvertToJsonOrThrow(resolved, EventMapper$ReventlessInterop.resolvedOutputsSchema));
|
|
634
|
+
} else {
|
|
635
|
+
return Pulumi.output(undefined);
|
|
636
|
+
}
|
|
637
|
+
}))).apply(Stdlib_Array.keepSome));
|
|
631
638
|
}
|
|
632
639
|
|
|
633
640
|
function exportPlatformOutputs(extensionPointsOutputs, aggregatesOutputs, readModelsOutputs, dcbEventLogOutputs, stateChangeSlicesOutputs, stateViewSlicesOutputs, automationSlicesOutputs, outboundTranslationSlicesOutputs, inboundTranslationSlicesOutputs) {
|
|
@@ -913,6 +920,14 @@ let finishAggregates = Builder_Helpers$ReventlessCore.finishAggregates;
|
|
|
913
920
|
|
|
914
921
|
let addEventMappers = Builder_Helpers$ReventlessCore.addEventMappers;
|
|
915
922
|
|
|
923
|
+
let taskSideEffectGates = Builder_Helpers$ReventlessCore.taskSideEffectGates;
|
|
924
|
+
|
|
925
|
+
let taskSideEffectFinishFns = Builder_Helpers$ReventlessCore.taskSideEffectFinishFns;
|
|
926
|
+
|
|
927
|
+
let registerTaskSideEffectHandler = Builder_Helpers$ReventlessCore.registerTaskSideEffectHandler;
|
|
928
|
+
|
|
929
|
+
let finishTasks = Builder_Helpers$ReventlessCore.finishTasks;
|
|
930
|
+
|
|
916
931
|
let readModelNamesForSourceName = Builder_Helpers$ReventlessCore.readModelNamesForSourceName;
|
|
917
932
|
|
|
918
933
|
let publishToReadModels = Builder_Helpers$ReventlessCore.publishToReadModels;
|
|
@@ -968,6 +983,10 @@ export {
|
|
|
968
983
|
createAggregatesWithoutEventMappers,
|
|
969
984
|
finishAggregates,
|
|
970
985
|
addEventMappers,
|
|
986
|
+
taskSideEffectGates,
|
|
987
|
+
taskSideEffectFinishFns,
|
|
988
|
+
registerTaskSideEffectHandler,
|
|
989
|
+
finishTasks,
|
|
971
990
|
readModelNamesForSourceName,
|
|
972
991
|
publishToReadModels,
|
|
973
992
|
finishReadModels,
|