1import * as cw from "@distilled.cloud/gcp/contentwarehouse_v1";
2import * as Effect from "effect/Effect";
3import { Unowned } from "../../AdoptPolicy.ts";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { GcpEnvironment } from "../Environment.ts";
8import type { Providers } from "../Providers.ts";
9import {
10 ensureProject,
11 lastSegment,
12 locationParent,
13 normalizeLocation,
14 parseName,
15 replaceOnIdentity,
16 sameJson,
17 sameText,
18 toPhysicalId,
19 waitUntilExists,
20 waitUntilGone,
21 ResourceNotResolved,
22} from "./internal.ts";
23
24export type DocumentProperty = cw.GoogleCloudContentwarehouseV1Property;
25
26export type DocumentProps = {
27 /**
28 * Document id (the `{document}` segment of
29 * `projects/{project}/locations/{location}/documents/{document}`).
30 * Assigned by the API on create. Immutable — changing it replaces the
31 * document. Supply it to adopt an existing document.
32 */
33 documentId?: string;
34 /**
35 * Customer-assigned reference id, unique per project and location.
36 * If omitted, a unique id is generated and used for lookup.
37 * Immutable — changing it replaces the document.
38 */
39 referenceId?: string;
40 /**
41 * Multi-region location (`us` or `eu`). Immutable — changing it
42 * replaces the document. `US` is accepted and normalized to `us`.
43 * @default "us"
44 */
45 location?: string;
46 /**
47 * Parent document schema resource name
48 * `projects/{project}/locations/{location}/documentSchemas/{schema}`.
49 * Immutable — changing it replaces the document.
50 */
51 documentSchemaName: string;
52 /**
53 * Display name shown in the UI.
54 * @default the document's `referenceId`
55 */
56 displayName?: string;
57 /**
58 * Title describing the document.
59 */
60 title?: string;
61 /**
62 * Inline plain-text content.
63 */
64 plainText?: string;
65 /**
66 * Cloud Storage path of the raw document (`gs://bucket/object`).
67 */
68 rawDocumentPath?: string;
69 /**
70 * URI used to display the document.
71 */
72 displayUri?: string;
73 /**
74 * Category of the original content (`CONTENT_CATEGORY_IMAGE`, …).
75 */
76 contentCategory?: string;
77 /**
78 * When true, skip text extraction on create.
79 * @default true
80 */
81 textExtractionDisabled?: boolean;
82 /**
83 * User-supplied metadata properties matching the schema.
84 */
85 properties?: DocumentProperty[];
86};
87
88export type Document = Resource<
89 "GCP.ContentWarehouse.Document",
90 DocumentProps,
91 {
92 /** Full resource name. */
93 name: string;
94 /** Document id (last path segment). */
95 documentId: string;
96 /** Customer-assigned reference id. */
97 referenceId: string | undefined;
98 /** Project id. */
99 project: string;
100 /** Location id. */
101 location: string;
102 /** Parent document schema resource name. */
103 documentSchemaName: string | undefined;
104 /** Display name. */
105 displayName: string | undefined;
106 /** Title. */
107 title: string | undefined;
108 /** Inline plain-text content. */
109 plainText: string | undefined;
110 /** Cloud Storage path of the raw document. */
111 rawDocumentPath: string | undefined;
112 /** Display URI. */
113 displayUri: string | undefined;
114 /** Content category. */
115 contentCategory: string | undefined;
116 /** User-supplied metadata properties. */
117 properties: DocumentProperty[] | undefined;
118 /** Creator. */
119 creator: string | undefined;
120 /** RFC3339 creation timestamp. */
121 createTime: string | undefined;
122 /** RFC3339 last-update timestamp. */
123 updateTime: string | undefined;
124 },
125 never,
126 Providers
127>;
128
129/**
130 * A Document AI Warehouse document — structured content plus metadata
131 * that conforms to a {@link DocumentSchema}.
132 *
133 * Documents have no labels field, so Alchemy identifies a document by its
134 * `referenceId`: one found under the generated id is adopted, one found
135 * under an explicit `referenceId` without state is reported as unowned.
136 * Location, schema, and
137 * reference id are immutable. Display name, title, and plain text update
138 * in place.
139 *
140 * ### Creating a Document
141 * **Example:** Plain-text document
142 * ```typescript
143 * const schema = yield* GCP.ContentWarehouse.DocumentSchema("Note", {});
144 * const document = yield* GCP.ContentWarehouse.Document("Welcome", {
145 * documentSchemaName: schema.name,
146 * displayName: "welcome",
147 * plainText: "hello warehouse",
148 * });
149 * ```
150 *
151 * @resource
152 * @category ContentWarehouse
153 */
154export const Document = Resource<Document>("GCP.ContentWarehouse.Document");
155
156const resourceName = (project: string, location: string, documentId: string) =>
157 `${locationParent(project, location)}/documents/${documentId}`;
158
159const referenceName = (
160 project: string,
161 location: string,
162 referenceId: string,
163) =>
164 `${locationParent(project, location)}/documents/referenceId/${referenceId}`;
165
166const toAttrs = (
167 item: cw.GoogleCloudContentwarehouseV1Document,
168 project: string,
169) => {
170 const name = item.name ?? "";
171 const parsed = parseName(name, "documents");
172 const id = parsed.id === "referenceId" ? lastSegment(name) : parsed.id;
173 return {
174 name,
175 documentId: id,
176 referenceId: item.referenceId,
177 project: parsed.project || project,
178 location: parsed.location,
179 documentSchemaName: item.documentSchemaName,
180 displayName: item.displayName,
181 title: item.title,
182 plainText: item.plainText,
183 rawDocumentPath: item.rawDocumentPath,
184 displayUri: item.displayUri,
185 contentCategory: item.contentCategory,
186 properties: item.properties,
187 creator: item.creator,
188 createTime: item.createTime,
189 updateTime: item.updateTime,
190 };
191};
192
193const getByName = (name: string) =>
194 name.length === 0
195 ? Effect.succeed(undefined)
196 : cw
197 .getProjectsLocationsDocuments({ name })
198 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
199
200export const DocumentProvider = () =>
201 Provider.succeed(Document, {
202 stables: [
203 "name",
204 "documentId",
205 "referenceId",
206 "project",
207 "location",
208 "createTime",
209 ],
210
211 diff: Effect.fn(function* ({ news, olds, output }) {
212 if (!isResolved(news)) return undefined;
213 const schemaChanged =
214 (olds?.documentSchemaName ?? output?.documentSchemaName) !==
215 undefined &&
216 news.documentSchemaName !==
217 (olds?.documentSchemaName ?? output?.documentSchemaName);
218 return replaceOnIdentity({
219 previousId: olds?.referenceId ?? output?.referenceId,
220 nextId: news.referenceId,
221 previousLocation: olds?.location ?? output?.location,
222 nextLocation: news.location ?? olds?.location ?? output?.location,
223 extra: schemaChanged,
224 });
225 }),
226
227 read: Effect.fn(function* ({ id, olds, output }) {
228 const env = yield* GcpEnvironment.current;
229 const location = normalizeLocation(olds?.location ?? output?.location);
230 const documentId = olds?.documentId ?? output?.documentId;
231 const referenceId = olds?.referenceId ?? output?.referenceId;
232 const name =
233 output?.name ??
234 (documentId
235 ? resourceName(env.project, location, documentId)
236 : referenceId
237 ? referenceName(env.project, location, referenceId)
238 : "");
239 let existing = yield* getByName(name);
240 if (existing === undefined && referenceId) {
241 existing = yield* getByName(
242 referenceName(env.project, location, referenceId),
243 );
244 }
245 if (existing === undefined && output === undefined) {
246 // No labels: a document is identified by its reference id.
247 const generated = yield* toPhysicalId(id, undefined, undefined, "doc");
248 existing = yield* getByName(
249 referenceName(env.project, location, referenceId ?? generated),
250 );
251 if (existing === undefined) return undefined;
252 const attrs = toAttrs(existing, env.project);
253 return referenceId === undefined ? attrs : Unowned(attrs);
254 }
255 if (existing === undefined) return undefined;
256 return toAttrs(existing, env.project);
257 }),
258
259 reconcile: Effect.fn(function* ({ id, news, output }) {
260 const env = yield* GcpEnvironment.current;
261 const location = normalizeLocation(news.location ?? output?.location);
262 const parent = locationParent(env.project, location);
263 yield* ensureProject(parent);
264 const referenceId = yield* toPhysicalId(
265 id,
266 news.referenceId,
267 output?.referenceId,
268 "doc",
269 );
270 const displayName = news.displayName ?? referenceId;
271 const title = news.title ?? news.displayName ?? referenceId;
272 const textExtractionDisabled = news.textExtractionDisabled ?? true;
273 const lookup =
274 output?.name ?? referenceName(env.project, location, referenceId);
275
276 let current = yield* getByName(lookup);
277 if (current === undefined) {
278 current = yield* getByName(
279 referenceName(env.project, location, referenceId),
280 );
281 }
282
283 if (current === undefined) {
284 const created = yield* cw
285 .createProjectsLocationsDocuments({
286 parent,
287 body: {
288 document: {
289 displayName,
290 title,
291 plainText: news.plainText ?? title,
292 documentSchemaName: news.documentSchemaName,
293 referenceId,
294 rawDocumentPath: news.rawDocumentPath,
295 displayUri: news.displayUri,
296 contentCategory: news.contentCategory,
297 textExtractionDisabled,
298 properties: news.properties,
299 },
300 },
301 })
302 .pipe(
303 Effect.map((response) => response.document),
304 Effect.catchTag("Conflict", () =>
305 getByName(referenceName(env.project, location, referenceId)),
306 ),
307 );
308 current =
309 created ??
310 (yield* waitUntilExists(
311 getByName(referenceName(env.project, location, referenceId)),
312 referenceName(env.project, location, referenceId),
313 ));
314 }
315
316 if (current === undefined) {
317 return yield* new ResourceNotResolved({ name: lookup });
318 }
319
320 const currentName = current.name ?? lookup;
321 const displayChanged = !sameText(current.displayName, displayName);
322 const titleChanged = !sameText(current.title, title);
323 const textChanged = !sameText(current.plainText, news.plainText ?? title);
324 const uriChanged = !sameText(current.displayUri, news.displayUri);
325 const pathChanged = !sameText(
326 current.rawDocumentPath,
327 news.rawDocumentPath,
328 );
329 const propertiesChanged = !sameJson(current.properties, news.properties);
330 if (
331 displayChanged ||
332 titleChanged ||
333 textChanged ||
334 uriChanged ||
335 pathChanged ||
336 propertiesChanged
337 ) {
338 const updated = yield* cw.patchProjectsLocationsDocuments({
339 name: currentName,
340 body: {
341 document: {
342 name: currentName,
343 displayName,
344 title,
345 plainText: news.plainText ?? title,
346 documentSchemaName: news.documentSchemaName,
347 referenceId,
348 rawDocumentPath: news.rawDocumentPath,
349 displayUri: news.displayUri,
350 contentCategory: news.contentCategory,
351 properties: news.properties,
352 },
353 updateOptions: {
354 updateType: "UPDATE_TYPE_REPLACE",
355 },
356 },
357 });
358 current = updated.document ?? current;
359 }
360
361 return toAttrs(current, env.project);
362 }),
363
364 delete: Effect.fn(function* ({ output }) {
365 yield* cw
366 .deleteProjectsLocationsDocuments({ name: output.name })
367 .pipe(Effect.catchTag("NotFound", () => Effect.void));
368 yield* waitUntilGone(getByName(output.name), output.name);
369 }),
370 });