@reventlessdev/reventless-aws 3.0.0-alpha.318 → 3.0.0-alpha.320

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 (40) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/package.json +11 -7
  3. package/rescript.json +4 -0
  4. package/run-provision-identity.mjs +3 -0
  5. package/scripts/ProvisionIdentity.res +303 -0
  6. package/scripts/ProvisionIdentity.res.mjs +376 -0
  7. package/src/Platform.res +54 -30
  8. package/src/Platform.res.mjs +34 -32
  9. package/src/Platform_Stack.res +102 -19
  10. package/src/Platform_Stack.res.mjs +43 -7
  11. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +267 -32
  12. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs +124 -14
  13. package/src/adapter/Auth/Auth_ActiveRoleStore.res +93 -5
  14. package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +53 -4
  15. package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res +50 -4
  16. package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res.mjs +24 -3
  17. package/src/adapter/Auth/Auth_ActiveRoleStore_Schema.res +92 -0
  18. package/src/adapter/Auth/Auth_ActiveRoleStore_Schema.res.mjs +49 -0
  19. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +21 -7
  20. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs +15 -7
  21. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +6 -3
  22. package/src/util/Util_AwsError.res +56 -0
  23. package/src/util/Util_AwsError.res.mjs +59 -0
  24. package/src/util/Util_ShellConfig.res +28 -0
  25. package/src/util/Util_ShellConfig.res.mjs +22 -0
  26. package/tests/Auth_ActiveRolePoolAttachmentTest.res +175 -0
  27. package/tests/Auth_ActiveRolePoolAttachmentTest.res.mjs +136 -0
  28. package/tests/Auth_ActiveRoleStoreTest.res +65 -0
  29. package/tests/Auth_ActiveRoleStoreTest.res.mjs +48 -0
  30. package/tests/Auth_ActiveRoleStore_SchemaTest.res +98 -0
  31. package/tests/Auth_ActiveRoleStore_SchemaTest.res.mjs +97 -0
  32. package/tests/AwsErrorFixtures.mjs +16 -0
  33. package/tests/PgQueryResolver_LambdaTest.res +166 -0
  34. package/tests/PgQueryResolver_LambdaTest.res.mjs +196 -0
  35. package/tests/ProvisionIdentityTest.res +109 -0
  36. package/tests/ProvisionIdentityTest.res.mjs +132 -0
  37. package/tests/Util_AwsErrorTest.res +127 -0
  38. package/tests/Util_AwsErrorTest.res.mjs +76 -0
  39. package/tests/Util_ShellConfigTest.res +51 -0
  40. package/tests/Util_ShellConfigTest.res.mjs +42 -0
@@ -629,6 +629,196 @@ globalThis.describe("owner scoping", () => {
629
629
  });
630
630
  });
631
631
 
