MongoDB
Not ready for production use yet. See what works and what is in the pipeline.
import { connect, mongodb } from "runtime:db"; import { BSON } from "runtime:serialization";
mongodb takes mongodb: URLs for MongoDB 7.0 or later: one server, or a replica set. Its protocol, server monitoring and connection pools run in the runtime. Documents are BSON: the types and how JavaScript values map to them are in runtime:serialization. The @opentf/esrun-mongodb package re-exports the driver as driver, with the BSON classes and the types.
Connecting needs net for the server's host and port. tlsCAFile is read through runtime:fs, so it needs read as well.
Connection options
mongodb://[user[:password]@]host[:port][,host[:port]…][/database][?options], or mongodb+srv://[user[:password]@]host[/database][?options]. The hosts are seeds: from any member the client discovers the rest of the replica set. With mongodb+srv://, DNS gives the seeds; see mongodb+srv://. Options passed to connect() win over the URL's. An option not listed here is refused with ERR_DB_UNSUPPORTED rather than ignored.
| Option | Type | Default | Meaning |
|---|---|---|---|
auth | { username, password } | from the URL | Credentials. A name or password in the URL must percent-encode :, / and @. |
authSource | string | the URL's database, else admin | Where the user is defined. |
authMechanism | "SCRAM-SHA-256" | "SCRAM-SHA-1" | negotiated | Taken from what the server offers when absent. |
tls / ssl | boolean | false; true with mongodb+srv:// | Connect with TLS. The certificate is always verified. |
tlsCAFile | string | — | A PEM CA bundle to trust besides the public roots. Implies tls. |
ca | string | Uint8Array | — | The same, as PEM. |
appName | string | — | Reported to the server in the handshake. |
maxPoolSize | number | 100 | Connections the client may open; 0 for no limit. pool: { max } sets the same. |
connectTimeoutMS | number | 30000 | Milliseconds to connect and authenticate; 0 waits forever. |
timeoutMS | number | — | A deadline for every operation, unless the call gives its own. |
w, journal, wtimeoutMS | — | The write concern sent with every write. | |
readConcernLevel | string | — | The read concern sent with every read. |
replicaSet | string | — | The set's name. Members of another set are dropped; without it, the set is discovered. |
directConnection | boolean | false | Use the one host named, whatever it is, without discovering others. Not with several hosts. |
readPreference | mode | primary | Where collection reads go: primary, primaryPreferred, secondary, secondaryPreferred or nearest. |
readPreferenceTags | { [tag]: string }[] | — | Tag sets, tried in order; in a URL, repeat readPreferenceTags=dc:ny,rack:1, an empty one matching any member. |
maxStalenessSeconds | number | none | Secondaries estimated further behind are not read from. At least 90; -1 is none. |
serverSelectionTimeoutMS | number | 30000 | How long an operation waits for a suitable server. |
heartbeatFrequencyMS | number | 10000 | How often servers are checked; at least 500. |
localThresholdMS | number | 15 | How much slower than the fastest suitable server another may be and still be chosen. |
retryWrites | boolean | true | Retry a write once after a network or retryable error; see Retries. |
retryReads | boolean | true | The same for reads. |
compressors | string | Accepted; compression is not negotiated yet. | |
srvServiceName | string | mongodb | mongodb+srv:// only: the SRV service asked for, _{name}._tcp.{host}. |
srvMaxHosts | number | 0 | mongodb+srv:// only: use at most this many of the SRV hosts, chosen at random; 0 for all. Not with replicaSet. |
loadBalanced | boolean | false | Only false is accepted; load-balanced deployments are not supported yet. |
Refused with ERR_DB_UNSUPPORTED: loadBalanced=true, a Unix socket, an authentication mechanism other than SCRAM, a read preference that contradicts itself (tags or staleness with primary), and tlsInsecure, tlsAllowInvalidCertificates or tlsAllowInvalidHostnames set to true.
mongodb+srv://
One host name stands for the deployment. Before connecting, the client looks up the SRV records _mongodb._tcp.{host} for the seeds and the TXT record on {host} for defaults:
Every SRV target must be in the host's domain — the host without its first label, or, for a name of one or two labels, the whole name with at least one more in front. One target outside it fails the connect, and no host is contacted.
srvAllowedHostsSuffixandsrvHostValidator, which relax this, are not offered.At most one TXT record, setting only
authSource,replicaSetandloadBalanced. The URL and the options passed toconnect()win over it.A port, a second host, or
directConnection=trueis refused before any lookup.
The lookups are resolveDns's (runtime:net), so they need --allow-net for the host, and each seed needs it again to be connected to. DNS is asked once, at connect().
connect() checks the seeds and every member they name, then opens one authenticated connection, so a wrong password or an unreachable deployment fails there. When no seed answers at all it fails at once with ERR_DB_CONNECTION_LOST, rather than waiting out serverSelectionTimeoutMS.
The client
connect(url, { driver: mongodb }) returns a client: what it knows of the deployment, a pool of connections to each server, and a check of every server each heartbeatFrequencyMS. The checks run from an unreferenced timer, so a program that has finished exits without close().
| Member | Returns | Meaning |
|---|---|---|
db(name?) | Db | The database name; the URL's, or test, by default. |
command(command, options?) | Promise<object> | Runs a command on options.db (default admin). |
ping(options?) | Promise<void> | { ping: 1 }. |
startSession({ causalConsistency?, defaultTransactionOptions? }) | Session | An explicit session. |
transaction(fn, options?) | Promise<T> | Runs fn(tx) in a transaction and commits it; see Transactions. |
close() | Promise<void> | Closes every connection. await using calls it. |
backend | "mongodb" |
query and execute refuse with ERR_DB_QUERY_FORM: MongoDB is reached through db().
Every operation below takes session, signal (an AbortSignal) and timeoutMS in its options. Either one stops the operation wherever it is: still waiting for a connection, or on the server, whose connection is then closed rather than reused. The rejection carries the signal's reason, or a DbError with ERR_DB_TIMEOUT. timeoutMS is also sent to the server as maxTimeMS.
Replica sets and read preference
Writes go to the primary. A read goes where its read preference allows: the call's readPreference option, or for collection reads (find, aggregate, counts, distinct, listings) the client's. command() runs on the primary unless its options say otherwise, as runCommand does. A cursor's getMores go to the member that opened it.
Among the members a read may use, one is chosen from those whose average round trip is within localThresholdMS of the fastest, preferring the one running fewer operations. When none is suitable — no primary during an election, or no member matching the tags — the operation checks the deployment again and retries until serverSelectionTimeoutMS, then fails with ERR_DB_TIMEOUT. A network error marks that member unavailable and closes its pooled connections; a "not writable primary" or "node is recovering" error makes the client check the deployment at once.
Discovery, monitoring and selection follow MongoDB's server discovery and monitoring and server selection specifications, and pass their test suites (tsr test:mongodb-specs).
Transactions
Transactions need a replica set; on a standalone, transaction and startTransaction fail with ERR_DB_UNSUPPORTED.
client.transaction(fn, options?) starts a session, runs fn(tx) in a transaction and commits it, then ends the session, and returns what fn returned. tx is the client whose db() and command() run in the transaction; tx.session is the session. If fn throws, the transaction is aborted and the error propagates. As MongoDB's convenient transactions API specifies, an error labelled TransientTransactionError runs fn again in a new transaction, and a commit labelled UnknownTransactionCommitResult is committed again, for up to 120 seconds with a short jittered backoff; fn may therefore run more than once.
Session member | Meaning |
|---|---|
startTransaction(options?) | Starts a transaction. Nothing is sent until the first operation in it. |
commitTransaction() | Commits. Committing again after a commit is allowed, and waits for a majority. |
abortTransaction() | Abandons the transaction. |
withTransaction(fn, options?) | What client.transaction does, on this session; fn receives the session. |
inTransaction | Whether a transaction is open. |
Transaction options are readConcern, writeConcern (default: the client's) and maxCommitTimeMS; readPreference may only be primary. Operations in a transaction go to the primary, are not retried on their own, and refuse their own writeConcern or a read preference other than primary. A network error in one is labelled TransientTransactionError. A commit whose outcome is unknown (a lost connection, a time limit, a retryable error, or a write concern not yet satisfied) is labelled UnknownTransactionCommitResult. Ending a session with a transaction open aborts it.
Retries
A write is sent once more when its first attempt is lost with its connection or fails with an error the server labels RetryableWriteError (a step-down, a shutdown, or such a write concern error). The retry goes to the primary then selected, with the same session id and transaction number, so the server applies the write at most once. That covers insertOne, insertMany, updateOne, replaceOne, deleteOne, bulkWrite commands with no multi-document statement, and the findOneAnd* methods. updateMany, deleteMany, writes with w: 0, and writes to a standalone are sent once.
A read is sent once more after a network error or one of the "node is recovering" or "not writable primary" codes: the first command of find and aggregate (without $out or $merge), findOne, countDocuments, estimatedDocumentCount, distinct, listCollections and listIndexes. A cursor's later batches are not retried, nor is command().
When the retry also fails, the error is the retry's, unless the server says it wrote nothing (NoWritesPerformed), in which case it is the first attempt's. A deployment that takes no retryable writes answers with an error naming retryWrites=false as the fix.
Sessions
Every command runs in a session, as the MongoDB sessions specification requires: the one passed as { session }, or an implicit one held for that command, or for a cursor until it is exhausted or closed. The client reuses server sessions, and ends the ones it holds with endSessions when it closes. An unacknowledged write (w: 0) runs in no session, and refuses an explicit one.
Session member | Meaning |
|---|---|
id | The session id, { id: UUID }. |
causalConsistency | true by default: a find, aggregate, count or distinct in the session waits for the session's earlier operations (afterClusterTime). |
clusterTime, operationTime | The latest the session has seen. advanceClusterTime(t) and advanceOperationTime(t) take later ones from elsewhere. |
hasEnded | |
endSession() | Returns the server session for reuse. await using calls it; ending twice does nothing. |
A session from another client, or one that has ended, is refused. A server session left unused for about the server's session timeout (30 minutes by default) is discarded by the server, so use a session soon after starting it.
Db
| Member | Returns | Meaning |
|---|---|---|
databaseName | string | |
collection(name) | Collection | |
command(command, options?) | Promise<object> | A reply with ok: 0 is thrown as a DbError. |
aggregate(pipeline?, options?) | Cursor | A database-level aggregation ($currentOp, $documents). |
listCollections(filter?, { nameOnly?, batchSize? }) | Cursor | |
createCollection(name, options?) | Promise<Collection> | Options are the create command's (validator, capped, timeseries, …). |
dropCollection(name, options?) | Promise<boolean> | |
dropDatabase(options?) | Promise<boolean> |
Collection
| Method | Returns |
|---|---|
insertOne(document, options?) | { acknowledged, insertedId } |
insertMany(documents, { ordered? }) | { acknowledged, insertedCount, insertedIds } |
updateOne(filter, update, options?), updateMany(…) | { acknowledged, matchedCount, modifiedCount, upsertedCount, upsertedId } |
replaceOne(filter, replacement, options?) | the same |
deleteOne(filter, options?), deleteMany(…) | { acknowledged, deletedCount } |
bulkWrite(operations, { ordered? }) | { acknowledged, insertedCount, matchedCount, modifiedCount, deletedCount, upsertedCount, insertedIds, upsertedIds } |
find(filter?, options?) | Cursor |
findOne(filter?, options?) | the document, or null |
aggregate(pipeline?, options?) | Cursor |
countDocuments(filter?, { skip?, limit? }) | number |
estimatedDocumentCount(options?) | number, from the collection's metadata |
distinct(key, filter?, options?) | an array of values |
findOneAndUpdate(filter, update, options?), findOneAndReplace(…), findOneAndDelete(filter, options?) | the document before (default) or after (returnDocument: "after"), or null |
createIndex(keys, options?) | the index name |
createIndexes(specs, options?) | the names |
dropIndex(name, options?), dropIndexes(options?) | void |
listIndexes(options?) | Cursor |
drop(options?) | true, whether or not the collection existed |
insertOne and insertMany insert a copy given an ObjectId _id when a document has none; the document passed in is not changed. An update must use update operators or be a pipeline, and a replacement must not use them; either mistake is a TypeError. Writes take writeConcern, comment, let and bypassDocumentValidation; updates also upsert, arrayFilters, hint and collation. find takes sort, projection, skip, limit, batchSize, hint, comment, maxTimeMS, collation, allowDiskUse, let and readConcern.
bulkWrite operations are { insertOne: { document } }, { updateOne | updateMany: { filter, update, … } }, { replaceOne: { filter, replacement, … } } and { deleteOne | deleteMany: { filter } }. Ordered (the default), they run in order and stop at the first error; unordered, they may be grouped by kind and all run.
A write larger than one message, or with more statements than the server takes in one batch, is sent as several commands. A document larger than the server's 16 MiB limit is refused before it is sent, with backendCode 10334.
Cursor
Nothing is sent until a cursor is read.
| Member | Meaning |
|---|---|
sort(sort), limit(n), skip(n), project(projection), batchSize(n) | Chain before the first read; after it they throw ERR_DB_UNSUPPORTED. |
next() | The next document, or null. |
toArray() | Every document, then closes. |
for await (const doc of cursor) | Leaving the loop early closes the cursor. |
close() | Releases the cursor on the server. await using calls it. |
Errors
A failure is a DbError. Its code is the portable code, its backendCode the server's number, and server carries { code, codeName, errorLabels }, with keyValue for a duplicate key and errInfo when the server sent it. A write error also carries every writeErrors entry and the partial result.
| Server code | code |
|---|---|
| 11000, 11001, 12582 (duplicate key) | ERR_DB_UNIQUE_VIOLATION |
| 18 (authentication failed) | ERR_DB_AUTH_FAILED |
| 50, 262 (time limit exceeded) | ERR_DB_TIMEOUT |
| 26 (namespace not found) | ERR_DB_NOT_FOUND |
| 112 (write conflict) | ERR_DB_SERIALIZATION_FAILURE |
| 121 (document failed validation) | ERR_DB_CHECK_VIOLATION |
| any other | ERR_DB_BACKEND |
A lost connection is ERR_DB_CONNECTION_LOST, and a server older than MongoDB 7.0 is refused at connect with ERR_DB_UNSUPPORTED.