1import * as schemas from "@distilled.cloud/aws/schemas";
2import * as Effect from "effect/Effect";
3import { createInternalTags, diffTags, tagRecord } from "../../Tags.ts";
4
5/**
6 * Reads the observed tag map for an EventBridge Schemas resource ARN. A
7 * missing resource yields `{}` so callers treat it as "no tags observed".
8 */
9export const readSchemasTags = Effect.fn(function* (resourceArn: string) {
10 const { Tags } = yield* schemas
11 .listTagsForResource({ ResourceArn: resourceArn })
12 .pipe(
13 Effect.catchTag("NotFoundException", () =>
14 Effect.succeed({
15 Tags: {} as Record<string, string | undefined>,
16 }),
17 ),
18 );
19 return tagRecord(Tags);
20});
21
22/**
23 * Reconciles the tags on a Schemas resource to `{ ...user, ...internal }`,
24 * diffing against the OBSERVED cloud tags (so adoption converges). Applies
25 * only the delta: `tagResource` for upserts, `untagResource` for removals.
26 */
27export const syncSchemasTags = Effect.fn(function* (
28 resourceArn: string,
29 id: string,
30 userTags: Record<string, string> | undefined,
31) {
32 const internalTags = yield* createInternalTags(id);
33 const desired = { ...userTags, ...internalTags };
34 const observed = yield* readSchemasTags(resourceArn);
35 const { upsert, removed } = diffTags(observed, desired);
36 if (upsert.length > 0) {
37 yield* schemas.tagResource({
38 ResourceArn: resourceArn,
39 Tags: Object.fromEntries(upsert.map((t) => [t.Key, t.Value])),
40 });
41 }
42 if (removed.length > 0) {
43 yield* schemas.untagResource({
44 ResourceArn: resourceArn,
45 TagKeys: removed,
46 });
47 }
48});
49
50/**
51 * Canonicalizes a JSON document string for comparison: parses and re-stringifies
52 * with recursively sorted object keys so semantically-equal documents compare
53 * equal regardless of key order or whitespace. Non-JSON content is returned
54 * verbatim.
55 */
56export const canonicalJson = (content: string): string => {
57 const sortKeys = (value: unknown): unknown => {
58 if (Array.isArray(value)) return value.map(sortKeys);
59 if (value !== null && typeof value === "object") {
60 return Object.fromEntries(
61 Object.entries(value as Record<string, unknown>)
62 .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
63 .map(([k, v]) => [k, sortKeys(v)]),
64 );
65 }
66 return value;
67 };
68 try {
69 return JSON.stringify(sortKeys(JSON.parse(content)));
70 } catch {
71 return content;
72 }
73};