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 sameJson,
27 toPhysicalId,
28 updateMaskOf,
29 userLabels,
30 waitUntilGone,
31} from "./internal.ts";
32
33export type Hl7V2NotificationConfig = {
34 /** Pub/Sub topic for matching messages. */
35 pubsubTopic?: string;
36 /** Optional filter restricting which messages notify. */
37 filter?: string;
38};
39
40export type Hl7V2ParserConfig = {
41 /** Allow messages with no header. */
42 allowNullHeader?: boolean;
43 /**
44 * Parser version. Immutable after create.
45 */
46 version?: healthcare.ParserConfigVersionEnum | (string & {});
47};
48
49export type DatasetsHl7V2StoreProps = {
50 /**
51 * Parent dataset. Full name
52 * `projects/{project}/locations/{location}/datasets/{dataset}` or the
53 * dataset id (combined with `location`). Immutable — changing it
54 * replaces the HL7v2 store.
55 */
56 dataset: string;
57 /**
58 * Region used when `dataset` is a bare id.
59 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
60 */
61 location?: string;
62 /**
63 * HL7v2 store id. Must match letters, numbers, underscores, hyphens,
64 * and periods; 1-256 characters. Immutable — changing it replaces the
65 * store.
66 */
67 hl7V2StoreId?: string;
68 /**
69 * User labels. Alchemy ownership labels are merged in automatically.
70 */
71 labels?: Record<string, string>;
72 /**
73 * Reject ingest/create of duplicate raw messages.
74 * @default false
75 */
76 rejectDuplicateMessage?: boolean;
77 /**
78 * Parser configuration. `version` is immutable after create.
79 */
80 parserConfig?: Hl7V2ParserConfig;
81 /**
82 * Destinations for filtered message notifications.
83 */
84 notificationConfigs?: Hl7V2NotificationConfig[];
85};
86
87export type DatasetsHl7V2Store = Resource<
88 "GCP.Healthcare.DatasetsHl7V2Store",
89 DatasetsHl7V2StoreProps,
90 {
91 /** Full resource name `.../datasets/{dataset}/hl7V2Stores/{hl7V2Store}`. */
92 name: string;
93 /** HL7v2 store id. */
94 hl7V2StoreId: string;
95 /** Parent dataset resource name. */
96 dataset: string;
97 /** Project id. */
98 project: string;
99 /** Location id. */
100 location: string;
101 /** User labels (Alchemy ownership labels stripped). */
102 labels: Record<string, string>;
103 /** Whether duplicate messages are rejected. */
104 rejectDuplicateMessage: boolean;
105 /** Parser configuration. */
106 parserConfig: Hl7V2ParserConfig | undefined;
107 /** Notification destinations. */
108 notificationConfigs: Hl7V2NotificationConfig[] | undefined;
109 },
110 never,
111 Providers
112>;
113
114/**
115 * A Cloud Healthcare HL7v2 store inside a dataset.
116 *
117 * Dataset, store id, location, and parser version are immutable. Labels,
118 * duplicate rejection, `allowNullHeader`, and notification configs update
119 * in place.
120 *
121 * ### Creating an HL7v2 Store
122 * **Example:** Store with labels
123 * ```typescript
124 * const store = yield* GCP.Healthcare.DatasetsHl7V2Store("Adt", {
125 * dataset: dataset.name,
126 * labels: { env: "test" },
127 * });
128 * ```
129 *
130 * **Example:** Named store that rejects duplicates
131 * ```typescript
132 * const store = yield* GCP.Healthcare.DatasetsHl7V2Store("Adt", {
133 * dataset: dataset.name,
134 * hl7V2StoreId: "clinic-adt",
135 * rejectDuplicateMessage: true,
136 * });
137 * ```
138 *
139 * @resource
140 * @category Healthcare
141 */
142export const DatasetsHl7V2Store = Resource<DatasetsHl7V2Store>(
143 "GCP.Healthcare.DatasetsHl7V2Store",
144);
145
146export class DatasetsHl7V2StoreNotResolved extends Data.TaggedError(
147 "GCP.Healthcare.DatasetsHl7V2StoreNotResolved",
148)<{
149 name: string;
150}> {}
151
152const datasetOf = (dataset: string, project: string, location: string) =>
153 expandParent(dataset, project, location, "datasets");
154
155const resourceName = (dataset: string, hl7V2StoreId: string) =>
156 `${dataset}/hl7V2Stores/${hl7V2StoreId}`;
157
158const parserOf = (
159 config: healthcare.ParserConfig | undefined,
160): Hl7V2ParserConfig | undefined => {
161 if (config === undefined) return undefined;
162 return {
163 allowNullHeader: config.allowNullHeader,
164 version: config.version,
165 };
166};
167
168const toAttrs = (
169 store: healthcare.Hl7V2Store,
170 project: string,
171 region: string,
172) => {
173 const name = store.name ?? "";
174 const parsed = parseResourceName(name, "hl7V2Stores", region);
175 return {
176 name,
177 hl7V2StoreId: parsed.id,
178 dataset: parsed.parent,
179 project: parsed.project || project,
180 location: parsed.location,
181 labels: userLabels(store.labels),
182 rejectDuplicateMessage: store.rejectDuplicateMessage === true,
183 parserConfig: parserOf(store.parserConfig),
184 notificationConfigs: store.notificationConfigs,
185 };
186};
187
188const getByName = (name: string) =>
189 name.length === 0
190 ? Effect.succeed(undefined)
191 : healthcare
192 .getProjectsLocationsDatasetsHl7V2Stores({ name })
193 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
194
195export const DatasetsHl7V2StoreProvider = () =>
196 Provider.succeed(DatasetsHl7V2Store, {
197 stables: ["name", "hl7V2StoreId", "dataset", "project", "location"],
198
199 diff: Effect.fn(function* ({ news, olds, output }) {
200 if (!isResolved(news)) return undefined;
201 const env = yield* GcpEnvironment.current;
202 const previousVersion =
203 olds?.parserConfig?.version ?? output?.parserConfig?.version;
204 const nextVersion = news.parserConfig?.version;
205 const extra =
206 previousVersion !== undefined &&
207 nextVersion !== undefined &&
208 previousVersion !== nextVersion;
209 return replaceOnIdentity({
210 previousId: olds?.hl7V2StoreId ?? output?.hl7V2StoreId,
211 nextId: news.hl7V2StoreId,
212 previousParent: olds?.dataset ?? output?.dataset,
213 nextParent: datasetOf(
214 news.dataset,
215 env.project,
216 normalizeLocation(news.location ?? output?.location, env.region),
217 ),
218 extra,
219 });
220 }),
221
222 read: Effect.fn(function* ({ id, olds, output }) {
223 const env = yield* GcpEnvironment.current;
224 const location = normalizeLocation(
225 olds?.location ?? output?.location,
226 env.region,
227 );
228 const hl7V2StoreId = yield* toPhysicalId(
229 id,
230 olds?.hl7V2StoreId,
231 output?.hl7V2StoreId,
232 );
233 const dataset =
234 olds?.dataset !== undefined
235 ? datasetOf(olds.dataset, env.project, location)
236 : (output?.dataset ?? "");
237 const name =
238 output?.name ??
239 (dataset.length > 0 ? resourceName(dataset, hl7V2StoreId) : "");
240 const existing = yield* getByName(name);
241 if (existing === undefined) return undefined;
242 const attrs = toAttrs(existing, env.project, env.region);
243 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
244 ? attrs
245 : Unowned(attrs);
246 }),
247
248 list: () =>
249 Effect.gen(function* () {
250 const env = yield* GcpEnvironment.current;
251 const stores = yield* forEachDataset(
252 env.project,
253 env.region,
254 (parent) =>
255 collectPages(
256 healthcare.listProjectsLocationsDatasetsHl7V2Stores.pages({
257 parent,
258 pageSize: 1000,
259 }),
260 (page) => page.hl7V2Stores,
261 ),
262 );
263 return stores
264 .filter((store) => hasAlchemyLabelMap(store.labels))
265 .map((store) => toAttrs(store, env.project, env.region));
266 }),
267
268 reconcile: Effect.fn(function* ({ id, news, output }) {
269 const env = yield* GcpEnvironment.current;
270 const location = normalizeLocation(
271 news.location ?? output?.location,
272 env.region,
273 );
274 const dataset = datasetOf(news.dataset, env.project, location);
275 const hl7V2StoreId = yield* toPhysicalId(
276 id,
277 news.hl7V2StoreId,
278 output?.hl7V2StoreId,
279 );
280 const name = output?.name ?? resourceName(dataset, hl7V2StoreId);
281 const desiredLabels = {
282 ...toLabels(news.labels),
283 ...(yield* createInternalLabels(id)),
284 };
285
286 let current = yield* getByName(name);
287
288 if (current === undefined) {
289 const created = yield* retryTransient(
290 healthcare.createProjectsLocationsDatasetsHl7V2Stores({
291 parent: dataset,
292 hl7V2StoreId,
293 body: {
294 labels: desiredLabels,
295 rejectDuplicateMessage: news.rejectDuplicateMessage,
296 parserConfig: news.parserConfig,
297 notificationConfigs: news.notificationConfigs,
298 },
299 }),
300 ).pipe(Effect.catchTag("Conflict", () => getByName(name)));
301 current = created ?? undefined;
302 }
303
304 if (current === undefined) {
305 return yield* new DatasetsHl7V2StoreNotResolved({ name });
306 }
307
308 const currentName = current.name ?? name;
309 const observedLabels = tagRecord(current.labels);
310 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
311 const labelsChanged = upsert.length > 0 || removed.length > 0;
312 const rejectChanged =
313 (current.rejectDuplicateMessage === true) !==
314 (news.rejectDuplicateMessage === true);
315 const parserChanged =
316 news.parserConfig !== undefined &&
317 (current.parserConfig?.allowNullHeader === true) !==
318 (news.parserConfig.allowNullHeader === true);
319 const notificationsChanged = !sameJson(
320 current.notificationConfigs,
321 news.notificationConfigs,
322 );
323
324 if (
325 labelsChanged ||
326 rejectChanged ||
327 parserChanged ||
328 notificationsChanged
329 ) {
330 current = yield* retryTransient(
331 healthcare.patchProjectsLocationsDatasetsHl7V2Stores({
332 name: currentName,
333 updateMask: updateMaskOf(
334 labelsChanged ? "labels" : undefined,
335 rejectChanged ? "rejectDuplicateMessage" : undefined,
336 parserChanged ? "parserConfig.allowNullHeader" : undefined,
337 notificationsChanged ? "notificationConfigs" : undefined,
338 ),
339 body: {
340 labels: desiredLabels,
341 rejectDuplicateMessage: news.rejectDuplicateMessage,
342 parserConfig: news.parserConfig,
343 notificationConfigs: news.notificationConfigs,
344 },
345 }),
346 );
347 }
348
349 return toAttrs(current, env.project, env.region);
350 }),
351
352 delete: Effect.fn(function* ({ output }) {
353 if (!output.name) return;
354 yield* retryTransient(
355 healthcare.deleteProjectsLocationsDatasetsHl7V2Stores({
356 name: output.name,
357 }),
358 ).pipe(Effect.catchTag("NotFound", () => Effect.void));
359 yield* waitUntilGone(getByName(output.name), output.name);
360 }),
361 });