@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.
Files changed (30) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +7 -7
  3. package/src/admin/Platform_Admin.res.mjs +4 -0
  4. package/src/admin/Platform_Admin_Structure.res +1 -0
  5. package/src/admin/Platform_Admin_Structure.res.mjs +2 -1
  6. package/src/components/Aggregate/Aggregate.res +7 -7
  7. package/src/components/Aggregate/Aggregate.res.mjs +7 -2
  8. package/src/components/Aggregate/Aggregate_Builder.res +4 -1
  9. package/src/components/Aggregate/Aggregate_Builder.res.mjs +11 -3
  10. package/src/components/Builder_Helpers.res +37 -4
  11. package/src/components/Builder_Helpers.res.mjs +24 -2
  12. package/src/components/SideEffectHandler/SideEffectHandler.res +11 -0
  13. package/src/components/SideEffectHandler/SideEffectHandler_Builder.res +2 -0
  14. package/src/components/SideEffectHandler/SideEffectHandler_Builder.res.mjs +3 -1
  15. package/src/components/Task/Task_Builder.res +11 -0
  16. package/src/components/Task/Task_Builder.res.mjs +2 -0
  17. package/src/plugin/component/Capability_Inference.res +68 -0
  18. package/src/plugin/component/Capability_Inference.res.mjs +79 -0
  19. package/src/plugin/component/Plugin_Helpers.res +18 -7
  20. package/src/plugin/component/Plugin_Helpers.res.mjs +20 -1
  21. package/src/plugin/component/Plugin_Structure.res +91 -34
  22. package/src/plugin/component/Plugin_Structure.res.mjs +82 -12
  23. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +2 -0
  24. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +8 -4
  25. package/tests/message/MessageTest.res +1 -0
  26. package/tests/message/MessageTest.res.mjs +2 -1
  27. package/tests/plugin/PluginStructureTest.res +145 -0
  28. package/tests/plugin/PluginStructureTest.res.mjs +138 -0
  29. package/tests/plugin/StateChangeSlice/PsChangePhoto.res +33 -0
  30. 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.191",
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-jest": "1.0.0-alpha.9",
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-ssh2": "1.1.0-alpha.14",
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.109",
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.84"
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,
@@ -130,4 +130,5 @@ let structure: pluginStructure = {
130
130
  extensions: [],
131
131
  extensionPoints: None,
132
132
  requiredStores: None,
133
+ requiredStoreDeclarations: None,
133
134
  }
@@ -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 = switch outputs.eventMapper {
59
- | Some(emOutput) =>
60
- emOutput
61
- ->Pulumi.Output.flatMap(EventMapper.toResolvedOutputs)
62
- ->Pulumi.Output.apply(resolved => Some(resolved))
63
- | None => Pulumi.Output.make(None)
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 emOutput = outputs.eventMapper;
43
- let eventMapperResolved = emOutput !== undefined ? Output$Pulumi.flatMap(emOutput, EventMapper$ReventlessCore.toResolvedOutputs).apply(resolved => resolved) : Pulumi.output(undefined);
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 newrecord = {...Component$ReventlessCore.outputs(extra)};
101
- newrecord.eventMapper = eventMapper.apply(Component$ReventlessCore.outputs);
102
- return newrecord;
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 = Stdlib_Array.keepSome(allOutputs.map(aggregateOutputs => aggregateOutputs.eventMapper));
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
  }
@@ -122,4 +122,6 @@ module Make = (
122
122
  ~opts,
123
123
  )
124
124
  }
125
+
126
+ let finish = () => EventCollectorRuntimeBuilder.finish()
125
127
  }
@@ -55,8 +55,10 @@ function Make(RuntimeEnvironment) {
55
55
  });
56
56
  }, opts);
57
57
  };
58
+ let finish = () => EventCollectorRuntimeBuilder.finish();
58
59
  return {
59
- make: make
60
+ make: make,
61
+ finish: finish
60
62
  };
61
63
  }));
62
64
  }
@@ -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.filterMap((agg: Aggregate.outputs) =>
1252
- agg.eventMapper->Option.map(eventMapperOutput =>
1253
- eventMapperOutput->Pulumi.Output.flatMap(em =>
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
- resolved->S.reverseConvertToJsonOrThrow(
1258
- ReventlessInterop.EventMapper.resolvedOutputsSchema,
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(Stdlib_Array.filterMap(Object.values(aggregates), agg => Stdlib_Option.map(agg.eventMapper, eventMapperOutput => Output$Pulumi.flatMap(eventMapperOutput, em => EventMapper$ReventlessCore.toResolvedOutputs(em).apply(resolved => S.reverseConvertToJsonOrThrow(resolved, EventMapper$ReventlessInterop.resolvedOutputsSchema)))))).apply(arr => arr));
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,