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

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 (47) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +11 -11
  3. package/src/Platform.res +16 -0
  4. package/src/Platform.res.mjs +17 -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 +33 -0
  8. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res.mjs +38 -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 +64 -0
  12. package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res.mjs +78 -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 +130 -5
  24. package/src/adapter/LocalPlatformObserve.res.mjs +145 -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/src/reexport/CommandTopic.res.mjs +6 -0
  38. package/tests/adapter/GraphQL_SchemaInspectorTest.res +3 -2
  39. package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +1 -1
  40. package/tests/adapter/LocalEvents_ServerTest.res +11 -0
  41. package/tests/adapter/LocalEvents_ServerTest.res.mjs +7 -1
  42. package/tests/adapter/LocalIdleExitTest.res +57 -0
  43. package/tests/adapter/LocalIdleExitTest.res.mjs +62 -0
  44. package/tests/adapter/LocalPlatformObserveTest.res +191 -0
  45. package/tests/adapter/LocalPlatformObserveTest.res.mjs +187 -0
  46. package/tests/adapter/LocalRequestLogTest.res +46 -0
  47. package/tests/adapter/LocalRequestLogTest.res.mjs +61 -0
@@ -1,9 +1,11 @@
1
1
  // What the platform API and the platform MCP server offer a tool that watches
2
- // this platform: every event of every log newest first (`Platform_EventLog`),
2
+ // this platform: every event of every log newest first (`Platform_EventLog`), every row
3
+ // change and command outcome kept (`Platform_ViewChanges`, `Platform_CommandOutcomes`),
3
4
  // the log level while running (`Platform_LogLevels`, `Platform_SetLogLevel`),
4
5
  // the log feed and the event tap as subscriptions (`Platform_LogAppended`,
5
- // `Platform_EventAppended`), and the MCP resources `platform/logs/recent` and
6
- // `platform/events`.
6
+ // `Platform_EventAppended`), the metrics counted since start (`Platform_Metrics`),
7
+ // and the MCP resources `platform/logs/recent`, `platform/events` and
8
+ // `platform/metrics`.
7
9
  //
8
10
  // Every field here is admin-checked, also when the platform serves one API for
9
11
  // both, where nothing else would check it.
