@reventlessdev/reventless-local 3.0.0-alpha.281 → 3.0.0-alpha.283
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 +18 -0
- package/package.json +9 -9
- package/src/Platform.res +5 -4
- package/src/Platform.res.mjs +12 -12
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +6 -25
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +4 -20
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +34 -38
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res.mjs +14 -6
- package/src/adapter/PlatformGraphQL_Server.res +1 -1
- package/tests/adapter/CommandAuthorizationTest.res +7 -8
- package/tests/adapter/CommandAuthorizationTest.res.mjs +5 -5
- package/tests/adapter/InboundTranslationMutationTest.res +95 -7
- package/tests/adapter/InboundTranslationMutationTest.res.mjs +97 -9
- package/tests/components/aggregate/AggregateFixtures.res.mjs +3 -3
- package/tests/components/aggregate/AggregateSelfMappingFixtures.res.mjs +3 -3
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +4 -4
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +8 -8
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +2 -2
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res.mjs +3 -3
- package/tests/components/commandtopic/CommandTopicFixtures.res.mjs +2 -2
- package/tests/components/commandtopic/CommandTopicStreamFixtures.res.mjs +2 -2
- package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +3 -3
- package/tests/components/dcb/DcbFixtures.res.mjs +3 -3
- package/tests/components/extensionpoint/ExtensionPointFixtures.res.mjs +5 -5
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +116 -8
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs +160 -19
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +58 -0
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs +145 -7
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +3 -3
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res.mjs +3 -3
- package/tests/plugin/PluginBehavior_GWT.res.mjs +2 -2
- package/tests/plugin/UiFragmentRegistry_GWT.res.mjs +1 -1
|
@@ -224,11 +224,8 @@ describe("InboundTranslationSlice Callback", () => {
|
|
|
224
224
|
|
|
225
225
|
type role = Reventless.Role.name
|
|
226
226
|
|
|
227
|
-
let
|
|
227
|
+
let authorizationOf = (_: string): Reventless.Authorization.rule<role> =>
|
|
228
228
|
AllowAuthenticated
|
|
229
|
-
type lifecycleState = unit
|
|
230
|
-
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> =>
|
|
231
|
-
Unrestricted
|
|
232
229
|
}
|
|
233
230
|
|
|
234
231
|
module MultiTranslation = {
|
|
@@ -293,11 +290,8 @@ describe("InboundTranslationSlice Callback", () => {
|
|
|
293
290
|
|
|
294
291
|
type role = Reventless.Role.name
|
|
295
292
|
|
|
296
|
-
let
|
|
293
|
+
let authorizationOf = (_: string): Reventless.Authorization.rule<role> =>
|
|
297
294
|
AllowAuthenticated
|
|
298
|
-
type lifecycleState = unit
|
|
299
|
-
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> =>
|
|
300
|
-
Unrestricted
|
|
301
295
|
}
|
|
302
296
|
|
|
303
297
|
module EmptyTranslation = {
|
|
@@ -329,3 +323,117 @@ describe("InboundTranslationSlice Callback", () => {
|
|
|
329
323
|
)
|
|
330
324
|
})
|
|
331
325
|
})
|
|
326
|
+
|
|
327
|
+
module CatalogFeedTranslation = {
|
|
328
|
+
let translate = CatalogFeedSpec.translate
|
|
329
|
+
let moduleUrl = CatalogFeedSpec.moduleUrl
|
|
330
|
+
}
|
|
331
|
+
module CatalogFeedCallback = ReventlessCore.InboundTranslationSlice_Callback.Make(
|
|
332
|
+
CatalogFeedSpec,
|
|
333
|
+
CatalogFeedTranslation,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
let caller = (groups): Reventless.Identity.t => {
|
|
337
|
+
userId: "u-1",
|
|
338
|
+
username: "alice",
|
|
339
|
+
groups,
|
|
340
|
+
provider: InMemory,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let catalogInput = (kind): JSON.t =>
|
|
344
|
+
Dict.fromArray([
|
|
345
|
+
("kind", JSON.Encode.string(kind)),
|
|
346
|
+
("id", JSON.Encode.string("x-1")),
|
|
347
|
+
])->JSON.Encode.object
|
|
348
|
+
|
|
349
|
+
describe("receive checks each command against its own rule", () => {
|
|
350
|
+
let published = ref([])
|
|
351
|
+
let publish: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
|
|
352
|
+
published := published.contents->Array.concat(cmds)
|
|
353
|
+
let _ = beforeEach(() => published := [])
|
|
354
|
+
|
|
355
|
+
testPromise(
|
|
356
|
+
"a caller lacking one command's role is refused, and nothing is published",
|
|
357
|
+
async () => {
|
|
358
|
+
let result = await CatalogFeedCallback.receive(
|
|
359
|
+
publish,
|
|
360
|
+
catalogInput("both"),
|
|
361
|
+
~caller=caller(["Merchandiser"]),
|
|
362
|
+
)
|
|
363
|
+
switch result {
|
|
364
|
+
| Error({requestId, ?errorCode}) =>
|
|
365
|
+
expect(errorCode)->toEqual(Some("Forbidden"))
|
|
366
|
+
let row = CatalogFeedCallback.auditLog->Dict.get(requestId)
|
|
367
|
+
expect(row->Option.map(r => r.status))->toEqual(
|
|
368
|
+
Some(ReventlessCore.InboundTranslationSlice_Callback.Failure),
|
|
369
|
+
)
|
|
370
|
+
| Ok(_) => expect(true)->toBe(false)
|
|
371
|
+
}
|
|
372
|
+
expect(published.contents->Array.length)->toBe(0)
|
|
373
|
+
},
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
testPromise("a caller holding every command's role publishes them all", async () => {
|
|
377
|
+
let result = await CatalogFeedCallback.receive(
|
|
378
|
+
publish,
|
|
379
|
+
catalogInput("both"),
|
|
380
|
+
~caller=caller(["Merchandiser", "Admin"]),
|
|
381
|
+
)
|
|
382
|
+
expect(result->Result.isOk)->toBe(true)
|
|
383
|
+
expect(published.contents->Array.length)->toBe(2)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
testPromise("the role one command needs is enough when the input yields only it", async () => {
|
|
387
|
+
let result = await CatalogFeedCallback.receive(
|
|
388
|
+
publish,
|
|
389
|
+
catalogInput("product"),
|
|
390
|
+
~caller=caller(["Merchandiser"]),
|
|
391
|
+
)
|
|
392
|
+
expect(result->Result.isOk)->toBe(true)
|
|
393
|
+
expect(published.contents->Array.length)->toBe(1)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
testPromise("no caller is the platform itself, and publishes", async () => {
|
|
397
|
+
let result = await CatalogFeedCallback.receive(publish, catalogInput("both"))
|
|
398
|
+
expect(result->Result.isOk)->toBe(true)
|
|
399
|
+
expect(published.contents->Array.length)->toBe(2)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
testPromise("a refusal answers CommandRejected with Forbidden", async () => {
|
|
403
|
+
let result = await CatalogFeedCallback.receive(
|
|
404
|
+
publish,
|
|
405
|
+
catalogInput("both"),
|
|
406
|
+
~caller=Reventless.Identity.anonymous,
|
|
407
|
+
)
|
|
408
|
+
switch result->ReventlessCore.InboundTranslationSlice_Callback.receiveResultToOutcome {
|
|
409
|
+
| Rejected({errorCode}) => expect(errorCode)->toBe("Forbidden")
|
|
410
|
+
| _ => expect(true)->toBe(false)
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
module NoteFeedTranslation = {
|
|
416
|
+
let translate = NoteFeedSpec.translate
|
|
417
|
+
let moduleUrl = NoteFeedSpec.moduleUrl
|
|
418
|
+
}
|
|
419
|
+
module NoteFeedCallback = ReventlessCore.InboundTranslationSlice_Callback.Make(
|
|
420
|
+
NoteFeedSpec,
|
|
421
|
+
NoteFeedTranslation,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
describe("receive encodes commands with their schema", () => {
|
|
425
|
+
testPromise("an absent option the schema writes as null decodes at the target", async () => {
|
|
426
|
+
let published = ref([])
|
|
427
|
+
let publish: ReventlessInfra.CommandTopic.publishJsons = async cmds => published := cmds
|
|
428
|
+
let _ = await NoteFeedCallback.receive(
|
|
429
|
+
publish,
|
|
430
|
+
Dict.fromArray([("orderId", JSON.Encode.string("ord-1"))])->JSON.Encode.object,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
let commandJson = (published.contents->Array.getUnsafe(0)).commandJson
|
|
434
|
+
// The target decodes with the same schema; the runtime value would omit `note`.
|
|
435
|
+
let decoded = commandJson->S.parseOrThrow(~to=NoteFeedSpec.commandSchema)
|
|
436
|
+
expect(decoded)->toEqual(NoteFeedSpec.Annotate({orderId: "ord-1", note: None}))
|
|
437
|
+
expect(commandJson->JSON.stringify->String.includes(`"note":null`))->toBe(true)
|
|
438
|
+
})
|
|
439
|
+
})
|
package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import * as Sury from "sury";
|
|
4
4
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
6
7
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
7
8
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
9
|
+
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
8
10
|
import * as InboundTranslationSliceFixtures$ReventlessLocal from "./InboundTranslationSliceFixtures.res.mjs";
|
|
9
11
|
import * as InboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
|
|
10
12
|
|
|
@@ -24,8 +26,7 @@ let Callback = InboundTranslationSlice_Callback$ReventlessCore.Make({
|
|
|
24
26
|
commandSchema: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.commandSchema,
|
|
25
27
|
targetName: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.targetName,
|
|
26
28
|
externalSystem: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.externalSystem,
|
|
27
|
-
|
|
28
|
-
commandTransition: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.commandTransition
|
|
29
|
+
authorizationOf: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.authorizationOf
|
|
29
30
|
})(PaymentWebhookTranslation);
|
|
30
31
|
|
|
31
32
|
globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
@@ -45,7 +46,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
45
46
|
orderId: "ord-1",
|
|
46
47
|
status: "completed"
|
|
47
48
|
};
|
|
48
|
-
let result = await Callback.receive(mockPublish, inputJson);
|
|
49
|
+
let result = await Callback.receive(mockPublish, inputJson, undefined);
|
|
49
50
|
if (result.TAG === "Ok") {
|
|
50
51
|
let match = result._0;
|
|
51
52
|
globalThis.expect(match.targetIds).toEqual(["ord-1"]);
|
|
@@ -76,7 +77,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
76
77
|
orderId: "ord-1",
|
|
77
78
|
status: "pending"
|
|
78
79
|
};
|
|
79
|
-
let result = await Callback.receive(mockPublish, inputJson);
|
|
80
|
+
let result = await Callback.receive(mockPublish, inputJson, undefined);
|
|
80
81
|
if (result.TAG === "Ok") {
|
|
81
82
|
globalThis.expect(true).toBe(false);
|
|
82
83
|
} else {
|
|
@@ -93,7 +94,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
93
94
|
let inputJson = {
|
|
94
95
|
unexpected: "data"
|
|
95
96
|
};
|
|
96
|
-
let result = await Callback.receive(mockPublish, inputJson);
|
|
97
|
+
let result = await Callback.receive(mockPublish, inputJson, undefined);
|
|
97
98
|
if (result.TAG === "Ok") {
|
|
98
99
|
globalThis.expect(true).toBe(false);
|
|
99
100
|
} else {
|
|
@@ -111,7 +112,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
111
112
|
orderId: "ord-1",
|
|
112
113
|
status: "completed"
|
|
113
114
|
};
|
|
114
|
-
let result = await Callback.receive(failingPublish, inputJson);
|
|
115
|
+
let result = await Callback.receive(failingPublish, inputJson, undefined);
|
|
115
116
|
if (result.TAG === "Ok") {
|
|
116
117
|
globalThis.expect(true).toBe(false);
|
|
117
118
|
} else {
|
|
@@ -130,7 +131,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
130
131
|
paymentId: "pay-1",
|
|
131
132
|
orderId: "ord-1",
|
|
132
133
|
status: "completed"
|
|
133
|
-
});
|
|
134
|
+
}, undefined);
|
|
134
135
|
let firstId = InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(first);
|
|
135
136
|
globalThis.expect(Stdlib_Option.isSome(InboundTranslationSlice_Callback$ReventlessCore.takeAuditRow(Callback.auditLog, firstId))).toBe(true);
|
|
136
137
|
globalThis.expect(Object.entries(Callback.auditLog).length).toBe(0);
|
|
@@ -138,7 +139,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
138
139
|
paymentId: "pay-1",
|
|
139
140
|
orderId: "ord-1",
|
|
140
141
|
status: "completed"
|
|
141
|
-
});
|
|
142
|
+
}, undefined);
|
|
142
143
|
let secondId = InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(second);
|
|
143
144
|
globalThis.expect(secondId === firstId).toBe(false);
|
|
144
145
|
globalThis.expect(Object.entries(Callback.auditLog).length).toBe(1);
|
|
@@ -151,7 +152,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
151
152
|
paymentId: "pay-1",
|
|
152
153
|
orderId: "ord-1",
|
|
153
154
|
status: "pending"
|
|
154
|
-
});
|
|
155
|
+
}, undefined);
|
|
155
156
|
let id = InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result);
|
|
156
157
|
let row = InboundTranslationSlice_Callback$ReventlessCore.takeAuditRow(Callback.auditLog, id);
|
|
157
158
|
if (row !== undefined) {
|
|
@@ -185,8 +186,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
185
186
|
}
|
|
186
187
|
])
|
|
187
188
|
});
|
|
188
|
-
let
|
|
189
|
-
let commandTransition = param => "Unrestricted";
|
|
189
|
+
let authorizationOf = param => "AllowAuthenticated";
|
|
190
190
|
let MultiTranslation = {
|
|
191
191
|
translate: translate,
|
|
192
192
|
moduleUrl: moduleUrl
|
|
@@ -198,8 +198,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
198
198
|
commandSchema: commandSchema,
|
|
199
199
|
targetName: "ConfirmPayment",
|
|
200
200
|
externalSystem: undefined,
|
|
201
|
-
|
|
202
|
-
commandTransition: commandTransition
|
|
201
|
+
authorizationOf: authorizationOf
|
|
203
202
|
})(MultiTranslation);
|
|
204
203
|
let publishedCommands = {
|
|
205
204
|
contents: []
|
|
@@ -215,7 +214,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
215
214
|
"pay-3"
|
|
216
215
|
]
|
|
217
216
|
};
|
|
218
|
-
let result = await MultiCallback.receive(mockPublish, inputJson);
|
|
217
|
+
let result = await MultiCallback.receive(mockPublish, inputJson, undefined);
|
|
219
218
|
if (result.TAG === "Ok") {
|
|
220
219
|
let match = result._0;
|
|
221
220
|
globalThis.expect(match.targetIds).toEqual([
|
|
@@ -249,8 +248,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
249
248
|
TAG: "Ok",
|
|
250
249
|
_0: []
|
|
251
250
|
});
|
|
252
|
-
let
|
|
253
|
-
let commandTransition = param => "Unrestricted";
|
|
251
|
+
let authorizationOf = param => "AllowAuthenticated";
|
|
254
252
|
let EmptyTranslation = {
|
|
255
253
|
translate: translate,
|
|
256
254
|
moduleUrl: moduleUrl
|
|
@@ -262,8 +260,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
262
260
|
commandSchema: commandSchema,
|
|
263
261
|
targetName: "ConfirmPayment",
|
|
264
262
|
externalSystem: undefined,
|
|
265
|
-
|
|
266
|
-
commandTransition: commandTransition
|
|
263
|
+
authorizationOf: authorizationOf
|
|
267
264
|
})(EmptyTranslation);
|
|
268
265
|
let publishedCommands = {
|
|
269
266
|
contents: []
|
|
@@ -274,7 +271,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
274
271
|
let inputJson = {
|
|
275
272
|
orderId: "ord-1"
|
|
276
273
|
};
|
|
277
|
-
let result = await EmptyCallback.receive(mockPublish, inputJson);
|
|
274
|
+
let result = await EmptyCallback.receive(mockPublish, inputJson, undefined);
|
|
278
275
|
if (result.TAG === "Ok") {
|
|
279
276
|
let match = result._0;
|
|
280
277
|
globalThis.expect(match.targetIds).toEqual([]);
|
|
@@ -287,8 +284,152 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
|
|
|
287
284
|
});
|
|
288
285
|
});
|
|
289
286
|
|
|
287
|
+
let translate$1 = InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.translate;
|
|
288
|
+
|
|
289
|
+
let moduleUrl$1 = InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.moduleUrl;
|
|
290
|
+
|
|
291
|
+
let CatalogFeedTranslation = {
|
|
292
|
+
translate: translate$1,
|
|
293
|
+
moduleUrl: moduleUrl$1
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
let CatalogFeedCallback = InboundTranslationSlice_Callback$ReventlessCore.Make({
|
|
297
|
+
name: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.name,
|
|
298
|
+
moduleUrl: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.moduleUrl,
|
|
299
|
+
externalInputSchema: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.externalInputSchema,
|
|
300
|
+
commandSchema: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.commandSchema,
|
|
301
|
+
targetName: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.targetName,
|
|
302
|
+
externalSystem: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.externalSystem,
|
|
303
|
+
authorizationOf: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.authorizationOf
|
|
304
|
+
})(CatalogFeedTranslation);
|
|
305
|
+
|
|
306
|
+
function caller(groups) {
|
|
307
|
+
return {
|
|
308
|
+
userId: "u-1",
|
|
309
|
+
username: "alice",
|
|
310
|
+
groups: groups,
|
|
311
|
+
provider: "InMemory"
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function catalogInput(kind) {
|
|
316
|
+
return Object.fromEntries([
|
|
317
|
+
[
|
|
318
|
+
"kind",
|
|
319
|
+
kind
|
|
320
|
+
],
|
|
321
|
+
[
|
|
322
|
+
"id",
|
|
323
|
+
"x-1"
|
|
324
|
+
]
|
|
325
|
+
]);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
globalThis.describe("receive checks each command against its own rule", () => {
|
|
329
|
+
let published = {
|
|
330
|
+
contents: []
|
|
331
|
+
};
|
|
332
|
+
let publish = async cmds => {
|
|
333
|
+
published.contents = published.contents.concat(cmds);
|
|
334
|
+
};
|
|
335
|
+
globalThis.beforeEach(() => {
|
|
336
|
+
published.contents = [];
|
|
337
|
+
});
|
|
338
|
+
globalThis.test("a caller lacking one command's role is refused, and nothing is published", async () => {
|
|
339
|
+
let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), caller(["Merchandiser"]));
|
|
340
|
+
if (result.TAG === "Ok") {
|
|
341
|
+
globalThis.expect(true).toBe(false);
|
|
342
|
+
} else {
|
|
343
|
+
let match = result._0;
|
|
344
|
+
globalThis.expect(match.errorCode).toEqual("Forbidden");
|
|
345
|
+
let row = CatalogFeedCallback.auditLog[match.requestId];
|
|
346
|
+
globalThis.expect(Stdlib_Option.map(row, r => r.status)).toEqual("Failure");
|
|
347
|
+
}
|
|
348
|
+
globalThis.expect(published.contents.length).toBe(0);
|
|
349
|
+
});
|
|
350
|
+
globalThis.test("a caller holding every command's role publishes them all", async () => {
|
|
351
|
+
let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), caller([
|
|
352
|
+
"Merchandiser",
|
|
353
|
+
"Admin"
|
|
354
|
+
]));
|
|
355
|
+
globalThis.expect(Stdlib_Result.isOk(result)).toBe(true);
|
|
356
|
+
globalThis.expect(published.contents.length).toBe(2);
|
|
357
|
+
});
|
|
358
|
+
globalThis.test("the role one command needs is enough when the input yields only it", async () => {
|
|
359
|
+
let result = await CatalogFeedCallback.receive(publish, catalogInput("product"), caller(["Merchandiser"]));
|
|
360
|
+
globalThis.expect(Stdlib_Result.isOk(result)).toBe(true);
|
|
361
|
+
globalThis.expect(published.contents.length).toBe(1);
|
|
362
|
+
});
|
|
363
|
+
globalThis.test("no caller is the platform itself, and publishes", async () => {
|
|
364
|
+
let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), undefined);
|
|
365
|
+
globalThis.expect(Stdlib_Result.isOk(result)).toBe(true);
|
|
366
|
+
globalThis.expect(published.contents.length).toBe(2);
|
|
367
|
+
});
|
|
368
|
+
globalThis.test("a refusal answers CommandRejected with Forbidden", async () => {
|
|
369
|
+
let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), Identity$Reventless.anonymous);
|
|
370
|
+
let match = InboundTranslationSlice_Callback$ReventlessCore.receiveResultToOutcome(result);
|
|
371
|
+
switch (match.TAG) {
|
|
372
|
+
case "Rejected" :
|
|
373
|
+
globalThis.expect(match.errorCode).toBe("Forbidden");
|
|
374
|
+
return;
|
|
375
|
+
case "Accepted" :
|
|
376
|
+
case "Pending" :
|
|
377
|
+
globalThis.expect(true).toBe(false);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
let translate$2 = InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.translate;
|
|
384
|
+
|
|
385
|
+
let moduleUrl$2 = InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.moduleUrl;
|
|
386
|
+
|
|
387
|
+
let NoteFeedTranslation = {
|
|
388
|
+
translate: translate$2,
|
|
389
|
+
moduleUrl: moduleUrl$2
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
let NoteFeedCallback = InboundTranslationSlice_Callback$ReventlessCore.Make({
|
|
393
|
+
name: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.name,
|
|
394
|
+
moduleUrl: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.moduleUrl,
|
|
395
|
+
externalInputSchema: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.externalInputSchema,
|
|
396
|
+
commandSchema: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.commandSchema,
|
|
397
|
+
targetName: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.targetName,
|
|
398
|
+
externalSystem: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.externalSystem,
|
|
399
|
+
authorizationOf: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.authorizationOf
|
|
400
|
+
})(NoteFeedTranslation);
|
|
401
|
+
|
|
402
|
+
globalThis.describe("receive encodes commands with their schema", () => {
|
|
403
|
+
globalThis.test("an absent option the schema writes as null decodes at the target", async () => {
|
|
404
|
+
let published = {
|
|
405
|
+
contents: []
|
|
406
|
+
};
|
|
407
|
+
let publish = async cmds => {
|
|
408
|
+
published.contents = cmds;
|
|
409
|
+
};
|
|
410
|
+
await NoteFeedCallback.receive(publish, Object.fromEntries([[
|
|
411
|
+
"orderId",
|
|
412
|
+
"ord-1"
|
|
413
|
+
]]), undefined);
|
|
414
|
+
let commandJson = published.contents[0].commandJson;
|
|
415
|
+
let decoded = Sury.parseOrThrow(commandJson, InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.commandSchema);
|
|
416
|
+
globalThis.expect(decoded).toEqual({
|
|
417
|
+
TAG: "Annotate",
|
|
418
|
+
orderId: "ord-1",
|
|
419
|
+
note: undefined
|
|
420
|
+
});
|
|
421
|
+
globalThis.expect(JSON.stringify(commandJson).includes(`"note":null`)).toBe(true);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
290
425
|
export {
|
|
291
426
|
PaymentWebhookTranslation,
|
|
292
427
|
Callback,
|
|
428
|
+
CatalogFeedTranslation,
|
|
429
|
+
CatalogFeedCallback,
|
|
430
|
+
caller,
|
|
431
|
+
catalogInput,
|
|
432
|
+
NoteFeedTranslation,
|
|
433
|
+
NoteFeedCallback,
|
|
293
434
|
}
|
|
294
435
|
/* Callback Not a pure module */
|
|
@@ -36,3 +36,61 @@ module PaymentWebhookSpec = {
|
|
|
36
36
|
| status => Error("Unknown payment status: " ++ status)
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
// Two constructors under different roles; `kind` picks which the input becomes.
|
|
41
|
+
module CatalogFeedSpec = {
|
|
42
|
+
let name = "CatalogFeed"
|
|
43
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
44
|
+
|
|
45
|
+
@schema
|
|
46
|
+
type externalInput = {kind: string, id: string}
|
|
47
|
+
|
|
48
|
+
@schema
|
|
49
|
+
type command =
|
|
50
|
+
| AddProduct({productId: @s.matches(Reventless.DcbTag.string) string})
|
|
51
|
+
| AddCategory({categoryId: @s.matches(Reventless.DcbTag.string) string})
|
|
52
|
+
|
|
53
|
+
let targetName = "AddProduct"
|
|
54
|
+
let externalSystem = None
|
|
55
|
+
|
|
56
|
+
type role = Reventless.Role.name
|
|
57
|
+
let merchandiser = Reventless.Role.make("Merchandiser")
|
|
58
|
+
let admin = Reventless.Role.make("Admin")
|
|
59
|
+
let authorizationOf = (name: string): Reventless.Authorization.rule<role> =>
|
|
60
|
+
switch name {
|
|
61
|
+
| "AddProduct" => AllowRoles([merchandiser])
|
|
62
|
+
| "AddCategory" => AllowRoles([admin])
|
|
63
|
+
| _ => DenyAll
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let translate = (input: externalInput) =>
|
|
67
|
+
switch input.kind {
|
|
68
|
+
| "product" => Ok([(input.id, AddProduct({productId: input.id}))])
|
|
69
|
+
| "both" =>
|
|
70
|
+
Ok([
|
|
71
|
+
(input.id, AddProduct({productId: input.id})),
|
|
72
|
+
(input.id, AddCategory({categoryId: input.id})),
|
|
73
|
+
])
|
|
74
|
+
| kind => Error("Unknown kind: " ++ kind)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A command whose schema writes an absent note as `null`, which the runtime value does not.
|
|
79
|
+
module NoteFeedSpec = {
|
|
80
|
+
let name = "NoteFeed"
|
|
81
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
82
|
+
|
|
83
|
+
@schema
|
|
84
|
+
type externalInput = {orderId: string}
|
|
85
|
+
|
|
86
|
+
@schema
|
|
87
|
+
type command =
|
|
88
|
+
Annotate({orderId: @s.matches(Reventless.DcbTag.string) string, note: @s.null option<string>})
|
|
89
|
+
|
|
90
|
+
let targetName = "Annotate"
|
|
91
|
+
let externalSystem = None
|
|
92
|
+
|
|
93
|
+
let translate = (input: externalInput) => Ok([
|
|
94
|
+
(input.orderId, Annotate({orderId: input.orderId, note: None})),
|
|
95
|
+
])
|
|
96
|
+
}
|
|
@@ -52,14 +52,10 @@ function translate(input) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function
|
|
55
|
+
function authorizationOf(param) {
|
|
56
56
|
return "AllowAuthenticated";
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function commandTransition(param) {
|
|
60
|
-
return "Undeclared";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
59
|
let traits = [];
|
|
64
60
|
|
|
65
61
|
let PaymentWebhookSpec = {
|
|
@@ -70,13 +66,155 @@ let PaymentWebhookSpec = {
|
|
|
70
66
|
targetName: "ConfirmPayment",
|
|
71
67
|
externalSystem: undefined,
|
|
72
68
|
translate: translate,
|
|
73
|
-
|
|
74
|
-
commandTransition: commandTransition,
|
|
69
|
+
authorizationOf: authorizationOf,
|
|
75
70
|
traits: traits
|
|
76
71
|
};
|
|
77
72
|
|
|
73
|
+
let moduleUrl$2 = import.meta.url;
|
|
74
|
+
|
|
75
|
+
let externalInputSchema$1 = Sury.$schema(s => ({
|
|
76
|
+
kind: s.m(Sury.string),
|
|
77
|
+
id: s.m(Sury.string)
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
let commandSchema$1 = Sury.union([
|
|
81
|
+
Sury.$schema(s => ({
|
|
82
|
+
TAG: "AddProduct",
|
|
83
|
+
productId: s.m(DcbTag$Reventless.string)
|
|
84
|
+
})),
|
|
85
|
+
Sury.$schema(s => ({
|
|
86
|
+
TAG: "AddCategory",
|
|
87
|
+
categoryId: s.m(DcbTag$Reventless.string)
|
|
88
|
+
}))
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
let merchandiser = "Merchandiser";
|
|
92
|
+
|
|
93
|
+
let admin = "Admin";
|
|
94
|
+
|
|
95
|
+
function authorizationOf$1(name) {
|
|
96
|
+
switch (name) {
|
|
97
|
+
case "AddCategory" :
|
|
98
|
+
return {
|
|
99
|
+
TAG: "AllowRoles",
|
|
100
|
+
_0: [admin]
|
|
101
|
+
};
|
|
102
|
+
case "AddProduct" :
|
|
103
|
+
return {
|
|
104
|
+
TAG: "AllowRoles",
|
|
105
|
+
_0: [merchandiser]
|
|
106
|
+
};
|
|
107
|
+
default:
|
|
108
|
+
return "DenyAll";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function translate$1(input) {
|
|
113
|
+
let kind = input.kind;
|
|
114
|
+
switch (kind) {
|
|
115
|
+
case "both" :
|
|
116
|
+
return {
|
|
117
|
+
TAG: "Ok",
|
|
118
|
+
_0: [
|
|
119
|
+
[
|
|
120
|
+
input.id,
|
|
121
|
+
{
|
|
122
|
+
TAG: "AddProduct",
|
|
123
|
+
productId: input.id
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
input.id,
|
|
128
|
+
{
|
|
129
|
+
TAG: "AddCategory",
|
|
130
|
+
categoryId: input.id
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
]
|
|
134
|
+
};
|
|
135
|
+
case "product" :
|
|
136
|
+
return {
|
|
137
|
+
TAG: "Ok",
|
|
138
|
+
_0: [[
|
|
139
|
+
input.id,
|
|
140
|
+
{
|
|
141
|
+
TAG: "AddProduct",
|
|
142
|
+
productId: input.id
|
|
143
|
+
}
|
|
144
|
+
]]
|
|
145
|
+
};
|
|
146
|
+
default:
|
|
147
|
+
return {
|
|
148
|
+
TAG: "Error",
|
|
149
|
+
_0: "Unknown kind: " + kind
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let traits$1 = [];
|
|
155
|
+
|
|
156
|
+
let CatalogFeedSpec = {
|
|
157
|
+
name: "CatalogFeed",
|
|
158
|
+
moduleUrl: moduleUrl$2,
|
|
159
|
+
externalInputSchema: externalInputSchema$1,
|
|
160
|
+
commandSchema: commandSchema$1,
|
|
161
|
+
targetName: "AddProduct",
|
|
162
|
+
externalSystem: undefined,
|
|
163
|
+
merchandiser: merchandiser,
|
|
164
|
+
admin: admin,
|
|
165
|
+
authorizationOf: authorizationOf$1,
|
|
166
|
+
translate: translate$1,
|
|
167
|
+
traits: traits$1
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
let moduleUrl$3 = import.meta.url;
|
|
171
|
+
|
|
172
|
+
let externalInputSchema$2 = Sury.$schema(s => ({
|
|
173
|
+
orderId: s.m(Sury.string)
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
let commandSchema$2 = Sury.$schema(s => ({
|
|
177
|
+
TAG: "Annotate",
|
|
178
|
+
orderId: s.m(DcbTag$Reventless.string),
|
|
179
|
+
note: s.m(Sury.$nullAsOption(Sury.string))
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
function translate$2(input) {
|
|
183
|
+
return {
|
|
184
|
+
TAG: "Ok",
|
|
185
|
+
_0: [[
|
|
186
|
+
input.orderId,
|
|
187
|
+
{
|
|
188
|
+
TAG: "Annotate",
|
|
189
|
+
orderId: input.orderId,
|
|
190
|
+
note: undefined
|
|
191
|
+
}
|
|
192
|
+
]]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function authorizationOf$2(param) {
|
|
197
|
+
return "AllowAuthenticated";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let traits$2 = [];
|
|
201
|
+
|
|
202
|
+
let NoteFeedSpec = {
|
|
203
|
+
name: "NoteFeed",
|
|
204
|
+
moduleUrl: moduleUrl$3,
|
|
205
|
+
externalInputSchema: externalInputSchema$2,
|
|
206
|
+
commandSchema: commandSchema$2,
|
|
207
|
+
targetName: "Annotate",
|
|
208
|
+
externalSystem: undefined,
|
|
209
|
+
translate: translate$2,
|
|
210
|
+
authorizationOf: authorizationOf$2,
|
|
211
|
+
traits: traits$2
|
|
212
|
+
};
|
|
213
|
+
|
|
78
214
|
export {
|
|
79
215
|
OrderEventLog,
|
|
80
216
|
PaymentWebhookSpec,
|
|
217
|
+
CatalogFeedSpec,
|
|
218
|
+
NoteFeedSpec,
|
|
81
219
|
}
|
|
82
220
|
/* moduleUrl Not a pure module */
|
package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs
CHANGED
|
@@ -37,7 +37,7 @@ let commandSchema = Sury.$schema(s => ({
|
|
|
37
37
|
|
|
38
38
|
let errorSchema = Sury.literal("AlreadyPlaced");
|
|
39
39
|
|
|
40
|
-
function
|
|
40
|
+
function authorizationOf(param) {
|
|
41
41
|
return "AllowAuthenticated";
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -55,7 +55,7 @@ let PlaceSpec = {
|
|
|
55
55
|
consumedEventSchema: consumedEventSchema,
|
|
56
56
|
errorSchema: errorSchema,
|
|
57
57
|
commandSchema: commandSchema,
|
|
58
|
-
|
|
58
|
+
authorizationOf: authorizationOf,
|
|
59
59
|
commandTransition: commandTransition,
|
|
60
60
|
traits: traits,
|
|
61
61
|
readConsistency: "EscalateOnRetry"
|
|
@@ -222,7 +222,7 @@ let PlaceMaker = StateChangeSlice_Builder$ReventlessLocal.Make({
|
|
|
222
222
|
errorSchema: errorSchema,
|
|
223
223
|
eventSchema: eventSchema,
|
|
224
224
|
commandSchema: commandSchema,
|
|
225
|
-
|
|
225
|
+
authorizationOf: authorizationOf,
|
|
226
226
|
commandTransition: commandTransition,
|
|
227
227
|
traits: traits,
|
|
228
228
|
readConsistency: "EscalateOnRetry"
|