@reventlessdev/reventless-local 3.0.0-alpha.167 → 3.0.0-alpha.169

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 (34) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +11 -11
  3. package/src/Platform.res +11 -1
  4. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +16 -8
  5. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res.mjs +7 -7
  6. package/src/adapter/DomainGraphQL_Server.res +96 -0
  7. package/src/adapter/DomainGraphQL_Server.res.mjs +84 -0
  8. package/src/adapter/GraphQL_Server.res.mjs +18 -0
  9. package/src/adapter/LocalObjectStore.res +64 -0
  10. package/src/adapter/LocalObjectStore.res.mjs +55 -0
  11. package/src/adapter/PgProjectionCatchup.res +6 -1
  12. package/src/adapter/PgProjectionCatchup.res.mjs +4 -2
  13. package/src/adapter/ProjectionCheckpoint.res +11 -2
  14. package/src/adapter/ProjectionCheckpoint.res.mjs +9 -3
  15. package/tests/PluginEventDecodeTest.res +2 -0
  16. package/tests/PluginEventDecodeTest.res.mjs +2 -2
  17. package/tests/adapter/GraphQL_SchemaInspectorTest.res +52 -1
  18. package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +39 -1
  19. package/tests/adapter/InboundTranslationMutationTest.res +139 -0
  20. package/tests/adapter/InboundTranslationMutationTest.res.mjs +132 -0
  21. package/tests/adapter/QueryDbListResolverTest.res +2 -0
  22. package/tests/adapter/QueryDbListResolverTest.res.mjs +2 -0
  23. package/tests/adapter/ServedBucketHttpTest.res +174 -0
  24. package/tests/adapter/ServedBucketHttpTest.res.mjs +177 -0
  25. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +13 -9
  26. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs +21 -13
  27. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +215 -0
  28. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +273 -0
  29. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformTest.res +38 -0
  30. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformTest.res.mjs +28 -0
  31. package/tests/components/stateviewslice/StateViewSliceFixtures.res +1 -1
  32. package/tests/components/stateviewslice/StateViewSliceFixtures.res.mjs +2 -1
  33. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +1 -1
  34. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res.mjs +2 -1
@@ -180,16 +180,21 @@ let dcbCatchupEnvelope = (
180
180
  ~eventType: string,
181
181
  ~dataText: string,
182
182
  ~metaText: string,
183
+ ~recordedAt: string,
183
184
  ~firstTagValue: option<string>,
184
185
  ): option<JSON.t> =>
