1import * as healthcare from "@distilled.cloud/gcp/healthcare_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 { tagRecord } from "../../Tags.ts";
9import { GcpEnvironment } from "../Environment.ts";
10import {
11 createInternalLabels,
12 diffLabels,
13 hasAlchemyLabels,
14 toLabels,
15} from "../Labels.ts";
16import type { Providers } from "../Providers.ts";
17import {
18 collectPages,
19 expandParent,
20 forEachDataset,
21 hasAlchemyLabelMap,
22 normalizeLocation,
23 parseResourceName,
24 replaceOnIdentity,
25 retryTransient,
26 toPhysicalId,
27 updateMaskOf,
28 userLabels,
29 waitUntilGone,
30} from "./internal.ts";
31
32const DEFAULT_VERSION = "R4";
33
34export type DatasetsFhirStoreProps = {
35 /**
36 * Parent dataset. Full name
37 * `projects/{project}/locations/{location}/datasets/{dataset}` or the
38 * dataset id (combined with `location`). Immutable — changing it
39 * replaces the FHIR store.
40 */
41 dataset: string;
42 /**
43 * Region used when `dataset` is a bare id.
44 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
45 */
46 location?: string;
47 /**
48 * FHIR store id. Must match letters, numbers, underscores, hyphens,
49 * and periods; 1-256 characters. Immutable — changing it replaces the
50 * store.
51 */
52 fhirStoreId?: string;
53 /**
54 * Native FHIR version. Immutable — changing it replaces the store.
55 * @default "R4"
56 */
57 version?: healthcare.FhirStoreVersionEnum | (string & {});
58 /**
59 * User labels. Alchemy ownership labels are merged in automatically.
60 */
61 labels?: Record<string, string>;
62 /**
63 * Allow Update to create a resource with a client-specified id.
64 * @default false
65 */
66 enableUpdateCreate?: boolean;
67 /**
68 * Disable referential integrity checks. Immutable — changing it
69 * replaces the store.
70 * @default false
71 */
72 disableReferentialIntegrity?: boolean;
73 /**
74 * Disable resource versioning. Immutable — changing it replaces the
75 * store.
76 * @default false
77 */
78 disableResourceVersioning?: boolean;
79 /**
80 * Use `handling=strict` as the default FHIR search behavior.
81 * @default false
82 */
83 defaultSearchHandlingStrict?: boolean;
84};
85
86export type DatasetsFhirStore = Resource<
87 "GCP.Healthcare.DatasetsFhirStore",
88 DatasetsFhirStoreProps,
89 {
90 /** Full resource name `.../datasets/{dataset}/fhirStores/{fhirStore}`. */
91 name: string;
92 /** FHIR store id. */
93 fhirStoreId: string;
94 /** Parent dataset resource name. */
95 dataset: string;
96 /** Project id. */
97 project: string;
98 /** Location id. */
99 location: string;
100 /** Native FHIR version. */
101 version: string;
102 /** User labels (Alchemy ownership labels stripped). */
103 labels: Record<string, string>;
104 /** Whether Update can create. */
105 enableUpdateCreate: boolean;
106 /** Whether referential integrity is disabled. */
107 disableReferentialIntegrity: boolean;
108 /** Whether resource versioning is disabled. */
109 disableResourceVersioning: boolean;
110 /** Whether default search handling is strict. */
111 defaultSearchHandlingStrict: boolean;
112 },
113 never,
114 Providers
115>;
116
117/**
118 * A Cloud Healthcare FHIR store inside a dataset.
119 *
120 * Dataset, store id, location, FHIR version, referential-integrity, and
121 * versioning flags are immutable. Labels, `enableUpdateCreate`, and
122 * search-handling update in place.
123 *
124 * ### Creating a FHIR Store
125 * **Example:** R4 store with labels
126 * ```typescript
127 * const store = yield* GCP.Healthcare.DatasetsFhirStore("Records", {
128 * dataset: dataset.name,
129 * version: "R4",
130 * labels: { env: "test" },
131 * });
132 * ```
133 *
134 * **Example:** Named store that allows client-assigned ids
135 * ```typescript
136 * const store = yield* GCP.Healthcare.DatasetsFhirStore("Records", {
137 * dataset: dataset.name,
138 * fhirStoreId: "clinic-fhir",
139 * version: "R4",
140 * enableUpdateCreate: true,
141 * });
142 * ```
143 *
144 * @resource
145 * @category Healthcare
146 */
147export const DatasetsFhirStore = Resource<DatasetsFhirStore>(
148 "GCP.Healthcare.DatasetsFhirStore",
149);
150
151export class DatasetsFhirStoreNotResolved extends Data.TaggedError(
152 "GCP.Healthcare.DatasetsFhirStoreNotResolved",
153)<{
154 name: string;
155}> {}
156
157const datasetOf = (dataset: string, project: string, location: string) =>
158 expandParent(dataset, project, location, "datasets");
159
160const resourceName = (dataset: string, fhirStoreId: string) =>
161 `${dataset}/fhirStores/${fhirStoreId}`;
162
163const toAttrs = (
164 store: healthcare.FhirStore,
165 project: string,
166 region: string,
167) => {
168 const name = store.name ?? "";
169 const parsed = parseResourceName(name, "fhirStores", region);
170 return {
171 name,
172 fhirStoreId: parsed.id,
173 dataset: parsed.parent,
174 project: parsed.project || project,
175 location: parsed.location,
176 version: store.version ?? DEFAULT_VERSION,
177 labels: userLabels(store.labels),
178 enableUpdateCreate: store.enableUpdateCreate === true,
179 disableReferentialIntegrity: store.disableReferentialIntegrity === true,
180 disableResourceVersioning: store.disableResourceVersioning === true,
181 defaultSearchHandlingStrict: store.defaultSearchHandlingStrict === true,
182 };
183};
184
185const getByName = (name: string) =>
186 name.length === 0
187 ? Effect.succeed(undefined)
188 : healthcare
189 .getProjectsLocationsDatasetsFhirStores({ name })
190 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
191
192export const DatasetsFhirStoreProvider = () =>
193 Provider.succeed(DatasetsFhirStore, {
194 stables: [
195 "name",
196 "fhirStoreId",
197 "dataset",
198 "project",
199 "location",
200 "version",
201 "disableReferentialIntegrity",
202 "disableResourceVersioning",
203 ],
204
205 diff: Effect.fn(function* ({ news, olds, output }) {
206 if (!isResolved(news)) return undefined;
207 const env = yield* GcpEnvironment.current;
208 const previousVersion = olds?.version ?? output?.version;
209 const nextVersion = news.version ?? DEFAULT_VERSION;
210 const extra =
211 (previousVersion !== undefined && previousVersion !== nextVersion) ||
212 ((olds?.disableReferentialIntegrity ??
213 output?.disableReferentialIntegrity) ===
214 true) !==
215 (news.disableReferentialIntegrity === true) ||
216 ((olds?.disableResourceVersioning ??
217 output?.disableResourceVersioning) ===
218 true) !==
219 (news.disableResourceVersioning === true);
220 return replaceOnIdentity({
221 previousId: olds?.fhirStoreId ?? output?.fhirStoreId,
222 nextId: news.fhirStoreId,
223 previousParent: olds?.dataset ?? output?.dataset,
224 nextParent: datasetOf(
225 news.dataset,
226 env.project,
227 normalizeLocation(news.location ?? output?.location, env.region),
228 ),
229 extra,
230 });
231 }),
232
233 read: Effect.fn(function* ({ id, olds, output }) {
234 const env = yield* GcpEnvironment.current;
235 const location = normalizeLocation(
236 olds?.location ?? output?.location,
237 env.region,
238 );
239 const fhirStoreId = yield* toPhysicalId(
240 id,
241 olds?.fhirStoreId,
242 output?.fhirStoreId,
243 );
244 const dataset =
245 olds?.dataset !== undefined
246 ? datasetOf(olds.dataset, env.project, location)
247 : (output?.dataset ?? "");
248 const name =
249 output?.name ??
250 (dataset.length > 0 ? resourceName(dataset, fhirStoreId) : "");
251 const existing = yield* getByName(name);
252 if (existing === undefined) return undefined;
253 const attrs = toAttrs(existing, env.project, env.region);
254 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
255 ? attrs
256 : Unowned(attrs);
257 }),
258
259 list: () =>
260 Effect.gen(function* () {
261 const env = yield* GcpEnvironment.current;
262 const stores = yield* forEachDataset(
263 env.project,
264 env.region,
265 (parent) =>
266 collectPages(
267 healthcare.listProjectsLocationsDatasetsFhirStores.pages({
268 parent,
269 pageSize: 1000,
270 }),
271 (page) => page.fhirStores,
272 ),
273 );
274 return stores
275 .filter((store) => hasAlchemyLabelMap(store.labels))
276 .map((store) => toAttrs(store, env.project, env.region));
277 }),
278
279 reconcile: Effect.fn(function* ({ id, news, output }) {
280 const env = yield* GcpEnvironment.current;
281 const location = normalizeLocation(
282 news.location ?? output?.location,
283 env.region,
284 );
285 const dataset = datasetOf(news.dataset, env.project, location);
286 const fhirStoreId = yield* toPhysicalId(
287 id,
288 news.fhirStoreId,
289 output?.fhirStoreId,
290 );
291 const name = output?.name ?? resourceName(dataset, fhirStoreId);
292 const version = news.version ?? DEFAULT_VERSION;
293 const desiredLabels = {
294 ...toLabels(news.labels),
295 ...(yield* createInternalLabels(id)),
296 };
297
298 let current = yield* getByName(name);
299
300 if (current === undefined) {
301 const created = yield* retryTransient(
302 healthcare.createProjectsLocationsDatasetsFhirStores({
303 parent: dataset,
304 fhirStoreId,
305 body: {
306 version,
307 labels: desiredLabels,
308 enableUpdateCreate: news.enableUpdateCreate,
309 disableReferentialIntegrity: news.disableReferentialIntegrity,
310 disableResourceVersioning: news.disableResourceVersioning,
311 defaultSearchHandlingStrict: news.defaultSearchHandlingStrict,
312 },
313 }),
314 ).pipe(Effect.catchTag("Conflict", () => getByName(name)));
315 current = created ?? undefined;
316 }
317
318 if (current === undefined) {
319 return yield* new DatasetsFhirStoreNotResolved({ name });
320 }
321
322 const currentName = current.name ?? name;
323 const observedLabels = tagRecord(current.labels);
324 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
325 const labelsChanged = upsert.length > 0 || removed.length > 0;
326 const updateCreateChanged =
327 (current.enableUpdateCreate === true) !==
328 (news.enableUpdateCreate === true);
329 const searchChanged =
330 (current.defaultSearchHandlingStrict === true) !==
331 (news.defaultSearchHandlingStrict === true);
332
333 if (labelsChanged || updateCreateChanged || searchChanged) {
334 current = yield* retryTransient(
335 healthcare.patchProjectsLocationsDatasetsFhirStores({
336 name: currentName,
337 updateMask: updateMaskOf(
338 labelsChanged ? "labels" : undefined,
339 updateCreateChanged ? "enableUpdateCreate" : undefined,
340 searchChanged ? "defaultSearchHandlingStrict" : undefined,
341 ),
342 body: {
343 labels: desiredLabels,
344 enableUpdateCreate: news.enableUpdateCreate,
345 defaultSearchHandlingStrict: news.defaultSearchHandlingStrict,
346 },
347 }),
348 );
349 }
350
351 return toAttrs(current, env.project, env.region);
352 }),
353
354 delete: Effect.fn(function* ({ output }) {
355 if (!output.name) return;
356 yield* retryTransient(
357 healthcare.deleteProjectsLocationsDatasetsFhirStores({
358 name: output.name,
359 }),
360 ).pipe(Effect.catchTag("NotFound", () => Effect.void));
361 yield* waitUntilGone(getByName(output.name), output.name);
362 }),
363 });