@reventlessdev/reventless-gwt 1.0.0-alpha.231 → 1.0.0-alpha.233
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 +22 -0
- package/package.json +6 -6
- package/src/Behavior_GWT.res +294 -19
- package/src/Behavior_GWT.res.mjs +429 -41
- package/src/StateChange/ExternalAddCategorySlice.res +5 -0
- package/src/StateChange/ExternalAddCategorySlice.res.mjs +5 -0
- package/src/StateChange/WithFixtures.res +5 -0
- package/src/StateChange/WithFixtures.res.mjs +5 -0
- package/src/StateChange/WithManualOpen.res +5 -0
- package/src/StateChange/WithManualOpen.res.mjs +5 -0
- package/tests/CallerTest.res +63 -0
- package/tests/CallerTest.res.mjs +70 -0
- package/tests/DelegateGwtTest.res +10 -5
- package/tests/FlowCrossPluginGwtTest.res +8 -4
- package/tests/FlowCrossPluginGwtTest.res.mjs +4 -2
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +9 -0
- package/tests/StateChange/WithFixtures_GWT.res.mjs +9 -0
- package/tests/StateChange/WithManualOpen_GWT.res.mjs +9 -0
- package/tests/StateChangeSliceAuthorizeGwtTest.res +174 -0
- package/tests/StateChangeSliceAuthorizeGwtTest.res.mjs +326 -0
- package/tests/StateChangeSliceGwtTest.res.mjs +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
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
|
+
# 1.0.0-alpha.233 (2026-10-01)
|
|
7
|
+
|
|
8
|
+
* feat!: roles a plugin declares, groups a platform provides ([43daace](https://github.com/ReventlessDev/reventless-core/commit/43daace6ca588afe22562d079af1e6f1da81942e))
|
|
9
|
+
|
|
10
|
+
### BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* AllowGroups and AdminGroup are removed — write AllowRoles over
|
|
13
|
+
the plugin's Roles.t, and use Role.admin / Platform.adminGroup(). Spec module
|
|
14
|
+
types declare `type role`, and their rules are Authorization.rule<role>; a spec
|
|
15
|
+
that writes its own rule declares `type role` (Reventless.Role.name for a rule
|
|
16
|
+
over Authorization.permission). Role.name is a private string: make one from a
|
|
17
|
+
string with Role.make.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# 1.0.0-alpha.232 (2026-10-01)
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* **spec, core, gwt, ppx:** a command acts only on what its caller owns ([2636f0e](https://github.com/ReventlessDev/reventless-core/commit/2636f0ee44797ee2c0b0c40a7107cf306113173f))
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
# 1.0.0-alpha.231 (2026-09-29)
|
|
7
29
|
|
|
8
30
|
**Note:** Version bump only for package @reventlessdev/reventless-gwt
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-gwt",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.233",
|
|
4
4
|
"description": "Given-When-Then DSLs and test harness for Reventless slice testing",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
"sury": "11.0.0",
|
|
17
17
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.34",
|
|
18
18
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.11",
|
|
19
|
-
"@reventlessdev/
|
|
20
|
-
"@reventlessdev/reventless-
|
|
21
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
22
|
-
"@reventlessdev/
|
|
19
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.20",
|
|
20
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.184",
|
|
21
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.286",
|
|
22
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.156"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"rescript": "12.3.1",
|
|
26
26
|
"sury-ppx": "11.0.0",
|
|
27
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
27
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.96"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"rescript": "^12.3.1"
|
package/src/Behavior_GWT.res
CHANGED
|
@@ -22,6 +22,204 @@ open ReventlessCore
|
|
|
22
22
|
// and slice users *cannot* use [MakeFromAggregate] (their Spec/Behavior shape
|
|
23
23
|
// differs). Pick the matching entry point.
|
|
24
24
|
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Who issues the command. `asCaller` names the caller before `whenCmd`; the
|
|
27
|
+
// command is then checked as production checks it: its `@authorize` rule against
|
|
28
|
+
// the caller's roles, then — for slices and aggregates that mark `@owner` on the
|
|
29
|
+
// events they decide on — against the owners the given events record. Without
|
|
30
|
+
// `asCaller` the command carries no caller claim, which the handler reads as the
|
|
31
|
+
// platform acting for itself — so every scenario written before this existed is
|
|
32
|
+
// unchanged.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
module Caller = {
|
|
35
|
+
external unsafeAsString: 'a => string = "%identity"
|
|
36
|
+
|
|
37
|
+
type t = {
|
|
38
|
+
claim: Message.CallerClaim.t,
|
|
39
|
+
/** The roles held, by name. A scenario names them by its plugin's own
|
|
40
|
+
`Roles.t`, whose cases are their names at run time. */
|
|
41
|
+
roles: array<Reventless.Role.name>,
|
|
42
|
+
signedIn: bool,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A plugin's roles are payload-less variants, strings at run time; anything
|
|
46
|
+
// else would be compared with a rule's role names and never match.
|
|
47
|
+
let roleNames = (roles: array<'role>): array<Reventless.Role.name> =>
|
|
48
|
+
roles->Array.map(role =>
|
|
49
|
+
if Reventless.OwnerScope.isJsString(role) {
|
|
50
|
+
Reventless.Role.make(role->unsafeAsString)
|
|
51
|
+
} else {
|
|
52
|
+
JsError.throwWithMessage(
|
|
53
|
+
"Caller: expected roles of the plugin's Roles.t (payload-less, so strings at runtime)",
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
A caller who owns only what records their id, holding `roles` (none by default).
|
|
60
|
+
|
|
61
|
+
Takes the id as the scenario has it — `Caller.owner(c1)` with `c1: CustomerId.t`,
|
|
62
|
+
or a plain string — because every `Id.Make` identity is a string on the wire.
|
|
63
|
+
The type is not checked, and need not be: an id of the wrong entity matches no
|
|
64
|
+
recorded owner, so it can only be refused, which fails the scenario that
|
|
65
|
+
expected the owner to act. A value that is not a string at all is refused here.
|
|
66
|
+
*/
|
|
67
|
+
let owner = (id: 'id, ~roles: array<'role>=[]): t =>
|
|
68
|
+
if Reventless.OwnerScope.isJsString(id) {
|
|
69
|
+
{claim: Owned({userId: id->unsafeAsString}), roles: roleNames(roles), signedIn: true}
|
|
70
|
+
} else {
|
|
71
|
+
JsError.throwWithMessage(
|
|
72
|
+
"Caller.owner: expected an id (a string at runtime, such as an Id.Make identity)",
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A signed-in caller holding `roles`, who owns nothing the given events record. */
|
|
77
|
+
let inRoles = (roles: array<'role>): t => {
|
|
78
|
+
claim: Owned({userId: "caller-in-roles"}),
|
|
79
|
+
roles: roleNames(roles),
|
|
80
|
+
signedIn: true,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A caller exempt from ownership rules, acting on anyone's behalf. Holds no
|
|
84
|
+
role: elevation does not grant a command whose rule the caller fails. */
|
|
85
|
+
let operator: t = {claim: Exempt, roles: [], signedIn: true}
|
|
86
|
+
/** A caller with no identity. */
|
|
87
|
+
let anonymous: t = {claim: Unidentified, roles: [], signedIn: false}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `Caller` as a scenario sees it: roles typed by the spec's own `role`, so they
|
|
91
|
+
are written bare (`Caller.inRoles([Merchandiser])`) and another plugin's role
|
|
92
|
+
does not compile. */
|
|
93
|
+
module type Callers = {
|
|
94
|
+
type role
|
|
95
|
+
type t = Caller.t
|
|
96
|
+
let owner: ('id, ~roles: array<role>=?) => t
|
|
97
|
+
let inRoles: array<role> => t
|
|
98
|
+
let operator: t
|
|
99
|
+
let anonymous: t
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module Callers = (
|
|
103
|
+
R: {
|
|
104
|
+
type role
|
|
105
|
+
},
|
|
106
|
+
): (Callers with type role = R.role) => {
|
|
107
|
+
type role = R.role
|
|
108
|
+
type t = Caller.t
|
|
109
|
+
let owner = (id, ~roles: array<role>=[]) => Caller.owner(id, ~roles)
|
|
110
|
+
let inRoles = (roles: array<role>) => Caller.inRoles(roles)
|
|
111
|
+
let operator = Caller.operator
|
|
112
|
+
let anonymous = Caller.anonymous
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module type ActingSpec = {
|
|
116
|
+
let name: string
|
|
117
|
+
@schema
|
|
118
|
+
type history
|
|
119
|
+
@schema
|
|
120
|
+
type command
|
|
121
|
+
type role
|
|
122
|
+
let commandAuthorization: command => Reventless.Authorization.rule<role>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module Acting = (Spec: ActingSpec) => {
|
|
126
|
+
let caller: ref<option<Caller.t>> = ref(None)
|
|
127
|
+
let refusal: ref<option<CommandTopic_Helpers.rejectedResult>> = ref(None)
|
|
128
|
+
|
|
129
|
+
let ownerFieldsByEventType = Reventless.Owner.fieldNamesByVariant(
|
|
130
|
+
Spec.historySchema->S.castToUnknown,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
let asCaller = (history, who: Caller.t) => {
|
|
134
|
+
caller := Some(who)
|
|
135
|
+
history
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let commandName = command =>
|
|
139
|
+
command->Message.encode(Spec.commandSchema)->Message.variantNameOfJson
|
|
140
|
+
|
|
141
|
+
// The rule first, as production checks it, against the roles alone: which group
|
|
142
|
+
// a role maps to is a deployment's fact, not the scenario's.
|
|
143
|
+
let refusedByRule = (who: Caller.t, command: Spec.command): bool =>
|
|
144
|
+
if (
|
|
145
|
+
Spec.commandAuthorization(command)
|
|
146
|
+
->Reventless.Authorization.named
|
|
147
|
+
->Reventless.Authorization.admits(~signedIn=who.signedIn, ~holds=role =>
|
|
148
|
+
who.roles->Array.includes(role)
|
|
149
|
+
)
|
|
150
|
+
) {
|
|
151
|
+
false
|
|
152
|
+
} else {
|
|
153
|
+
refusal :=
|
|
154
|
+
Some({
|
|
155
|
+
errorCode: "Forbidden",
|
|
156
|
+
errorDetail: `${Spec.name}.${commandName(
|
|
157
|
+
command,
|
|
158
|
+
)}: the caller holds no role this command's rule admits`,
|
|
159
|
+
})
|
|
160
|
+
true
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Every given event is the command's own history here, so all of them count.
|
|
164
|
+
// The caller is consumed: the next scenario starts with no claim.
|
|
165
|
+
let refuses = (history: array<Spec.history>, command: Spec.command): bool => {
|
|
166
|
+
let who = caller.contents
|
|
167
|
+
caller := None
|
|
168
|
+
refusal := None
|
|
169
|
+
let claim = who->Option.map(w => w.claim)
|
|
170
|
+
if who->Option.mapOr(false, refusedByRule(_, command)) {
|
|
171
|
+
true
|
|
172
|
+
} else if ownerFieldsByEventType->Dict.keysToArray->Array.length == 0 {
|
|
173
|
+
false
|
|
174
|
+
} else {
|
|
175
|
+
let owners = history->Array.reduce([], (owners, event) => {
|
|
176
|
+
let (eventType, data) = event->Message.encode(Spec.historySchema)->Message.splitMessage
|
|
177
|
+
switch ownerFieldsByEventType->Dict.get(eventType) {
|
|
178
|
+
| Some(fields) => owners->Reventless.OwnerScope.recordOwners(~fields, data)
|
|
179
|
+
| None => owners
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
refusal :=
|
|
183
|
+
Reventless.OwnerScope.decideActing(~claim, ~recordedOwners=Some(owners))
|
|
184
|
+
->CommandTopic_Helpers.ownershipRefusal(~component=Spec.name, ~command=commandName(command))
|
|
185
|
+
->Option.map(r => r.rejected)
|
|
186
|
+
refusal.contents->Option.isSome
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let encRefusal = (r: CommandTopic_Helpers.rejectedResult) =>
|
|
191
|
+
JSON.Encode.object(
|
|
192
|
+
Dict.fromArray([
|
|
193
|
+
("errorCode", JSON.Encode.string(r.errorCode)),
|
|
194
|
+
("errorDetail", JSON.Encode.string(r.errorDetail)),
|
|
195
|
+
]),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// Any other `then*` after a refused command fails: `decide` never ran, so
|
|
199
|
+
// there is nothing for it to compare.
|
|
200
|
+
let unexpectedRefusal = (): option<Outcome.outcome> =>
|
|
201
|
+
refusal.contents->Option.map(r =>
|
|
202
|
+
Outcome.fail(
|
|
203
|
+
ErrorMismatch({expected: JSON.Encode.null, actual: Some(encRefusal(r)), actualEvents: []}),
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
let thenRefused = (~encEvents, ~decided: option<JSON.t>, events) =>
|
|
208
|
+
switch refusal.contents {
|
|
209
|
+
| Some(_) => Outcome.pass
|
|
210
|
+
| None =>
|
|
211
|
+
Outcome.fail(
|
|
212
|
+
ErrorMismatch({
|
|
213
|
+
expected: JSON.Encode.object(
|
|
214
|
+
Dict.fromArray([("errorCode", JSON.Encode.string("Forbidden"))]),
|
|
215
|
+
),
|
|
216
|
+
actual: decided,
|
|
217
|
+
actualEvents: encEvents(events),
|
|
218
|
+
}),
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
25
223
|
// ---------------------------------------------------------------------------
|
|
26
224
|
// Slice form — replaces the legacy `StateChangeSlice_GWT`.
|
|
27
225
|
// ---------------------------------------------------------------------------
|
|
@@ -40,6 +238,9 @@ module type BehaviorSpec = {
|
|
|
40
238
|
|
|
41
239
|
@schema
|
|
42
240
|
type event
|
|
241
|
+
|
|
242
|
+
type role
|
|
243
|
+
let commandAuthorization: command => Reventless.Authorization.rule<role>
|
|
43
244
|
}
|
|
44
245
|
|
|
45
246
|
module type Behavior = {
|
|
@@ -60,6 +261,10 @@ module type T = {
|
|
|
60
261
|
|
|
61
262
|
let givenEvents: array<Spec.consumedEvent> => array<Spec.consumedEvent>
|
|
62
263
|
|
|
264
|
+
// Who issues the next `whenCmd`; see `Caller`.
|
|
265
|
+
module Caller: Callers with type role = Spec.role
|
|
266
|
+
let asCaller: (array<Spec.consumedEvent>, Caller.t) => array<Spec.consumedEvent>
|
|
267
|
+
|
|
63
268
|
let whenCmd: (array<Spec.consumedEvent>, Spec.command) => array<Spec.event>
|
|
64
269
|
|
|
65
270
|
let thenEvent: (array<Spec.event>, Spec.event) => Outcome.outcome
|
|
@@ -68,6 +273,9 @@ module type T = {
|
|
|
68
273
|
let thenEventWithError: (array<Spec.event>, Spec.event, Spec.error) => Outcome.outcome
|
|
69
274
|
let thenEventsWithError: (array<Spec.event>, array<Spec.event>, Spec.error) => Outcome.outcome
|
|
70
275
|
let thenError: (array<Spec.event>, Spec.error) => Outcome.outcome
|
|
276
|
+
// Refused before `decide`: by the command's rule, or because the caller does not
|
|
277
|
+
// own what it acts on.
|
|
278
|
+
let thenRefused: array<Spec.event> => Outcome.outcome
|
|
71
279
|
|
|
72
280
|
// DCB optimistic-concurrency assertions.
|
|
73
281
|
let thenAppendsConditionedOn: (array<Spec.event>, Reventless.DcbTag.query) => Outcome.outcome
|
|
@@ -242,6 +450,20 @@ module Make = (Spec: BehaviorSpec, Behavior: Behavior with module Spec := Spec):
|
|
|
242
450
|
module Core = AssertionCore(Spec)
|
|
243
451
|
let errors = Core.errors
|
|
244
452
|
|
|
453
|
+
module Caller = Callers({
|
|
454
|
+
type role = Spec.role
|
|
455
|
+
})
|
|
456
|
+
module Guard = Acting({
|
|
457
|
+
let name = Spec.name
|
|
458
|
+
type history = Spec.consumedEvent
|
|
459
|
+
let historySchema = Spec.consumedEventSchema
|
|
460
|
+
type command = Spec.command
|
|
461
|
+
let commandSchema = Spec.commandSchema
|
|
462
|
+
type role = Spec.role
|
|
463
|
+
let commandAuthorization = Spec.commandAuthorization
|
|
464
|
+
})
|
|
465
|
+
let asCaller = Guard.asCaller
|
|
466
|
+
|
|
245
467
|
// DCB append-condition derived inside [whenCmd]; [None] until first call.
|
|
246
468
|
let derivedCondition: ref<option<Reventless.DcbTag.appendCondition>> = ref(None)
|
|
247
469
|
|
|
@@ -379,12 +601,15 @@ module Make = (Spec: BehaviorSpec, Behavior: Behavior with module Spec := Spec):
|
|
|
379
601
|
}
|
|
380
602
|
}
|
|
381
603
|
|
|
382
|
-
|
|
383
|
-
switch Behavior.decide(state, command) {
|
|
384
|
-
| Ok(events) => events
|
|
385
|
-
| Error(error) =>
|
|
386
|
-
errors := [error]
|
|
604
|
+
if Guard.refuses(history, command) {
|
|
387
605
|
[]
|
|
606
|
+
} else {
|
|
607
|
+
switch Behavior.decide(currentState(history), command) {
|
|
608
|
+
| Ok(events) => events
|
|
609
|
+
| Error(error) =>
|
|
610
|
+
errors := [error]
|
|
611
|
+
[]
|
|
612
|
+
}
|
|
388
613
|
}
|
|
389
614
|
}
|
|
390
615
|
|
|
@@ -392,7 +617,17 @@ module Make = (Spec: BehaviorSpec, Behavior: Behavior with module Spec := Spec):
|
|
|
392
617
|
let whenCmd = (history, cmd) => history->exec(cmd)
|
|
393
618
|
|
|
394
619
|
let checkAppendCondition = (): option<Outcome.outcome> =>
|
|
395
|
-
appendConditionFailure.contents
|
|
620
|
+
switch appendConditionFailure.contents {
|
|
621
|
+
| Some(m) => Some(Outcome.fail(m))
|
|
622
|
+
| None => Guard.unexpectedRefusal()
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
let thenRefused = events =>
|
|
626
|
+
Guard.thenRefused(
|
|
627
|
+
~encEvents=Core.encEvents,
|
|
628
|
+
~decided=errors.contents->Array.get(0)->Option.map(Core.encError),
|
|
629
|
+
events,
|
|
630
|
+
)
|
|
396
631
|
|
|
397
632
|
// The append-condition footgun is surfaced before the shared comparison core
|
|
398
633
|
// runs; `thenAppends*` below bypass it deliberately.
|
|
@@ -493,6 +728,8 @@ module type AggregateSpec = {
|
|
|
493
728
|
type event
|
|
494
729
|
@schema
|
|
495
730
|
type error
|
|
731
|
+
type role
|
|
732
|
+
let commandAuthorization: command => Reventless.Authorization.rule<role>
|
|
496
733
|
}
|
|
497
734
|
|
|
498
735
|
module type AggregateT = {
|
|
@@ -503,6 +740,10 @@ module type AggregateT = {
|
|
|
503
740
|
|
|
504
741
|
let givenEvents: array<Spec.event> => array<Spec.event>
|
|
505
742
|
|
|
743
|
+
// Who issues the next `whenCmd`; see `Caller`.
|
|
744
|
+
module Caller: Callers with type role = Spec.role
|
|
745
|
+
let asCaller: (array<Spec.event>, Caller.t) => array<Spec.event>
|
|
746
|
+
|
|
506
747
|
let whenCmd: (array<Spec.event>, Spec.command) => array<Spec.event>
|
|
507
748
|
|
|
508
749
|
let thenEvent: (array<Spec.event>, Spec.event) => Outcome.outcome
|
|
@@ -521,6 +762,9 @@ module type AggregateT = {
|
|
|
521
762
|
) => Outcome.outcome
|
|
522
763
|
let thenEventsWithError: (array<Spec.event>, array<Spec.event>, Spec.error) => Outcome.outcome
|
|
523
764
|
let thenError: (array<Spec.event>, Spec.error) => Outcome.outcome
|
|
765
|
+
// Refused before `decide`: by the command's rule, or because the caller does not
|
|
766
|
+
// own what it acts on.
|
|
767
|
+
let thenRefused: array<Spec.event> => Outcome.outcome
|
|
524
768
|
}
|
|
525
769
|
|
|
526
770
|
module MakeFromAggregate = (Spec: AggregateSpec, Behavior: Behavior.T with module Spec = Spec): (
|
|
@@ -536,14 +780,31 @@ module MakeFromAggregate = (Spec: AggregateSpec, Behavior: Behavior.T with modul
|
|
|
536
780
|
module Core = AssertionCore(Spec)
|
|
537
781
|
let errors = Core.errors
|
|
538
782
|
|
|
783
|
+
module Caller = Callers({
|
|
784
|
+
type role = Spec.role
|
|
785
|
+
})
|
|
786
|
+
module Guard = Acting({
|
|
787
|
+
let name = Spec.name
|
|
788
|
+
type history = Spec.event
|
|
789
|
+
let historySchema = Spec.eventSchema
|
|
790
|
+
type command = Spec.command
|
|
791
|
+
let commandSchema = Spec.commandSchema
|
|
792
|
+
type role = Spec.role
|
|
793
|
+
let commandAuthorization = Spec.commandAuthorization
|
|
794
|
+
})
|
|
795
|
+
let asCaller = Guard.asCaller
|
|
796
|
+
|
|
539
797
|
let exec = (history, command): array<Spec.event> => {
|
|
540
798
|
errors := []
|
|
541
|
-
|
|
542
|
-
switch Behavior.decide(state, command) {
|
|
543
|
-
| Ok(events) => events
|
|
544
|
-
| Error(error) =>
|
|
545
|
-
errors := [error]
|
|
799
|
+
if Guard.refuses(history, command) {
|
|
546
800
|
[]
|
|
801
|
+
} else {
|
|
802
|
+
switch Behavior.decide(currentState(history), command) {
|
|
803
|
+
| Ok(events) => events
|
|
804
|
+
| Error(error) =>
|
|
805
|
+
errors := [error]
|
|
806
|
+
[]
|
|
807
|
+
}
|
|
547
808
|
}
|
|
548
809
|
}
|
|
549
810
|
|
|
@@ -551,20 +812,34 @@ module MakeFromAggregate = (Spec: AggregateSpec, Behavior: Behavior.T with modul
|
|
|
551
812
|
let whenCmd = (history, cmd) => history->exec(cmd)
|
|
552
813
|
|
|
553
814
|
// The aggregate flavour has no append-condition footgun, so the shared core's
|
|
554
|
-
// comparisons are exposed directly
|
|
555
|
-
// specific to this surface.
|
|
556
|
-
let
|
|
557
|
-
|
|
815
|
+
// comparisons are exposed directly once a refusal has been ruled out; only
|
|
816
|
+
// `thenCompare*` (custom equality) is specific to this surface.
|
|
817
|
+
let unlessRefused = (events, compare) =>
|
|
818
|
+
switch Guard.unexpectedRefusal() {
|
|
819
|
+
| Some(o) => o
|
|
820
|
+
| None => compare(events)
|
|
821
|
+
}
|
|
822
|
+
let thenEvents = (events, expected) => events->unlessRefused(Core.compareEvents(_, expected))
|
|
823
|
+
let thenCompareEvents = (events, expected, cmp) =>
|
|
824
|
+
events->unlessRefused(Core.compareEventsWith(_, expected, cmp))
|
|
558
825
|
let thenEvent = (events, expectedEvent) => thenEvents(events, [expectedEvent])
|
|
559
826
|
let thenCompareEvent = (events, expectedEvent, cmp) =>
|
|
560
827
|
thenCompareEvents(events, [expectedEvent], cmp)
|
|
561
|
-
let thenNoEvent = Core.compareNoEvent
|
|
828
|
+
let thenNoEvent = events => events->unlessRefused(Core.compareNoEvent)
|
|
562
829
|
|
|
563
|
-
let thenError = (events, expectedError) =>
|
|
830
|
+
let thenError = (events, expectedError) =>
|
|
831
|
+
events->unlessRefused(Core.matchesError(_, [], expectedError))
|
|
564
832
|
|
|
565
833
|
let thenEventWithError = (events, expectedEvent, expectedError) =>
|
|
566
|
-
Core.matchesError(
|
|
834
|
+
events->unlessRefused(Core.matchesError(_, [expectedEvent], expectedError))
|
|
567
835
|
|
|
568
836
|
let thenEventsWithError = (events, expectedEvents, expectedError) =>
|
|
569
|
-
Core.matchesError(
|
|
837
|
+
events->unlessRefused(Core.matchesError(_, expectedEvents, expectedError))
|
|
838
|
+
|
|
839
|
+
let thenRefused = events =>
|
|
840
|
+
Guard.thenRefused(
|
|
841
|
+
~encEvents=Core.encEvents,
|
|
842
|
+
~decided=errors.contents->Array.get(0)->Option.map(Core.encError),
|
|
843
|
+
events,
|
|
844
|
+
)
|
|
570
845
|
}
|