1import * as personalize from "@distilled.cloud/aws/personalize";
2import * as Effect from "effect/Effect";
3import * as Redacted from "effect/Redacted";
4import { diffTags } from "../../Tags.ts";
5
6/**
7 * Unwrap a Personalize `SensitiveString` (decoded as `Redacted`) to its plain
8 * string value. `String(redacted)` would yield `"<redacted>"`, not the value.
9 */
10export const unredact = (value: string | Redacted.Redacted<string>): string =>
11 Redacted.isRedacted(value) ? Redacted.value(value) : value;
12
13/**
14 * Coerce a Personalize wire tag list (`{ tagKey, tagValue }[]`, values decode
15 * as `Redacted`) into a plain `Record<string, string>`.
16 */
17export const toTagRecord = (
18 tags: personalize.Tag[] | undefined,
19): Record<string, string> =>
20 Object.fromEntries(
21 (tags ?? []).map(
22 (t) => [unredact(t.tagKey), unredact(t.tagValue)] as const,
23 ),
24 );
25
26/**
27 * Read the observed tags of a Personalize resource by ARN. Tag reads are
28 * best-effort — a failure (e.g. a race with deletion) reports no tags.
29 */
30export const readPersonalizeTags = Effect.fn(function* (arn: string) {
31 const response = yield* personalize
32 .listTagsForResource({ resourceArn: arn })
33 .pipe(Effect.catch(() => Effect.succeed(undefined)));
34 return toTagRecord(response?.tags);
35});
36
37/**
38 * Sync tags on a Personalize resource: diff the OBSERVED cloud tags against
39 * the desired set and apply only the delta.
40 */
41export const syncPersonalizeTags = Effect.fn(function* (
42 arn: string,
43 desiredTags: Record<string, string>,
44) {
45 const observedTags = yield* readPersonalizeTags(arn);
46 const { removed, upsert } = diffTags(observedTags, desiredTags);
47 if (upsert.length > 0) {
48 yield* personalize.tagResource({
49 resourceArn: arn,
50 tags: upsert.map((t) => ({ tagKey: t.Key, tagValue: t.Value })),
51 });
52 }
53 if (removed.length > 0) {
54 // tagKeys decodes as SensitiveString — wrap in Redacted for the encoder
55 // (which unwraps back to the plain string on the wire).
56 yield* personalize.untagResource({
57 resourceArn: arn,
58 tagKeys: removed.map((key) => Redacted.make(key)),
59 });
60 }
61});