1import * as neptune from "@distilled.cloud/aws/neptune";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as Stream from "effect/Stream";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, diffTags } from "../../Tags.ts";
10import type { Providers } from "../Providers.ts";
11
12export interface DBClusterParameterGroupProps {
13 /**
14 * Name of the DB cluster parameter group. If omitted, a deterministic name
15 * is generated. Changing it forces replacement.
16 */
17 dbClusterParameterGroupName?: string;
18 /**
19 * DB cluster parameter group family, e.g. `neptune1.4`, `neptune1.3`.
20 * Immutable — forces replacement.
21 */
22 family: string;
23 /**
24 * Description for the parameter group.
25 * @default "Managed by Alchemy"
26 */
27 description?: string;
28 /**
29 * Cluster parameter overrides, e.g.
30 * `{ neptune_query_timeout: "120000" }`. Parameters removed from this map
31 * are reset to their engine defaults. Static parameters are applied with
32 * `pending-reboot`, dynamic parameters with `immediate`.
33 */
34 parameters?: Record<string, string>;
35 /**
36 * User-defined tags for the parameter group.
37 */
38 tags?: Record<string, string>;
39}
40
41export interface DBClusterParameterGroup extends Resource<
42 "AWS.Neptune.DBClusterParameterGroup",
43 DBClusterParameterGroupProps,
44 {
45 /** Name of the parameter group. */
46 dbClusterParameterGroupName: string;
47 /** ARN of the parameter group. */
48 dbClusterParameterGroupArn: string | undefined;
49 /** Parameter group family (e.g. `neptune1.4`). */
50 family: string;
51 /** Description of the parameter group. */
52 description: string | undefined;
53 /** Non-default parameter values applied to the group. */
54 parameters: Record<string, string>;
55 /** Tags on the parameter group (user + internal Alchemy tags). */
56 tags: Record<string, string>;
57 },
58 never,
59 Providers
60> {}
61
62/**
63 * An Amazon Neptune DB cluster parameter group — a named set of engine
64 * configuration parameters (query timeout, audit logging, ...) that can be
65 * attached to one or more Neptune {@link DBCluster}s.
66 * ### Creating a Parameter Group
67 * **Example:** Parameter group with a custom query timeout
68 * ```typescript
69 * const params = yield* DBClusterParameterGroup("Params", {
70 * family: "neptune1.4",
71 * parameters: {
72 * neptune_query_timeout: "120000",
73 * },
74 * });
75 * ```
76 *
77 * ### Attaching to a Cluster
78 * **Example:** Cluster using the parameter group
79 * ```typescript
80 * const cluster = yield* DBCluster("Graph", {
81 * dbSubnetGroupName: subnetGroup.dbSubnetGroupName,
82 * dbClusterParameterGroupName: params.dbClusterParameterGroupName,
83 * });
84 * ```
85 *
86 * @resource
87 */
88export const DBClusterParameterGroup = Resource<DBClusterParameterGroup>(
89 "AWS.Neptune.DBClusterParameterGroup",
90);
91
92const toTagRecord = (
93 tags: Array<{ Key?: string; Value?: string }> | undefined,
94): Record<string, string> =>
95 Object.fromEntries(
96 (tags ?? [])
97 .filter(
98 (tag): tag is { Key: string; Value: string } =>
99 typeof tag.Key === "string" && typeof tag.Value === "string",
100 )
101 .map((tag) => [tag.Key, tag.Value]),
102 );
103
104/**
105 * A parameter modify/reset issued immediately after another change fails with
106 * `InvalidDBParameterGroupStateFault` ("has pending changes") until the prior
107 * change settles — retry it on a short bounded schedule.
108 */
109const retryWhileParameterGroupBusy = <A, E extends { _tag: string }, R>(
110 self: Effect.Effect<A, E, R>,
111): Effect.Effect<A, E, R> =>
112 Effect.retry(self, {
113 while: (e) => e._tag === "InvalidDBParameterGroupStateFault",
114 schedule: Schedule.max([Schedule.fixed("5 seconds"), Schedule.recurs(10)]),
115 });
116
117export const DBClusterParameterGroupProvider = () =>
118 Provider.effect(
119 DBClusterParameterGroup,
120 Effect.gen(function* () {
121 const toName = (id: string, props: DBClusterParameterGroupProps) =>
122 props.dbClusterParameterGroupName
123 ? Effect.succeed(props.dbClusterParameterGroupName)
124 : createPhysicalName({ id, maxLength: 255 });
125
126 const readGroup = Effect.fn(function* (groupName: string) {
127 const response = yield* neptune
128 .describeDBClusterParameterGroups({
129 DBClusterParameterGroupName: groupName,
130 })
131 .pipe(
132 Effect.catchTag("DBParameterGroupNotFoundFault", () =>
133 Effect.succeed(undefined),
134 ),
135 );
136 return response?.DBClusterParameterGroups?.[0];
137 });
138
139 // All parameters (defaults + overrides) with their current values and
140 // apply types — the observed baseline for the parameter sync.
141 const readParameters = Effect.fn(function* (groupName: string) {
142 return yield* neptune.describeDBClusterParameters
143 .pages({ DBClusterParameterGroupName: groupName })
144 .pipe(
145 Stream.runCollect,
146 Effect.map((chunk) =>
147 Array.from(chunk).flatMap((page) => page.Parameters ?? []),
148 ),
149 );
150 });
151
152 // The subset of parameters the user has overridden (Source `user`) —
153 // used to compute resets when a prop entry is removed.
154 const readUserParameters = Effect.fn(function* (groupName: string) {
155 return yield* neptune.describeDBClusterParameters
156 .pages({ DBClusterParameterGroupName: groupName, Source: "user" })
157 .pipe(
158 Stream.runCollect,
159 Effect.map((chunk) =>
160 Array.from(chunk).flatMap((page) => page.Parameters ?? []),
161 ),
162 );
163 });
164
165 const readTags = Effect.fn(function* (arn: string | undefined) {
166 if (!arn) return {} as Record<string, string>;
167 const response = yield* neptune.listTagsForResource({
168 ResourceName: arn,
169 });
170 return toTagRecord(response?.TagList);
171 });
172
173 const toUserParameterRecord = (
174 parameters: neptune.Parameter[],
175 ): Record<string, string> =>
176 Object.fromEntries(
177 parameters.flatMap((p) =>
178 p.ParameterName !== undefined && p.ParameterValue !== undefined
179 ? [[p.ParameterName, p.ParameterValue]]
180 : [],
181 ),
182 );
183
184 return {
185 stables: [
186 "dbClusterParameterGroupArn",
187 "dbClusterParameterGroupName",
188 "family",
189 ],
190 diff: Effect.fn(function* ({ id, olds, news }) {
191 if (!isResolved(news)) return undefined;
192 if (
193 (yield* toName(id, olds ?? { family: "" })) !==
194 (yield* toName(id, news))
195 ) {
196 return { action: "replace" } as const;
197 }
198 // Family is immutable — any change forces a fresh group.
199 if (olds !== undefined && olds.family !== news.family) {
200 return { action: "replace" } as const;
201 }
202 }),
203 read: Effect.fn(function* ({ id, olds, output }) {
204 const name =
205 output?.dbClusterParameterGroupName ??
206 (yield* toName(
207 id,
208 olds ?? ({ family: "" } as DBClusterParameterGroupProps),
209 ));
210 const group = yield* readGroup(name);
211 if (!group?.DBClusterParameterGroupName) {
212 return undefined;
213 }
214 const userParameters = yield* readUserParameters(name);
215 const tags = yield* readTags(group.DBClusterParameterGroupArn);
216 return {
217 dbClusterParameterGroupName: group.DBClusterParameterGroupName,
218 dbClusterParameterGroupArn: group.DBClusterParameterGroupArn,
219 family: group.DBParameterGroupFamily ?? "",
220 description: group.Description,
221 parameters: toUserParameterRecord(userParameters),
222 tags,
223 };
224 }),
225 reconcile: Effect.fn(function* ({ id, news, output, session }) {
226 const name =
227 output?.dbClusterParameterGroupName ?? (yield* toName(id, news));
228 const internalTags = yield* createInternalTags(id);
229 const desiredTags = { ...internalTags, ...news.tags };
230
231 // Observe — fetch live parameter-group state.
232 let observed = yield* readGroup(name);
233
234 // Ensure — create if missing. Tolerate
235 // `DBParameterGroupAlreadyExistsFault` as a race with a peer
236 // reconciler by re-reading.
237 if (!observed?.DBClusterParameterGroupName) {
238 yield* neptune
239 .createDBClusterParameterGroup({
240 DBClusterParameterGroupName: name,
241 DBParameterGroupFamily: news.family,
242 Description: news.description ?? "Managed by Alchemy",
243 Tags: Object.entries(desiredTags).map(([Key, Value]) => ({
244 Key,
245 Value,
246 })),
247 })
248 .pipe(
249 Effect.catchTag(
250 "DBParameterGroupAlreadyExistsFault",
251 () => Effect.void,
252 ),
253 );
254 observed = yield* readGroup(name);
255 if (!observed?.DBClusterParameterGroupName) {
256 return yield* Effect.fail(
257 new Error(
258 `Failed to create DB cluster parameter group '${name}'`,
259 ),
260 );
261 }
262 }
263
264 // Sync parameters — diff observed cloud values against desired.
265 const desiredParameters = news.parameters ?? {};
266 const allParameters = yield* readParameters(name);
267 const byName = new Map(
268 allParameters.map((p) => [p.ParameterName, p]),
269 );
270
271 const toModify: neptune.Parameter[] = Object.entries(
272 desiredParameters,
273 ).flatMap(([ParameterName, ParameterValue]) => {
274 const current = byName.get(ParameterName);
275 if (current?.ParameterValue === ParameterValue) return [];
276 return [
277 {
278 ParameterName,
279 ParameterValue,
280 ApplyMethod:
281 current?.ApplyType === "static"
282 ? "pending-reboot"
283 : "immediate",
284 },
285 ];
286 });
287 if (toModify.length > 0) {
288 // The API caps a single call at 20 parameters.
289 for (let i = 0; i < toModify.length; i += 20) {
290 yield* retryWhileParameterGroupBusy(
291 neptune.modifyDBClusterParameterGroup({
292 DBClusterParameterGroupName: name,
293 Parameters: toModify.slice(i, i + 20),
294 }),
295 );
296 }
297 }
298
299 // Reset user-overridden parameters that were removed from props.
300 const userParameters = yield* readUserParameters(name);
301 const toReset = userParameters.flatMap((p) =>
302 p.ParameterName !== undefined &&
303 !(p.ParameterName in desiredParameters)
304 ? [
305 {
306 ParameterName: p.ParameterName,
307 ApplyMethod:
308 p.ApplyType === "static"
309 ? ("pending-reboot" as const)
310 : ("immediate" as const),
311 },
312 ]
313 : [],
314 );
315 if (toReset.length > 0) {
316 for (let i = 0; i < toReset.length; i += 20) {
317 yield* retryWhileParameterGroupBusy(
318 neptune.resetDBClusterParameterGroup({
319 DBClusterParameterGroupName: name,
320 ResetAllParameters: false,
321 Parameters: toReset.slice(i, i + 20),
322 }),
323 );
324 }
325 }
326
327 const arn = observed.DBClusterParameterGroupArn;
328
329 // Sync tags — diff observed cloud tags against desired.
330 const observedTags = yield* readTags(arn);
331 const { removed, upsert } = diffTags(observedTags, desiredTags);
332 if (upsert.length > 0 && arn) {
333 yield* neptune.addTagsToResource({
334 ResourceName: arn,
335 Tags: upsert,
336 });
337 }
338 if (removed.length > 0 && arn) {
339 yield* neptune.removeTagsFromResource({
340 ResourceName: arn,
341 TagKeys: removed,
342 });
343 }
344
345 yield* session.note(arn ?? name);
346 return {
347 dbClusterParameterGroupName: observed.DBClusterParameterGroupName,
348 dbClusterParameterGroupArn: arn,
349 family: observed.DBParameterGroupFamily ?? news.family,
350 description: observed.Description,
351 parameters: desiredParameters,
352 tags: desiredTags,
353 };
354 }),
355 list: () =>
356 // AWS account/region collection: the RDS-family control plane
357 // serves parameter groups for every engine, so keep only families
358 // beginning with `neptune`. Hydrating per-group parameters/tags
359 // would fan out one call per item — mirror `DBSubnetGroup.list` and
360 // emit empty maps instead.
361 neptune.describeDBClusterParameterGroups.pages({}).pipe(
362 Stream.runCollect,
363 Effect.map((chunk) =>
364 Array.from(chunk).flatMap((page) =>
365 (page.DBClusterParameterGroups ?? []).flatMap((group) =>
366 group.DBClusterParameterGroupName &&
367 group.DBParameterGroupFamily?.startsWith("neptune")
368 ? [
369 {
370 dbClusterParameterGroupName:
371 group.DBClusterParameterGroupName,
372 dbClusterParameterGroupArn:
373 group.DBClusterParameterGroupArn,
374 family: group.DBParameterGroupFamily,
375 description: group.Description,
376 parameters: {} as Record<string, string>,
377 tags: {} as Record<string, string>,
378 },
379 ]
380 : [],
381 ),
382 ),
383 ),
384 ),
385 delete: Effect.fn(function* ({ output }) {
386 yield* neptune
387 .deleteDBClusterParameterGroup({
388 DBClusterParameterGroupName: output.dbClusterParameterGroupName,
389 })
390 .pipe(
391 Effect.catchTag(
392 "DBParameterGroupNotFoundFault",
393 () => Effect.void,
394 ),
395 );
396 }),
397 };
398 }),
399 );