@reventlessdev/reventless-local 3.0.0-alpha.319 → 3.0.0-alpha.320

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/package.json +11 -11
  3. package/src/Platform.res +13 -0
  4. package/src/Platform.res.mjs +14 -0
  5. package/src/adapter/Api/LocalEvents_Server.res +34 -3
  6. package/src/adapter/Api/LocalEvents_Server.res.mjs +31 -3
  7. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res +24 -0
  8. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res.mjs +28 -0
  9. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_InMemory.res +26 -0
  10. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_InMemory.res.mjs +35 -0
  11. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res +61 -0
  12. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res.mjs +75 -0
  13. package/src/adapter/CommandOutcomeLog/LocalCommandOutcomeLog.res +88 -0
  14. package/src/adapter/CommandOutcomeLog/LocalCommandOutcomeLog.res.mjs +112 -0
  15. package/src/adapter/DomainGraphQL_Server.res +2 -1
  16. package/src/adapter/DomainGraphQL_Server.res.mjs +3 -1
  17. package/src/adapter/LocalFeed.res +16 -2
  18. package/src/adapter/LocalFeed.res.mjs +19 -2
  19. package/src/adapter/LocalIdleExit.res +71 -0
  20. package/src/adapter/LocalIdleExit.res.mjs +96 -0
  21. package/src/adapter/LocalMetrics.res +29 -0
  22. package/src/adapter/LocalMetrics.res.mjs +61 -0
  23. package/src/adapter/LocalPlatformObserve.res +129 -5
  24. package/src/adapter/LocalPlatformObserve.res.mjs +144 -2
  25. package/src/adapter/LocalRequestLog.res +62 -0
  26. package/src/adapter/LocalRequestLog.res.mjs +80 -0
  27. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +11 -3
  28. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +10 -3
  29. package/src/adapter/ViewChangeLog/LocalViewChangeLog.res +95 -0
  30. package/src/adapter/ViewChangeLog/LocalViewChangeLog.res.mjs +116 -0
  31. package/src/adapter/ViewChangeLog/ViewChangeLogEntry.res +33 -0
  32. package/src/adapter/ViewChangeLog/ViewChangeLogEntry.res.mjs +30 -0
  33. package/src/adapter/ViewChangeLog/ViewChangeLogStorage_InMemory.res +28 -0
  34. package/src/adapter/ViewChangeLog/ViewChangeLogStorage_InMemory.res.mjs +40 -0
  35. package/src/adapter/ViewChangeLog/ViewChangeLogStorage_Sqlite.res +76 -0
  36. package/src/adapter/ViewChangeLog/ViewChangeLogStorage_Sqlite.res.mjs +88 -0
  37. package/tests/adapter/GraphQL_SchemaInspectorTest.res +3 -2
  38. package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +1 -1
  39. package/tests/adapter/LocalEvents_ServerTest.res +11 -0
  40. package/tests/adapter/LocalEvents_ServerTest.res.mjs +7 -1
  41. package/tests/adapter/LocalIdleExitTest.res +57 -0
  42. package/tests/adapter/LocalIdleExitTest.res.mjs +62 -0
  43. package/tests/adapter/LocalPlatformObserveTest.res +183 -0
  44. package/tests/adapter/LocalPlatformObserveTest.res.mjs +181 -0
  45. package/tests/adapter/LocalRequestLogTest.res +46 -0
  46. package/tests/adapter/LocalRequestLogTest.res.mjs +61 -0
