@reventlessdev/reventless-aws 3.0.0-alpha.172 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/package.json +6 -6
  3. package/src/Platform.res.mjs +5 -5
  4. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +1 -1
  5. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res +26 -0
  6. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs +15 -0
  7. package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res +23 -0
  8. package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res.mjs +14 -0
  9. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +181 -0
  10. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +135 -0
  11. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +64 -0
  12. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +36 -0
  13. package/src/adapter/Runtime/AggregateEntryPoint.mjs +27 -9
  14. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +3 -3
  15. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res.mjs +3 -3
  16. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res.mjs +1 -1
  17. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res.mjs +1 -1
  18. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +1 -1
  19. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +1 -1
  20. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +1 -1
  21. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +20 -6
  22. package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +1 -1
  23. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +1 -1
  24. package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +1 -1
  25. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +52 -0
  26. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +38 -0
  27. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +20 -2
  28. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +1 -1
  29. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +1 -1
  30. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +1 -1
  31. package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +1 -1
  32. package/src/components/Api/AppSync_Adapter.res +72 -8
  33. package/src/components/Api/AppSync_Adapter.res.mjs +45 -6
  34. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
  35. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +2 -2
  36. package/tests/AppSync_AdapterTest.res +140 -0
  37. package/tests/AppSync_AdapterTest.res.mjs +182 -6
  38. package/tests/PgChangeFeedRelay_RuntimeTest.res +44 -0
  39. package/tests/PgChangeFeedRelay_RuntimeTest.res.mjs +70 -0
@@ -66,7 +66,7 @@ function forCommandTopic(param, connect, memorySizeOpt, timeoutOpt, param$1, par
66
66
  });
67
67
  envVars["HANDLER_CONFIG"] = handlerConfigJson;
68
68
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs", {}, undefined);
69
- return connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, opts));
69
+ return connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, undefined, opts));
70
70
  }
71
71
 
72
72
  function finish() {
@@ -300,7 +300,7 @@ function Make(EventCollectorChannel) {
300
300
  });
301
301
  let codeOutput = bundleOutput.apply(b => b.code);
302
302
  let sourceCodeHashOutput = bundleOutput.apply(b => b.sourceCodeHash);
303
- let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, codeOutput, sourceCodeHashOutput, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, opts);
303
+ let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, codeOutput, sourceCodeHashOutput, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, undefined, opts);
304
304
  EventCollectorChannel.connect(name, [{
305
305
  channel: eventCollector.channel,
306
306
  eventTopics: eventTopics,
@@ -422,7 +422,7 @@ function Make(EventCollectorChannel) {
422
422
  envVars["PLUGIN_ID"] = Pulumi.output(hbConfig.pluginId);
423
423
  envVars["HEARTBEAT_TIMEOUT"] = Pulumi.output(hbConfig.heartbeatTimeout.toString());
424
424
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/HeartbeatEntryPoint.mjs", {}, undefined);
425
- connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, opts));
425
+ connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, undefined, opts));
426
426
  };
427
427
  let forDcbCommandTopic = (param, connect, $staropt$star, $staropt$star$1, dcbCommandTopic) => {
428
428
  $staropt$star !== undefined;
@@ -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({
@@ -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 */