1import * as omics from "@distilled.cloud/aws/omics";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
10import type { Providers } from "../Providers.ts";
11import { fetchOmicsTags, syncOmicsTags } from "./internal.ts";
12
13export interface SequenceStoreProps {
14 /**
15 * A name for the sequence store. If omitted, a unique name is generated
16 * from the app, stage, and logical ID. Changing the name replaces the
17 * store. Must match `[\w -.]+` and be 1-127 characters.
18 */
19 name?: string;
20 /**
21 * A description for the sequence store. Changing the description replaces
22 * the store (HealthOmics has no update-store API).
23 */
24 description?: string;
25 /**
26 * Server-side encryption (SSE) settings. Defaults to an AWS-owned key.
27 * Changing this replaces the store.
28 */
29 sseConfig?: {
30 /** Encryption type. `KMS` uses a customer-managed key. */
31 type: "KMS";
32 /** ARN of the customer-managed KMS key. Required when `type` is `KMS`. */
33 keyArn?: string;
34 };
35 /**
36 * An S3 location that is used as the destination for read sets whose
37 * upload failed. Changing this replaces the store.
38 */
39 fallbackLocation?: string;
40 /**
41 * The ETag algorithm family to use for ingested read sets. Changing this
42 * replaces the store. Valid values are `MD5up` and `SHA256up`.
43 */
44 eTagAlgorithmFamily?: "MD5up" | "SHA256up";
45 /**
46 * The tags keys to propagate to the S3 objects associated with read sets
47 * in this store. Changing this replaces the store.
48 */
49 propagatedSetLevelTags?: string[];
50 /**
51 * Tags to apply to the sequence store. Merged with internal Alchemy tags.
52 */
53 tags?: Record<string, string>;
54}
55
56export interface SequenceStore extends Resource<
57 "AWS.Omics.SequenceStore",
58 SequenceStoreProps,
59 {
60 /**
61 * ID of the sequence store.
62 */
63 sequenceStoreId: string;
64 /**
65 * ARN of the sequence store.
66 */
67 sequenceStoreArn: string;
68 /**
69 * Name of the sequence store.
70 */
71 name: string;
72 /**
73 * When the sequence store was created (ISO-8601).
74 */
75 creationTime: string;
76 },
77 never,
78 Providers
79> {}
80
81/**
82 * An Amazon HealthOmics sequence store — a container for genomics read sets
83 * (FASTQ, BAM, CRAM).
84 *
85 * A sequence store name is auto-generated from the app, stage, and logical ID
86 * unless you provide one. HealthOmics offers no update-store API, so any
87 * change to an immutable property (name, description, encryption, fallback
88 * location, ETag algorithm) replaces the store. A store can only be deleted
89 * once it contains no read sets.
90 * ### Creating a Sequence Store
91 * **Example:** Basic Sequence Store
92 * ```typescript
93 * import * as Omics from "alchemy/AWS/Omics";
94 *
95 * const store = yield* Omics.SequenceStore("Reads");
96 * ```
97 *
98 * **Example:** Sequence Store with Fallback Location
99 * ```typescript
100 * const store = yield* Omics.SequenceStore("Reads", {
101 * name: "sample-reads",
102 * fallbackLocation: "s3://my-bucket/omics-fallback/",
103 * eTagAlgorithmFamily: "SHA256up",
104 * });
105 * ```
106 *
107 * ### Encryption
108 * **Example:** Customer-managed KMS key
109 * ```typescript
110 * const store = yield* Omics.SequenceStore("Reads", {
111 * sseConfig: {
112 * type: "KMS",
113 * keyArn: "arn:aws:kms:us-east-1:123456789012:key/abc-123",
114 * },
115 * });
116 * ```
117 *
118 * @resource
119 */
120export const SequenceStore = Resource<SequenceStore>("AWS.Omics.SequenceStore");
121
122export const SequenceStoreProvider = () =>
123 Provider.effect(
124 SequenceStore,
125 Effect.gen(function* () {
126 const createName = Effect.fn(function* (
127 id: string,
128 props: { name?: string | undefined },
129 ) {
130 return props.name ?? (yield* createPhysicalName({ id, maxLength: 96 }));
131 });
132
133 const toAttrs = (store: {
134 id: string;
135 arn: string;
136 name?: string;
137 creationTime: Date;
138 }) => ({
139 sequenceStoreId: store.id,
140 sequenceStoreArn: store.arn,
141 name: store.name ?? "",
142 creationTime: store.creationTime.toISOString(),
143 });
144
145 return SequenceStore.Provider.of({
146 stables: [
147 "sequenceStoreId",
148 "sequenceStoreArn",
149 "name",
150 "creationTime",
151 ],
152 list: () =>
153 omics.listSequenceStores.items({}).pipe(
154 Stream.map(toAttrs),
155 Stream.runCollect,
156 Effect.map((chunk) => Array.from(chunk)),
157 ),
158 read: Effect.fn(function* ({ id, output }) {
159 if (output?.sequenceStoreId === undefined) return undefined;
160 const found = yield* omics
161 .getSequenceStore({ id: output.sequenceStoreId })
162 .pipe(
163 Effect.catchTag("ResourceNotFoundException", () =>
164 Effect.succeed(undefined),
165 ),
166 );
167 if (found === undefined) return undefined;
168 const attrs = toAttrs(found);
169 const tags = yield* fetchOmicsTags(found.arn);
170 return (yield* hasAlchemyTags(id, tags)) ? attrs : 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 ((prev.description ?? "") !== (news.description ?? "")) {
179 return { action: "replace" } as const;
180 }
181 if (
182 prev.sseConfig?.type !== news.sseConfig?.type ||
183 prev.sseConfig?.keyArn !== news.sseConfig?.keyArn ||
184 (prev.fallbackLocation ?? "") !== (news.fallbackLocation ?? "") ||
185 (prev.eTagAlgorithmFamily ?? "") !==
186 (news.eTagAlgorithmFamily ?? "")
187 ) {
188 return { action: "replace" } as const;
189 }
190 }),
191 reconcile: Effect.fn(function* ({ id, news, output, session }) {
192 const name = output?.name ?? (yield* createName(id, news));
193 const internalTags = yield* createInternalTags(id);
194 const desiredTags = { ...news.tags, ...internalTags };
195
196 let store =
197 output?.sequenceStoreId === undefined
198 ? undefined
199 : yield* omics
200 .getSequenceStore({ id: output.sequenceStoreId })
201 .pipe(
202 Effect.catchTag("ResourceNotFoundException", () =>
203 Effect.succeed(undefined),
204 ),
205 );
206
207 if (store === undefined) {
208 store = yield* omics.createSequenceStore({
209 name,
210 description: news.description,
211 sseConfig: news.sseConfig,
212 fallbackLocation: news.fallbackLocation,
213 eTagAlgorithmFamily: news.eTagAlgorithmFamily,
214 propagatedSetLevelTags: news.propagatedSetLevelTags,
215 tags: desiredTags,
216 });
217 }
218
219 yield* syncOmicsTags(store.arn, desiredTags);
220
221 yield* session.note(store.id);
222 return toAttrs(store);
223 }),
224 delete: Effect.fn(function* ({ output }) {
225 yield* omics
226 .deleteSequenceStore({ id: output.sequenceStoreId })
227 .pipe(
228 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
229 );
230 }),
231 });
232 }),
233 );