@@ -0,0 +1,62 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as LocalIdleExit$ReventlessLocal from "../../src/adapter/LocalIdleExit.res.mjs";
4
+
5
+ function wait(ms) {
6
+ return new Promise((resolve, param) => {
7
+ setTimeout(() => resolve(), ms);
8
+ });
9
+ }
10
+
11
+ globalThis.describe("LocalIdleExit", () => {
12
+ let stopped = {
13
+ contents: 0
14
+ };
15
+ globalThis.beforeEach(() => {
16
+ LocalIdleExit$ReventlessLocal.reset();
17
+ stopped.contents = 0;
18
+ LocalIdleExit$ReventlessLocal.onIdle.contents = () => {
19
+ stopped.contents = stopped.contents + 1 | 0;
20
+ };
21
+ });
22
+ globalThis.afterEach(() => LocalIdleExit$ReventlessLocal.reset());
23
+ globalThis.test("reads its grace in seconds, and nothing else turns it on", () => {
24
+ globalThis.expect(LocalIdleExit$ReventlessLocal.graceOf("60")).toEqual(60000);
25
+ globalThis.expect(LocalIdleExit$ReventlessLocal.graceOf("0")).toEqual(undefined);
26
+ globalThis.expect(LocalIdleExit$ReventlessLocal.graceOf("soon")).toEqual(undefined);
27
+ globalThis.expect(LocalIdleExit$ReventlessLocal.graceOf(undefined)).toEqual(undefined);
28
+ });
29
+ globalThis.test("stops when nothing connects within its grace", async () => {
30
+ LocalIdleExit$ReventlessLocal.start({
31
+ REVENTLESS_IDLE_EXIT: "1"
32
+ });
33
+ await wait(1200);
34
+ globalThis.expect(stopped.contents).toBe(1);
35
+ });
36
+ globalThis.test("keeps running while watched, and a watcher back in time keeps it running", async () => {
37
+ LocalIdleExit$ReventlessLocal.start({
38
+ REVENTLESS_IDLE_EXIT: "1"
39
+ });
40
+ LocalIdleExit$ReventlessLocal.connected();
41
+ await wait(1200);
42
+ globalThis.expect(stopped.contents).toBe(0);
43
+ LocalIdleExit$ReventlessLocal.disconnected();
44
+ await wait(500);
45
+ LocalIdleExit$ReventlessLocal.connected();
46
+ await wait(800);
47
+ globalThis.expect(stopped.contents).toBe(0);
48
+ LocalIdleExit$ReventlessLocal.disconnected();
49
+ await wait(1200);
50
+ globalThis.expect(stopped.contents).toBe(1);
51
+ });
52
+ globalThis.test("does nothing without the variable", async () => {
53
+ LocalIdleExit$ReventlessLocal.start({});
54
+ await wait(1200);
55
+ globalThis.expect(stopped.contents).toBe(0);
56
+ });
57
+ });
58
+
59
+ export {
60
+ wait,
61
+ }
62
+ /* Not a pure module */
@@ -123,8 +123,11 @@ describe("the platform API's watching fields", () => {
123
123
  }
124
124
  }
125
125
  expect(await refused(s.getQueryResolver(LocalPlatformObserve.eventLogField)))->toBe(true)
126
+ expect(await refused(s.getQueryResolver(LocalPlatformObserve.viewChangesField)))->toBe(true)
127
+ expect(await refused(s.getQueryResolver(LocalPlatformObserve.commandOutcomesField)))->toBe(true)
126
128
  expect(await refused(s.getQueryResolver(LocalPlatformObserve.logLevelsField)))->toBe(true)
127
129
  expect(await refused(s.getMutationResolver(LocalPlatformObserve.setLogLevelField)))->toBe(true)
130
+ expect(await refused(s.getQueryResolver(LocalPlatformObserve.metricsField)))->toBe(true)
128
131
  })
129
132
 
