1import * as mm from "@distilled.cloud/aws/mailmanager";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import {
9 createInternalTags,
10 createTagsList,
11 hasAlchemyTags,
12} from "../../Tags.ts";
13import type { Providers } from "../Providers.ts";
14import { readMailManagerTags, syncMailManagerTags } from "./internal.ts";
15
16export interface AddonInstanceProps {
17 /**
18 * ID of the {@link AddonSubscription} this instance is created from.
19 * Immutable — changing the subscription replaces the instance.
20 */
21 addonSubscriptionId: string;
22 /**
23 * Tags applied to the instance. Alchemy ownership tags are merged in
24 * automatically.
25 */
26 tags?: Record<string, string>;
27}
28
29export interface AddonInstance extends Resource<
30 "AWS.MailManager.AddonInstance",
31 AddonInstanceProps,
32 {
33 /** Server-assigned ID of the Add On instance. */
34 addonInstanceId: string;
35 /** ARN of the Add On instance. */
36 addonInstanceArn: string;
37 /** ID of the subscription the instance was created from. */
38 addonSubscriptionId: string;
39 /** Name of the Add On product. */
40 addonName: string | undefined;
41 },
42 never,
43 Providers
44> {}
45
46/**
47 * An SES Mail Manager Add On instance — a usable deployment of a subscribed
48 * Add On that rule-set conditions and traffic-policy statements reference as
49 * an analyzer.
50 *
51 * Instances are immutable after creation (only tags update in place).
52 * ### Creating Add On Instances
53 * **Example:** Instance from a Subscription
54 * ```typescript
55 * import * as MailManager from "alchemy/AWS/MailManager";
56 *
57 * const subscription = yield* MailManager.AddonSubscription("Spamhaus", {
58 * addonName: "SPAMHAUS_DBL",
59 * });
60 * const instance = yield* MailManager.AddonInstance("SpamhausInstance", {
61 * addonSubscriptionId: subscription.addonSubscriptionId,
62 * });
63 * ```
64 *
65 * ### Referencing from a Traffic Policy
66 * **Example:** Analyzer Condition
67 * ```typescript
68 * const policy = yield* MailManager.TrafficPolicy("Edge", {
69 * defaultAction: "ALLOW",
70 * policyStatements: [
71 * {
72 * Action: "DENY",
73 * Conditions: [
74 * {
75 * BooleanExpression: {
76 * Evaluate: {
77 * Analysis: {
78 * Analyzer: instance.addonInstanceArn,
79 * ResultField: "IN_DBL",
80 * },
81 * },
82 * Operator: "IS_TRUE",
83 * },
84 * },
85 * ],
86 * },
87 * ],
88 * });
89 * ```
90 *
91 * @resource
92 */
93export const AddonInstance = Resource<AddonInstance>(
94 "AWS.MailManager.AddonInstance",
95);
96
97export const AddonInstanceProvider = () =>
98 Provider.effect(
99 AddonInstance,
100 Effect.gen(function* () {
101 const getById = (addonInstanceId: string) =>
102 mm
103 .getAddonInstance({ AddonInstanceId: addonInstanceId })
104 .pipe(
105 Effect.catchTag("ResourceNotFoundException", () =>
106 Effect.succeed(undefined),
107 ),
108 );
109
110 const listAll = mm.listAddonInstances.pages({}).pipe(
111 Stream.runCollect,
112 Effect.map((chunk) =>
113 Array.from(chunk).flatMap((page) => page.AddonInstances ?? []),
114 ),
115 );
116
117 // Instances have no user-supplied name — recover identity after a lost
118 // state write by searching for our ownership tags.
119 const findByAlchemyTags = Effect.fn(function* (id: string) {
120 const instances = yield* listAll;
121 for (const instance of instances) {
122 if (instance.AddonInstanceArn === undefined) continue;
123 const tags = yield* readMailManagerTags(instance.AddonInstanceArn);
124 if (yield* hasAlchemyTags(id, tags)) return instance;
125 }
126 return undefined;
127 });
128
129 const toAttrs = (instance: mm.AddonInstance) =>
130 instance.AddonInstanceId !== undefined &&
131 instance.AddonInstanceArn !== undefined &&
132 instance.AddonSubscriptionId !== undefined
133 ? {
134 addonInstanceId: instance.AddonInstanceId,
135 addonInstanceArn: instance.AddonInstanceArn,
136 addonSubscriptionId: instance.AddonSubscriptionId,
137 addonName: instance.AddonName,
138 }
139 : undefined;
140
141 const observe = Effect.fn(function* (
142 id: string,
143 output: AddonInstance["Attributes"] | undefined,
144 ) {
145 if (output?.addonInstanceId !== undefined) {
146 const found = yield* getById(output.addonInstanceId);
147 if (found !== undefined) {
148 return {
149 AddonInstanceId: output.addonInstanceId,
150 AddonInstanceArn: found.AddonInstanceArn,
151 AddonSubscriptionId: found.AddonSubscriptionId,
152 AddonName: found.AddonName,
153 } satisfies mm.AddonInstance;
154 }
155 }
156 return yield* findByAlchemyTags(id);
157 });
158
159 return AddonInstance.Provider.of({
160 stables: [
161 "addonInstanceId",
162 "addonInstanceArn",
163 "addonSubscriptionId",
164 "addonName",
165 ],
166
167 list: () =>
168 listAll.pipe(
169 Effect.map((instances) =>
170 instances.flatMap((i) => {
171 const attrs = toAttrs(i);
172 return attrs === undefined ? [] : [attrs];
173 }),
174 ),
175 ),
176
177 read: Effect.fn(function* ({ id, output }) {
178 const instance = yield* observe(id, output);
179 if (instance === undefined) return undefined;
180 const attrs = toAttrs(instance);
181 if (attrs === undefined) return undefined;
182 const tags = yield* readMailManagerTags(attrs.addonInstanceArn);
183 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
184 }),
185
186 // The subscription is create-only — changing it replaces the
187 // instance.
188 diff: Effect.fn(function* ({ news, olds }) {
189 if (!isResolved(news)) return undefined;
190 if (
191 olds !== undefined &&
192 olds.addonSubscriptionId !== news.addonSubscriptionId
193 ) {
194 return { action: "replace" } as const;
195 }
196 }),
197
198 reconcile: Effect.fn(function* ({ id, news, output, session }) {
199 const internalTags = yield* createInternalTags(id);
200 const desiredTags = { ...news.tags, ...internalTags };
201
202 // 1. OBSERVE — by cached id, falling back to an ownership-tag
203 // search.
204 let instance = yield* observe(id, output);
205
206 // 2. ENSURE — create if missing; a Conflict race re-observes via
207 // the tag search.
208 if (instance === undefined) {
209 yield* session.note(
210 `creating Add On instance for subscription ${news.addonSubscriptionId}`,
211 );
212 const created = yield* mm
213 .createAddonInstance({
214 AddonSubscriptionId: news.addonSubscriptionId,
215 Tags: createTagsList(desiredTags),
216 })
217 .pipe(
218 Effect.catchTag("ConflictException", () =>
219 Effect.succeed(undefined),
220 ),
221 );
222 if (created !== undefined) {
223 const found = yield* getById(created.AddonInstanceId);
224 instance = {
225 AddonInstanceId: created.AddonInstanceId,
226 AddonInstanceArn: found?.AddonInstanceArn,
227 AddonSubscriptionId:
228 found?.AddonSubscriptionId ?? news.addonSubscriptionId,
229 AddonName: found?.AddonName,
230 };
231 } else {
232 instance = yield* findByAlchemyTags(id);
233 }
234 }
235 const attrs = instance !== undefined ? toAttrs(instance) : undefined;
236 if (attrs === undefined) {
237 return yield* Effect.fail(
238 new Error(
239 `Mail Manager Add On instance for subscription '${news.addonSubscriptionId}' not found after create`,
240 ),
241 );
242 }
243
244 // 3. SYNC — only tags are mutable.
245 yield* syncMailManagerTags(attrs.addonInstanceArn, desiredTags);
246
247 yield* session.note(attrs.addonInstanceId);
248 return attrs;
249 }),
250
251 delete: Effect.fn(function* ({ output }) {
252 // DeleteAddonInstance is natively idempotent — deleting a
253 // nonexistent instance succeeds (verified live).
254 yield* mm
255 .deleteAddonInstance({ AddonInstanceId: output.addonInstanceId })
256 .pipe(Effect.asVoid);
257 }),
258 });
259 }),
260 );