@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.
- package/CHANGELOG.md +1564 -0
- package/README.md +62 -0
- package/__mocks__/emptyModule.js +1 -0
- package/all-licenses.csv +565 -0
- package/docker-compose.dynamodb-local.yml +12 -0
- package/jest.setup.cjs +5 -0
- package/licenses-analysis.ods +0 -0
- package/package.json +73 -0
- package/rescript.json +39 -0
- package/reventless-licenses-analysis.xls +0 -0
- package/scripts/run-integration-tests.sh +43 -0
- package/src/Platform.res +1940 -0
- package/src/Platform.res.mjs +2281 -0
- package/src/Platform_Casts.res +106 -0
- package/src/Platform_Casts.res.mjs +13 -0
- package/src/Platform_Stack.res +153 -0
- package/src/Platform_Stack.res.mjs +121 -0
- package/src/Types.res +39 -0
- package/src/Types.res.mjs +21 -0
- package/src/adapter/AWS.res +96 -0
- package/src/adapter/AWS.res.mjs +186 -0
- package/src/adapter/AWS_Tags.res +16 -0
- package/src/adapter/AWS_Tags.res.mjs +48 -0
- package/src/adapter/Adapter_Helpers.res +19 -0
- package/src/adapter/Adapter_Helpers.res.mjs +37 -0
- package/src/adapter/Api/AppSync_EventsApi.res +172 -0
- package/src/adapter/Api/AppSync_EventsApi.res.mjs +96 -0
- package/src/adapter/Api/AppSync_Resolver_Native.res +93 -0
- package/src/adapter/Api/AppSync_Resolver_Native.res.mjs +67 -0
- package/src/adapter/Api/AppSync_Resolver_Retrying.res +653 -0
- package/src/adapter/Api/AppSync_Resolver_Retrying.res.mjs +462 -0
- package/src/adapter/Api/CommandSubscriptionResolvers_AppSync.res +29 -0
- package/src/adapter/Api/CommandSubscriptionResolvers_AppSync.res.mjs +15 -0
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +238 -0
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +179 -0
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +229 -0
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +245 -0
- package/src/adapter/Auth/Auth_Cognito.res +209 -0
- package/src/adapter/Auth/Auth_Cognito.res.mjs +135 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers.res +2 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers.res.mjs +12 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res +251 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res.mjs +126 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync_NoOp.res +20 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync_NoOp.res.mjs +23 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync_Runtime.res +1 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync_Runtime.res.mjs +11 -0
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_AppSync.res +95 -0
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_AppSync.res.mjs +50 -0
- package/src/adapter/CommandTopic/CommandTopicChannel.res +6 -0
- package/src/adapter/CommandTopic/CommandTopicChannel.res.mjs +21 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_Helpers.res +196 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_Helpers.res.mjs +145 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res +91 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res.mjs +74 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res +69 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs +62 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res +60 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res.mjs +56 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res +73 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs +65 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res +88 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs +76 -0
- package/src/adapter/CommandTopic/CommandTopicRemoteChannel.res +1 -0
- package/src/adapter/CommandTopic/CommandTopicRemoteChannel.res.mjs +9 -0
- package/src/adapter/CommandTopic/CommandTopicRemoteChannel_SQS.res +7 -0
- package/src/adapter/CommandTopic/CommandTopicRemoteChannel_SQS.res.mjs +17 -0
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res +103 -0
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +68 -0
- package/src/adapter/Counter/CounterHandler_DynamoDbStream_Runtime.res +107 -0
- package/src/adapter/Counter/CounterHandler_DynamoDbStream_Runtime.res.mjs +145 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage.res +1 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage.res.mjs +9 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res +84 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res.mjs +52 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +1436 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +1143 -0
- package/src/adapter/EventCollector/EventCollectorChannel.res +6 -0
- package/src/adapter/EventCollector/EventCollectorChannel.res.mjs +21 -0
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res +63 -0
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs +56 -0
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res +20 -0
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs +32 -0
- package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res +266 -0
- package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res.mjs +166 -0
- package/src/adapter/EventCollector/EventCollectorChannel_SQS.res +81 -0
- package/src/adapter/EventCollector/EventCollectorChannel_SQS.res.mjs +54 -0
- package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res +59 -0
- package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res.mjs +45 -0
- package/src/adapter/EventCollector/EventCollectorChannel_SQS_Runtime.res +91 -0
- package/src/adapter/EventCollector/EventCollectorChannel_SQS_Runtime.res.mjs +89 -0
- package/src/adapter/EventLog/EventLogStorage.res +3 -0
- package/src/adapter/EventLog/EventLogStorage.res.mjs +15 -0
- package/src/adapter/EventLog/EventLogStorage_DynamoDb.res +25 -0
- package/src/adapter/EventLog/EventLogStorage_DynamoDb.res.mjs +34 -0
- package/src/adapter/EventLog/EventLogStorage_DynamoDbStream.res +26 -0
- package/src/adapter/EventLog/EventLogStorage_DynamoDbStream.res.mjs +35 -0
- package/src/adapter/EventLog/EventLogStorage_DynamoDb_Runtime.res +121 -0
- package/src/adapter/EventLog/EventLogStorage_DynamoDb_Runtime.res.mjs +137 -0
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +265 -0
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +185 -0
- package/src/adapter/EventTopic/EventTopicPublisher.res +6 -0
- package/src/adapter/EventTopic/EventTopicPublisher.res.mjs +21 -0
- package/src/adapter/EventTopic/EventTopicPublisher_DynamoDbStream.res +25 -0
- package/src/adapter/EventTopic/EventTopicPublisher_DynamoDbStream.res.mjs +28 -0
- package/src/adapter/EventTopic/EventTopicPublisher_SNS.res +40 -0
- package/src/adapter/EventTopic/EventTopicPublisher_SNS.res.mjs +39 -0
- package/src/adapter/EventTopic/EventTopicPublisher_SNS_FIFO.res +39 -0
- package/src/adapter/EventTopic/EventTopicPublisher_SNS_FIFO.res.mjs +38 -0
- package/src/adapter/EventTopic/EventTopicPublisher_SNS_Runtime.res +5 -0
- package/src/adapter/EventTopic/EventTopicPublisher_SNS_Runtime.res.mjs +18 -0
- package/src/adapter/Mcp/MCP_Lambda.res +502 -0
- package/src/adapter/Mcp/MCP_Lambda.res.mjs +289 -0
- package/src/adapter/QueryDb/NodeResolver_AppSync.res +71 -0
- package/src/adapter/QueryDb/NodeResolver_AppSync.res.mjs +44 -0
- package/src/adapter/QueryDb/QueryDbResolvers.res +2 -0
- package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +12 -0
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +434 -0
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +243 -0
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync_NoOp.res +21 -0
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync_NoOp.res.mjs +14 -0
- package/src/adapter/QueryDb/QueryDbResolvers_NoOp.res +21 -0
- package/src/adapter/QueryDb/QueryDbResolvers_NoOp.res.mjs +14 -0
- package/src/adapter/QueryDb/QueryDbStorage.res +5 -0
- package/src/adapter/QueryDb/QueryDbStorage.res.mjs +18 -0
- package/src/adapter/QueryDb/QueryDbStorage_DynamoDb.res +115 -0
- package/src/adapter/QueryDb/QueryDbStorage_DynamoDb.res.mjs +100 -0
- package/src/adapter/QueryDb/QueryDbStorage_DynamoDbStream.res +52 -0
- package/src/adapter/QueryDb/QueryDbStorage_DynamoDbStream.res.mjs +38 -0
- package/src/adapter/QueryDb/QueryDbStorage_DynamoDb_Runtime.res +271 -0
- package/src/adapter/QueryDb/QueryDbStorage_DynamoDb_Runtime.res.mjs +306 -0
- package/src/adapter/QueryDb/QueryInterceptor_Lambda.res +20 -0
- package/src/adapter/QueryDb/QueryInterceptor_Lambda.res.mjs +22 -0
- package/src/adapter/QueryEngine/QueryEngine.res +3 -0
- package/src/adapter/QueryEngine/QueryEngine.res.mjs +12 -0
- package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +184 -0
- package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs +221 -0
- package/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs +879 -0
- package/src/adapter/Runtime/AggregateEntryPoint.mjs +313 -0
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res +327 -0
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +239 -0
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res +331 -0
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res.mjs +240 -0
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res +247 -0
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res.mjs +185 -0
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res +244 -0
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res.mjs +185 -0
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +347 -0
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +244 -0
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +285 -0
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +212 -0
- package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +124 -0
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +187 -0
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +147 -0
- package/src/adapter/Runtime/CounterEntryPoint.mjs +91 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +265 -0
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res +122 -0
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +89 -0
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +193 -0
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +137 -0
- package/src/adapter/Runtime/EventMapperEntryPoint.mjs +97 -0
- package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +101 -0
- package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res +83 -0
- package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +59 -0
- package/src/adapter/Runtime/HandlerFactoryHelpers.mjs +142 -0
- package/src/adapter/Runtime/HeartbeatEntryPoint.mjs +38 -0
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs +137 -0
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +143 -0
- package/src/adapter/Runtime/RuntimeEnvironment.res +1 -0
- package/src/adapter/Runtime/RuntimeEnvironment.res.mjs +9 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +254 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +169 -0
- package/src/adapter/Runtime/SideEffectEntryPoint.mjs +81 -0
- package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +178 -0
- package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +129 -0
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +166 -0
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +87 -0
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +136 -0
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +344 -0
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +242 -0
- package/src/adapter/Runtime/TaskBucketEntryPoint.mjs +150 -0
- package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res +123 -0
- package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +74 -0
- package/src/adapter/ScheduledPublisher/ScheduledPublisher.res +2 -0
- package/src/adapter/ScheduledPublisher/ScheduledPublisher.res.mjs +12 -0
- package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents.res +54 -0
- package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents.res.mjs +41 -0
- package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents_Runtime.res +97 -0
- package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents_Runtime.res.mjs +85 -0
- package/src/adapter/StateTopic/StateTopic_AppSync.res +418 -0
- package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +295 -0
- package/src/adapter/Task/TaskBucket.res +1 -0
- package/src/adapter/Task/TaskBucket.res.mjs +9 -0
- package/src/adapter/Task/TaskBucket_S3.res +157 -0
- package/src/adapter/Task/TaskBucket_S3.res.mjs +108 -0
- package/src/adapter/Task/TaskBucket_S3_Runtime.res +11 -0
- package/src/adapter/Task/TaskBucket_S3_Runtime.res.mjs +15 -0
- package/src/components/Aggregate_Builder.res +1 -0
- package/src/components/Aggregate_Builder.res.mjs +25 -0
- package/src/components/Aggregate_Builder_Micro.res +63 -0
- package/src/components/Aggregate_Builder_Micro.res.mjs +80 -0
- package/src/components/Aggregate_Builder_NoResolver.res +61 -0
- package/src/components/Aggregate_Builder_NoResolver.res.mjs +80 -0
- package/src/components/Aggregate_Builder_PerAggregate.res +57 -0
- package/src/components/Aggregate_Builder_PerAggregate.res.mjs +80 -0
- package/src/components/Aggregate_Builder_Single.res +59 -0
- package/src/components/Aggregate_Builder_Single.res.mjs +80 -0
- package/src/components/Aggregate_Builder_Single_Async.res +59 -0
- package/src/components/Aggregate_Builder_Single_Async.res.mjs +80 -0
- package/src/components/Api/AppSync_Adapter.res +500 -0
- package/src/components/Api/AppSync_Adapter.res.mjs +396 -0
- package/src/components/AutomationSlice_Builder.res +52 -0
- package/src/components/AutomationSlice_Builder.res.mjs +72 -0
- package/src/components/AutomationSlice_Builder_Bundled.res.mjs +70 -0
- package/src/components/Counter_Builder.res +10 -0
- package/src/components/Counter_Builder.res.mjs +16 -0
- package/src/components/DataCleaner.res +170 -0
- package/src/components/DataCleaner.res.mjs +2 -0
- package/src/components/DcbEventLog_Builder.res +8 -0
- package/src/components/DcbEventLog_Builder.res.mjs +14 -0
- package/src/components/ExtensionPoint_Builder.res +16 -0
- package/src/components/ExtensionPoint_Builder.res.mjs +37 -0
- package/src/components/ForeignReadModel_Builder.res +19 -0
- package/src/components/ForeignReadModel_Builder.res.mjs +48 -0
- package/src/components/InboundTranslationSlice_Builder.res +25 -0
- package/src/components/InboundTranslationSlice_Builder.res.mjs +31 -0
- package/src/components/OutboundTranslationSlice_Builder.res +60 -0
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +76 -0
- package/src/components/OutboundTranslationSlice_Builder_Bundled.res.mjs +75 -0
- package/src/components/Plugin.res +29 -0
- package/src/components/Plugin.res.mjs +63 -0
- package/src/components/ReadModel_Builder.res +1 -0
- package/src/components/ReadModel_Builder.res.mjs +19 -0
- package/src/components/ReadModel_Builder_NoResolver.res +55 -0
- package/src/components/ReadModel_Builder_NoResolver.res.mjs +64 -0
- package/src/components/ReadModel_Builder_NoResolver_Stream.res +60 -0
- package/src/components/ReadModel_Builder_NoResolver_Stream.res.mjs +64 -0
- package/src/components/ReadModel_Builder_PerReadModel.res +53 -0
- package/src/components/ReadModel_Builder_PerReadModel.res.mjs +66 -0
- package/src/components/ReadModel_Builder_Single.res +52 -0
- package/src/components/ReadModel_Builder_Single.res.mjs +66 -0
- package/src/components/ReadModel_Builder_Single_Stream.res +60 -0
- package/src/components/ReadModel_Builder_Single_Stream.res.mjs +66 -0
- package/src/components/Scheduler.res +1 -0
- package/src/components/Scheduler.res.mjs +13 -0
- package/src/components/SideEffectHandler.res +1 -0
- package/src/components/SideEffectHandler.res.mjs +19 -0
- package/src/components/SideEffectHandler_PerSideEffectHandler.res +13 -0
- package/src/components/SideEffectHandler_PerSideEffectHandler.res.mjs +52 -0
- package/src/components/SideEffectHandler_Single.res +54 -0
- package/src/components/SideEffectHandler_Single.res.mjs +60 -0
- package/src/components/StateChangeSlice_Builder.res +17 -0
- package/src/components/StateChangeSlice_Builder.res.mjs +23 -0
- package/src/components/StateViewSlice_Builder.res +46 -0
- package/src/components/StateViewSlice_Builder.res.mjs +68 -0
- package/src/components/StateViewSlice_Builder_Bundled.res.mjs +69 -0
- package/src/components/StateViewSlice_Builder_Stream.res +50 -0
- package/src/components/StateViewSlice_Builder_Stream.res.mjs +68 -0
- package/src/components/Task_Builder.res +1 -0
- package/src/components/Task_Builder.res.mjs +28 -0
- package/src/components/Task_Builder_PerBucket.res +22 -0
- package/src/components/Task_Builder_PerBucket.res.mjs +63 -0
- package/src/components/Vpc.res +214 -0
- package/src/components/Vpc.res.mjs +117 -0
- package/src/components/Vpc.resi +22 -0
- package/src/errors/AppSync_Error.res +36 -0
- package/src/errors/AppSync_Error.res.mjs +38 -0
- package/src/errors/Cognito_Error.res +36 -0
- package/src/errors/Cognito_Error.res.mjs +38 -0
- package/src/errors/DynamoDb_Error.res +75 -0
- package/src/errors/DynamoDb_Error.res.mjs +86 -0
- package/src/errors/Kinesis_Error.res +36 -0
- package/src/errors/Kinesis_Error.res.mjs +38 -0
- package/src/errors/SNS_Error.res +34 -0
- package/src/errors/SNS_Error.res.mjs +38 -0
- package/src/errors/SQS_Error.res +40 -0
- package/src/errors/SQS_Error.res.mjs +41 -0
- package/src/plugin/cloner/ClonerRunner.res +1 -0
- package/src/plugin/cloner/ClonerRunner.res.mjs +9 -0
- package/src/plugin/cloner/ClonerRunner_Fargate.res +362 -0
- package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +246 -0
- package/src/plugin/cloner/ClonerRunner_Fargate_Runtime.res +42 -0
- package/src/plugin/cloner/ClonerRunner_Fargate_Runtime.res.mjs +45 -0
- package/src/plugin/heartbeat/HeartbeatRunner.res +1 -0
- package/src/plugin/heartbeat/HeartbeatRunner.res.mjs +9 -0
- package/src/plugin/heartbeat/HeartbeatRunner_CloudWatchEvents.res +96 -0
- package/src/plugin/heartbeat/HeartbeatRunner_CloudWatchEvents.res.mjs +69 -0
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res +115 -0
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +88 -0
- package/src/plugin/runtime/PluginRuntimeOperations.res +5 -0
- package/src/plugin/runtime/PluginRuntimeOperations.res.mjs +14 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res +769 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +464 -0
- package/src/plugin/runtime/Util_PluginMessage_Runtime.res +13 -0
- package/src/plugin/runtime/Util_PluginMessage_Runtime.res.mjs +19 -0
- package/src/plugin/stack/Plugin_ExtensionPoint_Builder.res +28 -0
- package/src/plugin/stack/Plugin_ExtensionPoint_Builder.res.mjs +67 -0
- package/src/plugin/stack/Plugin_Stack.res +439 -0
- package/src/plugin/stack/Plugin_Stack.res.mjs +263 -0
- package/src/util/Util.res +29 -0
- package/src/util/Util.res.mjs +93 -0
- package/src/util/Util_AppSync.res +36 -0
- package/src/util/Util_AppSync.res.mjs +35 -0
- package/src/util/Util_AppSync_Caller.res +213 -0
- package/src/util/Util_AppSync_Caller.res.mjs +170 -0
- package/src/util/Util_Bundle.res +255 -0
- package/src/util/Util_Bundle.res.mjs +179 -0
- package/src/util/Util_Cloudwatch.res +14 -0
- package/src/util/Util_Cloudwatch.res.mjs +19 -0
- package/src/util/Util_CognitoGroupUser.res +39 -0
- package/src/util/Util_CognitoGroupUser.res.mjs +32 -0
- package/src/util/Util_CognitoGroupUser_Runtime.res +65 -0
- package/src/util/Util_CognitoGroupUser_Runtime.res.mjs +50 -0
- package/src/util/Util_Cognito_Runtime.res +52 -0
- package/src/util/Util_Cognito_Runtime.res.mjs +35 -0
- package/src/util/Util_DeadLetterQueue.res +154 -0
- package/src/util/Util_DeadLetterQueue.res.mjs +158 -0
- package/src/util/Util_DynamoDb.res +210 -0
- package/src/util/Util_DynamoDb.res.mjs +218 -0
- package/src/util/Util_DynamoDbStream.res +138 -0
- package/src/util/Util_DynamoDbStream.res.mjs +134 -0
- package/src/util/Util_DynamoDbStream_Runtime.res +94 -0
- package/src/util/Util_DynamoDbStream_Runtime.res.mjs +131 -0
- package/src/util/Util_DynamoDb_Runtime.res +216 -0
- package/src/util/Util_DynamoDb_Runtime.res.mjs +257 -0
- package/src/util/Util_DynamoDb_TableManager.res +23 -0
- package/src/util/Util_DynamoDb_TableManager.res.mjs +35 -0
- package/src/util/Util_DynamoDb_TableManager.resi +3 -0
- package/src/util/Util_Env.res +5 -0
- package/src/util/Util_Env.res.mjs +2 -0
- package/src/util/Util_EventSourceMapping.res +49 -0
- package/src/util/Util_EventSourceMapping.res.mjs +32 -0
- package/src/util/Util_HostUiDomain.res +28 -0
- package/src/util/Util_HostUiDomain.res.mjs +26 -0
- package/src/util/Util_IAM_Role.res +14 -0
- package/src/util/Util_IAM_Role.res.mjs +28 -0
- package/src/util/Util_Kinesis_Runtime.res +20 -0
- package/src/util/Util_Kinesis_Runtime.res.mjs +24 -0
- package/src/util/Util_Lambda.res +39 -0
- package/src/util/Util_Lambda.res.mjs +45 -0
- package/src/util/Util_LocalConfig.res +119 -0
- package/src/util/Util_LocalConfig.res.mjs +112 -0
- package/src/util/Util_ResourceNaming.res +15 -0
- package/src/util/Util_ResourceNaming.res.mjs +26 -0
- package/src/util/Util_S3.res +17 -0
- package/src/util/Util_S3.res.mjs +30 -0
- package/src/util/Util_S3_Runtime.res +11 -0
- package/src/util/Util_S3_Runtime.res.mjs +15 -0
- package/src/util/Util_SNS.res +33 -0
- package/src/util/Util_SNS.res.mjs +50 -0
- package/src/util/Util_SNS_FIFO.res +21 -0
- package/src/util/Util_SNS_FIFO.res.mjs +27 -0
- package/src/util/Util_SNS_Runtime.res +33 -0
- package/src/util/Util_SNS_Runtime.res.mjs +27 -0
- package/src/util/Util_SQS.res +70 -0
- package/src/util/Util_SQS.res.mjs +83 -0
- package/src/util/Util_SQS_FIFO.res +16 -0
- package/src/util/Util_SQS_FIFO.res.mjs +28 -0
- package/src/util/Util_SQS_Runtime.res +169 -0
- package/src/util/Util_SQS_Runtime.res.mjs +150 -0
- package/src/util/Util_SesPolicy_Runtime.res +54 -0
- package/src/util/Util_SesPolicy_Runtime.res.mjs +55 -0
- package/src/util/Util_StaticBundle.res +124 -0
- package/src/util/Util_StaticBundle.res.mjs +132 -0
- package/src/util/Util_Vpc.res +22 -0
- package/src/util/Util_Vpc.res.mjs +27 -0
- package/tests/AppSync_AdapterTest.res +396 -0
- package/tests/AppSync_AdapterTest.res.mjs +326 -0
- package/tests/AppSync_Resolver_RetryingTest.res +59 -0
- package/tests/AppSync_Resolver_RetryingTest.res.mjs +66 -0
- package/tests/Auth_CognitoTest.res +203 -0
- package/tests/Auth_CognitoTest.res.mjs +248 -0
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +602 -0
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +742 -0
- package/tests/EventLogStorage_DynamoDb_RuntimeTest.res +57 -0
- package/tests/EventLogStorage_DynamoDb_RuntimeTest.res.mjs +68 -0
- package/tests/MCP_LambdaTest.res +101 -0
- package/tests/MCP_LambdaTest.res.mjs +56 -0
- package/tests/Util_HostUiDomainTest.res +90 -0
- package/tests/Util_HostUiDomainTest.res.mjs +50 -0
- package/tests/Util_SQS_RuntimeTest.res +46 -0
- package/tests/Util_SQS_RuntimeTest.res.mjs +45 -0
- package/tests/Util_StaticBundleTest.res +136 -0
- package/tests/Util_StaticBundleTest.res.mjs +149 -0
- package/tests/integration/DcbCommandTopicEntryPoint_IntegrationTest.res +101 -0
- package/tests/integration/DcbCommandTopicEntryPoint_IntegrationTest.res.mjs +91 -0
- package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res +315 -0
- package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res.mjs +435 -0
- package/tests/integration/DcbIntegrationHarness.res +102 -0
- package/tests/integration/DcbIntegrationHarness.res.mjs +163 -0
- package/tests/integration/EpTestSlice.res +28 -0
- package/tests/integration/EpTestSlice.res.mjs +43 -0
- package/tests/integration/EpTestSliceBehavior.res +18 -0
- package/tests/integration/EpTestSliceBehavior.res.mjs +35 -0
package/src/Platform.res
ADDED
|
@@ -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
|
+
}
|