632
+ function geoRow(id, status) {
633
+ let geo = {};
634
+ geo["__typename"] = "GeolocationLocated";
635
+ geo["lat"] = 48.2082;
636
+ geo["lng"] = 16.3738;
637
+ let o = {};
638
+ o["id"] = id;
639
+ o["status"] = status;
640
+ o["name"] = "Geo " + id;
641
+ o["owner"] = "u-a";
642
+ o["geolocation"] = geo;
643
+ return o;
644
+ }
645
+
646
+ let geoStore = Object.fromEntries([
647
+ [
648
+ "g-1",
649
+ geoRow("g-1", "active")
650
+ ],
651
+ [
652
+ "g-2",
653
+ geoRow("g-2", "active")
654
+ ]
655
+ ]);
656
+
657
+ function geoItems() {
658
+ return Object.values(geoStore);
659
+ }
660
+
661
+ function geoConnection() {
662
+ return Object.fromEntries([[
663
+ "edges",
664
+ Object.values(geoStore).map(item => Object.fromEntries([
665
+ [
666
+ "cursor",
667
+ "c"
668
+ ],
669
+ [
670
+ "node",
671
+ item
672
+ ]
673
+ ]))
674
+ ]]);
675
+ }
676
+
677
+ function geoBinding() {
678
+ return {
679
+ ops: {
680
+ load: async id => ({
681
+ TAG: "Ok",
682
+ _0: Stdlib_Option.mapOr(geoStore[id], [], i => [i])
683
+ }),
684
+ loadStream: 0,
685
+ save: 0,
686
+ saveBatch: 0,
687
+ count: 0,
688
+ delete: 0,
689
+ deleteBatch: 0
690
+ },
691
+ pushdowns: {
692
+ indexLookup: async (param, _field, _value) => Object.values(geoStore),
693
+ byIds: async (param, ids) => Stdlib_Array.filterMap(ids, id => geoStore[id]),
694
+ listPage: async (param, param$1, param$2, param$3, param$4, param$5) => geoConnection(),
695
+ itemsPage: async (param, param$1, param$2, param$3, param$4, param$5) => geoConnection(),
696
+ scanAll: async param => Object.values(geoStore)
697
+ },
698
+ indexes: [{
699
+ index: "byStatus",
700
+ type_: "S",
701
+ idField: "status",
702
+ projectionType: "ALL"
703
+ }],
704
+ subIdField: "lineNo",
705
+ capability: capability,
706
+ labelField: "name",
707
+ includeIdParam: true,
708
+ authorization: "AllowAnonymous",
709
+ ownerField: undefined,
710
+ retiredField: undefined,
711
+ retiredValues: undefined,
712
+ namedWhenRetired: false
713
+ };
714
+ }
715
+
716
+ function memberOf(r) {
717
+ let fromRow = row => Stdlib_Option.flatMap(field(row, "geolocation"), g => str(g, "__typename"));
718
+ let rows = Stdlib_JSON.Decode.array(r);
719
+ if (rows !== undefined) {
720
+ return Stdlib_Option.flatMap(rows[0], fromRow);
721
+ }
722
+ let edges = Stdlib_Option.flatMap(field(r, "edges"), Stdlib_JSON.Decode.array);
723
+ if (edges !== undefined) {
724
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(edges[0], e => field(e, "node")), fromRow);
725
+ } else {
726
+ return fromRow(r);
727
+ }
728
+ }
729
+
730
+ globalThis.describe("PgQueryResolver — a nested union survives every door", () => {
731
+ let doors = [
732
+ [
733
+ "getById",
734
+ mkPayload("getById", undefined, Object.fromEntries([[
735
+ "id",
736
+ "g-1"
737
+ ]]), undefined, undefined)
738
+ ],
739
+ [
740
+ "byIds",
741
+ mkPayload("byIds", undefined, Object.fromEntries([[
742
+ "ids",
743
+ ["g-1"]
744
+ ]]), undefined, undefined)
745
+ ],
746
+ [
747
+ "index",
748
+ mkPayload("index", "byStatus", Object.fromEntries([[
749
+ "byStatus",
750
+ "active"
751
+ ]]), undefined, undefined)
752
+ ],
753
+ [
754
+ "list",
755
+ mkPayload("list", undefined, undefined, undefined, undefined)
756
+ ],
757
+ [
758
+ "items",
759
+ mkPayload("items", undefined, Object.fromEntries([[
760
+ "id",
761
+ "g-1"
762
+ ]]), undefined, undefined)
763
+ ],
764
+ [
765
+ "resolveOne",
766
+ {
767
+ readModelName: "Geos",
768
+ kind: "resolveOne",
769
+ target: "Geos",
770
+ source: Object.fromEntries([[
771
+ "ref",
772
+ "g-1"
773
+ ]]),
774
+ sourceIdField: "ref",
775
+ multi: false,
776
+ arguments: Object.fromEntries([]),
777
+ identity: Identity$Reventless.anonymous
778
+ }
779
+ ],
780
+ [
781
+ "resolveMany",
782
+ {
783
+ readModelName: "Geos",
784
+ kind: "resolveMany",
785
+ target: "Geos",
786
+ source: Object.fromEntries([[
787
+ "refs",
788
+ ["g-1"]
789
+ ]]),
790
+ sourceIdsField: "refs",
791
+ arguments: Object.fromEntries([]),
792
+ identity: Identity$Reventless.anonymous
793
+ }
794
+ ]
795
+ ];
796
+ doors.forEach(param => {
797
+ let payload = param[1];
798
+ globalThis.test(param[0] + " hands the member type back as stored", async () => {
799
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(geoBinding(), undefined, payload);
800
+ globalThis.expect(memberOf(r)).toEqual("GeolocationLocated");
801
+ });
802
+ });
803
+ globalThis.test("node stamps the top level without disturbing the member", async () => {
804
+ PgQueryResolver_Lambda$ReventlessAws.register("Geos", geoBinding());
805
+ PgQueryResolver_Lambda$ReventlessAws.registerNodeType("Geo", "Geos");
806
+ let payload_arguments = Object.fromEntries([[
807
+ "id",
808
+ btoa("Geo:g-1")
809
+ ]]);
810
+ let payload = {
811
+ readModelName: "",
812
+ kind: "node",
813
+ arguments: payload_arguments,
814
+ identity: Identity$Reventless.anonymous
815
+ };
816
+ let r = await PgQueryResolver_Lambda$ReventlessAws.handler(payload, null);
817
+ globalThis.expect(str(r, "__typename")).toEqual("Geo");
818
+ globalThis.expect(memberOf(r)).toEqual("GeolocationLocated");
819
+ });
820
+ });
821
+
632
822
  export {
633
823
  mk,
634
824
  store,
@@ -647,5 +837,11 @@ export {
647
837
  field,
648
838
  str,
649
839
  ids,
840
+ geoRow,
841
+ geoStore,
842
+ geoItems,
843
+ geoConnection,
844
+ geoBinding,
845
+ memberOf,
650
846
  }
651
847
  /* store Not a pure module */
@@ -0,0 +1,109 @@
1
+ open JestGlobals
2
+
3
+ // Argument handling for the operator script. Testable at all only because the
4
+ // module no longer calls `main` at the top level — `run-provision-identity.mjs`
5
+ // does. While it ran itself on import, nothing in here could be reached, and
6
+ // that is how a guard that could never match shipped in the same file.
7
+ //
8
+ // What these cover is the one thing this script does that is hard to undo:
9
+ // deciding whether to *adopt* a user pool or *create* one.
10
+
11
+ module Provision = ProvisionIdentity
12
+
13
+ describe("ProvisionIdentity.parseArgs", () => {
14
+ testSync("no arguments means adopt-or-create the default name", () =>
15
+ expect(Provision.parseArgs([]))->toEqual(
16
+ Ok({
17
+ Provision.poolName: Provision.defaultPoolName,
18
+ providerId: None,
19
+ help: false,
20
+ }),
21
+ )
22
+ )
23
+
24
+ testSync("--provider-id names an existing pool", () =>
25
+ expect(Provision.parseArgs(["--provider-id", "eu-west-1_AbCdEfGhI"]))->toEqual(
26
+ Ok({
27
+ Provision.poolName: Provision.defaultPoolName,
28
+ providerId: Some("eu-west-1_AbCdEfGhI"),
29
+ help: false,
30
+ }),
31
+ )
32
+ )
33
+
34
+ testSync("--name selects which pool to adopt by name", () =>
35
+ expect(Provision.parseArgs(["--name", "examples-dev"]))->toEqual(
36
+ Ok({Provision.poolName: "examples-dev", providerId: None, help: false}),
37
+ )
38
+ )
39
+
40
+ testSync("both together are accepted, and the id wins at resolve time", () =>
41
+ expect(Provision.parseArgs(["--name", "ignored", "--provider-id", "eu-west-1_x"]))->toEqual(
42
+ Ok({Provision.poolName: "ignored", providerId: Some("eu-west-1_x"), help: false}),
43
+ )
44
+ )
45
+
46
+ // 🚨 The reason unknown flags are refused rather than skipped. A misspelled
47
+ // `--provider-id` that parsed as "absent" would silently take the *create* path
48
+ // and stand up a second user pool beside the one the operator meant to extend.
49
+ testSync("a misspelled flag is refused, not ignored", () =>
50
+ expect(Provision.parseArgs(["--provider_id", "eu-west-1_x"]))->toEqual(
51
+ Error(`unknown argument "--provider_id"`),
52
+ )
53
+ )
54
+
55
+ // Same hazard from the other direction: a flag whose value was left off must
56
+ // not read as "no id given".
57
+ testSync("a flag with no value is refused", () =>
58
+ expect(Provision.parseArgs(["--provider-id"]))->toEqual(
59
+ Error("--provider-id needs a value"),
60
+ )
61
+ )
62
+
63
+ testSync("--name with no value is refused too", () =>
64
+ expect(Provision.parseArgs(["--name"]))->toEqual(Error("--name needs a value"))
65
+ )
66
+
67
+ testSync("--help and -h both ask for the usage text", () =>
68
+ expect((
69
+ Provision.parseArgs(["--help"])->Result.map(a => a.help),
70
+ Provision.parseArgs(["-h"])->Result.map(a => a.help),
71
+ ))->toEqual((Ok(true), Ok(true)))
72
+ )
73
+
74
+ // The first error stops the scan rather than the last one winning: an operator
75
+ // fixing what they are told about should not then meet a second surprise.
76
+ testSync("the first refusal is the one reported", () =>
77
+ expect(Provision.parseArgs(["--nope", "--also-nope"]))->toEqual(
78
+ Error(`unknown argument "--nope"`),
79
+ )
80
+ )
81
+ })
82
+
83
+ // The pool this script creates when it creates one. Asserted because a pool is
84
+ // the one thing here that outlives every stack — nothing in a `pulumi destroy`
85
+ // touches it — so its settings are not a detail an operator can revise cheaply
86
+ // once accounts exist in it.
87
+ describe("ProvisionIdentity.poolSettings", () => {
88
+ let settings = Provision.poolSettings(~poolName="MyIdentity")
89
+
90
+ testSync("carries the name it was asked for", () =>
91
+ expect(settings.poolName)->toBe("MyIdentity")
92
+ )
93
+
94
+ testSync("matches what auto mode declares: email sign-in, no MFA, admin-only", () =>
95
+ expect((
96
+ settings.usernameAttributes,
97
+ settings.mfaConfiguration,
98
+ settings.adminCreateUserConfig->Option.flatMap(c => c.allowAdminCreateUserOnly),
99
+ ))->toEqual((Some(["email"]), Some("OFF"), Some(true)))
100
+ )
101
+
102
+ testSync("keeps the 12-character password policy", () =>
103
+ expect(
104
+ settings.policies
105
+ ->Option.flatMap(p => p.passwordPolicy)
106
+ ->Option.flatMap(p => p.minimumLength),
107
+ )->toEqual(Some(12))
108
+ )
109
+ })
@@ -0,0 +1,132 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
5
+ import * as ProvisionIdentity$ReventlessAws from "../scripts/ProvisionIdentity.res.mjs";
6
+
7
+ globalThis.describe("ProvisionIdentity.parseArgs", () => {
8
+ globalThis.test("no arguments means adopt-or-create the default name", () => {
9
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs([])).toEqual({
10
+ TAG: "Ok",
11
+ _0: {
12
+ poolName: ProvisionIdentity$ReventlessAws.defaultPoolName,
13
+ providerId: undefined,
14
+ help: false
15
+ }
16
+ });
17
+ });
18
+ globalThis.test("--provider-id names an existing pool", () => {
19
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs([
20
+ "--provider-id",
21
+ "eu-west-1_AbCdEfGhI"
22
+ ])).toEqual({
23
+ TAG: "Ok",
24
+ _0: {
25
+ poolName: ProvisionIdentity$ReventlessAws.defaultPoolName,
26
+ providerId: "eu-west-1_AbCdEfGhI",
27
+ help: false
28
+ }
29
+ });
30
+ });
31
+ globalThis.test("--name selects which pool to adopt by name", () => {
32
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs([
33
+ "--name",
34
+ "examples-dev"
35
+ ])).toEqual({
36
+ TAG: "Ok",
37
+ _0: {
38
+ poolName: "examples-dev",
39
+ providerId: undefined,
40
+ help: false
41
+ }
42
+ });
43
+ });
44
+ globalThis.test("both together are accepted, and the id wins at resolve time", () => {
45
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs([
46
+ "--name",
47
+ "ignored",
48
+ "--provider-id",
49
+ "eu-west-1_x"
50
+ ])).toEqual({
51
+ TAG: "Ok",
52
+ _0: {
53
+ poolName: "ignored",
54
+ providerId: "eu-west-1_x",
55
+ help: false
56
+ }
57
+ });
58
+ });
59
+ globalThis.test("a misspelled flag is refused, not ignored", () => {
60
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs([
61
+ "--provider_id",
62
+ "eu-west-1_x"
63
+ ])).toEqual({
64
+ TAG: "Error",
65
+ _0: `unknown argument "--provider_id"`
66
+ });
67
+ });
68
+ globalThis.test("a flag with no value is refused", () => {
69
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs(["--provider-id"])).toEqual({
70
+ TAG: "Error",
71
+ _0: "--provider-id needs a value"
72
+ });
73
+ });
74
+ globalThis.test("--name with no value is refused too", () => {
75
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs(["--name"])).toEqual({
76
+ TAG: "Error",
77
+ _0: "--name needs a value"
78
+ });
79
+ });
80
+ globalThis.test("--help and -h both ask for the usage text", () => {
81
+ globalThis.expect([
82
+ Stdlib_Result.map(ProvisionIdentity$ReventlessAws.parseArgs(["--help"]), a => a.help),
83
+ Stdlib_Result.map(ProvisionIdentity$ReventlessAws.parseArgs(["-h"]), a => a.help)
84
+ ]).toEqual([
85
+ {
86
+ TAG: "Ok",
87
+ _0: true
88
+ },
89
+ {
90
+ TAG: "Ok",
91
+ _0: true
92
+ }
93
+ ]);
94
+ });
95
+ globalThis.test("the first refusal is the one reported", () => {
96
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs([
97
+ "--nope",
98
+ "--also-nope"
99
+ ])).toEqual({
100
+ TAG: "Error",
101
+ _0: `unknown argument "--nope"`
102
+ });
103
+ });
104
+ });
105
+
106
+ globalThis.describe("ProvisionIdentity.poolSettings", () => {
107
+ let settings = ProvisionIdentity$ReventlessAws.poolSettings("MyIdentity");
108
+ globalThis.test("carries the name it was asked for", () => {
109
+ globalThis.expect(settings.PoolName).toBe("MyIdentity");
110
+ });
111
+ globalThis.test("matches what auto mode declares: email sign-in, no MFA, admin-only", () => {
112
+ globalThis.expect([
113
+ settings.UsernameAttributes,
114
+ settings.MfaConfiguration,
115
+ Stdlib_Option.flatMap(settings.AdminCreateUserConfig, c => c.AllowAdminCreateUserOnly)
116
+ ]).toEqual([
117
+ ["email"],
118
+ "OFF",
119
+ true
120
+ ]);
121
+ });
122
+ globalThis.test("keeps the 12-character password policy", () => {
123
+ globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(settings.Policies, p => p.PasswordPolicy), p => p.MinimumLength)).toEqual(12);
124
+ });
125
+ });
126
+
127
+ let Provision;
128
+
129
+ export {
130
+ Provision,
131
+ }
132
+ /* Not a pure module */
@@ -0,0 +1,127 @@
1
+ open JestGlobals
2
+
3
+ // 🚨 These exist because the bug they cover shipped, and shipped precisely
4
+ // because the predicate lived inside a script that runs on import — nothing could
5
+ // reach it to assert anything.
6
+ //
7
+ // The fixtures below are the **real** shapes, captured from live calls rather
8
+ // than imagined. That is the whole point: the original guard tested the message
9
+ // for the error code, and the message never contains it.
10
+
11
+ module AwsError = Util_AwsError
12
+
13
+ @module("./AwsErrorFixtures.mjs")
14
+ external throwAwsError: (string, string) => unit = "throwAwsError"
15
+
16
+ /** The shape an AWS SDK v3 client actually throws, caught the way the real code
17
+ catches it.
18
+
19
+ 🚨 **Thrown and caught, not constructed.** ReScript's `exn` for a JS error is
20
+ the wrapper its own `catch` builds — a bare `Error` value is a different thing
21
+ and `JsExn.fromException` does not recognise it. A fixture that returned one
22
+ would make every predicate here look broken while the code was fine, which is
23
+ exactly the wrong way round for a regression test. */
24
+ let awsError = (~name: string, ~message: string): exn =>
25
+ try {
26
+ throwAwsError(name, message)
27
+ Not_found
28
+ } catch {
29
+ | e => e
30
+ }
31
+
32
+ describe("Util_AwsError.isNotFound", () => {
33
+ // Captured from `DescribeTable` on a table that does not exist. Note the
34
+ // message: it does NOT contain "ResourceNotFoundException" anywhere, which is
35
+ // exactly why a message-only guard could never match it.
36
+ let dynamoNotFound = awsError(
37
+ ~name="ResourceNotFoundException",
38
+ ~message="Requested resource not found: Table: ReventlessNoSuchTable-probe not found",
39
+ )
40
+
41
+ // Captured from `DescribeUserPool` on a pool that does not exist.
42
+ let cognitoNotFound = awsError(
43
+ ~name="ResourceNotFoundException",
44
+ ~message="User pool eu-west-1_zzzNoSuchPl does not exist.",
45
+ )
46
+
47
+ testSync("a missing DynamoDB table is recognised from the name alone", () =>
48
+ expect(AwsError.isNotFound(dynamoNotFound))->toBe(true)
49
+ )
50
+
51
+ // The regression, stated as the property rather than as the fix: this is what
52
+ // the original guard tested, and it is false for the real error.
53
+ testSync("...and its message does not carry the code, which is why", () =>
54
+ expect(
55
+ AwsError.describe(dynamoNotFound)->String.includes(
56
+ "Requested resource not found",
57
+ ),
58
+ )->toBe(true)
59
+ )
60
+
61
+ testSync("a missing Cognito pool is recognised too", () =>
62
+ expect(AwsError.isNotFound(cognitoNotFound))->toBe(true)
63
+ )
64
+
65
+ // A wrapped or re-thrown error can lose its `name` while keeping the code in
66
+ // the text, so the fallback earns its place.
67
+ testSync("the code in the message alone still matches", () =>
68
+ expect(
69
+ AwsError.isNotFound(
70
+ awsError(~name="Error", ~message="ResourceNotFoundException: wrapped somewhere"),
71
+ ),
72
+ )->toBe(true)
73
+ )
74
+
75
+ // 🚨 The direction that matters for safety. `describeTable` swallows this
76
+ // exception; a guard that said `true` here would report a table as absent when
77
+ // the call was actually refused, and the script would then try to create a
78
+ // table that already exists.
79
+ testSync("a permission failure is not an absence", () =>
80
+ expect(
81
+ AwsError.isNotFound(
82
+ awsError(
83
+ ~name="AccessDeniedException",
84
+ ~message="User is not authorized to perform: dynamodb:DescribeTable",
85
+ ),
86
+ ),
87
+ )->toBe(false)
88
+ )
89
+
90
+ testSync("a throttle is not an absence either", () =>
91
+ expect(
92
+ AwsError.isNotFound(
93
+ awsError(~name="ProvisionedThroughputExceededException", ~message="slow down"),
94
+ ),
95
+ )->toBe(false)
96
+ )
97
+
98
+ testSync("something that is not a JS error at all is not an absence", () =>
99
+ expect(AwsError.isNotFound(Not_found))->toBe(false)
100
+ )
101
+ })
102
+
103
+ // Without this the top of a CLI reports Node's own
104
+ // `UnhandledPromiseRejection ... "#<Object>"`, which names neither the failing
105
+ // call nor the reason.
106
+ describe("Util_AwsError.describe", () => {
107
+ testSync("names the code and the reason", () =>
108
+ expect(
109
+ AwsError.describe(awsError(~name="AccessDeniedException", ~message="not authorized")),
110
+ )->toBe("AccessDeniedException: not authorized")
111
+ )
112
+
113
+ testSync("a non-error value still produces a sentence", () =>
114
+ expect(AwsError.describe(Not_found))->toBe("an unexpected error escaped")
115
+ )
116
+
117
+ // Total on purpose: a describer that threw would replace one unreadable
118
+ // failure with another.
119
+ testSync("every shape yields something an operator can read", () =>
120
+ expect(
121
+ [
122
+ awsError(~name="X", ~message="y"),
123
+ Not_found,
124
+ ]->Array.every(e => AwsError.describe(e)->String.length > 0),
125
+ )->toBe(true)
126
+ )
127
+ })
@@ -0,0 +1,76 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
4
+ import * as AwsErrorFixturesMjs from "./AwsErrorFixtures.mjs";
5
+ import * as Util_AwsError$ReventlessAws from "../src/util/Util_AwsError.res.mjs";
6
+
7
+ function throwAwsError(prim0, prim1) {
8
+ AwsErrorFixturesMjs.throwAwsError(prim0, prim1);
9
+ }
10
+
11
+ function awsError(name, message) {
12
+ try {
13
+ AwsErrorFixturesMjs.throwAwsError(name, message);
14
+ return {
15
+ RE_EXN_ID: "Not_found"
16
+ };
17
+ } catch (raw_e) {
18
+ return Primitive_exceptions.internalToException(raw_e);
19
+ }
20
+ }
21
+
22
+ globalThis.describe("Util_AwsError.isNotFound", () => {
23
+ let dynamoNotFound = awsError("ResourceNotFoundException", "Requested resource not found: Table: ReventlessNoSuchTable-probe not found");
24
+ let cognitoNotFound = awsError("ResourceNotFoundException", "User pool eu-west-1_zzzNoSuchPl does not exist.");
25
+ globalThis.test("a missing DynamoDB table is recognised from the name alone", () => {
26
+ globalThis.expect(Util_AwsError$ReventlessAws.isNotFound(dynamoNotFound)).toBe(true);
27
+ });
28
+ globalThis.test("...and its message does not carry the code, which is why", () => {
29
+ globalThis.expect(Util_AwsError$ReventlessAws.describe(dynamoNotFound).includes("Requested resource not found")).toBe(true);
30
+ });
31
+ globalThis.test("a missing Cognito pool is recognised too", () => {
32
+ globalThis.expect(Util_AwsError$ReventlessAws.isNotFound(cognitoNotFound)).toBe(true);
33
+ });
34
+ globalThis.test("the code in the message alone still matches", () => {
35
+ globalThis.expect(Util_AwsError$ReventlessAws.isNotFound(awsError("Error", "ResourceNotFoundException: wrapped somewhere"))).toBe(true);
36
+ });
37
+ globalThis.test("a permission failure is not an absence", () => {
38
+ globalThis.expect(Util_AwsError$ReventlessAws.isNotFound(awsError("AccessDeniedException", "User is not authorized to perform: dynamodb:DescribeTable"))).toBe(false);
39
+ });
40
+ globalThis.test("a throttle is not an absence either", () => {
41
+ globalThis.expect(Util_AwsError$ReventlessAws.isNotFound(awsError("ProvisionedThroughputExceededException", "slow down"))).toBe(false);
42
+ });
43
+ globalThis.test("something that is not a JS error at all is not an absence", () => {
44
+ globalThis.expect(Util_AwsError$ReventlessAws.isNotFound({
45
+ RE_EXN_ID: "Not_found"
46
+ })).toBe(false);
47
+ });
48
+ });
49
+
50
+ globalThis.describe("Util_AwsError.describe", () => {
51
+ globalThis.test("names the code and the reason", () => {
52
+ globalThis.expect(Util_AwsError$ReventlessAws.describe(awsError("AccessDeniedException", "not authorized"))).toBe("AccessDeniedException: not authorized");
53
+ });
54
+ globalThis.test("a non-error value still produces a sentence", () => {
55
+ globalThis.expect(Util_AwsError$ReventlessAws.describe({
56
+ RE_EXN_ID: "Not_found"
57
+ })).toBe("an unexpected error escaped");
58
+ });
59
+ globalThis.test("every shape yields something an operator can read", () => {
60
+ globalThis.expect([
61
+ awsError("X", "y"),
62
+ {
63
+ RE_EXN_ID: "Not_found"
64
+ }
65
+ ].every(e => Util_AwsError$ReventlessAws.describe(e).length > 0)).toBe(true);
66
+ });
67
+ });
68
+
69
+ let AwsError;
70
+
71
+ export {
72
+ AwsError,
73
+ throwAwsError,
74
+ awsError,
75
+ }
76
+ /* Not a pure module */