1import * as discoveryengine from "@distilled.cloud/gcp/discoveryengine_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 parentBefore,
12 parseResourceName,
13 sameStringList,
14 toResourceId,
15} from "./internal.ts";
16
17export type CollectionsEnginesSessionProps = {
18 /**
19 * Parent Engine resource name
20 * `projects/{project}/locations/{location}/collections/{collection}/engines/{engine}`.
21 * Immutable — changing it replaces the session.
22 */
23 engine: string;
24 /**
25 * Session id. If omitted, a unique id is generated. Immutable —
26 * changing it replaces the session.
27 */
28 sessionId?: string;
29 /**
30 * User-facing display name.
31 */
32 displayName?: string;
33 /**
34 * User tracking id.
35 */
36 userPseudoId?: string;
37 /**
38 * Session state.
39 */
40 state?: discoveryengine.GoogleCloudDiscoveryengineV1SessionStateEnum;
41 /**
42 * Pin the session to the top of the session list.
43 * @default false
44 */
45 isPinned?: boolean;
46 /**
47 * Free-form session labels.
48 */
49 labels?: string[];
50};
51
52export type CollectionsEnginesSession = Resource<
53 "GCP.DiscoveryEngine.CollectionsEnginesSession",
54 CollectionsEnginesSessionProps,
55 {
56 /** Full resource name. */
57 name: string;
58 /** Session id (last path segment). */
59 sessionId: string;
60 /** Parent engine resource name. */
61 engine: string;
62 /** Project id. */
63 project: string;
64 /** Location id. */
65 location: string;
66 /** Collection id. */
67 collectionId: string;
68 /** Display name. */
69 displayName: string | undefined;
70 /** User tracking id. */
71 userPseudoId: string | undefined;
72 /** Session state. */
73 state: string | undefined;
74 /** Whether the session is pinned. */
75 isPinned: boolean;
76 /** Labels. */
77 labels: string[];
78 /** RFC3339 start timestamp. */
79 startTime: string | undefined;
80 /** RFC3339 end timestamp. */
81 endTime: string | undefined;
82 },
83 never,
84 Providers
85>;
86
87/**
88 * A Discovery Engine Session on a collection Engine.
89 *
90 * Without labels, ownership rests on the deterministic id: `read` reports a
91 * resource it finds without prior state as unowned (adopt it with `--adopt`).
92 * Parent engine and session id are immutable; display name, pin, and labels
93 * update in place.
94 *
95 * ### Creating a Session
96 * **Example:** Named session
97 * ```typescript
98 * const session = yield* GCP.DiscoveryEngine.CollectionsEnginesSession(
99 * "Chat",
100 * {
101 * engine: engine.name,
102 * displayName: "support",
103 * },
104 * );
105 * ```
106 *
107 * ### Updating a Session
108 * **Example:** Pin and rename
109 * ```typescript
110 * // Same logical id as before; only the changed props differ.
111 * const session = yield* GCP.DiscoveryEngine.CollectionsEnginesSession(
112 * "Chat",
113 * {
114 * engine: engine.name,
115 * displayName: "support-prod",
116 * isPinned: true,
117 * },
118 * );
119 * ```
120 *
121 * @resource
122 * @category DiscoveryEngine
123 */
124export const CollectionsEnginesSession = Resource<CollectionsEnginesSession>(
125 "GCP.DiscoveryEngine.CollectionsEnginesSession",
126);
127
128export class CollectionsEnginesSessionNotResolved extends Data.TaggedError(
129 "GCP.DiscoveryEngine.CollectionsEnginesSessionNotResolved",
130)<{
131 name: string;
132}> {}
133
134const resourceName = (engine: string, sessionId: string) =>
135 `${engine}/sessions/${sessionId}`;
136
137const getByName = (name: string) =>
138 discoveryengine
139 .getProjectsLocationsCollectionsEnginesSessions({ name })
140 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
141
142const userLabels = (labels: readonly string[] | undefined) => [
143 ...(labels ?? []),
144];
145
146const toAttrs = (
147 session: discoveryengine.GoogleCloudDiscoveryengineV1Session,
148 project: string,
149) => {
150 const name = session.name ?? "";
151 const parsed = parseResourceName(name, "sessions");
152 return {
153 name,
154 sessionId: parsed.id,
155 engine: parentBefore(name, "sessions"),
156 project: parsed.project || project,
157 location: parsed.location,
158 collectionId: parsed.collectionId,
159 displayName: session.displayName,
160 userPseudoId: session.userPseudoId,
161 state: session.state,
162 isPinned: session.isPinned === true,
163 labels: userLabels(session.labels),
164 startTime: session.startTime,
165 endTime: session.endTime,
166 };
167};
168
169export const CollectionsEnginesSessionProvider = () =>
170 Provider.succeed(CollectionsEnginesSession, {
171 stables: [
172 "name",
173 "sessionId",
174 "engine",
175 "project",
176 "location",
177 "collectionId",
178 "startTime",
179 ],
180
181 diff: Effect.fn(function* ({ news, olds, output }) {
182 if (!isResolved(news)) return undefined;
183 const previousEngine = olds?.engine ?? output?.engine;
184 const previousId = olds?.sessionId ?? output?.sessionId;
185 const nextId = news.sessionId ?? previousId;
186 if (
187 (previousEngine !== undefined && news.engine !== previousEngine) ||
188 (previousId !== undefined &&
189 nextId !== undefined &&
190 nextId !== previousId)
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 = olds?.engine ?? output?.engine;
200 const childId = yield* toResourceId(
201 id,
202 olds?.sessionId,
203 output?.sessionId,
204 );
205 const name =
206 output?.name ??
207 (parent !== undefined ? resourceName(parent, childId) : undefined);
208 if (name === undefined) return undefined;
209 const existing = yield* getByName(name);
210 if (existing === undefined) return undefined;
211 const attrs = toAttrs(existing, env.project);
212 // No labels field: without prior state it may not be ours.
213 return output !== undefined ? attrs : Unowned(attrs);
214 }),
215
216 reconcile: Effect.fn(function* ({ id, news, output }) {
217 const env = yield* GcpEnvironment.current;
218 const sessionId = yield* toResourceId(
219 id,
220 news.sessionId,
221 output?.sessionId,
222 );
223 const displayName = news.displayName;
224 const isPinned = news.isPinned === true;
225 const labels = news.labels;
226 const fallbackName = output?.name ?? resourceName(news.engine, sessionId);
227
228 let current = yield* getByName(fallbackName);
229
230 if (current === undefined) {
231 const created = yield* discoveryengine
232 .createProjectsLocationsCollectionsEnginesSessions({
233 parent: news.engine,
234 sessionId,
235 body: {
236 displayName,
237 userPseudoId: news.userPseudoId,
238 state: news.state,
239 isPinned: isPinned ? true : undefined,
240 labels,
241 },
242 })
243 .pipe(Effect.catchTag("Conflict", () => getByName(fallbackName)));
244 current = created ?? undefined;
245 }
246
247 if (current === undefined) {
248 return yield* new CollectionsEnginesSessionNotResolved({
249 name: fallbackName,
250 });
251 }
252
253 const name = current.name ?? fallbackName;
254 const displayNameChanged =
255 (current.displayName ?? "") !== (displayName ?? "");
256 const userChanged =
257 (current.userPseudoId ?? "") !== (news.userPseudoId ?? "");
258 const stateChanged = (current.state ?? "") !== (news.state ?? "");
259 const pinnedChanged = (current.isPinned === true) !== isPinned;
260 const labelsChanged = !sameStringList(userLabels(current.labels), labels);
261
262 if (
263 displayNameChanged ||
264 userChanged ||
265 stateChanged ||
266 pinnedChanged ||
267 labelsChanged
268 ) {
269 current =
270 yield* discoveryengine.patchProjectsLocationsCollectionsEnginesSessions(
271 {
272 name,
273 updateMask: [
274 displayNameChanged ? "display_name" : undefined,
275 userChanged ? "user_pseudo_id" : undefined,
276 stateChanged ? "state" : undefined,
277 pinnedChanged ? "is_pinned" : undefined,
278 labelsChanged ? "labels" : undefined,
279 ]
280 .filter((field): field is string => field !== undefined)
281 .join(","),
282 body: {
283 name,
284 displayName,
285 userPseudoId: news.userPseudoId,
286 state: news.state,
287 isPinned,
288 labels,
289 },
290 },
291 );
292 }
293
294 return toAttrs(current, env.project);
295 }),
296
297 delete: Effect.fn(function* ({ output }) {
298 const existing = yield* getByName(output.name);
299 if (existing === undefined) return;
300 yield* discoveryengine
301 .deleteProjectsLocationsCollectionsEnginesSessions({
302 name: output.name,
303 })
304 .pipe(Effect.catchTag("NotFound", () => Effect.void));
305 }),
306 });