1import * as appintegrations from "@distilled.cloud/aws/appintegrations";
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, diffTags, hasAlchemyTags } from "../../Tags.ts";
11import type { Providers } from "../Providers.ts";
12import { definedTags } from "./internal.ts";
13
14export interface EventIntegrationProps {
15 /**
16 * Name of the event integration. Unique per account/region. If omitted, a
17 * unique name is generated from the app, stage, and logical ID. Changing
18 * the name replaces the event integration.
19 */
20 name?: string;
21 /**
22 * Description of the event integration (1-1000 characters).
23 */
24 description?: string;
25 /**
26 * The partner event source that pushes events to the EventBridge bus,
27 * e.g. `aws.partner/examplepartner.com`. Changing the source replaces the
28 * event integration.
29 */
30 source: string;
31 /**
32 * The name of the Amazon EventBridge bus the partner events are delivered
33 * to, e.g. `default`. Only metadata is persisted — the bus itself is not
34 * created. Changing the bus replaces the event integration.
35 */
36 eventBridgeBus: string;
37 /**
38 * Tags to apply to the event integration. Merged with internal Alchemy tags.
39 */
40 tags?: Record<string, string>;
41}
42
43export interface EventIntegration extends Resource<
44 "AWS.AppIntegrations.EventIntegration",
45 EventIntegrationProps,
46 {
47 eventIntegrationName: string;
48 eventIntegrationArn: string;
49 eventBridgeBus: string;
50 source: string;
51 },
52 never,
53 Providers
54> {}
55
56/**
57 * An Amazon AppIntegrations event integration. An event integration
58 * associates a partner event source with an Amazon EventBridge bus in your
59 * account so external applications (e.g. Amazon Connect third-party apps)
60 * can publish events into it. Only metadata is persisted — no EventBridge
61 * objects are created.
62 *
63 * The event source and EventBridge bus are immutable; changing either
64 * replaces the event integration. Only the description can be updated in
65 * place.
66 * ### Creating an Event Integration
67 * **Example:** Basic Event Integration
68 * ```typescript
69 * import * as AppIntegrations from "alchemy/AWS/AppIntegrations";
70 *
71 * const events = yield* AppIntegrations.EventIntegration("PartnerEvents", {
72 * source: "aws.partner/examplepartner.com",
73 * eventBridgeBus: "default",
74 * });
75 * ```
76 *
77 * **Example:** Event Integration with Description and Tags
78 * ```typescript
79 * const events = yield* AppIntegrations.EventIntegration("PartnerEvents", {
80 * source: "aws.partner/examplepartner.com",
81 * eventBridgeBus: "default",
82 * description: "Events from Example Partner",
83 * tags: { team: "integrations" },
84 * });
85 * ```
86 *
87 * @resource
88 */
89export const EventIntegration = Resource<EventIntegration>(
90 "AWS.AppIntegrations.EventIntegration",
91);
92
93/**
94 * Raised when the AppIntegrations API returns an event integration without
95 * the fields required to build the resource attributes.
96 */
97export class EventIntegrationIncomplete extends Data.TaggedError(
98 "EventIntegrationIncomplete",
99)<{ message: string }> {}
100
101export const EventIntegrationProvider = () =>
102 Provider.effect(
103 EventIntegration,
104 Effect.gen(function* () {
105 const createName = Effect.fn(function* (
106 id: string,
107 props: { name?: string },
108 ) {
109 return (
110 props.name ?? (yield* createPhysicalName({ id, maxLength: 255 }))
111 );
112 });
113
114 /** Observe a single event integration by name; undefined if absent. */
115 const observe = (name: string) =>
116 appintegrations
117 .getEventIntegration({ Name: name })
118 .pipe(
119 Effect.catchTag("ResourceNotFoundException", () =>
120 Effect.succeed(undefined),
121 ),
122 );
123
124 const toAttrs = Effect.fn(function* (
125 live: appintegrations.GetEventIntegrationResponse,
126 ) {
127 if (
128 live.Name === undefined ||
129 live.EventIntegrationArn === undefined ||
130 live.EventBridgeBus === undefined ||
131 live.EventFilter?.Source === undefined
132 ) {
133 return yield* new EventIntegrationIncomplete({
134 message: `event integration '${live.Name}' is missing Name, Arn, EventBridgeBus, or EventFilter.Source`,
135 });
136 }
137 return {
138 eventIntegrationName: live.Name,
139 eventIntegrationArn: live.EventIntegrationArn,
140 eventBridgeBus: live.EventBridgeBus,
141 source: live.EventFilter.Source,
142 };
143 });
144
145 return EventIntegration.Provider.of({
146 stables: [
147 "eventIntegrationName",
148 "eventIntegrationArn",
149 "eventBridgeBus",
150 "source",
151 ],
152
153 list: () =>
154 appintegrations.listEventIntegrations.items({}).pipe(
155 Stream.runCollect,
156 Effect.map((chunk) =>
157 Array.from(chunk).flatMap((item) =>
158 item.Name !== undefined &&
159 item.EventIntegrationArn !== undefined &&
160 item.EventBridgeBus !== undefined &&
161 item.EventFilter?.Source !== undefined
162 ? [
163 {
164 eventIntegrationName: item.Name,
165 eventIntegrationArn: item.EventIntegrationArn,
166 eventBridgeBus: item.EventBridgeBus,
167 source: item.EventFilter.Source,
168 },
169 ]
170 : [],
171 ),
172 ),
173 ),
174
175 read: Effect.fn(function* ({ id, olds, output }) {
176 const name =
177 output?.eventIntegrationName ?? (yield* createName(id, olds ?? {}));
178 const live = yield* observe(name);
179 if (live === undefined) return undefined;
180 const attrs = yield* toAttrs(live);
181 const tags = definedTags(live.Tags);
182 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
183 }),
184
185 diff: Effect.fn(function* ({ id, news, olds }) {
186 if (!isResolved(news)) return undefined;
187 if (olds === undefined) return undefined;
188 const oldName = yield* createName(id, olds);
189 const newName = yield* createName(id, news);
190 if (
191 oldName !== newName ||
192 olds.source !== news.source ||
193 olds.eventBridgeBus !== news.eventBridgeBus
194 ) {
195 return { action: "replace" } as const;
196 }
197 // fall through: default update path (description, tags)
198 }),
199
200 reconcile: Effect.fn(function* ({ id, news, output, session }) {
201 const name =
202 output?.eventIntegrationName ?? (yield* createName(id, news));
203 const internalTags = yield* createInternalTags(id);
204 const desiredTags = { ...news.tags, ...internalTags };
205
206 // 1. Observe — cloud state is authoritative.
207 let live = yield* observe(name);
208
209 // 2. Ensure — create if missing; tolerate the concurrent-create race.
210 if (live === undefined) {
211 yield* appintegrations
212 .createEventIntegration({
213 Name: name,
214 Description: news.description,
215 EventFilter: { Source: news.source },
216 EventBridgeBus: news.eventBridgeBus,
217 Tags: desiredTags,
218 })
219 .pipe(
220 Effect.catchTag(
221 "DuplicateResourceException",
222 () => Effect.void,
223 ),
224 );
225 live = yield* appintegrations.getEventIntegration({ Name: name });
226 }
227 const attrs = yield* toAttrs(live);
228
229 // 3. Sync description — the only mutable field. The API cannot
230 // clear a description (min length 1), so only push a defined
231 // value that differs from the observed one.
232 if (
233 news.description !== undefined &&
234 news.description !== live.Description
235 ) {
236 yield* appintegrations.updateEventIntegration({
237 Name: name,
238 Description: news.description,
239 });
240 }
241
242 // 4. Sync tags — diff against OBSERVED cloud tags so adoption
243 // converges.
244 const observedTags = yield* appintegrations
245 .listTagsForResource({ resourceArn: attrs.eventIntegrationArn })
246 .pipe(
247 Effect.map((r) => definedTags(r.tags)),
248 Effect.catchTag("ResourceNotFoundException", () =>
249 Effect.succeed({} as Record<string, string>),
250 ),
251 );
252 const { upsert, removed } = diffTags(observedTags, desiredTags);
253 if (upsert.length > 0) {
254 yield* appintegrations.tagResource({
255 resourceArn: attrs.eventIntegrationArn,
256 tags: Object.fromEntries(upsert.map((t) => [t.Key, t.Value])),
257 });
258 }
259 if (removed.length > 0) {
260 yield* appintegrations.untagResource({
261 resourceArn: attrs.eventIntegrationArn,
262 tagKeys: removed,
263 });
264 }
265
266 yield* session.note(name);
267 return attrs;
268 }),
269
270 delete: Effect.fn(function* ({ output }) {
271 yield* appintegrations
272 .deleteEventIntegration({ Name: output.eventIntegrationName })
273 .pipe(
274 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
275 );
276 }),
277 });
278 }),
279 );