1import * as bda from "@distilled.cloud/aws/bedrock-data-automation";
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 {
12 readBdaTags,
13 syncBdaTags,
14 toBdaTagList,
15 unredact,
16} from "./internal.ts";
17
18export interface BlueprintProps {
19 /**
20 * Name of the blueprint. Must match `[a-zA-Z0-9-_]+` and be at most 128
21 * characters. Changing the name replaces the blueprint.
22 * @default ${app}-${stage}-${id}
23 */
24 blueprintName?: string;
25 /**
26 * The modality the blueprint extracts custom output from: `DOCUMENT`,
27 * `IMAGE`, `AUDIO`, or `VIDEO`. Changing the type replaces the blueprint.
28 */
29 type: bda.Type;
30 /**
31 * The JSON schema (as a string) describing the custom output fields the
32 * blueprint extracts. See the Bedrock Data Automation blueprint schema
33 * reference for the expected document shape.
34 */
35 schema: string;
36 /**
37 * The stage of the blueprint: `DEVELOPMENT` or `LIVE`. Stages are distinct
38 * addressable copies, so changing the stage replaces the blueprint.
39 * @default LIVE
40 */
41 blueprintStage?: bda.BlueprintStage;
42 /**
43 * Customer-managed KMS encryption for the blueprint. When omitted, the
44 * service uses an AWS-owned key.
45 */
46 encryptionConfiguration?: bda.EncryptionConfiguration;
47 /**
48 * Tags to apply to the blueprint. Merged with internal Alchemy tags.
49 */
50 tags?: Record<string, string>;
51}
52
53export interface Blueprint extends Resource<
54 "AWS.BedrockDataAutomation.Blueprint",
55 BlueprintProps,
56 {
57 /**
58 * The ARN of the blueprint.
59 */
60 blueprintArn: string;
61 /**
62 * Name of the blueprint.
63 */
64 blueprintName: string;
65 /**
66 * The stage (`DEVELOPMENT` or `LIVE`) of the blueprint.
67 */
68 blueprintStage: string;
69 /**
70 * The modality (`DOCUMENT`, `IMAGE`, `AUDIO`, or `VIDEO`) the blueprint
71 * extracts custom output from.
72 */
73 type: string;
74 },
75 never,
76 Providers
77> {}
78
79/**
80 * An Amazon Bedrock Data Automation Blueprint — a JSON schema describing the
81 * custom output fields to extract from documents, images, audio, or video.
82 * Attach blueprints to a `DataAutomationProject` via its
83 * `customOutputConfiguration`.
84 *
85 * ### Creating Blueprints
86 * **Example:** Document blueprint with custom fields
87 * ```typescript
88 * import * as BDA from "alchemy/AWS/BedrockDataAutomation";
89 *
90 * const blueprint = yield* BDA.Blueprint("InvoiceBlueprint", {
91 * type: "DOCUMENT",
92 * schema: JSON.stringify({
93 * $schema: "http://json-schema.org/draft-07/schema#",
94 * description: "Extract invoice fields",
95 * class: "invoice",
96 * type: "object",
97 * properties: {
98 * invoice_number: {
99 * type: "string",
100 * inferenceType: "explicit",
101 * instruction: "The invoice number",
102 * },
103 * },
104 * }),
105 * });
106 * ```
107 *
108 * **Example:** Attach a blueprint to a project
109 * ```typescript
110 * const project = yield* BDA.DataAutomationProject("Docs", {
111 * standardOutputConfiguration: {},
112 * customOutputConfiguration: {
113 * blueprints: [{ blueprintArn: blueprint.blueprintArn }],
114 * },
115 * });
116 * ```
117 *
118 * @resource
119 */
120export const Blueprint = Resource<Blueprint>(
121 "AWS.BedrockDataAutomation.Blueprint",
122);
123
124export const BlueprintProvider = () =>
125 Provider.effect(
126 Blueprint,
127 Effect.gen(function* () {
128 const createName = Effect.fn(function* (
129 id: string,
130 props: Pick<BlueprintProps, "blueprintName">,
131 ) {
132 return (
133 props.blueprintName ??
134 (yield* createPhysicalName({ id, maxLength: 128 }))
135 );
136 });
137
138 const toAttributes = (blueprint: bda.Blueprint) => ({
139 blueprintArn: blueprint.blueprintArn,
140 blueprintName: unredact(blueprint.blueprintName),
141 blueprintStage: blueprint.blueprintStage ?? "LIVE",
142 type: blueprint.type as string,
143 });
144
145 const observeBlueprint = Effect.fn(function* (
146 blueprintArn: string,
147 blueprintStage?: bda.BlueprintStage,
148 ) {
149 return yield* bda.getBlueprint({ blueprintArn, blueprintStage }).pipe(
150 Effect.map((r) => r.blueprint),
151 Effect.catchTag("ResourceNotFoundException", () =>
152 Effect.succeed(undefined),
153 ),
154 );
155 });
156
157 const findBlueprintArn = Effect.fn(function* (name: string) {
158 const summaries = yield* bda.listBlueprints
159 .items({ resourceOwner: "ACCOUNT" })
160 .pipe(Stream.runCollect);
161 return Array.from(summaries).find(
162 (s) =>
163 s.blueprintName !== undefined && unredact(s.blueprintName) === name,
164 )?.blueprintArn;
165 });
166
167 return Blueprint.Provider.of({
168 stables: ["blueprintArn", "blueprintName", "type"],
169
170 list: () =>
171 Effect.gen(function* () {
172 const summaries = yield* bda.listBlueprints
173 .items({ resourceOwner: "ACCOUNT" })
174 .pipe(Stream.runCollect);
175 const attrs = yield* Effect.forEach(
176 Array.from(summaries),
177 (s) =>
178 observeBlueprint(s.blueprintArn, s.blueprintStage).pipe(
179 Effect.map((blueprint) =>
180 blueprint === undefined ? [] : [toAttributes(blueprint)],
181 ),
182 ),
183 { concurrency: 5 },
184 );
185 return attrs.flat();
186 }),
187
188 read: Effect.fn(function* ({ id, olds, output }) {
189 const blueprintArn =
190 output?.blueprintArn ??
191 (yield* findBlueprintArn(yield* createName(id, olds ?? {})));
192 if (blueprintArn === undefined) return undefined;
193 const found = yield* observeBlueprint(
194 blueprintArn,
195 olds?.blueprintStage,
196 );
197 if (found === undefined) return undefined;
198 const attrs = toAttributes(found);
199 const tags = yield* readBdaTags(blueprintArn);
200 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
201 }),
202
203 diff: Effect.fn(function* ({ id, news, olds }) {
204 if (!isResolved(news)) return undefined;
205 const oldName = yield* createName(id, olds ?? {});
206 const newName = yield* createName(id, news ?? {});
207 if (
208 oldName !== newName ||
209 olds?.type !== news.type ||
210 olds?.blueprintStage !== news.blueprintStage
211 ) {
212 return { action: "replace" } as const;
213 }
214 // fall through: engine default update logic for mutable fields
215 }),
216
217 reconcile: Effect.fn(function* ({ id, news, output, session }) {
218 const blueprintName =
219 output?.blueprintName ?? (yield* createName(id, news));
220 const internalTags = yield* createInternalTags(id);
221 const desiredTags = { ...news.tags, ...internalTags };
222
223 // 1. OBSERVE — cloud state is authoritative; output caches the ARN.
224 const cachedArn =
225 output?.blueprintArn ?? (yield* findBlueprintArn(blueprintName));
226 let live =
227 cachedArn === undefined
228 ? undefined
229 : yield* observeBlueprint(cachedArn, news.blueprintStage);
230
231 // 2. ENSURE — create when missing; a concurrent create surfaces as
232 // the typed ConflictException, which we treat as a race and
233 // re-observe by name.
234 if (live === undefined) {
235 live = yield* bda
236 .createBlueprint({
237 blueprintName,
238 type: news.type,
239 schema: news.schema,
240 blueprintStage: news.blueprintStage,
241 encryptionConfiguration: news.encryptionConfiguration,
242 tags: toBdaTagList(desiredTags),
243 })
244 .pipe(
245 Effect.map((r) => r.blueprint),
246 Effect.catchTag("ConflictException", (conflict) =>
247 Effect.gen(function* () {
248 const arn = yield* findBlueprintArn(blueprintName);
249 const observed =
250 arn === undefined
251 ? undefined
252 : yield* observeBlueprint(arn, news.blueprintStage);
253 return observed === undefined
254 ? yield* Effect.fail(conflict)
255 : observed;
256 }),
257 ),
258 );
259 }
260
261 // 3. SYNC — diff the OBSERVED schema and encryption key against the
262 // desired state; apply the idempotent PUT only on drift.
263 const kmsDrift =
264 news.encryptionConfiguration !== undefined &&
265 live.kmsKeyId !== news.encryptionConfiguration.kmsKeyId;
266 if (unredact(live.schema) !== news.schema || kmsDrift) {
267 live = yield* bda
268 .updateBlueprint({
269 blueprintArn: live.blueprintArn,
270 schema: news.schema,
271 blueprintStage: news.blueprintStage,
272 encryptionConfiguration: news.encryptionConfiguration,
273 })
274 .pipe(Effect.map((r) => r.blueprint));
275 }
276
277 // 3b. SYNC TAGS against observed cloud tags.
278 yield* syncBdaTags(live.blueprintArn, desiredTags);
279
280 yield* session.note(blueprintName);
281 return toAttributes(live);
282 }),
283
284 delete: Effect.fn(function* ({ output }) {
285 // Versions (immutable snapshots, e.g. from CreateBlueprintVersion)
286 // block deletion of the blueprint itself — delete them first.
287 // NOTE: the blueprintArn filter cannot be combined with other
288 // filters ("Invalid List filter combination").
289 const versions = yield* bda.listBlueprints
290 .items({ blueprintArn: output.blueprintArn })
291 .pipe(
292 Stream.runCollect,
293 Effect.map((summaries) =>
294 Array.from(summaries)
295 .map((s) => s.blueprintVersion)
296 .filter((v): v is string => v !== undefined),
297 ),
298 Effect.catchTag("ResourceNotFoundException", () =>
299 Effect.succeed([] as string[]),
300 ),
301 );
302 yield* Effect.forEach(
303 versions,
304 (blueprintVersion) =>
305 bda
306 .deleteBlueprint({
307 blueprintArn: output.blueprintArn,
308 blueprintVersion,
309 })
310 .pipe(
311 Effect.catchTag(
312 "ResourceNotFoundException",
313 () => Effect.void,
314 ),
315 ),
316 { concurrency: 1 },
317 );
318 yield* bda
319 .deleteBlueprint({ blueprintArn: output.blueprintArn })
320 .pipe(
321 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
322 );
323 }),
324 });
325 }),
326 );