@reventlessdev/reventless-aws 3.0.0-alpha.167

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 (394) hide show
  1. package/CHANGELOG.md +1564 -0
  2. package/README.md +62 -0
  3. package/__mocks__/emptyModule.js +1 -0
  4. package/all-licenses.csv +565 -0
  5. package/docker-compose.dynamodb-local.yml +12 -0
  6. package/jest.setup.cjs +5 -0
  7. package/licenses-analysis.ods +0 -0
  8. package/package.json +73 -0
  9. package/rescript.json +39 -0
  10. package/reventless-licenses-analysis.xls +0 -0
  11. package/scripts/run-integration-tests.sh +43 -0
  12. package/src/Platform.res +1940 -0
  13. package/src/Platform.res.mjs +2281 -0
  14. package/src/Platform_Casts.res +106 -0
  15. package/src/Platform_Casts.res.mjs +13 -0
  16. package/src/Platform_Stack.res +153 -0
  17. package/src/Platform_Stack.res.mjs +121 -0
  18. package/src/Types.res +39 -0
  19. package/src/Types.res.mjs +21 -0
  20. package/src/adapter/AWS.res +96 -0
  21. package/src/adapter/AWS.res.mjs +186 -0
  22. package/src/adapter/AWS_Tags.res +16 -0
  23. package/src/adapter/AWS_Tags.res.mjs +48 -0
  24. package/src/adapter/Adapter_Helpers.res +19 -0
  25. package/src/adapter/Adapter_Helpers.res.mjs +37 -0
  26. package/src/adapter/Api/AppSync_EventsApi.res +172 -0
  27. package/src/adapter/Api/AppSync_EventsApi.res.mjs +96 -0
  28. package/src/adapter/Api/AppSync_Resolver_Native.res +93 -0
  29. package/src/adapter/Api/AppSync_Resolver_Native.res.mjs +67 -0
  30. package/src/adapter/Api/AppSync_Resolver_Retrying.res +653 -0
  31. package/src/adapter/Api/AppSync_Resolver_Retrying.res.mjs +462 -0
  32. package/src/adapter/Api/CommandSubscriptionResolvers_AppSync.res +29 -0
  33. package/src/adapter/Api/CommandSubscriptionResolvers_AppSync.res.mjs +15 -0
  34. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +238 -0
  35. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +179 -0
  36. package/src/adapter/Api/Platform_UIFragments_Lambda.res +229 -0
  37. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +245 -0
  38. package/src/adapter/Auth/Auth_Cognito.res +209 -0
  39. package/src/adapter/Auth/Auth_Cognito.res.mjs +135 -0
  40. package/src/adapter/CommandGenerator/CommandGeneratorResolvers.res +2 -0
  41. package/src/adapter/CommandGenerator/CommandGeneratorResolvers.res.mjs +12 -0
  42. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res +251 -0
  43. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res.mjs +126 -0
  44. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync_NoOp.res +20 -0
  45. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync_NoOp.res.mjs +23 -0
  46. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync_Runtime.res +1 -0
  47. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync_Runtime.res.mjs +11 -0
  48. package/src/adapter/CommandGenerator/InboundTranslationResolvers_AppSync.res +95 -0
  49. package/src/adapter/CommandGenerator/InboundTranslationResolvers_AppSync.res.mjs +50 -0
  50. package/src/adapter/CommandTopic/CommandTopicChannel.res +6 -0
  51. package/src/adapter/CommandTopic/CommandTopicChannel.res.mjs +21 -0
  52. package/src/adapter/CommandTopic/CommandTopicChannel_Helpers.res +196 -0
  53. package/src/adapter/CommandTopic/CommandTopicChannel_Helpers.res.mjs +145 -0
  54. package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res +91 -0
  55. package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res.mjs +74 -0
  56. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res +69 -0
  57. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs +62 -0
  58. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res +60 -0
  59. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res.mjs +56 -0
  60. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res +73 -0
  61. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs +65 -0
  62. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res +88 -0
  63. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs +76 -0
  64. package/src/adapter/CommandTopic/CommandTopicRemoteChannel.res +1 -0
  65. package/src/adapter/CommandTopic/CommandTopicRemoteChannel.res.mjs +9 -0
  66. package/src/adapter/CommandTopic/CommandTopicRemoteChannel_SQS.res +7 -0
  67. package/src/adapter/CommandTopic/CommandTopicRemoteChannel_SQS.res.mjs +17 -0
  68. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res +103 -0
  69. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +68 -0
  70. package/src/adapter/Counter/CounterHandler_DynamoDbStream_Runtime.res +107 -0
  71. package/src/adapter/Counter/CounterHandler_DynamoDbStream_Runtime.res.mjs +145 -0
  72. package/src/adapter/DcbEventLog/DcbEventLogStorage.res +1 -0
  73. package/src/adapter/DcbEventLog/DcbEventLogStorage.res.mjs +9 -0
  74. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res +84 -0
  75. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res.mjs +52 -0
  76. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +1436 -0
  77. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +1143 -0
  78. package/src/adapter/EventCollector/EventCollectorChannel.res +6 -0
  79. package/src/adapter/EventCollector/EventCollectorChannel.res.mjs +21 -0
  80. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res +63 -0
  81. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs +56 -0
  82. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res +20 -0
  83. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs +32 -0
  84. package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res +266 -0
  85. package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res.mjs +166 -0
  86. package/src/adapter/EventCollector/EventCollectorChannel_SQS.res +81 -0
  87. package/src/adapter/EventCollector/EventCollectorChannel_SQS.res.mjs +54 -0
  88. package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res +59 -0
  89. package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res.mjs +45 -0
  90. package/src/adapter/EventCollector/EventCollectorChannel_SQS_Runtime.res +91 -0
  91. package/src/adapter/EventCollector/EventCollectorChannel_SQS_Runtime.res.mjs +89 -0
  92. package/src/adapter/EventLog/EventLogStorage.res +3 -0
  93. package/src/adapter/EventLog/EventLogStorage.res.mjs +15 -0
  94. package/src/adapter/EventLog/EventLogStorage_DynamoDb.res +25 -0
  95. package/src/adapter/EventLog/EventLogStorage_DynamoDb.res.mjs +34 -0
  96. package/src/adapter/EventLog/EventLogStorage_DynamoDbStream.res +26 -0
  97. package/src/adapter/EventLog/EventLogStorage_DynamoDbStream.res.mjs +35 -0
  98. package/src/adapter/EventLog/EventLogStorage_DynamoDb_Runtime.res +121 -0
  99. package/src/adapter/EventLog/EventLogStorage_DynamoDb_Runtime.res.mjs +137 -0
  100. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +265 -0
  101. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +185 -0
  102. package/src/adapter/EventTopic/EventTopicPublisher.res +6 -0
  103. package/src/adapter/EventTopic/EventTopicPublisher.res.mjs +21 -0
  104. package/src/adapter/EventTopic/EventTopicPublisher_DynamoDbStream.res +25 -0
  105. package/src/adapter/EventTopic/EventTopicPublisher_DynamoDbStream.res.mjs +28 -0
  106. package/src/adapter/EventTopic/EventTopicPublisher_SNS.res +40 -0
  107. package/src/adapter/EventTopic/EventTopicPublisher_SNS.res.mjs +39 -0
  108. package/src/adapter/EventTopic/EventTopicPublisher_SNS_FIFO.res +39 -0
  109. package/src/adapter/EventTopic/EventTopicPublisher_SNS_FIFO.res.mjs +38 -0
  110. package/src/adapter/EventTopic/EventTopicPublisher_SNS_Runtime.res +5 -0
  111. package/src/adapter/EventTopic/EventTopicPublisher_SNS_Runtime.res.mjs +18 -0
  112. package/src/adapter/Mcp/MCP_Lambda.res +502 -0
  113. package/src/adapter/Mcp/MCP_Lambda.res.mjs +289 -0
  114. package/src/adapter/QueryDb/NodeResolver_AppSync.res +71 -0
  115. package/src/adapter/QueryDb/NodeResolver_AppSync.res.mjs +44 -0
  116. package/src/adapter/QueryDb/QueryDbResolvers.res +2 -0
  117. package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +12 -0
  118. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +434 -0
  119. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +243 -0
  120. package/src/adapter/QueryDb/QueryDbResolvers_AppSync_NoOp.res +21 -0
  121. package/src/adapter/QueryDb/QueryDbResolvers_AppSync_NoOp.res.mjs +14 -0
  122. package/src/adapter/QueryDb/QueryDbResolvers_NoOp.res +21 -0
  123. package/src/adapter/QueryDb/QueryDbResolvers_NoOp.res.mjs +14 -0
  124. package/src/adapter/QueryDb/QueryDbStorage.res +5 -0
  125. package/src/adapter/QueryDb/QueryDbStorage.res.mjs +18 -0
  126. package/src/adapter/QueryDb/QueryDbStorage_DynamoDb.res +115 -0
  127. package/src/adapter/QueryDb/QueryDbStorage_DynamoDb.res.mjs +100 -0
  128. package/src/adapter/QueryDb/QueryDbStorage_DynamoDbStream.res +52 -0
  129. package/src/adapter/QueryDb/QueryDbStorage_DynamoDbStream.res.mjs +38 -0
  130. package/src/adapter/QueryDb/QueryDbStorage_DynamoDb_Runtime.res +271 -0
  131. package/src/adapter/QueryDb/QueryDbStorage_DynamoDb_Runtime.res.mjs +306 -0
  132. package/src/adapter/QueryDb/QueryInterceptor_Lambda.res +20 -0
  133. package/src/adapter/QueryDb/QueryInterceptor_Lambda.res.mjs +22 -0
  134. package/src/adapter/QueryEngine/QueryEngine.res +3 -0
  135. package/src/adapter/QueryEngine/QueryEngine.res.mjs +12 -0
  136. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +184 -0
  137. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs +221 -0
  138. package/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs +879 -0
  139. package/src/adapter/Runtime/AggregateEntryPoint.mjs +313 -0
  140. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res +327 -0
  141. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +239 -0
  142. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res +331 -0
  143. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res.mjs +240 -0
  144. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res +247 -0
  145. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res.mjs +185 -0
  146. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res +244 -0
  147. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res.mjs +185 -0
  148. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +347 -0
  149. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +244 -0
  150. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +285 -0
  151. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +212 -0
  152. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +124 -0
  153. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +187 -0
  154. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +147 -0
  155. package/src/adapter/Runtime/CounterEntryPoint.mjs +91 -0
  156. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +265 -0
  157. package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res +122 -0
  158. package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +89 -0
  159. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +193 -0
  160. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +137 -0
  161. package/src/adapter/Runtime/EventMapperEntryPoint.mjs +97 -0
  162. package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +101 -0
  163. package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res +83 -0
  164. package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +59 -0
  165. package/src/adapter/Runtime/HandlerFactoryHelpers.mjs +142 -0
  166. package/src/adapter/Runtime/HeartbeatEntryPoint.mjs +38 -0
  167. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs +137 -0
  168. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +143 -0
  169. package/src/adapter/Runtime/RuntimeEnvironment.res +1 -0
  170. package/src/adapter/Runtime/RuntimeEnvironment.res.mjs +9 -0
  171. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +254 -0
  172. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +169 -0
  173. package/src/adapter/Runtime/SideEffectEntryPoint.mjs +81 -0
  174. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +178 -0
  175. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +129 -0
  176. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +166 -0
  177. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +87 -0
  178. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +136 -0
  179. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +344 -0
  180. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +242 -0
  181. package/src/adapter/Runtime/TaskBucketEntryPoint.mjs +150 -0
  182. package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res +123 -0
  183. package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +74 -0
  184. package/src/adapter/ScheduledPublisher/ScheduledPublisher.res +2 -0
  185. package/src/adapter/ScheduledPublisher/ScheduledPublisher.res.mjs +12 -0
  186. package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents.res +54 -0
  187. package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents.res.mjs +41 -0
  188. package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents_Runtime.res +97 -0
  189. package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents_Runtime.res.mjs +85 -0
  190. package/src/adapter/StateTopic/StateTopic_AppSync.res +418 -0
  191. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +295 -0
  192. package/src/adapter/Task/TaskBucket.res +1 -0
  193. package/src/adapter/Task/TaskBucket.res.mjs +9 -0
  194. package/src/adapter/Task/TaskBucket_S3.res +157 -0
  195. package/src/adapter/Task/TaskBucket_S3.res.mjs +108 -0
  196. package/src/adapter/Task/TaskBucket_S3_Runtime.res +11 -0
  197. package/src/adapter/Task/TaskBucket_S3_Runtime.res.mjs +15 -0
  198. package/src/components/Aggregate_Builder.res +1 -0
  199. package/src/components/Aggregate_Builder.res.mjs +25 -0
  200. package/src/components/Aggregate_Builder_Micro.res +63 -0
  201. package/src/components/Aggregate_Builder_Micro.res.mjs +80 -0
  202. package/src/components/Aggregate_Builder_NoResolver.res +61 -0
  203. package/src/components/Aggregate_Builder_NoResolver.res.mjs +80 -0
  204. package/src/components/Aggregate_Builder_PerAggregate.res +57 -0
  205. package/src/components/Aggregate_Builder_PerAggregate.res.mjs +80 -0
  206. package/src/components/Aggregate_Builder_Single.res +59 -0
  207. package/src/components/Aggregate_Builder_Single.res.mjs +80 -0
  208. package/src/components/Aggregate_Builder_Single_Async.res +59 -0
  209. package/src/components/Aggregate_Builder_Single_Async.res.mjs +80 -0
  210. package/src/components/Api/AppSync_Adapter.res +500 -0
  211. package/src/components/Api/AppSync_Adapter.res.mjs +396 -0
  212. package/src/components/AutomationSlice_Builder.res +52 -0
  213. package/src/components/AutomationSlice_Builder.res.mjs +72 -0
  214. package/src/components/AutomationSlice_Builder_Bundled.res.mjs +70 -0
  215. package/src/components/Counter_Builder.res +10 -0
  216. package/src/components/Counter_Builder.res.mjs +16 -0
  217. package/src/components/DataCleaner.res +170 -0
  218. package/src/components/DataCleaner.res.mjs +2 -0
  219. package/src/components/DcbEventLog_Builder.res +8 -0
  220. package/src/components/DcbEventLog_Builder.res.mjs +14 -0
  221. package/src/components/ExtensionPoint_Builder.res +16 -0
  222. package/src/components/ExtensionPoint_Builder.res.mjs +37 -0
  223. package/src/components/ForeignReadModel_Builder.res +19 -0
  224. package/src/components/ForeignReadModel_Builder.res.mjs +48 -0
  225. package/src/components/InboundTranslationSlice_Builder.res +25 -0
  226. package/src/components/InboundTranslationSlice_Builder.res.mjs +31 -0
  227. package/src/components/OutboundTranslationSlice_Builder.res +60 -0
  228. package/src/components/OutboundTranslationSlice_Builder.res.mjs +76 -0
  229. package/src/components/OutboundTranslationSlice_Builder_Bundled.res.mjs +75 -0
  230. package/src/components/Plugin.res +29 -0
  231. package/src/components/Plugin.res.mjs +63 -0
  232. package/src/components/ReadModel_Builder.res +1 -0
  233. package/src/components/ReadModel_Builder.res.mjs +19 -0
  234. package/src/components/ReadModel_Builder_NoResolver.res +55 -0
  235. package/src/components/ReadModel_Builder_NoResolver.res.mjs +64 -0
  236. package/src/components/ReadModel_Builder_NoResolver_Stream.res +60 -0
  237. package/src/components/ReadModel_Builder_NoResolver_Stream.res.mjs +64 -0
  238. package/src/components/ReadModel_Builder_PerReadModel.res +53 -0
  239. package/src/components/ReadModel_Builder_PerReadModel.res.mjs +66 -0
  240. package/src/components/ReadModel_Builder_Single.res +52 -0
  241. package/src/components/ReadModel_Builder_Single.res.mjs +66 -0
  242. package/src/components/ReadModel_Builder_Single_Stream.res +60 -0
  243. package/src/components/ReadModel_Builder_Single_Stream.res.mjs +66 -0
  244. package/src/components/Scheduler.res +1 -0
  245. package/src/components/Scheduler.res.mjs +13 -0
  246. package/src/components/SideEffectHandler.res +1 -0
  247. package/src/components/SideEffectHandler.res.mjs +19 -0
  248. package/src/components/SideEffectHandler_PerSideEffectHandler.res +13 -0
  249. package/src/components/SideEffectHandler_PerSideEffectHandler.res.mjs +52 -0
  250. package/src/components/SideEffectHandler_Single.res +54 -0
  251. package/src/components/SideEffectHandler_Single.res.mjs +60 -0
  252. package/src/components/StateChangeSlice_Builder.res +17 -0
  253. package/src/components/StateChangeSlice_Builder.res.mjs +23 -0
  254. package/src/components/StateViewSlice_Builder.res +46 -0
  255. package/src/components/StateViewSlice_Builder.res.mjs +68 -0
  256. package/src/components/StateViewSlice_Builder_Bundled.res.mjs +69 -0
  257. package/src/components/StateViewSlice_Builder_Stream.res +50 -0
  258. package/src/components/StateViewSlice_Builder_Stream.res.mjs +68 -0
  259. package/src/components/Task_Builder.res +1 -0
  260. package/src/components/Task_Builder.res.mjs +28 -0
  261. package/src/components/Task_Builder_PerBucket.res +22 -0
  262. package/src/components/Task_Builder_PerBucket.res.mjs +63 -0
  263. package/src/components/Vpc.res +214 -0
  264. package/src/components/Vpc.res.mjs +117 -0
  265. package/src/components/Vpc.resi +22 -0
  266. package/src/errors/AppSync_Error.res +36 -0
  267. package/src/errors/AppSync_Error.res.mjs +38 -0
  268. package/src/errors/Cognito_Error.res +36 -0
  269. package/src/errors/Cognito_Error.res.mjs +38 -0
  270. package/src/errors/DynamoDb_Error.res +75 -0
  271. package/src/errors/DynamoDb_Error.res.mjs +86 -0
  272. package/src/errors/Kinesis_Error.res +36 -0
  273. package/src/errors/Kinesis_Error.res.mjs +38 -0
  274. package/src/errors/SNS_Error.res +34 -0
  275. package/src/errors/SNS_Error.res.mjs +38 -0
  276. package/src/errors/SQS_Error.res +40 -0
  277. package/src/errors/SQS_Error.res.mjs +41 -0
  278. package/src/plugin/cloner/ClonerRunner.res +1 -0
  279. package/src/plugin/cloner/ClonerRunner.res.mjs +9 -0
  280. package/src/plugin/cloner/ClonerRunner_Fargate.res +362 -0
  281. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +246 -0
  282. package/src/plugin/cloner/ClonerRunner_Fargate_Runtime.res +42 -0
  283. package/src/plugin/cloner/ClonerRunner_Fargate_Runtime.res.mjs +45 -0
  284. package/src/plugin/heartbeat/HeartbeatRunner.res +1 -0
  285. package/src/plugin/heartbeat/HeartbeatRunner.res.mjs +9 -0
  286. package/src/plugin/heartbeat/HeartbeatRunner_CloudWatchEvents.res +96 -0
  287. package/src/plugin/heartbeat/HeartbeatRunner_CloudWatchEvents.res.mjs +69 -0
  288. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res +115 -0
  289. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +88 -0
  290. package/src/plugin/runtime/PluginRuntimeOperations.res +5 -0
  291. package/src/plugin/runtime/PluginRuntimeOperations.res.mjs +14 -0
  292. package/src/plugin/runtime/PluginRuntime_Builder.res +769 -0
  293. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +464 -0
  294. package/src/plugin/runtime/Util_PluginMessage_Runtime.res +13 -0
  295. package/src/plugin/runtime/Util_PluginMessage_Runtime.res.mjs +19 -0
  296. package/src/plugin/stack/Plugin_ExtensionPoint_Builder.res +28 -0
  297. package/src/plugin/stack/Plugin_ExtensionPoint_Builder.res.mjs +67 -0
  298. package/src/plugin/stack/Plugin_Stack.res +439 -0
  299. package/src/plugin/stack/Plugin_Stack.res.mjs +263 -0
  300. package/src/util/Util.res +29 -0
  301. package/src/util/Util.res.mjs +93 -0
  302. package/src/util/Util_AppSync.res +36 -0
  303. package/src/util/Util_AppSync.res.mjs +35 -0
  304. package/src/util/Util_AppSync_Caller.res +213 -0
  305. package/src/util/Util_AppSync_Caller.res.mjs +170 -0
  306. package/src/util/Util_Bundle.res +255 -0
  307. package/src/util/Util_Bundle.res.mjs +179 -0
  308. package/src/util/Util_Cloudwatch.res +14 -0
  309. package/src/util/Util_Cloudwatch.res.mjs +19 -0
  310. package/src/util/Util_CognitoGroupUser.res +39 -0
  311. package/src/util/Util_CognitoGroupUser.res.mjs +32 -0
  312. package/src/util/Util_CognitoGroupUser_Runtime.res +65 -0
  313. package/src/util/Util_CognitoGroupUser_Runtime.res.mjs +50 -0
  314. package/src/util/Util_Cognito_Runtime.res +52 -0
  315. package/src/util/Util_Cognito_Runtime.res.mjs +35 -0
  316. package/src/util/Util_DeadLetterQueue.res +154 -0
  317. package/src/util/Util_DeadLetterQueue.res.mjs +158 -0
  318. package/src/util/Util_DynamoDb.res +210 -0
  319. package/src/util/Util_DynamoDb.res.mjs +218 -0
  320. package/src/util/Util_DynamoDbStream.res +138 -0
  321. package/src/util/Util_DynamoDbStream.res.mjs +134 -0
  322. package/src/util/Util_DynamoDbStream_Runtime.res +94 -0
  323. package/src/util/Util_DynamoDbStream_Runtime.res.mjs +131 -0
  324. package/src/util/Util_DynamoDb_Runtime.res +216 -0
  325. package/src/util/Util_DynamoDb_Runtime.res.mjs +257 -0
  326. package/src/util/Util_DynamoDb_TableManager.res +23 -0
  327. package/src/util/Util_DynamoDb_TableManager.res.mjs +35 -0
  328. package/src/util/Util_DynamoDb_TableManager.resi +3 -0
  329. package/src/util/Util_Env.res +5 -0
  330. package/src/util/Util_Env.res.mjs +2 -0
  331. package/src/util/Util_EventSourceMapping.res +49 -0
  332. package/src/util/Util_EventSourceMapping.res.mjs +32 -0
  333. package/src/util/Util_HostUiDomain.res +28 -0
  334. package/src/util/Util_HostUiDomain.res.mjs +26 -0
  335. package/src/util/Util_IAM_Role.res +14 -0
  336. package/src/util/Util_IAM_Role.res.mjs +28 -0
  337. package/src/util/Util_Kinesis_Runtime.res +20 -0
  338. package/src/util/Util_Kinesis_Runtime.res.mjs +24 -0
  339. package/src/util/Util_Lambda.res +39 -0
  340. package/src/util/Util_Lambda.res.mjs +45 -0
  341. package/src/util/Util_LocalConfig.res +119 -0
  342. package/src/util/Util_LocalConfig.res.mjs +112 -0
  343. package/src/util/Util_ResourceNaming.res +15 -0
  344. package/src/util/Util_ResourceNaming.res.mjs +26 -0
  345. package/src/util/Util_S3.res +17 -0
  346. package/src/util/Util_S3.res.mjs +30 -0
  347. package/src/util/Util_S3_Runtime.res +11 -0
  348. package/src/util/Util_S3_Runtime.res.mjs +15 -0
  349. package/src/util/Util_SNS.res +33 -0
  350. package/src/util/Util_SNS.res.mjs +50 -0
  351. package/src/util/Util_SNS_FIFO.res +21 -0
  352. package/src/util/Util_SNS_FIFO.res.mjs +27 -0
  353. package/src/util/Util_SNS_Runtime.res +33 -0
  354. package/src/util/Util_SNS_Runtime.res.mjs +27 -0
  355. package/src/util/Util_SQS.res +70 -0
  356. package/src/util/Util_SQS.res.mjs +83 -0
  357. package/src/util/Util_SQS_FIFO.res +16 -0
  358. package/src/util/Util_SQS_FIFO.res.mjs +28 -0
  359. package/src/util/Util_SQS_Runtime.res +169 -0
  360. package/src/util/Util_SQS_Runtime.res.mjs +150 -0
  361. package/src/util/Util_SesPolicy_Runtime.res +54 -0
  362. package/src/util/Util_SesPolicy_Runtime.res.mjs +55 -0
  363. package/src/util/Util_StaticBundle.res +124 -0
  364. package/src/util/Util_StaticBundle.res.mjs +132 -0
  365. package/src/util/Util_Vpc.res +22 -0
  366. package/src/util/Util_Vpc.res.mjs +27 -0
  367. package/tests/AppSync_AdapterTest.res +396 -0
  368. package/tests/AppSync_AdapterTest.res.mjs +326 -0
  369. package/tests/AppSync_Resolver_RetryingTest.res +59 -0
  370. package/tests/AppSync_Resolver_RetryingTest.res.mjs +66 -0
  371. package/tests/Auth_CognitoTest.res +203 -0
  372. package/tests/Auth_CognitoTest.res.mjs +248 -0
  373. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +602 -0
  374. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +742 -0
  375. package/tests/EventLogStorage_DynamoDb_RuntimeTest.res +57 -0
  376. package/tests/EventLogStorage_DynamoDb_RuntimeTest.res.mjs +68 -0
  377. package/tests/MCP_LambdaTest.res +101 -0
  378. package/tests/MCP_LambdaTest.res.mjs +56 -0
  379. package/tests/Util_HostUiDomainTest.res +90 -0
  380. package/tests/Util_HostUiDomainTest.res.mjs +50 -0
  381. package/tests/Util_SQS_RuntimeTest.res +46 -0
  382. package/tests/Util_SQS_RuntimeTest.res.mjs +45 -0
  383. package/tests/Util_StaticBundleTest.res +136 -0
  384. package/tests/Util_StaticBundleTest.res.mjs +149 -0
  385. package/tests/integration/DcbCommandTopicEntryPoint_IntegrationTest.res +101 -0
  386. package/tests/integration/DcbCommandTopicEntryPoint_IntegrationTest.res.mjs +91 -0
  387. package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res +315 -0
  388. package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res.mjs +435 -0
  389. package/tests/integration/DcbIntegrationHarness.res +102 -0
  390. package/tests/integration/DcbIntegrationHarness.res.mjs +163 -0
  391. package/tests/integration/EpTestSlice.res +28 -0
  392. package/tests/integration/EpTestSlice.res.mjs +43 -0
  393. package/tests/integration/EpTestSliceBehavior.res +18 -0
  394. package/tests/integration/EpTestSliceBehavior.res.mjs +35 -0
