@reventlessdev/reventless-local 3.0.0-alpha.317 → 3.0.0-alpha.319
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 +10 -10
- package/src/LocalPlatformRegistry.res +26 -1
- package/src/LocalPlatformRegistry.res.mjs +25 -5
- package/src/Platform.res +27 -2
- package/src/Platform.res.mjs +24 -4
- 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 +149 -0
- package/src/adapter/LocalTapLines.res.mjs +232 -0
- package/src/adapter/PlatformGraphQL_Server.res +36 -3
- package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
- package/src/adapter/ProjectionCheckpoint.res.mjs +3 -3
- package/src/components/AutomationSlice_Builder.res.mjs +1 -1
- package/src/components/LocalAutomationSchedules.res.mjs +1 -1
- package/src/components/LocalAutomationSweep.res.mjs +1 -1
- package/src/reexport/CommandTopic.res.mjs +5 -2
- package/tests/LocalPlatformRegistryTest.res +17 -0
- package/tests/LocalPlatformRegistryTest.res.mjs +14 -2
- package/tests/LocalPlatformStartTest.res.mjs +1 -1
- 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 +207 -0
- package/tests/adapter/LocalTapLinesTest.res.mjs +274 -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,165 @@
|
|
|
1
|
+
// What the platform API and the platform MCP server offer a watching tool: the
|
|
2
|
+
// MCP resources for recent logs and all events answer and filter, and the API
|
|
3
|
+
// fields are refused to a caller without the admin group.
|
|
4
|
+
|
|
5
|
+
@@warning("-44")
|
|
6
|
+
|
|
7
|
+
open JestGlobals
|
|
8
|
+
|
|
9
|
+
module Logger = ReventlessCore.Logger
|
|
10
|
+
|
|
11
|
+
let _ = TestRunner.setup()
|
|
12
|
+
let opts: Pulumi.CustomResourceOptions.t = {}
|
|
13
|
+
|
|
14
|
+
let identity = (~groups): Reventless.Identity.t => {
|
|
15
|
+
userId: "u-1",
|
|
16
|
+
username: "u",
|
|
17
|
+
groups,
|
|
18
|
+
provider: InMemory,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let context = (~groups): JSON.t => Obj.magic({"identity": identity(~groups), "authenticated": true})
|
|
22
|
+
|
|
23
|
+
let admin = () => context(~groups=[Reventless.Role.adminGroup()])
|
|
24
|
+
|
|
25
|
+
let textOf = (r: McpSdk.readResourceResult): JSON.t =>
|
|
26
|
+
(r.contents->Array.getUnsafe(0)).text->JSON.parseOrThrow
|
|
27
|
+
|
|
28
|
+
let field = (json: JSON.t, key: string): JSON.t =>
|
|
29
|
+
json->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))->Option.getOr(JSON.Encode.null)
|
|
30
|
+
|
|
31
|
+
let strings = (items: JSON.t, key: string): array<string> =>
|
|
32
|
+
items
|
|
33
|
+
->JSON.Decode.array
|
|
34
|
+
->Option.getOr([])
|
|
35
|
+
->Array.filterMap(i => i->field(key)->JSON.Decode.string)
|
|
36
|
+
|
|
37
|
+
describe("MCP resource platform/logs/recent", () => {
|
|
38
|
+
afterEach(() => LocalLogFeed.resetForTests())
|
|
39
|
+
|
|
40
|
+
testPromise("answers with the recent records, filtered by its query", async () => {
|
|
41
|
+
LocalLogFeed.install(~env=Dict.make())
|
|
42
|
+
Logger.emit(~level=Warn, ~comp="Projection", ~plugin="Ordering", "warned")
|
|
43
|
+
Logger.emit(~level=Info, ~comp="Projection", ~plugin="Ordering", "informed")
|
|
44
|
+
Logger.emit(~level=Error, ~comp="Projection", ~plugin="Catalog", "failed")
|
|
45
|
+
let all = await LocalPlatformObserve.recentLogs("platform/logs/recent")
|
|
46
|
+
expect(all->textOf->field("records")->strings("message"))->toEqual([
|
|
47
|
+
"warned",
|
|
48
|
+
"informed",
|
|
49
|
+
"failed",
|
|
50
|
+
])
|
|
51
|
+
let narrowed = await LocalPlatformObserve.recentLogs(
|
|
52
|
+
"platform/logs/recent?plugins=Ordering&level=warn",
|
|
53
|
+
)
|
|
54
|
+
expect(narrowed->textOf->field("records")->strings("message"))->toEqual(["warned"])
|
|
55
|
+
let last = await LocalPlatformObserve.recentLogs("platform/logs/recent?limit=1")
|
|
56
|
+
expect(last->textOf->field("records")->strings("message"))->toEqual(["failed"])
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe("MCP resource platform/events", () => {
|
|
61
|
+
testPromise("answers with every log's events newest first, filtered by its query", async () => {
|
|
62
|
+
BackendState.setMemory()
|
|
63
|
+
LocalEventLogRead.resetForTests()
|
|
64
|
+
module Bus = LocalBus.MakeSilent()
|
|
65
|
+
module Dcb = LocalDcbEventLogStorage.Make(Bus)
|
|
66
|
+
let catalog = await Dcb.make(
|
|
67
|
+
~name="CatalogDcbEventLog",
|
|
68
|
+
~indexes=[],
|
|
69
|
+
~partitionTag=Reventless.DcbTag.Simple({key: "productId"}),
|
|
70
|
+
~opts,
|
|
71
|
+
).operations->TestRunner.resolve
|
|
72
|
+
LocalEventLogRead.notePlugin(~log="CatalogDcbEventLog", ~plugin="Catalog")
|
|
73
|
+
let add = async (productId, correlationId) => {
|
|
74
|
+
let _ = await catalog.append([
|
|
75
|
+
{
|
|
76
|
+
eventType: "Added",
|
|
77
|
+
data: JSON.Encode.null,
|
|
78
|
+
tags: [{key: "productId", value: productId}],
|
|
79
|
+
meta: ReventlessCore.Message.generateMeta(~service="test", ~correlationId),
|
|
80
|
+
},
|
|
81
|
+
])
|
|
82
|
+
}
|
|
83
|
+
await add("p-1", "flow-a")
|
|
84
|
+
await add("p-2", "flow-b")
|
|
85
|
+
await add("p-3", "flow-a")
|
|
86
|
+
let all = await LocalPlatformObserve.events("platform/events")
|
|
87
|
+
let tagValues = page =>
|
|
88
|
+
page
|
|
89
|
+
->textOf
|
|
90
|
+
->field("edges")
|
|
91
|
+
->JSON.Decode.array
|
|
92
|
+
->Option.getOr([])
|
|
93
|
+
->Array.map(edge => edge->field("node")->field("tags")->strings("value")->Array.join(","))
|
|
94
|
+
expect(all->tagValues)->toEqual(["p-3", "p-2", "p-1"])
|
|
95
|
+
let flow = await LocalPlatformObserve.events("platform/events?correlationId=flow-a&first=1")
|
|
96
|
+
expect(flow->tagValues)->toEqual(["p-3"])
|
|
97
|
+
let cursor = flow->textOf->field("pageInfo")->field("endCursor")->JSON.Decode.string
|
|
98
|
+
let next = await LocalPlatformObserve.events(
|
|
99
|
+
`platform/events?correlationId=flow-a&first=1&after=${cursor->Option.getOr("")}`,
|
|
100
|
+
)
|
|
101
|
+
expect(next->tagValues)->toEqual(["p-1"])
|
|
102
|
+
let none = await LocalPlatformObserve.events("platform/events?plugins=Ordering")
|
|
103
|
+
expect(none->tagValues)->toEqual([])
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe("the platform API's watching fields", () => {
|
|
108
|
+
afterEach(() => Logger.clearLevels())
|
|
109
|
+
|
|
110
|
+
let server = () => {
|
|
111
|
+
let s = ReventlessGraphqlServer.GraphQL_ServerInstance.make(~label="Test")
|
|
112
|
+
LocalPlatformObserve.registerGraphQL(s)
|
|
113
|
+
s
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
testPromise("are refused to a caller without the admin group", async () => {
|
|
117
|
+
let s = server()
|
|
118
|
+
let refused = async (resolver: option<GraphqlYoga.resolverFn>) => {
|
|
119
|
+
let run = resolver->Option.getOrThrow
|
|
120
|
+
switch await run(JSON.Encode.null, JSON.Encode.null, context(~groups=["Customer"])) {
|
|
121
|
+
| _ => false
|
|
122
|
+
| exception _ => true
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
expect(await refused(s.getQueryResolver(LocalPlatformObserve.eventLogField)))->toBe(true)
|
|
126
|
+
expect(await refused(s.getQueryResolver(LocalPlatformObserve.logLevelsField)))->toBe(true)
|
|
127
|
+
expect(await refused(s.getMutationResolver(LocalPlatformObserve.setLogLevelField)))->toBe(true)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
testPromise("set and list the log levels for an admin", async () => {
|
|
131
|
+
let s = server()
|
|
132
|
+
let set = s.getMutationResolver(LocalPlatformObserve.setLogLevelField)->Option.getOrThrow
|
|
133
|
+
let args = JSON.parseOrThrow(`{"plugin":"Ordering","comp":"Projection","level":"DEBUG"}`)
|
|
134
|
+
let answer = await set(JSON.Encode.null, args, admin())
|
|
135
|
+
expect(answer->field("settings"))->toEqual(
|
|
136
|
+
JSON.parseOrThrow(`[{"level":"DEBUG","plugin":"Ordering","comp":"Projection"}]`),
|
|
137
|
+
)
|
|
138
|
+
expect(Logger.minLevelFor(~plugin="Ordering", ~comp="Projection", ()))->toEqual(
|
|
139
|
+
Some(Logger.Debug),
|
|
140
|
+
)
|
|
141
|
+
let cleared = await set(
|
|
142
|
+
JSON.Encode.null,
|
|
143
|
+
JSON.parseOrThrow(`{"plugin":"Ordering","comp":"Projection","level":null}`),
|
|
144
|
+
admin(),
|
|
145
|
+
)
|
|
146
|
+
expect(cleared->field("settings"))->toEqual(JSON.parseOrThrow(`[]`))
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
testPromise("the event log answers an admin with a connection", async () => {
|
|
150
|
+
BackendState.setMemory()
|
|
151
|
+
LocalEventLogRead.resetForTests()
|
|
152
|
+
let s = server()
|
|
153
|
+
let read = s.getQueryResolver(LocalPlatformObserve.eventLogField)->Option.getOrThrow
|
|
154
|
+
let answer = await read(JSON.Encode.null, JSON.parseOrThrow(`{"first":5}`), admin())
|
|
155
|
+
expect(answer->field("edges"))->toEqual(JSON.Encode.array([]))
|
|
156
|
+
expect(answer->field("pageInfo")->field("hasNextPage"))->toEqual(JSON.Encode.bool(false))
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
testSync("the SDL builds into a schema", () => {
|
|
160
|
+
let s = server()
|
|
161
|
+
let sdl = s.buildSdl()
|
|
162
|
+
expect(sdl->String.includes("Platform_EventLog(filter: Platform_EventLogFilter"))->toBe(true)
|
|
163
|
+
let _ = GraphqlYoga.createSchema({"typeDefs": sdl, "resolvers": Dict.make()})
|
|
164
|
+
})
|
|
165
|
+
})
|
|
@@ -0,0 +1,183 @@
|
|
|
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_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as GraphqlYoga from "graphql-yoga";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
|
|
8
|
+
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
9
|
+
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
10
|
+
import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
|
|
11
|
+
import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
|
|
12
|
+
import * as BackendState$ReventlessLocal from "../../src/adapter/BackendState.res.mjs";
|
|
13
|
+
import * as LocalLogFeed$ReventlessLocal from "../../src/adapter/LocalLogFeed.res.mjs";
|
|
14
|
+
import * as LocalEventLogRead$ReventlessLocal from "../../src/adapter/LocalEventLogRead.res.mjs";
|
|
15
|
+
import * as LocalPlatformObserve$ReventlessLocal from "../../src/adapter/LocalPlatformObserve.res.mjs";
|
|
16
|
+
import * as LocalDcbEventLogStorage$ReventlessLocal from "../../src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs";
|
|
17
|
+
import * as GraphQL_ServerInstance$ReventlessGraphqlServer from "@reventlessdev/reventless-graphql-server/src/GraphQL_ServerInstance.res.mjs";
|
|
18
|
+
|
|
19
|
+
TestRunner$ReventlessLocal.setup();
|
|
20
|
+
|
|
21
|
+
let opts = {};
|
|
22
|
+
|
|
23
|
+
function identity(groups) {
|
|
24
|
+
return {
|
|
25
|
+
userId: "u-1",
|
|
26
|
+
username: "u",
|
|
27
|
+
groups: groups,
|
|
28
|
+
provider: "InMemory"
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function context(groups) {
|
|
33
|
+
return {
|
|
34
|
+
identity: identity(groups),
|
|
35
|
+
authenticated: true
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function admin() {
|
|
40
|
+
return context([Role$Reventless.adminGroup()]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function textOf(r) {
|
|
44
|
+
return JSON.parse(r.contents[0].text);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function field(json, key) {
|
|
48
|
+
return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d[key]), null);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function strings(items, key) {
|
|
52
|
+
return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_JSON.Decode.array(items), []), i => Stdlib_JSON.Decode.string(field(i, key)));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
globalThis.describe("MCP resource platform/logs/recent", () => {
|
|
56
|
+
globalThis.afterEach(() => LocalLogFeed$ReventlessLocal.resetForTests());
|
|
57
|
+
globalThis.test("answers with the recent records, filtered by its query", async () => {
|
|
58
|
+
LocalLogFeed$ReventlessLocal.install({});
|
|
59
|
+
Logger$ReventlessCore.emit("Warn", "Projection", "Ordering", undefined, undefined, "warned");
|
|
60
|
+
Logger$ReventlessCore.emit("Info", "Projection", "Ordering", undefined, undefined, "informed");
|
|
61
|
+
Logger$ReventlessCore.emit("Error", "Projection", "Catalog", undefined, undefined, "failed");
|
|
62
|
+
let all = await LocalPlatformObserve$ReventlessLocal.recentLogs("platform/logs/recent");
|
|
63
|
+
globalThis.expect(strings(field(textOf(all), "records"), "message")).toEqual([
|
|
64
|
+
"warned",
|
|
65
|
+
"informed",
|
|
66
|
+
"failed"
|
|
67
|
+
]);
|
|
68
|
+
let narrowed = await LocalPlatformObserve$ReventlessLocal.recentLogs("platform/logs/recent?plugins=Ordering&level=warn");
|
|
69
|
+
globalThis.expect(strings(field(textOf(narrowed), "records"), "message")).toEqual(["warned"]);
|
|
70
|
+
let last = await LocalPlatformObserve$ReventlessLocal.recentLogs("platform/logs/recent?limit=1");
|
|
71
|
+
globalThis.expect(strings(field(textOf(last), "records"), "message")).toEqual(["failed"]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
globalThis.describe("MCP resource platform/events", () => {
|
|
76
|
+
globalThis.test("answers with every log's events newest first, filtered by its query", async () => {
|
|
77
|
+
BackendState$ReventlessLocal.setMemory();
|
|
78
|
+
LocalEventLogRead$ReventlessLocal.resetForTests();
|
|
79
|
+
let Bus = LocalBus$ReventlessLocal.MakeSilent({});
|
|
80
|
+
let Dcb = LocalDcbEventLogStorage$ReventlessLocal.Make(Bus);
|
|
81
|
+
let catalog = await TestRunner$ReventlessLocal.resolve(Dcb.make("CatalogDcbEventLog", [], {
|
|
82
|
+
TAG: "Simple",
|
|
83
|
+
_0: {
|
|
84
|
+
key: "productId"
|
|
85
|
+
}
|
|
86
|
+
}, undefined, opts).operations);
|
|
87
|
+
LocalEventLogRead$ReventlessLocal.notePlugin("CatalogDcbEventLog", "Catalog");
|
|
88
|
+
let add = async (productId, correlationId) => {
|
|
89
|
+
await catalog.append([{
|
|
90
|
+
eventType: "Added",
|
|
91
|
+
data: null,
|
|
92
|
+
tags: [{
|
|
93
|
+
key: "productId",
|
|
94
|
+
value: productId
|
|
95
|
+
}],
|
|
96
|
+
meta: Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, correlationId, undefined, undefined)
|
|
97
|
+
}], undefined);
|
|
98
|
+
};
|
|
99
|
+
await add("p-1", "flow-a");
|
|
100
|
+
await add("p-2", "flow-b");
|
|
101
|
+
await add("p-3", "flow-a");
|
|
102
|
+
let all = await LocalPlatformObserve$ReventlessLocal.events("platform/events");
|
|
103
|
+
let tagValues = page => Stdlib_Option.getOr(Stdlib_JSON.Decode.array(field(textOf(page), "edges")), []).map(edge => strings(field(field(edge, "node"), "tags"), "value").join(","));
|
|
104
|
+
globalThis.expect(tagValues(all)).toEqual([
|
|
105
|
+
"p-3",
|
|
106
|
+
"p-2",
|
|
107
|
+
"p-1"
|
|
108
|
+
]);
|
|
109
|
+
let flow = await LocalPlatformObserve$ReventlessLocal.events("platform/events?correlationId=flow-a&first=1");
|
|
110
|
+
globalThis.expect(tagValues(flow)).toEqual(["p-3"]);
|
|
111
|
+
let cursor = Stdlib_JSON.Decode.string(field(field(textOf(flow), "pageInfo"), "endCursor"));
|
|
112
|
+
let next = await LocalPlatformObserve$ReventlessLocal.events(`platform/events?correlationId=flow-a&first=1&after=` + Stdlib_Option.getOr(cursor, ""));
|
|
113
|
+
globalThis.expect(tagValues(next)).toEqual(["p-1"]);
|
|
114
|
+
let none = await LocalPlatformObserve$ReventlessLocal.events("platform/events?plugins=Ordering");
|
|
115
|
+
globalThis.expect(tagValues(none)).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
globalThis.describe("the platform API's watching fields", () => {
|
|
120
|
+
globalThis.afterEach(() => Logger$ReventlessCore.clearLevels());
|
|
121
|
+
let server = () => {
|
|
122
|
+
let s = GraphQL_ServerInstance$ReventlessGraphqlServer.make("Test");
|
|
123
|
+
LocalPlatformObserve$ReventlessLocal.registerGraphQL(s);
|
|
124
|
+
return s;
|
|
125
|
+
};
|
|
126
|
+
globalThis.test("are refused to a caller without the admin group", async () => {
|
|
127
|
+
let s = server();
|
|
128
|
+
let refused = async resolver => {
|
|
129
|
+
let run = Stdlib_Option.getOrThrow(resolver, undefined);
|
|
130
|
+
try {
|
|
131
|
+
await run(null, null, context(["Customer"]));
|
|
132
|
+
return false;
|
|
133
|
+
} catch (exn) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
globalThis.expect(await refused(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.eventLogField))).toBe(true);
|
|
138
|
+
globalThis.expect(await refused(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.logLevelsField))).toBe(true);
|
|
139
|
+
globalThis.expect(await refused(s.getMutationResolver(LocalPlatformObserve$ReventlessLocal.setLogLevelField))).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
globalThis.test("set and list the log levels for an admin", async () => {
|
|
142
|
+
let s = server();
|
|
143
|
+
let set = Stdlib_Option.getOrThrow(s.getMutationResolver(LocalPlatformObserve$ReventlessLocal.setLogLevelField), undefined);
|
|
144
|
+
let args = JSON.parse(`{"plugin":"Ordering","comp":"Projection","level":"DEBUG"}`);
|
|
145
|
+
let answer = await set(null, args, context([Role$Reventless.adminGroup()]));
|
|
146
|
+
globalThis.expect(field(answer, "settings")).toEqual(JSON.parse(`[{"level":"DEBUG","plugin":"Ordering","comp":"Projection"}]`));
|
|
147
|
+
globalThis.expect(Logger$ReventlessCore.minLevelFor("Ordering", "Projection", undefined)).toEqual("Debug");
|
|
148
|
+
let cleared = await set(null, JSON.parse(`{"plugin":"Ordering","comp":"Projection","level":null}`), context([Role$Reventless.adminGroup()]));
|
|
149
|
+
globalThis.expect(field(cleared, "settings")).toEqual(JSON.parse(`[]`));
|
|
150
|
+
});
|
|
151
|
+
globalThis.test("the event log answers an admin with a connection", async () => {
|
|
152
|
+
BackendState$ReventlessLocal.setMemory();
|
|
153
|
+
LocalEventLogRead$ReventlessLocal.resetForTests();
|
|
154
|
+
let s = server();
|
|
155
|
+
let read = Stdlib_Option.getOrThrow(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.eventLogField), undefined);
|
|
156
|
+
let answer = await read(null, JSON.parse(`{"first":5}`), context([Role$Reventless.adminGroup()]));
|
|
157
|
+
globalThis.expect(field(answer, "edges")).toEqual([]);
|
|
158
|
+
globalThis.expect(field(field(answer, "pageInfo"), "hasNextPage")).toEqual(false);
|
|
159
|
+
});
|
|
160
|
+
globalThis.test("the SDL builds into a schema", () => {
|
|
161
|
+
let s = server();
|
|
162
|
+
let sdl = s.buildSdl();
|
|
163
|
+
globalThis.expect(sdl.includes("Platform_EventLog(filter: Platform_EventLogFilter")).toBe(true);
|
|
164
|
+
GraphqlYoga.createSchema({
|
|
165
|
+
typeDefs: sdl,
|
|
166
|
+
resolvers: {}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
let Logger;
|
|
172
|
+
|
|
173
|
+
export {
|
|
174
|
+
Logger,
|
|
175
|
+
opts,
|
|
176
|
+
identity,
|
|
177
|
+
context,
|
|
178
|
+
admin,
|
|
179
|
+
textOf,
|
|
180
|
+
field,
|
|
181
|
+
strings,
|
|
182
|
+
}
|
|
183
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// The event tap's lines besides events: each command's outcome, once, and each
|
|
2
|
+
// row a projection changed, its values redacted; a rejected command is also kept
|
|
3
|
+
// in the problem ring.
|
|
4
|
+
|
|
5
|
+
open JestGlobals
|
|
6
|
+
|
|
7
|
+
module Outcome = ReventlessCore.CommandTopic_Helpers
|
|
8
|
+
module ViewChange = ReventlessCore.ViewChange
|
|
9
|
+
|
|
10
|
+
let sleep = (ms: int): promise<unit> =>
|
|
11
|
+
Promise.make((resolve, _) => {
|
|
12
|
+
let _ = setTimeout(() => resolve(), ms)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
let until = async (~ms=2000, cond: unit => bool): bool => {
|
|
16
|
+
let waited = ref(0)
|
|
17
|
+
while !cond() && waited.contents < ms {
|
|
18
|
+
await sleep(5)
|
|
19
|
+
waited := waited.contents + 5
|
|
20
|
+
}
|
|
21
|
+
cond()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type client = {socket: NodeNet.socket, mutable received: string}
|
|
25
|
+
|
|
26
|
+
let connect = (port: int): promise<client> =>
|
|
27
|
+
Promise.make((resolve, _) => {
|
|
28
|
+
let c = ref(None)
|
|
29
|
+
let socket = NodeNet.connect(port, "127.0.0.1", () =>
|
|
30
|
+
c.contents->Option.forEach(client => resolve(client))
|
|
31
|
+
)
|
|
32
|
+
socket->NodeNet.setEncoding("utf8")
|
|
33
|
+
socket->NodeNet.onSocketError(_ => ())
|
|
34
|
+
let client = {socket, received: ""}
|
|
35
|
+
socket->NodeNet.onSocketData(chunk => client.received = client.received ++ chunk)
|
|
36
|
+
c := Some(client)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/** The lines of a kind, as objects, without their time. */
|
|
40
|
+
let linesOf = (c: client, ~kind: string): array<dict<JSON.t>> =>
|
|
41
|
+
c.received
|
|
42
|
+
->String.split("\n")
|
|
43
|
+
->Array.filterMap(l =>
|
|
44
|
+
switch l->JSON.parseOrThrow->JSON.Decode.object {
|
|
45
|
+
| Some(d) if d->Dict.get("kind") == Some(JSON.String(kind)) =>
|
|
46
|
+
d->Dict.delete("ts")
|
|
47
|
+
Some(d)
|
|
48
|
+
| _ => None
|
|
49
|
+
| exception _ => None
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@schema
|
|
54
|
+
type row = {
|
|
55
|
+
name: string,
|
|
56
|
+
token: @s.matches(Reventless.Sensitive.string) string,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let connectTap = async () => {
|
|
60
|
+
LocalEventTap.resetForTests()
|
|
61
|
+
LocalLogFeed.resetForTests()
|
|
62
|
+
LocalTapLines.resetForTests()
|
|
63
|
+
LocalLogFeed.install(~env=Dict.make())
|
|
64
|
+
LocalTapLines.install()
|
|
65
|
+
let port = await Promise.make((resolve, _) =>
|
|
66
|
+
LocalEventTap.start(~env=Dict.make(), ~onBound=resolve, ())
|
|
67
|
+
)
|
|
68
|
+
let tap = await connect(port)
|
|
69
|
+
let _ = await until(() => LocalEventTap.hasReaders())
|
|
70
|
+
tap
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let disconnect = async (tap: client) => {
|
|
74
|
+
NodeNet.destroySocket(tap.socket)
|
|
75
|
+
LocalTapLines.resetForTests()
|
|
76
|
+
await LocalEventTap.stopForTests()
|
|
77
|
+
LocalLogFeed.resetForTests()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("LocalTapLines", () => {
|
|
81
|
+
test("an accepted and a rejected command each appear once on the tap", async () => {
|
|
82
|
+
let tap = await connectTap()
|
|
83
|
+
|
|
84
|
+
Outcome.reportAccepted(
|
|
85
|
+
~component="Order",
|
|
86
|
+
~correlationId="flow-1",
|
|
87
|
+
"m-1",
|
|
88
|
+
{entityId: "o-1", eventCount: 2},
|
|
89
|
+
)
|
|
90
|
+
Outcome.reportRejected(
|
|
91
|
+
~component="Order",
|
|
92
|
+
~cause=DomainRejection,
|
|
93
|
+
~correlationId="flow-1",
|
|
94
|
+
"m-2",
|
|
95
|
+
{errorCode: "AlreadyExists", errorDetail: `{"name":"Widget"}`},
|
|
96
|
+
)
|
|
97
|
+
let _ = await until(() => tap->linesOf(~kind="commandOutcome")->Array.length >= 2)
|
|
98
|
+
await sleep(20)
|
|
99
|
+
|
|
100
|
+
expect(tap->linesOf(~kind="commandOutcome"))->toEqual([
|
|
101
|
+
Dict.fromArray([
|
|
102
|
+
("kind", JSON.String("commandOutcome")),
|
|
103
|
+
("component", JSON.String("Order")),
|
|
104
|
+
("msgId", JSON.String("m-1")),
|
|
105
|
+
("correlationId", JSON.String("flow-1")),
|
|
106
|
+
("outcome", JSON.String("accepted")),
|
|
107
|
+
("entityId", JSON.String("o-1")),
|
|
108
|
+
("eventCount", JSON.Number(2.)),
|
|
109
|
+
]),
|
|
110
|
+
Dict.fromArray([
|
|
111
|
+
("kind", JSON.String("commandOutcome")),
|
|
112
|
+
("component", JSON.String("Order")),
|
|
113
|
+
("msgId", JSON.String("m-2")),
|
|
114
|
+
("correlationId", JSON.String("flow-1")),
|
|
115
|
+
("outcome", JSON.String("rejected")),
|
|
116
|
+
("errorCode", JSON.String("AlreadyExists")),
|
|
117
|
+
("cause", JSON.String("DOMAIN")),
|
|
118
|
+
]),
|
|
119
|
+
])
|
|
120
|
+
// Not an event line: no sentinel prefix.
|
|
121
|
+
expect(tap.received->String.includes("@@RVLESS_EVT@@"))->toBe(false)
|
|
122
|
+
|
|
123
|
+
// The rejection, and only it, is kept as a warning in the problem ring.
|
|
124
|
+
let kept =
|
|
125
|
+
LocalLogFeed.recentRecords(~filter={level: Warn})->Array.filter(
|
|
126
|
+
r => r.message == "command rejected",
|
|
127
|
+
)
|
|
128
|
+
expect(kept->Array.map(r => (r.comp, r.annotations->Dict.get("msgId"))))->toEqual([
|
|
129
|
+
(Some("CommandTopic(Order)"), Some("m-2")),
|
|
130
|
+
])
|
|
131
|
+
expect(
|
|
132
|
+
(kept->Array.getUnsafe(0)).detail->Option.map(j => j->JSON.stringify)->Option.getOr(""),
|
|
133
|
+
)->toBe(`{"component":"Order","errorCode":"AlreadyExists","cause":"DOMAIN","errorDetail":{"name":"Widget"}}`)
|
|
134
|
+
|
|
135
|
+
await disconnect(tap)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test("an infrastructure failure is kept as an error", async () => {
|
|
139
|
+
let tap = await connectTap()
|
|
140
|
+
Outcome.reportRejected(
|
|
141
|
+
~component="Order",
|
|
142
|
+
~cause=InfrastructureFailure,
|
|
143
|
+
"m-3",
|
|
144
|
+
{errorCode: "AppendFailed", errorDetail: "store down"},
|
|
145
|
+
)
|
|
146
|
+
let kept =
|
|
147
|
+
LocalLogFeed.recentRecords(~filter={level: Error})->Array.filter(
|
|
148
|
+
r => r.message == "command rejected",
|
|
149
|
+
)
|
|
150
|
+
expect(kept->Array.length)->toBe(1)
|
|
151
|
+
await disconnect(tap)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test("a view change is sent with its values redacted", async () => {
|
|
155
|
+
let tap = await connectTap()
|
|
156
|
+
let onChange = ViewChange.reporter(
|
|
157
|
+
~comp="ReadModel(Accounts)",
|
|
158
|
+
~view="Accounts",
|
|
159
|
+
~stateToJson=r => r->Reventless.Util_Sury.toJson(rowSchema),
|
|
160
|
+
)
|
|
161
|
+
onChange({
|
|
162
|
+
key: "a-1",
|
|
163
|
+
kind: Changed,
|
|
164
|
+
before: {name: "Ada", token: "old-secret"},
|
|
165
|
+
after: {name: "Ada L.", token: "new-secret"},
|
|
166
|
+
sources: [{msgId: "m-1", correlationId: "flow-1", entityId: "a-1", entityType: "Account"}],
|
|
167
|
+
})
|
|
168
|
+
let _ = await until(() => tap->linesOf(~kind="viewChange")->Array.length >= 1)
|
|
169
|
+
|
|
170
|
+
let lines = tap->linesOf(~kind="viewChange")
|
|
171
|
+
expect(lines->Array.length)->toBe(1)
|
|
172
|
+
let line = lines->Array.getUnsafe(0)->JSON.Encode.object->JSON.stringify
|
|
173
|
+
expect(line->String.includes("secret"))->toBe(false)
|
|
174
|
+
expect(line->String.includes(Reventless.LogRedaction.placeholder))->toBe(true)
|
|
175
|
+
expect(line->String.includes(`"name":"Ada L."`))->toBe(true)
|
|
176
|
+
expect(
|
|
177
|
+
line->String.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":`),
|
|
178
|
+
)->toBe(true)
|
|
179
|
+
|
|
180
|
+
await disconnect(tap)
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe("the outcome line names its cause", () => {
|
|
185
|
+
testSync("a command an automation sent says which event it acted on", () => {
|
|
186
|
+
let line = LocalTapLines.outcomeLine({
|
|
187
|
+
component: "Notice",
|
|
188
|
+
reference: "cmd-2",
|
|
189
|
+
correlationId: "flow-1",
|
|
190
|
+
causationId: "evt-7",
|
|
191
|
+
outcome: OutcomeAccepted({entityId: Some("n-1"), eventCount: 1}),
|
|
192
|
+
})
|
|
193
|
+
let d = line->JSON.parseOrThrow->JSON.Decode.object->Option.getOrThrow
|
|
194
|
+
expect(d->Dict.get("causationId"))->toEqual(Some(JSON.String("evt-7")))
|
|
195
|
+
expect(d->Dict.get("msgId"))->toEqual(Some(JSON.String("cmd-2")))
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
testSync("a command sent from outside has none", () => {
|
|
199
|
+
let line = LocalTapLines.outcomeLine({
|
|
200
|
+
component: "Order",
|
|
201
|
+
reference: "cmd-1",
|
|
202
|
+
outcome: OutcomeAccepted({entityId: None, eventCount: 1}),
|
|
203
|
+
})
|
|
204
|
+
let d = line->JSON.parseOrThrow->JSON.Decode.object->Option.getOrThrow
|
|
205
|
+
expect(d->Dict.get("causationId"))->toEqual(None)
|
|
206
|
+
})
|
|
207
|
+
})
|