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 controlIdOf,
12 parentOf,
13 parseResourceName,
14 sameJson,
15 sameStringList,
16 toPhysical,
17} from "./internal.ts";
18
19export type ControlCondition = {
20 /** Query regex. Cannot be set together with `queryTerms`. */
21 queryRegex?: string;
22 /** Query terms to match. */
23 queryTerms?: Array<{
24 value: string;
25 fullMatch?: boolean;
26 }>;
27};
28
29export type CollectionsDataStoresControlProps = {
30 /**
31 * Parent data store resource name. Immutable — changing it replaces
32 * the control.
33 */
34 dataStore: string;
35 /**
36 * Control id (`[a-z-_]`, 1-63 characters). If omitted, a unique id is
37 * generated. Immutable — changing it replaces the control.
38 */
39 controlId?: string;
40 /**
41 * Human-readable name (max 128 characters).
42 */
43 displayName?: string;
44 /**
45 * Solution type. Immutable.
46 * @default "SOLUTION_TYPE_SEARCH"
47 */
48 solutionType?:
49 | "SOLUTION_TYPE_UNSPECIFIED"
50 | "SOLUTION_TYPE_RECOMMENDATION"
51 | "SOLUTION_TYPE_SEARCH"
52 | "SOLUTION_TYPE_CHAT"
53 | "SOLUTION_TYPE_GENERATIVE_CHAT"
54 | "SOLUTION_TYPE_AI_MODE"
55 | (string & {});
56 /**
57 * Search use cases. Required when `solutionType` is
58 * `SOLUTION_TYPE_SEARCH`.
59 * @default ["SEARCH_USE_CASE_SEARCH"]
60 */
61 useCases?: Array<
62 | "SEARCH_USE_CASE_UNSPECIFIED"
63 | "SEARCH_USE_CASE_SEARCH"
64 | "SEARCH_USE_CASE_BROWSE"
65 | (string & {})
66 >;
67 /**
68 * Synonym set (at least two terms).
69 */
70 synonyms?: string[];
71 /**
72 * Redirect URI for a redirect control.
73 */
74 redirectUri?: string;
75 /**
76 * Filter expression for a filter control.
77 */
78 filter?: string;
79 /**
80 * Data store name the filter applies to. Defaults to `dataStore`.
81 */
82 filterDataStore?: string;
83 /**
84 * Trigger conditions. Omit to always apply the action.
85 */
86 conditions?: ControlCondition[];
87};
88
89export type CollectionsDataStoresControl = Resource<
90 "GCP.DiscoveryEngine.CollectionsDataStoresControl",
91 CollectionsDataStoresControlProps,
92 {
93 /** Full resource name. */
94 name: string;
95 /** Control id. */
96 controlId: string;
97 /** Parent data store resource name. */
98 dataStore: string;
99 /** Project id. */
100 project: string;
101 /** Location id. */
102 location: string;
103 /** Display name. */
104 displayName: string | undefined;
105 /** Solution type. */
106 solutionType: string | undefined;
107 /** Search use cases. */
108 useCases: string[];
109 /** Synonym set. */
110 synonyms: string[];
111 /** Redirect URI. */
112 redirectUri: string | undefined;
113 /** Filter expression. */
114 filter: string | undefined;
115 /** Serving configs this control is attached to. */
116 associatedServingConfigIds: string[];
117 },
118 never,
119 Providers
120>;
121
122/**
123 * A Discovery Engine serving control on a collection data store.
124 *
125 * Without labels, ownership rests on the deterministic id: `read` reports a
126 * resource it finds without prior state as unowned (adopt it with `--adopt`).
127 * Parent, control id, and solution type are immutable. Display name,
128 * synonyms, redirect, filter, and conditions update in place.
129 *
130 * ### Creating a Control
131 * **Example:** Synonym control
132 * ```typescript
133 * const store = yield* GCP.DiscoveryEngine.CollectionsDataStore("Docs", {});
134 * const control = yield* GCP.DiscoveryEngine.CollectionsDataStoresControl(
135 * "Greetings",
136 * {
137 * dataStore: store.name,
138 * displayName: "greetings",
139 * synonyms: ["hello", "hi"],
140 * },
141 * );
142 * ```
143 *
144 * @resource
145 * @category DiscoveryEngine
146 */
147export const CollectionsDataStoresControl =
148 Resource<CollectionsDataStoresControl>(
149 "GCP.DiscoveryEngine.CollectionsDataStoresControl",
150 );
151
152export class CollectionsDataStoresControlNotResolved extends Data.TaggedError(
153 "GCP.DiscoveryEngine.CollectionsDataStoresControlNotResolved",
154)<{
155 name: string;
156}> {}
157
158const toAttrs = (
159 control: discoveryengine.GoogleCloudDiscoveryengineV1Control,
160 project: string,
161) => {
162 const name = control.name ?? "";
163 const parsed = parseResourceName(name, "controls");
164 return {
165 name,
166 controlId: parsed.id,
167 dataStore: parentOf(name, "controls"),
168 project: parsed.project || project,
169 location: parsed.location,
170 displayName: control.displayName,
171 solutionType: control.solutionType,
172 useCases: [...(control.useCases ?? [])],
173 synonyms: [...(control.synonymsAction?.synonyms ?? [])],
174 redirectUri: control.redirectAction?.redirectUri,
175 filter: control.filterAction?.filter,
176 associatedServingConfigIds: [...(control.associatedServingConfigIds ?? [])],
177 };
178};
179
180const resourceName = (dataStore: string, controlId: string) =>
181 `${dataStore}/controls/${controlId}`;
182
183const toBody = (
184 news: CollectionsDataStoresControlProps,
185 displayName: string,
186): discoveryengine.GoogleCloudDiscoveryengineV1Control => ({
187 displayName,
188 solutionType: news.solutionType ?? "SOLUTION_TYPE_SEARCH",
189 useCases: news.useCases ?? ["SEARCH_USE_CASE_SEARCH"],
190 synonymsAction:
191 news.synonyms && news.synonyms.length > 0
192 ? { synonyms: news.synonyms }
193 : undefined,
194 redirectAction: news.redirectUri
195 ? { redirectUri: news.redirectUri }
196 : undefined,
197 filterAction: news.filter
198 ? {
199 filter: news.filter,
200 dataStore: news.filterDataStore ?? news.dataStore,
201 }
202 : undefined,
203 conditions: news.conditions,
204});
205
206const getByName = (name: string) =>
207 name.length === 0
208 ? Effect.succeed(undefined)
209 : discoveryengine
210 .getProjectsLocationsCollectionsDataStoresControls({ name })
211 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
212
213export const CollectionsDataStoresControlProvider = () =>
214 Provider.succeed(CollectionsDataStoresControl, {
215 stables: [
216 "name",
217 "controlId",
218 "dataStore",
219 "project",
220 "location",
221 "solutionType",
222 ],
223
224 diff: Effect.fn(function* ({ news, olds, output }) {
225 if (!isResolved(news)) return undefined;
226 const previousParent = olds?.dataStore ?? output?.dataStore;
227 const previousId = olds?.controlId ?? output?.controlId;
228 const previousType = olds?.solutionType ?? output?.solutionType;
229 const nextType = news.solutionType ?? previousType;
230 if (
231 (previousParent !== undefined && news.dataStore !== previousParent) ||
232 (previousId !== undefined &&
233 news.controlId !== undefined &&
234 news.controlId !== previousId) ||
235 (previousType !== undefined &&
236 nextType !== undefined &&
237 previousType !== nextType)
238 ) {
239 return {
240 action: "replace" as const,
241 deleteFirst:
242 previousParent === news.dataStore &&
243 previousId !== undefined &&
244 news.controlId === previousId,
245 };
246 }
247 return undefined;
248 }),
249
250 read: Effect.fn(function* ({ id, olds, output }) {
251 const env = yield* GcpEnvironment.current;
252 const parent = olds?.dataStore ?? output?.dataStore;
253 const childId = yield* toPhysical(
254 id,
255 olds?.controlId,
256 output?.controlId,
257 controlIdOf,
258 );
259 const name =
260 output?.name ??
261 (parent !== undefined ? resourceName(parent, childId) : undefined);
262 if (name === undefined) return undefined;
263 const existing = yield* getByName(name);
264 if (existing === undefined) return undefined;
265 const attrs = toAttrs(existing, env.project);
266 // No labels field: without prior state it may not be ours.
267 return output !== undefined ? attrs : Unowned(attrs);
268 }),
269
270 reconcile: Effect.fn(function* ({ id, news, output }) {
271 const env = yield* GcpEnvironment.current;
272 const controlId = yield* toPhysical(
273 id,
274 news.controlId,
275 output?.controlId,
276 controlIdOf,
277 );
278 const name = resourceName(news.dataStore, controlId);
279 const displayName = news.displayName ?? controlId;
280 const body = toBody(news, displayName);
281
282 let current = yield* getByName(output?.name ?? name);
283
284 if (current === undefined) {
285 const created = yield* discoveryengine
286 .createProjectsLocationsCollectionsDataStoresControls({
287 parent: news.dataStore,
288 controlId,
289 body,
290 })
291 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
292 current = created ?? undefined;
293 }
294
295 if (current === undefined) {
296 return yield* new CollectionsDataStoresControlNotResolved({ name });
297 }
298
299 const resource = current.name ?? name;
300 const displayNameChanged = (current.displayName ?? "") !== displayName;
301 const conditionsChanged = !sameJson(current.conditions, news.conditions);
302 const useCasesChanged = !sameStringList(
303 current.useCases,
304 news.useCases ?? ["SEARCH_USE_CASE_SEARCH"],
305 );
306
307 if (displayNameChanged || conditionsChanged || useCasesChanged) {
308 current =
309 yield* discoveryengine.patchProjectsLocationsCollectionsDataStoresControls(
310 {
311 name: resource,
312 updateMask: [
313 displayNameChanged ? "display_name" : undefined,
314 conditionsChanged ? "conditions" : undefined,
315 useCasesChanged ? "use_cases" : undefined,
316 ]
317 .filter((field): field is string => field !== undefined)
318 .join(","),
319 body: { ...body, name: resource },
320 },
321 );
322 }
323
324 return toAttrs(current, env.project);
325 }),
326
327 delete: Effect.fn(function* ({ output }) {
328 yield* discoveryengine
329 .deleteProjectsLocationsCollectionsDataStoresControls({
330 name: output.name,
331 })
332 .pipe(Effect.catchTag("NotFound", () => Effect.void));
333 }),
334 });