@reventlessdev/reventless-aws 3.0.0-alpha.272 → 3.0.0-alpha.274

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 (36) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +8 -7
  3. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +15 -13
  4. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +3 -2
  5. package/src/adapter/Api/Platform_UIFragments_Lambda.res +15 -13
  6. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +3 -2
  7. package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res +8 -1
  8. package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res.mjs +3 -2
  9. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +15 -13
  10. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +3 -2
  11. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +6 -12
  12. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +4 -8
  13. package/src/adapter/QueryDb/QueryInterceptor_Provisioning.res +155 -0
  14. package/src/adapter/QueryDb/QueryInterceptor_Provisioning.res.mjs +92 -0
  15. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +4 -9
  16. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +8 -2
  17. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +32 -62
  18. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +7 -20
  19. package/src/adapter/StateTopic/StateTopic_AppSync.res +15 -13
  20. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +3 -2
  21. package/src/adapter/Upload/Upload_Claim_S3.res +15 -13
  22. package/src/adapter/Upload/Upload_Claim_S3.res.mjs +3 -2
  23. package/src/adapter/Upload/Upload_Presign_S3.res +15 -13
  24. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +3 -2
  25. package/src/components/Api/AppSync_Adapter.res +13 -9
  26. package/src/components/Api/AppSync_Adapter.res.mjs +4 -3
  27. package/src/util/Util_LambdaLogging.res +83 -21
  28. package/src/util/Util_LambdaLogging.res.mjs +21 -8
  29. package/src/util/Util_LogGroup_Adopting.res +480 -0
  30. package/src/util/Util_LogGroup_Adopting.res.mjs +365 -0
  31. package/src/util/Util_LogGroup_Adopting_Js.mjs +20 -0
  32. package/src/util/Util_LogRetention.res +11 -12
  33. package/tests/Util_LambdaLoggingTest.res +48 -0
  34. package/tests/Util_LambdaLoggingTest.res.mjs +30 -0
  35. package/tests/Util_LogGroup_AdoptingTest.res +169 -0
  36. package/tests/Util_LogGroup_AdoptingTest.res.mjs +208 -0
