@reventlessdev/reventless-local 3.0.0-alpha.301 → 3.0.0-alpha.302

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,28 @@
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.302 (2026-10-29)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **local:** emit-capabilities writes an empty manifest for a plugin with no parts yet ([56586a6](https://github.com/ReventlessDev/reventless-core/commit/56586a687aa4658e2357c836878b44050b424d41))
11
+ * refactor!: drop `dcbSources`, the cross-plugin DCB subscription nobody declared ([cf3b66d](https://github.com/ReventlessDev/reventless-core/commit/cf3b66d8c1b5d87673197b64eaee174598bb8253))
12
+ * refactor!: a read model hears another plugin only through an Extension and a slice ([15c7cae](https://github.com/ReventlessDev/reventless-core/commit/15c7cae214d5aa04404779fcd87a8429aad1965c))
13
+ ### Features
14
+
15
+ * **core:** a handler drops a command it has already handled ([605862b](https://github.com/ReventlessDev/reventless-core/commit/605862bba765b1501eb19d08f3924bf619159e21))
16
+
17
+ ### BREAKING CHANGES
18
+
19
+ * `Reventless.Plugin.extensionDefinition` has no `dcbSources`
20
+ field, and `Platform_PluginExtensions.dcbSources` is gone from the GraphQL
21
+ schema. A query that selects it fails as a whole; drop the selection.
22
+ * `ReventlessInfra.Extension.T.make` and the core
23
+ `Extension_Builder.make` no longer take `~readModelNamesForSourceName` and
24
+ `~publishToReadModels`. Both arguments did nothing; drop them at the call site.
25
+
26
+
27
+
6
28
  # 3.0.0-alpha.301 (2026-10-25)
7
29
 
8
30
  **Note:** Version bump only for package @reventlessdev/reventless-local
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.301",
3
+ "version": "3.0.0-alpha.302",
4
4
  "description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -37,16 +37,16 @@
37
37
  "@reventlessdev/rescript-effect": "1.0.0-alpha.1",
38
38
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
39
39
  "@reventlessdev/rescript-jest": "1.0.0-alpha.13",
40
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
41
40
  "@reventlessdev/rescript-node": "2.0.0-alpha.23",
41
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
42
42
  "@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
43
- "@reventlessdev/reventless-core": "3.0.0-alpha.310",
44
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.258",
45
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.158",
46
- "@reventlessdev/reventless-infra": "3.0.0-alpha.207",
43
+ "@reventlessdev/reventless-core": "3.0.0-alpha.311",
44
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.259",
45
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.208",
46
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.175",
47
47
  "@reventlessdev/reventless-seed": "1.0.0-alpha.38",
48
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.174",
49
- "@reventlessdev/reventless-spec": "3.0.0-alpha.178"
48
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.179",
49
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.159"
50
50
  },
51
51
  "devDependencies": {
52
52
  "rescript": "12.3.1",
@@ -25,9 +25,19 @@
25
25
 
26
26
  type platform
27
27
  type localPlatformExports = {"Make": unit => platform}
28
- type builtPlugin = {"pluginStructure": Reventless.Plugin.pluginStructure}
28
+ // A plugin with no parts yet has no structure: `generate-plugin` leaves `pluginStructure` out of
29
+ // its composition root.
30
+ type builtPlugin = {"pluginStructure": Nullable.t<Reventless.Plugin.pluginStructure>}
29
31
  type compositionExports = {"Make": platform => builtPlugin}
30
32
 
33
+ /** The manifest's text for a plugin's structure. A plugin with no parts yet has none and
34
+ declares nothing: its manifest says so, as one for a structure with no declarations does. */
35
+ let manifestText = (structure: option<Reventless.Plugin.pluginStructure>): string =>
36
+ switch structure {
37
+ | Some(structure) => Reventless.CapabilityManifest.renderForStructure(structure)
38
+ | None => Reventless.CapabilityManifest.render({capabilities: []})
39
+ }
40
+
31
41
  // ── Which module holds the composition root ──────────────────────────────────
32
42
 
33
43
  /** `Plugin.res.mjs` is what `generate-plugin` emits, so it is the default and
@@ -99,10 +109,7 @@ let main = () =>
99
109
  let built = composition["Make"](platform)
100
110
 
101
111
  let manifestPath = NodePath.join([srcDir, "capabilities.json"])
102
- NodeFs.writeFileSync(
103
- manifestPath,
104
- Reventless.CapabilityManifest.renderForStructure(built["pluginStructure"]),
105
- )
112
+ NodeFs.writeFileSync(manifestPath, manifestText(built["pluginStructure"]->Nullable.toOption))
106
113
  Console.log("Generated: " ++ manifestPath)
107
114
 
108
115
  // Applying the platform functor wires in-process infrastructure; exit
@@ -4,9 +4,20 @@ import * as Nodefs from "node:fs";
4
4
  import * as Nodeurl from "node:url";
5
5
  import * as Nodepath from "node:path";
6
6
  import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
7
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
7
8
  import * as CliArgs$Reventless from "@reventlessdev/reventless-spec/src/CliArgs.res.mjs";
8
9
  import * as CapabilityManifest$Reventless from "@reventlessdev/reventless-spec/src/components/CapabilityManifest.res.mjs";
9
10
 
11
+ function manifestText(structure) {
12
+ if (structure !== undefined) {
13
+ return CapabilityManifest$Reventless.renderForStructure(structure);
14
+ } else {
15
+ return CapabilityManifest$Reventless.render({
16
+ capabilities: []
17
+ });
18
+ }
19
+ }
20
+
10
21
  function compositionModulePath(srcDir, moduleArg) {
11
22
  let withExtension = name => {
12
23
  if (name.endsWith(".mjs")) {
@@ -84,13 +95,14 @@ function main() {
84
95
  let composition = await import(Nodeurl.pathToFileURL(modulePath).href);
85
96
  let built = composition.Make(platform);
86
97
  let manifestPath = Nodepath.join(srcDir, "capabilities.json");
87
- Nodefs.writeFileSync(manifestPath, CapabilityManifest$Reventless.renderForStructure(built.pluginStructure), "utf8");
98
+ Nodefs.writeFileSync(manifestPath, manifestText(Primitive_option.fromNullable(built.pluginStructure)), "utf8");
88
99
  console.log("Generated: " + manifestPath);
89
100
  process.exit(0);
90
101
  });
91
102
  }
92
103
 
93
104
  export {
105
+ manifestText,
94
106
  compositionModulePath,
95
107
  fail,
96
108
  usage,
package/src/Platform.res CHANGED
@@ -1255,7 +1255,6 @@ module MakeWithConfig = (
1255
1255
  ((_, ext: ReventlessInfra.Extension.outputs)) => {
1256
1256
  Reventless.Plugin.name: ext.name,
1257
1257
  extensionPointName: ext.extensionPointName,
1258
- dcbSources: [],
1259
1258
  },
1260
1259
  ),
1261
1260
  extensionProtocols: [],
@@ -1063,8 +1063,7 @@ function MakeWithConfig(Config) {
1063
1063
  let ext = param[1];
1064
1064
  return {
1065
1065
  name: ext.name,
1066
- extensionPointName: ext.extensionPointName,
1067
- dcbSources: []
1066
+ extensionPointName: ext.extensionPointName
1068
1067
  };
1069
1068
  }),
1070
1069
  eventCollector: param[2].name,
@@ -2858,8 +2857,7 @@ function Make($star) {
2858
2857
  let ext = param[1];
2859
2858
  return {
2860
2859
  name: ext.name,
2861
- extensionPointName: ext.extensionPointName,
2862
- dcbSources: []
2860
+ extensionPointName: ext.extensionPointName
2863
2861
  };
2864
2862
  }),
2865
2863
  eventCollector: param[2].name,
@@ -19,6 +19,12 @@ describe("EmitCapabilities.parseArgs", () => {
19
19
  )
20
20
  })
21
21
 
22
+ describe("EmitCapabilities.manifestText", () => {
23
+ testSync("a plugin with no parts yet declares nothing", () =>
24
+ expect(EmitCapabilities.manifestText(None))->toBe(`{\n "capabilities": []\n}\n`)
25
+ )
26
+ })
27
+
22
28
  describe("emit-capabilities on the command line", () => {
23
29
  module CliArgs = Reventless.CliArgs
24
30
  let usage = EmitCapabilities.usage->String.trim
@@ -30,6 +30,12 @@ globalThis.describe("EmitCapabilities.parseArgs", () => {
30
30
  });
31
31
  });
32
32
 
33
+ globalThis.describe("EmitCapabilities.manifestText", () => {
34
+ globalThis.test("a plugin with no parts yet declares nothing", () => {
35
+ globalThis.expect(EmitCapabilities$ReventlessLocal.manifestText(undefined)).toBe(`{\n "capabilities": []\n}\n`);
36
+ });
37
+ });
38
+
33
39
  globalThis.describe("emit-capabilities on the command line", () => {
34
40
  let usage = EmitCapabilities$ReventlessLocal.usage.trim();
35
41
  globalThis.test("--help anywhere prints the usage to stdout and exits 0", async () => {
@@ -0,0 +1,291 @@
1
+ // Another plugin's fact arrives through an Extension and a slice, and delivery is
2
+ // at-least-once. The slice here has no check of its own — every RecordDemand
3
+ // appends — so what keeps a redelivered ItemOrdered from counting twice is the
4
+ // framework: the Extension's command carries a stable id, and the handler drops a
5
+ // command the partition already holds events for. On both local backends, and
6
+ // for two concurrent copies.
7
+
8
+ open JestGlobals
9
+
10
+ let _ = TestRunner.setup()
11
+ let opts: Pulumi.CustomResourceOptions.t = {}
12
+
13
+ module Message = ReventlessCore.Message
14
+
15
+ // ─── The provider's extension point and the consumer's slice ────────────────
16
+
17
+ module Port = {
18
+ let name = "Ordering.Orders"
19
+
20
+ @schema
21
+ type command = Noop
22
+
23
+ @schema
24
+ type event = ItemOrdered({productId: string, orderId: string})
25
+
26
+ @schema
27
+ type directive = NoDirective
28
+
29
+ let moduleUrl: string = %raw(`import.meta.url`)
30
+ }
31
+
32
+ module DemandSlice = {
33
+ let name = "RecordDemandNoCheck"
34
+ module Id = Reventless.Id.String
35
+ let moduleUrl: string = %raw(`import.meta.url`)
36
+
37
+ // The produced event joins a product and an order, so the partition is
38
+ // declared, as the shop's RecordProductDemand does.
39
+ @schema
40
+ type event =
41
+ DemandRecorded({productId: @s.matches(Reventless.DcbTag.partition) string, orderId: string})
42
+
43
+ @schema
44
+ type consumedEvent =
45
+ DemandRecorded({productId: @s.matches(Reventless.DcbTag.string) string, orderId: string})
46
+
47
+ @schema
48
+ type command =
49
+ RecordDemand({productId: @s.matches(Reventless.DcbTag.string) string, orderId: string})
50
+
51
+ @schema
52
+ type error = Never
53
+
54
+ type role = Reventless.Role.name
55
+ let authorizationOf = (_: string): Reventless.Authorization.rule<role> => AllowAuthenticated
56
+ type lifecycleState = unit
57
+ let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
58
+ let traits: array<Reventless.Trait.t> = []
59
+ }
60
+
61
+ module DemandDecision = {
62
+ type state = {orders: array<string>}
63
+ let initialState = {orders: []}
64
+ let moduleUrl = DemandSlice.moduleUrl
65
+
66
+ let evolve = (state, event: DemandSlice.consumedEvent) =>
67
+ switch event {
68
+ | DemandRecorded({orderId}) => {orders: state.orders->Array.concat([orderId])}
69
+ }
70
+
71
+ // Deliberately no "already counted" rule.
72
+ let decide = (_state, command: DemandSlice.command): result<
73
+ array<DemandSlice.event>,
74
+ DemandSlice.error,
75
+ > =>
76
+ switch command {
77
+ | RecordDemand({productId, orderId}) => Ok([DemandRecorded({productId, orderId})])
78
+ }
79
+ }
80
+
81
+ module DemandHandler = ReventlessCore.StateChangeSlice_Callback.Make(DemandSlice, DemandDecision)
82
+
83
+ let partitionTag = Reventless.DcbTag.Simple({key: "productId"})
84
+
85
+ // The consumer's Extension, as the catalog's Orders_Extension is written.
86
+ module DemandExtension = ReventlessInfra.ExtensionMapping.Make({
87
+ module ExtensionPoint = Port
88
+ module Delegate = DemandSlice
89
+ let moduleUrl: string = %raw(`import.meta.url`)
90
+ let delegateModuleUrl = DemandSlice.moduleUrl
91
+ let mapIncomingEvent = (_id, event: Port.event, _meta, _pluginDef, _queryEngine) =>
92
+ switch event {
93
+ | ItemOrdered({productId, orderId}) => [
94
+ ReventlessInfra.ExtensionMapping.PublishStateChangeSliceCommand(
95
+ DemandSlice.RecordDemand({productId, orderId}),
96
+ ),
97
+ ]
98
+ }
99
+ let mapOutgoingEvent = None
100
+ let handledEvents = []
101
+ let issuedCommands = []
102
+ })
103
+
104
+ let queryEngine: Reventless.QueryEngine.operations = {
105
+ scan: async (~readModelName as _, ~filterConfigs as _, ~limit as _) => [],
106
+ query: async (
107
+ ~readModelName as _,
108
+ ~key as _=?,
109
+ ~id as _,
110
+ ~subIdConfig as _=?,
111
+ ~filterConfigs as _=?,
112
+ ~ascending as _=?,
113
+ ~limit as _=?,
114
+ ) => [],
115
+ }
116
+
117
+ let pluginDefinition: Reventless.Plugin.pluginDefinition = {
118
+ id: "Catalog@1",
119
+ name: "Catalog",
120
+ version: "1",
121
+ extensionPoints: [],
122
+ extensions: [{name: "Catalog", extensionPointName: Port.name}],
123
+ eventCollector: "eventCollector",
124
+ extensionProtocols: [],
125
+ apiSchemaFragment: None,
126
+ structure: None,
127
+ kind: Domain,
128
+ }
129
+
130
+ // One published ItemOrdered, as the extension point stored and published it;
131
+ // every delivery of it carries this same envelope.
132
+ let published: Message.event'<string, Port.event> = {
133
+ id: "order-1",
134
+ meta: {
135
+ service: Port.name,
136
+ time: "2024-01-01T00:00:00Z",
137
+ msgId: "item-ordered-msg",
138
+ correlationId: "place-order-cmd",
139
+ causationId: "order-placed-event",
140
+ },
141
+ event: ItemOrdered({productId: "product-1", orderId: "order-1"}),
142
+ }
143
+
144
+ // What the Extension sends for one delivery of `published`.
145
+ let commandOf = (event': Message.event'<string, Port.event>) =>
146
+ switch DemandExtension.mapIncomingEvent(event', pluginDefinition, queryEngine) {
147
+ | [ReventlessInfra.ExtensionMapping.AbstractPublishAggregateCommand(_, cmdJson)] => cmdJson
148
+ | _ => JsError.throwWithMessage("expected one slice command")
149
+ }
150
+
151
+ // ─── Backends ───────────────────────────────────────────────────────────────
152
+
153
+ let eventLogOn = async (~name) => {
154
+ module Bus = LocalBus.Make()
155
+ module Storage = LocalDcbEventLogStorage.Make(Bus)
156
+ let s = Storage.make(~name, ~indexes=[], ~partitionTag, ~opts)
157
+ let storage = await s.operations->TestRunner.resolve
158
+ module Log = ReventlessCore.DcbEventLog_Operations.Make({
159
+ let name = name
160
+ let serviceName = name
161
+ let storage = storage
162
+ let publishJson = async (_, _, _) => ()
163
+ })
164
+ let eventLog: ReventlessCore.DcbEventLog.operations = {
165
+ read: Log.read,
166
+ append: Log.append,
167
+ readStream: Log.readStream,
168
+ appendStream: Log.appendStream,
169
+ }
170
+ eventLog
171
+ }
172
+
173
+ let runUnderMemory = async fn => {
174
+ BackendState.setMemory()
175
+ await fn()
176
+ }
177
+
178
+ let runUnderSqlite = async fn => {
179
+ let db = SqliteDriver.openDb(~path=":memory:")
180
+ BackendState.setSqlite(~db, ~path=":memory:")
181
+ await fn()
182
+ BackendState.setMemory()
183
+ db->SqliteDriver.close
184
+ }
185
+
186
+ let deliver = (eventLog, cmd: Message.commandJson) =>
187
+ DemandHandler.handleCommands(
188
+ ~partitionTag,
189
+ eventLog,
190
+ Stream.fromIterable([
191
+ {
192
+ ReventlessInfra.CommandTopic.command: cmd
193
+ ->ReventlessCore.CommandTopic_Helpers.encodeCommandJson
194
+ ->Message.decodeCommand'(Reventless.Id.String.schema, DemandSlice.commandSchema),
195
+ reference: cmd.meta.msgId,
196
+ },
197
+ ]),
198
+ )->Effect.runPromise
199
+
200
+ let recordedDemands = async (eventLog: ReventlessCore.DcbEventLog.operations) =>
201
+ (await eventLog.read(~query=[{eventTypes: ["DemandRecorded"]}])).events
202
+
203
+ let counter = ref(0)
204
+ let fresh = prefix => {
205
+ counter := counter.contents + 1
206
+ `${prefix}-${counter.contents->Int.toString}`
207
+ }
208
+
209
+ let _ = beforeEach(() => DemandHandler.resetCache())
210
+
211
+ let backends = [("in-memory", runUnderMemory), ("SQLite", runUnderSqlite)]
212
+
213
+ backends->Array.forEach(((label, runUnder)) =>
214
+ describe(`a redelivered fact is recorded once (${label} backend)`, () => {
215
+ testPromise(
216
+ "the same published event delivered twice appends one event",
217
+ async () =>
218
+ await runUnder(
219
+ async () => {
220
+ let log = await eventLogOn(~name=fresh("RepeatOnce"))
221
+ let first = await deliver(log, commandOf(published))
222
+ let again = await deliver(log, commandOf(published))
223
+ expect(first->Array.length)->toBe(1)
224
+ expect(again)->toEqual(first) // accepted both times
225
+ let events = await recordedDemands(log)
226
+ expect(events->Array.length)->toBe(1)
227
+ // The command is the stored event's cause, and the published event the command's.
228
+ let cmd = commandOf(published)
229
+ expect((events->Array.getUnsafe(0)).meta.causationId)->toEqual(Some(cmd.meta.msgId))
230
+ expect(cmd.meta.causationId)->toEqual(Some(published.meta.msgId))
231
+ },
232
+ ),
233
+ )
234
+
235
+ testPromise(
236
+ "a different order for the same product still counts",
237
+ async () =>
238
+ await runUnder(
239
+ async () => {
240
+ let log = await eventLogOn(~name=fresh("RepeatOther"))
241
+ let _ = await deliver(log, commandOf(published))
242
+ let second: Message.event'<string, Port.event> = {
243
+ id: "order-2",
244
+ meta: {...published.meta, msgId: "item-ordered-msg-2"},
245
+ event: Port.ItemOrdered({productId: "product-1", orderId: "order-2"}),
246
+ }
247
+ let _ = await deliver(log, commandOf(second))
248
+ let _ = await deliver(log, commandOf(published))
249
+ let _ = await deliver(log, commandOf(second))
250
+ expect((await recordedDemands(log))->Array.length)->toBe(2)
251
+ },
252
+ ),
253
+ )
254
+
255
+ testPromise(
256
+ "a command with a random id is decided every time",
257
+ async () =>
258
+ await runUnder(
259
+ async () => {
260
+ let log = await eventLogOn(~name=fresh("RepeatRandom"))
261
+ let cmd = commandOf(published)
262
+ // The envelope of a command nobody derived: fresh id, no mark.
263
+ let plain = {...cmd, meta: Message.deriveMeta(~parent=cmd.meta)}
264
+ let _ = await deliver(log, plain)
265
+ let _ = await deliver(log, plain)
266
+ expect((await recordedDemands(log))->Array.length)->toBe(2)
267
+ },
268
+ ),
269
+ )
270
+
271
+ testPromise(
272
+ "two concurrent copies append one event",
273
+ async () =>
274
+ await runUnder(
275
+ async () => {
276
+ let log = await eventLogOn(~name=fresh("RepeatConcurrent"))
277
+ let cmd = commandOf(published)
278
+ // Both read an empty partition; the second append conflicts, re-reads,
279
+ // and finds the first one's event.
280
+ let (a, b) = await Promise.all2((deliver(log, cmd), deliver(log, cmd)))
281
+ expect(a->Array.length)->toBe(1)
282
+ expect(b->Array.length)->toBe(1)
283
+ expect(a->Array.getUnsafe(0)->Result.isOk && b->Array.getUnsafe(0)->Result.isOk)->toBe(
284
+ true,
285
+ )
286
+ expect((await recordedDemands(log))->Array.length)->toBe(1)
287
+ },
288
+ ),
289
+ )
290
+ })
291
+ )
@@ -0,0 +1,420 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
5
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Stream from "effect/Stream";
8
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
10
+ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
11
+ import * as LocalBus$ReventlessLocal from "../../../src/adapter/LocalBus.res.mjs";
12
+ import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
13
+ import * as BackendState$ReventlessLocal from "../../../src/adapter/BackendState.res.mjs";
14
+ import * as SqliteDriver$ReventlessLocal from "../../../src/adapter/SqliteDriver.res.mjs";
15
+ import * as ExtensionMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionMapping.res.mjs";
16
+ import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
17
+ import * as DcbEventLog_Operations$ReventlessCore from "@reventlessdev/reventless-core/src/components/DcbEventLog/DcbEventLog_Operations.res.mjs";
18
+ import * as LocalDcbEventLogStorage$ReventlessLocal from "../../../src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs";
19
+ import * as StateChangeSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs";
20
+
21
+ TestRunner$ReventlessLocal.setup();
22
+
23
+ let opts = {};
24
+
25
+ let name = "Ordering.Orders";
26
+
27
+ let commandSchema = Sury.literal("Noop");
28
+
29
+ let eventSchema = Sury.$schema(s => ({
30
+ TAG: "ItemOrdered",
31
+ productId: s.m(Sury.string),
32
+ orderId: s.m(Sury.string)
33
+ }));
34
+
35
+ let directiveSchema = Sury.literal("NoDirective");
36
+
37
+ let moduleUrl = import.meta.url;
38
+
39
+ function authorizationOf(param) {
40
+ return "AllowAuthenticated";
41
+ }
42
+
43
+ function commandTransition(param) {
44
+ return "Undeclared";
45
+ }
46
+
47
+ let traits = [];
48
+
49
+ let Port = {
50
+ name: name,
51
+ commandSchema: commandSchema,
52
+ eventSchema: eventSchema,
53
+ directiveSchema: directiveSchema,
54
+ moduleUrl: moduleUrl,
55
+ authorizationOf: authorizationOf,
56
+ commandTransition: commandTransition,
57
+ traits: traits
58
+ };
59
+
60
+ let name$1 = "RecordDemandNoCheck";
61
+
62
+ let moduleUrl$1 = import.meta.url;
63
+
64
+ let eventSchema$1 = Sury.$schema(s => ({
65
+ TAG: "DemandRecorded",
66
+ productId: s.m(DcbTag$Reventless.partition),
67
+ orderId: s.m(Sury.string)
68
+ }));
69
+
70
+ let consumedEventSchema = Sury.$schema(s => ({
71
+ TAG: "DemandRecorded",
72
+ productId: s.m(DcbTag$Reventless.string),
73
+ orderId: s.m(Sury.string)
74
+ }));
75
+
76
+ let commandSchema$1 = Sury.$schema(s => ({
77
+ TAG: "RecordDemand",
78
+ productId: s.m(DcbTag$Reventless.string),
79
+ orderId: s.m(Sury.string)
80
+ }));
81
+
82
+ let errorSchema = Sury.literal("Never");
83
+
84
+ function authorizationOf$1(param) {
85
+ return "AllowAuthenticated";
86
+ }
87
+
88
+ function commandTransition$1(param) {
89
+ return "Unrestricted";
90
+ }
91
+
92
+ let traits$1 = [];
93
+
94
+ let DemandSlice = {
95
+ name: name$1,
96
+ Id: undefined,
97
+ moduleUrl: moduleUrl$1,
98
+ eventSchema: eventSchema$1,
99
+ consumedEventSchema: consumedEventSchema,
100
+ commandSchema: commandSchema$1,
101
+ errorSchema: errorSchema,
102
+ authorizationOf: authorizationOf$1,
103
+ commandTransition: commandTransition$1,
104
+ traits: traits$1,
105
+ readConsistency: "EscalateOnRetry"
106
+ };
107
+
108
+ let initialState = {
109
+ orders: []
110
+ };
111
+
112
+ function evolve(state, event) {
113
+ return {
114
+ orders: state.orders.concat([event.orderId])
115
+ };
116
+ }
117
+
118
+ function decide(_state, command) {
119
+ return {
120
+ TAG: "Ok",
121
+ _0: [{
122
+ TAG: "DemandRecorded",
123
+ productId: command.productId,
124
+ orderId: command.orderId
125
+ }]
126
+ };
127
+ }
128
+
129
+ let DemandDecision = {
130
+ initialState: initialState,
131
+ moduleUrl: moduleUrl$1,
132
+ evolve: evolve,
133
+ decide: decide
134
+ };
135
+
136
+ let DemandHandler = StateChangeSlice_Callback$ReventlessCore.Make({
137
+ name: name$1,
138
+ moduleUrl: moduleUrl$1,
139
+ consumedEventSchema: consumedEventSchema,
140
+ errorSchema: errorSchema,
141
+ eventSchema: eventSchema$1,
142
+ commandSchema: commandSchema$1,
143
+ authorizationOf: authorizationOf$1,
144
+ commandTransition: commandTransition$1,
145
+ traits: traits$1,
146
+ readConsistency: "EscalateOnRetry"
147
+ })({
148
+ initialState: initialState,
149
+ evolve: evolve,
150
+ decide: decide,
151
+ moduleUrl: moduleUrl$1
152
+ });
153
+
154
+ let partitionTag = {
155
+ TAG: "Simple",
156
+ _0: {
157
+ key: "productId"
158
+ }
159
+ };
160
+
161
+ let moduleUrl$2 = import.meta.url;
162
+
163
+ function mapIncomingEvent(_id, event, _meta, _pluginDef, _queryEngine) {
164
+ return [{
165
+ TAG: "PublishStateChangeSliceCommand",
166
+ _0: {
167
+ TAG: "RecordDemand",
168
+ productId: event.productId,
169
+ orderId: event.orderId
170
+ }
171
+ }];
172
+ }
173
+
174
+ let handledEvents = [];
175
+
176
+ let issuedCommands = [];
177
+
178
+ let DemandExtension = ExtensionMapping$ReventlessInfra.Make({
179
+ ExtensionPoint: {
180
+ name: name,
181
+ moduleUrl: moduleUrl,
182
+ commandSchema: commandSchema,
183
+ eventSchema: eventSchema,
184
+ directiveSchema: directiveSchema
185
+ },
186
+ Delegate: {
187
+ Id: Id$Reventless.$$String,
188
+ name: name$1,
189
+ eventSchema: eventSchema$1,
190
+ errorSchema: errorSchema,
191
+ commandSchema: commandSchema$1,
192
+ moduleUrl: moduleUrl$1,
193
+ authorizationOf: authorizationOf$1,
194
+ commandTransition: commandTransition$1,
195
+ traits: traits$1
196
+ },
197
+ moduleUrl: moduleUrl$2,
198
+ delegateModuleUrl: moduleUrl$1,
199
+ mapIncomingEvent: mapIncomingEvent,
200
+ mapOutgoingEvent: undefined,
201
+ handledEvents: handledEvents,
202
+ issuedCommands: issuedCommands
203
+ });
204
+
205
+ async function queryEngine_scan(param, param$1, param$2) {
206
+ return [];
207
+ }
208
+
209
+ async function queryEngine_query(param, param$1, param$2, param$3, param$4, param$5, param$6) {
210
+ return [];
211
+ }
212
+
213
+ let queryEngine = {
214
+ scan: queryEngine_scan,
215
+ query: queryEngine_query
216
+ };
217
+
218
+ let pluginDefinition = {
219
+ id: "Catalog@1",
220
+ name: "Catalog",
221
+ version: "1",
222
+ extensionPoints: [],
223
+ extensions: [{
224
+ name: "Catalog",
225
+ extensionPointName: name
226
+ }],
227
+ eventCollector: "eventCollector",
228
+ extensionProtocols: [],
229
+ apiSchemaFragment: undefined,
230
+ structure: undefined,
231
+ kind: "Domain"
232
+ };
233
+
234
+ let published_meta = {
235
+ service: name,
236
+ time: "2024-01-01T00:00:00Z",
237
+ msgId: "item-ordered-msg",
238
+ correlationId: "place-order-cmd",
239
+ causationId: "order-placed-event"
240
+ };
241
+
242
+ let published_event = {
243
+ TAG: "ItemOrdered",
244
+ productId: "product-1",
245
+ orderId: "order-1"
246
+ };
247
+
248
+ let published = {
249
+ id: "order-1",
250
+ meta: published_meta,
251
+ event: published_event
252
+ };
253
+
254
+ function commandOf(event$p) {
255
+ let match = DemandExtension.mapIncomingEvent(event$p, pluginDefinition, queryEngine);
256
+ if (match.length !== 1) {
257
+ return Stdlib_JsError.throwWithMessage("expected one slice command");
258
+ }
259
+ let match$1 = match[0];
260
+ if (match$1.TAG === "AbstractPublishAggregateCommand") {
261
+ return match$1._1;
262
+ } else {
263
+ return Stdlib_JsError.throwWithMessage("expected one slice command");
264
+ }
265
+ }
266
+
267
+ async function eventLogOn(name) {
268
+ let Bus = LocalBus$ReventlessLocal.Make({});
269
+ let Storage = LocalDcbEventLogStorage$ReventlessLocal.Make(Bus);
270
+ let s = Storage.make(name, [], partitionTag, undefined, opts);
271
+ let storage = await TestRunner$ReventlessLocal.resolve(s.operations);
272
+ let publishJson = async (param, param$1, param$2) => {};
273
+ let Log = DcbEventLog_Operations$ReventlessCore.Make({
274
+ name: name,
275
+ serviceName: name,
276
+ storage: storage,
277
+ publishJson: publishJson
278
+ });
279
+ return {
280
+ read: Log.read,
281
+ append: Log.append,
282
+ readStream: Log.readStream,
283
+ appendStream: Log.appendStream
284
+ };
285
+ }
286
+
287
+ async function runUnderMemory(fn) {
288
+ BackendState$ReventlessLocal.setMemory();
289
+ return await fn();
290
+ }
291
+
292
+ async function runUnderSqlite(fn) {
293
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
294
+ BackendState$ReventlessLocal.setSqlite(db, ":memory:");
295
+ await fn();
296
+ BackendState$ReventlessLocal.setMemory();
297
+ return SqliteDriver$ReventlessLocal.close(db);
298
+ }
299
+
300
+ function deliver(eventLog, cmd) {
301
+ return Effect.runPromise(DemandHandler.handleCommands(undefined, undefined, partitionTag, eventLog, Stream.fromIterable([{
302
+ command: Message$ReventlessCore.decodeCommand$p(CommandTopic_Helpers$ReventlessCore.encodeCommandJson(cmd), Id$Reventless.$$String.schema, commandSchema$1),
303
+ reference: cmd.meta.msgId
304
+ }])));
305
+ }
306
+
307
+ async function recordedDemands(eventLog) {
308
+ return (await eventLog.read([{
309
+ eventTypes: ["DemandRecorded"]
310
+ }], undefined)).events;
311
+ }
312
+
313
+ let counter = {
314
+ contents: 0
315
+ };
316
+
317
+ function fresh(prefix) {
318
+ counter.contents = counter.contents + 1 | 0;
319
+ return prefix + `-` + counter.contents.toString();
320
+ }
321
+
322
+ globalThis.beforeEach(() => DemandHandler.resetCache());
323
+
324
+ let backends = [
325
+ [
326
+ "in-memory",
327
+ runUnderMemory
328
+ ],
329
+ [
330
+ "SQLite",
331
+ runUnderSqlite
332
+ ]
333
+ ];
334
+
335
+ backends.forEach(param => {
336
+ let runUnder = param[1];
337
+ globalThis.describe(`a redelivered fact is recorded once (` + param[0] + ` backend)`, () => {
338
+ globalThis.test("the same published event delivered twice appends one event", async () => await runUnder(async () => {
339
+ let log = await eventLogOn(fresh("RepeatOnce"));
340
+ let first = await deliver(log, commandOf(published));
341
+ let again = await deliver(log, commandOf(published));
342
+ globalThis.expect(first.length).toBe(1);
343
+ globalThis.expect(again).toEqual(first);
344
+ let events = await recordedDemands(log);
345
+ globalThis.expect(events.length).toBe(1);
346
+ let cmd = commandOf(published);
347
+ globalThis.expect(events[0].meta.causationId).toEqual(cmd.meta.msgId);
348
+ globalThis.expect(cmd.meta.causationId).toEqual(published_meta.msgId);
349
+ }));
350
+ globalThis.test("a different order for the same product still counts", async () => await runUnder(async () => {
351
+ let log = await eventLogOn(fresh("RepeatOther"));
352
+ await deliver(log, commandOf(published));
353
+ let newrecord = {...published_meta};
354
+ let second_meta = (newrecord.msgId = "item-ordered-msg-2", newrecord);
355
+ let second_event = {
356
+ TAG: "ItemOrdered",
357
+ productId: "product-1",
358
+ orderId: "order-2"
359
+ };
360
+ let second = {
361
+ id: "order-2",
362
+ meta: second_meta,
363
+ event: second_event
364
+ };
365
+ await deliver(log, commandOf(second));
366
+ await deliver(log, commandOf(published));
367
+ await deliver(log, commandOf(second));
368
+ globalThis.expect((await recordedDemands(log)).length).toBe(2);
369
+ }));
370
+ globalThis.test("a command with a random id is decided every time", async () => await runUnder(async () => {
371
+ let log = await eventLogOn(fresh("RepeatRandom"));
372
+ let cmd = commandOf(published);
373
+ let newrecord = {...cmd};
374
+ newrecord.meta = Message$ReventlessCore.deriveMeta(cmd.meta, undefined);
375
+ await deliver(log, newrecord);
376
+ await deliver(log, newrecord);
377
+ globalThis.expect((await recordedDemands(log)).length).toBe(2);
378
+ }));
379
+ globalThis.test("two concurrent copies append one event", async () => await runUnder(async () => {
380
+ let log = await eventLogOn(fresh("RepeatConcurrent"));
381
+ let cmd = commandOf(published);
382
+ let match = await Promise.all([
383
+ deliver(log, cmd),
384
+ deliver(log, cmd)
385
+ ]);
386
+ let b = match[1];
387
+ let a = match[0];
388
+ globalThis.expect(a.length).toBe(1);
389
+ globalThis.expect(b.length).toBe(1);
390
+ globalThis.expect(Stdlib_Result.isOk(a[0]) && Stdlib_Result.isOk(b[0])).toBe(true);
391
+ globalThis.expect((await recordedDemands(log)).length).toBe(1);
392
+ }));
393
+ });
394
+ });
395
+
396
+ let Message;
397
+
398
+ export {
399
+ opts,
400
+ Message,
401
+ Port,
402
+ DemandSlice,
403
+ DemandDecision,
404
+ DemandHandler,
405
+ partitionTag,
406
+ DemandExtension,
407
+ queryEngine,
408
+ pluginDefinition,
409
+ published,
410
+ commandOf,
411
+ eventLogOn,
412
+ runUnderMemory,
413
+ runUnderSqlite,
414
+ deliver,
415
+ recordedDemands,
416
+ counter,
417
+ fresh,
418
+ backends,
419
+ }
420
+ /* Not a pure module */
@@ -1,6 +1,7 @@
1
- // Verifies Phase 2 of Plan 03: a ReadModel whose `Mapping.sourceName` doesn't
2
- // match any key in `allEventTopics` fails plugin assembly (here: ReadModel
3
- // construction) with a clear error, instead of silently producing zero events.
1
+ // A ReadModel whose `Mapping.sourceName` doesn't match any key in `allEventTopics`
2
+ // fails plugin assembly (here: ReadModel construction) with a clear error, instead
3
+ // of silently producing zero events. A dotted name is an extension point's, and
4
+ // the error then also says how another plugin's facts do reach a read model.
4
5
 
5
6
  open JestGlobals
6
7
  open Reventless
@@ -68,22 +69,77 @@ let allEventTopics: ReventlessInfra.EventTopic.allOutputs = Dict.fromArray([
68
69
  ("ProductCatalogDcbEventLog", {ReventlessInfra.EventTopic.resources: [topicResource]}),
69
70
  ])
70
71
 
72
+ // A Source named like an extension point ("Ordering.Orders"): a read model
73
+ // cannot subscribe to one, so the message must say where to go instead.
74
+ module ExtensionPointSource = {
75
+ module Id = Reventless.Id.String
76
+ let name = "Ordering.Orders"
77
+
78
+ @schema
79
+ type event = ItemOrdered({productId: string, orderId: string})
80
+ }
81
+
82
+ module EpRmSpec = {
83
+ module Id = Reventless.Id.String
84
+ let name = "EpCheckReadModel"
85
+ let moduleUrl: string = %raw(`import.meta.url`)
86
+
87
+ @schema
88
+ type state = {productId: string, orderCount: int}
89
+
90
+ let config = ReadModel.config()
91
+ let subIdConfig = None
92
+ }
93
+
94
+ module EpMapping = Mapping.Make(
95
+ ExtensionPointSource,
96
+ EpRmSpec,
97
+ {
98
+ let project = (msg: Message.event'<ExtensionPointSource.Id.t, ExtensionPointSource.event>) =>
99
+ switch msg.event {
100
+ | ItemOrdered({productId}) =>
101
+ Set(EpRmSpec.Id.makeFromString(productId), ({productId, orderCount: 1}: EpRmSpec.state))
102
+ }
103
+ },
104
+ )
105
+
106
+ module EpMappings: Mappings with module Target := EpRmSpec = {
107
+ module Mappings = Mappings.Make(EpRmSpec)
108
+ module type Mapping = Mappings.Mapping
109
+ let moduleUrl: string = %raw(`import.meta.url`)
110
+ let mappings: array<module(Mapping)> = [module(EpMapping)]
111
+ }
112
+
71
113
  module ReadModelMaker = ReadModel_Builder.Make(Bus)
72
114
  module RmMaker = ReadModelMaker.Make(RmSpec, TypoMappings)
115
+ module EpRmMaker = ReadModelMaker.Make(EpRmSpec, EpMappings)
116
+
117
+ let messageOf = make => {
118
+ let msg = ref(None)
119
+ try {
120
+ let _ = make()
121
+ } catch {
122
+ | JsExn(err) => msg := Some(err->JsExn.message->Option.getOr(""))
123
+ }
124
+ msg.contents
125
+ }
73
126
 
74
127
  describe("ReadModel source-name fail-fast:", () => {
75
128
  testSync("constructing a ReadModel whose Mapping.sourceName is missing throws", () => {
76
- let threw = ref(false)
77
- let msg = ref("")
78
- try {
79
- let _ = RmMaker.make(~api=(), ~apiRole=(), ~allEventTopics)
80
- } catch {
81
- | JsExn(err) =>
82
- threw := true
83
- msg := err->JsExn.message->Option.getOr("")
84
- }
85
- expect(threw.contents)->toBe(true)->ignore
86
- expect(msg.contents->String.includes("MissingDcb"))->toBe(true)->ignore
87
- expect(msg.contents->String.includes("ProductCatalogDcbEventLog"))->toBe(true)
129
+ let msg =
130
+ messageOf(() => RmMaker.make(~api=(), ~apiRole=(), ~allEventTopics))->Option.getOrThrow
131
+ expect(msg->String.includes("MissingDcb"))->toBe(true)
132
+ expect(msg->String.includes("ProductCatalogDcbEventLog"))->toBe(true)
133
+ // A typo keeps the plain message: no extension point is being named.
134
+ expect(msg->String.includes("extension point"))->toBe(false)
135
+ })
136
+
137
+ testSync("a dotted sourceName names the Extension → slice route instead", () => {
138
+ let msg =
139
+ messageOf(() => EpRmMaker.make(~api=(), ~apiRole=(), ~allEventTopics))->Option.getOrThrow
140
+ expect(msg->String.includes("Ordering.Orders"))->toBe(true)
141
+ expect(msg->String.includes("looks like an extension point name"))->toBe(true)
142
+ expect(msg->String.includes("Declare an Extension"))->toBe(true)
143
+ expect(msg->String.includes("Targeting a StateChangeSlice instead of an Aggregate"))->toBe(true)
88
144
  })
89
145
  })
@@ -104,6 +104,84 @@ let allEventTopics = Object.fromEntries([[
104
104
  }
105
105
  ]]);
106
106
 
107
+ let name$2 = "Ordering.Orders";
108
+
109
+ let eventSchema$1 = Sury.$schema(s => ({
110
+ TAG: "ItemOrdered",
111
+ productId: s.m(Sury.string),
112
+ orderId: s.m(Sury.string)
113
+ }));
114
+
115
+ let ExtensionPointSource = {
116
+ Id: undefined,
117
+ name: name$2,
118
+ eventSchema: eventSchema$1
119
+ };
120
+
121
+ let name$3 = "EpCheckReadModel";
122
+
123
+ let moduleUrl$2 = import.meta.url;
124
+
125
+ let stateSchema$1 = Sury.$schema(s => ({
126
+ productId: s.m(Sury.string),
127
+ orderCount: s.m(Sury.int32)
128
+ }));
129
+
130
+ let config$1 = ReadModel$Reventless.config(undefined, undefined, undefined);
131
+
132
+ let EpRmSpec = {
133
+ Id: undefined,
134
+ name: name$3,
135
+ moduleUrl: moduleUrl$2,
136
+ stateSchema: stateSchema$1,
137
+ config: config$1,
138
+ subIdConfig: undefined,
139
+ authorization: "AllowAuthenticated",
140
+ visibility: "Public"
141
+ };
142
+
143
+ function project$1(msg) {
144
+ let match = msg.event;
145
+ let productId = match.productId;
146
+ return {
147
+ TAG: "Set",
148
+ _0: Id$Reventless.$$String.makeFromString(productId),
149
+ _1: {
150
+ productId: productId,
151
+ orderCount: 1
152
+ }
153
+ };
154
+ }
155
+
156
+ let EpMapping = Projection$Reventless.Mapping.Make({
157
+ Id: Id$Reventless.$$String,
158
+ name: name$2,
159
+ eventSchema: eventSchema$1
160
+ })({
161
+ Id: Id$Reventless.$$String,
162
+ name: name$3,
163
+ stateSchema: stateSchema$1,
164
+ subIdConfig: undefined
165
+ })({
166
+ project: project$1
167
+ });
168
+
169
+ Projection$Reventless.Mappings.Make({
170
+ Id: Id$Reventless.$$String,
171
+ name: name$3,
172
+ stateSchema: stateSchema$1,
173
+ subIdConfig: undefined
174
+ });
175
+
176
+ let moduleUrl$3 = import.meta.url;
177
+
178
+ let mappings$1 = [EpMapping];
179
+
180
+ let EpMappings = {
181
+ moduleUrl: moduleUrl$3,
182
+ mappings: mappings$1
183
+ };
184
+
107
185
  let ReadModelMaker = ReadModel_Builder$ReventlessLocal.Make(Bus);
108
186
 
109
187
  let RmMaker = ReadModelMaker.Make({
@@ -117,24 +195,45 @@ let RmMaker = ReadModelMaker.Make({
117
195
  visibility: "Public"
118
196
  })(TypoMappings);
119
197
 
198
+ let EpRmMaker = ReadModelMaker.Make({
199
+ Id: Id$Reventless.$$String,
200
+ name: name$3,
201
+ moduleUrl: moduleUrl$2,
202
+ stateSchema: stateSchema$1,
203
+ config: config$1,
204
+ subIdConfig: undefined,
205
+ authorization: "AllowAuthenticated",
206
+ visibility: "Public"
207
+ })(EpMappings);
208
+
209
+ function messageOf(make) {
210
+ let msg;
211
+ try {
212
+ make();
213
+ } catch (raw_err) {
214
+ let err = Primitive_exceptions.internalToException(raw_err);
215
+ if (err.RE_EXN_ID === "JsExn") {
216
+ msg = Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "");
217
+ } else {
218
+ throw err;
219
+ }
220
+ }
221
+ return msg;
222
+ }
223
+
120
224
  globalThis.describe("ReadModel source-name fail-fast:", () => {
121
225
  globalThis.test("constructing a ReadModel whose Mapping.sourceName is missing throws", () => {
122
- let threw = false;
123
- let msg = "";
124
- try {
125
- RmMaker.make(undefined, undefined, allEventTopics, undefined, undefined);
126
- } catch (raw_err) {
127
- let err = Primitive_exceptions.internalToException(raw_err);
128
- if (err.RE_EXN_ID === "JsExn") {
129
- threw = true;
130
- msg = Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "");
131
- } else {
132
- throw err;
133
- }
134
- }
135
- globalThis.expect(threw).toBe(true);
226
+ let msg = Stdlib_Option.getOrThrow(messageOf(() => RmMaker.make(undefined, undefined, allEventTopics, undefined, undefined)), undefined);
136
227
  globalThis.expect(msg.includes("MissingDcb")).toBe(true);
137
228
  globalThis.expect(msg.includes("ProductCatalogDcbEventLog")).toBe(true);
229
+ globalThis.expect(msg.includes("extension point")).toBe(false);
230
+ });
231
+ globalThis.test("a dotted sourceName names the Extension → slice route instead", () => {
232
+ let msg = Stdlib_Option.getOrThrow(messageOf(() => EpRmMaker.make(undefined, undefined, allEventTopics, undefined, undefined)), undefined);
233
+ globalThis.expect(msg.includes("Ordering.Orders")).toBe(true);
234
+ globalThis.expect(msg.includes("looks like an extension point name")).toBe(true);
235
+ globalThis.expect(msg.includes("Declare an Extension")).toBe(true);
236
+ globalThis.expect(msg.includes("Targeting a StateChangeSlice instead of an Aggregate")).toBe(true);
138
237
  });
139
238
  });
140
239
 
@@ -146,7 +245,13 @@ export {
146
245
  TypoMappings,
147
246
  topicResource,
148
247
  allEventTopics,
248
+ ExtensionPointSource,
249
+ EpRmSpec,
250
+ EpMapping,
251
+ EpMappings,
149
252
  ReadModelMaker,
150
253
  RmMaker,
254
+ EpRmMaker,
255
+ messageOf,
151
256
  }
152
257
  /* Bus Not a pure module */
@@ -7,9 +7,7 @@ let pluginDefinition: Reventless.Plugin.pluginDefinition = {
7
7
  name: "name",
8
8
  version: "1",
9
9
  extensionPoints: [],
10
- extensions: [
11
- {name: "Platform.Plugin.Test", extensionPointName: "Platform.Plugin", dcbSources: []},
12
- ],
10
+ extensions: [{name: "Platform.Plugin.Test", extensionPointName: "Platform.Plugin"}],
13
11
  eventCollector: "eventCollector",
14
12
  extensionProtocols: [],
15
13
  apiSchemaFragment: None,
@@ -10,8 +10,7 @@ let pluginDefinition = {
10
10
  extensionPoints: [],
11
11
  extensions: [{
12
12
  name: "Platform.Plugin.Test",
13
- extensionPointName: "Platform.Plugin",
14
- dcbSources: []
13
+ extensionPointName: "Platform.Plugin"
15
14
  }],
16
15
  eventCollector: "eventCollector",
17
16
  extensionProtocols: [],