@reventlessdev/reventless-aws 3.0.0-alpha.377 → 3.0.0-alpha.379
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 +21 -0
- package/package.json +9 -9
- package/src/Platform.res.mjs +4 -4
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +4 -7
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +15 -24
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +2 -2
- package/tests/AppSync_AdapterTest.res +29 -0
- package/tests/AppSync_AdapterTest.res.mjs +46 -0
- package/tests/DcbInboundReceiverStub.mjs +12 -0
- package/tests/DcbInboundTranslationRoutingTest.res +49 -1
- package/tests/DcbInboundTranslationRoutingTest.res.mjs +49 -1
- package/tests/EpInboundTestSlice.res +7 -4
- package/tests/EpInboundTestSlice.res.mjs +8 -0
- package/tests/QueryDbResolvers_AppSyncTest.res +2 -2
- package/tests/QueryDbResolvers_AppSyncTest.res.mjs +1 -1
- package/tests/integration/AggTestAggregate.res +2 -2
- package/tests/integration/AggTestAggregate.res.mjs +2 -2
- package/tests/integration/EpCompositeSlice.res +1 -1
- package/tests/integration/EpCompositeSlice.res.mjs +2 -2
- package/tests/integration/EpTestSlice.res +1 -1
- package/tests/integration/EpTestSlice.res.mjs +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
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.379 (2026-10-10)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **pulumi-aws:** a list's last page no longer promises another ([00b141f](https://github.com/ReventlessDev/reventless-core/commit/00b141fbfdbf44544fbea8ab979356831381f48a))
|
|
11
|
+
* feat!: a command's rule is looked up by its name, without a cast ([5f02c40](https://github.com/ReventlessDev/reventless-core/commit/5f02c4081d077bf4a6951d8c9b2b16553a8ca87b))
|
|
12
|
+
|
|
13
|
+
### BREAKING CHANGES
|
|
14
|
+
|
|
15
|
+
* a spec that writes `commandAuthorization` by hand writes
|
|
16
|
+
`authorizationOf = (name: string) => …` instead; the PPX refuses the old name.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# 3.0.0-alpha.378 (2026-10-10)
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* an inbound translation is gated alike on every door, and encodes its commands by schema ([6ec3ce6](https://github.com/ReventlessDev/reventless-core/commit/6ec3ce672e53e2c162c64a90d103be691391be5e))
|
|
25
|
+
|
|
26
|
+
|
|
6
27
|
# 3.0.0-alpha.377 (2026-10-08)
|
|
7
28
|
|
|
8
29
|
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.379",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -23,18 +23,18 @@
|
|
|
23
23
|
"uuid": "^13.0.0",
|
|
24
24
|
"yaml": "^2.8.3",
|
|
25
25
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.30",
|
|
26
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.11",
|
|
27
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
28
|
-
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.25",
|
|
29
26
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.34",
|
|
27
|
+
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.27",
|
|
28
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
29
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.11",
|
|
30
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
|
|
30
31
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
|
|
31
32
|
"@reventlessdev/rescript-web": "1.0.0-alpha.7",
|
|
32
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
33
|
-
"@reventlessdev/
|
|
34
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.188",
|
|
35
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.155",
|
|
33
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.293",
|
|
34
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.190",
|
|
36
35
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.39",
|
|
37
|
-
"@reventlessdev/reventless-
|
|
36
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.157",
|
|
37
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.162"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"rescript": "12.3.1",
|
package/src/Platform.res.mjs
CHANGED
|
@@ -485,7 +485,7 @@ function MakeWithConfig(Config) {
|
|
|
485
485
|
errorSchema: UiFragmentRegistry$ReventlessCore.errorSchema,
|
|
486
486
|
eventSchema: UiFragmentRegistry$ReventlessCore.eventSchema,
|
|
487
487
|
commandSchema: UiFragmentRegistry$ReventlessCore.commandSchema,
|
|
488
|
-
|
|
488
|
+
authorizationOf: UiFragmentRegistry$ReventlessCore.authorizationOf,
|
|
489
489
|
commandTransition: UiFragmentRegistry$ReventlessCore.commandTransition,
|
|
490
490
|
traits: UiFragmentRegistry$ReventlessCore.traits,
|
|
491
491
|
readConsistency: UiFragmentRegistry$ReventlessCore.readConsistency
|
|
@@ -682,7 +682,7 @@ function MakeWithConfig(Config) {
|
|
|
682
682
|
errorSchema: PluginSpec$ReventlessCore.errorSchema,
|
|
683
683
|
commandSchema: PluginSpec$ReventlessCore.commandSchema,
|
|
684
684
|
moduleUrl: PluginSpec$ReventlessCore.moduleUrl,
|
|
685
|
-
|
|
685
|
+
authorizationOf: PluginSpec$ReventlessCore.authorizationOf,
|
|
686
686
|
commandTransition: PluginSpec$ReventlessCore.commandTransition,
|
|
687
687
|
traits: PluginSpec$ReventlessCore.traits
|
|
688
688
|
})({
|
|
@@ -1901,7 +1901,7 @@ function Make($star) {
|
|
|
1901
1901
|
errorSchema: UiFragmentRegistry$ReventlessCore.errorSchema,
|
|
1902
1902
|
eventSchema: UiFragmentRegistry$ReventlessCore.eventSchema,
|
|
1903
1903
|
commandSchema: UiFragmentRegistry$ReventlessCore.commandSchema,
|
|
1904
|
-
|
|
1904
|
+
authorizationOf: UiFragmentRegistry$ReventlessCore.authorizationOf,
|
|
1905
1905
|
commandTransition: UiFragmentRegistry$ReventlessCore.commandTransition,
|
|
1906
1906
|
traits: UiFragmentRegistry$ReventlessCore.traits,
|
|
1907
1907
|
readConsistency: UiFragmentRegistry$ReventlessCore.readConsistency
|
|
@@ -2098,7 +2098,7 @@ function Make($star) {
|
|
|
2098
2098
|
errorSchema: PluginSpec$ReventlessCore.errorSchema,
|
|
2099
2099
|
commandSchema: PluginSpec$ReventlessCore.commandSchema,
|
|
2100
2100
|
moduleUrl: PluginSpec$ReventlessCore.moduleUrl,
|
|
2101
|
-
|
|
2101
|
+
authorizationOf: PluginSpec$ReventlessCore.authorizationOf,
|
|
2102
2102
|
commandTransition: PluginSpec$ReventlessCore.commandTransition,
|
|
2103
2103
|
traits: PluginSpec$ReventlessCore.traits
|
|
2104
2104
|
})({
|
|
@@ -168,7 +168,7 @@ export async function buildHandlersForConfig(config, opts = {}) {
|
|
|
168
168
|
|
|
169
169
|
// Route 0 registry: InboundTranslationSlice receive handlers keyed by their
|
|
170
170
|
// AppSync mutation field name. The resolver invokes this Lambda with
|
|
171
|
-
// {__inboundTranslation, fieldName, arguments}; the field name is
|
|
171
|
+
// {__inboundTranslation, fieldName, arguments, identity}; the field name is
|
|
172
172
|
// `<pluginName>_<sliceName>` (Api_Naming.sliceMutationField — the deploy side
|
|
173
173
|
// uses the same `name` that HANDLER_CONFIG.pluginName carries). The spec +
|
|
174
174
|
// translation modules are the one untyped seam (dynamic import); the typed
|
|
@@ -316,11 +316,8 @@ export async function handler(event, context) {
|
|
|
316
316
|
const [sqsHandler, cmdGenHandler, comp, plugin, inboundReceivers] = await initPromise;
|
|
317
317
|
|
|
318
318
|
// Route 0: InboundTranslationSlice mutation — the AppSync resolver invokes this
|
|
319
|
-
// Lambda with `{__inboundTranslation: true, fieldName, arguments}
|
|
320
|
-
//
|
|
321
|
-
// publishes and returns a commandOutcome JSON byte-compatible with Route 1's
|
|
322
|
-
// `commandOutcomeToJson`. Without this branch the payload fell through to Route 2
|
|
323
|
-
// and crashed on `event.records` being undefined.
|
|
319
|
+
// Lambda with `{__inboundTranslation: true, fieldName, arguments, identity}`.
|
|
320
|
+
// The field's receiver checks each translated command against `identity`.
|
|
324
321
|
if (event.__inboundTranslation === true) {
|
|
325
322
|
const fieldName = event.fieldName;
|
|
326
323
|
const receiver = (inboundReceivers || {})[fieldName];
|
|
@@ -329,7 +326,7 @@ export async function handler(event, context) {
|
|
|
329
326
|
throw new Error("no inbound translation receiver for field: " + fieldName);
|
|
330
327
|
}
|
|
331
328
|
log.debug("InboundTranslation invocation (" + fieldName + ")", { comp: "DcbCommandTopicRuntime" });
|
|
332
|
-
return await receiver(event.arguments);
|
|
329
|
+
return await receiver(event.arguments, event.identity ?? null);
|
|
333
330
|
}
|
|
334
331
|
|
|
335
332
|
// Route 1: AppSync direct invocation — payload carries the CommandGenerator.payload
|
|
@@ -246,13 +246,8 @@ let buildSliceHandler = (
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
// ── Inbound translation receiver wiring (Route 0) ───────────────────────────
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
// `{__inboundTranslation, fieldName, arguments}` payload. Building the per-field
|
|
252
|
-
// `receive` here keeps the curried `InboundTranslationSlice_Callback.Make` functor
|
|
253
|
-
// call compiler-checked (same rationale as `buildSliceHandler`) and mirrors the
|
|
254
|
-
// in-process composite handler in `Dcb_Builder.res`, so the deployed surface and
|
|
255
|
-
// the local surface encode the same `commandOutcome`.
|
|
249
|
+
// Every inbound mutation's resolver invokes this Lambda with a `doorInvocation`.
|
|
250
|
+
// The functor call lives here to be compiler-checked, as in `buildSliceHandler`.
|
|
256
251
|
|
|
257
252
|
// The dynamically-imported Translation module — opaque; a clean pass-through to
|
|
258
253
|
// the functor (only `translate` is read, inside the compiled callback).
|
|
@@ -262,6 +257,7 @@ type inboundCallback = {
|
|
|
262
257
|
receive: (
|
|
263
258
|
ReventlessInfra.CommandTopic.publishJsons,
|
|
264
259
|
JSON.t,
|
|
260
|
+
~caller: Reventless.Identity.t=?,
|
|
265
261
|
) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>,
|
|
266
262
|
auditLog: dict<ReventlessCore.InboundTranslationSlice_Callback.auditRow>,
|
|
267
263
|
}
|
|
@@ -270,12 +266,9 @@ type inboundCallback = {
|
|
|
270
266
|
)
|
|
271
267
|
external makeInboundCallback: specModule => translationModule => inboundCallback = "Make"
|
|
272
268
|
|
|
273
|
-
// Builds the inbound receive handler:
|
|
274
|
-
// the
|
|
275
|
-
//
|
|
276
|
-
// drain the in-memory audit log to that table (best-effort, matching
|
|
277
|
-
// `InboundTranslationSlice_Builder`'s inline `syncToQueryDb`). Returns the
|
|
278
|
-
// `commandOutcome` JSON, byte-compatible with the AppSync direct-invocation route.
|
|
269
|
+
// Builds the inbound receive handler: `receive` for the resolver's caller, then
|
|
270
|
+
// the request's audit row to its table when one was threaded. Answers the
|
|
271
|
+
// `commandOutcome` JSON the AppSync direct-invocation route answers with.
|
|
279
272
|
let buildInboundReceiver = (
|
|
280
273
|
spec: specModule,
|
|
281
274
|
translation: translationModule,
|
|
@@ -283,8 +276,12 @@ let buildInboundReceiver = (
|
|
|
283
276
|
~auditQueryDbOps: option<ReventlessCore.QueryDb_Adapter.operations>,
|
|
284
277
|
) => {
|
|
285
278
|
let callback = makeInboundCallback(spec)(translation)
|
|
286
|
-
async (args: JSON.t): JSON.t => {
|
|
287
|
-
let result = await callback.receive(
|
|
279
|
+
async (args: JSON.t, identity: Nullable.t<Reventless.Identity.t>): JSON.t => {
|
|
280
|
+
let result = await callback.receive(
|
|
281
|
+
publishJsons,
|
|
282
|
+
args,
|
|
283
|
+
~caller=ReventlessCore.InboundTranslationSlice_Callback.doorCaller(identity),
|
|
284
|
+
)
|
|
288
285
|
switch auditQueryDbOps {
|
|
289
286
|
| Some(ops) =>
|
|
290
287
|
let id = result->ReventlessCore.InboundTranslationSlice_Callback.requestIdOf
|
|
@@ -294,12 +291,8 @@ let buildInboundReceiver = (
|
|
|
294
291
|
row->Reventless.Util_Sury.toJson(
|
|
295
292
|
ReventlessCore.InboundTranslationSlice_Callback.auditRowSchema,
|
|
296
293
|
)
|
|
297
|
-
// A failed audit write
|
|
298
|
-
//
|
|
299
|
-
// with `Error(_)` on a storage failure (it does not throw), so the
|
|
300
|
-
// result must be inspected — a bare `let _ = await ops.save(...)` would
|
|
301
|
-
// swallow the failure and leave the audit view permanently, silently
|
|
302
|
-
// empty. `try` still guards an unexpected reject.
|
|
294
|
+
// A failed audit write is logged, not raised: the commands are published.
|
|
295
|
+
// `save` resolves `Error(_)` rather than throwing, so its result is read.
|
|
303
296
|
try {
|
|
304
297
|
switch await ops.save(id, json, ReventlessCore.QueryDb.Overwrite, None) {
|
|
305
298
|
| Ok() => ()
|
|
@@ -324,9 +317,7 @@ let buildInboundReceiver = (
|
|
|
324
317
|
}
|
|
325
318
|
result
|
|
326
319
|
->ReventlessCore.InboundTranslationSlice_Callback.receiveResultToOutcome
|
|
327
|
-
// `CommandTopic_Helpers`, not `CommandTopic
|
|
328
|
-
// (→ `@pulumi/pulumi`) for a deploy-time helper, which would crash this
|
|
329
|
-
// runtime Lambda's cold start. The encoder itself lives in the pure Helpers.
|
|
320
|
+
// `CommandTopic_Helpers`, not `CommandTopic`, which imports `@pulumi/pulumi`.
|
|
330
321
|
->ReventlessCore.CommandTopic_Helpers.commandOutcomeToJson
|
|
331
322
|
}
|
|
332
323
|
}
|
|
@@ -120,8 +120,8 @@ function buildSliceHandler(spec, behavior, scope, dcbEventLog) {
|
|
|
120
120
|
|
|
121
121
|
function buildInboundReceiver(spec, translation, publishJsons, auditQueryDbOps) {
|
|
122
122
|
let callback = InboundTranslationSlice_CallbackResMjs.Make(spec)(translation);
|
|
123
|
-
return async args => {
|
|
124
|
-
let result = await callback.receive(publishJsons, args);
|
|
123
|
+
return async (args, identity) => {
|
|
124
|
+
let result = await callback.receive(publishJsons, args, InboundTranslationSlice_Callback$ReventlessCore.doorCaller(identity));
|
|
125
125
|
if (auditQueryDbOps !== undefined) {
|
|
126
126
|
let id = InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result);
|
|
127
127
|
let row = InboundTranslationSlice_Callback$ReventlessCore.takeAuditRow(callback.auditLog, id);
|
|
@@ -149,6 +149,19 @@ describe("AppSync_Adapter.generateFragment", () => {
|
|
|
149
149
|
// `@authorize` PPX annotations) into `@aws_cognito_user_pools(cognito_groups: [...])`
|
|
150
150
|
// directives on the corresponding SDL fields.
|
|
151
151
|
|
|
152
|
+
// Two constructors under different roles, the case one field gate must widen for.
|
|
153
|
+
module TwoRuleFeed = {
|
|
154
|
+
@schema
|
|
155
|
+
type command = AddProduct({sku: string}) | AddCategory({sku: string})
|
|
156
|
+
|
|
157
|
+
let authorizationOf = (name: string): Reventless.Authorization.rule<Reventless.Role.name> =>
|
|
158
|
+
switch name {
|
|
159
|
+
| "AddProduct" => AllowRoles([Reventless.Role.make("Merchandiser")])
|
|
160
|
+
| "AddCategory" => AllowRoles([Reventless.Role.make("Admin")])
|
|
161
|
+
| _ => DenyAll
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
152
165
|
describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () => {
|
|
153
166
|
// Minimal mutation fragment with two fields. Field names must match what
|
|
154
167
|
// the entries declare so the stitcher's extractLeadingName can pair them.
|
|
@@ -225,6 +238,22 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
225
238
|
)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "shop-merch-team"])`)
|
|
226
239
|
})
|
|
227
240
|
|
|
241
|
+
// An inbound slice has one field for every constructor, so its gate admits any
|
|
242
|
+
// constructor's roles; each command is checked against its own in `receive`.
|
|
243
|
+
testSync("an inbound slice's field carries every constructor's groups", () => {
|
|
244
|
+
let permission = ReventlessCore.Dcb_Builder.inboundDoorPermission(
|
|
245
|
+
TwoRuleFeed.commandSchema,
|
|
246
|
+
TwoRuleFeed.authorizationOf,
|
|
247
|
+
)
|
|
248
|
+
let fp = Dict.fromArray(permission->Option.mapOr([], p => [("p_TwoRuleFeed", p)]))
|
|
249
|
+
let entry = mutationEntry(~fieldNames=["p_TwoRuleFeed"], ~fieldPermissions=fp)
|
|
250
|
+
let frag = makeFragment(["p_TwoRuleFeed(sku: String!): String"], [])
|
|
251
|
+
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
|
|
252
|
+
expect(
|
|
253
|
+
decodeFragment(aug).mutations->Array.getUnsafe(0),
|
|
254
|
+
)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Merchandiser", "Admin"])`)
|
|
255
|
+
})
|
|
256
|
+
|
|
228
257
|
testSync("AllowAuthenticated emits the group-less Cognito directive", () => {
|
|
229
258
|
let fp = Dict.fromArray([("p_Add", Reventless.Authorization.AllowAuthenticated)])
|
|
230
259
|
let entry = mutationEntry(~fieldNames=["p_Add"], ~fieldPermissions=fp)
|
|
@@ -6,6 +6,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
6
6
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
7
7
|
import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
|
|
8
8
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
9
|
+
import * as Dcb_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/Dcb/Dcb_Builder.res.mjs";
|
|
9
10
|
import * as AppSync_Adapter$ReventlessAws from "../src/components/Api/AppSync_Adapter.res.mjs";
|
|
10
11
|
import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
11
12
|
import * as Platform_AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminApi.res.mjs";
|
|
@@ -109,6 +110,39 @@ globalThis.describe("AppSync_Adapter.generateFragment", () => {
|
|
|
109
110
|
});
|
|
110
111
|
});
|
|
111
112
|
|
|
113
|
+
let commandSchema = Sury.union([
|
|
114
|
+
Sury.$schema(s => ({
|
|
115
|
+
TAG: "AddProduct",
|
|
116
|
+
sku: s.m(Sury.string)
|
|
117
|
+
})),
|
|
118
|
+
Sury.$schema(s => ({
|
|
119
|
+
TAG: "AddCategory",
|
|
120
|
+
sku: s.m(Sury.string)
|
|
121
|
+
}))
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
function authorizationOf(name) {
|
|
125
|
+
switch (name) {
|
|
126
|
+
case "AddCategory" :
|
|
127
|
+
return {
|
|
128
|
+
TAG: "AllowRoles",
|
|
129
|
+
_0: ["Admin"]
|
|
130
|
+
};
|
|
131
|
+
case "AddProduct" :
|
|
132
|
+
return {
|
|
133
|
+
TAG: "AllowRoles",
|
|
134
|
+
_0: ["Merchandiser"]
|
|
135
|
+
};
|
|
136
|
+
default:
|
|
137
|
+
return "DenyAll";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let TwoRuleFeed = {
|
|
142
|
+
commandSchema: commandSchema,
|
|
143
|
+
authorizationOf: authorizationOf
|
|
144
|
+
};
|
|
145
|
+
|
|
112
146
|
globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () => {
|
|
113
147
|
let makeFragment = (mutationFields, queryFields) => GraphQL_Stitcher$ReventlessCore.encode({
|
|
114
148
|
types: [],
|
|
@@ -169,6 +203,17 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
169
203
|
Role$Reventless.clearGroups();
|
|
170
204
|
globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(aug).mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "shop-merch-team"])`);
|
|
171
205
|
});
|
|
206
|
+
globalThis.test("an inbound slice's field carries every constructor's groups", () => {
|
|
207
|
+
let permission = Dcb_Builder$ReventlessCore.inboundDoorPermission(commandSchema, authorizationOf);
|
|
208
|
+
let fp = Object.fromEntries(Stdlib_Option.mapOr(permission, [], p => [[
|
|
209
|
+
"p_TwoRuleFeed",
|
|
210
|
+
p
|
|
211
|
+
]]));
|
|
212
|
+
let entry = mutationEntry(["p_TwoRuleFeed"], fp);
|
|
213
|
+
let frag = makeFragment(["p_TwoRuleFeed(sku: String!): String"], []);
|
|
214
|
+
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
215
|
+
globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(aug).mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Merchandiser", "Admin"])`);
|
|
216
|
+
});
|
|
172
217
|
globalThis.test("AllowAuthenticated emits the group-less Cognito directive", () => {
|
|
173
218
|
let fp = Object.fromEntries([[
|
|
174
219
|
"p_Add",
|
|
@@ -875,5 +920,6 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — subscriptions", () => {
|
|
|
875
920
|
|
|
876
921
|
export {
|
|
877
922
|
decodeFragment,
|
|
923
|
+
TwoRuleFeed,
|
|
878
924
|
}
|
|
879
925
|
/* Not a pure module */
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// `buildInboundReceiver` over the EpInboundTestSlice fixture, publishing into an
|
|
2
|
+
// array. Answers `[outcome, publishedCount]`.
|
|
3
|
+
import { buildInboundReceiver } from "@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs";
|
|
4
|
+
import * as spec from "./EpInboundTestSlice.res.mjs";
|
|
5
|
+
import * as translation from "./EpInboundTestSliceTranslation.res.mjs";
|
|
6
|
+
|
|
7
|
+
export async function receiveWithStubPublish(args, identity) {
|
|
8
|
+
const published = [];
|
|
9
|
+
const receive = buildInboundReceiver(spec, translation, async (cmds) => { published.push(...cmds); }, undefined);
|
|
10
|
+
const outcome = await receive(args, identity);
|
|
11
|
+
return [outcome, published.length];
|
|
12
|
+
}
|
|
@@ -39,10 +39,33 @@ let routeInbound: JSON.t => promise<JSON.t> = %raw(`
|
|
|
39
39
|
// Mirror handler's Route 0 dispatch.
|
|
40
40
|
const receiver = (inboundReceivers || {})[event.fieldName];
|
|
41
41
|
if (receiver === undefined) throw new Error("no inbound receiver for " + event.fieldName);
|
|
42
|
-
return await receiver(event.arguments);
|
|
42
|
+
return await receiver(event.arguments, event.identity ?? null);
|
|
43
43
|
}
|
|
44
44
|
`)
|
|
45
45
|
|
|
46
|
+
// The receiver over the fixture modules with a publish that records, so an
|
|
47
|
+
// admitted caller's commands are seen without SQS.
|
|
48
|
+
@module("./DcbInboundReceiverStub.mjs")
|
|
49
|
+
external receiveWithStubPublish: (
|
|
50
|
+
JSON.t,
|
|
51
|
+
Nullable.t<Reventless.Identity.t>,
|
|
52
|
+
) => promise<(JSON.t, int)> = "receiveWithStubPublish"
|
|
53
|
+
|
|
54
|
+
let usdArguments: JSON.t =
|
|
55
|
+
Dict.fromArray([
|
|
56
|
+
("sku", "SKU-1"->JSON.Encode.string),
|
|
57
|
+
("currency", "USD"->JSON.Encode.string),
|
|
58
|
+
])->JSON.Encode.object
|
|
59
|
+
|
|
60
|
+
let cognito = (groups): Reventless.Identity.t => {
|
|
61
|
+
userId: "u-1",
|
|
62
|
+
username: "alice",
|
|
63
|
+
groups,
|
|
64
|
+
provider: Cognito,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let typenameOf = (outcome: JSON.t) => outcome->JSON.stringify
|
|
68
|
+
|
|
46
69
|
let inboundEvent = (~fieldName, ~currency): JSON.t => {
|
|
47
70
|
let arguments = Dict.fromArray([
|
|
48
71
|
("sku", "SKU-1"->JSON.Encode.string),
|
|
@@ -70,4 +93,29 @@ describe("DcbCommandTopicEntryPoint Route 0 (InboundTranslation)", () => {
|
|
|
70
93
|
expect(s->String.includes("Unsupported currency"))->toBe(true)
|
|
71
94
|
},
|
|
72
95
|
)
|
|
96
|
+
|
|
97
|
+
test("the payload's caller reaches the per-command check", async () => {
|
|
98
|
+
let (outcome, count) = await receiveWithStubPublish(
|
|
99
|
+
usdArguments,
|
|
100
|
+
Nullable.make(cognito(["Ops"])),
|
|
101
|
+
)
|
|
102
|
+
expect(typenameOf(outcome)->String.includes("Forbidden"))->toBe(true)
|
|
103
|
+
expect(count)->toBe(0)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("a caller holding the command's role publishes", async () => {
|
|
107
|
+
let (outcome, count) = await receiveWithStubPublish(
|
|
108
|
+
usdArguments,
|
|
109
|
+
Nullable.make(cognito(["Admin"])),
|
|
110
|
+
)
|
|
111
|
+
expect(typenameOf(outcome)->String.includes("CommandAccepted"))->toBe(true)
|
|
112
|
+
expect(count)->toBe(1)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// A resolver that identified nobody must not be taken for the platform itself.
|
|
116
|
+
test("a payload with no identity is refused", async () => {
|
|
117
|
+
let (outcome, count) = await receiveWithStubPublish(usdArguments, Nullable.null)
|
|
118
|
+
expect(typenameOf(outcome)->String.includes("Forbidden"))->toBe(true)
|
|
119
|
+
expect(count)->toBe(0)
|
|
120
|
+
})
|
|
73
121
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as DcbInboundReceiverStubMjs from "./DcbInboundReceiverStub.mjs";
|
|
4
5
|
|
|
5
6
|
let routeInbound = (async (event) => {
|
|
6
7
|
const { buildHandlersForConfig } = await import(
|
|
@@ -24,9 +25,37 @@ let routeInbound = (async (event) => {
|
|
|
24
25
|
// Mirror handler's Route 0 dispatch.
|
|
25
26
|
const receiver = (inboundReceivers || {})[event.fieldName];
|
|
26
27
|
if (receiver === undefined) throw new Error("no inbound receiver for " + event.fieldName);
|
|
27
|
-
return await receiver(event.arguments);
|
|
28
|
+
return await receiver(event.arguments, event.identity ?? null);
|
|
28
29
|
});
|
|
29
30
|
|
|
31
|
+
function receiveWithStubPublish(prim0, prim1) {
|
|
32
|
+
return DcbInboundReceiverStubMjs.receiveWithStubPublish(prim0, prim1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let usdArguments = Object.fromEntries([
|
|
36
|
+
[
|
|
37
|
+
"sku",
|
|
38
|
+
"SKU-1"
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"currency",
|
|
42
|
+
"USD"
|
|
43
|
+
]
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
function cognito(groups) {
|
|
47
|
+
return {
|
|
48
|
+
userId: "u-1",
|
|
49
|
+
username: "alice",
|
|
50
|
+
groups: groups,
|
|
51
|
+
provider: "Cognito"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function typenameOf(outcome) {
|
|
56
|
+
return JSON.stringify(outcome);
|
|
57
|
+
}
|
|
58
|
+
|
|
30
59
|
function inboundEvent(fieldName, currency) {
|
|
31
60
|
let $$arguments = Object.fromEntries([
|
|
32
61
|
[
|
|
@@ -62,10 +91,29 @@ globalThis.describe("DcbCommandTopicEntryPoint Route 0 (InboundTranslation)", ()
|
|
|
62
91
|
globalThis.expect(s.includes("CommandRejected")).toBe(true);
|
|
63
92
|
globalThis.expect(s.includes("Unsupported currency")).toBe(true);
|
|
64
93
|
});
|
|
94
|
+
globalThis.test("the payload's caller reaches the per-command check", async () => {
|
|
95
|
+
let match = await DcbInboundReceiverStubMjs.receiveWithStubPublish(usdArguments, cognito(["Ops"]));
|
|
96
|
+
globalThis.expect(JSON.stringify(match[0]).includes("Forbidden")).toBe(true);
|
|
97
|
+
globalThis.expect(match[1]).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
globalThis.test("a caller holding the command's role publishes", async () => {
|
|
100
|
+
let match = await DcbInboundReceiverStubMjs.receiveWithStubPublish(usdArguments, cognito(["Admin"]));
|
|
101
|
+
globalThis.expect(JSON.stringify(match[0]).includes("CommandAccepted")).toBe(true);
|
|
102
|
+
globalThis.expect(match[1]).toBe(1);
|
|
103
|
+
});
|
|
104
|
+
globalThis.test("a payload with no identity is refused", async () => {
|
|
105
|
+
let match = await DcbInboundReceiverStubMjs.receiveWithStubPublish(usdArguments, null);
|
|
106
|
+
globalThis.expect(JSON.stringify(match[0]).includes("Forbidden")).toBe(true);
|
|
107
|
+
globalThis.expect(match[1]).toBe(0);
|
|
108
|
+
});
|
|
65
109
|
});
|
|
66
110
|
|
|
67
111
|
export {
|
|
68
112
|
routeInbound,
|
|
113
|
+
receiveWithStubPublish,
|
|
114
|
+
usdArguments,
|
|
115
|
+
cognito,
|
|
116
|
+
typenameOf,
|
|
69
117
|
inboundEvent,
|
|
70
118
|
}
|
|
71
119
|
/* routeInbound Not a pure module */
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// component, so no folder-based ppx; only the fields
|
|
4
|
-
// InboundTranslationSlice_Callback.Make reads at runtime are provided.
|
|
1
|
+
// Inbound spec fixture for DcbInboundTranslationRoutingTest. Hand-written, so it
|
|
2
|
+
// declares the members the PPX would otherwise inject.
|
|
5
3
|
|
|
6
4
|
let name = "EpInboundTest"
|
|
7
5
|
let moduleUrl = "ep-inbound-test://spec"
|
|
@@ -17,3 +15,8 @@ type command = AddThing({thingId: string})
|
|
|
17
15
|
|
|
18
16
|
let targetName = "AddThing"
|
|
19
17
|
let externalSystem: option<string> = Some("TestFeed")
|
|
18
|
+
|
|
19
|
+
type role = Reventless.Role.name
|
|
20
|
+
let authorizationOf = (_: string): Reventless.Authorization.rule<role> => AllowRoles([
|
|
21
|
+
Reventless.Role.make("Admin"),
|
|
22
|
+
])
|
|
@@ -12,6 +12,13 @@ let commandSchema = Sury.$schema(s => ({
|
|
|
12
12
|
thingId: s.m(Sury.string)
|
|
13
13
|
}));
|
|
14
14
|
|
|
15
|
+
function authorizationOf(param) {
|
|
16
|
+
return {
|
|
17
|
+
TAG: "AllowRoles",
|
|
18
|
+
_0: ["Admin"]
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
let name = "EpInboundTest";
|
|
16
23
|
|
|
17
24
|
let moduleUrl = "ep-inbound-test://spec";
|
|
@@ -27,5 +34,6 @@ export {
|
|
|
27
34
|
commandSchema,
|
|
28
35
|
targetName,
|
|
29
36
|
externalSystem,
|
|
37
|
+
authorizationOf,
|
|
30
38
|
}
|
|
31
39
|
/* externalInputSchema Not a pure module */
|
|
@@ -58,9 +58,9 @@ describe("listAllItemsConnection — paging", () => {
|
|
|
58
58
|
|
|
59
59
|
testSync("a filtered read examines more rows than it serves", () => {
|
|
60
60
|
// The budget reads as deep as the page needs, which for a backward cut is
|
|
61
|
-
// `_upTo` rather than `_first + _from`.
|
|
61
|
+
// `_upTo` rather than one row past `_first + _from`.
|
|
62
62
|
expect(
|
|
63
|
-
code->String.includes("parts.length > 0 ? ((_backward ? _upTo : _first + _from) > 1000"),
|
|
63
|
+
code->String.includes("parts.length > 0 ? ((_backward ? _upTo : _first + _from + 1) > 1000"),
|
|
64
64
|
)->toBe(true)
|
|
65
65
|
expect(
|
|
66
66
|
code->String.includes("const _page = _backward ? _rest : _rest.slice(0, _first);"),
|
|
@@ -37,7 +37,7 @@ globalThis.describe("listAllItemsConnection — paging", () => {
|
|
|
37
37
|
globalThis.expect(code.includes("ctx.args.after + '_' + i")).toBe(false);
|
|
38
38
|
});
|
|
39
39
|
globalThis.test("a filtered read examines more rows than it serves", () => {
|
|
40
|
-
globalThis.expect(code.includes("parts.length > 0 ? ((_backward ? _upTo : _first + _from) > 1000")).toBe(true);
|
|
40
|
+
globalThis.expect(code.includes("parts.length > 0 ? ((_backward ? _upTo : _first + _from + 1) > 1000")).toBe(true);
|
|
41
41
|
globalThis.expect(code.includes("const _page = _backward ? _rest : _rest.slice(0, _first);")).toBe(true);
|
|
42
42
|
globalThis.expect(code.includes("hasNextPage: _more || !!_next,")).toBe(true);
|
|
43
43
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// wired into reventless-core's rescript.json, not reventless-aws's), exposing
|
|
6
6
|
// exactly the fields the compiled `@@reventless.spec` output would: name, Id
|
|
7
7
|
// (patched in at runtime by patchSpecId), commandSchema/eventSchema/errorSchema
|
|
8
|
-
// (via sury-ppx), moduleUrl,
|
|
8
|
+
// (via sury-ppx), moduleUrl, authorizationOf.
|
|
9
9
|
|
|
10
10
|
@schema
|
|
11
11
|
type command = Add({name: string})
|
|
@@ -19,6 +19,6 @@ type error = AlreadyExists
|
|
|
19
19
|
let name = "AggTestAggregate"
|
|
20
20
|
let moduleUrl = "agg-test://AggTestAggregate"
|
|
21
21
|
type role = Reventless.Role.name
|
|
22
|
-
let
|
|
22
|
+
let authorizationOf = (_: string): Reventless.Authorization.rule<role> => AllowAnonymous
|
|
23
23
|
type lifecycleState = unit
|
|
24
24
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
@@ -14,7 +14,7 @@ let eventSchema = Sury.$schema(s => ({
|
|
|
14
14
|
|
|
15
15
|
let errorSchema = Sury.literal("AlreadyExists");
|
|
16
16
|
|
|
17
|
-
function
|
|
17
|
+
function authorizationOf(param) {
|
|
18
18
|
return "AllowAnonymous";
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -32,7 +32,7 @@ export {
|
|
|
32
32
|
errorSchema,
|
|
33
33
|
name,
|
|
34
34
|
moduleUrl,
|
|
35
|
-
|
|
35
|
+
authorizationOf,
|
|
36
36
|
commandTransition,
|
|
37
37
|
}
|
|
38
38
|
/* commandSchema Not a pure module */
|
|
@@ -64,7 +64,7 @@ type event =
|
|
|
64
64
|
let name = "EpCompositeSlice"
|
|
65
65
|
let moduleUrl = "ep-test://EpCompositeSlice"
|
|
66
66
|
type role = Reventless.Role.name
|
|
67
|
-
let
|
|
67
|
+
let authorizationOf = (_: string): Reventless.Authorization.rule<role> => AllowAnonymous
|
|
68
68
|
type lifecycleState = unit
|
|
69
69
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
70
70
|
let readConsistency = Reventless.ReadConsistency.EscalateOnRetry
|
|
@@ -47,7 +47,7 @@ let eventSchema = Sury.union([
|
|
|
47
47
|
}))
|
|
48
48
|
]);
|
|
49
49
|
|
|
50
|
-
function
|
|
50
|
+
function authorizationOf(param) {
|
|
51
51
|
return "AllowAnonymous";
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -68,7 +68,7 @@ export {
|
|
|
68
68
|
eventSchema,
|
|
69
69
|
name,
|
|
70
70
|
moduleUrl,
|
|
71
|
-
|
|
71
|
+
authorizationOf,
|
|
72
72
|
commandTransition,
|
|
73
73
|
readConsistency,
|
|
74
74
|
}
|
|
@@ -22,7 +22,7 @@ type event = WidgetAdded({widgetId: @s.matches(Reventless.DcbTag.partition) stri
|
|
|
22
22
|
let name = "EpTestSlice"
|
|
23
23
|
let moduleUrl = "ep-test://EpTestSlice"
|
|
24
24
|
type role = Reventless.Role.name
|
|
25
|
-
let
|
|
25
|
+
let authorizationOf = (_: string): Reventless.Authorization.rule<role> => AllowAnonymous
|
|
26
26
|
type lifecycleState = unit
|
|
27
27
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
28
28
|
let readConsistency = Reventless.ReadConsistency.EscalateOnRetry
|
|
@@ -20,7 +20,7 @@ let eventSchema = Sury.$schema(s => ({
|
|
|
20
20
|
widgetId: s.m(DcbTag$Reventless.partition)
|
|
21
21
|
}));
|
|
22
22
|
|
|
23
|
-
function
|
|
23
|
+
function authorizationOf(param) {
|
|
24
24
|
return "AllowAnonymous";
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -41,7 +41,7 @@ export {
|
|
|
41
41
|
eventSchema,
|
|
42
42
|
name,
|
|
43
43
|
moduleUrl,
|
|
44
|
-
|
|
44
|
+
authorizationOf,
|
|
45
45
|
commandTransition,
|
|
46
46
|
readConsistency,
|
|
47
47
|
}
|