1import * as appflow from "@distilled.cloud/aws/appflow";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import type { Providers } from "../Providers.ts";
8
9export interface ConnectorProfileProps {
10 /**
11 * The name of the connector profile. Unique per account/region and used as
12 * the profile's identity. Changing it replaces the profile.
13 */
14 connectorProfileName: string;
15 /**
16 * The type of connector (e.g. `Salesforce`, `Redshift`, `Snowflake`). S3
17 * and EventBridge do not require a connector profile.
18 */
19 connectorType: appflow.ConnectorType;
20 /**
21 * The label of the custom connector, required only for the
22 * `CustomConnector` type.
23 */
24 connectorLabel?: string;
25 /**
26 * Whether the connector connects over the public internet or a private
27 * PrivateLink connection.
28 * @default "Public"
29 */
30 connectionMode?: "Public" | "Private";
31 /**
32 * The connector-specific properties and credentials. Credentials
33 * (OAuth tokens, API keys, secrets) are supplied here — these typically
34 * require a human-in-the-loop authorization step with the vendor.
35 */
36 connectorProfileConfig: appflow.ConnectorProfileConfig;
37 /**
38 * The ARN of a KMS key AppFlow uses to encrypt the stored credentials.
39 */
40 kmsArn?: string;
41}
42
43export interface ConnectorProfile extends Resource<
44 "AWS.AppFlow.ConnectorProfile",
45 ConnectorProfileProps,
46 {
47 connectorProfileName: string;
48 connectorProfileArn: string;
49 connectorType: string | undefined;
50 credentialsArn: string | undefined;
51 },
52 never,
53 Providers
54> {}
55
56/**
57 * An Amazon AppFlow connector profile. A connector profile stores the
58 * connection settings and credentials for a SaaS/data-warehouse connector
59 * (Salesforce, Snowflake, Redshift, etc.) so flows can reference it.
60 *
61 * Most connectors require vendor credentials obtained through a
62 * human-in-the-loop OAuth or API-key step, so a connector profile's live
63 * lifecycle generally cannot be created purely programmatically. S3 and
64 * EventBridge flows do not need a connector profile at all.
65 * ### Creating a Connector Profile
66 * **Example:** Redshift Connector Profile
67 * ```typescript
68 * const profile = yield* AppFlow.ConnectorProfile("Warehouse", {
69 * connectorProfileName: "warehouse",
70 * connectorType: "Redshift",
71 * connectionMode: "Public",
72 * connectorProfileConfig: {
73 * connectorProfileProperties: {
74 * Redshift: {
75 * databaseUrl: "jdbc:redshift://cluster:5439/db",
76 * bucketName: "appflow-staging",
77 * roleArn: role.roleArn,
78 * },
79 * },
80 * connectorProfileCredentials: {
81 * Redshift: { username: "admin", password: "..." },
82 * },
83 * },
84 * });
85 * ```
86 *
87 * @resource
88 */
89export const ConnectorProfile = Resource<ConnectorProfile>(
90 "AWS.AppFlow.ConnectorProfile",
91);
92
93export const ConnectorProfileProvider = () =>
94 Provider.effect(
95 ConnectorProfile,
96 Effect.gen(function* () {
97 /** Describe a single connector profile by name; undefined if absent. */
98 const observe = (name: string) =>
99 appflow
100 .describeConnectorProfiles({ connectorProfileNames: [name] })
101 .pipe(
102 Effect.map((r) =>
103 (r.connectorProfileDetails ?? []).find(
104 (p) => p.connectorProfileName === name,
105 ),
106 ),
107 );
108
109 const toAttrs = (name: string, profile: appflow.ConnectorProfile) => ({
110 connectorProfileName: name,
111 connectorProfileArn: profile.connectorProfileArn!,
112 connectorType: profile.connectorType,
113 credentialsArn: profile.credentialsArn,
114 });
115
116 return {
117 stables: ["connectorProfileName", "connectorProfileArn"],
118
119 diff: Effect.fn(function* ({ olds = {}, news }) {
120 if (!isResolved(news)) return undefined;
121 if (
122 (olds.connectorProfileName !== undefined &&
123 olds.connectorProfileName !== news.connectorProfileName) ||
124 (olds.connectorType !== undefined &&
125 olds.connectorType !== news.connectorType)
126 ) {
127 return { action: "replace" } as const;
128 }
129 }),
130
131 read: Effect.fn(function* ({ olds, output }) {
132 const name =
133 output?.connectorProfileName ?? olds?.connectorProfileName;
134 if (name === undefined) return undefined;
135 const profile = yield* observe(name);
136 if (
137 profile === undefined ||
138 profile.connectorProfileArn === undefined
139 )
140 return undefined;
141 return toAttrs(name, profile);
142 }),
143
144 reconcile: Effect.fn(function* ({ news, session }) {
145 const name = news.connectorProfileName;
146 const connectionMode = news.connectionMode ?? "Public";
147
148 // 1. Observe.
149 let live = yield* observe(name);
150
151 // 2/3. Ensure + sync — create if missing, else push the new config
152 // (credentials cannot be read back, so an update is always sent).
153 if (live === undefined) {
154 yield* appflow.createConnectorProfile({
155 connectorProfileName: name,
156 connectorType: news.connectorType,
157 connectorLabel: news.connectorLabel,
158 connectionMode,
159 kmsArn: news.kmsArn,
160 connectorProfileConfig: news.connectorProfileConfig,
161 });
162 } else {
163 yield* appflow.updateConnectorProfile({
164 connectorProfileName: name,
165 connectionMode,
166 connectorProfileConfig: news.connectorProfileConfig,
167 });
168 }
169 live = yield* observe(name);
170 if (live === undefined || live.connectorProfileArn === undefined) {
171 return yield* Effect.fail(
172 new Error(
173 `Connector profile '${name}' was not found after reconcile`,
174 ),
175 );
176 }
177
178 yield* session.note(name);
179 return toAttrs(name, live);
180 }),
181
182 delete: Effect.fn(function* ({ output }) {
183 yield* appflow
184 .deleteConnectorProfile({
185 connectorProfileName: output.connectorProfileName,
186 forceDelete: true,
187 })
188 .pipe(
189 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
190 );
191 }),
192
193 // maxResults is load-bearing: a fully-empty request body serializes
194 // to null and AppFlow rejects it ("The request object must be
195 // non-null", verified live).
196 list: () =>
197 appflow.describeConnectorProfiles.pages({ maxResults: 100 }).pipe(
198 Stream.runCollect,
199 Effect.map((chunk) =>
200 Array.from(chunk).flatMap((page) =>
201 (page.connectorProfileDetails ?? [])
202 .filter(
203 (
204 p,
205 ): p is appflow.ConnectorProfile & {
206 connectorProfileName: string;
207 connectorProfileArn: string;
208 } =>
209 p.connectorProfileName !== undefined &&
210 p.connectorProfileArn !== undefined,
211 )
212 .map((p) => toAttrs(p.connectorProfileName, p)),
213 ),
214 ),
215 ),
216 };
217 }),
218 );