1import * as smsvoice from "@distilled.cloud/aws/pinpoint-sms-voice-v2";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import { createPhysicalName } from "../../PhysicalName.ts";
8import * as Provider from "../../Provider.ts";
9import { Resource } from "../../Resource.ts";
10import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
11import type { Providers } from "../Providers.ts";
12import {
13 readSmsVoiceTags,
14 retrySmsVoiceThrottled,
15 syncSmsVoiceTags,
16 toTagList,
17} from "./internal.ts";
18
19export interface ConfigurationSetProps {
20 /**
21 * Name of the configuration set (`[A-Za-z0-9_-]+`, 1-64 characters).
22 * Changing the name replaces the configuration set.
23 * @default ${app}-${stage}-${id}
24 */
25 configurationSetName?: string;
26 /**
27 * Default message type applied to messages sent through this
28 * configuration set. Use `TRANSACTIONAL` for time-sensitive messages
29 * (e.g. one-time passcodes) and `PROMOTIONAL` for marketing content.
30 * Omitting the prop clears the default.
31 * @default no default message type
32 */
33 defaultMessageType?: "TRANSACTIONAL" | "PROMOTIONAL";
34 /**
35 * Tags to apply to the configuration set. Merged with internal Alchemy
36 * tags.
37 */
38 tags?: Record<string, string>;
39}
40
41export interface ConfigurationSet extends Resource<
42 "AWS.PinpointSMSVoiceV2.ConfigurationSet",
43 ConfigurationSetProps,
44 {
45 /**
46 * Name of the configuration set.
47 */
48 configurationSetName: string;
49 /**
50 * ARN of the configuration set.
51 */
52 configurationSetArn: string;
53 },
54 never,
55 Providers
56> {}
57
58/**
59 * An AWS End User Messaging SMS (Pinpoint SMS Voice v2) configuration
60 * set — a named set of rules applied to SMS and voice messages sent
61 * through it.
62 *
63 * Attach `EventDestination`s to a configuration set to route message
64 * events (sends, deliveries, failures) to CloudWatch Logs, Kinesis Data
65 * Firehose, or SNS.
66 * ### Creating Configuration Sets
67 * **Example:** Basic Configuration Set
68 * ```typescript
69 * import * as PinpointSMSVoiceV2 from "alchemy/AWS/PinpointSMSVoiceV2";
70 *
71 * const configSet = yield* PinpointSMSVoiceV2.ConfigurationSet("Messaging");
72 * ```
73 *
74 * **Example:** Configuration Set with a Default Message Type
75 * ```typescript
76 * const configSet = yield* PinpointSMSVoiceV2.ConfigurationSet("Otp", {
77 * defaultMessageType: "TRANSACTIONAL",
78 * tags: { team: "auth" },
79 * });
80 * ```
81 *
82 * ### Event Destinations
83 * **Example:** Stream message events to SNS
84 * ```typescript
85 * const events = yield* SNS.Topic("SmsEvents");
86 * const destination = yield* PinpointSMSVoiceV2.EventDestination("Events", {
87 * configurationSetName: configSet.configurationSetName,
88 * matchingEventTypes: ["ALL"],
89 * snsDestination: { topicArn: events.topicArn },
90 * });
91 * ```
92 *
93 * @resource
94 */
95export const ConfigurationSet = Resource<ConfigurationSet>(
96 "AWS.PinpointSMSVoiceV2.ConfigurationSet",
97);
98
99/**
100 * Raised when a configuration set cannot be observed immediately after
101 * it was created — the create call succeeded (or raced a peer) but the
102 * follow-up describe found nothing.
103 */
104export class SmsVoiceConfigurationSetMissing extends Data.TaggedError(
105 "SmsVoiceConfigurationSetMissing",
106)<{ message: string }> {}
107
108export const ConfigurationSetProvider = () =>
109 Provider.effect(
110 ConfigurationSet,
111 Effect.gen(function* () {
112 const toName = (id: string, props: ConfigurationSetProps) =>
113 props.configurationSetName
114 ? Effect.succeed(props.configurationSetName)
115 : createPhysicalName({ id, maxLength: 64 });
116
117 /**
118 * Observe a configuration set by name. `DescribeConfigurationSets`
119 * raises a typed `ResourceNotFoundException` for an unknown name.
120 */
121 const getByName = Effect.fn(function* (name: string) {
122 const result = yield* smsvoice
123 .describeConfigurationSets({ ConfigurationSetNames: [name] })
124 .pipe(
125 retrySmsVoiceThrottled,
126 Effect.catchTag("ResourceNotFoundException", () =>
127 Effect.succeed(undefined),
128 ),
129 );
130 return result?.ConfigurationSets?.find(
131 (cs) => cs.ConfigurationSetName === name,
132 );
133 });
134
135 return {
136 stables: ["configurationSetName", "configurationSetArn"],
137
138 read: Effect.fn(function* ({ id, olds, output }) {
139 const name =
140 output?.configurationSetName ?? (yield* toName(id, olds ?? {}));
141 const observed = yield* getByName(name);
142 if (observed === undefined) return undefined;
143 const attrs = {
144 configurationSetName: observed.ConfigurationSetName,
145 configurationSetArn: observed.ConfigurationSetArn,
146 };
147 const tags = yield* readSmsVoiceTags(observed.ConfigurationSetArn);
148 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
149 }),
150
151 diff: Effect.fn(function* ({ id, news, olds }) {
152 if (!isResolved(news)) return undefined;
153 const oldName = yield* toName(id, olds ?? {});
154 const newName = yield* toName(id, news);
155 if (oldName !== newName) return { action: "replace" } as const;
156 }),
157
158 reconcile: Effect.fn(function* ({ id, news, output, session }) {
159 const name =
160 output?.configurationSetName ?? (yield* toName(id, news));
161 const internalTags = yield* createInternalTags(id);
162 const desiredTags = { ...news.tags, ...internalTags };
163
164 // 1. Observe — cloud state is authoritative.
165 let observed = yield* getByName(name);
166
167 // 2. Ensure — create if missing; a ConflictException is a race
168 // with a peer reconciler, so fall through to re-observe.
169 if (observed === undefined) {
170 yield* smsvoice
171 .createConfigurationSet({
172 ConfigurationSetName: name,
173 Tags: toTagList(desiredTags),
174 })
175 .pipe(
176 retrySmsVoiceThrottled,
177 Effect.catchTag("ConflictException", () => Effect.void),
178 Effect.asVoid,
179 );
180 observed = yield* getByName(name);
181 }
182 if (observed === undefined) {
183 return yield* Effect.fail(
184 new SmsVoiceConfigurationSetMissing({
185 message: `configuration set '${name}' not observable after create`,
186 }),
187 );
188 }
189
190 // 3. Sync default message type — diff observed against desired;
191 // an absent prop clears any observed default.
192 if (
193 news.defaultMessageType !== undefined &&
194 observed.DefaultMessageType !== news.defaultMessageType
195 ) {
196 yield* smsvoice
197 .setDefaultMessageType({
198 ConfigurationSetName: name,
199 MessageType: news.defaultMessageType,
200 })
201 .pipe(retrySmsVoiceThrottled);
202 } else if (
203 news.defaultMessageType === undefined &&
204 observed.DefaultMessageType !== undefined
205 ) {
206 yield* smsvoice
207 .deleteDefaultMessageType({ ConfigurationSetName: name })
208 .pipe(
209 retrySmsVoiceThrottled,
210 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
211 Effect.asVoid,
212 );
213 }
214
215 // 3b. Sync tags — diff against OBSERVED cloud tags.
216 yield* syncSmsVoiceTags(observed.ConfigurationSetArn, desiredTags);
217
218 yield* session.note(observed.ConfigurationSetArn);
219 return {
220 configurationSetName: observed.ConfigurationSetName,
221 configurationSetArn: observed.ConfigurationSetArn,
222 };
223 }),
224
225 delete: Effect.fn(function* ({ output }) {
226 yield* smsvoice
227 .deleteConfigurationSet({
228 ConfigurationSetName: output.configurationSetName,
229 })
230 .pipe(
231 retrySmsVoiceThrottled,
232 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
233 Effect.asVoid,
234 );
235 }),
236
237 list: () =>
238 smsvoice.describeConfigurationSets.items({}).pipe(
239 Stream.runCollect,
240 Effect.map((chunk) =>
241 Array.from(chunk).map((cs) => ({
242 configurationSetName: cs.ConfigurationSetName,
243 configurationSetArn: cs.ConfigurationSetArn,
244 })),
245 ),
246 ),
247 };
248 }),
249 );