@reventlessdev/reventless-aws 3.0.0-alpha.370 → 3.0.0-alpha.372

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.
@@ -80,6 +80,7 @@ import * as EventTopicPublisher_SNS$ReventlessAws from "./adapter/EventTopic/Eve
80
80
  import * as ExtensionPointMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionPointMapping.res.mjs";
81
81
  import * as PgQueryResolver_Builder$ReventlessAws from "./adapter/QueryDb/PgQueryResolver_Builder.res.mjs";
82
82
  import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
83
+ import * as Platform_ProvidedGroups$ReventlessAws from "./Platform_ProvidedGroups.res.mjs";
83
84
  import * as UiFragments_Projection$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments_Projection.res.mjs";
84
85
  import * as Aggregate_Builder_Single$ReventlessAws from "./components/Aggregate_Builder_Single.res.mjs";
85
86
  import * as ReadModel_Builder_Single$ReventlessAws from "./components/ReadModel_Builder_Single.res.mjs";
@@ -152,6 +153,24 @@ let messagingEmailProviderRef = {
152
153
  contents: Pulumi.output("")
153
154
  };
154
155
 
156
+ let platformStackRefCache = {
157
+ contents: undefined
158
+ };
159
+
160
+ function platformStackReference() {
161
+ let stackRef = platformStackRefCache.contents;
162
+ if (stackRef !== undefined) {
163
+ return Primitive_option.valFromOption(stackRef);
164
+ }
165
+ let stackRef$1 = Stdlib_Option.map(new Pulumi.Config("platform").get("stack"), stack => new Pulumi.StackReference(stack));
166
+ platformStackRefCache.contents = Primitive_option.some(stackRef$1);
167
+ return stackRef$1;
168
+ }
169
+
170
+ function loadProvidedGroups() {
171
+ return Platform_ProvidedGroups$ReventlessAws.load(platformStackReference());
172
+ }
173
+
155
174
  function MakeWithConfig(Config) {
156
175
  OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
157
176
  Role$Reventless.provideGroupsFrom(() => [Role$Reventless.adminGroup()].concat(AccountsManifest$Reventless.declaredGroups()));
@@ -183,7 +202,7 @@ function MakeWithConfig(Config) {
183
202
  let currentDeployTarget = {
184
203
  contents: "Domain"
185
204
  };
186
- let platformStackRef = Stdlib_Option.map(new Pulumi.Config("platform").get("stack"), stack => new Pulumi.StackReference(stack));
205
+ let platformStackRef = platformStackReference();
187
206
  let claimStores = () => {
188
207
  if (platformStackRef !== undefined) {
189
208
  return Primitive_option.valFromOption(platformStackRef).getOutput("objectStores").apply(objectStores => Stdlib_Array.filterMap(Object.entries(Stdlib_Option.getOr(Stdlib_Option.flatMap(objectStores, Stdlib_JSON.Decode.object), {})), param => {
@@ -1599,7 +1618,7 @@ function Make($star) {
1599
1618
  let currentDeployTarget = {
1600
1619
  contents: "Domain"
1601
1620
  };
1602
- let platformStackRef = Stdlib_Option.map(new Pulumi.Config("platform").get("stack"), stack => new Pulumi.StackReference(stack));
1621
+ let platformStackRef = platformStackReference();
1603
1622
  let claimStores = () => {
1604
1623
  if (platformStackRef !== undefined) {
1605
1624
  return Primitive_option.valFromOption(platformStackRef).getOutput("objectStores").apply(objectStores => Stdlib_Array.filterMap(Object.entries(Stdlib_Option.getOr(Stdlib_Option.flatMap(objectStores, Stdlib_JSON.Decode.object), {})), param => {
@@ -2984,6 +3003,9 @@ export {
2984
3003
  messagingEmailProviderRef,
2985
3004
  roleGroups,
2986
3005
  providedGroups,
3006
+ platformStackRefCache,
3007
+ platformStackReference,
3008
+ loadProvidedGroups,
2987
3009
  groupOf,
2988
3010
  adminGroup,
2989
3011
  MakeWithConfig,
@@ -0,0 +1,97 @@
1
+ /***
2
+ The user pool's groups, read before a plugin is built so the role check compares
3
+ its roles with the pool, not a manifest (`Role.provideListedGroups`). A group the
4
+ pool lacks refuses every caller, which nothing at runtime can tell from a caller
5
+ without the role. Works in a preview: `ListGroups` is a read, and the pool id is
6
+ read outside any `Output.apply`.
7
+ */
8
+
9
+ module Cognito = AwsSdk.CognitoIdentityServiceProvider
10
+
11
+ type pool = {id: string, region: option<string>}
12
+
13
+ type send = Cognito.ListGroupsCommand.input => promise<Cognito.ListGroupsCommand.output>
14
+
15
+ /** Every group of the pool, following `NextToken` across pages. */
16
+ let listAll = async (~send: send, ~userPoolId: string): array<string> => {
17
+ let rec page = async (nextToken, acc) => {
18
+ let out = await send({userPoolId, limit: 60, ?nextToken})
19
+ let names = out.groups->Option.getOr([])->Array.filterMap(g => g.groupName)
20
+ let acc = acc->Array.concat(names)
21
+ switch out.nextToken {
22
+ | Some(_) as next => await page(next, acc)
23
+ | None => acc
24
+ }
25
+ }
26
+ await page(None, [])
27
+ }
28
+
29
+ /** A plugin stack's pool, from the platform stack's exports. The new key before
30
+ the old one, and each directly before under `default`, where ESM programs
31
+ nest their exports. */
32
+ let poolFromExports = (~get: string => option<JSON.t>): option<pool> => {
33
+ let nested = get("default")->Option.flatMap(JSON.Decode.object)
34
+ let read = key =>
35
+ get(key)
36
+ ->Option.orElse(nested->Option.flatMap(d => d->Dict.get(key)))
37
+ ->Option.flatMap(JSON.Decode.string)
38
+ read("identityProviderId")
39
+ ->Option.orElse(read("cognitoUserPoolId"))
40
+ ->Option.map(id => {
41
+ id,
42
+ region: read("identityProviderRegion")->Option.orElse(read("cognitoRegion")),
43
+ })
44
+ }
45
+
46
+ /**
47
+ The pool this program deploys against, or `None` where it cannot be listed yet:
48
+ a pool this program creates does not exist before its first deploy.
49
+
50
+ A plugin stack reads its platform stack's exports, the same ones its resolvers
51
+ use. A program that is its own platform reads a supplied pool's id from config.
52
+ */
53
+ let resolvePool = async (~stackRef: option<Pulumi.StackReference.t>): option<pool> =>
54
+ switch stackRef {
55
+ | Some(stackRef) =>
56
+ let keys = [
57
+ "identityProviderId",
58
+ "cognitoUserPoolId",
59
+ "identityProviderRegion",
60
+ "cognitoRegion",
61
+ "default",
62
+ ]
63
+ let values = await Promise.all(
64
+ keys->Array.map(key => stackRef->Pulumi.StackReference.getOutputValue(key)),
65
+ )
66
+ let exports = Dict.fromArray(
67
+ keys->Array.mapWithIndex((key, i) => (key, values->Array.getUnsafe(i))),
68
+ )
69
+ poolFromExports(~get=key => exports->Dict.get(key)->Option.flatMap(v => v))
70
+ | None =>
71
+ Platform_Stack._identityProviderId(
72
+ ~cfg=Pulumi.Config.make(Some("platform")),
73
+ )->Option.map(id => {
74
+ id,
75
+ region: Pulumi.Config.make(Some("aws"))->Pulumi.Config.get("region"),
76
+ })
77
+ }
78
+
79
+ let sendTo = (~region: option<string>): send => {
80
+ let client = Cognito.Raw.client(~options={region: ?region})
81
+ input => Cognito.ListGroupsCommand.make(input)->Cognito.ListGroupsCommand.Raw.send(client, _)
82
+ }
83
+
84
+ /** List the pool's groups and record them for the role check. Leaves the check
85
+ on its other sources where there is no pool to ask yet. */
86
+ let load = async (~stackRef: option<Pulumi.StackReference.t>): unit =>
87
+ switch await resolvePool(~stackRef) {
88
+ | None => ()
89
+ | Some({id, region}) =>
90
+ let groups = try await listAll(~send=sendTo(~region), ~userPoolId=id) catch {
91
+ | exn if exn->Util_AwsError.hasCode(~code="AccessDeniedException") =>
92
+ JsError.throwWithMessage(
93
+ `Cannot list the groups of the user pool ${id}: the deploying principal needs ` ++ `cognito-idp:ListGroups on it, so the deploy can check that every role a plugin needs has a group.`,
94
+ )
95
+ }
96
+ Reventless.Role.provideListedGroups(~source=`the user pool ${id}`, groups)
97
+ }
@@ -0,0 +1,103 @@
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_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Pulumi from "@pulumi/pulumi";
7
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
9
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
10
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
11
+ import * as Util_AwsError$ReventlessAws from "./util/Util_AwsError.res.mjs";
12
+ import * as Platform_Stack$ReventlessAws from "./Platform_Stack.res.mjs";
13
+ import * as ClientCognitoIdentityProvider from "@aws-sdk/client-cognito-identity-provider";
14
+
15
+ async function listAll(send, userPoolId) {
16
+ let page = async (nextToken, acc) => {
17
+ let out = await send({
18
+ UserPoolId: userPoolId,
19
+ Limit: 60,
20
+ NextToken: nextToken
21
+ });
22
+ let names = Stdlib_Array.filterMap(Stdlib_Option.getOr(out.Groups, []), g => g.GroupName);
23
+ let acc$1 = acc.concat(names);
24
+ let next = out.NextToken;
25
+ if (next !== undefined) {
26
+ return await page(next, acc$1);
27
+ } else {
28
+ return acc$1;
29
+ }
30
+ };
31
+ return await page(undefined, []);
32
+ }
33
+
34
+ function poolFromExports(get) {
35
+ let nested = Stdlib_Option.flatMap(get("default"), Stdlib_JSON.Decode.object);
36
+ let read = key => Stdlib_Option.flatMap(Stdlib_Option.orElse(get(key), Stdlib_Option.flatMap(nested, d => d[key])), Stdlib_JSON.Decode.string);
37
+ return Stdlib_Option.map(Stdlib_Option.orElse(read("identityProviderId"), read("cognitoUserPoolId")), id => ({
38
+ id: id,
39
+ region: Stdlib_Option.orElse(read("identityProviderRegion"), read("cognitoRegion"))
40
+ }));
41
+ }
42
+
43
+ async function resolvePool(stackRef) {
44
+ if (stackRef === undefined) {
45
+ return Stdlib_Option.map(Platform_Stack$ReventlessAws._identityProviderId(new Pulumi.Config("platform")), id => ({
46
+ id: id,
47
+ region: new Pulumi.Config("aws").get("region")
48
+ }));
49
+ }
50
+ let stackRef$1 = Primitive_option.valFromOption(stackRef);
51
+ let keys = [
52
+ "identityProviderId",
53
+ "cognitoUserPoolId",
54
+ "identityProviderRegion",
55
+ "cognitoRegion",
56
+ "default"
57
+ ];
58
+ let values = await Promise.all(keys.map(key => stackRef$1.getOutputValue(key)));
59
+ let $$exports = Object.fromEntries(keys.map((key, i) => [
60
+ key,
61
+ values[i]
62
+ ]));
63
+ return poolFromExports(key => Stdlib_Option.flatMap($$exports[key], v => v));
64
+ }
65
+
66
+ function sendTo(region) {
67
+ let client = new ClientCognitoIdentityProvider.CognitoIdentityProviderClient({
68
+ region: region
69
+ });
70
+ return input => client.send(new ClientCognitoIdentityProvider.ListGroupsCommand(input));
71
+ }
72
+
73
+ async function load(stackRef) {
74
+ let match = await resolvePool(stackRef);
75
+ if (match === undefined) {
76
+ return;
77
+ }
78
+ let id = match.id;
79
+ let groups;
80
+ try {
81
+ groups = await listAll(sendTo(match.region), id);
82
+ } catch (raw_exn) {
83
+ let exn = Primitive_exceptions.internalToException(raw_exn);
84
+ if (Util_AwsError$ReventlessAws.hasCode(exn, "AccessDeniedException")) {
85
+ groups = Stdlib_JsError.throwWithMessage(`Cannot list the groups of the user pool ` + id + `: the deploying principal needs cognito-idp:ListGroups on it, so the deploy can check that every role a plugin needs has a group.`);
86
+ } else {
87
+ throw exn;
88
+ }
89
+ }
90
+ return Role$Reventless.provideListedGroups(`the user pool ` + id, groups);
91
+ }
92
+
93
+ let Cognito;
94
+
95
+ export {
96
+ Cognito,
97
+ listAll,
98
+ poolFromExports,
99
+ resolvePool,
100
+ sendTo,
101
+ load,
102
+ }
103
+ /* @pulumi/pulumi Not a pure module */
@@ -21,6 +21,8 @@ plugins:
21
21
  depends-on: []
22
22
  - name: ordering
23
23
  path: ordering-aws
24
+ region: eu-central-1
25
+ review: false
24
26
  `
25
27
 
26
28
  testSync("resolves every folder against the manifest's own folder", () => {
@@ -36,10 +38,21 @@ plugins:
36
38
  name: "shop-platform",
37
39
  dir: "/repo/shop/platform-aws",
38
40
  stackDefaults: Dict.fromArray([("platform:messagingEmailProvider", "log")]),
41
+ region: None,
39
42
  },
40
43
  plugins: [
41
- {name: "catalog", dir: "/repo/shop/catalog-aws", stackDefaults: Dict.make()},
42
- {name: "ordering", dir: "/repo/shop/ordering-aws", stackDefaults: Dict.make()},
44
+ {
45
+ name: "catalog",
46
+ dir: "/repo/shop/catalog-aws",
47
+ stackDefaults: Dict.make(),
48
+ region: None,
49
+ },
50
+ {
51
+ name: "ordering",
52
+ dir: "/repo/shop/ordering-aws",
53
+ stackDefaults: Dict.make(),
54
+ region: Some("eu-central-1"),
55
+ },
43
56
  ],
44
57
  }),
45
58
  )
@@ -25,6 +25,8 @@ plugins:
25
25
  depends-on: []
26
26
  - name: ordering
27
27
  path: ordering-aws
28
+ region: eu-central-1
29
+ review: false
28
30
  `), m => DeployManifest$ReventlessAws.resolve(m, "/repo/shop/deploy-manifest.yaml"));
29
31
  globalThis.expect(resolved).toEqual({
30
32
  TAG: "Ok",
@@ -37,18 +39,21 @@ plugins:
37
39
  stackDefaults: Object.fromEntries([[
38
40
  "platform:messagingEmailProvider",
39
41
  "log"
40
- ]])
42
+ ]]),
43
+ region: undefined
41
44
  },
42
45
  plugins: [
43
46
  {
44
47
  name: "catalog",
45
48
  dir: "/repo/shop/catalog-aws",
46
- stackDefaults: {}
49
+ stackDefaults: {},
50
+ region: undefined
47
51
  },
48
52
  {
49
53
  name: "ordering",
50
54
  dir: "/repo/shop/ordering-aws",
51
- stackDefaults: {}
55
+ stackDefaults: {},
56
+ region: "eu-central-1"
52
57
  }
53
58
  ]
54
59
  }
@@ -0,0 +1,93 @@
1
+ open JestGlobals
2
+
3
+ // Which pending operations the deploy workflow's recovery step drops and which
4
+ // stop it. The pulumi calls around them are checked against a real backend by
5
+ // hand; what is tested here is the decision.
6
+
7
+ module Clear = ClearPendingCreates
8
+
9
+ let export = (operations: string) =>
10
+ JSON.parseOrThrow(
11
+ `{"version": 3, "deployment": {"resources": [], "pending_operations": ${operations}}}`,
12
+ )
13
+
14
+ let operation = (~urn, ~type_) =>
15
+ `{"resource": {"urn": "${urn}", "type": "${type_}"}, "type": "creating"}`
16
+
17
+ let validation = operation(
18
+ ~urn="urn:pulumi:alpha::app::aws:acm/certificateValidation:CertificateValidation::cert",
19
+ ~type_="aws:acm/certificateValidation:CertificateValidation",
20
+ )
21
+ let lambda = operation(
22
+ ~urn="urn:pulumi:alpha::app::aws:lambda/function:Function::handler",
23
+ ~type_="aws:lambda/function:Function",
24
+ )
25
+
26
+ let refused = (state: JSON.t) =>
27
+ state
28
+ ->Clear.pendingOperations
29
+ ->Array.filter(p => !Clear.isSafe(p))
30
+ ->Array.map(p => p.resourceType)
31
+
32
+ describe("ClearPendingCreates.pendingOperations", () => {
33
+ testSync("a stack with none, or with no deployment yet, has none", () => {
34
+ expect(Clear.pendingOperations(export("[]")))->toEqual([])
35
+ expect(
36
+ Clear.pendingOperations(JSON.parseOrThrow(`{"version": 3, "deployment": {}}`)),
37
+ )->toEqual([])
38
+ expect(Clear.pendingOperations(JSON.parseOrThrow(`{"version": 3}`)))->toEqual([])
39
+ })
40
+
41
+ testSync("each operation with its resource and kind", () =>
42
+ expect(Clear.pendingOperations(export(`[${lambda}]`)))->toEqual([
43
+ {
44
+ Clear.urn: "urn:pulumi:alpha::app::aws:lambda/function:Function::handler",
45
+ resourceType: "aws:lambda/function:Function",
46
+ operation: "creating",
47
+ },
48
+ ])
49
+ )
50
+ })
51
+
52
+ describe("ClearPendingCreates.isSafe", () => {
53
+ testSync("a certificate validation is dropped", () =>
54
+ expect(refused(export(`[${validation}]`)))->toEqual([])
55
+ )
56
+
57
+ testSync("a resource that may exist in AWS stops the deploy", () =>
58
+ expect(refused(export(`[${lambda}]`)))->toEqual(["aws:lambda/function:Function"])
59
+ )
60
+
61
+ testSync("in a mix, only the unsafe one is refused", () =>
62
+ expect(refused(export(`[${validation}, ${lambda}]`)))->toEqual(["aws:lambda/function:Function"])
63
+ )
64
+
65
+ testSync("the type must match whole, not as a prefix", () =>
66
+ expect(
67
+ refused(
68
+ export(
69
+ `[${operation(~urn="u", ~type_="aws:acm/certificateValidation:CertificateValidationX")}]`,
70
+ ),
71
+ ),
72
+ )->toEqual(["aws:acm/certificateValidation:CertificateValidationX"])
73
+ )
74
+ })
75
+
76
+ describe("ClearPendingCreates.parseArgs", () => {
77
+ testSync("the stack, and the project directory", () =>
78
+ expect(Clear.parseArgs(["--stack", "alpha", "--cwd", "platform-aws"]))->toEqual(
79
+ Ok({Clear.stack: "alpha", cwd: "platform-aws"}),
80
+ )
81
+ )
82
+
83
+ testSync("the project directory defaults to the working directory", () =>
84
+ expect(Clear.parseArgs(["--stack", "alpha"])->Result.map(a => a.cwd))->toEqual(
85
+ Ok(NodeProcess.cwd()),
86
+ )
87
+ )
88
+
89
+ test("no stack is a usage mistake", async () => {
90
+ let observed = await Reventless.CliArgs.observe(Clear.cli, [])
91
+ expect((observed.exitCode, observed.reachedMain))->toEqual((Some(2), false))
92
+ })
93
+ })
@@ -0,0 +1,99 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
4
+ import * as CliArgs$Reventless from "@reventlessdev/reventless-spec/src/CliArgs.res.mjs";
5
+ import * as ClearPendingCreates$ReventlessAws from "../scripts/ClearPendingCreates.res.mjs";
6
+
7
+ function $$export(operations) {
8
+ return JSON.parse(`{"version": 3, "deployment": {"resources": [], "pending_operations": ` + operations + `}}`);
9
+ }
10
+
11
+ function operation(urn, type_) {
12
+ return `{"resource": {"urn": "` + urn + `", "type": "` + type_ + `"}, "type": "creating"}`;
13
+ }
14
+
15
+ let validation = `{"resource": {"urn": "` + "urn:pulumi:alpha::app::aws:acm/certificateValidation:CertificateValidation::cert" + `", "type": "` + "aws:acm/certificateValidation:CertificateValidation" + `"}, "type": "creating"}`;
16
+
17
+ let lambda = `{"resource": {"urn": "` + "urn:pulumi:alpha::app::aws:lambda/function:Function::handler" + `", "type": "` + "aws:lambda/function:Function" + `"}, "type": "creating"}`;
18
+
19
+ function refused(state) {
20
+ return ClearPendingCreates$ReventlessAws.pendingOperations(state).filter(p => !ClearPendingCreates$ReventlessAws.isSafe(p)).map(p => p.resourceType);
21
+ }
22
+
23
+ globalThis.describe("ClearPendingCreates.pendingOperations", () => {
24
+ globalThis.test("a stack with none, or with no deployment yet, has none", () => {
25
+ globalThis.expect(ClearPendingCreates$ReventlessAws.pendingOperations($$export("[]"))).toEqual([]);
26
+ globalThis.expect(ClearPendingCreates$ReventlessAws.pendingOperations(JSON.parse(`{"version": 3, "deployment": {}}`))).toEqual([]);
27
+ globalThis.expect(ClearPendingCreates$ReventlessAws.pendingOperations(JSON.parse(`{"version": 3}`))).toEqual([]);
28
+ });
29
+ globalThis.test("each operation with its resource and kind", () => {
30
+ globalThis.expect(ClearPendingCreates$ReventlessAws.pendingOperations($$export(`[` + lambda + `]`))).toEqual([{
31
+ urn: "urn:pulumi:alpha::app::aws:lambda/function:Function::handler",
32
+ resourceType: "aws:lambda/function:Function",
33
+ operation: "creating"
34
+ }]);
35
+ });
36
+ });
37
+
38
+ globalThis.describe("ClearPendingCreates.isSafe", () => {
39
+ globalThis.test("a certificate validation is dropped", () => {
40
+ globalThis.expect(refused($$export(`[` + validation + `]`))).toEqual([]);
41
+ });
42
+ globalThis.test("a resource that may exist in AWS stops the deploy", () => {
43
+ globalThis.expect(refused($$export(`[` + lambda + `]`))).toEqual(["aws:lambda/function:Function"]);
44
+ });
45
+ globalThis.test("in a mix, only the unsafe one is refused", () => {
46
+ globalThis.expect(refused($$export(`[` + validation + `, ` + lambda + `]`))).toEqual(["aws:lambda/function:Function"]);
47
+ });
48
+ globalThis.test("the type must match whole, not as a prefix", () => {
49
+ globalThis.expect(refused($$export(`[` + (`{"resource": {"urn": "` + "u" + `", "type": "` + "aws:acm/certificateValidation:CertificateValidationX" + `"}, "type": "creating"}`) + `]`))).toEqual(["aws:acm/certificateValidation:CertificateValidationX"]);
50
+ });
51
+ });
52
+
53
+ globalThis.describe("ClearPendingCreates.parseArgs", () => {
54
+ globalThis.test("the stack, and the project directory", () => {
55
+ globalThis.expect(ClearPendingCreates$ReventlessAws.parseArgs([
56
+ "--stack",
57
+ "alpha",
58
+ "--cwd",
59
+ "platform-aws"
60
+ ])).toEqual({
61
+ TAG: "Ok",
62
+ _0: {
63
+ stack: "alpha",
64
+ cwd: "platform-aws"
65
+ }
66
+ });
67
+ });
68
+ globalThis.test("the project directory defaults to the working directory", () => {
69
+ globalThis.expect(Stdlib_Result.map(ClearPendingCreates$ReventlessAws.parseArgs([
70
+ "--stack",
71
+ "alpha"
72
+ ]), a => a.cwd)).toEqual({
73
+ TAG: "Ok",
74
+ _0: process.cwd()
75
+ });
76
+ });
77
+ globalThis.test("no stack is a usage mistake", async () => {
78
+ let observed = await CliArgs$Reventless.observe(ClearPendingCreates$ReventlessAws.cli, []);
79
+ globalThis.expect([
80
+ observed.exitCode,
81
+ observed.reachedMain
82
+ ]).toEqual([
83
+ 2,
84
+ false
85
+ ]);
86
+ });
87
+ });
88
+
89
+ let Clear;
90
+
91
+ export {
92
+ Clear,
93
+ $$export,
94
+ operation,
95
+ validation,
96
+ lambda,
97
+ refused,
98
+ }
99
+ /* Not a pure module */
@@ -55,8 +55,19 @@ describe("DeployApp stacks and layer", () => {
55
55
  projectName: "shop-platform-aws",
56
56
  program: "/app/platform-aws/src/Main.res.mjs",
57
57
  stackDefaults: Dict.fromArray([("aws:region", "us-east-1"), ("platform:x", "y")]),
58
+ region: None,
58
59
  }
59
60
 
61
+ testSync("a stack the manifest puts in another region is refused", () => {
62
+ let ingest = {...project, label: "ingest", region: Some("eu-central-1")}
63
+ expect(
64
+ DeployApp.regionProblems(~projects=[project, ingest], ~region="eu-west-1")->Array.length,
65
+ )->toBe(1)
66
+ expect(
67
+ DeployApp.regionProblems(~projects=[project, ingest], ~region="eu-central-1"),
68
+ )->toEqual([])
69
+ })
70
+
60
71
  // The region and the try-out flag come last, so an app default cannot turn a
61
72
  // try-out into a stack `down` refuses to remove.
62
73
  testSync("a new stack gets the app's defaults, then the region and the try-out flag", () =>
@@ -79,8 +79,29 @@ globalThis.describe("DeployApp stacks and layer", () => {
79
79
  dir: "/app/platform-aws",
80
80
  projectName: "shop-platform-aws",
81
81
  program: "/app/platform-aws/src/Main.res.mjs",
82
- stackDefaults: project_stackDefaults
82
+ stackDefaults: project_stackDefaults,
83
+ region: undefined
83
84
  };
85
+ globalThis.test("a stack the manifest puts in another region is refused", () => {
86
+ let ingest_stackDefaults = project_stackDefaults;
87
+ let ingest_region = "eu-central-1";
88
+ let ingest = {
89
+ label: "ingest",
90
+ dir: "/app/platform-aws",
91
+ projectName: "shop-platform-aws",
92
+ program: "/app/platform-aws/src/Main.res.mjs",
93
+ stackDefaults: ingest_stackDefaults,
94
+ region: ingest_region
95
+ };
96
+ globalThis.expect(DeployApp$ReventlessAws.regionProblems([
97
+ project,
98
+ ingest
99
+ ], "eu-west-1").length).toBe(1);
100
+ globalThis.expect(DeployApp$ReventlessAws.regionProblems([
101
+ project,
102
+ ingest
103
+ ], "eu-central-1")).toEqual([]);
104
+ });
84
105
  globalThis.test("a new stack gets the app's defaults, then the region and the try-out flag", () => {
85
106
  globalThis.expect(DeployApp$ReventlessAws.newStackConfig(project, "eu-west-1")).toEqual([
86
107
  [
@@ -0,0 +1,52 @@
1
+ open JestGlobals
2
+
3
+ // What the role check compares a plugin's roles with on AWS: the groups the user
4
+ // pool reports, found through the platform stack's exports.
5
+
6
+ module Cognito = AwsSdk.CognitoIdentityServiceProvider
7
+
8
+ let group = (name): Cognito.ListGroupsCommand.group => {groupName: name}
9
+
10
+ describe("Platform_ProvidedGroups.listAll", () => {
11
+ test("follows NextToken until the last page", async () => {
12
+ let seen = []
13
+ let send = async (input: Cognito.ListGroupsCommand.input): Cognito.ListGroupsCommand.output => {
14
+ seen->Array.push(input.nextToken)
15
+ switch input.nextToken {
16
+ | None => {groups: [group("Admin"), group("Shopper")], nextToken: "p2"}
17
+ | Some(_) => {groups: [group("Merchandiser")]}
18
+ }
19
+ }
20
+ let groups = await Platform_ProvidedGroups.listAll(~send, ~userPoolId="p1")
21
+ expect((groups, seen))->toEqual((["Admin", "Shopper", "Merchandiser"], [None, Some("p2")]))
22
+ })
23
+ })
24
+
25
+ describe("Platform_ProvidedGroups.poolFromExports", () => {
26
+ let from = (pairs: array<(string, JSON.t)>) => {
27
+ let exports = Dict.fromArray(pairs)
28
+ Platform_ProvidedGroups.poolFromExports(~get=key => exports->Dict.get(key))
29
+ }
30
+
31
+ testSync("reads the pool id and region", () =>
32
+ expect(
33
+ from([
34
+ ("identityProviderId", JSON.Encode.string("p1")),
35
+ ("identityProviderRegion", JSON.Encode.string("eu-west-1")),
36
+ ]),
37
+ )->toEqual(Some({Platform_ProvidedGroups.id: "p1", region: Some("eu-west-1")}))
38
+ )
39
+
40
+ testSync("reads them nested under default, as an ESM platform exports them", () =>
41
+ expect(
42
+ from([
43
+ (
44
+ "default",
45
+ JSON.Encode.object(Dict.fromArray([("cognitoUserPoolId", JSON.Encode.string("p-old"))])),
46
+ ),
47
+ ]),
48
+ )->toEqual(Some({Platform_ProvidedGroups.id: "p-old", region: None}))
49
+ )
50
+
51
+ testSync("no pool id is no pool to list", () => expect(from([]))->toEqual(None))
52
+ })