@reventlessdev/reventless-local 3.0.0-alpha.316 → 3.0.0-alpha.318
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 +11 -11
- package/src/LocalPlatformRegistry.res +19 -0
- package/src/LocalPlatformRegistry.res.mjs +19 -1
- package/src/Platform.res +24 -2
- package/src/Platform.res.mjs +22 -2
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +38 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +29 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +87 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +76 -0
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +8 -1
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +17 -4
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +49 -2
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +57 -21
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +85 -0
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +50 -0
- package/src/adapter/EventLog/LocalEventLogStorage.res +3 -0
- package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +6 -2
- package/src/adapter/LocalBus.res +32 -5
- package/src/adapter/LocalBus.res.mjs +27 -7
- package/src/adapter/LocalEventLogRead.res +477 -0
- package/src/adapter/LocalEventLogRead.res.mjs +507 -0
- package/src/adapter/LocalEventTap.res +18 -80
- package/src/adapter/LocalEventTap.res.mjs +19 -108
- package/src/adapter/LocalFeed.res +433 -0
- package/src/adapter/LocalFeed.res.mjs +419 -0
- package/src/adapter/LocalLogFeed.res +217 -0
- package/src/adapter/LocalLogFeed.res.mjs +295 -0
- package/src/adapter/LocalPgEventLogRead.res +85 -0
- package/src/adapter/LocalPgEventLogRead.res.mjs +91 -0
- package/src/adapter/LocalPlatformObserve.res +456 -0
- package/src/adapter/LocalPlatformObserve.res.mjs +505 -0
- package/src/adapter/LocalTapLines.res +147 -0
- package/src/adapter/LocalTapLines.res.mjs +229 -0
- package/src/adapter/PlatformGraphQL_Server.res +36 -3
- package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
- package/src/reexport/CommandTopic.res.mjs +5 -2
- package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
- package/tests/adapter/LocalEventLogReadTest.res +539 -0
- package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
- package/tests/adapter/LocalFeedTest.res +314 -0
- package/tests/adapter/LocalFeedTest.res.mjs +329 -0
- package/tests/adapter/LocalPlatformObserveTest.res +165 -0
- package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
- package/tests/adapter/LocalTapLinesTest.res +182 -0
- package/tests/adapter/LocalTapLinesTest.res.mjs +242 -0
- package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
- package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePiiTest.res.mjs +7 -7
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Nodenet from "node:net";
|
|
5
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
6
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
7
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
8
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
9
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
10
|
+
import * as Sensitive$Reventless from "@reventlessdev/reventless-spec/src/components/Sensitive.res.mjs";
|
|
11
|
+
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
12
|
+
import * as LogRedaction$Reventless from "@reventlessdev/reventless-spec/src/util/LogRedaction.res.mjs";
|
|
13
|
+
import * as ViewChange$ReventlessCore from "@reventlessdev/reventless-core/src/ViewChange.res.mjs";
|
|
14
|
+
import * as LocalLogFeed$ReventlessLocal from "../../src/adapter/LocalLogFeed.res.mjs";
|
|
15
|
+
import * as LocalEventTap$ReventlessLocal from "../../src/adapter/LocalEventTap.res.mjs";
|
|
16
|
+
import * as LocalTapLines$ReventlessLocal from "../../src/adapter/LocalTapLines.res.mjs";
|
|
17
|
+
import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
18
|
+
|
|
19
|
+
function sleep(ms) {
|
|
20
|
+
return new Promise((resolve, param) => {
|
|
21
|
+
setTimeout(() => resolve(), ms);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function until(msOpt, cond) {
|
|
26
|
+
let ms = msOpt !== undefined ? msOpt : 2000;
|
|
27
|
+
let waited = 0;
|
|
28
|
+
while (!cond() && waited < ms) {
|
|
29
|
+
await sleep(5);
|
|
30
|
+
waited = waited + 5 | 0;
|
|
31
|
+
};
|
|
32
|
+
return cond();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function connect(port) {
|
|
36
|
+
return new Promise((resolve, param) => {
|
|
37
|
+
let c = {
|
|
38
|
+
contents: undefined
|
|
39
|
+
};
|
|
40
|
+
let socket = Nodenet.createConnection(port, "127.0.0.1", () => Stdlib_Option.forEach(c.contents, client => resolve(client)));
|
|
41
|
+
socket.setEncoding("utf8");
|
|
42
|
+
socket.on("error", param => {});
|
|
43
|
+
let client = {
|
|
44
|
+
socket: socket,
|
|
45
|
+
received: ""
|
|
46
|
+
};
|
|
47
|
+
socket.on("data", chunk => {
|
|
48
|
+
client.received = client.received + chunk;
|
|
49
|
+
});
|
|
50
|
+
c.contents = client;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function linesOf(c, kind) {
|
|
55
|
+
return Stdlib_Array.filterMap(c.received.split("\n"), l => {
|
|
56
|
+
let d;
|
|
57
|
+
try {
|
|
58
|
+
d = Stdlib_JSON.Decode.object(JSON.parse(l));
|
|
59
|
+
} catch (exn) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (d !== undefined && Primitive_object.equal(d["kind"], kind)) {
|
|
63
|
+
Stdlib_Dict.$$delete(d, "ts");
|
|
64
|
+
return d;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let rowSchema = Sury.$schema(s => ({
|
|
70
|
+
name: s.m(Sury.string),
|
|
71
|
+
token: s.m(Sensitive$Reventless.string)
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
async function connectTap() {
|
|
75
|
+
LocalEventTap$ReventlessLocal.resetForTests();
|
|
76
|
+
LocalLogFeed$ReventlessLocal.resetForTests();
|
|
77
|
+
LocalTapLines$ReventlessLocal.resetForTests();
|
|
78
|
+
LocalLogFeed$ReventlessLocal.install({});
|
|
79
|
+
LocalTapLines$ReventlessLocal.install();
|
|
80
|
+
let port = await new Promise((resolve, param) => LocalEventTap$ReventlessLocal.start({}, resolve, undefined));
|
|
81
|
+
let tap = await connect(port);
|
|
82
|
+
await until(undefined, () => LocalEventTap$ReventlessLocal.hasReaders());
|
|
83
|
+
return tap;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function disconnect(tap) {
|
|
87
|
+
tap.socket.destroy();
|
|
88
|
+
LocalTapLines$ReventlessLocal.resetForTests();
|
|
89
|
+
await LocalEventTap$ReventlessLocal.stopForTests();
|
|
90
|
+
return LocalLogFeed$ReventlessLocal.resetForTests();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
globalThis.describe("LocalTapLines", () => {
|
|
94
|
+
globalThis.test("an accepted and a rejected command each appear once on the tap", async () => {
|
|
95
|
+
let tap = await connectTap();
|
|
96
|
+
CommandTopic_Helpers$ReventlessCore.reportAccepted("Order", "flow-1", "m-1", {
|
|
97
|
+
entityId: "o-1",
|
|
98
|
+
eventCount: 2
|
|
99
|
+
});
|
|
100
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected("Order", "DomainRejection", "flow-1", "m-2", {
|
|
101
|
+
errorCode: "AlreadyExists",
|
|
102
|
+
errorDetail: `{"name":"Widget"}`
|
|
103
|
+
});
|
|
104
|
+
await until(undefined, () => linesOf(tap, "commandOutcome").length >= 2);
|
|
105
|
+
await sleep(20);
|
|
106
|
+
globalThis.expect(linesOf(tap, "commandOutcome")).toEqual([
|
|
107
|
+
Object.fromEntries([
|
|
108
|
+
[
|
|
109
|
+
"kind",
|
|
110
|
+
"commandOutcome"
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
"component",
|
|
114
|
+
"Order"
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
"msgId",
|
|
118
|
+
"m-1"
|
|
119
|
+
],
|
|
120
|
+
[
|
|
121
|
+
"correlationId",
|
|
122
|
+
"flow-1"
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
"outcome",
|
|
126
|
+
"accepted"
|
|
127
|
+
],
|
|
128
|
+
[
|
|
129
|
+
"entityId",
|
|
130
|
+
"o-1"
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
"eventCount",
|
|
134
|
+
2
|
|
135
|
+
]
|
|
136
|
+
]),
|
|
137
|
+
Object.fromEntries([
|
|
138
|
+
[
|
|
139
|
+
"kind",
|
|
140
|
+
"commandOutcome"
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
"component",
|
|
144
|
+
"Order"
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
"msgId",
|
|
148
|
+
"m-2"
|
|
149
|
+
],
|
|
150
|
+
[
|
|
151
|
+
"correlationId",
|
|
152
|
+
"flow-1"
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
"outcome",
|
|
156
|
+
"rejected"
|
|
157
|
+
],
|
|
158
|
+
[
|
|
159
|
+
"errorCode",
|
|
160
|
+
"AlreadyExists"
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
"cause",
|
|
164
|
+
"DOMAIN"
|
|
165
|
+
]
|
|
166
|
+
])
|
|
167
|
+
]);
|
|
168
|
+
globalThis.expect(tap.received.includes("@@RVLESS_EVT@@")).toBe(false);
|
|
169
|
+
let kept = LocalLogFeed$ReventlessLocal.recentRecords({
|
|
170
|
+
level: "Warn"
|
|
171
|
+
}, undefined).filter(r => r.message === "command rejected");
|
|
172
|
+
globalThis.expect(kept.map(r => [
|
|
173
|
+
r.comp,
|
|
174
|
+
r.annotations["msgId"]
|
|
175
|
+
])).toEqual([[
|
|
176
|
+
"CommandTopic(Order)",
|
|
177
|
+
"m-2"
|
|
178
|
+
]]);
|
|
179
|
+
globalThis.expect(Stdlib_Option.getOr(Stdlib_Option.map(kept[0].detail, j => JSON.stringify(j)), "")).toBe(`{"component":"Order","errorCode":"AlreadyExists","cause":"DOMAIN","errorDetail":{"name":"Widget"}}`);
|
|
180
|
+
return await disconnect(tap);
|
|
181
|
+
});
|
|
182
|
+
globalThis.test("an infrastructure failure is kept as an error", async () => {
|
|
183
|
+
let tap = await connectTap();
|
|
184
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected("Order", "InfrastructureFailure", undefined, "m-3", {
|
|
185
|
+
errorCode: "AppendFailed",
|
|
186
|
+
errorDetail: "store down"
|
|
187
|
+
});
|
|
188
|
+
let kept = LocalLogFeed$ReventlessLocal.recentRecords({
|
|
189
|
+
level: "Error"
|
|
190
|
+
}, undefined).filter(r => r.message === "command rejected");
|
|
191
|
+
globalThis.expect(kept.length).toBe(1);
|
|
192
|
+
return await disconnect(tap);
|
|
193
|
+
});
|
|
194
|
+
globalThis.test("a view change is sent with its values redacted", async () => {
|
|
195
|
+
let tap = await connectTap();
|
|
196
|
+
let onChange = ViewChange$ReventlessCore.reporter("ReadModel(Accounts)", "Accounts", r => Util_Sury$Reventless.toJson(r, rowSchema));
|
|
197
|
+
onChange({
|
|
198
|
+
key: "a-1",
|
|
199
|
+
kind: "Changed",
|
|
200
|
+
before: {
|
|
201
|
+
name: "Ada",
|
|
202
|
+
token: "old-secret"
|
|
203
|
+
},
|
|
204
|
+
after: {
|
|
205
|
+
name: "Ada L.",
|
|
206
|
+
token: "new-secret"
|
|
207
|
+
},
|
|
208
|
+
sources: [{
|
|
209
|
+
msgId: "m-1",
|
|
210
|
+
correlationId: "flow-1",
|
|
211
|
+
entityId: "a-1",
|
|
212
|
+
entityType: "Account"
|
|
213
|
+
}]
|
|
214
|
+
});
|
|
215
|
+
await until(undefined, () => linesOf(tap, "viewChange").length >= 1);
|
|
216
|
+
let lines = linesOf(tap, "viewChange");
|
|
217
|
+
globalThis.expect(lines.length).toBe(1);
|
|
218
|
+
let line = JSON.stringify(lines[0]);
|
|
219
|
+
globalThis.expect(line.includes("secret")).toBe(false);
|
|
220
|
+
globalThis.expect(line.includes(LogRedaction$Reventless.placeholder)).toBe(true);
|
|
221
|
+
globalThis.expect(line.includes(`"name":"Ada L."`)).toBe(true);
|
|
222
|
+
globalThis.expect(line.startsWith(`{"kind":"viewChange","view":"Accounts","key":"a-1","change":"changed","fields":["name","token"],"sources":[{"msgId":"m-1","correlationId":"flow-1","entityId":"a-1","entityType":"Account"}],"correlationId":"flow-1","before":`)).toBe(true);
|
|
223
|
+
return await disconnect(tap);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
let Outcome;
|
|
228
|
+
|
|
229
|
+
let ViewChange;
|
|
230
|
+
|
|
231
|
+
export {
|
|
232
|
+
Outcome,
|
|
233
|
+
ViewChange,
|
|
234
|
+
sleep,
|
|
235
|
+
until,
|
|
236
|
+
connect,
|
|
237
|
+
linesOf,
|
|
238
|
+
rowSchema,
|
|
239
|
+
connectTap,
|
|
240
|
+
disconnect,
|
|
241
|
+
}
|
|
242
|
+
/* rowSchema Not a pure module */
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// The platform API's subscriptions over the WebSocket: checked for the admin
|
|
2
|
+
// group like its queries, with the identity built from the connection; and the
|
|
3
|
+
// two feeds delivered as subscriptions, filtered.
|
|
4
|
+
|
|
5
|
+
@@warning("-44")
|
|
6
|
+
|
|
7
|
+
open JestGlobals
|
|
8
|
+
|
|
9
|
+
module Logger = ReventlessCore.Logger
|
|
10
|
+
|
|
11
|
+
type outcome = {values: array<JSON.t>, error: Nullable.t<string>}
|
|
12
|
+
|
|
13
|
+
@module("./GraphQLWsFixtures.mjs")
|
|
14
|
+
external collect: (string, string, dict<string>, int, int, unit => unit) => promise<outcome> =
|
|
15
|
+
"collect"
|
|
16
|
+
|
|
17
|
+
let sleep = (ms: int): promise<unit> =>
|
|
18
|
+
Promise.make((resolve, _) => {
|
|
19
|
+
let _ = setTimeout(() => resolve(), ms)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// A port of its own, away from the platform's defaults.
|
|
23
|
+
let port = 47_000 + Float.toInt(Math.random() *. 2000.)
|
|
24
|
+
let url = `ws://127.0.0.1:${port->Int.toString}/graphql`
|
|
25
|
+
|
|
26
|
+
let asAdmin = Dict.fromArray([("X-User", "admin")])
|
|
27
|
+
let asUser = Dict.make()
|
|
28
|
+
|
|
29
|
+
let field = (json: JSON.t, key: string): option<string> =>
|
|
30
|
+
json
|
|
31
|
+
->JSON.Decode.object
|
|
32
|
+
->Option.flatMap(d => d->Dict.get(key))
|
|
33
|
+
->Option.flatMap(JSON.Decode.string)
|
|
34
|
+
|
|
35
|
+
describe("the platform API's subscriptions", () => {
|
|
36
|
+
beforeAllAsync(async () => {
|
|
37
|
+
LocalLogFeed.resetForTests()
|
|
38
|
+
LocalLogFeed.install(~env=Dict.make())
|
|
39
|
+
LocalEventTap.resetForTests()
|
|
40
|
+
PlatformGraphQL_Server.reset()
|
|
41
|
+
LocalPlatformObserve.registerGraphQL(PlatformGraphQL_Server.asInterface)
|
|
42
|
+
// One of the kind the platform registered before: a plain topic field.
|
|
43
|
+
PlatformGraphQL_Server.registerSubscriptions(
|
|
44
|
+
~sdlFields=[" onTestChange: String"],
|
|
45
|
+
~resolvers=Dict.fromArray([
|
|
46
|
+
("onTestChange", LocalGraphQL_SubscriptionResolvers.makeFieldResolver("onTestChange")),
|
|
47
|
+
]),
|
|
48
|
+
)
|
|
49
|
+
PlatformGraphQL_Server.start(~port, ~contextFactory=Auth_GraphqlContext.buildAuthContext, ())
|
|
50
|
+
await sleep(100)
|
|
51
|
+
})
|
|
52
|
+
afterAll(() => {
|
|
53
|
+
PlatformGraphQL_Server.stop()
|
|
54
|
+
PlatformGraphQL_Server.reset()
|
|
55
|
+
LocalLogFeed.resetForTests()
|
|
56
|
+
LocalEventTap.resetForTests()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("are refused to a caller without the admin group", async () => {
|
|
60
|
+
let refused = async query => {
|
|
61
|
+
let o = await collect(url, query, asUser, 1, 2000, () => ())
|
|
62
|
+
o.values->Array.length == 0 && o.error->Nullable.toOption->Option.isSome
|
|
63
|
+
}
|
|
64
|
+
expect(await refused("subscription { Platform_LogAppended }"))->toBe(true)
|
|
65
|
+
expect(await refused("subscription { Platform_EventAppended }"))->toBe(true)
|
|
66
|
+
expect(await refused("subscription { onTestChange }"))->toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test("deliver the log feed to an admin, filtered, the recent records first", async () => {
|
|
70
|
+
Logger.emit(~level=Info, ~comp="Projection", ~plugin="Ordering", "quiet")
|
|
71
|
+
Logger.emit(~level=Warn, ~comp="Projection", ~plugin="Ordering", "loud")
|
|
72
|
+
Logger.emit(~level=Error, ~comp="Projection", ~plugin="Catalog", "elsewhere")
|
|
73
|
+
let o = await collect(
|
|
74
|
+
url,
|
|
75
|
+
`subscription { Platform_LogAppended(filter: {plugins: ["Ordering"], level: WARN}) }`,
|
|
76
|
+
asAdmin,
|
|
77
|
+
2,
|
|
78
|
+
3000,
|
|
79
|
+
() => {
|
|
80
|
+
let _ = setTimeout(
|
|
81
|
+
() => {
|
|
82
|
+
Logger.emit(~level=Info, ~comp="Projection", ~plugin="Ordering", "quiet again")
|
|
83
|
+
Logger.emit(~level=Error, ~comp="Projection", ~plugin="Ordering", "live")
|
|
84
|
+
},
|
|
85
|
+
100,
|
|
86
|
+
)
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
expect(o.error->Nullable.toOption)->toEqual(None)
|
|
90
|
+
expect(o.values->Array.map(v => v->field("message")))->toEqual([Some("loud"), Some("live")])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test("deliver the event tap to an admin, filtered", async () => {
|
|
94
|
+
let ticker = ref(None)
|
|
95
|
+
let o = await collect(
|
|
96
|
+
url,
|
|
97
|
+
`subscription { Platform_EventAppended(filter: {eventTypes: ["Placed"]}) }`,
|
|
98
|
+
asAdmin,
|
|
99
|
+
2,
|
|
100
|
+
3000,
|
|
101
|
+
() =>
|
|
102
|
+
ticker :=
|
|
103
|
+
Some(
|
|
104
|
+
setInterval(
|
|
105
|
+
() => {
|
|
106
|
+
LocalEventTap.publish({
|
|
107
|
+
line: `@@RVLESS_EVT@@ {"event":"domainEvent","name":"shipped"}`,
|
|
108
|
+
facets: {eventType: "Shipped"},
|
|
109
|
+
})
|
|
110
|
+
LocalEventTap.publish({
|
|
111
|
+
line: `@@RVLESS_EVT@@ {"event":"domainEvent","name":"placed"}`,
|
|
112
|
+
facets: {eventType: "Placed"},
|
|
113
|
+
})
|
|
114
|
+
},
|
|
115
|
+
20,
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
ticker.contents->Option.forEach(clearInterval)
|
|
120
|
+
expect(o.error->Nullable.toOption)->toEqual(None)
|
|
121
|
+
expect(o.values->Array.map(v => v->field("name")))->toEqual([Some("placed"), Some("placed")])
|
|
122
|
+
})
|
|
123
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
6
|
+
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
7
|
+
import * as GraphQLWsFixturesMjs from "./GraphQLWsFixtures.mjs";
|
|
8
|
+
import * as LocalLogFeed$ReventlessLocal from "../../src/adapter/LocalLogFeed.res.mjs";
|
|
9
|
+
import * as LocalEventTap$ReventlessLocal from "../../src/adapter/LocalEventTap.res.mjs";
|
|
10
|
+
import * as Auth_GraphqlContext$ReventlessLocal from "../../src/adapter/Auth/Auth_GraphqlContext.res.mjs";
|
|
11
|
+
import * as LocalPlatformObserve$ReventlessLocal from "../../src/adapter/LocalPlatformObserve.res.mjs";
|
|
12
|
+
import * as PlatformGraphQL_Server$ReventlessLocal from "../../src/adapter/PlatformGraphQL_Server.res.mjs";
|
|
13
|
+
import * as LocalGraphQL_SubscriptionResolvers$ReventlessLocal from "../../src/adapter/Api/LocalGraphQL_SubscriptionResolvers.res.mjs";
|
|
14
|
+
|
|
15
|
+
function collect(prim0, prim1, prim2, prim3, prim4, prim5) {
|
|
16
|
+
return GraphQLWsFixturesMjs.collect(prim0, prim1, prim2, prim3, prim4, prim5);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sleep(ms) {
|
|
20
|
+
return new Promise((resolve, param) => {
|
|
21
|
+
setTimeout(() => resolve(), ms);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let port = 47000 + (Math.random() * 2000 | 0) | 0;
|
|
26
|
+
|
|
27
|
+
let url = `ws://127.0.0.1:` + port.toString() + `/graphql`;
|
|
28
|
+
|
|
29
|
+
let asAdmin = Object.fromEntries([[
|
|
30
|
+
"X-User",
|
|
31
|
+
"admin"
|
|
32
|
+
]]);
|
|
33
|
+
|
|
34
|
+
let asUser = {};
|
|
35
|
+
|
|
36
|
+
function field(json, key) {
|
|
37
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d[key]), Stdlib_JSON.Decode.string);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
globalThis.describe("the platform API's subscriptions", () => {
|
|
41
|
+
globalThis.beforeAll(async () => {
|
|
42
|
+
LocalLogFeed$ReventlessLocal.resetForTests();
|
|
43
|
+
LocalLogFeed$ReventlessLocal.install({});
|
|
44
|
+
LocalEventTap$ReventlessLocal.resetForTests();
|
|
45
|
+
PlatformGraphQL_Server$ReventlessLocal.reset();
|
|
46
|
+
LocalPlatformObserve$ReventlessLocal.registerGraphQL(PlatformGraphQL_Server$ReventlessLocal.asInterface);
|
|
47
|
+
PlatformGraphQL_Server$ReventlessLocal.registerSubscriptions([" onTestChange: String"], Object.fromEntries([[
|
|
48
|
+
"onTestChange",
|
|
49
|
+
LocalGraphQL_SubscriptionResolvers$ReventlessLocal.makeFieldResolver("onTestChange")
|
|
50
|
+
]]));
|
|
51
|
+
PlatformGraphQL_Server$ReventlessLocal.start(port, Auth_GraphqlContext$ReventlessLocal.buildAuthContext, undefined);
|
|
52
|
+
return await sleep(100);
|
|
53
|
+
});
|
|
54
|
+
globalThis.afterAll(() => {
|
|
55
|
+
PlatformGraphQL_Server$ReventlessLocal.stop();
|
|
56
|
+
PlatformGraphQL_Server$ReventlessLocal.reset();
|
|
57
|
+
LocalLogFeed$ReventlessLocal.resetForTests();
|
|
58
|
+
LocalEventTap$ReventlessLocal.resetForTests();
|
|
59
|
+
});
|
|
60
|
+
globalThis.test("are refused to a caller without the admin group", async () => {
|
|
61
|
+
let refused = async query => {
|
|
62
|
+
let o = await collect(url, query, asUser, 1, 2000, () => {});
|
|
63
|
+
if (o.values.length === 0) {
|
|
64
|
+
return Stdlib_Option.isSome(Primitive_option.fromNullable(o.error));
|
|
65
|
+
} else {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
globalThis.expect(await refused("subscription { Platform_LogAppended }")).toBe(true);
|
|
70
|
+
globalThis.expect(await refused("subscription { Platform_EventAppended }")).toBe(true);
|
|
71
|
+
globalThis.expect(await refused("subscription { onTestChange }")).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
globalThis.test("deliver the log feed to an admin, filtered, the recent records first", async () => {
|
|
74
|
+
Logger$ReventlessCore.emit("Info", "Projection", "Ordering", undefined, undefined, "quiet");
|
|
75
|
+
Logger$ReventlessCore.emit("Warn", "Projection", "Ordering", undefined, undefined, "loud");
|
|
76
|
+
Logger$ReventlessCore.emit("Error", "Projection", "Catalog", undefined, undefined, "elsewhere");
|
|
77
|
+
let o = await collect(url, `subscription { Platform_LogAppended(filter: {plugins: ["Ordering"], level: WARN}) }`, asAdmin, 2, 3000, () => {
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
Logger$ReventlessCore.emit("Info", "Projection", "Ordering", undefined, undefined, "quiet again");
|
|
80
|
+
Logger$ReventlessCore.emit("Error", "Projection", "Ordering", undefined, undefined, "live");
|
|
81
|
+
}, 100);
|
|
82
|
+
});
|
|
83
|
+
globalThis.expect(Primitive_option.fromNullable(o.error)).toEqual(undefined);
|
|
84
|
+
globalThis.expect(o.values.map(v => field(v, "message"))).toEqual([
|
|
85
|
+
"loud",
|
|
86
|
+
"live"
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
globalThis.test("deliver the event tap to an admin, filtered", async () => {
|
|
90
|
+
let ticker = {
|
|
91
|
+
contents: undefined
|
|
92
|
+
};
|
|
93
|
+
let o = await collect(url, `subscription { Platform_EventAppended(filter: {eventTypes: ["Placed"]}) }`, asAdmin, 2, 3000, () => {
|
|
94
|
+
ticker.contents = Primitive_option.some(setInterval(() => {
|
|
95
|
+
LocalEventTap$ReventlessLocal.publish({
|
|
96
|
+
line: `@@RVLESS_EVT@@ {"event":"domainEvent","name":"shipped"}`,
|
|
97
|
+
facets: {
|
|
98
|
+
eventType: "Shipped"
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
LocalEventTap$ReventlessLocal.publish({
|
|
102
|
+
line: `@@RVLESS_EVT@@ {"event":"domainEvent","name":"placed"}`,
|
|
103
|
+
facets: {
|
|
104
|
+
eventType: "Placed"
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}, 20));
|
|
108
|
+
});
|
|
109
|
+
Stdlib_Option.forEach(ticker.contents, prim => {
|
|
110
|
+
clearInterval(prim);
|
|
111
|
+
});
|
|
112
|
+
globalThis.expect(Primitive_option.fromNullable(o.error)).toEqual(undefined);
|
|
113
|
+
globalThis.expect(o.values.map(v => field(v, "name"))).toEqual([
|
|
114
|
+
"placed",
|
|
115
|
+
"placed"
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
let Logger;
|
|
121
|
+
|
|
122
|
+
export {
|
|
123
|
+
Logger,
|
|
124
|
+
collect,
|
|
125
|
+
sleep,
|
|
126
|
+
port,
|
|
127
|
+
url,
|
|
128
|
+
asAdmin,
|
|
129
|
+
asUser,
|
|
130
|
+
field,
|
|
131
|
+
}
|
|
132
|
+
/* port Not a pure module */
|
|
@@ -333,4 +333,84 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
333
333
|
},
|
|
334
334
|
)
|
|
335
335
|
})
|
|
336
|
+
|
|
337
|
+
// A row keeps the event that created it, so the command sent for it continues
|
|
338
|
+
// the event's flow; a row stored before rows kept one still runs.
|
|
339
|
+
describe("the flow", () => {
|
|
340
|
+
let withMeta = (json: JSON.t, ~msgId, ~correlationId) =>
|
|
341
|
+
switch json {
|
|
342
|
+
| Object(d) =>
|
|
343
|
+
let meta: Reventless.Message.meta = {
|
|
344
|
+
service: ShipOrderSource.name,
|
|
345
|
+
time: "2026-01-01T00:00:00Z",
|
|
346
|
+
user: "user-7",
|
|
347
|
+
msgId,
|
|
348
|
+
correlationId,
|
|
349
|
+
traceparent: "00-trace-span-01",
|
|
350
|
+
}
|
|
351
|
+
d->Dict.set("meta", meta->Reventless.Util_Sury.toJson(Reventless.Message.metaSchema))
|
|
352
|
+
JSON.Object(d)
|
|
353
|
+
| other => other
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
testPromise(
|
|
357
|
+
"the command derives its meta from the event that created the row",
|
|
358
|
+
async () => {
|
|
359
|
+
let json =
|
|
360
|
+
encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))->withMeta(
|
|
361
|
+
~msgId="evt-1",
|
|
362
|
+
~correlationId="flow-1",
|
|
363
|
+
)
|
|
364
|
+
await Callback.phase1([json], testContext, ~capabilities=Reventless.Capabilities.none)
|
|
365
|
+
expect((Callback.todoItems->Dict.getUnsafe("ord-1")).source)->toEqual(
|
|
366
|
+
Some({
|
|
367
|
+
ReventlessCore.Message.msgId: "evt-1",
|
|
368
|
+
correlationId: "flow-1",
|
|
369
|
+
user: "user-7",
|
|
370
|
+
traceparent: "00-trace-span-01",
|
|
371
|
+
}),
|
|
372
|
+
)
|
|
373
|
+
let published = ref([])
|
|
374
|
+
await Callback.phase2(async cmds => published := cmds)
|
|
375
|
+
let meta = (published.contents->Array.getUnsafe(0)).meta
|
|
376
|
+
expect(meta.correlationId)->toBe("flow-1")
|
|
377
|
+
expect(meta.causationId)->toEqual(Some("evt-1"))
|
|
378
|
+
expect(meta.user)->toEqual(Some("user-7"))
|
|
379
|
+
expect(meta.traceparent)->toEqual(Some("00-trace-span-01"))
|
|
380
|
+
expect(meta.service)->toBe(ShipOrderSpec.targetName)
|
|
381
|
+
expect(meta.msgId != "evt-1")->toBe(true)
|
|
382
|
+
},
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
testPromise(
|
|
386
|
+
"a stored row without a source decodes and starts a new flow",
|
|
387
|
+
async () => {
|
|
388
|
+
let stored = JSON.parseOrThrow(`{"item":{"orderId":"ord-9","address":"x"},"status":"Pending","createdAt":"2026-01-01T00:00:00Z","retryCount":0}`)
|
|
389
|
+
let row = stored->Reventless.Util_Sury.fromJson(todoRowSchema)
|
|
390
|
+
expect(row.source)->toEqual(None)
|
|
391
|
+
Callback.todoItems->Dict.set("ord-9", row)
|
|
392
|
+
let published = ref([])
|
|
393
|
+
await Callback.phase2(async cmds => published := cmds)
|
|
394
|
+
let meta = (published.contents->Array.getUnsafe(0)).meta
|
|
395
|
+
expect(meta.causationId)->toEqual(None)
|
|
396
|
+
expect(meta.correlationId)->toBe(meta.msgId)
|
|
397
|
+
},
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
testSync(
|
|
401
|
+
"a row with a source round-trips through its schema",
|
|
402
|
+
() => {
|
|
403
|
+
let row: todoRow = {
|
|
404
|
+
item: JSON.Encode.object(Dict.make()),
|
|
405
|
+
status: Pending,
|
|
406
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
407
|
+
retryCount: 0,
|
|
408
|
+
source: {ReventlessCore.Message.msgId: "evt-1", correlationId: "flow-1"},
|
|
409
|
+
}
|
|
410
|
+
let json = row->Reventless.Util_Sury.toJson(todoRowSchema)
|
|
411
|
+
expect(json->JSON.stringify->String.includes(`"source":{"msgId":"evt-1"`))->toBe(true)
|
|
412
|
+
expect(json->Reventless.Util_Sury.fromJson(todoRowSchema))->toEqual(row)
|
|
413
|
+
},
|
|
414
|
+
)
|
|
415
|
+
})
|
|
336
416
|
})
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
5
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
6
|
+
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
7
|
+
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
6
8
|
import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
|
|
7
9
|
import * as AutomationSliceFixtures$ReventlessLocal from "./AutomationSliceFixtures.res.mjs";
|
|
8
10
|
import * as AutomationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs";
|
|
@@ -280,6 +282,85 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
280
282
|
globalThis.expect(publishedCommands2.contents.length).toBe(0);
|
|
281
283
|
});
|
|
282
284
|
});
|
|
285
|
+
globalThis.describe("the flow", () => {
|
|
286
|
+
let withMeta = (json, msgId, correlationId) => {
|
|
287
|
+
if (typeof json !== "object" || json === null || Array.isArray(json)) {
|
|
288
|
+
return json;
|
|
289
|
+
}
|
|
290
|
+
let meta_service = AutomationSliceFixtures$ReventlessLocal.ShipOrderSource.name;
|
|
291
|
+
let meta_user = "user-7";
|
|
292
|
+
let meta_traceparent = "00-trace-span-01";
|
|
293
|
+
let meta = {
|
|
294
|
+
service: meta_service,
|
|
295
|
+
time: "2026-01-01T00:00:00Z",
|
|
296
|
+
user: meta_user,
|
|
297
|
+
msgId: msgId,
|
|
298
|
+
correlationId: correlationId,
|
|
299
|
+
traceparent: meta_traceparent
|
|
300
|
+
};
|
|
301
|
+
json["meta"] = Util_Sury$Reventless.toJson(meta, Message$Reventless.metaSchema);
|
|
302
|
+
return json;
|
|
303
|
+
};
|
|
304
|
+
globalThis.test("the command derives its meta from the event that created the row", async () => {
|
|
305
|
+
let json = withMeta(AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
306
|
+
TAG: "OrderPlaced",
|
|
307
|
+
orderId: "ord-1",
|
|
308
|
+
address: "123 Main St"
|
|
309
|
+
}), "evt-1", "flow-1");
|
|
310
|
+
await Callback.phase1([json], AutomationSliceFixtures$ReventlessLocal.testContext, Capabilities$Reventless.none);
|
|
311
|
+
globalThis.expect(Callback.todoItems["ord-1"].source).toEqual({
|
|
312
|
+
msgId: "evt-1",
|
|
313
|
+
correlationId: "flow-1",
|
|
314
|
+
user: "user-7",
|
|
315
|
+
traceparent: "00-trace-span-01"
|
|
316
|
+
});
|
|
317
|
+
let published = {
|
|
318
|
+
contents: []
|
|
319
|
+
};
|
|
320
|
+
await Callback.phase2(async cmds => {
|
|
321
|
+
published.contents = cmds;
|
|
322
|
+
});
|
|
323
|
+
let meta = published.contents[0].meta;
|
|
324
|
+
globalThis.expect(meta.correlationId).toBe("flow-1");
|
|
325
|
+
globalThis.expect(meta.causationId).toEqual("evt-1");
|
|
326
|
+
globalThis.expect(meta.user).toEqual("user-7");
|
|
327
|
+
globalThis.expect(meta.traceparent).toEqual("00-trace-span-01");
|
|
328
|
+
globalThis.expect(meta.service).toBe(AutomationSliceFixtures$ReventlessLocal.ShipOrderSpec.targetName);
|
|
329
|
+
globalThis.expect(meta.msgId !== "evt-1").toBe(true);
|
|
330
|
+
});
|
|
331
|
+
globalThis.test("a stored row without a source decodes and starts a new flow", async () => {
|
|
332
|
+
let stored = JSON.parse(`{"item":{"orderId":"ord-9","address":"x"},"status":"Pending","createdAt":"2026-01-01T00:00:00Z","retryCount":0}`);
|
|
333
|
+
let row = Util_Sury$Reventless.fromJson(stored, AutomationSlice_Callback$ReventlessCore.todoRowSchema);
|
|
334
|
+
globalThis.expect(row.source).toEqual(undefined);
|
|
335
|
+
Callback.todoItems["ord-9"] = row;
|
|
336
|
+
let published = {
|
|
337
|
+
contents: []
|
|
338
|
+
};
|
|
339
|
+
await Callback.phase2(async cmds => {
|
|
340
|
+
published.contents = cmds;
|
|
341
|
+
});
|
|
342
|
+
let meta = published.contents[0].meta;
|
|
343
|
+
globalThis.expect(meta.causationId).toEqual(undefined);
|
|
344
|
+
globalThis.expect(meta.correlationId).toBe(meta.msgId);
|
|
345
|
+
});
|
|
346
|
+
globalThis.test("a row with a source round-trips through its schema", () => {
|
|
347
|
+
let row_item = {};
|
|
348
|
+
let row_source = {
|
|
349
|
+
msgId: "evt-1",
|
|
350
|
+
correlationId: "flow-1"
|
|
351
|
+
};
|
|
352
|
+
let row = {
|
|
353
|
+
item: row_item,
|
|
354
|
+
status: "Pending",
|
|
355
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
356
|
+
retryCount: 0,
|
|
357
|
+
source: row_source
|
|
358
|
+
};
|
|
359
|
+
let json = Util_Sury$Reventless.toJson(row, AutomationSlice_Callback$ReventlessCore.todoRowSchema);
|
|
360
|
+
globalThis.expect(JSON.stringify(json).includes(`"source":{"msgId":"evt-1"`)).toBe(true);
|
|
361
|
+
globalThis.expect(Util_Sury$Reventless.fromJson(json, AutomationSlice_Callback$ReventlessCore.todoRowSchema)).toEqual(row);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
283
364
|
});
|
|
284
365
|
|
|
285
366
|
export {
|