1import * as amp from "@distilled.cloud/aws/amp";
2import * as Effect from "effect/Effect";
3import { diffTags } from "../../Tags.ts";
4
5/**
6 * Coerce an AMP wire tag map (values are `string | undefined`) into a plain
7 * `Record<string, string>`, dropping undefined values.
8 */
9export const toTagRecord = (
10 tags: { [key: string]: string | undefined } | undefined,
11): Record<string, string> =>
12 Object.fromEntries(
13 Object.entries(tags ?? {}).filter(
14 (entry): entry is [string, string] => typeof entry[1] === "string",
15 ),
16 );
17
18/**
19 * Read the observed tags of an AMP resource by ARN. Tag reads are
20 * best-effort — a failure (e.g. a race with deletion) reports no tags.
21 */
22export const readAmpTags = Effect.fn(function* (arn: string) {
23 const response = yield* amp
24 .listTagsForResource({ resourceArn: arn })
25 .pipe(Effect.catch(() => Effect.succeed(undefined)));
26 return toTagRecord(response?.tags);
27});
28
29/**
30 * Sync tags on an AMP resource: diff the OBSERVED cloud tags against the
31 * desired set and apply only the delta. AMP's `tagResource` takes a tag
32 * map (not a list), so the `upsert` delta is folded back into a record.
33 */
34export const syncAmpTags = Effect.fn(function* (
35 arn: string,
36 desiredTags: Record<string, string>,
37) {
38 const observedTags = yield* readAmpTags(arn);
39 const { removed, upsert } = diffTags(observedTags, desiredTags);
40 if (upsert.length > 0) {
41 yield* amp.tagResource({
42 resourceArn: arn,
43 tags: Object.fromEntries(upsert.map((t) => [t.Key, t.Value])),
44 });
45 }
46 if (removed.length > 0) {
47 yield* amp.untagResource({ resourceArn: arn, tagKeys: removed });
48 }
49});
50
51/**
52 * AMP's logging APIs expect the destination CloudWatch Logs log group ARN
53 * with a trailing `:*` (the log-stream wildcard). Append it when missing so
54 * callers can pass a `Logs.LogGroup`'s `logGroupArn` directly.
55 */
56export const normalizeAmpLogGroupArn = (arn: string): string =>
57 arn.endsWith(":*") ? arn : `${arn}:*`;
58
59/** Encode a UTF-8 string definition into the wire blob AMP expects. */
60export const encodeDefinition = (
61 definition: string,
62): Effect.Effect<Uint8Array> =>
63 Effect.sync(() => new TextEncoder().encode(definition));
64
65/** Decode an AMP definition blob back into a UTF-8 string. */
66export const decodeDefinition = (data: Uint8Array): Effect.Effect<string> =>
67 Effect.sync(() => new TextDecoder().decode(data));