@reventlessdev/reventless-gwt 1.0.0-alpha.74 → 1.0.0-alpha.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 1.0.0-alpha.75 (2026-06-29)
7
+
8
+ ### Features
9
+
10
+ * external-system boxes for translation slices (Event Graph data) ([3f8ad39](https://github.com/ReventlessDev/reventless-core/commit/3f8ad39b78a3cb1182d59a0e1fb203b7dcb7379b))
11
+
12
+
6
13
  # 1.0.0-alpha.74 (2026-06-27)
7
14
 
8
15
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-gwt",
3
- "version": "1.0.0-alpha.74",
3
+ "version": "1.0.0-alpha.75",
4
4
  "description": "Given-When-Then DSLs and CLI runner for Reventless slice testing",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -21,16 +21,16 @@
21
21
  "picocolors": "^1.1.1",
22
22
  "sury": "11.0.0-alpha.4",
23
23
  "@reventlessdev/rescript-effect": "0.1.0-alpha.23",
24
- "@reventlessdev/reventless-core": "3.0.0-alpha.134",
25
- "@reventlessdev/reventless-infra": "3.0.0-alpha.81",
26
- "@reventlessdev/reventless-spec": "3.0.0-alpha.61",
24
+ "@reventlessdev/reventless-core": "3.0.0-alpha.135",
27
25
  "@reventlessdev/rescript-jest": "1.0.0-alpha.3",
28
- "@reventlessdev/reventless-vscode-protocol": "1.0.0-alpha.8"
26
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.82",
27
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.62",
28
+ "@reventlessdev/reventless-vscode-protocol": "1.0.0-alpha.9"
29
29
  },
30
30
  "devDependencies": {
31
31
  "rescript": "^12.3.0",
32
32
  "sury-ppx": "11.0.0-alpha.2",
33
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.47"
33
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.48"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "rescript": "^12.3.0"
Binary file
Binary file
@@ -262,7 +262,7 @@ let graph = (g: DomainGraph.graph) =>
262
262
  label: n.label,
263
263
  plugin: n.plugin,
264
264
  }),
265
- edges: g.edges->Array.map((e): P.graphEdge => {from: e.from, to_: e.to, kind: e.kind}),
265
+ edges: g.edges->Array.map((e): P.graphEdge => {from: e.from, to_: e.to, kind: e.kind, label: ?e.label}),
266
266
  }),
267
267
  )
268
268
 
