@reventlessdev/reventless-local 3.0.0-alpha.130 → 3.0.0-alpha.132

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,20 @@
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.132 (2026-07-09)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **reventless-core:** DCB StateChangeSlices expose one mutation per command constructor ([136fe93](https://github.com/ReventlessDev/reventless-core/commit/136fe93c56c2e5115eb60d792e9d5e9c07eecc2b))
11
+
12
+
13
+ # 3.0.0-alpha.131 (2026-07-08)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **reventless-core:** composite DCB slices can read back their own events ([4604a91](https://github.com/ReventlessDev/reventless-core/commit/4604a9159fb8bf59b2191ad69fee6613c7f75cd9))
18
+
19
+
6
20
  # 3.0.0-alpha.130 (2026-07-08)
7
21
 
8
22
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.130",
3
+ "version": "3.0.0-alpha.132",
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
  "jest": {
@@ -31,15 +31,15 @@
31
31
  "graphql-yoga": "^5.21.0",
32
32
  "sury": "11.0.0-alpha.4",
33
33
  "ws": "^8.18.0",
34
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.25",
34
35
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.18",
35
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.17",
36
36
  "@reventlessdev/rescript-jest": "1.0.0-alpha.6",
37
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.17",
37
38
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
38
- "@reventlessdev/rescript-effect": "0.1.0-alpha.25",
39
- "@reventlessdev/reventless-core": "3.0.0-alpha.146",
40
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.91",
39
+ "@reventlessdev/reventless-core": "3.0.0-alpha.148",
40
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.93",
41
41
  "@reventlessdev/reventless-infra": "3.0.0-alpha.91",
42
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.10",
42
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.12",
43
43
  "@reventlessdev/reventless-spec": "3.0.0-alpha.69"
44
44
  },
45
45
  "devDependencies": {
@@ -54,7 +54,7 @@ let bridgeSourceA = (
54
54
  let ps = getPubSub()
55
55
  let yogaTopic = sourceATopic(displayName)
56
56
  // Push the raw event JSON directly — it already contains the event data.
57
- // In-memory events don't carry a separate position/originatorSlice envelope.
57
+ // In-memory events don't carry a separate position envelope.
58
58
  subscribeToEvents(busTopicName, async (_service, _meta, json) => {
59
59
  ps->YG.pubSubPublish(yogaTopic, json)
60
60
  })
@@ -237,12 +237,20 @@ let registerDcb = (
237
237
  ~server: GraphQL_ServerInstance.t,
238
238
  ) => {
239
239
  ensureCommandResultTypes(server)
240
- let variantSchema = extractVariantSchema(commandSchema)
240
+ // Derive the constructor from the field name's trailing `_` segment (mirrors the
241
+ // aggregate resolver). A single-command slice's `Plugin_Slice` resolves to its sole
242
+ // constructor (slice name == constructor); a multi-command slice's `Plugin_Slice_Ctor`
243
+ // resolves to `Ctor`. Position 0 was previously hardcoded, which dropped every
244
+ // non-first constructor of a multi-command slice.
245
+ let constructorNames = Reventless.DcbTag.extractAllVariantNames(commandSchema->Obj.magic)
246
+ let variantIndex = variantIndexForField(commandSchema, ~field=fieldName)
247
+ let variantSchema = extractVariantSchema(commandSchema, ~index=variantIndex >= 0 ? variantIndex : 0)
241
248
  let sdlFields = [deriveSdlField(~fieldName, variantSchema)]
242
249
 
243
250
  // Extract TAG (variant constructor name) for routing
244
- let constructorNames = Reventless.DcbTag.extractAllVariantNames(commandSchema->Obj.magic)
245
- let tag = constructorNames->Array.get(0)->Option.getOr(fieldName)
251
+ let tag =
252
+ (variantIndex >= 0 ? constructorNames->Array.get(variantIndex) : constructorNames->Array.get(0))
253
+ ->Option.getOr(fieldName)
246
254
  let hasPayload = Reventless.DcbTag.isVariantPayloadBearing(commandSchema->Obj.magic, tag)
247
255
 
248
256
  let handlerRef = ref(None)
@@ -198,10 +198,11 @@ function register(fields, commandSchema, commandAuthorization, server) {
198
198
 
199
199
  function registerDcb(fieldName, commandSchema, commandAuthorization, server) {
200
200
  ensureCommandResultTypes(server);
201
- let variantSchema = extractVariantSchema(commandSchema, undefined);
202
- let sdlFields = [deriveSdlField(fieldName, variantSchema)];
203
201
  let constructorNames = DcbTag$Reventless.extractAllVariantNames(commandSchema);
204
- let tag = Stdlib_Option.getOr(constructorNames[0], fieldName);
202
+ let variantIndex = variantIndexForField(commandSchema, fieldName);
203
+ let variantSchema = extractVariantSchema(commandSchema, variantIndex >= 0 ? variantIndex : 0);
204
+ let sdlFields = [deriveSdlField(fieldName, variantSchema)];
205
+ let tag = Stdlib_Option.getOr(variantIndex >= 0 ? constructorNames[variantIndex] : constructorNames[0], fieldName);
205
206
  let hasPayload = DcbTag$Reventless.isVariantPayloadBearing(commandSchema, tag);
206
207
  let handlerRef = {
207
208
  contents: undefined
@@ -38,6 +38,23 @@ type unionCommand =
38
38
  | Create({itemId: @s.matches(Reventless.DcbTag.string) string, name: string})
39
39
  | Rename({itemId: @s.matches(Reventless.DcbTag.string) string, newName: string})
40
40
 
41
+ // DCB StateChangeSlice command shapes for the per-constructor mutation fan-out.
42
+ // A single-variant union models a single-command slice (must keep the byte-identical
43
+ // `Plugin_Slice` field); a multi-variant union models a multi-command slice (must
44
+ // expose one `Plugin_Slice_Ctor` field per constructor).
45
+ @schema
46
+ type dcbSingleCommand =
47
+ | AddCategory({categoryId: @s.matches(Reventless.DcbTag.string) string, name: string})
48
+
49
+ @schema
50
+ type dcbMultiCommand =
51
+ | SyncNewProduct({
52
+ productId: @s.matches(Reventless.DcbTag.string) string,
53
+ name: string,
54
+ price: float,
55
+ })
56
+ | ChangeSyncedPrice({productId: @s.matches(Reventless.DcbTag.string) string, price: float})
57
+
41
58
  // State schemas exercising structural annotations (Phase 1 server capability)
42
59
  @schema
43
60
  type plainState = {
@@ -769,6 +786,75 @@ describe("GraphQL_SchemaInspector", () => {
769
786
  // the field must fold a `kindEq` equality filter into Platform_PluginFilter so the
770
787
  // admin console can page the main list vs the System/Infrastructure panel
771
788
  // server-side.
789
+ // ── DCB StateChangeSlice: one mutation per command constructor ───────────────
790
+ // A multi-command DCB slice must expose one GraphQL mutation per command
791
+ // constructor (mirroring aggregates), each carrying its own argument shape and its
792
+ // own subscription — not a single slice-named mutation that drops the extra
793
+ // constructors. Single-command slices must stay byte-identical (no schema churn).
794
+ describe("DCB StateChangeSlice — one mutation per command constructor", () => {
795
+ testPromise("single-command slice keeps the byte-identical Plugin_Slice field", async () => {
796
+ let fieldNames =
797
+ ReventlessCore.Api_Naming.sliceMutationFields(
798
+ ~plugin="Catalog",
799
+ ~slice="AddCategory",
800
+ ~commandSchema=dcbSingleCommandSchema->S.castToUnknown,
801
+ )->Array.map(((f, _)) => f)
802
+ expect(fieldNames)->toEqual(["Catalog_AddCategory"])
803
+ })
804
+
805
+ testPromise("multi-command slice emits one field per constructor", async () => {
806
+ let fieldNames =
807
+ ReventlessCore.Api_Naming.sliceMutationFields(
808
+ ~plugin="Ordering",
809
+ ~slice="SyncCatalogProduct",
810
+ ~commandSchema=dcbMultiCommandSchema->S.castToUnknown,
811
+ )->Array.map(((f, _)) => f)
812
+ expect(fieldNames)->toEqual([
813
+ "Ordering_SyncCatalogProduct_SyncNewProduct",
814
+ "Ordering_SyncCatalogProduct_ChangeSyncedPrice",
815
+ ])
816
+ })
817
+
818
+ testPromise(
819
+ "multi-command slice: both mutations carry their own args, plus both subscriptions",
820
+ async () => {
821
+ let fieldNames =
822
+ ReventlessCore.Api_Naming.sliceMutationFields(
823
+ ~plugin="Ordering",
824
+ ~slice="SyncCatalogProduct",
825
+ ~commandSchema=dcbMultiCommandSchema->S.castToUnknown,
826
+ )->Array.map(((f, _)) => f)
827
+ let commandSchema = dcbMultiCommandSchema->S.castToUnknown
828
+
829
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
830
+ ~mutationEntries=[{fieldNames, commandSchema}],
831
+ ~queryEntries=[],
832
+ )
833
+ let sdl = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment).sdlPreview
834
+ // Each constructor's mutation carries that constructor's own argument shape.
835
+ expect(
836
+ sdl->String.includes(
837
+ "Ordering_SyncCatalogProduct_SyncNewProduct(id: ID!, productId: ID!, name: String!, price: Float!)",
838
+ ),
839
+ )->toBe(true)
840
+ expect(
841
+ sdl->String.includes(
842
+ "Ordering_SyncCatalogProduct_ChangeSyncedPrice(id: ID!, productId: ID!, price: Float!)",
843
+ ),
844
+ )->toBe(true)
845
+
846
+ // Subscriptions fan out one-per-field over the same entry.
847
+ let subs = ReventlessCore.Plugin_SubscriptionSchema.generate(
848
+ ~mutationEntries=[{fieldNames, commandSchema}],
849
+ ~eventLogEntries=[],
850
+ )
851
+ let subSdl = subs.subscriptionFields->Array.join("\n")
852
+ expect(subSdl->String.includes("onOrdering_SyncCatalogProduct_SyncNewProduct"))->toBe(true)
853
+ expect(subSdl->String.includes("onOrdering_SyncCatalogProduct_ChangeSyncedPrice"))->toBe(true)
854
+ },
855
+ )
856
+ })
857
+
772
858
  describe("Plugin admin fragment — kind enum + kindEq filter", () => {
773
859
  testPromise("Platform_Plugin exposes a PluginKind enum and Platform_Plugins gains kindEq", async () => {
774
860
  let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
@@ -3,6 +3,7 @@
3
3
  import * as S from "sury/src/S.res.mjs";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
6
+ import * as Api_Naming$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Naming.res.mjs";
6
7
  import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
7
8
  import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
8
9
  import * as GraphQL_Server$ReventlessLocal from "../../src/adapter/GraphQL_Server.res.mjs";
@@ -10,6 +11,7 @@ import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core
10
11
  import * as PluginBaseFragment$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/api/PluginBaseFragment.res.mjs";
11
12
  import * as GraphQL_SchemaInspector$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_SchemaInspector.res.mjs";
12
13
  import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
14
+ import * as Plugin_SubscriptionSchema$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_SubscriptionSchema.res.mjs";
13
15
 
14
16
  TestRunner$ReventlessLocal.setup();
15
17
 
@@ -44,6 +46,26 @@ let unionCommandSchema = S.union([
44
46
  }))
45
47
  ]);
46
48
 
49
+ let dcbSingleCommandSchema = S.schema(s => ({
50
+ TAG: "AddCategory",
51
+ categoryId: s.m(DcbTag$Reventless.string),
52
+ name: s.m(S.string)
53
+ }));
54
+
55
+ let dcbMultiCommandSchema = S.union([
56
+ S.schema(s => ({
57
+ TAG: "SyncNewProduct",
58
+ productId: s.m(DcbTag$Reventless.string),
59
+ name: s.m(S.string),
60
+ price: s.m(S.float)
61
+ })),
62
+ S.schema(s => ({
63
+ TAG: "ChangeSyncedPrice",
64
+ productId: s.m(DcbTag$Reventless.string),
65
+ price: s.m(S.float)
66
+ }))
67
+ ]);
68
+
47
69
  let plainStateSchema = S.schema(s => ({
48
70
  productId: s.m(DcbTag$Reventless.string),
49
71
  name: s.m(S.string)
@@ -551,6 +573,37 @@ globalThis.describe("GraphQL_SchemaInspector", () => {
551
573
  });
552
574
  });
553
575
 
576
+ globalThis.describe("DCB StateChangeSlice — one mutation per command constructor", () => {
577
+ globalThis.test("single-command slice keeps the byte-identical Plugin_Slice field", async () => {
578
+ let fieldNames = Api_Naming$ReventlessCore.sliceMutationFields("Catalog", "AddCategory", dcbSingleCommandSchema).map(param => param[0]);
579
+ globalThis.expect(fieldNames).toEqual(["Catalog_AddCategory"]);
580
+ });
581
+ globalThis.test("multi-command slice emits one field per constructor", async () => {
582
+ let fieldNames = Api_Naming$ReventlessCore.sliceMutationFields("Ordering", "SyncCatalogProduct", dcbMultiCommandSchema).map(param => param[0]);
583
+ globalThis.expect(fieldNames).toEqual([
584
+ "Ordering_SyncCatalogProduct_SyncNewProduct",
585
+ "Ordering_SyncCatalogProduct_ChangeSyncedPrice"
586
+ ]);
587
+ });
588
+ globalThis.test("multi-command slice: both mutations carry their own args, plus both subscriptions", async () => {
589
+ let fieldNames = Api_Naming$ReventlessCore.sliceMutationFields("Ordering", "SyncCatalogProduct", dcbMultiCommandSchema).map(param => param[0]);
590
+ let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate([{
591
+ fieldNames: fieldNames,
592
+ commandSchema: dcbMultiCommandSchema
593
+ }], []);
594
+ let sdl = GraphQL_SchemaInspector$ReventlessCore.inspectFragment(fragment).sdlPreview;
595
+ globalThis.expect(sdl.includes("Ordering_SyncCatalogProduct_SyncNewProduct(id: ID!, productId: ID!, name: String!, price: Float!)")).toBe(true);
596
+ globalThis.expect(sdl.includes("Ordering_SyncCatalogProduct_ChangeSyncedPrice(id: ID!, productId: ID!, price: Float!)")).toBe(true);
597
+ let subs = Plugin_SubscriptionSchema$ReventlessCore.generate([{
598
+ fieldNames: fieldNames,
599
+ commandSchema: dcbMultiCommandSchema
600
+ }], []);
601
+ let subSdl = subs.subscriptionFields.join("\n");
602
+ globalThis.expect(subSdl.includes("onOrdering_SyncCatalogProduct_SyncNewProduct")).toBe(true);
603
+ globalThis.expect(subSdl.includes("onOrdering_SyncCatalogProduct_ChangeSyncedPrice")).toBe(true);
604
+ });
605
+ });
606
+
554
607
  globalThis.describe("Plugin admin fragment — kind enum + kindEq filter", () => {
555
608
  globalThis.test("Platform_Plugin exposes a PluginKind enum and Platform_Plugins gains kindEq", async () => {
556
609
  let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate([], PluginBaseFragment$ReventlessCore.queryEntries);
@@ -579,6 +632,8 @@ export {
579
632
  svStateSchema,
580
633
  addCommandSchema,
581
634
  unionCommandSchema,
635
+ dcbSingleCommandSchema,
636
+ dcbMultiCommandSchema,
582
637
  plainStateSchema,
583
638
  indexedStateSchema,
584
639
  indexedStateSchemaWithAnnotations,