1import * as integrations from "@distilled.cloud/gcp/integrations_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { GcpEnvironment } from "../Environment.ts";
9import { createInternalLabels } from "../Labels.ts";
10import type { Providers } from "../Providers.ts";
11import {
12 findSfdcChannelByDescription,
13 listOwnedSfdcChannels,
14 parseResourceName,
15} from "./internal.ts";
16import {
17 encodeOwnership,
18 expandSfdcInstance,
19 hasOwnershipMarker,
20 isDeleted,
21 lastSegment,
22 locationOf,
23 normalizeLocation,
24 ownedByAlchemy,
25 parseOwnership,
26 withProjectId,
27 replaceOnIdentity,
28 sameText,
29 toResourceId,
30 updateMaskOf,
31} from "./ownership.ts";
32
33export type SfdcInstancesSfdcChannelProps = {
34 /**
35 * Parent SFDC instance resource name
36 * `projects/{project}/locations/{location}/sfdcInstances/{sfdcInstance}`
37 * or a bare instance id (combined with `location`). Immutable —
38 * changing it replaces the channel.
39 */
40 sfdcInstance: string;
41 /**
42 * Channel id (the `{sfdcChannel}` segment). Server-assigned on create
43 * when omitted. Immutable — changing it replaces the channel.
44 */
45 sfdcChannelId?: string;
46 /**
47 * Location used when `sfdcInstance` is a bare id.
48 * @default the stack's GCP region (`GCP.Region`, else the profile
49 * region, else `us-central1`)
50 */
51 location?: string;
52 /**
53 * Client-level unique alias for the channel.
54 */
55 displayName?: string;
56 /**
57 * Human-readable description. Channels have no labels field, so Alchemy
58 * stamps ownership into a `[alchemy …]` prefix and strips it from
59 * attributes.
60 */
61 description?: string;
62 /**
63 * Salesforce channel topic assigned when the channel is opened (CDC
64 * or Platform Event).
65 */
66 channelTopic: string;
67};
68
69export type SfdcInstancesSfdcChannel = Resource<
70 "GCP.Integrations.SfdcInstancesSfdcChannel",
71 SfdcInstancesSfdcChannelProps,
72 {
73 /** Full resource name `.../sfdcInstances/{instance}/sfdcChannels/{channel}`. */
74 name: string;
75 /** Channel id (last path segment). */
76 sfdcChannelId: string;
77 /** Parent SFDC instance resource name. */
78 sfdcInstance: string;
79 /** Location id. */
80 location: string;
81 /** Project id. */
82 project: string;
83 /** User-selected alias. */
84 displayName: string | undefined;
85 /** User description with the Alchemy ownership prefix stripped. */
86 description: string | undefined;
87 /** Salesforce channel topic. */
88 channelTopic: string | undefined;
89 /** Whether any published integration references this channel. */
90 isActive: boolean;
91 /** Last Salesforce message replay id. */
92 lastReplayId: string | undefined;
93 /** RFC3339 creation timestamp. */
94 createTime: string | undefined;
95 /** RFC3339 last-update timestamp. */
96 updateTime: string | undefined;
97 },
98 never,
99 Providers
100>;
101
102/**
103 * A Salesforce CDC or Platform Event channel under an Application
104 * Integration SFDC instance.
105 *
106 * Channels have no labels field, so Alchemy stamps ownership into the
107 * description for `list` / nuke. Parent instance and id are identity —
108 * changing them replaces the channel. Display name, description, and
109 * channel topic update in place.
110 *
111 * ### Creating an SFDC Channel
112 * **Example:** Platform Event channel
113 * ```typescript
114 * const channel = yield* GCP.Integrations.SfdcInstancesSfdcChannel("Events", {
115 * sfdcInstance: instance.name,
116 * displayName: "account-events",
117 * channelTopic: "/event/AlchemyTest__e",
118 * });
119 * ```
120 *
121 * ### Updating an SFDC Channel
122 * Re-declare the same logical id with changed props; the engine keeps the
123 * physical resource and updates it in place.
124 *
125 * **Example:** Rename the alias
126 * ```typescript
127 * const channel = yield* GCP.Integrations.SfdcInstancesSfdcChannel("Events", {
128 * sfdcInstance: instance.name,
129 * displayName: "account-events-v2",
130 * channelTopic: "/event/AlchemyTest__e",
131 * });
132 * ```
133 *
134 * @resource
135 * @category Integrations
136 */
137export const SfdcInstancesSfdcChannel = Resource<SfdcInstancesSfdcChannel>(
138 "GCP.Integrations.SfdcInstancesSfdcChannel",
139);
140
141export class SfdcInstancesSfdcChannelNotResolved extends Data.TaggedError(
142 "GCP.Integrations.SfdcInstancesSfdcChannelNotResolved",
143)<{
144 name: string;
145}> {}
146
147const resourceName = (sfdcInstance: string, sfdcChannelId: string) =>
148 `${sfdcInstance}/sfdcChannels/${sfdcChannelId}`;
149
150const toAttrs = (
151 channel: integrations.GoogleCloudIntegrationsV1alphaSfdcChannel,
152 project: string,
153 region: string,
154 instanceHint?: string,
155) => {
156 const name = withProjectId(channel.name ?? "", project);
157 const parsed = parseResourceName(name, "sfdcChannels", region);
158 return {
159 name,
160 sfdcChannelId: lastSegment(name),
161 sfdcInstance: name.includes("/sfdcChannels/")
162 ? parsed.sfdcInstance
163 : (instanceHint ?? parsed.parent),
164 location: locationOf(name, region),
165 project,
166 displayName: channel.displayName,
167 description: parseOwnership(channel.description).text,
168 channelTopic: channel.channelTopic,
169 isActive: channel.isActive === true,
170 lastReplayId: channel.lastReplayId,
171 createTime: channel.createTime,
172 updateTime: channel.updateTime,
173 };
174};
175
176const getByName = (name: string) =>
177 name.length === 0
178 ? Effect.succeed(undefined)
179 : integrations.getProjectsLocationsSfdcInstancesSfdcChannels({ name }).pipe(
180 Effect.map((channel) => (isDeleted(channel) ? undefined : channel)),
181 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
182 );
183
184export const SfdcInstancesSfdcChannelProvider = () =>
185 Provider.succeed(SfdcInstancesSfdcChannel, {
186 stables: [
187 "name",
188 "sfdcChannelId",
189 "sfdcInstance",
190 "location",
191 "project",
192 "createTime",
193 ],
194
195 diff: Effect.fn(function* ({ news, olds, output }) {
196 if (!isResolved(news)) return undefined;
197 const env = yield* GcpEnvironment.current;
198 const previousInstance = olds?.sfdcInstance ?? output?.sfdcInstance;
199 const instanceChanged =
200 previousInstance !== undefined &&
201 lastSegment(news.sfdcInstance) !== lastSegment(previousInstance);
202 const previousId = olds?.sfdcChannelId ?? output?.sfdcChannelId;
203 const idChanged =
204 previousId !== undefined &&
205 news.sfdcChannelId !== undefined &&
206 news.sfdcChannelId !== previousId;
207 const previousLocation = olds?.location ?? output?.location;
208 const locationChanged =
209 previousLocation !== undefined &&
210 news.location !== undefined &&
211 normalizeLocation(news.location, env.region) !==
212 normalizeLocation(previousLocation, env.region);
213 return replaceOnIdentity(instanceChanged || idChanged || locationChanged);
214 }),
215
216 read: Effect.fn(function* ({ id, olds, output }) {
217 const env = yield* GcpEnvironment.current;
218 const location = normalizeLocation(
219 olds?.location ?? output?.location,
220 env.region,
221 );
222 const sfdcInstance = olds?.sfdcInstance
223 ? expandSfdcInstance(olds.sfdcInstance, env.project, location)
224 : output?.sfdcInstance;
225 const sfdcChannelId = yield* toResourceId(
226 id,
227 olds?.sfdcChannelId,
228 output?.sfdcChannelId,
229 );
230 const name =
231 output?.name ??
232 (sfdcInstance !== undefined
233 ? resourceName(sfdcInstance, sfdcChannelId)
234 : "");
235 let existing = yield* getByName(name);
236 if (
237 existing === undefined &&
238 sfdcInstance !== undefined &&
239 output?.name === undefined
240 ) {
241 const ownership = yield* createInternalLabels(id);
242 existing = yield* findSfdcChannelByDescription(
243 sfdcInstance,
244 encodeOwnership(ownership, olds?.description),
245 );
246 }
247 if (existing === undefined) return undefined;
248 const attrs = toAttrs(existing, env.project, env.region, sfdcInstance);
249 return (yield* ownedByAlchemy(id, existing.description))
250 ? attrs
251 : Unowned(attrs);
252 }),
253
254 list: () =>
255 Effect.gen(function* () {
256 const env = yield* GcpEnvironment.current;
257 const items = yield* listOwnedSfdcChannels(env.project, env.region);
258 return items
259 .filter((channel) => hasOwnershipMarker(channel.description))
260 .map((channel) => toAttrs(channel, env.project, env.region));
261 }),
262
263 reconcile: Effect.fn(function* ({ id, news, output }) {
264 const env = yield* GcpEnvironment.current;
265 const location = normalizeLocation(
266 news.location ?? output?.location,
267 env.region,
268 );
269 const sfdcInstance = expandSfdcInstance(
270 news.sfdcInstance,
271 env.project,
272 location,
273 );
274 const sfdcChannelId = yield* toResourceId(
275 id,
276 news.sfdcChannelId,
277 output?.sfdcChannelId,
278 );
279 const name = output?.name ?? resourceName(sfdcInstance, sfdcChannelId);
280 const ownership = yield* createInternalLabels(id);
281 const description = encodeOwnership(ownership, news.description);
282 const displayName = news.displayName ?? sfdcChannelId;
283 const body: integrations.GoogleCloudIntegrationsV1alphaSfdcChannel = {
284 displayName,
285 description,
286 channelTopic: news.channelTopic,
287 };
288
289 let current = yield* getByName(output?.name ?? name);
290 if (current === undefined) {
291 current = yield* findSfdcChannelByDescription(
292 sfdcInstance,
293 description,
294 );
295 }
296
297 if (current === undefined) {
298 const created = yield* integrations
299 .createProjectsLocationsSfdcInstancesSfdcChannels({
300 parent: sfdcInstance,
301 body,
302 })
303 .pipe(
304 Effect.catchTag("Conflict", () =>
305 findSfdcChannelByDescription(sfdcInstance, description),
306 ),
307 );
308 current = created ?? undefined;
309 }
310
311 if (current === undefined) {
312 return yield* new SfdcInstancesSfdcChannelNotResolved({ name });
313 }
314
315 const currentName = withProjectId(current.name ?? name, env.project);
316 const displayChanged = !sameText(current.displayName, displayName);
317 const descriptionChanged = (current.description ?? "") !== description;
318 const topicChanged = !sameText(current.channelTopic, news.channelTopic);
319
320 if (displayChanged || descriptionChanged || topicChanged) {
321 current =
322 yield* integrations.patchProjectsLocationsSfdcInstancesSfdcChannels({
323 name: currentName,
324 updateMask: updateMaskOf(
325 displayChanged ? "displayName" : undefined,
326 descriptionChanged ? "description" : undefined,
327 topicChanged ? "channelTopic" : undefined,
328 ),
329 body: { ...body, name: currentName },
330 });
331 }
332
333 return toAttrs(current, env.project, env.region, sfdcInstance);
334 }),
335
336 delete: Effect.fn(function* ({ output }) {
337 yield* integrations
338 .deleteProjectsLocationsSfdcInstancesSfdcChannels({
339 name: output.name,
340 })
341 .pipe(Effect.catchTag("NotFound", () => Effect.void));
342 }),
343 });