@reventlessdev/reventless-local 3.0.0-alpha.164 → 3.0.0-alpha.166

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.166 (2026-07-23)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** round-trip the DynamoDB Scan cursor in full-list connections ([0e21008](https://github.com/ReventlessDev/reventless-core/commit/0e2100860767a153a5c2f50a88fd6b501787c7f4))
11
+
12
+
13
+ # 3.0.0-alpha.165 (2026-07-22)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **aws:** keep an empty DCB tag value from breaking the append ([1c0ab40](https://github.com/ReventlessDev/reventless-core/commit/1c0ab40f25b596702a81f42686d77c5e423e9b44))
18
+
19
+
6
20
  # 3.0.0-alpha.164 (2026-07-22)
7
21
 
8
22
  * refactor!: retire the stale "Admin" vocabulary for platform-owned resources ([2770bd9](https://github.com/ReventlessDev/reventless-core/commit/2770bd96f309142bdfc65b9f36bd1a3ef60641bd))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.164",
3
+ "version": "3.0.0-alpha.166",
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": {
@@ -32,16 +32,16 @@
32
32
  "sury": "11.0.0-alpha.4",
33
33
  "ws": "^8.18.0",
34
34
  "@reventlessdev/rescript-effect": "0.1.0-alpha.30",
35
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.20",
35
36
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.24",
36
37
  "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
37
38
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
38
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.20",
39
- "@reventlessdev/reventless-core": "3.0.0-alpha.177",
40
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.24",
41
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.124",
39
+ "@reventlessdev/reventless-core": "3.0.0-alpha.179",
40
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.26",
41
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.126",
42
42
  "@reventlessdev/reventless-infra": "3.0.0-alpha.104",
43
43
  "@reventlessdev/reventless-spec": "3.0.0-alpha.80",
44
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.41"
44
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.43"
45
45
  },
46
46
  "devDependencies": {
47
47
  "rescript": "12.3.0",
@@ -91,6 +91,39 @@ describe("DcbEventLogStorage_Sqlite", () => {
91
91
  })
92
92
  })
93
93
 
94
+ // Backend contract: an empty tag value (an absent composite-partition member) is
95
+ // legitimate — it appends, is recorded verbatim, and keeps matching through both
96
+ // the partition read and a composite read. Held identically by the in-memory and
97
+ // DynamoDB backends. See docs/plans/dcb-empty-tag-values-break-append.md.
98
+ testPromise("an empty tag value appends and stays readable", async () => {
99
+ await runUnderSqlite(async () => {
100
+ module TestBus = LocalBus.Make()
101
+ module Storage = LocalDcbEventLogStorage.Make(TestBus)
102
+ let s = Storage.make(
103
+ ~name="dcb-emptytag",
104
+ ~indexes=[],
105
+ ~partitionTag=Reventless.DcbTag.Simple({key: "itemId"}),
106
+ ~opts,
107
+ )
108
+ let ops = await s.operations->TestRunner.resolve
109
+
110
+ let tags = [{Reventless.DcbTag.key: "itemId", value: "i1"}, {key: "variantId", value: ""}]
111
+ let result = await ops.append([stored("ItemAdded", tags, JSON.Encode.string("x"))])
112
+ switch result {
113
+ | Ok(_) => expect(true)->toBe(true)
114
+ | Error(e) => expect(appendErrMsg(e))->toBe("Ok")
115
+ }
116
+
117
+ let byPartition = await ops.read(~query=[{tags: [{key: "itemId", value: "i1"}]}])
118
+ expect(byPartition.events->Array.length)->toBe(1)
119
+ expect((byPartition.events->Array.getUnsafe(0)).tags->Array.map(t => (t.key, t.value)))
120
+ ->toEqual([("itemId", "i1"), ("variantId", "")])
121
+
122
+ let byComposite = await ops.read(~query=[{tags: tags}])
123
+ expect(byComposite.events->Array.length)->toBe(1)
124
+ })
125
+ })
126
+
94
127
  testPromise("batched tag hydration preserves per-event tags and order", async () => {
95
128
  await runUnderSqlite(async () => {
96
129
  module TestBus = LocalBus.Make()
@@ -96,6 +96,57 @@ globalThis.describe("DcbEventLogStorage_Sqlite", () => {
96
96
  }], undefined);
97
97
  globalThis.expect(read.events.length).toBe(2);
98
98
  }));
99
+ globalThis.test("an empty tag value appends and stays readable", async () => await runUnderSqlite(async () => {
100
+ let TestBus = LocalBus$ReventlessLocal.Make({});
101
+ let Storage = LocalDcbEventLogStorage$ReventlessLocal.Make(TestBus);
102
+ let s = Storage.make("dcb-emptytag", [], {
103
+ TAG: "Simple",
104
+ _0: {
105
+ key: "itemId"
106
+ }
107
+ }, undefined, opts);
108
+ let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
109
+ let tags = [
110
+ {
111
+ key: "itemId",
112
+ value: "i1"
113
+ },
114
+ {
115
+ key: "variantId",
116
+ value: ""
117
+ }
118
+ ];
119
+ let result = await ops.append([stored("ItemAdded", tags, "x")], undefined);
120
+ if (result.TAG === "Ok") {
121
+ globalThis.expect(true).toBe(true);
122
+ } else {
123
+ globalThis.expect(appendErrMsg(result._0)).toBe("Ok");
124
+ }
125
+ let byPartition = await ops.read([{
126
+ tags: [{
127
+ key: "itemId",
128
+ value: "i1"
129
+ }]
130
+ }], undefined);
131
+ globalThis.expect(byPartition.events.length).toBe(1);
132
+ globalThis.expect(byPartition.events[0].tags.map(t => [
133
+ t.key,
134
+ t.value
135
+ ])).toEqual([
136
+ [
137
+ "itemId",
138
+ "i1"
139
+ ],
140
+ [
141
+ "variantId",
142
+ ""
143
+ ]
144
+ ]);
145
+ let byComposite = await ops.read([{
146
+ tags: tags
147
+ }], undefined);
148
+ globalThis.expect(byComposite.events.length).toBe(1);
149
+ }));
99
150
  globalThis.test("batched tag hydration preserves per-event tags and order", async () => await runUnderSqlite(async () => {
100
151
  let TestBus = LocalBus$ReventlessLocal.Make({});
101
152
  let Storage = LocalDcbEventLogStorage$ReventlessLocal.Make(TestBus);
@@ -142,6 +142,32 @@ describe("DcbEventLogStorage_InMemory", () => {
142
142
  expect((result.events->Array.getUnsafe(1)).eventType)->toBe("B")
143
143
  })
144
144
 
145
+ // An empty tag value is a legitimate model state — an absent member of a
146
+ // composite partition key. Every backend must accept it, record it verbatim,
147
+ // and keep matching the event through its partition and composite reads. See
148
+ // docs/plans/dcb-empty-tag-values-break-append.md.
149
+ testPromise("an empty tag value appends and stays readable", async () => {
150
+ let storage = makeStorage()
151
+ let ops = await storage.operations->TestRunner.resolve
152
+ let tags = [
153
+ {Reventless.DcbTag.key: "itemId", value: "i1"},
154
+ {Reventless.DcbTag.key: "variantId", value: ""},
155
+ ]
156
+ let appended = await ops.append([makeEvent(~eventType="ItemAdded", ~data=JSON.Null, ~tags)])
157
+ expect(appended)->toEqual(Ok("1"))
158
+
159
+ let byPartition = await ops.read(
160
+ ~query=[{Reventless.DcbTag.tags: [{Reventless.DcbTag.key: "itemId", value: "i1"}]}],
161
+ )
162
+ expect(byPartition.events->Array.length)->toBe(1)
163
+ // The empty value is recorded verbatim, not dropped or substituted.
164
+ expect((byPartition.events->Array.getUnsafe(0)).tags->Array.map(t => (t.key, t.value)))
165
+ ->toEqual([("itemId", "i1"), ("variantId", "")])
166
+
167
+ let byComposite = await ops.read(~query=[{Reventless.DcbTag.tags: tags}])
168
+ expect(byComposite.events->Array.length)->toBe(1)
169
+ })
170
+
145
171
  testPromise("after parameter skips events at or before that position", async () => {
146
172
  let storage = makeStorage()
147
173
  let ops = await storage.operations->TestRunner.resolve
@@ -165,6 +165,49 @@ globalThis.describe("DcbEventLogStorage_InMemory", () => {
165
165
  globalThis.expect(result.events[0].eventType).toBe("A");
166
166
  globalThis.expect(result.events[1].eventType).toBe("B");
167
167
  });
168
+ globalThis.test("an empty tag value appends and stays readable", async () => {
169
+ let storage = makeStorage();
170
+ let ops = await TestRunner$ReventlessLocal.resolve(storage.operations);
171
+ let tags = [
172
+ {
173
+ key: "itemId",
174
+ value: "i1"
175
+ },
176
+ {
177
+ key: "variantId",
178
+ value: ""
179
+ }
180
+ ];
181
+ let appended = await ops.append([makeEvent("ItemAdded", null, tags)], undefined);
182
+ globalThis.expect(appended).toEqual({
183
+ TAG: "Ok",
184
+ _0: "1"
185
+ });
186
+ let byPartition = await ops.read([{
187
+ tags: [{
188
+ key: "itemId",
189
+ value: "i1"
190
+ }]
191
+ }], undefined);
192
+ globalThis.expect(byPartition.events.length).toBe(1);
193
+ globalThis.expect(byPartition.events[0].tags.map(t => [
194
+ t.key,
195
+ t.value
196
+ ])).toEqual([
197
+ [
198
+ "itemId",
199
+ "i1"
200
+ ],
201
+ [
202
+ "variantId",
203
+ ""
204
+ ]
205
+ ]);
206
+ let byComposite = await ops.read([{
207
+ tags: tags
208
+ }], undefined);
209
+ globalThis.expect(byComposite.events.length).toBe(1);
210
+ });
168
211
  globalThis.test("after parameter skips events at or before that position", async () => {
169
212
  let storage = makeStorage();
170
213
  let ops = await TestRunner$ReventlessLocal.resolve(storage.operations);
@@ -667,6 +667,8 @@ describe("GraphQL_SchemaInspector", () => {
667
667
  expect(msg->String.includes("ScanItem"))->toBe(true)
668
668
  expect(msg->String.includes("name"))->toBe(true)
669
669
  expect(msg->String.includes("per-page"))->toBe(true)
670
+ // The warning must state the correctness consequence, not just the cost.
671
+ expect(msg->String.includes("globally ordered"))->toBe(true)
670
672
  },
671
673
  )
672
674
  testPromise(
@@ -500,6 +500,7 @@ globalThis.describe("GraphQL_SchemaInspector", () => {
500
500
  globalThis.expect(msg.includes("ScanItem")).toBe(true);
501
501
  globalThis.expect(msg.includes("name")).toBe(true);
502
502
  globalThis.expect(msg.includes("per-page")).toBe(true);
503
+ globalThis.expect(msg.includes("globally ordered")).toBe(true);
503
504
  });
504
505
  globalThis.test("validateScanSortAlignment is silent when @scanSort field aligns with a known sort key", async () => {
505
506
  let warnings = GraphQL_FragmentGenerator$ReventlessCore.validateScanSortAlignment(scanStateSchemaWithAnnotations, "ScanItem", ["name"]);