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 ReferenceStoreProps {
14 /**
15 * A name for the reference 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 reference 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 * Tags to apply to the reference store. Merged with internal Alchemy tags.
37 */
38 tags?: Record<string, string>;
39}
40
41export interface ReferenceStore extends Resource<
42 "AWS.Omics.ReferenceStore",
43 ReferenceStoreProps,
44 {
45 /**
46 * ID of the reference store.
47 */
48 referenceStoreId: string;
49 /**
50 * ARN of the reference store.
51 */
52 referenceStoreArn: string;
53 /**
54 * Name of the reference store.
55 */
56 name: string;
57 /**
58 * When the reference store was created (ISO-8601).
59 */
60 creationTime: string;
61 },
62 never,
63 Providers
64> {}
65
66/**
67 * An Amazon HealthOmics reference store — a container for reference genomes
68 * that read sets are aligned against.
69 *
70 * A reference store name is auto-generated from the app, stage, and logical
71 * ID unless you provide one. HealthOmics offers no update-store API, so any
72 * change to `name`, `description`, or `sseConfig` replaces the store. A store
73 * can only be deleted once it contains no reference genomes.
74 * ### Creating a Reference Store
75 * **Example:** Basic Reference Store
76 * ```typescript
77 * import * as Omics from "alchemy/AWS/Omics";
78 *
79 * const store = yield* Omics.ReferenceStore("References");
80 * ```
81 *
82 * **Example:** Named Reference Store with Description
83 * ```typescript
84 * const store = yield* Omics.ReferenceStore("References", {
85 * name: "human-references",
86 * description: "GRCh38 reference genomes",
87 * });
88 * ```
89 *
90 * ### Encryption
91 * **Example:** Customer-managed KMS key
92 * ```typescript
93 * const store = yield* Omics.ReferenceStore("References", {
94 * sseConfig: {
95 * type: "KMS",
96 * keyArn: "arn:aws:kms:us-east-1:123456789012:key/abc-123",
97 * },
98 * });
99 * ```
100 *
101 * @resource
102 */
103export const ReferenceStore = Resource<ReferenceStore>(
104 "AWS.Omics.ReferenceStore",
105);
106
107export const ReferenceStoreProvider = () =>
108 Provider.effect(
109 ReferenceStore,
110 Effect.gen(function* () {
111 const createName = Effect.fn(function* (
112 id: string,
113 props: { name?: string | undefined },
114 ) {
115 return props.name ?? (yield* createPhysicalName({ id, maxLength: 96 }));
116 });
117
118 const toAttrs = (store: {
119 id: string;
120 arn: string;
121 name?: string;
122 creationTime: Date;
123 }) => ({
124 referenceStoreId: store.id,
125 referenceStoreArn: store.arn,
126 name: store.name ?? "",
127 creationTime: store.creationTime.toISOString(),
128 });
129
130 return ReferenceStore.Provider.of({
131 stables: [
132 "referenceStoreId",
133 "referenceStoreArn",
134 "name",
135 "creationTime",
136 ],
137 list: () =>
138 omics.listReferenceStores.items({}).pipe(
139 Stream.map(toAttrs),
140 Stream.runCollect,
141 Effect.map((chunk) => Array.from(chunk)),
142 ),
143 read: Effect.fn(function* ({ id, output }) {
144 if (output?.referenceStoreId === undefined) return undefined;
145 const found = yield* omics
146 .getReferenceStore({ id: output.referenceStoreId })
147 .pipe(
148 Effect.catchTag("ResourceNotFoundException", () =>
149 Effect.succeed(undefined),
150 ),
151 );
152 if (found === undefined) return undefined;
153 const attrs = toAttrs(found);
154 const tags = yield* fetchOmicsTags(found.arn);
155 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
156 }),
157 diff: Effect.fn(function* ({ id, news, olds }) {
158 if (!isResolved(news)) return undefined;
159 const prev = olds ?? {};
160 const oldName = yield* createName(id, prev);
161 const newName = yield* createName(id, news);
162 if (oldName !== newName) return { action: "replace" } as const;
163 if ((prev.description ?? "") !== (news.description ?? "")) {
164 return { action: "replace" } as const;
165 }
166 if (
167 prev.sseConfig?.type !== news.sseConfig?.type ||
168 prev.sseConfig?.keyArn !== news.sseConfig?.keyArn
169 ) {
170 return { action: "replace" } as const;
171 }
172 }),
173 reconcile: Effect.fn(function* ({ id, news, output, session }) {
174 const name = output?.name ?? (yield* createName(id, news));
175 const internalTags = yield* createInternalTags(id);
176 const desiredTags = { ...news.tags, ...internalTags };
177
178 // OBSERVE — the store id is a server-generated cache; if the store
179 // was deleted out-of-band, getReferenceStore returns NotFound and we
180 // recreate.
181 let store =
182 output?.referenceStoreId === undefined
183 ? undefined
184 : yield* omics
185 .getReferenceStore({ id: output.referenceStoreId })
186 .pipe(
187 Effect.catchTag("ResourceNotFoundException", () =>
188 Effect.succeed(undefined),
189 ),
190 );
191
192 // ENSURE — create if missing. The store name is not a unique key, so
193 // there is no AlreadyExists race to tolerate here.
194 if (store === undefined) {
195 store = yield* omics.createReferenceStore({
196 name,
197 description: news.description,
198 sseConfig: news.sseConfig,
199 tags: desiredTags,
200 });
201 }
202
203 // SYNC TAGS — diff against observed cloud tags so adoption converges.
204 yield* syncOmicsTags(store.arn, desiredTags);
205
206 yield* session.note(store.id);
207 return toAttrs(store);
208 }),
209 delete: Effect.fn(function* ({ output }) {
210 yield* omics
211 .deleteReferenceStore({ id: output.referenceStoreId })
212 .pipe(
213 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
214 );
215 }),
216 });
217 }),
218 );