@reventlessdev/reventless-local 3.0.0-alpha.164 → 3.0.0-alpha.165
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.165 (2026-07-22)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** keep an empty DCB tag value from breaking the append ([1c0ab40](https://github.com/ReventlessDev/reventless-core/commit/1c0ab40f25b596702a81f42686d77c5e423e9b44))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# 3.0.0-alpha.164 (2026-07-22)
|
|
7
14
|
|
|
8
15
|
* 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.
|
|
3
|
+
"version": "3.0.0-alpha.165",
|
|
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,17 +31,17 @@
|
|
|
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.30",
|
|
35
34
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.24",
|
|
35
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.30",
|
|
36
|
+
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.20",
|
|
36
37
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
37
38
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
38
|
-
"@reventlessdev/
|
|
39
|
-
"@reventlessdev/reventless-
|
|
40
|
-
"@reventlessdev/reventless-
|
|
41
|
-
"@reventlessdev/reventless-gwt": "1.0.0-alpha.124",
|
|
39
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.178",
|
|
40
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.25",
|
|
41
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.125",
|
|
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.
|
|
44
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.42"
|
|
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);
|