@reventlessdev/reventless-core 3.0.0-alpha.134 → 3.0.0-alpha.135

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
+ # 3.0.0-alpha.135 (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
  # 3.0.0-alpha.134 (2026-06-27)
7
14
 
8
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.134",
3
+ "version": "3.0.0-alpha.135",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -29,21 +29,21 @@
29
29
  "sury": "11.0.0-alpha.4",
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.23",
32
+ "@reventlessdev/rescript-hash-object": "1.2.0-alpha.10",
32
33
  "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
33
34
  "@reventlessdev/rescript-jest": "1.0.0-alpha.3",
34
35
  "@reventlessdev/rescript-node-streams": "1.1.0-alpha.10",
35
- "@reventlessdev/rescript-hash-object": "1.2.0-alpha.10",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
37
37
  "@reventlessdev/rescript-ssh2": "1.1.0-alpha.11",
38
- "@reventlessdev/reventless-infra": "3.0.0-alpha.81",
38
+ "@reventlessdev/rescript-uuid": "1.1.0-alpha.13",
39
39
  "@reventlessdev/reventless-interop": "3.0.0-alpha.21",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.61",
41
- "@reventlessdev/rescript-uuid": "1.1.0-alpha.13"
40
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.62",
41
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.82"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "^12.3.0",
45
45
  "sury-ppx": "11.0.0-alpha.2",
46
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.47"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.48"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "^12.3.0"
@@ -19,6 +19,11 @@ module type T = {
19
19
  ~opts: Pulumi.ComponentResource.options=?,
20
20
  ) => component
21
21
  let sourceNames: array<string>
22
+ /** Bare event-variant names consumed across every projection mapping's source event
23
+ schema — names the actual events (so a DCB-log-sourced mapping is captured), unlike
24
+ `sourceNames` which only lists source aggregate / DCB-log names. Surfaced by
25
+ Plugin_Structure as `queryableDef.consumedEventTypes`. */
26
+ let consumedEventNames: array<string>
22
27
  let outputs: component => outputs
23
28
  let operations: component => Pulumi.Output.t<operations>
24
29
  let finish: unit => unit
