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 MAX_DOCUMENT_ID_LENGTH,
13 branchParent,
14 parentOf,
15 parseResourceName,
16 rfc1035,
17 sameJson,
18 toPhysical,
19} from "./internal.ts";
20
21export type DocumentContent = {
22 /** MIME type of `rawBytes` or the object at `uri`. */
23 mimeType?: string;
24 /** GCS URI (`gs://bucket/path`). */
25 uri?: string;
26 /** Base64-encoded bytes. Max 1,000,000 bytes. */
27 rawBytes?: string;
28};
29
30export type CollectionsDataStoresBranchesDocumentProps = {
31 /**
32 * Parent data store resource name
33 * `projects/{project}/locations/{location}/collections/{collection}/dataStores/{data_store}`.
34 * Immutable — changing it replaces the document.
35 */
36 dataStore: string;
37 /**
38 * Branch id. Immutable.
39 * @default "default_branch"
40 */
41 branchId?: string;
42 /**
43 * Document id (RFC-1034, max 128 characters). If omitted, a unique id
44 * is generated. Immutable — changing it replaces the document.
45 */
46 documentId?: string;
47 /**
48 * JSON document payload. Must conform to the data store schema.
49 */
50 jsonData?: string;
51 /**
52 * Structured document payload.
53 */
54 structData?: Record<string, unknown>;
55 /**
56 * Unstructured content. Required when the parent data store uses
57 * `CONTENT_REQUIRED`.
58 */
59 content?: DocumentContent;
60 /**
61 * Parent document id (RFC-1034, max 63 characters).
62 */
63 parentDocumentId?: string;
64 /**
65 * Schema id in the same data store.
66 */
67 schemaId?: string;
68};
69
70export type CollectionsDataStoresBranchesDocument = Resource<
71 "GCP.DiscoveryEngine.CollectionsDataStoresBranchesDocument",
72 CollectionsDataStoresBranchesDocumentProps,
73 {
74 /** Full resource name. */
75 name: string;
76 /** Document id. */
77 documentId: string;
78 /** Parent data store resource name. */
79 dataStore: string;
80 /** Branch id. */
81 branchId: string;
82 /** Project id. */
83 project: string;
84 /** Location id. */
85 location: string;
86 /** JSON payload. */
87 jsonData: string | undefined;
88 /** Structured payload. */
89 structData: Record<string, unknown> | undefined;
90 /** Unstructured content. */
91 content: DocumentContent | undefined;
92 /** Parent document id. */
93 parentDocumentId: string | undefined;
94 /** Schema id. */
95 schemaId: string | undefined;
96 /** Last index time. */
97 indexTime: string | undefined;
98 },
99 never,
100 Providers
101>;
102
103/**
104 * A Discovery Engine document under a collection data store branch.
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 * Parent data store, branch, and document id are immutable. Payload fields
109 * update in place.
110 *
111 * ### Creating a Document
112 * **Example:** JSON document on the default branch
113 * ```typescript
114 * const store = yield* GCP.DiscoveryEngine.CollectionsDataStore("Docs", {});
115 * const doc = yield* GCP.DiscoveryEngine.CollectionsDataStoresBranchesDocument(
116 * "Intro",
117 * {
118 * dataStore: store.name,
119 * jsonData: JSON.stringify({ title: "Getting started" }),
120 * },
121 * );
122 * ```
123 *
124 * @resource
125 * @category DiscoveryEngine
126 */
127export const CollectionsDataStoresBranchesDocument =
128 Resource<CollectionsDataStoresBranchesDocument>(
129 "GCP.DiscoveryEngine.CollectionsDataStoresBranchesDocument",
130 );
131
132export class CollectionsDataStoresBranchesDocumentNotResolved extends Data.TaggedError(
133 "GCP.DiscoveryEngine.CollectionsDataStoresBranchesDocumentNotResolved",
134)<{
135 name: string;
136}> {}
137
138const contentOf = (
139 content:
140 | discoveryengine.GoogleCloudDiscoveryengineV1DocumentContent
141 | undefined,
142): DocumentContent | undefined =>
143 content === undefined
144 ? undefined
145 : {
146 mimeType: content.mimeType,
147 uri: content.uri,
148 rawBytes: content.rawBytes,
149 };
150
151const toAttrs = (
152 document: discoveryengine.GoogleCloudDiscoveryengineV1Document,
153 project: string,
154) => {
155 const name = document.name ?? "";
156 const parsed = parseResourceName(name, "documents");
157 const branches = parseResourceName(name, "branches");
158 return {
159 name,
160 documentId: document.id ?? parsed.id,
161 dataStore: parentOf(name, "branches"),
162 branchId: branches.id,
163 project: parsed.project || project,
164 location: parsed.location,
165 jsonData: document.jsonData,
166 structData: document.structData as Record<string, unknown> | undefined,
167 content: contentOf(document.content),
168 parentDocumentId: document.parentDocumentId,
169 schemaId: document.schemaId,
170 indexTime: document.indexTime,
171 };
172};
173
174const resourceName = (
175 dataStore: string,
176 branchId: string,
177 documentId: string,
178) => `${branchParent(dataStore, branchId)}/documents/${documentId}`;
179
180const getByName = (name: string) =>
181 name.length === 0
182 ? Effect.succeed(undefined)
183 : discoveryengine
184 .getProjectsLocationsCollectionsDataStoresBranchesDocuments({ name })
185 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
186
187export const CollectionsDataStoresBranchesDocumentProvider = () =>
188 Provider.succeed(CollectionsDataStoresBranchesDocument, {
189 stables: [
190 "name",
191 "documentId",
192 "dataStore",
193 "branchId",
194 "project",
195 "location",
196 ],
197
198 diff: Effect.fn(function* ({ news, olds, output }) {
199 if (!isResolved(news)) return undefined;
200 const previousParent = olds?.dataStore ?? output?.dataStore;
201 const previousBranch =
202 olds?.branchId ?? output?.branchId ?? DEFAULT_BRANCH;
203 const nextBranch = news.branchId ?? previousBranch;
204 const previousId = olds?.documentId ?? output?.documentId;
205 if (
206 (previousParent !== undefined && news.dataStore !== previousParent) ||
207 previousBranch !== nextBranch ||
208 (previousId !== undefined &&
209 news.documentId !== undefined &&
210 news.documentId !== previousId)
211 ) {
212 return {
213 action: "replace" as const,
214 deleteFirst:
215 previousParent === news.dataStore &&
216 previousBranch === nextBranch &&
217 previousId !== undefined &&
218 news.documentId === previousId,
219 };
220 }
221 return undefined;
222 }),
223
224 read: Effect.fn(function* ({ id, olds, output }) {
225 const env = yield* GcpEnvironment.current;
226 const dataStore = olds?.dataStore ?? output?.dataStore;
227 const branchId = olds?.branchId ?? output?.branchId ?? DEFAULT_BRANCH;
228 const documentId = yield* toPhysical(
229 id,
230 olds?.documentId,
231 output?.documentId,
232 (name) => rfc1035(name, MAX_DOCUMENT_ID_LENGTH),
233 MAX_DOCUMENT_ID_LENGTH,
234 );
235 const name =
236 output?.name ??
237 (dataStore !== undefined
238 ? resourceName(dataStore, branchId, documentId)
239 : undefined);
240 if (name === undefined) return undefined;
241 const existing = yield* getByName(name);
242 if (existing === undefined) return undefined;
243 const attrs = toAttrs(existing, env.project);
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* ({ id, news, output }) {
249 const env = yield* GcpEnvironment.current;
250 const branchId = news.branchId ?? output?.branchId ?? DEFAULT_BRANCH;
251 const documentId = yield* toPhysical(
252 id,
253 news.documentId,
254 output?.documentId,
255 (name) => rfc1035(name, MAX_DOCUMENT_ID_LENGTH),
256 MAX_DOCUMENT_ID_LENGTH,
257 );
258 const name = resourceName(news.dataStore, branchId, documentId);
259 const jsonData = news.jsonData;
260 const parent = branchParent(news.dataStore, branchId);
261
262 let current = yield* getByName(output?.name ?? name);
263
264 if (current === undefined) {
265 const created = yield* discoveryengine
266 .createProjectsLocationsCollectionsDataStoresBranchesDocuments({
267 parent,
268 documentId,
269 body: {
270 jsonData,
271 structData: news.structData,
272 content: news.content,
273 parentDocumentId: news.parentDocumentId,
274 schemaId: news.schemaId,
275 },
276 })
277 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
278 current = created ?? undefined;
279 }
280
281 if (current === undefined) {
282 return yield* new CollectionsDataStoresBranchesDocumentNotResolved({
283 name,
284 });
285 }
286
287 const resource = current.name ?? name;
288 const jsonChanged =
289 jsonData !== undefined && (current.jsonData ?? "") !== jsonData;
290 const structChanged = !sameJson(current.structData, news.structData);
291 const contentChanged = !sameJson(
292 contentOf(current.content),
293 news.content,
294 );
295 const parentChanged =
296 (current.parentDocumentId ?? "") !== (news.parentDocumentId ?? "");
297 const schemaChanged = (current.schemaId ?? "") !== (news.schemaId ?? "");
298
299 if (
300 jsonChanged ||
301 structChanged ||
302 contentChanged ||
303 parentChanged ||
304 schemaChanged
305 ) {
306 current =
307 yield* discoveryengine.patchProjectsLocationsCollectionsDataStoresBranchesDocuments(
308 {
309 name: resource,
310 body: {
311 name: resource,
312 jsonData,
313 structData: news.structData,
314 content: news.content,
315 parentDocumentId: news.parentDocumentId,
316 schemaId: news.schemaId,
317 },
318 },
319 );
320 }
321
322 return toAttrs(current, env.project);
323 }),
324
325 delete: Effect.fn(function* ({ output }) {
326 yield* discoveryengine
327 .deleteProjectsLocationsCollectionsDataStoresBranchesDocuments({
328 name: output.name,
329 })
330 .pipe(Effect.catchTag("NotFound", () => Effect.void));
331 }),
332 });