@reventlessdev/reventless-core 3.0.0-alpha.184 → 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 CHANGED
@@ -3,6 +3,20 @@
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
+
13
+ # 3.0.0-alpha.185 (2026-07-27)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **api:** don't inject id: ID! into multi-variant DCB slice mutations ([03cfaad](https://github.com/ReventlessDev/reventless-core/commit/03cfaadce3bee3d7010835edc10f57ab45ddfa5b))
18
+
19
+
6
20
  # 3.0.0-alpha.184 (2026-07-27)
7
21
 
8
22
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.184",
3
+ "version": "3.0.0-alpha.186",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -30,15 +30,15 @@
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.31",
32
32
  "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
33
- "@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
34
33
  "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
35
34
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
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
38
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
39
- "@reventlessdev/reventless-infra": "3.0.0-alpha.105",
40
39
  "@reventlessdev/reventless-interop": "3.0.0-alpha.29",
41
- "@reventlessdev/reventless-spec": "3.0.0-alpha.81"
40
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.81",
41
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.106"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
@@ -396,6 +396,12 @@ let generate = (
396
396
 
397
397
  mutationEntries->Array.forEach(entry => {
398
398
  let schema = entry.commandSchema
399
+ // Aggregates carry the instance id as a separate `id: ID!` argument ahead of
400
+ // the command payload; DCB slices don't (their key field is part of the
401
+ // payload). A multi-variant slice command is a sury `Union` just like an
402
+ // aggregate command, so the injection must key off the entry flag, not the
403
+ // schema shape — see `mutationSchemaEntry.injectIdArg`.
404
+ let injectIdArg = entry.injectIdArg->Option.getOr(true)
399
405
  switch schema {
400
406
  | Union({anyOf}) =>
401
407
  // Map each fieldName to its variant in anyOf by matching the constructor
@@ -433,7 +439,9 @@ let generate = (
433
439
  variantSchema,
434
440
  ) {
435
441
  | Some(field) =>
436
- let withId = if field->String.includes("(") {
442
+ let withId = if !injectIdArg {
443
+ field
444
+ } else if field->String.includes("(") {
437
445
  field->String.replace(`${fieldName}(`, `${fieldName}(id: ID!, `)
438
446
  } else {
439
447
  field->String.replace(`${fieldName}:`, `${fieldName}(id: ID!):`)
@@ -442,7 +450,10 @@ let generate = (
442
450
  | None =>
443
451
  // Payload-less variant (S.literal("Ctor")) — no Object fields to
444
452
  // derive args from, but still a valid mutation.
445
- mutations->Array.push(` ${fieldName}(id: ID!): CommandResult!`)
453
+ let field = injectIdArg
454
+ ? ` ${fieldName}(id: ID!): CommandResult!`
455
+ : ` ${fieldName}: CommandResult!`
456
+ mutations->Array.push(field)
446
457
  }
447
458
  })
448
459
  | Object(_) =>
@@ -319,6 +319,7 @@ function generate(mutationEntries, queryEntries) {
319
319
  let constructorNameOf = fieldName => Stdlib_Option.getOr(fieldName.split("_")[fieldName.split("_").length - 1 | 0], fieldName);
320
320
  mutationEntries.forEach(entry => {
321
321
  let schema = entry.commandSchema;
322
+ let injectIdArg = Stdlib_Option.getOr(entry.injectIdArg, true);
322
323
  switch (schema.type) {
323
324
  case "object" :
324
325
  let fieldName = Stdlib_Option.getOr(entry.fieldNames[0], "");
@@ -361,11 +362,14 @@ function generate(mutationEntries, queryEntries) {
361
362
  let variantSchema = variantIndex >= 0 ? Stdlib_Option.getOr(anyOf[variantIndex], schema) : schema;
362
363
  let field = deriveMutationFieldFromObject(fieldName, types, seenTypes, variantSchema);
363
364
  if (field !== undefined) {
364
- let withId = field.includes("(") ? field.replace(fieldName + `(`, fieldName + `(id: ID!, `) : field.replace(fieldName + `:`, fieldName + `(id: ID!):`);
365
+ let withId = injectIdArg ? (
366
+ field.includes("(") ? field.replace(fieldName + `(`, fieldName + `(id: ID!, `) : field.replace(fieldName + `:`, fieldName + `(id: ID!):`)
367
+ ) : field;
365
368
  mutations.push(withId);
366
369
  return;
367
370
  }
368
- mutations.push(` ` + fieldName + `(id: ID!): CommandResult!`);
371
+ let field$1 = injectIdArg ? ` ` + fieldName + `(id: ID!): CommandResult!` : ` ` + fieldName + `: CommandResult!`;
372
+ mutations.push(field$1);
369
373
  });
370
374
  return;
371
375
  default:
@@ -914,6 +914,10 @@ module Make = (
914
914
  ReventlessInfra.Api.fieldNames: fieldSpecs->Array.map(((f, _)) => f),
915
915
  commandSchema,
916
916
  fieldPermissions,
917
+ // Slice key fields (e.g. `orderId`) are payload args; there is no
918
+ // separate aggregate `id`. A multi-variant slice command is a sury
919
+ // `Union`, so without this the generator would inject `id: ID!`.
920
+ injectIdArg: false,
917
921
  systemCallable: systemCallableComponents->Array.includes(S.Spec.name),
918
922
  linkedViews: ?sliceDef->Option.map(d => d.linkedViews),
919
923
  consistencyRead: ?sliceDef->Option.flatMap(d => d.consistencyRead),
@@ -938,6 +942,7 @@ module Make = (
938
942
  ReventlessInfra.Api.fieldNames: [fieldName],
939
943
  commandSchema: ITS.Spec.externalInputSchema->S.castToUnknown,
940
944
  fieldPermissions,
945
+ injectIdArg: false,
941
946
  }
942
947
  })
943
948
 
@@ -484,6 +484,7 @@ function Make(DcbEventLogStorage) {
484
484
  commandSchema: commandSchema,
485
485
  fieldPermissions: fieldPermissions,
486
486
  systemCallable: systemCallableComponents.includes(S.Spec.name),
487
+ injectIdArg: false,
487
488
  linkedViews: Stdlib_Option.map(sliceDef, d => d.linkedViews),
488
489
  consistencyRead: Stdlib_Option.flatMap(sliceDef, d => d.consistencyRead)
489
490
  };
@@ -498,7 +499,8 @@ function Make(DcbEventLogStorage) {
498
499
  return {
499
500
  fieldNames: [fieldName],
500
501
  commandSchema: ITS.Spec.externalInputSchema,
501
- fieldPermissions: fieldPermissions
502
+ fieldPermissions: fieldPermissions,
503
+ injectIdArg: false
502
504
  };
503
505
  });
504
506
  let stateViewEntries = stateViewSlices.map(V => {
@@ -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.concat(parts.types, subResult.extraTypes),
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: parts.types.concat(subResult.extraTypes),
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,49 @@
1
+ open JestGlobals
2
+
3
+ // A multi-variant command union serializes as a sury `Union`, the same shape an
4
+ // aggregate command produces. The generator must decide whether to inject the
5
+ // aggregate-style `id: ID!` argument from the entry's `injectIdArg` flag, not
6
+ // from the schema shape — otherwise a multi-variant DCB slice (e.g. CancelOrder
7
+ // + @noApi ReopenOrder) wrongly inherits a spurious `id: ID!` and callers that
8
+ // send only the slice's own key field get `MissingFieldArgument: id`.
9
+ module OrderSliceCmd = {
10
+ @schema
11
+ type command =
12
+ | CancelOrder({orderId: string})
13
+ | ReopenOrder({orderId: string})
14
+ }
15
+
16
+ let mutationFor = (fragment, needle) =>
17
+ GraphQL_Stitcher.decode(fragment).mutations->Array.find(m => m->String.includes(needle))
18
+
19
+ describe("GraphQL_FragmentGenerator.generate mutation id injection", () => {
20
+ testSync("multi-variant slice entry (injectIdArg: false) omits the id argument", () => {
21
+ let fragment = GraphQL_FragmentGenerator.generate(
22
+ ~mutationEntries=[
23
+ {
24
+ ReventlessInfra.Api.fieldNames: ["Ordering_CancelOrder"],
25
+ commandSchema: OrderSliceCmd.commandSchema->S.castToUnknown,
26
+ injectIdArg: false,
27
+ },
28
+ ],
29
+ ~queryEntries=[],
30
+ )
31
+ let field = mutationFor(fragment, "Ordering_CancelOrder")->Option.getOr("")
32
+ expect(field->String.includes("orderId"))->toBe(true)
33
+ expect(field->String.includes("id: ID!"))->toBe(false)
34
+ })
35
+
36
+ testSync("aggregate-style entry (default) injects id: ID! ahead of the payload", () => {
37
+ let fragment = GraphQL_FragmentGenerator.generate(
38
+ ~mutationEntries=[
39
+ {
40
+ ReventlessInfra.Api.fieldNames: ["Ordering_CancelOrder"],
41
+ commandSchema: OrderSliceCmd.commandSchema->S.castToUnknown,
42
+ },
43
+ ],
44
+ ~queryEntries=[],
45
+ )
46
+ let field = mutationFor(fragment, "Ordering_CancelOrder")->Option.getOr("")
47
+ expect(field->String.includes("id: ID!, orderId"))->toBe(true)
48
+ })
49
+ })
@@ -0,0 +1,57 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as GraphQL_Stitcher$ReventlessCore from "../../src/components/Api/GraphQL_Stitcher.res.mjs";
6
+ import * as GraphQL_FragmentGenerator$ReventlessCore from "../../src/components/Api/GraphQL_FragmentGenerator.res.mjs";
7
+
8
+ let commandSchema = S.union([
9
+ S.schema(s => ({
10
+ TAG: "CancelOrder",
11
+ orderId: s.m(S.string)
12
+ })),
13
+ S.schema(s => ({
14
+ TAG: "ReopenOrder",
15
+ orderId: s.m(S.string)
16
+ }))
17
+ ]);
18
+
19
+ function commandAuthorization(param) {
20
+ return "AllowAuthenticated";
21
+ }
22
+
23
+ let OrderSliceCmd = {
24
+ commandSchema: commandSchema,
25
+ commandAuthorization: commandAuthorization
26
+ };
27
+
28
+ function mutationFor(fragment, needle) {
29
+ return GraphQL_Stitcher$ReventlessCore.decode(fragment).mutations.find(m => m.includes(needle));
30
+ }
31
+
32
+ globalThis.describe("GraphQL_FragmentGenerator.generate mutation id injection", () => {
33
+ globalThis.test("multi-variant slice entry (injectIdArg: false) omits the id argument", () => {
34
+ let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate([{
35
+ fieldNames: ["Ordering_CancelOrder"],
36
+ commandSchema: commandSchema,
37
+ injectIdArg: false
38
+ }], []);
39
+ let field = Stdlib_Option.getOr(mutationFor(fragment, "Ordering_CancelOrder"), "");
40
+ globalThis.expect(field.includes("orderId")).toBe(true);
41
+ globalThis.expect(field.includes("id: ID!")).toBe(false);
42
+ });
43
+ globalThis.test("aggregate-style entry (default) injects id: ID! ahead of the payload", () => {
44
+ let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate([{
45
+ fieldNames: ["Ordering_CancelOrder"],
46
+ commandSchema: commandSchema
47
+ }], []);
48
+ let field = Stdlib_Option.getOr(mutationFor(fragment, "Ordering_CancelOrder"), "");
49
+ globalThis.expect(field.includes("id: ID!, orderId")).toBe(true);
50
+ });
51
+ });
52
+
53
+ export {
54
+ OrderSliceCmd,
55
+ mutationFor,
56
+ }
57
+ /* commandSchema Not a pure module */
@@ -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 */