1import * as essentialcontacts from "@distilled.cloud/gcp/essentialcontacts_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 type { Providers } from "../Providers.ts";
10import {
11 desiredCategories,
12 desiredLanguage,
13 encodeEmail,
14 findOwnedContact,
15 folderParent,
16 hasOwnershipMarker,
17 lastSegment,
18 listContacts,
19 listFolderParents,
20 normalizeEmail,
21 ownedByAlchemy,
22 ownershipLabels,
23 resolveFolder,
24 sameCategories,
25 sameText,
26 toCoreAttrs,
27 toCreateBody,
28 toEmail,
29 UPDATE_MASK,
30 type NotificationCategory,
31} from "./internal.ts";
32
33export type FolderContactProps = {
34 /**
35 * Parent folder (`folders/{folder}` or the numeric id). Defaults to
36 * the project's Resource Manager folder
37 * ancestor. Immutable — changing it replaces the contact.
38 */
39 folderId?: string;
40 /**
41 * Email address that receives Google Cloud notifications. Does not
42 * need to be a Google Account. Immutable — changing it replaces the
43 * contact. If omitted, a unique `example.com` address is generated.
44 * Essential Contacts have no labels field, so Alchemy stamps
45 * ownership into a `+alc.{stack}.{stage}.{id}` plus-tag and strips it
46 * from attributes.
47 */
48 email?: string;
49 /**
50 * Preferred notification language as a BCP-47 / ISO 639-1 tag
51 * (`en-US`, `en-GB`, `ja`, …).
52 * @default "en-US"
53 */
54 languageTag?: string;
55 /**
56 * Notification categories this contact subscribes to.
57 * @default ["ALL"]
58 */
59 notificationCategorySubscriptions?: NotificationCategory[];
60};
61
62export type FolderContact = Resource<
63 "GCP.EssentialContacts.FolderContact",
64 FolderContactProps,
65 {
66 /** Full resource name `folders/{folder}/contacts/{contact}`. */
67 name: string;
68 /** Server-assigned contact id (last path segment). */
69 contactId: string;
70 /** Parent resource `folders/{folder}`. */
71 parent: string;
72 /** Folder id (last path segment of `parent`). */
73 folderId: string;
74 /** Project id used when the contact was reconciled. */
75 project: string;
76 /** User email with the Alchemy plus-tag stripped. */
77 email: string;
78 /** Preferred notification language. */
79 languageTag: string;
80 /** Subscribed notification categories. */
81 notificationCategorySubscriptions: NotificationCategory[];
82 /** Server-reported validation state. */
83 validationState: string | undefined;
84 /** RFC3339 time the validation state was last updated. */
85 validateTime: string | undefined;
86 },
87 never,
88 Providers
89>;
90
91/**
92 * A folder-level Google Cloud Essential Contact.
93 *
94 * Essential Contacts have no labels field, so Alchemy stamps ownership
95 * into the email plus-tag for `list` / nuke. Folder and email are
96 * identity — changing either replaces the contact. Language and
97 * notification categories update in place.
98 *
99 * ### Creating a Folder Contact
100 * **Example:** Technical contact on a folder
101 * ```typescript
102 * const contact = yield* GCP.EssentialContacts.FolderContact("Ops", {
103 * folderId: "folders/123",
104 * email: "ops@example.com",
105 * languageTag: "en-US",
106 * notificationCategorySubscriptions: ["TECHNICAL"],
107 * });
108 * ```
109 *
110 * **Example:** Subscribe to every category
111 * ```typescript
112 * const contact = yield* GCP.EssentialContacts.FolderContact("Ops", {
113 * folderId: "123",
114 * email: "ops@example.com",
115 * notificationCategorySubscriptions: ["ALL"],
116 * });
117 * ```
118 *
119 * ### Updating a Folder Contact
120 * **Example:** Change language and categories
121 * ```typescript
122 * const contact = yield* GCP.EssentialContacts.FolderContact("Ops", {
123 * folderId: "folders/123",
124 * email: "ops@example.com",
125 * languageTag: "en-GB",
126 * notificationCategorySubscriptions: ["SECURITY", "TECHNICAL"],
127 * });
128 * ```
129 *
130 * @resource
131 * @category EssentialContacts
132 */
133export const FolderContact = Resource<FolderContact>(
134 "GCP.EssentialContacts.FolderContact",
135);
136
137export class FolderContactNotResolved extends Data.TaggedError(
138 "GCP.EssentialContacts.FolderContactNotResolved",
139)<{
140 name: string;
141}> {}
142
143const getByName = (name: string) =>
144 name.length === 0
145 ? Effect.succeed(undefined)
146 : essentialcontacts
147 .getFoldersContacts({ name })
148 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
149
150const listAt = (parent: string) =>
151 parent.length === 0
152 ? Effect.succeed([])
153 : listContacts(
154 essentialcontacts.listFoldersContacts.pages({
155 parent,
156 pageSize: 200,
157 }),
158 );
159
160const toAttrs = (
161 contact: essentialcontacts.GoogleCloudEssentialcontactsV1Contact,
162 parent: string,
163 project: string,
164) => {
165 const core = toCoreAttrs(contact, parent, project);
166 return {
167 ...core,
168 folderId: lastSegment(core.parent),
169 };
170};
171
172export const FolderContactProvider = () =>
173 Provider.succeed(FolderContact, {
174 stables: ["name", "contactId", "parent", "folderId", "project", "email"],
175
176 diff: Effect.fn(function* ({ news, olds, output }) {
177 if (!isResolved(news)) return undefined;
178 const previousEmail = olds?.email ?? output?.email;
179 if (
180 news.email !== undefined &&
181 previousEmail !== undefined &&
182 normalizeEmail(news.email) !== normalizeEmail(previousEmail)
183 ) {
184 return { action: "replace" as const, deleteFirst: false };
185 }
186 const previousFolder = olds?.folderId ?? output?.folderId;
187 if (
188 news.folderId !== undefined &&
189 previousFolder !== undefined &&
190 folderParent(news.folderId) !== folderParent(previousFolder)
191 ) {
192 return { action: "replace" as const, deleteFirst: false };
193 }
194 return undefined;
195 }),
196
197 read: Effect.fn(function* ({ id, olds, output }) {
198 const env = yield* GcpEnvironment.current;
199 const parent = yield* resolveFolder(
200 olds?.folderId ?? output?.folderId,
201 output?.parent,
202 );
203 let existing = yield* getByName(output?.name ?? "");
204 if (existing === undefined) {
205 existing = yield* findOwnedContact(
206 yield* listAt(parent),
207 id,
208 output?.name,
209 olds?.email ?? output?.email,
210 );
211 }
212 if (existing === undefined) return undefined;
213 const attrs = toAttrs(existing, parent, env.project);
214 return (yield* ownedByAlchemy(id, existing.email))
215 ? attrs
216 : Unowned(attrs);
217 }),
218
219 list: () =>
220 Effect.gen(function* () {
221 const env = yield* GcpEnvironment.current;
222 const parents = yield* listFolderParents();
223 const pages = yield* Effect.forEach(parents, listAt, {
224 concurrency: 4,
225 });
226 return pages.flatMap((contacts, index) =>
227 contacts
228 .filter((contact) => hasOwnershipMarker(contact.email))
229 .map((contact) =>
230 toAttrs(contact, parents[index] ?? "", env.project),
231 ),
232 );
233 }),
234
235 reconcile: Effect.fn(function* ({ id, news, output }) {
236 const env = yield* GcpEnvironment.current;
237 const parent = yield* resolveFolder(
238 news.folderId ?? output?.folderId,
239 output?.parent,
240 );
241 const ownership = yield* ownershipLabels(id);
242 const userEmail = yield* toEmail(id, news.email, output?.email);
243 const stampedEmail = encodeEmail(ownership, userEmail);
244 const languageTag = desiredLanguage(news.languageTag);
245 const categories = desiredCategories(
246 news.notificationCategorySubscriptions,
247 );
248 const body = toCreateBody({
249 email: stampedEmail,
250 languageTag,
251 notificationCategorySubscriptions: categories,
252 });
253
254 let current = yield* getByName(output?.name ?? "");
255 if (current === undefined) {
256 current = yield* findOwnedContact(
257 yield* listAt(parent),
258 id,
259 output?.name,
260 userEmail,
261 );
262 }
263
264 if (current === undefined) {
265 const created = yield* essentialcontacts
266 .createFoldersContacts({ parent, body })
267 .pipe(
268 Effect.catchTag("Conflict", () =>
269 listAt(parent).pipe(
270 Effect.flatMap((contacts) =>
271 findOwnedContact(contacts, id, undefined, userEmail),
272 ),
273 ),
274 ),
275 );
276 current = created ?? undefined;
277 if (current?.name) {
278 current = (yield* getByName(current.name)) ?? current;
279 }
280 }
281
282 if (current === undefined) {
283 return yield* new FolderContactNotResolved({
284 name: output?.name ?? `${parent}/contacts`,
285 });
286 }
287
288 const languageChanged = !sameText(current.languageTag, languageTag);
289 const categoriesChanged = !sameCategories(
290 current.notificationCategorySubscriptions,
291 categories,
292 );
293 if (languageChanged || categoriesChanged) {
294 current = yield* essentialcontacts.patchFoldersContacts({
295 name: current.name ?? "",
296 updateMask: UPDATE_MASK,
297 body: {
298 languageTag,
299 notificationCategorySubscriptions: categories,
300 },
301 });
302 }
303
304 return toAttrs(current, parent, env.project);
305 }),
306
307 delete: Effect.fn(function* ({ output }) {
308 if (!output.name) return;
309 yield* essentialcontacts
310 .deleteFoldersContacts({ name: output.name })
311 .pipe(Effect.catchTag("NotFound", () => Effect.void));
312 }),
313 });