1import * as Data from "effect/Data";
2import * as Effect from "effect/Effect";
3import * as Redacted from "effect/Redacted";
4import type { Db, MongoClient, MongoClientOptions } from "mongodb";
5import { makeExecutionMemo } from "../../Runtime/ExecutionMemo.ts";
6import type { MongoConnectionInfo } from "./Connect.ts";
7
8// `mongodb` is an optional peer dependency — loaded lazily so importing the
9// AWS provider barrel never requires the driver unless a client is built.
10const importMongodb = () =>
11 import("mongodb").catch((cause) => {
12 throw new Error(
13 "Failed to load the 'mongodb' driver. Install the optional peer dependency 'mongodb' to connect to DocumentDB.",
14 { cause },
15 );
16 });
17
18/** A failure raised by the underlying `mongodb` driver. */
19export class MongoError extends Data.TaggedError("AWS.DocDB.MongoError")<{
20 cause: unknown;
21}> {
22 override get message() {
23 return String(this.cause);
24 }
25}
26
27/**
28 * A connected, Effect-typed MongoDB client over a DocumentDB cluster. Built
29 * at most once per execution (Lambda invoke, Worker event) and closed when
30 * the execution settles — see {@link mongo}.
31 */
32export interface MongoClusterClient {
33 /** The connected native `mongodb` driver client. */
34 readonly client: MongoClient;
35 /**
36 * The default database — from {@link MongoOptions.database}, the
37 * connection info's `database`, or the cluster's default.
38 */
39 readonly db: Db;
40 /**
41 * Run a promise-returning function against the client as a typed Effect —
42 * driver rejections surface as {@link MongoError}.
43 */
44 readonly use: <T>(
45 fn: (db: Db, client: MongoClient) => Promise<T>,
46 ) => Effect.Effect<T, MongoError>;
47}
48
49export interface MongoOptions {
50 /** Database to select (overrides the connection info's `database`). */
51 database?: string;
52 /**
53 * PEM bundle of the Amazon RDS CA (`global-bundle.pem` from
54 * `truststore.pki.rds.amazonaws.com`). When provided, full TLS identity
55 * verification is enabled; without it the connection encrypts but does not
56 * verify the server certificate (DocumentDB certs chain to a private CA
57 * absent from Node's trust store).
58 */
59 ca?: string;
60 /** Extra options passed through to the `mongodb` driver. */
61 clientOptions?: MongoClientOptions;
62}
63
64/**
65 * Open an Effect-typed MongoDB client from a DocumentDB connection (the
66 * runtime Effect produced by binding `AWS.DocDB.Connect`).
67 *
68 * The connect work is deferred until first use and memoized on the current
69 * execution's `Scope` (via `makeExecutionMemo`), so the driver connection is
70 * built at most once per execution — a Lambda invocation or Worker event —
71 * and its `close` finalizer fires when the execution settles, never held
72 * across events. This is the one legal pooling shape on workerd (sockets are
73 * IoContext-pinned) and the correct one on Lambda.
74 *
75 * DocumentDB authenticates over the MongoDB wire protocol: database users
76 * and their built-in roles (`read`, `readWrite`, `dbAdmin`,
77 * `clusterAdmin`, …) are managed *inside* the database with
78 * `db.createUser(...)` — IAM only governs the management plane.
79 * ### Connecting to a Cluster
80 * **Example:** Query a Collection inside a Function
81 * ```typescript
82 * // init — bind the cluster, then build the client
83 * const connect = yield* AWS.DocDB.Connect(cluster, { database: "app" });
84 * const db = yield* AWS.DocDB.mongo(connect);
85 *
86 * // runtime — one driver connection per execution, closed on settle
87 * const { use } = yield* db;
88 * const open = yield* use((db) =>
89 * db.collection("orders").find({ open: true }).toArray(),
90 * );
91 * ```
92 *
93 * **Example:** Create a Database User (DB-plane auth)
94 * ```typescript
95 * const { use } = yield* db;
96 * yield* use((db) =>
97 * db.admin().command({
98 * createUser: "reporting",
99 * pwd: reportingPassword,
100 * roles: [{ role: "read", db: "app" }],
101 * }),
102 * );
103 * ```
104 *
105 * @binding
106 */
107export const mongo = <E, R>(
108 connection: Effect.Effect<MongoConnectionInfo, E, R>,
109 options?: MongoOptions,
110) =>
111 makeExecutionMemo(
112 Effect.gen(function* () {
113 const info = yield* connection;
114 const client = yield* Effect.acquireRelease(
115 Effect.tryPromise({
116 try: async () => {
117 const { MongoClient } = await importMongodb();
118 return new MongoClient(Redacted.value(info.url), {
119 ...(options?.ca !== undefined
120 ? // A caller-supplied CA restores full identity verification
121 // (overriding the URL's tlsAllowInvalidCertificates).
122 { ca: options.ca, tlsAllowInvalidCertificates: false }
123 : {}),
124 ...options?.clientOptions,
125 }).connect();
126 },
127 catch: (cause) => new MongoError({ cause }),
128 }),
129 (client) => Effect.promise(() => client.close().catch(() => {})),
130 );
131 const db = client.db(options?.database ?? info.database);
132 const use = <T>(fn: (db: Db, client: MongoClient) => Promise<T>) =>
133 Effect.tryPromise({
134 try: () => fn(db, client),
135 catch: (cause) => new MongoError({ cause }),
136 });
137 return { client, db, use } satisfies MongoClusterClient;
138 }),
139 );