@reventlessdev/reventless-local 3.0.0-alpha.113
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 +1197 -0
- package/LICENSE +202 -0
- package/__mocks__/emptyModule.js +2 -0
- package/__mocks__/nodeSqlite.mjs +6 -0
- package/package.json +67 -0
- package/rescript.json +39 -0
- package/src/Platform.res +2162 -0
- package/src/Platform.res.mjs +3389 -0
- package/src/adapter/Api/LocalGraphQL_Adapter.res +29 -0
- package/src/adapter/Api/LocalGraphQL_Adapter.res.mjs +26 -0
- package/src/adapter/Api/LocalGraphQL_SubscriptionResolvers.res +146 -0
- package/src/adapter/Api/LocalGraphQL_SubscriptionResolvers.res.mjs +88 -0
- package/src/adapter/Auth/Auth_GraphqlContext.res +83 -0
- package/src/adapter/Auth/Auth_GraphqlContext.res.mjs +76 -0
- package/src/adapter/Auth/LocalAuth.res +239 -0
- package/src/adapter/Auth/LocalAuth.res.mjs +247 -0
- package/src/adapter/Auth/UserStore.res +134 -0
- package/src/adapter/Auth/UserStore.res.mjs +146 -0
- package/src/adapter/Backend.res +46 -0
- package/src/adapter/Backend.res.mjs +66 -0
- package/src/adapter/BackendState.res +34 -0
- package/src/adapter/BackendState.res.mjs +42 -0
- package/src/adapter/Cloner/LocalClonerRunner.res +13 -0
- package/src/adapter/Cloner/LocalClonerRunner.res.mjs +12 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +329 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +301 -0
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +76 -0
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res.mjs +57 -0
- package/src/adapter/CommandGenerator/LocalCommandGeneratorResolvers.res +19 -0
- package/src/adapter/CommandGenerator/LocalCommandGeneratorResolvers.res.mjs +19 -0
- package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +104 -0
- package/src/adapter/CommandTopic/LocalCommandTopicChannel.res.mjs +82 -0
- package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +32 -0
- package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res.mjs +38 -0
- package/src/adapter/Counter/LocalCounterHandler.res +42 -0
- package/src/adapter/Counter/LocalCounterHandler.res.mjs +45 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +131 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +140 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +334 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +299 -0
- package/src/adapter/DomainGraphQL_Server.res +565 -0
- package/src/adapter/DomainGraphQL_Server.res.mjs +576 -0
- package/src/adapter/DomainMCP_Server.res +369 -0
- package/src/adapter/DomainMCP_Server.res.mjs +358 -0
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +87 -0
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res.mjs +45 -0
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +105 -0
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +117 -0
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +145 -0
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +155 -0
- package/src/adapter/EventTopic/LocalEventTopicPublisher.res +36 -0
- package/src/adapter/EventTopic/LocalEventTopicPublisher.res.mjs +31 -0
- package/src/adapter/GraphQL_Server.res +6 -0
- package/src/adapter/GraphQL_Server.res.mjs +154 -0
- package/src/adapter/GraphQL_ServerInstance.res +310 -0
- package/src/adapter/GraphQL_ServerInstance.res.mjs +257 -0
- package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +57 -0
- package/src/adapter/Heartbeat/LocalHeartbeatRunner.res.mjs +34 -0
- package/src/adapter/LocalBus.res +498 -0
- package/src/adapter/LocalBus.res.mjs +1011 -0
- package/src/adapter/LocalPluginSpec.res +16 -0
- package/src/adapter/LocalPluginSpec.res.mjs +33 -0
- package/src/adapter/LocalSideEffectHandler.res +44 -0
- package/src/adapter/LocalSideEffectHandler.res.mjs +34 -0
- package/src/adapter/MCP_Server.res +4 -0
- package/src/adapter/MCP_Server.res.mjs +67 -0
- package/src/adapter/MCP_ServerInstance.res +362 -0
- package/src/adapter/MCP_ServerInstance.res.mjs +313 -0
- package/src/adapter/PlatformGraphQL_Server.res +75 -0
- package/src/adapter/PlatformGraphQL_Server.res.mjs +84 -0
- package/src/adapter/PlatformMCP_Server.res +23 -0
- package/src/adapter/PlatformMCP_Server.res.mjs +46 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +793 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +569 -0
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +242 -0
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +241 -0
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +339 -0
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +318 -0
- package/src/adapter/QueryDb/SortKey_Filter.res +48 -0
- package/src/adapter/QueryDb/SortKey_Filter.res.mjs +46 -0
- package/src/adapter/QueryEngine/LocalQueryEngine.res +57 -0
- package/src/adapter/QueryEngine/LocalQueryEngine.res.mjs +56 -0
- package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +91 -0
- package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res.mjs +50 -0
- package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +47 -0
- package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res.mjs +37 -0
- package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +11 -0
- package/src/adapter/Runtime/LocalPluginRuntime_Builder.res.mjs +30 -0
- package/src/adapter/Runtime/LocalRuntimeEnvironment.res +54 -0
- package/src/adapter/Runtime/LocalRuntimeEnvironment.res.mjs +40 -0
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +102 -0
- package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +84 -0
- package/src/adapter/SqliteDriver.res +52 -0
- package/src/adapter/SqliteDriver.res.mjs +76 -0
- package/src/adapter/SqliteDriver.resi +31 -0
- package/src/adapter/Task/LocalTaskBucket.res +75 -0
- package/src/adapter/Task/LocalTaskBucket.res.mjs +48 -0
- package/src/adapter/Task/TaskBucket_Sqlite.res +47 -0
- package/src/adapter/Task/TaskBucket_Sqlite.res.mjs +40 -0
- package/src/components/Aggregate_Builder.res +55 -0
- package/src/components/Aggregate_Builder.res.mjs +140 -0
- package/src/components/AutomationSlice_Builder.res +41 -0
- package/src/components/AutomationSlice_Builder.res.mjs +57 -0
- package/src/components/Counter_Builder.res +14 -0
- package/src/components/Counter_Builder.res.mjs +32 -0
- package/src/components/DcbEventLog_Builder.res +18 -0
- package/src/components/DcbEventLog_Builder.res.mjs +24 -0
- package/src/components/ExtensionPoint_Builder.res +25 -0
- package/src/components/ExtensionPoint_Builder.res.mjs +44 -0
- package/src/components/InboundTranslationSlice_Builder.res +34 -0
- package/src/components/InboundTranslationSlice_Builder.res.mjs +45 -0
- package/src/components/OutboundTranslationSlice_Builder.res +43 -0
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +59 -0
- package/src/components/Plugin_Builder.res +37 -0
- package/src/components/Plugin_Builder.res.mjs +67 -0
- package/src/components/ReadModel_Builder.res +47 -0
- package/src/components/ReadModel_Builder.res.mjs +54 -0
- package/src/components/StateChangeSlice_Builder.res +19 -0
- package/src/components/StateChangeSlice_Builder.res.mjs +21 -0
- package/src/components/StateViewSlice_Builder.res +42 -0
- package/src/components/StateViewSlice_Builder.res.mjs +58 -0
- package/src/components/Task_Builder.res +28 -0
- package/src/components/Task_Builder.res.mjs +41 -0
- package/src/reexport/CommandTopic.res +3 -0
- package/src/reexport/CommandTopic.res.mjs +55 -0
- package/src/test/Mocks/MockDcbEventLogStorage.res +136 -0
- package/src/test/Mocks/MockDcbEventLogStorage.res.mjs +146 -0
- package/src/test/Mocks/MockEventLogStorage.res +57 -0
- package/src/test/Mocks/MockEventLogStorage.res.mjs +65 -0
- package/src/test/Mocks/MockPublisher.res +29 -0
- package/src/test/Mocks/MockPublisher.res.mjs +28 -0
- package/src/test/Mocks/MockQueryDbStorage.res +104 -0
- package/src/test/Mocks/MockQueryDbStorage.res.mjs +136 -0
- package/src/test/TestRunner.res +96 -0
- package/src/test/TestRunner.res.mjs +71 -0
- package/tests/SplitApiFixtures.res +84 -0
- package/tests/SplitApiFixtures.res.mjs +88 -0
- package/tests/SplitApiTest.res +102 -0
- package/tests/SplitApiTest.res.mjs +82 -0
- package/tests/TestFixtures.res +54 -0
- package/tests/TestFixtures.res.mjs +65 -0
- package/tests/adapter/BackendParityTest.res +94 -0
- package/tests/adapter/BackendParityTest.res.mjs +96 -0
- package/tests/adapter/CollectNEventsTest.res +82 -0
- package/tests/adapter/CollectNEventsTest.res.mjs +83 -0
- package/tests/adapter/CommandAuthorizationTest.res +200 -0
- package/tests/adapter/CommandAuthorizationTest.res.mjs +190 -0
- package/tests/adapter/CommandTopicChannelTest.res +174 -0
- package/tests/adapter/CommandTopicChannelTest.res.mjs +170 -0
- package/tests/adapter/CounterHandlerTest.res +113 -0
- package/tests/adapter/CounterHandlerTest.res.mjs +96 -0
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +157 -0
- package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +165 -0
- package/tests/adapter/DcbEventLogStorageTest.res +187 -0
- package/tests/adapter/DcbEventLogStorageTest.res.mjs +195 -0
- package/tests/adapter/EventCollectorChannelTest.res +166 -0
- package/tests/adapter/EventCollectorChannelTest.res.mjs +178 -0
- package/tests/adapter/EventLogStorageSqliteTest.res +105 -0
- package/tests/adapter/EventLogStorageSqliteTest.res.mjs +116 -0
- package/tests/adapter/EventTapTest.res +102 -0
- package/tests/adapter/EventTapTest.res.mjs +94 -0
- package/tests/adapter/EventTopicPublisherTest.res +76 -0
- package/tests/adapter/EventTopicPublisherTest.res.mjs +86 -0
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +763 -0
- package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +578 -0
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res +161 -0
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res.mjs +134 -0
- package/tests/adapter/HeartbeatRunnerTest.res +136 -0
- package/tests/adapter/HeartbeatRunnerTest.res.mjs +96 -0
- package/tests/adapter/LocalAuthHttpTest.res +224 -0
- package/tests/adapter/LocalAuthHttpTest.res.mjs +237 -0
- package/tests/adapter/LocalAuthLoginTest.res +180 -0
- package/tests/adapter/LocalAuthLoginTest.res.mjs +207 -0
- package/tests/adapter/LocalAuthTest.res +106 -0
- package/tests/adapter/LocalAuthTest.res.mjs +145 -0
- package/tests/adapter/LocalAuthUserStoreTest.res +172 -0
- package/tests/adapter/LocalAuthUserStoreTest.res.mjs +208 -0
- package/tests/adapter/LocalBusBoundedTest.res +157 -0
- package/tests/adapter/LocalBusBoundedTest.res.mjs +168 -0
- package/tests/adapter/LocalBusPubSubTest.res +81 -0
- package/tests/adapter/LocalBusPubSubTest.res.mjs +81 -0
- package/tests/adapter/QueryDbGsiTtlTest.res +268 -0
- package/tests/adapter/QueryDbGsiTtlTest.res.mjs +209 -0
- package/tests/adapter/QueryDbListResolverTest.res +361 -0
- package/tests/adapter/QueryDbListResolverTest.res.mjs +426 -0
- package/tests/adapter/QueryDbStorageSqliteTest.res +159 -0
- package/tests/adapter/QueryDbStorageSqliteTest.res.mjs +126 -0
- package/tests/adapter/QueryDbStorageTest.res +171 -0
- package/tests/adapter/QueryDbStorageTest.res.mjs +170 -0
- package/tests/adapter/QueryEngineTest.res +152 -0
- package/tests/adapter/QueryEngineTest.res.mjs +148 -0
- package/tests/adapter/ScheduledPublisherTest.res +199 -0
- package/tests/adapter/ScheduledPublisherTest.res.mjs +203 -0
- package/tests/adapter/SortKey_FilterTest.res +103 -0
- package/tests/adapter/SortKey_FilterTest.res.mjs +135 -0
- package/tests/adapter/SqliteDriverTest.res +86 -0
- package/tests/adapter/SqliteDriverTest.res.mjs +101 -0
- package/tests/adapter/TaskBucketSqliteTest.res +44 -0
- package/tests/adapter/TaskBucketSqliteTest.res.mjs +40 -0
- package/tests/adapter/TaskBucketTest.res +106 -0
- package/tests/adapter/TaskBucketTest.res.mjs +118 -0
- package/tests/components/aggregate/AggregateFixtures.res +78 -0
- package/tests/components/aggregate/AggregateFixtures.res.mjs +109 -0
- package/tests/components/aggregate/AggregateTest.res +129 -0
- package/tests/components/aggregate/AggregateTest.res.mjs +129 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +212 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +233 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res +189 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +276 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +289 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +453 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +47 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +31 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +213 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +268 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +118 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceTest.res.mjs +99 -0
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res +73 -0
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res.mjs +109 -0
- package/tests/components/commandgenerator/CommandGeneratorTest.res +43 -0
- package/tests/components/commandgenerator/CommandGeneratorTest.res.mjs +46 -0
- package/tests/components/commandtopic/CommandTopicFixtures.res +37 -0
- package/tests/components/commandtopic/CommandTopicFixtures.res.mjs +68 -0
- package/tests/components/commandtopic/CommandTopicStreamFixtures.res +43 -0
- package/tests/components/commandtopic/CommandTopicStreamFixtures.res.mjs +91 -0
- package/tests/components/commandtopic/CommandTopicStreamTest.res +30 -0
- package/tests/components/commandtopic/CommandTopicStreamTest.res.mjs +35 -0
- package/tests/components/commandtopic/CommandTopicTest.res +55 -0
- package/tests/components/commandtopic/CommandTopicTest.res.mjs +62 -0
- package/tests/components/counter/CounterFixtures.res +42 -0
- package/tests/components/counter/CounterFixtures.res.mjs +34 -0
- package/tests/components/counter/CounterTest.res +46 -0
- package/tests/components/counter/CounterTest.res.mjs +61 -0
- package/tests/components/dcb/DcbCrossPartitionFixtures.res +170 -0
- package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +276 -0
- package/tests/components/dcb/DcbCrossPartitionReadTest.res +68 -0
- package/tests/components/dcb/DcbCrossPartitionReadTest.res.mjs +78 -0
- package/tests/components/dcb/DcbEventLogAppendStreamTest.res +72 -0
- package/tests/components/dcb/DcbEventLogAppendStreamTest.res.mjs +82 -0
- package/tests/components/dcb/DcbEventLogStreamTest.res +103 -0
- package/tests/components/dcb/DcbEventLogStreamTest.res.mjs +91 -0
- package/tests/components/dcb/DcbFixtures.res +145 -0
- package/tests/components/dcb/DcbFixtures.res.mjs +221 -0
- package/tests/components/dcb/DcbTest.res +55 -0
- package/tests/components/dcb/DcbTest.res.mjs +55 -0
- package/tests/components/eventcollector/EventCollectorFixtures.res +42 -0
- package/tests/components/eventcollector/EventCollectorFixtures.res.mjs +49 -0
- package/tests/components/eventcollector/EventCollectorTest.res +37 -0
- package/tests/components/eventcollector/EventCollectorTest.res.mjs +36 -0
- package/tests/components/eventlog/EventLogAppendStreamTest.res +93 -0
- package/tests/components/eventlog/EventLogAppendStreamTest.res.mjs +113 -0
- package/tests/components/eventlog/EventLogConflictTest.res +86 -0
- package/tests/components/eventlog/EventLogConflictTest.res.mjs +81 -0
- package/tests/components/eventlog/EventLogFixtures.res +52 -0
- package/tests/components/eventlog/EventLogFixtures.res.mjs +80 -0
- package/tests/components/eventlog/EventLogStreamTest.res +98 -0
- package/tests/components/eventlog/EventLogStreamTest.res.mjs +142 -0
- package/tests/components/eventlog/EventLogTest.res +53 -0
- package/tests/components/eventlog/EventLogTest.res.mjs +74 -0
- package/tests/components/eventtopic/EventTopicFixtures.res +47 -0
- package/tests/components/eventtopic/EventTopicFixtures.res.mjs +77 -0
- package/tests/components/eventtopic/EventTopicStreamFixtures.res +27 -0
- package/tests/components/eventtopic/EventTopicStreamFixtures.res.mjs +62 -0
- package/tests/components/eventtopic/EventTopicStreamTest.res +35 -0
- package/tests/components/eventtopic/EventTopicStreamTest.res.mjs +44 -0
- package/tests/components/eventtopic/EventTopicTest.res +35 -0
- package/tests/components/eventtopic/EventTopicTest.res.mjs +43 -0
- package/tests/components/extensionpoint/ExtensionPointFixtures.res +145 -0
- package/tests/components/extensionpoint/ExtensionPointFixtures.res.mjs +188 -0
- package/tests/components/extensionpoint/ExtensionPointTest.res +62 -0
- package/tests/components/extensionpoint/ExtensionPointTest.res.mjs +55 -0
- package/tests/components/heartbeat/HeartbeatFixtures.res +48 -0
- package/tests/components/heartbeat/HeartbeatFixtures.res.mjs +46 -0
- package/tests/components/heartbeat/HeartbeatTest.res +67 -0
- package/tests/components/heartbeat/HeartbeatTest.res.mjs +65 -0
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +241 -0
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs +239 -0
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +36 -0
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs +73 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +191 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +297 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +78 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res.mjs +137 -0
- package/tests/components/querydb/QueryDbFixtures.res +39 -0
- package/tests/components/querydb/QueryDbFixtures.res.mjs +73 -0
- package/tests/components/querydb/QueryDbSubIdFixtures.res +44 -0
- package/tests/components/querydb/QueryDbSubIdFixtures.res.mjs +80 -0
- package/tests/components/querydb/QueryDbSubIdTest.res +104 -0
- package/tests/components/querydb/QueryDbSubIdTest.res.mjs +104 -0
- package/tests/components/querydb/QueryDbTest.res +81 -0
- package/tests/components/querydb/QueryDbTest.res.mjs +126 -0
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res +215 -0
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res.mjs +308 -0
- package/tests/components/readmodel/DcbReadModelE2ETest.res +45 -0
- package/tests/components/readmodel/DcbReadModelE2ETest.res.mjs +37 -0
- package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res +88 -0
- package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res.mjs +152 -0
- package/tests/components/readmodel/MixedSourceReadModelFixtures.res +181 -0
- package/tests/components/readmodel/MixedSourceReadModelFixtures.res.mjs +277 -0
- package/tests/components/readmodel/MixedSourceReadModelTest.res +61 -0
- package/tests/components/readmodel/MixedSourceReadModelTest.res.mjs +68 -0
- package/tests/components/readmodel/ReadModelFixtures.res +138 -0
- package/tests/components/readmodel/ReadModelFixtures.res.mjs +169 -0
- package/tests/components/readmodel/ReadModelTest.res +46 -0
- package/tests/components/readmodel/ReadModelTest.res.mjs +36 -0
- package/tests/components/scheduler/SchedulerFixtures.res +59 -0
- package/tests/components/scheduler/SchedulerFixtures.res.mjs +63 -0
- package/tests/components/scheduler/SchedulerTest.res +76 -0
- package/tests/components/scheduler/SchedulerTest.res.mjs +86 -0
- package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +133 -0
- package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res.mjs +149 -0
- package/tests/components/sideeffecthandler/SideEffectHandlerTest.res +39 -0
- package/tests/components/sideeffecthandler/SideEffectHandlerTest.res.mjs +28 -0
- package/tests/components/stateviewslice/StateViewSliceFixtures.res +117 -0
- package/tests/components/stateviewslice/StateViewSliceFixtures.res.mjs +188 -0
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +117 -0
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res.mjs +183 -0
- package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +78 -0
- package/tests/components/stateviewslice/StateViewSliceSubIdTest.res.mjs +107 -0
- package/tests/components/stateviewslice/StateViewSliceTest.res +59 -0
- package/tests/components/stateviewslice/StateViewSliceTest.res.mjs +72 -0
- package/tests/components/task/TaskFixtures.res +87 -0
- package/tests/components/task/TaskFixtures.res.mjs +82 -0
- package/tests/components/task/TaskTest.res +98 -0
- package/tests/components/task/TaskTest.res.mjs +46 -0
- package/tests/plugin/PluginBehavior_GWT.res +146 -0
- package/tests/plugin/PluginBehavior_GWT.res.mjs +281 -0
- package/tests/plugin/Plugin_Fixtures.res +61 -0
- package/tests/plugin/Plugin_Fixtures.res.mjs +105 -0
- package/tests/plugin/PluginsProjection_GWT.res +95 -0
- package/tests/plugin/PluginsProjection_GWT.res.mjs +120 -0
- package/tests/plugin/UIFragmentRegistryProjection_GWT.res +72 -0
- package/tests/plugin/UIFragmentRegistryProjection_GWT.res.mjs +117 -0
- package/tests/setup/sqliteGlobal.cjs +12 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// LocalGraphQL_Adapter — implements ReventlessInfra.Api_Adapter.Provider for
|
|
2
|
+
// the in-memory graphql-yoga server.
|
|
3
|
+
//
|
|
4
|
+
// makeApiResource: no-op (server lifecycle is managed by DomainGraphQL_Server.start/rebuildSchema)
|
|
5
|
+
// generateFragment: delegates to ReventlessCore.GraphQL_FragmentGenerator
|
|
6
|
+
// updateSchema: rebuilds the stitched SDL via DomainGraphQL_Server.rebuildSchema
|
|
7
|
+
|
|
8
|
+
type api = unit
|
|
9
|
+
type role = unit
|
|
10
|
+
|
|
11
|
+
let makeApiResource = (
|
|
12
|
+
~name as _: string,
|
|
13
|
+
~opts as _: Pulumi.ComponentResource.options,
|
|
14
|
+
): (Pulumi.Output.t<api>, Pulumi.Output.t<role>) => (Pulumi.Output.make(()), Pulumi.Output.make(()))
|
|
15
|
+
|
|
16
|
+
let generateFragment = (
|
|
17
|
+
~mutationEntries: array<ReventlessInfra.Api.mutationSchemaEntry>,
|
|
18
|
+
~queryEntries: array<ReventlessInfra.Api.querySchemaEntry>,
|
|
19
|
+
): Reventless.Plugin.apiSchemaFragment =>
|
|
20
|
+
ReventlessCore.GraphQL_FragmentGenerator.generate(~mutationEntries, ~queryEntries)
|
|
21
|
+
|
|
22
|
+
let updateSchema = (
|
|
23
|
+
~api as _: Pulumi.Output.t<api>,
|
|
24
|
+
~baseFragment: Reventless.Plugin.apiSchemaFragment,
|
|
25
|
+
~pluginFragments: array<Reventless.Plugin.apiSchemaFragment>,
|
|
26
|
+
): promise<unit> => {
|
|
27
|
+
DomainGraphQL_Server.rebuildSchema(~baseFragment, ~pluginFragments)
|
|
28
|
+
Promise.resolve()
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
4
|
+
import * as DomainGraphQL_Server$ReventlessLocal from "../DomainGraphQL_Server.res.mjs";
|
|
5
|
+
import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
|
|
6
|
+
|
|
7
|
+
function makeApiResource(param, param$1) {
|
|
8
|
+
return [
|
|
9
|
+
Pulumi.output(),
|
|
10
|
+
Pulumi.output()
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let generateFragment = GraphQL_FragmentGenerator$ReventlessCore.generate;
|
|
15
|
+
|
|
16
|
+
function updateSchema(param, baseFragment, pluginFragments) {
|
|
17
|
+
DomainGraphQL_Server$ReventlessLocal.rebuildSchema(baseFragment, pluginFragments);
|
|
18
|
+
return Promise.resolve();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
makeApiResource,
|
|
23
|
+
generateFragment,
|
|
24
|
+
updateSchema,
|
|
25
|
+
}
|
|
26
|
+
/* @pulumi/pulumi Not a pure module */
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// LocalGraphQL_SubscriptionResolvers.res
|
|
2
|
+
// WebSocket subscription resolvers for Sources A and B (in-memory / yoga).
|
|
3
|
+
//
|
|
4
|
+
// Source A (raw event stream): subscribes to Bus.subscribeToEvents for each event log
|
|
5
|
+
// topic and publishes the event envelope to a yoga PubSub channel.
|
|
6
|
+
//
|
|
7
|
+
// Source B (state changes): subscribes to Bus.subscribeToStateChanges for each QueryDb
|
|
8
|
+
// and publishes saved state items to a yoga PubSub channel.
|
|
9
|
+
//
|
|
10
|
+
// Source C (mutation-triggered): mutation resolvers would need to publish to the PubSub
|
|
11
|
+
// after each successful command. This is left as future work — currently mutations do
|
|
12
|
+
// not cross-post to the subscription PubSub in-memory.
|
|
13
|
+
//
|
|
14
|
+
// Usage: call `setup` once per server build, then `registerAll` to wire into the GraphQL
|
|
15
|
+
// server instance. Both must be called before the server starts.
|
|
16
|
+
//
|
|
17
|
+
// Note: `scalar AWSJSON` is injected here so yoga accepts the AppSync scalar used in
|
|
18
|
+
// Source A event log types. It serialises as a plain string.
|
|
19
|
+
|
|
20
|
+
module YG = GraphqlYoga
|
|
21
|
+
|
|
22
|
+
// ── PubSub instance ───────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
let pubSub: ref<option<YG.pubSub>> = ref(None)
|
|
25
|
+
|
|
26
|
+
let getPubSub = () =>
|
|
27
|
+
switch pubSub.contents {
|
|
28
|
+
| Some(ps) => ps
|
|
29
|
+
| None =>
|
|
30
|
+
let ps = YG.createPubSub()
|
|
31
|
+
pubSub.contents = Some(ps)
|
|
32
|
+
ps
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Topic naming ──────────────────────────────────────────────────────────────
|
|
36
|
+
// Matches the field names generated by Plugin_SubscriptionSchema:
|
|
37
|
+
// Source A: on{DisplayName}EventLog_eventAppended
|
|
38
|
+
// Source B: on{ReturnTypeName}_stateChanged
|
|
39
|
+
|
|
40
|
+
let sourceATopic = (displayName: string) => `on${displayName}EventLog_eventAppended`
|
|
41
|
+
let sourceBTopic = (returnTypeName: string) => `on${returnTypeName}_stateChanged`
|
|
42
|
+
|
|
43
|
+
// ── Bridge: Bus → PubSub ──────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/** Wire Source A: subscribe to event bus topic → publish to yoga PubSub channel. */
|
|
46
|
+
let bridgeSourceA = (
|
|
47
|
+
~subscribeToEvents: (
|
|
48
|
+
string,
|
|
49
|
+
(string, ReventlessCore.Message.meta, JSON.t) => promise<unit>,
|
|
50
|
+
) => unit,
|
|
51
|
+
~displayName: string,
|
|
52
|
+
~busTopicName: string,
|
|
53
|
+
) => {
|
|
54
|
+
let ps = getPubSub()
|
|
55
|
+
let yogaTopic = sourceATopic(displayName)
|
|
56
|
+
// Push the raw event JSON directly — it already contains the event data.
|
|
57
|
+
// In-memory events don't carry a separate position/originatorSlice envelope.
|
|
58
|
+
subscribeToEvents(busTopicName, async (_service, _meta, json) => {
|
|
59
|
+
ps->YG.pubSubPublish(yogaTopic, json)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Wire Source B: subscribe to state-change hook → publish to yoga PubSub channel. */
|
|
64
|
+
let bridgeSourceB = (
|
|
65
|
+
~subscribeToStateChanges: (string, JSON.t => unit) => unit,
|
|
66
|
+
~readModelName: string,
|
|
67
|
+
~returnTypeName: string,
|
|
68
|
+
) => {
|
|
69
|
+
let ps = getPubSub()
|
|
70
|
+
let yogaTopic = sourceBTopic(returnTypeName)
|
|
71
|
+
subscribeToStateChanges(readModelName, state => {
|
|
72
|
+
ps->YG.pubSubPublish(yogaTopic, state)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Subscription resolver builder ─────────────────────────────────────────────
|
|
77
|
+
//
|
|
78
|
+
// Builds the yoga subscription resolver object for one field:
|
|
79
|
+
// { subscribe: (_root, _args) => pubSub.subscribe(topic), resolve: x => x }
|
|
80
|
+
//
|
|
81
|
+
// The resolver dict value is typed as JSON.t (the generic resolverFn) and cast via
|
|
82
|
+
// Obj.magic — yoga's createSchema accepts { subscribe, resolve } objects for the
|
|
83
|
+
// Subscription type, not plain functions. This is safe at runtime.
|
|
84
|
+
|
|
85
|
+
let makeFieldResolver = (topic: string): YG.resolverFn => {
|
|
86
|
+
let ps = getPubSub()
|
|
87
|
+
Obj.magic({
|
|
88
|
+
"subscribe": (_root: JSON.t, _args: JSON.t, _ctx: JSON.t) => ps->YG.pubSubSubscribe(topic),
|
|
89
|
+
"resolve": (payload: JSON.t) => payload,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
type subscriptionEntry = {
|
|
96
|
+
fieldName: string,
|
|
97
|
+
topic: string,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Register subscriptions with the GraphQL server instance.
|
|
102
|
+
*
|
|
103
|
+
* `sourceAEntries`: list of {fieldName, topic} for Source A (event stream).
|
|
104
|
+
* Bridge must already be wired via bridgeSourceA before calling this.
|
|
105
|
+
*
|
|
106
|
+
* `sourceBEntries`: list of {fieldName, topic} for Source B (state changes).
|
|
107
|
+
* Bridge must already be wired via bridgeSourceB before calling this.
|
|
108
|
+
*
|
|
109
|
+
* `sdlFields`: the `subscriptions` SDL field strings from the plugin fragment
|
|
110
|
+
* (after stripping `@aws_subscribe` directives — not valid in yoga).
|
|
111
|
+
*/
|
|
112
|
+
let registerAll = (
|
|
113
|
+
~server: GraphQL_ServerInstance.t,
|
|
114
|
+
~sdlFields: array<string>,
|
|
115
|
+
~sourceAEntries: array<subscriptionEntry>,
|
|
116
|
+
~sourceBEntries: array<subscriptionEntry>,
|
|
117
|
+
) => {
|
|
118
|
+
// Strip @aws_subscribe directives — only valid in AppSync, not yoga
|
|
119
|
+
let cleanedFields =
|
|
120
|
+
sdlFields->Array.map(field =>
|
|
121
|
+
field->String.split("\n")->Array.filter(line => !(line->String.includes("@aws_subscribe")))->Array.join("\n")
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
let resolvers: dict<YG.resolverFn> = Dict.make()
|
|
125
|
+
|
|
126
|
+
sourceAEntries->Array.forEach(({fieldName, topic}) => {
|
|
127
|
+
resolvers->Dict.set(fieldName, makeFieldResolver(topic))
|
|
128
|
+
})
|
|
129
|
+
sourceBEntries->Array.forEach(({fieldName, topic}) => {
|
|
130
|
+
resolvers->Dict.set(fieldName, makeFieldResolver(topic))
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
server.registerSubscriptions(~sdlFields=cleanedFields, ~resolvers)
|
|
134
|
+
|
|
135
|
+
// Inject scalar AWSJSON so yoga accepts it in Source A event log types
|
|
136
|
+
server.registerTypes(~sdlTypes=["scalar AWSJSON"])
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Publish a payload to a named PubSub topic (for Source C mutation resolvers). */
|
|
140
|
+
let publish = (topic: string, payload: JSON.t) => {
|
|
141
|
+
getPubSub()->YG.pubSubPublish(topic, payload)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let reset = () => {
|
|
145
|
+
pubSub.contents = None
|
|
146
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as GraphqlYoga from "graphql-yoga";
|
|
4
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
5
|
+
|
|
6
|
+
let pubSub = {
|
|
7
|
+
contents: undefined
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function getPubSub() {
|
|
11
|
+
let ps = pubSub.contents;
|
|
12
|
+
if (ps !== undefined) {
|
|
13
|
+
return Primitive_option.valFromOption(ps);
|
|
14
|
+
}
|
|
15
|
+
let ps$1 = GraphqlYoga.createPubSub();
|
|
16
|
+
pubSub.contents = Primitive_option.some(ps$1);
|
|
17
|
+
return ps$1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sourceATopic(displayName) {
|
|
21
|
+
return `on` + displayName + `EventLog_eventAppended`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sourceBTopic(returnTypeName) {
|
|
25
|
+
return `on` + returnTypeName + `_stateChanged`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function bridgeSourceA(subscribeToEvents, displayName, busTopicName) {
|
|
29
|
+
let ps = getPubSub();
|
|
30
|
+
let yogaTopic = sourceATopic(displayName);
|
|
31
|
+
subscribeToEvents(busTopicName, async (_service, _meta, json) => {
|
|
32
|
+
ps.publish(yogaTopic, json);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function bridgeSourceB(subscribeToStateChanges, readModelName, returnTypeName) {
|
|
37
|
+
let ps = getPubSub();
|
|
38
|
+
let yogaTopic = sourceBTopic(returnTypeName);
|
|
39
|
+
subscribeToStateChanges(readModelName, state => {
|
|
40
|
+
ps.publish(yogaTopic, state);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeFieldResolver(topic) {
|
|
45
|
+
let ps = getPubSub();
|
|
46
|
+
return {
|
|
47
|
+
subscribe: (_root, _args, _ctx) => ps.subscribe(topic),
|
|
48
|
+
resolve: payload => payload
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function registerAll(server, sdlFields, sourceAEntries, sourceBEntries) {
|
|
53
|
+
let cleanedFields = sdlFields.map(field => field.split("\n").filter(line => !line.includes("@aws_subscribe")).join("\n"));
|
|
54
|
+
let resolvers = {};
|
|
55
|
+
sourceAEntries.forEach(param => {
|
|
56
|
+
resolvers[param.fieldName] = makeFieldResolver(param.topic);
|
|
57
|
+
});
|
|
58
|
+
sourceBEntries.forEach(param => {
|
|
59
|
+
resolvers[param.fieldName] = makeFieldResolver(param.topic);
|
|
60
|
+
});
|
|
61
|
+
server.registerSubscriptions(cleanedFields, resolvers);
|
|
62
|
+
server.registerTypes(["scalar AWSJSON"]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function publish(topic, payload) {
|
|
66
|
+
getPubSub().publish(topic, payload);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function reset() {
|
|
70
|
+
pubSub.contents = undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let YG;
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
YG,
|
|
77
|
+
pubSub,
|
|
78
|
+
getPubSub,
|
|
79
|
+
sourceATopic,
|
|
80
|
+
sourceBTopic,
|
|
81
|
+
bridgeSourceA,
|
|
82
|
+
bridgeSourceB,
|
|
83
|
+
makeFieldResolver,
|
|
84
|
+
registerAll,
|
|
85
|
+
publish,
|
|
86
|
+
reset,
|
|
87
|
+
}
|
|
88
|
+
/* graphql-yoga Not a pure module */
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Yoga context factory that translates the incoming Fetch Request into an
|
|
2
|
+
// `identity` attached to resolver context. Shared between
|
|
3
|
+
// `DomainGraphQL_Server` (data API) and `GraphQL_ServerInstance` (split-mode
|
|
4
|
+
// admin API) so both endpoints enforce identical bearer-token rules:
|
|
5
|
+
// - Valid `Authorization: Bearer <token>` → signed-in identity
|
|
6
|
+
// - Missing header → `defaultUser`
|
|
7
|
+
// - Invalid bearer is rejected at HTTP level before this runs (see
|
|
8
|
+
// `DomainGraphQL_Server._dispatch`); on the admin server there is no
|
|
9
|
+
// dispatch layer, so an invalid bearer falls through to `defaultUser`
|
|
10
|
+
// too. AppSync enforces the same `@aws_auth(cognito_groups: ["Admin"])`
|
|
11
|
+
// directive at the schema layer in production.
|
|
12
|
+
|
|
13
|
+
module YG = GraphqlYoga
|
|
14
|
+
|
|
15
|
+
type fetchHeaders
|
|
16
|
+
type fetchRequest = {headers: fetchHeaders}
|
|
17
|
+
type yogaInitialCtx = {request: fetchRequest}
|
|
18
|
+
|
|
19
|
+
@send
|
|
20
|
+
external headersForEach: (fetchHeaders, (string, string) => unit) => unit = "forEach"
|
|
21
|
+
|
|
22
|
+
let extractHeaders = (headers: fetchHeaders): dict<string> => {
|
|
23
|
+
let acc = Dict.make()
|
|
24
|
+
headers->headersForEach((value, key) => acc->Dict.set(key->String.toLowerCase, value))
|
|
25
|
+
acc
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let identityFromAuthResult = (result: Reventless.Identity.authResult): Reventless.Identity.t =>
|
|
29
|
+
switch result {
|
|
30
|
+
| Authenticated(id) => id
|
|
31
|
+
| Anonymous => Reventless.Identity.anonymous
|
|
32
|
+
| AuthError(_) => Reventless.Identity.anonymous
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let buildAuthContext = async (initial: YG.initialContext): JSON.t => {
|
|
36
|
+
let ctx: yogaInitialCtx = Obj.magic(initial)
|
|
37
|
+
let headers = extractHeaders(ctx.request.headers)
|
|
38
|
+
let requestContext: ReventlessCore.Auth_Adapter.requestContext = {headers: headers}
|
|
39
|
+
let result = await LocalAuth.authenticate(requestContext)
|
|
40
|
+
let identity = identityFromAuthResult(result)
|
|
41
|
+
Obj.magic({"identity": identity})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Read the identity attached by `buildAuthContext` from the resolver context.
|
|
45
|
+
// Falls back to `anonymous` if the context is malformed or the server wasn't
|
|
46
|
+
// started with `~contextFactory`.
|
|
47
|
+
let extractIdentity = (ctx: JSON.t): Reventless.Identity.t =>
|
|
48
|
+
try {
|
|
49
|
+
switch (ctx->Obj.magic)["identity"]->Nullable.toOption {
|
|
50
|
+
| Some(id) => (id: Reventless.Identity.t)
|
|
51
|
+
| None => Reventless.Identity.anonymous
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
| _ => Reventless.Identity.anonymous
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Throw a `GraphQLError` from the `graphql` package — yoga's `maskedErrors`
|
|
58
|
+
// option preserves these (only opaque thrown values get masked as
|
|
59
|
+
// "Unexpected error / INTERNAL_SERVER_ERROR"). Mirrors the directive-level
|
|
60
|
+
// `@aws_auth` rejection that AppSync surfaces in production.
|
|
61
|
+
@new @module("graphql")
|
|
62
|
+
external makeGraphqlError: (string, {"extensions": {"code": string}}) => exn = "GraphQLError"
|
|
63
|
+
|
|
64
|
+
let unauthorizedError = (~group: string): exn =>
|
|
65
|
+
makeGraphqlError(
|
|
66
|
+
`Unauthorized: requires group "${group}"`,
|
|
67
|
+
{"extensions": {"code": "UNAUTHORIZED"}},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// Wrap a resolver so it raises a GraphQL "Unauthorized" error when the
|
|
71
|
+
// requesting identity lacks the required group. Mirrors AppSync's
|
|
72
|
+
// `@aws_auth(cognito_groups: [...])` semantics for admin fields on the
|
|
73
|
+
// in-memory adapter. Use for fields whose corresponding read-model entry
|
|
74
|
+
// carries `authorization: Some({group, ...})` — pass the group string here.
|
|
75
|
+
let requireGroup = (~group: string, resolver: YG.resolverFn): YG.resolverFn =>
|
|
76
|
+
async (root, args, ctx) => {
|
|
77
|
+
let identity = extractIdentity(ctx)
|
|
78
|
+
if identity.groups->Array.includes(group) {
|
|
79
|
+
await resolver(root, args, ctx)
|
|
80
|
+
} else {
|
|
81
|
+
throw(unauthorizedError(~group))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Graphql from "graphql";
|
|
4
|
+
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
5
|
+
import * as LocalAuth$ReventlessLocal from "./LocalAuth.res.mjs";
|
|
6
|
+
|
|
7
|
+
function extractHeaders(headers) {
|
|
8
|
+
let acc = {};
|
|
9
|
+
headers.forEach((value, key) => {
|
|
10
|
+
acc[key.toLowerCase()] = value;
|
|
11
|
+
});
|
|
12
|
+
return acc;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function identityFromAuthResult(result) {
|
|
16
|
+
if (typeof result !== "object" || result.TAG !== "Authenticated") {
|
|
17
|
+
return Identity$Reventless.anonymous;
|
|
18
|
+
} else {
|
|
19
|
+
return result._0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function buildAuthContext(initial) {
|
|
24
|
+
let headers = extractHeaders(initial.request.headers);
|
|
25
|
+
let requestContext = {
|
|
26
|
+
headers: headers
|
|
27
|
+
};
|
|
28
|
+
let result = await LocalAuth$ReventlessLocal.authenticate(requestContext);
|
|
29
|
+
return {
|
|
30
|
+
identity: identityFromAuthResult(result)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function extractIdentity(ctx) {
|
|
35
|
+
try {
|
|
36
|
+
let id = ctx.identity;
|
|
37
|
+
if (id == null) {
|
|
38
|
+
return Identity$Reventless.anonymous;
|
|
39
|
+
} else {
|
|
40
|
+
return id;
|
|
41
|
+
}
|
|
42
|
+
} catch (exn) {
|
|
43
|
+
return Identity$Reventless.anonymous;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function unauthorizedError(group) {
|
|
48
|
+
return new Graphql.GraphQLError(`Unauthorized: requires group "` + group + `"`, {
|
|
49
|
+
extensions: {
|
|
50
|
+
code: "UNAUTHORIZED"
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function requireGroup(group, resolver) {
|
|
56
|
+
return async (root, args, ctx) => {
|
|
57
|
+
let identity = extractIdentity(ctx);
|
|
58
|
+
if (identity.groups.includes(group)) {
|
|
59
|
+
return await resolver(root, args, ctx);
|
|
60
|
+
}
|
|
61
|
+
throw unauthorizedError(group);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let YG;
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
YG,
|
|
69
|
+
extractHeaders,
|
|
70
|
+
identityFromAuthResult,
|
|
71
|
+
buildAuthContext,
|
|
72
|
+
extractIdentity,
|
|
73
|
+
unauthorizedError,
|
|
74
|
+
requireGroup,
|
|
75
|
+
}
|
|
76
|
+
/* graphql Not a pure module */
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// In-memory `Auth_Adapter.Provider` implementation. Designed for local
|
|
2
|
+
// development and integration tests.
|
|
3
|
+
//
|
|
4
|
+
// Header semantics (host-ui-login-core stages A2 + A4):
|
|
5
|
+
// Authorization: Bearer <token> → decode + HMAC-verify a Login-issued token
|
|
6
|
+
// X-User: <username> → resolve identity from the user store
|
|
7
|
+
// X-Groups: A,B,C → override the resolved identity's groups
|
|
8
|
+
// none of the above → Authenticated(defaultUser) (dev default)
|
|
9
|
+
//
|
|
10
|
+
// Stage A4 adds the `Login` submodule (token issuance + verification) and
|
|
11
|
+
// the companion `UserStore` module (YAML hydration).
|
|
12
|
+
|
|
13
|
+
open Reventless
|
|
14
|
+
|
|
15
|
+
// ── Built-in identities ────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
let defaultUser: Identity.t = {
|
|
18
|
+
userId: "local-user",
|
|
19
|
+
username: "user",
|
|
20
|
+
groups: ["User"],
|
|
21
|
+
provider: InMemory,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let adminUser: Identity.t = {
|
|
25
|
+
userId: "local-admin",
|
|
26
|
+
username: "admin",
|
|
27
|
+
groups: ["Admin", "User"],
|
|
28
|
+
provider: InMemory,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── User registry ─────────────────────────────────────────────────────────
|
|
32
|
+
//
|
|
33
|
+
// A `dict<Identity.t>` keyed by `username`. A4 hydrates this from
|
|
34
|
+
// `.reventless/users.yaml`; tests inject via `registerUser`.
|
|
35
|
+
|
|
36
|
+
let initialUsers = () =>
|
|
37
|
+
Dict.fromArray([("user", defaultUser), ("admin", adminUser)])
|
|
38
|
+
|
|
39
|
+
let users: ref<dict<Identity.t>> = ref(initialUsers())
|
|
40
|
+
|
|
41
|
+
let registerUser = (~username: string, ~identity: Identity.t): unit =>
|
|
42
|
+
users.contents->Dict.set(username, identity)
|
|
43
|
+
|
|
44
|
+
let resetUsers = (): unit => users := initialUsers()
|
|
45
|
+
|
|
46
|
+
let lookupUser = (username: string): option<Identity.t> =>
|
|
47
|
+
users.contents->Dict.get(username)
|
|
48
|
+
|
|
49
|
+
// ── Header parsing ────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
let parseGroups = (s: string): array<string> =>
|
|
52
|
+
s
|
|
53
|
+
->String.split(",")
|
|
54
|
+
->Array.map(g => g->String.trim)
|
|
55
|
+
->Array.filter(g => g !== "")
|
|
56
|
+
|
|
57
|
+
// HTTP headers are case-insensitive; the context factory in
|
|
58
|
+
// DomainGraphQL_Server normalises keys to lower-case before calling
|
|
59
|
+
// authenticate.
|
|
60
|
+
let getHeader = (headers: dict<string>, name: string): option<string> =>
|
|
61
|
+
headers->Dict.get(name->String.toLowerCase)
|
|
62
|
+
|
|
63
|
+
// ── Login (Stage A4 — token issuance) ──────────────────────────────────────
|
|
64
|
+
//
|
|
65
|
+
// Issues opaque tokens of the form `<base64url-payload>.<base64url-sig>` where
|
|
66
|
+
// payload = JSON(Identity.t) and sig = HMAC-SHA256(payload, secret). The
|
|
67
|
+
// secret is process-local (random 32-byte hex at first use) and can be pinned
|
|
68
|
+
// for tests via `Login.setTokenSecret`.
|
|
69
|
+
//
|
|
70
|
+
// Not security-grade — local dev only. AWS deployments verify Cognito-issued
|
|
71
|
+
// JWTs at AppSync (Stage D) and never see these tokens.
|
|
72
|
+
|
|
73
|
+
module Login = {
|
|
74
|
+
type credentials = {password: string, identity: Identity.t}
|
|
75
|
+
|
|
76
|
+
// Hydrated by `UserStore` at platform start; tests can inject directly.
|
|
77
|
+
let store: ref<dict<credentials>> = ref(Dict.make())
|
|
78
|
+
|
|
79
|
+
let setCredentials = (~username: string, ~password: string, ~identity: Identity.t): unit => {
|
|
80
|
+
store.contents->Dict.set(username, {password, identity})
|
|
81
|
+
// Mirror into the authenticate-side user registry so X-User flows still
|
|
82
|
+
// resolve a Login-registered user.
|
|
83
|
+
users.contents->Dict.set(username, identity)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let resetStore = (): unit => {
|
|
87
|
+
store := Dict.make()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── HMAC secret (lazy) ──
|
|
91
|
+
//
|
|
92
|
+
// Reads `REVENTLESS_INMEMORY_TOKEN_SECRET` once on first use. When the env
|
|
93
|
+
// var is set (≥16 chars) the issued tokens survive process restarts, which
|
|
94
|
+
// matches the dev loop's expectation that a logged-in tab keeps working
|
|
95
|
+
// after a backend reload. When unset, falls back to a random per-process
|
|
96
|
+
// secret — secure for ephemeral test runs but invalidates every previously
|
|
97
|
+
// issued token on every restart.
|
|
98
|
+
type buf
|
|
99
|
+
@module("node:crypto") external _randomBytes: int => buf = "randomBytes"
|
|
100
|
+
@send external _bufToString: (buf, string) => string = "toString"
|
|
101
|
+
type hmac
|
|
102
|
+
@module("node:crypto") external _createHmac: (string, string) => hmac = "createHmac"
|
|
103
|
+
@send external _update: (hmac, string) => hmac = "update"
|
|
104
|
+
@send external _digest: (hmac, string) => string = "digest"
|
|
105
|
+
|
|
106
|
+
@val external _processEnv: Dict.t<string> = "process.env"
|
|
107
|
+
|
|
108
|
+
let _secret: ref<option<string>> = ref(None)
|
|
109
|
+
let _getSecret = (): string =>
|
|
110
|
+
switch _secret.contents {
|
|
111
|
+
| Some(s) => s
|
|
112
|
+
| None =>
|
|
113
|
+
let s = switch _processEnv->Dict.get("REVENTLESS_INMEMORY_TOKEN_SECRET") {
|
|
114
|
+
| Some(envSecret) if String.length(envSecret) >= 16 => envSecret
|
|
115
|
+
| _ => _randomBytes(32)->_bufToString("hex")
|
|
116
|
+
}
|
|
117
|
+
_secret := Some(s)
|
|
118
|
+
s
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let setTokenSecret = (s: string): unit => _secret := Some(s)
|
|
122
|
+
|
|
123
|
+
let _sign = (payload: string): string =>
|
|
124
|
+
_createHmac("sha256", _getSecret())->_update(payload)->_digest("base64url")
|
|
125
|
+
|
|
126
|
+
// base64url over arbitrary JSON strings: btoa-encode + URL-safe alphabet,
|
|
127
|
+
// strip `=` padding. Avoids pulling in Buffer for a single-line conversion.
|
|
128
|
+
@val external _btoa: string => string = "btoa"
|
|
129
|
+
@val external _atob: string => string = "atob"
|
|
130
|
+
|
|
131
|
+
let _b64urlEncode = (s: string): string =>
|
|
132
|
+
_btoa(s)
|
|
133
|
+
->String.replaceAll("+", "-")
|
|
134
|
+
->String.replaceAll("/", "_")
|
|
135
|
+
->String.replaceAll("=", "")
|
|
136
|
+
|
|
137
|
+
let _b64urlDecode = (s: string): option<string> => {
|
|
138
|
+
let padLen = mod(4 - mod(s->String.length, 4), 4)
|
|
139
|
+
let padded = s ++ String.repeat("=", padLen)
|
|
140
|
+
let std = padded->String.replaceAll("-", "+")->String.replaceAll("_", "/")
|
|
141
|
+
try {
|
|
142
|
+
Some(_atob(std))
|
|
143
|
+
} catch {
|
|
144
|
+
| _ => None
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Verifies credentials and returns a signed token (or an error string). */
|
|
149
|
+
let issue = async (~username: string, ~password: string): result<string, string> =>
|
|
150
|
+
switch store.contents->Dict.get(username) {
|
|
151
|
+
| Some({password: stored, identity}) if stored === password =>
|
|
152
|
+
let json = identity->S.reverseConvertToJsonOrThrow(Identity.schema)->JSON.stringify
|
|
153
|
+
let payload = _b64urlEncode(json)
|
|
154
|
+
let sig = _sign(payload)
|
|
155
|
+
Ok(`${payload}.${sig}`)
|
|
156
|
+
| _ => Error("Invalid credentials")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Returns the embedded Identity if signature verifies and the payload
|
|
161
|
+
* decodes; `None` for any tampered, malformed, or unsigned token.
|
|
162
|
+
*/
|
|
163
|
+
let verifyAndDecode = (token: string): option<Identity.t> => {
|
|
164
|
+
let parts = token->String.split(".")
|
|
165
|
+
switch (parts->Array.get(0), parts->Array.get(1)) {
|
|
166
|
+
| (Some(payload), Some(sig)) if _sign(payload) === sig =>
|
|
167
|
+
_b64urlDecode(payload)->Option.flatMap(json =>
|
|
168
|
+
try {
|
|
169
|
+
Some(json->JSON.parseOrThrow->S.parseOrThrow(Identity.schema))
|
|
170
|
+
} catch {
|
|
171
|
+
| _ => None
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
| _ => None
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Provider implementation ───────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
type authConfig = unit
|
|
182
|
+
|
|
183
|
+
let _bearerToken = (header: string): option<string> =>
|
|
184
|
+
if header->String.startsWith("Bearer ") {
|
|
185
|
+
Some(header->String.slice(~start=7, ~end=header->String.length)->String.trim)
|
|
186
|
+
} else {
|
|
187
|
+
None
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let authenticate = async (
|
|
191
|
+
ctx: ReventlessCore.Auth_Adapter.requestContext,
|
|
192
|
+
): Identity.authResult => {
|
|
193
|
+
// Decoding order: Bearer (when signature valid) → X-User/X-Groups → default.
|
|
194
|
+
// A *present* Bearer that fails verification is rejected (AuthError) rather
|
|
195
|
+
// than falling through to defaultUser — otherwise a client whose token was
|
|
196
|
+
// invalidated (e.g. process-local HMAC secret rotated on restart) would
|
|
197
|
+
// silently keep working as defaultUser, and the host-shell `on401` logout
|
|
198
|
+
// path would never fire. No-Authorization stays permissive (defaultUser)
|
|
199
|
+
// so anonymous dev flows still work.
|
|
200
|
+
let bearerHeader = getHeader(ctx.headers, "Authorization")->Option.flatMap(_bearerToken)
|
|
201
|
+
switch bearerHeader {
|
|
202
|
+
| Some(token) =>
|
|
203
|
+
switch Login.verifyAndDecode(token) {
|
|
204
|
+
| Some(identity) => Authenticated(identity)
|
|
205
|
+
| None => AuthError("Invalid bearer token")
|
|
206
|
+
}
|
|
207
|
+
| None =>
|
|
208
|
+
let userHeader = getHeader(ctx.headers, "X-User")
|
|
209
|
+
let groupsHeader = getHeader(ctx.headers, "X-Groups")
|
|
210
|
+
switch userHeader {
|
|
211
|
+
| None =>
|
|
212
|
+
switch groupsHeader {
|
|
213
|
+
| None =>
|
|
214
|
+
// No headers: in-memory mode defaults to defaultUser so local dev
|
|
215
|
+
// against an AllowAuthenticated-defaulted backend "just works" without
|
|
216
|
+
// setting headers. Anonymous behaviour is reachable only by building
|
|
217
|
+
// an Identity.anonymous explicitly in code.
|
|
218
|
+
Authenticated(defaultUser)
|
|
219
|
+
| Some(g) =>
|
|
220
|
+
// X-Groups without X-User: defaultUser identity with the requested
|
|
221
|
+
// groups. Useful for tests that exercise group-only authorization paths.
|
|
222
|
+
Authenticated({...defaultUser, groups: parseGroups(g)})
|
|
223
|
+
}
|
|
224
|
+
| Some(username) =>
|
|
225
|
+
switch lookupUser(username) {
|
|
226
|
+
| None => Anonymous
|
|
227
|
+
| Some(identity) =>
|
|
228
|
+
let withGroups = switch groupsHeader {
|
|
229
|
+
| Some(g) => {...identity, groups: parseGroups(g)}
|
|
230
|
+
| None => identity
|
|
231
|
+
}
|
|
232
|
+
Authenticated(withGroups)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let make = (~name as _: string, ~opts as _: option<Pulumi.ComponentResource.options>=?): Pulumi.Output.t<authConfig> =>
|
|
239
|
+
Pulumi.Output.make()
|