1import * as ds from "@distilled.cloud/aws/directory-service";
2import * as Effect from "effect/Effect";
3import { isResolved } from "../../Diff.ts";
4import * as Provider from "../../Provider.ts";
5import { Resource } from "../../Resource.ts";
6import type { Providers } from "../Providers.ts";
7
8export interface EventTopicProps {
9 /**
10 * Id of the {@link Directory} whose status notifications are published.
11 * Changing the directory replaces the association.
12 */
13 directoryId: string;
14 /**
15 * Name of the Amazon SNS topic (same account and region) the directory
16 * publishes status messages to. Changing the topic replaces the
17 * association.
18 */
19 topicName: string;
20}
21
22export interface EventTopic extends Resource<
23 "AWS.DirectoryService.EventTopic",
24 EventTopicProps,
25 {
26 /** The ID of the directory publishing status notifications. */
27 directoryId: string;
28 /** The name of the SNS topic receiving status notifications. */
29 topicName: string;
30 /** The ARN of the SNS topic receiving status notifications. */
31 topicArn: string | undefined;
32 /** The status of the association, e.g. `Registered`. */
33 status: string | undefined;
34 },
35 never,
36 Providers
37> {}
38
39/**
40 * An association between an AWS Directory Service {@link Directory} and an
41 * Amazon SNS topic. The directory publishes a status message to the topic
42 * whenever it changes stage — e.g. from `Active` to `Impaired` or
43 * `Inoperable`, and back to `Active` — which is Directory Service's native
44 * event mechanism.
45 *
46 * To consume the notifications from a Lambda function, use
47 * {@link consumeDirectoryStatus}, which creates this association and
48 * subscribes the function to the topic.
49 * ### Publishing Directory Status Notifications
50 * **Example:** Publish Status Changes to an SNS Topic
51 * ```typescript
52 * const topic = yield* SNS.Topic("DirectoryStatus", {});
53 * const eventTopic = yield* DirectoryService.EventTopic("Status", {
54 * directoryId: directory.directoryId,
55 * topicName: topic.topicName,
56 * });
57 * ```
58 *
59 * @resource
60 */
61export const EventTopic = Resource<EventTopic>(
62 "AWS.DirectoryService.EventTopic",
63);
64
65export const EventTopicProvider = () =>
66 Provider.effect(
67 EventTopic,
68 Effect.gen(function* () {
69 const readEventTopic = Effect.fn(function* (
70 directoryId: string,
71 topicName: string,
72 ) {
73 const response = yield* ds
74 .describeEventTopics({
75 DirectoryId: directoryId,
76 TopicNames: [topicName],
77 })
78 .pipe(
79 Effect.catchTag("EntityDoesNotExistException", () =>
80 Effect.succeed(undefined),
81 ),
82 );
83 return response?.EventTopics?.find(
84 (topic) => topic.TopicName === topicName,
85 );
86 });
87
88 const toAttrs = (
89 directoryId: string,
90 topicName: string,
91 topic: ds.EventTopic,
92 ) => ({
93 directoryId,
94 topicName,
95 topicArn: topic.TopicArn,
96 status: topic.Status,
97 });
98
99 return {
100 stables: ["directoryId", "topicName"],
101
102 diff: Effect.fn(function* ({ olds, news }) {
103 if (!isResolved(news)) return undefined;
104 if (olds === undefined || news === undefined) return undefined;
105 if (news.directoryId !== olds.directoryId) {
106 return { action: "replace" } as const;
107 }
108 if (news.topicName !== olds.topicName) {
109 return { action: "replace" } as const;
110 }
111 }),
112
113 read: Effect.fn(function* ({ olds, output }) {
114 const directoryId = output?.directoryId ?? olds?.directoryId;
115 const topicName = output?.topicName ?? olds?.topicName;
116 if (directoryId === undefined || topicName === undefined) {
117 return undefined;
118 }
119 const topic = yield* readEventTopic(directoryId, topicName);
120 if (topic === undefined) return undefined;
121 return toAttrs(directoryId, topicName, topic);
122 }),
123
124 // Existence-only resource: the association has no mutable aspects,
125 // so reconcile is observe → ensure.
126 reconcile: Effect.fn(function* ({ news, session }) {
127 const props = news!;
128
129 // 1. Observe — cloud state is authoritative.
130 let observed = yield* readEventTopic(
131 props.directoryId,
132 props.topicName,
133 );
134
135 // 2. Ensure — register if missing. Re-registering an existing
136 // association is treated as a race and tolerated by
137 // re-observing below.
138 if (observed === undefined) {
139 yield* ds.registerEventTopic({
140 DirectoryId: props.directoryId,
141 TopicName: props.topicName,
142 });
143 observed = yield* readEventTopic(
144 props.directoryId,
145 props.topicName,
146 );
147 }
148 if (observed === undefined) {
149 return yield* Effect.fail(
150 new Error(
151 `event topic '${props.topicName}' on '${props.directoryId}' not visible after register`,
152 ),
153 );
154 }
155
156 yield* session.note(`${props.directoryId}/${props.topicName}`);
157 return toAttrs(props.directoryId, props.topicName, observed);
158 }),
159
160 delete: Effect.fn(function* ({ output }) {
161 yield* ds
162 .deregisterEventTopic({
163 DirectoryId: output.directoryId,
164 TopicName: output.topicName,
165 })
166 .pipe(
167 // The association — or the whole parent directory — being
168 // gone is success.
169 Effect.catchTag("EntityDoesNotExistException", () => Effect.void),
170 );
171 }),
172
173 // Event topics are keyed by their parent directory; there is no
174 // account-wide enumeration.
175 list: () => Effect.succeed([]),
176 };
177 }),
178 );