@reventlessdev/reventless-gwt 1.0.0-alpha.74

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 (172) hide show
  1. package/CHANGELOG.md +698 -0
  2. package/LICENSE +202 -0
  3. package/bin/reventless-dev.mjs +21 -0
  4. package/package.json +54 -0
  5. package/rescript.json +36 -0
  6. package/src/.gwtignore +10 -0
  7. package/src/Automation_GWT.res +207 -0
  8. package/src/Automation_GWT.res.mjs +160 -0
  9. package/src/Behavior_GWT.res +603 -0
  10. package/src/Behavior_GWT.res.mjs +426 -0
  11. package/src/Bind.res +29 -0
  12. package/src/Bind.res.mjs +22 -0
  13. package/src/BuildClassifier.res +137 -0
  14. package/src/BuildClassifier.res.mjs +136 -0
  15. package/src/Cancellation.res +31 -0
  16. package/src/Cancellation.res.mjs +45 -0
  17. package/src/ChildProcess.res +84 -0
  18. package/src/ChildProcess.res.mjs +92 -0
  19. package/src/Cli.res +671 -0
  20. package/src/Cli.res.mjs +754 -0
  21. package/src/Collector.res +201 -0
  22. package/src/Collector.res.mjs +215 -0
  23. package/src/ComponentMeta.res +104 -0
  24. package/src/ComponentMeta.res.mjs +120 -0
  25. package/src/ComponentScan.res +103 -0
  26. package/src/ComponentScan.res.mjs +105 -0
  27. package/src/Delegate_GWT.res +303 -0
  28. package/src/Delegate_GWT.res.mjs +372 -0
  29. package/src/Diff.res +91 -0
  30. package/src/Diff.res.mjs +98 -0
  31. package/src/Discovery.res +105 -0
  32. package/src/Discovery.res.mjs +103 -0
  33. package/src/DomainDeadCode.res +93 -0
  34. package/src/DomainDeadCode.res.mjs +69 -0
  35. package/src/DomainGraph.res +0 -0
  36. package/src/DomainGraph.res.mjs +0 -0
  37. package/src/EventMapping_GWT.res +29 -0
  38. package/src/EventMapping_GWT.res.mjs +44 -0
  39. package/src/Filter.res +36 -0
  40. package/src/Filter.res.mjs +40 -0
  41. package/src/Flow_GWT.res +664 -0
  42. package/src/Flow_GWT.res.mjs +604 -0
  43. package/src/FormatterHuman.res +138 -0
  44. package/src/FormatterHuman.res.mjs +122 -0
  45. package/src/FormatterJson.res +257 -0
  46. package/src/FormatterJson.res.mjs +320 -0
  47. package/src/FormatterJunit.res +93 -0
  48. package/src/FormatterJunit.res.mjs +90 -0
  49. package/src/FormatterTap.res +139 -0
  50. package/src/FormatterTap.res.mjs +149 -0
  51. package/src/FormatterVsCode.res +362 -0
  52. package/src/FormatterVsCode.res.mjs +564 -0
  53. package/src/Hint.res +119 -0
  54. package/src/Hint.res.mjs +124 -0
  55. package/src/InboundTranslation_GWT.res +83 -0
  56. package/src/InboundTranslation_GWT.res.mjs +94 -0
  57. package/src/JestBind.res +70 -0
  58. package/src/JestBind.res.mjs +63 -0
  59. package/src/Loader.res +24 -0
  60. package/src/Loader.res.mjs +23 -0
  61. package/src/LocalHost.res +135 -0
  62. package/src/LocalHost.res.mjs +127 -0
  63. package/src/Mapping_GWT.res +474 -0
  64. package/src/Mapping_GWT.res.mjs +322 -0
  65. package/src/MultiSourceProjection_GWT.res +316 -0
  66. package/src/MultiSourceProjection_GWT.res.mjs +354 -0
  67. package/src/OutboundTranslation_GWT.res +238 -0
  68. package/src/OutboundTranslation_GWT.res.mjs +186 -0
  69. package/src/Outcome.res +161 -0
  70. package/src/Outcome.res.mjs +140 -0
  71. package/src/PackageScan.res +80 -0
  72. package/src/PackageScan.res.mjs +81 -0
  73. package/src/PlatformRunner.res +160 -0
  74. package/src/PlatformRunner.res.mjs +155 -0
  75. package/src/PlatformScan.res +138 -0
  76. package/src/PlatformScan.res.mjs +131 -0
  77. package/src/ProcessManager.res +91 -0
  78. package/src/ProcessManager.res.mjs +80 -0
  79. package/src/Projection_GWT.res +312 -0
  80. package/src/Projection_GWT.res.mjs +332 -0
  81. package/src/Query_GWT.res +379 -0
  82. package/src/Query_GWT.res.mjs +367 -0
  83. package/src/RenderRescript.res +125 -0
  84. package/src/RenderRescript.res.mjs +141 -0
  85. package/src/RunnerTypes.res +68 -0
  86. package/src/RunnerTypes.res.mjs +60 -0
  87. package/src/SideEffect_GWT.res +157 -0
  88. package/src/SideEffect_GWT.res.mjs +113 -0
  89. package/src/StateChange/ExternalAddCategorySlice.res +25 -0
  90. package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
  91. package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
  92. package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
  93. package/src/StateChange/WithFixtures.res +22 -0
  94. package/src/StateChange/WithFixtures.res.mjs +31 -0
  95. package/src/StateChange/WithFixtures_Behavior.res +17 -0
  96. package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
  97. package/src/StateChange/WithManualOpen.res +23 -0
  98. package/src/StateChange/WithManualOpen.res.mjs +31 -0
  99. package/src/StateChange/WithManualOpen_Behavior.res +17 -0
  100. package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
  101. package/src/StubRuntime.res +53 -0
  102. package/src/StubRuntime.res.mjs +54 -0
  103. package/src/TestFixtures.res +16 -0
  104. package/src/TestFixtures.res.mjs +34 -0
  105. package/src/Watch.res +92 -0
  106. package/src/Watch.res.mjs +83 -0
  107. package/src/WatcherProbe.res +48 -0
  108. package/src/WatcherProbe.res.mjs +46 -0
  109. package/tests/AutomationGwtTest.res +71 -0
  110. package/tests/AutomationGwtTest.res.mjs +189 -0
  111. package/tests/BuildClassifierTest.res +94 -0
  112. package/tests/BuildClassifierTest.res.mjs +132 -0
  113. package/tests/ComponentMetaTest.res +52 -0
  114. package/tests/ComponentMetaTest.res.mjs +57 -0
  115. package/tests/DelegateGwtTest.res +487 -0
  116. package/tests/DelegateGwtTest.res.mjs +891 -0
  117. package/tests/DiscoveryTest.res +42 -0
  118. package/tests/DiscoveryTest.res.mjs +43 -0
  119. package/tests/DomainDeadCodeTest.res +116 -0
  120. package/tests/DomainDeadCodeTest.res.mjs +155 -0
  121. package/tests/DomainGraphTest.res +365 -0
  122. package/tests/DomainGraphTest.res.mjs +413 -0
  123. package/tests/FlowAggregateGwtTest.res +169 -0
  124. package/tests/FlowAggregateGwtTest.res.mjs +269 -0
  125. package/tests/FlowCrossPluginGwtTest.res +340 -0
  126. package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
  127. package/tests/FlowGwtTest.res +222 -0
  128. package/tests/FlowGwtTest.res.mjs +432 -0
  129. package/tests/FormatterVsCodeMessageTest.res +41 -0
  130. package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
  131. package/tests/InboundTranslationGwtTest.res +34 -0
  132. package/tests/InboundTranslationGwtTest.res.mjs +98 -0
  133. package/tests/LocalHostIntegration.res +70 -0
  134. package/tests/LocalHostIntegration.res.mjs +64 -0
  135. package/tests/LocalHostTest.res +71 -0
  136. package/tests/LocalHostTest.res.mjs +63 -0
  137. package/tests/MappingGwtTest.res +348 -0
  138. package/tests/MappingGwtTest.res.mjs +592 -0
  139. package/tests/OutboundTranslationGwtTest.res +59 -0
  140. package/tests/OutboundTranslationGwtTest.res.mjs +138 -0
  141. package/tests/PlatformRunnerTest.res +66 -0
  142. package/tests/PlatformRunnerTest.res.mjs +85 -0
  143. package/tests/PlatformScanTest.res +61 -0
  144. package/tests/PlatformScanTest.res.mjs +56 -0
  145. package/tests/QueryGwtTest.res +161 -0
  146. package/tests/QueryGwtTest.res.mjs +314 -0
  147. package/tests/QueryResolverGwtTest.res +71 -0
  148. package/tests/QueryResolverGwtTest.res.mjs +136 -0
  149. package/tests/RunnerUnitTest.res +219 -0
  150. package/tests/RunnerUnitTest.res.mjs +244 -0
  151. package/tests/SideEffect/FakeOrderNotification.res +23 -0
  152. package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
  153. package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
  154. package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
  155. package/tests/SideEffect/MockEmail.res +18 -0
  156. package/tests/SideEffect/MockEmail.res.mjs +47 -0
  157. package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
  158. package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
  159. package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
  160. package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
  161. package/tests/StateChange/WithFixtures_GWT.res +23 -0
  162. package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
  163. package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
  164. package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
  165. package/tests/StateChange/WithManualOpen_GWT.res +17 -0
  166. package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
  167. package/tests/StateChangeSliceGwtTest.res +160 -0
  168. package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
  169. package/tests/StateViewSliceGwtTest.res +64 -0
  170. package/tests/StateViewSliceGwtTest.res.mjs +161 -0
  171. package/tests/WatchDebounceTest.res +78 -0
  172. package/tests/WatchDebounceTest.res.mjs +97 -0
