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 parentOf,
22 parseOwnership,
23 sameText,
24 toResourceId,
25 updateMaskOf,
26} from "./ownership.ts";
27
28export type AuthorizedViewSetsAuthorizedViewProps = {
29 /**
30 * Parent AuthorizedViewSet resource name
31 * (`projects/{project}/locations/{location}/authorizedViewSets/{authorized_view_set}`).
32 * Immutable — changing it replaces the view.
33 */
34 parent: string;
35 /**
36 * AuthorizedView id (the `{authorized_view}` segment). If omitted, a
37 * unique id is generated. Must match
38 * `^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$`. Immutable — changing it replaces
39 * the view.
40 */
41 authorizedViewId?: string;
42 /**
43 * Display name (max 64 characters including Alchemy's ownership
44 * marker, which takes at least 55). AuthorizedViews have no labels
45 * field, so ownership is stored in a `[alchemy …]` prefix and stripped
46 * from attributes; longer names are truncated.
47 */
48 displayName?: string;
49 /**
50 * Filter restricting which conversations this view can access. Empty
51 * means no restriction.
52 */
53 conversationFilter?: string;
54};
55
56export type AuthorizedViewSetsAuthorizedView = Resource<
57 "GCP.ContactCenterInsights.AuthorizedViewSetsAuthorizedView",
58 AuthorizedViewSetsAuthorizedViewProps,
59 {
60 /** Full resource name `projects/{project}/locations/{location}/authorizedViewSets/{set}/authorizedViews/{view}`. */
61 name: string;
62 /** AuthorizedView id (last path segment). */
63 authorizedViewId: string;
64 /** Parent AuthorizedViewSet resource name. */
65 parent: string;
66 /** Location id. */
67 location: string;
68 /** Project id. */
69 project: string;
70 /** User display name with the Alchemy ownership prefix stripped. */
71 displayName: string | undefined;
72 /** Conversation filter. */
73 conversationFilter: string | undefined;
74 /** RFC3339 creation timestamp. */
75 createTime: string | undefined;
76 /** RFC3339 last-update timestamp. */
77 updateTime: string | undefined;
78 },
79 never,
80 Providers
81>;
82
83/**
84 * A Contact Center AI Insights AuthorizedView, a filtered view of
85 * conversations and related Insights resources.
86 *
87 * AuthorizedViews have no labels field — Alchemy stamps ownership into
88 * the display name. Parent set and id are immutable. Display name and
89 * conversation filter update in place.
90 *
91 * ### Creating an AuthorizedView
92 * **Example:** View under a set
93 * ```typescript
94 * const view = yield* GCP.ContactCenterInsights.AuthorizedViewSetsAuthorizedView(
95 * "Reviewers",
96 * {
97 * parent: set.name,
98 * displayName: "reviewers",
99 * conversationFilter: 'language_code="en-US"',
100 * },
101 * );
102 * ```
103 *
104 * @resource
105 * @category ContactCenterInsights
106 */
107export const AuthorizedViewSetsAuthorizedView =
108 Resource<AuthorizedViewSetsAuthorizedView>(
109 "GCP.ContactCenterInsights.AuthorizedViewSetsAuthorizedView",
110 );
111
112export class AuthorizedViewSetsAuthorizedViewNotResolved extends Data.TaggedError(
113 "GCP.ContactCenterInsights.AuthorizedViewSetsAuthorizedViewNotResolved",
114)<{
115 name: string;
116}> {}
117
118const resourceName = (parent: string, authorizedViewId: string) =>
119 `${parent}/authorizedViews/${authorizedViewId}`;
120
121const toAttrs = (
122 view: cci.GoogleCloudContactcenterinsightsV1AuthorizedView,
123 project: string,
124) => {
125 const name = view.name ?? "";
126 const parsed = parseOwnership(view.displayName);
127 return {
128 name,
129 authorizedViewId: lastSegment(name),
130 parent: parentOf(name),
131 location: locationOf(name),
132 project,
133 displayName: parsed.text,
134 conversationFilter: view.conversationFilter,
135 createTime: view.createTime,
136 updateTime: view.updateTime,
137 };
138};
139
140const getByName = (name: string) =>
141 name.length === 0
142 ? Effect.succeed(undefined)
143 : cci
144 .getProjectsLocationsAuthorizedViewSetsAuthorizedViews({ name })
145 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
146
147const listAtParent = (parent: string, project: string) =>
148 cci.listProjectsLocationsAuthorizedViewSetsAuthorizedViews
149 .pages({ parent, pageSize: 100 })
150 .pipe(
151 Stream.flatMap((page) => Stream.fromIterable(page.authorizedViews ?? [])),
152 Stream.filter((view) => hasOwnershipMarker(view.displayName)),
153 Stream.map((view) => toAttrs(view, project)),
154 Stream.runCollect,
155 Effect.map((chunk) => Array.from(chunk)),
156 Effect.catchTag("NotFound", () => Effect.succeed([])),
157 );
158
159export const AuthorizedViewSetsAuthorizedViewProvider = () =>
160 Provider.succeed(AuthorizedViewSetsAuthorizedView, {
161 stables: [
162 "name",
163 "authorizedViewId",
164 "parent",
165 "location",
166 "project",
167 "createTime",
168 ],
169
170 diff: Effect.fn(function* ({ news, olds, output }) {
171 if (!isResolved(news)) return undefined;
172 const previousParent = olds?.parent ?? output?.parent;
173 if (previousParent !== undefined && news.parent !== previousParent) {
174 return { action: "replace" as const, deleteFirst: false };
175 }
176 const previousId = olds?.authorizedViewId ?? output?.authorizedViewId;
177 if (
178 previousId !== undefined &&
179 news.authorizedViewId !== undefined &&
180 news.authorizedViewId !== previousId
181 ) {
182 return { action: "replace" as const, deleteFirst: false };
183 }
184 return undefined;
185 }),
186
187 read: Effect.fn(function* ({ id, olds, output }) {
188 const env = yield* GcpEnvironment.current;
189 const authorizedViewId = yield* toResourceId(
190 id,
191 olds?.authorizedViewId,
192 output?.authorizedViewId,
193 );
194 const name =
195 output?.name ??
196 (olds?.parent !== undefined
197 ? resourceName(olds.parent, authorizedViewId)
198 : "");
199 const existing = yield* getByName(name);
200 if (existing === undefined) return undefined;
201 const attrs = toAttrs(existing, env.project);
202 return (yield* ownedByAlchemy(id, existing.displayName))
203 ? attrs
204 : Unowned(attrs);
205 }),
206
207 list: () =>
208 Effect.gen(function* () {
209 const env = yield* GcpEnvironment.current;
210 const parent = `${locationParent(env.project, env.region)}/authorizedViewSets/-`;
211 return yield* listAtParent(parent, env.project);
212 }),
213
214 reconcile: Effect.fn(function* ({ id, news, output }) {
215 const env = yield* GcpEnvironment.current;
216 const authorizedViewId = yield* toResourceId(
217 id,
218 news.authorizedViewId,
219 output?.authorizedViewId,
220 );
221 const name = resourceName(news.parent, authorizedViewId);
222 const ownership = yield* createInternalLabels(id);
223 const displayName = encodeOwnershipLine(
224 ownership,
225 news.displayName,
226 MAX_VIEW_DISPLAY_NAME_LENGTH,
227 );
228
229 let current = yield* getByName(output?.name ?? name);
230
231 if (current === undefined) {
232 const created = yield* cci
233 .createProjectsLocationsAuthorizedViewSetsAuthorizedViews({
234 parent: news.parent,
235 authorizedViewId,
236 body: {
237 displayName,
238 conversationFilter: news.conversationFilter,
239 },
240 })
241 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
242 current = created ?? undefined;
243 }
244
245 if (current === undefined) {
246 return yield* new AuthorizedViewSetsAuthorizedViewNotResolved({
247 name,
248 });
249 }
250
251 const currentName = current.name ?? name;
252 const displayChanged = (current.displayName ?? "") !== displayName;
253 const filterChanged = !sameText(
254 current.conversationFilter,
255 news.conversationFilter,
256 );
257
258 if (displayChanged || filterChanged) {
259 current =
260 yield* cci.patchProjectsLocationsAuthorizedViewSetsAuthorizedViews({
261 name: currentName,
262 updateMask: updateMaskOf(
263 displayChanged ? "display_name" : undefined,
264 filterChanged ? "conversation_filter" : undefined,
265 ),
266 body: {
267 name: currentName,
268 displayName,
269 conversationFilter: news.conversationFilter,
270 },
271 });
272 }
273
274 return toAttrs(current, env.project);
275 }),
276
277 delete: Effect.fn(function* ({ output }) {
278 yield* cci
279 .deleteProjectsLocationsAuthorizedViewSetsAuthorizedViews({
280 name: output.name,
281 })
282 .pipe(Effect.catchTag("NotFound", () => Effect.void));
283 }),
284 });