@reventlessdev/reventless-local 3.0.0-alpha.293 → 3.0.0-alpha.294

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,19 @@
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.294 (2026-10-21)
7
+
8
+ * feat!: a rejected command says whether the model, the access check or the platform said no ([6a8a6b0](https://github.com/ReventlessDev/reventless-core/commit/6a8a6b0f2f31e766ae26a944c23b0904e51a5865))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * a type error variant with one of those names fails to
13
+ compile; rename it for what the model means, such as AlreadyBooked. On
14
+ AWS, rebuild every plugin against this release before a client selects
15
+ cause: a plugin built earlier answers it with a non-null error.
16
+
17
+
18
+
6
19
  # 3.0.0-alpha.293 (2026-10-21)
7
20
 
8
21
  * feat!: a state view's row key is module Id ([08c9726](https://github.com/ReventlessDev/reventless-core/commit/08c972659a3d689a39fd5e2f2026597155e92210))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.293",
3
+ "version": "3.0.0-alpha.294",
4
4
  "description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -35,18 +35,18 @@
35
35
  "sury": "11.0.0",
36
36
  "ws": "^8.18.0",
37
37
  "@reventlessdev/rescript-effect": "1.0.0-alpha.1",
38
- "@reventlessdev/rescript-jest": "1.0.0-alpha.13",
39
38
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
39
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.13",
40
40
  "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
41
- "@reventlessdev/rescript-node": "2.0.0-alpha.23",
42
41
  "@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
43
- "@reventlessdev/reventless-core": "3.0.0-alpha.302",
44
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.150",
45
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.250",
46
- "@reventlessdev/reventless-infra": "3.0.0-alpha.199",
47
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.166",
48
- "@reventlessdev/reventless-spec": "3.0.0-alpha.171",
49
- "@reventlessdev/reventless-seed": "1.0.0-alpha.35"
42
+ "@reventlessdev/reventless-core": "3.0.0-alpha.303",
43
+ "@reventlessdev/rescript-node": "2.0.0-alpha.23",
44
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.151",
45
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.251",
46
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.167",
47
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.200",
48
+ "@reventlessdev/reventless-seed": "1.0.0-alpha.36",
49
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.172"
50
50
  },
51
51
  "devDependencies": {
52
52
  "rescript": "12.3.1",
package/src/Platform.res CHANGED
@@ -1862,9 +1862,12 @@ module MakeWithConfig = (
1862
1862
  }
1863
1863
  switch statusByName->Dict.get(pluginPrefix) {
1864
1864
  | Some(Connected) => None
1865
- | Some(Disconnected) => Some(("PluginUnavailable", "plugin is disconnected"))
1866
- | Some(Inactive) => Some(("PluginInactive", "plugin is inactive"))
1867
- | Some(Retired) => Some(("PluginRetired", "plugin version has been retired"))
1865
+ | Some(Disconnected) =>
1866
+ Some((Reventless.CommandOutcome.code(PluginUnavailable), "plugin is disconnected"))
1867
+ | Some(Inactive) =>
1868
+ Some((Reventless.CommandOutcome.code(PluginInactive), "plugin is inactive"))
1869
+ | Some(Retired) =>
1870
+ Some((Reventless.CommandOutcome.code(PluginRetired), "plugin version has been retired"))
1868
1871
  | None => None
1869
1872
  }
1870
1873
  }
@@ -26,6 +26,7 @@ import * as UiSlots$ReventlessLocal from "./UiSlots.res.mjs";
26
26
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
27
27
  import * as LocalBus$ReventlessLocal from "./adapter/LocalBus.res.mjs";
28
28
  import * as Api_Naming$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Naming.res.mjs";
29
+ import * as CommandOutcome$Reventless from "@reventlessdev/reventless-spec/src/components/CommandOutcome.res.mjs";
29
30
  import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
30
31
  import * as UserStore$ReventlessLocal from "./adapter/Auth/UserStore.res.mjs";
31
32
  import * as TestRunner$ReventlessLocal from "./test/TestRunner.res.mjs";
@@ -1393,17 +1394,17 @@ function MakeWithConfig(Config) {
1393
1394
  return;
1394
1395
  case "Disconnected" :
1395
1396
  return [
1396
- "PluginUnavailable",
1397
+ CommandOutcome$Reventless.code("PluginUnavailable"),
1397
1398
  "plugin is disconnected"
1398
1399
  ];
1399
1400
  case "Inactive" :
1400
1401
  return [
1401
- "PluginInactive",
1402
+ CommandOutcome$Reventless.code("PluginInactive"),
1402
1403
  "plugin is inactive"
1403
1404
  ];
1404
1405
  case "Retired" :
1405
1406
  return [
1406
- "PluginRetired",
1407
+ CommandOutcome$Reventless.code("PluginRetired"),
1407
1408
  "plugin version has been retired"
1408
1409
  ];
1409
1410
  }
@@ -3149,17 +3150,17 @@ function Make($star) {
3149
3150
  return;
3150
3151
  case "Disconnected" :
3151
3152
  return [
3152
- "PluginUnavailable",
3153
+ CommandOutcome$Reventless.code("PluginUnavailable"),
3153
3154
  "plugin is disconnected"
3154
3155
  ];
3155
3156
  case "Inactive" :
3156
3157
  return [
3157
- "PluginInactive",
3158
+ CommandOutcome$Reventless.code("PluginInactive"),
3158
3159
  "plugin is inactive"
3159
3160
  ];
3160
3161
  case "Retired" :
3161
3162
  return [
3162
- "PluginRetired",
3163
+ CommandOutcome$Reventless.code("PluginRetired"),
3163
3164
  "plugin version has been retired"
3164
3165
  ];
3165
3166
  }
@@ -35,14 +35,15 @@ let extractIdentity = (ctx: JSON.t): Reventless.Identity.t => {
35
35
  // Returns a `CommandRejected` outcome with a fresh msgId so the GraphQL union
36
36
  // resolves consistently — matches the existing CommandResult contract.
37
37
 
38
- let rejectForbidden = (~field: string): JSON.t => JSON.Object(
39
- Dict.fromArray([
40
- ("__typename", JSON.String("CommandRejected")),
41
- ("msgId", JSON.String(ReventlessCore.Message.uuid())),
42
- ("errorCode", JSON.String("Forbidden")),
43
- ("errorDetail", JSON.String(`Mutation.${field}: identity is not authorized`)),
44
- ]),
45
- )
38
+ let rejectForbidden = (~field: string): JSON.t =>
39
+ CommandTopic.commandOutcomeToJson(
40
+ Rejected({
41
+ msgId: ReventlessCore.Message.uuid(),
42
+ errorCode: Reventless.CommandOutcome.code(Forbidden),
43
+ errorDetail: Some(`Mutation.${field}: identity is not authorized`),
44
+ cause: AccessRefusal,
45
+ }),
46
+ )
46
47
 
47
48
  // -- Plugin status gate -------------------------------------------------------
48
49
  // Set by the platform after Admin.construct to wire a per-mutation lookup against
@@ -60,18 +61,16 @@ let pluginStatusGate: ref<option<string => option<(string, string)>>> = ref(None
60
61
  let setPluginStatusGate = fn => pluginStatusGate := Some(fn)
61
62
  let resetPluginStatusGate = () => pluginStatusGate := None
62
63
 
63
- let rejectPluginStatus = (
64
- ~field: string,
65
- ~errorCode: string,
66
- ~detail: string,
67
- ): JSON.t => JSON.Object(
68
- Dict.fromArray([
69
- ("__typename", JSON.String("CommandRejected")),
70
- ("msgId", JSON.String(ReventlessCore.Message.uuid())),
71
- ("errorCode", JSON.String(errorCode)),
72
- ("errorDetail", JSON.String(`Mutation.${field}: ${detail}`)),
73
- ]),
74
- )
64
+ // The plugin, not the model, is what said no.
65
+ let rejectPluginStatus = (~field: string, ~errorCode: string, ~detail: string): JSON.t =>
66
+ CommandTopic.commandOutcomeToJson(
67
+ Rejected({
68
+ msgId: ReventlessCore.Message.uuid(),
69
+ errorCode,
70
+ errorDetail: Some(`Mutation.${field}: ${detail}`),
71
+ cause: InfrastructureFailure,
72
+ }),
73
+ )
75
74
 
76
75
  let checkPluginStatus = (~field: string): option<JSON.t> =>
77
76
  switch pluginStatusGate.contents {
@@ -10,6 +10,7 @@ import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/I
10
10
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
11
11
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
12
12
  import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
13
+ import * as CommandOutcome$Reventless from "@reventlessdev/reventless-spec/src/components/CommandOutcome.res.mjs";
13
14
  import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
14
15
  import * as GraphQL_CallerError$ReventlessLocal from "../GraphQL_CallerError.res.mjs";
15
16
  import * as Plugin_ResolverError$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_ResolverError.res.mjs";
@@ -29,24 +30,13 @@ function extractIdentity(ctx) {
29
30
  }
30
31
 
31
32
  function rejectForbidden(field) {
32
- return Object.fromEntries([
33
- [
34
- "__typename",
35
- "CommandRejected"
36
- ],
37
- [
38
- "msgId",
39
- Message$ReventlessCore.uuid()
40
- ],
41
- [
42
- "errorCode",
43
- "Forbidden"
44
- ],
45
- [
46
- "errorDetail",
47
- `Mutation.` + field + `: identity is not authorized`
48
- ]
49
- ]);
33
+ return CommandTopic$ReventlessCore.commandOutcomeToJson({
34
+ TAG: "Rejected",
35
+ msgId: Message$ReventlessCore.uuid(),
36
+ errorCode: CommandOutcome$Reventless.code("Forbidden"),
37
+ errorDetail: `Mutation.` + field + `: identity is not authorized`,
38
+ cause: "AccessRefusal"
39
+ });
50
40
  }
51
41
 
52
42
  let pluginStatusGate = {
@@ -62,24 +52,13 @@ function resetPluginStatusGate() {
62
52
  }
63
53
 
64
54
  function rejectPluginStatus(field, errorCode, detail) {
65
- return Object.fromEntries([
66
- [
67
- "__typename",
68
- "CommandRejected"
69
- ],
70
- [
71
- "msgId",
72
- Message$ReventlessCore.uuid()
73
- ],
74
- [
75
- "errorCode",
76
- errorCode
77
- ],
78
- [
79
- "errorDetail",
80
- `Mutation.` + field + `: ` + detail
81
- ]
82
- ]);
55
+ return CommandTopic$ReventlessCore.commandOutcomeToJson({
56
+ TAG: "Rejected",
57
+ msgId: Message$ReventlessCore.uuid(),
58
+ errorCode: errorCode,
59
+ errorDetail: `Mutation.` + field + `: ` + detail,
60
+ cause: "InfrastructureFailure"
61
+ });
83
62
  }
84
63
 
85
64
  function checkPluginStatus(field) {
@@ -6,6 +6,8 @@ let componentType = CommandTopic$ReventlessCore.componentType;
6
6
 
7
7
  let NotPublishedToChannel = CommandTopic$ReventlessCore.NotPublishedToChannel;
8
8
 
9
+ let refusalCauseToGraphQL = CommandTopic$ReventlessCore.refusalCauseToGraphQL;
10
+
9
11
  let commandOutcomeHook = CommandTopic$ReventlessCore.commandOutcomeHook;
10
12
 
11
13
  let registerCommandOutcome = CommandTopic$ReventlessCore.registerCommandOutcome;
@@ -47,6 +49,7 @@ let filter = CommandTopic$ReventlessCore.filter;
47
49
  export {
48
50
  componentType,
49
51
  NotPublishedToChannel,
52
+ refusalCauseToGraphQL,
50
53
  commandOutcomeHook,
51
54
  registerCommandOutcome,
52
55
  clearCommandOutcome,
@@ -139,6 +139,12 @@ describe("CommandGeneratorResolvers_GraphQL — per-constructor authorization",
139
139
  )
140
140
  expect(getTypename(response))->toEqual("CommandRejected")
141
141
  expect(getErrorCode(response))->toEqual("Forbidden")
142
+ expect(
143
+ response
144
+ ->JSON.Decode.object
145
+ ->Option.flatMap(o => o->Dict.get("cause"))
146
+ ->Option.flatMap(JSON.Decode.string),
147
+ )->toEqual(Some("ACCESS"))
142
148
  expect(calls.contents)->toEqual([])
143
149
  })
144
150
 
@@ -134,6 +134,7 @@ globalThis.describe("CommandGeneratorResolvers_GraphQL — per-constructor autho
134
134
  let response = await match[2](null, {}, ctxFor(userIdentity));
135
135
  globalThis.expect(getTypename(response)).toEqual("CommandRejected");
136
136
  globalThis.expect(getErrorCode(response)).toEqual("Forbidden");
137
+ globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(response), o => o["cause"]), Stdlib_JSON.Decode.string)).toEqual("ACCESS");
137
138
  globalThis.expect(match[3].contents).toEqual([]);
138
139
  });
139
140
  globalThis.test("anonymous invoking Archive is rejected with Forbidden", async () => {
@@ -609,9 +609,9 @@ describe("GraphQL_SchemaInspector", () => {
609
609
  let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
610
610
  // TestState, TestStateEdge, TestStateConnection, TestStateFilter (connection filter)
611
611
  // + TestStateRef (the reference door's projection)
612
- // + CommandResult union + CommandAccepted + CommandRejected + CommandPending
613
- // (auto-injected whenever the fragment emits any mutation field).
614
- expect(inspection.types->Array.length)->toBe(9)
612
+ // + CommandResult union + CommandAccepted + CommandRejectionCause + CommandRejected
613
+ // + CommandPending (auto-injected whenever the fragment emits any mutation field).
614
+ expect(inspection.types->Array.length)->toBe(10)
615
615
  expect(inspection.mutations->Array.length)->toBe(1)
616
616
  // single Test_State(id), list Test_States(...), Test_StatesByIds(ids: [String!]!)
617
617
  // and Test_StatesRefs(ids: [ID!]!) — the by-ids read projected to a reference.
@@ -430,7 +430,7 @@ globalThis.describe("GraphQL_SchemaInspector", () => {
430
430
  authorization: undefined
431
431
  }]);
432
432
  let inspection = GraphQL_SchemaInspector$ReventlessCore.inspectFragment(fragment);
433
- globalThis.expect(inspection.types.length).toBe(9);
433
+ globalThis.expect(inspection.types.length).toBe(10);
434
434
  globalThis.expect(inspection.mutations.length).toBe(1);
435
435
  globalThis.expect(inspection.queries.length).toBe(4);
436
436
  globalThis.expect(inspection.sdlPreview.includes("type TestState")).toBe(true);
@@ -44,6 +44,11 @@ let selection = `__typename
44
44
  ... on CommandAccepted { msgId entityId eventCount }
45
45
  ... on CommandRejected { msgId errorCode errorDetail }`
46
46
 
47
+ // The same selection with `cause`, which a client opts into.
48
+ let selectionWithCause = `__typename
49
+ ... on CommandAccepted { msgId entityId eventCount }
50
+ ... on CommandRejected { msgId errorCode errorDetail cause }`
51
+
47
52
  let signedIn = (groups): Reventless.Identity.t => {
48
53
  userId: "u-1",
49
54
  username: "alice",
@@ -67,7 +72,7 @@ let execute = async (~caller, source) =>
67
72
  "contextValue": contextOf(caller),
68
73
  })
69
74
 
70
- let runMutation = async (~status: string) =>
75
+ let runMutation = async (~status: string, ~selection=selection) =>
71
76
  await execute(
72
77
  ~caller=Some(signedIn([])),
73
78
  `mutation {
@@ -153,6 +158,23 @@ describe("InboundTranslationSlice mutation — response envelope", () => {
153
158
  expect(published.contents->Array.length)->toBe(0)
154
159
  })
155
160
 
161
+ testPromise("a client selecting cause learns the model said no", async () => {
162
+ let result = await runMutation(~status="pending", ~selection=selectionWithCause)
163
+ expect(errorMessages(result))->toEqual([])
164
+ let node = payload(result)
165
+ expect(node->str("errorCode"))->toEqual(Some("TranslationFailed"))
166
+ expect(node->str("cause"))->toEqual(Some("DOMAIN"))
167
+ })
168
+
169
+ testPromise("a client not selecting cause gets exactly the fields it asked for", async () => {
170
+ let node = payload(await runMutation(~status="pending"))
171
+ expect(
172
+ node
173
+ ->JSON.Decode.object
174
+ ->Option.map(d => d->Dict.keysToArray->Array.toSorted(String.compare)),
175
+ )->toEqual(Some(["__typename", "errorCode", "errorDetail", "msgId"]))
176
+ })
177
+
156
178
  testSync("the mutation field is typed CommandResult!", () => {
157
179
  let fieldLine =
158
180
  DomainGraphQL_Server.asInterface.buildSdl()
@@ -176,7 +198,7 @@ let feedField = "Catalog_CatalogFeed"
176
198
  let runFeed = async (~caller, ~kind) =>
177
199
  await execute(
178
200
  ~caller,
179
- `mutation { r: ${feedField}(kind: "${kind}", id: "x-1") { ${selection} } }`,
201
+ `mutation { r: ${feedField}(kind: "${kind}", id: "x-1") { ${selectionWithCause} } }`,
180
202
  )
181
203
 
182
204
  describe("InboundTranslationSlice mutation — the door", () => {
@@ -206,6 +228,7 @@ describe("InboundTranslationSlice mutation — the door", () => {
206
228
  testPromise("a caller holding none of the commands' roles is refused", async () => {
207
229
  let node = payload(await runFeed(~caller=Some(signedIn(["Ops"])), ~kind="product"))
208
230
  expect(node->str("errorCode"))->toEqual(Some("Forbidden"))
231
+ expect(node->str("cause"))->toEqual(Some("ACCESS"))
209
232
  expect(published.contents->Array.length)->toBe(0)
210
233
  })
211
234
 
@@ -225,6 +248,7 @@ describe("InboundTranslationSlice mutation — the door", () => {
225
248
  testPromise("the caller reaches the per-command check", async () => {
226
249
  let node = payload(await runFeed(~caller=Some(signedIn(["Merchandiser"])), ~kind="both"))
227
250
  expect(node->str("errorCode"))->toEqual(Some("Forbidden"))
251
+ expect(node->str("cause"))->toEqual(Some("ACCESS"))
228
252
  expect(published.contents->Array.length)->toBe(0)
229
253
  })
230
254
  })
@@ -5,6 +5,7 @@ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
5
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
6
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
7
7
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
8
9
  import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
9
10
  import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
10
11
  import * as Dcb_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/Dcb/Dcb_Builder.res.mjs";
@@ -49,6 +50,10 @@ let selection = `__typename
49
50
  ... on CommandAccepted { msgId entityId eventCount }
50
51
  ... on CommandRejected { msgId errorCode errorDetail }`;
51
52
 
53
+ let selectionWithCause = `__typename
54
+ ... on CommandAccepted { msgId entityId eventCount }
55
+ ... on CommandRejected { msgId errorCode errorDetail cause }`;
56
+
52
57
  function signedIn(groups) {
53
58
  return {
54
59
  userId: "u-1",
@@ -77,9 +82,10 @@ async function execute(caller, source) {
77
82
  });
78
83
  }
79
84
 
80
- async function runMutation(status) {
85
+ async function runMutation(status, selectionOpt) {
86
+ let selection$1 = selectionOpt !== undefined ? selectionOpt : selection;
81
87
  return await execute(signedIn([]), `mutation {
82
- r: ` + fieldName + `(paymentId: "pay-1", orderId: "ord-1", status: "` + status + `") { ` + selection + ` }
88
+ r: ` + fieldName + `(paymentId: "pay-1", orderId: "ord-1", status: "` + status + `") { ` + selection$1 + ` }
83
89
  }`);
84
90
  }
85
91
 
@@ -110,7 +116,7 @@ globalThis.describe("InboundTranslationSlice mutation — response envelope", ()
110
116
  InboundTranslationResolvers_GraphQL$ReventlessLocal.bindReceive(fieldName, (inputJson, caller) => Callback.receive(publishJsons, inputJson, caller, undefined));
111
117
  });
112
118
  globalThis.test("a successful translation resolves as CommandAccepted, with no errors", async () => {
113
- let result = await runMutation("completed");
119
+ let result = await runMutation("completed", undefined);
114
120
  globalThis.expect(errorMessages(result)).toEqual([]);
115
121
  let node = payload(result);
116
122
  globalThis.expect(str(node, "__typename")).toEqual("CommandAccepted");
@@ -121,7 +127,7 @@ globalThis.describe("InboundTranslationSlice mutation — response envelope", ()
121
127
  globalThis.expect(published.contents.length).toBe(1);
122
128
  });
123
129
  globalThis.test("a rejected translation resolves as CommandRejected, with no errors", async () => {
124
- let result = await runMutation("pending");
130
+ let result = await runMutation("pending", undefined);
125
131
  globalThis.expect(errorMessages(result)).toEqual([]);
126
132
  let node = payload(result);
127
133
  globalThis.expect(str(node, "__typename")).toEqual("CommandRejected");
@@ -136,6 +142,22 @@ globalThis.describe("InboundTranslationSlice mutation — response envelope", ()
136
142
  }
137
143
  globalThis.expect(published.contents.length).toBe(0);
138
144
  });
145
+ globalThis.test("a client selecting cause learns the model said no", async () => {
146
+ let result = await runMutation("pending", selectionWithCause);
147
+ globalThis.expect(errorMessages(result)).toEqual([]);
148
+ let node = payload(result);
149
+ globalThis.expect(str(node, "errorCode")).toEqual("TranslationFailed");
150
+ globalThis.expect(str(node, "cause")).toEqual("DOMAIN");
151
+ });
152
+ globalThis.test("a client not selecting cause gets exactly the fields it asked for", async () => {
153
+ let node = payload(await runMutation("pending", undefined));
154
+ globalThis.expect(Stdlib_Option.map(Stdlib_JSON.Decode.object(node), d => Object.keys(d).toSorted(Primitive_string.compare))).toEqual([
155
+ "__typename",
156
+ "errorCode",
157
+ "errorDetail",
158
+ "msgId"
159
+ ]);
160
+ });
139
161
  globalThis.test("the mutation field is typed CommandResult!", () => {
140
162
  let fieldLine = DomainGraphQL_Server$ReventlessLocal.asInterface.buildSdl().split("\n").find(line => line.includes(fieldName));
141
163
  globalThis.expect(Stdlib_Option.map(fieldLine, l => l.endsWith(": CommandResult!"))).toEqual(true);
@@ -165,7 +187,7 @@ let CatalogFeedCallback = InboundTranslationSlice_Callback$ReventlessCore.Make({
165
187
  let feedField = "Catalog_CatalogFeed";
166
188
 
167
189
  async function runFeed(caller, kind) {
168
- return await execute(caller, `mutation { r: ` + feedField + `(kind: "` + kind + `", id: "x-1") { ` + selection + ` } }`);
190
+ return await execute(caller, `mutation { r: ` + feedField + `(kind: "` + kind + `", id: "x-1") { ` + selectionWithCause + ` } }`);
169
191
  }
170
192
 
171
193
  globalThis.describe("InboundTranslationSlice mutation — the door", () => {
@@ -180,6 +202,7 @@ globalThis.describe("InboundTranslationSlice mutation — the door", () => {
180
202
  globalThis.test("a caller holding none of the commands' roles is refused", async () => {
181
203
  let node = payload(await runFeed(signedIn(["Ops"]), "product"));
182
204
  globalThis.expect(str(node, "errorCode")).toEqual("Forbidden");
205
+ globalThis.expect(str(node, "cause")).toEqual("ACCESS");
183
206
  globalThis.expect(published.contents.length).toBe(0);
184
207
  });
185
208
  globalThis.test("an unauthenticated caller is refused", async () => {
@@ -195,6 +218,7 @@ globalThis.describe("InboundTranslationSlice mutation — the door", () => {
195
218
  globalThis.test("the caller reaches the per-command check", async () => {
196
219
  let node = payload(await runFeed(signedIn(["Merchandiser"]), "both"));
197
220
  globalThis.expect(str(node, "errorCode")).toEqual("Forbidden");
221
+ globalThis.expect(str(node, "cause")).toEqual("ACCESS");
198
222
  globalThis.expect(published.contents.length).toBe(0);
199
223
  });
200
224
  });
@@ -207,6 +231,7 @@ export {
207
231
  published,
208
232
  publishJsons,
209
233
  selection,
234
+ selectionWithCause,
210
235
  signedIn,
211
236
  contextOf,
212
237
  execute,
@@ -0,0 +1,271 @@
1
+ // A rejected command says who said no, on the wire a client reads. Driven through
2
+ // the producer path (`runInlineAndCollect` → `commandOutcomeToJson`) over the real
3
+ // in-memory and SQLite DCB stores: the model (`decide`), the caller (ownership),
4
+ // and the platform (exhausted retries, a failing store).
5
+
6
+ open JestGlobals
7
+
8
+ let _ = TestRunner.setup()
9
+ let opts: Pulumi.CustomResourceOptions.t = {}
10
+
11
+ module Message = ReventlessCore.Message
12
+ module CommandTopic_Helpers = ReventlessCore.CommandTopic_Helpers
13
+
14
+ module OrderSlice = {
15
+ let name = "CauseOrders"
16
+ module Id = Reventless.Id.String
17
+ let moduleUrl: string = %raw(`import.meta.url`)
18
+
19
+ @schema
20
+ type event =
21
+ | OrderPlaced({orderId: @s.matches(Reventless.DcbTag.string) string, customerId: string})
22
+ | OrderCancelled({orderId: @s.matches(Reventless.DcbTag.string) string})
23
+
24
+ @schema
25
+ type consumedEvent =
26
+ | OrderPlaced({
27
+ orderId: @s.matches(Reventless.DcbTag.string) string,
28
+ customerId: @s.matches(Reventless.Owner.string) string,
29
+ })
30
+ | OrderCancelled({orderId: @s.matches(Reventless.DcbTag.string) string})
31
+
32
+ @schema
33
+ type command =
34
+ | PlaceOrder({
35
+ orderId: @s.matches(Reventless.DcbTag.string) string,
36
+ customerId: @s.matches(Reventless.Owner.string) string,
37
+ })
38
+ | CancelOrder({orderId: @s.matches(Reventless.DcbTag.string) string})
39
+
40
+ @schema
41
+ type error = OrderAlreadyPlaced | OrderNotFound
42
+ }
43
+
44
+ module OrderDecision = {
45
+ type state = {placed: bool, cancelled: bool}
46
+ let initialState = {placed: false, cancelled: false}
47
+ let moduleUrl = OrderSlice.moduleUrl
48
+
49
+ let evolve = (state, event: OrderSlice.consumedEvent) =>
50
+ switch event {
51
+ | OrderPlaced(_) => {...state, placed: true}
52
+ | OrderCancelled(_) => {...state, cancelled: true}
53
+ }
54
+
55
+ let decide = (state, command: OrderSlice.command): result<
56
+ array<OrderSlice.event>,
57
+ OrderSlice.error,
58
+ > =>
59
+ switch command {
60
+ | PlaceOrder({orderId, customerId}) =>
61
+ state.placed ? Error(OrderAlreadyPlaced) : Ok([OrderPlaced({orderId, customerId})])
62
+ | CancelOrder({orderId}) =>
63
+ state.placed ? Ok([OrderCancelled({orderId: orderId})]) : Error(OrderNotFound)
64
+ }
65
+ }
66
+
67
+ module OrderHandler = ReventlessCore.StateChangeSlice_Callback.Make(OrderSlice, OrderDecision)
68
+
69
+ let partitionTag = Reventless.DcbTag.Simple({key: "orderId"})
70
+
71
+ // ─── Callers ────────────────────────────────────────────────────────────────
72
+
73
+ external asIdentity: 'a => Reventless.Identity.t = "%identity"
74
+
75
+ let cognito = (~userId): Reventless.Identity.t =>
76
+ asIdentity({"userId": userId, "username": userId, "groups": ["User"], "provider": "Cognito"})
77
+
78
+ let owner = cognito(~userId="cust-A")
79
+ let stranger = cognito(~userId="cust-B")
80
+
81
+ // ─── Generator → producer path ──────────────────────────────────────────────
82
+
83
+ let generated: ref<array<Message.commandJson>> = ref([])
84
+
85
+ let generate = ReventlessCore.CommandGenerator_Callback.makeGenerateCommand(
86
+ ~publishJsons=async cmds => generated := generated.contents->Array.concat(cmds),
87
+ ~serviceName=OrderSlice.name,
88
+ ~commandSchema=OrderSlice.commandSchema->S.castToUnknown,
89
+ ~componentKind=ReventlessCore.CommandGenerator_Callback.StateChangeSlice,
90
+ ~stripIdFromParams=false,
91
+ )
92
+
93
+ let envelope = async (~command, ~args, ~identity) => {
94
+ generated := []
95
+ let p: ReventlessCore.CommandGenerator.payload = Obj.magic({
96
+ "command": command,
97
+ "arguments": Obj.magic(Dict.fromArray(args)),
98
+ "meta": {"ip": [], "user": "test", "info": ""},
99
+ "identity": identity,
100
+ })
101
+ let _ = await generate(p)->Effect.runPromise
102
+ generated.contents->Array.getUnsafe(0)
103
+ }
104
+
105
+ let handlerOver = (eventLog): CommandTopic_Helpers.jsonCommandsHandler =>
106
+ stream =>
107
+ OrderHandler.handleCommands(
108
+ ~partitionTag,
109
+ eventLog,
110
+ stream->Stream.map((item: CommandTopic_Helpers.topicItem<JSON.t>) => {
111
+ ReventlessInfra.CommandTopic.command: item.command->Message.decodeCommand'(
112
+ Reventless.Id.String.schema,
113
+ OrderSlice.commandSchema,
114
+ ),
115
+ reference: item.reference,
116
+ }),
117
+ )
118
+
119
+ // The wire answer a client gets for one command: (`__typename`, `errorCode`, `cause`).
120
+ let send = async (eventLog, cmd: Message.commandJson) => {
121
+ let outcomes = await CommandTopic_Helpers.runInlineAndCollect([cmd], handlerOver(eventLog))
122
+ let json = outcomes->Array.getUnsafe(0)->CommandTopic_Helpers.commandOutcomeToJson
123
+ let str = key =>
124
+ json
125
+ ->JSON.Decode.object
126
+ ->Option.flatMap(o => o->Dict.get(key))
127
+ ->Option.flatMap(JSON.Decode.string)
128
+ (str("__typename"), str("errorCode"), str("cause"))
129
+ }
130
+
131
+ let str = JSON.Encode.string
132
+ let place = (~identity) =>
133
+ envelope(
134
+ ~command="PlaceOrder",
135
+ ~args=[("orderId", str("o-1")), ("customerId", str("cust-A"))],
136
+ ~identity,
137
+ )
138
+ let cancel = (~identity) =>
139
+ envelope(~command="CancelOrder", ~args=[("orderId", str("o-1"))], ~identity)
140
+
141
+ // ─── Backends ───────────────────────────────────────────────────────────────
142
+
143
+ type appendMode = Real | AlwaysConflict | StoreFails
144
+
145
+ // One fresh event log per scenario; `mode` replaces the store's append to fail
146
+ // the way the platform can.
147
+ let eventLogOn = async (~name, ~mode) => {
148
+ module Bus = LocalBus.Make()
149
+ module Storage = LocalDcbEventLogStorage.Make(Bus)
150
+ let s = Storage.make(
151
+ ~name,
152
+ ~indexes=[],
153
+ ~partitionTag=Reventless.DcbTag.Simple({key: "orderId"}),
154
+ ~opts,
155
+ )
156
+ let real = await s.operations->TestRunner.resolve
157
+ let storage: ReventlessCore.DcbEventLog_Adapter.operations = switch mode {
158
+ | Real => real
159
+ | AlwaysConflict => {...real, append: async (_, ~condition as _=?) => Error(Conflict)}
160
+ | StoreFails => {
161
+ ...real,
162
+ append: async (_, ~condition as _=?) => Error(StorageFailure("disk unavailable")),
163
+ }
164
+ }
165
+ module Log = ReventlessCore.DcbEventLog_Operations.Make({
166
+ let name = name
167
+ let serviceName = name
168
+ let storage = storage
169
+ let publishJson = async (_, _, _) => ()
170
+ })
171
+ let eventLog: ReventlessCore.DcbEventLog.operations = {
172
+ read: Log.read,
173
+ append: Log.append,
174
+ readStream: Log.readStream,
175
+ appendStream: Log.appendStream,
176
+ }
177
+ eventLog
178
+ }
179
+
180
+ let runUnderMemory = async fn => {
181
+ BackendState.setMemory()
182
+ await fn()
183
+ }
184
+
185
+ let runUnderSqlite = async fn => {
186
+ let db = SqliteDriver.openDb(~path=":memory:")
187
+ BackendState.setSqlite(~db, ~path=":memory:")
188
+ await fn()
189
+ BackendState.setMemory()
190
+ db->SqliteDriver.close
191
+ }
192
+
193
+ let _ = beforeEach(() => {
194
+ Reventless.OwnerScope.setElevatedGroups(["Admin"])
195
+ OrderHandler.resetCache()
196
+ })
197
+
198
+ let counter = ref(0)
199
+ let fresh = prefix => {
200
+ counter := counter.contents + 1
201
+ `${prefix}-${counter.contents->Int.toString}`
202
+ }
203
+
204
+ let backends = [("in-memory", runUnderMemory), ("SQLite", runUnderSqlite)]
205
+
206
+ backends->Array.forEach(((label, runUnder)) =>
207
+ describe(`a rejection's cause (${label} backend)`, () => {
208
+ testPromise(
209
+ "the model's no: a decide error arrives with DOMAIN",
210
+ async () =>
211
+ await runUnder(
212
+ async () => {
213
+ let log = await eventLogOn(~name=fresh("CauseDomain"), ~mode=Real)
214
+ let first = await send(log, await place(~identity=owner))
215
+ expect(first)->toEqual((Some("CommandAccepted"), None, None))
216
+ let again = await send(log, await place(~identity=owner))
217
+ expect(again)->toEqual((
218
+ Some("CommandRejected"),
219
+ Some("OrderAlreadyPlaced"),
220
+ Some("DOMAIN"),
221
+ ))
222
+ },
223
+ ),
224
+ )
225
+
226
+ testPromise(
227
+ "the caller's no: an ownership refusal arrives with ACCESS",
228
+ async () =>
229
+ await runUnder(
230
+ async () => {
231
+ let log = await eventLogOn(~name=fresh("CauseAccess"), ~mode=Real)
232
+ let _ = await send(log, await place(~identity=owner))
233
+ let refused = await send(log, await cancel(~identity=stranger))
234
+ expect(refused)->toEqual((Some("CommandRejected"), Some("Forbidden"), Some("ACCESS")))
235
+ },
236
+ ),
237
+ )
238
+
239
+ testPromise(
240
+ "the platform's no: an exhausted retry loop arrives with INFRASTRUCTURE",
241
+ async () =>
242
+ await runUnder(
243
+ async () => {
244
+ let log = await eventLogOn(~name=fresh("CauseConflict"), ~mode=AlwaysConflict)
245
+ let failed = await send(log, await place(~identity=owner))
246
+ expect(failed)->toEqual((
247
+ Some("CommandRejected"),
248
+ Some("Conflict"),
249
+ Some("INFRASTRUCTURE"),
250
+ ))
251
+ },
252
+ ),
253
+ )
254
+
255
+ testPromise(
256
+ "the platform's no: a failing store arrives with INFRASTRUCTURE",
257
+ async () =>
258
+ await runUnder(
259
+ async () => {
260
+ let log = await eventLogOn(~name=fresh("CauseStore"), ~mode=StoreFails)
261
+ let failed = await send(log, await place(~identity=owner))
262
+ expect(failed)->toEqual((
263
+ Some("CommandRejected"),
264
+ Some("AppendFailed"),
265
+ Some("INFRASTRUCTURE"),
266
+ ))
267
+ },
268
+ ),
269
+ )
270
+ })
271
+ )
@@ -0,0 +1,440 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Effect from "effect/Effect";
8
+ import * as Stream from "effect/Stream";
9
+ import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
10
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
11
+ import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
12
+ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
13
+ import * as LocalBus$ReventlessLocal from "../../../src/adapter/LocalBus.res.mjs";
14
+ import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
15
+ import * as BackendState$ReventlessLocal from "../../../src/adapter/BackendState.res.mjs";
16
+ import * as SqliteDriver$ReventlessLocal from "../../../src/adapter/SqliteDriver.res.mjs";
17
+ import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
18
+ import * as DcbEventLog_Operations$ReventlessCore from "@reventlessdev/reventless-core/src/components/DcbEventLog/DcbEventLog_Operations.res.mjs";
19
+ import * as LocalDcbEventLogStorage$ReventlessLocal from "../../../src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs";
20
+ import * as CommandGenerator_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs";
21
+ import * as StateChangeSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs";
22
+
23
+ TestRunner$ReventlessLocal.setup();
24
+
25
+ let opts = {};
26
+
27
+ let name = "CauseOrders";
28
+
29
+ let moduleUrl = import.meta.url;
30
+
31
+ let eventSchema = Sury.union([
32
+ Sury.$schema(s => ({
33
+ TAG: "OrderPlaced",
34
+ orderId: s.m(DcbTag$Reventless.string),
35
+ customerId: s.m(Sury.string)
36
+ })),
37
+ Sury.$schema(s => ({
38
+ TAG: "OrderCancelled",
39
+ orderId: s.m(DcbTag$Reventless.string)
40
+ }))
41
+ ]);
42
+
43
+ let consumedEventSchema = Sury.union([
44
+ Sury.$schema(s => ({
45
+ TAG: "OrderPlaced",
46
+ orderId: s.m(DcbTag$Reventless.string),
47
+ customerId: s.m(Owner$Reventless.string)
48
+ })),
49
+ Sury.$schema(s => ({
50
+ TAG: "OrderCancelled",
51
+ orderId: s.m(DcbTag$Reventless.string)
52
+ }))
53
+ ]);
54
+
55
+ let commandSchema = Sury.union([
56
+ Sury.$schema(s => ({
57
+ TAG: "PlaceOrder",
58
+ orderId: s.m(DcbTag$Reventless.string),
59
+ customerId: s.m(Owner$Reventless.string)
60
+ })),
61
+ Sury.$schema(s => ({
62
+ TAG: "CancelOrder",
63
+ orderId: s.m(DcbTag$Reventless.string)
64
+ }))
65
+ ]);
66
+
67
+ let errorSchema = Sury.union([
68
+ Sury.literal("OrderAlreadyPlaced"),
69
+ Sury.literal("OrderNotFound")
70
+ ]);
71
+
72
+ function authorizationOf(param) {
73
+ return "AllowAuthenticated";
74
+ }
75
+
76
+ function commandTransition(param) {
77
+ return "Undeclared";
78
+ }
79
+
80
+ let traits = [];
81
+
82
+ let OrderSlice = {
83
+ name: name,
84
+ Id: undefined,
85
+ moduleUrl: moduleUrl,
86
+ eventSchema: eventSchema,
87
+ consumedEventSchema: consumedEventSchema,
88
+ commandSchema: commandSchema,
89
+ errorSchema: errorSchema,
90
+ authorizationOf: authorizationOf,
91
+ commandTransition: commandTransition,
92
+ traits: traits,
93
+ readConsistency: "EscalateOnRetry"
94
+ };
95
+
96
+ let initialState = {
97
+ placed: false,
98
+ cancelled: false
99
+ };
100
+
101
+ function evolve(state, event) {
102
+ if (event.TAG === "OrderPlaced") {
103
+ return {
104
+ placed: true,
105
+ cancelled: state.cancelled
106
+ };
107
+ } else {
108
+ return {
109
+ placed: state.placed,
110
+ cancelled: true
111
+ };
112
+ }
113
+ }
114
+
115
+ function decide(state, command) {
116
+ if (command.TAG === "PlaceOrder") {
117
+ if (state.placed) {
118
+ return {
119
+ TAG: "Error",
120
+ _0: "OrderAlreadyPlaced"
121
+ };
122
+ } else {
123
+ return {
124
+ TAG: "Ok",
125
+ _0: [{
126
+ TAG: "OrderPlaced",
127
+ orderId: command.orderId,
128
+ customerId: command.customerId
129
+ }]
130
+ };
131
+ }
132
+ } else if (state.placed) {
133
+ return {
134
+ TAG: "Ok",
135
+ _0: [{
136
+ TAG: "OrderCancelled",
137
+ orderId: command.orderId
138
+ }]
139
+ };
140
+ } else {
141
+ return {
142
+ TAG: "Error",
143
+ _0: "OrderNotFound"
144
+ };
145
+ }
146
+ }
147
+
148
+ let OrderDecision = {
149
+ initialState: initialState,
150
+ moduleUrl: moduleUrl,
151
+ evolve: evolve,
152
+ decide: decide
153
+ };
154
+
155
+ let OrderHandler = StateChangeSlice_Callback$ReventlessCore.Make({
156
+ name: name,
157
+ moduleUrl: moduleUrl,
158
+ consumedEventSchema: consumedEventSchema,
159
+ errorSchema: errorSchema,
160
+ eventSchema: eventSchema,
161
+ commandSchema: commandSchema,
162
+ authorizationOf: authorizationOf,
163
+ commandTransition: commandTransition,
164
+ traits: traits,
165
+ readConsistency: "EscalateOnRetry"
166
+ })({
167
+ initialState: initialState,
168
+ evolve: evolve,
169
+ decide: decide,
170
+ moduleUrl: moduleUrl
171
+ });
172
+
173
+ let partitionTag = {
174
+ TAG: "Simple",
175
+ _0: {
176
+ key: "orderId"
177
+ }
178
+ };
179
+
180
+ function cognito(userId) {
181
+ return {
182
+ userId: userId,
183
+ username: userId,
184
+ groups: ["User"],
185
+ provider: "Cognito"
186
+ };
187
+ }
188
+
189
+ let owner = {
190
+ userId: "cust-A",
191
+ username: "cust-A",
192
+ groups: ["User"],
193
+ provider: "Cognito"
194
+ };
195
+
196
+ let stranger = {
197
+ userId: "cust-B",
198
+ username: "cust-B",
199
+ groups: ["User"],
200
+ provider: "Cognito"
201
+ };
202
+
203
+ let generated = {
204
+ contents: []
205
+ };
206
+
207
+ let generate = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(async cmds => {
208
+ generated.contents = generated.contents.concat(cmds);
209
+ }, undefined, name, commandSchema, "StateChangeSlice", false, undefined);
210
+
211
+ async function envelope(command, args, identity) {
212
+ generated.contents = [];
213
+ let p = {
214
+ command: command,
215
+ arguments: Object.fromEntries(args),
216
+ meta: {
217
+ ip: [],
218
+ user: "test",
219
+ info: ""
220
+ },
221
+ identity: identity
222
+ };
223
+ await Effect.runPromise(generate(p));
224
+ return generated.contents[0];
225
+ }
226
+
227
+ function handlerOver(eventLog) {
228
+ return stream => OrderHandler.handleCommands(undefined, undefined, partitionTag, eventLog, Stream.map(stream, item => ({
229
+ command: Message$ReventlessCore.decodeCommand$p(item.command, Id$Reventless.$$String.schema, commandSchema),
230
+ reference: item.reference
231
+ })));
232
+ }
233
+
234
+ async function send(eventLog, cmd) {
235
+ let outcomes = await CommandTopic_Helpers$ReventlessCore.runInlineAndCollect([cmd], handlerOver(eventLog));
236
+ let json = CommandTopic_Helpers$ReventlessCore.commandOutcomeToJson(outcomes[0]);
237
+ let str = key => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o[key]), Stdlib_JSON.Decode.string);
238
+ return [
239
+ str("__typename"),
240
+ str("errorCode"),
241
+ str("cause")
242
+ ];
243
+ }
244
+
245
+ function str(prim) {
246
+ return prim;
247
+ }
248
+
249
+ function place(identity) {
250
+ return envelope("PlaceOrder", [
251
+ [
252
+ "orderId",
253
+ "o-1"
254
+ ],
255
+ [
256
+ "customerId",
257
+ "cust-A"
258
+ ]
259
+ ], identity);
260
+ }
261
+
262
+ function cancel(identity) {
263
+ return envelope("CancelOrder", [[
264
+ "orderId",
265
+ "o-1"
266
+ ]], identity);
267
+ }
268
+
269
+ async function eventLogOn(name, mode) {
270
+ let Bus = LocalBus$ReventlessLocal.Make({});
271
+ let Storage = LocalDcbEventLogStorage$ReventlessLocal.Make(Bus);
272
+ let s = Storage.make(name, [], {
273
+ TAG: "Simple",
274
+ _0: {
275
+ key: "orderId"
276
+ }
277
+ }, undefined, opts);
278
+ let real = await TestRunner$ReventlessLocal.resolve(s.operations);
279
+ let storage;
280
+ switch (mode) {
281
+ case "Real" :
282
+ storage = real;
283
+ break;
284
+ case "AlwaysConflict" :
285
+ storage = {
286
+ read: real.read,
287
+ append: async (param, param$1) => ({
288
+ TAG: "Error",
289
+ _0: "Conflict"
290
+ }),
291
+ readStream: real.readStream
292
+ };
293
+ break;
294
+ case "StoreFails" :
295
+ storage = {
296
+ read: real.read,
297
+ append: async (param, param$1) => ({
298
+ TAG: "Error",
299
+ _0: {
300
+ TAG: "StorageFailure",
301
+ _0: "disk unavailable"
302
+ }
303
+ }),
304
+ readStream: real.readStream
305
+ };
306
+ break;
307
+ }
308
+ let publishJson = async (param, param$1, param$2) => {};
309
+ let Log = DcbEventLog_Operations$ReventlessCore.Make({
310
+ name: name,
311
+ serviceName: name,
312
+ storage: storage,
313
+ publishJson: publishJson
314
+ });
315
+ return {
316
+ read: Log.read,
317
+ append: Log.append,
318
+ readStream: Log.readStream,
319
+ appendStream: Log.appendStream
320
+ };
321
+ }
322
+
323
+ async function runUnderMemory(fn) {
324
+ BackendState$ReventlessLocal.setMemory();
325
+ return await fn();
326
+ }
327
+
328
+ async function runUnderSqlite(fn) {
329
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
330
+ BackendState$ReventlessLocal.setSqlite(db, ":memory:");
331
+ await fn();
332
+ BackendState$ReventlessLocal.setMemory();
333
+ return SqliteDriver$ReventlessLocal.close(db);
334
+ }
335
+
336
+ globalThis.beforeEach(() => {
337
+ OwnerScope$Reventless.setElevatedGroups(["Admin"]);
338
+ OrderHandler.resetCache();
339
+ });
340
+
341
+ let counter = {
342
+ contents: 0
343
+ };
344
+
345
+ function fresh(prefix) {
346
+ counter.contents = counter.contents + 1 | 0;
347
+ return prefix + `-` + counter.contents.toString();
348
+ }
349
+
350
+ let backends = [
351
+ [
352
+ "in-memory",
353
+ runUnderMemory
354
+ ],
355
+ [
356
+ "SQLite",
357
+ runUnderSqlite
358
+ ]
359
+ ];
360
+
361
+ backends.forEach(param => {
362
+ let runUnder = param[1];
363
+ globalThis.describe(`a rejection's cause (` + param[0] + ` backend)`, () => {
364
+ globalThis.test("the model's no: a decide error arrives with DOMAIN", async () => await runUnder(async () => {
365
+ let log = await eventLogOn(fresh("CauseDomain"), "Real");
366
+ let first = await send(log, await place(owner));
367
+ globalThis.expect(first).toEqual([
368
+ "CommandAccepted",
369
+ undefined,
370
+ undefined
371
+ ]);
372
+ let again = await send(log, await place(owner));
373
+ globalThis.expect(again).toEqual([
374
+ "CommandRejected",
375
+ "OrderAlreadyPlaced",
376
+ "DOMAIN"
377
+ ]);
378
+ }));
379
+ globalThis.test("the caller's no: an ownership refusal arrives with ACCESS", async () => await runUnder(async () => {
380
+ let log = await eventLogOn(fresh("CauseAccess"), "Real");
381
+ await send(log, await place(owner));
382
+ let refused = await send(log, await cancel(stranger));
383
+ globalThis.expect(refused).toEqual([
384
+ "CommandRejected",
385
+ "Forbidden",
386
+ "ACCESS"
387
+ ]);
388
+ }));
389
+ globalThis.test("the platform's no: an exhausted retry loop arrives with INFRASTRUCTURE", async () => await runUnder(async () => {
390
+ let log = await eventLogOn(fresh("CauseConflict"), "AlwaysConflict");
391
+ let failed = await send(log, await place(owner));
392
+ globalThis.expect(failed).toEqual([
393
+ "CommandRejected",
394
+ "Conflict",
395
+ "INFRASTRUCTURE"
396
+ ]);
397
+ }));
398
+ globalThis.test("the platform's no: a failing store arrives with INFRASTRUCTURE", async () => await runUnder(async () => {
399
+ let log = await eventLogOn(fresh("CauseStore"), "StoreFails");
400
+ let failed = await send(log, await place(owner));
401
+ globalThis.expect(failed).toEqual([
402
+ "CommandRejected",
403
+ "AppendFailed",
404
+ "INFRASTRUCTURE"
405
+ ]);
406
+ }));
407
+ });
408
+ });
409
+
410
+ let Message;
411
+
412
+ let CommandTopic_Helpers;
413
+
414
+ export {
415
+ opts,
416
+ Message,
417
+ CommandTopic_Helpers,
418
+ OrderSlice,
419
+ OrderDecision,
420
+ OrderHandler,
421
+ partitionTag,
422
+ cognito,
423
+ owner,
424
+ stranger,
425
+ generated,
426
+ generate,
427
+ envelope,
428
+ handlerOver,
429
+ send,
430
+ str,
431
+ place,
432
+ cancel,
433
+ eventLogOn,
434
+ runUnderMemory,
435
+ runUnderSqlite,
436
+ counter,
437
+ fresh,
438
+ backends,
439
+ }
440
+ /* Not a pure module */