@@ -25,6 +25,18 @@ module Make = (
25
25
  ->Belt.Set.String.fromArray
26
26
  ->Belt.Set.String.toArray
27
27
 
28
+ // Bare event-variant names consumed across every mapping's source event schema — the
29
+ // union the reflected graph turns into Event→ReadModel "projects" edges (see
30
+ // Plugin_Structure / DomainGraph). A DCB-log-sourced mapping declares a narrow event type
31
+ // (e.g. just `OrderPlaced`), so this captures cross-source events `sourceNames` can't name.
32
+ let consumedEventNames =
33
+ Mappings.mappings
34
+ ->Array.flatMap((module(Mapping: Mappings.Mapping)) =>
35
+ Reventless.DcbTag.extractVariantNames(Mapping.sourceEventSchema->S.castToUnknown)
36
+ )
37
+ ->Belt.Set.String.fromArray
38
+ ->Belt.Set.String.toArray
39
+
28
40
  type api = QueryDbStorage.api
29
41
  type role = QueryDbStorage.role
30
42
  type component = ReadModel.component
@@ -4,6 +4,7 @@ import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output
4
4
  import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
5
5
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
6
6
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
7
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
7
8
  import * as Component$ReventlessCore from "../Component.res.mjs";
8
9
  import * as ReadModel$ReventlessCore from "./ReadModel.res.mjs";
9
10
  import * as EventTopic$ReventlessCore from "../EventTopic/EventTopic.res.mjs";
@@ -15,6 +16,7 @@ import * as EventCollector_Builder$ReventlessCore from "../EventCollector/EventC
15
16
  function Make(Spec) {
16
17
  return Mappings => (RuntimeEnvironment => (QueryDbStorage => (QueryDbResolvers => (EventCollectorChannel => (EventCollectorRuntimeBuilder => {
17
18
  let sourceNames = Belt_SetString.toArray(Belt_SetString.fromArray(Mappings.mappings.map(Mapping => Mapping.sourceName)));
19
+ let consumedEventNames = Belt_SetString.toArray(Belt_SetString.fromArray(Mappings.mappings.flatMap(Mapping => DcbTag$Reventless.extractVariantNames(Mapping.sourceEventSchema))));
18
20
  let make = (api, apiRole, allEventTopics, opts) => Component$ReventlessCore.make(ComponentType$ReventlessCore.toString(ReadModel$ReventlessCore.componentType), Spec.name, (extra, extra$1) => {
19
21
  let opts_parent = Component$ReventlessCore.toPulumiResource(extra);
20
22
  let opts = {
@@ -88,6 +90,7 @@ function Make(Spec) {
88
90
  EventCollectorRuntimeBuilder: EventCollectorRuntimeBuilder,
89
91
  make: make,
90
92
  sourceNames: sourceNames,
93
+ consumedEventNames: consumedEventNames,
91
94
  outputs: Component$ReventlessCore.outputs,
92
95
  operations: Component$ReventlessCore.operations,
93
96
  finish: finish
@@ -390,12 +390,17 @@ let make = (
390
390
  ~entityName=R.Spec.name,
391
391
  stateSchema,
392
392
  )
393
+ // The events this read model projects from, qualified to the plugin's event ids so
394
+ // they match the producers' nodes in the graph. Was empty — which dropped projection
395
+ // edges for any event reaching the read model via a DCB-log-sourced mapping (a classic
396
+ // aggregate→view link is also drawn from the producer's linkedViews, deduped downstream).
397
+ let consumed = qualify(~prefix=name, R.consumedEventNames)
393
398
  ({
394
399
  Reventless.Plugin.name: R.Spec.name,
395
400
  queryField: qf.listFieldName,
396
401
  schema: stateSchema->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
397
- consumedEventTypes: [],
398
- linkedWriteSide: [],
402
+ consumedEventTypes: consumed,
403
+ linkedWriteSide: linkedWriteSideFor(consumed),
399
404
  labelField,
400
405
  searchableFields,
401
406
  statusField: statusFieldFromStateSchema(stateSchema),
@@ -497,6 +502,7 @@ let make = (
497
502
  consumedEventTypes: qualify(~prefix=name, eventVariantNames(OTS.Spec.consumedEventSchema)),
498
503
  inboundCommandTypes: qualify(~prefix=name, commandVariantNames(OTS.Spec.inboundCommandSchema)),
499
504
  targetName: OTS.Spec.targetName,
505
+ externalSystem: OTS.Spec.externalSystem,
500
506
  }: Reventless.Plugin.outboundTranslationSliceDef))
501
507
 
502
508
  // ── Inbound translation slices ────────────────────────────────────────────
@@ -506,6 +512,7 @@ let make = (
506
512
  Reventless.Plugin.name: ITS.Spec.name,
507
513
  commandTypes: qualify(~prefix=name, commandVariantNames(ITS.Spec.commandSchema)),
508
514
  targetName: ITS.Spec.targetName,
515
+ externalSystem: ITS.Spec.externalSystem,
509
516
  }: Reventless.Plugin.inboundTranslationSliceDef))
510
517
 
511
518
  // ── Extensions ───────────────────────────────────────────────────────────
@@ -323,12 +323,13 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
323
323
  let qf = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(name, R.Spec.name, undefined);
324
324
  let stateSchema = R.Spec.stateSchema;
325
325
  let match = labelFieldsFromStateSchema(R.Spec.name, stateSchema);
326
+ let consumed = qualify(name, R.consumedEventNames);
326
327
  return {
327
328
  name: R.Spec.name,
328
329
  queryField: qf.listFieldName,
329
330
  schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(stateSchema)),
330
- consumedEventTypes: [],
331
- linkedWriteSide: [],
331
+ consumedEventTypes: consumed,
332
+ linkedWriteSide: linkedWriteSideFor(consumed),
332
333
  labelField: match[0],
333
334
  searchableFields: match[1],
334
335
  statusField: statusFieldFromStateSchema(stateSchema),
@@ -394,12 +395,14 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
394
395
  name: OTS.Spec.name,
395
396
  consumedEventTypes: qualify(name, DcbTag$Reventless.extractVariantNames(OTS.Spec.consumedEventSchema)),
396
397
  inboundCommandTypes: qualify(name, DcbTag$Reventless.extractAllVariantNames(OTS.Spec.inboundCommandSchema)),
397
- targetName: OTS.Spec.targetName
398
+ targetName: OTS.Spec.targetName,
399
+ externalSystem: OTS.Spec.externalSystem
398
400
  }));
399
401
  let inboundTranslationSliceDefs = inboundTranslationSlices.map(ITS => ({
400
402
  name: ITS.Spec.name,
401
403
  commandTypes: qualify(name, DcbTag$Reventless.extractAllVariantNames(ITS.Spec.commandSchema)),
402
- targetName: ITS.Spec.targetName
404
+ targetName: ITS.Spec.targetName,
405
+ externalSystem: ITS.Spec.externalSystem
403
406
  }));
404
407
  let extensionDefs = extensions.map(E => {
405
408
  let delegateNames = E.mappings.map(M => M.delegateName);
@@ -59,10 +59,11 @@ let structure: pluginStructure = {
59
59
  consumedEventTypes: ["OrderPlaced"],
60
60
  inboundCommandTypes: ["Ship"],
61
61
  targetName: None,
62
+ externalSystem: None,
62
63
  },
63
64
  ],
64
65
  inboundTranslationSlices: [
65
- {name: "FromBilling", commandTypes: ["RecordPayment"], targetName: "Order"},
66
+ {name: "FromBilling", commandTypes: ["RecordPayment"], targetName: "Order", externalSystem: None},
66
67
  ],
67
68
  extensions: [
68
69
  {
@@ -85,13 +85,15 @@ let structure_outboundTranslationSlices = [{
85
85
  name: "ToShipper",
86
86
  consumedEventTypes: ["OrderPlaced"],
87
87
  inboundCommandTypes: ["Ship"],
88
- targetName: undefined
88
+ targetName: undefined,
89
+ externalSystem: undefined
89
90
  }];
90
91
 
91
92
  let structure_inboundTranslationSlices = [{
92
93
  name: "FromBilling",
93
94
  commandTypes: ["RecordPayment"],
94
- targetName: "Order"
95
+ targetName: "Order",
96
+ externalSystem: undefined
95
97
  }];
96
98
 
97
99
  let structure_extensions = [{
@@ -45,6 +45,9 @@ module PublicReadModel: ReventlessInfra.ReadModel.T
45
45
  type role = unit
46
46
  type component
47
47
  let sourceNames: array<string> = []
48
+ // A read model that projects an event reaching it via a (DCB-log-sourced) mapping — the
49
+ // case Plugin_Structure must surface as a qualified `consumedEventTypes` entry.
50
+ let consumedEventNames: array<string> = ["OrderPlaced"]
48
51
  let make = (~api as _, ~apiRole as _, ~allEventTopics as _, ~opts as _=?): component =>
49
52
  Obj.magic(0)
50
53
  let outputs = (_: component): ReventlessInfra.ReadModel.outputs => Obj.magic(0)
@@ -60,6 +63,7 @@ module InternalReadModel: ReventlessInfra.ReadModel.T
60
63
  type role = unit
61
64
  type component
62
65
  let sourceNames: array<string> = []
66
+ let consumedEventNames: array<string> = []
63
67
  let make = (~api as _, ~apiRole as _, ~allEventTopics as _, ~opts as _=?): component =>
64
68
  Obj.magic(0)
65
69
  let outputs = (_: component): ReventlessInfra.ReadModel.outputs => Obj.magic(0)
@@ -128,6 +132,16 @@ describe("Plugin_Structure.make — visibility tagging", () => {
128
132
  expect(internal->Option.map(d => d.visibility))->toEqual(Some(Some("Internal")))
129
133
  })
130
134
 
135
+ testSync("a read model's mapping-consumed events surface as qualified consumedEventTypes", () => {
136
+ // Q6 #2: a classic read model fed via a (DCB-log-sourced) mapping now reflects the
137
+ // events it projects from, qualified to the plugin — so DomainGraph draws the
138
+ // Event→ReadModel projection edge (e.g. OrderPlaced → Customers).
139
+ let public = structure.readModels->Array.find(d => d.name == "PublicView")
140
+ expect(public->Option.map(d => d.consumedEventTypes))->toEqual(
141
+ Some(["VisibilityPlugin.OrderPlaced"]),
142
+ )
143
+ })
144
+
131
145
  testSync("aggregates / stateViewSlices stay empty when none are passed (sanity)", () => {
132
146
  expect((
133
147
  structure.aggregates->Array.length,
@@ -53,6 +53,8 @@ let InternalSpec = {
53
53
 
54
54
  let sourceNames = [];
55
55
 
56
+ let consumedEventNames = ["OrderPlaced"];
57
+
56
58
  function make(param, param$1, param$2, param$3) {
57
59
  return 0;
58
60
  }
@@ -89,6 +91,7 @@ let PublicReadModel_Spec = {
89
91
  let PublicReadModel = {
90
92
  Spec: PublicReadModel_Spec,
91
93
  sourceNames: sourceNames,
94
+ consumedEventNames: consumedEventNames,
92
95
  make: make,
93
96
  outputs: outputs,
94
97
  operations: operations,
@@ -97,6 +100,8 @@ let PublicReadModel = {
97
100
 
98
101
  let sourceNames$1 = [];
99
102
 
103
+ let consumedEventNames$1 = [];
104
+
100
105
  function make$1(param, param$1, param$2, param$3) {
101
106
  return 0;
102
107
  }
@@ -133,6 +138,7 @@ let InternalReadModel_Spec = {
133
138
  let InternalReadModel = {
134
139
  Spec: InternalReadModel_Spec,
135
140
  sourceNames: sourceNames$1,
141
+ consumedEventNames: consumedEventNames$1,
136
142
  make: make$1,
137
143
  outputs: outputs$1,
138
144
  operations: operations$1,
@@ -183,6 +189,10 @@ globalThis.describe("Plugin_Structure.make — visibility tagging", () => {
183
189
  let internal = structure.readModels.find(d => d.name === "InternalView");
184
190
  globalThis.expect(Stdlib_Option.map(internal, d => d.visibility)).toEqual("Internal");
185
191
  });
192
+ globalThis.test("a read model's mapping-consumed events surface as qualified consumedEventTypes", () => {
193
+ let $$public = structure.readModels.find(d => d.name === "PublicView");
194
+ globalThis.expect(Stdlib_Option.map($$public, d => d.consumedEventTypes)).toEqual(["VisibilityPlugin.OrderPlaced"]);
195
+ });
186
196
  globalThis.test("aggregates / stateViewSlices stay empty when none are passed (sanity)", () => {
187
197
  globalThis.expect([
188
198
  structure.aggregates.length,