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 { createInternalLabels, hasAlchemyLabels } from "../Labels.ts";
11import type { Providers } from "../Providers.ts";
12import {
13 collectPages,
14 expandParent,
15 hasAlchemyLabelMap,
16 listAlchemyConsentStores,
17 normalizeLocation,
18 parseResourceName,
19 retryTransient,
20 userLabels,
21 waitUntilGone,
22 withOwnershipMetadata,
23} from "./internal.ts";
24
25export type ConsentSignature = {
26 /** Signing user id. */
27 userId?: string;
28 /** RFC3339 signature time. */
29 signatureTime?: string;
30 /** Signature metadata. */
31 metadata?: Record<string, string>;
32};
33
34export type DatasetsConsentStoresConsentArtifactProps = {
35 /**
36 * Parent consent store. Full name
37 * `.../consentStores/{consentStore}` or the store id (combined with
38 * `dataset` and `location`). Immutable — changing it replaces the
39 * artifact.
40 */
41 consentStore: string;
42 /**
43 * Parent dataset used when `consentStore` is a bare id.
44 */
45 dataset?: string;
46 /**
47 * Region used when `consentStore` is a bare id.
48 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
49 */
50 location?: string;
51 /**
52 * User UUID supplied by the client.
53 */
54 userId: string;
55 /**
56 * Version string of the consent information shown to the user.
57 */
58 consentContentVersion?: string;
59 /**
60 * Artifact metadata. Alchemy ownership keys are merged in
61 * automatically (`alchemy-stack`, `alchemy-stage`, `alchemy-id`).
62 */
63 metadata?: Record<string, string>;
64 /**
65 * User signature.
66 */
67 userSignature?: ConsentSignature;
68 /**
69 * Guardian signature.
70 */
71 guardianSignature?: ConsentSignature;
72 /**
73 * Witness signature.
74 */
75 witnessSignature?: ConsentSignature;
76};
77
78export type DatasetsConsentStoresConsentArtifact = Resource<
79 "GCP.Healthcare.DatasetsConsentStoresConsentArtifact",
80 DatasetsConsentStoresConsentArtifactProps,
81 {
82 /** Full resource name `.../consentArtifacts/{consentArtifact}`. */
83 name: string;
84 /** Server-assigned artifact id. */
85 consentArtifactId: string;
86 /** Parent consent store resource name. */
87 consentStore: string;
88 /** Project id. */
89 project: string;
90 /** Location id. */
91 location: string;
92 /** User id. */
93 userId: string | undefined;
94 /** Consent content version. */
95 consentContentVersion: string | undefined;
96 /** User metadata (Alchemy ownership keys stripped). */
97 metadata: Record<string, string>;
98 },
99 never,
100 Providers
101>;
102
103/**
104 * Documentation of a user's consent (artifact) in a consent store.
105 *
106 * Artifact ids are server-assigned. There is no patch API — changing
107 * `userId`, version, metadata, or parent replaces the artifact.
108 * Ownership is stamped into `metadata`.
109 *
110 * ### Creating a Consent Artifact
111 * **Example:** Artifact for a user
112 * ```typescript
113 * const artifact = yield* GCP.Healthcare.DatasetsConsentStoresConsentArtifact(
114 * "Proof",
115 * {
116 * consentStore: store.name,
117 * userId: "user-123",
118 * consentContentVersion: "v1",
119 * metadata: { locale: "en" },
120 * },
121 * );
122 * ```
123 *
124 * @resource
125 * @category Healthcare
126 */
127export const DatasetsConsentStoresConsentArtifact =
128 Resource<DatasetsConsentStoresConsentArtifact>(
129 "GCP.Healthcare.DatasetsConsentStoresConsentArtifact",
130 );
131
132export class DatasetsConsentStoresConsentArtifactNotResolved extends Data.TaggedError(
133 "GCP.Healthcare.DatasetsConsentStoresConsentArtifactNotResolved",
134)<{
135 name: string;
136}> {}
137
138const storeOf = (
139 consentStore: string,
140 project: string,
141 location: string,
142 dataset: string | undefined,
143) => {
144 if (consentStore.includes("/")) return consentStore.replace(/\/+$/, "");
145 if (dataset === undefined) {
146 return expandParent(consentStore, project, location, "consentStores");
147 }
148 const datasetName = expandParent(dataset, project, location, "datasets");
149 return `${datasetName}/consentStores/${consentStore}`;
150};
151
152const toAttrs = (
153 artifact: healthcare.ConsentArtifact,
154 project: string,
155 region: string,
156) => {
157 const name = artifact.name ?? "";
158 const parsed = parseResourceName(name, "consentArtifacts", region);
159 return {
160 name,
161 consentArtifactId: parsed.id,
162 consentStore: parsed.parent,
163 project: parsed.project || project,
164 location: parsed.location,
165 userId: artifact.userId,
166 consentContentVersion: artifact.consentContentVersion,
167 metadata: userLabels(artifact.metadata),
168 };
169};
170
171const getByName = (name: string) =>
172 name.length === 0
173 ? Effect.succeed(undefined)
174 : healthcare
175 .getProjectsLocationsDatasetsConsentStoresConsentArtifacts({ name })
176 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
177
178export const DatasetsConsentStoresConsentArtifactProvider = () =>
179 Provider.succeed(DatasetsConsentStoresConsentArtifact, {
180 stables: [
181 "name",
182 "consentArtifactId",
183 "consentStore",
184 "project",
185 "location",
186 ],
187
188 diff: Effect.fn(function* ({ news, olds, output }) {
189 if (!isResolved(news)) return undefined;
190 const env = yield* GcpEnvironment.current;
191 const previousParent = olds?.consentStore ?? output?.consentStore;
192 const nextParent = storeOf(
193 news.consentStore,
194 env.project,
195 normalizeLocation(news.location ?? output?.location, env.region),
196 news.dataset,
197 );
198 const parentChanged =
199 previousParent !== undefined && previousParent !== nextParent;
200 const userChanged =
201 olds?.userId !== undefined && olds.userId !== news.userId;
202 const versionChanged =
203 (olds?.consentContentVersion ?? output?.consentContentVersion) !==
204 undefined &&
205 (olds?.consentContentVersion ?? output?.consentContentVersion) !==
206 news.consentContentVersion;
207 if (parentChanged || userChanged || versionChanged) {
208 return {
209 action: "replace" as const,
210 deleteFirst: !parentChanged,
211 };
212 }
213 return undefined;
214 }),
215
216 read: Effect.fn(function* ({ id, output }) {
217 const env = yield* GcpEnvironment.current;
218 const existing = yield* getByName(output?.name ?? "");
219 if (existing === undefined) return undefined;
220 const attrs = toAttrs(existing, env.project, env.region);
221 return (yield* hasAlchemyLabels(id, tagRecord(existing.metadata)))
222 ? attrs
223 : Unowned(attrs);
224 }),
225
226 list: () =>
227 Effect.gen(function* () {
228 const env = yield* GcpEnvironment.current;
229 const stores = yield* listAlchemyConsentStores(env.project, env.region);
230 const artifacts = yield* Effect.forEach(
231 stores,
232 (store) =>
233 collectPages(
234 healthcare.listProjectsLocationsDatasetsConsentStoresConsentArtifacts.pages(
235 {
236 parent: store.name ?? "",
237 pageSize: 1000,
238 },
239 ),
240 (page) => page.consentArtifacts,
241 ),
242 { concurrency: 4 },
243 );
244 return artifacts
245 .flat()
246 .filter((artifact) => hasAlchemyLabelMap(artifact.metadata))
247 .map((artifact) => toAttrs(artifact, env.project, env.region));
248 }),
249
250 reconcile: Effect.fn(function* ({ id, news, output }) {
251 const env = yield* GcpEnvironment.current;
252 const location = normalizeLocation(
253 news.location ?? output?.location,
254 env.region,
255 );
256 const consentStore = storeOf(
257 news.consentStore,
258 env.project,
259 location,
260 news.dataset,
261 );
262 const ownership = yield* createInternalLabels(id);
263 const metadata = withOwnershipMetadata(news.metadata, ownership);
264
265 let current = yield* getByName(output?.name ?? "");
266
267 if (current === undefined) {
268 current = yield* retryTransient(
269 healthcare.createProjectsLocationsDatasetsConsentStoresConsentArtifacts(
270 {
271 parent: consentStore,
272 body: {
273 userId: news.userId,
274 consentContentVersion: news.consentContentVersion,
275 metadata,
276 userSignature: news.userSignature,
277 guardianSignature: news.guardianSignature,
278 witnessSignature: news.witnessSignature,
279 },
280 },
281 ),
282 );
283 }
284
285 if (current === undefined) {
286 return yield* new DatasetsConsentStoresConsentArtifactNotResolved({
287 name: consentStore,
288 });
289 }
290
291 return toAttrs(current, env.project, env.region);
292 }),
293
294 delete: Effect.fn(function* ({ output }) {
295 if (!output.name) return;
296 yield* retryTransient(
297 healthcare.deleteProjectsLocationsDatasetsConsentStoresConsentArtifacts(
298 {
299 name: output.name,
300 },
301 ),
302 ).pipe(Effect.catchTag("NotFound", () => Effect.void));
303 yield* waitUntilGone(getByName(output.name), output.name);
304 }),
305 });