@@ -0,0 +1,332 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Stream from "effect/Stream";
8
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
9
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
10
+ import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
11
+ import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
12
+ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
13
+ import * as Projection$ReventlessCore from "@reventlessdev/reventless-core/src/Projection.res.mjs";
14
+ import * as TestFixtures$ReventlessGwt from "./TestFixtures.res.mjs";
15
+
16
+ function Make(Spec) {
17
+ return Projection => {
18
+ S.enableJson();
19
+ let testId = {
20
+ contents: TestFixtures$ReventlessGwt.id
21
+ };
22
+ let describeWithId = (description, id, fn) => {
23
+ testId.contents = id;
24
+ JestBind$ReventlessGwt.describe(description, fn);
25
+ };
26
+ let test = (name, timeout, body) => JestBind$ReventlessGwt.testPromise(Spec.name, name, timeout, body);
27
+ let getSubId = state => Stdlib_Option.map(Spec.subIdConfig, param => param.getSubId(state));
28
+ let hasSubId = (state, subId) => Stdlib_Option.getOrThrow(getSubId(state), undefined) === subId;
29
+ let states = (store, id) => Stdlib_Option.getOr(store[id], []);
30
+ let setStates = (store, id, states) => {
31
+ store[id] = states;
32
+ };
33
+ let updateState = (store, id, subId, newState) => states(store, id).map(state => {
34
+ if (hasSubId(state, subId)) {
35
+ return newState;
36
+ } else {
37
+ return state;
38
+ }
39
+ });
40
+ let addState = (store, id, newState) => {
41
+ let subId = getSubId(newState);
42
+ let match = subId !== undefined ? (
43
+ states(store, id).some(state => hasSubId(state, subId)) ? [
44
+ updateState(store, id, subId, newState),
45
+ []
46
+ ] : [
47
+ states(store, id),
48
+ [newState]
49
+ ]
50
+ ) : [
51
+ states(store, id),
52
+ [newState]
53
+ ];
54
+ store[id] = match[0].concat(match[1]);
55
+ };
56
+ let deleteStates = (store, id) => {
57
+ store[id] = [];
58
+ };
59
+ let deleteSubState = (store, id, subId, getSubId) => {
60
+ store[id] = Stdlib_Option.getOr(Stdlib_Option.map(store[id], states => states.filter(state => Primitive_object.notequal(getSubId(state), subId))), []);
61
+ };
62
+ let update = async (store, events) => {
63
+ let actions = events.map(ev => Projection.project(ev)).flat();
64
+ await Projection$ReventlessCore.handleActions(actions, {
65
+ load: extra => Promise.resolve({
66
+ TAG: "Ok",
67
+ _0: states(store, extra)
68
+ }),
69
+ loadStream: id => Stream.fromIterable(states(store, id)),
70
+ save: (extra, extra$1, extra$2, extra$3) => {
71
+ let match = states(store, extra);
72
+ let match$1 = Spec.subIdConfig;
73
+ let exit = 0;
74
+ switch (extra$2) {
75
+ case "Init" :
76
+ if (match$1 !== undefined) {
77
+ addState(store, extra, extra$1);
78
+ return Promise.resolve({
79
+ TAG: "Ok",
80
+ _0: undefined
81
+ });
82
+ }
83
+ exit = 2;
84
+ break;
85
+ case "Overwrite" :
86
+ exit = 2;
87
+ break;
88
+ case "Any" :
89
+ if (match$1 !== undefined) {
90
+ addState(store, extra, extra$1);
91
+ return Promise.resolve({
92
+ TAG: "Ok",
93
+ _0: undefined
94
+ });
95
+ }
96
+ break;
97
+ }
98
+ if (exit === 2) {
99
+ let len = match.length;
100
+ if (len !== 1) {
101
+ if (len !== 0) {
102
+ return Promise.resolve({
103
+ TAG: "Error",
104
+ _0: "StaleState"
105
+ });
106
+ }
107
+ if (extra$2 !== "Init") {
108
+ return Promise.resolve({
109
+ TAG: "Error",
110
+ _0: "StaleState"
111
+ });
112
+ }
113
+ } else if (extra$2 === "Init") {
114
+ return Promise.resolve({
115
+ TAG: "Error",
116
+ _0: "StaleState"
117
+ });
118
+ }
119
+ }
120
+ setStates(store, extra, [extra$1]);
121
+ return Promise.resolve({
122
+ TAG: "Ok",
123
+ _0: undefined
124
+ });
125
+ },
126
+ saveBatch: extra => {
127
+ extra.forEach(param => addState(store, param[0], param[1]));
128
+ return Promise.resolve({
129
+ TAG: "Ok",
130
+ _0: undefined
131
+ });
132
+ },
133
+ count: async (param, param$1, param$2) => ({
134
+ TAG: "Ok",
135
+ _0: 0
136
+ }),
137
+ delete: (extra, extra$1) => {
138
+ let match = Spec.subIdConfig;
139
+ if (extra$1 !== undefined) {
140
+ if (match !== undefined) {
141
+ deleteSubState(store, extra, extra$1[1], match.getSubId);
142
+ return Promise.resolve({
143
+ TAG: "Ok",
144
+ _0: undefined
145
+ });
146
+ } else {
147
+ return Promise.resolve({
148
+ TAG: "Ok",
149
+ _0: undefined
150
+ });
151
+ }
152
+ } else {
153
+ deleteStates(store, extra);
154
+ return Promise.resolve({
155
+ TAG: "Ok",
156
+ _0: undefined
157
+ });
158
+ }
159
+ },
160
+ deleteBatch: extra => {
161
+ extra.forEach(param => {
162
+ let subId = param[1];
163
+ let id = param[0];
164
+ let match = Spec.subIdConfig;
165
+ if (subId !== undefined) {
166
+ if (match !== undefined) {
167
+ return deleteSubState(store, id, subId[1], match.getSubId);
168
+ } else {
169
+ return;
170
+ }
171
+ } else {
172
+ return deleteStates(store, id);
173
+ }
174
+ });
175
+ return Promise.resolve({
176
+ TAG: "Ok",
177
+ _0: undefined
178
+ });
179
+ }
180
+ }, Spec.subIdConfig);
181
+ if (Spec.subIdConfig !== undefined) {
182
+ return Stdlib_Dict.mapValues(store, states => states.toSorted((s1, s2) => Primitive_string.compare(getSubId(s1), getSubId(s2))));
183
+ } else {
184
+ return store;
185
+ }
186
+ };
187
+ let givenEvents = events => update({}, events);
188
+ let whenEvent = (store, event) => (() => (async () => await update(await store, [event]))());
189
+ let whenEvents = (store, events) => (() => (async () => await update(await store, events))());
190
+ let encState = s => Message$ReventlessCore.encode(s, Spec.stateSchema);
191
+ let statesEq = (a, b) => {
192
+ if (a.length === b.length) {
193
+ return Stdlib_Array.zip(a, b).every(param => {
194
+ let a = param[0];
195
+ let b = param[1];
196
+ return JSON.stringify(Message$ReventlessCore.encode(a, Spec.stateSchema)) === JSON.stringify(Message$ReventlessCore.encode(b, Spec.stateSchema));
197
+ });
198
+ } else {
199
+ return false;
200
+ }
201
+ };
202
+ let storeEq = (a, b) => {
203
+ let ka = Object.keys(a).toSorted(Primitive_string.compare);
204
+ let kb = Object.keys(b).toSorted(Primitive_string.compare);
205
+ if (Primitive_object.equal(ka, kb)) {
206
+ return ka.every(key => statesEq(Stdlib_Option.getOr(a[key], []), Stdlib_Option.getOr(b[key], [])));
207
+ } else {
208
+ return false;
209
+ }
210
+ };
211
+ let encStore = s => {
212
+ let obj = {};
213
+ Object.entries(s).forEach(param => {
214
+ obj[param[0]] = param[1].map(encState);
215
+ });
216
+ return obj;
217
+ };
218
+ let thenStates = async (thunk, expectedStates) => {
219
+ let store = await thunk();
220
+ let keys = Object.keys(store);
221
+ let actualId = keys[0];
222
+ let actualStates = Stdlib_Option.getOr(Object.values(store)[0], []);
223
+ if (keys.length === 1 && Primitive_object.equal(actualId, testId.contents) && statesEq(actualStates, expectedStates)) {
224
+ return Outcome$ReventlessGwt.pass;
225
+ } else {
226
+ return Outcome$ReventlessGwt.fail({
227
+ TAG: "StateMismatch",
228
+ key: testId.contents,
229
+ expected: expectedStates.map(encState),
230
+ actual: encStore(store)
231
+ });
232
+ }
233
+ };
234
+ let thenStatesWithId = async (thunk, id, expectedStates) => {
235
+ let store = await thunk();
236
+ let keys = Object.keys(store);
237
+ let actualId = keys[0];
238
+ let actualStates = Stdlib_Option.getOr(Object.values(store)[0], []);
239
+ if (keys.length === 1 && Primitive_object.equal(actualId, id) && statesEq(actualStates, expectedStates)) {
240
+ return Outcome$ReventlessGwt.pass;
241
+ } else {
242
+ return Outcome$ReventlessGwt.fail({
243
+ TAG: "StateMismatch",
244
+ key: id,
245
+ expected: expectedStates.map(encState),
246
+ actual: encStore(store)
247
+ });
248
+ }
249
+ };
250
+ let thenAllStates = async (thunk, expectedStore) => {
251
+ let store = await thunk();
252
+ if (storeEq(store, expectedStore)) {
253
+ return Outcome$ReventlessGwt.pass;
254
+ } else {
255
+ return Outcome$ReventlessGwt.fail({
256
+ TAG: "StateMismatch",
257
+ key: "<all>",
258
+ expected: encStore(expectedStore),
259
+ actual: encStore(store)
260
+ });
261
+ }
262
+ };
263
+ let thenState = (thunk, expectedState) => thenStates(thunk, [expectedState]);
264
+ let thenStateWithId = (thunk, id, expectedState) => thenStatesWithId(thunk, id, [expectedState]);
265
+ let thenNoState = async thunk => {
266
+ let store = await thunk();
267
+ let total = Stdlib_Array.reduce(Object.values(store), 0, (acc, states) => acc + states.length | 0);
268
+ if (total === 0) {
269
+ return Outcome$ReventlessGwt.pass;
270
+ } else {
271
+ return Outcome$ReventlessGwt.fail({
272
+ TAG: "StateMismatch",
273
+ key: "<any>",
274
+ expected: undefined,
275
+ actual: encStore(store)
276
+ });
277
+ }
278
+ };
279
+ let thenThrow = async thunk => {
280
+ let val;
281
+ try {
282
+ val = await thunk();
283
+ } catch (exn) {
284
+ return Outcome$ReventlessGwt.pass;
285
+ }
286
+ return Outcome$ReventlessGwt.fail({
287
+ TAG: "Throw",
288
+ error: "Expected thunk to throw but it returned normally",
289
+ stack: ""
290
+ });
291
+ };
292
+ let thenFail = async thunk => {
293
+ let val;
294
+ try {
295
+ val = await thunk();
296
+ } catch (exn) {
297
+ return Outcome$ReventlessGwt.pass;
298
+ }
299
+ return Outcome$ReventlessGwt.fail({
300
+ TAG: "Throw",
301
+ error: "Expected failure but thunk returned normally",
302
+ stack: ""
303
+ });
304
+ };
305
+ return {
306
+ Spec: Spec,
307
+ describe: JestBind$ReventlessGwt.describe,
308
+ describeWithId: describeWithId,
309
+ todo: JestBind$ReventlessGwt.todo,
310
+ test: test,
311
+ givenEvents: givenEvents,
312
+ whenEvent: whenEvent,
313
+ whenEvents: whenEvents,
314
+ thenStates: thenStates,
315
+ thenStatesWithId: thenStatesWithId,
316
+ thenAllStates: thenAllStates,
317
+ thenState: thenState,
318
+ thenStateWithId: thenStateWithId,
319
+ thenNoState: thenNoState,
320
+ thenThrow: thenThrow,
321
+ thenFail: thenFail
322
+ };
323
+ };
324
+ }
325
+
326
+ let handleActions = Projection$ReventlessCore.handleActions;
327
+
328
+ export {
329
+ handleActions,
330
+ Make,
331
+ }
332
+ /* S Not a pure module */
@@ -0,0 +1,379 @@
1
+ // Stage 6 — query-pattern GWT for ReadModels and StateViewSlices.
2
+ //
3
+ // Where `MultiSourceProjection_GWT` (multi-source ReadModel) and `Projection_GWT`
4
+ // (single-source StateViewSlice) cover the projection *write*
5
+ // side (event → state actions), `Query_GWT` covers the read side: what queries
6
+ // must the projected state support? Indexes, sub-IDs, and GraphQL resolvers
7
+ // all exist to answer specific query patterns — none of which are derivable
8
+ // from the projection function alone.
9
+ //
10
+ // The DSL accepts the minimal intersection of `ReadModel.Spec` and
11
+ // `StateViewSlice.Spec` (`name`, `state`, `stateSchema`, `config`,
12
+ // `subIdConfig`), so both kinds of consumers use the same combinator set.
13
+ // Adapter functors `FromReadModel` and `FromStateViewSlice` lift their
14
+ // respective specs into the shared shape — mirroring how `Mapping_GWT` lifts
15
+ // `Aggregate.Spec`/`StateChangeSlice.Spec` via `FromBehavior`/`FromStateChangeSlice`.
16
+
17
+ // -- Unified spec shape ------------------------------------------------------
18
+
19
+ module type QueryableSpec = {
20
+ let name: string
21
+
22
+ @schema
23
+ type state
24
+ let stateSchema: S.t<state>
25
+
26
+ let config: Reventless.ReadModel.config
27
+ let subIdConfig: option<Reventless.ReadModel.subIdConfig<state>>
28
+ }
29
+
30
+ module FromReadModel = (Spec: Reventless.ReadModel.Spec) => {
31
+ let name = Spec.name
32
+ type state = Spec.state
33
+ let stateSchema = Spec.stateSchema
34
+ let config = Spec.config
35
+ let subIdConfig = Spec.subIdConfig
36
+ }
37
+
38
+ module FromStateViewSlice = (Spec: Reventless.StateViewSlice.Spec) => {
39
+ let name = Spec.name
40
+ type state = Spec.state
41
+ let stateSchema = Spec.stateSchema
42
+ let config = Spec.config
43
+ let subIdConfig = Spec.subIdConfig
44
+ }
45
+
46
+ // -- Query argument shapes ---------------------------------------------------
47
+
48
+ type compositeId = {id: string, subId: string}
49
+
50
+ // A `whenQuery` scenario — `by` names the state field being filtered, `value`
51
+ // the expected value. `index` names a GSI when the field isn't the primary
52
+ // key. `filter` lets tests constrain the result set after the index lookup;
53
+ // `limit` truncates (used with `thenRowCount` for pagination).
54
+ type queryArgs<'state> = {
55
+ by: string,
56
+ value: string,
57
+ index?: string,
58
+ filter?: 'state => bool,
59
+ limit?: int,
60
+ }
61
+
62
+ // -- Query_GWT DSL ----------------------------------------------------------
63
+
64
+ module type T = {
65
+ module Spec: QueryableSpec
66
+
67
+ let describe: (string, unit => unit) => unit
68
+ let test: (string, unit => Outcome.outcome) => unit
69
+
70
+ type store
71
+ let givenStore: array<(string, Spec.state)> => store
72
+ let givenCompositeStore: array<(string, string, Spec.state)> => store
73
+
74
+ let whenQueryById: (store, string) => option<Spec.state>
75
+ let whenQueryByCompositeId: (store, compositeId) => result<option<Spec.state>, Outcome.mismatch>
76
+ let whenQuery: (store, queryArgs<Spec.state>) => result<array<Spec.state>, Outcome.mismatch>
77
+
78
+ let thenRow: (option<Spec.state>, option<Spec.state>) => Outcome.outcome
79
+ let thenRows: (result<array<Spec.state>, Outcome.mismatch>, array<Spec.state>) => Outcome.outcome
80
+ let thenRowCount: (result<array<Spec.state>, Outcome.mismatch>, int) => Outcome.outcome
81
+ let thenRowFromComposite: (
82
+ result<option<Spec.state>, Outcome.mismatch>,
83
+ option<Spec.state>,
84
+ ) => Outcome.outcome
85
+ }
86
+
87
+ module Make = (Spec: QueryableSpec): (T with module Spec = Spec) => {
88
+ module Spec = Spec
89
+
90
+ S.enableJson()
91
+
92
+ let describe = JestBind.describe
93
+ let test = (name, body) => JestBind.test(~slice=Spec.name, name, body)
94
+
95
+ type row = {id: string, subId: option<string>, state: Spec.state}
96
+ type store = array<row>
97
+
98
+ let encState = (s: Spec.state): JSON.t => s->S.reverseConvertToJsonOrThrow(Spec.stateSchema)
99
+ let encStates = arr => arr->Array.map(encState)
100
+
101
+ let givenStore = pairs =>
102
+ pairs->Array.map(((id, state)) => {id, subId: None, state})
103
+
104
+ let givenCompositeStore = triples =>
105
+ triples->Array.map(((id, subId, state)) => {id, subId: Some(subId), state})
106
+
107
+ // `whenQueryById` ignores `subId` — returns the first row with the given id.
108
+ // Callers who want composite-id lookup go through `whenQueryByCompositeId`.
109
+ let whenQueryById = (s: store, id) =>
110
+ s->Array.findMap(r => if r.id == id {Some(r.state)} else {None})
111
+
112
+ let whenQueryByCompositeId = (s: store, {id, subId}: compositeId): result<
113
+ option<Spec.state>,
114
+ Outcome.mismatch,
115
+ > =>
116
+ switch Spec.subIdConfig {
117
+ | None =>
118
+ Error(
119
+ Outcome.QueryRowsMismatch({
120
+ expected: [JSON.Encode.string(
121
+ `composite-id lookup requires subIdConfig = Some({subIdField, getSubId}) in ${Spec.name}.config`,
122
+ )],
123
+ actual: [],
124
+ }),
125
+ )
126
+ | Some(_) =>
127
+ Ok(
128
+ s->Array.findMap(r =>
129
+ if r.id == id && r.subId == Some(subId) {
130
+ Some(r.state)
131
+ } else {
132
+ None
133
+ }
134
+ ),
135
+ )
136
+ }
137
+
138
+ // Does the schema declare an index with this name whose pk covers `field`?
139
+ let indexCovers = (indexName: string, field: string): bool =>
140
+ Spec.config.indexes->Array.some(idx =>
141
+ idx.index == indexName &&
142
+ (idx.idField == Some(field) ||
143
+ switch idx.pkFields {
144
+ | Some(fields) => fields->Array.includes(field)
145
+ | None => false
146
+ })
147
+ )
148
+
149
+ let whenQuery = (s: store, args: queryArgs<Spec.state>): result<
150
+ array<Spec.state>,
151
+ Outcome.mismatch,
152
+ > => {
153
+ // Config validation: if the caller named an `index`, verify the schema
154
+ // actually declares it. Without this, the test could "pass" against an
155
+ // in-memory store while the production config would reject the query.
156
+ let indexError = switch args.index {
157
+ | Some(name) if !indexCovers(name, args.by) =>
158
+ Some(
159
+ Outcome.QueryRowsMismatch({
160
+ expected: [
161
+ JSON.Encode.string(
162
+ `index "${name}" covering field "${args.by}" is missing from ${Spec.name}.config.indexes`,
163
+ ),
164
+ ],
165
+ actual: [],
166
+ }),
167
+ )
168
+ | _ => None
169
+ }
170
+
171
+ switch indexError {
172
+ | Some(err) => Error(err)
173
+ | None =>
174
+ let field = args.by
175
+ let expected = args.value
176
+ let matches = s->Array.filter(r => {
177
+ let json = r.state->S.reverseConvertToJsonOrThrow(Spec.stateSchema)
178
+ switch json->JSON.Decode.object->Option.flatMap(d => d->Dict.get(field)) {
179
+ | Some(v) =>
180
+ let actualStr = switch v {
181
+ | JSON.String(s) => s
182
+ | JSON.Number(n) => n->Float.toString
183
+ | JSON.Boolean(b) => b ? "true" : "false"
184
+ | _ => v->JSON.stringify
185
+ }
186
+ actualStr == expected
187
+ | None => false
188
+ }
189
+ })
190
+ let afterFilter = switch args.filter {
191
+ | Some(p) => matches->Array.filter(r => p(r.state))
192
+ | None => matches
193
+ }
194
+ let afterLimit = switch args.limit {
195
+ | Some(n) => afterFilter->Array.slice(~start=0, ~end=n)
196
+ | None => afterFilter
197
+ }
198
+ Ok(afterLimit->Array.map(r => r.state))
199
+ }
200
+ }
201
+
202
+ // -- Then combinators ----------------------------------------------------
203
+
204
+ let thenRow = (actual: option<Spec.state>, expected: option<Spec.state>): Outcome.outcome =>
205
+ if actual == expected {
206
+ Outcome.pass
207
+ } else {
208
+ let actualJson = actual->Option.mapOr([], s => [encState(s)])
209
+ let expectedJson = expected->Option.mapOr([], s => [encState(s)])
210
+ Outcome.fail(QueryRowsMismatch({expected: expectedJson, actual: actualJson}))
211
+ }
212
+
213
+ let thenRows = (
214
+ result: result<array<Spec.state>, Outcome.mismatch>,
215
+ expected: array<Spec.state>,
216
+ ): Outcome.outcome =>
217
+ switch result {
218
+ | Error(m) => Outcome.fail(m)
219
+ | Ok(actual) =>
220
+ if actual == expected {
221
+ Outcome.pass
222
+ } else {
223
+ Outcome.fail(
224
+ QueryRowsMismatch({expected: encStates(expected), actual: encStates(actual)}),
225
+ )
226
+ }
227
+ }
228
+
229
+ let thenRowCount = (
230
+ result: result<array<Spec.state>, Outcome.mismatch>,
231
+ expected: int,
232
+ ): Outcome.outcome =>
233
+ switch result {
234
+ | Error(m) => Outcome.fail(m)
235
+ | Ok(actual) =>
236
+ if actual->Array.length == expected {
237
+ Outcome.pass
238
+ } else {
239
+ Outcome.fail(
240
+ QueryRowsMismatch({
241
+ expected: [JSON.Encode.string(`row count = ${expected->Int.toString}`)],
242
+ actual: [JSON.Encode.string(`row count = ${actual->Array.length->Int.toString}`)],
243
+ }),
244
+ )
245
+ }
246
+ }
247
+
248
+ let thenRowFromComposite = (
249
+ result: result<option<Spec.state>, Outcome.mismatch>,
250
+ expected: option<Spec.state>,
251
+ ): Outcome.outcome =>
252
+ switch result {
253
+ | Error(m) => Outcome.fail(m)
254
+ | Ok(actual) => thenRow(actual, expected)
255
+ }
256
+ }
257
+
258
+ // -- Cross-spec resolvers ----------------------------------------------------
259
+ //
260
+ // `@resolves` / `@resolvesMany` declare GraphQL resolvers that join a row in one
261
+ // read model to row(s) in a foreign table by a `*Id` / `*Ids` field. The
262
+ // single-spec `Make` above can't exercise these — the join target lives in a
263
+ // different spec. `MakeResolver(Primary, Target)` lifts both into one scenario:
264
+ // a primary store with the foreign-key field, a target store keyed by id, and
265
+ // `whenResolve` / `whenResolveMany` that follow the key across the boundary.
266
+ //
267
+ // The DSL validates that `Primary.config` actually declares the resolver for the
268
+ // field — so a test fails if the `@resolves` annotation is missing, exactly as
269
+ // `whenQuery` validates a named index.
270
+ module MakeResolver = (Primary: QueryableSpec, Target: QueryableSpec) => {
271
+ S.enableJson()
272
+
273
+ let describe = JestBind.describe
274
+ let sliceName = `${Primary.name}→${Target.name}`
275
+ let test = (name, body) => JestBind.test(~slice=sliceName, name, body)
276
+
277
+ type scenario = {
278
+ primary: array<(string, Primary.state)>,
279
+ target: array<(string, Target.state)>,
280
+ }
281
+
282
+ let givenStores = (primary, target): scenario => {primary, target}
283
+
284
+ let encTarget = (s: Target.state): JSON.t => s->S.reverseConvertToJsonOrThrow(Target.stateSchema)
285
+ let encTargets = arr => arr->Array.map(encTarget)
286
+
287
+ let resolverFor = field =>
288
+ Primary.config.idResolvers->Array.find(r => r.source.idField == field)
289
+ let resolverManyFor = field =>
290
+ Primary.config.idsResolvers->Array.find(r => r.source.idsField == field)
291
+
292
+ let missing = msg =>
293
+ Outcome.QueryRowsMismatch({expected: [JSON.Encode.string(msg)], actual: []})
294
+
295
+ // Read a JSON field off the primary row's encoded state.
296
+ let readField = (state: Primary.state, field): option<JSON.t> =>
297
+ state
298
+ ->S.reverseConvertToJsonOrThrow(Primary.stateSchema)
299
+ ->JSON.Decode.object
300
+ ->Option.flatMap(d => d->Dict.get(field))
301
+
302
+ let jsonToStr = v =>
303
+ switch v {
304
+ | JSON.String(s) => s
305
+ | JSON.Number(n) => n->Float.toString
306
+ | _ => v->JSON.stringify
307
+ }
308
+
309
+ let lookupTarget = (target, fkId) =>
310
+ target->Array.findMap(((id, st)) => id == fkId ? Some(st) : None)
311
+
312
+ let whenResolve = (scenario: scenario, ~field, primaryId): result<
313
+ option<Target.state>,
314
+ Outcome.mismatch,
315
+ > =>
316
+ switch resolverFor(field) {
317
+ | None =>
318
+ Error(missing(`@resolves on field "${field}" is missing from ${Primary.name}.config.idResolvers`))
319
+ | Some(_) =>
320
+ switch scenario.primary->Array.findMap(((id, st)) => id == primaryId ? Some(st) : None) {
321
+ | None => Ok(None)
322
+ | Some(pstate) =>
323
+ switch readField(pstate, field) {
324
+ | None => Ok(None)
325
+ | Some(fk) => Ok(lookupTarget(scenario.target, jsonToStr(fk)))
326
+ }
327
+ }
328
+ }
329
+
330
+ let whenResolveMany = (scenario: scenario, ~field, primaryId): result<
331
+ array<Target.state>,
332
+ Outcome.mismatch,
333
+ > =>
334
+ switch resolverManyFor(field) {
335
+ | None =>
336
+ Error(
337
+ missing(`@resolvesMany on field "${field}" is missing from ${Primary.name}.config.idsResolvers`),
338
+ )
339
+ | Some(_) =>
340
+ switch scenario.primary->Array.findMap(((id, st)) => id == primaryId ? Some(st) : None) {
341
+ | None => Ok([])
342
+ | Some(pstate) =>
343
+ switch readField(pstate, field) {
344
+ | Some(JSON.Array(arr)) =>
345
+ Ok(arr->Array.map(jsonToStr)->Array.filterMap(fkId => lookupTarget(scenario.target, fkId)))
346
+ | _ => Ok([])
347
+ }
348
+ }
349
+ }
350
+
351
+ let thenResolved = (
352
+ result: result<option<Target.state>, Outcome.mismatch>,
353
+ expected: option<Target.state>,
354
+ ): Outcome.outcome =>
355
+ switch result {
356
+ | Error(m) => Outcome.fail(m)
357
+ | Ok(actual) =>
358
+ actual == expected
359
+ ? Outcome.pass
360
+ : Outcome.fail(
361
+ QueryRowsMismatch({
362
+ expected: expected->Option.mapOr([], s => [encTarget(s)]),
363
+ actual: actual->Option.mapOr([], s => [encTarget(s)]),
364
+ }),
365
+ )
366
+ }
367
+
368
+ let thenResolvedMany = (
369
+ result: result<array<Target.state>, Outcome.mismatch>,
370
+ expected: array<Target.state>,
371
+ ): Outcome.outcome =>
372
+ switch result {
373
+ | Error(m) => Outcome.fail(m)
374
+ | Ok(actual) =>
375
+ actual == expected
376
+ ? Outcome.pass
377
+ : Outcome.fail(QueryRowsMismatch({expected: encTargets(expected), actual: encTargets(actual)}))
378
+ }
379
+ }