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