@reventlessdev/reventless-local 3.0.0-alpha.292 → 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 +26 -0
- package/package.json +11 -11
- package/src/Platform.res +6 -3
- package/src/Platform.res.mjs +11 -10
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +19 -20
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +15 -36
- package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +1 -1
- package/src/adapter/Runtime/LocalRuntimeEnvironment.res +8 -3
- package/src/adapter/Task/LocalTaskBucket.res +1 -1
- package/src/reexport/CommandTopic.res.mjs +3 -0
- package/tests/adapter/CommandAuthorizationTest.res +6 -0
- package/tests/adapter/CommandAuthorizationTest.res.mjs +1 -0
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +3 -3
- package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +1 -1
- package/tests/adapter/InboundTranslationMutationTest.res +26 -2
- package/tests/adapter/InboundTranslationMutationTest.res.mjs +30 -5
- package/tests/components/dcb/DcbRejectionCauseTest.res +271 -0
- package/tests/components/dcb/DcbRejectionCauseTest.res.mjs +440 -0
- package/tests/components/heartbeat/HeartbeatFixtures.res +1 -1
- package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +1 -1
- package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res.mjs +2 -2
- package/tests/components/stateviewslice/StateViewSliceFixtures.res +1 -1
- package/tests/components/stateviewslice/StateViewSliceFixtures.res.mjs +2 -2
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +1 -1
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res.mjs +2 -2
- package/tests/plugin/UiFragments_GWT.res.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
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
|
+
|
|
19
|
+
# 3.0.0-alpha.293 (2026-10-21)
|
|
20
|
+
|
|
21
|
+
* feat!: a state view's row key is module Id ([08c9726](https://github.com/ReventlessDev/reventless-core/commit/08c972659a3d689a39fd5e2f2026597155e92210))
|
|
22
|
+
|
|
23
|
+
### BREAKING CHANGES
|
|
24
|
+
|
|
25
|
+
* a state view spec declares its row key as module Id. A
|
|
26
|
+
hand-written module Key in a spec under StateView/ or StateViewStream/
|
|
27
|
+
fails the build with "A state view's row key is `module Id`, not
|
|
28
|
+
`module Key`".
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
6
32
|
# 3.0.0-alpha.292 (2026-10-21)
|
|
7
33
|
|
|
8
34
|
* feat!: the write-side logic module is called the decision, and a codemod moves an app over ([cb573ff](https://github.com/ReventlessDev/reventless-core/commit/cb573ff5b928c7d7f3e75988dfaa65066f8c95eb))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
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": {
|
|
@@ -34,24 +34,24 @@
|
|
|
34
34
|
"graphql-yoga": "^5.21.0",
|
|
35
35
|
"sury": "11.0.0",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
|
+
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
37
38
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
38
39
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
39
|
-
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
40
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.23",
|
|
41
40
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
|
|
42
41
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
43
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
44
|
-
"@reventlessdev/
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-
|
|
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",
|
|
53
53
|
"sury-ppx": "11.0.0",
|
|
54
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
54
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.104"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"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) =>
|
|
1866
|
-
|
|
1867
|
-
| Some(
|
|
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
|
}
|
package/src/Platform.res.mjs
CHANGED
|
@@ -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";
|
|
@@ -672,7 +673,7 @@ function MakeWithConfig(Config) {
|
|
|
672
673
|
isAsync: UiFragmentRegistrySlice_isAsync,
|
|
673
674
|
make: UiFragmentRegistrySlice_make
|
|
674
675
|
};
|
|
675
|
-
let
|
|
676
|
+
let Spec_Id = {
|
|
676
677
|
schema: Id$Reventless.StringPure.schema,
|
|
677
678
|
make: prim => prim,
|
|
678
679
|
makeFromString: prim => prim,
|
|
@@ -683,7 +684,7 @@ function MakeWithConfig(Config) {
|
|
|
683
684
|
name: UiFragments$ReventlessCore.name,
|
|
684
685
|
moduleUrl: UiFragments$ReventlessCore.moduleUrl,
|
|
685
686
|
stateSchema: UiFragments$ReventlessCore.stateSchema,
|
|
686
|
-
|
|
687
|
+
Id: Spec_Id,
|
|
687
688
|
consumedEventSchema: UiFragments$ReventlessCore.consumedEventSchema,
|
|
688
689
|
config: UiFragments$ReventlessCore.config,
|
|
689
690
|
subIdConfig: undefined,
|
|
@@ -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
|
}
|
|
@@ -2433,7 +2434,7 @@ function Make($star) {
|
|
|
2433
2434
|
isAsync: UiFragmentRegistrySlice_isAsync,
|
|
2434
2435
|
make: UiFragmentRegistrySlice_make
|
|
2435
2436
|
};
|
|
2436
|
-
let
|
|
2437
|
+
let Spec_Id = {
|
|
2437
2438
|
schema: Id$Reventless.StringPure.schema,
|
|
2438
2439
|
make: prim => prim,
|
|
2439
2440
|
makeFromString: prim => prim,
|
|
@@ -2444,7 +2445,7 @@ function Make($star) {
|
|
|
2444
2445
|
name: UiFragments$ReventlessCore.name,
|
|
2445
2446
|
moduleUrl: UiFragments$ReventlessCore.moduleUrl,
|
|
2446
2447
|
stateSchema: UiFragments$ReventlessCore.stateSchema,
|
|
2447
|
-
|
|
2448
|
+
Id: Spec_Id,
|
|
2448
2449
|
consumedEventSchema: UiFragments$ReventlessCore.consumedEventSchema,
|
|
2449
2450
|
config: UiFragments$ReventlessCore.config,
|
|
2450
2451
|
subIdConfig: undefined,
|
|
@@ -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 =>
|
|
39
|
-
|
|
40
|
-
(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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) {
|
|
@@ -62,7 +62,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
62
62
|
let handleChannelEvent = (
|
|
63
63
|
handleCmds: ReventlessCore.CommandTopic.jsonCommandsHandler,
|
|
64
64
|
): Pulumi.Output.t<
|
|
65
|
-
ReventlessCore.Runtime.
|
|
65
|
+
ReventlessCore.Runtime.effectInvocationHandler<callbackEvent, 'context, unit, string>,
|
|
66
66
|
> => {
|
|
67
67
|
handleCmdsRef.contents = Some(handleCmds)
|
|
68
68
|
|
|
@@ -20,7 +20,7 @@ external asUnitHandler: ((JSON.t, unit) => promise<'r>) => (JSON.t, unit) => pro
|
|
|
20
20
|
|
|
21
21
|
let make = (
|
|
22
22
|
~name as _: string,
|
|
23
|
-
~handler: Pulumi.Output.t<ReventlessCore.Runtime.
|
|
23
|
+
~handler: Pulumi.Output.t<ReventlessCore.Runtime.invocationHandler<event, context, 'result>>,
|
|
24
24
|
~memorySize as _=1024,
|
|
25
25
|
~timeout as _=30,
|
|
26
26
|
~opts as _=?,
|
|
@@ -62,6 +62,11 @@ let extractSentTimestamp = (_event: event) => Some(Date.now())
|
|
|
62
62
|
// No redelivery in-process — the bus calls each handler once.
|
|
63
63
|
let extractRetryCount = (_event: event) => Some(1)
|
|
64
64
|
|
|
65
|
-
external asEventHandler: 'a => ReventlessCore.Runtime.
|
|
66
|
-
external asEffectHandler: 'a => ReventlessCore.Runtime.effectHandler<event, context, 'r, 'error> =
|
|
65
|
+
external asEventHandler: 'a => ReventlessCore.Runtime.invocationHandler<event, context, 'r> =
|
|
67
66
|
"%identity"
|
|
67
|
+
external asEffectHandler: 'a => ReventlessCore.Runtime.effectInvocationHandler<
|
|
68
|
+
event,
|
|
69
|
+
context,
|
|
70
|
+
'r,
|
|
71
|
+
'error,
|
|
72
|
+
> = "%identity"
|
|
@@ -17,7 +17,7 @@ let connect: Task_Adapter.connect<bucketParts, runtimeParts> = (
|
|
|
17
17
|
~opts as _,
|
|
18
18
|
) => ()
|
|
19
19
|
|
|
20
|
-
let makeHandler = (callback: Task.bucketCallback): Runtime.
|
|
20
|
+
let makeHandler = (callback: Task.bucketCallback): Runtime.invocationHandler<
|
|
21
21
|
callbackEvent,
|
|
22
22
|
context,
|
|
23
23
|
array<Task.taskAction>,
|
|
@@ -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 +
|
|
613
|
-
// (auto-injected whenever the fragment emits any mutation field).
|
|
614
|
-
expect(inspection.types->Array.length)->toBe(
|
|
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(
|
|
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") { ${
|
|
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") { ` +
|
|
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,
|