185
186
  switch (JSON.parseOrThrow(dataText), JSON.parseOrThrow(metaText)) {
186
187
  | (data, meta) =>
187
188
  let dataDict = data->JSON.Decode.object->Option.getOr(Dict.make())
188
189
  let entityId = firstTagValue->Option.getOr(logName)
190
+ // The stored `recorded_at` column is the authoritative storage time; unlike
191
+ // live publish we must NOT re-stamp here (catch-up runs at startup, long
192
+ // after append). StateViewSlice projections read this as `consumed.recordedAt`.
189
193
  Some(
190
194
  Dict.fromArray([
191
195
  ("id", JSON.Encode.string(entityId)),
192
196
  ("meta", meta),
197
+ ("recordedAt", JSON.Encode.string(recordedAt)),
193
198
  ("event", Message.combineMessage(eventType, dataDict)),
194
199
  ])->JSON.Encode.object,
195
200
  )
@@ -226,7 +231,7 @@ let missedRows = (
226
231
  // dcb_tag rows are inserted in tag order, so MIN(rowid) is the first tag.
227
232
  db
228
233
  ->SqliteDriver.prepare(
229
- "SELECT rowid AS pos, log_name, event_type, data, meta, (SELECT t.tag_value FROM dcb_tag t WHERE t.log_name = dcb_event.log_name AND t.position = dcb_event.position ORDER BY t.rowid ASC LIMIT 1) AS first_tag FROM dcb_event WHERE rowid > ? AND rowid <= ? ORDER BY rowid ASC",
234
+ "SELECT rowid AS pos, log_name, event_type, data, meta, recorded_at, (SELECT t.tag_value FROM dcb_tag t WHERE t.log_name = dcb_event.log_name AND t.position = dcb_event.position ORDER BY t.rowid ASC LIMIT 1) AS first_tag FROM dcb_event WHERE rowid > ? AND rowid <= ? ORDER BY rowid ASC",
230
235
  )
231
236
  ->SqliteDriver.all([JSON.Encode.int(afterPos), JSON.Encode.int(upTo)])
232
237
  ->Array.filterMap(row =>
@@ -248,9 +253,13 @@ let missedRows = (
248
253
  | Some(JSON.String(v)) => Some(v)
249
254
  | _ => None
250
255
  }
256
+ let recordedAt = switch row->Dict.get("recorded_at") {
257
+ | Some(JSON.String(v)) => v
258
+ | _ => ""
259
+ }
251
260
  Some((
252
261
  Float.toInt(pos),
253
- dcbCatchupEnvelope(~logName, ~eventType, ~dataText, ~metaText, ~firstTagValue),
262
+ dcbCatchupEnvelope(~logName, ~eventType, ~dataText, ~metaText, ~recordedAt, ~firstTagValue),
254
263
  ))
255
264
  | _ => None
256
265
  }
@@ -129,7 +129,7 @@ function catchupEnvelope(flat) {
129
129
  }
130
130
  }
131
131
 
132
- function dcbCatchupEnvelope(logName, eventType, dataText, metaText, firstTagValue) {
132
+ function dcbCatchupEnvelope(logName, eventType, dataText, metaText, recordedAt, firstTagValue) {
133
133
  let val;
134
134
  let val$1;
135
135
  try {
@@ -149,6 +149,10 @@ function dcbCatchupEnvelope(logName, eventType, dataText, metaText, firstTagValu
149
149
  "meta",
150
150
  val$1
151
151
  ],
152
+ [
153
+ "recordedAt",
154
+ recordedAt
155
+ ],
152
156
  [
153
157
  "event",
154
158
  Message$ReventlessCore.combineMessage(eventType, dataDict)
@@ -191,7 +195,7 @@ function missedRows(db, axis, afterPos, upTo) {
191
195
  ];
192
196
  });
193
197
  } else {
194
- return Stdlib_Array.filterMap(SqliteDriver$ReventlessLocal.all(SqliteDriver$ReventlessLocal.prepare(db, "SELECT rowid AS pos, log_name, event_type, data, meta, (SELECT t.tag_value FROM dcb_tag t WHERE t.log_name = dcb_event.log_name AND t.position = dcb_event.position ORDER BY t.rowid ASC LIMIT 1) AS first_tag FROM dcb_event WHERE rowid > ? AND rowid <= ? ORDER BY rowid ASC"), [
198
+ return Stdlib_Array.filterMap(SqliteDriver$ReventlessLocal.all(SqliteDriver$ReventlessLocal.prepare(db, "SELECT rowid AS pos, log_name, event_type, data, meta, recorded_at, (SELECT t.tag_value FROM dcb_tag t WHERE t.log_name = dcb_event.log_name AND t.position = dcb_event.position ORDER BY t.rowid ASC LIMIT 1) AS first_tag FROM dcb_event WHERE rowid > ? AND rowid <= ? ORDER BY rowid ASC"), [
195
199
  afterPos,
196
200
  upTo
197
201
  ]), row => {
@@ -232,9 +236,11 @@ function missedRows(db, axis, afterPos, upTo) {
232
236
  }
233
237
  let match$5 = row["first_tag"];
234
238
  let firstTagValue = typeof match$5 === "string" ? match$5 : undefined;
239
+ let match$6 = row["recorded_at"];
240
+ let recordedAt = typeof match$6 === "string" ? match$6 : "";
235
241
  return [
236
242
  match | 0,
237
- dcbCatchupEnvelope(match$1, match$2, match$3, match$4, firstTagValue)
243
+ dcbCatchupEnvelope(match$1, match$2, match$3, match$4, recordedAt, firstTagValue)
238
244
  ];
239
245
  });
240
246
  }
@@ -15,6 +15,7 @@ let envelopeOf = (event: ReventlessCore.PluginSpec.event) =>
15
15
  ReventlessCore.Message.composeEventJson'(
16
16
  "Catalog",
17
17
  meta,
18
+ ~recordedAt="2024-01-01T00:00:00Z",
18
19
  event->S.reverseConvertToJsonOrThrow(ReventlessCore.PluginSpec.eventSchema),
19
20
  )
20
21
 
@@ -68,6 +69,7 @@ describe("Platform.decodeUiFragmentRegistryEventEnvelope", () => {
68
69
  ReventlessCore.Message.composeEventJson'(
69
70
  "Catalog",
70
71
  dcbMeta,
72
+ ~recordedAt="2024-01-01T00:00:00Z",
71
73
  ReventlessCore.Message.combineMessage(eventType, data),
72
74
  )
73
75
  }
@@ -15,7 +15,7 @@ TestRunner$ReventlessLocal.setup();
15
15
  let meta = Message$ReventlessCore.generateMeta(PluginSpec$ReventlessCore.name, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
16
16
 
17
17
  function envelopeOf(event) {
18
- return Message$ReventlessCore.composeEventJson$p("Catalog", meta, S.reverseConvertToJsonOrThrow(event, PluginSpec$ReventlessCore.eventSchema));
18
+ return Message$ReventlessCore.composeEventJson$p("Catalog", meta, "2024-01-01T00:00:00Z", S.reverseConvertToJsonOrThrow(event, PluginSpec$ReventlessCore.eventSchema));
19
19
  }
20
20
 
21
21
  globalThis.describe("Platform.decodePluginEventEnvelope", () => {
@@ -64,7 +64,7 @@ globalThis.describe("Platform.decodeUiFragmentRegistryEventEnvelope", () => {
64
64
  let dcbEnvelopeOf = event => {
65
65
  let json = S.reverseConvertToJsonOrThrow(event, UiFragmentRegistry$ReventlessCore.eventSchema);
66
66
  let match = Message$ReventlessCore.splitMessage(json);
67
- return Message$ReventlessCore.composeEventJson$p("Catalog", dcbMeta, Message$ReventlessCore.combineMessage(match[0], match[1]));
67
+ return Message$ReventlessCore.composeEventJson$p("Catalog", dcbMeta, "2024-01-01T00:00:00Z", Message$ReventlessCore.combineMessage(match[0], match[1]));
68
68
  };
69
69
  globalThis.test("decodes a UiFragmentRegistered event from the published envelope", () => {
70
70
  let event = {
@@ -27,6 +27,21 @@ type svState = {
27
27
  price: float,
28
28
  }
29
29
 
30
+ // Multi-variant enum used both as a required and as an optional state field, to pin
31
+ // the nullability the sury→GraphQL deriver must preserve for the optional case.
32
+ @schema
33
+ type sourceKind =
34
+ | @as("tagged") Tagged
35
+ | @as("ancestry") Ancestry
36
+ | @as("substrate") Substrate
37
+
38
+ @schema
39
+ type enumState = {
40
+ productId: @s.matches(Reventless.DcbTag.string) string,
41
+ requiredSource: sourceKind,
42
+ optionalSource?: sourceKind,
43
+ }
44
+
30
45
  @schema
31
46
  type addCommand = {
32
47
  productId: @s.matches(Reventless.DcbTag.string) string,
@@ -84,6 +99,8 @@ let indexedStateSchemaWithAnnotations = indexedStateSchema->S.Metadata.set(
84
99
  collapsed: [],
85
100
  scan: [],
86
101
  scanSort: [],
102
+ semantic: [],
103
+ metric: [],
87
104
  status: None,
88
105
  groupBy: None,
89
106
  visibility: None,
@@ -111,6 +128,8 @@ let orderedStateSchemaWithAnnotations = orderedStateSchema->S.Metadata.set(
111
128
  collapsed: [],
112
129
  scan: [],
113
130
  scanSort: [],
131
+ semantic: [],
132
+ metric: [],
114
133
  status: None,
115
134
  groupBy: None,
116
135
  visibility: None,
@@ -139,6 +158,8 @@ let scanStateSchemaWithAnnotations = scanStateSchema->S.Metadata.set(
139
158
  collapsed: [],
140
159
  scan: ["status"],
141
160
  scanSort: ["name"],
161
+ semantic: [],
162
+ metric: [],
142
163
  status: None,
143
164
  groupBy: None,
144
165
  visibility: None,
@@ -901,8 +922,38 @@ describe("Plugin admin fragment — kind enum + kindEq filter", () => {
901
922
  // kind is a real enum on the Plugin type (auto-derived from the payload-less variant)
902
923
  expect(sdl->String.includes("enum Platform_PluginKind"))->toBe(true)
903
924
  expect(sdl->String.includes("PlatformInfrastructure"))->toBe(true)
904
- expect(sdl->String.includes("kind: Platform_PluginKind!"))->toBe(true)
925
+ // `kind` is `option<pluginKind>` — nullable on purpose so a kind-less legacy row
926
+ // can't collapse the whole Platform_Plugins query. It must render without a `!`.
927
+ expect(sdl->String.includes("kind: Platform_PluginKind"))->toBe(true)
928
+ expect(sdl->String.includes("kind: Platform_PluginKind!"))->toBe(false)
905
929
  // @scan folds a server-side equality filter for the panel split
906
930
  expect(sdl->String.includes("kindEq: String"))->toBe(true)
907
931
  })
908
932
  })
933
+
934
+ describe("optional enum fields preserve GraphQL nullability", () => {
935
+ testPromise(
936
+ "required variant field → SomeEnum!; optional variant field → nullable SomeEnum",
937
+ async () => {
938
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
939
+ ~mutationEntries=[],
940
+ ~queryEntries=[
941
+ {
942
+ singleFieldName: "SV_Sourced",
943
+ listFieldName: "SV_Sourceds",
944
+ returnTypeName: "EnumState",
945
+ stateSchema: enumStateSchema->S.castToUnknown,
946
+ authorization: None,
947
+ includeIdParam: false,
948
+ },
949
+ ],
950
+ )
951
+ let sdl = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment).sdlPreview
952
+ // A required variant field stays non-null.
953
+ expect(sdl->String.includes("requiredSource: EnumStateRequiredSource!"))->toBe(true)
954
+ // An optional variant field is nullable: the enum is present but never with a `!`.
955
+ expect(sdl->String.includes("optionalSource: EnumStateOptionalSource"))->toBe(true)
956
+ expect(sdl->String.includes("optionalSource: EnumStateOptionalSource!"))->toBe(false)
957
+ },
958
+ )
959
+ })
@@ -29,6 +29,18 @@ let svStateSchema = S.schema(s => ({
29
29
  price: s.m(S.float)
30
30
  }));
31
31
 
32
+ let sourceKindSchema = S.union([
33
+ S.literal("tagged"),
34
+ S.literal("ancestry"),
35
+ S.literal("substrate")
36
+ ]);
37
+
38
+ let enumStateSchema = S.schema(s => ({
39
+ productId: s.m(DcbTag$Reventless.string),
40
+ requiredSource: s.m(sourceKindSchema),
41
+ optionalSource: s.m(S.option(sourceKindSchema))
42
+ }));
43
+
32
44
  let addCommandSchema = S.schema(s => ({
33
45
  productId: s.m(DcbTag$Reventless.string),
34
46
  name: s.m(S.string)
@@ -95,6 +107,8 @@ let indexedStateSchemaWithAnnotations = S.Metadata.set(indexedStateSchema, State
95
107
  collapsed: [],
96
108
  scan: [],
97
109
  scanSort: [],
110
+ semantic: [],
111
+ metric: [],
98
112
  status: undefined,
99
113
  groupBy: undefined,
100
114
  visibility: undefined
@@ -119,6 +133,8 @@ let orderedStateSchemaWithAnnotations = S.Metadata.set(orderedStateSchema, State
119
133
  collapsed: [],
120
134
  scan: [],
121
135
  scanSort: [],
136
+ semantic: [],
137
+ metric: [],
122
138
  status: undefined,
123
139
  groupBy: undefined,
124
140
  visibility: undefined
@@ -144,6 +160,8 @@ let scanStateSchemaWithAnnotations = S.Metadata.set(scanStateSchema, StateAnnota
144
160
  collapsed: [],
145
161
  scan: ["status"],
146
162
  scanSort: ["name"],
163
+ semantic: [],
164
+ metric: [],
147
165
  status: undefined,
148
166
  groupBy: undefined,
149
167
  visibility: undefined
@@ -635,11 +653,29 @@ globalThis.describe("Plugin admin fragment — kind enum + kindEq filter", () =>
635
653
  let sdl = GraphQL_SchemaInspector$ReventlessCore.inspectFragment(fragment).sdlPreview;
636
654
  globalThis.expect(sdl.includes("enum Platform_PluginKind")).toBe(true);
637
655
  globalThis.expect(sdl.includes("PlatformInfrastructure")).toBe(true);
638
- globalThis.expect(sdl.includes("kind: Platform_PluginKind!")).toBe(true);
656
+ globalThis.expect(sdl.includes("kind: Platform_PluginKind")).toBe(true);
657
+ globalThis.expect(sdl.includes("kind: Platform_PluginKind!")).toBe(false);
639
658
  globalThis.expect(sdl.includes("kindEq: String")).toBe(true);
640
659
  });
641
660
  });
642
661
 
662
+ globalThis.describe("optional enum fields preserve GraphQL nullability", () => {
663
+ globalThis.test("required variant field → SomeEnum!; optional variant field → nullable SomeEnum", async () => {
664
+ let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate([], [{
665
+ singleFieldName: "SV_Sourced",
666
+ listFieldName: "SV_Sourceds",
667
+ returnTypeName: "EnumState",
668
+ stateSchema: enumStateSchema,
669
+ authorization: undefined,
670
+ includeIdParam: false
671
+ }]);
672
+ let sdl = GraphQL_SchemaInspector$ReventlessCore.inspectFragment(fragment).sdlPreview;
673
+ globalThis.expect(sdl.includes("requiredSource: EnumStateRequiredSource!")).toBe(true);
674
+ globalThis.expect(sdl.includes("optionalSource: EnumStateOptionalSource")).toBe(true);
675
+ globalThis.expect(sdl.includes("optionalSource: EnumStateOptionalSource!")).toBe(false);
676
+ });
677
+ });
678
+
643
679
  let stringSchema = S.string;
644
680
 
645
681
  let taggedSchema = DcbTag$Reventless.string;
@@ -655,6 +691,8 @@ export {
655
691
  boolSchema,
656
692
  testStateSchema,
657
693
  svStateSchema,
694
+ sourceKindSchema,
695
+ enumStateSchema,
658
696
  addCommandSchema,
659
697
  unionCommandSchema,
660
698
  dcbSingleCommandSchema,
@@ -0,0 +1,139 @@
1
+ // Response-envelope tests for InboundTranslationSlice mutations.
2
+ //
3
+ // The field is declared `CommandResult!`, an abstract type. A resolver invoked
4
+ // directly cannot show whether the value it returns actually resolves to a union
5
+ // member — only a real execution can, and until one existed every inbound
6
+ // mutation came back as `Abstract type "CommandResult" must resolve to an Object
7
+ // type at runtime` even though the translation had succeeded.
8
+ //
9
+ // So these tests compose the schema the way the running platform does
10
+ // (DomainGraphQL_Server.composeSchema) and execute a document against it, then
11
+ // assert on the FULL envelope — `errors` as well as `data`.
12
+ //
13
+ // The resolver is driven by a real InboundTranslationSlice_Callback, so the
14
+ // mutation exercises parse -> translate -> publish -> encode end to end.
15
+
16
+ @@warning("-44")
17
+
18
+ open JestGlobals
19
+ open InboundTranslationSliceFixtures
20
+
21
+ module PaymentWebhookTranslation = {
22
+ let translate = PaymentWebhookSpec.translate
23
+ let moduleUrl = PaymentWebhookSpec.moduleUrl
24
+ }
25
+
26
+ module Callback = ReventlessCore.InboundTranslationSlice_Callback.Make(
27
+ PaymentWebhookSpec,
28
+ PaymentWebhookTranslation,
29
+ )
30
+
31
+ // Registered inside a plugin scope, not the platform one: a plugin bucket's
32
+ // document is validated standalone before the cross-plugin merge, so this also
33
+ // pins that the inbound path registers the CommandResult union itself rather
34
+ // than borrowing a registration from a sibling command handler.
35
+ let scope = "Payments"
36
+ let fieldName = "Payments_PaymentWebhook"
37
+
38
+ let published: ref<array<Reventless.Message.commandJson>> = ref([])
39
+
40
+ let publishJsons: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
41
+ published.contents = published.contents->Array.concat(cmds)
42
+
43
+ let selection = `__typename
44
+ ... on CommandAccepted { msgId entityId eventCount }
45
+ ... on CommandRejected { msgId errorCode errorDetail }`
46
+
47
+ let runMutation = async (~status: string) => {
48
+ let source = `mutation {
49
+ r: ${fieldName}(paymentId: "pay-1", orderId: "ord-1", status: "${status}") { ${selection} }
50
+ }`
51
+ await GraphqlYoga.graphql({
52
+ "schema": DomainGraphQL_Server.composeSchema(),
53
+ "source": source,
54
+ "contextValue": JSON.Encode.null,
55
+ })
56
+ }
57
+
58
+ let errorMessages = (result: GraphqlYoga.executionResult): array<string> =>
59
+ result.errors
60
+ ->Option.getOr([])
61
+ ->Array.map(e => e->JsExn.message->Option.getOr("unknown error"))
62
+
63
+ let payload = (result: GraphqlYoga.executionResult): JSON.t =>
64
+ result.data
65
+ ->Option.getOr(JSON.Encode.null)
66
+ ->JSON.Decode.object
67
+ ->Option.flatMap(d => d->Dict.get("r"))
68
+ ->Option.getOr(JSON.Encode.null)
69
+
70
+ let str = (node: JSON.t, key: string): option<string> =>
71
+ node->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))->Option.flatMap(JSON.Decode.string)
72
+
73
+ let num = (node: JSON.t, key: string): option<float> =>
74
+ node->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))->Option.flatMap(JSON.Decode.float)
75
+
76
+ describe("InboundTranslationSlice mutation — response envelope", () => {
77
+ beforeEach(() => {
78
+ DomainGraphQL_Server.asInterface.reset()
79
+ published.contents = []
80
+ Callback.auditLog->Dict.keysToArray->Array.forEach(k => Callback.auditLog->Dict.delete(k))
81
+
82
+ DomainGraphQL_Server.setScope(scope)
83
+ InboundTranslationResolvers_GraphQL.register(
84
+ ~fieldName,
85
+ ~externalInputSchema=PaymentWebhookSpec.externalInputSchema->S.castToUnknown,
86
+ ~server=DomainGraphQL_Server.asInterface,
87
+ )
88
+ DomainGraphQL_Server.resetScope()
89
+
90
+ InboundTranslationResolvers_GraphQL.bindReceive(
91
+ ~fieldName,
92
+ ~receive=inputJson => Callback.receive(publishJsons, inputJson),
93
+ )
94
+ })
95
+
96
+ testPromise("a successful translation resolves as CommandAccepted, with no errors", async () => {
97
+ let result = await runMutation(~status="completed")
98
+
99
+ expect(errorMessages(result))->toEqual([])
100
+
101
+ let node = payload(result)
102
+ expect(node->str("__typename"))->toEqual(Some("CommandAccepted"))
103
+ expect(node->str("entityId"))->toEqual(Some("ord-1"))
104
+ expect(node->num("eventCount"))->toEqual(Some(1.0))
105
+
106
+ // msgId is the audit-row key, so the caller can look the request up.
107
+ let msgId = node->str("msgId")->Option.getOr("")
108
+ expect(Callback.auditLog->Dict.get(msgId)->Option.isSome)->toBe(true)
109
+
110
+ expect(published.contents->Array.length)->toBe(1)
111
+ })
112
+
113
+ testPromise("a rejected translation resolves as CommandRejected, with no errors", async () => {
114
+ let result = await runMutation(~status="pending")
115
+
116
+ expect(errorMessages(result))->toEqual([])
117
+
118
+ let node = payload(result)
119
+ expect(node->str("__typename"))->toEqual(Some("CommandRejected"))
120
+ expect(node->str("errorCode"))->toEqual(Some("TranslationFailed"))
121
+ expect(node->str("errorDetail"))->toEqual(Some("Unknown payment status: pending"))
122
+
123
+ let msgId = node->str("msgId")->Option.getOr("")
124
+ switch Callback.auditLog->Dict.get(msgId) {
125
+ | Some(row) => expect(row.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Failure)
126
+ | None => expect(true)->toBe(false)
127
+ }
128
+
129
+ expect(published.contents->Array.length)->toBe(0)
130
+ })
131
+
132
+ testSync("the mutation field is typed CommandResult!", () => {
133
+ let fieldLine =
134
+ DomainGraphQL_Server.asInterface.buildSdl()
135
+ ->String.split("\n")
136
+ ->Array.find(line => line->String.includes(fieldName))
137
+ expect(fieldLine->Option.map(l => l->String.endsWith(": CommandResult!")))->toEqual(Some(true))
138
+ })
139
+ })
@@ -0,0 +1,132 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Graphql from "graphql";
4
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
7
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as DomainGraphQL_Server$ReventlessLocal from "../../src/adapter/DomainGraphQL_Server.res.mjs";
9
+ import * as InboundTranslationSliceFixtures$ReventlessLocal from "../components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs";
10
+ import * as InboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
11
+ import * as InboundTranslationResolvers_GraphQL$ReventlessLocal from "../../src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res.mjs";
12
+
13
+ let translate = InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.translate;
14
+
15
+ let moduleUrl = InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.moduleUrl;
16
+
17
+ let PaymentWebhookTranslation = {
18
+ translate: translate,
19
+ moduleUrl: moduleUrl
20
+ };
21
+
22
+ let Callback = InboundTranslationSlice_Callback$ReventlessCore.Make({
23
+ name: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.name,
24
+ moduleUrl: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.moduleUrl,
25
+ externalInputSchema: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.externalInputSchema,
26
+ commandSchema: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.commandSchema,
27
+ targetName: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.targetName,
28
+ externalSystem: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.externalSystem,
29
+ commandAuthorization: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.commandAuthorization
30
+ })(PaymentWebhookTranslation);
31
+
32
+ let scope = "Payments";
33
+
34
+ let fieldName = "Payments_PaymentWebhook";
35
+
36
+ let published = {
37
+ contents: []
38
+ };
39
+
40
+ async function publishJsons(cmds) {
41
+ published.contents = published.contents.concat(cmds);
42
+ }
43
+
44
+ let selection = `__typename
45
+ ... on CommandAccepted { msgId entityId eventCount }
46
+ ... on CommandRejected { msgId errorCode errorDetail }`;
47
+
48
+ async function runMutation(status) {
49
+ let source = `mutation {
50
+ r: ` + fieldName + `(paymentId: "pay-1", orderId: "ord-1", status: "` + status + `") { ` + selection + ` }
51
+ }`;
52
+ return await Graphql.graphql({
53
+ schema: DomainGraphQL_Server$ReventlessLocal.composeSchema(),
54
+ source: source,
55
+ contextValue: null
56
+ });
57
+ }
58
+
59
+ function errorMessages(result) {
60
+ return Stdlib_Option.getOr(result.errors, []).map(e => Stdlib_Option.getOr(Stdlib_JsExn.message(e), "unknown error"));
61
+ }
62
+
63
+ function payload(result) {
64
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(Stdlib_Option.getOr(result.data, null)), d => d["r"]), null);
65
+ }
66
+
67
+ function str(node, key) {
68
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(node), d => d[key]), Stdlib_JSON.Decode.string);
69
+ }
70
+
71
+ function num(node, key) {
72
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(node), d => d[key]), Stdlib_JSON.Decode.float);
73
+ }
74
+
75
+ globalThis.describe("InboundTranslationSlice mutation — response envelope", () => {
76
+ globalThis.beforeEach(() => {
77
+ DomainGraphQL_Server$ReventlessLocal.asInterface.reset();
78
+ published.contents = [];
79
+ Object.keys(Callback.auditLog).forEach(k => Stdlib_Dict.$$delete(Callback.auditLog, k));
80
+ DomainGraphQL_Server$ReventlessLocal.setScope(scope);
81
+ InboundTranslationResolvers_GraphQL$ReventlessLocal.register(fieldName, InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.externalInputSchema, DomainGraphQL_Server$ReventlessLocal.asInterface);
82
+ DomainGraphQL_Server$ReventlessLocal.resetScope();
83
+ InboundTranslationResolvers_GraphQL$ReventlessLocal.bindReceive(fieldName, inputJson => Callback.receive(publishJsons, inputJson));
84
+ });
85
+ globalThis.test("a successful translation resolves as CommandAccepted, with no errors", async () => {
86
+ let result = await runMutation("completed");
87
+ globalThis.expect(errorMessages(result)).toEqual([]);
88
+ let node = payload(result);
89
+ globalThis.expect(str(node, "__typename")).toEqual("CommandAccepted");
90
+ globalThis.expect(str(node, "entityId")).toEqual("ord-1");
91
+ globalThis.expect(num(node, "eventCount")).toEqual(1.0);
92
+ let msgId = Stdlib_Option.getOr(str(node, "msgId"), "");
93
+ globalThis.expect(Stdlib_Option.isSome(Callback.auditLog[msgId])).toBe(true);
94
+ globalThis.expect(published.contents.length).toBe(1);
95
+ });
96
+ globalThis.test("a rejected translation resolves as CommandRejected, with no errors", async () => {
97
+ let result = await runMutation("pending");
98
+ globalThis.expect(errorMessages(result)).toEqual([]);
99
+ let node = payload(result);
100
+ globalThis.expect(str(node, "__typename")).toEqual("CommandRejected");
101
+ globalThis.expect(str(node, "errorCode")).toEqual("TranslationFailed");
102
+ globalThis.expect(str(node, "errorDetail")).toEqual("Unknown payment status: pending");
103
+ let msgId = Stdlib_Option.getOr(str(node, "msgId"), "");
104
+ let row = Callback.auditLog[msgId];
105
+ if (row !== undefined) {
106
+ globalThis.expect(row.status).toBe("Failure");
107
+ } else {
108
+ globalThis.expect(true).toBe(false);
109
+ }
110
+ globalThis.expect(published.contents.length).toBe(0);
111
+ });
112
+ globalThis.test("the mutation field is typed CommandResult!", () => {
113
+ let fieldLine = DomainGraphQL_Server$ReventlessLocal.asInterface.buildSdl().split("\n").find(line => line.includes(fieldName));
114
+ globalThis.expect(Stdlib_Option.map(fieldLine, l => l.endsWith(": CommandResult!"))).toEqual(true);
115
+ });
116
+ });
117
+
118
+ export {
119
+ PaymentWebhookTranslation,
120
+ Callback,
121
+ scope,
122
+ fieldName,
123
+ published,
124
+ publishJsons,
125
+ selection,
126
+ runMutation,
127
+ errorMessages,
128
+ payload,
129
+ str,
130
+ num,
131
+ }
132
+ /* Callback Not a pure module */
@@ -35,6 +35,8 @@ let rowStateSchemaWithAnnotations =
35
35
  collapsed: [],
36
36
  scan: ["status"],
37
37
  scanSort: ["name"],
38
+ semantic: [],
39
+ metric: [],
38
40
  status: None,
39
41
  groupBy: None,
40
42
  visibility: None,
@@ -43,6 +43,8 @@ let rowStateSchemaWithAnnotations = S.Metadata.set(rowStateSchema, StateAnnotati
43
43
  collapsed: [],
44
44
  scan: ["status"],
45
45
  scanSort: ["name"],
46
+ semantic: [],
47
+ metric: [],
46
48
  status: undefined,
47
49
  groupBy: undefined,
48
50
  visibility: undefined