1import * as cci from "@distilled.cloud/gcp/contactcenterinsights_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { GcpEnvironment } from "../Environment.ts";
10import { createInternalLabels } from "../Labels.ts";
11import type { Providers } from "../Providers.ts";
12import {
13 encodeOwnershipLine,
14 hasOwnershipMarker,
15 lastSegment,
16 locationOf,
17 locationParent,
18 MAX_VIEW_DISPLAY_NAME_LENGTH,
19 normalizeLocation,
20 ownedByAlchemy,
21 parseOwnership,
22 toResourceId,
23} from "./ownership.ts";
24
25export type AuthorizedViewSetProps = {
26 /**
27 * AuthorizedViewSet id (the `{authorized_view_set}` segment of
28 * `projects/{project}/locations/{location}/authorizedViewSets/{authorized_view_set}`).
29 * If omitted, a unique id is generated. Must match
30 * `^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$`. Immutable — changing it replaces
31 * the set.
32 */
33 authorizedViewSetId?: string;
34 /**
35 * Location (`us-central1`, …). Immutable — changing it replaces the set.
36 * `US-CENTRAL1` is accepted and normalized to `us-central1`.
37 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
38 */
39 location?: string;
40 /**
41 * Display name (max 64 characters including Alchemy's ownership
42 * marker). AuthorizedViewSets have no labels field, so ownership is
43 * stored in a `[alchemy …]` prefix and stripped from attributes.
44 */
45 displayName?: string;
46};
47
48export type AuthorizedViewSet = Resource<
49 "GCP.ContactCenterInsights.AuthorizedViewSet",
50 AuthorizedViewSetProps,
51 {
52 /** Full resource name `projects/{project}/locations/{location}/authorizedViewSets/{authorized_view_set}`. */
53 name: string;
54 /** AuthorizedViewSet id (last path segment). */
55 authorizedViewSetId: string;
56 /** Location id. */
57 location: string;
58 /** Project id. */
59 project: string;
60 /** User display name with the Alchemy ownership prefix stripped. */
61 displayName: string | undefined;
62 /** RFC3339 creation timestamp. */
63 createTime: string | undefined;
64 /** RFC3339 last-update timestamp. */
65 updateTime: string | undefined;
66 },
67 never,
68 Providers
69>;
70
71/**
72 * A Contact Center AI Insights AuthorizedViewSet, a container for
73 * AuthorizedView resources.
74 *
75 * AuthorizedViewSets have no labels field — Alchemy stamps ownership into
76 * the display name. Location and id are immutable. Display name updates
77 * in place.
78 *
79 * ### Creating an AuthorizedViewSet
80 * **Example:** Generated id
81 * ```typescript
82 * const set = yield* GCP.ContactCenterInsights.AuthorizedViewSet("QaViews", {
83 * displayName: "qa-reviewers",
84 * });
85 * ```
86 *
87 * **Example:** Named set
88 * ```typescript
89 * const set = yield* GCP.ContactCenterInsights.AuthorizedViewSet("QaViews", {
90 * authorizedViewSetId: "qa-reviewers",
91 * displayName: "qa-reviewers",
92 * });
93 * ```
94 *
95 * @resource
96 * @category ContactCenterInsights
97 */
98export const AuthorizedViewSet = Resource<AuthorizedViewSet>(
99 "GCP.ContactCenterInsights.AuthorizedViewSet",
100);
101
102export class AuthorizedViewSetNotResolved extends Data.TaggedError(
103 "GCP.ContactCenterInsights.AuthorizedViewSetNotResolved",
104)<{
105 name: string;
106}> {}
107
108const resourceName = (
109 project: string,
110 location: string,
111 authorizedViewSetId: string,
112) =>
113 `${locationParent(project, location)}/authorizedViewSets/${authorizedViewSetId}`;
114
115const toAttrs = (
116 set: cci.GoogleCloudContactcenterinsightsV1AuthorizedViewSet,
117 project: string,
118) => {
119 const name = set.name ?? "";
120 const parsed = parseOwnership(set.displayName);
121 return {
122 name,
123 authorizedViewSetId: lastSegment(name),
124 location: locationOf(name),
125 project,
126 displayName: parsed.text,
127 createTime: set.createTime,
128 updateTime: set.updateTime,
129 };
130};
131
132const getByName = (name: string) =>
133 name.length === 0
134 ? Effect.succeed(undefined)
135 : cci
136 .getProjectsLocationsAuthorizedViewSets({ name })
137 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
138
139const listAt = (parent: string, project: string) =>
140 cci.listProjectsLocationsAuthorizedViewSets
141 .pages({ parent, pageSize: 100 })
142 .pipe(
143 Stream.flatMap((page) =>
144 Stream.fromIterable(page.authorizedViewSets ?? []),
145 ),
146 Stream.filter((set) => hasOwnershipMarker(set.displayName)),
147 Stream.map((set) => toAttrs(set, project)),
148 Stream.runCollect,
149 Effect.map((chunk) => Array.from(chunk)),
150 Effect.catchTag("NotFound", () => Effect.succeed([])),
151 );
152
153export const AuthorizedViewSetProvider = () =>
154 Provider.succeed(AuthorizedViewSet, {
155 stables: [
156 "name",
157 "authorizedViewSetId",
158 "location",
159 "project",
160 "createTime",
161 ],
162
163 diff: Effect.fn(function* ({ news, olds, output }) {
164 if (!isResolved(news)) return undefined;
165 const previousLocation = olds?.location ?? output?.location;
166 if (
167 previousLocation !== undefined &&
168 news.location !== undefined &&
169 normalizeLocation(previousLocation) !== normalizeLocation(news.location)
170 ) {
171 return { action: "replace" as const, deleteFirst: false };
172 }
173 const previousId =
174 olds?.authorizedViewSetId ?? output?.authorizedViewSetId;
175 if (
176 previousId !== undefined &&
177 news.authorizedViewSetId !== undefined &&
178 news.authorizedViewSetId !== previousId
179 ) {
180 return { action: "replace" as const, deleteFirst: false };
181 }
182 return undefined;
183 }),
184
185 read: Effect.fn(function* ({ id, olds, output }) {
186 const env = yield* GcpEnvironment.current;
187 const authorizedViewSetId = yield* toResourceId(
188 id,
189 olds?.authorizedViewSetId,
190 output?.authorizedViewSetId,
191 );
192 const location = normalizeLocation(
193 olds?.location ?? output?.location ?? env.region,
194 );
195 const name =
196 output?.name ??
197 resourceName(env.project, location, authorizedViewSetId);
198 const existing = yield* getByName(name);
199 if (existing === undefined) return undefined;
200 const attrs = toAttrs(existing, env.project);
201 return (yield* ownedByAlchemy(id, existing.displayName))
202 ? attrs
203 : Unowned(attrs);
204 }),
205
206 list: () =>
207 Effect.gen(function* () {
208 const env = yield* GcpEnvironment.current;
209 return yield* listAt(
210 locationParent(env.project, env.region),
211 env.project,
212 );
213 }),
214
215 reconcile: Effect.fn(function* ({ id, news, output }) {
216 const env = yield* GcpEnvironment.current;
217 const location = normalizeLocation(
218 news.location ?? output?.location ?? env.region,
219 );
220 const parent = locationParent(env.project, location);
221 const authorizedViewSetId = yield* toResourceId(
222 id,
223 news.authorizedViewSetId,
224 output?.authorizedViewSetId,
225 );
226 const name = resourceName(env.project, location, authorizedViewSetId);
227 const ownership = yield* createInternalLabels(id);
228 const displayName = encodeOwnershipLine(
229 ownership,
230 news.displayName,
231 MAX_VIEW_DISPLAY_NAME_LENGTH,
232 );
233
234 let current = yield* getByName(output?.name ?? name);
235
236 if (current === undefined) {
237 const created = yield* cci
238 .createProjectsLocationsAuthorizedViewSets({
239 parent,
240 authorizedViewSetId,
241 body: { displayName },
242 })
243 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
244 current = created ?? undefined;
245 }
246
247 if (current === undefined) {
248 return yield* new AuthorizedViewSetNotResolved({ name });
249 }
250
251 const currentName = current.name ?? name;
252 if ((current.displayName ?? "") !== displayName) {
253 current = yield* cci.patchProjectsLocationsAuthorizedViewSets({
254 name: currentName,
255 updateMask: "display_name",
256 body: { name: currentName, displayName },
257 });
258 }
259
260 return toAttrs(current, env.project);
261 }),
262
263 delete: Effect.fn(function* ({ output }) {
264 yield* cci
265 .deleteProjectsLocationsAuthorizedViewSets({
266 name: output.name,
267 force: true,
268 })
269 .pipe(Effect.catchTag("NotFound", () => Effect.void));
270 }),
271 });