@@ -0,0 +1,1940 @@
1
+ // Platform — concrete AWS implementation of ReventlessInfra.Platform.T.
2
+ //
3
+ // Creates a platform instance with pre-wired AWS builders (DynamoDB, Lambda, SQS, SNS).
4
+ // Config is applied once at platform creation; component Make functors then take only
5
+ // the application-defined arguments (Spec, Behavior, Mappings).
6
+ //
7
+ // Example:
8
+ // module Platform = Platform.Make()
9
+ // module App = MyPlugin.Make(Platform)
10
+ //
11
+ // Custom config:
12
+ // module Platform = Platform.MakeWithConfig({let splitApi = false; let cloner = true})
13
+
14
+ let log = ReventlessCore.Logger.fromEnv()
15
+
16
+ // Deploy-time schema-push shrink-guard threshold (deploy-time analogue of the
17
+ // runtime RUNTIME_SCHEMA_SHRINK_THRESHOLD in AdminEventCollectorEntryPoint.mjs).
18
+ // A push whose stitched SDL has fewer than (threshold × live) root fields is
19
+ // refused as a likely stale concurrent-deploy stitch. Default 0.5; override via
20
+ // DEPLOY_SCHEMA_SHRINK_THRESHOLD; values outside (0, 1) fall back to the default.
21
+ @val @scope("process") external processEnv: Dict.t<string> = "env"
22
+ let deploySchemaShrinkThreshold: float =
23
+ switch processEnv->Dict.get("DEPLOY_SCHEMA_SHRINK_THRESHOLD")->Option.flatMap(Float.fromString) {
24
+ | Some(n) if n > 0. && n < 1. => n
25
+ | _ => 0.5
26
+ }
27
+
28
+ // API config ref — populated during MakeWithConfig so slice builders
29
+ // can access api/apiRole outside the functor constraint.
30
+ //
31
+ // domainApi/domainApiRole: the API that plugin stacks publish mutations to.
32
+ // platformApi/platformApiRole: the API that hosts admin and Platform_Sync* resolvers.
33
+ // In unified (non-split) mode both pairs point to the same AppSync resource.
34
+ // In split mode they are separate resources created by deployPlatform/makePlatform.
35
+ type apiConfig = {
36
+ domainApi: Types.AppSync.api,
37
+ domainApiRole: Types.AppSync.role,
38
+ platformApi: Types.AppSync.api,
39
+ platformApiRole: Types.AppSync.role,
40
+ }
41
+ let apiConfigRef: ref<option<apiConfig>> = ref(None)
42
+ let getApiConfig = () =>
43
+ apiConfigRef.contents->Option.getOrThrow(
44
+ ~message="Platform.getApiConfig() called before MakeWithConfig",
45
+ )
46
+
47
+ // Split API outputs — populated by makePlatform when splitApi=true.
48
+ // Access via getSplitApiOutputs() after makePlatform has been called.
49
+ type splitApiOutputs = {
50
+ platformApi: Types.AppSync.api,
51
+ platformApiRole: Types.AppSync.role,
52
+ }
53
+ let splitApiOutputsRef: ref<option<splitApiOutputs>> = ref(None)
54
+
55
+ /** Returns the Platform API outputs created in split mode.
56
+ Call after `makePlatform` — returns `None` in unified mode. */
57
+ let getSplitApiOutputs = () => splitApiOutputsRef.contents
58
+
59
+ module MakeWithConfig = (
60
+ Config: {
61
+ let splitApi: bool
62
+ let cloner: bool
63
+ let commandHandlerConfig: ReventlessCore.Runtime.commandHandlerConfigs
64
+ },
65
+ ): (
66
+ ReventlessInfra.Platform.T with type api = Types.AppSync.api and type role = Types.AppSync.role
67
+ ) => {
68
+ // Dispatch per-flavor commandHandlerConfig records to the four runtime
69
+ // builders. Every sub-record is optional; only branches the caller actually
70
+ // supplied propagate, and within each sub-record every field is optional too
71
+ // — `setConfig` just installs the override, defaults remain in the builder.
72
+ Config.commandHandlerConfig.aggregates->Option.forEach(({?sync, ?async}) => {
73
+ sync->Option.forEach(AggregateRuntime_Builder_Single.setConfig)
74
+ async->Option.forEach(AggregateRuntime_Builder_Single_Async.setConfig)
75
+ })
76
+ Config.commandHandlerConfig.stateChanges->Option.forEach(({?sync, ?async}) =>
77
+ PluginRuntime_Builder.setStateChangesConfig(~sync?, ~async?, ())
78
+ )
79
+ type api = Types.AppSync.api
80
+ type role = Types.AppSync.role
81
+ type apiTarget = Domain | Platform
82
+
83
+ // Ref tracking which API target deployPlugin is currently building for.
84
+ // Domain (default): resolvers/schema go to the application AppSync API.
85
+ // Platform: resolvers/schema go to the admin/Platform_Sync* Core API.
86
+ let currentDeployTarget: ref<apiTarget> = ref(Domain)
87
+
88
+ // Determine API source based on platform:stack config.
89
+ // - Platform/monolithic mode (no config): create a real AppSync API resource.
90
+ // - Plugin mode (config set): reference the platform's shared API via StackReference.
91
+ let platformStackRef =
92
+ Pulumi.Config.make(Some("platform"))
93
+ ->Pulumi.Config.get("stack")
94
+ ->Option.map(stack => Pulumi.StackReference.make(stack))
95
+
96
+ // Helper: build a phantom GraphQLApi.t from resolved id + endpoint strings.
97
+ let makePhantomApi = (apiId, apiEndpoint): PulumiAws.AppSync.GraphQLApi.t => ({
98
+ id: Pulumi.Output.make(apiId),
99
+ arn: Pulumi.Output.make(""),
100
+ name: Pulumi.Output.make(""),
101
+ uris: Pulumi.Output.make(({graphQL: apiEndpoint, realtime: ""}: PulumiAws.AppSync.GraphQLApi.uris)),
102
+ }: PulumiAws.AppSync.GraphQLApi.t)
103
+
104
+ // Helper: build a phantom IAM.Role.t from a resolved role ARN.
105
+ let makePhantomRole = (apiRoleArn): PulumiAws.IAM.Role.t => {
106
+ let roleName =
107
+ apiRoleArn->String.split("/")->Array.at(-1)->Option.getOr(apiRoleArn)
108
+ ({
109
+ arn: Pulumi.Output.make(apiRoleArn),
110
+ id: Pulumi.Output.make(roleName),
111
+ name: Pulumi.Output.make(roleName),
112
+ }: PulumiAws.IAM.Role.t)
113
+ }
114
+
115
+ let (domainApi, domainApiRole, platformApi, platformApiRole) = switch platformStackRef {
116
+ | None =>
117
+ let (api, role) = AppSync_Adapter.makeApiResource(~name="DomainApi", ~opts={})
118
+ // In platform/monolithic mode the platform API is not yet known — it is created
119
+ // during deployPlatform/makePlatform and the ref is updated there.
120
+ (api, role, api, role)
121
+ | Some(stackRef) =>
122
+ // Plugin mode — reconstruct phantom API/role from the platform's exported IDs.
123
+ // Consumers only access api.id and role.arn, so other fields are unused.
124
+ //
125
+ // In ESM mode, Pulumi exports are inside the "default" output.
126
+ // Try top-level field first (CJS), fall back to "default".<field> (ESM).
127
+ let defaultOutput: Pulumi.Output.t<option<JSON.t>> =
128
+ stackRef->Pulumi.StackReference.getOutput("default")
129
+
130
+ // ── Domain API (application mutations) ──────────────────────────────────
131
+ let domainApiIdOutput: Pulumi.Output.t<option<string>> =
132
+ stackRef->Pulumi.StackReference.getOutput("domainApiId")
133
+ let domainApiEndpointOutput: Pulumi.Output.t<option<string>> =
134
+ stackRef->Pulumi.StackReference.getOutput("domainApiEndpoint")
135
+ let domainApiRoleArnOutput: Pulumi.Output.t<option<string>> =
136
+ stackRef->Pulumi.StackReference.getOutput("domainApiRoleArn")
137
+
138
+ let phantomApi: Types.AppSync.api =
139
+ (domainApiIdOutput, domainApiEndpointOutput, defaultOutput)
140
+ ->Pulumi.Output.all3
141
+ ->Pulumi.Output.apply(((directId, directEndpoint, default)) => {
142
+ let apiId = switch directId {
143
+ | Some(id) => id
144
+ | None =>
145
+ default
146
+ ->Option.flatMap(d => d->JSON.Decode.object)
147
+ ->Option.flatMap(d => d->Dict.get("domainApiId"))
148
+ ->Option.flatMap(v => v->JSON.Decode.string)
149
+ ->Option.getOrThrow
150
+ }
151
+ let apiEndpoint = switch directEndpoint {
152
+ | Some(ep) => ep
153
+ | None =>
154
+ default
155
+ ->Option.flatMap(d => d->JSON.Decode.object)
156
+ ->Option.flatMap(d => d->Dict.get("domainApiEndpoint"))
157
+ ->Option.flatMap(v => v->JSON.Decode.string)
158
+ ->Option.getOrThrow(
159
+ ~message="Platform stack does not export 'domainApiEndpoint' — redeploy the platform stack first",
160
+ )
161
+ }
162
+ makePhantomApi(apiId, apiEndpoint)
163
+ })
164
+ let phantomRole: Types.AppSync.role =
165
+ (domainApiRoleArnOutput, defaultOutput)
166
+ ->Pulumi.Output.all2
167
+ ->Pulumi.Output.apply(((direct, default)) => {
168
+ let domainApiRoleArn = switch direct {
169
+ | Some(arn) => arn
170
+ | None =>
171
+ default
172
+ ->Option.flatMap(d => d->JSON.Decode.object)
173
+ ->Option.flatMap(d => d->Dict.get("domainApiRoleArn"))
174
+ ->Option.flatMap(v => v->JSON.Decode.string)
175
+ ->Option.getOrThrow
176
+ }
177
+ makePhantomRole(domainApiRoleArn)
178
+ })
179
+
180
+ // ── Platform API (admin / Platform_Sync* resolvers) ─────────────────────
181
+ let platformApiIdOutput: Pulumi.Output.t<option<string>> =
182
+ stackRef->Pulumi.StackReference.getOutput("platformApiId")
183
+ let platformApiEndpointOutput: Pulumi.Output.t<option<string>> =
184
+ stackRef->Pulumi.StackReference.getOutput("platformApiEndpoint")
185
+ let platformApiRoleArnOutput: Pulumi.Output.t<option<string>> =
186
+ stackRef->Pulumi.StackReference.getOutput("platformApiRoleArn")
187
+
188
+ let phantomPlatformApi: Types.AppSync.api =
189
+ (platformApiIdOutput, platformApiEndpointOutput, defaultOutput)
190
+ ->Pulumi.Output.all3
191
+ ->Pulumi.Output.apply(((directPlatId, directPlatEndpoint, default)) => {
192
+ let getFromDefault = key =>
193
+ default
194
+ ->Option.flatMap(d => d->JSON.Decode.object)
195
+ ->Option.flatMap(d => d->Dict.get(key))
196
+ ->Option.flatMap(v => v->JSON.Decode.string)
197
+ let platformApiId =
198
+ directPlatId
199
+ ->Option.orElse(getFromDefault("platformApiId"))
200
+ ->Option.getOrThrow
201
+ let platformApiEndpoint =
202
+ directPlatEndpoint
203
+ ->Option.orElse(getFromDefault("platformApiEndpoint"))
204
+ ->Option.getOrThrow
205
+ makePhantomApi(platformApiId, platformApiEndpoint)
206
+ })
207
+ let phantomPlatformRole: Types.AppSync.role =
208
+ (platformApiRoleArnOutput, defaultOutput)
209
+ ->Pulumi.Output.all2
210
+ ->Pulumi.Output.apply(((directPlat, default)) => {
211
+ let getFromDefault = key =>
212
+ default
213
+ ->Option.flatMap(d => d->JSON.Decode.object)
214
+ ->Option.flatMap(d => d->Dict.get(key))
215
+ ->Option.flatMap(v => v->JSON.Decode.string)
216
+ let platformApiRoleArn =
217
+ directPlat
218
+ ->Option.orElse(getFromDefault("platformApiRoleArn"))
219
+ ->Option.getOrThrow
220
+ makePhantomRole(platformApiRoleArn)
221
+ })
222
+
223
+ (phantomApi, phantomRole, phantomPlatformApi, phantomPlatformRole)
224
+ }
225
+
226
+ // Expose api/apiRole as Platform.T value bindings so DCB slice builders
227
+ // can access them through the platform interface.
228
+ let api = domainApi
229
+ let apiRole = domainApiRole
230
+
231
+ // Populate apiConfig with both domain and platform API references.
232
+ // In platform/monolithic mode, platformApi starts as domainApi and is updated
233
+ // by deployPlatform/makePlatform once the core API resource is created (split mode).
234
+ let () = apiConfigRef := Some({
235
+ domainApi: domainApi,
236
+ domainApiRole: domainApiRole,
237
+ platformApi: platformApi,
238
+ platformApiRole: platformApiRole,
239
+ })
240
+
241
+ // AppSync Events API — companion to the GraphQL API for Sources A and B subscriptions.
242
+ // In platform/monolithic mode: created here as a real resource.
243
+ // In plugin mode: reconstructed as a phantom from platform stack exports (eventsApiArn, eventsApiDns).
244
+ let domainEventsApiOpt: option<AppSync_EventsApi.t> =
245
+ switch platformStackRef {
246
+ | None =>
247
+ // Browser host-shell subscribes over WebSocket with a Cognito IdToken,
248
+ // so the Events API must carry a Cognito auth provider. The pool is the
249
+ // same process-cached one the GraphQL API + config.json use.
250
+ let cognitoPool = Platform_Stack.resolveCognitoUserPool()
251
+ let awsRegion =
252
+ Pulumi.Config.make(Some("aws"))->Pulumi.Config.get("region")->Option.getOr("unknown")
253
+ Some(
254
+ AppSync_EventsApi.make(
255
+ ~name="DomainEventsApi",
256
+ ~cognitoUserPoolId=cognitoPool.poolId->Pulumi.Output.asInput,
257
+ ~awsRegion,
258
+ ~opts={},
259
+ ),
260
+ )
261
+ | Some(stackRef) =>
262
+ let eventsApiArnOutput: Pulumi.Output.t<option<string>> =
263
+ stackRef->Pulumi.StackReference.getOutput("eventsApiArn")
264
+ let eventsApiDnsOutput: Pulumi.Output.t<option<string>> =
265
+ stackRef->Pulumi.StackReference.getOutput("eventsApiDns")
266
+ let defaultEventsOutput: Pulumi.Output.t<option<JSON.t>> =
267
+ stackRef->Pulumi.StackReference.getOutput("default")
268
+ let getFromDefault = (default, key) =>
269
+ default
270
+ ->Option.flatMap(d => d->JSON.Decode.object)
271
+ ->Option.flatMap(d => d->Dict.get(key))
272
+ ->Option.flatMap(v => v->JSON.Decode.string)
273
+ let apiArn =
274
+ (eventsApiArnOutput, defaultEventsOutput)
275
+ ->Pulumi.Output.all2
276
+ ->Pulumi.Output.apply(((direct, default)) =>
277
+ direct->Option.orElse(getFromDefault(default, "eventsApiArn"))
278
+ )
279
+ let dns =
280
+ (eventsApiDnsOutput, defaultEventsOutput)
281
+ ->Pulumi.Output.all2
282
+ ->Pulumi.Output.apply(((direct, default)) =>
283
+ direct->Option.orElse(getFromDefault(default, "eventsApiDns"))
284
+ )
285
+ let api: PulumiAws.AwsNative.AppSync.Api.t = {
286
+ apiId: Pulumi.Output.make(""),
287
+ apiArn: apiArn->Pulumi.Output.apply(v => v->Option.getOr("")),
288
+ dns: dns->Pulumi.Output.apply(dnsHttp => ({
289
+ PulumiAws.AwsNative.AppSync.Api.http: ?dnsHttp,
290
+ }: PulumiAws.AwsNative.AppSync.Api.dns)),
291
+ name: Pulumi.Output.make(""),
292
+ }
293
+ let eventsApi: AppSync_EventsApi.t = {
294
+ name: "DomainEventsApi",
295
+ api,
296
+ defaultNamespace: None,
297
+ }
298
+ Some(eventsApi)
299
+ }
300
+
301
+ // Returns the AppSync API/role to attach resolvers to for the current deploy target.
302
+ // Hoisted above ApiConfig so the module can reference them as thunks.
303
+ let resolveTargetApi = () =>
304
+ switch currentDeployTarget.contents {
305
+ | Domain => domainApi
306
+ | Platform =>
307
+ switch apiConfigRef.contents {
308
+ | Some({platformApi}) => platformApi
309
+ | None => domainApi // fallback: platform API not yet constructed
310
+ }
311
+ }
312
+
313
+ let resolveTargetApiRole = () =>
314
+ switch currentDeployTarget.contents {
315
+ | Domain => domainApiRole
316
+ | Platform =>
317
+ switch apiConfigRef.contents {
318
+ | Some({platformApiRole}) => platformApiRole
319
+ | None => domainApiRole
320
+ }
321
+ }
322
+
323
+ // Thunk-based ApiConfig — evaluated at make() time, reads currentDeployTarget set by deployPlugin.
324
+ module ApiConfig = {
325
+ let api = resolveTargetApi
326
+ let apiRole = resolveTargetApiRole
327
+ }
328
+
329
+ module Aggregate = {
330
+ // Default deployment strategy is Single (one Lambda per plugin shared by all
331
+ // aggregates). To override per-plugin or per-aggregate, swap the builder at
332
+ // the call site (e.g., ReventlessAws.Aggregate_Builder_PerAggregate.Make,
333
+ // _Micro, _NoResolver) — see docs/plans/Backlog/aws-deployment-strategy.md.
334
+ module Make = (
335
+ Spec: Reventless.Aggregate.Spec,
336
+ Behavior: Reventless.Behavior.T with module Spec := Spec,
337
+ EventMappings: ReventlessInfra.EventMapper.Mappings with module Target := Spec,
338
+ ): (
339
+ ReventlessInfra.Aggregate.T with type api = Types.AppSync.api
340
+ ) =>
341
+ Aggregate_Builder_Single.Make(Spec, Behavior, EventMappings)
342
+ /** Async variant — uses FIFO SQS channel, commands return `CommandPending`. */
343
+ module MakeAsync = (
344
+ Spec: Reventless.Aggregate.Spec,
345
+ Behavior: Reventless.Behavior.T with module Spec := Spec,
346
+ EventMappings: ReventlessInfra.EventMapper.Mappings with module Target := Spec,
347
+ ): (
348
+ ReventlessInfra.Aggregate.T with type api = Types.AppSync.api
349
+ ) =>
350
+ Aggregate_Builder_Single_Async.Make(Spec, Behavior, EventMappings)
351
+ }
352
+
353
+ module ReadModel = {
354
+ module Make = (
355
+ Spec: Reventless.ReadModel.Spec,
356
+ Mappings: Reventless.Projection.Mappings with module Target := Spec,
357
+ ): (
358
+ ReventlessInfra.ReadModel.T
359
+ with module Spec = Spec
360
+ and type api = Types.AppSync.api
361
+ and type role = Types.AppSync.role
362
+ ) =>
363
+ ReadModel_Builder_Single.Make(Spec, Mappings)
364
+ }
365
+
366
+ /** Stream-enabled read model — DynamoDB Stream + StateTopic Lambda for
367
+ AppSync Events (Source B) live updates. */
368
+ module ReadModelStream = {
369
+ module Make = (
370
+ Spec: Reventless.ReadModel.Spec,
371
+ Mappings: Reventless.Projection.Mappings with module Target := Spec,
372
+ ): (
373
+ ReventlessInfra.ReadModel.T
374
+ with module Spec = Spec
375
+ and type api = Types.AppSync.api
376
+ and type role = Types.AppSync.role
377
+ ) =>
378
+ ReadModel_Builder_Single_Stream.Make(Spec, Mappings)
379
+ }
380
+
381
+ module ExtensionPoint = {
382
+ module Make = (
383
+ Mapping: ReventlessInfra.ExtensionPointMapping.Mapping,
384
+ ): ReventlessInfra.ExtensionPoint.T => {
385
+ module Spec = Mapping.ExtensionPoint
386
+ module CompiledMapping = ReventlessInfra.ExtensionPointMapping.Make(Mapping)
387
+ module Mappings: ReventlessInfra.ExtensionPoint.Mappings with module Spec := Spec = {
388
+ module type Mapping = ReventlessInfra.ExtensionPointMapping.T
389
+ with module ExtensionPoint := Spec
390
+ let name = Mapping.Delegate.name
391
+ // The mapping FILE's moduleUrl, not the spec's — the EventCollector
392
+ // runtime dynamic-imports this URL to find mapOutgoingEvent. Read
393
+ // through %raw because moduleUrl isn't part of the Mapping signature
394
+ // (would force every test/internal mapping to declare it); user
395
+ // mapping files reliably expose it via the @@reventless.spec PPX.
396
+ // Spec.moduleUrl is the fallback when the JS-level property is absent.
397
+ let moduleUrl = Mapping.moduleUrl
398
+ let mappings: array<module(Mapping)> = [module(CompiledMapping)]
399
+ }
400
+ module Inner = ExtensionPoint_Builder.Make(Spec, Mappings)
401
+ include Inner
402
+ }
403
+
404
+ module Make2 = (
405
+ Mapping1: ReventlessInfra.ExtensionPointMapping.Mapping,
406
+ Mapping2: ReventlessInfra.ExtensionPointMapping.Mapping
407
+ with module ExtensionPoint = Mapping1.ExtensionPoint,
408
+ ): ReventlessInfra.ExtensionPoint.T => {
409
+ module Spec = Mapping1.ExtensionPoint
410
+ module CM1 = ReventlessInfra.ExtensionPointMapping.Make(Mapping1)
411
+ module CM2 = ReventlessInfra.ExtensionPointMapping.Make(Mapping2)
412
+ module Mappings: ReventlessInfra.ExtensionPoint.Mappings with module Spec := Spec = {
413
+ module type Mapping = ReventlessInfra.ExtensionPointMapping.T
414
+ with module ExtensionPoint := Spec
415
+ let name =
416
+ Mapping1.Delegate.name ++ "+" ++ Mapping2.Delegate.name
417
+ // First mapping's URL — matches the user-extension merge convention.
418
+ // See Make's moduleUrl comment for the %raw rationale.
419
+ let moduleUrl = Mapping1.moduleUrl
420
+ let mappings: array<module(Mapping)> = [module(CM1), module(CM2)]
421
+ }
422
+ module Inner = ExtensionPoint_Builder.Make(Spec, Mappings)
423
+ include Inner
424
+ }
425
+
426
+ module Make3 = (
427
+ Mapping1: ReventlessInfra.ExtensionPointMapping.Mapping,
428
+ Mapping2: ReventlessInfra.ExtensionPointMapping.Mapping
429
+ with module ExtensionPoint = Mapping1.ExtensionPoint,
430
+ Mapping3: ReventlessInfra.ExtensionPointMapping.Mapping
431
+ with module ExtensionPoint = Mapping1.ExtensionPoint,
432
+ ): ReventlessInfra.ExtensionPoint.T => {
433
+ module Spec = Mapping1.ExtensionPoint
434
+ module CM1 = ReventlessInfra.ExtensionPointMapping.Make(Mapping1)
435
+ module CM2 = ReventlessInfra.ExtensionPointMapping.Make(Mapping2)
436
+ module CM3 = ReventlessInfra.ExtensionPointMapping.Make(Mapping3)
437
+ module Mappings: ReventlessInfra.ExtensionPoint.Mappings with module Spec := Spec = {
438
+ module type Mapping = ReventlessInfra.ExtensionPointMapping.T
439
+ with module ExtensionPoint := Spec
440
+ let name =
441
+ Mapping1.Delegate.name ++
442
+ "+" ++
443
+ Mapping2.Delegate.name ++
444
+ "+" ++
445
+ Mapping3.Delegate.name
446
+ // First mapping's URL — matches the user-extension merge convention.
447
+ // See Make's moduleUrl comment for the %raw rationale.
448
+ let moduleUrl = Mapping1.moduleUrl
449
+ let mappings: array<module(Mapping)> = [module(CM1), module(CM2), module(CM3)]
450
+ }
451
+ module Inner = ExtensionPoint_Builder.Make(Spec, Mappings)
452
+ include Inner
453
+ }
454
+
455
+ module MakeMulti = (
456
+ Spec: ReventlessInfra.ExtensionPointMapping.Spec,
457
+ Mappings: ReventlessInfra.ExtensionPoint.Mappings with module Spec := Spec,
458
+ ): ReventlessInfra.ExtensionPoint.T => ExtensionPoint_Builder.Make(Spec, Mappings)
459
+ }
460
+
461
+ module Extension = {
462
+ module Make = (
463
+ Mapping: ReventlessInfra.ExtensionMapping.Mapping,
464
+ ): ReventlessInfra.Extension.Blueprint => {
465
+ module Spec = Mapping.ExtensionPoint
466
+ module CompiledMapping = ReventlessInfra.ExtensionMapping.Make(Mapping)
467
+ module type Mapping = ReventlessInfra.ExtensionMapping.T
468
+ with module ExtensionPoint := Spec
469
+ let name = Mapping.Delegate.name
470
+ let moduleUrl = Mapping.moduleUrl
471
+ let delegateModuleUrl = Mapping.delegateModuleUrl
472
+ let mappings: array<module(Mapping)> = [module(CompiledMapping)]
473
+ }
474
+
475
+ }
476
+
477
+ module Task = {
478
+ module Make = (Spec: ReventlessInfra.Task.Spec): (
479
+ ReventlessInfra.Task.T with module Spec = Spec
480
+ ) => Task_Builder_PerBucket.Make(Spec)
481
+ }
482
+
483
+ module Counter = Counter_Builder.Make(ApiConfig)
484
+
485
+ module StateChangeSlice = {
486
+ module Make = (
487
+ Spec: Reventless.StateChangeSlice.Spec,
488
+ Behavior: Reventless.StateChangeSlice.Behavior with module Spec := Spec,
489
+ ): (ReventlessInfra.StateChangeSlice.T with module Spec = Spec) =>
490
+ StateChangeSlice_Builder.Make(Spec, Behavior)
491
+ /** Async variant — uses FIFO SQS channel, commands return `CommandPending`. */
492
+ module MakeAsync = (
493
+ Spec: Reventless.StateChangeSlice.Spec,
494
+ Behavior: Reventless.StateChangeSlice.Behavior with module Spec := Spec,
495
+ ): (ReventlessInfra.StateChangeSlice.T with module Spec = Spec) => {
496
+ include StateChangeSlice_Builder.Make(Spec, Behavior)
497
+ let isAsync = true
498
+ }
499
+ }
500
+
501
+ module StateViewSlice = {
502
+ include StateViewSlice_Builder.Make(ApiConfig)
503
+ }
504
+ module StateViewSliceStream = {
505
+ @@warning("-60")
506
+ include StateViewSlice_Builder_Stream.Make(ApiConfig)
507
+ }
508
+ module AutomationSlice = {
509
+ include AutomationSlice_Builder.Make(ApiConfig)
510
+ }
511
+ module OutboundTranslationSlice = {
512
+ include OutboundTranslationSlice_Builder.Make(ApiConfig)
513
+ }
514
+ module InboundTranslationSlice = InboundTranslationSlice_Builder.Make(ApiConfig)
515
+
516
+ // Empty base fragment — no types, no mutations, no queries.
517
+ // Used by the plugin Api in split mode so plugin schema has no core fields.
518
+ let emptyBaseFragment = ReventlessCore.GraphQL_Stitcher.encode({
519
+ types: [],
520
+ mutations: [],
521
+ queries: [],
522
+ subscriptions: [],
523
+ })
524
+
525
+ module Api = {
526
+ module Make = (
527
+ FragmentConfig: {
528
+ let baseFragment: ReventlessInfra.Api.schemaFragment
529
+ },
530
+ ): ReventlessInfra.Api.T => {
531
+ module Builder = ReventlessCore.Api_Builder.Make(AppSync_Adapter)
532
+ // In split mode, the plugin API uses an empty base fragment so plugin schema
533
+ // has no core fields. In unified mode, use the provided base fragment as-is.
534
+ let effectiveBaseFragment = if Config.splitApi {
535
+ emptyBaseFragment
536
+ } else {
537
+ FragmentConfig.baseFragment
538
+ }
539
+ let make = (~name, ~opts=?) =>
540
+ Builder.make(~name, ~baseFragment=effectiveBaseFragment, ~opts?)
541
+ }
542
+ }
543
+
544
+ // ── Typed identity casts — see Platform_Casts.res for rationale ─────────
545
+ open Platform_Casts
546
+
547
+ // AWS platform hooks — all AWS-specific callbacks defined as a record.
548
+ // In-memory hooks (mutationResolverHook etc.) are absent (optional = None).
549
+ let deploySchemaPrefix = "deploy-schema:"
550
+ let deploySchemaPlatformPrefix = "deploy-schema-platform:"
551
+ let deploySchemaHashPrefix = "deploy-schema-hash:"
552
+
553
+ let readSchemaHash = async (~tableName: string, ~apiId: string): option<string> => {
554
+ open AwsSdk.DynamoDb.DocumentClient
555
+ let key = Dict.fromArray([("id", `${deploySchemaHashPrefix}${apiId}`->JSON.Encode.string)])
556
+ try {
557
+ let result = await GetCommand.send(GetCommand.make({GetCommand.tableName, key}))
558
+ result.item
559
+ ->Option.flatMap(item => item->JSON.Decode.object)
560
+ ->Option.flatMap(d => d->Dict.get("hash"))
561
+ ->Option.flatMap(v => v->JSON.Decode.string)
562
+ } catch {
563
+ | _ => None
564
+ }
565
+ }
566
+
567
+ let writeSchemaHash = async (~tableName: string, ~apiId: string, ~hash: string): unit => {
568
+ open AwsSdk.DynamoDb.DocumentClient
569
+ let item =
570
+ Dict.fromArray([
571
+ ("id", `${deploySchemaHashPrefix}${apiId}`->JSON.Encode.string),
572
+ ("hash", hash->JSON.Encode.string),
573
+ ])->JSON.Encode.object
574
+ let _ = await PutCommand.send(PutCommand.make({PutCommand.tableName, item}))
575
+ }
576
+
577
+ // (Deploy-time retire hook removed: supersession is now decided by the
578
+ // name-keyed Plugin aggregate (VersionSuperseded) — no RM scan drives a command.)
579
+
580
+ // Define api/apiRole refs before the hooks record so the hook closures can capture
581
+ // them directly. This is necessary because the dcbAppSyncResolverHook and
582
+ // inboundAppSyncResolverHook fire inside a deferred Pulumi.Output.apply callback
583
+ // (in Plugin_Builder.builderOutputs), AFTER deployPlugin has returned and reset
584
+ // currentDeployTarget to Domain. Reading hooks.apiRef directly gives the correct
585
+ // targetApi that was set before plugin.make() and is never reset.
586
+ let hooksApiRef: ref<option<ReventlessCore.Plugin_Helpers.hookedValue<unknown>>> = ref(None)
587
+ let hooksApiRoleRef: ref<option<ReventlessCore.Plugin_Helpers.hookedValue<unknown>>> = ref(None)
588
+
589
+ let resolveHookedApi = (): Types.AppSync.api =>
590
+ switch hooksApiRef.contents {
591
+ | Some({val}) => Obj.magic(val)
592
+ | None => resolveTargetApi()
593
+ }
594
+
595
+ let hooks: ReventlessCore.Plugin_Helpers.platformHooks = {
596
+ // AWS uses Interstack for admin extension points — leave ref at empty dict.
597
+ adminExtensionPoints: ref(Pulumi.Output.make(Dict.make())),
598
+ // Platform context — populated by makePlatform/deployPlugin before plugin build.
599
+ scheduler: ref(None),
600
+ schedulerRoleUrn: ref(Pulumi.Output.make("")),
601
+ api: hooksApiRef,
602
+ apiRole: hooksApiRoleRef,
603
+ deployTarget: ref("Domain"),
604
+ inboundAppSyncResolverHook: ({runtime, fieldNames, externalInputSchemas: _, opts}) => {
605
+ let runtimeTyped: ReventlessCore.Runtime.environment<Util.Lambda.runtimeParts> =
606
+ runtime->asLambdaRuntime
607
+ InboundTranslationResolvers_AppSync.make(
608
+ ~api=resolveHookedApi(),
609
+ ~runtime=runtimeTyped,
610
+ ~fieldNames,
611
+ ~opts,
612
+ )
613
+ },
614
+ dcbAppSyncResolverHook: ({runtime, fieldNames, tags, opts}) => {
615
+ let runtimeTyped: ReventlessCore.Runtime.environment<Util.Lambda.runtimeParts> =
616
+ runtime->asLambdaRuntime
617
+ CommandGeneratorResolvers_AppSync.makeDcb(
618
+ ~api=resolveHookedApi(),
619
+ ~runtime=runtimeTyped,
620
+ ~fieldNames,
621
+ ~tags,
622
+ ~opts,
623
+ )
624
+ },
625
+
626
+ // Admin-side schema push, gated on admin DataSources via adminBarrier.
627
+ // Platform_Admin.construct invokes this and chains createResolvers behind
628
+ // the returned Output — so admin CreateResolver calls fire only after
629
+ // StartSchemaCreation completes and the AppSync API-level lock is
630
+ // released. This replaces the prior outer schema pushes that lived in
631
+ // makePlatform / deployPlatform and raced admin createResolvers.
632
+ //
633
+ // Target API selection (read late so splitApiOutputsRef, populated by
634
+ // makePlatform / deployPlatform before Admin.construct fires, is visible):
635
+ // - split mode: platformApi from splitApiOutputsRef
636
+ // - unified mode / not-yet-populated: domainApi
637
+ preAdminResolversSchemaHook: (~adminBarrier) => {
638
+ let targetApi = switch splitApiOutputsRef.contents {
639
+ | Some({platformApi}) => platformApi
640
+ | None => domainApi
641
+ }
642
+ let adminBaseFragment = AppSync_Adapter.injectAwsAuthAll(
643
+ ReventlessCore.AdminApi.baseFragment(~cloner=Config.cloner),
644
+ ~group="Admin",
645
+ )
646
+ let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
647
+ ~baseFragment=adminBaseFragment,
648
+ ~pluginFragments=[],
649
+ )
650
+ (targetApi, adminBarrier)
651
+ ->Pulumi.Output.all2
652
+ ->Pulumi.Output.flatMap(((api, _)) =>
653
+ api.id->Pulumi.Output.flatMap(apiId => {
654
+ log.info(~comp="preAdminResolversSchemaHook", `Pushing admin schema to ${apiId}`)
655
+ let client = AppSync_Adapter.getClient()
656
+ client
657
+ ->AppSync_Adapter.startSchemaCreationRetrying({apiId, definition: sdl})
658
+ ->Promise.then(async _ => {
659
+ log.info(
660
+ ~comp="preAdminResolversSchemaHook",
661
+ "startSchemaCreation called, waiting for ACTIVE",
662
+ )
663
+ await AppSync_Adapter.waitForSchemaActive(client, apiId)
664
+ log.info(~comp="preAdminResolversSchemaHook", "schema is ACTIVE")
665
+ })
666
+ ->Pulumi.Output.fromPromise
667
+ })
668
+ )
669
+ },
670
+
671
+ // Accumulate fragments across independent plugin deployments: each plugin
672
+ // writes its fragment to the Plugin RM table (keyed "deploy-schema:<name>")
673
+ // at deploy time. The hook then scans for ALL deploy-schema entries and
674
+ // stitches them together — ensuring the schema is cumulative rather than
675
+ // overwritten by each plugin deployment.
676
+ preResolversSchemaHook: (~name, ~version, pluginFragment) => {
677
+ log.info(
678
+ ~comp="preResolversSchemaHook",
679
+ `Pushing schema for plugin ${name}@${version} to AppSync`,
680
+ )
681
+
682
+ // Capture deploy target synchronously — deployPlugin resets currentDeployTarget to
683
+ // Domain after P.make() returns, before any Pulumi.Output async callbacks run.
684
+ let capturedDeployTarget = currentDeployTarget.contents
685
+
686
+ // Select DynamoDB key prefix and target AppSync API based on the current deploy target.
687
+ // Domain plugins use "deploy-schema:" and the Domain API (default behaviour).
688
+ // Platform plugins use a separate "deploy-schema-platform:" namespace and the Core API,
689
+ // so their cumulative schema is kept independent from the Domain API's schema.
690
+ let (schemaPrefix, targetApi) = switch capturedDeployTarget {
691
+ | Domain => (deploySchemaPrefix, domainApi)
692
+ | Platform =>
693
+ let api = switch apiConfigRef.contents {
694
+ | Some({platformApi}) => platformApi
695
+ | None => domainApi // fallback: Core API not yet constructed
696
+ }
697
+ (deploySchemaPlatformPrefix, api)
698
+ }
699
+
700
+ // Read a string output from the platform StackReference, falling back to
701
+ // the bundled "default" output object if the named export is not present
702
+ // (matches the layout Pulumi emits when a stack uses a single default
703
+ // export rather than per-key exports).
704
+ let readStackRefString = (stackRef, key) => {
705
+ let direct: Pulumi.Output.t<option<JSON.t>> =
706
+ stackRef->Pulumi.StackReference.getOutput(key)
707
+ let defaultOutput: Pulumi.Output.t<option<JSON.t>> =
708
+ stackRef->Pulumi.StackReference.getOutput("default")
709
+ (direct, defaultOutput)
710
+ ->Pulumi.Output.all2
711
+ ->Pulumi.Output.apply(((direct, default)) =>
712
+ switch direct->Option.flatMap(v => v->JSON.Decode.string) {
713
+ | Some(name) => Some(name)
714
+ | None =>
715
+ default
716
+ ->Option.flatMap(d => d->JSON.Decode.object)
717
+ ->Option.flatMap(d => d->Dict.get(key))
718
+ ->Option.flatMap(v => v->JSON.Decode.string)
719
+ }
720
+ )
721
+ }
722
+
723
+ // Prefer the dedicated PluginSchemaPersistence table (post-platform-fix);
724
+ // fall back to the Plugin RM table for backward compatibility with
725
+ // platforms deployed before the schema-persistence table existed. The
726
+ // Plugin RM table must not be reused for new schema-fragment writes —
727
+ // doing so leaks deploy-schema rows through the Platform_Plugins AppSync
728
+ // Connection resolver.
729
+ let schemaPersistenceTableNameOutput: Pulumi.Output.t<option<string>> = switch platformStackRef {
730
+ | Some(stackRef) =>
731
+ (
732
+ readStackRefString(stackRef, "pluginSchemaPersistenceTableName"),
733
+ readStackRefString(stackRef, "pluginRmTableName"),
734
+ )
735
+ ->Pulumi.Output.all2
736
+ ->Pulumi.Output.apply(((dedicated, legacy)) =>
737
+ switch dedicated {
738
+ | Some(_) as s => s
739
+ | None => legacy
740
+ }
741
+ )
742
+ | None => Pulumi.Output.make(None)
743
+ }
744
+
745
+ schemaPersistenceTableNameOutput
746
+ ->Pulumi.Output.flatMap(tableNameOpt => {
747
+ // Write this plugin's fragment to DynamoDB, then scan all deploy-schema
748
+ // entries to collect every deployed plugin's fragment.
749
+ let writeAndScanFragments = () =>
750
+ switch tableNameOpt {
751
+ | None =>
752
+ log.info(
753
+ ~comp="preResolversSchemaHook",
754
+ "No pluginSchemaPersistenceTableName / pluginRmTableName — skipping fragment persistence",
755
+ )
756
+ Promise.resolve([pluginFragment])
757
+ | Some(tableName) =>
758
+ open AwsSdk.DynamoDb.DocumentClient
759
+ // Write this plugin's fragment so subsequent plugin deployments find it.
760
+ let deployItem =
761
+ Dict.fromArray([
762
+ ("id", `${schemaPrefix}${name}`->JSON.Encode.string),
763
+ ("fragment", pluginFragment.encoded->JSON.Encode.string),
764
+ ])->JSON.Encode.object
765
+ log.info(
766
+ ~comp="preResolversSchemaHook",
767
+ `Writing deploy-schema entry for ${name} to ${tableName}`,
768
+ )
769
+ // Paginated scan — accumulate every deploy-schema entry across pages.
770
+ // A single ScanCommand returns at most 1 MB before yielding a
771
+ // LastEvaluatedKey; loop until the table is exhausted so a platform
772
+ // with many plugin fragments never stitches a partial schema.
773
+ let scanAllDeploySchemaItems = async () => {
774
+ let allItems = []
775
+ let startKey = ref(None)
776
+ let more = ref(true)
777
+ while more.contents {
778
+ let result = await ScanCommand.send(
779
+ ScanCommand.make({
780
+ ScanCommand.tableName: tableName,
781
+ filterExpression: "begins_with(#id, :prefix)",
782
+ expressionAttributeNames: Dict.fromArray([("#id", "id")]),
783
+ expressionAttributeValues: Dict.fromArray([
784
+ (":prefix", schemaPrefix->JSON.Encode.string),
785
+ ]),
786
+ exclusiveStartKey: ?startKey.contents,
787
+ }),
788
+ )
789
+ result.items->Option.getOr([])->Array.forEach(item => allItems->Array.push(item))
790
+ switch result.lastEvaluatedKey {
791
+ | Some(_) as k => startKey := k
792
+ | None => more := false
793
+ }
794
+ }
795
+ allItems
796
+ }
797
+
798
+ PutCommand.send(PutCommand.make({PutCommand.tableName: tableName, item: deployItem}))
799
+ ->Promise.then(_ => {
800
+ // Scan for all deploy-schema entries from previously deployed plugins.
801
+ log.info(
802
+ ~comp="preResolversSchemaHook",
803
+ `Scanning ${tableName} for deploy-schema entries`,
804
+ )
805
+ scanAllDeploySchemaItems()
806
+ })
807
+ ->Promise.then(items => {
808
+ let fragments = items->Array.filterMap(item => {
809
+ try {
810
+ let obj = item->JSON.stringify->JSON.parseOrThrow
811
+ switch obj->JSON.Decode.object->Option.flatMap(d => d->Dict.get("fragment")) {
812
+ | Some(fragmentJson) =>
813
+ switch fragmentJson->JSON.Decode.string {
814
+ | Some(encoded) =>
815
+ Some({Reventless.Plugin.encoded, protocol: "graphql"})
816
+ | None => None
817
+ }
818
+ | None => None
819
+ }
820
+ } catch {
821
+ | _ => None
822
+ }
823
+ })
824
+ log.info(
825
+ ~comp="preResolversSchemaHook",
826
+ `Found ${fragments->Array.length->Int.toString} deploy-schema entries`,
827
+ )
828
+ Promise.resolve(fragments)
829
+ })
830
+ ->Promise.catch(err => {
831
+ let msg =
832
+ err
833
+ ->JsExn.fromException
834
+ ->Option.flatMap(JsExn.message)
835
+ ->Option.getOr("unknown")
836
+ log.info(
837
+ ~comp="preResolversSchemaHook",
838
+ `DynamoDB write/scan failed (${msg}) — using current plugin only`,
839
+ )
840
+ Promise.resolve([pluginFragment])
841
+ })
842
+ }
843
+
844
+ targetApi->Pulumi.Output.flatMap(api =>
845
+ api.id->Pulumi.Output.flatMap(apiId => {
846
+ // Serialise write-row → scan → stitch → push under the shared
847
+ // schema-push lease so a concurrent peer's stale scan can't clobber
848
+ // this stack's fields (see AppSync_Adapter.withSchemaPushLock).
849
+ let runSchemaPush = () => {
850
+ writeAndScanFragments()
851
+ ->Promise.then(async allPluginFragments => {
852
+ // Base fragment selection:
853
+ // - Platform target: always include admin base (the Core API owns admin ops).
854
+ // - Domain target, split mode: empty base (admin lives on Core API).
855
+ // - Domain target, unified mode: include admin base (single API has everything).
856
+ // Use capturedDeployTarget (set synchronously above) — currentDeployTarget has
857
+ // been reset to Domain by deployPlugin before this async callback runs.
858
+ let baseFragment = switch capturedDeployTarget {
859
+ | Platform =>
860
+ AppSync_Adapter.injectAwsAuthAll(
861
+ ReventlessCore.AdminApi.baseFragment(~cloner=Config.cloner),
862
+ ~group="Admin",
863
+ )
864
+ | Domain =>
865
+ if Config.splitApi {
866
+ emptyBaseFragment
867
+ } else {
868
+ AppSync_Adapter.injectAwsAuthAll(
869
+ ReventlessCore.AdminApi.baseFragment(~cloner=Config.cloner),
870
+ ~group="Admin",
871
+ )
872
+ }
873
+ }
874
+ let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
875
+ ~baseFragment,
876
+ ~pluginFragments=allPluginFragments,
877
+ )
878
+ let currentHash = AppSync_Adapter.sha256Hex(sdl)
879
+ let storedHash = switch tableNameOpt {
880
+ | Some(tn) => await readSchemaHash(~tableName=tn, ~apiId)
881
+ | None => None
882
+ }
883
+ let client = AppSync_Adapter.getClient()
884
+
885
+ // Introspect the live schema once — reused for the hash-match
886
+ // drift/repair check and the catastrophic-shrink guard on the push.
887
+ let liveSdl = await AppSync_Adapter.getIntrospectionSdl(client, apiId)
888
+
889
+ // The stored hash records what the DEPLOY last pushed. A runtime
890
+ // re-stitch (mkUpdateApiSchema) can clobber the live schema
891
+ // out-of-band WITHOUT updating this hash, so a matching hash does
892
+ // not guarantee the live schema is intact. Before trusting the
893
+ // hash to skip the push, introspect the live schema and confirm it
894
+ // still carries at least as many root-type (Mutation + Query +
895
+ // Subscription) fields as the SDL we would push. If it has drifted
896
+ // (shrunk) — or cannot be introspected despite a stored hash, which
897
+ // means a real failure rather than a first deploy — force the
898
+ // repair push so a clobbered schema heals on the next deploy.
899
+ let countRoots = s =>
900
+ ReventlessCore.GraphQL_Stitcher.countRootTypeFields(~sdl=s, ~typeName="Mutation") +
901
+ ReventlessCore.GraphQL_Stitcher.countRootTypeFields(~sdl=s, ~typeName="Query") +
902
+ ReventlessCore.GraphQL_Stitcher.countRootTypeFields(
903
+ ~sdl=s,
904
+ ~typeName="Subscription",
905
+ )
906
+ let skipPush = switch storedHash {
907
+ | Some(prev) if prev == currentHash =>
908
+ let expected = countRoots(sdl)
909
+ let live = countRoots(liveSdl)
910
+ if liveSdl == "" {
911
+ log.info(
912
+ ~comp="preResolversSchemaHook",
913
+ `hash matches but live schema could not be introspected — forcing repair push (check appsync:GetIntrospectionSchema permission)`,
914
+ )
915
+ false
916
+ } else if live < expected {
917
+ log.info(
918
+ ~comp="preResolversSchemaHook",
919
+ `hash matches but live schema drifted (${live->Int.toString} root field(s) live vs ${expected->Int.toString} expected) — forcing repair push`,
920
+ )
921
+ false
922
+ } else {
923
+ log.info(
924
+ ~comp="preResolversSchemaHook",
925
+ `SDL unchanged (hash ${currentHash->String.slice(~start=0, ~end=12)}…) and live schema intact (${live->Int.toString} root fields); skipping push`,
926
+ )
927
+ true
928
+ }
929
+ | _ => false
930
+ }
931
+ if !skipPush {
932
+ // Shrink guard — deploy-time counterpart of the runtime
933
+ // mkUpdateApiSchema guard (AdminEventCollectorEntryPoint.mjs).
934
+ // Plugin/service stacks share one AppSync API and StartSchemaCreation
935
+ // REPLACES the whole schema. A concurrent peer that scanned the
936
+ // deploy-schema table before this stack wrote its fragment row
937
+ // stitches an SDL missing this stack's fields; pushing it would drop
938
+ // the live fields and orphan their resolvers (NotFoundException: No
939
+ // field named X). Refuse a push that would catastrophically shrink
940
+ // the live schema — the field-owner's own deploy (whose scan includes
941
+ // its freshly-written row) pushes the complete set.
942
+ // isCatastrophicSchemaShrink returns false when the live schema is
943
+ // empty (first deploy / introspection unavailable), so the initial
944
+ // push still proceeds.
945
+ if (
946
+ ReventlessCore.GraphQL_Stitcher.isCatastrophicSchemaShrink(
947
+ ~currentSdl=liveSdl,
948
+ ~newSdl=sdl,
949
+ ~threshold=deploySchemaShrinkThreshold,
950
+ )
951
+ ) {
952
+ log.error(
953
+ ~comp="preResolversSchemaHook",
954
+ `ABORTED schema push for ${apiId}: stitched SDL (${countRoots(
955
+ sdl,
956
+ )->Int.toString} root field(s)) would catastrophically shrink the live schema (${countRoots(
957
+ liveSdl,
958
+ )->Int.toString} root field(s), threshold ${deploySchemaShrinkThreshold->Float.toString}) — refusing to clobber resolvers (likely a stale concurrent-deploy scan)`,
959
+ )
960
+ } else {
961
+ log.info(
962
+ ~comp="preResolversSchemaHook",
963
+ `Pushing schema to API ${apiId} (${allPluginFragments->Array.length->Int.toString} plugin fragments, new hash: ${currentHash->String.slice(~start=0, ~end=12)}…)`,
964
+ )
965
+ await client->AppSync_Adapter.startSchemaCreationRetrying({
966
+ apiId,
967
+ definition: sdl,
968
+ })
969
+ log.info(
970
+ ~comp="preResolversSchemaHook",
971
+ "startSchemaCreation called, waiting for ACTIVE",
972
+ )
973
+ await AppSync_Adapter.waitForSchemaActive(client, apiId)
974
+ log.info(~comp="preResolversSchemaHook", "Schema is ACTIVE")
975
+ switch tableNameOpt {
976
+ | Some(tn) =>
977
+ await writeSchemaHash(~tableName=tn, ~apiId, ~hash=currentHash)
978
+ | None => ()
979
+ }
980
+ }
981
+ }
982
+
983
+ // No deploy-time retire scan: the name-keyed Plugin aggregate
984
+ // decides supersession itself (VersionSuperseded) when the new
985
+ // version connects, so the manifest carries only the current
986
+ // version without any RM-read-driven command.
987
+ })
988
+ }
989
+
990
+ (
991
+ switch tableNameOpt {
992
+ | Some(tableName) =>
993
+ AppSync_Adapter.withSchemaPushLock(~tableName, ~apiId, runSchemaPush)
994
+ | None => runSchemaPush()
995
+ }
996
+ )->Pulumi.Output.fromPromise
997
+ })
998
+ )
999
+ })
1000
+ },
1001
+ // DCB EventLog created hook — extracts DynamoDB table name for DCB CommandTopic Lambda handler.
1002
+ onDcbEventLogCreated: dcbEventLogUnknown => {
1003
+ let dcbEventLog = dcbEventLogUnknown->asDcbEventLogComponent
1004
+ let outputs = dcbEventLog->ReventlessCore.Component.outputs
1005
+ let tableResource = outputs.resources->Array.getUnsafe(0)
1006
+ PluginRuntime_Builder.registerDcbTableName(tableResource.name)
1007
+ },
1008
+ // DCB CommandTopic created hook — extracts SQS queue URL for slice builders.
1009
+ onDcbCommandTopicCreated: dcbCommandTopicUnknown => {
1010
+ let commandTopic = dcbCommandTopicUnknown->asDcbCommandTopicComponent
1011
+ let channel = commandTopic->ReventlessCore.CommandTopic_Adapter.channel
1012
+ let channelParts = channel.parts->asSqsChannelParts
1013
+ AutomationSliceRuntime_Builder_Single.setDcbQueueUrl(channelParts.queue.id)
1014
+ },
1015
+ // DCB slices created hook — finalize slice Lambdas.
1016
+ onDcbSlicesCreated: dcbEventLogUnknown => {
1017
+ let dcbEventLog = dcbEventLogUnknown->asDcbEventLog
1018
+ StateViewSliceRuntime_Builder_Single.finishWithDcbEventLog(dcbEventLog)
1019
+ AutomationSliceRuntime_Builder_Single.finish()
1020
+ },
1021
+ // Heartbeat EP channel hook — extracts SQS queue URL for heartbeat Lambda handler
1022
+ // and records the calling plugin's id so the Lambda runtime can emit Connect commands
1023
+ // with a non-empty SQS MessageGroupId (FIFO requirement).
1024
+ onHeartbeatEpChannelAvailable: (remoteChannelUnknown, ~pluginId) => {
1025
+ let remoteChannel = remoteChannelUnknown->asRemoteChannel
1026
+ switch remoteChannel.resources->Array.get(0) {
1027
+ | Some(resource) =>
1028
+ PluginRuntime_Builder.registerHeartbeatConfig(
1029
+ ~pluginId,
1030
+ ~epQueueUrl=resource.id->Pulumi.Output.make,
1031
+ (),
1032
+ )
1033
+ | None =>
1034
+ log.warn(~comp="Platform", "heartbeat EP channel has no resources")
1035
+ }
1036
+ },
1037
+
1038
+ // Phase 4 + 5: wire StateTopic and EventLogSubscription Lambdas per plugin.
1039
+ // Only active when the Events API resource exists (platform/monolithic mode).
1040
+ subscriptionInfraHook: ?domainEventsApiOpt->Option.map(eventsApi =>
1041
+ (params: ReventlessCore.Plugin_Helpers.subscriptionInfraParams) => {
1042
+ let {allQueryDbs, allEventTopics, eventLogEntries, opts} = params
1043
+ let customOpts =
1044
+ opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
1045
+ // StateTopic Lambda per stream-enabled QueryDb — publishes row changes
1046
+ // to AppSync Events channels. No GraphQL Subscription resolver is
1047
+ // wired: Source B uses the Events WebSocket directly, not @aws_subscribe.
1048
+ allQueryDbs->Dict.forEachWithKey((_queryDbOutputs, readModelName) => {
1049
+ if QueryDbStorage_DynamoDbStream.streamRegistry->Set.has(readModelName) {
1050
+ // Channel root MUST equal what the host-shell subscribes to. The
1051
+ // AutoUI manifest sets queryableDef.queryField = listFieldName
1052
+ // (plural, e.g. "Catalog_Products") and AutoLive subscribes on that.
1053
+ // Publishing on the singular returnTypeName ("Catalog_Product")
1054
+ // would land descriptors on a channel no client listens to.
1055
+ let topicName =
1056
+ ReventlessCore.Plugin_Helpers.queryFieldNamesRegistry
1057
+ ->Dict.get(readModelName)
1058
+ ->Option.map(qn => qn.listFieldName)
1059
+ ->Option.getOr(readModelName)
1060
+ StateTopic_AppSync.make(
1061
+ ~readModelName,
1062
+ ~topicName,
1063
+ ~allQueryDbs,
1064
+ ~eventsApi,
1065
+ ~opts=customOpts,
1066
+ )
1067
+ }
1068
+ })
1069
+
1070
+ // Phase 5: EventLogSubscription per SNS-backed event log entry.
1071
+ // DynamoDB stream event topics (Category, DCB) are skipped — no SNS subscription needed.
1072
+ eventLogEntries->Array.forEach(entry => {
1073
+ // Aggregate EventTopics are keyed by Spec.name (= displayName).
1074
+ // DCB EventTopic is keyed by busKey (= pluginName ++ "DcbEventLog").
1075
+ let isSns =
1076
+ EventTopicPublisher_SNS.snsRegistry->Set.has(entry.displayName) ||
1077
+ EventTopicPublisher_SNS.snsRegistry->Set.has(entry.busKey)
1078
+ let eventTopicOutputs =
1079
+ isSns
1080
+ ? allEventTopics
1081
+ ->Dict.get(entry.displayName)
1082
+ ->Option.orElse(allEventTopics->Dict.get(entry.busKey))
1083
+ : None
1084
+ eventTopicOutputs->Option.forEach(outputs =>
1085
+ EventLogSubscription_AppSync.make(
1086
+ ~name=entry.displayName,
1087
+ ~topicName=entry.displayName,
1088
+ ~eventTopicOutputs=outputs,
1089
+ ~eventsApi,
1090
+ ~opts=customOpts,
1091
+ )
1092
+ )
1093
+ })
1094
+ // Finalize the shared StateTopic Lambda + IAM + per-stream ESMs from the
1095
+ // entries the loop above just registered. Must run INSIDE this hook because
1096
+ // Plugin_Builder fires it from inside a Pulumi.Output.apply chain — calling
1097
+ // `finish` from deployPlugin (after P.make returns) would see the registry
1098
+ // empty because the hook hasn't run yet. Admin's hook fires synchronously,
1099
+ // but the call site is unified here so admin and plugins share one path.
1100
+ StateTopic_AppSync.finish(~eventsApi, ~opts={})
1101
+ }
1102
+ ),
1103
+ }
1104
+
1105
+ // Apply Plugin functor with the platform hooks, then constrain the result to Plugin.T.
1106
+ module PluginBuilderImpl = Plugin.Make({let hooks = hooks})
1107
+ module Plugin: ReventlessInfra.Plugin.T
1108
+ with type api = Types.AppSync.api
1109
+ and type role = Types.AppSync.role
1110
+ and type component = ReventlessCore.Plugin.component = {
1111
+ type api = Types.AppSync.api
1112
+ type role = Types.AppSync.role
1113
+ type component = ReventlessCore.Plugin.component
1114
+ let make = PluginBuilderImpl.make
1115
+ let makeAutoUIManifest = PluginBuilderImpl.makeAutoUIManifest
1116
+ let makePluginDefinition = PluginBuilderImpl.makePluginDefinition
1117
+ }
1118
+
1119
+ module RuntimeEnvironment = RuntimeEnvironment.Lambda
1120
+ module EventCollectorChannel = EventCollectorChannel.SQS
1121
+ module Admin = ReventlessCore.Platform_Admin.Make(
1122
+ RuntimeEnvironment,
1123
+ EventCollectorChannel,
1124
+ QueryEngine.DynamoDb,
1125
+ ClonerRunner.Fargate,
1126
+ PluginRuntime_Builder.Make(EventCollectorChannel),
1127
+ DcbEventLogStorage.DynamoDb,
1128
+ EventTopicPublisher.DynamoDbStream,
1129
+ CommandTopicChannel.SQS_Sync,
1130
+ CommandTopicChannel.SQS_Async,
1131
+ {
1132
+ let silent = false
1133
+ let splitApi = Config.splitApi
1134
+ let cloner = Config.cloner
1135
+ let hooks = hooks
1136
+ },
1137
+ )
1138
+
1139
+ // Admin-internal Plugin aggregate — standalone component so the PluginExtensionPoint
1140
+ // can publish commands to it and its infrastructure appears in stack outputs.
1141
+ // Uses the standard `Aggregate_Builder_Single` so admin-facing variants of
1142
+ // `PluginSpec.command` (Activate / Deactivate) are exposed as AppSync mutations
1143
+ // via the auto-resolver flow. Internal-protocol variants (Heartbeat, Connect,
1144
+ // Disconnect, ReportIncompatibility) carry `@noApi` and are filtered out before
1145
+ // SDL/resolver generation.
1146
+ module PluginAggregate: (
1147
+ ReventlessInfra.Aggregate.T with type api = Types.AppSync.api
1148
+ ) = Aggregate_Builder_Single.Make(
1149
+ ReventlessCore.PluginSpec,
1150
+ ReventlessCore.PluginBehavior,
1151
+ ReventlessInfra.NoEventMappings.Make(ReventlessCore.PluginSpec),
1152
+ )
1153
+
1154
+ // Admin-internal Plugin read model — standalone component for the Plugin QueryDb
1155
+ // (DynamoDB table) that backs queryEngine.scan(~readModelName="Plugins", ...).
1156
+ module PluginReadModelMappings: Reventless.Projection.Mappings
1157
+ with module Target := ReventlessCore.PluginsReadModelSpec = {
1158
+ // Reuse the already-instantiated Mappings from PluginsProjection so the Mapping
1159
+ // module type is the same nominal type — no coercion needed.
1160
+ module M = ReventlessCore.PluginsProjection.Mappings
1161
+ module type Mapping = M.Mapping
1162
+ // moduleUrl points at PluginsProjection.res.mjs (the file that exports `mappings`),
1163
+ // NOT this Platform.res file — the bundled ReadModel Lambda dynamic-imports this URL
1164
+ // at cold start, and Platform.res transitively imports @pulumi/aws (deploy-time only).
1165
+ let moduleUrl: string = ReventlessCore.PluginsProjection.moduleUrl
1166
+ let mappings: array<module(Mapping)> = ReventlessCore.PluginsProjection.mappings
1167
+ }
1168
+
1169
+ // Standard ReadModel builder — attaches AppSync DynamoDB resolvers to the
1170
+ // admin-prefixed `Platform_Plugin` / `Platform_Plugins` SDL fields declared by
1171
+ // PluginBaseFragment.queryEntries. Field-name alignment is handled by the
1172
+ // queryFieldNamesRegistry entries that Platform_Admin.construct populates
1173
+ // from AdminApi.queryEntries before this builder runs.
1174
+ module PluginReadModel = ReadModel_Builder_Single_Stream.Make(
1175
+ ReventlessCore.PluginsReadModelSpec,
1176
+ PluginReadModelMappings,
1177
+ )
1178
+
1179
+ // Admin-internal UIFragmentRegistry read model — projects UIFragmentRegistered/
1180
+ // Updated/Deregistered events from the Plugin aggregate into a DynamoDB table the
1181
+ // Platform_UIFragments admin query Lambda scans.
1182
+ module UIFragmentRegistryReadModelMappings: Reventless.Projection.Mappings
1183
+ with module Target := ReventlessCore.UIFragmentRegistryReadModelSpec = {
1184
+ module M = ReventlessCore.UIFragmentRegistryProjection.Mappings
1185
+ module type Mapping = M.Mapping
1186
+ // See PluginReadModelMappings.moduleUrl note above.
1187
+ let moduleUrl: string = ReventlessCore.UIFragmentRegistryProjection.moduleUrl
1188
+ let mappings: array<module(Mapping)> = ReventlessCore.UIFragmentRegistryProjection.mappings
1189
+ }
1190
+
1191
+ module UIFragmentRegistryReadModel = ReadModel_Builder_NoResolver_Stream.Make(
1192
+ ReventlessCore.UIFragmentRegistryReadModelSpec,
1193
+ UIFragmentRegistryReadModelMappings,
1194
+ )
1195
+
1196
+ module type PluginMaker = {
1197
+ let make: unit => Plugin.component
1198
+ }
1199
+
1200
+ let makeScheduler = () => {
1201
+ let component = Scheduler.make()
1202
+ // Surface the IAM role ARN so bundled Task Lambdas can call PutRule with
1203
+ // the right `roleArn`. ScheduledPublisher_CloudWatchEvents stashes it as
1204
+ // `outputs.resource.urn` (no dedicated field on Scheduler.outputs).
1205
+ let outputs = component->ReventlessCore.Component.outputs
1206
+ hooks.schedulerRoleUrn := outputs.resource.urn
1207
+ component->ReventlessCore.Component.operations
1208
+ }
1209
+
1210
+ type mcpSupported = | @as(true) McpSupported | @as(false) McpNotSupported
1211
+ let mcpSupported = McpNotSupported
1212
+
1213
+ // In split mode, create a dedicated core AppSync API and push the core schema.
1214
+ // In unified mode, makePlatform is a no-op (schema stitching handled by events).
1215
+ let makePlatform = (~version, ~plugins: array<module(PluginMaker)>) => {
1216
+ log.info(~comp="Platform", `v${version}`)
1217
+ // Create scheduler and populate platform context refs so Plugin_Builder
1218
+ // can read them without app plugins having to pass them through.
1219
+ let scheduler = makeScheduler()
1220
+ hooks.scheduler := Some(scheduler)
1221
+ hooks.api := Some(domainApi->wrapHookedValue)
1222
+ hooks.apiRole := Some(domainApiRole->wrapHookedValue)
1223
+
1224
+ // Phase 2: create the Platform API resource early — before Admin.construct —
1225
+ // so admin resolvers are attached to the correct API in split mode.
1226
+ // In unified mode this is the same resource as the Domain API.
1227
+ let (platformApi, platformApiRole) = if Config.splitApi {
1228
+ AppSync_Adapter.makeApiResource(~name="PlatformApi", ~opts={})
1229
+ } else {
1230
+ (domainApi, domainApiRole)
1231
+ }
1232
+
1233
+ // Update splitApiOutputsRef and apiConfig with the now-known Platform API.
1234
+ if Config.splitApi {
1235
+ splitApiOutputsRef := Some({platformApi, platformApiRole})
1236
+ switch apiConfigRef.contents {
1237
+ | Some(c) =>
1238
+ apiConfigRef := Some({
1239
+ domainApi: c.domainApi,
1240
+ domainApiRole: c.domainApiRole,
1241
+ platformApi: platformApi,
1242
+ platformApiRole: platformApiRole,
1243
+ })
1244
+ | None => ()
1245
+ }
1246
+ }
1247
+
1248
+ // Phase 2: Admin resolvers go on the Platform API (platformApi) in split mode,
1249
+ // on the shared Domain API in unified mode.
1250
+ let admin = Admin.construct(
1251
+ ~version,
1252
+ ~extensionPoints=[],
1253
+ ~aggregates=[module(PluginAggregate)],
1254
+ ~readModels=[
1255
+ module(PluginReadModel),
1256
+ module(UIFragmentRegistryReadModel),
1257
+ ],
1258
+ ~scheduler,
1259
+ ~resourceNaming=Util_ResourceNaming.operations,
1260
+ ~api=platformApi,
1261
+ ~apiRole=platformApiRole,
1262
+ ~stateChangeSlices=[],
1263
+ ~stateViewSlices=[],
1264
+ ~automationSlices=[],
1265
+ ~outboundTranslationSlices=[],
1266
+ ~inboundTranslationSlices=[],
1267
+ )
1268
+
1269
+ // Mount the Platform_ComponentDefinitions Lambda resolver on the Platform API
1270
+ // (split mode) or Domain API (unified mode — platformApi == domainApi above).
1271
+ // Also register the Plugin RM table with the AllAggregates Lambda runtime
1272
+ // so its in-Lambda plugin status gate can read plugin status at command
1273
+ // dispatch time (Part 2.3 of the resolver plan).
1274
+ switch admin.readModelsOutputs->Dict.get("Plugins") {
1275
+ | Some(pluginRm) =>
1276
+ switch pluginRm.queryDb.resources->Array.get(0) {
1277
+ | Some(r) =>
1278
+ Platform_ComponentDefinitions_Lambda.make(
1279
+ ~api=platformApi,
1280
+ ~pluginReadModelTableName=r.name,
1281
+ ~opts={},
1282
+ )
1283
+ AggregateRuntime_Builder_Single.setPluginReadModelTable(~name=r.name)
1284
+ | None => ()
1285
+ }
1286
+ | None => ()
1287
+ }
1288
+
1289
+ // Mount the Platform_UIFragments Lambda resolver — scans the UIFragmentRegistry
1290
+ // table provisioned above and returns one entry per registered plugin UI.
1291
+ switch admin.readModelsOutputs->Dict.get("UIFragmentRegistry") {
1292
+ | Some(rm) =>
1293
+ switch rm.queryDb.resources->Array.get(0) {
1294
+ | Some(r) =>
1295
+ Platform_UIFragments_Lambda.make(
1296
+ ~api=platformApi,
1297
+ ~uiFragmentRegistryTableName=r.name,
1298
+ ~opts={},
1299
+ )
1300
+ | None => ()
1301
+ }
1302
+ | None => ()
1303
+ }
1304
+
1305
+ // Build each plugin.
1306
+ let pluginComponents = plugins->Array.map(plugin => {
1307
+ module P = unpack(plugin)
1308
+ P.make()
1309
+ })
1310
+
1311
+ // Note: StateTopic_AppSync.finish runs from inside subscriptionInfraHook
1312
+ // (Phase 4 wiring above), not here — Plugin_Builder fires the hook from
1313
+ // within a Pulumi.Output.apply chain, so finishing synchronously here would
1314
+ // see an empty registry.
1315
+
1316
+ // Export first plugin's outputs (monolithic mode = typically single plugin).
1317
+ switch pluginComponents->Array.get(0) {
1318
+ | Some(pluginComponent) =>
1319
+ let pluginOutputs = pluginComponent->ReventlessCore.Component.outputs
1320
+ ReventlessCore.Plugin_Helpers.exportPluginOutputs(pluginOutputs)
1321
+ | None => ()
1322
+ }
1323
+
1324
+ // Admin schema push is fired by preAdminResolversSchemaHook from inside
1325
+ // Admin.construct (with createResolvers gated on it). Only exports below.
1326
+ if Config.splitApi {
1327
+ // Platform API exports (split mode).
1328
+ Pulumi.Pulumi.export("platformApiId", platformApi->Pulumi.Output.flatMap(api => api.id))
1329
+ Pulumi.Pulumi.export(
1330
+ "platformApiEndpoint",
1331
+ platformApi->Pulumi.Output.flatMap(api =>
1332
+ api.uris->Pulumi.Output.apply(uris => uris.graphQL)
1333
+ ),
1334
+ )
1335
+ Pulumi.Pulumi.export("platformApiRoleArn", platformApiRole->Pulumi.Output.flatMap(role => role.arn))
1336
+ } else {
1337
+ // Platform API exports (unified mode — same resource as Domain API).
1338
+ Pulumi.Pulumi.export("platformApiId", domainApi->Pulumi.Output.flatMap(api => api.id))
1339
+ Pulumi.Pulumi.export(
1340
+ "platformApiEndpoint",
1341
+ domainApi->Pulumi.Output.flatMap(api =>
1342
+ api.uris->Pulumi.Output.apply(uris => uris.graphQL)
1343
+ ),
1344
+ )
1345
+ Pulumi.Pulumi.export("platformApiRoleArn", domainApiRole->Pulumi.Output.flatMap(role => role.arn))
1346
+ }
1347
+
1348
+ // Domain API exports.
1349
+ Pulumi.Pulumi.export("domainApiId", domainApi->Pulumi.Output.flatMap(api => api.id))
1350
+ Pulumi.Pulumi.export(
1351
+ "domainApiEndpoint",
1352
+ domainApi->Pulumi.Output.flatMap(api =>
1353
+ api.uris->Pulumi.Output.apply(uris => uris.graphQL)
1354
+ ),
1355
+ )
1356
+ Pulumi.Pulumi.export("domainApiRoleArn", domainApiRole->Pulumi.Output.flatMap(role => role.arn))
1357
+ }
1358
+
1359
+ // Optional host UI shell bundle: a static SPA (e.g. reventless-ui's host-shell)
1360
+ // hosted on the platform's CloudFront-fronted S3 bucket. The platform writes a
1361
+ // `config.json` next to `index.html` so the shell discovers `apiEndpoint` and
1362
+ // `region` at boot without rebuild.
1363
+ type hostUiBundleConfig = {
1364
+ assetsDir: string,
1365
+ bundleVersion: string,
1366
+ }
1367
+
1368
+ let deployPlatform = (~version, ~hostUiBundle: option<hostUiBundleConfig>=?) => {
1369
+ log.info(~comp="Platform:deployPlatform", `v${version}`)
1370
+ let scheduler = makeScheduler()
1371
+ hooks.scheduler := Some(scheduler)
1372
+ hooks.api := Some(domainApi->wrapHookedValue)
1373
+ hooks.apiRole := Some(domainApiRole->wrapHookedValue)
1374
+
1375
+ // Phase 2: create the Platform API resource early — before Admin.construct —
1376
+ // so admin resolvers are attached to the correct API in split mode.
1377
+ // In unified mode this is the same resource as the Domain API.
1378
+ let (platformApi, platformApiRole) = if Config.splitApi {
1379
+ AppSync_Adapter.makeApiResource(~name="PlatformApi", ~opts={})
1380
+ } else {
1381
+ (domainApi, domainApiRole)
1382
+ }
1383
+
1384
+ // Update splitApiOutputsRef and apiConfig with the now-known Platform API.
1385
+ if Config.splitApi {
1386
+ splitApiOutputsRef := Some({platformApi, platformApiRole})
1387
+ switch apiConfigRef.contents {
1388
+ | Some(c) =>
1389
+ apiConfigRef := Some({
1390
+ domainApi: c.domainApi,
1391
+ domainApiRole: c.domainApiRole,
1392
+ platformApi: platformApi,
1393
+ platformApiRole: platformApiRole,
1394
+ })
1395
+ | None => ()
1396
+ }
1397
+ }
1398
+
1399
+ // Create PluginExtensionPoint with runtime schema stitching.
1400
+ // When plugins connect/disconnect, the handler queries the Plugin read model
1401
+ // for all active schema fragments, stitches them with the admin base, and
1402
+ // pushes the combined schema to AppSync via the SDK.
1403
+ //
1404
+ // The closure captures domainApiId (a Pulumi Output). Pulumi serializes
1405
+ // captured Outputs into the CallbackFunction Lambda; at runtime, Output.get
1406
+ // returns the resolved string synchronously.
1407
+ let domainApiId = domainApi->Pulumi.Output.flatMap(api => api.id)
1408
+
1409
+ module PluginExtensionPoint = Plugin_ExtensionPoint_Builder.MakeWithConfig({
1410
+ // Cross-plugin SNS subscription management runs in the bundled
1411
+ // AdminEventCollector Lambda (AdminEventCollectorEntryPoint.mjs), not in
1412
+ // this deploy-time EP Lambda — which only handles incoming commands
1413
+ // (Heartbeat, ForwardCommand). None here keeps the deploy-time path
1414
+ // unchanged; the .mjs entry point supplies a real implementation.
1415
+ let manageSubscriptions = None
1416
+ let updateApiSchema = Some(async (queryEngine: Reventless.QueryEngine.operations) => {
1417
+ open Reventless.QueryEngine.Filter
1418
+ let apiId = domainApiId->Pulumi.Output.get
1419
+ let plugins = await queryEngine.scan(
1420
+ ~readModelName="Plugins",
1421
+ ~filterConfigs=[("status", Contains, String("Connected"))],
1422
+ ~limit=1000,
1423
+ )
1424
+ let fragments = plugins->Array.filterMap(json =>
1425
+ try {
1426
+ let state = json->S.parseOrThrow(ReventlessCore.PluginsReadModelSpec.stateSchema)
1427
+ // Exclude Platform-target plugins — their schema belongs on the PlatformApi,
1428
+ // not the DomainApi. Absent apiTarget defaults to "Domain".
1429
+ switch state.apiTarget {
1430
+ | Some("Platform") => None
1431
+ | _ => state.apiSchemaFragment
1432
+ }
1433
+ } catch {
1434
+ | _ => None
1435
+ }
1436
+ )
1437
+ // In split mode, the plugin API only has plugin schema (admin is on the core API).
1438
+ // In unified mode, stitch admin + plugins into the single shared API.
1439
+ let baseFragment = if Config.splitApi {
1440
+ emptyBaseFragment
1441
+ } else {
1442
+ AppSync_Adapter.injectAwsAuthAll(
1443
+ ReventlessCore.AdminApi.baseFragment(~cloner=Config.cloner),
1444
+ ~group="Admin",
1445
+ )
1446
+ }
1447
+ let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
1448
+ ~baseFragment,
1449
+ ~pluginFragments=fragments,
1450
+ )
1451
+ await AppSync_Adapter.getClient()->AppSync_Adapter.startSchemaCreationRetrying({
1452
+ apiId,
1453
+ definition: sdl,
1454
+ })
1455
+ })
1456
+ })
1457
+
1458
+ // Phase 2: Admin resolvers go on the Platform API (platformApi) in split mode,
1459
+ // on the shared Domain API in unified mode.
1460
+ let admin = Admin.construct(
1461
+ ~version,
1462
+ ~extensionPoints=[module(PluginExtensionPoint)],
1463
+ ~aggregates=[module(PluginAggregate)],
1464
+ ~readModels=[
1465
+ module(PluginReadModel),
1466
+ module(UIFragmentRegistryReadModel),
1467
+ ],
1468
+ ~scheduler,
1469
+ ~resourceNaming=Util_ResourceNaming.operations,
1470
+ ~api=platformApi,
1471
+ ~apiRole=platformApiRole,
1472
+ ~stateChangeSlices=[],
1473
+ ~stateViewSlices=[],
1474
+ ~automationSlices=[],
1475
+ ~outboundTranslationSlices=[],
1476
+ ~inboundTranslationSlices=[],
1477
+ )
1478
+
1479
+ // Extract Plugin RM table name as Output.t<string>.
1480
+ // IMPORTANT: Do NOT use option<Pulumi.Output.t<…>> — Pulumi Outputs use property
1481
+ // lifting, which breaks ReScript's internal option encoding (BS_PRIVATE_NESTED_SOME_NONE).
1482
+ // Instead, use Output.t<string> with a placeholder for "not available".
1483
+ let pluginReadModelTableName = switch admin.readModelsOutputs->Dict.get("Plugins") {
1484
+ | Some(pluginRm) =>
1485
+ switch pluginRm.queryDb.resources->Array.get(0) {
1486
+ | Some(r) => Some(r.name)
1487
+ | None => None
1488
+ }
1489
+ | None => None
1490
+ }
1491
+
1492
+ // Dedicated DynamoDB table for deploy-time schema-fragment persistence
1493
+ // (deploy-schema:<name>, deploy-schema-platform:<name>, deploy-schema-hash:<apiId>).
1494
+ // Previously these infrastructure rows shared the Plugin RM table, which
1495
+ // caused them to leak through Platform_Plugins' auto-generated AppSync
1496
+ // Connection resolver (an unfiltered Scan). Hosting them on their own
1497
+ // table keeps Plugin RM = Plugin aggregate entities only and restores
1498
+ // parity with the in-memory adapter (which has no preResolversSchemaHook).
1499
+ let pluginSchemaPersistenceTable = Util.DynamoDb.makeTable(
1500
+ "PluginSchemaPersistence",
1501
+ ~attributes=[{name: "id", type_: "S"}],
1502
+ ~opts={},
1503
+ )
1504
+
1505
+ PluginExtensionPointRuntime_Builder.registerPluginExtensionPoint(
1506
+ ~pluginReadModelTableName?,
1507
+ ~schedulerRoleArn=hooks.schedulerRoleUrn.contents,
1508
+ (),
1509
+ )
1510
+
1511
+ // Extract the Plugin ExtensionPoint's EventTopic (SNS) ARN so the
1512
+ // AdminEventCollector runtime can publish outgoing EP events (e.g.
1513
+ // UnknownPluginDetected). Without this, AdminEventColl's HANDLER_CONFIG
1514
+ // carries eventTopicArn="NOT_AVAILABLE" and the SNS publish silently
1515
+ // targets a non-existent ARN, breaking the
1516
+ // admin → plugin EventCollector → ConnectPlugin round-trip and leaving
1517
+ // the Plugin read model empty.
1518
+ let pluginEpEventTopicArn =
1519
+ admin.extensionPointsOutputs->Pulumi.Output.flatMap(eps =>
1520
+ switch eps->Array.find(ep =>
1521
+ ep.name == ReventlessInfra.PluginExtensionPointSpec.name
1522
+ ) {
1523
+ | Some(ep) =>
1524
+ ep.eventTopic->Pulumi.Output.flatMap(et =>
1525
+ switch et.resources->Array.get(0) {
1526
+ | Some(r) => r.urn
1527
+ | None => Pulumi.Output.make("NOT_AVAILABLE")
1528
+ }
1529
+ )
1530
+ | None => Pulumi.Output.make("NOT_AVAILABLE")
1531
+ }
1532
+ )
1533
+
1534
+ PluginRuntime_Builder.registerConfig(
1535
+ ~eventTopicArn=pluginEpEventTopicArn,
1536
+ ~appSyncApiId=domainApiId,
1537
+ ~pluginReadModelTableName?,
1538
+ // Runtime schema stitch reads deploy-time fragments from this durable table
1539
+ // rather than the lifecycle-volatile Plugin RM Connected rows.
1540
+ ~pluginSchemaPersistenceTableName=pluginSchemaPersistenceTable.name,
1541
+ ~schedulerRoleArn=hooks.schedulerRoleUrn.contents,
1542
+ ~clonerEnabled=Config.cloner,
1543
+ (),
1544
+ )
1545
+
1546
+ // Mount the Platform_ComponentDefinitions Lambda resolver on the Platform API
1547
+ // (split mode) or Domain API (unified mode — platformApi == domainApi above).
1548
+ // Also register the Plugin RM table with the AllAggregates Lambda runtime
1549
+ // so its in-Lambda plugin status gate can read plugin status at command
1550
+ // dispatch time (Part 2.3 of the resolver plan).
1551
+ switch pluginReadModelTableName {
1552
+ | Some(tableName) =>
1553
+ AggregateRuntime_Builder_Single.setPluginReadModelTable(~name=tableName)
1554
+ Platform_ComponentDefinitions_Lambda.make(
1555
+ ~api=platformApi,
1556
+ ~pluginReadModelTableName=tableName,
1557
+ ~opts={},
1558
+ )
1559
+ | None => ()
1560
+ }
1561
+
1562
+ // Mount the Platform_UIFragments Lambda resolver — scans the UIFragmentRegistry
1563
+ // table provisioned above and returns one entry per registered plugin UI.
1564
+ switch admin.readModelsOutputs->Dict.get("UIFragmentRegistry") {
1565
+ | Some(rm) =>
1566
+ switch rm.queryDb.resources->Array.get(0) {
1567
+ | Some(r) =>
1568
+ Platform_UIFragments_Lambda.make(
1569
+ ~api=platformApi,
1570
+ ~uiFragmentRegistryTableName=r.name,
1571
+ ~opts={},
1572
+ )
1573
+ | None => ()
1574
+ }
1575
+ | None => ()
1576
+ }
1577
+
1578
+ // Note: StateTopic_AppSync.finish runs from inside subscriptionInfraHook
1579
+ // (Phase 4 wiring above), not here — see the note in deployPlatform.
1580
+
1581
+ // Admin schema push is fired by preAdminResolversSchemaHook from inside
1582
+ // Admin.construct (gated on a read-model-resources barrier, with admin
1583
+ // createResolvers chained behind the resulting Output). Only exports below.
1584
+ if Config.splitApi {
1585
+ // Platform API exports (split mode).
1586
+ Pulumi.Pulumi.export("platformApiId", platformApi->Pulumi.Output.flatMap(api => api.id))
1587
+ Pulumi.Pulumi.export(
1588
+ "platformApiEndpoint",
1589
+ platformApi->Pulumi.Output.flatMap(api =>
1590
+ api.uris->Pulumi.Output.apply(uris => uris.graphQL)
1591
+ ),
1592
+ )
1593
+ Pulumi.Pulumi.export("platformApiRoleArn", platformApiRole->Pulumi.Output.flatMap(role => role.arn))
1594
+ } else {
1595
+ // Platform API exports (unified mode — same resource as Domain API).
1596
+ Pulumi.Pulumi.export("platformApiId", domainApi->Pulumi.Output.flatMap(api => api.id))
1597
+ Pulumi.Pulumi.export(
1598
+ "platformApiEndpoint",
1599
+ domainApi->Pulumi.Output.flatMap(api =>
1600
+ api.uris->Pulumi.Output.apply(uris => uris.graphQL)
1601
+ ),
1602
+ )
1603
+ Pulumi.Pulumi.export("platformApiRoleArn", domainApiRole->Pulumi.Output.flatMap(role => role.arn))
1604
+ }
1605
+
1606
+ // Domain API exports.
1607
+ Pulumi.Pulumi.export("domainApiId", domainApi->Pulumi.Output.flatMap(api => api.id))
1608
+ Pulumi.Pulumi.export(
1609
+ "domainApiEndpoint",
1610
+ domainApi->Pulumi.Output.flatMap(api =>
1611
+ api.uris->Pulumi.Output.apply(uris => uris.graphQL)
1612
+ ),
1613
+ )
1614
+ Pulumi.Pulumi.export("domainApiRoleArn", domainApiRole->Pulumi.Output.flatMap(role => role.arn))
1615
+
1616
+ // Events API exports — consumed by plugin stacks to wire Source B (StateTopic) Lambdas.
1617
+ switch domainEventsApiOpt {
1618
+ | Some(eventsApi) =>
1619
+ Pulumi.Pulumi.export("eventsApiArn", eventsApi.api.apiArn)
1620
+ Pulumi.Pulumi.export(
1621
+ "eventsApiDns",
1622
+ eventsApi.api.dns->Pulumi.Output.apply(dns => dns.http->Option.getOr("")),
1623
+ )
1624
+ | None => ()
1625
+ }
1626
+
1627
+ // Export Plugin RM table name so plugin stacks can query existing plugins
1628
+ // for cumulative schema stitching (preResolversSchemaHook).
1629
+ switch pluginReadModelTableName {
1630
+ | Some(tableName) => Pulumi.Pulumi.export("pluginRmTableName", tableName)
1631
+ | None => ()
1632
+ }
1633
+
1634
+ // Export the dedicated schema-persistence table name. preResolversSchemaHook
1635
+ // prefers this over pluginRmTableName so deploy-schema rows no longer share
1636
+ // the Plugin RM table.
1637
+ Pulumi.Pulumi.export(
1638
+ "pluginSchemaPersistenceTableName",
1639
+ pluginSchemaPersistenceTable.name,
1640
+ )
1641
+
1642
+ // (No pluginAggrCmdTopicUrl export: the deploy-time retire hook that
1643
+ // published Retire commands to the Plugin aggregate queue is gone —
1644
+ // supersession is decided by the aggregate on connect.)
1645
+
1646
+ // Export admin component outputs (same pattern as deployPlugin).
1647
+ ReventlessCore.Plugin_Helpers.exportPlatformOutputs(
1648
+ ~extensionPointsOutputs=admin.extensionPointsOutputs,
1649
+ ~aggregatesOutputs=admin.aggregatesOutputs,
1650
+ ~readModelsOutputs=admin.readModelsOutputs,
1651
+ ~dcbEventLogOutputs=admin.dcbEventLogOutputs,
1652
+ ~stateChangeSlicesOutputs=admin.stateChangeSlicesOutputs,
1653
+ ~stateViewSlicesOutputs=admin.stateViewSlicesOutputs,
1654
+ ~automationSlicesOutputs=admin.automationSlicesOutputs,
1655
+ ~outboundTranslationSlicesOutputs=admin.outboundTranslationSlicesOutputs,
1656
+ ~inboundTranslationSlicesOutputs=admin.inboundTranslationSlicesOutputs,
1657
+ )
1658
+
1659
+ // Fire onPlatformDeployed hook with resolved platform metadata.
1660
+ let resolvedDomainApiEndpoint = domainApi->Pulumi.Output.flatMap(api =>
1661
+ api.uris->Pulumi.Output.apply(uris => uris.graphQL)
1662
+ )
1663
+ let resolvedDomainApiRoleArn = domainApiRole->Pulumi.Output.flatMap(role => role.arn)
1664
+ let resolvedPlatformApiEndpoint = platformApi->Pulumi.Output.flatMap(api =>
1665
+ api.uris->Pulumi.Output.apply(uris => uris.graphQL)
1666
+ )
1667
+ let resolvedPlatformApiRoleArn = platformApiRole->Pulumi.Output.flatMap(role => role.arn)
1668
+ // Collect admin aggregate + read model resources.
1669
+ let adminResourcesOutput =
1670
+ admin.aggregatesOutputs
1671
+ ->Dict.valuesToArray
1672
+ ->Array.map(agg =>
1673
+ agg
1674
+ ->ReventlessCore.Aggregate.toResolvedOutputs
1675
+ ->Pulumi.Output.apply((r: ReventlessInterop.Aggregate.resolvedOutputs) =>
1676
+ Array.flat([
1677
+ r.eventLog.resources,
1678
+ r.eventLog.eventTopic.resources,
1679
+ r.commandTopic.resources,
1680
+ r.commandGenerator.resources,
1681
+ ])
1682
+ )
1683
+ )
1684
+ ->Array.concat(
1685
+ admin.readModelsOutputs
1686
+ ->Dict.valuesToArray
1687
+ ->Array.map(rm =>
1688
+ rm
1689
+ ->ReventlessCore.ReadModel.toResolvedOutputs
1690
+ ->Pulumi.Output.apply((r: ReventlessInterop.ReadModel.resolvedOutputs) =>
1691
+ r.queryDb.resources
1692
+ )
1693
+ ),
1694
+ )
1695
+ ->Pulumi.Output.all
1696
+ ->Pulumi.Output.apply(arrays => Array.flat(arrays))
1697
+ let _ =
1698
+ (
1699
+ resolvedDomainApiEndpoint,
1700
+ resolvedDomainApiRoleArn,
1701
+ resolvedPlatformApiEndpoint,
1702
+ resolvedPlatformApiRoleArn,
1703
+ adminResourcesOutput,
1704
+ )
1705
+ ->Pulumi.Output.all5
1706
+ ->Pulumi.Output.apply(((domainApiEndpoint, domainApiRoleArn, platformApiEndpoint, platformApiRoleArn, adminResources)) => {
1707
+ let region =
1708
+ Pulumi.Config.make(Some("aws"))->Pulumi.Config.get("region")->Option.getOr("unknown")
1709
+ ReventlessCore.Plugin_Helpers.firePlatformDeployedHook({
1710
+ name: Pulumi.Pulumi.getProjectName(),
1711
+ environment: Pulumi.Pulumi.getStackName(),
1712
+ region,
1713
+ domainApiEndpoint,
1714
+ domainApiRoleArn,
1715
+ platformApiEndpoint,
1716
+ platformApiRoleArn,
1717
+ adminResources,
1718
+ })
1719
+ })
1720
+
1721
+ // Host UI shell deployment — opt-in via ~hostUiBundle. The shell SPA is
1722
+ // hosted on its own CloudFront distribution; `config.json` is generated
1723
+ // at deploy time with the resolved API endpoints, region, and Cognito
1724
+ // pool/client IDs so the shell can boot without knowing them at build
1725
+ // time. `authMode: "cognito"` matches the AppSync auth wiring established
1726
+ // by Stage D (Auth_Cognito.make). Cognito values come from the
1727
+ // process-cached `Platform_Stack.resolveCognitoUserPool` so no extra
1728
+ // resources or stack exports are introduced here.
1729
+ switch hostUiBundle {
1730
+ | None => ()
1731
+ | Some(cfg) =>
1732
+ // Auto-derive the host-shell FQDN when both `hostUiBaseDomain` and
1733
+ // `hostUiHostedZoneId` are configured (env var → `Pulumi.local.yaml` →
1734
+ // `Pulumi.<stack>.yaml`). When either is missing the framework keeps the
1735
+ // default *.cloudfront.net URL — no surprise opt-in. See
1736
+ // [docs/plans/done/host-ui-custom-domain.md].
1737
+ let customDomain = switch (
1738
+ Util_LocalConfig.get("hostUiBaseDomain"),
1739
+ Util_LocalConfig.get("hostUiHostedZoneId"),
1740
+ ) {
1741
+ | (Some(bd), Some(hz)) =>
1742
+ let stack = Pulumi.Pulumi.getStackName()
1743
+ let baseName =
1744
+ Util_LocalConfig.get("hostUiBaseName")->Option.getOr(Pulumi.Pulumi.getProjectName())
1745
+ let prodStacks =
1746
+ Util_LocalConfig.get("hostUiProdStacks")
1747
+ ->Option.map(Util_HostUiDomain.parseProdStacks)
1748
+ ->Option.getOr(Util_HostUiDomain.defaultProdStacks)
1749
+ let fqdn = Util_HostUiDomain.deriveFqdn(
1750
+ ~baseName,
1751
+ ~stack,
1752
+ ~baseDomain=bd,
1753
+ ~prodStacks,
1754
+ )
1755
+ Some({Plugin_Stack.fqdn, hostedZoneId: hz})
1756
+ | _ => None
1757
+ }
1758
+
1759
+ let {distributionUrl, bucketName} = Plugin_Stack.makeUiBundleDistribution(
1760
+ ~pluginId="host-ui",
1761
+ ~bundleVersion=cfg.bundleVersion,
1762
+ ~assetsDir=cfg.assetsDir,
1763
+ ~spaFallback=true,
1764
+ ~stableName=true,
1765
+ // The explicit BucketObject below writes the production config.json
1766
+ // with the resolved API endpoints + Cognito IDs. The host-shell
1767
+ // bundle's public/config.json is the dev-mode fallback — exclude
1768
+ // it from the bundle upload so both BucketObjects don't race on
1769
+ // the same S3 key.
1770
+ ~excludeFiles=["config.json"],
1771
+ ~customDomain?,
1772
+ )
1773
+
1774
+ let regionStr =
1775
+ Pulumi.Config.make(Some("aws"))
1776
+ ->Pulumi.Config.get("region")
1777
+ ->Option.getOr("unknown")
1778
+
1779
+ let cognitoPool = Platform_Stack.resolveCognitoUserPool()
1780
+
1781
+ // AppSync Events API endpoint for live (real-time) read-model updates.
1782
+ // The UI's LiveConnection swaps the host to the realtime domain and appends
1783
+ // `/realtime`, so it expects the HTTPS `…/event` form. A single Events API
1784
+ // serves both domain and platform read-model channels, so both config
1785
+ // fields point at it. `liveUpdates: true` arms the client-side kill switch;
1786
+ // without these three fields LiveConnection.isLive is false and the UI
1787
+ // never subscribes (lists then only refresh on a full reload).
1788
+ let domainEventsEndpointOutput: Pulumi.Output.t<option<string>> = switch domainEventsApiOpt {
1789
+ | Some(eventsApi) =>
1790
+ AppSync_EventsApi.httpEndpoint(eventsApi)->Pulumi.Output.apply(ep => Some(ep ++ "/event"))
1791
+ | None => Pulumi.Output.make(None)
1792
+ }
1793
+
1794
+ let configJsonContent =
1795
+ (
1796
+ (
1797
+ resolvedDomainApiEndpoint,
1798
+ resolvedPlatformApiEndpoint,
1799
+ cognitoPool.poolId,
1800
+ cognitoPool.clientId,
1801
+ )->Pulumi.Output.all4,
1802
+ domainEventsEndpointOutput,
1803
+ )
1804
+ ->Pulumi.Output.all2
1805
+ ->Pulumi.Output.apply((((domainEp, platformEp, poolId, clientId), eventsEpOpt)) => {
1806
+ let fields = [
1807
+ ("apiEndpoint", JSON.Encode.string(domainEp)),
1808
+ ("platformApiEndpoint", JSON.Encode.string(platformEp)),
1809
+ ("region", JSON.Encode.string(regionStr)),
1810
+ ("authMode", JSON.Encode.string("cognito")),
1811
+ ("cognitoUserPoolId", JSON.Encode.string(poolId)),
1812
+ ("cognitoClientId", JSON.Encode.string(clientId)),
1813
+ ("liveUpdates", JSON.Encode.bool(true)),
1814
+ ]
1815
+ let withEvents = switch eventsEpOpt {
1816
+ | Some(ep) =>
1817
+ Array.concat(
1818
+ fields,
1819
+ [
1820
+ ("domainApiEventsEndpoint", JSON.Encode.string(ep)),
1821
+ ("platformApiEventsEndpoint", JSON.Encode.string(ep)),
1822
+ ],
1823
+ )
1824
+ | None => fields
1825
+ }
1826
+ withEvents->Dict.fromArray->JSON.Encode.object->JSON.stringify
1827
+ })
1828
+
1829
+ let _ = PulumiAws.S3.BucketObject.make(
1830
+ ~name="host-ui-config-json",
1831
+ ~args={
1832
+ bucket: bucketName->Pulumi.Output.asInput,
1833
+ key: Pulumi.Input.make("config.json"),
1834
+ content: configJsonContent->Pulumi.Output.asInput,
1835
+ contentType: Pulumi.Input.make("application/json"),
1836
+ },
1837
+ )
1838
+
1839
+ Pulumi.Pulumi.export("hostShellUrl", distributionUrl)
1840
+ }
1841
+
1842
+ Pulumi.Pulumi.getOutputs()
1843
+ }
1844
+
1845
+ let startServers = () => ()
1846
+
1847
+ let deployPlugin = (~plugin: module(PluginMaker), ~apiTarget=Domain) => {
1848
+ log.info(
1849
+ ~comp="Platform:deployPlugin",
1850
+ `target=${switch apiTarget {
1851
+ | Domain => "Domain"
1852
+ | Platform => "Platform"
1853
+ }}`,
1854
+ )
1855
+ currentDeployTarget := apiTarget
1856
+ // Expose deploy target via hooks so Plugin_Builder can stamp pluginDefinition.apiTarget.
1857
+ // This must be set before P.make() and is captured synchronously by Plugin_Builder
1858
+ // (same timing requirement as hooks.api/apiRole).
1859
+ hooks.deployTarget := switch apiTarget { | Domain => "Domain" | Platform => "Platform" }
1860
+ // Each plugin stack creates its own scheduler (closures can't cross stacks).
1861
+ let scheduler = makeScheduler()
1862
+ hooks.scheduler := Some(scheduler)
1863
+ let targetApi = resolveTargetApi()
1864
+ let targetApiRole = resolveTargetApiRole()
1865
+ hooks.api := Some(targetApi->wrapHookedValue)
1866
+ hooks.apiRole := Some(targetApiRole->wrapHookedValue)
1867
+
1868
+ module P = unpack(plugin)
1869
+ let pluginComponent = P.make()
1870
+ currentDeployTarget := Domain // reset after build
1871
+
1872
+ // Note: StateTopic_AppSync.finish runs from inside subscriptionInfraHook —
1873
+ // Plugin_Builder fires the hook from within a Pulumi.Output.apply chain, so
1874
+ // finishing here would see the registry empty (the hook hasn't fired yet
1875
+ // when P.make() returns).
1876
+
1877
+ // Export interop metadata for cross-stack consumption.
1878
+ Pulumi.Pulumi.export("_interopMeta", ReventlessCore.Plugin_Helpers.getInteropMeta())
1879
+
1880
+ // Export plugin outputs (plugin, tasks, eventMappers, extensionPoints) for cross-stack access.
1881
+ let pluginOutputs = pluginComponent->ReventlessCore.Component.outputs
1882
+ ReventlessCore.Plugin_Helpers.exportPluginOutputs(pluginOutputs)
1883
+
1884
+ // Zero-downtime handover (Part 3.1): fire ONE synthetic heartbeat for the
1885
+ // just-deployed plugin so its new version runs the connect handshake within
1886
+ // seconds instead of waiting for the CloudWatch heartbeat rule's first
1887
+ // natural tick (up to `heartbeatTimeout` minutes). Routes through the exact
1888
+ // runtime heartbeat path — a `Heartbeat(timeout)` command (id = name@version)
1889
+ // onto the EP FIFO command-topic queue — mirroring HeartbeatEntryPoint.mjs and
1890
+ // reusing CommandTopicChannel_SQS_Runtime.publishJsons (correct FIFO
1891
+ // message-group handling). If it no-ops (config missing / send fails) the
1892
+ // handover still completes on the next natural heartbeat — graceful degradation.
1893
+ // heartbeatConfigRef holds the just-built plugin's config (deployPlugin builds
1894
+ // exactly one plugin per call, and registerHeartbeatConfig ran during P.make()).
1895
+ let hbConfig = PluginRuntime_Builder.heartbeatConfigRef.contents
1896
+ switch (Pulumi.Pulumi.isDryRun(), hbConfig.epQueueUrl) {
1897
+ | (true, _) => () // `pulumi preview` — no deploy-time side effects
1898
+ | (false, Some(epQueueUrl)) =>
1899
+ let _ = epQueueUrl->Pulumi.Output.apply(async url => {
1900
+ let queue: Util_SQS_Runtime.resolvedQueue = {id: url, name: "", arn: ""}
1901
+ let publishHeartbeat = queue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS_FIFO)
1902
+ let message: ReventlessCore.Message.commandJson = {
1903
+ id: hbConfig.pluginId,
1904
+ meta: ReventlessCore.Message.generateMeta(
1905
+ ~service=ReventlessInfra.PluginExtensionPointSpec.name,
1906
+ ~user="DeployHeartbeat",
1907
+ ),
1908
+ commandJson: ReventlessInfra.PluginExtensionPointSpec.Heartbeat(
1909
+ hbConfig.heartbeatTimeout,
1910
+ )->S.reverseConvertToJsonOrThrow(ReventlessInfra.PluginExtensionPointSpec.commandSchema),
1911
+ }
1912
+ try await publishHeartbeat([message]) catch {
1913
+ | _ =>
1914
+ log.warn(
1915
+ ~comp="Platform:deployPlugin",
1916
+ `synthetic heartbeat failed for ${hbConfig.pluginId} (handover falls back to the next natural heartbeat)`,
1917
+ )
1918
+ }
1919
+ })
1920
+ | (false, None) =>
1921
+ log.warn(
1922
+ ~comp="Platform:deployPlugin",
1923
+ "synthetic heartbeat skipped: no EP queue URL in heartbeat config",
1924
+ )
1925
+ }
1926
+
1927
+ Pulumi.Pulumi.getOutputs()
1928
+ }
1929
+ }
1930
+
1931
+ // Default platform — split API, no cloner, framework-default Lambda tuning.
1932
+ module Make = (): (
1933
+ ReventlessInfra.Platform.T with type api = Types.AppSync.api and type role = Types.AppSync.role
1934
+ ) => {
1935
+ include MakeWithConfig({
1936
+ let splitApi = true
1937
+ let cloner = false
1938
+ let commandHandlerConfig: ReventlessCore.Runtime.commandHandlerConfigs = {}
1939
+ })
1940
+ }