1import * as omics from "@distilled.cloud/aws/omics";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import { createPhysicalName } from "../../PhysicalName.ts";
8import * as Provider from "../../Provider.ts";
9import { Resource } from "../../Resource.ts";
10import { createInternalTags, hasAlchemyTags, tagRecord } from "../../Tags.ts";
11import type { Providers } from "../Providers.ts";
12import { syncOmicsTags } from "./internal.ts";
13
14// Variant store names must match `[a-z]([a-z0-9_]){0,254}` — the generated
15// physical name (hyphens, mixed case) is coerced to fit.
16const toStoreName = (raw: string) => {
17 const sanitized = raw.toLowerCase().replace(/[^a-z0-9_]/g, "_");
18 const prefixed = /^[a-z]/.test(sanitized) ? sanitized : `a${sanitized}`;
19 return prefixed.slice(0, 255);
20};
21
22export interface VariantStoreProps {
23 /**
24 * A name for the variant store. If omitted, a unique name is generated from
25 * the app, stage, and logical ID. Must be lowercase, start with a letter,
26 * and contain only `[a-z0-9_]`. Changing the name replaces the store.
27 */
28 name?: string;
29 /**
30 * A description for the variant store. Mutable.
31 */
32 description?: string;
33 /**
34 * The genome reference for the store, as a reference ARN. Required.
35 * Immutable — changing it replaces the store.
36 */
37 reference: {
38 /** ARN of a reference in a HealthOmics reference store. */
39 referenceArn: string;
40 };
41 /**
42 * Server-side encryption (SSE) settings. Defaults to an AWS-owned key.
43 * Immutable.
44 */
45 sseConfig?: {
46 /** Encryption type. `KMS` uses a customer-managed key. */
47 type: "KMS";
48 /** ARN of the customer-managed KMS key. Required when `type` is `KMS`. */
49 keyArn?: string;
50 };
51 /**
52 * Tags to apply to the variant store. Merged with internal Alchemy tags.
53 */
54 tags?: Record<string, string>;
55}
56
57export interface VariantStore extends Resource<
58 "AWS.Omics.VariantStore",
59 VariantStoreProps,
60 {
61 /**
62 * ID of the variant store.
63 */
64 variantStoreId: string;
65 /**
66 * ARN of the variant store.
67 */
68 variantStoreArn: string;
69 /**
70 * Name of the variant store.
71 */
72 name: string;
73 /**
74 * Store status (e.g. `ACTIVE`, `CREATING`, `UPDATING`, `FAILED`).
75 */
76 status: string;
77 },
78 never,
79 Providers
80> {}
81
82/**
83 * An Amazon HealthOmics variant store — a container for genomic variant data
84 * (VCF) aligned to a reference genome.
85 *
86 * A variant store name is auto-generated from the app, stage, and logical ID
87 * unless you provide one. The `reference` and `sseConfig` are immutable —
88 * changing either replaces the store. `description` is updated in place.
89 * ### Creating a Variant Store
90 * **Example:** Basic Variant Store
91 * ```typescript
92 * import * as Omics from "alchemy/AWS/Omics";
93 *
94 * const store = yield* Omics.VariantStore("Variants", {
95 * reference: {
96 * referenceArn: "arn:aws:omics:us-east-1:123456789012:referenceStore/1234567890/reference/0987654321",
97 * },
98 * });
99 * ```
100 *
101 * @resource
102 */
103export const VariantStore = Resource<VariantStore>("AWS.Omics.VariantStore");
104
105export const VariantStoreProvider = () =>
106 Provider.effect(
107 VariantStore,
108 Effect.gen(function* () {
109 const createName = Effect.fn(function* (
110 id: string,
111 props: { name?: string | undefined },
112 ) {
113 return (
114 props.name ??
115 toStoreName(yield* createPhysicalName({ id, maxLength: 96 }))
116 );
117 });
118
119 const waitUntilActive = Effect.fn(function* (name: string) {
120 const final = yield* omics.getVariantStore({ name }).pipe(
121 Effect.repeat({
122 schedule: Schedule.max([
123 Schedule.fixed("5 seconds"),
124 Schedule.recurs(23),
125 ]),
126 until: (s) => s.status === "ACTIVE" || s.status === "FAILED",
127 }),
128 );
129 if (final.status === "FAILED") {
130 return yield* Effect.fail(
131 new omics.ValidationException({
132 message: `Variant store ${name} failed: ${final.statusMessage ?? "unknown"}`,
133 }),
134 );
135 }
136 return final;
137 });
138
139 return VariantStore.Provider.of({
140 stables: ["variantStoreId", "variantStoreArn", "name"],
141 list: () =>
142 omics.listVariantStores.items({}).pipe(
143 Stream.map((item) => ({
144 variantStoreId: item.id!,
145 variantStoreArn: item.storeArn!,
146 name: item.name ?? "",
147 status: item.status ?? "",
148 })),
149 Stream.runCollect,
150 Effect.map((chunk) => Array.from(chunk)),
151 ),
152 read: Effect.fn(function* ({ id, olds, output }) {
153 const name = output?.name ?? (yield* createName(id, olds ?? {}));
154 const found = yield* omics
155 .getVariantStore({ name })
156 .pipe(
157 Effect.catchTag("ResourceNotFoundException", () =>
158 Effect.succeed(undefined),
159 ),
160 );
161 if (found === undefined) return undefined;
162 const attrs = {
163 variantStoreId: found.id,
164 variantStoreArn: found.storeArn,
165 name: found.name,
166 status: found.status,
167 };
168 return (yield* hasAlchemyTags(id, tagRecord(found.tags)))
169 ? attrs
170 : Unowned(attrs);
171 }),
172 diff: Effect.fn(function* ({ id, news, olds }) {
173 if (!isResolved(news)) return undefined;
174 const prev = olds ?? {};
175 const oldName = yield* createName(id, prev);
176 const newName = yield* createName(id, news);
177 if (oldName !== newName) return { action: "replace" } as const;
178 if (
179 (prev.reference?.referenceArn ?? "") !==
180 (news.reference?.referenceArn ?? "") ||
181 prev.sseConfig?.type !== news.sseConfig?.type ||
182 prev.sseConfig?.keyArn !== news.sseConfig?.keyArn
183 ) {
184 return { action: "replace" } as const;
185 }
186 }),
187 reconcile: Effect.fn(function* ({ id, news, output, session }) {
188 const name = output?.name ?? (yield* createName(id, news));
189 const internalTags = yield* createInternalTags(id);
190 const desiredTags = { ...news.tags, ...internalTags };
191
192 let store = yield* omics
193 .getVariantStore({ name })
194 .pipe(
195 Effect.catchTag("ResourceNotFoundException", () =>
196 Effect.succeed(undefined),
197 ),
198 );
199
200 if (store === undefined) {
201 yield* omics
202 .createVariantStore({
203 name,
204 description: news.description,
205 reference: news.reference,
206 sseConfig: news.sseConfig,
207 tags: desiredTags,
208 })
209 .pipe(Effect.catchTag("ConflictException", () => Effect.void));
210 store = yield* waitUntilActive(name);
211 } else if (store.status === "CREATING") {
212 store = yield* waitUntilActive(name);
213 }
214
215 if (
216 news.description !== undefined &&
217 news.description !== store.description
218 ) {
219 yield* omics.updateVariantStore({
220 name,
221 description: news.description,
222 });
223 store = yield* omics.getVariantStore({ name });
224 }
225
226 yield* syncOmicsTags(store.storeArn, desiredTags);
227
228 yield* session.note(store.id);
229 return {
230 variantStoreId: store.id,
231 variantStoreArn: store.storeArn,
232 name: store.name,
233 status: store.status,
234 };
235 }),
236 delete: Effect.fn(function* ({ output }) {
237 yield* omics
238 .deleteVariantStore({ name: output.name, force: true })
239 .pipe(
240 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
241 );
242 }),
243 });
244 }),
245 );