@reventlessdev/reventless-core 3.0.0-alpha.190 → 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 +22 -0
- package/package.json +5 -5
- package/src/admin/Platform_Admin.res +2 -2
- package/src/admin/Platform_Admin.res.mjs +6 -2
- package/src/admin/Platform_Admin_Structure.res +4 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +5 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res +5 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -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/Dcb/Dcb_Builder.res +2 -2
- package/src/components/Dcb/Dcb_Builder.res.mjs +4 -4
- 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_Builder.res +2 -2
- package/src/plugin/component/Plugin_Builder.res.mjs +4 -4
- 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 +215 -79
- package/src/plugin/component/Plugin_Structure.res.mjs +195 -50
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +5 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +15 -4
- package/tests/message/MessageTest.res +1 -0
- package/tests/message/MessageTest.res.mjs +2 -1
- package/tests/plugin/PluginStructureTest.res +391 -0
- package/tests/plugin/PluginStructureTest.res.mjs +355 -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,28 @@
|
|
|
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
|
+
|
|
18
|
+
# 3.0.0-alpha.191 (2026-07-29)
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **plugin:** the field a read model is named by ([c679bb9](https://github.com/ReventlessDev/reventless-core/commit/c679bb9f610be9947417174bc3dfceea38207ba0))
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* **plugin:** why a read model is named by that field ([699ef8e](https://github.com/ReventlessDev/reventless-core/commit/699ef8e72f1b1d82fb04672739e5e4a8db61ae9a))
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
# 3.0.0-alpha.190 (2026-07-28)
|
|
7
29
|
|
|
8
30
|
### 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.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
31
|
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
|
|
33
32
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
|
|
34
|
-
"@reventlessdev/rescript-
|
|
33
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.31",
|
|
35
34
|
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
|
|
35
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
37
37
|
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
|
|
38
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.108",
|
|
39
38
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
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",
|
|
@@ -252,7 +252,7 @@ module Make = (
|
|
|
252
252
|
// pushed SDL — CreateResolver then loops on
|
|
253
253
|
// `NotFoundException: No field named ...`.
|
|
254
254
|
let registryKey = entry.specName->Option.getOr(entityName)
|
|
255
|
-
let
|
|
255
|
+
let label: Plugin_Structure.labelResolution = Plugin_Structure.labelFieldsFromStateSchema(
|
|
256
256
|
~entityName,
|
|
257
257
|
entry.stateSchema,
|
|
258
258
|
)
|
|
@@ -263,7 +263,7 @@ module Make = (
|
|
|
263
263
|
pluralTypeName: entry.listFieldName,
|
|
264
264
|
includeIdParam: entry.includeIdParam->Option.getOr(true),
|
|
265
265
|
connectionSpec: entry.connectionSpec->Option.getOr(true),
|
|
266
|
-
labelField,
|
|
266
|
+
labelField: label.field,
|
|
267
267
|
connectionFilterTypeName: entry.returnTypeName ++ "Filter",
|
|
268
268
|
}
|
|
269
269
|
Plugin_Helpers.queryFieldNamesRegistry->Dict.set(registryKey, qn)
|
|
@@ -110,13 +110,13 @@ function Make(RuntimeEnvironment) {
|
|
|
110
110
|
let prefix = "Platform_";
|
|
111
111
|
let entityName = entry.returnTypeName.startsWith(prefix) ? entry.returnTypeName.slice(prefix.length, entry.returnTypeName.length) : entry.returnTypeName;
|
|
112
112
|
let registryKey = Stdlib_Option.getOr(entry.specName, entityName);
|
|
113
|
-
let
|
|
113
|
+
let label = Plugin_Structure$ReventlessCore.labelFieldsFromStateSchema(entityName, entry.stateSchema);
|
|
114
114
|
let qn_singleFieldName = entry.singleFieldName;
|
|
115
115
|
let qn_listFieldName = entry.listFieldName;
|
|
116
116
|
let qn_returnTypeName = entry.returnTypeName;
|
|
117
117
|
let qn_pluralTypeName = entry.listFieldName;
|
|
118
118
|
let qn_connectionFilterTypeName = entry.returnTypeName + "Filter";
|
|
119
|
-
let qn_labelField =
|
|
119
|
+
let qn_labelField = label.field;
|
|
120
120
|
let qn_includeIdParam = Stdlib_Option.getOr(entry.includeIdParam, true);
|
|
121
121
|
let qn_connectionSpec = Stdlib_Option.getOr(entry.connectionSpec, true);
|
|
122
122
|
let qn = {
|
|
@@ -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,
|
|
@@ -111,6 +111,9 @@ let pluginReadModel: queryableDef = {
|
|
|
111
111
|
linkedWriteSide: ["Plugin"],
|
|
112
112
|
labelField: "name",
|
|
113
113
|
searchableFields: ["name"],
|
|
114
|
+
// Hand-rolled rather than resolved, but the rung is the same one
|
|
115
|
+
// `labelFieldsFromStateSchema` would report for a field literally named `name`.
|
|
116
|
+
labelFieldSource: Some("convention"),
|
|
114
117
|
statusField: Some("status"),
|
|
115
118
|
visibility: None,
|
|
116
119
|
chapter: None,
|
|
@@ -127,4 +130,5 @@ let structure: pluginStructure = {
|
|
|
127
130
|
extensions: [],
|
|
128
131
|
extensionPoints: None,
|
|
129
132
|
requiredStores: None,
|
|
133
|
+
requiredStoreDeclarations: None,
|
|
130
134
|
}
|
|
@@ -126,6 +126,8 @@ let pluginReadModel_linkedWriteSide = ["Plugin"];
|
|
|
126
126
|
|
|
127
127
|
let pluginReadModel_searchableFields = ["name"];
|
|
128
128
|
|
|
129
|
+
let pluginReadModel_labelFieldSource = "convention";
|
|
130
|
+
|
|
129
131
|
let pluginReadModel_statusField = "status";
|
|
130
132
|
|
|
131
133
|
let pluginReadModel = {
|
|
@@ -136,6 +138,7 @@ let pluginReadModel = {
|
|
|
136
138
|
linkedWriteSide: pluginReadModel_linkedWriteSide,
|
|
137
139
|
labelField: "name",
|
|
138
140
|
searchableFields: pluginReadModel_searchableFields,
|
|
141
|
+
labelFieldSource: pluginReadModel_labelFieldSource,
|
|
139
142
|
statusField: pluginReadModel_statusField,
|
|
140
143
|
visibility: undefined,
|
|
141
144
|
chapter: undefined
|
|
@@ -167,7 +170,8 @@ let structure = {
|
|
|
167
170
|
inboundTranslationSlices: structure_inboundTranslationSlices,
|
|
168
171
|
extensions: structure_extensions,
|
|
169
172
|
extensionPoints: undefined,
|
|
170
|
-
requiredStores: undefined
|
|
173
|
+
requiredStores: undefined,
|
|
174
|
+
requiredStoreDeclarations: undefined
|
|
171
175
|
};
|
|
172
176
|
|
|
173
177
|
let pluginName = pluginId;
|
|
@@ -11,7 +11,7 @@ let sdlTypes: array<string> = [
|
|
|
11
11
|
`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}`,
|
|
12
12
|
`type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
13
13
|
`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}`,
|
|
14
|
-
`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 statusField: String\n}`,
|
|
14
|
+
`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}`,
|
|
15
15
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n}`,
|
|
16
16
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n}`,
|
|
17
17
|
`type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n}`,
|
|
@@ -72,6 +72,10 @@ let encodeQueryableDef = (r: queryableDef): JSON.t =>
|
|
|
72
72
|
("linkedWriteSide", encodeStrings(r.linkedWriteSide)),
|
|
73
73
|
("labelField", JSON.Encode.string(r.labelField)),
|
|
74
74
|
("searchableFields", encodeStrings(r.searchableFields)),
|
|
75
|
+
(
|
|
76
|
+
"labelFieldSource",
|
|
77
|
+
r.labelFieldSource->Option.mapOr(JSON.Encode.null, JSON.Encode.string),
|
|
78
|
+
),
|
|
75
79
|
("statusField", r.statusField->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
76
80
|
("chapter", r.chapter->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
77
81
|
])->JSON.Encode.object
|
|
@@ -9,7 +9,7 @@ let sdlTypes = [
|
|
|
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
11
|
`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}`,
|
|
12
|
-
`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 statusField: String\n}`,
|
|
12
|
+
`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}`,
|
|
13
13
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n}`,
|
|
14
14
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n}`,
|
|
15
15
|
`type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n}`,
|
|
@@ -116,6 +116,10 @@ function encodeQueryableDef(r) {
|
|
|
116
116
|
"searchableFields",
|
|
117
117
|
r.searchableFields.map(prim => prim)
|
|
118
118
|
],
|
|
119
|
+
[
|
|
120
|
+
"labelFieldSource",
|
|
121
|
+
Stdlib_Option.mapOr(r.labelFieldSource, null, prim => prim)
|
|
122
|
+
],
|
|
119
123
|
[
|
|
120
124
|
"statusField",
|
|
121
125
|
Stdlib_Option.mapOr(r.statusField, null, prim => prim)
|
|
@@ -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,
|
|
@@ -526,13 +526,13 @@ module Make = (
|
|
|
526
526
|
module(V: StateViewSlice.T),
|
|
527
527
|
) => {
|
|
528
528
|
let qn = Api_Naming.queryFieldNamesForStateView(~plugin=name, ~viewName=V.Spec.name)
|
|
529
|
-
let
|
|
529
|
+
let label = Plugin_Structure.labelFieldsFromStateSchema(
|
|
530
530
|
~entityName=V.Spec.name,
|
|
531
531
|
V.Spec.stateSchema->S.castToUnknown,
|
|
532
532
|
)
|
|
533
533
|
let qn = {
|
|
534
534
|
...qn,
|
|
535
|
-
labelField,
|
|
535
|
+
labelField: label.field,
|
|
536
536
|
connectionFilterTypeName: qn.returnTypeName ++ "Filter",
|
|
537
537
|
}
|
|
538
538
|
let qn = switch V.Spec.subIdConfig {
|
|
@@ -276,13 +276,13 @@ function Make(DcbEventLogStorage) {
|
|
|
276
276
|
}
|
|
277
277
|
stateViewSlices.forEach(V => {
|
|
278
278
|
let qn = Api_Naming$ReventlessCore.queryFieldNamesForStateView(name, V.Spec.name, undefined);
|
|
279
|
-
let
|
|
279
|
+
let label = Plugin_Structure$ReventlessCore.labelFieldsFromStateSchema(V.Spec.name, V.Spec.stateSchema);
|
|
280
280
|
let newrecord = {...qn};
|
|
281
|
-
newrecord.labelField =
|
|
281
|
+
newrecord.labelField = label.field;
|
|
282
282
|
newrecord.connectionFilterTypeName = qn.returnTypeName + "Filter";
|
|
283
|
-
let match
|
|
283
|
+
let match = V.Spec.subIdConfig;
|
|
284
284
|
let qn$1;
|
|
285
|
-
if (match
|
|
285
|
+
if (match !== undefined) {
|
|
286
286
|
let newrecord$1 = {...newrecord};
|
|
287
287
|
newrecord$1.itemsFilterTypeName = newrecord.returnTypeName + "ItemsFilter";
|
|
288
288
|
newrecord$1.itemsFieldName = newrecord.singleFieldName + "Items";
|
|
@@ -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.`
|