130
133
  testPromise("set and list the log levels for an admin", async () => {
@@ -156,6 +159,186 @@ describe("the platform API's watching fields", () => {
156
159
  expect(answer->field("pageInfo")->field("hasNextPage"))->toEqual(JSON.Encode.bool(false))
157
160
  })
158
161
 
162
+ // Two flows: two changes in one, one in the other.
163
+ let keepChanges = () => {
164
+ let change = (~view, ~key, ~flow, msgId): ReventlessCore.ViewChange.t => {
165
+ view,
166
+ key,
167
+ kind: Changed,
168
+ fields: ["status"],
169
+ after: JSON.parseOrThrow(`{"status":"shipped"}`),
170
+ sources: [{msgId, correlationId: flow}],
171
+ correlationId: flow,
172
+ }
173
+ LocalViewChangeLog.record(change(~view="Orders", ~key="o-1", ~flow="k-1", "m-1"))
174
+ LocalViewChangeLog.record(change(~view="Customers", ~key="c-1", ~flow="k-2", "m-2"))
175
+ LocalViewChangeLog.record(change(~view="Customers", ~key="c-1", ~flow="k-1", "m-3"))
176
+ }
177
+
178
+ let readChanges = async (args: string) => {
179
+ let s = server()
180
+ let read = s.getQueryResolver(LocalPlatformObserve.viewChangesField)->Option.getOrThrow
181
+ await read(JSON.Encode.null, JSON.parseOrThrow(args), admin())
182
+ }
183
+
184
+ let views = (answer: JSON.t) =>
185
+ answer
186
+ ->field("edges")
187
+ ->JSON.Decode.array
188
+ ->Option.getOr([])
189
+ ->Array.map(e => e->field("node")->field("view")->JSON.Decode.string->Option.getOr(""))
190
+
191
+ let readsBack = async () => {
192
+ keepChanges()
193
+ let flow = await readChanges(`{"filter":{"correlationId":"k-1"}}`)
194
+ let byEvent = await readChanges(`{"filter":{"msgId":"m-2"}}`)
195
+ let firstPage = await readChanges(`{"filter":{"correlationId":"k-1"},"first":1}`)
196
+ let cursor = firstPage->field("pageInfo")->field("endCursor")->JSON.Decode.string
197
+ let secondPage = await readChanges(
198
+ `{"filter":{"correlationId":"k-1"},"first":1,"after":"${cursor->Option.getOr("")}"}`,
199
+ )
200
+ (
201
+ views(flow),
202
+ views(byEvent),
203
+ views(firstPage),
204
+ firstPage->field("pageInfo")->field("hasNextPage"),
205
+ views(secondPage),
206
+ secondPage->field("pageInfo")->field("hasNextPage"),
207
+ )
208
+ }
209
+
210
+ let expected = (
211
+ ["Customers", "Orders"],
212
+ ["Customers"],
213
+ ["Customers"],
214
+ JSON.Encode.bool(true),
215
+ ["Orders"],
216
+ JSON.Encode.bool(false),
217
+ )
218
+
219
+ testPromise(
220
+ "the view changes answer an admin by flow and event, newest first, in pages",
221
+ async () => {
222
+ BackendState.setMemory()
223
+ LocalViewChangeLog.resetForTests()
224
+ expect(await readsBack())->toEqual(expected)
225
+ LocalViewChangeLog.resetForTests()
226
+ },
227
+ )
228
+
229
+ testPromise("the view changes are kept in a SQLite store alike", async () => {
230
+ let db = SqliteDriver.openDb(~path=":memory:")
231
+ BackendState.setSqlite(~db, ~path=":memory:")
232
+ LocalViewChangeLog.resetForTests()
233
+ expect(await readsBack())->toEqual(expected)
234
+ LocalViewChangeLog.resetForTests()
235
+ BackendState.setMemory()
236
+ SqliteDriver.close(db)
237
+ })
238
+
239
+ // Three commands: two in one flow, one rejected.
240
+ let keepOutcomes = () => {
241
+ let report = (
242
+ ~flow,
243
+ ~outcome,
244
+ msgId,
245
+ ): ReventlessCore.CommandTopic_Helpers.commandOutcomeReport => {
246
+ component: "Order",
247
+ reference: msgId,
248
+ correlationId: flow,
249
+ outcome,
250
+ }
251
+ let accepted = ReventlessCore.CommandTopic_Helpers.OutcomeAccepted({
252
+ entityId: Some("o-1"),
253
+ eventCount: 1,
254
+ })
255
+ LocalCommandOutcomeLog.record(report(~flow="k-1", ~outcome=accepted, "c-1"))
256
+ LocalCommandOutcomeLog.record(report(~flow="k-2", ~outcome=accepted, "c-2"))
257
+ LocalCommandOutcomeLog.record(
258
+ report(
259
+ ~flow="k-1",
260
+ ~outcome=OutcomeRejected({
261
+ errorCode: "AlreadyShipped",
262
+ errorDetail: "",
263
+ cause: ReventlessCore.CommandTopic_Helpers.DomainRejection,
264
+ }),
265
+ "c-3",
266
+ ),
267
+ )
268
+ }
269
+
270
+ let outcomesRead = async () => {
271
+ keepOutcomes()
272
+ let s = server()
273
+ let read = s.getQueryResolver(LocalPlatformObserve.commandOutcomesField)->Option.getOrThrow
274
+ let ask = async args => {
275
+ let answer = await read(JSON.Encode.null, JSON.parseOrThrow(args), admin())
276
+ answer
277
+ ->field("edges")
278
+ ->JSON.Decode.array
279
+ ->Option.getOr([])
280
+ ->Array.map(e => {
281
+ let node = e->field("node")
282
+ (
283
+ node->field("msgId")->JSON.Decode.string->Option.getOr(""),
284
+ node->field("outcome")->JSON.Decode.string->Option.getOr(""),
285
+ )
286
+ })
287
+ }
288
+ (await ask(`{"filter":{"correlationId":"k-1"}}`), await ask(`{"filter":{"msgId":"c-2"}}`))
289
+ }
290
+
291
+ let outcomesExpected = ([("c-3", "rejected"), ("c-1", "accepted")], [("c-2", "accepted")])
292
+
293
+ testPromise(
294
+ "the command outcomes answer an admin by flow and by command, newest first",
295
+ async () => {
296
+ BackendState.setMemory()
297
+ LocalCommandOutcomeLog.resetForTests()
298
+ expect(await outcomesRead())->toEqual(outcomesExpected)
299
+ LocalCommandOutcomeLog.resetForTests()
300
+ },
301
+ )
302
+
303
+ testPromise("the command outcomes are kept in a SQLite store alike", async () => {
304
+ let db = SqliteDriver.openDb(~path=":memory:")
305
+ BackendState.setSqlite(~db, ~path=":memory:")
306
+ LocalCommandOutcomeLog.resetForTests()
307
+ expect(await outcomesRead())->toEqual(outcomesExpected)
308
+ LocalCommandOutcomeLog.resetForTests()
309
+ BackendState.setMemory()
310
+ SqliteDriver.close(db)
311
+ })
312
+
313
+ testPromise("the metrics answer an admin with the counts, narrowed by plugin", async () => {
314
+ let counts = LocalMetrics.counts
315
+ ReventlessCore.Metrics.Counts.clear(counts)
316
+ let sample = (~metric, ~plugin, ~value=1): ReventlessCore.Metrics.sample => {
317
+ metric,
318
+ slice: "PlaceOrder",
319
+ plugin,
320
+ value,
321
+ unit: "Count",
322
+ at: Date.now(),
323
+ }
324
+ counts->ReventlessCore.Metrics.Counts.add(sample(~metric="AppendRetry", ~plugin="Ordering"))
325
+ counts->ReventlessCore.Metrics.Counts.add(sample(~metric="AppendRetry", ~plugin="Ordering"))
326
+ counts->ReventlessCore.Metrics.Counts.add(sample(~metric="AppendRetry", ~plugin="Catalog"))
327
+ let s = server()
328
+ let read = s.getQueryResolver(LocalPlatformObserve.metricsField)->Option.getOrThrow
329
+ let answer = await read(
330
+ JSON.Encode.null,
331
+ JSON.parseOrThrow(`{"plugins":["Ordering"]}`),
332
+ admin(),
333
+ )
334
+ let rows = answer->JSON.Decode.array->Option.getOr([])
335
+ expect(rows->Array.length)->toBe(1)
336
+ let row = rows->Array.getUnsafe(0)
337
+ expect(row->field("plugin"))->toEqual(JSON.Encode.string("Ordering"))
338
+ expect(row->field("count"))->toEqual(JSON.Encode.int(2))
339
+ ReventlessCore.Metrics.Counts.clear(counts)
340
+ })
341
+
159
342
  testSync("the SDL builds into a schema", () => {
160
343
  let s = server()
161
344
  let sdl = s.buildSdl()
@@ -7,12 +7,17 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
7
  import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
8
8
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
9
9
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
10
+ import * as Metrics$ReventlessCore from "@reventlessdev/reventless-core/src/util/Metrics.res.mjs";
10
11
  import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
11
12
  import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
12
13
  import * as BackendState$ReventlessLocal from "../../src/adapter/BackendState.res.mjs";
13
14
  import * as LocalLogFeed$ReventlessLocal from "../../src/adapter/LocalLogFeed.res.mjs";
15
+ import * as LocalMetrics$ReventlessLocal from "../../src/adapter/LocalMetrics.res.mjs";
16
+ import * as SqliteDriver$ReventlessLocal from "../../src/adapter/SqliteDriver.res.mjs";
14
17
  import * as LocalEventLogRead$ReventlessLocal from "../../src/adapter/LocalEventLogRead.res.mjs";
18
+ import * as LocalViewChangeLog$ReventlessLocal from "../../src/adapter/ViewChangeLog/LocalViewChangeLog.res.mjs";
15
19
  import * as LocalPlatformObserve$ReventlessLocal from "../../src/adapter/LocalPlatformObserve.res.mjs";
20
+ import * as LocalCommandOutcomeLog$ReventlessLocal from "../../src/adapter/CommandOutcomeLog/LocalCommandOutcomeLog.res.mjs";
16
21
  import * as LocalDcbEventLogStorage$ReventlessLocal from "../../src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs";
17
22
  import * as GraphQL_ServerInstance$ReventlessGraphqlServer from "@reventlessdev/reventless-graphql-server/src/GraphQL_ServerInstance.res.mjs";
18
23
 
@@ -135,8 +140,11 @@ globalThis.describe("the platform API's watching fields", () => {
135
140
  }
136
141
  };
137
142
  globalThis.expect(await refused(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.eventLogField))).toBe(true);
143
+ globalThis.expect(await refused(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.viewChangesField))).toBe(true);
144
+ globalThis.expect(await refused(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.commandOutcomesField))).toBe(true);
138
145
  globalThis.expect(await refused(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.logLevelsField))).toBe(true);