@@ -291,7 +291,8 @@ function graph(g) {
291
291
  let e_1 = g.edges.map(e => ({
292
292
  from: e.from,
293
293
  to: e.to,
294
- kind: e.kind
294
+ kind: e.kind,
295
+ label: e.label
295
296
  }));
296
297
  let e = {
297
298
  event: "graph",
@@ -47,17 +47,19 @@ let automation = (~name, ~consumes=[], ~produces=[], ~target=""): Reventless.Plu
47
47
  targetName: target,
48
48
  }
49
49
 
50
- let inbound = (~name, ~produces=[], ~target=""): Reventless.Plugin.inboundTranslationSliceDef => {
50
+ let inbound = (~name, ~produces=[], ~target="", ~ext=None): Reventless.Plugin.inboundTranslationSliceDef => {
51
51
  name,
52
52
  commandTypes: produces,
53
53
  targetName: target,
54
+ externalSystem: ext,
54
55
  }
55
56
 
56
- let outbound = (~name, ~consumes=[], ~produces=[], ~target=None): Reventless.Plugin.outboundTranslationSliceDef => {
57
+ let outbound = (~name, ~consumes=[], ~produces=[], ~target=None, ~ext=None): Reventless.Plugin.outboundTranslationSliceDef => {
57
58
  name,
58
59
  consumedEventTypes: consumes,
59
60
  inboundCommandTypes: produces,
60
61
  targetName: target,
62
+ externalSystem: ext,
61
63
  }
62
64
 
63
65
  let structure = (
@@ -83,6 +85,8 @@ let structure = (
83
85
 
84
86
  let hasEdge = (g: DomainGraph.graph, from, to, kind) =>
85
87
  g.edges->Array.some(e => e.from == from && e.to == to && e.kind == kind)
88
+ let edgeLabel = (g: DomainGraph.graph, from, to, kind) =>
89
+ g.edges->Array.find(e => e.from == from && e.to == to && e.kind == kind)->Option.flatMap(e => e.label)
86
90
  let nodeKind = (g: DomainGraph.graph, id) =>
87
91
  g.nodes->Array.find(n => n.id == id)->Option.map(n => n.kind)
88
92
 
@@ -128,6 +132,25 @@ describe("DomainGraph.build", () => {
128
132
  expect(hasEdge(g, "Catalog:Category", "Catalog.Archived", "emits"))->toBe(true)
129
133
  })
130
134
 
135
+ testPromise("a payload-less event also projects into the aggregate's linked view", async () => {
136
+ // Classic aggregate → linked view: the projection drew only producedEventTypes, which
137
+ // drops payload-less events — so a bare `| Archived` emitted but never reached the view.
138
+ // It must now project too (matching the emit), just like the payloaded `Added`.
139
+ let shop = structure(
140
+ ~aggregates=[
141
+ writable(
142
+ ~name="Category",
143
+ ~produces=["Catalog.Added"],
144
+ ~events=[evt(~name="Added"), evt(~name="Archived")],
145
+ ~linkedViews=["Categories"],
146
+ ),
147
+ ],
148
+ )
149
+ let g = DomainGraph.build(~structures=[("Catalog", shop)])
150
+ expect(hasEdge(g, "Catalog.Added", "Catalog:Categories", "projects"))->toBe(true)
151
+ expect(hasEdge(g, "Catalog.Archived", "Catalog:Categories", "projects"))->toBe(true)
152
+ })
153
+
131
154
  testPromise("an Internal StateViewSlice still gets a node + projects edge (dev graph shows it)", async () => {
132
155
  // Internal components are hidden from the deployed AutoUI but carried in pluginStructure
133
156
  // so the developer-facing domain graph can render them — see Plugin_Structure.res /
@@ -217,6 +240,60 @@ describe("DomainGraph.build", () => {
217
240
  expect(hasEdge(g, "Shop:ExportProduct", "Shop_Product_Sync", "delegatesTo"))->toBe(true)
218
241
  })
219
242
 
243
+ testPromise("an inbound translation naming an external system draws a box feeding it", async () => {
244
+ let shop = structure(
245
+ ~inboundTranslationSlices=[
246
+ inbound(~name="ImportProduct", ~produces=["ImportProduct.Add"], ~target="Product", ~ext=Some("SupplierFeed")),
247
+ ],
248
+ )
249
+ let g = DomainGraph.build(~structures=[("Shop", shop)])
250
+ // External box: drawn OUTSIDE the plugin (plugin: "") with a stable "ext:<name>" id.
251
+ expect(nodeKind(g, "ext:SupplierFeed"))->toEqual(Some("ExternalSystem"))
252
+ expect(g.nodes->Array.find(n => n.id == "ext:SupplierFeed")->Option.map(n => n.plugin))->toEqual(Some(""))
253
+ // Inbound direction: external → slice (unlabelled — the arrowhead carries the meaning).
254
+ expect(hasEdge(g, "ext:SupplierFeed", "Shop:ImportProduct", "translatesIn"))->toBe(true)
255
+ expect(edgeLabel(g, "ext:SupplierFeed", "Shop:ImportProduct", "translatesIn"))->toEqual(None)
256
+ })
257
+
258
+ testPromise("an outbound translation naming an external system feeds the box", async () => {
259
+ let shop = structure(
260
+ ~outboundTranslationSlices=[
261
+ outbound(~name="SendEmail", ~consumes=["Shop.Placed"], ~target=None, ~ext=Some("EmailService")),
262
+ ],
263
+ )
264
+ let g = DomainGraph.build(~structures=[("Shop", shop)])
265
+ expect(nodeKind(g, "ext:EmailService"))->toEqual(Some("ExternalSystem"))
266
+ // Outbound direction: slice → external (unlabelled — the arrowhead carries the meaning).
267
+ expect(hasEdge(g, "Shop:SendEmail", "ext:EmailService", "translatesOut"))->toBe(true)
268
+ expect(edgeLabel(g, "Shop:SendEmail", "ext:EmailService", "translatesOut"))->toEqual(None)
269
+ })
270
+
271
+ testPromise("translation slices with no external system draw no box (opt-in)", async () => {
272
+ let shop = structure(
273
+ ~inboundTranslationSlices=[inbound(~name="ImportProduct", ~target="Product")],
274
+ )
275
+ let g = DomainGraph.build(~structures=[("Shop", shop)])
276
+ expect(g.nodes->Array.some(n => n.kind == "ExternalSystem"))->toBe(false)
277
+ })
278
+
279
+ testPromise("two slices naming the same external system share one deduped box", async () => {
280
+ let a = structure(
281
+ ~outboundTranslationSlices=[
282
+ outbound(~name="SendEmailA", ~consumes=["A.X"], ~target=None, ~ext=Some("EmailService")),
283
+ ],
284
+ )
285
+ let b = structure(
286
+ ~inboundTranslationSlices=[
287
+ inbound(~name="FromEmail", ~produces=["FromEmail.Record"], ~target="Inbox", ~ext=Some("EmailService")),
288
+ ],
289
+ )
290
+ let g = DomainGraph.build(~structures=[("PluginA", a), ("PluginB", b)])
291
+ // One shared box (per-platform dedup by name) with both an out- and an in-edge.
292
+ expect(g.nodes->Array.filter(n => n.id == "ext:EmailService")->Array.length)->toBe(1)
293
+ expect(hasEdge(g, "PluginA:SendEmailA", "ext:EmailService", "translatesOut"))->toBe(true)
294
+ expect(hasEdge(g, "ext:EmailService", "PluginB:FromEmail", "translatesIn"))->toBe(true)
295
+ })
296
+
220
297
  testPromise("an event produced by two write-sides is one node with two emit edges", async () => {
221
298
  let shop = structure(
222
299
  ~aggregates=[
@@ -71,25 +71,29 @@ function automation(name, consumesOpt, producesOpt, targetOpt) {
71
71
  };
72
72
  }
73
73
 
74
- function inbound(name, producesOpt, targetOpt) {
74
+ function inbound(name, producesOpt, targetOpt, extOpt) {
75
75
  let produces = producesOpt !== undefined ? producesOpt : [];
76
76
  let target = targetOpt !== undefined ? targetOpt : "";
77
+ let ext = extOpt !== undefined ? Primitive_option.valFromOption(extOpt) : undefined;
77
78
  return {
78
79
  name: name,
79
80
  commandTypes: produces,
80
- targetName: target
81
+ targetName: target,
82
+ externalSystem: ext
81
83
  };
82
84
  }
83
85
 
84
- function outbound(name, consumesOpt, producesOpt, targetOpt) {
86
+ function outbound(name, consumesOpt, producesOpt, targetOpt, extOpt) {
85
87
  let consumes = consumesOpt !== undefined ? consumesOpt : [];
86
88
  let produces = producesOpt !== undefined ? producesOpt : [];
87
89
  let target = targetOpt !== undefined ? Primitive_option.valFromOption(targetOpt) : undefined;
90
+ let ext = extOpt !== undefined ? Primitive_option.valFromOption(extOpt) : undefined;
88
91
  return {
89
92
  name: name,
90
93
  consumedEventTypes: consumes,
91
94
  inboundCommandTypes: produces,
92
- targetName: target
95
+ targetName: target,
96
+ externalSystem: ext
93
97
  };
94
98
  }
95
99
 
@@ -125,6 +129,16 @@ function hasEdge(g, from, to, kind) {
125
129
  });
126
130
  }
127
131
 
132
+ function edgeLabel(g, from, to, kind) {
133
+ return Stdlib_Option.flatMap(g.edges.find(e => {
134
+ if (e.from === from && e.to === to) {
135
+ return e.kind === kind;
136
+ } else {
137
+ return false;
138
+ }
139
+ }), e => e.label);
140
+ }
141
+
128
142
  function nodeKind(g, id) {
129
143
  return Stdlib_Option.map(g.nodes.find(n => n.id === id), n => n.kind);
130
144
  }
@@ -166,6 +180,26 @@ globalThis.describe("DomainGraph.build", () => {
166
180
  globalThis.expect(hasEdge(g, "Catalog:Category", "Catalog.Added", "emits")).toBe(true);
167
181
  globalThis.expect(hasEdge(g, "Catalog:Category", "Catalog.Archived", "emits")).toBe(true);
168
182
  });
183
+ globalThis.test("a payload-less event also projects into the aggregate's linked view", async () => {
184
+ let shop = structure([writable("Category", undefined, ["Catalog.Added"], undefined, ["Categories"], [
185
+ {
186
+ name: "Added",
187
+ schema: "",
188
+ references: []
189
+ },
190
+ {
191
+ name: "Archived",
192
+ schema: "",
193
+ references: []
194
+ }
195
+ ])], undefined, undefined, undefined, undefined, undefined, undefined, undefined);
196
+ let g = DomainGraph$ReventlessGwt.build([[
197
+ "Catalog",
198
+ shop
199
+ ]]);
200
+ globalThis.expect(hasEdge(g, "Catalog.Added", "Catalog:Categories", "projects")).toBe(true);
201
+ globalThis.expect(hasEdge(g, "Catalog.Archived", "Catalog:Categories", "projects")).toBe(true);
202
+ });
169
203
  globalThis.test("an Internal StateViewSlice still gets a node + projects edge (dev graph shows it)", async () => {
170
204
  let shop = structure([writable("Order", undefined, ["Shop.Placed"], undefined, undefined, undefined)], undefined, [queryable("AvailableView", ["Shop.Placed"], "Internal")], undefined, undefined, undefined, undefined, undefined);
171
205
  let g = DomainGraph$ReventlessGwt.build([[
@@ -209,7 +243,7 @@ globalThis.describe("DomainGraph.build", () => {
209
243
  globalThis.expect(hasEdge(g, "Shop:AutoShip", "Shop_Ship_Place", "delegatesTo")).toBe(false);
210
244
  });
211
245
  globalThis.test("an inbound translation routes to the command it raises on its target", async () => {
212
- let shop = structure(undefined, undefined, undefined, [writable("Product", [command("Add", "Shop_Product_Add", undefined)], undefined, undefined, undefined, undefined)], undefined, undefined, [inbound("ImportProduct", ["ImportProduct.Add"], "Product")], undefined);
246
+ let shop = structure(undefined, undefined, undefined, [writable("Product", [command("Add", "Shop_Product_Add", undefined)], undefined, undefined, undefined, undefined)], undefined, undefined, [inbound("ImportProduct", ["ImportProduct.Add"], "Product", undefined)], undefined);
213
247
  let g = DomainGraph$ReventlessGwt.build([[
214
248
  "Shop",
215
249
  shop
@@ -218,13 +252,59 @@ globalThis.describe("DomainGraph.build", () => {
218
252
  globalThis.expect(hasEdge(g, "Shop:ImportProduct", "Shop_Product_Add", "delegatesTo")).toBe(true);
219
253
  });
220
254
  globalThis.test("an outbound translation with a target routes to the command it raises", async () => {
221
- let shop = structure(undefined, undefined, undefined, [writable("Product", [command("Sync", "Shop_Product_Sync", undefined)], undefined, undefined, undefined, undefined)], undefined, [outbound("ExportProduct", ["Shop.Added"], ["ExportProduct.Sync"], "Product")], undefined, undefined);
255
+ let shop = structure(undefined, undefined, undefined, [writable("Product", [command("Sync", "Shop_Product_Sync", undefined)], undefined, undefined, undefined, undefined)], undefined, [outbound("ExportProduct", ["Shop.Added"], ["ExportProduct.Sync"], "Product", undefined)], undefined, undefined);
222
256
  let g = DomainGraph$ReventlessGwt.build([[
223
257
  "Shop",
224
258
  shop
225
259
  ]]);
226
260
  globalThis.expect(hasEdge(g, "Shop:ExportProduct", "Shop_Product_Sync", "delegatesTo")).toBe(true);
227
261
  });
262
+ globalThis.test("an inbound translation naming an external system draws a box feeding it", async () => {
263
+ let shop = structure(undefined, undefined, undefined, undefined, undefined, undefined, [inbound("ImportProduct", ["ImportProduct.Add"], "Product", "SupplierFeed")], undefined);
264
+ let g = DomainGraph$ReventlessGwt.build([[
265
+ "Shop",
266
+ shop
267
+ ]]);
268
+ globalThis.expect(nodeKind(g, "ext:SupplierFeed")).toEqual("ExternalSystem");
269
+ globalThis.expect(Stdlib_Option.map(g.nodes.find(n => n.id === "ext:SupplierFeed"), n => n.plugin)).toEqual("");
270
+ globalThis.expect(hasEdge(g, "ext:SupplierFeed", "Shop:ImportProduct", "translatesIn")).toBe(true);
271
+ globalThis.expect(edgeLabel(g, "ext:SupplierFeed", "Shop:ImportProduct", "translatesIn")).toEqual(undefined);
272
+ });
273
+ globalThis.test("an outbound translation naming an external system feeds the box", async () => {
274
+ let shop = structure(undefined, undefined, undefined, undefined, undefined, [outbound("SendEmail", ["Shop.Placed"], undefined, Primitive_option.some(undefined), "EmailService")], undefined, undefined);
275
+ let g = DomainGraph$ReventlessGwt.build([[
276
+ "Shop",
277
+ shop
278
+ ]]);
279
+ globalThis.expect(nodeKind(g, "ext:EmailService")).toEqual("ExternalSystem");
280
+ globalThis.expect(hasEdge(g, "Shop:SendEmail", "ext:EmailService", "translatesOut")).toBe(true);
281
+ globalThis.expect(edgeLabel(g, "Shop:SendEmail", "ext:EmailService", "translatesOut")).toEqual(undefined);
282
+ });
283
+ globalThis.test("translation slices with no external system draw no box (opt-in)", async () => {
284
+ let shop = structure(undefined, undefined, undefined, undefined, undefined, undefined, [inbound("ImportProduct", undefined, "Product", undefined)], undefined);
285
+ let g = DomainGraph$ReventlessGwt.build([[
286
+ "Shop",
287
+ shop
288
+ ]]);
289
+ globalThis.expect(g.nodes.some(n => n.kind === "ExternalSystem")).toBe(false);
290
+ });
291
+ globalThis.test("two slices naming the same external system share one deduped box", async () => {
292
+ let a = structure(undefined, undefined, undefined, undefined, undefined, [outbound("SendEmailA", ["A.X"], undefined, Primitive_option.some(undefined), "EmailService")], undefined, undefined);
293
+ let b = structure(undefined, undefined, undefined, undefined, undefined, undefined, [inbound("FromEmail", ["FromEmail.Record"], "Inbox", "EmailService")], undefined);
294
+ let g = DomainGraph$ReventlessGwt.build([
295
+ [
296
+ "PluginA",
297
+ a
298
+ ],
299
+ [
300
+ "PluginB",
301
+ b
302
+ ]
303
+ ]);
304
+ globalThis.expect(g.nodes.filter(n => n.id === "ext:EmailService").length).toBe(1);
305
+ globalThis.expect(hasEdge(g, "PluginA:SendEmailA", "ext:EmailService", "translatesOut")).toBe(true);
306
+ globalThis.expect(hasEdge(g, "ext:EmailService", "PluginB:FromEmail", "translatesIn")).toBe(true);
307
+ });
228
308
  globalThis.test("an event produced by two write-sides is one node with two emit edges", async () => {
229
309
  let shop = structure([
230
310
  writable("A", undefined, ["Shop.Touched"], undefined, undefined, undefined),
@@ -408,6 +488,7 @@ export {
408
488
  outbound,
409
489
  structure,
410
490
  hasEdge,
491
+ edgeLabel,
411
492
  nodeKind,
412
493
  }
413
494
  /* Not a pure module */
@@ -307,7 +307,8 @@ let ConfirmSlice = {
307
307
  consumedEventSchema: consumedEventSchema$4,
308
308
  outboundItemSchema: outboundItemSchema,
309
309
  inboundCommandSchema: S.unit,
310
- collect: collect$1
310
+ collect: collect$1,
311
+ externalSystem: undefined
311
312
  };
312
313
 
313
314
  let Place = Flow_GWT$ReventlessGwt.CommandStep(PlaceOrderSlice)({
@@ -46,7 +46,8 @@ let PaymentWebhookSlice = {
46
46
  externalInputSchema: externalInputSchema,
47
47
  commandSchema: commandSchema,
48
48
  translate: translate,
49
- commandAuthorization: commandAuthorization
49
+ commandAuthorization: commandAuthorization,
50
+ externalSystem: undefined
50
51
  };
51
52
 
52
53
  let include = InboundTranslation_GWT$ReventlessGwt.Make(PaymentWebhookSlice);
@@ -32,7 +32,8 @@ let SendTrackingEmailSlice = {
32
32
  consumedEventSchema: consumedEventSchema,
33
33
  outboundItemSchema: outboundItemSchema,
34
34
  inboundCommandSchema: inboundCommandSchema,
35
- collect: collect
35
+ collect: collect,
36
+ externalSystem: undefined
36
37
  };
37
38
 
38
39
  let include = OutboundTranslation_GWT$ReventlessGwt.Make(SendTrackingEmailSlice);