@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.
Files changed (334) hide show
  1. package/CHANGELOG.md +1197 -0
  2. package/LICENSE +202 -0
  3. package/__mocks__/emptyModule.js +2 -0
  4. package/__mocks__/nodeSqlite.mjs +6 -0
  5. package/package.json +67 -0
  6. package/rescript.json +39 -0
  7. package/src/Platform.res +2162 -0
  8. package/src/Platform.res.mjs +3389 -0
  9. package/src/adapter/Api/LocalGraphQL_Adapter.res +29 -0
  10. package/src/adapter/Api/LocalGraphQL_Adapter.res.mjs +26 -0
  11. package/src/adapter/Api/LocalGraphQL_SubscriptionResolvers.res +146 -0
  12. package/src/adapter/Api/LocalGraphQL_SubscriptionResolvers.res.mjs +88 -0
  13. package/src/adapter/Auth/Auth_GraphqlContext.res +83 -0
  14. package/src/adapter/Auth/Auth_GraphqlContext.res.mjs +76 -0
  15. package/src/adapter/Auth/LocalAuth.res +239 -0
  16. package/src/adapter/Auth/LocalAuth.res.mjs +247 -0
  17. package/src/adapter/Auth/UserStore.res +134 -0
  18. package/src/adapter/Auth/UserStore.res.mjs +146 -0
  19. package/src/adapter/Backend.res +46 -0
  20. package/src/adapter/Backend.res.mjs +66 -0
  21. package/src/adapter/BackendState.res +34 -0
  22. package/src/adapter/BackendState.res.mjs +42 -0
  23. package/src/adapter/Cloner/LocalClonerRunner.res +13 -0
  24. package/src/adapter/Cloner/LocalClonerRunner.res.mjs +12 -0
  25. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +329 -0
  26. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +301 -0
  27. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +76 -0
  28. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res.mjs +57 -0
  29. package/src/adapter/CommandGenerator/LocalCommandGeneratorResolvers.res +19 -0
  30. package/src/adapter/CommandGenerator/LocalCommandGeneratorResolvers.res.mjs +19 -0
  31. package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +104 -0
  32. package/src/adapter/CommandTopic/LocalCommandTopicChannel.res.mjs +82 -0
  33. package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +32 -0
  34. package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res.mjs +38 -0
  35. package/src/adapter/Counter/LocalCounterHandler.res +42 -0
  36. package/src/adapter/Counter/LocalCounterHandler.res.mjs +45 -0
  37. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +131 -0
  38. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +140 -0
  39. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +334 -0
  40. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +299 -0
  41. package/src/adapter/DomainGraphQL_Server.res +565 -0
  42. package/src/adapter/DomainGraphQL_Server.res.mjs +576 -0
  43. package/src/adapter/DomainMCP_Server.res +369 -0
  44. package/src/adapter/DomainMCP_Server.res.mjs +358 -0
  45. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +87 -0
  46. package/src/adapter/EventCollector/LocalEventCollectorChannel.res.mjs +45 -0
  47. package/src/adapter/EventLog/EventLogStorage_InMemory.res +105 -0
  48. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +117 -0
  49. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +145 -0
  50. package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +155 -0
  51. package/src/adapter/EventTopic/LocalEventTopicPublisher.res +36 -0
  52. package/src/adapter/EventTopic/LocalEventTopicPublisher.res.mjs +31 -0
  53. package/src/adapter/GraphQL_Server.res +6 -0
  54. package/src/adapter/GraphQL_Server.res.mjs +154 -0
  55. package/src/adapter/GraphQL_ServerInstance.res +310 -0
  56. package/src/adapter/GraphQL_ServerInstance.res.mjs +257 -0
  57. package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +57 -0
  58. package/src/adapter/Heartbeat/LocalHeartbeatRunner.res.mjs +34 -0
  59. package/src/adapter/LocalBus.res +498 -0
  60. package/src/adapter/LocalBus.res.mjs +1011 -0
  61. package/src/adapter/LocalPluginSpec.res +16 -0
  62. package/src/adapter/LocalPluginSpec.res.mjs +33 -0
  63. package/src/adapter/LocalSideEffectHandler.res +44 -0
  64. package/src/adapter/LocalSideEffectHandler.res.mjs +34 -0
  65. package/src/adapter/MCP_Server.res +4 -0
  66. package/src/adapter/MCP_Server.res.mjs +67 -0
  67. package/src/adapter/MCP_ServerInstance.res +362 -0
  68. package/src/adapter/MCP_ServerInstance.res.mjs +313 -0
  69. package/src/adapter/PlatformGraphQL_Server.res +75 -0
  70. package/src/adapter/PlatformGraphQL_Server.res.mjs +84 -0
  71. package/src/adapter/PlatformMCP_Server.res +23 -0
  72. package/src/adapter/PlatformMCP_Server.res.mjs +46 -0
  73. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +793 -0
  74. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +569 -0
  75. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +242 -0
  76. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +241 -0
  77. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +339 -0
  78. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +318 -0
  79. package/src/adapter/QueryDb/SortKey_Filter.res +48 -0
  80. package/src/adapter/QueryDb/SortKey_Filter.res.mjs +46 -0
  81. package/src/adapter/QueryEngine/LocalQueryEngine.res +57 -0
  82. package/src/adapter/QueryEngine/LocalQueryEngine.res.mjs +56 -0
  83. package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +91 -0
  84. package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res.mjs +50 -0
  85. package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +47 -0
  86. package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res.mjs +37 -0
  87. package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +11 -0
  88. package/src/adapter/Runtime/LocalPluginRuntime_Builder.res.mjs +30 -0
  89. package/src/adapter/Runtime/LocalRuntimeEnvironment.res +54 -0
  90. package/src/adapter/Runtime/LocalRuntimeEnvironment.res.mjs +40 -0
  91. package/src/adapter/Scheduler/LocalScheduledPublisher.res +102 -0
  92. package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +84 -0
  93. package/src/adapter/SqliteDriver.res +52 -0
  94. package/src/adapter/SqliteDriver.res.mjs +76 -0
  95. package/src/adapter/SqliteDriver.resi +31 -0
  96. package/src/adapter/Task/LocalTaskBucket.res +75 -0
  97. package/src/adapter/Task/LocalTaskBucket.res.mjs +48 -0
  98. package/src/adapter/Task/TaskBucket_Sqlite.res +47 -0
  99. package/src/adapter/Task/TaskBucket_Sqlite.res.mjs +40 -0
  100. package/src/components/Aggregate_Builder.res +55 -0
  101. package/src/components/Aggregate_Builder.res.mjs +140 -0
  102. package/src/components/AutomationSlice_Builder.res +41 -0
  103. package/src/components/AutomationSlice_Builder.res.mjs +57 -0
  104. package/src/components/Counter_Builder.res +14 -0
  105. package/src/components/Counter_Builder.res.mjs +32 -0
  106. package/src/components/DcbEventLog_Builder.res +18 -0
  107. package/src/components/DcbEventLog_Builder.res.mjs +24 -0
  108. package/src/components/ExtensionPoint_Builder.res +25 -0
  109. package/src/components/ExtensionPoint_Builder.res.mjs +44 -0
  110. package/src/components/InboundTranslationSlice_Builder.res +34 -0
  111. package/src/components/InboundTranslationSlice_Builder.res.mjs +45 -0
  112. package/src/components/OutboundTranslationSlice_Builder.res +43 -0
  113. package/src/components/OutboundTranslationSlice_Builder.res.mjs +59 -0
  114. package/src/components/Plugin_Builder.res +37 -0
  115. package/src/components/Plugin_Builder.res.mjs +67 -0
  116. package/src/components/ReadModel_Builder.res +47 -0
  117. package/src/components/ReadModel_Builder.res.mjs +54 -0
  118. package/src/components/StateChangeSlice_Builder.res +19 -0
  119. package/src/components/StateChangeSlice_Builder.res.mjs +21 -0
  120. package/src/components/StateViewSlice_Builder.res +42 -0
  121. package/src/components/StateViewSlice_Builder.res.mjs +58 -0
  122. package/src/components/Task_Builder.res +28 -0
  123. package/src/components/Task_Builder.res.mjs +41 -0
  124. package/src/reexport/CommandTopic.res +3 -0
  125. package/src/reexport/CommandTopic.res.mjs +55 -0
  126. package/src/test/Mocks/MockDcbEventLogStorage.res +136 -0
  127. package/src/test/Mocks/MockDcbEventLogStorage.res.mjs +146 -0
  128. package/src/test/Mocks/MockEventLogStorage.res +57 -0
  129. package/src/test/Mocks/MockEventLogStorage.res.mjs +65 -0
  130. package/src/test/Mocks/MockPublisher.res +29 -0
  131. package/src/test/Mocks/MockPublisher.res.mjs +28 -0
  132. package/src/test/Mocks/MockQueryDbStorage.res +104 -0
  133. package/src/test/Mocks/MockQueryDbStorage.res.mjs +136 -0
  134. package/src/test/TestRunner.res +96 -0
  135. package/src/test/TestRunner.res.mjs +71 -0
  136. package/tests/SplitApiFixtures.res +84 -0
  137. package/tests/SplitApiFixtures.res.mjs +88 -0
  138. package/tests/SplitApiTest.res +102 -0
  139. package/tests/SplitApiTest.res.mjs +82 -0
  140. package/tests/TestFixtures.res +54 -0
  141. package/tests/TestFixtures.res.mjs +65 -0
  142. package/tests/adapter/BackendParityTest.res +94 -0
  143. package/tests/adapter/BackendParityTest.res.mjs +96 -0
  144. package/tests/adapter/CollectNEventsTest.res +82 -0
  145. package/tests/adapter/CollectNEventsTest.res.mjs +83 -0
  146. package/tests/adapter/CommandAuthorizationTest.res +200 -0
  147. package/tests/adapter/CommandAuthorizationTest.res.mjs +190 -0
  148. package/tests/adapter/CommandTopicChannelTest.res +174 -0
  149. package/tests/adapter/CommandTopicChannelTest.res.mjs +170 -0
  150. package/tests/adapter/CounterHandlerTest.res +113 -0
  151. package/tests/adapter/CounterHandlerTest.res.mjs +96 -0
  152. package/tests/adapter/DcbEventLogStorageSqliteTest.res +157 -0
  153. package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +165 -0
  154. package/tests/adapter/DcbEventLogStorageTest.res +187 -0
  155. package/tests/adapter/DcbEventLogStorageTest.res.mjs +195 -0
  156. package/tests/adapter/EventCollectorChannelTest.res +166 -0
  157. package/tests/adapter/EventCollectorChannelTest.res.mjs +178 -0
  158. package/tests/adapter/EventLogStorageSqliteTest.res +105 -0
  159. package/tests/adapter/EventLogStorageSqliteTest.res.mjs +116 -0
  160. package/tests/adapter/EventTapTest.res +102 -0
  161. package/tests/adapter/EventTapTest.res.mjs +94 -0
  162. package/tests/adapter/EventTopicPublisherTest.res +76 -0
  163. package/tests/adapter/EventTopicPublisherTest.res.mjs +86 -0
  164. package/tests/adapter/GraphQL_SchemaInspectorTest.res +763 -0
  165. package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +578 -0
  166. package/tests/adapter/GraphQL_SubscriptionResolversTest.res +161 -0
  167. package/tests/adapter/GraphQL_SubscriptionResolversTest.res.mjs +134 -0
  168. package/tests/adapter/HeartbeatRunnerTest.res +136 -0
  169. package/tests/adapter/HeartbeatRunnerTest.res.mjs +96 -0
  170. package/tests/adapter/LocalAuthHttpTest.res +224 -0
  171. package/tests/adapter/LocalAuthHttpTest.res.mjs +237 -0
  172. package/tests/adapter/LocalAuthLoginTest.res +180 -0
  173. package/tests/adapter/LocalAuthLoginTest.res.mjs +207 -0
  174. package/tests/adapter/LocalAuthTest.res +106 -0
  175. package/tests/adapter/LocalAuthTest.res.mjs +145 -0
  176. package/tests/adapter/LocalAuthUserStoreTest.res +172 -0
  177. package/tests/adapter/LocalAuthUserStoreTest.res.mjs +208 -0
  178. package/tests/adapter/LocalBusBoundedTest.res +157 -0
  179. package/tests/adapter/LocalBusBoundedTest.res.mjs +168 -0
  180. package/tests/adapter/LocalBusPubSubTest.res +81 -0
  181. package/tests/adapter/LocalBusPubSubTest.res.mjs +81 -0
  182. package/tests/adapter/QueryDbGsiTtlTest.res +268 -0
  183. package/tests/adapter/QueryDbGsiTtlTest.res.mjs +209 -0
  184. package/tests/adapter/QueryDbListResolverTest.res +361 -0
  185. package/tests/adapter/QueryDbListResolverTest.res.mjs +426 -0
  186. package/tests/adapter/QueryDbStorageSqliteTest.res +159 -0
  187. package/tests/adapter/QueryDbStorageSqliteTest.res.mjs +126 -0
  188. package/tests/adapter/QueryDbStorageTest.res +171 -0
  189. package/tests/adapter/QueryDbStorageTest.res.mjs +170 -0
  190. package/tests/adapter/QueryEngineTest.res +152 -0
  191. package/tests/adapter/QueryEngineTest.res.mjs +148 -0
  192. package/tests/adapter/ScheduledPublisherTest.res +199 -0
  193. package/tests/adapter/ScheduledPublisherTest.res.mjs +203 -0
  194. package/tests/adapter/SortKey_FilterTest.res +103 -0
  195. package/tests/adapter/SortKey_FilterTest.res.mjs +135 -0
  196. package/tests/adapter/SqliteDriverTest.res +86 -0
  197. package/tests/adapter/SqliteDriverTest.res.mjs +101 -0
  198. package/tests/adapter/TaskBucketSqliteTest.res +44 -0
  199. package/tests/adapter/TaskBucketSqliteTest.res.mjs +40 -0
  200. package/tests/adapter/TaskBucketTest.res +106 -0
  201. package/tests/adapter/TaskBucketTest.res.mjs +118 -0
  202. package/tests/components/aggregate/AggregateFixtures.res +78 -0
  203. package/tests/components/aggregate/AggregateFixtures.res.mjs +109 -0
  204. package/tests/components/aggregate/AggregateTest.res +129 -0
  205. package/tests/components/aggregate/AggregateTest.res.mjs +129 -0
  206. package/tests/components/automationslice/AutomationSliceCallbackTest.res +212 -0
  207. package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +233 -0
  208. package/tests/components/automationslice/AutomationSliceFixtures.res +189 -0
  209. package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +276 -0
  210. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +289 -0
  211. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +453 -0
  212. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +47 -0
  213. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +31 -0
  214. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +213 -0
  215. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +268 -0
  216. package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +118 -0
  217. package/tests/components/automationslice/MixedSourceAutomationSliceTest.res.mjs +99 -0
  218. package/tests/components/commandgenerator/CommandGeneratorFixtures.res +73 -0
  219. package/tests/components/commandgenerator/CommandGeneratorFixtures.res.mjs +109 -0
  220. package/tests/components/commandgenerator/CommandGeneratorTest.res +43 -0
  221. package/tests/components/commandgenerator/CommandGeneratorTest.res.mjs +46 -0
  222. package/tests/components/commandtopic/CommandTopicFixtures.res +37 -0
  223. package/tests/components/commandtopic/CommandTopicFixtures.res.mjs +68 -0
  224. package/tests/components/commandtopic/CommandTopicStreamFixtures.res +43 -0
  225. package/tests/components/commandtopic/CommandTopicStreamFixtures.res.mjs +91 -0
  226. package/tests/components/commandtopic/CommandTopicStreamTest.res +30 -0
  227. package/tests/components/commandtopic/CommandTopicStreamTest.res.mjs +35 -0
  228. package/tests/components/commandtopic/CommandTopicTest.res +55 -0
  229. package/tests/components/commandtopic/CommandTopicTest.res.mjs +62 -0
  230. package/tests/components/counter/CounterFixtures.res +42 -0
  231. package/tests/components/counter/CounterFixtures.res.mjs +34 -0
  232. package/tests/components/counter/CounterTest.res +46 -0
  233. package/tests/components/counter/CounterTest.res.mjs +61 -0
  234. package/tests/components/dcb/DcbCrossPartitionFixtures.res +170 -0
  235. package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +276 -0
  236. package/tests/components/dcb/DcbCrossPartitionReadTest.res +68 -0
  237. package/tests/components/dcb/DcbCrossPartitionReadTest.res.mjs +78 -0
  238. package/tests/components/dcb/DcbEventLogAppendStreamTest.res +72 -0
  239. package/tests/components/dcb/DcbEventLogAppendStreamTest.res.mjs +82 -0
  240. package/tests/components/dcb/DcbEventLogStreamTest.res +103 -0
  241. package/tests/components/dcb/DcbEventLogStreamTest.res.mjs +91 -0
  242. package/tests/components/dcb/DcbFixtures.res +145 -0
  243. package/tests/components/dcb/DcbFixtures.res.mjs +221 -0
  244. package/tests/components/dcb/DcbTest.res +55 -0
  245. package/tests/components/dcb/DcbTest.res.mjs +55 -0
  246. package/tests/components/eventcollector/EventCollectorFixtures.res +42 -0
  247. package/tests/components/eventcollector/EventCollectorFixtures.res.mjs +49 -0
  248. package/tests/components/eventcollector/EventCollectorTest.res +37 -0
  249. package/tests/components/eventcollector/EventCollectorTest.res.mjs +36 -0
  250. package/tests/components/eventlog/EventLogAppendStreamTest.res +93 -0
  251. package/tests/components/eventlog/EventLogAppendStreamTest.res.mjs +113 -0
  252. package/tests/components/eventlog/EventLogConflictTest.res +86 -0
  253. package/tests/components/eventlog/EventLogConflictTest.res.mjs +81 -0
  254. package/tests/components/eventlog/EventLogFixtures.res +52 -0
  255. package/tests/components/eventlog/EventLogFixtures.res.mjs +80 -0
  256. package/tests/components/eventlog/EventLogStreamTest.res +98 -0
  257. package/tests/components/eventlog/EventLogStreamTest.res.mjs +142 -0
  258. package/tests/components/eventlog/EventLogTest.res +53 -0
  259. package/tests/components/eventlog/EventLogTest.res.mjs +74 -0
  260. package/tests/components/eventtopic/EventTopicFixtures.res +47 -0
  261. package/tests/components/eventtopic/EventTopicFixtures.res.mjs +77 -0
  262. package/tests/components/eventtopic/EventTopicStreamFixtures.res +27 -0
  263. package/tests/components/eventtopic/EventTopicStreamFixtures.res.mjs +62 -0
  264. package/tests/components/eventtopic/EventTopicStreamTest.res +35 -0
  265. package/tests/components/eventtopic/EventTopicStreamTest.res.mjs +44 -0
  266. package/tests/components/eventtopic/EventTopicTest.res +35 -0
  267. package/tests/components/eventtopic/EventTopicTest.res.mjs +43 -0
  268. package/tests/components/extensionpoint/ExtensionPointFixtures.res +145 -0
  269. package/tests/components/extensionpoint/ExtensionPointFixtures.res.mjs +188 -0
  270. package/tests/components/extensionpoint/ExtensionPointTest.res +62 -0
  271. package/tests/components/extensionpoint/ExtensionPointTest.res.mjs +55 -0
  272. package/tests/components/heartbeat/HeartbeatFixtures.res +48 -0
  273. package/tests/components/heartbeat/HeartbeatFixtures.res.mjs +46 -0
  274. package/tests/components/heartbeat/HeartbeatTest.res +67 -0
  275. package/tests/components/heartbeat/HeartbeatTest.res.mjs +65 -0
  276. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +241 -0
  277. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs +239 -0
  278. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +36 -0
  279. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs +73 -0
  280. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +191 -0
  281. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +297 -0
  282. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +78 -0
  283. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res.mjs +137 -0
  284. package/tests/components/querydb/QueryDbFixtures.res +39 -0
  285. package/tests/components/querydb/QueryDbFixtures.res.mjs +73 -0
  286. package/tests/components/querydb/QueryDbSubIdFixtures.res +44 -0
  287. package/tests/components/querydb/QueryDbSubIdFixtures.res.mjs +80 -0
  288. package/tests/components/querydb/QueryDbSubIdTest.res +104 -0
  289. package/tests/components/querydb/QueryDbSubIdTest.res.mjs +104 -0
  290. package/tests/components/querydb/QueryDbTest.res +81 -0
  291. package/tests/components/querydb/QueryDbTest.res.mjs +126 -0
  292. package/tests/components/readmodel/DcbReadModelE2EFixtures.res +215 -0
  293. package/tests/components/readmodel/DcbReadModelE2EFixtures.res.mjs +308 -0
  294. package/tests/components/readmodel/DcbReadModelE2ETest.res +45 -0
  295. package/tests/components/readmodel/DcbReadModelE2ETest.res.mjs +37 -0
  296. package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res +88 -0
  297. package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res.mjs +152 -0
  298. package/tests/components/readmodel/MixedSourceReadModelFixtures.res +181 -0
  299. package/tests/components/readmodel/MixedSourceReadModelFixtures.res.mjs +277 -0
  300. package/tests/components/readmodel/MixedSourceReadModelTest.res +61 -0
  301. package/tests/components/readmodel/MixedSourceReadModelTest.res.mjs +68 -0
  302. package/tests/components/readmodel/ReadModelFixtures.res +138 -0
  303. package/tests/components/readmodel/ReadModelFixtures.res.mjs +169 -0
  304. package/tests/components/readmodel/ReadModelTest.res +46 -0
  305. package/tests/components/readmodel/ReadModelTest.res.mjs +36 -0
  306. package/tests/components/scheduler/SchedulerFixtures.res +59 -0
  307. package/tests/components/scheduler/SchedulerFixtures.res.mjs +63 -0
  308. package/tests/components/scheduler/SchedulerTest.res +76 -0
  309. package/tests/components/scheduler/SchedulerTest.res.mjs +86 -0
  310. package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +133 -0
  311. package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res.mjs +149 -0
  312. package/tests/components/sideeffecthandler/SideEffectHandlerTest.res +39 -0
  313. package/tests/components/sideeffecthandler/SideEffectHandlerTest.res.mjs +28 -0
  314. package/tests/components/stateviewslice/StateViewSliceFixtures.res +117 -0
  315. package/tests/components/stateviewslice/StateViewSliceFixtures.res.mjs +188 -0
  316. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +117 -0
  317. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res.mjs +183 -0
  318. package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +78 -0
  319. package/tests/components/stateviewslice/StateViewSliceSubIdTest.res.mjs +107 -0
  320. package/tests/components/stateviewslice/StateViewSliceTest.res +59 -0
  321. package/tests/components/stateviewslice/StateViewSliceTest.res.mjs +72 -0
  322. package/tests/components/task/TaskFixtures.res +87 -0
  323. package/tests/components/task/TaskFixtures.res.mjs +82 -0
  324. package/tests/components/task/TaskTest.res +98 -0
  325. package/tests/components/task/TaskTest.res.mjs +46 -0
  326. package/tests/plugin/PluginBehavior_GWT.res +146 -0
  327. package/tests/plugin/PluginBehavior_GWT.res.mjs +281 -0
  328. package/tests/plugin/Plugin_Fixtures.res +61 -0
  329. package/tests/plugin/Plugin_Fixtures.res.mjs +105 -0
  330. package/tests/plugin/PluginsProjection_GWT.res +95 -0
  331. package/tests/plugin/PluginsProjection_GWT.res.mjs +120 -0
  332. package/tests/plugin/UIFragmentRegistryProjection_GWT.res +72 -0
  333. package/tests/plugin/UIFragmentRegistryProjection_GWT.res.mjs +117 -0
  334. package/tests/setup/sqliteGlobal.cjs +12 -0