@@ -76,6 +78,19 @@ let logLevelTypes = [
76
78
  }`,
77
79
  ]
78
80
 
81
+ let metricTypes = [
82
+ `type Platform_MetricCount {
83
+ metric: String!
84
+ plugin: String!
85
+ slice: String!
86
+ unit: String!
87
+ count: Int!
88
+ sum: Float!
89
+ lastAt: String!
90
+ perMinute: [Int!]!
91
+ }`,
92
+ ]
93
+
79
94
  let feedTypes = [
80
95
  `input Platform_FeedFilter {
81
96
  plugins: [String!]
@@ -87,12 +102,51 @@ let feedTypes = [
87
102
  ]
88
103
 
89
104
  let eventLogField = "Platform_EventLog"
105
+ let viewChangesField = "Platform_ViewChanges"
106
+ let commandOutcomesField = "Platform_CommandOutcomes"
107
+
108
+ // Every row change kept since the store began, newest first; each node is the tap's
109
+ // `viewChange` line as JSON.
110
+ let viewChangeTypes = [
111
+ `input Platform_ViewChangeFilter {
112
+ correlationId: String
113
+ msgId: String
114
+ view: String
115
+ key: String
116
+ }`,
117
+ `type Platform_ViewChangeEdge {
118
+ node: AWSJSON!
119
+ cursor: String!
120
+ }`,
121
+ `type Platform_ViewChangeConnection {
122
+ edges: [Platform_ViewChangeEdge!]!
123
+ pageInfo: Platform_EventLogPageInfo!
124
+ }`,
125
+ // Every command outcome kept, newest first; each node is the tap's `commandOutcome` line.
126
+ `input Platform_CommandOutcomeFilter {
127
+ correlationId: String
128
+ msgId: String
129
+ startsFlow: Boolean
130
+ }`,
131
+ `type Platform_CommandOutcomeEdge {
132
+ node: AWSJSON!
133
+ cursor: String!
134
+ }`,
135
+ `type Platform_CommandOutcomeConnection {
136
+ edges: [Platform_CommandOutcomeEdge!]!
137
+ pageInfo: Platform_EventLogPageInfo!
138
+ }`,
139
+ ]
90
140
  let logLevelsField = "Platform_LogLevels"
91
141
  let setLogLevelField = "Platform_SetLogLevel"
142
+ let metricsField = "Platform_Metrics"
92
143
 
93
144
  let queryFields = [
94
145
  ` ${eventLogField}(filter: Platform_EventLogFilter, first: Int, after: String): Platform_EventLogConnection!`,
146
+ ` ${viewChangesField}(filter: Platform_ViewChangeFilter, first: Int, after: String): Platform_ViewChangeConnection!`,
147
+ ` ${commandOutcomesField}(filter: Platform_CommandOutcomeFilter, first: Int, after: String): Platform_CommandOutcomeConnection!`,
95
148
  ` ${logLevelsField}: Platform_LogLevels!`,
149
+ ` ${metricsField}(plugins: [String!]): [Platform_MetricCount!]!`,
96
150
  ]
97
151
 
98
152
  let logAppendedField = "Platform_LogAppended"
@@ -145,6 +199,43 @@ let eventLog: GQL.resolverFn = async (_root, args, _ctx) => {
145
199
  pageJson(page)
146
200
  }
147
201
 
202
+ /** A page of kept lines as a connection: each node the tap's line, each cursor its place. */
203
+ let keptPageJson = (page: LocalViewChangeLog.page): JSON.t =>
204
+ Dict.fromArray([
205
+ (
206
+ "edges",
207
+ page.records
208
+ ->Array.map(({item, cursor}) =>
209
+ Dict.fromArray([("node", item), ("cursor", JSON.Encode.string(cursor))])->JSON.Encode.object
210
+ )
211
+ ->JSON.Encode.array,
212
+ ),
213
+ (
214
+ "pageInfo",
215
+ Dict.fromArray([
216
+ ("hasNextPage", JSON.Encode.bool(page.hasNextPage)),
217
+ ("endCursor", page.endCursor->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
218
+ ])->JSON.Encode.object,
219
+ ),
220
+ ])->JSON.Encode.object
221
+
222
+ let firstOf = args => arg(args, "first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
223
+ let afterOf = args => arg(args, "after")->Option.flatMap(JSON.Decode.string)
224
+
225
+ let viewChanges: GQL.resolverFn = async (_root, args, _ctx) =>
226
+ LocalViewChangeLog.read(
227
+ ~filter=ViewChangeLogEntry.filterOfJson(arg(args, "filter")),
228
+ ~first=?firstOf(args),
229
+ ~after=?afterOf(args),
230
+ )->keptPageJson
231
+
232
+ let commandOutcomes: GQL.resolverFn = async (_root, args, _ctx) =>
233
+ LocalCommandOutcomeLog.read(
234
+ ~filter=CommandOutcomeLogEntry.filterOfJson(arg(args, "filter")),
235
+ ~first=?firstOf(args),
236
+ ~after=?afterOf(args),
237
+ )->keptPageJson
238
+
148
239
  let levelOfWire = (s: string): option<Logger.level> =>
149
240
  switch s {
150
241
  | "DEBUG" => Some(Debug)
@@ -309,6 +400,15 @@ let feedField = (feed: LocalFeed.t): GQL.resolverFn =>
309
400
  resolve: payload => payload,
310
401
  })
311
402
 
403
+ /** The metrics counted since the platform started, of the given plugins or of every one:
404
+ per metric and slice, how often, the sum of their values, and the last 60 minutes. */
405
+ let metrics: GQL.resolverFn = async (_root, args, _ctx) =>
406
+ LocalMetrics.read(
407
+ ~plugins=?arg(args, "plugins")
408
+ ->Option.flatMap(JSON.Decode.array)
409
+ ->Option.map(a => a->Array.filterMap(JSON.Decode.string)),
410
+ )
411
+
312
412
  let adminOnly = (resolvers: array<(string, GQL.resolverFn)>): dict<GQL.resolverFn> =>
313
413
  resolvers
314
414
  ->Array.map(((name, resolver)) => (
@@ -320,11 +420,21 @@ let adminOnly = (resolvers: array<(string, GQL.resolverFn)>): dict<GQL.resolverF
320
420
  /** Registers the fields on the platform API. */
321
421
  let registerGraphQL = (server: GQL.t) => {
322
422
  server.registerTypes(
323
- ~sdlTypes=eventLogTypes->Array.concat(logLevelTypes)->Array.concat(feedTypes),
423
+ ~sdlTypes=eventLogTypes
424
+ ->Array.concat(viewChangeTypes)
425
+ ->Array.concat(logLevelTypes)
426
+ ->Array.concat(metricTypes)
427
+ ->Array.concat(feedTypes),
324
428
  )
325
429
  server.registerQueries(
326
430
  ~sdlFields=queryFields,
327
- ~resolvers=adminOnly([(eventLogField, eventLog), (logLevelsField, logLevels)]),
431
+ ~resolvers=adminOnly([
432
+ (eventLogField, eventLog),
433
+ (viewChangesField, viewChanges),
434
+ (commandOutcomesField, commandOutcomes),
435
+ (logLevelsField, logLevels),
436
+ (metricsField, metrics),
437
+ ]),
328
438
  )
329
439
  server.registerMutations(
330
440
  ~sdlFields=mutationFields,
@@ -343,6 +453,7 @@ let registerGraphQL = (server: GQL.t) => {
343
453
 
344
454
  let recentLogsUri = "platform/logs/recent"
345
455
  let eventsUri = "platform/events"
456
+ let metricsUri = "platform/metrics"
346
457
 
347
458
  @val external decodeURIComponent: string => string = "decodeURIComponent"
348
459
 
@@ -431,6 +542,10 @@ let events: MCP_ServerInstance.resourceHandler = async uri => {
431
542
  resourceResult(~uri, pageJson(page))
432
543
  }
433
544
 
545
+ /** `platform/metrics?plugins=a,b`: the metrics counted since the platform started. */
546
+ let metricsResource: MCP_ServerInstance.resourceHandler = async uri =>
547
+ resourceResult(~uri, LocalMetrics.read(~plugins=?listOf(queryOf(uri), "plugins")))
548
+
434
549
  /** Registers the resources on the platform MCP server. */
435
550
  let registerMcp = (server: MCP_ServerInstance.t) => {
436
551
  server.registerResource(
@@ -453,4 +568,14 @@ let registerMcp = (server: MCP_ServerInstance.t) => {
453
568
  },
454
569
  ~handler=events,
455
570
  )
571
+ server.registerResource(
572
+ ~name=metricsUri,
573
+ ~definition={
574
+ uriTemplate: metricsUri,
575
+ name: metricsUri,
576
+ description: "The metrics counted since the platform started, per metric, plugin and slice: count, sum of values, last time, and counts of the last 60 minutes. Query parameter: plugins (narrow).",
577
+ mimeType: "application/json",
578
+ },
579
+ ~handler=metricsResource,
580
+ )
456
581
  }
@@ -8,9 +8,14 @@ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.
8
8
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
9
9
  import * as LocalFeed$ReventlessLocal from "./LocalFeed.res.mjs";
10
10
  import * as LocalLogFeed$ReventlessLocal from "./LocalLogFeed.res.mjs";
11
+ import * as LocalMetrics$ReventlessLocal from "./LocalMetrics.res.mjs";
11
12
  import * as LocalEventTap$ReventlessLocal from "./LocalEventTap.res.mjs";
12
13
  import * as LocalEventLogRead$ReventlessLocal from "./LocalEventLogRead.res.mjs";
14
+ import * as LocalViewChangeLog$ReventlessLocal from "./ViewChangeLog/LocalViewChangeLog.res.mjs";
15
+ import * as ViewChangeLogEntry$ReventlessLocal from "./ViewChangeLog/ViewChangeLogEntry.res.mjs";
13
16
  import * as Auth_GraphqlContext$ReventlessLocal from "./Auth/Auth_GraphqlContext.res.mjs";
17
+ import * as CommandOutcomeLogEntry$ReventlessLocal from "./CommandOutcomeLog/CommandOutcomeLogEntry.res.mjs";
18
+ import * as LocalCommandOutcomeLog$ReventlessLocal from "./CommandOutcomeLog/LocalCommandOutcomeLog.res.mjs";
14
19
 
15
20
  let eventLogTypes = [
16
21
  "scalar AWSJSON",
@@ -75,6 +80,17 @@ let logLevelTypes = [
75
80
  }`
76
81
  ];
77
82
 
83
+ let metricTypes = [`type Platform_MetricCount {
84
+ metric: String!
85
+ plugin: String!
86
+ slice: String!
87
+ unit: String!
88
+ count: Int!
89
+ sum: Float!
90
+ lastAt: String!
91
+ perMinute: [Int!]!
92
+ }`];
93
+
78
94
  let feedTypes = [`input Platform_FeedFilter {
79
95
  plugins: [String!]
80
96
  level: Platform_LogLevel
@@ -85,13 +101,52 @@ let feedTypes = [`input Platform_FeedFilter {
85
101
 
86
102
  let eventLogField = "Platform_EventLog";
87
103
 
104
+ let viewChangesField = "Platform_ViewChanges";
105
+
106
+ let commandOutcomesField = "Platform_CommandOutcomes";
107
+
108
+ let viewChangeTypes = [
109
+ `input Platform_ViewChangeFilter {
110
+ correlationId: String
111
+ msgId: String
112
+ view: String
113
+ key: String
114
+ }`,
115
+ `type Platform_ViewChangeEdge {
116
+ node: AWSJSON!
117
+ cursor: String!
118
+ }`,
119
+ `type Platform_ViewChangeConnection {
120
+ edges: [Platform_ViewChangeEdge!]!
121
+ pageInfo: Platform_EventLogPageInfo!
122
+ }`,
123
+ `input Platform_CommandOutcomeFilter {
124
+ correlationId: String
125
+ msgId: String
126
+ startsFlow: Boolean
127
+ }`,
128
+ `type Platform_CommandOutcomeEdge {
129
+ node: AWSJSON!
130
+ cursor: String!
131
+ }`,
132
+ `type Platform_CommandOutcomeConnection {
133
+ edges: [Platform_CommandOutcomeEdge!]!
134
+ pageInfo: Platform_EventLogPageInfo!
135
+ }`
136
+ ];
137
+
88
138
  let logLevelsField = "Platform_LogLevels";
89
139
 
90
140
  let setLogLevelField = "Platform_SetLogLevel";
91
141
 
142
+ let metricsField = "Platform_Metrics";
143
+
92
144
  let queryFields = [
93
145
  ` ` + eventLogField + `(filter: Platform_EventLogFilter, first: Int, after: String): Platform_EventLogConnection!`,
94
- ` ` + logLevelsField + `: Platform_LogLevels!`
146
+ ` ` + viewChangesField + `(filter: Platform_ViewChangeFilter, first: Int, after: String): Platform_ViewChangeConnection!`,
147
+ ` ` + commandOutcomesField + `(filter: Platform_CommandOutcomeFilter, first: Int, after: String): Platform_CommandOutcomeConnection!`,
148
+ ` ` + logLevelsField + `: Platform_LogLevels!`,
149
+ ` ` + metricsField + `(plugins: [String!]): [Platform_MetricCount!]!`
95
150
  ];
96
151
 
97
152
  let logAppendedField = "Platform_LogAppended";
@@ -144,6 +199,53 @@ async function eventLog(_root, args, _ctx) {
144
199
  return pageJson(await LocalEventLogRead$ReventlessLocal.read(LocalEventLogRead$ReventlessLocal.filterOfJson(arg(args, "filter")), Stdlib_Option.map(Stdlib_Option.flatMap(arg(args, "first"), Stdlib_JSON.Decode.float), prim => prim | 0), Stdlib_Option.flatMap(arg(args, "after"), Stdlib_JSON.Decode.string)));
145
200
  }
146
201
 
202
+ function keptPageJson(page) {
203
+ return Object.fromEntries([
204
+ [
205
+ "edges",
206
+ page.records.map(param => Object.fromEntries([
207
+ [
208
+ "node",
209
+ param.item
210
+ ],
211
+ [
212
+ "cursor",
213
+ param.cursor
214
+ ]
215
+ ]))
216
+ ],
217
+ [
218
+ "pageInfo",
219
+ Object.fromEntries([
220
+ [
221
+ "hasNextPage",
222
+ page.hasNextPage
223
+ ],
224
+ [
225
+ "endCursor",
226
+ Stdlib_Option.mapOr(page.endCursor, null, prim => prim)
227
+ ]
228
+ ])
229
+ ]
230
+ ]);
231
+ }
232
+
233
+ function firstOf(args) {
234
+ return Stdlib_Option.map(Stdlib_Option.flatMap(arg(args, "first"), Stdlib_JSON.Decode.float), prim => prim | 0);
235
+ }
236
+
237
+ function afterOf(args) {
238
+ return Stdlib_Option.flatMap(arg(args, "after"), Stdlib_JSON.Decode.string);
239
+ }
240
+
241
+ async function viewChanges(_root, args, _ctx) {
242
+ return keptPageJson(LocalViewChangeLog$ReventlessLocal.read(ViewChangeLogEntry$ReventlessLocal.filterOfJson(arg(args, "filter")), firstOf(args), afterOf(args)));
243
+ }
244
+
245
+ async function commandOutcomes(_root, args, _ctx) {
246
+ return keptPageJson(LocalCommandOutcomeLog$ReventlessLocal.read(CommandOutcomeLogEntry$ReventlessLocal.filterOfJson(arg(args, "filter")), firstOf(args), afterOf(args)));
247
+ }
248
+
147
249
  function levelOfWire(s) {
148
250
  switch (s) {
149
251
  case "DEBUG" :
@@ -311,6 +413,10 @@ function feedField(feed) {
311
413
  };
312
414
  }
313
415
 
416
+ async function metrics(_root, args, _ctx) {
417
+ return LocalMetrics$ReventlessLocal.read(Stdlib_Option.map(Stdlib_Option.flatMap(arg(args, "plugins"), Stdlib_JSON.Decode.array), a => Stdlib_Array.filterMap(a, Stdlib_JSON.Decode.string)));
418
+ }
419
+
314
420
  function adminOnly(resolvers) {
315
421
  return Object.fromEntries(resolvers.map(param => [
316
422
  param[0],
@@ -319,15 +425,27 @@ function adminOnly(resolvers) {
319
425
  }
320
426
 
321
427
  function registerGraphQL(server) {
322
- server.registerTypes(eventLogTypes.concat(logLevelTypes).concat(feedTypes));
428
+ server.registerTypes(eventLogTypes.concat(viewChangeTypes).concat(logLevelTypes).concat(metricTypes).concat(feedTypes));
323
429
  server.registerQueries(queryFields, adminOnly([
324
430
  [
325
431
  eventLogField,
326
432
  eventLog
327
433
  ],
434
+ [
435
+ viewChangesField,
436
+ viewChanges
437
+ ],
438
+ [
439
+ commandOutcomesField,
440
+ commandOutcomes
441
+ ],
328
442
  [
329
443
  logLevelsField,
330
444
  logLevels
445
+ ],
446
+ [
447
+ metricsField,
448
+ metrics
331
449
  ]
332
450
  ]));
333
451
  server.registerMutations(mutationFields, adminOnly([[
@@ -350,6 +468,8 @@ let recentLogsUri = "platform/logs/recent";
350
468
 
351
469
  let eventsUri = "platform/events";
352
470
 
471
+ let metricsUri = "platform/metrics";
472
+
353
473
  function queryOf(uri) {
354
474
  let params = {};
355
475
  let qs = uri.split("?")[1];
@@ -440,6 +560,10 @@ async function events(uri) {
440
560
  return resourceResult(uri, pageJson(page));
441
561
  }
442
562
 
563
+ async function metricsResource(uri) {
564
+ return resourceResult(uri, LocalMetrics$ReventlessLocal.read(listOf(queryOf(uri), "plugins")));
565
+ }
566
+
443
567
  function registerMcp(server) {
444
568
  server.registerResource(recentLogsUri, {
445
569
  uriTemplate: recentLogsUri,
@@ -453,6 +577,12 @@ function registerMcp(server) {
453
577
  description: "Every event of every log, newest first, one page at a time. Query parameters: plugins, logs, eventTypes, entityId, tagKey, tagValue, msgId, correlationId, causationId, user, timeFrom, timeTo, first, after (the page's endCursor).",
454
578
  mimeType: "application/json"
455
579
  }, events);
580
+ server.registerResource(metricsUri, {
581
+ uriTemplate: metricsUri,
582
+ name: metricsUri,
583
+ description: "The metrics counted since the platform started, per metric, plugin and slice: count, sum of values, last time, and counts of the last 60 minutes. Query parameter: plugins (narrow).",
584
+ mimeType: "application/json"
585
+ }, metricsResource);
456
586
  }
457
587
 
458
588
  let Logger;
@@ -466,10 +596,15 @@ export {
466
596
  GQL,
467
597
  eventLogTypes,
468
598
  logLevelTypes,
599
+ metricTypes,
469
600
  feedTypes,
470
601
  eventLogField,
602
+ viewChangesField,
603
+ commandOutcomesField,
604
+ viewChangeTypes,
471
605
  logLevelsField,
472
606
  setLogLevelField,
607
+ metricsField,
473
608
  queryFields,
474
609
  logAppendedField,
475
610
  eventAppendedField,
@@ -478,6 +613,11 @@ export {
478
613
  arg,
479
614
  pageJson,
480
615
  eventLog,
616
+ keptPageJson,
617
+ firstOf,
618
+ afterOf,
619
+ viewChanges,
620
+ commandOutcomes,
481
621
  levelOfWire,
482
622
  levelsJson,
483
623
  logLevels,
@@ -490,16 +630,19 @@ export {
490
630
  lineToJson,
491
631
  feedSubscription,
492
632
  feedField,
633
+ metrics,
493
634
  adminOnly,
494
635
  registerGraphQL,
495
636
  recentLogsUri,
496
637
  eventsUri,
638
+ metricsUri,
497
639
  queryOf,
498
640
  listOf,
499
641
  intOf,
500
642
  resourceResult,
501
643
  recentLogs,
502
644
  events,
645
+ metricsResource,
503
646
  registerMcp,
504
647
  }
505
648
  /* log Not a pure module */
@@ -0,0 +1,62 @@
1
+ // One log line per GraphQL operation the app's API answers: a query or a subscription at debug, a
2
+ // mutation at info, one that answered with errors at warn, so it shows among the problems.
3
+
4
+ module YG = GraphqlYoga
5
+ module L = ReventlessCore.Logger
6
+
7
+ type line = {level: L.level, message: string, data: JSON.t}
8
+
9
+ let lineOf = (r: YG.requestReport): line => {
10
+ let name = switch (r.operationName, r.fields) {
11
+ | (Some(n), _) => n
12
+ | (None, []) => "(anonymous)"
13
+ | (None, fields) => fields->Array.join(", ")
14
+ }
15
+ let who = r.user->Option.mapOr("", u => ` by ${u}`)
16
+ let took = r.kind == "subscription" ? "" : ` in ${r.ms->Float.toString} ms`
17
+ let message = switch r.errors->Array.get(0) {
18
+ | None => `${r.kind} ${name}${who}${took}`
19
+ | Some(first) =>
20
+ let more = r.errors->Array.length - 1
21
+ `${r.kind} ${name}${who} failed${took}: ${first}${more > 0
22
+ ? ` (and ${more->Int.toString} more)`
23
+ : ""}`
24
+ }
25
+ let level: L.level = if r.errors != [] {
26
+ Warn
27
+ } else if r.kind == "mutation" {
28
+ Info
29
+ } else {
30
+ Debug
31
+ }
32
+ let data = JSON.Encode.object(
33
+ Dict.fromArray(
34
+ [
35
+ ("operation", JSON.Encode.string(r.kind)),
36
+ ("fields", JSON.Encode.array(r.fields->Array.map(JSON.Encode.string))),
37
+ ("ms", JSON.Encode.float(r.ms)),
38
+ ]
39
+ ->Array.concat(
40
+ r.operationName->Option.mapOr([], n => [("operationName", JSON.Encode.string(n))]),
41
+ )
42
+ ->Array.concat(r.user->Option.mapOr([], u => [("user", JSON.Encode.string(u))]))
43
+ ->Array.concat(
44
+ r.errors == []
45
+ ? []
46
+ : [("errors", JSON.Encode.array(r.errors->Array.map(JSON.Encode.string)))],
47
+ ),
48
+ ),
49
+ )
50
+ {level, message, data}
51
+ }
52
+
53
+ /** The plugin that writes the line under the API's component, its fields as the line's detail
54
+ rather than in its words. */
55
+ let plugin = (): YG.plugin =>
56
+ YG.requestLog(r => {
57
+ let {level, message, data} = lineOf(r)
58
+ let comp = "GraphQL:Domain"
59
+ if L.shouldLog(level, ~comp, ()) {
60
+ L.emit(~level, ~comp, ~detail=data, message)
61
+ }
62
+ })
@@ -0,0 +1,80 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as GraphqlYoga from "@reventlessdev/rescript-graphql-yoga/src/GraphqlYoga.res.mjs";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
7
+
8
+ function lineOf(r) {
9
+ let match = r.operationName;
10
+ let match$1 = r.fields;
11
+ let name = match !== undefined ? match : (
12
+ match$1.length !== 0 ? match$1.join(", ") : "(anonymous)"
13
+ );
14
+ let who = Stdlib_Option.mapOr(r.user, "", u => ` by ` + u);
15
+ let took = r.kind === "subscription" ? "" : ` in ` + r.ms.toString() + ` ms`;
16
+ let first = r.errors[0];
17
+ let message;
18
+ if (first !== undefined) {
19
+ let more = r.errors.length - 1 | 0;
20
+ message = r.kind + ` ` + name + who + ` failed` + took + `: ` + first + (
21
+ more > 0 ? ` (and ` + more.toString() + ` more)` : ""
22
+ );
23
+ } else {
24
+ message = r.kind + ` ` + name + who + took;
25
+ }
26
+ let level = Primitive_object.notequal(r.errors, []) ? "Warn" : (
27
+ r.kind === "mutation" ? "Info" : "Debug"
28
+ );
29
+ let data = Object.fromEntries([
30
+ [
31
+ "operation",
32
+ r.kind
33
+ ],
34
+ [
35
+ "fields",
36
+ r.fields.map(prim => prim)
37
+ ],
38
+ [
39
+ "ms",
40
+ r.ms
41
+ ]
42
+ ].concat(Stdlib_Option.mapOr(r.operationName, [], n => [[
43
+ "operationName",
44
+ n
45
+ ]])).concat(Stdlib_Option.mapOr(r.user, [], u => [[
46
+ "user",
47
+ u
48
+ ]])).concat(Primitive_object.equal(r.errors, []) ? [] : [[
49
+ "errors",
50
+ r.errors.map(prim => prim)
51
+ ]]));
52
+ return {
53
+ level: level,
54
+ message: message,
55
+ data: data
56
+ };
57
+ }
58
+
59
+ function plugin() {
60
+ return GraphqlYoga.requestLog(r => {
61
+ let match = lineOf(r);
62
+ let level = match.level;
63
+ let comp = "GraphQL:Domain";
64
+ if (Logger$ReventlessCore.shouldLog(level, undefined, comp, undefined)) {
65
+ return Logger$ReventlessCore.emit(level, comp, undefined, match.data, undefined, match.message);
66
+ }
67
+ });
68
+ }
69
+
70
+ let YG;
71
+
72
+ let L;
73
+
74
+ export {
75
+ YG,
76
+ L,
77
+ lineOf,
78
+ plugin,
79
+ }
80
+ /* GraphqlYoga Not a pure module */
@@ -1221,9 +1221,17 @@ module Make = (Bus: LocalBus.T) => {
1221
1221
  fieldResolvers->Dict.set(resolvedField, resolver)
1222
1222
  })
1223
1223
 
1224
- if fieldResolvers->Dict.keysToArray->Array.length > 0 {
1225
- server.registerFieldResolvers(~typeName=returnTypeName, ~resolvers=fieldResolvers)
1226
- }
1224
+ // A row's last change is read by an elevated caller only; anyone else reads null.
1225
+ let changeResolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
1226
+ root,
1227
+ _args,
1228
+ ctx,
1229
+ ) =>
1230
+ extractIdentity(ctx)->Reventless.OwnerScope.resolve->Reventless.OwnerScope.isExempt
1231
+ ? root->sourceField(ReventlessCore.RowChange.key)->Option.getOr(JSON.Encode.null)
1232
+ : JSON.Encode.null
1233
+ fieldResolvers->Dict.set(ReventlessCore.RowChange.key, changeResolver)
1234
+ server.registerFieldResolvers(~typeName=returnTypeName, ~resolvers=fieldResolvers)
1227
1235
 
1228
1236
  // -- Register all fields --------------------------------------------------
1229
1237
  let allSdl =
@@ -16,6 +16,7 @@ import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/compon
16
16
  import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
17
17
  import * as Api_Ids$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Ids.res.mjs";
18
18
  import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
19
+ import * as RowChange$ReventlessCore from "@reventlessdev/reventless-core/src/RowChange.res.mjs";
19
20
  import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
20
21
  import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
21
22
  import * as SortKey_Filter$ReventlessLocal from "./SortKey_Filter.res.mjs";
@@ -749,9 +750,15 @@ function Make(Bus) {
749
750
  };
750
751
  fieldResolvers[match.resolvedField] = resolver;
751
752
  });
752
- if (Object.keys(fieldResolvers).length !== 0) {
753
- server.registerFieldResolvers(returnTypeName, fieldResolvers);
754
- }
753
+ let changeResolver = async (root, _args, ctx) => {
754
+ if (OwnerScope$Reventless.isExempt(OwnerScope$Reventless.resolve(extractIdentity(ctx), undefined))) {
755
+ return Stdlib_Option.getOr(sourceField(root, RowChange$ReventlessCore.key), null);
756
+ } else {
757
+ return null;
758
+ }
759
+ };
760
+ fieldResolvers[RowChange$ReventlessCore.key] = changeResolver;
761
+ server.registerFieldResolvers(returnTypeName, fieldResolvers);
755
762
  let allSdl = [byIdSdl].concat(byIdsSdl).concat(refsSdl).concat(match[0]).concat(itemsSdl).concat(indexSdlFields);
756
763
  let resolvers = {};
757
764
  resolvers[singleQueryName] = byIdResolver;