@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,138 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as OutboundTranslation_GWT$ReventlessGwt from "../src/OutboundTranslation_GWT.res.mjs";
5
+
6
+ let consumedEventSchema = S.schema(s => ({
7
+ TAG: "OrderShipped",
8
+ orderId: s.m(S.string),
9
+ email: s.m(S.string)
10
+ }));
11
+
12
+ let outboundItemSchema = S.schema(s => ({
13
+ orderId: s.m(S.string),
14
+ email: s.m(S.string)
15
+ }));
16
+
17
+ let inboundCommandSchema = S.literal("NoOp");
18
+
19
+ function collect(event) {
20
+ let orderId = event.orderId;
21
+ return [[
22
+ orderId,
23
+ {
24
+ orderId: orderId,
25
+ email: event.email
26
+ }
27
+ ]];
28
+ }
29
+
30
+ let SendTrackingEmailSlice = {
31
+ name: "SendTrackingEmail",
32
+ consumedEventSchema: consumedEventSchema,
33
+ outboundItemSchema: outboundItemSchema,
34
+ inboundCommandSchema: inboundCommandSchema,
35
+ collect: collect
36
+ };
37
+
38
+ let include = OutboundTranslation_GWT$ReventlessGwt.Make(SendTrackingEmailSlice);
39
+
40
+ let describe = include.describe;
41
+
42
+ let test = include.test;
43
+
44
+ let givenEvent = include.givenEvent;
45
+
46
+ let whenCollect = include.whenCollect;
47
+
48
+ let thenTodos = include.thenTodos;
49
+
50
+ let givenTodo = include.givenTodo;
51
+
52
+ let whenTranslateMocked = include.whenTranslateMocked;
53
+
54
+ let whenTranslateRetrying = include.whenTranslateRetrying;
55
+
56
+ let thenRetryRecorded = include.thenRetryRecorded;
57
+
58
+ let thenTodoStatus = include.thenTodoStatus;
59
+
60
+ describe("SendTrackingEmail OutboundTranslationSlice", () => {
61
+ test("collect: OrderShipped queues an outbound TODO", undefined, async () => thenTodos(whenCollect(givenEvent({
62
+ TAG: "OrderShipped",
63
+ orderId: "o1",
64
+ email: "x@y"
65
+ })), [[
66
+ "o1",
67
+ {
68
+ orderId: "o1",
69
+ email: "x@y"
70
+ }
71
+ ]]));
72
+ test("translate success is fire-and-forget → #Completed", undefined, () => thenTodoStatus(whenTranslateMocked(givenTodo("o1", {
73
+ orderId: "o1",
74
+ email: "x@y"
75
+ }), (_id, _item) => Promise.resolve({
76
+ TAG: "Ok",
77
+ _0: undefined
78
+ })), "o1", "Completed"));
79
+ test("translate failure records a retry → #Pending", undefined, () => thenTodoStatus(whenTranslateMocked(givenTodo("o1", {
80
+ orderId: "o1",
81
+ email: "x@y"
82
+ }), (_id, _item) => Promise.resolve({
83
+ TAG: "Error",
84
+ _0: "smtp down"
85
+ })), "o1", "Pending"));
86
+ test("retrying translate succeeds on the third attempt → 2 retries recorded", undefined, () => {
87
+ let calls = {
88
+ contents: 0
89
+ };
90
+ return thenRetryRecorded(whenTranslateRetrying(givenTodo("o1", {
91
+ orderId: "o1",
92
+ email: "x@y"
93
+ }), 3, (_id, _item) => {
94
+ calls.contents = calls.contents + 1 | 0;
95
+ return Promise.resolve(calls.contents < 3 ? ({
96
+ TAG: "Error",
97
+ _0: "smtp down"
98
+ }) : ({
99
+ TAG: "Ok",
100
+ _0: undefined
101
+ }));
102
+ }), 2);
103
+ });
104
+ test("translate that keeps failing exhausts maxRetries", undefined, () => thenRetryRecorded(whenTranslateRetrying(givenTodo("o1", {
105
+ orderId: "o1",
106
+ email: "x@y"
107
+ }), 3, (_id, _item) => Promise.resolve({
108
+ TAG: "Error",
109
+ _0: "smtp down"
110
+ })), 3));
111
+ });
112
+
113
+ let Spec = include.Spec;
114
+
115
+ let testSync = include.testSync;
116
+
117
+ let thenCommand = include.thenCommand;
118
+
119
+ let thenNoCommand = include.thenNoCommand;
120
+
121
+ export {
122
+ SendTrackingEmailSlice,
123
+ Spec,
124
+ describe,
125
+ test,
126
+ testSync,
127
+ givenEvent,
128
+ whenCollect,
129
+ thenTodos,
130
+ givenTodo,
131
+ whenTranslateMocked,
132
+ whenTranslateRetrying,
133
+ thenCommand,
134
+ thenNoCommand,
135
+ thenRetryRecorded,
136
+ thenTodoStatus,
137
+ }
138
+ /* consumedEventSchema Not a pure module */
@@ -0,0 +1,66 @@
1
+ // Unit tests for PlatformRunner.classifyLine — the pure line classifier that turns
2
+ // the spawned reventless-local platform's stdout into structured runner events
3
+ // (features plan Phase 9). The tap emits sentinel-prefixed JSON; the Domain
4
+ // GraphQL "listening" line marks readiness; everything else is a passthrough log.
5
+
6
+ open JestGlobals
7
+
8
+ describe("PlatformRunner.classifyLine", () => {
9
+ testPromise("classifies a tap line as Domain with the parsed payload", async () => {
10
+ let line =
11
+ `@@RVLESS_EVT@@ {"event":"domainEvent","seq":1,"topic":"CatalogEventTopic","service":"CatalogDcbEventLog","payload":{"event":{"TAG":"ProductAdded"}},"ts":"2026-06-07T00:00:00.000Z"}`
12
+ switch PlatformRunner.classifyLine(line) {
13
+ | Domain(json) =>
14
+ let topic =
15
+ json
16
+ ->JSON.Decode.object
17
+ ->Option.flatMap(d => d->Dict.get("topic"))
18
+ ->Option.flatMap(JSON.Decode.string)
19
+ expect(topic)->toEqual(Some("CatalogEventTopic"))
20
+ | _ => expect("not Domain")->toEqual("Domain")
21
+ }
22
+ })
23
+
24
+ testPromise("a malformed tap line falls back to Log (never throws)", async () => {
25
+ switch PlatformRunner.classifyLine("@@RVLESS_EVT@@ {not json") {
26
+ | Log(_) => expect(true)->toBe(true)
27
+ | _ => expect("not Log")->toEqual("Log")
28
+ }
29
+ })
30
+
31
+ testPromise("classifies the Domain GraphQL listening line as Ready", async () => {
32
+ // Real platform output carries ANSI escapes around the [GraphQL:Domain] tag;
33
+ // the substring checks see through them.
34
+ let line = `12:00:00 I [GraphQL:Domain] listening on http://localhost:4100/graphql (SDL: /sdl)`
35
+ switch PlatformRunner.classifyLine(line) {
36
+ | Ready => expect(true)->toBe(true)
37
+ | _ => expect("not Ready")->toEqual("Ready")
38
+ }
39
+ })
40
+
41
+ testPromise("the Platform (not Domain) listening line is NOT Ready", async () => {
42
+ let line = `12:00:00 I [GraphQL:Platform] listening on http://localhost:4101/graphql`
43
+ switch PlatformRunner.classifyLine(line) {
44
+ | Log(_) => expect(true)->toBe(true)
45
+ | _ => expect("expected Log")->toEqual("Log")
46
+ }
47
+ })
48
+
49
+ testPromise("an arbitrary log line is Log", async () => {
50
+ switch PlatformRunner.classifyLine("12:00:00 I [Catalog][Aggregate(Product)] handling command") {
51
+ | Log(_) => expect(true)->toBe(true)
52
+ | _ => expect("expected Log")->toEqual("Log")
53
+ }
54
+ })
55
+
56
+ testPromise("a Log line preserves its ANSI colour/bold escapes (rendered in the terminal)", async () => {
57
+ // The child emits coloured logs; the Log payload keeps the ANSI so the
58
+ // extension's Pseudoterminal renders colour/bold. Build real ESC (0x1b) bytes.
59
+ let esc = String.fromCharCode(27)
60
+ let line = `12:00:00 ${esc}[36mI${esc}[0m ${esc}[1m[Catalog]${esc}[0m handling command`
61
+ switch PlatformRunner.classifyLine(line) {
62
+ | Log(raw) => expect(raw)->toEqual(line)
63
+ | _ => expect("expected Log")->toEqual("Log")
64
+ }
65
+ })
66
+ })
@@ -0,0 +1,85 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as PlatformRunner$ReventlessGwt from "../src/PlatformRunner.res.mjs";
6
+
7
+ globalThis.describe("PlatformRunner.classifyLine", () => {
8
+ globalThis.test("classifies a tap line as Domain with the parsed payload", async () => {
9
+ let json = PlatformRunner$ReventlessGwt.classifyLine(`@@RVLESS_EVT@@ {"event":"domainEvent","seq":1,"topic":"CatalogEventTopic","service":"CatalogDcbEventLog","payload":{"event":{"TAG":"ProductAdded"}},"ts":"2026-06-07T00:00:00.000Z"}`);
10
+ if (typeof json !== "object") {
11
+ globalThis.expect("not Domain").toEqual("Domain");
12
+ return;
13
+ }
14
+ if (json.TAG === "Domain") {
15
+ let topic = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json._0), d => d["topic"]), Stdlib_JSON.Decode.string);
16
+ globalThis.expect(topic).toEqual("CatalogEventTopic");
17
+ return;
18
+ }
19
+ globalThis.expect("not Domain").toEqual("Domain");
20
+ });
21
+ globalThis.test("a malformed tap line falls back to Log (never throws)", async () => {
22
+ let match = PlatformRunner$ReventlessGwt.classifyLine("@@RVLESS_EVT@@ {not json");
23
+ if (typeof match !== "object") {
24
+ globalThis.expect("not Log").toEqual("Log");
25
+ return;
26
+ }
27
+ if (match.TAG === "Domain") {
28
+ globalThis.expect("not Log").toEqual("Log");
29
+ return;
30
+ }
31
+ globalThis.expect(true).toBe(true);
32
+ });
33
+ globalThis.test("classifies the Domain GraphQL listening line as Ready", async () => {
34
+ let match = PlatformRunner$ReventlessGwt.classifyLine(`12:00:00 I [GraphQL:Domain] listening on http://localhost:4100/graphql (SDL: /sdl)`);
35
+ if (typeof match !== "object") {
36
+ globalThis.expect(true).toBe(true);
37
+ return;
38
+ }
39
+ if (match.TAG === "Domain") {
40
+ globalThis.expect("not Ready").toEqual("Ready");
41
+ return;
42
+ }
43
+ globalThis.expect("not Ready").toEqual("Ready");
44
+ });
45
+ globalThis.test("the Platform (not Domain) listening line is NOT Ready", async () => {
46
+ let match = PlatformRunner$ReventlessGwt.classifyLine(`12:00:00 I [GraphQL:Platform] listening on http://localhost:4101/graphql`);
47
+ if (typeof match !== "object") {
48
+ globalThis.expect("expected Log").toEqual("Log");
49
+ return;
50
+ }
51
+ if (match.TAG === "Domain") {
52
+ globalThis.expect("expected Log").toEqual("Log");
53
+ return;
54
+ }
55
+ globalThis.expect(true).toBe(true);
56
+ });
57
+ globalThis.test("an arbitrary log line is Log", async () => {
58
+ let match = PlatformRunner$ReventlessGwt.classifyLine("12:00:00 I [Catalog][Aggregate(Product)] handling command");
59
+ if (typeof match !== "object") {
60
+ globalThis.expect("expected Log").toEqual("Log");
61
+ return;
62
+ }
63
+ if (match.TAG === "Domain") {
64
+ globalThis.expect("expected Log").toEqual("Log");
65
+ return;
66
+ }
67
+ globalThis.expect(true).toBe(true);
68
+ });
69
+ globalThis.test("a Log line preserves its ANSI colour/bold escapes (rendered in the terminal)", async () => {
70
+ let esc = String.fromCharCode(27);
71
+ let line = `12:00:00 ` + esc + `[36mI` + esc + `[0m ` + esc + `[1m[Catalog]` + esc + `[0m handling command`;
72
+ let raw = PlatformRunner$ReventlessGwt.classifyLine(line);
73
+ if (typeof raw !== "object") {
74
+ globalThis.expect("expected Log").toEqual("Log");
75
+ return;
76
+ }
77
+ if (raw.TAG === "Domain") {
78
+ globalThis.expect("expected Log").toEqual("Log");
79
+ return;
80
+ }
81
+ globalThis.expect(raw._0).toEqual(line);
82
+ });
83
+ });
84
+
85
+ /* Not a pure module */
@@ -0,0 +1,61 @@
1
+ // Unit tests for PlatformScan.matchPlatform — the pure predicate deciding whether
2
+ // a package.json + presence of src/Main.res.mjs make a launchable reventless-local
3
+ // platform package, and which serve script to prefer (features plan Phase 9).
4
+
5
+ open JestGlobals
6
+
7
+ let pkgJson = (~deps=`{}`, ~scripts=`{}`) =>
8
+ `{"name":"@app/platform-local","dependencies":${deps},"scripts":${scripts}}`
9
+
10
+ describe("PlatformScan.matchPlatform", () => {
11
+ testPromise("matches when it depends on reventless-local and Main.res.mjs exists", async () => {
12
+ let text = pkgJson(~deps=`{"@reventlessdev/reventless-local":"workspace:*"}`)
13
+ switch PlatformScan.matchPlatform(~pkgJsonText=text, ~mainExists=true) {
14
+ | Some((name, _)) => expect(name)->toEqual("@app/platform-local")
15
+ | None => expect("None")->toEqual("Some")
16
+ }
17
+ })
18
+
19
+ testPromise("no match when src/Main.res.mjs is absent", async () => {
20
+ let text = pkgJson(~deps=`{"@reventlessdev/reventless-local":"workspace:*"}`)
21
+ expect(PlatformScan.matchPlatform(~pkgJsonText=text, ~mainExists=false)->Option.isNone)->toBe(
22
+ true,
23
+ )
24
+ })
25
+
26
+ testPromise("no match without the reventless-local dependency", async () => {
27
+ let text = pkgJson(~deps=`{"sury":"^11"}`)
28
+ expect(PlatformScan.matchPlatform(~pkgJsonText=text, ~mainExists=true)->Option.isNone)->toBe(
29
+ true,
30
+ )
31
+ })
32
+
33
+ testPromise("prefers the serve:memory script over serve", async () => {
34
+ let text = pkgJson(
35
+ ~deps=`{"@reventlessdev/reventless-local":"workspace:*"}`,
36
+ ~scripts=`{"serve":"tsx Main","serve:memory":"REVENTLESS_LOCAL_BACKEND=memory tsx Main"}`,
37
+ )
38
+ switch PlatformScan.matchPlatform(~pkgJsonText=text, ~mainExists=true) {
39
+ | Some((_, serveScript)) => expect(serveScript)->toEqual(Some("serve:memory"))
40
+ | None => expect("None")->toEqual("Some")
41
+ }
42
+ })
43
+
44
+ testPromise("falls back to serve when serve:memory is absent", async () => {
45
+ let text = pkgJson(
46
+ ~deps=`{"@reventlessdev/reventless-local":"workspace:*"}`,
47
+ ~scripts=`{"serve":"tsx Main"}`,
48
+ )
49
+ switch PlatformScan.matchPlatform(~pkgJsonText=text, ~mainExists=true) {
50
+ | Some((_, serveScript)) => expect(serveScript)->toEqual(Some("serve"))
51
+ | None => expect("None")->toEqual("Some")
52
+ }
53
+ })
54
+
55
+ testPromise("matches via devDependencies too", async () => {
56
+ let text = `{"name":"p","devDependencies":{"@reventlessdev/reventless-local":"*"}}`
57
+ expect(PlatformScan.matchPlatform(~pkgJsonText=text, ~mainExists=true)->Option.isSome)->toBe(
58
+ true,
59
+ )
60
+ })
61
+ })
@@ -0,0 +1,56 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as PlatformScan$ReventlessGwt from "../src/PlatformScan.res.mjs";
5
+
6
+ function pkgJson(depsOpt, scriptsOpt) {
7
+ let deps = depsOpt !== undefined ? depsOpt : `{}`;
8
+ let scripts = scriptsOpt !== undefined ? scriptsOpt : `{}`;
9
+ return `{"name":"@app/platform-local","dependencies":` + deps + `,"scripts":` + scripts + `}`;
10
+ }
11
+
12
+ globalThis.describe("PlatformScan.matchPlatform", () => {
13
+ globalThis.test("matches when it depends on reventless-local and Main.res.mjs exists", async () => {
14
+ let text = pkgJson(`{"@reventlessdev/reventless-local":"workspace:*"}`, undefined);
15
+ let match = PlatformScan$ReventlessGwt.matchPlatform(text, true);
16
+ if (match !== undefined) {
17
+ globalThis.expect(match[0]).toEqual("@app/platform-local");
18
+ } else {
19
+ globalThis.expect("None").toEqual("Some");
20
+ }
21
+ });
22
+ globalThis.test("no match when src/Main.res.mjs is absent", async () => {
23
+ let text = pkgJson(`{"@reventlessdev/reventless-local":"workspace:*"}`, undefined);
24
+ globalThis.expect(Stdlib_Option.isNone(PlatformScan$ReventlessGwt.matchPlatform(text, false))).toBe(true);
25
+ });
26
+ globalThis.test("no match without the reventless-local dependency", async () => {
27
+ let text = pkgJson(`{"sury":"^11"}`, undefined);
28
+ globalThis.expect(Stdlib_Option.isNone(PlatformScan$ReventlessGwt.matchPlatform(text, true))).toBe(true);
29
+ });
30
+ globalThis.test("prefers the serve:memory script over serve", async () => {
31
+ let text = pkgJson(`{"@reventlessdev/reventless-local":"workspace:*"}`, `{"serve":"tsx Main","serve:memory":"REVENTLESS_LOCAL_BACKEND=memory tsx Main"}`);
32
+ let match = PlatformScan$ReventlessGwt.matchPlatform(text, true);
33
+ if (match !== undefined) {
34
+ globalThis.expect(match[1]).toEqual("serve:memory");
35
+ } else {
36
+ globalThis.expect("None").toEqual("Some");
37
+ }
38
+ });
39
+ globalThis.test("falls back to serve when serve:memory is absent", async () => {
40
+ let text = pkgJson(`{"@reventlessdev/reventless-local":"workspace:*"}`, `{"serve":"tsx Main"}`);
41
+ let match = PlatformScan$ReventlessGwt.matchPlatform(text, true);
42
+ if (match !== undefined) {
43
+ globalThis.expect(match[1]).toEqual("serve");
44
+ } else {
45
+ globalThis.expect("None").toEqual("Some");
46
+ }
47
+ });
48
+ globalThis.test("matches via devDependencies too", async () => {
49
+ globalThis.expect(Stdlib_Option.isSome(PlatformScan$ReventlessGwt.matchPlatform(`{"name":"p","devDependencies":{"@reventlessdev/reventless-local":"*"}}`, true))).toBe(true);
50
+ });
51
+ });
52
+
53
+ export {
54
+ pkgJson,
55
+ }
56
+ /* Not a pure module */
@@ -0,0 +1,161 @@
1
+ // Worked examples for Query_GWT — covers both a ReadModel and a StateViewSlice
2
+ // to prove the unified DSL works for either consumer type. Stage 6 of
3
+ // `docs/plans/reventless-gwt.md`.
4
+
5
+ S.enableJson()
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Case 1 — ReadModel: Categories.
9
+ // ---------------------------------------------------------------------------
10
+
11
+ module CategoriesReadModel = {
12
+ module Id = Reventless.Id.StringPure
13
+ let name = "Categories"
14
+ let moduleUrl = ""
15
+
16
+ @schema
17
+ type state = {categoryId: string, name: string, archived: bool}
18
+
19
+ let config = Reventless.ReadModel.config(
20
+ ~indexes=[
21
+ {
22
+ index: "byName",
23
+ type_: "S",
24
+ idField: "name",
25
+ projectionType: ALL,
26
+ },
27
+ ],
28
+ )
29
+ let subIdConfig = None
30
+ }
31
+
32
+ module CategoriesQuery = Query_GWT.Make(Query_GWT.FromReadModel(CategoriesReadModel))
33
+
34
+ CategoriesQuery.describe("Categories ReadModel queries", () => {
35
+ CategoriesQuery.test("primary-id lookup returns the row", () =>
36
+ CategoriesQuery.givenStore([
37
+ ("c1", {categoryId: "c1", name: "Electronics", archived: false}),
38
+ ("c2", {categoryId: "c2", name: "Books", archived: true}),
39
+ ])
40
+ ->CategoriesQuery.whenQueryById("c1")
41
+ ->CategoriesQuery.thenRow(Some({categoryId: "c1", name: "Electronics", archived: false}))
42
+ )
43
+
44
+ CategoriesQuery.test("by-name (GSI) returns matching rows", () =>
45
+ CategoriesQuery.givenStore([
46
+ ("c1", {categoryId: "c1", name: "Electronics", archived: false}),
47
+ ("c2", {categoryId: "c2", name: "Books", archived: true}),
48
+ ])
49
+ ->CategoriesQuery.whenQuery({by: "name", value: "Electronics", index: "byName"})
50
+ ->CategoriesQuery.thenRows([{categoryId: "c1", name: "Electronics", archived: false}])
51
+ )
52
+
53
+ CategoriesQuery.test("missing index produces QueryRowsMismatch", () => {
54
+ let outcome =
55
+ CategoriesQuery.givenStore([
56
+ ("c1", {categoryId: "c1", name: "Electronics", archived: false}),
57
+ ])
58
+ ->CategoriesQuery.whenQuery({by: "name", value: "Electronics", index: "byArchived"})
59
+ ->CategoriesQuery.thenRows([])
60
+ switch outcome {
61
+ | Error(QueryRowsMismatch(_)) => Outcome.pass
62
+ | Error(other) =>
63
+ Outcome.fail(
64
+ Throw({error: "expected QueryRowsMismatch, got: " ++ Outcome.kindName(other), stack: ""}),
65
+ )
66
+ | Ok() => Outcome.fail(Throw({error: "expected failure, got pass", stack: ""}))
67
+ }
68
+ })
69
+
70
+ CategoriesQuery.test("filter narrows results after the index scan", () =>
71
+ CategoriesQuery.givenStore([
72
+ ("c1", {categoryId: "c1", name: "Books", archived: false}),
73
+ ("c2", {categoryId: "c2", name: "Books", archived: true}),
74
+ ])
75
+ ->CategoriesQuery.whenQuery({
76
+ by: "name",
77
+ value: "Books",
78
+ index: "byName",
79
+ filter: r => !r.archived,
80
+ })
81
+ ->CategoriesQuery.thenRows([{categoryId: "c1", name: "Books", archived: false}])
82
+ )
83
+
84
+ CategoriesQuery.test("limit truncates results and thenRowCount verifies", () =>
85
+ CategoriesQuery.givenStore(
86
+ Array.fromInitializer(~length=10, i => {
87
+ let id = "c" ++ i->Int.toString
88
+ let state: CategoriesReadModel.state = {categoryId: id, name: "Books", archived: false}
89
+ (id, state)
90
+ }),
91
+ )
92
+ ->CategoriesQuery.whenQuery({by: "name", value: "Books", index: "byName", limit: 3})
93
+ ->CategoriesQuery.thenRowCount(3)
94
+ )
95
+
96
+ CategoriesQuery.test("composite-id lookup without subIdConfig fails", () => {
97
+ let outcome =
98
+ CategoriesQuery.givenStore([])
99
+ ->CategoriesQuery.whenQueryByCompositeId({id: "c1", subId: "v1"})
100
+ ->CategoriesQuery.thenRowFromComposite(None)
101
+ switch outcome {
102
+ | Error(QueryRowsMismatch(_)) => Outcome.pass
103
+ | Error(other) =>
104
+ Outcome.fail(
105
+ Throw({error: "expected QueryRowsMismatch, got: " ++ Outcome.kindName(other), stack: ""}),
106
+ )
107
+ | Ok() => Outcome.fail(Throw({error: "expected failure, got pass", stack: ""}))
108
+ }
109
+ })
110
+ })
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Case 2 — StateViewSlice: Orders-by-customer, composite key (id=orderId, subId=customerId).
114
+ // ---------------------------------------------------------------------------
115
+
116
+ module OrdersView = {
117
+ let name = "OrdersView"
118
+ let moduleUrl = ""
119
+
120
+ @schema
121
+ type state = {orderId: string, customerId: string, total: int}
122
+
123
+ @schema
124
+ type consumedEvent = OrderPlaced({orderId: string, customerId: string, total: int})
125
+
126
+ let project = event =>
127
+ switch event {
128
+ | OrderPlaced({orderId, customerId, total}) => [
129
+ Reventless.Projection.Set(orderId, {orderId, customerId, total}),
130
+ ]
131
+ }
132
+
133
+ let config = Reventless.ReadModel.config()
134
+ let subIdConfig = Some({
135
+ Reventless.ReadModel.subIdField: "customerId",
136
+ getSubId: (s: state) => s.customerId,
137
+ })
138
+ }
139
+
140
+ module OrdersQuery = Query_GWT.Make(Query_GWT.FromStateViewSlice(OrdersView))
141
+
142
+ OrdersQuery.describe("OrdersView StateViewSlice queries", () => {
143
+ OrdersQuery.test("composite-id lookup returns the row", () =>
144
+ OrdersQuery.givenCompositeStore([
145
+ ("ord-1", "cust-a", {orderId: "ord-1", customerId: "cust-a", total: 100}),
146
+ ("ord-2", "cust-b", {orderId: "ord-2", customerId: "cust-b", total: 200}),
147
+ ])
148
+ ->OrdersQuery.whenQueryByCompositeId({id: "ord-1", subId: "cust-a"})
149
+ ->OrdersQuery.thenRowFromComposite(
150
+ Some({orderId: "ord-1", customerId: "cust-a", total: 100}),
151
+ )
152
+ )
153
+
154
+ OrdersQuery.test("composite-id lookup with wrong subId returns None", () =>
155
+ OrdersQuery.givenCompositeStore([
156
+ ("ord-1", "cust-a", {orderId: "ord-1", customerId: "cust-a", total: 100}),
157
+ ])
158
+ ->OrdersQuery.whenQueryByCompositeId({id: "ord-1", subId: "cust-z"})
159
+ ->OrdersQuery.thenRowFromComposite(None)
160
+ )
161
+ })