1import * as jobs from "@distilled.cloud/gcp/jobs_v4";
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 companyNameOf,
12 deleteCompany,
13 encodeOwnershipLine,
14 findOwnedCompany,
15 getCompany,
16 listOwnedCompanies,
17 MAX_DISPLAY_NAME_LENGTH,
18 MAX_EXTERNAL_ID_LENGTH,
19 ownedByAlchemy,
20 ownershipLabels,
21 parseCompanyName,
22 parseOwnership,
23 replaceOnIdentity,
24 sameBoolean,
25 sameStringList,
26 sameText,
27 tenantNameOf,
28 toGeneratedName,
29 updateMaskOf,
30 waitUntilGone,
31} from "./internal.ts";
32
33export type TenantsCompanyProps = {
34 /**
35 * Parent tenant resource name `projects/{project}/tenants/{tenant}`
36 * or the tenant id. Immutable — changing it replaces the company.
37 */
38 parent: string;
39 /**
40 * Server-assigned company id. Leave blank on create. Immutable —
41 * changing it replaces the company.
42 */
43 companyId?: string;
44 /**
45 * Client-side company identifier (max 255 characters). Ownership is
46 * also stamped here for `list` / nuke.
47 */
48 externalId?: string;
49 /**
50 * Display name shown to job seekers, for example `"Google LLC"`.
51 */
52 displayName?: string;
53 /**
54 * Primary website URI, for example `"https://www.google.com"`.
55 */
56 websiteUri?: string;
57 /**
58 * Careers page URI, for example `"https://careers.google.com"`.
59 */
60 careerSiteUri?: string;
61 /**
62 * URI that hosts the employer logo.
63 */
64 imageUri?: string;
65 /**
66 * Street address of the company headquarters.
67 */
68 headquartersAddress?: string;
69 /**
70 * Employer company size.
71 */
72 size?: jobs.CompanySizeEnum | (string & {});
73 /**
74 * Whether this company posts jobs for other employers.
75 * @default false
76 */
77 hiringAgency?: boolean;
78 /**
79 * Equal Employment Opportunity legal disclaimer (max 500 characters).
80 */
81 eeoText?: string;
82 /**
83 * Deprecated. Filterable custom attribute keys used in keyword search.
84 */
85 keywordSearchableJobCustomAttributes?: string[];
86};
87
88export type TenantsCompany = Resource<
89 "GCP.Jobs.TenantsCompany",
90 TenantsCompanyProps,
91 {
92 /**
93 * Full resource name
94 * `projects/{project}/tenants/{tenant}/companies/{company}`.
95 */
96 name: string;
97 /** Server-assigned company id. */
98 companyId: string;
99 /** Parent tenant resource name. */
100 parent: string;
101 /** Project id. */
102 project: string;
103 /** Client identifier with the Alchemy prefix stripped. */
104 externalId: string | undefined;
105 /** Display name with the Alchemy prefix stripped. */
106 displayName: string | undefined;
107 /** Primary website URI. */
108 websiteUri: string | undefined;
109 /** Careers page URI. */
110 careerSiteUri: string | undefined;
111 /** Logo URI. */
112 imageUri: string | undefined;
113 /** Headquarters street address. */
114 headquartersAddress: string | undefined;
115 /** Employer company size. */
116 size: string | undefined;
117 /** Whether this company is a hiring agency. */
118 hiringAgency: boolean;
119 /** EEO disclaimer text. */
120 eeoText: string | undefined;
121 /** Whether the company is suspended from public availability. */
122 suspended: boolean | undefined;
123 /** Derived headquarters location. */
124 derivedInfo: jobs.CompanyDerivedInfo | undefined;
125 },
126 never,
127 Providers
128>;
129
130/**
131 * A Cloud Talent Solution company — the hiring entity that owns job
132 * postings.
133 *
134 * Companies have no labels field, so Alchemy stamps ownership into the
135 * client-side `externalId` for `list` / nuke (never into the
136 * seeker-visible display name). Parent tenant and
137 * company id are identity — changing either replaces the company.
138 * Display name, URIs, size, and related fields update in place.
139 *
140 * ### Creating a Company
141 * **Example:** Company under a tenant
142 * ```typescript
143 * const tenant = yield* GCP.Jobs.Tenant("Acme", {});
144 * const company = yield* GCP.Jobs.TenantsCompany("Labs", {
145 * parent: tenant.name,
146 * displayName: "Acme Labs",
147 * websiteUri: "https://www.example.com",
148 * });
149 * ```
150 *
151 * ### Updating a Company
152 * Re-declare the same logical id with changed props; the engine keeps the
153 * physical resource and updates it in place.
154 *
155 * **Example:** Rename and set headquarters
156 * ```typescript
157 * const company = yield* GCP.Jobs.TenantsCompany("Labs", {
158 * parent: tenant.name,
159 * displayName: "Acme Research",
160 * headquartersAddress: "Mountain View, CA",
161 * });
162 * ```
163 *
164 * @resource
165 * @category Jobs
166 */
167export const TenantsCompany = Resource<TenantsCompany>(
168 "GCP.Jobs.TenantsCompany",
169);
170
171export class TenantsCompanyNotResolved extends Data.TaggedError(
172 "GCP.Jobs.TenantsCompanyNotResolved",
173)<{
174 name: string;
175}> {}
176
177const toParent = (project: string, parent: string) =>
178 tenantNameOf(project, parent);
179
180const toAttrs = (company: jobs.Company, project: string) => {
181 const name = company.name ?? "";
182 const parsed = parseCompanyName(name, project);
183 return {
184 name,
185 companyId: parsed.companyId,
186 parent: parsed.parent,
187 project: parsed.project || project,
188 externalId: parseOwnership(company.externalId).text,
189 displayName: parseOwnership(company.displayName).text,
190 websiteUri: company.websiteUri,
191 careerSiteUri: company.careerSiteUri,
192 imageUri: company.imageUri,
193 headquartersAddress: company.headquartersAddress,
194 size: company.size,
195 hiringAgency: company.hiringAgency === true,
196 eeoText: company.eeoText,
197 suspended: company.suspended,
198 derivedInfo: company.derivedInfo,
199 };
200};
201
202export const TenantsCompanyProvider = () =>
203 Provider.succeed(TenantsCompany, {
204 stables: ["name", "companyId", "parent", "project"],
205
206 diff: Effect.fn(function* ({ news, olds, output }) {
207 if (!isResolved(news)) return undefined;
208 const env = yield* GcpEnvironment.current;
209 return replaceOnIdentity({
210 previousId: olds?.companyId ?? output?.companyId,
211 nextId: news.companyId,
212 previousParent: olds?.parent ?? output?.parent,
213 nextParent: toParent(env.project, news.parent),
214 });
215 }),
216
217 read: Effect.fn(function* ({ id, olds, output }) {
218 const env = yield* GcpEnvironment.current;
219 const parent = toParent(
220 env.project,
221 olds?.parent ?? output?.parent ?? "",
222 );
223 const name =
224 output?.name ??
225 companyNameOf(parent, olds?.companyId ?? output?.companyId ?? "");
226 let existing = yield* getCompany(name);
227 if (existing === undefined) {
228 existing = yield* findOwnedCompany(parent, id);
229 }
230 if (existing === undefined) return undefined;
231 const attrs = toAttrs(existing, env.project);
232 const owned =
233 (yield* ownedByAlchemy(id, existing.displayName)) ||
234 (yield* ownedByAlchemy(id, existing.externalId));
235 return owned ? attrs : Unowned(attrs);
236 }),
237
238 list: () =>
239 Effect.gen(function* () {
240 const env = yield* GcpEnvironment.current;
241 const companies = yield* listOwnedCompanies(env.project);
242 return companies.map((company) => toAttrs(company, env.project));
243 }),
244
245 reconcile: Effect.fn(function* ({ id, news, output }) {
246 const env = yield* GcpEnvironment.current;
247 const parent = toParent(env.project, news.parent);
248 const ownership = yield* ownershipLabels(id);
249 const clientId = yield* toGeneratedName(
250 id,
251 news.externalId,
252 output?.externalId,
253 );
254 const display = yield* toGeneratedName(
255 id,
256 news.displayName,
257 output?.displayName,
258 );
259 const externalId = encodeOwnershipLine(
260 ownership,
261 clientId,
262 MAX_EXTERNAL_ID_LENGTH,
263 );
264 // Shown to job seekers, so no ownership marker (externalId has it).
265 const displayName = display.slice(0, MAX_DISPLAY_NAME_LENGTH);
266 const name = output?.name ?? companyNameOf(parent, news.companyId ?? "");
267
268 let current = yield* getCompany(name);
269 if (current === undefined) {
270 current = yield* findOwnedCompany(parent, id);
271 }
272
273 if (current === undefined) {
274 const created = yield* jobs
275 .createProjectsTenantsCompanies({
276 parent,
277 body: {
278 externalId,
279 displayName,
280 websiteUri: news.websiteUri,
281 careerSiteUri: news.careerSiteUri,
282 imageUri: news.imageUri,
283 headquartersAddress: news.headquartersAddress,
284 size: news.size,
285 hiringAgency: news.hiringAgency === true ? true : undefined,
286 eeoText: news.eeoText,
287 keywordSearchableJobCustomAttributes:
288 news.keywordSearchableJobCustomAttributes,
289 },
290 })
291 .pipe(
292 Effect.catchTag("Conflict", () => findOwnedCompany(parent, id)),
293 );
294 current = created ?? undefined;
295 }
296
297 if (current === undefined) {
298 return yield* new TenantsCompanyNotResolved({
299 name: name || `${parent}/companies/${clientId}`,
300 });
301 }
302
303 const currentName = current.name ?? name;
304 const desiredHiring = news.hiringAgency === true;
305 const optionalText = (
306 currentValue: string | undefined,
307 next: string | undefined,
308 field: string,
309 ) =>
310 next === undefined || sameText(currentValue, next) ? undefined : field;
311 const updateMask = updateMaskOf(
312 sameText(current.externalId, externalId) ? undefined : "externalId",
313 sameText(current.displayName, displayName) ? undefined : "displayName",
314 optionalText(current.websiteUri, news.websiteUri, "websiteUri"),
315 optionalText(
316 current.careerSiteUri,
317 news.careerSiteUri,
318 "careerSiteUri",
319 ),
320 optionalText(current.imageUri, news.imageUri, "imageUri"),
321 optionalText(
322 current.headquartersAddress,
323 news.headquartersAddress,
324 "headquartersAddress",
325 ),
326 optionalText(current.size, news.size, "size"),
327 news.hiringAgency !== undefined &&
328 !sameBoolean(current.hiringAgency, desiredHiring)
329 ? "hiringAgency"
330 : undefined,
331 optionalText(current.eeoText, news.eeoText, "eeoText"),
332 news.keywordSearchableJobCustomAttributes !== undefined &&
333 !sameStringList(
334 current.keywordSearchableJobCustomAttributes,
335 news.keywordSearchableJobCustomAttributes,
336 )
337 ? "keywordSearchableJobCustomAttributes"
338 : undefined,
339 );
340
341 if (updateMask.length > 0) {
342 current = yield* jobs.patchProjectsTenantsCompanies({
343 name: currentName,
344 updateMask,
345 body: {
346 name: currentName,
347 externalId,
348 displayName,
349 websiteUri: news.websiteUri ?? "",
350 careerSiteUri: news.careerSiteUri ?? "",
351 imageUri: news.imageUri ?? "",
352 headquartersAddress: news.headquartersAddress ?? "",
353 size: news.size,
354 hiringAgency: desiredHiring,
355 eeoText: news.eeoText ?? "",
356 keywordSearchableJobCustomAttributes:
357 news.keywordSearchableJobCustomAttributes ?? [],
358 },
359 });
360 }
361
362 return toAttrs(current, env.project);
363 }),
364
365 delete: Effect.fn(function* ({ output }) {
366 if (!output.name) return;
367 yield* deleteCompany(output.name);
368 yield* waitUntilGone(getCompany(output.name));
369 }),
370 });