@reventlessdev/reventless-aws 3.0.0-alpha.171 → 3.0.0-alpha.173
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 +23 -0
- package/package.json +6 -5
- package/rescript.json +2 -1
- package/src/Platform.res.mjs +5 -5
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +1 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +12 -6
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +17 -5
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res +26 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs +15 -0
- package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res +23 -0
- package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res.mjs +14 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +181 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +135 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +64 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +36 -0
- package/src/adapter/Postgres/PgConnection.res +169 -0
- package/src/adapter/Postgres/PgConnection.res.mjs +112 -0
- package/src/adapter/Postgres/PgRuntime.res +71 -0
- package/src/adapter/Postgres/PgRuntime.res.mjs +71 -0
- package/src/adapter/Runtime/AggregateEntryPoint.mjs +27 -9
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +3 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res.mjs +3 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res.mjs +1 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res.mjs +1 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +1 -1
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +20 -6
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +1 -1
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +1 -1
- package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +52 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +38 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +20 -2
- package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +1 -1
- package/src/components/Api/AppSync_Adapter.res +72 -8
- package/src/components/Api/AppSync_Adapter.res.mjs +45 -6
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +2 -2
- package/src/util/Util_AppSync_Caller.res +24 -4
- package/src/util/Util_AppSync_Caller.res.mjs +7 -4
- package/tests/AppSync_AdapterTest.res +140 -0
- package/tests/AppSync_AdapterTest.res.mjs +182 -6
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +7 -3
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +18 -3
- package/tests/PgChangeFeedRelay_RuntimeTest.res +44 -0
- package/tests/PgChangeFeedRelay_RuntimeTest.res.mjs +70 -0
- package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res +2 -1
- package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res.mjs +2 -2
|
@@ -47,9 +47,11 @@ function jsonToLiteral(value) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function buildQuery(mutation, variablesDict) {
|
|
50
|
+
function buildQuery(mutation, selectionOpt, variablesDict) {
|
|
51
|
+
let selection = selectionOpt !== undefined ? selectionOpt : "{ __typename }";
|
|
51
52
|
let args = Object.entries(variablesDict).map(param => param[0] + `: ` + jsonToLiteral(param[1])).join(", ");
|
|
52
|
-
|
|
53
|
+
let sel = selection === "" ? "" : ` ` + selection;
|
|
54
|
+
return `mutation { ` + mutation + `(` + args + `)` + sel + ` }`;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
async function sendQuery(endpoint, region, queryString) {
|
|
@@ -104,7 +106,8 @@ async function sendQuery(endpoint, region, queryString) {
|
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
async function sendMutation(endpoint, region, mutation, variables) {
|
|
109
|
+
async function sendMutation(endpoint, region, mutation, selectionOpt, variables) {
|
|
110
|
+
let selection = selectionOpt !== undefined ? selectionOpt : "{ __typename }";
|
|
108
111
|
let url = new URL(endpoint);
|
|
109
112
|
let hostname = url.hostname;
|
|
110
113
|
let path = url.pathname;
|
|
@@ -116,7 +119,7 @@ async function sendMutation(endpoint, region, mutation, variables) {
|
|
|
116
119
|
} else {
|
|
117
120
|
variablesDict = {};
|
|
118
121
|
}
|
|
119
|
-
let query = buildQuery(mutation, variablesDict);
|
|
122
|
+
let query = buildQuery(mutation, selection, variablesDict);
|
|
120
123
|
let body = JSON.stringify(Object.fromEntries([[
|
|
121
124
|
"query",
|
|
122
125
|
query
|
|
@@ -347,6 +347,146 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
347
347
|
})
|
|
348
348
|
})
|
|
349
349
|
|
|
350
|
+
// ── Dual-auth (Cognito + IAM) for deploy-time system callers ────────────────
|
|
351
|
+
//
|
|
352
|
+
// A field flagged `iamCallable` must be reachable by BOTH the console UI
|
|
353
|
+
// (Cognito) and the deploy-time SigV4 system caller (AWS_IAM). It switches from
|
|
354
|
+
// the single-mode `@aws_auth(...)` form to the multi-auth
|
|
355
|
+
// `@aws_cognito_user_pools(...) @aws_iam` form.
|
|
356
|
+
|
|
357
|
+
describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", () => {
|
|
358
|
+
let makeFragment = (mutationFields, queryFields) =>
|
|
359
|
+
ReventlessCore.GraphQL_Stitcher.encode({
|
|
360
|
+
types: [],
|
|
361
|
+
mutations: mutationFields,
|
|
362
|
+
queries: queryFields,
|
|
363
|
+
subscriptions: [],
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
testSync("iamCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
|
|
367
|
+
let entry: ReventlessInfra.Api.mutationSchemaEntry = {
|
|
368
|
+
fieldNames: ["p_Sync"],
|
|
369
|
+
commandSchema: S.unknown,
|
|
370
|
+
fieldPermissions: Dict.fromArray([("p_Sync", Reventless.Authorization.AllowGroups(["Admin"]))]),
|
|
371
|
+
iamCallable: true,
|
|
372
|
+
}
|
|
373
|
+
let frag = makeFragment(["p_Sync(id: ID!): String"], [])
|
|
374
|
+
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
|
|
375
|
+
let m = decodeFragment(aug).mutations->Array.getUnsafe(0)
|
|
376
|
+
expect(m)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
377
|
+
expect(m)->toContain("@aws_iam")
|
|
378
|
+
// Must NOT emit the single-mode form, which does not admit IAM.
|
|
379
|
+
expect(m)->not_->toContain("@aws_auth")
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
testSync("iamCallable mutation without a group restriction stays open to Cognito + IAM", () => {
|
|
383
|
+
let entry: ReventlessInfra.Api.mutationSchemaEntry = {
|
|
384
|
+
fieldNames: ["p_Sync"],
|
|
385
|
+
commandSchema: S.unknown,
|
|
386
|
+
fieldPermissions: Dict.fromArray([("p_Sync", Reventless.Authorization.AllowAuthenticated)]),
|
|
387
|
+
iamCallable: true,
|
|
388
|
+
}
|
|
389
|
+
let frag = makeFragment(["p_Sync(id: ID!): String"], [])
|
|
390
|
+
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
|
|
391
|
+
let m = decodeFragment(aug).mutations->Array.getUnsafe(0)
|
|
392
|
+
// Bare @aws_cognito_user_pools (any authenticated Cognito user) + IAM.
|
|
393
|
+
expect(m)->toContain("@aws_cognito_user_pools @aws_iam")
|
|
394
|
+
expect(m)->not_->toContain("cognito_groups")
|
|
395
|
+
expect(m)->not_->toContain("@aws_auth")
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
testSync("a non-iamCallable sibling keeps single-mode @aws_auth (no @aws_iam)", () => {
|
|
399
|
+
let entry: ReventlessInfra.Api.mutationSchemaEntry = {
|
|
400
|
+
fieldNames: ["p_Sync", "p_Other"],
|
|
401
|
+
commandSchema: S.unknown,
|
|
402
|
+
fieldPermissions: Dict.fromArray([
|
|
403
|
+
("p_Sync", Reventless.Authorization.AllowGroups(["Admin"])),
|
|
404
|
+
("p_Other", Reventless.Authorization.AllowGroups(["Admin"])),
|
|
405
|
+
]),
|
|
406
|
+
// iamCallable applies to every field in the entry; split the fields so only
|
|
407
|
+
// p_Sync opts in.
|
|
408
|
+
iamCallable: false,
|
|
409
|
+
}
|
|
410
|
+
let iamEntry: ReventlessInfra.Api.mutationSchemaEntry = {
|
|
411
|
+
fieldNames: ["p_Sync"],
|
|
412
|
+
commandSchema: S.unknown,
|
|
413
|
+
fieldPermissions: Dict.fromArray([("p_Sync", Reventless.Authorization.AllowGroups(["Admin"]))]),
|
|
414
|
+
iamCallable: true,
|
|
415
|
+
}
|
|
416
|
+
let frag = makeFragment(["p_Sync(id: ID!): String", "p_Other(id: ID!): String"], [])
|
|
417
|
+
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry, iamEntry], ~queryEntries=[])
|
|
418
|
+
let parts = decodeFragment(aug)
|
|
419
|
+
switch parts.mutations->Array.find(f => f->String.includes("p_Other")) {
|
|
420
|
+
| Some(other) =>
|
|
421
|
+
expect(other)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
|
|
422
|
+
expect(other)->not_->toContain("@aws_iam")
|
|
423
|
+
| None => JsError.throwWithMessage("missing p_Other field")
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
testSync("iamCallable query applies dual-auth to BOTH single and list fields", () => {
|
|
428
|
+
let entry: ReventlessInfra.Api.querySchemaEntry = {
|
|
429
|
+
singleFieldName: "p_Item",
|
|
430
|
+
listFieldName: "p_Items",
|
|
431
|
+
returnTypeName: "p_Item",
|
|
432
|
+
stateSchema: S.unknown,
|
|
433
|
+
authorization: None,
|
|
434
|
+
permission: Reventless.Authorization.AllowGroups(["Admin"]),
|
|
435
|
+
iamCallable: true,
|
|
436
|
+
}
|
|
437
|
+
let frag = makeFragment(
|
|
438
|
+
[],
|
|
439
|
+
["p_Item(id: ID!): Item", "p_Items(first: Int, after: String): ItemConnection!"],
|
|
440
|
+
)
|
|
441
|
+
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[], ~queryEntries=[entry])
|
|
442
|
+
let parts = decodeFragment(aug)
|
|
443
|
+
switch (
|
|
444
|
+
parts.queries->Array.find(f => f->String.includes("p_Item(")),
|
|
445
|
+
parts.queries->Array.find(f => f->String.includes("p_Items(")),
|
|
446
|
+
) {
|
|
447
|
+
| (Some(single), Some(list)) =>
|
|
448
|
+
expect(single)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
449
|
+
expect(single)->toContain("@aws_iam")
|
|
450
|
+
expect(list)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
451
|
+
expect(list)->toContain("@aws_iam")
|
|
452
|
+
| _ => JsError.throwWithMessage("missing query fields")
|
|
453
|
+
}
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
describe("AppSync_Adapter.injectAwsAuthAll — ~iamFieldNames", () => {
|
|
458
|
+
let baseFragment = ReventlessCore.AdminApi.baseFragment(~cloner=true)
|
|
459
|
+
|
|
460
|
+
testSync("marks only the named field dual-auth, leaving others single-mode", () => {
|
|
461
|
+
// Pick a real mutation field name from the admin base fragment.
|
|
462
|
+
let firstMutationName =
|
|
463
|
+
decodeFragment(baseFragment).mutations
|
|
464
|
+
->Array.getUnsafe(0)
|
|
465
|
+
->ReventlessCore.GraphQL_Stitcher.extractLeadingName
|
|
466
|
+
let augmented =
|
|
467
|
+
AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin", ~iamFieldNames=[firstMutationName])
|
|
468
|
+
let parts = decodeFragment(augmented)
|
|
469
|
+
parts.mutations->Array.forEach(field => {
|
|
470
|
+
let name = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
|
|
471
|
+
if name == firstMutationName {
|
|
472
|
+
expect(field)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
473
|
+
expect(field)->toContain("@aws_iam")
|
|
474
|
+
expect(field)->not_->toContain("@aws_auth")
|
|
475
|
+
} else {
|
|
476
|
+
expect(field)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
|
|
477
|
+
expect(field)->not_->toContain("@aws_iam")
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
testSync("default (no ~iamFieldNames) leaves every field single-mode @aws_auth", () => {
|
|
483
|
+
let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
|
|
484
|
+
let parts = decodeFragment(augmented)
|
|
485
|
+
parts.mutations->Array.forEach(field => expect(field)->not_->toContain("@aws_iam"))
|
|
486
|
+
parts.queries->Array.forEach(field => expect(field)->not_->toContain("@aws_iam"))
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
|
|
350
490
|
describe("Split mode — empty base fragment", () => {
|
|
351
491
|
testSync("empty stitcher encode produces fragment with no fields", () => {
|
|
352
492
|
let emptyFragment = ReventlessCore.GraphQL_Stitcher.encode({
|
|
@@ -27,14 +27,14 @@ let decodeFragment = GraphQL_Stitcher$ReventlessCore.decode;
|
|
|
27
27
|
globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
28
28
|
let baseFragment = AdminApi$ReventlessCore.baseFragment(true);
|
|
29
29
|
globalThis.test("adds @aws_auth directive to all mutation fields", () => {
|
|
30
|
-
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin");
|
|
30
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
31
31
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
32
32
|
parts.mutations.forEach(field => {
|
|
33
33
|
globalThis.expect(field).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
|
|
34
34
|
});
|
|
35
35
|
});
|
|
36
36
|
globalThis.test("adds @aws_auth directive to all query fields", () => {
|
|
37
|
-
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin");
|
|
37
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
38
38
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
39
39
|
parts.queries.forEach(field => {
|
|
40
40
|
globalThis.expect(field).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
|
|
@@ -42,12 +42,12 @@ globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
|
42
42
|
});
|
|
43
43
|
globalThis.test("preserves type definitions unchanged", () => {
|
|
44
44
|
let original = GraphQL_Stitcher$ReventlessCore.decode(baseFragment);
|
|
45
|
-
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin");
|
|
45
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
46
46
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
47
47
|
globalThis.expect(parts.types).toEqual(original.types);
|
|
48
48
|
});
|
|
49
49
|
globalThis.test("uses specified group name", () => {
|
|
50
|
-
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "SuperAdmin");
|
|
50
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "SuperAdmin", undefined);
|
|
51
51
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
52
52
|
parts.mutations.forEach(field => {
|
|
53
53
|
globalThis.expect(field).toContain(`cognito_groups: ["SuperAdmin"]`);
|
|
@@ -55,7 +55,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
|
55
55
|
});
|
|
56
56
|
globalThis.test("preserves subscription fields with @aws_auth directive", () => {
|
|
57
57
|
let original = GraphQL_Stitcher$ReventlessCore.decode(baseFragment);
|
|
58
|
-
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin");
|
|
58
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
59
59
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
60
60
|
globalThis.expect(parts.subscriptions.length).toBe(original.subscriptions.length);
|
|
61
61
|
globalThis.expect(parts.subscriptions.length).toBeGreaterThan(0);
|
|
@@ -64,7 +64,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
66
|
globalThis.test("admin Plugin aggregate subscription fields survive the round-trip", () => {
|
|
67
|
-
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin");
|
|
67
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
68
68
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
69
69
|
let joined = parts.subscriptions.join("\n");
|
|
70
70
|
globalThis.expect(joined).toContain("onPlatform_Plugin_Activate");
|
|
@@ -282,6 +282,182 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
282
282
|
});
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
+
globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", () => {
|
|
286
|
+
let makeFragment = (mutationFields, queryFields) => GraphQL_Stitcher$ReventlessCore.encode({
|
|
287
|
+
types: [],
|
|
288
|
+
mutations: mutationFields,
|
|
289
|
+
queries: queryFields,
|
|
290
|
+
subscriptions: []
|
|
291
|
+
});
|
|
292
|
+
globalThis.test("iamCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
|
|
293
|
+
let entry_fieldNames = ["p_Sync"];
|
|
294
|
+
let entry_fieldPermissions = Object.fromEntries([[
|
|
295
|
+
"p_Sync",
|
|
296
|
+
{
|
|
297
|
+
TAG: "AllowGroups",
|
|
298
|
+
_0: ["Admin"]
|
|
299
|
+
}
|
|
300
|
+
]]);
|
|
301
|
+
let entry_iamCallable = true;
|
|
302
|
+
let entry = {
|
|
303
|
+
fieldNames: entry_fieldNames,
|
|
304
|
+
commandSchema: S.unknown,
|
|
305
|
+
fieldPermissions: entry_fieldPermissions,
|
|
306
|
+
iamCallable: entry_iamCallable
|
|
307
|
+
};
|
|
308
|
+
let frag = makeFragment(["p_Sync(id: ID!): String"], []);
|
|
309
|
+
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
310
|
+
let m = GraphQL_Stitcher$ReventlessCore.decode(aug).mutations[0];
|
|
311
|
+
globalThis.expect(m).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
312
|
+
globalThis.expect(m).toContain("@aws_iam");
|
|
313
|
+
globalThis.expect(m).not.toContain("@aws_auth");
|
|
314
|
+
});
|
|
315
|
+
globalThis.test("iamCallable mutation without a group restriction stays open to Cognito + IAM", () => {
|
|
316
|
+
let entry_fieldNames = ["p_Sync"];
|
|
317
|
+
let entry_fieldPermissions = Object.fromEntries([[
|
|
318
|
+
"p_Sync",
|
|
319
|
+
"AllowAuthenticated"
|
|
320
|
+
]]);
|
|
321
|
+
let entry_iamCallable = true;
|
|
322
|
+
let entry = {
|
|
323
|
+
fieldNames: entry_fieldNames,
|
|
324
|
+
commandSchema: S.unknown,
|
|
325
|
+
fieldPermissions: entry_fieldPermissions,
|
|
326
|
+
iamCallable: entry_iamCallable
|
|
327
|
+
};
|
|
328
|
+
let frag = makeFragment(["p_Sync(id: ID!): String"], []);
|
|
329
|
+
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
330
|
+
let m = GraphQL_Stitcher$ReventlessCore.decode(aug).mutations[0];
|
|
331
|
+
globalThis.expect(m).toContain("@aws_cognito_user_pools @aws_iam");
|
|
332
|
+
globalThis.expect(m).not.toContain("cognito_groups");
|
|
333
|
+
globalThis.expect(m).not.toContain("@aws_auth");
|
|
334
|
+
});
|
|
335
|
+
globalThis.test("a non-iamCallable sibling keeps single-mode @aws_auth (no @aws_iam)", () => {
|
|
336
|
+
let entry_fieldNames = [
|
|
337
|
+
"p_Sync",
|
|
338
|
+
"p_Other"
|
|
339
|
+
];
|
|
340
|
+
let entry_fieldPermissions = Object.fromEntries([
|
|
341
|
+
[
|
|
342
|
+
"p_Sync",
|
|
343
|
+
{
|
|
344
|
+
TAG: "AllowGroups",
|
|
345
|
+
_0: ["Admin"]
|
|
346
|
+
}
|
|
347
|
+
],
|
|
348
|
+
[
|
|
349
|
+
"p_Other",
|
|
350
|
+
{
|
|
351
|
+
TAG: "AllowGroups",
|
|
352
|
+
_0: ["Admin"]
|
|
353
|
+
}
|
|
354
|
+
]
|
|
355
|
+
]);
|
|
356
|
+
let entry_iamCallable = false;
|
|
357
|
+
let entry = {
|
|
358
|
+
fieldNames: entry_fieldNames,
|
|
359
|
+
commandSchema: S.unknown,
|
|
360
|
+
fieldPermissions: entry_fieldPermissions,
|
|
361
|
+
iamCallable: entry_iamCallable
|
|
362
|
+
};
|
|
363
|
+
let iamEntry_fieldNames = ["p_Sync"];
|
|
364
|
+
let iamEntry_fieldPermissions = Object.fromEntries([[
|
|
365
|
+
"p_Sync",
|
|
366
|
+
{
|
|
367
|
+
TAG: "AllowGroups",
|
|
368
|
+
_0: ["Admin"]
|
|
369
|
+
}
|
|
370
|
+
]]);
|
|
371
|
+
let iamEntry_iamCallable = true;
|
|
372
|
+
let iamEntry = {
|
|
373
|
+
fieldNames: iamEntry_fieldNames,
|
|
374
|
+
commandSchema: S.unknown,
|
|
375
|
+
fieldPermissions: iamEntry_fieldPermissions,
|
|
376
|
+
iamCallable: iamEntry_iamCallable
|
|
377
|
+
};
|
|
378
|
+
let frag = makeFragment([
|
|
379
|
+
"p_Sync(id: ID!): String",
|
|
380
|
+
"p_Other(id: ID!): String"
|
|
381
|
+
], []);
|
|
382
|
+
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [
|
|
383
|
+
entry,
|
|
384
|
+
iamEntry
|
|
385
|
+
], []);
|
|
386
|
+
let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
|
|
387
|
+
let other = parts.mutations.find(f => f.includes("p_Other"));
|
|
388
|
+
if (other !== undefined) {
|
|
389
|
+
globalThis.expect(other).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
|
|
390
|
+
globalThis.expect(other).not.toContain("@aws_iam");
|
|
391
|
+
return;
|
|
392
|
+
} else {
|
|
393
|
+
return Stdlib_JsError.throwWithMessage("missing p_Other field");
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
globalThis.test("iamCallable query applies dual-auth to BOTH single and list fields", () => {
|
|
397
|
+
let entry_permission = {
|
|
398
|
+
TAG: "AllowGroups",
|
|
399
|
+
_0: ["Admin"]
|
|
400
|
+
};
|
|
401
|
+
let entry_iamCallable = true;
|
|
402
|
+
let entry = {
|
|
403
|
+
singleFieldName: "p_Item",
|
|
404
|
+
listFieldName: "p_Items",
|
|
405
|
+
returnTypeName: "p_Item",
|
|
406
|
+
stateSchema: S.unknown,
|
|
407
|
+
authorization: undefined,
|
|
408
|
+
permission: entry_permission,
|
|
409
|
+
iamCallable: entry_iamCallable
|
|
410
|
+
};
|
|
411
|
+
let frag = makeFragment([], [
|
|
412
|
+
"p_Item(id: ID!): Item",
|
|
413
|
+
"p_Items(first: Int, after: String): ItemConnection!"
|
|
414
|
+
]);
|
|
415
|
+
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [], [entry]);
|
|
416
|
+
let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
|
|
417
|
+
let match = parts.queries.find(f => f.includes("p_Item("));
|
|
418
|
+
let match$1 = parts.queries.find(f => f.includes("p_Items("));
|
|
419
|
+
if (match !== undefined && match$1 !== undefined) {
|
|
420
|
+
globalThis.expect(match).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
421
|
+
globalThis.expect(match).toContain("@aws_iam");
|
|
422
|
+
globalThis.expect(match$1).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
423
|
+
globalThis.expect(match$1).toContain("@aws_iam");
|
|
424
|
+
return;
|
|
425
|
+
} else {
|
|
426
|
+
return Stdlib_JsError.throwWithMessage("missing query fields");
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
globalThis.describe("AppSync_Adapter.injectAwsAuthAll — ~iamFieldNames", () => {
|
|
432
|
+
let baseFragment = AdminApi$ReventlessCore.baseFragment(true);
|
|
433
|
+
globalThis.test("marks only the named field dual-auth, leaving others single-mode", () => {
|
|
434
|
+
let firstMutationName = GraphQL_Stitcher$ReventlessCore.extractLeadingName(GraphQL_Stitcher$ReventlessCore.decode(baseFragment).mutations[0]);
|
|
435
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", [firstMutationName]);
|
|
436
|
+
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
437
|
+
parts.mutations.forEach(field => {
|
|
438
|
+
let name = GraphQL_Stitcher$ReventlessCore.extractLeadingName(field);
|
|
439
|
+
if (name === firstMutationName) {
|
|
440
|
+
globalThis.expect(field).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
441
|
+
globalThis.expect(field).toContain("@aws_iam");
|
|
442
|
+
globalThis.expect(field).not.toContain("@aws_auth");
|
|
443
|
+
} else {
|
|
444
|
+
globalThis.expect(field).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
|
|
445
|
+
globalThis.expect(field).not.toContain("@aws_iam");
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
globalThis.test("default (no ~iamFieldNames) leaves every field single-mode @aws_auth", () => {
|
|
450
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
451
|
+
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
452
|
+
parts.mutations.forEach(field => {
|
|
453
|
+
globalThis.expect(field).not.toContain("@aws_iam");
|
|
454
|
+
});
|
|
455
|
+
parts.queries.forEach(field => {
|
|
456
|
+
globalThis.expect(field).not.toContain("@aws_iam");
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
285
461
|
globalThis.describe("Split mode — empty base fragment", () => {
|
|
286
462
|
globalThis.test("empty stitcher encode produces fragment with no fields", () => {
|
|
287
463
|
let emptyFragment = GraphQL_Stitcher$ReventlessCore.encode({
|
|
@@ -316,7 +316,9 @@ describe("Runtime.appendUnconditional", () => {
|
|
|
316
316
|
let events = manyTags->Array.map(t => event("Foo", [t]))
|
|
317
317
|
let result = await Runtime.appendUnconditional(table, events)
|
|
318
318
|
switch result {
|
|
319
|
-
| Error(msg) =>
|
|
319
|
+
| Error(ReventlessInfra.DcbEventLog.StorageFailure(msg)) =>
|
|
320
|
+
expect(msg->String.includes("limit exceeded"))->toBe(true)
|
|
321
|
+
| Error(Conflict) => expect("expected StorageFailure, got Conflict")->toBe("")
|
|
320
322
|
| Ok(_) => expect("expected Error, got Ok")->toBe("")
|
|
321
323
|
}
|
|
322
324
|
})
|
|
@@ -555,8 +557,9 @@ describe("Runtime.appendConditional", () => {
|
|
|
555
557
|
}
|
|
556
558
|
let result = await Runtime.appendConditional(table, [], cond)
|
|
557
559
|
switch result {
|
|
558
|
-
| Error(msg) =>
|
|
560
|
+
| Error(ReventlessInfra.DcbEventLog.StorageFailure(msg)) =>
|
|
559
561
|
expect(msg->String.includes("tagless"))->toBe(true)
|
|
562
|
+
| Error(Conflict) => expect("expected StorageFailure, got Conflict")->toBe("")
|
|
560
563
|
| Ok(_) => expect("expected Error, got Ok")->toBe("")
|
|
561
564
|
}
|
|
562
565
|
})
|
|
@@ -579,8 +582,9 @@ describe("Runtime.appendConditional", () => {
|
|
|
579
582
|
}
|
|
580
583
|
let result = await Runtime.appendConditional(table, [event], cond)
|
|
581
584
|
switch result {
|
|
582
|
-
| Error(msg) =>
|
|
585
|
+
| Error(ReventlessInfra.DcbEventLog.StorageFailure(msg)) =>
|
|
583
586
|
expect(msg->String.includes("limit exceeded"))->toBe(true)
|
|
587
|
+
| Error(Conflict) => expect("expected StorageFailure, got Conflict")->toBe("")
|
|
584
588
|
| Ok(_) => expect("expected Error, got Ok")->toBe("")
|
|
585
589
|
}
|
|
586
590
|
})
|
|
@@ -363,7 +363,12 @@ globalThis.describe("Runtime.appendUnconditional", () => {
|
|
|
363
363
|
globalThis.expect("expected Error, got Ok").toBe("");
|
|
364
364
|
return;
|
|
365
365
|
}
|
|
366
|
-
|
|
366
|
+
let msg = result._0;
|
|
367
|
+
if (typeof msg !== "object") {
|
|
368
|
+
globalThis.expect("expected StorageFailure, got Conflict").toBe("");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
globalThis.expect(msg._0.includes("limit exceeded")).toBe(true);
|
|
367
372
|
});
|
|
368
373
|
});
|
|
369
374
|
|
|
@@ -683,7 +688,12 @@ globalThis.describe("Runtime.appendConditional", () => {
|
|
|
683
688
|
globalThis.expect("expected Error, got Ok").toBe("");
|
|
684
689
|
return;
|
|
685
690
|
}
|
|
686
|
-
|
|
691
|
+
let msg = result._0;
|
|
692
|
+
if (typeof msg !== "object") {
|
|
693
|
+
globalThis.expect("expected StorageFailure, got Conflict").toBe("");
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
globalThis.expect(msg._0.includes("tagless")).toBe(true);
|
|
687
697
|
});
|
|
688
698
|
globalThis.test("rejects appends exceeding 100 items with a clear error", async () => {
|
|
689
699
|
let manyTags = Stdlib_Array.fromInitializer(100, i => ({
|
|
@@ -716,7 +726,12 @@ globalThis.describe("Runtime.appendConditional", () => {
|
|
|
716
726
|
globalThis.expect("expected Error, got Ok").toBe("");
|
|
717
727
|
return;
|
|
718
728
|
}
|
|
719
|
-
|
|
729
|
+
let msg = result._0;
|
|
730
|
+
if (typeof msg !== "object") {
|
|
731
|
+
globalThis.expect("expected StorageFailure, got Conflict").toBe("");
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
globalThis.expect(msg._0.includes("limit exceeded")).toBe(true);
|
|
720
735
|
});
|
|
721
736
|
});
|
|
722
737
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// A realistic DCB feed event as PgChangeFeed.readBatch would surface it.
|
|
4
|
+
let sampleEvent: ReventlessCore.DcbEventLog_Adapter.rawSequencedEvent = {
|
|
5
|
+
position: "00000000000000000042:00000000000000000007",
|
|
6
|
+
eventType: "OrderCreated",
|
|
7
|
+
data: [("customerId", JSON.Encode.string("c-1"))]->Dict.fromArray->JSON.Encode.object,
|
|
8
|
+
tags: [{key: "orderId", value: "o-1"}],
|
|
9
|
+
meta: {
|
|
10
|
+
service: "Orders",
|
|
11
|
+
time: "2026-07-05T10:00:00.000Z",
|
|
12
|
+
msgId: "m-1",
|
|
13
|
+
correlationId: "m-1",
|
|
14
|
+
},
|
|
15
|
+
recordedAt: "2026-07-05T10:00:00.000Z",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("PgChangeFeedRelay_Runtime.toEventCollectorJson", () => {
|
|
19
|
+
testSync("produces the EventCollector {id, meta, event} body from a feed event", () => {
|
|
20
|
+
let json = PgChangeFeedRelay_Runtime.toEventCollectorJson(sampleEvent)->Option.getOrThrow
|
|
21
|
+
let obj = json->JSON.Decode.object->Option.getOrThrow
|
|
22
|
+
|
|
23
|
+
// id = derivePartitionKey with no partitionTag → "<firstTag.key>:<value>"
|
|
24
|
+
expect(obj->Dict.get("id"))->toEqual(Some(JSON.Encode.string("orderId:o-1")))
|
|
25
|
+
|
|
26
|
+
// event carries the event type + payload (combineMessage encodes a
|
|
27
|
+
// payload-bearing variant as an object, e.g. {"OrderCreated": {...}})
|
|
28
|
+
let eventJson = obj->Dict.get("event")->Option.getOrThrow
|
|
29
|
+
expect(eventJson->JSON.stringify->String.includes("OrderCreated"))->toBe(true)
|
|
30
|
+
expect(eventJson->JSON.stringify->String.includes("c-1"))->toBe(true)
|
|
31
|
+
|
|
32
|
+
// meta round-trips the producing service
|
|
33
|
+
let metaObj =
|
|
34
|
+
obj->Dict.get("meta")->Option.flatMap(JSON.Decode.object)->Option.getOrThrow
|
|
35
|
+
expect(metaObj->Dict.get("service"))->toEqual(Some(JSON.Encode.string("Orders")))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
testSync("derives the 'dcb' id when the event carries no tags", () => {
|
|
39
|
+
let noTags = {...sampleEvent, tags: []}
|
|
40
|
+
let json = PgChangeFeedRelay_Runtime.toEventCollectorJson(noTags)->Option.getOrThrow
|
|
41
|
+
let obj = json->JSON.Decode.object->Option.getOrThrow
|
|
42
|
+
expect(obj->Dict.get("id"))->toEqual(Some(JSON.Encode.string("dcb")))
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as PgChangeFeedRelay_Runtime$ReventlessAws from "../src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs";
|
|
6
|
+
|
|
7
|
+
let sampleEvent_data = Object.fromEntries([[
|
|
8
|
+
"customerId",
|
|
9
|
+
"c-1"
|
|
10
|
+
]]);
|
|
11
|
+
|
|
12
|
+
let sampleEvent_tags = [{
|
|
13
|
+
key: "orderId",
|
|
14
|
+
value: "o-1"
|
|
15
|
+
}];
|
|
16
|
+
|
|
17
|
+
let sampleEvent_meta = {
|
|
18
|
+
service: "Orders",
|
|
19
|
+
time: "2026-07-05T10:00:00.000Z",
|
|
20
|
+
msgId: "m-1",
|
|
21
|
+
correlationId: "m-1"
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let sampleEvent = {
|
|
25
|
+
position: "00000000000000000042:00000000000000000007",
|
|
26
|
+
eventType: "OrderCreated",
|
|
27
|
+
data: sampleEvent_data,
|
|
28
|
+
tags: sampleEvent_tags,
|
|
29
|
+
meta: sampleEvent_meta,
|
|
30
|
+
recordedAt: "2026-07-05T10:00:00.000Z"
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
globalThis.describe("PgChangeFeedRelay_Runtime.toEventCollectorJson", () => {
|
|
34
|
+
globalThis.test("produces the EventCollector {id, meta, event} body from a feed event", () => {
|
|
35
|
+
let json = Stdlib_Option.getOrThrow(PgChangeFeedRelay_Runtime$ReventlessAws.toEventCollectorJson(sampleEvent, undefined), undefined);
|
|
36
|
+
let obj = Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(json), undefined);
|
|
37
|
+
globalThis.expect(obj["id"]).toEqual("orderId:o-1");
|
|
38
|
+
let eventJson = Stdlib_Option.getOrThrow(obj["event"], undefined);
|
|
39
|
+
globalThis.expect(JSON.stringify(eventJson).includes("OrderCreated")).toBe(true);
|
|
40
|
+
globalThis.expect(JSON.stringify(eventJson).includes("c-1")).toBe(true);
|
|
41
|
+
let metaObj = Stdlib_Option.getOrThrow(Stdlib_Option.flatMap(obj["meta"], Stdlib_JSON.Decode.object), undefined);
|
|
42
|
+
globalThis.expect(metaObj["service"]).toEqual("Orders");
|
|
43
|
+
});
|
|
44
|
+
globalThis.test("derives the 'dcb' id when the event carries no tags", () => {
|
|
45
|
+
let noTags_data = sampleEvent_data;
|
|
46
|
+
let noTags_tags = [];
|
|
47
|
+
let noTags_meta = {
|
|
48
|
+
service: "Orders",
|
|
49
|
+
time: "2026-07-05T10:00:00.000Z",
|
|
50
|
+
msgId: "m-1",
|
|
51
|
+
correlationId: "m-1"
|
|
52
|
+
};
|
|
53
|
+
let noTags = {
|
|
54
|
+
position: "00000000000000000042:00000000000000000007",
|
|
55
|
+
eventType: "OrderCreated",
|
|
56
|
+
data: noTags_data,
|
|
57
|
+
tags: noTags_tags,
|
|
58
|
+
meta: noTags_meta,
|
|
59
|
+
recordedAt: "2026-07-05T10:00:00.000Z"
|
|
60
|
+
};
|
|
61
|
+
let json = Stdlib_Option.getOrThrow(PgChangeFeedRelay_Runtime$ReventlessAws.toEventCollectorJson(noTags, undefined), undefined);
|
|
62
|
+
let obj = Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(json), undefined);
|
|
63
|
+
globalThis.expect(obj["id"]).toEqual("dcb");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
sampleEvent,
|
|
69
|
+
}
|
|
70
|
+
/* sampleEvent Not a pure module */
|