@reventlessdev/reventless-local 3.0.0-alpha.320 → 3.0.0-alpha.322
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 +16 -0
- package/package.json +9 -9
- package/src/Platform.res +3 -0
- package/src/Platform.res.mjs +3 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res +13 -4
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res.mjs +13 -3
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res +3 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res.mjs +3 -0
- package/src/adapter/LocalPlatformObserve.res +1 -0
- package/src/adapter/LocalPlatformObserve.res.mjs +1 -0
- package/src/reexport/CommandTopic.res.mjs +6 -0
- package/tests/adapter/LocalPlatformObserveTest.res +14 -6
- package/tests/adapter/LocalPlatformObserveTest.res.mjs +12 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.322 (2026-11-05)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-local
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.321 (2026-11-05)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* each command's outcome is written as a line of its own ([84e5e3f](https://github.com/ReventlessDev/reventless-core/commit/84e5e3fa4d77e629c6f72d9079a8f82f6b26a5ac))
|
|
19
|
+
* **local:** the command outcomes can be read by the flows they started ([e43819d](https://github.com/ReventlessDev/reventless-core/commit/e43819da448ab09ea39a39636a0ef065e3f0c34b))
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# 3.0.0-alpha.320 (2026-11-02)
|
|
7
23
|
|
|
8
24
|
### Features
|
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.322",
|
|
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": {
|
|
@@ -34,19 +34,19 @@
|
|
|
34
34
|
"graphql-yoga": "^5.21.0",
|
|
35
35
|
"sury": "11.0.0",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
|
+
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
37
38
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.31",
|
|
38
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
39
39
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
|
|
40
|
-
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
41
40
|
"@reventlessdev/rescript-node": "2.0.0-alpha.26",
|
|
42
|
-
"@reventlessdev/
|
|
43
|
-
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.176",
|
|
44
|
-
"@reventlessdev/reventless-gwt": "1.0.0-alpha.277",
|
|
41
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
45
42
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
46
|
-
"@reventlessdev/reventless-
|
|
43
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.330",
|
|
44
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.178",
|
|
45
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.279",
|
|
46
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.223",
|
|
47
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.194",
|
|
47
48
|
"@reventlessdev/reventless-seed": "1.0.0-alpha.41",
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.192"
|
|
49
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.194"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.1",
|
package/src/Platform.res
CHANGED
|
@@ -120,6 +120,9 @@ module MakeWithConfig = (
|
|
|
120
120
|
LocalViewChangeLog.install()
|
|
121
121
|
// And what became of every command, for the same reason.
|
|
122
122
|
LocalCommandOutcomeLog.install()
|
|
123
|
+
// The tap carries each outcome, so the line a deployed platform writes for it would only
|
|
124
|
+
// repeat it.
|
|
125
|
+
ReventlessCore.CommandTopic_Helpers.outcomeLines := false
|
|
123
126
|
|
|
124
127
|
// What this platform provides for the roles its plugins need: every account it
|
|
125
128
|
// can sign in as, built-in and loaded alike, or the file the store will load
|
package/src/Platform.res.mjs
CHANGED
|
@@ -78,6 +78,7 @@ import * as LocalViewChangeLog$ReventlessLocal from "./adapter/ViewChangeLog/Loc
|
|
|
78
78
|
import * as PlatformMCP_Server$ReventlessLocal from "./adapter/PlatformMCP_Server.res.mjs";
|
|
79
79
|
import * as ResourceAttribution$ReventlessCore from "@reventlessdev/reventless-core/src/ResourceAttribution.res.mjs";
|
|
80
80
|
import * as Auth_GraphqlContext$ReventlessLocal from "./adapter/Auth/Auth_GraphqlContext.res.mjs";
|
|
81
|
+
import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
81
82
|
import * as PgProjectionCatchup$ReventlessLocal from "./adapter/PgProjectionCatchup.res.mjs";
|
|
82
83
|
import * as PluginsReadModelSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs";
|
|
83
84
|
import * as StorePrefixCollision$ReventlessCore from "@reventlessdev/reventless-core/src/util/StorePrefixCollision.res.mjs";
|
|
@@ -160,6 +161,7 @@ function MakeWithConfig(Config) {
|
|
|
160
161
|
LocalTapLines$ReventlessLocal.install();
|
|
161
162
|
LocalViewChangeLog$ReventlessLocal.install();
|
|
162
163
|
LocalCommandOutcomeLog$ReventlessLocal.install();
|
|
164
|
+
CommandTopic_Helpers$ReventlessCore.outcomeLines.contents = false;
|
|
163
165
|
Role$Reventless.provideGroupsFrom(AccountStore$ReventlessLocal.providedGroups);
|
|
164
166
|
OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
|
|
165
167
|
TestRunner$ReventlessLocal.setup();
|
|
@@ -1972,6 +1974,7 @@ function Make($star) {
|
|
|
1972
1974
|
LocalTapLines$ReventlessLocal.install();
|
|
1973
1975
|
LocalViewChangeLog$ReventlessLocal.install();
|
|
1974
1976
|
LocalCommandOutcomeLog$ReventlessLocal.install();
|
|
1977
|
+
CommandTopic_Helpers$ReventlessCore.outcomeLines.contents = false;
|
|
1975
1978
|
Role$Reventless.provideGroupsFrom(AccountStore$ReventlessLocal.providedGroups);
|
|
1976
1979
|
OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
|
|
1977
1980
|
TestRunner$ReventlessLocal.setup();
|
|
@@ -10,15 +10,24 @@ type t = {
|
|
|
10
10
|
item: JSON.t,
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/** What a read asks for: every field narrows, none reads every outcome.
|
|
14
|
-
|
|
13
|
+
/** What a read asks for: every field narrows, none reads every outcome. `startsFlow` keeps the
|
|
14
|
+
commands that started a flow: an API request, an inbound translation, a timer's work item. */
|
|
15
|
+
type filter = {correlationId?: string, msgId?: string, startsFlow?: bool}
|
|
16
|
+
|
|
17
|
+
/** A flow's first command is the one whose flow id is its own message id. */
|
|
18
|
+
let startsFlow = (e: t) => e.flow == Some(e.msgId)
|
|
15
19
|
|
|
16
20
|
let matches = (f: filter, e: t) =>
|
|
17
21
|
f.correlationId->Option.mapOr(true, c => e.flow == Some(c)) &&
|
|
18
|
-
|
|
22
|
+
f.msgId->Option.mapOr(true, m => e.msgId == m) &&
|
|
23
|
+
(f.startsFlow != Some(true) || startsFlow(e))
|
|
19
24
|
|
|
20
25
|
let filterOfJson = (j: option<JSON.t>): filter => {
|
|
21
26
|
let d = j->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
|
|
22
27
|
let s = k => d->Dict.get(k)->Option.flatMap(JSON.Decode.string)
|
|
23
|
-
{
|
|
28
|
+
{
|
|
29
|
+
correlationId: ?s("correlationId"),
|
|
30
|
+
msgId: ?s("msgId"),
|
|
31
|
+
startsFlow: ?(d->Dict.get("startsFlow")->Option.flatMap(JSON.Decode.bool)),
|
|
32
|
+
}
|
|
24
33
|
}
|
|
@@ -4,9 +4,17 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
5
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
6
|
|
|
7
|
+
function startsFlow(e) {
|
|
8
|
+
return Primitive_object.equal(e.flow, e.msgId);
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
function matches(f, e) {
|
|
8
|
-
if (Stdlib_Option.mapOr(f.correlationId, true, c => Primitive_object.equal(e.flow, c))) {
|
|
9
|
-
|
|
12
|
+
if (Stdlib_Option.mapOr(f.correlationId, true, c => Primitive_object.equal(e.flow, c)) && Stdlib_Option.mapOr(f.msgId, true, m => e.msgId === m)) {
|
|
13
|
+
if (Primitive_object.notequal(f.startsFlow, true)) {
|
|
14
|
+
return true;
|
|
15
|
+
} else {
|
|
16
|
+
return startsFlow(e);
|
|
17
|
+
}
|
|
10
18
|
} else {
|
|
11
19
|
return false;
|
|
12
20
|
}
|
|
@@ -17,11 +25,13 @@ function filterOfJson(j) {
|
|
|
17
25
|
let s = k => Stdlib_Option.flatMap(d[k], Stdlib_JSON.Decode.string);
|
|
18
26
|
return {
|
|
19
27
|
correlationId: s("correlationId"),
|
|
20
|
-
msgId: s("msgId")
|
|
28
|
+
msgId: s("msgId"),
|
|
29
|
+
startsFlow: Stdlib_Option.flatMap(d["startsFlow"], Stdlib_JSON.Decode.bool)
|
|
21
30
|
};
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
export {
|
|
34
|
+
startsFlow,
|
|
25
35
|
matches,
|
|
26
36
|
filterOfJson,
|
|
27
37
|
}
|
|
@@ -52,6 +52,9 @@ let read = (db, ~filter: CommandOutcomeLogEntry.filter, ~before: option<int>, ~f
|
|
|
52
52
|
}
|
|
53
53
|
filter.correlationId->Option.forEach(c => add("flow = ?", str(c)))
|
|
54
54
|
filter.msgId->Option.forEach(m => add("msg_id = ?", str(m)))
|
|
55
|
+
if filter.startsFlow == Some(true) {
|
|
56
|
+
where->Array.push("flow = msg_id")
|
|
57
|
+
}
|
|
55
58
|
before->Option.forEach(b => add("seq < ?", JSON.Encode.int(b)))
|
|
56
59
|
let sql = `SELECT seq, flow, msg_id, item FROM ${table}${where == []
|
|
57
60
|
? ""
|
|
@@ -56,6 +56,9 @@ function read(db, filter, before, first) {
|
|
|
56
56
|
};
|
|
57
57
|
Stdlib_Option.forEach(filter.correlationId, c => add("flow = ?", c));
|
|
58
58
|
Stdlib_Option.forEach(filter.msgId, m => add("msg_id = ?", m));
|
|
59
|
+
if (Primitive_object.equal(filter.startsFlow, true)) {
|
|
60
|
+
where.push("flow = msg_id");
|
|
61
|
+
}
|
|
59
62
|
Stdlib_Option.forEach(before, b => add("seq < ?", b));
|
|
60
63
|
let sql = `SELECT seq, flow, msg_id, item FROM ` + table + (
|
|
61
64
|
Primitive_object.equal(where, []) ? "" : " WHERE " + where.join(" AND ")
|
|
@@ -16,6 +16,10 @@ let registerCommandOutcome = CommandTopic$ReventlessCore.registerCommandOutcome;
|
|
|
16
16
|
|
|
17
17
|
let clearCommandOutcome = CommandTopic$ReventlessCore.clearCommandOutcome;
|
|
18
18
|
|
|
19
|
+
let outcomeLines = CommandTopic$ReventlessCore.outcomeLines;
|
|
20
|
+
|
|
21
|
+
let logOutcome = CommandTopic$ReventlessCore.logOutcome;
|
|
22
|
+
|
|
19
23
|
let fireCommandOutcome = CommandTopic$ReventlessCore.fireCommandOutcome;
|
|
20
24
|
|
|
21
25
|
let acceptedResultChannel = CommandTopic$ReventlessCore.acceptedResultChannel;
|
|
@@ -56,6 +60,8 @@ export {
|
|
|
56
60
|
nextOutcomeHookId,
|
|
57
61
|
registerCommandOutcome,
|
|
58
62
|
clearCommandOutcome,
|
|
63
|
+
outcomeLines,
|
|
64
|
+
logOutcome,
|
|
59
65
|
fireCommandOutcome,
|
|
60
66
|
acceptedResultChannel,
|
|
61
67
|
rejectedResultChannel,
|
|
@@ -236,7 +236,7 @@ describe("the platform API's watching fields", () => {
|
|
|
236
236
|
SqliteDriver.close(db)
|
|
237
237
|
})
|
|
238
238
|
|
|
239
|
-
// Three commands: two in one flow, one rejected.
|
|
239
|
+
// Three commands: two in one flow, which the first started, one rejected.
|
|
240
240
|
let keepOutcomes = () => {
|
|
241
241
|
let report = (
|
|
242
242
|
~flow,
|
|
@@ -252,11 +252,11 @@ describe("the platform API's watching fields", () => {
|
|
|
252
252
|
entityId: Some("o-1"),
|
|
253
253
|
eventCount: 1,
|
|
254
254
|
})
|
|
255
|
-
LocalCommandOutcomeLog.record(report(~flow="
|
|
255
|
+
LocalCommandOutcomeLog.record(report(~flow="c-1", ~outcome=accepted, "c-1"))
|
|
256
256
|
LocalCommandOutcomeLog.record(report(~flow="k-2", ~outcome=accepted, "c-2"))
|
|
257
257
|
LocalCommandOutcomeLog.record(
|
|
258
258
|
report(
|
|
259
|
-
~flow="
|
|
259
|
+
~flow="c-1",
|
|
260
260
|
~outcome=OutcomeRejected({
|
|
261
261
|
errorCode: "AlreadyShipped",
|
|
262
262
|
errorDetail: "",
|
|
@@ -285,13 +285,21 @@ describe("the platform API's watching fields", () => {
|
|
|
285
285
|
)
|
|
286
286
|
})
|
|
287
287
|
}
|
|
288
|
-
(
|
|
288
|
+
(
|
|
289
|
+
await ask(`{"filter":{"correlationId":"c-1"}}`),
|
|
290
|
+
await ask(`{"filter":{"msgId":"c-2"}}`),
|
|
291
|
+
await ask(`{"filter":{"startsFlow":true}}`),
|
|
292
|
+
)
|
|
289
293
|
}
|
|
290
294
|
|
|
291
|
-
let outcomesExpected = (
|
|
295
|
+
let outcomesExpected = (
|
|
296
|
+
[("c-3", "rejected"), ("c-1", "accepted")],
|
|
297
|
+
[("c-2", "accepted")],
|
|
298
|
+
[("c-1", "accepted")],
|
|
299
|
+
)
|
|
292
300
|
|
|
293
301
|
testPromise(
|
|
294
|
-
"the command outcomes answer an admin by flow and by
|
|
302
|
+
"the command outcomes answer an admin by flow, by command and by flow start, newest first",
|
|
295
303
|
async () => {
|
|
296
304
|
BackendState.setMemory()
|
|
297
305
|
LocalCommandOutcomeLog.resetForTests()
|
|
@@ -246,12 +246,12 @@ globalThis.describe("the platform API's watching fields", () => {
|
|
|
246
246
|
entityId: "o-1",
|
|
247
247
|
eventCount: 1
|
|
248
248
|
};
|
|
249
|
-
LocalCommandOutcomeLog$ReventlessLocal.record(report("
|
|
249
|
+
LocalCommandOutcomeLog$ReventlessLocal.record(report("c-1", accepted, "c-1"));
|
|
250
250
|
LocalCommandOutcomeLog$ReventlessLocal.record(report("k-2", accepted, "c-2"));
|
|
251
251
|
LocalCommandOutcomeLog$ReventlessLocal.record({
|
|
252
252
|
component: "Order",
|
|
253
253
|
reference: "c-3",
|
|
254
|
-
correlationId: "
|
|
254
|
+
correlationId: "c-1",
|
|
255
255
|
outcome: {
|
|
256
256
|
TAG: "OutcomeRejected",
|
|
257
257
|
errorCode: "AlreadyShipped",
|
|
@@ -275,8 +275,9 @@ globalThis.describe("the platform API's watching fields", () => {
|
|
|
275
275
|
});
|
|
276
276
|
};
|
|
277
277
|
return [
|
|
278
|
-
await ask(`{"filter":{"correlationId":"
|
|
279
|
-
await ask(`{"filter":{"msgId":"c-2"}}`)
|
|
278
|
+
await ask(`{"filter":{"correlationId":"c-1"}}`),
|
|
279
|
+
await ask(`{"filter":{"msgId":"c-2"}}`),
|
|
280
|
+
await ask(`{"filter":{"startsFlow":true}}`)
|
|
280
281
|
];
|
|
281
282
|
};
|
|
282
283
|
let outcomesExpected_0 = [
|
|
@@ -293,11 +294,16 @@ globalThis.describe("the platform API's watching fields", () => {
|
|
|
293
294
|
"c-2",
|
|
294
295
|
"accepted"
|
|
295
296
|
]];
|
|
297
|
+
let outcomesExpected_2 = [[
|
|
298
|
+
"c-1",
|
|
299
|
+
"accepted"
|
|
300
|
+
]];
|
|
296
301
|
let outcomesExpected = [
|
|
297
302
|
outcomesExpected_0,
|
|
298
|
-
outcomesExpected_1
|
|
303
|
+
outcomesExpected_1,
|
|
304
|
+
outcomesExpected_2
|
|
299
305
|
];
|
|
300
|
-
globalThis.test("the command outcomes answer an admin by flow and by
|
|
306
|
+
globalThis.test("the command outcomes answer an admin by flow, by command and by flow start, newest first", async () => {
|
|
301
307
|
BackendState$ReventlessLocal.setMemory();
|
|
302
308
|
LocalCommandOutcomeLog$ReventlessLocal.resetForTests();
|
|
303
309
|
globalThis.expect(await outcomesRead()).toEqual(outcomesExpected);
|