@@ -0,0 +1,365 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Alias$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Alias.res.mjs";
4
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
+ import * as Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
8
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
9
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
10
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
11
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
12
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
13
+ import * as Util_LogGroup_Adopting_JsMjs from "./Util_LogGroup_Adopting_Js.mjs";
14
+ import * as IndexJs from "@pulumi/pulumi/dynamic/index.js";
15
+
16
+ let log = Logger$ReventlessCore.fromEnv();
17
+
18
+ function newCommand(prim0, prim1) {
19
+ return Util_LogGroup_Adopting_JsMjs.newCommand(prim0, prim1);
20
+ }
21
+
22
+ function newClient(prim) {
23
+ return Util_LogGroup_Adopting_JsMjs.newClient(prim);
24
+ }
25
+
26
+ function rethrow(prim) {
27
+ return Util_LogGroup_Adopting_JsMjs.rethrow(prim);
28
+ }
29
+
30
+ let _sdk = {
31
+ contents: undefined
32
+ };
33
+
34
+ let _client = {
35
+ contents: undefined
36
+ };
37
+
38
+ async function getSdk() {
39
+ let m = _sdk.contents;
40
+ if (m !== undefined) {
41
+ return m;
42
+ }
43
+ let m$1 = await import("@aws-sdk/client-cloudwatch-logs");
44
+ _sdk.contents = m$1;
45
+ return m$1;
46
+ }
47
+
48
+ async function getClient() {
49
+ let c = _client.contents;
50
+ if (c !== undefined) {
51
+ return Primitive_option.valFromOption(c);
52
+ }
53
+ let sdk = await getSdk();
54
+ let c$1 = Util_LogGroup_Adopting_JsMjs.newClient(sdk.CloudWatchLogsClient);
55
+ _client.contents = Primitive_option.some(c$1);
56
+ return c$1;
57
+ }
58
+
59
+ function matchesAws(jsErr, code) {
60
+ let match = jsErr.name;
61
+ let match$1 = Stdlib_JsExn.message(jsErr);
62
+ if (match == null) {
63
+ if (match$1 !== undefined) {
64
+ return match$1.includes(code);
65
+ } else {
66
+ return false;
67
+ }
68
+ } else if (match === code) {
69
+ return true;
70
+ } else if (match$1 !== undefined) {
71
+ return match$1.includes(code);
72
+ } else {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ function isAlreadyExistsError(jsErr) {
78
+ return matchesAws(jsErr, "ResourceAlreadyExistsException");
79
+ }
80
+
81
+ function isAlreadyGoneError(jsErr) {
82
+ return matchesAws(jsErr, "ResourceNotFoundException");
83
+ }
84
+
85
+ let transientNames = [
86
+ "OperationAbortedException",
87
+ "ThrottlingException",
88
+ "TooManyRequestsException",
89
+ "ServiceUnavailableException"
90
+ ];
91
+
92
+ function isRetryableError(jsErr) {
93
+ return transientNames.some(code => matchesAws(jsErr, code));
94
+ }
95
+
96
+ async function runWithRetry(attemptOpt, maxAttemptsOpt, delayMsOpt, maxDelayMsOpt, makeCall) {
97
+ let attempt = attemptOpt !== undefined ? attemptOpt : 0;
98
+ let maxAttempts = maxAttemptsOpt !== undefined ? maxAttemptsOpt : 5;
99
+ let delayMs = delayMsOpt !== undefined ? delayMsOpt : 1000;
100
+ let maxDelayMs = maxDelayMsOpt !== undefined ? maxDelayMsOpt : 8000;
101
+ try {
102
+ return await makeCall();
103
+ } catch (raw_exn) {
104
+ let exn = Primitive_exceptions.internalToException(raw_exn);
105
+ let jsExn = Stdlib_JsExn.fromException(exn);
106
+ let isRetryable = attempt < maxAttempts && Stdlib_Option.mapOr(jsExn, false, isRetryableError);
107
+ if (isRetryable) {
108
+ await new Promise((resolve, param) => {
109
+ setTimeout(resolve, delayMs);
110
+ });
111
+ let nextDelay = (delayMs << 1);
112
+ return await runWithRetry(attempt + 1 | 0, maxAttempts, nextDelay > maxDelayMs ? maxDelayMs : nextDelay, maxDelayMs, makeCall);
113
+ }
114
+ if (jsExn !== undefined) {
115
+ return Util_LogGroup_Adopting_JsMjs.rethrow(Primitive_option.valFromOption(jsExn));
116
+ }
117
+ throw exn;
118
+ }
119
+ }
120
+
121
+ let marker = "reventless:adopting-log-group";
122
+
123
+ async function describe(logGroupName) {
124
+ let sdk = await getSdk();
125
+ let client = await getClient();
126
+ let result = await runWithRetry(undefined, undefined, undefined, undefined, () => client.send(Util_LogGroup_Adopting_JsMjs.newCommand(sdk.DescribeLogGroupsCommand, {
127
+ logGroupNamePrefix: logGroupName
128
+ })));
129
+ return Stdlib_Option.getOr(result.logGroups, []).find(g => Primitive_object.equal(g.logGroupName, logGroupName));
130
+ }
131
+
132
+ function taggableArn(arn) {
133
+ if (arn.endsWith(":*")) {
134
+ return arn.slice(0, arn.length - 2 | 0);
135
+ } else {
136
+ return arn;
137
+ }
138
+ }
139
+
140
+ async function applyRetention(logGroupName, retentionInDays) {
141
+ let sdk = await getSdk();
142
+ let client = await getClient();
143
+ return await runWithRetry(undefined, undefined, undefined, undefined, () => {
144
+ if (retentionInDays > 0) {
145
+ return client.send(Util_LogGroup_Adopting_JsMjs.newCommand(sdk.PutRetentionPolicyCommand, {
146
+ logGroupName: logGroupName,
147
+ retentionInDays: retentionInDays
148
+ }));
149
+ } else {
150
+ return client.send(Util_LogGroup_Adopting_JsMjs.newCommand(sdk.DeleteRetentionPolicyCommand, logGroupName));
151
+ }
152
+ });
153
+ }
154
+
155
+ async function applyTags(arn, tags) {
156
+ let sdk = await getSdk();
157
+ let client = await getClient();
158
+ return await runWithRetry(undefined, undefined, undefined, undefined, () => client.send(Util_LogGroup_Adopting_JsMjs.newCommand(sdk.TagResourceCommand, {
159
+ resourceArn: taggableArn(arn),
160
+ tags: tags
161
+ })));
162
+ }
163
+
164
+ async function create(inputs) {
165
+ let sdk = await getSdk();
166
+ let client = await getClient();
167
+ let tags = inputs.tags;
168
+ let retentionInDays = inputs.retentionInDays;
169
+ let logGroupName = inputs.logGroupName;
170
+ let adopted;
171
+ try {
172
+ await runWithRetry(undefined, undefined, undefined, undefined, () => client.send(Util_LogGroup_Adopting_JsMjs.newCommand(sdk.CreateLogGroupCommand, {
173
+ logGroupName: logGroupName,
174
+ tags: tags
175
+ })));
176
+ adopted = false;
177
+ } catch (raw_exn) {
178
+ let exn = Primitive_exceptions.internalToException(raw_exn);
179
+ if (Stdlib_Option.mapOr(Stdlib_JsExn.fromException(exn), false, isAlreadyExistsError)) {
180
+ adopted = true;
181
+ } else {
182
+ throw exn;
183
+ }
184
+ }
185
+ let match = await describe(logGroupName);
186
+ let arn;
187
+ if (match !== undefined) {
188
+ let arn$1 = match.arn;
189
+ arn = arn$1 !== undefined ? arn$1 : Stdlib_JsError.throwWithMessage(`log group "` + logGroupName + `" is missing immediately after create/adopt`);
190
+ } else {
191
+ arn = Stdlib_JsError.throwWithMessage(`log group "` + logGroupName + `" is missing immediately after create/adopt`);
192
+ }
193
+ if (adopted) {
194
+ await applyTags(arn, tags);
195
+ log.info("Util_LogGroup_Adopting", undefined, `adopted existing log group "` + logGroupName + `"; applied retention ` + retentionInDays.toString() + `d and tags`);
196
+ }
197
+ await applyRetention(logGroupName, retentionInDays);
198
+ return {
199
+ id: logGroupName,
200
+ outs: {
201
+ logGroupName: logGroupName,
202
+ arn: arn,
203
+ retentionInDays: retentionInDays,
204
+ tags: tags,
205
+ managedBy: marker
206
+ }
207
+ };
208
+ }
209
+
210
+ async function update(_id, _olds, news) {
211
+ let tags = news.tags;
212
+ let retentionInDays = news.retentionInDays;
213
+ let logGroupName = news.logGroupName;
214
+ let match = await describe(logGroupName);
215
+ let arn;
216
+ if (match !== undefined) {
217
+ let arn$1 = match.arn;
218
+ arn = arn$1 !== undefined ? arn$1 : undefined;
219
+ } else {
220
+ arn = undefined;
221
+ }
222
+ if (arn !== undefined) {
223
+ await applyTags(arn, tags);
224
+ await applyRetention(logGroupName, retentionInDays);
225
+ } else {
226
+ log.warn("Util_LogGroup_Adopting", undefined, `log group "` + logGroupName + `" was gone at update; recreating it`);
227
+ await create(news);
228
+ }
229
+ return {
230
+ outs: {
231
+ logGroupName: logGroupName,
232
+ arn: Stdlib_Nullable.fromOption(arn),
233
+ retentionInDays: retentionInDays,
234
+ tags: tags,
235
+ managedBy: marker
236
+ }
237
+ };
238
+ }
239
+
240
+ async function delete_(id, _props) {
241
+ let sdk = await getSdk();
242
+ let client = await getClient();
243
+ try {
244
+ return await runWithRetry(undefined, undefined, undefined, undefined, () => client.send(Util_LogGroup_Adopting_JsMjs.newCommand(sdk.DeleteLogGroupCommand, {
245
+ logGroupName: id
246
+ })));
247
+ } catch (raw_exn) {
248
+ let exn = Primitive_exceptions.internalToException(raw_exn);
249
+ if (Stdlib_Option.mapOr(Stdlib_JsExn.fromException(exn), false, isAlreadyGoneError)) {
250
+ return;
251
+ }
252
+ throw exn;
253
+ }
254
+ }
255
+
256
+ function sameTags(a, b) {
257
+ let normalise = d => {
258
+ let entries = Object.entries(d);
259
+ entries.sort((param, param$1) => Primitive_string.compare(param[0], param$1[0]));
260
+ return entries.map(param => param[0] + `=` + param[1]).join("");
261
+ };
262
+ return normalise(a) === normalise(b);
263
+ }
264
+
265
+ function diff_(_id, olds, news) {
266
+ let oldName = olds.logGroupName;
267
+ let replaces = (oldName == null) ? [] : (
268
+ oldName !== news.logGroupName ? ["logGroupName"] : []
269
+ );
270
+ let notNormalisedYet = Stdlib_Option.isNone(Primitive_option.fromNullable(olds.managedBy));
271
+ let days = olds.retentionInDays;
272
+ let retentionChanged = (days == null) ? false : days !== news.retentionInDays;
273
+ let tags = olds.tags;
274
+ let tagsChanged = (tags == null) ? false : !sameTags(tags, news.tags);
275
+ return {
276
+ changes: replaces.length !== 0 || notNormalisedYet || retentionChanged || tagsChanged,
277
+ replaces: replaces,
278
+ deleteBeforeReplace: false
279
+ };
280
+ }
281
+
282
+ async function read_(id, props) {
283
+ let live = await describe(id);
284
+ if (live !== undefined) {
285
+ return {
286
+ id: id,
287
+ props: {
288
+ logGroupName: id,
289
+ arn: Stdlib_Nullable.fromOption(live.arn),
290
+ retentionInDays: Stdlib_Option.getOr(live.retentionInDays, 0),
291
+ tags: props.tags,
292
+ managedBy: props.managedBy
293
+ }
294
+ };
295
+ } else {
296
+ log.info("Util_LogGroup_Adopting", undefined, `log group "` + id + `" is gone; reporting drift`);
297
+ return {};
298
+ }
299
+ }
300
+
301
+ let provider = {
302
+ create: create,
303
+ update: update,
304
+ delete: delete_,
305
+ diff: diff_,
306
+ read: read_
307
+ };
308
+
309
+ function resourcePropsOf(props) {
310
+ return {
311
+ logGroupName: props.logGroupName,
312
+ retentionInDays: props.retentionInDays,
313
+ tags: props.tags,
314
+ name: undefined,
315
+ arn: undefined,
316
+ managedBy: undefined
317
+ };
318
+ }
319
+
320
+ function make(name, props, opts) {
321
+ let migrationAlias = Alias$Pulumi.make(name, "aws:cloudwatch/logGroup:LogGroup", undefined);
322
+ let finalOpts;
323
+ if (opts !== undefined) {
324
+ let newrecord = {...opts};
325
+ newrecord.aliases = [migrationAlias];
326
+ finalOpts = newrecord;
327
+ } else {
328
+ finalOpts = {
329
+ aliases: [migrationAlias]
330
+ };
331
+ }
332
+ return new IndexJs.Resource(provider, name, resourcePropsOf(props), finalOpts);
333
+ }
334
+
335
+ export {
336
+ log,
337
+ newCommand,
338
+ newClient,
339
+ rethrow,
340
+ _sdk,
341
+ _client,
342
+ getSdk,
343
+ getClient,
344
+ matchesAws,
345
+ isAlreadyExistsError,
346
+ isAlreadyGoneError,
347
+ transientNames,
348
+ isRetryableError,
349
+ runWithRetry,
350
+ marker,
351
+ describe,
352
+ taggableArn,
353
+ applyRetention,
354
+ applyTags,
355
+ create,
356
+ update,
357
+ delete_,
358
+ sameTags,
359
+ diff_,
360
+ read_,
361
+ provider,
362
+ resourcePropsOf,
363
+ make,
364
+ }
365
+ /* log Not a pure module */
@@ -0,0 +1,20 @@
1
+ // Untyped JS primitives for Util_LogGroup_Adopting.
2
+ //
3
+ // The AWS SDK is imported dynamically there (a Pulumi dynamic provider
4
+ // serialises its whole captured closure into stack state, and a statically
5
+ // captured SDK does not survive that), so its command classes arrive as plain
6
+ // values on a module namespace and cannot be reached through `@new external`.
7
+ // These three helpers are the only untyped reflection that needs, kept out of
8
+ // the ReScript source.
9
+
10
+ /** `new Ctor(input)` — the SDK exports plain classes. */
11
+ export const newCommand = (Ctor, input) => new Ctor(input)
12
+
13
+ /** `new Ctor()` — the client constructor takes no required argument. */
14
+ export const newClient = Ctor => new Ctor()
15
+
16
+ /** Rethrows a JavaScript error as-is, bypassing the ReScript exception wrapper,
17
+ so Pulumi prints AWS's own message rather than "error: undefined". */
18
+ export const rethrow = e => {
19
+ throw e
20
+ }
@@ -62,19 +62,18 @@ let logLevelFor = (
62
62
  Whether the framework creates a *managed* CloudWatch log group for this stack, or
63
63
  leaves Lambda/AppSync to auto-create an unmanaged one.
64
64
 
65
- **Managed for every stack by default.** A stack deployed on this framework gets
66
- its log groups from Pulumi on the first `pulumi up`, before Lambda/AppSync would
67
- lazily auto-create them so on a fresh stack there is nothing to adopt and no
68
- `ResourceAlreadyExists` hazard. Every current stack is `alpha`, and there is no
69
- prod stack to migrate, so extending management to all stacks costs nothing today.
65
+ **Managed for every stack by default**, and for Lambda there is nothing to adopt
66
+ on any stack: the group is created *before* the function and under a name the
67
+ program chooses (`Util_LambdaLogging.logGroupNameFor`), which is never the name
68
+ Lambda auto-creates. The `ResourceAlreadyExists` hazard that made this gate
69
+ necessary is structural for AppSync only, whose group name comes from a
70
+ server-assigned api id.
70
71
 
71
- The one case that still needs care is *adopting* a stack that predates managed
72
- groups: it already carries Lambda's auto-created group, and `CreateLogGroup`
73
- fails with `ResourceAlreadyExists` rather than adopting it. When such a stack
74
- ever appears, either `pulumi import` its groups first, or name it in the
75
- `unmanagedLogGroupStacks` config key to keep it on auto-created groups until the
76
- import is done. That escape hatch is the "prepared for the future" seam — turning
77
- a prod adoption into a config flip rather than an edit to this function.
72
+ `unmanagedLogGroupStacks` remains the escape hatch for a stack that has adopted
73
+ **nothing** it makes the program stop *declaring* log groups. Do not reach for
74
+ it once a stack holds managed groups in state: "stop declaring them" reads to the
75
+ engine as removed-from-the-program, and the next deploy deletes them along with
76
+ their retention, tags and any attached metric filters.
78
77
  */
79
78
  let managesLogGroup = (~stack: string, ~unmanagedStacks: array<string>=[]): bool =>
80
79
  !(unmanagedStacks->Array.includes(stack))
@@ -0,0 +1,48 @@
1
+ open JestGlobals
2
+
3
+ // The managed log group's name is now chosen by the program rather than derived
4
+ // from the function's physical name output. That choice is what removes the race
5
+ // (the group can be created before the function, and pointed at from
6
+ // `loggingConfig`), so the properties it has to keep are worth pinning.
7
+ describe("Util_LambdaLogging.logGroupNameFor", () => {
8
+ let nameFor = (~project="online-shop-catalog-aws", ~stack="alpha", ~name) =>
9
+ Util_LambdaLogging.logGroupNameFor(~project, ~stack, ~name)
10
+
11
+ testSync("is the conventional Lambda group prefix", () =>
12
+ expect(nameFor(~name="AllReadModels"))->toBe(
13
+ "/aws/lambda/online-shop-catalog-aws-alpha-AllReadModels",
14
+ )
15
+ )
16
+
17
+ testSync("carries no Pulumi physical-name suffix, so a replaced function keeps its group", () => {
18
+ let before = nameFor(~name="AllReadModels")
19
+ let afterReplacement = nameFor(~name="AllReadModels")
20
+ expect(before)->toBe(afterReplacement)
21
+ })
22
+
23
+ testSync("is stack-scoped, so environments of one project cannot collide", () =>
24
+ expect(nameFor(~stack="pr-42", ~name="AllReadModels"))->not_->toBe(
25
+ nameFor(~stack="alpha", ~name="AllReadModels"),
26
+ )
27
+ )
28
+
29
+ // The regression this scoping exists for. Every plugin of one platform is its
30
+ // own Pulumi PROJECT deployed under the SAME stack name, and components are
31
+ // named by role rather than by owner — so several projects each host an
32
+ // `AllStateViewSlices`. Scoping on the stack alone gave them one name, and
33
+ // every deploy after the first failed with ResourceAlreadyExistsException.
34
+ testSync("is project-scoped, so one platform's plugins cannot collide", () =>
35
+ expect(nameFor(~project="online-shop-catalog-aws", ~name="AllStateViewSlices"))
36
+ ->not_
37
+ ->toBe(nameFor(~project="online-shop-ordering-aws", ~name="AllStateViewSlices"))
38
+ )
39
+
40
+ testSync("differs from the group Lambda would auto-create for the same function", () => {
41
+ // Lambda auto-creates `/aws/lambda/<physical name>`, and the physical name
42
+ // always carries Pulumi's `-<7hex>` suffix. Not colliding with it is what
43
+ // makes the create unconditionally safe on a stack whose groups AWS already
44
+ // made.
45
+ let autoCreated = "/aws/lambda/AllReadModels-0287438"
46
+ expect(nameFor(~name="AllReadModels"))->not_->toBe(autoCreated)
47
+ })
48
+ })
@@ -0,0 +1,30 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Util_LambdaLogging$ReventlessAws from "../src/util/Util_LambdaLogging.res.mjs";
4
+
5
+ globalThis.describe("Util_LambdaLogging.logGroupNameFor", () => {
6
+ let nameFor = (projectOpt, stackOpt, name) => {
7
+ let project = projectOpt !== undefined ? projectOpt : "online-shop-catalog-aws";
8
+ let stack = stackOpt !== undefined ? stackOpt : "alpha";
9
+ return Util_LambdaLogging$ReventlessAws.logGroupNameFor(project, stack, name);
10
+ };
11
+ globalThis.test("is the conventional Lambda group prefix", () => {
12
+ globalThis.expect(nameFor(undefined, undefined, "AllReadModels")).toBe("/aws/lambda/online-shop-catalog-aws-alpha-AllReadModels");
13
+ });
14
+ globalThis.test("carries no Pulumi physical-name suffix, so a replaced function keeps its group", () => {
15
+ let before = nameFor(undefined, undefined, "AllReadModels");
16
+ let afterReplacement = nameFor(undefined, undefined, "AllReadModels");
17
+ globalThis.expect(before).toBe(afterReplacement);
18
+ });
19
+ globalThis.test("is stack-scoped, so environments of one project cannot collide", () => {
20
+ globalThis.expect(nameFor(undefined, "pr-42", "AllReadModels")).not.toBe(nameFor(undefined, "alpha", "AllReadModels"));
21
+ });
22
+ globalThis.test("is project-scoped, so one platform's plugins cannot collide", () => {
23
+ globalThis.expect(nameFor("online-shop-catalog-aws", undefined, "AllStateViewSlices")).not.toBe(nameFor("online-shop-ordering-aws", undefined, "AllStateViewSlices"));
24
+ });
25
+ globalThis.test("differs from the group Lambda would auto-create for the same function", () => {
26
+ globalThis.expect(nameFor(undefined, undefined, "AllReadModels")).not.toBe("/aws/lambda/AllReadModels-0287438");
27
+ });
28
+ });
29
+
30
+ /* Not a pure module */
@@ -0,0 +1,169 @@
1
+ open JestGlobals
2
+
3
+ // Build a plain JS object shaped like a JS Error / SDK exception, cast to
4
+ // JsExn.t so the classifiers can be called without a real throw/catch.
5
+ type testError = {name: string, message: string}
6
+ let mkErr = (~name, ~message): JsExn.t => Obj.magic(({name, message}: testError))
7
+
8
+ module Adopting = Util_LogGroup_Adopting
9
+
10
+ let tags = Dict.fromArray([("Name", "PlatformApiAppSyncLogGroup"), ("reventless:role", "Logs")])
11
+
12
+ let normalisedOuts: Adopting.outs = {
13
+ logGroupName: Nullable.make("/aws/appsync/apis/abc123"),
14
+ arn: Nullable.make("arn:aws:logs:eu-west-1:1:log-group:/aws/appsync/apis/abc123:*"),
15
+ retentionInDays: Nullable.make(7),
16
+ tags: Nullable.make(tags),
17
+ managedBy: Nullable.make(Adopting.marker),
18
+ }
19
+
20
+ let inputs: Adopting.providerInputs = {
21
+ logGroupName: "/aws/appsync/apis/abc123",
22
+ retentionInDays: 7,
23
+ tags,
24
+ }
25
+
26
+ describe("Util_LogGroup_Adopting.isAlreadyExistsError", () => {
27
+ testSync("true for the error this provider exists to absorb", () =>
28
+ expect(
29
+ Adopting.isAlreadyExistsError(
30
+ mkErr(~name="ResourceAlreadyExistsException", ~message="The specified log group already exists"),
31
+ ),
32
+ )->toBe(true)
33
+ )
34
+
35
+ testSync("true when the code rides in the message", () =>
36
+ expect(
37
+ Adopting.isAlreadyExistsError(
38
+ mkErr(~name="InvalidParameterException", ~message="ResourceAlreadyExistsException: exists"),
39
+ ),
40
+ )->toBe(true)
41
+ )
42
+
43
+ testSync("false for a permission failure, which must still fail loudly", () =>
44
+ expect(
45
+ Adopting.isAlreadyExistsError(mkErr(~name="AccessDeniedException", ~message="not authorized")),
46
+ )->toBe(false)
47
+ )
48
+ })
49
+
50
+ describe("Util_LogGroup_Adopting.isRetryableError", () => {
51
+ testSync("true for the concurrent-write rejection on one group", () =>
52
+ expect(Adopting.isRetryableError(mkErr(~name="OperationAbortedException", ~message="x")))->toBe(
53
+ true,
54
+ )
55
+ )
56
+
57
+ testSync("false for an already-exists error — that is absorbed, not retried", () =>
58
+ expect(
59
+ Adopting.isRetryableError(mkErr(~name="ResourceAlreadyExistsException", ~message="x")),
60
+ )->toBe(false)
61
+ )
62
+ })
63
+
64
+ describe("Util_LogGroup_Adopting.taggableArn", () => {
65
+ testSync("strips the trailing `:*` TagResource rejects", () =>
66
+ expect(
67
+ Adopting.taggableArn("arn:aws:logs:eu-west-1:1:log-group:/aws/appsync/apis/abc123:*"),
68
+ )->toBe("arn:aws:logs:eu-west-1:1:log-group:/aws/appsync/apis/abc123")
69
+ )
70
+
71
+ testSync("leaves an already-taggable arn alone", () => {
72
+ let arn = "arn:aws:logs:eu-west-1:1:log-group:/aws/appsync/apis/abc123"
73
+ expect(Adopting.taggableArn(arn))->toBe(arn)
74
+ })
75
+ })
76
+
77
+ describe("Util_LogGroup_Adopting.sameTags", () => {
78
+ testSync("is insensitive to key order", () =>
79
+ expect(
80
+ Adopting.sameTags(
81
+ Dict.fromArray([("a", "1"), ("b", "2")]),
82
+ Dict.fromArray([("b", "2"), ("a", "1")]),
83
+ ),
84
+ )->toBe(true)
85
+ )
86
+
87
+ testSync("sees a changed value", () =>
88
+ expect(
89
+ Adopting.sameTags(Dict.fromArray([("a", "1")]), Dict.fromArray([("a", "2")])),
90
+ )->toBe(false)
91
+ )
92
+
93
+ testSync("sees an added key", () =>
94
+ expect(
95
+ Adopting.sameTags(Dict.fromArray([("a", "1")]), Dict.fromArray([("a", "1"), ("b", "2")])),
96
+ )->toBe(false)
97
+ )
98
+ })
99
+
100
+ describe("Util_LogGroup_Adopting.diff_", () => {
101
+ testSync("no change when the live state already matches", () => {
102
+ let result = Adopting.diff_("id", normalisedOuts, inputs)
103
+ expect(result.changes)->toBe(false)
104
+ expect(result.replaces)->toEqual([])
105
+ })
106
+
107
+ testSync("a retention change is an update, not a replace", () => {
108
+ let result = Adopting.diff_("id", normalisedOuts, {...inputs, retentionInDays: 30})
109
+ expect(result.changes)->toBe(true)
110
+ expect(result.replaces)->toEqual([])
111
+ })
112
+
113
+ testSync("a tag change is an update, not a replace", () => {
114
+ let result = Adopting.diff_(
115
+ "id",
116
+ normalisedOuts,
117
+ {...inputs, tags: Dict.fromArray([("Name", "renamed")])},
118
+ )
119
+ expect(result.changes)->toBe(true)
120
+ expect(result.replaces)->toEqual([])
121
+ })
122
+
123
+ testSync("a name change replaces — CloudWatch cannot rename a group in place", () => {
124
+ let result = Adopting.diff_(
125
+ "id",
126
+ normalisedOuts,
127
+ {...inputs, logGroupName: "/aws/appsync/apis/def456"},
128
+ )
129
+ expect(result.changes)->toBe(true)
130
+ expect(result.replaces)->toEqual(["logGroupName"])
131
+ })
132
+
133
+ testSync("replacing creates before deleting, so the old group stays readable", () => {
134
+ let result = Adopting.diff_(
135
+ "id",
136
+ normalisedOuts,
137
+ {...inputs, logGroupName: "/aws/appsync/apis/def456"},
138
+ )
139
+ expect(result.deleteBeforeReplace)->toBe(false)
140
+ })
141
+
142
+ // State written by the classic `aws:cloudwatch/logGroup:LogGroup` resource the
143
+ // alias adopts carries none of this provider's fields. It has to report a
144
+ // change even when nothing differs, because the following update is what
145
+ // writes this provider's output shape — and Pulumi's `__provider` — into state.
146
+ testSync("adopted classic state is always a change, so the update normalises it", () => {
147
+ let classicOuts: Adopting.outs = {
148
+ logGroupName: Nullable.undefined,
149
+ arn: Nullable.undefined,
150
+ retentionInDays: Nullable.undefined,
151
+ tags: Nullable.undefined,
152
+ managedBy: Nullable.undefined,
153
+ }
154
+ let result = Adopting.diff_("id", classicOuts, inputs)
155
+ expect(result.changes)->toBe(true)
156
+ })
157
+
158
+ testSync("adopted classic state never replaces on a field it does not carry", () => {
159
+ let classicOuts: Adopting.outs = {
160
+ logGroupName: Nullable.undefined,
161
+ arn: Nullable.undefined,
162
+ retentionInDays: Nullable.undefined,
163
+ tags: Nullable.undefined,
164
+ managedBy: Nullable.undefined,
165
+ }
166
+ let result = Adopting.diff_("id", classicOuts, inputs)
167
+ expect(result.replaces)->toEqual([])
168
+ })
169
+ })