1import type * as secretsmanager from "@distilled.cloud/aws/secrets-manager";
2import type * as Effect from "effect/Effect";
3import * as Redacted from "effect/Redacted";
4import * as Binding from "../../Binding.ts";
5import type { Input } from "../../Input.ts";
6import type { RuntimeContext } from "../../RuntimeContext.ts";
7import { connectEnvPrefix as makeConnectEnvPrefix } from "../Connection/internal.ts";
8import type { SecurityGroupId } from "../EC2/SecurityGroup.ts";
9import type { SubnetId } from "../EC2/Subnet.ts";
10import type { Secret } from "../SecretsManager/Secret.ts";
11import type { DBCluster } from "./DBCluster.ts";
12
13/**
14 * Connection descriptor for a DocumentDB cluster's MongoDB-compatible data
15 * plane. The `url` field is a ready-to-use `mongodb://` connection string —
16 * it feeds the {@link mongo} Effect client (or any MongoDB driver) directly.
17 */
18export interface MongoConnectionInfo {
19 /** Writer (cluster) endpoint hostname. */
20 host: string;
21 /** Listener port (27017 by default). */
22 port: number;
23 /** Database name, when one was requested. */
24 database?: string;
25 /** Login user resolved from the secret. */
26 username?: string;
27 /** Login password resolved from the secret. */
28 password?: Redacted.Redacted<string>;
29 /** Whether the connection uses TLS (DocumentDB clusters default to on). */
30 tls: boolean;
31 /**
32 * `mongodb://` connection URL carrying the DocumentDB-recommended options
33 * (`replicaSet=rs0`, `readPreference=secondaryPreferred`,
34 * `retryWrites=false`). `Redacted` because it embeds the password.
35 *
36 * DocumentDB certificates chain to the private Amazon RDS CA that Node's
37 * trust store doesn't carry, so the URL sets
38 * `tlsAllowInvalidCertificates=true` (TLS encryption on, identity
39 * verification off — libpq `require` semantics, matching
40 * `AWS.RDS.Connect`). Pass the RDS CA bundle to {@link mongo}'s `ca`
41 * option to restore full verification.
42 */
43 url: Redacted.Redacted<string>;
44}
45
46/**
47 * Environment variable prefix under which the connect binding publishes the
48 * cluster endpoint on the host Function, derived from the cluster's logical
49 * ID. A cluster with logical ID `Docs` yields `DOCDB_DOCS` and the variables
50 * `DOCDB_DOCS_HOST` and `DOCDB_DOCS_PORT`.
51 */
52export const connectEnvPrefix = (logicalId: string): string =>
53 makeConnectEnvPrefix("DOCDB", logicalId);
54
55export interface ConnectOptions {
56 /**
57 * Secrets Manager secret holding `{ username, password }` for the
58 * MongoDB-protocol login. Defaults to the cluster's managed master user
59 * secret (`manageMasterUserPassword: true`).
60 *
61 * DocumentDB authenticates over the MongoDB wire protocol (SCRAM) —
62 * database users and roles (`read`, `readWrite`, `dbAdmin`, …) are created
63 * *inside* the database with `db.createUser(...)`, not through IAM. IAM
64 * only governs the management plane, so unlike Aurora there is no
65 * `auth: "iam"` mode.
66 */
67 secret?: Secret;
68 /** Database to select in the connection URL and {@link mongo} client. */
69 database?: string;
70 /** Override the cluster's listener port. */
71 port?: number;
72 /**
73 * Whether the connection uses TLS. Must match the cluster's `tls`
74 * parameter (on by default for DocumentDB).
75 * @default true
76 */
77 tls?: boolean;
78 /**
79 * Subnets the host Function should be attached to so it can open a socket
80 * to the cluster — DocumentDB is VPC-only. Wired through the host's `vpc`
81 * binding channel, equivalent to setting the Function's `vpc` prop.
82 */
83 subnetIds?: Input<SubnetId[]>;
84 /**
85 * Security groups for the host Function's VPC attachment. The referenced
86 * groups must be allowed ingress on the cluster port by the cluster's
87 * security groups.
88 */
89 securityGroupIds?: Input<SecurityGroupId[]>;
90}
91
92/**
93 * Runtime binding that resolves MongoDB connection settings for a DocumentDB
94 * {@link DBCluster} from a Secrets Manager secret (the cluster's managed
95 * master user secret by default).
96 *
97 * Binding it yields an Effect (not a callable) that resolves a
98 * {@link MongoConnectionInfo} — host, port, credentials, and a ready-to-use
99 * `mongodb://` URL — fresh on every execution. No socket is opened; feed the
100 * result into {@link mongo} (the bundled Effect client over the `mongodb`
101 * driver) or any MongoDB driver. The deploy half grants
102 * `secretsmanager:GetSecretValue` on the secret and publishes the endpoint
103 * as environment variables. Provide the implementation with
104 * `Effect.provide(AWS.DocDB.ConnectHttp)`.
105 * ### Connecting to a Cluster
106 * **Example:** Query DocumentDB from a Function
107 * ```typescript
108 * export default MyFunction.make(
109 * { main: import.meta.url, functionUrl: true },
110 * Effect.gen(function* () {
111 * // init — bind the cluster's managed master secret; grants
112 * // secretsmanager:GetSecretValue and attaches the function to the
113 * // cluster's VPC subnets/security groups
114 * const connect = yield* AWS.DocDB.Connect(cluster, {
115 * database: "app",
116 * subnetIds: [subnetA.subnetId, subnetB.subnetId],
117 * securityGroupIds: [appSecurityGroup.groupId],
118 * });
119 * // init — build the Effect mongo client (one connection per execution)
120 * const db = yield* AWS.DocDB.mongo(connect);
121 *
122 * return {
123 * fetch: Effect.gen(function* () {
124 * const { use } = yield* db;
125 * const orders = yield* use((db) =>
126 * db.collection("orders").find({ open: true }).toArray(),
127 * );
128 * return yield* HttpServerResponse.json({ count: orders.length });
129 * }).pipe(Effect.orDie),
130 * };
131 * }).pipe(Effect.provide(AWS.DocDB.ConnectHttp)),
132 * );
133 * ```
134 *
135 * **Example:** Resolve Raw Connection Info
136 * ```typescript
137 * // init
138 * const connect = yield* AWS.DocDB.Connect(cluster, { database: "app" });
139 *
140 * // runtime — host/port/credentials plus a ready-to-use mongodb:// URL
141 * const info = yield* connect;
142 * ```
143 *
144 * @binding
145 */
146export interface Connect extends Binding.Service<
147 Connect,
148 "AWS.DocDB.Connect",
149 (
150 cluster: DBCluster,
151 options?: ConnectOptions,
152 ) => Effect.Effect<
153 Effect.Effect<
154 MongoConnectionInfo,
155 secretsmanager.GetSecretValueError,
156 RuntimeContext
157 >
158 >
159> {}
160export const Connect = Binding.Service<Connect>("AWS.DocDB.Connect");
161
162export interface MongoConnectionUrlOptions {
163 host: string;
164 port?: number;
165 database?: string;
166 username?: string;
167 password?: string | Redacted.Redacted<string>;
168 /** Appends the DocumentDB TLS options when `true`. */
169 tls?: boolean;
170 /** Extra query parameters to append verbatim. */
171 params?: Record<string, string>;
172}
173
174/**
175 * Format a `mongodb://` connection URL with the DocumentDB-recommended
176 * options (`replicaSet=rs0`, `readPreference=secondaryPreferred`,
177 * `retryWrites=false`). Username and password are percent-encoded; the
178 * result is `Redacted` because it embeds the password.
179 */
180export const formatMongoConnectionUrl = (
181 options: MongoConnectionUrlOptions,
182): Redacted.Redacted<string> => {
183 const password =
184 options.password === undefined
185 ? undefined
186 : typeof options.password === "string"
187 ? options.password
188 : Redacted.value(options.password);
189 const auth =
190 options.username !== undefined
191 ? password !== undefined
192 ? `${encodeURIComponent(options.username)}:${encodeURIComponent(password)}@`
193 : `${encodeURIComponent(options.username)}@`
194 : "";
195 const port = options.port !== undefined ? `:${options.port}` : "";
196 const database =
197 options.database !== undefined
198 ? `/${encodeURIComponent(options.database)}`
199 : "/";
200 const query = new URLSearchParams({
201 replicaSet: "rs0",
202 readPreference: "secondaryPreferred",
203 retryWrites: "false",
204 ...options.params,
205 });
206 if (options.tls === true) {
207 query.set("tls", "true");
208 // DocumentDB certs chain to the private Amazon RDS CA — keep TLS on with
209 // identity verification off unless the caller supplies the CA bundle
210 // (see `mongo`'s `ca` option, which restores full verification).
211 if (!query.has("tlsCAFile")) {
212 query.set("tlsAllowInvalidCertificates", "true");
213 }
214 }
215 return Redacted.make(
216 `mongodb://${auth}${options.host}${port}${database}?${query.toString()}`,
217 );
218};