@reventlessdev/reventless-core 3.0.0-alpha.302 → 3.0.0-alpha.303
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 +13 -0
- package/package.json +6 -6
- package/src/components/Aggregate/Aggregate_Callback.res +1 -1
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +2 -1
- package/src/components/Api/GraphQL_FragmentGenerator.res +9 -0
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +6 -0
- package/src/components/CommandTopic/CommandTopic.res.mjs +3 -0
- package/src/components/CommandTopic/CommandTopic_Helpers.res +33 -15
- package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +34 -11
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res +20 -3
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs +18 -4
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +3 -3
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -3
- package/tests/aggregate/AggregateRejectionTest.res +8 -1
- package/tests/aggregate/AggregateRejectionTest.res.mjs +9 -1
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +54 -2
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res.mjs +51 -0
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.303 (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.302 (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-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.303",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -28,17 +28,17 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"sury": "11.0.0",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
|
-
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
32
31
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.15",
|
|
33
|
-
"@reventlessdev/rescript-
|
|
32
|
+
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
34
33
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
34
|
+
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.23",
|
|
35
35
|
"@reventlessdev/rescript-node": "2.0.0-alpha.23",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
37
|
-
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.24",
|
|
38
37
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
|
|
39
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
38
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.200",
|
|
39
|
+
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.24",
|
|
40
40
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.42",
|
|
41
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.172"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.1",
|
|
@@ -392,7 +392,7 @@ module Make = (
|
|
|
392
392
|
~component=Spec.name,
|
|
393
393
|
~cause=InfrastructureFailure,
|
|
394
394
|
reference,
|
|
395
|
-
{errorCode:
|
|
395
|
+
{errorCode: Reventless.CommandOutcome.code(AppendFailed), errorDetail: appendErrorDetail},
|
|
396
396
|
)
|
|
397
397
|
Error(reference)
|
|
398
398
|
}
|
|
@@ -15,6 +15,7 @@ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/U
|
|
|
15
15
|
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
16
16
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
17
17
|
import * as LogFormat$ReventlessCore from "../../util/LogFormat.res.mjs";
|
|
18
|
+
import * as CommandOutcome$Reventless from "@reventlessdev/reventless-spec/src/components/CommandOutcome.res.mjs";
|
|
18
19
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
19
20
|
import * as SchemaWalker$ReventlessCore from "../../plugin/component/SchemaWalker.res.mjs";
|
|
20
21
|
import * as CommandTopic_Helpers$ReventlessCore from "../CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
@@ -254,7 +255,7 @@ function Make(Spec) {
|
|
|
254
255
|
};
|
|
255
256
|
} else {
|
|
256
257
|
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "InfrastructureFailure", reference, {
|
|
257
|
-
errorCode: "AppendFailed",
|
|
258
|
+
errorCode: CommandOutcome$Reventless.code("AppendFailed"),
|
|
258
259
|
errorDetail: appendErrorDetail
|
|
259
260
|
});
|
|
260
261
|
return {
|
|
@@ -12,17 +12,26 @@ let log = Logger.fromEnv()
|
|
|
12
12
|
// fragments. These SDL declarations are injected into any fragment that emits
|
|
13
13
|
// at least one mutation field; the stitcher dedupes by type name so
|
|
14
14
|
// concatenating across plugin + admin fragments stays safe.
|
|
15
|
+
//
|
|
16
|
+
// `cause` says which no a rejection is: the model's (DOMAIN, `errorCode` is the
|
|
17
|
+
// declared error), the caller's (ACCESS) or the platform's (INFRASTRUCTURE).
|
|
15
18
|
let commandResultSdlTypes: array<string> = [
|
|
16
19
|
`union CommandResult = CommandAccepted | CommandRejected | CommandPending`,
|
|
17
20
|
`type CommandAccepted {
|
|
18
21
|
msgId: ID!
|
|
19
22
|
entityId: ID
|
|
20
23
|
eventCount: Int!
|
|
24
|
+
}`,
|
|
25
|
+
`enum CommandRejectionCause {
|
|
26
|
+
DOMAIN
|
|
27
|
+
ACCESS
|
|
28
|
+
INFRASTRUCTURE
|
|
21
29
|
}`,
|
|
22
30
|
`type CommandRejected {
|
|
23
31
|
msgId: ID!
|
|
24
32
|
errorCode: String!
|
|
25
33
|
errorDetail: String
|
|
34
|
+
cause: CommandRejectionCause!
|
|
26
35
|
}`,
|
|
27
36
|
`type CommandPending {
|
|
28
37
|
msgId: ID!
|
|
@@ -22,11 +22,17 @@ let commandResultSdlTypes = [
|
|
|
22
22
|
msgId: ID!
|
|
23
23
|
entityId: ID
|
|
24
24
|
eventCount: Int!
|
|
25
|
+
}`,
|
|
26
|
+
`enum CommandRejectionCause {
|
|
27
|
+
DOMAIN
|
|
28
|
+
ACCESS
|
|
29
|
+
INFRASTRUCTURE
|
|
25
30
|
}`,
|
|
26
31
|
`type CommandRejected {
|
|
27
32
|
msgId: ID!
|
|
28
33
|
errorCode: String!
|
|
29
34
|
errorDetail: String
|
|
35
|
+
cause: CommandRejectionCause!
|
|
30
36
|
}`,
|
|
31
37
|
`type CommandPending {
|
|
32
38
|
msgId: ID!
|
|
@@ -23,6 +23,8 @@ function filter(allCommandTopics, names) {
|
|
|
23
23
|
|
|
24
24
|
let componentType = "CommandTopic";
|
|
25
25
|
|
|
26
|
+
let refusalCauseToGraphQL = CommandTopic_Helpers$ReventlessCore.refusalCauseToGraphQL;
|
|
27
|
+
|
|
26
28
|
let commandOutcomeHook = CommandTopic_Helpers$ReventlessCore.commandOutcomeHook;
|
|
27
29
|
|
|
28
30
|
let registerCommandOutcome = CommandTopic_Helpers$ReventlessCore.registerCommandOutcome;
|
|
@@ -60,6 +62,7 @@ let getHandlers = CommandTopic_Helpers$ReventlessCore.getHandlers;
|
|
|
60
62
|
export {
|
|
61
63
|
componentType,
|
|
62
64
|
NotPublishedToChannel,
|
|
65
|
+
refusalCauseToGraphQL,
|
|
63
66
|
commandOutcomeHook,
|
|
64
67
|
registerCommandOutcome,
|
|
65
68
|
clearCommandOutcome,
|
|
@@ -31,6 +31,14 @@ own what the command acts on (`OwnerScope.decideActing`).
|
|
|
31
31
|
*/
|
|
32
32
|
type refusalCause = DomainRejection | InfrastructureFailure | AccessRefusal
|
|
33
33
|
|
|
34
|
+
/** The `CommandRejectionCause` enum value a client sees for a cause. */
|
|
35
|
+
let refusalCauseToGraphQL = (cause: refusalCause): string =>
|
|
36
|
+
switch cause {
|
|
37
|
+
| DomainRejection => "DOMAIN"
|
|
38
|
+
| AccessRefusal => "ACCESS"
|
|
39
|
+
| InfrastructureFailure => "INFRASTRUCTURE"
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
/**
|
|
35
43
|
What became of one command. Constructors are prefixed to stay clear of `commandOutcome`'s
|
|
36
44
|
`Accepted`/`Rejected` below — that type is the producer-facing result shape (and the GraphQL
|
|
@@ -91,10 +99,9 @@ let fireCommandOutcome = (~component: string, ~reference: string, outcome: obser
|
|
|
91
99
|
// Callback implementations call reportAccepted during inline dispatch.
|
|
92
100
|
let acceptedResultChannel: ref<option<(string, acceptedResult) => unit>> = ref(None)
|
|
93
101
|
|
|
94
|
-
// Sibling side-channel for
|
|
95
|
-
// `
|
|
96
|
-
|
|
97
|
-
let rejectedResultChannel: ref<option<(string, rejectedResult) => unit>> = ref(None)
|
|
102
|
+
// Sibling side-channel for rejections, with their cause. Read by runInlineAndCollect
|
|
103
|
+
// to build `Rejected` outcomes that carry the real error code, detail and cause.
|
|
104
|
+
let rejectedResultChannel: ref<option<(string, rejectedResult, refusalCause) => unit>> = ref(None)
|
|
98
105
|
|
|
99
106
|
/**
|
|
100
107
|
Turn an `acting` verdict into the refusal a command handler reports, or `None` to let
|
|
@@ -112,7 +119,10 @@ let ownershipRefusal = (
|
|
|
112
119
|
~command: string,
|
|
113
120
|
): option<ownershipRefusal> => {
|
|
114
121
|
let refuse = (~isDefect=false, why) => Some({
|
|
115
|
-
rejected: {
|
|
122
|
+
rejected: {
|
|
123
|
+
errorCode: Reventless.CommandOutcome.code(Forbidden),
|
|
124
|
+
errorDetail: `${component}.${command}: ${why}`,
|
|
125
|
+
},
|
|
116
126
|
isDefect,
|
|
117
127
|
})
|
|
118
128
|
switch verdict {
|
|
@@ -148,7 +158,7 @@ let reportRejected = (
|
|
|
148
158
|
reference: string,
|
|
149
159
|
result: rejectedResult,
|
|
150
160
|
) => {
|
|
151
|
-
rejectedResultChannel.contents->Option.forEach(cb => cb(reference, result))
|
|
161
|
+
rejectedResultChannel.contents->Option.forEach(cb => cb(reference, result, cause))
|
|
152
162
|
fireCommandOutcome(
|
|
153
163
|
~component,
|
|
154
164
|
~reference,
|
|
@@ -160,7 +170,7 @@ let reportRejected = (
|
|
|
160
170
|
// without importing Adapter.res → @pulumi/pulumi. Re-exported by CommandTopic via include.
|
|
161
171
|
type commandOutcome =
|
|
162
172
|
| Accepted({msgId: string, entityId?: string, eventCount: int})
|
|
163
|
-
| Rejected({msgId: string, errorCode: string, errorDetail: option<string
|
|
173
|
+
| Rejected({msgId: string, errorCode: string, errorDetail: option<string>, cause: refusalCause})
|
|
164
174
|
| Pending({msgId: string})
|
|
165
175
|
|
|
166
176
|
// Serialise a commandOutcome to the JSON shape the CommandResult GraphQL union
|
|
@@ -179,13 +189,14 @@ let commandOutcomeToJson = (outcome: commandOutcome): JSON.t =>
|
|
|
179
189
|
("eventCount", JSON.Number(eventCount->Int.toFloat)),
|
|
180
190
|
]),
|
|
181
191
|
)
|
|
182
|
-
| Rejected({msgId, errorCode, errorDetail}) =>
|
|
192
|
+
| Rejected({msgId, errorCode, errorDetail, cause}) =>
|
|
183
193
|
JSON.Object(
|
|
184
194
|
Dict.fromArray([
|
|
185
195
|
("__typename", JSON.String("CommandRejected")),
|
|
186
196
|
("msgId", JSON.String(msgId)),
|
|
187
197
|
("errorCode", JSON.String(errorCode)),
|
|
188
198
|
("errorDetail", errorDetail->Option.map(s => JSON.String(s))->Option.getOr(JSON.Null)),
|
|
199
|
+
("cause", JSON.String(refusalCauseToGraphQL(cause))),
|
|
189
200
|
]),
|
|
190
201
|
)
|
|
191
202
|
| Pending({msgId}) =>
|
|
@@ -222,27 +233,34 @@ let runInlineAndCollect = async (
|
|
|
222
233
|
item
|
|
223
234
|
})
|
|
224
235
|
let acceptedResults: dict<acceptedResult> = Dict.make()
|
|
225
|
-
let rejectedResults: dict<rejectedResult> = Dict.make()
|
|
236
|
+
let rejectedResults: dict<(rejectedResult, refusalCause)> = Dict.make()
|
|
226
237
|
acceptedResultChannel.contents = Some(
|
|
227
238
|
(reference, result) => acceptedResults->Dict.set(reference, result),
|
|
228
239
|
)
|
|
229
240
|
rejectedResultChannel.contents = Some(
|
|
230
|
-
(reference, result) => rejectedResults->Dict.set(reference, result),
|
|
241
|
+
(reference, result, cause) => rejectedResults->Dict.set(reference, (result, cause)),
|
|
231
242
|
)
|
|
232
243
|
let results = await handleCmds(Stream.fromIterable(items))->Effect.runPromise
|
|
233
244
|
acceptedResultChannel.contents = None
|
|
234
245
|
rejectedResultChannel.contents = None
|
|
235
246
|
jsons->Array.mapWithIndex((cmdJson, i) => {
|
|
236
247
|
let msgId = cmdJson.meta.msgId
|
|
237
|
-
//
|
|
238
|
-
//
|
|
248
|
+
// A reported rejection takes precedence over both Accepted and the
|
|
249
|
+
// synthesized "Conflict" — it carries the most informative error.
|
|
239
250
|
switch rejectedResults->Dict.get(msgId) {
|
|
240
|
-
| Some({errorCode, errorDetail}) =>
|
|
251
|
+
| Some(({errorCode, errorDetail}, cause)) =>
|
|
241
252
|
let detail = errorDetail == "" ? None : Some(errorDetail)
|
|
242
|
-
Rejected({msgId, errorCode, errorDetail: detail})
|
|
253
|
+
Rejected({msgId, errorCode, errorDetail: detail, cause})
|
|
243
254
|
| None =>
|
|
244
255
|
switch results->Array.get(i) {
|
|
245
|
-
|
|
256
|
+
// The handler failed outright: the platform, not the model.
|
|
257
|
+
| Some(Error(msg)) =>
|
|
258
|
+
Rejected({
|
|
259
|
+
msgId,
|
|
260
|
+
errorCode: Reventless.CommandOutcome.code(Conflict),
|
|
261
|
+
errorDetail: Some(msg),
|
|
262
|
+
cause: InfrastructureFailure,
|
|
263
|
+
})
|
|
246
264
|
| _ =>
|
|
247
265
|
let ar = acceptedResults->Dict.get(msgId)->Option.getOr({eventCount: 0})
|
|
248
266
|
switch ar.entityId {
|
|
@@ -8,8 +8,20 @@ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/component
|
|
|
8
8
|
import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
|
|
9
9
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
10
10
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
11
|
+
import * as CommandOutcome$Reventless from "@reventlessdev/reventless-spec/src/components/CommandOutcome.res.mjs";
|
|
11
12
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
12
13
|
|
|
14
|
+
function refusalCauseToGraphQL(cause) {
|
|
15
|
+
switch (cause) {
|
|
16
|
+
case "DomainRejection" :
|
|
17
|
+
return "DOMAIN";
|
|
18
|
+
case "InfrastructureFailure" :
|
|
19
|
+
return "INFRASTRUCTURE";
|
|
20
|
+
case "AccessRefusal" :
|
|
21
|
+
return "ACCESS";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
let commandOutcomeHook = {
|
|
14
26
|
contents: undefined
|
|
15
27
|
};
|
|
@@ -51,7 +63,7 @@ function ownershipRefusal(verdict, component, command) {
|
|
|
51
63
|
let isDefect = isDefectOpt !== undefined ? isDefectOpt : false;
|
|
52
64
|
return {
|
|
53
65
|
rejected: {
|
|
54
|
-
errorCode: "Forbidden",
|
|
66
|
+
errorCode: CommandOutcome$Reventless.code("Forbidden"),
|
|
55
67
|
errorDetail: component + `.` + command + `: ` + why
|
|
56
68
|
},
|
|
57
69
|
isDefect: isDefect
|
|
@@ -84,7 +96,7 @@ function reportAccepted(component, reference, result) {
|
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
function reportRejected(component, cause, reference, result) {
|
|
87
|
-
Stdlib_Option.forEach(rejectedResultChannel.contents, cb => cb(reference, result));
|
|
99
|
+
Stdlib_Option.forEach(rejectedResultChannel.contents, cb => cb(reference, result, cause));
|
|
88
100
|
fireCommandOutcome(component, reference, {
|
|
89
101
|
TAG: "OutcomeRejected",
|
|
90
102
|
errorCode: result.errorCode,
|
|
@@ -131,6 +143,10 @@ function commandOutcomeToJson(outcome) {
|
|
|
131
143
|
[
|
|
132
144
|
"errorDetail",
|
|
133
145
|
Stdlib_Option.getOr(Stdlib_Option.map(outcome.errorDetail, s => (s)), null)
|
|
146
|
+
],
|
|
147
|
+
[
|
|
148
|
+
"cause",
|
|
149
|
+
refusalCauseToGraphQL(outcome.cause)
|
|
134
150
|
]
|
|
135
151
|
]);
|
|
136
152
|
case "Pending" :
|
|
@@ -177,8 +193,11 @@ async function runInlineAndCollect(jsons, handleCmds) {
|
|
|
177
193
|
acceptedResultChannel.contents = (reference, result) => {
|
|
178
194
|
acceptedResults[reference] = result;
|
|
179
195
|
};
|
|
180
|
-
rejectedResultChannel.contents = (reference, result) => {
|
|
181
|
-
rejectedResults[reference] =
|
|
196
|
+
rejectedResultChannel.contents = (reference, result, cause) => {
|
|
197
|
+
rejectedResults[reference] = [
|
|
198
|
+
result,
|
|
199
|
+
cause
|
|
200
|
+
];
|
|
182
201
|
};
|
|
183
202
|
let results = await Effect.runPromise(handleCmds(Stream.fromIterable(items)));
|
|
184
203
|
acceptedResultChannel.contents = undefined;
|
|
@@ -187,22 +206,25 @@ async function runInlineAndCollect(jsons, handleCmds) {
|
|
|
187
206
|
let msgId = cmdJson.meta.msgId;
|
|
188
207
|
let match = rejectedResults[msgId];
|
|
189
208
|
if (match !== undefined) {
|
|
190
|
-
let
|
|
209
|
+
let match$1 = match[0];
|
|
210
|
+
let errorDetail = match$1.errorDetail;
|
|
191
211
|
let detail = errorDetail === "" ? undefined : errorDetail;
|
|
192
212
|
return {
|
|
193
213
|
TAG: "Rejected",
|
|
194
214
|
msgId: msgId,
|
|
195
|
-
errorCode: match.errorCode,
|
|
196
|
-
errorDetail: detail
|
|
215
|
+
errorCode: match$1.errorCode,
|
|
216
|
+
errorDetail: detail,
|
|
217
|
+
cause: match[1]
|
|
197
218
|
};
|
|
198
219
|
}
|
|
199
|
-
let match$
|
|
200
|
-
if (match$
|
|
220
|
+
let match$2 = results[i];
|
|
221
|
+
if (match$2 !== undefined && match$2.TAG !== "Ok") {
|
|
201
222
|
return {
|
|
202
223
|
TAG: "Rejected",
|
|
203
224
|
msgId: msgId,
|
|
204
|
-
errorCode: "Conflict",
|
|
205
|
-
errorDetail: match$
|
|
225
|
+
errorCode: CommandOutcome$Reventless.code("Conflict"),
|
|
226
|
+
errorDetail: match$2._0,
|
|
227
|
+
cause: "InfrastructureFailure"
|
|
206
228
|
};
|
|
207
229
|
}
|
|
208
230
|
let ar = Stdlib_Option.getOr(acceptedResults[msgId], {
|
|
@@ -249,6 +271,7 @@ function getHandlers(typeName) {
|
|
|
249
271
|
}
|
|
250
272
|
|
|
251
273
|
export {
|
|
274
|
+
refusalCauseToGraphQL,
|
|
252
275
|
commandOutcomeHook,
|
|
253
276
|
registerCommandOutcome,
|
|
254
277
|
clearCommandOutcome,
|
|
@@ -19,6 +19,17 @@ type auditRow = {
|
|
|
19
19
|
door?: ReventlessInfra.InboundTranslationSlice.door,
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** A refused caller is access, a failed publish the platform; anything else is
|
|
23
|
+
the translation refusing its input. */
|
|
24
|
+
let rejectionCauseOf = (errorCode: string): CommandTopic_Helpers.refusalCause =>
|
|
25
|
+
if errorCode == Reventless.CommandOutcome.code(Forbidden) {
|
|
26
|
+
AccessRefusal
|
|
27
|
+
} else if errorCode == Reventless.CommandOutcome.code(PublishFailed) {
|
|
28
|
+
InfrastructureFailure
|
|
29
|
+
} else {
|
|
30
|
+
DomainRejection
|
|
31
|
+
}
|
|
32
|
+
|
|
22
33
|
/** The `CommandResult` a `receive` outcome answers with: the first target as
|
|
23
34
|
`entityId` (omitted when nothing was produced), the fan-out as `eventCount`. */
|
|
24
35
|
let receiveResultToOutcome = (
|
|
@@ -31,10 +42,12 @@ let receiveResultToOutcome = (
|
|
|
31
42
|
| None => Accepted({msgId: requestId, eventCount: commandCount})
|
|
32
43
|
}
|
|
33
44
|
| Error({requestId, error, ?errorCode}) =>
|
|
45
|
+
let errorCode = errorCode->Option.getOr(Reventless.CommandOutcome.code(TranslationFailed))
|
|
34
46
|
Rejected({
|
|
35
47
|
msgId: requestId,
|
|
36
|
-
errorCode
|
|
48
|
+
errorCode,
|
|
37
49
|
errorDetail: Some(error),
|
|
50
|
+
cause: rejectionCauseOf(errorCode),
|
|
38
51
|
})
|
|
39
52
|
}
|
|
40
53
|
|
|
@@ -216,7 +229,7 @@ module Make = (
|
|
|
216
229
|
)
|
|
217
230
|
) =>
|
|
218
231
|
fail(
|
|
219
|
-
~errorCode=
|
|
232
|
+
~errorCode=Reventless.CommandOutcome.code(Forbidden),
|
|
220
233
|
`${Spec.name}: the caller is not authorized for every command this input translates into`,
|
|
221
234
|
)
|
|
222
235
|
| Ok([]) => succeed([])
|
|
@@ -225,7 +238,11 @@ module Make = (
|
|
|
225
238
|
await publishJsons(msgs)
|
|
226
239
|
succeed(pairs->Array.map(((targetId, _)) => targetId))
|
|
227
240
|
} catch {
|
|
228
|
-
| exn =>
|
|
241
|
+
| exn =>
|
|
242
|
+
fail(
|
|
243
|
+
~errorCode=Reventless.CommandOutcome.code(PublishFailed),
|
|
244
|
+
messageOf(exn, "publish failed"),
|
|
245
|
+
)
|
|
229
246
|
}
|
|
230
247
|
}
|
|
231
248
|
}
|
|
@@ -16,6 +16,7 @@ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/U
|
|
|
16
16
|
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
17
17
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
18
18
|
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
19
|
+
import * as CommandOutcome$Reventless from "@reventlessdev/reventless-spec/src/components/CommandOutcome.res.mjs";
|
|
19
20
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
20
21
|
import * as ApiNoApiHelpers$ReventlessCore from "../Api/ApiNoApiHelpers.res.mjs";
|
|
21
22
|
import * as InboundTranslationSlice$ReventlessInfra from "@reventlessdev/reventless-infra/src/components/InboundTranslationSlice.res.mjs";
|
|
@@ -36,6 +37,16 @@ let auditRowSchema = Sury.$schema(s => ({
|
|
|
36
37
|
door: s.m(Sury.$option(InboundTranslationSlice$ReventlessInfra.doorSchema))
|
|
37
38
|
}));
|
|
38
39
|
|
|
40
|
+
function rejectionCauseOf(errorCode) {
|
|
41
|
+
if (errorCode === CommandOutcome$Reventless.code("Forbidden")) {
|
|
42
|
+
return "AccessRefusal";
|
|
43
|
+
} else if (errorCode === CommandOutcome$Reventless.code("PublishFailed")) {
|
|
44
|
+
return "InfrastructureFailure";
|
|
45
|
+
} else {
|
|
46
|
+
return "DomainRejection";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
function receiveResultToOutcome(result) {
|
|
40
51
|
if (result.TAG === "Ok") {
|
|
41
52
|
let match = result._0;
|
|
@@ -58,11 +69,13 @@ function receiveResultToOutcome(result) {
|
|
|
58
69
|
}
|
|
59
70
|
}
|
|
60
71
|
let match$1 = result._0;
|
|
72
|
+
let errorCode = Stdlib_Option.getOr(match$1.errorCode, CommandOutcome$Reventless.code("TranslationFailed"));
|
|
61
73
|
return {
|
|
62
74
|
TAG: "Rejected",
|
|
63
75
|
msgId: match$1.requestId,
|
|
64
|
-
errorCode:
|
|
65
|
-
errorDetail: match$1.error
|
|
76
|
+
errorCode: errorCode,
|
|
77
|
+
errorDetail: match$1.error,
|
|
78
|
+
cause: rejectionCauseOf(errorCode)
|
|
66
79
|
};
|
|
67
80
|
}
|
|
68
81
|
|
|
@@ -213,7 +226,7 @@ function Make(Spec) {
|
|
|
213
226
|
}
|
|
214
227
|
let msgs = msg$1._0;
|
|
215
228
|
if (msgs.some(param => !callerAdmits(Authorization$Reventless.named(Spec.authorizationOf(Message$Reventless.variantNameOfJson(param.commandJson))), caller))) {
|
|
216
|
-
return fail("Forbidden", Spec.name + `: the caller is not authorized for every command this input translates into`);
|
|
229
|
+
return fail(CommandOutcome$Reventless.code("Forbidden"), Spec.name + `: the caller is not authorized for every command this input translates into`);
|
|
217
230
|
}
|
|
218
231
|
if (msgs.length === 0) {
|
|
219
232
|
return succeed([]);
|
|
@@ -223,7 +236,7 @@ function Make(Spec) {
|
|
|
223
236
|
return succeed(pairs.map(param => param[0]));
|
|
224
237
|
} catch (raw_exn$1) {
|
|
225
238
|
let exn$1 = Primitive_exceptions.internalToException(raw_exn$1);
|
|
226
|
-
return fail("PublishFailed", messageOf(exn$1, "publish failed"));
|
|
239
|
+
return fail(CommandOutcome$Reventless.code("PublishFailed"), messageOf(exn$1, "publish failed"));
|
|
227
240
|
}
|
|
228
241
|
};
|
|
229
242
|
let refuse = (body, errorCode, error) => failWith(Uuid.v4(), body, "Webhook", errorCode, error);
|
|
@@ -241,6 +254,7 @@ function Make(Spec) {
|
|
|
241
254
|
export {
|
|
242
255
|
auditStatusSchema,
|
|
243
256
|
auditRowSchema,
|
|
257
|
+
rejectionCauseOf,
|
|
244
258
|
receiveResultToOutcome,
|
|
245
259
|
requestIdOf,
|
|
246
260
|
takeAuditRow,
|
|
@@ -478,10 +478,10 @@ module Make = (
|
|
|
478
478
|
// which disagreed with the backends' casing).
|
|
479
479
|
let (errorCode, errDetail) = switch err {
|
|
480
480
|
| ReventlessInfra.DcbEventLog.Conflict => (
|
|
481
|
-
|
|
481
|
+
Reventless.CommandOutcome.code(Conflict),
|
|
482
482
|
"conflict: condition check failed",
|
|
483
483
|
)
|
|
484
|
-
| StorageFailure(msg) => (
|
|
484
|
+
| StorageFailure(msg) => (Reventless.CommandOutcome.code(AppendFailed), msg)
|
|
485
485
|
}
|
|
486
486
|
if retries > 0 {
|
|
487
487
|
// Re-seed from the just-read snapshot so the retry reads only
|
|
@@ -504,7 +504,7 @@ module Make = (
|
|
|
504
504
|
// A surfaced Conflict means the 3-retry loop could not absorb
|
|
505
505
|
// the contention — the operator signal for "this slice is hot"
|
|
506
506
|
// (consider sharding / async — Issue 10, §4 of the analysis).
|
|
507
|
-
if errorCode ==
|
|
507
|
+
if errorCode == Reventless.CommandOutcome.code(Conflict) {
|
|
508
508
|
Metrics.emitCount(~metric="AppendConflict", ~slice=Spec.name)
|
|
509
509
|
}
|
|
510
510
|
// Both arms here are storage, not the model: a surfaced Conflict is
|
|
@@ -17,6 +17,7 @@ import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types
|
|
|
17
17
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
18
18
|
import * as Metrics$ReventlessCore from "../../util/Metrics.res.mjs";
|
|
19
19
|
import * as LogFormat$ReventlessCore from "../../util/LogFormat.res.mjs";
|
|
20
|
+
import * as CommandOutcome$Reventless from "@reventlessdev/reventless-spec/src/components/CommandOutcome.res.mjs";
|
|
20
21
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
21
22
|
import * as CommandTopic_Helpers$ReventlessCore from "../CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
22
23
|
|
|
@@ -311,10 +312,10 @@ function Make(Spec) {
|
|
|
311
312
|
let err = appendResult._0;
|
|
312
313
|
let match;
|
|
313
314
|
match = typeof err !== "object" ? [
|
|
314
|
-
"Conflict",
|
|
315
|
+
CommandOutcome$Reventless.code("Conflict"),
|
|
315
316
|
"conflict: condition check failed"
|
|
316
317
|
] : [
|
|
317
|
-
"AppendFailed",
|
|
318
|
+
CommandOutcome$Reventless.code("AppendFailed"),
|
|
318
319
|
err._0
|
|
319
320
|
];
|
|
320
321
|
let errDetail = match[1];
|
|
@@ -329,7 +330,7 @@ function Make(Spec) {
|
|
|
329
330
|
return Effect.flatMap(EffectLogger$ReventlessCore.logWarn(comp, undefined, `append failed (retrying ` + ((3 - retries | 0) + 1 | 0).toString() + `/` + (3).toString() + `): ` + errDetail), () => attempt(retries - 1 | 0));
|
|
330
331
|
} else {
|
|
331
332
|
cacheInvalidate();
|
|
332
|
-
if (errorCode === "Conflict") {
|
|
333
|
+
if (errorCode === CommandOutcome$Reventless.code("Conflict")) {
|
|
333
334
|
Metrics$ReventlessCore.emitCount("AppendConflict", Spec.name, undefined);
|
|
334
335
|
}
|
|
335
336
|
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "InfrastructureFailure", cmdJson.meta.msgId, {
|
|
@@ -14,13 +14,18 @@ let _ = beforeEach(() => {
|
|
|
14
14
|
|
|
15
15
|
// Captures whatever flows through CommandTopic_Helpers.reportRejected during a test.
|
|
16
16
|
let capturedRejections: ref<array<(string, CommandTopic_Helpers.rejectedResult)>> = ref([])
|
|
17
|
+
let capturedCauses: ref<array<CommandTopic_Helpers.refusalCause>> = ref([])
|
|
17
18
|
let capturedAccepts: ref<array<(string, CommandTopic_Helpers.acceptedResult)>> = ref([])
|
|
18
19
|
|
|
19
20
|
let installChannels = () => {
|
|
20
21
|
capturedRejections := []
|
|
22
|
+
capturedCauses := []
|
|
21
23
|
capturedAccepts := []
|
|
22
24
|
CommandTopic_Helpers.rejectedResultChannel.contents = Some(
|
|
23
|
-
(ref, info) =>
|
|
25
|
+
(ref, info, cause) => {
|
|
26
|
+
capturedRejections := capturedRejections.contents->Array.concat([(ref, info)])
|
|
27
|
+
capturedCauses := capturedCauses.contents->Array.concat([cause])
|
|
28
|
+
},
|
|
24
29
|
)
|
|
25
30
|
CommandTopic_Helpers.acceptedResultChannel.contents = Some(
|
|
26
31
|
(ref, info) => capturedAccepts := capturedAccepts.contents->Array.concat([(ref, info)]),
|
|
@@ -58,6 +63,8 @@ describe("Aggregate decide-rejection propagation:", () => {
|
|
|
58
63
|
let (ref, info) = capturedRejections.contents->Array.getUnsafe(0)
|
|
59
64
|
expect(ref)->toEqual("ref-1")
|
|
60
65
|
expect(info.errorCode)->toEqual("AlreadyExists")
|
|
66
|
+
// `decide` said no: the model, not the platform.
|
|
67
|
+
expect(capturedCauses.contents)->toEqual([CommandTopic_Helpers.DomainRejection])
|
|
61
68
|
// No event was appended.
|
|
62
69
|
expect(mock.getAll()->Array.length)->toBe(1)
|
|
63
70
|
},
|
|
@@ -14,18 +14,24 @@ let capturedRejections = {
|
|
|
14
14
|
contents: []
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
let capturedCauses = {
|
|
18
|
+
contents: []
|
|
19
|
+
};
|
|
20
|
+
|
|
17
21
|
let capturedAccepts = {
|
|
18
22
|
contents: []
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
function installChannels() {
|
|
22
26
|
capturedRejections.contents = [];
|
|
27
|
+
capturedCauses.contents = [];
|
|
23
28
|
capturedAccepts.contents = [];
|
|
24
|
-
CommandTopic_Helpers$ReventlessCore.rejectedResultChannel.contents = (ref, info) => {
|
|
29
|
+
CommandTopic_Helpers$ReventlessCore.rejectedResultChannel.contents = (ref, info, cause) => {
|
|
25
30
|
capturedRejections.contents = capturedRejections.contents.concat([[
|
|
26
31
|
ref,
|
|
27
32
|
info
|
|
28
33
|
]]);
|
|
34
|
+
capturedCauses.contents = capturedCauses.contents.concat([cause]);
|
|
29
35
|
};
|
|
30
36
|
CommandTopic_Helpers$ReventlessCore.acceptedResultChannel.contents = (ref, info) => {
|
|
31
37
|
capturedAccepts.contents = capturedAccepts.contents.concat([[
|
|
@@ -61,6 +67,7 @@ globalThis.describe("Aggregate decide-rejection propagation:", () => {
|
|
|
61
67
|
let match = capturedRejections.contents[0];
|
|
62
68
|
globalThis.expect(match[0]).toEqual("ref-1");
|
|
63
69
|
globalThis.expect(match[1].errorCode).toEqual("AlreadyExists");
|
|
70
|
+
globalThis.expect(capturedCauses.contents).toEqual(["DomainRejection"]);
|
|
64
71
|
globalThis.expect(AggregateFixtures$ReventlessCore.mock.getAll().length).toBe(1);
|
|
65
72
|
});
|
|
66
73
|
});
|
|
@@ -159,6 +166,7 @@ globalThis.describe("Aggregate decide-rejection propagation:", () => {
|
|
|
159
166
|
|
|
160
167
|
export {
|
|
161
168
|
capturedRejections,
|
|
169
|
+
capturedCauses,
|
|
162
170
|
capturedAccepts,
|
|
163
171
|
installChannels,
|
|
164
172
|
uninstallChannels,
|
|
@@ -50,10 +50,11 @@ describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
|
|
|
50
50
|
let outcomes = await CommandTopic_Helpers.runInlineAndCollect([cmdJson("msg-1")], handler)
|
|
51
51
|
expect(outcomes->Array.length)->toBe(1)
|
|
52
52
|
switch outcomes->Array.getUnsafe(0) {
|
|
53
|
-
| Rejected({msgId, errorCode, errorDetail}) =>
|
|
53
|
+
| Rejected({msgId, errorCode, errorDetail, cause}) =>
|
|
54
54
|
expect(msgId)->toEqual("msg-1")
|
|
55
55
|
expect(errorCode)->toEqual("AlreadyExists")
|
|
56
56
|
expect(errorDetail)->toEqual(None)
|
|
57
|
+
expect(cause)->toEqual(CommandTopic_Helpers.DomainRejection)
|
|
57
58
|
| Accepted(_) | Pending(_) => expect("expected Rejected")->toEqual("got other")
|
|
58
59
|
}
|
|
59
60
|
},
|
|
@@ -99,8 +100,59 @@ describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
|
|
|
99
100
|
|
|
100
101
|
let outcomes = await CommandTopic_Helpers.runInlineAndCollect([cmdJson("msg-1")], handler)
|
|
101
102
|
switch outcomes->Array.getUnsafe(0) {
|
|
102
|
-
| Rejected({errorCode}) =>
|
|
103
|
+
| Rejected({errorCode, cause}) =>
|
|
104
|
+
expect(errorCode)->toEqual("Conflict")
|
|
105
|
+
expect(cause)->toEqual(CommandTopic_Helpers.InfrastructureFailure)
|
|
103
106
|
| _ => expect("expected Rejected(Conflict)")->toEqual("got other")
|
|
104
107
|
}
|
|
105
108
|
})
|
|
109
|
+
|
|
110
|
+
testPromise("a reported access refusal keeps its cause", async () => {
|
|
111
|
+
let handler: CommandTopic_Helpers.jsonCommandsHandler = stream =>
|
|
112
|
+
stream
|
|
113
|
+
->Stream.runCollect
|
|
114
|
+
->Effect.map(
|
|
115
|
+
items => {
|
|
116
|
+
items->Array.forEach(
|
|
117
|
+
item =>
|
|
118
|
+
CommandTopic_Helpers.reportRejected(
|
|
119
|
+
~component="TestService",
|
|
120
|
+
~cause=AccessRefusal,
|
|
121
|
+
item.reference,
|
|
122
|
+
{errorCode: "Forbidden", errorDetail: "not the owner"},
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
items->Array.map(item => Ok(item.reference))
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
let outcomes = await CommandTopic_Helpers.runInlineAndCollect([cmdJson("msg-1")], handler)
|
|
130
|
+
switch outcomes->Array.getUnsafe(0) {
|
|
131
|
+
| Rejected({errorCode, cause}) =>
|
|
132
|
+
expect(errorCode)->toEqual("Forbidden")
|
|
133
|
+
expect(cause)->toEqual(CommandTopic_Helpers.AccessRefusal)
|
|
134
|
+
| _ => expect("expected Rejected(Forbidden)")->toEqual("got other")
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe("CommandTopic_Helpers.commandOutcomeToJson:", () => {
|
|
140
|
+
let causeOf = (cause: CommandTopic_Helpers.refusalCause) =>
|
|
141
|
+
CommandTopic_Helpers.commandOutcomeToJson(
|
|
142
|
+
Rejected({msgId: "m", errorCode: "X", errorDetail: None, cause}),
|
|
143
|
+
)
|
|
144
|
+
->JSON.Decode.object
|
|
145
|
+
->Option.flatMap(o => o->Dict.get("cause"))
|
|
146
|
+
->Option.flatMap(JSON.Decode.string)
|
|
147
|
+
|
|
148
|
+
testSync("a rejection carries its cause as the GraphQL enum value", () => {
|
|
149
|
+
expect(causeOf(DomainRejection))->toEqual(Some("DOMAIN"))
|
|
150
|
+
expect(causeOf(AccessRefusal))->toEqual(Some("ACCESS"))
|
|
151
|
+
expect(causeOf(InfrastructureFailure))->toEqual(Some("INFRASTRUCTURE"))
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
testSync("an accepted outcome carries no cause", () => {
|
|
155
|
+
let json = CommandTopic_Helpers.commandOutcomeToJson(Accepted({msgId: "m", eventCount: 1}))
|
|
156
|
+
expect(json->JSON.Decode.object->Option.flatMap(o => o->Dict.get("cause")))->toEqual(None)
|
|
157
|
+
})
|
|
106
158
|
})
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
|
|
4
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
6
|
import * as Effect from "effect/Effect";
|
|
5
7
|
import * as CommandTopic_Helpers$ReventlessCore from "../../src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
6
8
|
|
|
@@ -52,6 +54,7 @@ globalThis.describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
|
|
|
52
54
|
globalThis.expect(match.msgId).toEqual("msg-1");
|
|
53
55
|
globalThis.expect(match.errorCode).toEqual("AlreadyExists");
|
|
54
56
|
globalThis.expect(match.errorDetail).toEqual(undefined);
|
|
57
|
+
globalThis.expect(match.cause).toEqual("DomainRejection");
|
|
55
58
|
return;
|
|
56
59
|
case "Accepted" :
|
|
57
60
|
case "Pending" :
|
|
@@ -93,6 +96,7 @@ globalThis.describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
|
|
|
93
96
|
switch (match.TAG) {
|
|
94
97
|
case "Rejected" :
|
|
95
98
|
globalThis.expect(match.errorCode).toEqual("Conflict");
|
|
99
|
+
globalThis.expect(match.cause).toEqual("InfrastructureFailure");
|
|
96
100
|
return;
|
|
97
101
|
case "Accepted" :
|
|
98
102
|
case "Pending" :
|
|
@@ -100,6 +104,53 @@ globalThis.describe("CommandTopic_Helpers.runInlineAndCollect:", () => {
|
|
|
100
104
|
return;
|
|
101
105
|
}
|
|
102
106
|
});
|
|
107
|
+
globalThis.test("a reported access refusal keeps its cause", async () => {
|
|
108
|
+
let handler = stream => Effect.map(Stream.runCollect(stream), items => {
|
|
109
|
+
items.forEach(item => CommandTopic_Helpers$ReventlessCore.reportRejected("TestService", "AccessRefusal", item.reference, {
|
|
110
|
+
errorCode: "Forbidden",
|
|
111
|
+
errorDetail: "not the owner"
|
|
112
|
+
}));
|
|
113
|
+
return items.map(item => ({
|
|
114
|
+
TAG: "Ok",
|
|
115
|
+
_0: item.reference
|
|
116
|
+
}));
|
|
117
|
+
});
|
|
118
|
+
let outcomes = await CommandTopic_Helpers$ReventlessCore.runInlineAndCollect([cmdJson("msg-1")], handler);
|
|
119
|
+
let match = outcomes[0];
|
|
120
|
+
switch (match.TAG) {
|
|
121
|
+
case "Rejected" :
|
|
122
|
+
globalThis.expect(match.errorCode).toEqual("Forbidden");
|
|
123
|
+
globalThis.expect(match.cause).toEqual("AccessRefusal");
|
|
124
|
+
return;
|
|
125
|
+
case "Accepted" :
|
|
126
|
+
case "Pending" :
|
|
127
|
+
globalThis.expect("expected Rejected(Forbidden)").toEqual("got other");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
globalThis.describe("CommandTopic_Helpers.commandOutcomeToJson:", () => {
|
|
134
|
+
let causeOf = cause => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(CommandTopic_Helpers$ReventlessCore.commandOutcomeToJson({
|
|
135
|
+
TAG: "Rejected",
|
|
136
|
+
msgId: "m",
|
|
137
|
+
errorCode: "X",
|
|
138
|
+
errorDetail: undefined,
|
|
139
|
+
cause: cause
|
|
140
|
+
})), o => o["cause"]), Stdlib_JSON.Decode.string);
|
|
141
|
+
globalThis.test("a rejection carries its cause as the GraphQL enum value", () => {
|
|
142
|
+
globalThis.expect(causeOf("DomainRejection")).toEqual("DOMAIN");
|
|
143
|
+
globalThis.expect(causeOf("AccessRefusal")).toEqual("ACCESS");
|
|
144
|
+
globalThis.expect(causeOf("InfrastructureFailure")).toEqual("INFRASTRUCTURE");
|
|
145
|
+
});
|
|
146
|
+
globalThis.test("an accepted outcome carries no cause", () => {
|
|
147
|
+
let json = CommandTopic_Helpers$ReventlessCore.commandOutcomeToJson({
|
|
148
|
+
TAG: "Accepted",
|
|
149
|
+
msgId: "m",
|
|
150
|
+
eventCount: 1
|
|
151
|
+
});
|
|
152
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o["cause"])).toEqual(undefined);
|
|
153
|
+
});
|
|
103
154
|
});
|
|
104
155
|
|
|
105
156
|
export {
|