@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,149 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Fs from "fs";
|
|
4
|
+
import * as Os from "os";
|
|
5
|
+
import * as Path from "path";
|
|
6
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
7
|
+
import * as Util_StaticBundle$ReventlessAws from "../src/util/Util_StaticBundle.res.mjs";
|
|
8
|
+
|
|
9
|
+
function makeFixture() {
|
|
10
|
+
let base = Fs.mkdtempSync(Path.join(Os.tmpdir(), "static-bundle-"));
|
|
11
|
+
Fs.writeFileSync(Path.join(base, "index.html"), "<html></html>");
|
|
12
|
+
Fs.writeFileSync(Path.join(base, "app.js"), "console.log('x')");
|
|
13
|
+
Fs.writeFileSync(Path.join(base, "app.css"), "body{color:red}");
|
|
14
|
+
Fs.writeFileSync(Path.join(base, ".DS_Store"), "ignored");
|
|
15
|
+
let assets = Path.join(base, "assets");
|
|
16
|
+
Fs.mkdirSync(assets, {
|
|
17
|
+
recursive: true
|
|
18
|
+
});
|
|
19
|
+
Fs.writeFileSync(Path.join(assets, "logo.svg"), "<svg/>");
|
|
20
|
+
Fs.writeFileSync(Path.join(assets, "favicon.ico"), "ico");
|
|
21
|
+
return base;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
globalThis.describe("Util_StaticBundle.contentTypeFor", () => {
|
|
25
|
+
globalThis.test("html", () => {
|
|
26
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.contentTypeFor("index.html")).toBe("text/html; charset=utf-8");
|
|
27
|
+
});
|
|
28
|
+
globalThis.test("css", () => {
|
|
29
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.contentTypeFor("foo/bar.css")).toBe("text/css; charset=utf-8");
|
|
30
|
+
});
|
|
31
|
+
globalThis.test("js", () => {
|
|
32
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.contentTypeFor("app.js")).toBe("application/javascript; charset=utf-8");
|
|
33
|
+
});
|
|
34
|
+
globalThis.test("mjs", () => {
|
|
35
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.contentTypeFor("module.mjs")).toBe("application/javascript; charset=utf-8");
|
|
36
|
+
});
|
|
37
|
+
globalThis.test("json", () => {
|
|
38
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.contentTypeFor("data.json")).toBe("application/json; charset=utf-8");
|
|
39
|
+
});
|
|
40
|
+
globalThis.test("svg", () => {
|
|
41
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.contentTypeFor("logo.svg")).toBe("image/svg+xml");
|
|
42
|
+
});
|
|
43
|
+
globalThis.test("woff2", () => {
|
|
44
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.contentTypeFor("font.woff2")).toBe("font/woff2");
|
|
45
|
+
});
|
|
46
|
+
globalThis.test("wasm", () => {
|
|
47
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.contentTypeFor("blob.wasm")).toBe("application/wasm");
|
|
48
|
+
});
|
|
49
|
+
globalThis.test("uppercase extension is normalised", () => {
|
|
50
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.contentTypeFor("LOGO.PNG")).toBe("image/png");
|
|
51
|
+
});
|
|
52
|
+
globalThis.test("unknown extension defaults to octet-stream", () => {
|
|
53
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.contentTypeFor("blob.xyz")).toBe("application/octet-stream");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
globalThis.describe("Util_StaticBundle.sanitizeName", () => {
|
|
58
|
+
globalThis.test("replaces slashes with dashes", () => {
|
|
59
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.sanitizeName("foo/bar/baz")).toBe("foo-bar-baz");
|
|
60
|
+
});
|
|
61
|
+
globalThis.test("replaces dots with dashes", () => {
|
|
62
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.sanitizeName("index.html")).toBe("index-html");
|
|
63
|
+
});
|
|
64
|
+
globalThis.test("replaces both", () => {
|
|
65
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.sanitizeName("assets/logo.svg")).toBe("assets-logo-svg");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
globalThis.describe("Util_StaticBundle.walk", () => {
|
|
70
|
+
globalThis.test("returns one entry per file, skips dotfiles, uses forward slashes", () => {
|
|
71
|
+
let dir = makeFixture();
|
|
72
|
+
let entries = Util_StaticBundle$ReventlessAws.walk(dir);
|
|
73
|
+
let keys = entries.map(e => e.relativePath).toSorted(Primitive_string.compare);
|
|
74
|
+
globalThis.expect(keys).toEqual([
|
|
75
|
+
"app.css",
|
|
76
|
+
"app.js",
|
|
77
|
+
"assets/favicon.ico",
|
|
78
|
+
"assets/logo.svg",
|
|
79
|
+
"index.html"
|
|
80
|
+
]);
|
|
81
|
+
Fs.rmSync(dir, {
|
|
82
|
+
recursive: true,
|
|
83
|
+
force: true
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
globalThis.test("computes a non-empty content hash for each entry", () => {
|
|
87
|
+
let dir = makeFixture();
|
|
88
|
+
let entries = Util_StaticBundle$ReventlessAws.walk(dir);
|
|
89
|
+
entries.forEach(e => {
|
|
90
|
+
globalThis.expect(e.contentHash.length > 0).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
Fs.rmSync(dir, {
|
|
93
|
+
recursive: true,
|
|
94
|
+
force: true
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
globalThis.test("identical contents produce identical hashes", () => {
|
|
98
|
+
let dir = makeFixture();
|
|
99
|
+
let entries = Util_StaticBundle$ReventlessAws.walk(dir);
|
|
100
|
+
let indexEntry = entries.find(e => e.relativePath === "index.html");
|
|
101
|
+
let indexHash = indexEntry.contentHash;
|
|
102
|
+
let dir2 = Fs.mkdtempSync(Path.join(Os.tmpdir(), "static-bundle-"));
|
|
103
|
+
Fs.writeFileSync(Path.join(dir2, "index.html"), "<html></html>");
|
|
104
|
+
let entries2 = Util_StaticBundle$ReventlessAws.walk(dir2);
|
|
105
|
+
let indexHash2 = entries2[0].contentHash;
|
|
106
|
+
globalThis.expect(indexHash).toBe(indexHash2);
|
|
107
|
+
Fs.rmSync(dir, {
|
|
108
|
+
recursive: true,
|
|
109
|
+
force: true
|
|
110
|
+
});
|
|
111
|
+
Fs.rmSync(dir2, {
|
|
112
|
+
recursive: true,
|
|
113
|
+
force: true
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
globalThis.test("different contents produce different hashes", () => {
|
|
117
|
+
let dirA = Fs.mkdtempSync(Path.join(Os.tmpdir(), "static-bundle-"));
|
|
118
|
+
Fs.writeFileSync(Path.join(dirA, "f.txt"), "AAA");
|
|
119
|
+
let dirB = Fs.mkdtempSync(Path.join(Os.tmpdir(), "static-bundle-"));
|
|
120
|
+
Fs.writeFileSync(Path.join(dirB, "f.txt"), "BBB");
|
|
121
|
+
let hashA = Util_StaticBundle$ReventlessAws.walk(dirA)[0].contentHash;
|
|
122
|
+
let hashB = Util_StaticBundle$ReventlessAws.walk(dirB)[0].contentHash;
|
|
123
|
+
globalThis.expect(hashA === hashB).toBe(false);
|
|
124
|
+
Fs.rmSync(dirA, {
|
|
125
|
+
recursive: true,
|
|
126
|
+
force: true
|
|
127
|
+
});
|
|
128
|
+
Fs.rmSync(dirB, {
|
|
129
|
+
recursive: true,
|
|
130
|
+
force: true
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
globalThis.test("throws when assetsDir does not exist", () => {
|
|
134
|
+
let missing = Path.join(Os.tmpdir(), "static-bundle-does-not-exist-xyz123");
|
|
135
|
+
let threw;
|
|
136
|
+
try {
|
|
137
|
+
Util_StaticBundle$ReventlessAws.walk(missing);
|
|
138
|
+
threw = false;
|
|
139
|
+
} catch (exn) {
|
|
140
|
+
threw = true;
|
|
141
|
+
}
|
|
142
|
+
globalThis.expect(threw).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export {
|
|
147
|
+
makeFixture,
|
|
148
|
+
}
|
|
149
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Integration regression guard for the DCB CommandTopic Lambda entry point.
|
|
2
|
+
//
|
|
3
|
+
// Drives the real `DcbCommandTopicEntryPoint.mjs` against a DynamoDB Local
|
|
4
|
+
// table, with a test loader supplying a tiny `EpTestSlice` spec/behavior pair
|
|
5
|
+
// in place of the production Lambda's `/var/task/node_modules` dynamic import.
|
|
6
|
+
// Covers the regression class behind the 2026-06-21 incident: the entry point
|
|
7
|
+
// was passing the wrong number of positional args to ReScript-compiled
|
|
8
|
+
// `StateChangeSlice_Callback.handleCommands`, so `stream` landed as undefined
|
|
9
|
+
// and the Lambda crashed inside Effect's `Stream.mapEffect` with
|
|
10
|
+
// `'channel' in undefined`. Any future arity drift between the JS caller and
|
|
11
|
+
// the ReScript signature (e.g. a new optional labelled arg added before the
|
|
12
|
+
// positional ones) re-breaks the same path; this test fails it as
|
|
13
|
+
// CommandAccepted → Lambda:Unhandled.
|
|
14
|
+
//
|
|
15
|
+
// Boots via the same `pnpm run test:integration` Docker-gated suite as the
|
|
16
|
+
// storage-runtime integration tests; harnessed by `DcbIntegrationHarness`.
|
|
17
|
+
|
|
18
|
+
open JestGlobals
|
|
19
|
+
|
|
20
|
+
module H = DcbIntegrationHarness
|
|
21
|
+
|
|
22
|
+
// `cmdGenHandler(event)` returns an Effect that must be runPromise'd through
|
|
23
|
+
// the request context the production handler supplies. This raw helper drives
|
|
24
|
+
// the full real pipeline: build handlers via the entry point's exported
|
|
25
|
+
// factory, dispatch one AppSync event, run the Effect, return the AppSync-
|
|
26
|
+
// shape outcome JSON.
|
|
27
|
+
let runOneAppSyncEvent: (string, JSON.t) => promise<JSON.t> = %raw(`
|
|
28
|
+
async (tableName, event) => {
|
|
29
|
+
const { buildHandlersForConfig } = await import(
|
|
30
|
+
"@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs"
|
|
31
|
+
);
|
|
32
|
+
const Effect = await import("effect/Effect");
|
|
33
|
+
const { tag: requestContextTag } = await import(
|
|
34
|
+
"@reventlessdev/reventless-core/src/RequestContext.res.mjs"
|
|
35
|
+
);
|
|
36
|
+
const { commandOutcomeToJson } = await import(
|
|
37
|
+
"@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs"
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const loadModule = async (specifier) => {
|
|
41
|
+
if (specifier === "ep-test://spec") return await import("./EpTestSlice.res.mjs");
|
|
42
|
+
if (specifier === "ep-test://behavior") return await import("./EpTestSliceBehavior.res.mjs");
|
|
43
|
+
throw new Error("unknown test specifier: " + specifier);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const config = {
|
|
47
|
+
pluginName: "EpTestPlugin",
|
|
48
|
+
dcbEventLogTableName: tableName,
|
|
49
|
+
stateChangeSliceModules: [{ spec: "ep-test://spec", behavior: "ep-test://behavior" }],
|
|
50
|
+
queueUrl: "https://sqs.eu-west-1.amazonaws.com/000000000000/ep-test-queue",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const [, cmdGenHandler] = await buildHandlersForConfig(config, { loadModule });
|
|
54
|
+
|
|
55
|
+
const effect = cmdGenHandler(event)
|
|
56
|
+
.pipe(Effect.provideService(requestContextTag, { correlationId: "ep-test" }));
|
|
57
|
+
const outcome = await Effect.runPromise(effect);
|
|
58
|
+
return commandOutcomeToJson(outcome);
|
|
59
|
+
}
|
|
60
|
+
`)
|
|
61
|
+
|
|
62
|
+
let buildAppSyncEvent = (widgetId): JSON.t => {
|
|
63
|
+
// CommandGenerator payload: `{command, arguments, meta, identity?}` — the
|
|
64
|
+
// exact shape AppSync's direct-invoke resolver sends (matches the resolver
|
|
65
|
+
// template in `DcbCmdHandlerResolver_GraphQL.res`).
|
|
66
|
+
let command = Dict.fromArray([("AddWidget", Dict.make()->JSON.Encode.object)])
|
|
67
|
+
let arguments = Dict.fromArray([("widgetId", widgetId->JSON.Encode.string)])
|
|
68
|
+
let meta = Dict.fromArray([
|
|
69
|
+
("user", "ep-test"->JSON.Encode.string),
|
|
70
|
+
("ip", JSON.Encode.null),
|
|
71
|
+
])
|
|
72
|
+
Dict.fromArray([
|
|
73
|
+
("command", command->JSON.Encode.object),
|
|
74
|
+
("arguments", arguments->JSON.Encode.object),
|
|
75
|
+
("meta", meta->JSON.Encode.object),
|
|
76
|
+
])->JSON.Encode.object
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("DcbCommandTopicEntryPoint integration", () => {
|
|
80
|
+
testAsync(
|
|
81
|
+
"AppSync direct-invoke routes through the runtime handler to CommandAccepted",
|
|
82
|
+
async () => {
|
|
83
|
+
// Own table-name prefix avoids `ResourceInUseException` when the test
|
|
84
|
+
// shares a DDB Local instance with sibling suites that also use the
|
|
85
|
+
// harness's `DcbItTest_N` counter.
|
|
86
|
+
let table = await H.createDcbTable("EpTest_" ++ Date.now()->Float.toString)
|
|
87
|
+
let event = buildAppSyncEvent("widget-1")
|
|
88
|
+
let outcomeJson = await runOneAppSyncEvent(table.name, event)
|
|
89
|
+
|
|
90
|
+
// `commandOutcomeToJson` produces an AppSync-shape envelope:
|
|
91
|
+
// { __typename: "CommandAccepted" | "CommandRejected" | "CommandPending", ... }
|
|
92
|
+
// The arity bug surfaces as `Lambda:Unhandled` at the AppSync layer —
|
|
93
|
+
// here it would manifest as a thrown error from `runPromise` before
|
|
94
|
+
// `commandOutcomeToJson` ever runs, so reaching this assertion at all
|
|
95
|
+
// already proves the regression hasn't returned. We still assert on
|
|
96
|
+
// `CommandAccepted` so a future drift to silent-rejection also fails.
|
|
97
|
+
let s = outcomeJson->JSON.stringifyAny->Option.getOr("<unserializable>")
|
|
98
|
+
expect(s->String.includes("CommandAccepted"))->toBe(true)
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as DcbIntegrationHarness$ReventlessAws from "./DcbIntegrationHarness.res.mjs";
|
|
5
|
+
|
|
6
|
+
let runOneAppSyncEvent = (async (tableName, event) => {
|
|
7
|
+
const { buildHandlersForConfig } = await import(
|
|
8
|
+
"@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs"
|
|
9
|
+
);
|
|
10
|
+
const Effect = await import("effect/Effect");
|
|
11
|
+
const { tag: requestContextTag } = await import(
|
|
12
|
+
"@reventlessdev/reventless-core/src/RequestContext.res.mjs"
|
|
13
|
+
);
|
|
14
|
+
const { commandOutcomeToJson } = await import(
|
|
15
|
+
"@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs"
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const loadModule = async (specifier) => {
|
|
19
|
+
if (specifier === "ep-test://spec") return await import("./EpTestSlice.res.mjs");
|
|
20
|
+
if (specifier === "ep-test://behavior") return await import("./EpTestSliceBehavior.res.mjs");
|
|
21
|
+
throw new Error("unknown test specifier: " + specifier);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const config = {
|
|
25
|
+
pluginName: "EpTestPlugin",
|
|
26
|
+
dcbEventLogTableName: tableName,
|
|
27
|
+
stateChangeSliceModules: [{ spec: "ep-test://spec", behavior: "ep-test://behavior" }],
|
|
28
|
+
queueUrl: "https://sqs.eu-west-1.amazonaws.com/000000000000/ep-test-queue",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const [, cmdGenHandler] = await buildHandlersForConfig(config, { loadModule });
|
|
32
|
+
|
|
33
|
+
const effect = cmdGenHandler(event)
|
|
34
|
+
.pipe(Effect.provideService(requestContextTag, { correlationId: "ep-test" }));
|
|
35
|
+
const outcome = await Effect.runPromise(effect);
|
|
36
|
+
return commandOutcomeToJson(outcome);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function buildAppSyncEvent(widgetId) {
|
|
40
|
+
let command = Object.fromEntries([[
|
|
41
|
+
"AddWidget",
|
|
42
|
+
{}
|
|
43
|
+
]]);
|
|
44
|
+
let $$arguments = Object.fromEntries([[
|
|
45
|
+
"widgetId",
|
|
46
|
+
widgetId
|
|
47
|
+
]]);
|
|
48
|
+
let meta = Object.fromEntries([
|
|
49
|
+
[
|
|
50
|
+
"user",
|
|
51
|
+
"ep-test"
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"ip",
|
|
55
|
+
null
|
|
56
|
+
]
|
|
57
|
+
]);
|
|
58
|
+
return Object.fromEntries([
|
|
59
|
+
[
|
|
60
|
+
"command",
|
|
61
|
+
command
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"arguments",
|
|
65
|
+
$$arguments
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"meta",
|
|
69
|
+
meta
|
|
70
|
+
]
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
globalThis.describe("DcbCommandTopicEntryPoint integration", () => {
|
|
75
|
+
globalThis.test("AppSync direct-invoke routes through the runtime handler to CommandAccepted", async () => {
|
|
76
|
+
let table = await DcbIntegrationHarness$ReventlessAws.createDcbTable("EpTest_" + Date.now().toString());
|
|
77
|
+
let event = buildAppSyncEvent("widget-1");
|
|
78
|
+
let outcomeJson = await runOneAppSyncEvent(table.name, event);
|
|
79
|
+
let s = Stdlib_Option.getOr(JSON.stringify(outcomeJson), "<unserializable>");
|
|
80
|
+
globalThis.expect(s.includes("CommandAccepted")).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
let H;
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
H,
|
|
88
|
+
runOneAppSyncEvent,
|
|
89
|
+
buildAppSyncEvent,
|
|
90
|
+
}
|
|
91
|
+
/* runOneAppSyncEvent Not a pure module */
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// ⚠️ PENDING REWRITE (dcb-fence-event-type-granularity): this suite still encodes
|
|
2
|
+
// the OLD scalar-`lastPosition` fence + `create#…` create-guard model. The adapter
|
|
3
|
+
// now uses PER-TYPE `pos#<eventType>` fence attributes with the create guard folded
|
|
4
|
+
// in, so `H.setFence(~lastPosition=…)` seeding, the `create#…`-row assertions, and
|
|
5
|
+
// the tag-only (no `eventTypes`) queries below must be reworked and re-run against
|
|
6
|
+
// DynamoDB Local before they pass. Tracked as Test plan #2 in
|
|
7
|
+
// docs/plans/dcb-fence-event-type-granularity.md. Not run by the default unit suite
|
|
8
|
+
// (CI `pnpm test`), so it does not gate the build.
|
|
9
|
+
//
|
|
10
|
+
// DCB DynamoDB integration suite — exercises the real fence path against a
|
|
11
|
+
// DynamoDB Local engine (NOT run by the default unit suite; see
|
|
12
|
+
// `jest.integration.config.js` + `scripts/run-integration-tests.sh`).
|
|
13
|
+
//
|
|
14
|
+
// The unit suite (`DcbEventLogStorage_DynamoDb_RuntimeTest.res`) asserts the
|
|
15
|
+
// *shape* of the built `TransactWriteItems`. This suite asserts DynamoDB's
|
|
16
|
+
// *behaviour* — that the `ConditionExpression`s interpret as we expect — which
|
|
17
|
+
// is the only thing that proves the fence-scope model (Issue 1 fix) and the
|
|
18
|
+
// optimistic-concurrency guarantees end-to-end. See
|
|
19
|
+
// `docs/analysis/dcb-consistency-check-issues.md` and
|
|
20
|
+
// `docs/plans/dcb-consistency-hardening.md` (Phase 1).
|
|
21
|
+
|
|
22
|
+
open JestGlobals
|
|
23
|
+
|
|
24
|
+
module H = DcbIntegrationHarness
|
|
25
|
+
module Runtime = DcbEventLogStorage_DynamoDb_Runtime
|
|
26
|
+
|
|
27
|
+
let tag = (key, value): Reventless.DcbTag.tag => {key, value}
|
|
28
|
+
let meta = () => ReventlessCore.Message.generateMeta(~service="dcb-it")
|
|
29
|
+
|
|
30
|
+
let event = (eventType, tags): ReventlessCore.DcbEventLog_Adapter.rawStoredEvent => {
|
|
31
|
+
eventType,
|
|
32
|
+
data: JSON.Object(Dict.make()),
|
|
33
|
+
tags,
|
|
34
|
+
meta: meta(),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let simple = (key): Reventless.DcbTag.derivedPartitionTag => Simple({key: key})
|
|
38
|
+
|
|
39
|
+
let isOk = r =>
|
|
40
|
+
switch r {
|
|
41
|
+
| Ok(_) => true
|
|
42
|
+
| Error(_) => false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let isConflict = r =>
|
|
46
|
+
switch r {
|
|
47
|
+
| Error(msg) => msg->String.includes("Conflict")
|
|
48
|
+
| Ok(_) => false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Read a slice's decision-model head position for a query — the `after` an
|
|
52
|
+
// append rides with.
|
|
53
|
+
let readAfter = async (table, query: Reventless.DcbTag.query) => {
|
|
54
|
+
let result = await Runtime.read(table)(~query)
|
|
55
|
+
result.headPosition
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Seed a single event with no consistency check (import/replay path). Bumps the
|
|
59
|
+
// event's partition fence so later conditional readers observe it.
|
|
60
|
+
let seed = (table, ev, ~partitionTag) => Runtime.appendUnconditional(table, [ev], ~partitionTag)
|
|
61
|
+
|
|
62
|
+
describe("DCB DynamoDb integration — fence-scope = read-scope (Issue 1 regression)", () => {
|
|
63
|
+
let productTag = tag("productId", "P5")
|
|
64
|
+
|
|
65
|
+
// Place an order: read (orderId ∪ productId), then append OrderPlaced.
|
|
66
|
+
let placeOrder = async (table, ~orderId, ~customerId) => {
|
|
67
|
+
let query: Reventless.DcbTag.query = [
|
|
68
|
+
{tags: [tag("orderId", orderId)]},
|
|
69
|
+
{tags: [productTag]},
|
|
70
|
+
]
|
|
71
|
+
let after = await readAfter(table, query)
|
|
72
|
+
await Runtime.appendConditional(
|
|
73
|
+
table,
|
|
74
|
+
[event("OrderPlaced", [tag("orderId", orderId), tag("customerId", customerId), productTag])],
|
|
75
|
+
{query, after: ?after},
|
|
76
|
+
~partitionTag=simple("orderId"),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
testAsync("a second order of the same product (different order + customer) succeeds", async () => {
|
|
81
|
+
let table = await H.freshTable()
|
|
82
|
+
let _ = await seed(table, event("CatalogProductSynced", [productTag]), ~partitionTag=simple("productId"))
|
|
83
|
+
|
|
84
|
+
let r1 = await placeOrder(table, ~orderId="O1", ~customerId="C1")
|
|
85
|
+
let r2 = await placeOrder(table, ~orderId="O2", ~customerId="C2")
|
|
86
|
+
|
|
87
|
+
expect(isOk(r1))->toBe(true)
|
|
88
|
+
expect(isOk(r2))->toBe(true)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
testAsync("two concurrent orders of the same product both succeed (no productId contention)", async () => {
|
|
92
|
+
let table = await H.freshTable()
|
|
93
|
+
let _ = await seed(table, event("CatalogProductSynced", [productTag]), ~partitionTag=simple("productId"))
|
|
94
|
+
|
|
95
|
+
let results = await Promise.all([
|
|
96
|
+
placeOrder(table, ~orderId="OA", ~customerId="CA"),
|
|
97
|
+
placeOrder(table, ~orderId="OB", ~customerId="CB"),
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
expect(results->Array.every(isOk))->toBe(true)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
testAsync("a concurrent product re-sync after the order's read still conflicts (OCC preserved)", async () => {
|
|
104
|
+
let table = await H.freshTable()
|
|
105
|
+
let _ = await seed(table, event("CatalogProductSynced", [productTag]), ~partitionTag=simple("productId"))
|
|
106
|
+
|
|
107
|
+
// Order reads its decision model (after = the sync's position)…
|
|
108
|
+
let query: Reventless.DcbTag.query = [{tags: [tag("orderId", "O1")]}, {tags: [productTag]}]
|
|
109
|
+
let after = (await readAfter(table, query))->Option.getOr("")
|
|
110
|
+
expect(after == "")->toBe(false)
|
|
111
|
+
|
|
112
|
+
// …a concurrent re-sync advances fence#productId:P5 past `after`. We set the
|
|
113
|
+
// fence directly (rather than racing a second appendUnconditional) so the
|
|
114
|
+
// advance is deterministically > after — same-millisecond writers can
|
|
115
|
+
// otherwise tie on UUID order (analysis Issue 7). Appending "z" stays
|
|
116
|
+
// lexically greater than any real position.
|
|
117
|
+
let _ = await H.setFence(table, productTag, ~lastPosition=after ++ "z")
|
|
118
|
+
|
|
119
|
+
// …so the now-stale order append must conflict on the productId fence.
|
|
120
|
+
let r = await Runtime.appendConditional(
|
|
121
|
+
table,
|
|
122
|
+
[event("OrderPlaced", [tag("orderId", "O1"), tag("customerId", "C1"), productTag])],
|
|
123
|
+
{query, after: ?Some(after)},
|
|
124
|
+
~partitionTag=simple("orderId"),
|
|
125
|
+
)
|
|
126
|
+
expect(isConflict(r))->toBe(true)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
testAsync("placing an order does not poison the customer slice (secondary customerId not fenced)", async () => {
|
|
130
|
+
let table = await H.freshTable()
|
|
131
|
+
let customerTag = tag("customerId", "C1")
|
|
132
|
+
|
|
133
|
+
// Customer exists (partitioned by customerId) → fence#customerId:C1 seeded.
|
|
134
|
+
let _ = await seed(table, event("CustomerRegistered", [customerTag]), ~partitionTag=simple("customerId"))
|
|
135
|
+
|
|
136
|
+
// Order for C1 carries customerId as a SECONDARY tag (partitioned by orderId).
|
|
137
|
+
let orderQuery: Reventless.DcbTag.query = [{tags: [tag("orderId", "O1")]}]
|
|
138
|
+
let orderAfter = await readAfter(table, orderQuery)
|
|
139
|
+
let _ = await Runtime.appendConditional(
|
|
140
|
+
table,
|
|
141
|
+
[event("OrderPlaced", [tag("orderId", "O1"), customerTag, productTag])],
|
|
142
|
+
{query: orderQuery, after: ?orderAfter},
|
|
143
|
+
~partitionTag=simple("orderId"),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// The customer slice reads its own partition (sees only CustomerRegistered)
|
|
147
|
+
// and changes the email. Pre-fix the order would have bumped
|
|
148
|
+
// fence#customerId:C1, falsely conflicting this append.
|
|
149
|
+
let customerQuery: Reventless.DcbTag.query = [{tags: [customerTag]}]
|
|
150
|
+
let customerAfter = await readAfter(table, customerQuery)
|
|
151
|
+
let r = await Runtime.appendConditional(
|
|
152
|
+
table,
|
|
153
|
+
[event("EmailChanged", [customerTag])],
|
|
154
|
+
{query: customerQuery, after: ?customerAfter},
|
|
155
|
+
~partitionTag=simple("customerId"),
|
|
156
|
+
)
|
|
157
|
+
expect(isOk(r))->toBe(true)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe("DCB DynamoDb integration — optimistic concurrency primitives", () => {
|
|
162
|
+
testAsync("fresh first-writer (after=None) succeeds and seeds the fence", async () => {
|
|
163
|
+
let table = await H.freshTable()
|
|
164
|
+
let query: Reventless.DcbTag.query = [{tags: [tag("counterId", "K0")]}]
|
|
165
|
+
let r = await Runtime.appendConditional(
|
|
166
|
+
table,
|
|
167
|
+
[event("CounterStarted", [tag("counterId", "K0")])],
|
|
168
|
+
{query, after: ?None},
|
|
169
|
+
~partitionTag=simple("counterId"),
|
|
170
|
+
)
|
|
171
|
+
expect(isOk(r))->toBe(true)
|
|
172
|
+
|
|
173
|
+
// The seeded event is now readable.
|
|
174
|
+
let after = await readAfter(table, query)
|
|
175
|
+
expect(after->Option.isSome)->toBe(true)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
testAsync("two concurrent commits at the same after never both win (no duplicate)", async () => {
|
|
179
|
+
let table = await H.freshTable()
|
|
180
|
+
let counterTag = tag("counterId", "K1")
|
|
181
|
+
let _ = await seed(table, event("CounterStarted", [counterTag]), ~partitionTag=simple("counterId"))
|
|
182
|
+
|
|
183
|
+
let query: Reventless.DcbTag.query = [{tags: [counterTag]}]
|
|
184
|
+
let after = await readAfter(table, query)
|
|
185
|
+
|
|
186
|
+
let increment = () =>
|
|
187
|
+
Runtime.appendConditional(
|
|
188
|
+
table,
|
|
189
|
+
[event("CounterIncremented", [counterTag])],
|
|
190
|
+
{query, after: ?after},
|
|
191
|
+
~partitionTag=simple("counterId"),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
let results = await Promise.all([increment(), increment()])
|
|
195
|
+
let oks = results->Array.filter(isOk)->Array.length
|
|
196
|
+
let conflicts = results->Array.filter(isConflict)->Array.length
|
|
197
|
+
// Safety invariant at the raw-append layer: at most one writer commits, and
|
|
198
|
+
// every non-winner surfaces a Conflict. (DynamoDB may cancel BOTH with a
|
|
199
|
+
// mutual TransactionConflict — there is no retry here; the slice callback's
|
|
200
|
+
// 3-retry loop is what turns this into a guaranteed single winner in prod.)
|
|
201
|
+
expect(oks <= 1)->toBe(true)
|
|
202
|
+
expect(oks + conflicts)->toBe(2)
|
|
203
|
+
expect(conflicts >= 1)->toBe(true)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
testAsync("two concurrent first-writers to the same entity — exactly one creates it (Issue 2)", async () => {
|
|
207
|
+
let table = await H.freshTable()
|
|
208
|
+
// Both writers read an empty decision model (after=None) and try to create
|
|
209
|
+
// the same order. The per-(eventType, partition) create guard must let only
|
|
210
|
+
// one through.
|
|
211
|
+
let create = () =>
|
|
212
|
+
Runtime.appendConditional(
|
|
213
|
+
table,
|
|
214
|
+
[event("OrderCreated", [tag("orderId", "O1")])],
|
|
215
|
+
{query: [{tags: [tag("orderId", "O1")]}], after: ?None},
|
|
216
|
+
~partitionTag=simple("orderId"),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
let results = await Promise.all([create(), create()])
|
|
220
|
+
let oks = results->Array.filter(isOk)->Array.length
|
|
221
|
+
// Safety: the two first-writers must NOT both create the entity. At most one
|
|
222
|
+
// commits; every non-winner surfaces a Conflict. (DynamoDB may cancel both
|
|
223
|
+
// with a mutual TransactionConflict — the slice callback's retry loop then
|
|
224
|
+
// makes exactly one win. Pre-fix this asserted-2: both committed = duplicate.)
|
|
225
|
+
expect(oks <= 1)->toBe(true)
|
|
226
|
+
expect(oks + results->Array.filter(isConflict)->Array.length)->toBe(2)
|
|
227
|
+
|
|
228
|
+
// No duplicate landed — persisted OrderCreated count matches the winners.
|
|
229
|
+
let readResult = await Runtime.read(table)(~query=[{tags: [tag("orderId", "O1")]}])
|
|
230
|
+
expect(readResult.events->Array.length)->toBe(oks)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
testAsync("the create guard does not false-conflict a subset-event-type writer (Issue 4)", async () => {
|
|
234
|
+
let table = await H.freshTable()
|
|
235
|
+
// A productId partition already holds a ProductAdded (its own create guard +
|
|
236
|
+
// fence). A different slice reads only NameChanged on that partition, sees
|
|
237
|
+
// nothing (after=None), and appends the first NameChanged. Its guard is
|
|
238
|
+
// create#NameChanged#productId:P — distinct from ProductAdded's — so it must
|
|
239
|
+
// NOT conflict with the pre-existing partition state.
|
|
240
|
+
let productTag = tag("productId", "P")
|
|
241
|
+
let r1 = await Runtime.appendConditional(
|
|
242
|
+
table,
|
|
243
|
+
[event("ProductAdded", [productTag])],
|
|
244
|
+
{query: [{tags: [productTag]}], after: ?None},
|
|
245
|
+
~partitionTag=simple("productId"),
|
|
246
|
+
)
|
|
247
|
+
expect(isOk(r1))->toBe(true)
|
|
248
|
+
|
|
249
|
+
let r2 = await Runtime.appendConditional(
|
|
250
|
+
table,
|
|
251
|
+
[event("NameChanged", [productTag])],
|
|
252
|
+
{query: [{tags: [productTag]}], after: ?None},
|
|
253
|
+
~partitionTag=simple("productId"),
|
|
254
|
+
)
|
|
255
|
+
expect(isOk(r2))->toBe(true)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
testAsync("a chain of compatible commits all succeed", async () => {
|
|
259
|
+
let table = await H.freshTable()
|
|
260
|
+
let counterTag = tag("counterId", "K2")
|
|
261
|
+
let _ = await seed(table, event("CounterStarted", [counterTag]), ~partitionTag=simple("counterId"))
|
|
262
|
+
let query: Reventless.DcbTag.query = [{tags: [counterTag]}]
|
|
263
|
+
|
|
264
|
+
let step = async () => {
|
|
265
|
+
let after = await readAfter(table, query)
|
|
266
|
+
await Runtime.appendConditional(
|
|
267
|
+
table,
|
|
268
|
+
[event("CounterIncremented", [counterTag])],
|
|
269
|
+
{query, after: ?after},
|
|
270
|
+
~partitionTag=simple("counterId"),
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let r1 = await step()
|
|
275
|
+
let r2 = await step()
|
|
276
|
+
let r3 = await step()
|
|
277
|
+
expect([r1, r2, r3]->Array.every(isOk))->toBe(true)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
testAsync("a failed fence condition aborts the whole transaction (multi-tag atomicity)", async () => {
|
|
281
|
+
let table = await H.freshTable()
|
|
282
|
+
// Composite slice: one multi-tag clause → both tags get check+bump.
|
|
283
|
+
let tags = [tag("productId", "p1"), tag("orderId", "o1")]
|
|
284
|
+
let query: Reventless.DcbTag.query = [{tags: tags}]
|
|
285
|
+
|
|
286
|
+
// Seed one event so the next append rides after=Some.
|
|
287
|
+
let seedRes = await Runtime.appendConditional(
|
|
288
|
+
table,
|
|
289
|
+
[event("ProductDemandRecorded", tags)],
|
|
290
|
+
{query, after: ?None},
|
|
291
|
+
~partitionTag=simple("productId"),
|
|
292
|
+
)
|
|
293
|
+
expect(isOk(seedRes))->toBe(true)
|
|
294
|
+
|
|
295
|
+
let after = (await readAfter(table, query))->Option.getOr("")
|
|
296
|
+
expect(after == "")->toBe(false)
|
|
297
|
+
|
|
298
|
+
// A concurrent writer advances ONLY the productId fence past `after`.
|
|
299
|
+
// Appending "z" keeps the position lexically greater than any real position.
|
|
300
|
+
let _ = await H.setFence(table, tag("productId", "p1"), ~lastPosition=after ++ "z")
|
|
301
|
+
|
|
302
|
+
let r = await Runtime.appendConditional(
|
|
303
|
+
table,
|
|
304
|
+
[event("ProductDemandRecorded", tags)],
|
|
305
|
+
{query, after: ?Some(after)},
|
|
306
|
+
~partitionTag=simple("productId"),
|
|
307
|
+
)
|
|
308
|
+
expect(isConflict(r))->toBe(true)
|
|
309
|
+
|
|
310
|
+
// Atomic rollback: the second event was NOT written — the composite read
|
|
311
|
+
// still returns exactly the one seeded event.
|
|
312
|
+
let readResult = await Runtime.read(table)(~query)
|
|
313
|
+
expect(readResult.events->Array.length)->toBe(1)
|
|
314
|
+
})
|
|
315
|
+
})
|