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 findSfdcInstanceByDescription,
13 listSfdcInstances,
14} from "./internal.ts";
15import {
16 encodeOwnership,
17 hasOwnershipMarker,
18 isDeleted,
19 lastSegment,
20 locationOf,
21 locationParent,
22 normalizeLocation,
23 ownedByAlchemy,
24 parseOwnership,
25 withProjectId,
26 replaceOnIdentity,
27 sameText,
28 toResourceId,
29 updateMaskOf,
30} from "./ownership.ts";
31
32export type SfdcInstanceProps = {
33 /**
34 * Instance id (the `{sfdcInstance}` segment of
35 * `projects/{project}/locations/{location}/sfdcInstances/{sfdcInstance}`).
36 * Server-assigned on create when omitted. Immutable — changing it
37 * replaces the instance.
38 */
39 sfdcInstanceId?: string;
40 /**
41 * Location (`us-central1`, …). Immutable — changing it replaces the
42 * instance. `US-CENTRAL1` is accepted and normalized to `us-central1`.
43 * @default the stack's GCP region (`GCP.Region`, else the profile
44 * region, else `us-central1`)
45 */
46 location?: string;
47 /**
48 * User-selected unique alias for the instance.
49 */
50 displayName?: string;
51 /**
52 * Human-readable description. SFDC instances have no labels field, so
53 * Alchemy stamps ownership into a `[alchemy …]` prefix and strips it
54 * from attributes.
55 */
56 description?: string;
57 /**
58 * Salesforce org id (`00D…`). Stored on the instance; Application
59 * Integration uses it when opening channels.
60 */
61 sfdcOrgId?: string;
62 /**
63 * AuthConfig resource names that can be tried to open a channel to
64 * Salesforce.
65 */
66 authConfigId?: string[];
67 /**
68 * URL used for API calls after authentication (login authority lives
69 * on the referenced AuthConfig).
70 */
71 serviceAuthority?: string;
72};
73
74export type SfdcInstance = Resource<
75 "GCP.Integrations.SfdcInstance",
76 SfdcInstanceProps,
77 {
78 /** Full resource name `projects/{project}/locations/{location}/sfdcInstances/{id}`. */
79 name: string;
80 /** Instance id (last path segment). */
81 sfdcInstanceId: string;
82 /** Location id. */
83 location: string;
84 /** Project id. */
85 project: string;
86 /** User-selected alias. */
87 displayName: string | undefined;
88 /** User description with the Alchemy ownership prefix stripped. */
89 description: string | undefined;
90 /** Salesforce org id. */
91 sfdcOrgId: string | undefined;
92 /** AuthConfig resource names. */
93 authConfigId: string[];
94 /** Post-auth API authority URL. */
95 serviceAuthority: string | undefined;
96 /** RFC3339 creation timestamp. */
97 createTime: string | undefined;
98 /** RFC3339 last-update timestamp. */
99 updateTime: string | undefined;
100 },
101 never,
102 Providers
103>;
104
105/**
106 * A Salesforce instance record in Application Integration. Holds org
107 * identity and the AuthConfigs used to open SFDC channels.
108 *
109 * Instances have no labels field, so Alchemy stamps ownership into the
110 * description for `list` / nuke. Location and id are identity — changing
111 * them replaces the instance. Display name, description, org id, auth
112 * configs, and service authority update in place.
113 *
114 * ### Creating an SFDC Instance
115 * **Example:** Org alias in us-central1
116 * ```typescript
117 * const instance = yield* GCP.Integrations.SfdcInstance("Salesforce", {
118 * displayName: "prod-org",
119 * description: "production salesforce",
120 * sfdcOrgId: "00Dxx0000000001",
121 * });
122 * ```
123 *
124 * ### Updating an SFDC Instance
125 * Re-declare the same logical id with changed props; the engine keeps the
126 * physical resource and updates it in place.
127 *
128 * **Example:** Change alias and org id
129 * ```typescript
130 * const instance = yield* GCP.Integrations.SfdcInstance("Salesforce", {
131 * displayName: "prod-org-v2",
132 * description: "production salesforce v2",
133 * sfdcOrgId: "00Dxx0000000002",
134 * });
135 * ```
136 *
137 * @resource
138 * @category Integrations
139 */
140export const SfdcInstance = Resource<SfdcInstance>(
141 "GCP.Integrations.SfdcInstance",
142);
143
144export class SfdcInstanceNotResolved extends Data.TaggedError(
145 "GCP.Integrations.SfdcInstanceNotResolved",
146)<{
147 name: string;
148}> {}
149
150const resourceName = (
151 project: string,
152 location: string,
153 sfdcInstanceId: string,
154) => `${locationParent(project, location)}/sfdcInstances/${sfdcInstanceId}`;
155
156const toAttrs = (
157 instance: integrations.GoogleCloudIntegrationsV1alphaSfdcInstance,
158 project: string,
159 region: string,
160) => {
161 const name = withProjectId(instance.name ?? "", project);
162 return {
163 name,
164 sfdcInstanceId: lastSegment(name),
165 location: locationOf(name, region),
166 project,
167 displayName: instance.displayName,
168 description: parseOwnership(instance.description).text,
169 sfdcOrgId: instance.sfdcOrgId,
170 authConfigId: [...(instance.authConfigId ?? [])],
171 serviceAuthority: instance.serviceAuthority,
172 createTime: instance.createTime,
173 updateTime: instance.updateTime,
174 };
175};
176
177const getByName = (name: string) =>
178 name.length === 0
179 ? Effect.succeed(undefined)
180 : integrations.getProjectsLocationsSfdcInstances({ name }).pipe(
181 Effect.map((instance) => (isDeleted(instance) ? undefined : instance)),
182 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
183 );
184
185export const SfdcInstanceProvider = () =>
186 Provider.succeed(SfdcInstance, {
187 stables: ["name", "sfdcInstanceId", "location", "project", "createTime"],
188
189 diff: Effect.fn(function* ({ news, olds, output }) {
190 if (!isResolved(news)) return undefined;
191 const env = yield* GcpEnvironment.current;
192 const previousId = olds?.sfdcInstanceId ?? output?.sfdcInstanceId;
193 const idChanged =
194 previousId !== undefined &&
195 news.sfdcInstanceId !== undefined &&
196 news.sfdcInstanceId !== previousId;
197 const previousLocation = olds?.location ?? output?.location;
198 const locationChanged =
199 previousLocation !== undefined &&
200 news.location !== undefined &&
201 normalizeLocation(news.location, env.region) !==
202 normalizeLocation(previousLocation, env.region);
203 return replaceOnIdentity(idChanged || locationChanged);
204 }),
205
206 read: Effect.fn(function* ({ id, olds, output }) {
207 const env = yield* GcpEnvironment.current;
208 const sfdcInstanceId = yield* toResourceId(
209 id,
210 olds?.sfdcInstanceId,
211 output?.sfdcInstanceId,
212 );
213 const location = normalizeLocation(
214 olds?.location ?? output?.location,
215 env.region,
216 );
217 const name =
218 output?.name ?? resourceName(env.project, location, sfdcInstanceId);
219 let existing = yield* getByName(name);
220 if (existing === undefined && output?.name === undefined) {
221 const ownership = yield* createInternalLabels(id);
222 existing = yield* findSfdcInstanceByDescription(
223 locationParent(env.project, location),
224 encodeOwnership(ownership, olds?.description),
225 );
226 }
227 if (existing === undefined) return undefined;
228 const attrs = toAttrs(existing, env.project, env.region);
229 return (yield* ownedByAlchemy(id, existing.description))
230 ? attrs
231 : Unowned(attrs);
232 }),
233
234 list: () =>
235 Effect.gen(function* () {
236 const env = yield* GcpEnvironment.current;
237 const items = yield* listSfdcInstances(
238 locationParent(env.project, env.region),
239 );
240 return items
241 .filter((instance) => hasOwnershipMarker(instance.description))
242 .map((instance) => toAttrs(instance, env.project, env.region));
243 }),
244
245 reconcile: Effect.fn(function* ({ id, news, output }) {
246 const env = yield* GcpEnvironment.current;
247 const location = normalizeLocation(
248 news.location ?? output?.location,
249 env.region,
250 );
251 const parent = locationParent(env.project, location);
252 const sfdcInstanceId = yield* toResourceId(
253 id,
254 news.sfdcInstanceId,
255 output?.sfdcInstanceId,
256 );
257 const name =
258 output?.name ?? resourceName(env.project, location, sfdcInstanceId);
259 const ownership = yield* createInternalLabels(id);
260 const description = encodeOwnership(ownership, news.description);
261 const displayName = news.displayName ?? sfdcInstanceId;
262 const authConfigId = news.authConfigId;
263 const body: integrations.GoogleCloudIntegrationsV1alphaSfdcInstance = {
264 displayName,
265 description,
266 sfdcOrgId: news.sfdcOrgId,
267 authConfigId,
268 serviceAuthority: news.serviceAuthority,
269 };
270
271 let current = yield* getByName(output?.name ?? name);
272 if (current === undefined) {
273 current = yield* findSfdcInstanceByDescription(parent, description);
274 }
275
276 if (current === undefined) {
277 const created = yield* integrations
278 .createProjectsLocationsSfdcInstances({
279 parent,
280 body,
281 })
282 .pipe(
283 Effect.catchTag("Conflict", () =>
284 findSfdcInstanceByDescription(parent, description),
285 ),
286 );
287 current = created ?? undefined;
288 }
289
290 if (current === undefined) {
291 return yield* new SfdcInstanceNotResolved({ name });
292 }
293
294 const currentName = withProjectId(current.name ?? name, env.project);
295 const displayChanged = !sameText(current.displayName, displayName);
296 const descriptionChanged = (current.description ?? "") !== description;
297 const orgChanged = !sameText(current.sfdcOrgId, news.sfdcOrgId);
298 const authChanged = !sameStringList(current.authConfigId, authConfigId);
299 const authorityChanged = !sameText(
300 current.serviceAuthority,
301 news.serviceAuthority,
302 );
303
304 if (
305 displayChanged ||
306 descriptionChanged ||
307 orgChanged ||
308 authChanged ||
309 authorityChanged
310 ) {
311 current = yield* integrations.patchProjectsLocationsSfdcInstances({
312 name: currentName,
313 updateMask: updateMaskOf(
314 displayChanged ? "displayName" : undefined,
315 descriptionChanged ? "description" : undefined,
316 orgChanged ? "sfdcOrgId" : undefined,
317 authChanged ? "authConfigId" : undefined,
318 authorityChanged ? "serviceAuthority" : undefined,
319 ),
320 body: { ...body, name: currentName },
321 });
322 }
323
324 return toAttrs(current, env.project, env.region);
325 }),
326
327 delete: Effect.fn(function* ({ output }) {
328 yield* integrations
329 .deleteProjectsLocationsSfdcInstances({ name: output.name })
330 .pipe(Effect.catchTag("NotFound", () => Effect.void));
331 }),
332 });
333
334const sameStringList = (
335 left: readonly string[] | undefined,
336 right: readonly string[] | undefined,
337) => JSON.stringify([...(left ?? [])]) === JSON.stringify([...(right ?? [])]);