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 MAX_CONTEXT_LENGTH,
11 ensureProject,
12 locationParent,
13 normalizeLocation,
14 parseName,
15 replaceOnIdentity,
16 rfc1035,
17 sameJson,
18 sameText,
19 toPhysicalId,
20 userSynonyms,
21 waitUntilExists,
22 waitUntilGone,
23 ResourceNotResolved,
24} from "./internal.ts";
25
26export type Synonym = cw.GoogleCloudContentwarehouseV1SynonymSetSynonym;
27
28export type SynonymSetProps = {
29 /**
30 * Context id (the `{context}` segment of
31 * `projects/{project}/locations/{location}/synonymSets/{context}`).
32 * If omitted, a unique RFC1035 context is generated. Immutable —
33 * changing it replaces the synonym set.
34 */
35 context?: string;
36 /**
37 * Multi-region location (`us` or `eu`). Immutable — changing it
38 * replaces the synonym set. `US` is accepted and normalized to `us`.
39 * @default "us"
40 */
41 location?: string;
42 /**
43 * Groups of synonymous words.
44 */
45 synonyms?: Synonym[];
46};
47
48export type SynonymSet = Resource<
49 "GCP.ContentWarehouse.SynonymSet",
50 SynonymSetProps,
51 {
52 /** Full resource name. */
53 name: string;
54 /** Context id (last path segment). */
55 context: string;
56 /** Project id. */
57 project: string;
58 /** Location id. */
59 location: string;
60 /** Synonym groups. */
61 synonyms: Synonym[] | undefined;
62 },
63 never,
64 Providers
65>;
66
67const DEFAULT_SYNONYMS: Synonym[] = [{ words: ["sale", "invoice", "bill"] }];
68
69/**
70 * A Document AI Warehouse synonym set — custom search synonyms for a
71 * query context (`sales`, `engineering`, …).
72 *
73 * Synonym sets have no labels field; Alchemy identifies one by its
74 * context. Without state, a set found under an explicit `context` is
75 * reported as unowned. Context and location are immutable. Synonym groups update in place.
76 *
77 * ### Creating a Synonym Set
78 * **Example:** Sales synonyms
79 * ```typescript
80 * const synonyms = yield* GCP.ContentWarehouse.SynonymSet("Sales", {
81 * context: "sales",
82 * synonyms: [{ words: ["sale", "invoice", "bill"] }],
83 * });
84 * ```
85 *
86 * @resource
87 * @category ContentWarehouse
88 */
89export const SynonymSet = Resource<SynonymSet>(
90 "GCP.ContentWarehouse.SynonymSet",
91);
92
93const resourceName = (project: string, location: string, context: string) =>
94 `${locationParent(project, location)}/synonymSets/${context}`;
95
96const contextOf = (
97 id: string,
98 explicit: string | undefined,
99 existing: string | undefined,
100) =>
101 Effect.gen(function* () {
102 if (explicit !== undefined) {
103 return rfc1035(explicit, "sales").slice(0, MAX_CONTEXT_LENGTH);
104 }
105 if (existing !== undefined) return existing;
106 return yield* toPhysicalId(id, undefined, undefined, "syn");
107 });
108
109const toAttrs = (
110 item: cw.GoogleCloudContentwarehouseV1SynonymSet,
111 project: string,
112) => {
113 const name = item.name ?? "";
114 const parsed = parseName(name, "synonymSets");
115 return {
116 name,
117 context: item.context ?? parsed.id,
118 project: parsed.project || project,
119 location: parsed.location,
120 synonyms: userSynonyms(item.synonyms),
121 };
122};
123
124const getByName = (name: string) =>
125 name.length === 0
126 ? Effect.succeed(undefined)
127 : cw
128 .getProjectsLocationsSynonymSets({ name })
129 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
130
131export const SynonymSetProvider = () =>
132 Provider.succeed(SynonymSet, {
133 stables: ["name", "context", "project", "location"],
134
135 diff: Effect.fn(function* ({ news, olds, output }) {
136 if (!isResolved(news)) return undefined;
137 return replaceOnIdentity({
138 previousId: olds?.context ?? output?.context,
139 nextId: news.context,
140 previousLocation: olds?.location ?? output?.location,
141 nextLocation: news.location ?? olds?.location ?? output?.location,
142 });
143 }),
144
145 read: Effect.fn(function* ({ id, olds, output }) {
146 const env = yield* GcpEnvironment.current;
147 const location = normalizeLocation(olds?.location ?? output?.location);
148 const context = yield* contextOf(id, olds?.context, output?.context);
149 const existing = yield* getByName(
150 output?.name ?? resourceName(env.project, location, context),
151 );
152 if (existing === undefined) return undefined;
153 const attrs = toAttrs(existing, env.project);
154 // State or the generated context proves ownership.
155 return output !== undefined || olds?.context === undefined
156 ? attrs
157 : Unowned(attrs);
158 }),
159
160 reconcile: Effect.fn(function* ({ id, news, output }) {
161 const env = yield* GcpEnvironment.current;
162 const location = normalizeLocation(news.location ?? output?.location);
163 const parent = locationParent(env.project, location);
164 yield* ensureProject(parent);
165 const context = yield* contextOf(id, news.context, output?.context);
166 const name = resourceName(env.project, location, context);
167 const synonyms = news.synonyms ?? DEFAULT_SYNONYMS;
168
169 let current = yield* getByName(output?.name ?? name);
170
171 if (current === undefined) {
172 const created = yield* cw
173 .createProjectsLocationsSynonymSets({
174 parent,
175 body: { context, synonyms },
176 })
177 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
178 current = created ?? (yield* waitUntilExists(getByName(name), name));
179 }
180
181 if (current === undefined) {
182 return yield* new ResourceNotResolved({ name });
183 }
184
185 const currentName = current.name ?? name;
186 const contextChanged = !sameText(current.context, context);
187 // `current.synonyms` may still hold an ownership group written by an
188 // earlier version; comparing raw groups removes it on the next patch.
189 const synonymsChanged = !sameJson(current.synonyms ?? [], synonyms);
190 if (contextChanged || synonymsChanged) {
191 current = yield* cw.patchProjectsLocationsSynonymSets({
192 name: currentName,
193 body: {
194 name: currentName,
195 context,
196 synonyms,
197 },
198 });
199 }
200
201 return toAttrs(current, env.project);
202 }),
203
204 delete: Effect.fn(function* ({ output }) {
205 yield* cw
206 .deleteProjectsLocationsSynonymSets({ name: output.name })
207 .pipe(Effect.catchTag("NotFound", () => Effect.void));
208 yield* waitUntilGone(getByName(output.name), output.name);
209 }),
210 });