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 parentOf,
12 parseResourceName,
13 sessionIdOf,
14 toPhysical,
15} from "./internal.ts";
16
17export type CollectionsDataStoresSessionProps = {
18 /**
19 * Parent data store resource name. Immutable — changing it replaces
20 * the session.
21 */
22 dataStore: string;
23 /**
24 * Session id (`[a-z0-9]`, 1-63 characters). If omitted, a unique id is
25 * generated. Immutable — changing it replaces the session.
26 */
27 sessionId?: string;
28 /**
29 * Display name used in the UI (max 128 characters).
30 */
31 displayName?: string;
32 /**
33 * Session state.
34 * @default "IN_PROGRESS"
35 */
36 state?: "STATE_UNSPECIFIED" | "IN_PROGRESS" | (string & {});
37 /**
38 * End-user id.
39 */
40 userPseudoId?: string;
41 /**
42 * Session labels.
43 */
44 labels?: string[];
45 /**
46 * Pin the session to the top of the session list.
47 * @default false
48 */
49 isPinned?: boolean;
50};
51
52export type CollectionsDataStoresSession = Resource<
53 "GCP.DiscoveryEngine.CollectionsDataStoresSession",
54 CollectionsDataStoresSessionProps,
55 {
56 /** Full resource name. */
57 name: string;
58 /** Session id. */
59 sessionId: string;
60 /** Parent data store resource name. */
61 dataStore: string;
62 /** Project id. */
63 project: string;
64 /** Location id. */
65 location: string;
66 /** Display name. */
67 displayName: string | undefined;
68 /** Session state. */
69 state: string | undefined;
70 /** End-user id. */
71 userPseudoId: string | undefined;
72 /** Labels. */
73 labels: string[];
74 /** Whether the session is pinned. */
75 isPinned: boolean;
76 /** RFC3339 start time. */
77 startTime: string | undefined;
78 /** RFC3339 end time. */
79 endTime: string | undefined;
80 },
81 never,
82 Providers
83>;
84
85/**
86 * A Discovery Engine session on a collection data store.
87 *
88 * Without labels, ownership rests on the deterministic id: `read` reports a
89 * resource it finds without prior state as unowned (adopt it with `--adopt`).
90 * Parent and session id are immutable.
91 *
92 * ### Creating a Session
93 * **Example:** Pinned session
94 * ```typescript
95 * const store = yield* GCP.DiscoveryEngine.CollectionsDataStore("Docs", {});
96 * const session = yield* GCP.DiscoveryEngine.CollectionsDataStoresSession(
97 * "Chat",
98 * {
99 * dataStore: store.name,
100 * displayName: "support chat",
101 * isPinned: true,
102 * },
103 * );
104 * ```
105 *
106 * @resource
107 * @category DiscoveryEngine
108 */
109export const CollectionsDataStoresSession =
110 Resource<CollectionsDataStoresSession>(
111 "GCP.DiscoveryEngine.CollectionsDataStoresSession",
112 );
113
114export class CollectionsDataStoresSessionNotResolved extends Data.TaggedError(
115 "GCP.DiscoveryEngine.CollectionsDataStoresSessionNotResolved",
116)<{
117 name: string;
118}> {}
119
120const toAttrs = (
121 session: discoveryengine.GoogleCloudDiscoveryengineV1Session,
122 project: string,
123) => {
124 const name = session.name ?? "";
125 const parsed = parseResourceName(name, "sessions");
126 return {
127 name,
128 sessionId: parsed.id,
129 dataStore: parentOf(name, "sessions"),
130 project: parsed.project || project,
131 location: parsed.location,
132 displayName: session.displayName,
133 state: session.state,
134 userPseudoId: session.userPseudoId,
135 labels: [...(session.labels ?? [])],
136 isPinned: session.isPinned === true,
137 startTime: session.startTime,
138 endTime: session.endTime,
139 };
140};
141
142const resourceName = (dataStore: string, sessionId: string) =>
143 `${dataStore}/sessions/${sessionId}`;
144
145const getByName = (name: string) =>
146 name.length === 0
147 ? Effect.succeed(undefined)
148 : discoveryengine
149 .getProjectsLocationsCollectionsDataStoresSessions({ name })
150 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
151
152export const CollectionsDataStoresSessionProvider = () =>
153 Provider.succeed(CollectionsDataStoresSession, {
154 stables: [
155 "name",
156 "sessionId",
157 "dataStore",
158 "project",
159 "location",
160 "startTime",
161 ],
162
163 diff: Effect.fn(function* ({ news, olds, output }) {
164 if (!isResolved(news)) return undefined;
165 const previousParent = olds?.dataStore ?? output?.dataStore;
166 const previousId = olds?.sessionId ?? output?.sessionId;
167 if (
168 (previousParent !== undefined && news.dataStore !== previousParent) ||
169 (previousId !== undefined &&
170 news.sessionId !== undefined &&
171 news.sessionId !== previousId)
172 ) {
173 return {
174 action: "replace" as const,
175 deleteFirst:
176 previousParent === news.dataStore &&
177 previousId !== undefined &&
178 news.sessionId === previousId,
179 };
180 }
181 return undefined;
182 }),
183
184 read: Effect.fn(function* ({ id, olds, output }) {
185 const env = yield* GcpEnvironment.current;
186 const parent = olds?.dataStore ?? output?.dataStore;
187 const childId = yield* toPhysical(
188 id,
189 olds?.sessionId,
190 output?.sessionId,
191 sessionIdOf,
192 );
193 const name =
194 output?.name ??
195 (parent !== undefined ? resourceName(parent, childId) : undefined);
196 if (name === undefined) return undefined;
197 const existing = yield* getByName(name);
198 if (existing === undefined) return undefined;
199 const attrs = toAttrs(existing, env.project);
200 // No labels field: without prior state it may not be ours.
201 return output !== undefined ? attrs : Unowned(attrs);
202 }),
203
204 reconcile: Effect.fn(function* ({ id, news, output }) {
205 const env = yield* GcpEnvironment.current;
206 const sessionId = yield* toPhysical(
207 id,
208 news.sessionId,
209 output?.sessionId,
210 sessionIdOf,
211 );
212 const name = resourceName(news.dataStore, sessionId);
213 const displayName = news.displayName ?? sessionId;
214 const labels = news.labels ?? [];
215 const state = news.state ?? "IN_PROGRESS";
216 const desiredPinned = news.isPinned === true;
217
218 let current = yield* getByName(output?.name ?? name);
219
220 if (current === undefined) {
221 const created = yield* discoveryengine
222 .createProjectsLocationsCollectionsDataStoresSessions({
223 parent: news.dataStore,
224 sessionId,
225 body: {
226 displayName,
227 state,
228 userPseudoId: news.userPseudoId,
229 labels,
230 isPinned: desiredPinned ? true : undefined,
231 },
232 })
233 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
234 current = created ?? undefined;
235 }
236
237 if (current === undefined) {
238 return yield* new CollectionsDataStoresSessionNotResolved({ name });
239 }
240
241 const resource = current.name ?? name;
242 const displayNameChanged = (current.displayName ?? "") !== displayName;
243 const stateChanged = (current.state ?? "") !== state;
244 const userChanged =
245 (current.userPseudoId ?? "") !== (news.userPseudoId ?? "");
246 const observedLabels = [...(current.labels ?? [])].sort().join("\0");
247 const desiredLabels = [...labels].sort().join("\0");
248 const labelsChanged = observedLabels !== desiredLabels;
249 const pinnedChanged = (current.isPinned === true) !== desiredPinned;
250
251 if (
252 displayNameChanged ||
253 stateChanged ||
254 userChanged ||
255 labelsChanged ||
256 pinnedChanged
257 ) {
258 current =
259 yield* discoveryengine.patchProjectsLocationsCollectionsDataStoresSessions(
260 {
261 name: resource,
262 updateMask: [
263 displayNameChanged ? "display_name" : undefined,
264 stateChanged ? "state" : undefined,
265 userChanged ? "user_pseudo_id" : undefined,
266 labelsChanged ? "labels" : undefined,
267 pinnedChanged ? "is_pinned" : undefined,
268 ]
269 .filter((field): field is string => field !== undefined)
270 .join(","),
271 body: {
272 name: resource,
273 displayName,
274 state,
275 userPseudoId: news.userPseudoId,
276 labels,
277 isPinned: desiredPinned,
278 },
279 },
280 );
281 }
282
283 return toAttrs(current, env.project);
284 }),
285
286 delete: Effect.fn(function* ({ output }) {
287 yield* discoveryengine
288 .deleteProjectsLocationsCollectionsDataStoresSessions({
289 name: output.name,
290 })
291 .pipe(Effect.catchTag("NotFound", () => Effect.void));
292 }),
293 });