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 sameStringList,
14 servingConfigIdOf,
15 toPhysical,
16} from "./internal.ts";
17
18export type CollectionsDataStoresServingConfigProps = {
19 /**
20 * Parent data store resource name. Immutable — changing it replaces
21 * the serving config.
22 */
23 dataStore: string;
24 /**
25 * Serving config id (4-63 characters, `[a-zA-Z0-9_]`). If omitted, a
26 * unique id is generated. Immutable — changing it replaces the serving
27 * config.
28 */
29 servingConfigId?: string;
30 /**
31 * Human-readable name (max 128 characters).
32 */
33 displayName?: string;
34 /**
35 * Solution type. Immutable.
36 * @default "SOLUTION_TYPE_SEARCH"
37 */
38 solutionType?:
39 | "SOLUTION_TYPE_UNSPECIFIED"
40 | "SOLUTION_TYPE_RECOMMENDATION"
41 | "SOLUTION_TYPE_SEARCH"
42 | "SOLUTION_TYPE_CHAT"
43 | "SOLUTION_TYPE_GENERATIVE_CHAT"
44 | "SOLUTION_TYPE_AI_MODE"
45 | (string & {});
46 /**
47 * Filter control ids applied at serving time.
48 */
49 filterControlIds?: string[];
50 /**
51 * Boost control ids applied at serving time.
52 */
53 boostControlIds?: string[];
54 /**
55 * Redirect control ids.
56 */
57 redirectControlIds?: string[];
58 /**
59 * Synonyms control ids.
60 */
61 synonymsControlIds?: string[];
62 /**
63 * Ranking expression, e.g. `0.5 * relevance_score`.
64 */
65 rankingExpression?: string;
66 /**
67 * Recommendation diversity level.
68 */
69 diversityLevel?: string;
70 /**
71 * Recommendation model id. Required for `SOLUTION_TYPE_RECOMMENDATION`.
72 */
73 modelId?: string;
74};
75
76export type CollectionsDataStoresServingConfig = Resource<
77 "GCP.DiscoveryEngine.CollectionsDataStoresServingConfig",
78 CollectionsDataStoresServingConfigProps,
79 {
80 /** Full resource name. */
81 name: string;
82 /** Serving config id. */
83 servingConfigId: string;
84 /** Parent data store resource name. */
85 dataStore: string;
86 /** Project id. */
87 project: string;
88 /** Location id. */
89 location: string;
90 /** Display name. */
91 displayName: string | undefined;
92 /** Solution type. */
93 solutionType: string | undefined;
94 /** Filter control ids. */
95 filterControlIds: string[];
96 /** Boost control ids. */
97 boostControlIds: string[];
98 /** Redirect control ids. */
99 redirectControlIds: string[];
100 /** Synonyms control ids. */
101 synonymsControlIds: string[];
102 /** Ranking expression. */
103 rankingExpression: string | undefined;
104 /** Diversity level. */
105 diversityLevel: string | undefined;
106 /** Model id. */
107 modelId: string | undefined;
108 /** RFC3339 creation timestamp. */
109 createTime: string | undefined;
110 /** RFC3339 last-update timestamp. */
111 updateTime: string | undefined;
112 },
113 never,
114 Providers
115>;
116
117/**
118 * A Discovery Engine serving config on a collection data store.
119 *
120 * Without labels, ownership rests on the deterministic id: `read` reports a
121 * resource it finds without prior state as unowned (adopt it with `--adopt`).
122 * Parent, serving config id, and solution type are immutable. Display name
123 * and control-id lists update in place.
124 *
125 * ### Creating a Serving Config
126 * **Example:** Extra search serving config
127 * ```typescript
128 * const store = yield* GCP.DiscoveryEngine.CollectionsDataStore("Docs", {});
129 * const serving = yield* GCP.DiscoveryEngine.CollectionsDataStoresServingConfig(
130 * "Preview",
131 * {
132 * dataStore: store.name,
133 * displayName: "preview search",
134 * },
135 * );
136 * ```
137 *
138 * @resource
139 * @category DiscoveryEngine
140 */
141export const CollectionsDataStoresServingConfig =
142 Resource<CollectionsDataStoresServingConfig>(
143 "GCP.DiscoveryEngine.CollectionsDataStoresServingConfig",
144 );
145
146export class CollectionsDataStoresServingConfigNotResolved extends Data.TaggedError(
147 "GCP.DiscoveryEngine.CollectionsDataStoresServingConfigNotResolved",
148)<{
149 name: string;
150}> {}
151
152const toAttrs = (
153 config: discoveryengine.GoogleCloudDiscoveryengineV1ServingConfig,
154 project: string,
155) => {
156 const name = config.name ?? "";
157 const parsed = parseResourceName(name, "servingConfigs");
158 return {
159 name,
160 servingConfigId: parsed.id,
161 dataStore: parentOf(name, "servingConfigs"),
162 project: parsed.project || project,
163 location: parsed.location,
164 displayName: config.displayName,
165 solutionType: config.solutionType,
166 filterControlIds: [...(config.filterControlIds ?? [])],
167 boostControlIds: [...(config.boostControlIds ?? [])],
168 redirectControlIds: [...(config.redirectControlIds ?? [])],
169 synonymsControlIds: [...(config.synonymsControlIds ?? [])],
170 rankingExpression: config.rankingExpression,
171 diversityLevel: config.diversityLevel,
172 modelId: config.modelId,
173 createTime: config.createTime,
174 updateTime: config.updateTime,
175 };
176};
177
178const resourceName = (dataStore: string, servingConfigId: string) =>
179 `${dataStore}/servingConfigs/${servingConfigId}`;
180
181const toBody = (
182 news: CollectionsDataStoresServingConfigProps,
183 displayName: string,
184): discoveryengine.GoogleCloudDiscoveryengineV1ServingConfig => ({
185 displayName,
186 solutionType: news.solutionType ?? "SOLUTION_TYPE_SEARCH",
187 filterControlIds: news.filterControlIds,
188 boostControlIds: news.boostControlIds,
189 redirectControlIds: news.redirectControlIds,
190 synonymsControlIds: news.synonymsControlIds,
191 rankingExpression: news.rankingExpression,
192 diversityLevel: news.diversityLevel,
193 modelId: news.modelId,
194});
195
196const getByName = (name: string) =>
197 name.length === 0
198 ? Effect.succeed(undefined)
199 : discoveryengine
200 .getProjectsLocationsCollectionsDataStoresServingConfigs({ name })
201 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
202
203export const CollectionsDataStoresServingConfigProvider = () =>
204 Provider.succeed(CollectionsDataStoresServingConfig, {
205 stables: [
206 "name",
207 "servingConfigId",
208 "dataStore",
209 "project",
210 "location",
211 "solutionType",
212 "createTime",
213 ],
214
215 diff: Effect.fn(function* ({ news, olds, output }) {
216 if (!isResolved(news)) return undefined;
217 const previousParent = olds?.dataStore ?? output?.dataStore;
218 const previousId = olds?.servingConfigId ?? output?.servingConfigId;
219 const previousType = olds?.solutionType ?? output?.solutionType;
220 const nextType = news.solutionType ?? previousType;
221 if (
222 (previousParent !== undefined && news.dataStore !== previousParent) ||
223 (previousId !== undefined &&
224 news.servingConfigId !== undefined &&
225 news.servingConfigId !== previousId) ||
226 (previousType !== undefined &&
227 nextType !== undefined &&
228 previousType !== nextType)
229 ) {
230 return {
231 action: "replace" as const,
232 deleteFirst:
233 previousParent === news.dataStore &&
234 previousId !== undefined &&
235 news.servingConfigId === previousId,
236 };
237 }
238 return undefined;
239 }),
240
241 read: Effect.fn(function* ({ id, olds, output }) {
242 const env = yield* GcpEnvironment.current;
243 const parent = olds?.dataStore ?? output?.dataStore;
244 const childId = yield* toPhysical(
245 id,
246 olds?.servingConfigId,
247 output?.servingConfigId,
248 servingConfigIdOf,
249 );
250 const name =
251 output?.name ??
252 (parent !== undefined ? resourceName(parent, childId) : undefined);
253 if (name === undefined) return undefined;
254 const existing = yield* getByName(name);
255 if (existing === undefined) return undefined;
256 const attrs = toAttrs(existing, env.project);
257 // No labels field: without prior state it may not be ours.
258 return output !== undefined ? attrs : Unowned(attrs);
259 }),
260
261 reconcile: Effect.fn(function* ({ id, news, output }) {
262 const env = yield* GcpEnvironment.current;
263 const servingConfigId = yield* toPhysical(
264 id,
265 news.servingConfigId,
266 output?.servingConfigId,
267 servingConfigIdOf,
268 );
269 const name = resourceName(news.dataStore, servingConfigId);
270 const displayName = news.displayName ?? servingConfigId;
271 const body = toBody(news, displayName);
272
273 let current = yield* getByName(output?.name ?? name);
274
275 if (current === undefined) {
276 const created = yield* discoveryengine
277 .createProjectsLocationsCollectionsDataStoresServingConfigs({
278 parent: news.dataStore,
279 servingConfigId,
280 body,
281 })
282 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
283 current = created ?? undefined;
284 }
285
286 if (current === undefined) {
287 return yield* new CollectionsDataStoresServingConfigNotResolved({
288 name,
289 });
290 }
291
292 const resource = current.name ?? name;
293 const displayNameChanged = (current.displayName ?? "") !== displayName;
294 const filterChanged = !sameStringList(
295 current.filterControlIds,
296 news.filterControlIds,
297 );
298 const boostChanged = !sameStringList(
299 current.boostControlIds,
300 news.boostControlIds,
301 );
302 const redirectChanged = !sameStringList(
303 current.redirectControlIds,
304 news.redirectControlIds,
305 );
306 const synonymsChanged = !sameStringList(
307 current.synonymsControlIds,
308 news.synonymsControlIds,
309 );
310 const rankingChanged =
311 (current.rankingExpression ?? "") !== (news.rankingExpression ?? "");
312 const diversityChanged =
313 (current.diversityLevel ?? "") !== (news.diversityLevel ?? "");
314 const modelChanged = (current.modelId ?? "") !== (news.modelId ?? "");
315
316 if (
317 displayNameChanged ||
318 filterChanged ||
319 boostChanged ||
320 redirectChanged ||
321 synonymsChanged ||
322 rankingChanged ||
323 diversityChanged ||
324 modelChanged
325 ) {
326 current =
327 yield* discoveryengine.patchProjectsLocationsCollectionsDataStoresServingConfigs(
328 {
329 name: resource,
330 updateMask: [
331 displayNameChanged ? "display_name" : undefined,
332 filterChanged ? "filter_control_ids" : undefined,
333 boostChanged ? "boost_control_ids" : undefined,
334 redirectChanged ? "redirect_control_ids" : undefined,
335 synonymsChanged ? "synonyms_control_ids" : undefined,
336 rankingChanged ? "ranking_expression" : undefined,
337 diversityChanged ? "diversity_level" : undefined,
338 modelChanged ? "model_id" : undefined,
339 ]
340 .filter((field): field is string => field !== undefined)
341 .join(","),
342 body: { ...body, name: resource },
343 },
344 );
345 }
346
347 return toAttrs(current, env.project);
348 }),
349
350 delete: Effect.fn(function* ({ output }) {
351 yield* discoveryengine
352 .deleteProjectsLocationsCollectionsDataStoresServingConfigs({
353 name: output.name,
354 })
355 .pipe(Effect.catchTag("NotFound", () => Effect.void));
356 }),
357 });