@@ -0,0 +1,793 @@
1
+ // GraphQL query resolvers for in-memory QueryDb.
2
+ // Make(Bus) functor: registers query fields into the provided server during component construction.
3
+ // Supports: getById, list (when listFieldName provided), {name}ById (when subId), and {name}By{Index} per index.
4
+ //
5
+ // Query names are resolved from Plugin_Helpers.queryFieldNamesRegistry (populated by
6
+ // Plugin_Builder) to align with fragment SDL. Falls back to camelCase(name) for
7
+ // backward compatibility when no registry entry exists.
8
+ //
9
+ // Relay support is domain-only (platform QueryDbs pass relay=None):
10
+ // - encodeGlobalId / decodeGlobalId for Relay global IDs
11
+ // - node(id: ID!) resolution via registerNodeResolverCallback
12
+ // - registerNodeType for the Relay Node type registry
13
+ // Platform admin views are not Relay-paginated so relay=None is correct for them.
14
+
15
+ // Relay support type — lifted to module level so Platform.res can construct a value
16
+ // without a functor application in type position (not supported in ReScript).
17
+ // Domain plugins pass Some(domainRelaySupport); platform plugins pass None.
18
+ type relaySupport = {
19
+ encodeGlobalId: (~typeName: string, ~localId: string) => string,
20
+ registerNodeType: (~typeName: string, ~queryDbName: string) => unit,
21
+ registerNodeResolverCallback: DomainGraphQL_Server.nodeResolverCallback => unit,
22
+ nodeTypeRegistry: ref<dict<string>>,
23
+ }
24
+
25
+ @val external btoa: string => string = "btoa"
26
+ @val external atob: string => string = "atob"
27
+
28
+ // Shared keyset-cursor helpers. The cursor is base64 of the row's value for the
29
+ // active sort field (orderBy.field, or "id" when no orderBy is supplied). Used
30
+ // by both the connection list resolver and the items resolver so cursor encoding
31
+ // stays in lockstep.
32
+ let encodeCursor = (value: string): string => btoa(value)
33
+ let decodeCursor = (cursor: string): string => atob(cursor)
34
+
35
+ // Default page size for the connection list resolver when neither `first` nor
36
+ // `last` is supplied. Matches the UI's `defaultPageSize` constant; a bound is
37
+ // necessary for the keyset model to report `pageInfo` correctly.
38
+ let defaultListPageSize = 50
39
+
40
+ module Make = (Bus: LocalBus.T) => {
41
+ open ReventlessCore
42
+
43
+ type api = unit
44
+ type role = unit
45
+
46
+ // -- Identity extraction from GraphQL context ---------------------------------
47
+ // graphql-yoga's `context` factory in DomainGraphQL_Server.buildAuthContext
48
+ // runs LocalAuth.authenticate per request and attaches the resolved
49
+ // `Identity.t` to `ctx.identity`. Fallback to anonymous only when ctx is
50
+ // malformed.
51
+
52
+ let extractIdentity = (ctx: JSON.t): Reventless.Identity.t => {
53
+ try {
54
+ switch (ctx->Obj.magic)["identity"]->Nullable.toOption {
55
+ | Some(id) => (id: Reventless.Identity.t)
56
+ | None => Reventless.Identity.anonymous
57
+ }
58
+ } catch {
59
+ | _ => Reventless.Identity.anonymous
60
+ }
61
+ }
62
+
63
+ // -- Module-level server and relay refs ------------------------------------
64
+ // Set by Platform.res before component construction runs so the make function
65
+ // can pick up the correct target server and relay support (mirrors AWS ~api pattern).
66
+ let serverRef: ref<GraphQL_ServerInstance.t> = ref(DomainGraphQL_Server.asInterface)
67
+ let relayRef: ref<option<relaySupport>> = ref(Some({
68
+ encodeGlobalId: DomainGraphQL_Server.encodeGlobalId,
69
+ registerNodeType: DomainGraphQL_Server.registerNodeType,
70
+ registerNodeResolverCallback: DomainGraphQL_Server.registerNodeResolverCallback,
71
+ nodeTypeRegistry: DomainGraphQL_Server.nodeTypeRegistry,
72
+ }))
73
+
74
+ let make: QueryDb_Adapter.resolversMaker<unit, unit> = (
75
+ ~name,
76
+ ~api as _,
77
+ ~apiRole as _,
78
+ ~dataSourceName as _,
79
+ ~indexes,
80
+ ~subIdField,
81
+ ~idResolverConfigs as _,
82
+ ~idsResolverConfigs as _,
83
+ ~authorization,
84
+ ~opts as _,
85
+ ) => {
86
+ // Read the active server and relay support from module-level refs set by Platform.res.
87
+ // This mirrors how AWS adapters receive ~api from the hook; here the hook sets the refs
88
+ // before calling make() so the values are available at registration time.
89
+ let server = serverRef.contents
90
+ let relay = relayRef.contents
91
+
92
+ // Register the Relay node resolver callback once per QueryDb (domain only).
93
+ // Scans all QueryDb instances to resolve node(id: ID!) queries.
94
+ switch relay {
95
+ | Some(r) =>
96
+ r.registerNodeResolverCallback(async (~typeName, ~localId) => {
97
+ let queryDbName = switch r.nodeTypeRegistry.contents->Dict.get(typeName) {
98
+ | Some(n) => n
99
+ | None => typeName
100
+ }
101
+ switch Bus.getQueryDb(queryDbName) {
102
+ | Some(ops) =>
103
+ let items =
104
+ await ops.loadStream(localId)
105
+ ->Stream.runCollect
106
+ ->Effect.catchAll(_ => Effect.succeed([]))
107
+ ->Effect.runPromise
108
+ switch items->Array.get(0) {
109
+ | Some(item) =>
110
+ let obj = item->JSON.Decode.object->Option.getOr(Dict.make())
111
+ obj->Dict.set("__typename", JSON.Encode.string(typeName))
112
+ obj->Dict.set("id", r.encodeGlobalId(~typeName, ~localId)->JSON.Encode.string)
113
+ Some(JSON.Encode.object(obj))
114
+ | None => None
115
+ }
116
+ | None => None
117
+ }
118
+ })
119
+ | None => ()
120
+ }
121
+
122
+ let runInterceptor = async (~ctx, ~args): QueryDb_Callback.interceptResult => {
123
+ let identity = extractIdentity(ctx)
124
+ // Spec-level authorization runs first; failures short-circuit before
125
+ // the user-supplied interceptor (mirrors mutation enforcement).
126
+ if !Reventless.Authorization.isAllowed(authorization, identity) {
127
+ Deny("Forbidden")
128
+ } else {
129
+ switch QueryDb_Callback.queryInterceptorHook.contents {
130
+ | None => Allow
131
+ | Some(interceptor) => await interceptor(~identity, ~readModelName=name, ~args)
132
+ }
133
+ }
134
+ }
135
+
136
+ let cap = s => s->String.charAt(0)->String.toUpperCase ++ s->String.slice(~start=1)
137
+
138
+ // Resolve query field names: check registry first, fall back to safe defaults.
139
+ // Fallbacks use simple GraphQL built-in types to avoid referencing non-existent custom types.
140
+ let registryEntry = Plugin_Helpers.queryFieldNamesRegistry->Dict.get(name)
141
+ let singleQueryName = switch registryEntry {
142
+ | Some({singleFieldName}) => singleFieldName
143
+ | None => name->String.charAt(0)->String.toLowerCase ++ name->String.slice(~start=1)
144
+ }
145
+ let listQueryName = switch registryEntry {
146
+ | Some({listFieldName}) => listFieldName
147
+ | None => name ++ "s"
148
+ }
149
+ let returnTypeName = switch registryEntry {
150
+ | Some({returnTypeName: rt}) => rt
151
+ | None => "String"
152
+ }
153
+ let pluralTypeName = switch registryEntry {
154
+ | Some({pluralTypeName}) => pluralTypeName
155
+ | None => "[String]"
156
+ }
157
+
158
+ // Resolve includeIdParam flag from registry (defaults to true for ReadModels)
159
+ let includeIdParam = switch registryEntry {
160
+ | Some({includeIdParam}) => includeIdParam
161
+ | None => true
162
+ }
163
+
164
+ // Resolve connectionSpec flag from registry (defaults to true)
165
+ let connectionSpec = switch registryEntry {
166
+ | Some({connectionSpec}) => connectionSpec
167
+ | None => true
168
+ }
169
+
170
+ // Register this entity type in the Relay Node type registry (domain only).
171
+ if includeIdParam {
172
+ switch relay {
173
+ | Some(r) => r.registerNodeType(~typeName=returnTypeName, ~queryDbName=name)
174
+ | None => ()
175
+ }
176
+ }
177
+
178
+ let encodeId = switch relay {
179
+ | Some(r) => (~typeName, ~localId) => r.encodeGlobalId(~typeName, ~localId)
180
+ | None => (~typeName as _, ~localId) => localId
181
+ }
182
+
183
+ // -- Main query: getById ---------------------------------------------------
184
+ let byIdSdl = if includeIdParam {
185
+ switch subIdField {
186
+ | Some(sf) => ` ${singleQueryName}(id: ID!, ${sf}: String): ${returnTypeName}`
187
+ | None => ` ${singleQueryName}(id: ID!): ${returnTypeName}`
188
+ }
189
+ } else {
190
+ ` ${singleQueryName}: ${returnTypeName}`
191
+ }
192
+ let byIdResolver: GraphQL_ServerInstance.resolverFn = async (_root, args, ctx) => {
193
+ switch await runInterceptor(~ctx, ~args) {
194
+ | Deny(_) => JSON.Encode.null
195
+ | Allow =>
196
+ let id =
197
+ args->JSON.Decode.object->Option.flatMap(d => d->Dict.get("id"))->Option.flatMap(JSON.Decode.string)->Option.getOr("")
198
+ switch Bus.getQueryDb(name) {
199
+ | Some(ops) =>
200
+ let items =
201
+ await ops.loadStream(id)
202
+ ->Stream.runCollect
203
+ ->Effect.catchAll(_ => Effect.succeed([]))
204
+ ->Effect.runPromise
205
+ switch items->Array.get(0) {
206
+ | Some(item) =>
207
+ if includeIdParam {
208
+ let obj = item->JSON.Decode.object->Option.getOr(Dict.make())
209
+ obj->Dict.set("id", encodeId(~typeName=returnTypeName, ~localId=id)->JSON.Encode.string)
210
+ JSON.Encode.object(obj)
211
+ } else {
212
+ item
213
+ }
214
+ | None => JSON.Encode.null
215
+ }
216
+ | None => JSON.Encode.null
217
+ }
218
+ }
219
+ }
220
+
221
+ // -- Batched-by-ids query: {listFieldName}ByIds ---------------------------
222
+ // Mirrors the AppSync BatchGetItem resolver. Single-key projections only
223
+ // (subIdField=None) — matches the SDL emitted by FragmentGenerator. Missing
224
+ // ids drop out of the response (no cardinality preservation), matching
225
+ // BatchGetItem semantics.
226
+ let byIdsSdl = if includeIdParam && subIdField === None {
227
+ [GraphQL_FragmentGenerator.deriveByIdsQueryField(~listFieldName=listQueryName, ~returnTypeName)]
228
+ } else {
229
+ []
230
+ }
231
+ let byIdsResolverEntry: option<(string, GraphQL_ServerInstance.resolverFn)> =
232
+ if includeIdParam && subIdField === None {
233
+ let resolver: GraphQL_ServerInstance.resolverFn = async (_root, args, ctx) => {
234
+ switch await runInterceptor(~ctx, ~args) {
235
+ | Deny(_) => []->JSON.Encode.array
236
+ | Allow =>
237
+ let ids =
238
+ args
239
+ ->JSON.Decode.object
240
+ ->Option.flatMap(d => d->Dict.get("ids"))
241
+ ->Option.flatMap(JSON.Decode.array)
242
+ ->Option.getOr([])
243
+ ->Array.filterMap(JSON.Decode.string)
244
+ switch Bus.getQueryDb(name) {
245
+ | Some(ops) =>
246
+ let loaded = await ids->Array.map(id =>
247
+ ops.loadStream(id)
248
+ ->Stream.runCollect
249
+ ->Effect.catchAll(_ => Effect.succeed([]))
250
+ ->Effect.runPromise
251
+ ->Promise.thenResolve(items => (id, items->Array.get(0)))
252
+ )->Promise.all
253
+ loaded
254
+ ->Array.filterMap(((id, opt)) =>
255
+ opt->Option.map(item => {
256
+ let obj = item->JSON.Decode.object->Option.getOr(Dict.make())
257
+ obj->Dict.set(
258
+ "id",
259
+ encodeId(~typeName=returnTypeName, ~localId=id)->JSON.Encode.string,
260
+ )
261
+ JSON.Encode.object(obj)
262
+ })
263
+ )
264
+ ->JSON.Encode.array
265
+ | None => []->JSON.Encode.array
266
+ }
267
+ }
268
+ }
269
+ Some((listQueryName ++ "ByIds", resolver))
270
+ } else {
271
+ None
272
+ }
273
+
274
+ // Look up labelField from registry (Phase 3). Used by list-query filter.search /
275
+ // searchPrefix to target the entity's human-readable column without per-entity
276
+ // resolver wiring. Falls back to "id" so filtering still works on entities
277
+ // without a @displayName annotation (match on the partition key).
278
+ let labelField = switch registryEntry {
279
+ | Some({labelField: ?lf}) => lf->Option.getOr("id")
280
+ | None => "id"
281
+ }
282
+
283
+ // -- List query -------------------------------------------------------------
284
+ // Look up the registered state schema (populated alongside queryFieldNamesRegistry)
285
+ // so the resolver derives the same serverCapability the FragmentGenerator emitted.
286
+ let stateSchemaOpt = Plugin_Helpers.stateSchemaRegistry->Dict.get(name)
287
+ let capability = switch stateSchemaOpt {
288
+ | Some(s) => GraphQL_FragmentGenerator.deriveServerCapability(s)
289
+ | None => GraphQL_FragmentGenerator.emptyCapability
290
+ }
291
+ let (listSdl, listResolver): (array<string>, GraphQL_ServerInstance.resolverFn) = if connectionSpec {
292
+ // Relay Connection spec format
293
+ let filterTypeName = returnTypeName ++ "Filter"
294
+ let orderByTypes = GraphQL_FragmentGenerator.deriveConnectionOrderByType(
295
+ ~singularTypeName=returnTypeName,
296
+ ~capability,
297
+ )
298
+ let hasOrderBy = orderByTypes->Array.length > 0
299
+ let typesToRegister = [
300
+ GraphQL_FragmentGenerator.deriveConnectionFilterType(~filterTypeName, ~capability),
301
+ ]->Array.concat(orderByTypes)
302
+ server.registerTypes(~sdlTypes=typesToRegister)
303
+ let sdl = [
304
+ GraphQL_FragmentGenerator.deriveConnectionQueryField(
305
+ ~listFieldName=listQueryName,
306
+ ~singularTypeName=returnTypeName,
307
+ ~filterTypeName,
308
+ ~hasOrderBy,
309
+ ),
310
+ ]
311
+ let resolver: GraphQL_ServerInstance.resolverFn = async (_root, args, ctx) => {
312
+ switch await runInterceptor(~ctx, ~args) {
313
+ | Deny(_) =>
314
+ Obj.magic({
315
+ "edges": [],
316
+ "pageInfo": {
317
+ "hasNextPage": false,
318
+ "hasPreviousPage": false,
319
+ "startCursor": Nullable.null,
320
+ "endCursor": Nullable.null,
321
+ },
322
+ })
323
+ | Allow =>
324
+ let items = switch Bus.getQueryDbStream(name) {
325
+ | Some(makeStream) =>
326
+ await makeStream()->Stream.runCollect->Effect.runPromise
327
+ | None =>
328
+ switch Bus.getQueryDbScan(name) {
329
+ | Some(scanAll) => scanAll()
330
+ | None => []
331
+ }
332
+ }
333
+ // Apply filter (search, searchPrefix, ids) before any pagination.
334
+ let argsDict = args->JSON.Decode.object->Option.getOr(Dict.make())
335
+ let filterDict =
336
+ argsDict->Dict.get("filter")->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
337
+ let search = filterDict->Dict.get("search")->Option.flatMap(JSON.Decode.string)
338
+ let searchPrefix = filterDict->Dict.get("searchPrefix")->Option.flatMap(JSON.Decode.string)
339
+ let ids =
340
+ filterDict
341
+ ->Dict.get("ids")
342
+ ->Option.flatMap(JSON.Decode.array)
343
+ ->Option.map(arr => arr->Array.filterMap(JSON.Decode.string))
344
+ let getLabel = item =>
345
+ item
346
+ ->JSON.Decode.object
347
+ ->Option.flatMap(d => d->Dict.get(labelField))
348
+ ->Option.flatMap(JSON.Decode.string)
349
+ ->Option.getOr("")
350
+ let getId = item =>
351
+ item
352
+ ->JSON.Decode.object
353
+ ->Option.flatMap(d => d->Dict.get("id"))
354
+ ->Option.flatMap(JSON.Decode.string)
355
+ ->Option.getOr("")
356
+ // Per-field eq / from / to filters derived from capability — applied
357
+ // alongside the legacy search/searchPrefix/ids block.
358
+ let getFieldString = (item, field) =>
359
+ item
360
+ ->JSON.Decode.object
361
+ ->Option.flatMap(d => d->Dict.get(field))
362
+ ->Option.flatMap(v =>
363
+ switch v->JSON.Decode.string {
364
+ | Some(s) => Some(s)
365
+ | None =>
366
+ v
367
+ ->JSON.Decode.float
368
+ ->Option.map(f => Float.toString(f))
369
+ }
370
+ )
371
+ let perFieldChecks: array<JSON.t => bool> = capability.filterFields->Array.flatMap(f => {
372
+ let checks: array<JSON.t => bool> = []
373
+ switch filterDict->Dict.get(f.name ++ "Eq") {
374
+ | Some(v) when v != JSON.Encode.null =>
375
+ let expected = switch v->JSON.Decode.string {
376
+ | Some(s) => s
377
+ | None =>
378
+ v->JSON.Decode.float->Option.map(f => Float.toString(f))->Option.getOr("")
379
+ }
380
+ checks->Array.push(item =>
381
+ getFieldString(item, f.name)->Option.mapOr(false, v => v == expected)
382
+ )
383
+ | _ => ()
384
+ }
385
+ if f.range {
386
+ switch filterDict->Dict.get(f.name ++ "From") {
387
+ | Some(v) when v != JSON.Encode.null =>
388
+ let from =
389
+ v
390
+ ->JSON.Decode.string
391
+ ->Option.getOr(
392
+ v->JSON.Decode.float->Option.map(f => Float.toString(f))->Option.getOr(""),
393
+ )
394
+ checks->Array.push(item =>
395
+ getFieldString(item, f.name)->Option.mapOr(false, v => v >= from)
396
+ )
397
+ | _ => ()
398
+ }
399
+ switch filterDict->Dict.get(f.name ++ "To") {
400
+ | Some(v) when v != JSON.Encode.null =>
401
+ let to_ =
402
+ v
403
+ ->JSON.Decode.string
404
+ ->Option.getOr(
405
+ v->JSON.Decode.float->Option.map(f => Float.toString(f))->Option.getOr(""),
406
+ )
407
+ checks->Array.push(item =>
408
+ getFieldString(item, f.name)->Option.mapOr(false, v => v <= to_)
409
+ )
410
+ | _ => ()
411
+ }
412
+ }
413
+ checks
414
+ })
415
+
416
+ let filtered = items->Array.filter(item => {
417
+ let passSearch = switch search {
418
+ | Some(s) if s->String.length > 0 =>
419
+ getLabel(item)->String.toLowerCase->String.includes(s->String.toLowerCase)
420
+ | _ => true
421
+ }
422
+ let passPrefix = switch searchPrefix {
423
+ | Some(p) if p->String.length > 0 =>
424
+ getLabel(item)->String.toLowerCase->String.startsWith(p->String.toLowerCase)
425
+ | _ => true
426
+ }
427
+ // Filter `ids` accepts either the Relay-encoded global ID (the
428
+ // form returned by node.id) or the entity's raw local ID — so
429
+ // callers that hold a foreign-key value like `customerId =
430
+ // "cust-1"` can hydrate labels without first encoding to
431
+ // `Ordering_Customer:cust-1` base64.
432
+ let passIds = switch ids {
433
+ | Some(idList) if idList->Array.length > 0 =>
434
+ let itemId = getId(item)
435
+ let itemLocalId =
436
+ DomainGraphQL_Server.decodeGlobalId(itemId)->Option.map(((_, lid)) => lid)
437
+ idList->Array.some(i => i == itemId || itemLocalId == Some(i))
438
+ | _ => true
439
+ }
440
+ let passPerField = perFieldChecks->Array.every(check => check(item))
441
+ passSearch && passPrefix && passIds && passPerField
442
+ })
443
+
444
+ // Apply orderBy when provided. Sorts on the requested field; ties
445
+ // broken by id so keyset-style cursors stay stable across requests
446
+ // that share a sort field. When orderBy is omitted, items are sorted
447
+ // by id ascending so the natural order is deterministic and pagination
448
+ // cursors remain stable.
449
+ let orderByDict =
450
+ argsDict
451
+ ->Dict.get("orderBy")
452
+ ->Option.flatMap(JSON.Decode.object)
453
+ let orderByField =
454
+ orderByDict
455
+ ->Option.flatMap(ob => ob->Dict.get("field"))
456
+ ->Option.flatMap(JSON.Decode.string)
457
+ let direction =
458
+ orderByDict
459
+ ->Option.flatMap(ob => ob->Dict.get("direction"))
460
+ ->Option.flatMap(JSON.Decode.string)
461
+ ->Option.getOr("ASC")
462
+ let isDesc = direction == "DESC"
463
+ let sorted = switch orderByField {
464
+ | Some(f) =>
465
+ let cmp = (a, b) => {
466
+ let av = getFieldString(a, f)->Option.getOr("")
467
+ let bv = getFieldString(b, f)->Option.getOr("")
468
+ let primary = if av < bv {
469
+ -1
470
+ } else if av > bv {
471
+ 1
472
+ } else {
473
+ 0
474
+ }
475
+ let primary = isDesc ? -primary : primary
476
+ if primary != 0 {
477
+ primary
478
+ } else {
479
+ let aid = getId(a)
480
+ let bid = getId(b)
481
+ if aid < bid {
482
+ -1
483
+ } else if aid > bid {
484
+ 1
485
+ } else {
486
+ 0
487
+ }
488
+ }
489
+ }
490
+ filtered->Array.toSorted((a, b) => cmp(a, b)->Int.toFloat)
491
+ | None =>
492
+ // Default to id-ascending so cursor pagination has a stable order.
493
+ filtered->Array.toSorted((a, b) => {
494
+ let aid = getId(a)
495
+ let bid = getId(b)
496
+ if aid < bid {
497
+ -1.
498
+ } else if aid > bid {
499
+ 1.
500
+ } else {
501
+ 0.
502
+ }
503
+ })
504
+ }
505
+
506
+ // Keyset pagination. Cursor encodes the row's value for the active
507
+ // sort field — orderBy.field when supplied, "id" otherwise. Boundary
508
+ // is applied as a value comparison against the sorted array; in DESC
509
+ // sort the comparison flips. Note: when the cursor field has duplicate
510
+ // values, the boundary excludes all rows with the cursor's value, not
511
+ // just the row at the cursor position. Acceptable for in-memory dev;
512
+ // in practice common sort fields (id, createdAt) are unique.
513
+ let first =
514
+ argsDict->Dict.get("first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
515
+ let after = argsDict->Dict.get("after")->Option.flatMap(JSON.Decode.string)
516
+ let last =
517
+ argsDict->Dict.get("last")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
518
+ let before = argsDict->Dict.get("before")->Option.flatMap(JSON.Decode.string)
519
+ let isBackward = last->Option.isSome
520
+ let cursorField = orderByField->Option.getOr("id")
521
+ let getCursorValue = item =>
522
+ getFieldString(item, cursorField)->Option.getOr(getId(item))
523
+
524
+ let cursorBounded = switch (isBackward, after, before) {
525
+ | (false, Some(c), _) =>
526
+ let cv = decodeCursor(c)
527
+ sorted->Array.filter(item => {
528
+ let v = getCursorValue(item)
529
+ isDesc ? v < cv : v > cv
530
+ })
531
+ | (true, _, Some(c)) =>
532
+ let cv = decodeCursor(c)
533
+ sorted->Array.filter(item => {
534
+ let v = getCursorValue(item)
535
+ isDesc ? v > cv : v < cv
536
+ })
537
+ | _ => sorted
538
+ }
539
+
540
+ let pageSize = if isBackward {
541
+ last->Option.getOr(defaultListPageSize)
542
+ } else {
543
+ first->Option.getOr(defaultListPageSize)
544
+ }
545
+ let take = pageSize + 1
546
+ let (pageItems, hasMore) = if isBackward {
547
+ // Take the last `take` items from the cursor-bounded slice. If we
548
+ // grabbed an extra, drop the leading entry (the boundary marker).
549
+ let len = cursorBounded->Array.length
550
+ let startIdx = len > take ? len - take : 0
551
+ let arr = cursorBounded->Array.slice(~start=startIdx, ~end=len)
552
+ let hasMore = arr->Array.length > pageSize
553
+ let result = if hasMore {
554
+ arr->Array.slice(~start=1, ~end=arr->Array.length)
555
+ } else {
556
+ arr
557
+ }
558
+ (result, hasMore)
559
+ } else {
560
+ let arr = cursorBounded->Array.slice(~start=0, ~end=take)
561
+ let hasMore = arr->Array.length > pageSize
562
+ (arr->Array.slice(~start=0, ~end=pageSize), hasMore)
563
+ }
564
+
565
+ let edges = pageItems->Array.map(item =>
566
+ Obj.magic({"node": item, "cursor": encodeCursor(getCursorValue(item))})
567
+ )
568
+ let startCursor =
569
+ pageItems->Array.get(0)->Option.map(item => encodeCursor(getCursorValue(item)))
570
+ let endCursor =
571
+ pageItems
572
+ ->Array.get(pageItems->Array.length - 1)
573
+ ->Option.map(item => encodeCursor(getCursorValue(item)))
574
+ Obj.magic({
575
+ "edges": edges,
576
+ "pageInfo": {
577
+ "hasNextPage": !isBackward && hasMore,
578
+ "hasPreviousPage": isBackward && hasMore,
579
+ "startCursor": startCursor->Nullable.fromOption,
580
+ "endCursor": endCursor->Nullable.fromOption,
581
+ },
582
+ })
583
+ }
584
+ }
585
+ (sdl, resolver)
586
+ } else {
587
+ // Legacy AppSync-style format
588
+ let sdl = [` ${listQueryName}(nextToken: String, limit: Int): ${pluralTypeName}!`]
589
+ let resolver: GraphQL_ServerInstance.resolverFn = async (_root, args, ctx) => {
590
+ switch await runInterceptor(~ctx, ~args) {
591
+ | Deny(_) => Obj.magic({"nextToken": Nullable.null, "scannedCount": 0, "items": []})
592
+ | Allow =>
593
+ let items = switch Bus.getQueryDbStream(name) {
594
+ | Some(makeStream) =>
595
+ await makeStream()->Stream.runCollect->Effect.runPromise
596
+ | None =>
597
+ switch Bus.getQueryDbScan(name) {
598
+ | Some(scanAll) => scanAll()
599
+ | None => []
600
+ }
601
+ }
602
+ Obj.magic({"nextToken": Nullable.null, "scannedCount": items->Array.length, "items": items})
603
+ }
604
+ }
605
+ (sdl, resolver)
606
+ }
607
+ let listResolvers = [(listQueryName, listResolver)]
608
+
609
+ // -- Items query: {name}Items (only when subId configured) -----------------
610
+ // Relay Connection response. Accepts `filter` input object + first/after/last/before.
611
+ // Cursor is base64 of the sort key value (keyset pagination — see module-level
612
+ // `encodeCursor` / `decodeCursor` helpers).
613
+ let itemsSdl = switch subIdField {
614
+ | Some(_sf) =>
615
+ let filterTypeName = returnTypeName ++ "ItemsFilter"
616
+ let connectionTypeName = returnTypeName ++ "Connection"
617
+ server.registerTypes(
618
+ ~sdlTypes=[
619
+ `input ${filterTypeName} {\n prefix: String\n from: String\n to: String\n eq: String\n order: SortOrder\n}`,
620
+ ],
621
+ )
622
+ [` ${singleQueryName}Items(id: ID!, filter: ${filterTypeName}, first: Int, after: String, last: Int, before: String): ${connectionTypeName}!`]
623
+ | None => []
624
+ }
625
+ let itemsResolvers: array<(string, GraphQL_ServerInstance.resolverFn)> = switch subIdField {
626
+ | Some(sf) =>
627
+ let resolver: GraphQL_ServerInstance.resolverFn = async (_root, args, ctx) => {
628
+ let emptyConn = Obj.magic({
629
+ "edges": [],
630
+ "pageInfo": {
631
+ "hasNextPage": false,
632
+ "hasPreviousPage": false,
633
+ "startCursor": Nullable.null,
634
+ "endCursor": Nullable.null,
635
+ },
636
+ })
637
+ switch await runInterceptor(~ctx, ~args) {
638
+ | Deny(_) => emptyConn
639
+ | Allow =>
640
+ let argsDict = args->JSON.Decode.object->Option.getOr(Dict.make())
641
+ let id = argsDict->Dict.get("id")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
642
+ let filterDict =
643
+ argsDict->Dict.get("filter")->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
644
+ let filterPrefix = filterDict->Dict.get("prefix")->Option.flatMap(JSON.Decode.string)
645
+ let filterFrom = filterDict->Dict.get("from")->Option.flatMap(JSON.Decode.string)
646
+ let filterTo = filterDict->Dict.get("to")->Option.flatMap(JSON.Decode.string)
647
+ let filterEq = filterDict->Dict.get("eq")->Option.flatMap(JSON.Decode.string)
648
+ let orderDesc = filterDict->Dict.get("order")->Option.flatMap(JSON.Decode.string)->Option.map(o => o == "DESC")->Option.getOr(false)
649
+ let first = argsDict->Dict.get("first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
650
+ let after = argsDict->Dict.get("after")->Option.flatMap(JSON.Decode.string)
651
+ let last = argsDict->Dict.get("last")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
652
+ let before = argsDict->Dict.get("before")->Option.flatMap(JSON.Decode.string)
653
+ let isBackward = last->Option.isSome
654
+
655
+ switch Bus.getQueryDb(name) {
656
+ | None => emptyConn
657
+ | Some(ops) =>
658
+ let allItems =
659
+ await ops.loadStream(id)
660
+ ->Stream.runCollect
661
+ ->Effect.catchAll(_ => Effect.succeed([]))
662
+ ->Effect.runPromise
663
+
664
+ // Cursor-keyed filtering: exclude items on the cursor side of the boundary
665
+ let cursorFiltered = if isBackward {
666
+ switch before->Option.map(decodeCursor) {
667
+ | Some(beforeKey) =>
668
+ allItems->Array.filter(item =>
669
+ item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(sf))->Option.flatMap(JSON.Decode.string)->Option.map(v => v < beforeKey)->Option.getOr(false)
670
+ )
671
+ | None => allItems
672
+ }
673
+ } else {
674
+ switch after->Option.map(decodeCursor) {
675
+ | Some(afterKey) =>
676
+ allItems->Array.filter(item =>
677
+ item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(sf))->Option.flatMap(JSON.Decode.string)->Option.map(v => v > afterKey)->Option.getOr(false)
678
+ )
679
+ | None => allItems
680
+ }
681
+ }
682
+
683
+ // Apply SortKey_Filter for prefix/from/to/eq
684
+ let filtered = SortKey_Filter.apply(
685
+ ~items=cursorFiltered,
686
+ ~skField=sf,
687
+ ~prefix=?filterPrefix,
688
+ ~from=?filterFrom,
689
+ ~to_=?filterTo,
690
+ ~eq=?filterEq,
691
+ ~reverse=if isBackward { !orderDesc } else { orderDesc },
692
+ ~offset=0,
693
+ ).items
694
+
695
+ // For backward pagination flip to logical order after taking
696
+ let (pageItems, hasMore) = switch (isBackward, last, first) {
697
+ | (true, Some(n), _) =>
698
+ let take = n + 1
699
+ let taken = filtered->Array.slice(~start=0, ~end=take)
700
+ let hasMore = taken->Array.length > n
701
+ let result = taken->Array.slice(~start=0, ~end=n)->Array.toReversed
702
+ (result, hasMore)
703
+ | (_, _, Some(n)) =>
704
+ let take = n + 1
705
+ let taken = filtered->Array.slice(~start=0, ~end=take)
706
+ let hasMore = taken->Array.length > n
707
+ (taken->Array.slice(~start=0, ~end=n), hasMore)
708
+ | _ => (filtered, false)
709
+ }
710
+
711
+ let getSkValue = item =>
712
+ item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(sf))->Option.flatMap(JSON.Decode.string)->Option.getOr("")
713
+
714
+ let edges = pageItems->Array.map(item =>
715
+ Obj.magic({"node": item, "cursor": encodeCursor(getSkValue(item))})
716
+ )
717
+ let startCursor = pageItems->Array.get(0)->Option.map(item => encodeCursor(getSkValue(item)))
718
+ let endCursor = pageItems->Array.get(pageItems->Array.length - 1)->Option.map(item => encodeCursor(getSkValue(item)))
719
+
720
+ Obj.magic({
721
+ "edges": edges,
722
+ "pageInfo": {
723
+ "hasNextPage": !isBackward && hasMore,
724
+ "hasPreviousPage": isBackward && hasMore,
725
+ "startCursor": startCursor->Nullable.fromOption,
726
+ "endCursor": endCursor->Nullable.fromOption,
727
+ },
728
+ })
729
+ }
730
+ }
731
+ }
732
+ [(singleQueryName ++ "Items", resolver)]
733
+ | None => []
734
+ }
735
+
736
+ // -- Index queries: {name}By{Index} ---------------------------------------
737
+ let indexSdlFields = indexes->Array.map((ic: Reventless.ReadModel.indexConfig) =>
738
+ ` ${singleQueryName}By${cap(ic.index)}(${ic.index}: String!): [String]`
739
+ )
740
+ let indexResolvers: array<(string, GraphQL_ServerInstance.resolverFn)> = indexes->Array.map(
741
+ (ic: Reventless.ReadModel.indexConfig) => {
742
+ let index = ic.index
743
+ let resolverName = singleQueryName ++ "By" ++ cap(index)
744
+ let filterField = ic.idField->Option.getOr(index)
745
+ let resolver: GraphQL_ServerInstance.resolverFn = async (_root, args, ctx) => {
746
+ switch await runInterceptor(~ctx, ~args) {
747
+ | Deny(_) => []->JSON.Encode.array
748
+ | Allow =>
749
+ let value =
750
+ args->JSON.Decode.object->Option.flatMap(d => d->Dict.get(index))->Option.flatMap(JSON.Decode.string)->Option.getOr("")
751
+ switch Bus.getQueryDbScan(name) {
752
+ | Some(scanAll) =>
753
+ scanAll()
754
+ ->Array.filter(item =>
755
+ item
756
+ ->JSON.Decode.object
757
+ ->Option.flatMap(d => d->Dict.get(filterField))
758
+ ->Option.flatMap(JSON.Decode.string)
759
+ ->Option.map(v => v == value)
760
+ ->Option.getOr(false)
761
+ )
762
+ ->JSON.Encode.array
763
+ | None => []->JSON.Encode.array
764
+ }
765
+ }
766
+ }
767
+ (resolverName, resolver)
768
+ },
769
+ )
770
+
771
+ // -- Register all fields --------------------------------------------------
772
+ let allSdl =
773
+ [byIdSdl]
774
+ ->Array.concat(byIdsSdl)
775
+ ->Array.concat(listSdl)
776
+ ->Array.concat(itemsSdl)
777
+ ->Array.concat(indexSdlFields)
778
+
779
+ let resolvers = Dict.make()
780
+ resolvers->Dict.set(singleQueryName, byIdResolver)
781
+ byIdsResolverEntry->Option.forEach(((k, v)) => resolvers->Dict.set(k, v))
782
+ listResolvers->Array.forEach(((k, v)) => resolvers->Dict.set(k, v))
783
+ itemsResolvers->Array.forEach(((k, v)) => resolvers->Dict.set(k, v))
784
+ indexResolvers->Array.forEach(((k, v)) => resolvers->Dict.set(k, v))
785
+ server.registerQueries(~sdlFields=allSdl, ~resolvers)
786
+
787
+ {
788
+ resources: [],
789
+ resourcesMaker: _ => [],
790
+ }
791
+ }
792
+
793
+ }