139
146
  globalThis.expect(await refused(s.getMutationResolver(LocalPlatformObserve$ReventlessLocal.setLogLevelField))).toBe(true);
147
+ globalThis.expect(await refused(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.metricsField))).toBe(true);
140
148
  });
141
149
  globalThis.test("set and list the log levels for an admin", async () => {
142
150
  let s = server();
@@ -157,6 +165,179 @@ globalThis.describe("the platform API's watching fields", () => {
157
165
  globalThis.expect(field(answer, "edges")).toEqual([]);
158
166
  globalThis.expect(field(field(answer, "pageInfo"), "hasNextPage")).toEqual(false);
159
167
  });
168
+ let keepChanges = () => {
169
+ let change = (view, key, flow, msgId) => ({
170
+ view: view,
171
+ key: key,
172
+ kind: "Changed",
173
+ fields: ["status"],
174
+ after: JSON.parse(`{"status":"shipped"}`),
175
+ sources: [{
176
+ msgId: msgId,
177
+ correlationId: flow
178
+ }],
179
+ correlationId: flow
180
+ });
181
+ LocalViewChangeLog$ReventlessLocal.record(change("Orders", "o-1", "k-1", "m-1"));
182
+ LocalViewChangeLog$ReventlessLocal.record(change("Customers", "c-1", "k-2", "m-2"));
183
+ LocalViewChangeLog$ReventlessLocal.record(change("Customers", "c-1", "k-1", "m-3"));
184
+ };
185
+ let readChanges = async args => {
186
+ let s = server();
187
+ let read = Stdlib_Option.getOrThrow(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.viewChangesField), undefined);
188
+ return await read(null, JSON.parse(args), context([Role$Reventless.adminGroup()]));
189
+ };
190
+ let views = answer => Stdlib_Option.getOr(Stdlib_JSON.Decode.array(field(answer, "edges")), []).map(e => Stdlib_Option.getOr(Stdlib_JSON.Decode.string(field(field(e, "node"), "view")), ""));
191
+ let readsBack = async () => {
192
+ keepChanges();
193
+ let flow = await readChanges(`{"filter":{"correlationId":"k-1"}}`);
194
+ let byEvent = await readChanges(`{"filter":{"msgId":"m-2"}}`);
195
+ let firstPage = await readChanges(`{"filter":{"correlationId":"k-1"},"first":1}`);
196
+ let cursor = Stdlib_JSON.Decode.string(field(field(firstPage, "pageInfo"), "endCursor"));
197
+ let secondPage = await readChanges(`{"filter":{"correlationId":"k-1"},"first":1,"after":"` + Stdlib_Option.getOr(cursor, "") + `"}`);
198
+ return [
199
+ views(flow),
200
+ views(byEvent),
201
+ views(firstPage),
202
+ field(field(firstPage, "pageInfo"), "hasNextPage"),
203
+ views(secondPage),
204
+ field(field(secondPage, "pageInfo"), "hasNextPage")
205
+ ];
206
+ };
207
+ let expected_0 = [
208
+ "Customers",
209
+ "Orders"
210
+ ];
211
+ let expected_1 = ["Customers"];
212
+ let expected_2 = ["Customers"];
213
+ let expected_4 = ["Orders"];
214
+ let expected = [
215
+ expected_0,
216
+ expected_1,
217
+ expected_2,
218
+ true,
219
+ expected_4,
220
+ false
221
+ ];
222
+ globalThis.test("the view changes answer an admin by flow and event, newest first, in pages", async () => {
223
+ BackendState$ReventlessLocal.setMemory();
224
+ LocalViewChangeLog$ReventlessLocal.resetForTests();
225
+ globalThis.expect(await readsBack()).toEqual(expected);
226
+ return LocalViewChangeLog$ReventlessLocal.resetForTests();
227
+ });
228
+ globalThis.test("the view changes are kept in a SQLite store alike", async () => {
229
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
230
+ BackendState$ReventlessLocal.setSqlite(db, ":memory:");
231
+ LocalViewChangeLog$ReventlessLocal.resetForTests();
232
+ globalThis.expect(await readsBack()).toEqual(expected);
233
+ LocalViewChangeLog$ReventlessLocal.resetForTests();
234
+ BackendState$ReventlessLocal.setMemory();
235
+ return SqliteDriver$ReventlessLocal.close(db);
236
+ });
237
+ let keepOutcomes = () => {
238
+ let report = (flow, outcome, msgId) => ({
239
+ component: "Order",
240
+ reference: msgId,
241
+ correlationId: flow,
242
+ outcome: outcome
243
+ });
244
+ let accepted = {
245
+ TAG: "OutcomeAccepted",
246
+ entityId: "o-1",
247
+ eventCount: 1
248
+ };
249
+ LocalCommandOutcomeLog$ReventlessLocal.record(report("k-1", accepted, "c-1"));
250
+ LocalCommandOutcomeLog$ReventlessLocal.record(report("k-2", accepted, "c-2"));
251
+ LocalCommandOutcomeLog$ReventlessLocal.record({
252
+ component: "Order",
253
+ reference: "c-3",
254
+ correlationId: "k-1",
255
+ outcome: {
256
+ TAG: "OutcomeRejected",
257
+ errorCode: "AlreadyShipped",
258
+ errorDetail: "",
259
+ cause: "DomainRejection"
260
+ }
261
+ });
262
+ };
263
+ let outcomesRead = async () => {
264
+ keepOutcomes();
265
+ let s = server();
266
+ let read = Stdlib_Option.getOrThrow(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.commandOutcomesField), undefined);
267
+ let ask = async args => {
268
+ let answer = await read(null, JSON.parse(args), context([Role$Reventless.adminGroup()]));
269
+ return Stdlib_Option.getOr(Stdlib_JSON.Decode.array(field(answer, "edges")), []).map(e => {
270
+ let node = field(e, "node");
271
+ return [
272
+ Stdlib_Option.getOr(Stdlib_JSON.Decode.string(field(node, "msgId")), ""),
273
+ Stdlib_Option.getOr(Stdlib_JSON.Decode.string(field(node, "outcome")), "")
274
+ ];
275
+ });
276
+ };
277
+ return [
278
+ await ask(`{"filter":{"correlationId":"k-1"}}`),
279
+ await ask(`{"filter":{"msgId":"c-2"}}`)
280
+ ];
281
+ };
282
+ let outcomesExpected_0 = [
283
+ [
284
+ "c-3",
285
+ "rejected"
286
+ ],
287
+ [
288
+ "c-1",
289
+ "accepted"
290
+ ]
291
+ ];
292
+ let outcomesExpected_1 = [[
293
+ "c-2",
294
+ "accepted"
295
+ ]];
296
+ let outcomesExpected = [
297
+ outcomesExpected_0,
298
+ outcomesExpected_1
299
+ ];
300
+ globalThis.test("the command outcomes answer an admin by flow and by command, newest first", async () => {
301
+ BackendState$ReventlessLocal.setMemory();
302
+ LocalCommandOutcomeLog$ReventlessLocal.resetForTests();
303
+ globalThis.expect(await outcomesRead()).toEqual(outcomesExpected);
304
+ return LocalCommandOutcomeLog$ReventlessLocal.resetForTests();
305
+ });
306
+ globalThis.test("the command outcomes are kept in a SQLite store alike", async () => {
307
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
308
+ BackendState$ReventlessLocal.setSqlite(db, ":memory:");
309
+ LocalCommandOutcomeLog$ReventlessLocal.resetForTests();
310
+ globalThis.expect(await outcomesRead()).toEqual(outcomesExpected);
311
+ LocalCommandOutcomeLog$ReventlessLocal.resetForTests();
312
+ BackendState$ReventlessLocal.setMemory();
313
+ return SqliteDriver$ReventlessLocal.close(db);
314
+ });
315
+ globalThis.test("the metrics answer an admin with the counts, narrowed by plugin", async () => {
316
+ Metrics$ReventlessCore.Counts.clear(LocalMetrics$ReventlessLocal.counts);
317
+ let sample = (metric, plugin, valueOpt) => {
318
+ let value = valueOpt !== undefined ? valueOpt : 1;
319
+ return {
320
+ metric: metric,
321
+ slice: "PlaceOrder",
322
+ plugin: plugin,
323
+ value: value,
324
+ unit: "Count",
325
+ at: Date.now()
326
+ };
327
+ };
328
+ Metrics$ReventlessCore.Counts.add(LocalMetrics$ReventlessLocal.counts, sample("AppendRetry", "Ordering", undefined));
329
+ Metrics$ReventlessCore.Counts.add(LocalMetrics$ReventlessLocal.counts, sample("AppendRetry", "Ordering", undefined));
330
+ Metrics$ReventlessCore.Counts.add(LocalMetrics$ReventlessLocal.counts, sample("AppendRetry", "Catalog", undefined));
331
+ let s = server();
332
+ let read = Stdlib_Option.getOrThrow(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.metricsField), undefined);
333
+ let answer = await read(null, JSON.parse(`{"plugins":["Ordering"]}`), context([Role$Reventless.adminGroup()]));
334
+ let rows = Stdlib_Option.getOr(Stdlib_JSON.Decode.array(answer), []);
335
+ globalThis.expect(rows.length).toBe(1);
336
+ let row = rows[0];
337
+ globalThis.expect(field(row, "plugin")).toEqual("Ordering");
338
+ globalThis.expect(field(row, "count")).toEqual(2);
339
+ return Metrics$ReventlessCore.Counts.clear(LocalMetrics$ReventlessLocal.counts);
340
+ });
160
341
  globalThis.test("the SDL builds into a schema", () => {
161
342
  let s = server();
162
343
  let sdl = s.buildSdl();
@@ -0,0 +1,46 @@
1
+ // The line the app's API writes for each GraphQL operation: how it reads, and at which level.
2
+
3
+ open JestGlobals
4
+
5
+ let report = (
6
+ ~kind="query",
7
+ ~operationName=?,
8
+ ~fields=["Catalog_Products"],
9
+ ~user=?,
10
+ ~ms=12.,
11
+ ~errors=[],
12
+ (),
13
+ ): GraphqlYoga.requestReport => {kind, ?operationName, fields, ?user, ms, errors}
14
+
15
+ let line = r => LocalRequestLog.lineOf(r)
16
+
17
+ testSync("a query is a debug line naming it, who asked and how long it took", () => {
18
+ let l = line(report(~operationName="Products", ~user="alice", ()))
19
+ expect(l.message)->toBe("query Products by alice in 12 ms")
20
+ expect(l.level == Debug)->toBe(true)
21
+ })
22
+
23
+ testSync("an operation without a name goes by its root fields", () => {
24
+ let l = line(report(~fields=["Catalog_Products", "Catalog_Categories"], ()))
25
+ expect(l.message)->toBe("query Catalog_Products, Catalog_Categories in 12 ms")
26
+ })
27
+
28
+ testSync("a mutation is an info line", () => {
29
+ let l = line(report(~kind="mutation", ~fields=["Catalog_AddProduct"], ~user="bob", ~ms=40., ()))
30
+ expect(l.message)->toBe("mutation Catalog_AddProduct by bob in 40 ms")
31
+ expect(l.level == Info)->toBe(true)
32
+ })
33
+
34
+ testSync("an operation that answered with errors is a warning with the first of them", () => {
35
+ let l = line(
36
+ report(~kind="mutation", ~fields=["Catalog_AddProduct"], ~errors=["Forbidden", "Other"], ()),
37
+ )
38
+ expect(l.message)->toBe("mutation Catalog_AddProduct failed in 12 ms: Forbidden (and 1 more)")
39
+ expect(l.level == Warn)->toBe(true)
40
+ })
41
+
42
+ testSync("a subscription is said when it starts, with no time", () => {
43
+ let l = line(report(~kind="subscription", ~fields=["onOrderChanged"], ~ms=0., ()))
44
+ expect(l.message)->toBe("subscription onOrderChanged")
45
+ expect(l.level == Debug)->toBe(true)
46
+ })
@@ -0,0 +1,61 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as LocalRequestLog$ReventlessLocal from "../../src/adapter/LocalRequestLog.res.mjs";
4
+
5
+ function report(kindOpt, operationName, fieldsOpt, user, msOpt, errorsOpt, param) {
6
+ let kind = kindOpt !== undefined ? kindOpt : "query";
7
+ let fields = fieldsOpt !== undefined ? fieldsOpt : ["Catalog_Products"];
8
+ let ms = msOpt !== undefined ? msOpt : 12;
9
+ let errors = errorsOpt !== undefined ? errorsOpt : [];
10
+ return {
11
+ kind: kind,
12
+ operationName: operationName,
13
+ fields: fields,
14
+ user: user,
15
+ ms: ms,
16
+ errors: errors
17
+ };
18
+ }
19
+
20
+ let line = LocalRequestLog$ReventlessLocal.lineOf;
21
+
22
+ globalThis.test("a query is a debug line naming it, who asked and how long it took", () => {
23
+ let l = LocalRequestLog$ReventlessLocal.lineOf(report(undefined, "Products", undefined, "alice", undefined, undefined, undefined));
24
+ globalThis.expect(l.message).toBe("query Products by alice in 12 ms");
25
+ globalThis.expect(l.level === "Debug").toBe(true);
26
+ });
27
+
28
+ globalThis.test("an operation without a name goes by its root fields", () => {
29
+ let l = LocalRequestLog$ReventlessLocal.lineOf(report(undefined, undefined, [
30
+ "Catalog_Products",
31
+ "Catalog_Categories"
32
+ ], undefined, undefined, undefined, undefined));
33
+ globalThis.expect(l.message).toBe("query Catalog_Products, Catalog_Categories in 12 ms");
34
+ });
35
+
36
+ globalThis.test("a mutation is an info line", () => {
37
+ let l = LocalRequestLog$ReventlessLocal.lineOf(report("mutation", undefined, ["Catalog_AddProduct"], "bob", 40, undefined, undefined));
38
+ globalThis.expect(l.message).toBe("mutation Catalog_AddProduct by bob in 40 ms");
39
+ globalThis.expect(l.level === "Info").toBe(true);
40
+ });
41
+
42
+ globalThis.test("an operation that answered with errors is a warning with the first of them", () => {
43
+ let l = LocalRequestLog$ReventlessLocal.lineOf(report("mutation", undefined, ["Catalog_AddProduct"], undefined, undefined, [
44
+ "Forbidden",
45
+ "Other"
46
+ ], undefined));
47
+ globalThis.expect(l.message).toBe("mutation Catalog_AddProduct failed in 12 ms: Forbidden (and 1 more)");
48
+ globalThis.expect(l.level === "Warn").toBe(true);
49
+ });
50
+
51
+ globalThis.test("a subscription is said when it starts, with no time", () => {
52
+ let l = LocalRequestLog$ReventlessLocal.lineOf(report("subscription", undefined, ["onOrderChanged"], undefined, 0, undefined, undefined));
53
+ globalThis.expect(l.message).toBe("subscription onOrderChanged");
54
+ globalThis.expect(l.level === "Debug").toBe(true);
55
+ });
56
+
57
+ export {
58
+ report,
59
+ line,
60
+ }
61
+ /* Not a pure module */