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 bdaConfigEquals,
15 readBdaTags,
16 syncBdaTags,
17 toBdaTagList,
18 unredact,
19} from "./internal.ts";
20
21// Explicitly-typed pipeable repeat helper. Inlining `Effect.repeat` in a
22// provider lifecycle op leaks its conditional return type into declaration
23// emit and widens the provider layer for every consumer of `AWS.providers()`.
24const repeatUntilProjectSettled = <E, R>(
25 self: Effect.Effect<bda.DataAutomationProject | undefined, E, R>,
26): Effect.Effect<bda.DataAutomationProject | undefined, E, R> =>
27 Effect.repeat(self, {
28 schedule: Schedule.fixed("3 seconds"),
29 until: (project) =>
30 project === undefined || project.status !== "IN_PROGRESS",
31 times: 25,
32 });
33
34export interface DataAutomationProjectProps {
35 /**
36 * Name of the project. Must match `[a-zA-Z0-9-_]+` and be at most 128
37 * characters. Changing the name replaces the project.
38 * @default ${app}-${stage}-${id}
39 */
40 projectName?: string;
41 /**
42 * Human-readable description of the project.
43 */
44 projectDescription?: string;
45 /**
46 * The stage of the project: `DEVELOPMENT` or `LIVE`. Stages are distinct
47 * addressable copies, so changing the stage replaces the project.
48 * @default LIVE
49 */
50 projectStage?: bda.DataAutomationProjectStage;
51 /**
52 * Whether the project processes files asynchronously (`ASYNC`) or
53 * synchronously (`SYNC`). Immutable — changing it replaces the project.
54 * @default ASYNC
55 */
56 projectType?: bda.DataAutomationProjectType;
57 /**
58 * The standard (default) output the project produces per modality —
59 * document, image, video, and audio extraction settings. Pass `{}` to use
60 * the service defaults.
61 */
62 standardOutputConfiguration: bda.StandardOutputConfiguration;
63 /**
64 * Custom output configuration — the list of `Blueprint`s the project
65 * applies to matched files.
66 */
67 customOutputConfiguration?: bda.CustomOutputConfiguration;
68 /**
69 * Overrides for the project (e.g. document splitter and modality routing).
70 */
71 overrideConfiguration?: bda.OverrideConfiguration;
72 /**
73 * Data automation library (entity/vocabulary) configuration.
74 */
75 dataAutomationLibraryConfiguration?: bda.DataAutomationLibraryConfiguration;
76 /**
77 * Customer-managed KMS encryption for the project. When omitted, the
78 * service uses an AWS-owned key.
79 */
80 encryptionConfiguration?: bda.EncryptionConfiguration;
81 /**
82 * Tags to apply to the project. Merged with internal Alchemy tags.
83 */
84 tags?: Record<string, string>;
85}
86
87export interface DataAutomationProject extends Resource<
88 "AWS.BedrockDataAutomation.DataAutomationProject",
89 DataAutomationProjectProps,
90 {
91 /**
92 * The ARN of the project.
93 */
94 projectArn: string;
95 /**
96 * Name of the project.
97 */
98 projectName: string;
99 /**
100 * The stage (`DEVELOPMENT` or `LIVE`) of the project.
101 */
102 projectStage: string;
103 /**
104 * Current status of the project (e.g. `COMPLETED`).
105 */
106 status: string;
107 },
108 never,
109 Providers
110> {}
111
112/**
113 * An Amazon Bedrock Data Automation Project — the configuration unit that
114 * turns documents, images, audio, and video into structured output, with
115 * optional custom output driven by `Blueprint`s.
116 *
117 * ### Creating Projects
118 * **Example:** Project with default standard output
119 * ```typescript
120 * import * as BDA from "alchemy/AWS/BedrockDataAutomation";
121 *
122 * const project = yield* BDA.DataAutomationProject("Docs", {
123 * standardOutputConfiguration: {},
124 * });
125 * ```
126 *
127 * **Example:** Document project with granular extraction
128 * ```typescript
129 * const project = yield* BDA.DataAutomationProject("Docs", {
130 * projectDescription: "invoice pipeline",
131 * standardOutputConfiguration: {
132 * document: {
133 * extraction: {
134 * granularity: { types: ["DOCUMENT", "PAGE"] },
135 * boundingBox: { state: "DISABLED" },
136 * },
137 * generativeField: { state: "DISABLED" },
138 * outputFormat: {
139 * textFormat: { types: ["MARKDOWN"] },
140 * additionalFileFormat: { state: "DISABLED" },
141 * },
142 * },
143 * },
144 * });
145 * ```
146 *
147 * ### Custom Output
148 * **Example:** Attach blueprints for custom output
149 * ```typescript
150 * const blueprint = yield* BDA.Blueprint("InvoiceBlueprint", {
151 * type: "DOCUMENT",
152 * schema: invoiceSchemaJson,
153 * });
154 *
155 * const project = yield* BDA.DataAutomationProject("Docs", {
156 * standardOutputConfiguration: {},
157 * customOutputConfiguration: {
158 * blueprints: [{ blueprintArn: blueprint.blueprintArn }],
159 * },
160 * });
161 * ```
162 *
163 * @resource
164 */
165export const DataAutomationProject = Resource<DataAutomationProject>(
166 "AWS.BedrockDataAutomation.DataAutomationProject",
167);
168
169/**
170 * Raised when a Data Automation project reaches the terminal `FAILED` status
171 * during provisioning or update.
172 */
173export class DataAutomationProjectFailed extends Data.TaggedError(
174 "DataAutomationProjectFailed",
175)<{ projectArn: string; message: string }> {}
176
177export const DataAutomationProjectProvider = () =>
178 Provider.effect(
179 DataAutomationProject,
180 Effect.gen(function* () {
181 const createName = Effect.fn(function* (
182 id: string,
183 props: Pick<DataAutomationProjectProps, "projectName">,
184 ) {
185 return (
186 props.projectName ??
187 (yield* createPhysicalName({ id, maxLength: 128 }))
188 );
189 });
190
191 const toAttributes = (project: bda.DataAutomationProject) => ({
192 projectArn: project.projectArn,
193 projectName: unredact(project.projectName),
194 projectStage: project.projectStage ?? "LIVE",
195 status: project.status as string,
196 });
197
198 const observeProject = Effect.fn(function* (
199 projectArn: string,
200 projectStage?: bda.DataAutomationProjectStage,
201 ) {
202 return yield* bda
203 .getDataAutomationProject({ projectArn, projectStage })
204 .pipe(
205 Effect.map((r) => r.project),
206 Effect.catchTag("ResourceNotFoundException", () =>
207 Effect.succeed(undefined),
208 ),
209 );
210 });
211
212 const findProjectArn = Effect.fn(function* (name: string) {
213 const summaries = yield* bda.listDataAutomationProjects
214 .items({ resourceOwner: "ACCOUNT" })
215 .pipe(Stream.runCollect);
216 return Array.from(summaries).find(
217 (s) =>
218 s.projectName !== undefined && unredact(s.projectName) === name,
219 )?.projectArn;
220 });
221
222 // Project create/update settle asynchronously (status IN_PROGRESS →
223 // COMPLETED | FAILED). Bounded poll: 25 × 3s ≈ 75s worst case.
224 const waitForSettled = Effect.fn(function* (
225 projectArn: string,
226 projectStage?: bda.DataAutomationProjectStage,
227 ) {
228 const settled = yield* observeProject(projectArn, projectStage).pipe(
229 repeatUntilProjectSettled,
230 );
231 if (settled !== undefined && settled.status === "FAILED") {
232 return yield* Effect.fail(
233 new DataAutomationProjectFailed({
234 projectArn,
235 message: `Data Automation project '${projectArn}' reached status FAILED`,
236 }),
237 );
238 }
239 return settled;
240 });
241
242 return DataAutomationProject.Provider.of({
243 stables: ["projectArn", "projectName"],
244
245 list: () =>
246 Effect.gen(function* () {
247 const summaries = yield* bda.listDataAutomationProjects
248 .items({ resourceOwner: "ACCOUNT" })
249 .pipe(Stream.runCollect);
250 const attrs = yield* Effect.forEach(
251 Array.from(summaries),
252 (s) =>
253 observeProject(s.projectArn, s.projectStage).pipe(
254 Effect.map((project) =>
255 project === undefined ? [] : [toAttributes(project)],
256 ),
257 ),
258 { concurrency: 5 },
259 );
260 return attrs.flat();
261 }),
262
263 read: Effect.fn(function* ({ id, olds, output }) {
264 const projectArn =
265 output?.projectArn ??
266 (yield* findProjectArn(yield* createName(id, olds ?? {})));
267 if (projectArn === undefined) return undefined;
268 const found = yield* observeProject(projectArn, olds?.projectStage);
269 if (found === undefined) return undefined;
270 const attrs = toAttributes(found);
271 const tags = yield* readBdaTags(projectArn);
272 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
273 }),
274
275 diff: Effect.fn(function* ({ id, news, olds }) {
276 if (!isResolved(news)) return undefined;
277 const oldName = yield* createName(id, olds ?? {});
278 const newName = yield* createName(id, news ?? {});
279 if (
280 oldName !== newName ||
281 olds?.projectStage !== news.projectStage ||
282 olds?.projectType !== news.projectType
283 ) {
284 return { action: "replace" } as const;
285 }
286 // fall through: engine default update logic for mutable fields
287 }),
288
289 reconcile: Effect.fn(function* ({ id, news, output, session }) {
290 const projectName =
291 output?.projectName ?? (yield* createName(id, news));
292 const internalTags = yield* createInternalTags(id);
293 const desiredTags = { ...news.tags, ...internalTags };
294
295 // 1. OBSERVE — cloud state is authoritative; output caches the ARN.
296 const cachedArn =
297 output?.projectArn ?? (yield* findProjectArn(projectName));
298 let live =
299 cachedArn === undefined
300 ? undefined
301 : yield* observeProject(cachedArn, news.projectStage);
302
303 // 2. ENSURE — create when missing; a concurrent create surfaces as
304 // the typed ConflictException, which we treat as a race and
305 // re-observe by name. Wait for the async provisioning to settle.
306 if (live === undefined) {
307 const projectArn = yield* bda
308 .createDataAutomationProject({
309 projectName,
310 projectDescription: news.projectDescription,
311 projectStage: news.projectStage,
312 projectType: news.projectType,
313 standardOutputConfiguration: news.standardOutputConfiguration,
314 customOutputConfiguration: news.customOutputConfiguration,
315 overrideConfiguration: news.overrideConfiguration,
316 dataAutomationLibraryConfiguration:
317 news.dataAutomationLibraryConfiguration,
318 encryptionConfiguration: news.encryptionConfiguration,
319 tags: toBdaTagList(desiredTags),
320 })
321 .pipe(
322 Effect.map((r) => r.projectArn),
323 Effect.catchTag("ConflictException", (conflict) =>
324 Effect.gen(function* () {
325 const arn = yield* findProjectArn(projectName);
326 return arn === undefined
327 ? yield* Effect.fail(conflict)
328 : arn;
329 }),
330 ),
331 );
332 live = yield* waitForSettled(projectArn, news.projectStage);
333 }
334 if (live === undefined) {
335 return yield* Effect.fail(
336 new DataAutomationProjectFailed({
337 projectArn: cachedArn ?? projectName,
338 message: `Data Automation project '${projectName}' was not observable after create`,
339 }),
340 );
341 }
342
343 // 3. SYNC — diff OBSERVED configuration against desired; apply the
344 // idempotent PUT only on drift. The server may fill defaulted
345 // fields, in which case the PUT re-applies harmlessly.
346 const drift =
347 unredact(live.projectDescription ?? "") !==
348 (news.projectDescription ?? "") ||
349 !bdaConfigEquals(
350 live.standardOutputConfiguration,
351 news.standardOutputConfiguration,
352 ) ||
353 (news.customOutputConfiguration !== undefined &&
354 !bdaConfigEquals(
355 live.customOutputConfiguration,
356 news.customOutputConfiguration,
357 )) ||
358 (news.overrideConfiguration !== undefined &&
359 !bdaConfigEquals(
360 live.overrideConfiguration,
361 news.overrideConfiguration,
362 )) ||
363 (news.encryptionConfiguration !== undefined &&
364 live.kmsKeyId !== news.encryptionConfiguration.kmsKeyId);
365 if (drift) {
366 yield* bda.updateDataAutomationProject({
367 projectArn: live.projectArn,
368 projectStage: news.projectStage,
369 projectDescription: news.projectDescription,
370 standardOutputConfiguration: news.standardOutputConfiguration,
371 customOutputConfiguration: news.customOutputConfiguration,
372 overrideConfiguration: news.overrideConfiguration,
373 dataAutomationLibraryConfiguration:
374 news.dataAutomationLibraryConfiguration,
375 encryptionConfiguration: news.encryptionConfiguration,
376 });
377 live =
378 (yield* waitForSettled(live.projectArn, news.projectStage)) ??
379 live;
380 }
381
382 // 3b. SYNC TAGS against observed cloud tags.
383 yield* syncBdaTags(live.projectArn, desiredTags);
384
385 yield* session.note(projectName);
386 return toAttributes(live);
387 }),
388
389 delete: Effect.fn(function* ({ output }) {
390 yield* bda
391 .deleteDataAutomationProject({ projectArn: output.projectArn })
392 .pipe(
393 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
394 );
395 }),
396 });
397 }),
398 );