1import * as gmailpostmastertools from "@distilled.cloud/gcp/gmailpostmastertools_v2";
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 DEFAULT_PERMISSION,
12 findUser,
13 fullUserName,
14 ignoreMissing,
15 isAlchemyEmail,
16 isPatchablePermission,
17 listOwnedUsers,
18 normalize,
19 parentOfUserName,
20 sameText,
21 toDomainName,
22 toGeneratedUserId,
23 toUserName,
24 userEmailOf,
25} from "./internal.ts";
26
27export type DomainsUserProps = {
28 /**
29 * Parent domain (`domains/{domain}` or the FQDN). Immutable —
30 * changing it replaces the user.
31 */
32 parent: string;
33 /**
34 * User email. If omitted, a unique `alchemy-*@example.com` address is
35 * generated. Immutable — changing it replaces the user. Users have no
36 * labels field, so Alchemy stamps ownership into the generated local
37 * part for `list` / nuke.
38 */
39 userId?: string;
40 /**
41 * Permission on the parent domain. Updates in place for `READER` and
42 * `ADMIN`. `OWNER` and `NONE` cannot be patched.
43 * @default "READER"
44 */
45 permission?: gmailpostmastertools.UserPermissionEnum | (string & {});
46};
47
48export type DomainsUser = Resource<
49 "GCP.GmailPostmasterTools.DomainsUser",
50 DomainsUserProps,
51 {
52 /** Full resource name `domains/{domain}/users/{user}`. */
53 name: string;
54 /** Parent domain name `domains/{domain}`. */
55 parent: string;
56 /** User email. */
57 userId: string;
58 /** Project id used when the user was reconciled. */
59 project: string;
60 /** Permission on the parent domain. */
61 permission: string | undefined;
62 /** RFC3339 time access was granted. */
63 createTime: string | undefined;
64 /** Email of the user that granted access. */
65 accessGranter: string | undefined;
66 },
67 never,
68 Providers
69>;
70
71/**
72 * A Gmail Postmaster Tools domain user.
73 *
74 * Users have no labels or description, so identity is `(parent, userId)`
75 * and `list` / nuke returns rows whose email local-part starts with
76 * `alchemy-`. Permission updates in place (`READER` / `ADMIN`). Changing
77 * parent or email replaces the user.
78 *
79 * ### Creating a User
80 * **Example:** Generated email
81 * ```typescript
82 * const user = yield* GCP.GmailPostmasterTools.DomainsUser("Ada", {
83 * parent: domain.name,
84 * });
85 * ```
86 *
87 * **Example:** Explicit email and permission
88 * ```typescript
89 * const user = yield* GCP.GmailPostmasterTools.DomainsUser("Ada", {
90 * parent: domain.name,
91 * userId: "ada@example.com",
92 * permission: "ADMIN",
93 * });
94 * ```
95 *
96 * ### Updating a User
97 * Change props on the same logical id; the engine keeps the physical id.
98 *
99 * **Example:** Change permission
100 * ```typescript
101 * const user = yield* GCP.GmailPostmasterTools.DomainsUser("Ada", {
102 * parent: domain.name,
103 * userId: "ada@example.com",
104 * permission: "READER",
105 * });
106 * ```
107 *
108 * @resource
109 * @category Gmail Postmaster Tools
110 */
111export const DomainsUser = Resource<DomainsUser>(
112 "GCP.GmailPostmasterTools.DomainsUser",
113);
114
115export class DomainsUserNotResolved extends Data.TaggedError(
116 "GCP.GmailPostmasterTools.DomainsUserNotResolved",
117)<{
118 name: string;
119}> {}
120
121const toAttrs = (
122 user: gmailpostmastertools.User,
123 parent: string,
124 project: string,
125) => {
126 const resolvedParent =
127 parentOfUserName(user.name ?? "") || toDomainName(parent);
128 const name = fullUserName(resolvedParent, user);
129 return {
130 name,
131 parent: resolvedParent,
132 userId: userEmailOf(user),
133 project,
134 permission: user.permission,
135 createTime: user.createTime,
136 accessGranter: user.accessGranter,
137 };
138};
139
140export const DomainsUserProvider = () =>
141 Provider.succeed(DomainsUser, {
142 stables: ["name", "parent", "userId", "project", "createTime"],
143
144 diff: Effect.fn(function* ({ news, olds, output }) {
145 if (!isResolved(news)) return undefined;
146 const previousParent = olds?.parent ?? output?.parent;
147 if (
148 previousParent !== undefined &&
149 toDomainName(news.parent) !== toDomainName(previousParent)
150 ) {
151 return { action: "replace" as const, deleteFirst: true };
152 }
153 const previousUser = olds?.userId ?? output?.userId;
154 if (
155 previousUser !== undefined &&
156 news.userId !== undefined &&
157 normalize(news.userId) !== normalize(previousUser)
158 ) {
159 return { action: "replace" as const, deleteFirst: true };
160 }
161 return undefined;
162 }),
163
164 read: Effect.fn(function* ({ olds, output }) {
165 const env = yield* GcpEnvironment.current;
166 const parent = olds?.parent ?? output?.parent ?? "";
167 const userId = olds?.userId ?? output?.userId ?? "";
168 const existing = yield* findUser(
169 parent,
170 userId,
171 output?.name ?? toUserName(parent, userId),
172 );
173 if (existing === undefined) return undefined;
174 const attrs = toAttrs(existing, parent, env.project);
175 return output !== undefined || isAlchemyEmail(attrs.userId)
176 ? attrs
177 : Unowned(attrs);
178 }),
179
180 list: () =>
181 Effect.gen(function* () {
182 const env = yield* GcpEnvironment.current;
183 const users = yield* listOwnedUsers();
184 return users.map((user) =>
185 toAttrs(user, parentOfUserName(user.name ?? "") || "", env.project),
186 );
187 }),
188
189 reconcile: Effect.fn(function* ({ id, news, output }) {
190 const env = yield* GcpEnvironment.current;
191 const parent = toDomainName(news.parent);
192 const userId = yield* toGeneratedUserId(id, news.userId, output?.userId);
193 const name = toUserName(parent, userId);
194 const permission = news.permission ?? DEFAULT_PERMISSION;
195
196 let current = yield* findUser(parent, userId, output?.name ?? name);
197
198 if (current === undefined) {
199 const created = yield* gmailpostmastertools
200 .createDomainsUsers({
201 parent,
202 body: { userId, permission },
203 })
204 .pipe(
205 Effect.catchTag("Conflict", () => findUser(parent, userId, name)),
206 );
207 current = created ?? undefined;
208 }
209
210 if (current === undefined) {
211 return yield* new DomainsUserNotResolved({ name });
212 }
213
214 const resourceName = fullUserName(parent, current);
215 if (
216 isPatchablePermission(permission) &&
217 !sameText(current.permission, permission)
218 ) {
219 current = yield* gmailpostmastertools.patchDomainsUsers({
220 name: resourceName,
221 updateMask: "permission",
222 body: { permission },
223 });
224 }
225
226 const fresh = (yield* findUser(parent, userId, resourceName)) ?? current;
227 return toAttrs(fresh, parent, env.project);
228 }),
229
230 delete: Effect.fn(function* ({ output }) {
231 const name = output.name || toUserName(output.parent, output.userId);
232 if (name.length === 0) return;
233 yield* ignoreMissing(gmailpostmastertools.deleteDomainsUsers({ name }));
234 }),
235 });