1import * as neptune from "@distilled.cloud/aws/neptune";
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 { createInternalTags, diffTags } from "../../Tags.ts";
9import type { SubnetId } from "../EC2/Subnet.ts";
10import type { Providers } from "../Providers.ts";
11
12export interface DBSubnetGroupProps {
13 /**
14 * Name of the DB subnet group. If omitted, a deterministic name is generated.
15 */
16 dbSubnetGroupName?: string;
17 /**
18 * Description for the subnet group.
19 * @default "Managed by Alchemy"
20 */
21 description?: string;
22 /**
23 * Subnets the Neptune cluster may use. Must span at least two
24 * Availability Zones.
25 */
26 subnetIds: SubnetId[];
27 /**
28 * User-defined tags for the subnet group.
29 */
30 tags?: Record<string, string>;
31}
32
33export interface DBSubnetGroup extends Resource<
34 "AWS.Neptune.DBSubnetGroup",
35 DBSubnetGroupProps,
36 {
37 /** Name of the subnet group. */
38 dbSubnetGroupName: string;
39 /** ARN of the subnet group. */
40 dbSubnetGroupArn: string | undefined;
41 /** ID of the VPC the subnets belong to. */
42 vpcId: string | undefined;
43 /** IDs of the subnets in the group. */
44 subnetIds: string[];
45 /** Status of the subnet group (e.g. `Complete`). */
46 status: string | undefined;
47 /** Tags on the subnet group (user + internal Alchemy tags). */
48 tags: Record<string, string>;
49 },
50 never,
51 Providers
52> {}
53
54/**
55 * An Amazon Neptune DB subnet group — the set of VPC subnets a Neptune
56 * cluster and its instances are placed into. Neptune is VPC-only, so a
57 * subnet group spanning at least two Availability Zones is required before a
58 * cluster can be created.
59 * ### Creating a Subnet Group
60 * **Example:** Multi-AZ subnet group
61 * ```typescript
62 * const subnetGroup = yield* DBSubnetGroup("NeptuneSubnets", {
63 * subnetIds: [subnetA.subnetId, subnetB.subnetId],
64 * });
65 * ```
66 *
67 * @resource
68 */
69export const DBSubnetGroup = Resource<DBSubnetGroup>(
70 "AWS.Neptune.DBSubnetGroup",
71);
72
73export const DBSubnetGroupProvider = () =>
74 Provider.effect(
75 DBSubnetGroup,
76 Effect.gen(function* () {
77 const toName = (id: string, props: DBSubnetGroupProps) =>
78 props.dbSubnetGroupName
79 ? Effect.succeed(props.dbSubnetGroupName)
80 : createPhysicalName({ id, maxLength: 255 });
81
82 const readGroup = Effect.fn(function* (groupName: string) {
83 const response = yield* neptune
84 .describeDBSubnetGroups({
85 DBSubnetGroupName: groupName,
86 })
87 .pipe(
88 Effect.catchTag("DBSubnetGroupNotFoundFault", () =>
89 Effect.succeed(undefined),
90 ),
91 );
92 return response?.DBSubnetGroups?.[0];
93 });
94
95 return {
96 stables: ["dbSubnetGroupArn", "dbSubnetGroupName", "vpcId"],
97 diff: Effect.fn(function* ({ id, olds, news }) {
98 if (!isResolved(news)) return undefined;
99 if (
100 (yield* toName(id, olds ?? { subnetIds: [] })) !==
101 (yield* toName(id, news))
102 ) {
103 return { action: "replace" } as const;
104 }
105 }),
106 read: Effect.fn(function* ({ id, olds, output }) {
107 const name =
108 output?.dbSubnetGroupName ??
109 (yield* toName(
110 id,
111 olds ?? ({ subnetIds: [] } as DBSubnetGroupProps),
112 ));
113 const group = yield* readGroup(name);
114 if (!group?.DBSubnetGroupName) {
115 return undefined;
116 }
117
118 return {
119 dbSubnetGroupName: group.DBSubnetGroupName,
120 dbSubnetGroupArn: group.DBSubnetGroupArn,
121 vpcId: group.VpcId,
122 subnetIds: (group.Subnets ?? []).flatMap((subnet) =>
123 subnet.SubnetIdentifier ? [subnet.SubnetIdentifier] : [],
124 ),
125 status: group.SubnetGroupStatus,
126 tags: output?.tags ?? {},
127 };
128 }),
129 reconcile: Effect.fn(function* ({ id, news, output, session }) {
130 const dbSubnetGroupName =
131 output?.dbSubnetGroupName ?? (yield* toName(id, news));
132 const internalTags = yield* createInternalTags(id);
133 const desiredTags = { ...internalTags, ...news.tags };
134
135 // Observe — fetch live subnet-group state.
136 let observed = yield* readGroup(dbSubnetGroupName);
137
138 // Ensure — create if missing. Tolerate
139 // `DBSubnetGroupAlreadyExistsFault` as a race with a peer
140 // reconciler by re-reading.
141 if (!observed?.DBSubnetGroupName) {
142 yield* neptune
143 .createDBSubnetGroup({
144 DBSubnetGroupName: dbSubnetGroupName,
145 DBSubnetGroupDescription:
146 news.description ?? "Managed by Alchemy",
147 SubnetIds: news.subnetIds,
148 Tags: Object.entries(desiredTags).map(([Key, Value]) => ({
149 Key,
150 Value,
151 })),
152 })
153 .pipe(
154 Effect.catchTag(
155 "DBSubnetGroupAlreadyExistsFault",
156 () => Effect.void,
157 ),
158 );
159 observed = yield* readGroup(dbSubnetGroupName);
160 if (!observed?.DBSubnetGroupName) {
161 return yield* Effect.fail(
162 new Error(
163 `Failed to create DB subnet group '${dbSubnetGroupName}'`,
164 ),
165 );
166 }
167 } else {
168 // Sync description and subnet membership — `modifyDBSubnetGroup`
169 // accepts the full desired set and is idempotent for unchanged
170 // input.
171 yield* neptune.modifyDBSubnetGroup({
172 DBSubnetGroupName: dbSubnetGroupName,
173 DBSubnetGroupDescription:
174 news.description ?? "Managed by Alchemy",
175 SubnetIds: news.subnetIds,
176 });
177 observed = yield* readGroup(dbSubnetGroupName);
178 if (!observed?.DBSubnetGroupName) {
179 return yield* Effect.fail(
180 new Error(
181 `DB subnet group '${dbSubnetGroupName}' not found after update`,
182 ),
183 );
184 }
185 }
186
187 const dbSubnetGroupArn = observed.DBSubnetGroupArn;
188
189 // Sync tags — diff prior recorded tags against desired (describe
190 // does not surface tags inline).
191 const observedTags = output?.tags ?? {};
192 const { removed, upsert } = diffTags(observedTags, desiredTags);
193 if (upsert.length > 0 && dbSubnetGroupArn) {
194 yield* neptune.addTagsToResource({
195 ResourceName: dbSubnetGroupArn,
196 Tags: upsert,
197 });
198 }
199 if (removed.length > 0 && dbSubnetGroupArn) {
200 yield* neptune.removeTagsFromResource({
201 ResourceName: dbSubnetGroupArn,
202 TagKeys: removed,
203 });
204 }
205
206 yield* session.note(dbSubnetGroupArn ?? dbSubnetGroupName);
207 return {
208 dbSubnetGroupName: observed.DBSubnetGroupName,
209 dbSubnetGroupArn,
210 vpcId: observed.VpcId,
211 subnetIds: (observed.Subnets ?? []).flatMap((subnet) =>
212 subnet.SubnetIdentifier ? [subnet.SubnetIdentifier] : [],
213 ),
214 status: observed.SubnetGroupStatus,
215 tags: desiredTags,
216 };
217 }),
218 list: () =>
219 // AWS account/region collection: `describeDBSubnetGroups` is
220 // paginated (items field `DBSubnetGroups`). Subnet groups are shared
221 // across the RDS-family engines (RDS/DocumentDB/Neptune), so this
222 // enumerates all of them — mirroring the DocDB provider. `read` does
223 // not hydrate tags from the cloud (it returns the cached
224 // `output.tags`), so we mirror that here with an empty tag map.
225 neptune.describeDBSubnetGroups.pages({}).pipe(
226 Stream.runCollect,
227 Effect.map((chunk) =>
228 Array.from(chunk).flatMap((page) =>
229 (page.DBSubnetGroups ?? []).flatMap((group) =>
230 group.DBSubnetGroupName
231 ? [
232 {
233 dbSubnetGroupName: group.DBSubnetGroupName,
234 dbSubnetGroupArn: group.DBSubnetGroupArn,
235 vpcId: group.VpcId,
236 subnetIds: (group.Subnets ?? []).flatMap((subnet) =>
237 subnet.SubnetIdentifier
238 ? [subnet.SubnetIdentifier]
239 : [],
240 ),
241 status: group.SubnetGroupStatus,
242 tags: {} as Record<string, string>,
243 },
244 ]
245 : [],
246 ),
247 ),
248 ),
249 ),
250 delete: Effect.fn(function* ({ output }) {
251 yield* neptune
252 .deleteDBSubnetGroup({
253 DBSubnetGroupName: output.dbSubnetGroupName,
254 })
255 .pipe(
256 Effect.catchTag("DBSubnetGroupNotFoundFault", () => Effect.void),
257 );
258 }),
259 };
260 }),
261 );