@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
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
// Plugin EventCollector Lambda entry point — shared between the admin and every
|
|
2
|
+
// per-plugin EventCollector Lambda. Despite the historical "Admin" filename, this
|
|
3
|
+
// module is plugin-agnostic: behaviour is driven entirely by HANDLER_CONFIG.
|
|
4
|
+
//
|
|
5
|
+
// HANDLER_CONFIG schema (JSON in env var "HANDLER_CONFIG"):
|
|
6
|
+
//
|
|
7
|
+
// {
|
|
8
|
+
// "queueUrl": string // SQS URL of the EventCollector this Lambda drains
|
|
9
|
+
// "pluginExtensionPointCmdTopicUrl":string // SQS URL of CorePluginExtPoint cmd topic (admin's Plugin EP)
|
|
10
|
+
// "eventTopicArn": string // SNS ARN the EP's outgoing events publish to ("NOT_AVAILABLE" if none)
|
|
11
|
+
// "pluginReadModelTableName": string // DynamoDB table for Plugin RM (used by QueryEngine.scan)
|
|
12
|
+
// "pluginSchemaPersistenceTableName":string // DynamoDB table holding deploy-time SDL fragments
|
|
13
|
+
// // (rows keyed "deploy-schema:<name>"). Durable source
|
|
14
|
+
// // for the runtime schema stitch; "NOT_AVAILABLE" →
|
|
15
|
+
// // fall back to Plugin RM Connected-row scan.
|
|
16
|
+
// "appSyncApiId": string // "NOT_AVAILABLE" disables schema stitching
|
|
17
|
+
// "clonerEnabled": boolean
|
|
18
|
+
// "schedulerRoleArn": string // IAM role for CloudWatch Events schedule targets
|
|
19
|
+
// "schedulerQueueArn": string // ARN of the cmd topic the scheduler publishes to ("" → no commandTopicResources)
|
|
20
|
+
// "schedulerQueueName": string
|
|
21
|
+
// "pluginDefinition": Plugin.pluginDefinition // serialized; admin uses a fake one (id="Admin@INTERNAL")
|
|
22
|
+
// "extensionPoints": [ // EPs whose OUTGOING events this Lambda processes; admin: 1 entry, plugins: []
|
|
23
|
+
// {
|
|
24
|
+
// "specModule": string, // dynamic import specifier for the EP spec module
|
|
25
|
+
// "mappingsModule": string, // dynamic import specifier for the EP mapping factory (exports `Make`)
|
|
26
|
+
// "eventTopicArn": string, // SNS ARN the EP's outgoing events publish to (per-EP override of top-level eventTopicArn)
|
|
27
|
+
// "aggregateNames": string[] // delegate aggregate names — service keys for outgoingExtensionPointEventHandlers
|
|
28
|
+
// }
|
|
29
|
+
// ],
|
|
30
|
+
// "connectExtension": { // The auto-included PluginConnectExtension entry; null for admin Lambda
|
|
31
|
+
// "specModule": string,
|
|
32
|
+
// "mappingsModule": string,
|
|
33
|
+
// "extensionPointName": string // service key for incomingConnectExtensionEventHandlers (e.g. "Core.Plugin")
|
|
34
|
+
// } | null,
|
|
35
|
+
// "extensions": [ // User-declared extensions — wired into incoming/outgoing dicts at cold start
|
|
36
|
+
// {
|
|
37
|
+
// "name": string, // extension component name (used for logs / dedupe)
|
|
38
|
+
// "specModule": string, // dynamic import for the ExtensionPoint spec module
|
|
39
|
+
// "mappingsModule": string, // dynamic import for the user extension file (`module Mapping`)
|
|
40
|
+
// "delegateModule": string, // dynamic import for the Delegate spec (aggregate / slice)
|
|
41
|
+
// "extensionPointName": string, // service key for incomingExtensionEventHandlers (e.g. "Ordering.Orders")
|
|
42
|
+
// "aggregateNames": string[], // outgoing service keys + filter into top-level publishToAggregates
|
|
43
|
+
// "readModelNames": string[] // RMs this extension may enqueue events into (filter into readModelQueueUrls)
|
|
44
|
+
// }
|
|
45
|
+
// ],
|
|
46
|
+
// "publishToAggregates": { // aggregateName → env-var name holding the aggregate's cmd-topic SQS URL
|
|
47
|
+
// "Plugin": "PTA_Plugin_QUEUE_URL",
|
|
48
|
+
// "RecordProductDemand": "PTA_RecordProductDemand_QUEUE_URL"
|
|
49
|
+
// },
|
|
50
|
+
// "readModelQueueUrls": { // rmName → env-var name holding the RM's EventCollector SQS URL
|
|
51
|
+
// "ProductDemands": "PRM_ProductDemands_QUEUE_URL"
|
|
52
|
+
// },
|
|
53
|
+
// "readModelNamesForSourceName": { // deploy-side inversion of RM.sourceNames; sourceServiceName → [rmName]
|
|
54
|
+
// "Ordering.Orders": ["ProductDemands"]
|
|
55
|
+
// }
|
|
56
|
+
// }
|
|
57
|
+
//
|
|
58
|
+
// Why this shape and not separate entry points per Lambda: at deploy time both the
|
|
59
|
+
// admin EventCollector and every per-plugin EventCollector share the same code
|
|
60
|
+
// asset (this file). The runtime differences are entirely data — which EPs to
|
|
61
|
+
// process outgoing events for (admin only) and which Connect extension to wire
|
|
62
|
+
// (plugin only). Plugin_Callback.Make routes incoming SQS events to the right
|
|
63
|
+
// service-keyed handler dict; we reconstruct those dicts here from HANDLER_CONFIG.
|
|
64
|
+
|
|
65
|
+
import { readFileSync } from "node:fs";
|
|
66
|
+
import { createRequire } from "node:module";
|
|
67
|
+
import { pathToFileURL } from "node:url";
|
|
68
|
+
import * as Effect from "effect/Effect";
|
|
69
|
+
import { patchSpecId, makeQueueRef, scanByTableName, log, pluginName } from "./HandlerFactoryHelpers.mjs";
|
|
70
|
+
import { subscribeQueueToTopic, unsubscribeQueueFromTopic } from "@reventlessdev/rescript-aws-sdk/src/SNS_Helpers.res.mjs";
|
|
71
|
+
import { sendMessage } from "@reventlessdev/reventless-aws/src/plugin/runtime/Util_PluginMessage_Runtime.res.mjs";
|
|
72
|
+
import { publish as snsPublish } from "@reventlessdev/reventless-aws/src/adapter/EventTopic/EventTopicPublisher_SNS_Runtime.res.mjs";
|
|
73
|
+
import { publishJsons as sqsPublishJsons } from "@reventlessdev/reventless-aws/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
74
|
+
import { Make as extensionPointOperationsMake } from "@reventlessdev/reventless-core/src/components/ExtensionPoint/ExtensionPoint_Operations.res.mjs";
|
|
75
|
+
import { Make as extensionOperationsMake } from "@reventlessdev/reventless-core/src/components/Extension/Extension_Operations.res.mjs";
|
|
76
|
+
import { Make as extensionMappingMake } from "@reventlessdev/reventless-infra/src/types/ExtensionMapping.res.mjs";
|
|
77
|
+
import { Make as extensionPointMappingMake } from "@reventlessdev/reventless-infra/src/types/ExtensionPointMapping.res.mjs";
|
|
78
|
+
import { Make as pluginCallbackMake } from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Callback.res.mjs";
|
|
79
|
+
import { handleDynamoDbOrSqsEvent } from "@reventlessdev/reventless-aws/src/adapter/EventCollector/EventCollectorChannel_SQS_Runtime.res.mjs";
|
|
80
|
+
import { createSchedule as cwCreateSchedule, deleteSchedule as cwDeleteSchedule } from "@reventlessdev/reventless-aws/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents_Runtime.res.mjs";
|
|
81
|
+
import {
|
|
82
|
+
stitch as graphqlStitch,
|
|
83
|
+
decode as decodeFragment,
|
|
84
|
+
countRootTypeFields,
|
|
85
|
+
isCatastrophicSchemaShrink,
|
|
86
|
+
} from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
87
|
+
import { baseFragment as adminBaseFragment } from "@reventlessdev/reventless-core/src/admin/AdminApi.res.mjs";
|
|
88
|
+
import { tag as requestContextTag } from "@reventlessdev/reventless-core/src/RequestContext.res.mjs";
|
|
89
|
+
|
|
90
|
+
// The Plugin RM state schema uses `@s.matches(_jsNullable …)` for many
|
|
91
|
+
// optional fields — top-level AND nested. DDB writes drop undefined
|
|
92
|
+
// attributes, so the unmarshalled row arrives with the relevant keys
|
|
93
|
+
// *missing* (not set to null). Sury's strict parser then rejects them as
|
|
94
|
+
// "received undefined". Tracking and patching every nullable path by hand is
|
|
95
|
+
// whack-a-mole; instead, manageSubscriptions and reconcileSubscriptions only
|
|
96
|
+
// need a tiny subset of the state (id / status / extensions / extensionPoints
|
|
97
|
+
// / eventCollector), so they sidestep sury entirely and read the DDB row
|
|
98
|
+
// directly via this small projection. mkUpdateApiSchema does the same for
|
|
99
|
+
// apiSchemaFragment further below.
|
|
100
|
+
function projectPluginRow(row) {
|
|
101
|
+
if (!row || typeof row !== "object") return null;
|
|
102
|
+
if (typeof row.id !== "string" || typeof row.status !== "string") return null;
|
|
103
|
+
const dcbEventLog =
|
|
104
|
+
row.dcbEventLog &&
|
|
105
|
+
typeof row.dcbEventLog === "object" &&
|
|
106
|
+
typeof row.dcbEventLog.name === "string" &&
|
|
107
|
+
typeof row.dcbEventLog.eventTopicArn === "string"
|
|
108
|
+
? { name: row.dcbEventLog.name, eventTopicArn: row.dcbEventLog.eventTopicArn }
|
|
109
|
+
: null;
|
|
110
|
+
return {
|
|
111
|
+
id: row.id,
|
|
112
|
+
status: row.status,
|
|
113
|
+
eventCollector: typeof row.eventCollector === "string" ? row.eventCollector : "",
|
|
114
|
+
extensions: Array.isArray(row.extensions)
|
|
115
|
+
? row.extensions
|
|
116
|
+
.filter((e) => e && typeof e.extensionPointName === "string")
|
|
117
|
+
.map((e) => ({
|
|
118
|
+
name: typeof e.name === "string" ? e.name : "",
|
|
119
|
+
extensionPointName: e.extensionPointName,
|
|
120
|
+
dcbSources: Array.isArray(e.dcbSources)
|
|
121
|
+
? e.dcbSources.filter((s) => typeof s === "string")
|
|
122
|
+
: [],
|
|
123
|
+
}))
|
|
124
|
+
: [],
|
|
125
|
+
extensionPoints: Array.isArray(row.extensionPoints)
|
|
126
|
+
? row.extensionPoints
|
|
127
|
+
.filter((ep) => ep && typeof ep.name === "string" && typeof ep.eventTopic === "string")
|
|
128
|
+
.map((ep) => ({
|
|
129
|
+
name: ep.name,
|
|
130
|
+
commandTopic: typeof ep.commandTopic === "string" ? ep.commandTopic : "",
|
|
131
|
+
eventTopic: ep.eventTopic,
|
|
132
|
+
}))
|
|
133
|
+
: [],
|
|
134
|
+
dcbEventLog,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function parseHandlerConfig(rawJson) {
|
|
139
|
+
if (!rawJson) throw new Error("HANDLER_CONFIG env var is empty");
|
|
140
|
+
let config;
|
|
141
|
+
try {
|
|
142
|
+
config = JSON.parse(rawJson);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
throw new Error("HANDLER_CONFIG JSON parse error: " + (e && e.message));
|
|
145
|
+
}
|
|
146
|
+
const required = [
|
|
147
|
+
"queueUrl",
|
|
148
|
+
"pluginExtensionPointCmdTopicUrl",
|
|
149
|
+
"eventTopicArn",
|
|
150
|
+
"pluginReadModelTableName",
|
|
151
|
+
"appSyncApiId",
|
|
152
|
+
"schedulerRoleArn",
|
|
153
|
+
"schedulerQueueArn",
|
|
154
|
+
"schedulerQueueName",
|
|
155
|
+
"extensionPoints",
|
|
156
|
+
"extensions",
|
|
157
|
+
"publishToAggregates",
|
|
158
|
+
];
|
|
159
|
+
for (const k of required) {
|
|
160
|
+
if (config[k] === undefined) {
|
|
161
|
+
throw new Error("HANDLER_CONFIG missing required field: " + k);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (!Array.isArray(config.extensionPoints)) throw new Error("HANDLER_CONFIG.extensionPoints must be an array");
|
|
165
|
+
if (!Array.isArray(config.extensions)) throw new Error("HANDLER_CONFIG.extensions must be an array");
|
|
166
|
+
// connectExtension is allowed to be null or absent (admin case)
|
|
167
|
+
if (config.connectExtension === undefined) config.connectExtension = null;
|
|
168
|
+
// Optional fields with empty defaults — admin context omits them entirely.
|
|
169
|
+
if (config.readModelQueueUrls === undefined) config.readModelQueueUrls = {};
|
|
170
|
+
if (config.readModelNamesForSourceName === undefined) config.readModelNamesForSourceName = {};
|
|
171
|
+
// Per-extension defaults so older serialisers (or partial admin configs) keep working.
|
|
172
|
+
for (const ext of config.extensions) {
|
|
173
|
+
if (!Array.isArray(ext.aggregateNames)) ext.aggregateNames = [];
|
|
174
|
+
if (!Array.isArray(ext.readModelNames)) ext.readModelNames = [];
|
|
175
|
+
if (typeof ext.name !== "string") ext.name = ext.extensionPointName || "unknown";
|
|
176
|
+
if (typeof ext.delegateModule !== "string") ext.delegateModule = "";
|
|
177
|
+
}
|
|
178
|
+
return config;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function injectAwsAuthAll(fragment, group) {
|
|
182
|
+
const parts = decodeFragment(fragment);
|
|
183
|
+
const augmentedMutations = parts.mutations.map(
|
|
184
|
+
(field) => field + "\n @aws_auth(cognito_groups: [\"" + group + "\"])"
|
|
185
|
+
);
|
|
186
|
+
const augmentedQueries = parts.queries.map(
|
|
187
|
+
(field) => field + " @aws_auth(cognito_groups: [\"" + group + "\"])"
|
|
188
|
+
);
|
|
189
|
+
const encoded = JSON.stringify({
|
|
190
|
+
types: parts.types,
|
|
191
|
+
mutations: augmentedMutations,
|
|
192
|
+
queries: augmentedQueries,
|
|
193
|
+
});
|
|
194
|
+
return { encoded, protocol: "graphql" };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function updateAppSyncSchema(apiId, sdl) {
|
|
198
|
+
const { AppSyncClient, StartSchemaCreationCommand } = await import("@aws-sdk/client-appsync");
|
|
199
|
+
const client = new AppSyncClient({});
|
|
200
|
+
await client.send(new StartSchemaCreationCommand({ apiId, definition: sdl }));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Fetch the current live AppSync schema as an SDL string for the shrink guard.
|
|
204
|
+
// Returns "" (not an error) when the API has no schema yet (first deploy) or
|
|
205
|
+
// introspection fails — the caller treats an empty current schema as "no
|
|
206
|
+
// baseline to protect", so the push proceeds.
|
|
207
|
+
async function getCurrentSchemaSdl(apiId) {
|
|
208
|
+
try {
|
|
209
|
+
const { AppSyncClient, GetIntrospectionSchemaCommand } = await import("@aws-sdk/client-appsync");
|
|
210
|
+
const client = new AppSyncClient({});
|
|
211
|
+
const resp = await client.send(new GetIntrospectionSchemaCommand({ apiId, format: "SDL" }));
|
|
212
|
+
if (!resp || !resp.schema) return "";
|
|
213
|
+
// resp.schema is a Uint8Array of the SDL text.
|
|
214
|
+
return Buffer.from(resp.schema).toString("utf-8");
|
|
215
|
+
} catch (e) {
|
|
216
|
+
log.warn(`could not introspect current schema (${(e && e.message) || e}) — skipping shrink guard`, { comp: "updateApiSchema" });
|
|
217
|
+
return "";
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Shrink-guard threshold: abort the push if the new SDL has fewer than
|
|
222
|
+
// (threshold × current) root fields. Configurable via env; default 0.5 (50%).
|
|
223
|
+
// Anything outside (0, 1) falls back to the default.
|
|
224
|
+
function parseShrinkThreshold(raw) {
|
|
225
|
+
const n = raw ? Number(raw) : NaN;
|
|
226
|
+
return Number.isFinite(n) && n > 0 && n < 1 ? n : 0.5;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Emit a CloudWatch metric via Embedded Metric Format (EMF). Any Lambda log line
|
|
230
|
+
// shaped like this is auto-parsed by CloudWatch into the metric
|
|
231
|
+
// Reventless/Runtime SchemaShrinkRejected (dimension ApiId) — no PutMetricData
|
|
232
|
+
// call, SDK dependency, or extra IAM permission required.
|
|
233
|
+
// NOTE: must stay as a raw `console.log` of the exact EMF envelope — routing
|
|
234
|
+
// through `log.info` would add `time`/`level`/`message` siblings and break
|
|
235
|
+
// CloudWatch's EMF auto-detect (the `_aws` block must be at the root of the
|
|
236
|
+
// log record).
|
|
237
|
+
function emitShrinkRejectionMetric(apiId, currentRootFields, newRootFields) {
|
|
238
|
+
try {
|
|
239
|
+
// eslint-disable-next-line no-console
|
|
240
|
+
console.log(
|
|
241
|
+
JSON.stringify({
|
|
242
|
+
_aws: {
|
|
243
|
+
Timestamp: Date.now(),
|
|
244
|
+
CloudWatchMetrics: [
|
|
245
|
+
{
|
|
246
|
+
Namespace: "Reventless/Runtime",
|
|
247
|
+
Dimensions: [["ApiId"]],
|
|
248
|
+
Metrics: [{ Name: "SchemaShrinkRejected", Unit: "Count" }],
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
ApiId: apiId,
|
|
253
|
+
SchemaShrinkRejected: 1,
|
|
254
|
+
currentRootFields,
|
|
255
|
+
newRootFields,
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
} catch (_) {}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const DEPLOY_SCHEMA_PREFIX = "deploy-schema:";
|
|
262
|
+
|
|
263
|
+
// Read every plugin's deploy-time SDL fragment from the dedicated
|
|
264
|
+
// PluginSchemaPersistence table (rows keyed "deploy-schema:<name>", written by
|
|
265
|
+
// Platform.preResolversSchemaHook at deploy time). begins_with("deploy-schema:")
|
|
266
|
+
// matches only Domain fragments — the platform ("deploy-schema-platform:") and
|
|
267
|
+
// hash ("deploy-schema-hash:") rows have a hyphen at that position, so they are
|
|
268
|
+
// excluded. Each row's `fragment` attribute is the encoded SDL string; wrap it
|
|
269
|
+
// in the {encoded, protocol} shape the stitcher consumes.
|
|
270
|
+
async function collectDeploySchemaFragments(tableName) {
|
|
271
|
+
const rows = await scanByTableName(
|
|
272
|
+
tableName,
|
|
273
|
+
[["id", { TAG: "BeginsWith" }, { TAG: "String", _0: DEPLOY_SCHEMA_PREFIX }]],
|
|
274
|
+
1000
|
|
275
|
+
);
|
|
276
|
+
return rows
|
|
277
|
+
.map((row) =>
|
|
278
|
+
row && typeof row.fragment === "string"
|
|
279
|
+
? { encoded: row.fragment, protocol: "graphql" }
|
|
280
|
+
: null
|
|
281
|
+
)
|
|
282
|
+
.filter(Boolean);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Set at handler entry; read by runEffect to tag logs with the Lambda request id.
|
|
286
|
+
let _currentRequestId = "unknown";
|
|
287
|
+
|
|
288
|
+
function runEffect(correlationId, effect) {
|
|
289
|
+
let e = effect
|
|
290
|
+
// Promote correlationId/requestId/plugin to top-level JSON log fields —
|
|
291
|
+
// decoded by EffectLogger.install() from Effect log annotations. Harmless
|
|
292
|
+
// no-op if the unified logger isn't installed.
|
|
293
|
+
.pipe(Effect.annotateLogs("correlationId", correlationId || "unknown"))
|
|
294
|
+
.pipe(Effect.annotateLogs("requestId", _currentRequestId));
|
|
295
|
+
if (pluginName) e = e.pipe(Effect.annotateLogs("plugin", pluginName));
|
|
296
|
+
return e
|
|
297
|
+
.pipe(Effect.provideService(requestContextTag, { correlationId: correlationId || "unknown" }))
|
|
298
|
+
.pipe(Effect.runPromise);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function extractCorrelationId(records) {
|
|
302
|
+
const first = records && records[0];
|
|
303
|
+
if (!first) return undefined;
|
|
304
|
+
const body = first.body;
|
|
305
|
+
if (!body) return undefined;
|
|
306
|
+
try {
|
|
307
|
+
const parsed = JSON.parse(body);
|
|
308
|
+
if (parsed && typeof parsed === "object") {
|
|
309
|
+
const meta = parsed.meta;
|
|
310
|
+
if (meta && typeof meta === "object" && typeof meta.correlationId === "string") {
|
|
311
|
+
return meta.correlationId;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch (_) {}
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const invalidNameChars = /[^.\-_a-zA-Z0-9]/g;
|
|
319
|
+
const resourceNaming = {
|
|
320
|
+
validateName: (n) => n.replace(invalidNameChars, "_"),
|
|
321
|
+
urnName: (arn) => {
|
|
322
|
+
const parts = arn.split(":");
|
|
323
|
+
return parts[5] || "unknown";
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Admin-mediated cross-plugin SNS subscription manager (Plan Phase 3, Step 1).
|
|
328
|
+
// When DoConnectPlugin / DoDisconnectPlugin fires on the admin EP mapping, we
|
|
329
|
+
// scan the Plugin RM for currently-Connected peers and wire (or tear down) SNS
|
|
330
|
+
// subscriptions between this plugin's EventCollector queue and peer plugins'
|
|
331
|
+
// EP event topics — in both directions:
|
|
332
|
+
// - peer EP topic → this plugin's EC queue, for each of this plugin's
|
|
333
|
+
// extensions whose `extensionPointName` matches a peer EP
|
|
334
|
+
// - this plugin's EP topics → peer EC queues, for each peer extension whose
|
|
335
|
+
// `extensionPointName` matches one of this plugin's EPs
|
|
336
|
+
// subscribeQueueToTopic is idempotent (lists existing subscriptions first);
|
|
337
|
+
// unsubscribeQueueFromTopic is best-effort and swallows the "no subscription"
|
|
338
|
+
// case. Failures are logged but do not throw — admin's RM projection still
|
|
339
|
+
// proceeds and cold-start reconciliation will re-attempt missed work.
|
|
340
|
+
function mkManageSubscriptions(tableName) {
|
|
341
|
+
if (!tableName || tableName === "NOT_AVAILABLE") return undefined;
|
|
342
|
+
const trySubscribe = async (queueArn, topicArn, label) => {
|
|
343
|
+
if (!queueArn || !topicArn) return;
|
|
344
|
+
try {
|
|
345
|
+
await subscribeQueueToTopic(queueArn, topicArn);
|
|
346
|
+
log.info(`subscribed ${label}: ${topicArn} -> ${queueArn}`, { comp: "manageSubscriptions" });
|
|
347
|
+
} catch (e) {
|
|
348
|
+
const msg = (e && e.message) || String(e);
|
|
349
|
+
log.error(`subscribe failed ${label}: ${topicArn} -> ${queueArn}`, { comp: "manageSubscriptions", detail: msg });
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const tryUnsubscribe = async (queueArn, topicArn, label) => {
|
|
353
|
+
if (!queueArn || !topicArn) return;
|
|
354
|
+
try {
|
|
355
|
+
await unsubscribeQueueFromTopic(queueArn, topicArn);
|
|
356
|
+
log.info(`unsubscribed ${label}: ${topicArn} -> ${queueArn}`, { comp: "manageSubscriptions" });
|
|
357
|
+
} catch (e) {
|
|
358
|
+
const msg = (e && e.message) || String(e);
|
|
359
|
+
log.error(`unsubscribe failed ${label}: ${topicArn} -> ${queueArn}`, { comp: "manageSubscriptions", detail: msg });
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
const scanConnectedPeers = async (excludeId) => {
|
|
363
|
+
const rows = await scanByTableName(
|
|
364
|
+
tableName,
|
|
365
|
+
[["status", { TAG: "Contains" }, { TAG: "String", _0: "Connected" }]],
|
|
366
|
+
1000
|
|
367
|
+
);
|
|
368
|
+
const peers = [];
|
|
369
|
+
for (const row of rows) {
|
|
370
|
+
const state = projectPluginRow(row);
|
|
371
|
+
if (state && state.id !== excludeId) peers.push(state);
|
|
372
|
+
}
|
|
373
|
+
return peers;
|
|
374
|
+
};
|
|
375
|
+
// Extract the plugin display name from a versioned id like "Catalog@1.0.0-alpha.65".
|
|
376
|
+
const pluginNameOfId = (id) => (typeof id === "string" ? id.split("@")[0] : "");
|
|
377
|
+
return async (pluginDef, action) => {
|
|
378
|
+
const isConnect = action === "connect";
|
|
379
|
+
const op = isConnect ? trySubscribe : tryUnsubscribe;
|
|
380
|
+
const peers = await scanConnectedPeers(pluginDef.id);
|
|
381
|
+
// Subscriptions are wired between SNS topics and SQS queues that are owned
|
|
382
|
+
// per PLUGIN NAME, not per version (the EC queue, EP event topics, and DCB
|
|
383
|
+
// event topic ARNs all live in the plugin's stack and are stable across
|
|
384
|
+
// version-to-version upgrades). When a superseded version is Retired and
|
|
385
|
+
// its row transitions to Disconnected, this hook fires with action="disconnect"
|
|
386
|
+
// — but if a newer version of the same plugin is still Connected, the
|
|
387
|
+
// subscriptions are still in use. Tearing them down here would silently
|
|
388
|
+
// break cross-plugin event flow for the live version (and only cold-start
|
|
389
|
+
// reconciliation on the surviving plugin would heal it — which happens on
|
|
390
|
+
// its next Lambda init, not immediately). Skip the tear-down in that case.
|
|
391
|
+
if (!isConnect) {
|
|
392
|
+
const myName = pluginNameOfId(pluginDef.id);
|
|
393
|
+
const liveSibling = peers.find((p) => pluginNameOfId(p.id) === myName);
|
|
394
|
+
if (liveSibling) {
|
|
395
|
+
log.info(`disconnect skipped for ${pluginDef.id}: sibling version ${liveSibling.id} still Connected and shares the same EC queue / EP topics`, { comp: "manageSubscriptions" });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Subscriptions for this plugin's extensions: peer EP topic → this EC queue
|
|
400
|
+
for (const ext of pluginDef.extensions || []) {
|
|
401
|
+
for (const peer of peers) {
|
|
402
|
+
const peerEp = (peer.extensionPoints || []).find(
|
|
403
|
+
(ep) => ep.name === ext.extensionPointName
|
|
404
|
+
);
|
|
405
|
+
if (peerEp) {
|
|
406
|
+
await op(pluginDef.eventCollector, peerEp.eventTopic, `${ext.extensionPointName} -> ${pluginDef.id}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Subscriptions for this plugin's EPs: this EP topic → peer EC queues
|
|
411
|
+
for (const ep of pluginDef.extensionPoints || []) {
|
|
412
|
+
for (const peer of peers) {
|
|
413
|
+
const matchingExt = (peer.extensions || []).find(
|
|
414
|
+
(e) => e.extensionPointName === ep.name
|
|
415
|
+
);
|
|
416
|
+
if (matchingExt) {
|
|
417
|
+
await op(peer.eventCollector, ep.eventTopic, `${ep.name} -> ${peer.id}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// ── Phase 4: DCB topic subscriptions ──────────────────────────────────
|
|
422
|
+
// For each DCB source this plugin's extensions reference, subscribe this
|
|
423
|
+
// plugin's EventCollector to the owning peer plugin's DCB EventTopic.
|
|
424
|
+
for (const ext of pluginDef.extensions || []) {
|
|
425
|
+
for (const dcbSourceName of ext.dcbSources || []) {
|
|
426
|
+
const peer = peers.find(
|
|
427
|
+
(p) => p.dcbEventLog && p.dcbEventLog.name === dcbSourceName
|
|
428
|
+
);
|
|
429
|
+
if (peer) {
|
|
430
|
+
await op(
|
|
431
|
+
pluginDef.eventCollector,
|
|
432
|
+
peer.dcbEventLog.eventTopicArn,
|
|
433
|
+
`dcb ${dcbSourceName} -> ${pluginDef.id}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// If THIS plugin has a DCB EventLog, subscribe each peer plugin's
|
|
439
|
+
// EventCollector that references it via dcbSources.
|
|
440
|
+
if (pluginDef.dcbEventLog) {
|
|
441
|
+
const myDcbName = pluginDef.dcbEventLog.name;
|
|
442
|
+
for (const peer of peers) {
|
|
443
|
+
const consuming = (peer.extensions || []).some((e) =>
|
|
444
|
+
(e.dcbSources || []).includes(myDcbName)
|
|
445
|
+
);
|
|
446
|
+
if (consuming) {
|
|
447
|
+
await op(
|
|
448
|
+
peer.eventCollector,
|
|
449
|
+
pluginDef.dcbEventLog.eventTopicArn,
|
|
450
|
+
`dcb ${myDcbName} -> ${peer.id}`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Cold-start reconciliation — scan the Plugin RM for every Connected plugin
|
|
459
|
+
// and rerun manageSubscriptions(p, "connect") for each. Idempotent at the SNS
|
|
460
|
+
// level (subscribeQueueToTopic dedupes), so this is safe to run repeatedly;
|
|
461
|
+
// it catches subscriptions lost out-of-band (topic deleted+recreated, plugin
|
|
462
|
+
// restarted with a new EC ARN, etc.). Fire-and-forget — admin still serves
|
|
463
|
+
// incoming events while reconciliation completes.
|
|
464
|
+
async function reconcileSubscriptionsOnce(tableName, manageSubscriptions) {
|
|
465
|
+
if (!tableName || tableName === "NOT_AVAILABLE" || !manageSubscriptions) return;
|
|
466
|
+
try {
|
|
467
|
+
const rows = await scanByTableName(
|
|
468
|
+
tableName,
|
|
469
|
+
[["status", { TAG: "Contains" }, { TAG: "String", _0: "Connected" }]],
|
|
470
|
+
1000
|
|
471
|
+
);
|
|
472
|
+
for (const row of rows) {
|
|
473
|
+
const state = projectPluginRow(row);
|
|
474
|
+
if (state) {
|
|
475
|
+
try {
|
|
476
|
+
await manageSubscriptions(state, "connect");
|
|
477
|
+
} catch (e) {
|
|
478
|
+
log.warn("manageSubscriptions failed", { comp: "reconcileSubscriptions", detail: (e && e.message) || String(e) });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} catch (e) {
|
|
483
|
+
log.warn("scan failed", { comp: "reconcileSubscriptions", detail: (e && e.message) || String(e) });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Re-stitch and push the live AppSync schema on each plugin Connect/Disconnect.
|
|
488
|
+
//
|
|
489
|
+
// Source of plugin fragments (option A): the durable PluginSchemaPersistence
|
|
490
|
+
// table (deploy-time "deploy-schema:<name>" rows), NOT the lifecycle-volatile
|
|
491
|
+
// Plugin RM "Connected" rows. Reading the deploy-time source means lifecycle
|
|
492
|
+
// churn (a redeploy window where every plugin is briefly Disconnected, an
|
|
493
|
+
// eventually-consistent scan, etc.) can no longer shrink the stitched schema —
|
|
494
|
+
// every push re-asserts the full deployed set rather than clobbering field
|
|
495
|
+
// resolvers with whatever subset happened to be Connected at scan time. The
|
|
496
|
+
// Plugin RM scan remains only as a fallback for older platform stacks deployed
|
|
497
|
+
// before the dedicated table existed (schemaTableName === "NOT_AVAILABLE").
|
|
498
|
+
//
|
|
499
|
+
// Circuit breaker (option D, defense-in-depth): before pushing, introspect the
|
|
500
|
+
// current live schema and compare root-type (Mutation + Query) field counts. If
|
|
501
|
+
// the new SDL drops below the configured fraction of the live field count, abort
|
|
502
|
+
// the push, log loudly, and emit a CloudWatch metric — catching any
|
|
503
|
+
// catastrophic shrink the source switch alone misses.
|
|
504
|
+
function mkUpdateApiSchema(schemaTableName, apiId, clonerEnabled) {
|
|
505
|
+
if (!apiId || apiId === "NOT_AVAILABLE") return undefined;
|
|
506
|
+
const hasSchemaTable = !!schemaTableName && schemaTableName !== "NOT_AVAILABLE";
|
|
507
|
+
return async (queryEngine) => {
|
|
508
|
+
let fragments;
|
|
509
|
+
if (hasSchemaTable) {
|
|
510
|
+
fragments = await collectDeploySchemaFragments(schemaTableName);
|
|
511
|
+
log.info(`stitching ${fragments.length} deploy-schema fragment(s) from ${schemaTableName}`, { comp: "updateApiSchema" });
|
|
512
|
+
} else {
|
|
513
|
+
const resolved = await queryEngine.scan(
|
|
514
|
+
"Plugin",
|
|
515
|
+
[["status", { TAG: "Contains" }, { TAG: "String", _0: "Connected" }]],
|
|
516
|
+
1000
|
|
517
|
+
);
|
|
518
|
+
fragments = resolved.map((json) => json && json.apiSchemaFragment).filter(Boolean);
|
|
519
|
+
log.warn(`PluginSchemaPersistence table unavailable — falling back to ${fragments.length} Connected Plugin RM fragment(s)`, { comp: "updateApiSchema" });
|
|
520
|
+
}
|
|
521
|
+
const adminBase = injectAwsAuthAll(adminBaseFragment(clonerEnabled || false), "Admin");
|
|
522
|
+
const sdl = graphqlStitch(adminBase, fragments);
|
|
523
|
+
|
|
524
|
+
// Shrink guard — never let a transient/incomplete stitch clobber the live schema.
|
|
525
|
+
const threshold = parseShrinkThreshold(process.env["RUNTIME_SCHEMA_SHRINK_THRESHOLD"]);
|
|
526
|
+
const currentSdl = await getCurrentSchemaSdl(apiId);
|
|
527
|
+
if (isCatastrophicSchemaShrink(currentSdl, sdl, threshold)) {
|
|
528
|
+
const currentRootFields =
|
|
529
|
+
countRootTypeFields(currentSdl, "Mutation") + countRootTypeFields(currentSdl, "Query");
|
|
530
|
+
const newRootFields =
|
|
531
|
+
countRootTypeFields(sdl, "Mutation") + countRootTypeFields(sdl, "Query");
|
|
532
|
+
log.error(`ABORTED schema push for ${apiId}: new SDL has ${newRootFields} root field(s) vs ${currentRootFields} live (threshold ${threshold}). Refusing to clobber resolvers.`, { comp: "updateApiSchema" });
|
|
533
|
+
emitShrinkRejectionMetric(apiId, currentRootFields, newRootFields);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
await updateAppSyncSchema(apiId, sdl);
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function buildPublishToAggregates(map) {
|
|
541
|
+
const out = {};
|
|
542
|
+
for (const [aggName, envVarName] of Object.entries(map || {})) {
|
|
543
|
+
const queueUrl = process.env[envVarName] || "";
|
|
544
|
+
out[aggName] = sqsPublishJsons(makeQueueRef(queueUrl), "SQS_FIFO");
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function dictByServiceKeys(serviceKeys, handler) {
|
|
550
|
+
const dict = {};
|
|
551
|
+
for (const k of serviceKeys || []) {
|
|
552
|
+
if (!dict[k]) dict[k] = [];
|
|
553
|
+
dict[k].push(handler);
|
|
554
|
+
}
|
|
555
|
+
return dict;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function mergeDicts(...dicts) {
|
|
559
|
+
const out = {};
|
|
560
|
+
for (const d of dicts) {
|
|
561
|
+
for (const [k, v] of Object.entries(d)) {
|
|
562
|
+
out[k] = (out[k] || []).concat(v);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return out;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// pluginDefinition is shipped as a file in the asset zip rather than packed
|
|
569
|
+
// into HANDLER_CONFIG (AWS Lambda's UpdateFunctionConfiguration request has a
|
|
570
|
+
// 5120-byte limit; pluginStructure alone — schemas inline per component —
|
|
571
|
+
// blew past that). The Lambda runtime extracts the zip to /var/task, and the
|
|
572
|
+
// re-export index.mjs lives at /var/task/index.mjs alongside
|
|
573
|
+
// pluginDefinition.json. process.cwd() is /var/task in Lambda, so a relative
|
|
574
|
+
// path resolves correctly. The file is parsed once at cold start.
|
|
575
|
+
function loadPluginDefinition() {
|
|
576
|
+
try {
|
|
577
|
+
const raw = readFileSync(new URL("./pluginDefinition.json", `file://${process.cwd()}/`), "utf-8");
|
|
578
|
+
return JSON.parse(raw);
|
|
579
|
+
} catch (e) {
|
|
580
|
+
const msg = (e && e.message) || String(e);
|
|
581
|
+
throw new Error("Failed to load pluginDefinition.json from asset zip: " + msg);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Cross-plugin spec packages (e.g. "@reventlessdev/online-shop-hybrid-catalog-spec")
|
|
586
|
+
// are bundled into the function asset under /var/task/node_modules/ by
|
|
587
|
+
// PluginRuntime_Builder.forPluginEventCollector. This entry-point file, however,
|
|
588
|
+
// lives in the Lambda LAYER at /opt/nodejs/node_modules/.../AdminEventCollectorEntryPoint.mjs,
|
|
589
|
+
// so a bare `await import("@reventlessdev/online-shop-hybrid-catalog-spec/...")`
|
|
590
|
+
// from here resolves through /opt/nodejs/node_modules/ and never reaches the
|
|
591
|
+
// function asset. Anchoring createRequire at file:///var/task/index.mjs makes
|
|
592
|
+
// resolution walk /var/task/node_modules first (and NODE_PATH-listed dirs as
|
|
593
|
+
// fallback, which Lambda sets to /opt/nodejs/node_modules — so static-import
|
|
594
|
+
// targets like reventless-core still resolve too). We then turn the resolved
|
|
595
|
+
// filesystem path into a file:// URL and dynamic-import it.
|
|
596
|
+
const importFromAsset = (() => {
|
|
597
|
+
const requireFromAsset = createRequire(`file://${process.cwd()}/index.mjs`);
|
|
598
|
+
return async (specifier) => {
|
|
599
|
+
const resolved = requireFromAsset.resolve(specifier);
|
|
600
|
+
return import(pathToFileURL(resolved).href);
|
|
601
|
+
};
|
|
602
|
+
})();
|
|
603
|
+
|
|
604
|
+
async function buildHandler() {
|
|
605
|
+
const config = parseHandlerConfig(process.env["HANDLER_CONFIG"]);
|
|
606
|
+
const pluginDefinition = loadPluginDefinition();
|
|
607
|
+
const lambdaFunctionName = process.env["AWS_LAMBDA_FUNCTION_NAME"] || "unknown";
|
|
608
|
+
|
|
609
|
+
// runtimeOps carries only what the admin EP mapping's callHandler actually
|
|
610
|
+
// needs: sendMessageToChannel for ForwardCommand. Cross-plugin SNS
|
|
611
|
+
// subscription wiring moved to manageSubscriptionsFn (Phase 3 Step 1 +
|
|
612
|
+
// Step 3) — the topicSubscription stubs that the plugin Connect extension
|
|
613
|
+
// used to call no longer exist on either side.
|
|
614
|
+
const runtimeOps = {
|
|
615
|
+
messagePublish: { sendMessageToChannel: sendMessage },
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const queryEngine = {
|
|
619
|
+
scan: (readModelName, filterConfigs, limit) =>
|
|
620
|
+
scanByTableName(config.pluginReadModelTableName, filterConfigs, limit),
|
|
621
|
+
query: async () => {
|
|
622
|
+
throw new Error("QueryEngine.query not available in bundled Plugin EventCollector");
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const scheduler = {
|
|
627
|
+
createSchedule: cwCreateSchedule(config.schedulerRoleArn),
|
|
628
|
+
deleteSchedule: cwDeleteSchedule,
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const commandTopicResources = config.schedulerQueueArn !== ""
|
|
632
|
+
? [{
|
|
633
|
+
name: config.schedulerQueueName,
|
|
634
|
+
id: config.schedulerQueueName,
|
|
635
|
+
urn: config.schedulerQueueArn,
|
|
636
|
+
service: "unknown",
|
|
637
|
+
resourceInfo: "NoInfo",
|
|
638
|
+
role: "",
|
|
639
|
+
region: "",
|
|
640
|
+
resourceType: "",
|
|
641
|
+
configuration: {},
|
|
642
|
+
tags: {},
|
|
643
|
+
}]
|
|
644
|
+
: [];
|
|
645
|
+
|
|
646
|
+
const updateApiSchemaFn = mkUpdateApiSchema(
|
|
647
|
+
config.pluginSchemaPersistenceTableName,
|
|
648
|
+
config.appSyncApiId,
|
|
649
|
+
config.clonerEnabled,
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
const manageSubscriptionsFn = mkManageSubscriptions(config.pluginReadModelTableName);
|
|
653
|
+
|
|
654
|
+
// EP operations — admin has 1 entry (Plugin EP), plugins have N user EPs.
|
|
655
|
+
//
|
|
656
|
+
// Two mapping shapes flow through `config.extensionPoints`:
|
|
657
|
+
//
|
|
658
|
+
// 1. Admin-internal (PluginExtensionPoint_Plugin.res.mjs) — exports a
|
|
659
|
+
// `Make(Spec)` functor consuming runtime ops. Used only by the admin
|
|
660
|
+
// Lambda's Plugin EP. Returns `{Mapping}`.
|
|
661
|
+
//
|
|
662
|
+
// 2. User-authored (e.g. Products_ExtensionPointMapping.res.mjs) — exports
|
|
663
|
+
// the Mapping fields at top level (mapIncomingCommand, mapOutgoingEvent,
|
|
664
|
+
// Delegate, ...) with NO `Make` functor. Used by every plugin EC Lambda
|
|
665
|
+
// that declares an ExtensionPoint. The ExtensionPoint spec module is
|
|
666
|
+
// erased at compile time and must be re-attached at runtime.
|
|
667
|
+
//
|
|
668
|
+
// Dispatch on `typeof Make === function` to handle both. Mirrors the
|
|
669
|
+
// user-extensions branch below which already does this for ExtensionMappings.
|
|
670
|
+
const epHandlers = await Promise.all(config.extensionPoints.map(async (ep) => {
|
|
671
|
+
const specMod = patchSpecId(await importFromAsset(ep.specModule));
|
|
672
|
+
const mappingsMod = await importFromAsset(ep.mappingsModule);
|
|
673
|
+
|
|
674
|
+
let mappingsModule;
|
|
675
|
+
if (typeof mappingsMod.Make === "function") {
|
|
676
|
+
// Admin's Plugin EP: invoke the Make functor with runtime ops.
|
|
677
|
+
const epModule = mappingsMod.Make({
|
|
678
|
+
runtimeOps,
|
|
679
|
+
environment: lambdaFunctionName,
|
|
680
|
+
updateApiSchema: updateApiSchemaFn,
|
|
681
|
+
manageSubscriptions: manageSubscriptionsFn,
|
|
682
|
+
});
|
|
683
|
+
mappingsModule = { mappings: [epModule.Mapping] };
|
|
684
|
+
} else {
|
|
685
|
+
// User EP: reconstruct MappingImpl from top-level exports, re-attach
|
|
686
|
+
// the erased ExtensionPoint spec module, then transform input shape
|
|
687
|
+
// (mapIncomingCommand/mapOutgoingEvent) into the runtime T shape via
|
|
688
|
+
// ExtensionPointMapping.Make. `Delegate.Id` is also patched via
|
|
689
|
+
// patchSpecId — the inner `module Delegate = { module Id = Id.String; … }`
|
|
690
|
+
// is erased at compile time the same way the top-level `module Id` is,
|
|
691
|
+
// and ExtensionPointMapping.Make reads `Delegate.Id.schema` at runtime to
|
|
692
|
+
// decode incoming events.
|
|
693
|
+
const mappingImpl = {
|
|
694
|
+
...mappingsMod,
|
|
695
|
+
ExtensionPoint: specMod,
|
|
696
|
+
Delegate: patchSpecId(mappingsMod.Delegate),
|
|
697
|
+
moduleUrl: ep.mappingsModule,
|
|
698
|
+
};
|
|
699
|
+
const transformedMapping = extensionPointMappingMake(mappingImpl);
|
|
700
|
+
mappingsModule = { mappings: [transformedMapping] };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const resolvedTopic = {
|
|
704
|
+
name: ep.eventTopicArn,
|
|
705
|
+
id: ep.eventTopicArn,
|
|
706
|
+
arn: ep.eventTopicArn,
|
|
707
|
+
};
|
|
708
|
+
const ops = extensionPointOperationsMake(specMod)(mappingsModule)({
|
|
709
|
+
publishToEventTopic: (id, meta, json) => snsPublish(resolvedTopic, id, meta, json),
|
|
710
|
+
commandTopicResources,
|
|
711
|
+
scheduler,
|
|
712
|
+
queryEngine,
|
|
713
|
+
resourceNaming,
|
|
714
|
+
});
|
|
715
|
+
return {
|
|
716
|
+
aggregateNames: ep.aggregateNames || [],
|
|
717
|
+
outgoingHandler: ops.outgoingJsonEventsHandler,
|
|
718
|
+
};
|
|
719
|
+
}));
|
|
720
|
+
|
|
721
|
+
// Connect extension — exactly one entry per plugin Lambda; null for admin.
|
|
722
|
+
const publishToAggregates = buildPublishToAggregates(config.publishToAggregates);
|
|
723
|
+
const publishToPluginExtensionPoint = sqsPublishJsons(
|
|
724
|
+
makeQueueRef(config.pluginExtensionPointCmdTopicUrl),
|
|
725
|
+
"SQS_FIFO",
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
let connectExtensionHandler = null;
|
|
729
|
+
if (config.connectExtension) {
|
|
730
|
+
const ext = config.connectExtension;
|
|
731
|
+
const specMod = patchSpecId(await importFromAsset(ext.specModule));
|
|
732
|
+
const mappingsMod = await importFromAsset(ext.mappingsModule);
|
|
733
|
+
// After Phase 3 of plugin-eventcollector-runtime-rewire, the Connect
|
|
734
|
+
// extension Spec carries only pluginDefinition — cross-plugin subscribe /
|
|
735
|
+
// unsubscribe directives moved to admin's manageSubscriptions hook.
|
|
736
|
+
const extBuilder = mappingsMod.Make({
|
|
737
|
+
pluginDefinition,
|
|
738
|
+
});
|
|
739
|
+
// PluginConnectExtension_Builder.Make returns a module exposing:
|
|
740
|
+
// - ConnectPluginMapping (single mapping)
|
|
741
|
+
// - ConnectPluginMappings (Mappings module: {Spec, name, mappings, moduleUrl})
|
|
742
|
+
// Use ConnectPluginMappings as the Mappings arg to Extension_Operations.Make.
|
|
743
|
+
const mappingsModule = extBuilder.ConnectPluginMappings;
|
|
744
|
+
const ops = extensionOperationsMake(specMod)(mappingsModule)({
|
|
745
|
+
publishToAggregates,
|
|
746
|
+
publishToPluginExtensionPoint,
|
|
747
|
+
readModelNamesForSourceName: {},
|
|
748
|
+
publishToReadModels: {},
|
|
749
|
+
queryEngine,
|
|
750
|
+
});
|
|
751
|
+
connectExtensionHandler = {
|
|
752
|
+
extensionPointName: ext.extensionPointName,
|
|
753
|
+
incomingHandler: ops.incomingJsonEventsHandler,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// User-declared extensions — per-entry: three dynamic imports (EP spec,
|
|
758
|
+
// user mapping file, Delegate spec), reconstruct the full Mapping object
|
|
759
|
+
// (compiled .res.mjs erases Mapping.ExtensionPoint / Mapping.Delegate), run
|
|
760
|
+
// it through ExtensionMapping.Make, and build per-extension Extension_Operations.
|
|
761
|
+
const userExtensionHandlers = await Promise.all(config.extensions.map(async (ext) => {
|
|
762
|
+
if (!ext.specModule || !ext.mappingsModule || !ext.delegateModule) {
|
|
763
|
+
log.warn("extension '" + ext.name + "' missing module specifier(s); skipping. specModule=" + ext.specModule + ", mappingsModule=" + ext.mappingsModule + ", delegateModule=" + ext.delegateModule, { comp: "AdminEventCollectorEntryPoint" });
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
const epSpec = patchSpecId(await importFromAsset(ext.specModule));
|
|
767
|
+
const userMod = await importFromAsset(ext.mappingsModule);
|
|
768
|
+
const delegateSpec = patchSpecId(await importFromAsset(ext.delegateModule));
|
|
769
|
+
|
|
770
|
+
// Reconstruct Mapping with the freshly imported specs re-attached. The
|
|
771
|
+
// moduleUrl / delegateModuleUrl fields satisfy the ExtensionMapping.Mapping
|
|
772
|
+
// module type — ExtensionMapping.Make doesn't currently consume them at
|
|
773
|
+
// runtime, but we set them for shape parity.
|
|
774
|
+
const fullMapping = {
|
|
775
|
+
ExtensionPoint: epSpec,
|
|
776
|
+
Delegate: delegateSpec,
|
|
777
|
+
moduleUrl: ext.mappingsModule,
|
|
778
|
+
delegateModuleUrl: ext.delegateModule,
|
|
779
|
+
mapIncomingEvent: userMod.Mapping.mapIncomingEvent,
|
|
780
|
+
mapOutgoingEvent: userMod.Mapping.mapOutgoingEvent,
|
|
781
|
+
};
|
|
782
|
+
const transformedMapping = extensionMappingMake(fullMapping);
|
|
783
|
+
const mappingsModule = {
|
|
784
|
+
name: ext.name,
|
|
785
|
+
moduleUrl: ext.mappingsModule,
|
|
786
|
+
mappings: [transformedMapping],
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
// Per-extension publishToAggregates filtered to ext.aggregateNames —
|
|
790
|
+
// each aggregate's cmd-topic SQS URL is looked up via the
|
|
791
|
+
// PTA_<aggName>_QUEUE_URL env var name carried in HANDLER_CONFIG.
|
|
792
|
+
const extPublishToAggregates = {};
|
|
793
|
+
for (const aggName of ext.aggregateNames) {
|
|
794
|
+
const envVarName = config.publishToAggregates[aggName];
|
|
795
|
+
if (!envVarName) continue;
|
|
796
|
+
const queueUrl = process.env[envVarName] || "";
|
|
797
|
+
if (!queueUrl) continue;
|
|
798
|
+
extPublishToAggregates[aggName] = sqsPublishJsons(makeQueueRef(queueUrl), "SQS_FIFO");
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Per-extension publishToReadModels — each RM's EventCollector SQS URL
|
|
802
|
+
// is looked up via the PRM_<rmName>_QUEUE_URL env var name carried in
|
|
803
|
+
// HANDLER_CONFIG.readModelQueueUrls.
|
|
804
|
+
const extPublishToReadModels = {};
|
|
805
|
+
for (const rmName of ext.readModelNames) {
|
|
806
|
+
const envVarName = config.readModelQueueUrls[rmName];
|
|
807
|
+
if (!envVarName) continue;
|
|
808
|
+
const queueUrl = process.env[envVarName] || "";
|
|
809
|
+
if (!queueUrl) continue;
|
|
810
|
+
extPublishToReadModels[rmName] = sqsPublishJsons(makeQueueRef(queueUrl), "SQS_FIFO");
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// readModelNamesForSourceName scoped to this extension's EP — Extension_Operations
|
|
814
|
+
// uses this dict to decide which RMs receive each incoming event.
|
|
815
|
+
const extReadModelNamesForSourceName = {
|
|
816
|
+
[ext.extensionPointName]:
|
|
817
|
+
config.readModelNamesForSourceName[ext.extensionPointName] || [],
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
const ops = extensionOperationsMake(epSpec)(mappingsModule)({
|
|
821
|
+
publishToAggregates: extPublishToAggregates,
|
|
822
|
+
publishToPluginExtensionPoint,
|
|
823
|
+
readModelNamesForSourceName: extReadModelNamesForSourceName,
|
|
824
|
+
publishToReadModels: extPublishToReadModels,
|
|
825
|
+
queryEngine,
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
return {
|
|
829
|
+
extensionPointName: ext.extensionPointName,
|
|
830
|
+
aggregateNames: ext.aggregateNames,
|
|
831
|
+
incomingHandler: ops.incomingJsonEventsHandler,
|
|
832
|
+
outgoingHandler: ops.outgoingJsonEventsHandler,
|
|
833
|
+
};
|
|
834
|
+
}));
|
|
835
|
+
const extensionHandlers = userExtensionHandlers.filter(Boolean);
|
|
836
|
+
|
|
837
|
+
// Build the four service-keyed dicts that Plugin_Callback.Make consumes.
|
|
838
|
+
const outgoingExtensionPointEventHandlers = epHandlers
|
|
839
|
+
.map((eph) => dictByServiceKeys(eph.aggregateNames, eph.outgoingHandler))
|
|
840
|
+
.reduce(mergeDicts, {});
|
|
841
|
+
const incomingConnectExtensionEventHandlers = connectExtensionHandler
|
|
842
|
+
? { [connectExtensionHandler.extensionPointName]: [connectExtensionHandler.incomingHandler] }
|
|
843
|
+
: {};
|
|
844
|
+
const outgoingExtensionEventHandlers = extensionHandlers
|
|
845
|
+
.map((eh) => dictByServiceKeys(eh.aggregateNames, eh.outgoingHandler))
|
|
846
|
+
.reduce(mergeDicts, {});
|
|
847
|
+
const incomingExtensionEventHandlers = extensionHandlers
|
|
848
|
+
.map((eh) => ({ [eh.extensionPointName]: [eh.incomingHandler] }))
|
|
849
|
+
.reduce(mergeDicts, {});
|
|
850
|
+
|
|
851
|
+
const callback = pluginCallbackMake({
|
|
852
|
+
pluginDefinition,
|
|
853
|
+
incomingConnectExtensionEventHandlers,
|
|
854
|
+
outgoingExtensionPointEventHandlers,
|
|
855
|
+
outgoingExtensionEventHandlers,
|
|
856
|
+
incomingExtensionEventHandlers,
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// Cold-start reconciliation — awaited so it finishes inside the same
|
|
860
|
+
// invocation that initialised the Lambda. Fire-and-forget doesn't work on
|
|
861
|
+
// Lambda: any unfinished promise pauses when the runtime freezes between
|
|
862
|
+
// invocations and rarely gets a chance to resume. Idempotent at the SNS
|
|
863
|
+
// level, so the modest extra init latency is paid only on true cold starts.
|
|
864
|
+
await reconcileSubscriptionsOnce(config.pluginReadModelTableName, manageSubscriptionsFn);
|
|
865
|
+
|
|
866
|
+
return handleDynamoDbOrSqsEvent(makeQueueRef(config.queueUrl), callback.handleJsonEvents);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const sqsHandlerPromise = buildHandler();
|
|
870
|
+
|
|
871
|
+
export async function handler(event, context) {
|
|
872
|
+
_currentRequestId = context?.awsRequestId || "unknown";
|
|
873
|
+
const records = event.Records || [];
|
|
874
|
+
const correlationId = extractCorrelationId(records);
|
|
875
|
+
log.debug("processing " + records.length.toString() + " record(s)", { comp: "PluginEventCollectorRuntime" });
|
|
876
|
+
const sqsHandler = await sqsHandlerPromise;
|
|
877
|
+
await runEffect(correlationId, sqsHandler(event, context));
|
|
878
|
+
return "";
|
|
879
|
+
}
|