1import * as apprunner from "@distilled.cloud/aws/apprunner";
2import * as Effect from "effect/Effect";
3import { diffTags } from "../../Tags.ts";
4
5/**
6 * App Runner documents configuration statuses as `ACTIVE`/`INACTIVE` but
7 * the wire returns lowercase `active`/`inactive` (observed live for auto
8 * scaling configurations and VPC connectors) — compare case-insensitively.
9 */
10export const isActiveStatus = (status: string | undefined): boolean =>
11 status?.toUpperCase() === "ACTIVE";
12
13/**
14 * Convert an App Runner wire tag list into a plain record, dropping
15 * malformed entries.
16 */
17export const toTagRecord = (
18 tags: ReadonlyArray<{ Key?: string; Value?: string }> | undefined,
19): Record<string, string> =>
20 Object.fromEntries(
21 (tags ?? [])
22 .filter(
23 (tag): tag is { Key: string; Value: string } =>
24 typeof tag.Key === "string" && typeof tag.Value === "string",
25 )
26 .map((tag) => [tag.Key, tag.Value]),
27 );
28
29/**
30 * Read the observed tags of an App Runner resource. Tag reads are
31 * best-effort — a failure (e.g. a race with deletion) reports no tags.
32 */
33export const readAppRunnerTags = Effect.fn(function* (arn: string) {
34 const response = yield* apprunner
35 .listTagsForResource({ ResourceArn: arn })
36 .pipe(Effect.catch(() => Effect.succeed(undefined)));
37 return toTagRecord(response?.Tags);
38});
39
40/**
41 * Sync tags on an App Runner resource: diff the OBSERVED cloud tags
42 * against the desired set and apply only the delta.
43 */
44export const syncAppRunnerTags = Effect.fn(function* (
45 arn: string,
46 desiredTags: Record<string, string>,
47) {
48 const observedTags = yield* readAppRunnerTags(arn);
49 const { removed, upsert } = diffTags(observedTags, desiredTags);
50 if (upsert.length > 0) {
51 yield* apprunner.tagResource({ ResourceArn: arn, Tags: upsert });
52 }
53 if (removed.length > 0) {
54 yield* apprunner.untagResource({ ResourceArn: arn, TagKeys: removed });
55 }
56});
57
58/**
59 * Convert a desired tag record into the wire tag list for create calls.
60 */
61export const toWireTags = (tags: Record<string, string>): apprunner.Tag[] =>
62 Object.entries(tags).map(([Key, Value]) => ({ Key, Value }));