@reventlessdev/reventless-core 3.0.0-alpha.185 → 3.0.0-alpha.186
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 +7 -0
- package/package.json +5 -5
- package/src/plugin/component/Plugin_Builder.res +11 -1
- package/src/plugin/component/Plugin_Builder.res.mjs +12 -2
- package/src/plugin/component/Plugin_EventQuerySchema.res +121 -0
- package/src/plugin/component/Plugin_EventQuerySchema.res.mjs +102 -0
- package/src/plugin/component/Plugin_Helpers.res +16 -0
- package/tests/plugin/Plugin_EventQuerySchemaTest.res +98 -0
- package/tests/plugin/Plugin_EventQuerySchemaTest.res.mjs +113 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.186 (2026-07-27)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **api:** event-history query — the Source A read counterpart ([9268b33](https://github.com/ReventlessDev/reventless-core/commit/9268b33a4835d4cd4bc79b38f19bc2b974853fa4))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# 3.0.0-alpha.185 (2026-07-27)
|
|
7
14
|
|
|
8
15
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.186",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -29,16 +29,16 @@
|
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.31",
|
|
32
|
-
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
|
|
33
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
34
32
|
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
|
|
33
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
34
|
+
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
|
|
35
35
|
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
37
37
|
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
|
|
38
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.106",
|
|
39
38
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
39
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
40
40
|
"@reventlessdev/reventless-spec": "3.0.0-alpha.81",
|
|
41
|
-
"@reventlessdev/reventless-
|
|
41
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.106"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -298,10 +298,15 @@ module Make = (
|
|
|
298
298
|
~mutationEntries,
|
|
299
299
|
~eventLogEntries,
|
|
300
300
|
)
|
|
301
|
+
// The historical read counterpart of the Source A subscription — same
|
|
302
|
+
// event logs, same generation point, so the two cannot disagree about
|
|
303
|
+
// which streams exist.
|
|
304
|
+
let evtResult = Plugin_EventQuerySchema.generate(~plugin=name, ~eventLogEntries)
|
|
301
305
|
let parts = GraphQL_Stitcher.decode(baseFragment)
|
|
302
306
|
GraphQL_Stitcher.encode({
|
|
303
307
|
...parts,
|
|
304
|
-
types: Array.
|
|
308
|
+
types: Array.flat([parts.types, subResult.extraTypes, evtResult.extraTypes]),
|
|
309
|
+
queries: Array.concat(parts.queries, evtResult.queryFields),
|
|
305
310
|
subscriptions: subResult.subscriptionFields,
|
|
306
311
|
subscriptionSources: subResult.subscriptionSources,
|
|
307
312
|
})
|
|
@@ -324,6 +329,11 @@ module Make = (
|
|
|
324
329
|
})
|
|
325
330
|
)
|
|
326
331
|
|
|
332
|
+
// Register the event-history query resolvers matching the SDL emitted above.
|
|
333
|
+
Spec.hooks.eventQueryResolverHook->Option.forEach(registerEventQueries =>
|
|
334
|
+
registerEventQueries({pluginName: name, eventLogEntries})
|
|
335
|
+
)
|
|
336
|
+
|
|
327
337
|
// Register DCB StateChangeSlice publish functions in publishToAggregates so that
|
|
328
338
|
// extensions whose Delegate is a DCB slice can dispatch commands to them.
|
|
329
339
|
// All slices in a plugin share the same DCB command topic (same publishJsons).
|
|
@@ -32,6 +32,7 @@ import * as ExtensionMapping$ReventlessInfra from "@reventlessdev/reventless-inf
|
|
|
32
32
|
import * as ExtensionPoint$ReventlessInterop from "@reventlessdev/reventless-interop/src/components/ExtensionPoint.res.mjs";
|
|
33
33
|
import * as Heartbeat_Builder$ReventlessCore from "../../components/Heartbeat/Heartbeat_Builder.res.mjs";
|
|
34
34
|
import * as ResourceAttribution$ReventlessCore from "../../ResourceAttribution.res.mjs";
|
|
35
|
+
import * as Plugin_EventQuerySchema$ReventlessCore from "./Plugin_EventQuerySchema.res.mjs";
|
|
35
36
|
import * as PluginExtensionPointSpec$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/PluginExtensionPointSpec.res.mjs";
|
|
36
37
|
import * as Plugin_SubscriptionSchema$ReventlessCore from "./Plugin_SubscriptionSchema.res.mjs";
|
|
37
38
|
|
|
@@ -226,11 +227,16 @@ function Make(Spec) {
|
|
|
226
227
|
});
|
|
227
228
|
let baseFragment = FragmentProvider.generateFragment(mutationEntries, queryEntries);
|
|
228
229
|
let subResult = Plugin_SubscriptionSchema$ReventlessCore.generate(mutationEntries, eventLogEntries);
|
|
230
|
+
let evtResult = Plugin_EventQuerySchema$ReventlessCore.generate(extra$1, eventLogEntries);
|
|
229
231
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(baseFragment);
|
|
230
232
|
let apiSchemaFragment = GraphQL_Stitcher$ReventlessCore.encode({
|
|
231
|
-
types:
|
|
233
|
+
types: [
|
|
234
|
+
parts.types,
|
|
235
|
+
subResult.extraTypes,
|
|
236
|
+
evtResult.extraTypes
|
|
237
|
+
].flat(),
|
|
232
238
|
mutations: parts.mutations,
|
|
233
|
-
queries: parts.queries,
|
|
239
|
+
queries: parts.queries.concat(evtResult.queryFields),
|
|
234
240
|
subscriptions: subResult.subscriptionFields,
|
|
235
241
|
subscriptionSources: subResult.subscriptionSources
|
|
236
242
|
});
|
|
@@ -245,6 +251,10 @@ function Make(Spec) {
|
|
|
245
251
|
eventLogEntries: eventLogEntries,
|
|
246
252
|
subscriptionFields: GraphQL_Stitcher$ReventlessCore.decode(apiSchemaFragment).subscriptions
|
|
247
253
|
}));
|
|
254
|
+
Stdlib_Option.forEach(Spec.hooks.eventQueryResolverHook, registerEventQueries => registerEventQueries({
|
|
255
|
+
pluginName: extra$1,
|
|
256
|
+
eventLogEntries: eventLogEntries
|
|
257
|
+
}));
|
|
248
258
|
let slicePublishJsons = dcbResult.dcbPublishJsons;
|
|
249
259
|
if (slicePublishJsons !== undefined) {
|
|
250
260
|
stateChangeSlices.forEach(Sc => {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Plugin_EventQuerySchema.res
|
|
2
|
+
// Generates the query counterpart of the Source A raw-event subscription:
|
|
3
|
+
// one paginated `…EventHistory` connection per event log, so a client can ask
|
|
4
|
+
// "what happened to this entity, in order, with who and when".
|
|
5
|
+
//
|
|
6
|
+
// Called by Plugin_Builder alongside Plugin_SubscriptionSchema, from the same
|
|
7
|
+
// `eventLogEntries` array — so the query and the subscription can never drift
|
|
8
|
+
// about which logs exist.
|
|
9
|
+
//
|
|
10
|
+
// The subscription (Plugin_SubscriptionSchema.sourceAFieldsAndTypes) answers
|
|
11
|
+
// "something was just appended" and deliberately carries no envelope metadata.
|
|
12
|
+
// This answers a historical question, so it carries the audit-relevant subset
|
|
13
|
+
// of `Message.meta`: who (`user`), when (`time`), and the correlation /
|
|
14
|
+
// causation chain. Transport and tracing fields (`ip`, `traceparent`,
|
|
15
|
+
// `schemaVersion`) and the `headers` context bag are NOT exposed — `headers`
|
|
16
|
+
// in particular carries cross-cutting context (tenant ids, feature flags) that
|
|
17
|
+
// should not leave the server.
|
|
18
|
+
|
|
19
|
+
open ReventlessInfra.Api
|
|
20
|
+
|
|
21
|
+
// Shared supporting types, emitted once per fragment. The stitcher dedupes by
|
|
22
|
+
// type name across fragments, so concatenating plugin + admin fragments stays
|
|
23
|
+
// safe — the same contract `commandResultSdlTypes` relies on.
|
|
24
|
+
let sharedSdlTypes: array<string> = [
|
|
25
|
+
`type EventMeta {
|
|
26
|
+
service: String!
|
|
27
|
+
time: String!
|
|
28
|
+
user: String
|
|
29
|
+
msgId: String!
|
|
30
|
+
correlationId: String!
|
|
31
|
+
causationId: String
|
|
32
|
+
}`,
|
|
33
|
+
`type EventTag {
|
|
34
|
+
key: String!
|
|
35
|
+
value: String!
|
|
36
|
+
}`,
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
let recordTypeName = (~plugin: string, ~displayName: string) =>
|
|
40
|
+
`${plugin}_${displayName}EventRecord`
|
|
41
|
+
|
|
42
|
+
let historyFieldName = (~plugin: string, ~displayName: string) =>
|
|
43
|
+
`${plugin}_${displayName}EventHistory`
|
|
44
|
+
|
|
45
|
+
let filterTypeName = (~plugin: string, ~displayName: string) =>
|
|
46
|
+
`${plugin}_${displayName}EventHistoryFilter`
|
|
47
|
+
|
|
48
|
+
let recordSdl = (~typeName: string): string =>
|
|
49
|
+
`type ${typeName} {
|
|
50
|
+
position: String!
|
|
51
|
+
eventType: String!
|
|
52
|
+
payload: AWSJSON!
|
|
53
|
+
tags: [EventTag!]!
|
|
54
|
+
meta: EventMeta!
|
|
55
|
+
recordedAt: String!
|
|
56
|
+
}`
|
|
57
|
+
|
|
58
|
+
let connectionSdlTypes = (~typeName: string): array<string> => [
|
|
59
|
+
`type ${typeName}Edge {\n node: ${typeName}!\n cursor: String!\n}`,
|
|
60
|
+
`type ${typeName}Connection {\n edges: [${typeName}Edge!]!\n pageInfo: PageInfo!\n}`,
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
// `entityId` is the ergonomic filter and the audit-trail case: for an
|
|
64
|
+
// aggregate log it matches the aggregate id, for a DCB log it matches ANY tag
|
|
65
|
+
// value (the same semantics the MCP event-history handler uses).
|
|
66
|
+
// `tagKey` + `tagValue` is the precise form, for a DCB event carrying several
|
|
67
|
+
// ids where you mean one specific one.
|
|
68
|
+
let filterSdl = (~typeName: string): string =>
|
|
69
|
+
`input ${typeName} {
|
|
70
|
+
entityId: ID
|
|
71
|
+
tagKey: String
|
|
72
|
+
tagValue: String
|
|
73
|
+
eventTypes: [String!]
|
|
74
|
+
user: String
|
|
75
|
+
timeFrom: String
|
|
76
|
+
timeTo: String
|
|
77
|
+
}`
|
|
78
|
+
|
|
79
|
+
type result = {
|
|
80
|
+
queryFields: array<string>,
|
|
81
|
+
extraTypes: array<string>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
Emit the event-history query field + supporting types for every distinct event
|
|
86
|
+
log in `eventLogEntries`.
|
|
87
|
+
|
|
88
|
+
Deduped by `displayName` exactly as the Source A subscription is: an aggregate
|
|
89
|
+
and a DCB log that project the same display name describe one logical stream.
|
|
90
|
+
*/
|
|
91
|
+
let generate = (~plugin: string, ~eventLogEntries: array<eventLogSchemaEntry>): result => {
|
|
92
|
+
let seen: Set.t<string> = Set.make()
|
|
93
|
+
let queryFields: array<string> = []
|
|
94
|
+
let extraTypes: array<string> = []
|
|
95
|
+
|
|
96
|
+
eventLogEntries->Array.forEach(entry => {
|
|
97
|
+
let displayName = entry.displayName
|
|
98
|
+
if !(seen->Set.has(displayName)) {
|
|
99
|
+
seen->Set.add(displayName)
|
|
100
|
+
let typeName = recordTypeName(~plugin, ~displayName)
|
|
101
|
+
let filterName = filterTypeName(~plugin, ~displayName)
|
|
102
|
+
let fieldName = historyFieldName(~plugin, ~displayName)
|
|
103
|
+
|
|
104
|
+
extraTypes->Array.push(recordSdl(~typeName))
|
|
105
|
+
connectionSdlTypes(~typeName)->Array.forEach(t => extraTypes->Array.push(t))
|
|
106
|
+
extraTypes->Array.push(filterSdl(~typeName=filterName))
|
|
107
|
+
|
|
108
|
+
queryFields->Array.push(
|
|
109
|
+
` ${fieldName}(filter: ${filterName}, first: Int, after: String, last: Int, before: String): ${typeName}Connection!`,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Only pull in the shared types when this fragment actually emits a record
|
|
115
|
+
// type that references them — a plugin with no event logs stays untouched.
|
|
116
|
+
if queryFields->Array.length > 0 {
|
|
117
|
+
sharedSdlTypes->Array.forEach(t => extraTypes->Array.push(t))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
{queryFields, extraTypes}
|
|
121
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
let sharedSdlTypes = [
|
|
5
|
+
`type EventMeta {
|
|
6
|
+
service: String!
|
|
7
|
+
time: String!
|
|
8
|
+
user: String
|
|
9
|
+
msgId: String!
|
|
10
|
+
correlationId: String!
|
|
11
|
+
causationId: String
|
|
12
|
+
}`,
|
|
13
|
+
`type EventTag {
|
|
14
|
+
key: String!
|
|
15
|
+
value: String!
|
|
16
|
+
}`
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function recordTypeName(plugin, displayName) {
|
|
20
|
+
return plugin + `_` + displayName + `EventRecord`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function historyFieldName(plugin, displayName) {
|
|
24
|
+
return plugin + `_` + displayName + `EventHistory`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function filterTypeName(plugin, displayName) {
|
|
28
|
+
return plugin + `_` + displayName + `EventHistoryFilter`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function recordSdl(typeName) {
|
|
32
|
+
return `type ` + typeName + ` {
|
|
33
|
+
position: String!
|
|
34
|
+
eventType: String!
|
|
35
|
+
payload: AWSJSON!
|
|
36
|
+
tags: [EventTag!]!
|
|
37
|
+
meta: EventMeta!
|
|
38
|
+
recordedAt: String!
|
|
39
|
+
}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function connectionSdlTypes(typeName) {
|
|
43
|
+
return [
|
|
44
|
+
`type ` + typeName + `Edge {\n node: ` + typeName + `!\n cursor: String!\n}`,
|
|
45
|
+
`type ` + typeName + `Connection {\n edges: [` + typeName + `Edge!]!\n pageInfo: PageInfo!\n}`
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function filterSdl(typeName) {
|
|
50
|
+
return `input ` + typeName + ` {
|
|
51
|
+
entityId: ID
|
|
52
|
+
tagKey: String
|
|
53
|
+
tagValue: String
|
|
54
|
+
eventTypes: [String!]
|
|
55
|
+
user: String
|
|
56
|
+
timeFrom: String
|
|
57
|
+
timeTo: String
|
|
58
|
+
}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function generate(plugin, eventLogEntries) {
|
|
62
|
+
let seen = new Set();
|
|
63
|
+
let queryFields = [];
|
|
64
|
+
let extraTypes = [];
|
|
65
|
+
eventLogEntries.forEach(entry => {
|
|
66
|
+
let displayName = entry.displayName;
|
|
67
|
+
if (seen.has(displayName)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
seen.add(displayName);
|
|
71
|
+
let typeName = recordTypeName(plugin, displayName);
|
|
72
|
+
let filterName = filterTypeName(plugin, displayName);
|
|
73
|
+
let fieldName = historyFieldName(plugin, displayName);
|
|
74
|
+
extraTypes.push(recordSdl(typeName));
|
|
75
|
+
connectionSdlTypes(typeName).forEach(t => {
|
|
76
|
+
extraTypes.push(t);
|
|
77
|
+
});
|
|
78
|
+
extraTypes.push(filterSdl(filterName));
|
|
79
|
+
queryFields.push(` ` + fieldName + `(filter: ` + filterName + `, first: Int, after: String, last: Int, before: String): ` + typeName + `Connection!`);
|
|
80
|
+
});
|
|
81
|
+
if (queryFields.length !== 0) {
|
|
82
|
+
sharedSdlTypes.forEach(t => {
|
|
83
|
+
extraTypes.push(t);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
queryFields: queryFields,
|
|
88
|
+
extraTypes: extraTypes
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export {
|
|
93
|
+
sharedSdlTypes,
|
|
94
|
+
recordTypeName,
|
|
95
|
+
historyFieldName,
|
|
96
|
+
filterTypeName,
|
|
97
|
+
recordSdl,
|
|
98
|
+
connectionSdlTypes,
|
|
99
|
+
filterSdl,
|
|
100
|
+
generate,
|
|
101
|
+
}
|
|
102
|
+
/* No side effect */
|
|
@@ -892,6 +892,19 @@ type mcpRegistrationParams = {
|
|
|
892
892
|
subscriptionFields: array<string>,
|
|
893
893
|
}
|
|
894
894
|
|
|
895
|
+
// ---------------------------------------------------------------------------
|
|
896
|
+
// Event-history query registration params. The SDL for these fields is emitted
|
|
897
|
+
// by Plugin_EventQuerySchema from this same `eventLogEntries` array; this hook
|
|
898
|
+
// hands a platform what it needs to register the matching resolvers. Kept apart
|
|
899
|
+
// from mcpRegistrationParams (which carries the same two fields) because the
|
|
900
|
+
// two consumers are unrelated: one registers GraphQL resolvers, the other MCP
|
|
901
|
+
// resources, and either can exist without the other.
|
|
902
|
+
// ---------------------------------------------------------------------------
|
|
903
|
+
type eventQueryRegistrationParams = {
|
|
904
|
+
pluginName: string,
|
|
905
|
+
eventLogEntries: array<ReventlessInfra.Api.eventLogSchemaEntry>,
|
|
906
|
+
}
|
|
907
|
+
|
|
895
908
|
// Subscription infrastructure hook params — fired after allQueryDbs and allEventTopics
|
|
896
909
|
// are assembled inside builderOutputs. Lets the AWS platform wire StateTopic and
|
|
897
910
|
// EventLogSubscription Lambdas without touching reventless-core.
|
|
@@ -943,6 +956,9 @@ type platformHooks = {
|
|
|
943
956
|
subscriptionInfraHook?: subscriptionInfraParams => unit,
|
|
944
957
|
// MCP tools and resources.
|
|
945
958
|
mcpSchemaRegistrationHook?: mcpRegistrationParams => unit,
|
|
959
|
+
// Event-history query resolvers — the read counterpart of the Source A
|
|
960
|
+
// subscription (see Plugin_EventQuerySchema).
|
|
961
|
+
eventQueryResolverHook?: eventQueryRegistrationParams => unit,
|
|
946
962
|
// ── AppSync resolver creation (AWS) ───────────────────────────────────
|
|
947
963
|
preResolversSchemaHook?: (~name: string, ~version: string, Reventless.Plugin.apiSchemaFragment) => Pulumi.Output.t<unit>,
|
|
948
964
|
// Admin equivalent of preResolversSchemaHook. Set by AWS platforms to push
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// The event-history query is the read counterpart of the Source A raw-event
|
|
4
|
+
// subscription. These tests pin the two properties that make it safe to expose:
|
|
5
|
+
// it is deduped per event log exactly as the subscription is, and it leaks none
|
|
6
|
+
// of the transport / tenant-context fields of `Message.meta`.
|
|
7
|
+
|
|
8
|
+
module SomeEvent = {
|
|
9
|
+
@schema
|
|
10
|
+
type event = Happened({thingId: string})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let entry = (~displayName): ReventlessInfra.Api.eventLogSchemaEntry => {
|
|
14
|
+
busKey: displayName ++ "AggrEventLog",
|
|
15
|
+
displayName,
|
|
16
|
+
eventSchema: SomeEvent.eventSchema->S.castToUnknown,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let allTypes = (r: Plugin_EventQuerySchema.result) => r.extraTypes->Array.join("\n")
|
|
20
|
+
|
|
21
|
+
describe("Plugin_EventQuerySchema.generate", () => {
|
|
22
|
+
testSync("emits one history query per event log, plugin-prefixed", () => {
|
|
23
|
+
let r = Plugin_EventQuerySchema.generate(
|
|
24
|
+
~plugin="Ordering",
|
|
25
|
+
~eventLogEntries=[entry(~displayName="Order"), entry(~displayName="Shipment")],
|
|
26
|
+
)
|
|
27
|
+
expect(r.queryFields->Array.length)->toBe(2)
|
|
28
|
+
let joined = r.queryFields->Array.join("\n")
|
|
29
|
+
expect(joined->String.includes("Ordering_OrderEventHistory("))->toBe(true)
|
|
30
|
+
expect(joined->String.includes("Ordering_ShipmentEventHistory("))->toBe(true)
|
|
31
|
+
// Relay connection args + return type, identical in shape to every other
|
|
32
|
+
// list query — existing client pagination applies unchanged.
|
|
33
|
+
expect(joined->String.includes("first: Int, after: String, last: Int, before: String"))->toBe(
|
|
34
|
+
true,
|
|
35
|
+
)
|
|
36
|
+
expect(joined->String.includes(": Ordering_OrderEventRecordConnection!"))->toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
testSync("dedupes by display name — one logical stream, one field", () => {
|
|
40
|
+
let r = Plugin_EventQuerySchema.generate(
|
|
41
|
+
~plugin="Ordering",
|
|
42
|
+
~eventLogEntries=[entry(~displayName="Order"), entry(~displayName="Order")],
|
|
43
|
+
)
|
|
44
|
+
expect(r.queryFields->Array.length)->toBe(1)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
testSync("record carries the audit-relevant meta subset and nothing else", () => {
|
|
48
|
+
let sdl = allTypes(
|
|
49
|
+
Plugin_EventQuerySchema.generate(
|
|
50
|
+
~plugin="Ordering",
|
|
51
|
+
~eventLogEntries=[entry(~displayName="Order")],
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
expect(sdl->String.includes("type EventMeta {"))->toBe(true)
|
|
55
|
+
["user", "time", "service", "msgId", "correlationId", "causationId"]->Array.forEach(f =>
|
|
56
|
+
expect(sdl->String.includes(f))->toBe(true)
|
|
57
|
+
)
|
|
58
|
+
// Transport / tracing detail and the cross-cutting context bag must not
|
|
59
|
+
// leave the server — see the module comment.
|
|
60
|
+
["ip:", "traceparent", "schemaVersion", "headers"]->Array.forEach(f =>
|
|
61
|
+
expect(sdl->String.includes(f))->toBe(false)
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
testSync("record exposes position, payload, tags and both timestamps", () => {
|
|
66
|
+
let sdl = allTypes(
|
|
67
|
+
Plugin_EventQuerySchema.generate(
|
|
68
|
+
~plugin="Ordering",
|
|
69
|
+
~eventLogEntries=[entry(~displayName="Order")],
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
expect(sdl->String.includes("type Ordering_OrderEventRecord {"))->toBe(true)
|
|
73
|
+
["position: String!", "eventType: String!", "payload: AWSJSON!", "tags: [EventTag!]!", "meta: EventMeta!", "recordedAt: String!"]->Array.forEach(
|
|
74
|
+
f => expect(sdl->String.includes(f))->toBe(true),
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
testSync("filter offers the entity shortcut and the precise tag pair", () => {
|
|
79
|
+
let sdl = allTypes(
|
|
80
|
+
Plugin_EventQuerySchema.generate(
|
|
81
|
+
~plugin="Ordering",
|
|
82
|
+
~eventLogEntries=[entry(~displayName="Order")],
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
expect(sdl->String.includes("input Ordering_OrderEventHistoryFilter {"))->toBe(true)
|
|
86
|
+
["entityId: ID", "tagKey: String", "tagValue: String", "eventTypes: [String!]", "user: String", "timeFrom: String", "timeTo: String"]->Array.forEach(
|
|
87
|
+
f => expect(sdl->String.includes(f))->toBe(true),
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
testSync("a plugin with no event logs emits nothing at all", () => {
|
|
92
|
+
let r = Plugin_EventQuerySchema.generate(~plugin="Ordering", ~eventLogEntries=[])
|
|
93
|
+
expect(r.queryFields->Array.length)->toBe(0)
|
|
94
|
+
// Not even the shared types — an empty fragment stays byte-identical to
|
|
95
|
+
// what it was before this generator existed.
|
|
96
|
+
expect(r.extraTypes->Array.length)->toBe(0)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Plugin_EventQuerySchema$ReventlessCore from "../../src/plugin/component/Plugin_EventQuerySchema.res.mjs";
|
|
5
|
+
|
|
6
|
+
let eventSchema = S.schema(s => ({
|
|
7
|
+
TAG: "Happened",
|
|
8
|
+
thingId: s.m(S.string)
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
let SomeEvent = {
|
|
12
|
+
eventSchema: eventSchema
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function entry(displayName) {
|
|
16
|
+
return {
|
|
17
|
+
busKey: displayName + "AggrEventLog",
|
|
18
|
+
displayName: displayName,
|
|
19
|
+
eventSchema: eventSchema
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function allTypes(r) {
|
|
24
|
+
return r.extraTypes.join("\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
globalThis.describe("Plugin_EventQuerySchema.generate", () => {
|
|
28
|
+
globalThis.test("emits one history query per event log, plugin-prefixed", () => {
|
|
29
|
+
let r = Plugin_EventQuerySchema$ReventlessCore.generate("Ordering", [
|
|
30
|
+
entry("Order"),
|
|
31
|
+
entry("Shipment")
|
|
32
|
+
]);
|
|
33
|
+
globalThis.expect(r.queryFields.length).toBe(2);
|
|
34
|
+
let joined = r.queryFields.join("\n");
|
|
35
|
+
globalThis.expect(joined.includes("Ordering_OrderEventHistory(")).toBe(true);
|
|
36
|
+
globalThis.expect(joined.includes("Ordering_ShipmentEventHistory(")).toBe(true);
|
|
37
|
+
globalThis.expect(joined.includes("first: Int, after: String, last: Int, before: String")).toBe(true);
|
|
38
|
+
globalThis.expect(joined.includes(": Ordering_OrderEventRecordConnection!")).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
globalThis.test("dedupes by display name — one logical stream, one field", () => {
|
|
41
|
+
let r = Plugin_EventQuerySchema$ReventlessCore.generate("Ordering", [
|
|
42
|
+
entry("Order"),
|
|
43
|
+
entry("Order")
|
|
44
|
+
]);
|
|
45
|
+
globalThis.expect(r.queryFields.length).toBe(1);
|
|
46
|
+
});
|
|
47
|
+
globalThis.test("record carries the audit-relevant meta subset and nothing else", () => {
|
|
48
|
+
let r = Plugin_EventQuerySchema$ReventlessCore.generate("Ordering", [entry("Order")]);
|
|
49
|
+
let sdl = r.extraTypes.join("\n");
|
|
50
|
+
globalThis.expect(sdl.includes("type EventMeta {")).toBe(true);
|
|
51
|
+
[
|
|
52
|
+
"user",
|
|
53
|
+
"time",
|
|
54
|
+
"service",
|
|
55
|
+
"msgId",
|
|
56
|
+
"correlationId",
|
|
57
|
+
"causationId"
|
|
58
|
+
].forEach(f => {
|
|
59
|
+
globalThis.expect(sdl.includes(f)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
[
|
|
62
|
+
"ip:",
|
|
63
|
+
"traceparent",
|
|
64
|
+
"schemaVersion",
|
|
65
|
+
"headers"
|
|
66
|
+
].forEach(f => {
|
|
67
|
+
globalThis.expect(sdl.includes(f)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
globalThis.test("record exposes position, payload, tags and both timestamps", () => {
|
|
71
|
+
let r = Plugin_EventQuerySchema$ReventlessCore.generate("Ordering", [entry("Order")]);
|
|
72
|
+
let sdl = r.extraTypes.join("\n");
|
|
73
|
+
globalThis.expect(sdl.includes("type Ordering_OrderEventRecord {")).toBe(true);
|
|
74
|
+
[
|
|
75
|
+
"position: String!",
|
|
76
|
+
"eventType: String!",
|
|
77
|
+
"payload: AWSJSON!",
|
|
78
|
+
"tags: [EventTag!]!",
|
|
79
|
+
"meta: EventMeta!",
|
|
80
|
+
"recordedAt: String!"
|
|
81
|
+
].forEach(f => {
|
|
82
|
+
globalThis.expect(sdl.includes(f)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
globalThis.test("filter offers the entity shortcut and the precise tag pair", () => {
|
|
86
|
+
let r = Plugin_EventQuerySchema$ReventlessCore.generate("Ordering", [entry("Order")]);
|
|
87
|
+
let sdl = r.extraTypes.join("\n");
|
|
88
|
+
globalThis.expect(sdl.includes("input Ordering_OrderEventHistoryFilter {")).toBe(true);
|
|
89
|
+
[
|
|
90
|
+
"entityId: ID",
|
|
91
|
+
"tagKey: String",
|
|
92
|
+
"tagValue: String",
|
|
93
|
+
"eventTypes: [String!]",
|
|
94
|
+
"user: String",
|
|
95
|
+
"timeFrom: String",
|
|
96
|
+
"timeTo: String"
|
|
97
|
+
].forEach(f => {
|
|
98
|
+
globalThis.expect(sdl.includes(f)).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
globalThis.test("a plugin with no event logs emits nothing at all", () => {
|
|
102
|
+
let r = Plugin_EventQuerySchema$ReventlessCore.generate("Ordering", []);
|
|
103
|
+
globalThis.expect(r.queryFields.length).toBe(0);
|
|
104
|
+
globalThis.expect(r.extraTypes.length).toBe(0);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
SomeEvent,
|
|
110
|
+
entry,
|
|
111
|
+
allTypes,
|
|
112
|
+
}
|
|
113
|
+
/* eventSchema Not a pure module */
|