1import * as b2bi from "@distilled.cloud/aws/b2bi";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
8import type { Providers } from "../Providers.ts";
9import { readB2biTags, syncB2biTags, toWireTags } from "./internal.ts";
10
11export interface CapabilityProps {
12 /**
13 * The name of the capability.
14 */
15 name: string;
16 /**
17 * The type of the capability. EDI is the only supported type today.
18 * @default "edi"
19 */
20 type?: "edi";
21 /**
22 * The capability configuration. For EDI capabilities this describes the
23 * X12 transaction set, direction, transformer, and the input/output S3
24 * locations. `configuration.edi.transformerId` may reference a
25 * {@link Transformer}'s `transformerId` output.
26 */
27 configuration: b2bi.CapabilityConfiguration;
28 /**
29 * S3 locations of instruction documents (implementation guides) attached
30 * to the capability.
31 */
32 instructionsDocuments?: b2bi.S3Location[];
33 /**
34 * User-defined tags for the capability.
35 */
36 tags?: Record<string, string>;
37}
38
39export interface Capability extends Resource<
40 "AWS.B2BI.Capability",
41 CapabilityProps,
42 {
43 /**
44 * Service-assigned unique ID of the capability.
45 */
46 capabilityId: string;
47 /**
48 * ARN of the capability.
49 */
50 capabilityArn: string;
51 /**
52 * Name of the capability.
53 */
54 name: string;
55 /**
56 * Capability type (`edi`).
57 */
58 type: string;
59 },
60 never,
61 Providers
62> {}
63
64/**
65 * An AWS B2B Data Interchange (B2BI) capability. A capability contains the
66 * information required to transform incoming or outgoing EDI documents: the
67 * X12 transaction set, a transformer, and the S3 input/output locations.
68 * ### Creating a Capability
69 * **Example:** Inbound X12 850 Capability
70 * ```typescript
71 * const capability = yield* B2BI.Capability("Orders", {
72 * name: "inbound-orders",
73 * configuration: {
74 * edi: {
75 * capabilityDirection: "INBOUND",
76 * type: { x12Details: { transactionSet: "X12_850", version: "VERSION_4010" } },
77 * inputLocation: { bucketName: bucket.bucketName, key: "inbound/" },
78 * outputLocation: { bucketName: bucket.bucketName, key: "processed/" },
79 * transformerId: transformer.transformerId,
80 * },
81 * },
82 * });
83 * ```
84 *
85 * @resource
86 */
87export const Capability = Resource<Capability>("AWS.B2BI.Capability");
88
89const toAttrs = (
90 r:
91 | b2bi.GetCapabilityResponse
92 | b2bi.CreateCapabilityResponse
93 | b2bi.UpdateCapabilityResponse,
94) => ({
95 capabilityId: r.capabilityId,
96 capabilityArn: r.capabilityArn,
97 name: r.name,
98 type: r.type,
99});
100
101export const CapabilityProvider = () =>
102 Provider.effect(
103 Capability,
104 Effect.gen(function* () {
105 const findByName = (name: string) =>
106 b2bi.listCapabilities.items({}).pipe(
107 Stream.filter((s) => s.name === name),
108 Stream.runHead,
109 Effect.flatMap((head) =>
110 head._tag === "Some"
111 ? b2bi
112 .getCapability({ capabilityId: head.value.capabilityId })
113 .pipe(
114 Effect.catchTag("ResourceNotFoundException", () =>
115 Effect.succeed(undefined),
116 ),
117 )
118 : Effect.succeed(undefined),
119 ),
120 );
121
122 return {
123 stables: ["capabilityId", "capabilityArn"],
124
125 read: Effect.fn(function* ({ id, olds, output }) {
126 const found = output?.capabilityId
127 ? yield* b2bi
128 .getCapability({ capabilityId: output.capabilityId })
129 .pipe(
130 Effect.catchTag("ResourceNotFoundException", () =>
131 Effect.succeed(undefined),
132 ),
133 )
134 : yield* findByName(olds?.name ?? "");
135 if (found === undefined) return undefined;
136 const attrs = toAttrs(found);
137 const tags = yield* readB2biTags(attrs.capabilityArn);
138 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
139 }),
140
141 reconcile: Effect.fn(function* ({ id, news, output, session }) {
142 const internalTags = yield* createInternalTags(id);
143 const desiredTags = { ...internalTags, ...news.tags };
144
145 // 1. Observe.
146 let live = output?.capabilityId
147 ? yield* b2bi
148 .getCapability({ capabilityId: output.capabilityId })
149 .pipe(
150 Effect.catchTag("ResourceNotFoundException", () =>
151 Effect.succeed(undefined),
152 ),
153 )
154 : yield* findByName(news.name);
155
156 // 2. Ensure.
157 if (live === undefined) {
158 const created = yield* b2bi.createCapability({
159 name: news.name,
160 type: news.type ?? "edi",
161 configuration: news.configuration,
162 instructionsDocuments: news.instructionsDocuments,
163 tags: toWireTags(desiredTags),
164 });
165 live = yield* b2bi.getCapability({
166 capabilityId: created.capabilityId,
167 });
168 } else {
169 // 3. Sync — converge name, configuration, and instruction docs.
170 const nameDrift = live.name !== news.name;
171 const configDrift =
172 JSON.stringify(live.configuration) !==
173 JSON.stringify(news.configuration);
174 const docsDrift =
175 JSON.stringify(live.instructionsDocuments ?? null) !==
176 JSON.stringify(news.instructionsDocuments ?? null);
177 if (nameDrift || configDrift || docsDrift) {
178 const updated = yield* b2bi.updateCapability({
179 capabilityId: live.capabilityId,
180 name: news.name,
181 configuration: news.configuration,
182 instructionsDocuments: news.instructionsDocuments,
183 });
184 live = { ...live, ...updated };
185 }
186 }
187
188 // 3b. Sync tags.
189 yield* syncB2biTags(live.capabilityArn, desiredTags);
190
191 yield* session.note(live.capabilityId);
192 return toAttrs(live);
193 }),
194
195 delete: Effect.fn(function* ({ output }) {
196 yield* b2bi
197 .deleteCapability({ capabilityId: output.capabilityId })
198 .pipe(
199 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
200 );
201 }),
202
203 list: () =>
204 b2bi.listCapabilities.items({}).pipe(
205 Stream.mapEffect((s) =>
206 b2bi.getCapability({ capabilityId: s.capabilityId }).pipe(
207 Effect.map(toAttrs),
208 Effect.catchTag("ResourceNotFoundException", () =>
209 Effect.succeed(undefined),
210 ),
211 ),
212 ),
213 Stream.filter((item) => item !== undefined),
214 Stream.runCollect,
215 Effect.map((chunk) => Array.from(chunk)),
216 ),
217 };
218 }),
219 );