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 DEFAULT_BRANCH,
12 expandDataStore,
13 fingerprint,
14 MAX_DOCUMENT_ID_LENGTH,
15 parseResourceName,
16 rfc1035,
17 toPhysical,
18} from "./internal.ts";
19
20export type DataStoresDocumentContent = {
21 /**
22 * MIME type (`text/plain`, `text/html`, `application/pdf`,
23 * `application/json`, …).
24 */
25 mimeType?: string;
26 /**
27 * Cloud Storage URI (`gs://bucket/path`). Max 2.5 MB for text, 200 MB
28 * otherwise.
29 */
30 uri?: string;
31 /**
32 * Raw bytes as a base64 string. Max 1,000,000 bytes.
33 */
34 rawBytes?: string;
35};
36
37export type DataStoresBranchesDocumentProps = {
38 /**
39 * Parent Data Store resource name
40 * `projects/{project}/locations/{location}/dataStores/{dataStore}`.
41 * Immutable — changing it replaces the document.
42 */
43 dataStore: string;
44 /**
45 * Branch id. Use `default_branch` for the default branch.
46 * Immutable — changing it replaces the document.
47 * @default "default_branch"
48 */
49 branch?: string;
50 /**
51 * Document id (RFC-1034, max 128 characters). If omitted, a unique id
52 * is generated. Immutable — changing it replaces the document.
53 */
54 documentId?: string;
55 /**
56 * JSON document payload. Must conform to the data store schema.
57 */
58 jsonData?: string;
59 /**
60 * Structured document payload. Ignored when `jsonData` is set.
61 */
62 structData?: Record<string, unknown>;
63 /**
64 * Unstructured content. Required for `CONTENT_REQUIRED` data stores.
65 */
66 content?: DataStoresDocumentContent;
67 /**
68 * Parent document id for two-level hierarchies (RFC-1034, max 63).
69 */
70 parentDocumentId?: string;
71 /**
72 * Schema id in the same data store.
73 */
74 schemaId?: string;
75};
76
77export type DataStoresBranchesDocument = Resource<
78 "GCP.DiscoveryEngine.DataStoresBranchesDocument",
79 DataStoresBranchesDocumentProps,
80 {
81 /** Full resource name `.../branches/{branch}/documents/{document}`. */
82 name: string;
83 /** Document id (last path segment). */
84 documentId: string;
85 /** Parent data store resource name. */
86 dataStore: string;
87 /** Branch id. */
88 branch: string;
89 /** Project id. */
90 project: string;
91 /** Location id. */
92 location: string;
93 /** JSON payload. */
94 jsonData: string | undefined;
95 /** Structured payload, if set. */
96 structData: Record<string, unknown> | undefined;
97 /** Unstructured content, if set. */
98 content: DataStoresDocumentContent | undefined;
99 /** Parent document id, if set. */
100 parentDocumentId: string | undefined;
101 /** Schema id, if set. */
102 schemaId: string | undefined;
103 /** Last index timestamp, if indexed. */
104 indexTime: string | undefined;
105 },
106 never,
107 Providers
108>;
109
110/**
111 * A Vertex AI Search Document in a Data Store branch.
112 *
113 * Without labels, ownership rests on the deterministic id: `read` reports a
114 * resource it finds without prior state as unowned (adopt it with `--adopt`).
115 * Parent data store, branch, and document id are immutable; payload and
116 * content update in place.
117 *
118 * ### Creating a Document
119 * **Example:** JSON document on the default branch
120 * ```typescript
121 * const document = yield* GCP.DiscoveryEngine.DataStoresBranchesDocument(
122 * "About",
123 * {
124 * dataStore: dataStore.name,
125 * jsonData: JSON.stringify({
126 * title: "About us",
127 * uri: "https://example.com/about",
128 * }),
129 * },
130 * );
131 * ```
132 *
133 * ### Updating a Document
134 * **Example:** Change the title
135 * ```typescript
136 * // Same logical id as before; only the changed props differ.
137 * const document = yield* GCP.DiscoveryEngine.DataStoresBranchesDocument(
138 * "About",
139 * {
140 * dataStore: dataStore.name,
141 * jsonData: JSON.stringify({
142 * title: "About the team",
143 * uri: "https://example.com/about",
144 * }),
145 * },
146 * );
147 * ```
148 *
149 * @resource
150 * @category DiscoveryEngine
151 */
152export const DataStoresBranchesDocument = Resource<DataStoresBranchesDocument>(
153 "GCP.DiscoveryEngine.DataStoresBranchesDocument",
154);
155
156export class DataStoresBranchesDocumentNotResolved extends Data.TaggedError(
157 "GCP.DiscoveryEngine.DataStoresBranchesDocumentNotResolved",
158)<{
159 name: string;
160}> {}
161
162const branchParent = (dataStore: string, branch: string) =>
163 `${dataStore}/branches/${branch}`;
164
165const resourceName = (dataStore: string, branch: string, documentId: string) =>
166 `${branchParent(dataStore, branch)}/documents/${documentId}`;
167
168const toAttrs = (
169 document: discoveryengine.GoogleCloudDiscoveryengineV1Document,
170 project: string,
171) => {
172 const name = document.name ?? "";
173 const parsed = parseResourceName(name, "documents");
174 const branchParsed = parseResourceName(name, "branches");
175 return {
176 name,
177 documentId: document.id ?? parsed.id,
178 dataStore: parsed.dataStore,
179 branch: branchParsed.id || DEFAULT_BRANCH,
180 project: parsed.project || project,
181 location: parsed.location,
182 jsonData: document.jsonData,
183 structData: document.structData as Record<string, unknown> | undefined,
184 content: document.content
185 ? {
186 mimeType: document.content.mimeType,
187 uri: document.content.uri,
188 rawBytes: document.content.rawBytes,
189 }
190 : undefined,
191 parentDocumentId: document.parentDocumentId,
192 schemaId: document.schemaId,
193 indexTime: document.indexTime,
194 };
195};
196
197const getByName = (name: string) =>
198 name.length === 0
199 ? Effect.succeed(undefined)
200 : discoveryengine
201 .getProjectsLocationsDataStoresBranchesDocuments({ name })
202 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
203
204export const DataStoresBranchesDocumentProvider = () =>
205 Provider.succeed(DataStoresBranchesDocument, {
206 stables: [
207 "name",
208 "documentId",
209 "dataStore",
210 "branch",
211 "project",
212 "location",
213 ],
214
215 diff: Effect.fn(function* ({ news, olds, output }) {
216 if (!isResolved(news)) return undefined;
217 const previousParent = olds?.dataStore ?? output?.dataStore;
218 if (previousParent !== undefined && news.dataStore !== previousParent) {
219 return { action: "replace" as const, deleteFirst: false };
220 }
221 const previousBranch = olds?.branch ?? output?.branch ?? DEFAULT_BRANCH;
222 const nextBranch = news.branch ?? previousBranch;
223 if (previousBranch !== nextBranch) {
224 return { action: "replace" as const, deleteFirst: false };
225 }
226 const previousId = olds?.documentId ?? output?.documentId;
227 if (
228 previousId !== undefined &&
229 news.documentId !== undefined &&
230 news.documentId !== previousId
231 ) {
232 return { action: "replace" as const, deleteFirst: true };
233 }
234 return undefined;
235 }),
236
237 read: Effect.fn(function* ({ id, olds, output }) {
238 const env = yield* GcpEnvironment.current;
239 const documentId = yield* toPhysical(
240 id,
241 olds?.documentId,
242 output?.documentId,
243 (name) => rfc1035(name, MAX_DOCUMENT_ID_LENGTH),
244 MAX_DOCUMENT_ID_LENGTH,
245 );
246 const branch = olds?.branch ?? output?.branch ?? DEFAULT_BRANCH;
247 const parent = olds?.dataStore
248 ? expandDataStore(
249 olds.dataStore,
250 env.project,
251 output?.location ?? "global",
252 )
253 : undefined;
254 const name =
255 output?.name ??
256 (parent ? resourceName(parent, branch, documentId) : "");
257 const existing = yield* getByName(name);
258 if (existing === undefined) return undefined;
259 const attrs = toAttrs(existing, env.project);
260 // No labels field: without prior state it may not be ours.
261 return output !== undefined ? attrs : Unowned(attrs);
262 }),
263
264 reconcile: Effect.fn(function* ({ id, news, output }) {
265 const env = yield* GcpEnvironment.current;
266 const parent = expandDataStore(
267 news.dataStore,
268 env.project,
269 output?.location ?? "global",
270 );
271 const branch = news.branch ?? output?.branch ?? DEFAULT_BRANCH;
272 const documentId = yield* toPhysical(
273 id,
274 news.documentId,
275 output?.documentId,
276 (name) => rfc1035(name, MAX_DOCUMENT_ID_LENGTH),
277 MAX_DOCUMENT_ID_LENGTH,
278 );
279 const name = resourceName(parent, branch, documentId);
280 const jsonData = news.jsonData;
281 const body: discoveryengine.GoogleCloudDiscoveryengineV1Document = {
282 id: documentId,
283 jsonData,
284 structData: news.jsonData === undefined ? news.structData : undefined,
285 content: news.content,
286 parentDocumentId: news.parentDocumentId,
287 schemaId: news.schemaId,
288 };
289
290 let current = yield* getByName(output?.name ?? name);
291
292 if (current === undefined) {
293 const created = yield* discoveryengine
294 .createProjectsLocationsDataStoresBranchesDocuments({
295 parent: branchParent(parent, branch),
296 documentId,
297 body,
298 })
299 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
300 current = created ?? undefined;
301 }
302
303 if (current === undefined) {
304 return yield* new DataStoresBranchesDocumentNotResolved({ name });
305 }
306
307 const jsonChanged =
308 fingerprint(current.jsonData) !== fingerprint(jsonData);
309 const structChanged =
310 news.jsonData === undefined &&
311 fingerprint(current.structData) !== fingerprint(news.structData);
312 const contentChanged =
313 fingerprint(current.content) !== fingerprint(news.content);
314 const parentChanged =
315 news.parentDocumentId !== undefined &&
316 (current.parentDocumentId ?? "") !== news.parentDocumentId;
317 const schemaChanged =
318 news.schemaId !== undefined &&
319 (current.schemaId ?? "") !== news.schemaId;
320
321 if (
322 jsonChanged ||
323 structChanged ||
324 contentChanged ||
325 parentChanged ||
326 schemaChanged
327 ) {
328 current =
329 yield* discoveryengine.patchProjectsLocationsDataStoresBranchesDocuments(
330 {
331 name: current.name ?? name,
332 allowMissing: true,
333 updateMask: [
334 jsonChanged ? "json_data" : undefined,
335 structChanged ? "struct_data" : undefined,
336 contentChanged ? "content" : undefined,
337 parentChanged ? "parent_document_id" : undefined,
338 schemaChanged ? "schema_id" : undefined,
339 ]
340 .filter((field): field is string => field !== undefined)
341 .join(","),
342 body: { ...body, name: current.name ?? name },
343 },
344 );
345 }
346
347 return toAttrs(current, env.project);
348 }),
349
350 delete: Effect.fn(function* ({ output }) {
351 const existing = yield* getByName(output.name);
352 if (existing === undefined) return;
353 yield* discoveryengine
354 .deleteProjectsLocationsDataStoresBranchesDocuments({
355 name: output.name,
356 })
357 .pipe(Effect.catchTag("NotFound", () => Effect.void));
358 }),
359 });