@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
|
@@ -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 */
|