1import * as ds from "@distilled.cloud/aws/directory-service";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4
5/**
6 * Convert Directory Service's `[{ Key, Value }]` tag list into a plain
7 * record, dropping any entry missing a key or value.
8 */
9export const toTagRecord = (
10 tags: ReadonlyArray<{ Key?: string; Value?: string }> | undefined,
11): Record<string, string> =>
12 Object.fromEntries(
13 (tags ?? [])
14 .filter(
15 (tag): tag is { Key: string; Value: string } =>
16 typeof tag.Key === "string" && typeof tag.Value === "string",
17 )
18 .map((tag) => [tag.Key, tag.Value]),
19 );
20
21/**
22 * Read the observed tags for a directory by id. A directory mid-transition
23 * (or already deleted out of band) can reject `listTagsForResource`; treat
24 * any failure as "no observed tags" so tag reconciliation still converges on
25 * the next pass.
26 */
27export const readDirectoryTags = Effect.fn(function* (directoryId: string) {
28 const tags = yield* ds.listTagsForResource
29 .items({ ResourceId: directoryId })
30 .pipe(
31 Stream.runCollect,
32 Effect.catch(() => Effect.succeed([] as ds.Tag[])),
33 );
34 return toTagRecord(Array.from(tags));
35});
36
37/** True when two string sets are equal ignoring order and duplicates. */
38export const sameStringSet = (
39 a: readonly string[] | undefined,
40 b: readonly string[] | undefined,
41): boolean => {
42 const left = [...new Set(a ?? [])].sort();
43 const right = [...new Set(b ?? [])].sort();
44 return left.length === right.length && left.every((v, i) => v === right[i]);
45};