1import * as sitewise from "@distilled.cloud/aws/iotsitewise";
2import * as Effect from "effect/Effect";
3import { diffTags } from "../../Tags.ts";
4
5/**
6 * Fetch the observed tags on an IoT SiteWise resource ARN as a plain
7 * string map (SiteWise's TagMap allows `undefined` values on the wire).
8 */
9export const fetchSiteWiseTags = Effect.fn(function* (arn: string) {
10 const response = yield* sitewise
11 .listTagsForResource({ resourceArn: arn })
12 .pipe(
13 Effect.catchTag("ResourceNotFoundException", () =>
14 Effect.succeed(undefined),
15 ),
16 );
17 const tags: Record<string, string> = {};
18 for (const [key, value] of Object.entries(response?.tags ?? {})) {
19 if (value !== undefined) tags[key] = value;
20 }
21 return tags;
22});
23
24/**
25 * Converge the tags on a SiteWise resource ARN from the observed cloud
26 * tags to the desired map (untag removed keys, upsert added/changed).
27 */
28export const syncSiteWiseTags = Effect.fn(function* (
29 arn: string,
30 observed: Record<string, string>,
31 desired: Record<string, string>,
32) {
33 const { removed, upsert } = diffTags(observed, desired);
34 if (removed.length > 0) {
35 yield* sitewise.untagResource({ resourceArn: arn, tagKeys: removed });
36 }
37 if (upsert.length > 0) {
38 yield* sitewise.tagResource({
39 resourceArn: arn,
40 tags: Object.fromEntries(upsert.map((tag) => [tag.Key, tag.Value])),
41 });
42 }
43});
44
45/**
46 * Deep-project `observed` onto the key structure of `desired`: for every
47 * key path present in `desired`, take the observed value. Keys the caller
48 * never specified (server-side defaults like `processingConfig` or
49 * generated `path` segments) are dropped so they don't read as drift.
50 */
51const projectLike = (desired: unknown, observed: unknown): unknown => {
52 if (Array.isArray(desired)) {
53 if (!Array.isArray(observed)) return observed;
54 return desired.map((item, index) => projectLike(item, observed[index]));
55 }
56 if (desired !== null && typeof desired === "object") {
57 if (
58 observed === null ||
59 typeof observed !== "object" ||
60 Array.isArray(observed)
61 ) {
62 return observed;
63 }
64 const out: Record<string, unknown> = {};
65 for (const [key, value] of Object.entries(
66 desired as Record<string, unknown>,
67 )) {
68 if (value === undefined) continue;
69 out[key] = projectLike(value, (observed as Record<string, unknown>)[key]);
70 }
71 return out;
72 }
73 return observed;
74};
75
76const stripUndefined = (value: unknown): unknown =>
77 value === undefined ? undefined : JSON.parse(JSON.stringify(value));
78
79/**
80 * Structural equality between a desired shape and the observed cloud
81 * state, ignoring observed-only keys (server defaults, generated ids the
82 * caller didn't pin, etc.).
83 */
84export const matchesDesired = (desired: unknown, observed: unknown): boolean =>
85 JSON.stringify(stripUndefined(desired)) ===
86 JSON.stringify(stripUndefined(projectLike(desired, observed)));