1import * as bda from "@distilled.cloud/aws/bedrock-data-automation";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5import * as Stream from "effect/Stream";
6import { Unowned } from "../../AdoptPolicy.ts";
7import { isResolved } from "../../Diff.ts";
8import { createPhysicalName } from "../../PhysicalName.ts";
9import * as Provider from "../../Provider.ts";
10import { Resource } from "../../Resource.ts";
11import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
12import type { Providers } from "../Providers.ts";
13import {
14 readBdaTags,
15 syncBdaTags,
16 toBdaTagList,
17 unredact,
18} from "./internal.ts";
19
20// Explicitly-typed pipeable repeat helper. Inlining `Effect.repeat` in a
21// provider lifecycle op leaks its conditional return type into declaration
22// emit and widens the provider layer for every consumer of `AWS.providers()`.
23const repeatUntilLibraryGone = <E, R>(
24 self: Effect.Effect<bda.DataAutomationLibrary | undefined, E, R>,
25): Effect.Effect<bda.DataAutomationLibrary | undefined, E, R> =>
26 Effect.repeat(self, {
27 schedule: Schedule.fixed("3 seconds"),
28 until: (library) => library === undefined,
29 times: 10,
30 });
31
32export interface DataAutomationLibraryProps {
33 /**
34 * Name of the library. Changing the name replaces the library (the API has
35 * no rename operation).
36 * @default ${app}-${stage}-${id}
37 */
38 libraryName?: string;
39 /**
40 * Human-readable description of the library. Mutable in place.
41 */
42 libraryDescription?: string;
43 /**
44 * Customer-managed KMS encryption for the library. Create-only — changing
45 * the key replaces the library. When omitted, the service uses an
46 * AWS-owned key.
47 */
48 encryptionConfiguration?: bda.EncryptionConfiguration;
49 /**
50 * Tags to apply to the library. Merged with internal Alchemy tags.
51 */
52 tags?: Record<string, string>;
53}
54
55export interface DataAutomationLibrary extends Resource<
56 "AWS.BedrockDataAutomation.DataAutomationLibrary",
57 DataAutomationLibraryProps,
58 {
59 /**
60 * The ARN of the library.
61 */
62 libraryArn: string;
63 /**
64 * Name of the library.
65 */
66 libraryName: string;
67 /**
68 * Current status of the library (`ACTIVE` or `DELETING`).
69 */
70 status: string;
71 },
72 never,
73 Providers
74> {}
75
76/**
77 * An Amazon Bedrock Data Automation Library — a store of reusable entities
78 * (currently `VOCABULARY` entities: domain phrases with display forms) that
79 * data automation projects reference via their
80 * `dataAutomationLibraryConfiguration` to improve extraction accuracy.
81 *
82 * Entities are loaded into the library with ingestion jobs — see the
83 * `InvokeDataAutomationLibraryIngestionJob` binding.
84 *
85 * ### Creating Libraries
86 * **Example:** Library with a description
87 * ```typescript
88 * import * as BDA from "alchemy/AWS/BedrockDataAutomation";
89 *
90 * const library = yield* BDA.DataAutomationLibrary("Vocab", {
91 * libraryDescription: "domain vocabulary for invoice extraction",
92 * });
93 * ```
94 *
95 * **Example:** Reference the library from a project
96 * ```typescript
97 * const project = yield* BDA.DataAutomationProject("Docs", {
98 * standardOutputConfiguration: {},
99 * dataAutomationLibraryConfiguration: {
100 * libraries: [{ libraryArn: library.libraryArn }],
101 * },
102 * });
103 * ```
104 *
105 * @resource
106 */
107export const DataAutomationLibrary = Resource<DataAutomationLibrary>(
108 "AWS.BedrockDataAutomation.DataAutomationLibrary",
109);
110
111/**
112 * Raised when a Data Automation library cannot be observed immediately after
113 * a successful create — a race with a concurrent delete.
114 */
115export class DataAutomationLibraryNotObservable extends Data.TaggedError(
116 "DataAutomationLibraryNotObservable",
117)<{ libraryName: string; message: string }> {}
118
119export const DataAutomationLibraryProvider = () =>
120 Provider.effect(
121 DataAutomationLibrary,
122 Effect.gen(function* () {
123 const createName = Effect.fn(function* (
124 id: string,
125 props: Pick<DataAutomationLibraryProps, "libraryName">,
126 ) {
127 return (
128 props.libraryName ??
129 (yield* createPhysicalName({ id, maxLength: 128 }))
130 );
131 });
132
133 const toAttributes = (library: bda.DataAutomationLibrary) => ({
134 libraryArn: library.libraryArn,
135 libraryName: unredact(library.libraryName),
136 status: library.status as string,
137 });
138
139 const observeLibrary = Effect.fn(function* (libraryArn: string) {
140 return yield* bda.getDataAutomationLibrary({ libraryArn }).pipe(
141 Effect.map((r) => r.library),
142 Effect.catchTag("ResourceNotFoundException", () =>
143 Effect.succeed(undefined),
144 ),
145 );
146 });
147
148 const findLibraryArn = Effect.fn(function* (name: string) {
149 const summaries = yield* bda.listDataAutomationLibraries
150 .items({})
151 .pipe(Stream.runCollect);
152 return Array.from(summaries).find(
153 (s) =>
154 s.libraryName !== undefined && unredact(s.libraryName) === name,
155 )?.libraryArn;
156 });
157
158 return DataAutomationLibrary.Provider.of({
159 stables: ["libraryArn", "libraryName"],
160
161 list: () =>
162 Effect.gen(function* () {
163 const summaries = yield* bda.listDataAutomationLibraries
164 .items({})
165 .pipe(Stream.runCollect);
166 const attrs = yield* Effect.forEach(
167 Array.from(summaries),
168 (s) =>
169 observeLibrary(s.libraryArn).pipe(
170 Effect.map((library) =>
171 library === undefined ? [] : [toAttributes(library)],
172 ),
173 ),
174 { concurrency: 5 },
175 );
176 return attrs.flat();
177 }),
178
179 read: Effect.fn(function* ({ id, olds, output }) {
180 const libraryArn =
181 output?.libraryArn ??
182 (yield* findLibraryArn(yield* createName(id, olds ?? {})));
183 if (libraryArn === undefined) return undefined;
184 const found = yield* observeLibrary(libraryArn);
185 if (found === undefined) return undefined;
186 const attrs = toAttributes(found);
187 const tags = yield* readBdaTags(libraryArn);
188 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
189 }),
190
191 diff: Effect.fn(function* ({ id, news, olds }) {
192 if (!isResolved(news)) return undefined;
193 const oldName = yield* createName(id, olds ?? {});
194 const newName = yield* createName(id, news ?? {});
195 if (
196 oldName !== newName ||
197 // updateDataAutomationLibrary only takes a description, so a KMS
198 // key change can only be honored by replacement.
199 olds?.encryptionConfiguration?.kmsKeyId !==
200 news.encryptionConfiguration?.kmsKeyId
201 ) {
202 return { action: "replace" } as const;
203 }
204 // fall through: engine default update logic for mutable fields
205 }),
206
207 reconcile: Effect.fn(function* ({ id, news, output, session }) {
208 const libraryName =
209 output?.libraryName ?? (yield* createName(id, news));
210 const internalTags = yield* createInternalTags(id);
211 const desiredTags = { ...news.tags, ...internalTags };
212
213 // 1. OBSERVE — cloud state is authoritative; output caches the ARN.
214 const cachedArn =
215 output?.libraryArn ?? (yield* findLibraryArn(libraryName));
216 let live =
217 cachedArn === undefined
218 ? undefined
219 : yield* observeLibrary(cachedArn);
220
221 // 2. ENSURE — create when missing; a concurrent create surfaces as
222 // the typed ConflictException, which we treat as a race and
223 // re-observe by name. Create returns only arn + status, so
224 // re-observe for the full shape.
225 if (live === undefined) {
226 const createdArn = yield* bda
227 .createDataAutomationLibrary({
228 libraryName,
229 libraryDescription: news.libraryDescription,
230 encryptionConfiguration: news.encryptionConfiguration,
231 tags: toBdaTagList(desiredTags),
232 })
233 .pipe(
234 Effect.map((r) => r.libraryArn),
235 Effect.catchTag("ConflictException", (conflict) =>
236 Effect.gen(function* () {
237 const arn = yield* findLibraryArn(libraryName);
238 return arn === undefined
239 ? yield* Effect.fail(conflict)
240 : arn;
241 }),
242 ),
243 );
244 live =
245 createdArn === undefined
246 ? undefined
247 : yield* observeLibrary(createdArn);
248 }
249 if (live === undefined) {
250 return yield* Effect.fail(
251 new DataAutomationLibraryNotObservable({
252 libraryName,
253 message: `Data Automation library '${libraryName}' was not observable after create`,
254 }),
255 );
256 }
257
258 // 3. SYNC — diff the OBSERVED description against the desired one;
259 // apply the idempotent update only on drift.
260 if (
261 unredact(live.libraryDescription ?? "") !==
262 (news.libraryDescription ?? "")
263 ) {
264 yield* bda.updateDataAutomationLibrary({
265 libraryArn: live.libraryArn,
266 libraryDescription: news.libraryDescription ?? "",
267 });
268 live = (yield* observeLibrary(live.libraryArn)) ?? live;
269 }
270
271 // 3b. SYNC TAGS against observed cloud tags.
272 yield* syncBdaTags(live.libraryArn, desiredTags);
273
274 yield* session.note(libraryName);
275 return toAttributes(live);
276 }),
277
278 delete: Effect.fn(function* ({ output }) {
279 yield* bda
280 .deleteDataAutomationLibrary({ libraryArn: output.libraryArn })
281 .pipe(
282 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
283 );
284 // Deletion settles asynchronously (status ACTIVE → DELETING → gone)
285 // and library names are unique, so wait for the terminal
286 // disappearance — otherwise an immediate same-name re-create
287 // conflicts with the vanishing library. Bounded: 10 × 3s ≈ 30s.
288 yield* observeLibrary(output.libraryArn).pipe(repeatUntilLibraryGone);
289 }),
290 });
291 }),
292 );