1import * as rds from "@distilled.cloud/aws/rds";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { isResolved } from "../../Diff.ts";
5import { createPhysicalName } from "../../PhysicalName.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import type { Providers } from "../Providers.ts";
9import { createInternalTags, diffTags } from "../../Tags.ts";
10
11export interface DBClusterParameterGroupProps {
12 /**
13 * Name of the parameter group. If omitted, Alchemy generates one.
14 */
15 dbClusterParameterGroupName?: string;
16 /**
17 * Parameter group family, for example `aurora-postgresql16`.
18 */
19 family: string;
20 /**
21 * Human-readable description.
22 */
23 description?: string;
24 /**
25 * User-defined tags.
26 */
27 tags?: Record<string, string>;
28}
29
30export interface DBClusterParameterGroup extends Resource<
31 "AWS.RDS.DBClusterParameterGroup",
32 DBClusterParameterGroupProps,
33 {
34 /**
35 * Name of the cluster parameter group.
36 */
37 dbClusterParameterGroupName: string;
38 /**
39 * ARN of the cluster parameter group.
40 */
41 dbClusterParameterGroupArn: string | undefined;
42 /**
43 * Parameter group family (e.g. `aurora-postgresql16`).
44 */
45 family: string;
46 /**
47 * Description of the parameter group.
48 */
49 description: string | undefined;
50 /**
51 * Tags on the parameter group.
52 */
53 tags: Record<string, string>;
54 },
55 never,
56 Providers
57> {}
58
59/**
60 * An Aurora cluster parameter group — cluster-wide engine settings shared by
61 * every instance in a `DBCluster`.
62 *
63 * Name, family, and description changes force a replacement (RDS has no
64 * modify API for these); tags update in place.
65 * ### Creating a Cluster Parameter Group
66 * **Example:** Parameter Group for Aurora Postgres 16
67 * ```typescript
68 * const clusterParams = yield* DBClusterParameterGroup("ClusterParams", {
69 * family: "aurora-postgresql16",
70 * description: "Cluster-wide settings for the app database",
71 * });
72 * ```
73 *
74 * **Example:** Attach to a Cluster
75 * ```typescript
76 * const cluster = yield* DBCluster("Cluster", {
77 * engine: "aurora-postgresql",
78 * dbClusterParameterGroupName: clusterParams.dbClusterParameterGroupName,
79 * });
80 * ```
81 *
82 * @resource
83 */
84export const DBClusterParameterGroup = Resource<DBClusterParameterGroup>(
85 "AWS.RDS.DBClusterParameterGroup",
86);
87
88export const DBClusterParameterGroupProvider = () =>
89 Provider.effect(
90 DBClusterParameterGroup,
91 Effect.gen(function* () {
92 const toName = (id: string, props: DBClusterParameterGroupProps) =>
93 props.dbClusterParameterGroupName
94 ? Effect.succeed(props.dbClusterParameterGroupName)
95 : createPhysicalName({ id, maxLength: 255 });
96
97 const readGroup = Effect.fn(function* (name: string) {
98 const response = yield* rds
99 .describeDBClusterParameterGroups({
100 DBClusterParameterGroupName: name,
101 })
102 .pipe(
103 Effect.catchTag("DBParameterGroupNotFoundFault", () =>
104 Effect.succeed(undefined),
105 ),
106 );
107 return response?.DBClusterParameterGroups?.[0];
108 });
109
110 return {
111 stables: ["dbClusterParameterGroupArn", "dbClusterParameterGroupName"],
112 list: () =>
113 // AWS account/region collection (pattern (a)): exhaustively paginate
114 // describeDBClusterParameterGroups and map each group to the exact
115 // `read` Attributes shape. `read` derives `tags` from the cached
116 // output (the describe response does not surface tags), so list
117 // returns `tags: {}` to match — a future read/delete can hydrate
118 // them via listTagsForResource. Per-item not-found
119 // (DBParameterGroupNotFoundFault) cannot occur here: enumeration
120 // passes no name, so the describe never targets a single group.
121 rds.describeDBClusterParameterGroups.pages({}).pipe(
122 Stream.runCollect,
123 Effect.map((chunk) =>
124 Array.from(chunk).flatMap((page) =>
125 (page.DBClusterParameterGroups ?? [])
126 .filter(
127 (
128 g,
129 ): g is rds.DBClusterParameterGroup & {
130 DBClusterParameterGroupName: string;
131 } =>
132 g.DBClusterParameterGroupName != null &&
133 // AWS-managed `default.*` groups cannot be deleted
134 // (InvalidDBParameterGroupStateFault) — don't enumerate.
135 !g.DBClusterParameterGroupName.startsWith("default."),
136 )
137 .map((g) => ({
138 dbClusterParameterGroupName: g.DBClusterParameterGroupName,
139 dbClusterParameterGroupArn: g.DBClusterParameterGroupArn,
140 family: g.DBParameterGroupFamily ?? "",
141 description: g.Description,
142 tags: {} as Record<string, string>,
143 })),
144 ),
145 ),
146 ),
147 diff: Effect.fn(function* ({ id, olds, news }) {
148 if (!isResolved(news)) return;
149 if (
150 (yield* toName(
151 id,
152 olds ?? ({} as DBClusterParameterGroupProps),
153 )) !== (yield* toName(id, news))
154 ) {
155 return { action: "replace" } as const;
156 }
157 if (
158 olds?.family !== news.family ||
159 olds?.description !== news.description
160 ) {
161 return { action: "replace" } as const;
162 }
163 }),
164 read: Effect.fn(function* ({ id, olds, output }) {
165 const name =
166 output?.dbClusterParameterGroupName ??
167 (yield* toName(
168 id,
169 olds ?? ({ family: "" } as DBClusterParameterGroupProps),
170 ));
171 const group = yield* readGroup(name);
172 if (!group?.DBClusterParameterGroupName) {
173 return undefined;
174 }
175 return {
176 dbClusterParameterGroupName: group.DBClusterParameterGroupName,
177 dbClusterParameterGroupArn: group.DBClusterParameterGroupArn,
178 family: group.DBParameterGroupFamily ?? olds?.family ?? "",
179 description: group.Description,
180 tags: output?.tags ?? {},
181 };
182 }),
183 reconcile: Effect.fn(function* ({ id, news, output, session }) {
184 const name =
185 output?.dbClusterParameterGroupName ?? (yield* toName(id, news));
186 const internalTags = yield* createInternalTags(id);
187 const desiredTags = { ...internalTags, ...news.tags };
188
189 // Observe — fetch the parameter group's live state.
190 let observed = yield* readGroup(name);
191
192 // Ensure — create if missing. Tolerate
193 // `DBParameterGroupAlreadyExistsFault` as a race with a peer
194 // reconciler by re-reading.
195 if (!observed?.DBClusterParameterGroupName) {
196 yield* rds
197 .createDBClusterParameterGroup({
198 DBClusterParameterGroupName: name,
199 DBParameterGroupFamily: news.family,
200 Description:
201 news.description ?? `Alchemy parameter group ${name}`,
202 Tags: Object.entries(desiredTags).map(([Key, Value]) => ({
203 Key,
204 Value,
205 })),
206 })
207 .pipe(
208 Effect.catchTag(
209 "DBParameterGroupAlreadyExistsFault",
210 () => Effect.void,
211 ),
212 );
213 observed = yield* readGroup(name);
214 if (!observed?.DBClusterParameterGroupName) {
215 return yield* Effect.fail(
216 new Error(
217 `Failed to create DB cluster parameter group '${name}'`,
218 ),
219 );
220 }
221 }
222
223 const dbClusterParameterGroupArn =
224 observed.DBClusterParameterGroupArn;
225
226 // Sync tags — diff observed (the describe response does not
227 // surface tags, so use prior `output.tags` as the baseline) ↔
228 // desired.
229 const observedTags = output?.tags ?? {};
230 const { removed, upsert } = diffTags(observedTags, desiredTags);
231 if (upsert.length > 0 && dbClusterParameterGroupArn) {
232 yield* rds.addTagsToResource({
233 ResourceName: dbClusterParameterGroupArn,
234 Tags: upsert,
235 });
236 }
237 if (removed.length > 0 && dbClusterParameterGroupArn) {
238 yield* rds.removeTagsFromResource({
239 ResourceName: dbClusterParameterGroupArn,
240 TagKeys: removed,
241 });
242 }
243
244 yield* session.note(dbClusterParameterGroupArn ?? name);
245 return {
246 dbClusterParameterGroupName: observed.DBClusterParameterGroupName,
247 dbClusterParameterGroupArn,
248 family: observed.DBParameterGroupFamily ?? news.family,
249 description: observed.Description,
250 tags: desiredTags,
251 };
252 }),
253 delete: Effect.fn(function* ({ output }) {
254 yield* rds
255 .deleteDBClusterParameterGroup({
256 DBClusterParameterGroupName: output.dbClusterParameterGroupName,
257 })
258 .pipe(
259 Effect.catchTag(
260 "DBParameterGroupNotFoundFault",
261 () => Effect.void,
262 ),
263 );
264 }),
265 };
266 }),
267 );