1import * as discoveryengine from "@distilled.cloud/gcp/discoveryengine_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.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 expandDataStore,
13 matchesUriPattern,
14 parseResourceName,
15} from "./internal.ts";
16import { waitForOperation } from "./operations.ts";
17
18export type DataStoresSiteSearchEngineTargetSiteProps = {
19 /**
20 * Parent Data Store resource name
21 * `projects/{project}/locations/{location}/dataStores/{dataStore}`.
22 * The data store must use `PUBLIC_WEBSITE` content. Immutable —
23 * changing it replaces the target site.
24 */
25 dataStore: string;
26 /**
27 * User-provided URI pattern from which the generated URI pattern is
28 * derived (for example `www.example.com`). Identifies the target site
29 * within the data store. Changing it replaces the site.
30 */
31 providedUriPattern: string;
32 /**
33 * When true, match the provided URI pattern exactly. Immutable —
34 * changing it replaces the target site.
35 * @default false
36 */
37 exactMatch?: boolean;
38 /**
39 * Whether the site is included or excluded (`INCLUDE`, `EXCLUDE`).
40 * @default "INCLUDE"
41 */
42 type?: string;
43};
44
45export type DataStoresSiteSearchEngineTargetSite = Resource<
46 "GCP.DiscoveryEngine.DataStoresSiteSearchEngineTargetSite",
47 DataStoresSiteSearchEngineTargetSiteProps,
48 {
49 /** Full resource name `.../siteSearchEngine/targetSites/{targetSite}`. */
50 name: string;
51 /** Target site id (last path segment). Server-assigned on create. */
52 targetSiteId: string;
53 /** Parent data store resource name. */
54 dataStore: string;
55 /** Project id. */
56 project: string;
57 /** Location id. */
58 location: string;
59 /** User-provided URI pattern. */
60 providedUriPattern: string | undefined;
61 /** System-generated URI pattern. */
62 generatedUriPattern: string | undefined;
63 /** Root domain of the provided URI pattern. */
64 rootDomainUri: string | undefined;
65 /** Whether the URI pattern is an exact match. */
66 exactMatch: boolean;
67 /** Include or exclude. */
68 type: string | undefined;
69 /** Indexing status. */
70 indexingStatus: string | undefined;
71 /** RFC3339 last-update timestamp. */
72 updateTime: string | undefined;
73 },
74 never,
75 Providers
76>;
77
78/**
79 * A Vertex AI Search TargetSite under a Data Store site search engine.
80 *
81 * Its id is assigned by the API, so only a recorded resource can be read
82 * back. The target site id is server-assigned. Parent and exact-match flag
83 * are immutable; type updates in place (LRO).
84 *
85 * ### Creating a Target Site
86 * **Example:** Include a site
87 * ```typescript
88 * const site = yield* GCP.DiscoveryEngine.DataStoresSiteSearchEngineTargetSite(
89 * "Docs",
90 * {
91 * dataStore: dataStore.name,
92 * providedUriPattern: "www.example.com/docs",
93 * type: "INCLUDE",
94 * },
95 * );
96 * ```
97 *
98 * ### Updating a Target Site
99 * **Example:** Exclude the site
100 * ```typescript
101 * // Same logical id as before; only the changed props differ.
102 * const site = yield* GCP.DiscoveryEngine.DataStoresSiteSearchEngineTargetSite(
103 * "Docs",
104 * {
105 * dataStore: dataStore.name,
106 * providedUriPattern: "www.example.com/docs",
107 * type: "EXCLUDE",
108 * },
109 * );
110 * ```
111 *
112 * @resource
113 * @category DiscoveryEngine
114 */
115export const DataStoresSiteSearchEngineTargetSite =
116 Resource<DataStoresSiteSearchEngineTargetSite>(
117 "GCP.DiscoveryEngine.DataStoresSiteSearchEngineTargetSite",
118 );
119
120export class DataStoresSiteSearchEngineTargetSiteNotResolved extends Data.TaggedError(
121 "GCP.DiscoveryEngine.DataStoresSiteSearchEngineTargetSiteNotResolved",
122)<{
123 name: string;
124}> {}
125
126const siteSearchEngine = (dataStore: string) => `${dataStore}/siteSearchEngine`;
127
128const toAttrs = (
129 site: discoveryengine.GoogleCloudDiscoveryengineV1TargetSite,
130 project: string,
131 pattern?: string,
132) => {
133 const name = site.name ?? "";
134 const parsed = parseResourceName(name, "targetSites");
135 return {
136 name,
137 targetSiteId: parsed.id,
138 dataStore: parsed.dataStore,
139 project: parsed.project || project,
140 location: parsed.location,
141 providedUriPattern:
142 site.providedUriPattern ?? pattern ?? site.generatedUriPattern,
143 generatedUriPattern: site.generatedUriPattern,
144 rootDomainUri: site.rootDomainUri,
145 exactMatch: site.exactMatch === true,
146 type: site.type,
147 indexingStatus: site.indexingStatus,
148 updateTime: site.updateTime,
149 };
150};
151
152const getByName = (name: string) =>
153 name.length === 0
154 ? Effect.succeed(undefined)
155 : discoveryengine
156 .getProjectsLocationsDataStoresSiteSearchEngineTargetSites({ name })
157 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
158
159const listAtParent = (parent: string) =>
160 discoveryengine.listProjectsLocationsDataStoresSiteSearchEngineTargetSites
161 .pages({ parent, pageSize: 100 })
162 .pipe(
163 Stream.flatMap((page) => Stream.fromIterable(page.targetSites ?? [])),
164 Stream.runCollect,
165 Effect.map((chunk) => Array.from(chunk)),
166 Effect.catchTag("NotFound", () => Effect.succeed([])),
167 );
168
169const findByPattern = (
170 uri: string,
171 parent: string | undefined,
172 hinted?: string,
173) =>
174 Effect.gen(function* () {
175 if (hinted !== undefined && hinted.length > 0) {
176 const existing = yield* getByName(hinted);
177 if (existing !== undefined) return existing;
178 }
179 if (parent === undefined) return undefined;
180 const sites = yield* listAtParent(parent);
181 return sites.find((site) => matchesUriPattern(site, uri));
182 });
183
184export const DataStoresSiteSearchEngineTargetSiteProvider = () =>
185 Provider.succeed(DataStoresSiteSearchEngineTargetSite, {
186 stables: [
187 "name",
188 "targetSiteId",
189 "dataStore",
190 "project",
191 "location",
192 "providedUriPattern",
193 "exactMatch",
194 ],
195
196 diff: Effect.fn(function* ({ news, olds, output }) {
197 if (!isResolved(news)) return undefined;
198 const previousParent = olds?.dataStore ?? output?.dataStore;
199 if (previousParent !== undefined && news.dataStore !== previousParent) {
200 return { action: "replace" as const, deleteFirst: false };
201 }
202 const previousExact = olds?.exactMatch ?? output?.exactMatch ?? false;
203 const nextExact = news.exactMatch ?? previousExact;
204 if (previousExact !== nextExact) {
205 return { action: "replace" as const, deleteFirst: true };
206 }
207 const previousUri =
208 olds?.providedUriPattern ?? output?.providedUriPattern;
209 if (
210 previousUri !== undefined &&
211 news.providedUriPattern !== undefined &&
212 news.providedUriPattern !== previousUri
213 ) {
214 return { action: "replace" as const, deleteFirst: true };
215 }
216 const previousType = olds?.type ?? output?.type ?? "INCLUDE";
217 const nextType = news.type ?? previousType;
218 if (previousType !== nextType) {
219 return { action: "replace" as const, deleteFirst: true };
220 }
221 return undefined;
222 }),
223
224 read: Effect.fn(function* ({ olds, output }) {
225 const env = yield* GcpEnvironment.current;
226 const parent = olds?.dataStore
227 ? expandDataStore(
228 olds.dataStore,
229 env.project,
230 output?.location ?? "global",
231 )
232 : undefined;
233 const uri = olds?.providedUriPattern ?? output?.providedUriPattern;
234 const existing =
235 uri === undefined
236 ? undefined
237 : yield* findByPattern(
238 uri,
239 parent ? siteSearchEngine(parent) : undefined,
240 output?.name,
241 );
242 if (existing === undefined) return undefined;
243 const attrs = toAttrs(existing, env.project, uri);
244 // No labels field: without prior state it may not be ours.
245 return output !== undefined ? attrs : Unowned(attrs);
246 }),
247
248 reconcile: Effect.fn(function* ({ news, output }) {
249 const env = yield* GcpEnvironment.current;
250 const parent = expandDataStore(
251 news.dataStore,
252 env.project,
253 output?.location ?? "global",
254 );
255 const engine = siteSearchEngine(parent);
256 const providedUriPattern = news.providedUriPattern;
257 const exactMatch = news.exactMatch === true;
258 const type = news.type ?? "INCLUDE";
259
260 let current = yield* findByPattern(
261 providedUriPattern,
262 engine,
263 output?.name,
264 );
265
266 if (current === undefined) {
267 const created = yield* discoveryengine
268 .createProjectsLocationsDataStoresSiteSearchEngineTargetSites({
269 parent: engine,
270 body: {
271 providedUriPattern,
272 exactMatch,
273 type,
274 },
275 })
276 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
277 if (created !== undefined) {
278 yield* waitForOperation(created);
279 }
280 current = yield* findByPattern(providedUriPattern, engine);
281 }
282
283 if (current === undefined) {
284 return yield* new DataStoresSiteSearchEngineTargetSiteNotResolved({
285 name: output?.name ?? `${engine}/targetSites`,
286 });
287 }
288
289 const typeChanged =
290 news.type !== undefined && (current.type ?? "INCLUDE") !== type;
291
292 if (typeChanged) {
293 const patched =
294 yield* discoveryengine.patchProjectsLocationsDataStoresSiteSearchEngineTargetSites(
295 {
296 name: current.name ?? "",
297 body: {
298 name: current.name,
299 providedUriPattern,
300 exactMatch,
301 type,
302 },
303 },
304 );
305 yield* waitForOperation(patched);
306 current = (yield* getByName(current.name ?? "")) ?? current;
307 }
308
309 return toAttrs(current, env.project, providedUriPattern);
310 }),
311
312 delete: Effect.fn(function* ({ output }) {
313 const existing = yield* getByName(output.name);
314 if (existing === undefined) return;
315 const operation = yield* discoveryengine
316 .deleteProjectsLocationsDataStoresSiteSearchEngineTargetSites({
317 name: output.name,
318 })
319 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
320 if (operation !== undefined) {
321 yield* waitForOperation(operation, { notFoundOk: true });
322 }
323 }),
324 });