1import * as bda from "@distilled.cloud/aws/bedrock-data-automation";
2import * as Effect from "effect/Effect";
3import * as Redacted from "effect/Redacted";
4import { diffTags } from "../../Tags.ts";
5
6/**
7 * Unwrap a Bedrock Data Automation `SensitiveString` (decoded as `Redacted`)
8 * to its plain string value. `String(redacted)` would yield `"<redacted>"`,
9 * not the value.
10 */
11export const unredact = (value: string | Redacted.Redacted<string>): string =>
12 Redacted.isRedacted(value) ? Redacted.value(value) : value;
13
14/**
15 * Coerce a Bedrock Data Automation wire tag list (`{ key, value }[]`) into a
16 * plain `Record<string, string>`.
17 */
18export const toBdaTagRecord = (
19 tags: bda.Tag[] | undefined,
20): Record<string, string> =>
21 Object.fromEntries((tags ?? []).map((t) => [t.key, t.value] as const));
22
23/**
24 * Coerce a plain tag record into the Bedrock Data Automation wire tag list
25 * (`{ key, value }[]`).
26 */
27export const toBdaTagList = (tags: Record<string, string>): bda.Tag[] =>
28 Object.entries(tags).map(([key, value]) => ({ key, value }));
29
30/**
31 * Read the observed tags of a Bedrock Data Automation resource by ARN. A
32 * missing resource (race with deletion) reports no tags.
33 */
34export const readBdaTags = Effect.fn(function* (resourceARN: string) {
35 const response = yield* bda
36 .listTagsForResource({ resourceARN })
37 .pipe(
38 Effect.catchTag("ResourceNotFoundException", () =>
39 Effect.succeed(undefined),
40 ),
41 );
42 return toBdaTagRecord(response?.tags);
43});
44
45/**
46 * Sync tags on a Bedrock Data Automation resource: diff the OBSERVED cloud
47 * tags against the desired set and apply only the delta.
48 */
49export const syncBdaTags = Effect.fn(function* (
50 resourceARN: string,
51 desiredTags: Record<string, string>,
52) {
53 const observedTags = yield* readBdaTags(resourceARN);
54 const { removed, upsert } = diffTags(observedTags, desiredTags);
55 if (upsert.length > 0) {
56 yield* bda.tagResource({
57 resourceARN,
58 tags: upsert.map(({ Key, Value }) => ({ key: Key, value: Value })),
59 });
60 }
61 if (removed.length > 0) {
62 yield* bda.untagResource({ resourceARN, tagKeys: removed });
63 }
64});
65
66/**
67 * Key-order-independent structural equality for configuration objects, used
68 * to diff observed cloud configuration against the desired props. The server
69 * may fill defaulted fields the user never specified — a mismatch then just
70 * re-applies the idempotent PUT.
71 */
72export const bdaConfigEquals = (a: unknown, b: unknown): boolean =>
73 stableStringify(a) === stableStringify(b);
74
75const stableStringify = (value: unknown): string =>
76 JSON.stringify(sortKeysDeep(value));
77
78const sortKeysDeep = (value: unknown): unknown => {
79 if (Array.isArray(value)) return value.map(sortKeysDeep);
80 if (value !== null && typeof value === "object") {
81 return Object.fromEntries(
82 Object.entries(value as Record<string, unknown>)
83 .filter(([, v]) => v !== undefined)
84 .sort(([l], [r]) => l.localeCompare(r))
85 .map(([k, v]) => [k, sortKeysDeep(v)] as const),
86 );
87 }
88 return value;
89};