1import * as appconfig from "@distilled.cloud/aws/appconfig";
2import * as Effect from "effect/Effect";
3import { diffTags } from "../../Tags.ts";
4
5/**
6 * Build the ARN of an AppConfig application from its identity.
7 * `arn:aws:appconfig:{region}:{account}:application/{appId}`
8 */
9export const applicationArn = (
10 region: string,
11 accountId: string,
12 applicationId: string,
13): string =>
14 `arn:aws:appconfig:${region}:${accountId}:application/${applicationId}`;
15
16/**
17 * Build the ARN of an AppConfig environment.
18 * `arn:aws:appconfig:{region}:{account}:application/{appId}/environment/{envId}`
19 */
20export const environmentArn = (
21 region: string,
22 accountId: string,
23 applicationId: string,
24 environmentId: string,
25): string =>
26 `arn:aws:appconfig:${region}:${accountId}:application/${applicationId}/environment/${environmentId}`;
27
28/**
29 * Build the ARN of an AppConfig configuration profile.
30 * `arn:aws:appconfig:{region}:{account}:application/{appId}/configurationprofile/{id}`
31 */
32export const configurationProfileArn = (
33 region: string,
34 accountId: string,
35 applicationId: string,
36 configurationProfileId: string,
37): string =>
38 `arn:aws:appconfig:${region}:${accountId}:application/${applicationId}/configurationprofile/${configurationProfileId}`;
39
40/**
41 * Build the ARN of an AppConfig deployment strategy.
42 * `arn:aws:appconfig:{region}:{account}:deploymentstrategy/{id}`
43 */
44export const deploymentStrategyArn = (
45 region: string,
46 accountId: string,
47 deploymentStrategyId: string,
48): string =>
49 `arn:aws:appconfig:${region}:${accountId}:deploymentstrategy/${deploymentStrategyId}`;
50
51/**
52 * Data-plane resource ARN used by appconfigdata (StartConfigurationSession /
53 * GetLatestConfiguration). Note the segment is `configuration`, not
54 * `configurationprofile`.
55 * `arn:aws:appconfig:{region}:{account}:application/{appId}/environment/{envId}/configuration/{profileId}`
56 */
57export const configurationDataArn = (
58 region: string,
59 accountId: string,
60 applicationId: string,
61 environmentId: string,
62 configurationProfileId: string,
63): string =>
64 `arn:aws:appconfig:${region}:${accountId}:application/${applicationId}/environment/${environmentId}/configuration/${configurationProfileId}`;
65
66/**
67 * Build the ARN of an AppConfig extension association.
68 * `arn:aws:appconfig:{region}:{account}:extensionassociation/{id}`
69 */
70export const extensionAssociationArn = (
71 region: string,
72 accountId: string,
73 extensionAssociationId: string,
74): string =>
75 `arn:aws:appconfig:${region}:${accountId}:extensionassociation/${extensionAssociationId}`;
76
77/** Drop undefined values from an AppConfig tag map. */
78export const toTagRecord = (
79 tags: { [key: string]: string | undefined } | undefined,
80): Record<string, string> =>
81 Object.fromEntries(
82 Object.entries(tags ?? {}).filter(
83 (entry): entry is [string, string] => entry[1] !== undefined,
84 ),
85 );
86
87/**
88 * Read the observed tags of an AppConfig resource. Tag reads are best-effort —
89 * a failure (e.g. a race with deletion) reports no tags.
90 */
91export const readAppConfigTags = Effect.fn(function* (arn: string) {
92 const response = yield* appconfig
93 .listTagsForResource({ ResourceArn: arn })
94 .pipe(Effect.catch(() => Effect.succeed(undefined)));
95 return toTagRecord(response?.Tags);
96});
97
98/**
99 * Sync tags on an AppConfig resource: diff the OBSERVED cloud tags against the
100 * desired set and apply only the delta.
101 */
102export const syncAppConfigTags = Effect.fn(function* (
103 arn: string,
104 desiredTags: Record<string, string>,
105) {
106 const observedTags = yield* readAppConfigTags(arn);
107 const { removed, upsert } = diffTags(observedTags, desiredTags);
108 if (upsert.length > 0) {
109 yield* appconfig.tagResource({
110 ResourceArn: arn,
111 Tags: Object.fromEntries(upsert.map((t) => [t.Key, t.Value])),
112 });
113 }
114 if (removed.length > 0) {
115 yield* appconfig.untagResource({ ResourceArn: arn, TagKeys: removed });
116 }
117});