Mongoose 8.4.0 was released on May 17, 2024 and includes several noteworthy new features.
8.4 is the most significant minor release of the 8.x release series so far.
The biggest new feature is the transactionAsyncLocalStorage
option, which opts in to using Node.js' AsyncLocalStorage API to track transaction sessions automatically.
No need to explicitly pass a session
option to every operation in a transaction.
There's also an inferRawDocType
helper for TypeScript that addresses a key design issue in inferSchemaType
.
In this blog post, I'll cover both how to use transactionAsyncLocalStorage
and the reasoning behind inferRawDocType
.
transactionAsyncLocalStorage
, or, No More session
Options
Normally, MongoDB transactions require you to pass a session
option for every database operation in the transaction to track which transaction this operation is part of.
If you don't set the session
option, your operation won't be part of your transaction.
const Test = mongoose.model('Test', mongoose.Schema({ name: String }));
const doc = await Test.create({ name: 'test' });
// Save a new doc in a transaction that aborts
await connection.transaction(async(session) => {
doc.name = 'test-updated';
// You **need** to set `session` here by default. Otherwise
// `save()` won't execute as part of the transaction.
await doc.save({ session });
throw new Error('Oops');
}).catch(() => {});
The need to pass session
is a pain point, because it is easy to forget the session
option.
In the above example, if you forget the session
option, the changes to the document's name
won't get rolled back when the transaction fails.
Using Node's AsyncLocalStorage API, Mongoose's transaction()
function can set session
for you automatically.
Here's how you can enable transactionAsyncLocalStorage
globally:
mongoose.set('transactionAsyncLocalStorage', true);
Once you enable transactionAsyncLocalStorage
, Mongoose's transaction()
function will wrap your transaction executor function in an asyncLocalStorage.run()
call with the session
option.
Then, Mongoose will pull the session from async local storage automatically when you call save()
or find()
.
Which means every operation in the transaction()
executor will automatically be part of the transaction without you having to explicitly pass the session
option.
const doc = await Test.create({ name: 'test' });
// Save a new doc in a transaction that aborts. Even without
// `session`, `doc.save()` executes within the transaction
// because of `transactionAsyncLocalStorage`.
await connection.transaction(async() => {
doc.name = 'test-updated';
await doc.save(); // Notice no session here
throw new Error('Oops');
}).catch(() => {});
AsyncLocalStorage bubbles to any function calls, so your transactions can call helper functions and Mongoose will still automatically attach the session.
await connection.transaction(async() => {
doc.name = 'test-updated';
// The `save()` call in `saveDoc()` will still automatically
// get `session` attached even though it is in another function.
await saveDoc(doc);
throw new Error('Oops');
}).catch(() => {});
async function saveDoc(doc) {
await doc.save();
}
Currently transactionAsyncLocalStorage
is only supported as a global option.
There's no way to enable transactionAsyncLocalStorage
for only some transactions.
However, there is no harm to setting the session
option if you have transactionAsyncLocalStorage
enabled.
For example, the following still works as expected even though the session
option is unnecessary.
const doc = await Test.create({ name: 'test' });
await connection.transaction(async(session) => {
doc.name = 'test-updated';
// `session` isn't necessary here if `transactionAsyncLocalStorage`
// is enabled. But specifying `session` won't cause any errors or
// cause the operation to execute outside the transaction.
await doc.save({ session });
throw new Error('Oops');
}).catch(() => {});
Getting the Lean Document Type with inferRawDocType
Mongoose 8.4 also introduces an inferRawDocType
helper that returns the raw document type from a schema definition.
The raw document type is the return value from lean queries, .toObject()
, and .toJSON()
.
It also represents the shape of the object when serialized with JSON.stringify()
.
So the raw document type has ObjectIds, Dates, and Buffers as objects; but subdocuments are represented as POJOs and Mongoose arrays are represented as vanilla JavaScript arrays.
import mongoose from 'mongoose';
const schemaDefinition = {
name: {
type: String,
required: true
},
myId: {
type: mongoose.Schema.Types.ObjectId
},
subdocs: [new mongoose.Schema({ subdocName: String })]
};
const schema = new mongoose.Schema(schemaDefinition);
/**
* RawDocType is the following type:
* {
* name: string;
* myId?: mongoose.Types.ObjectId;
* subdocs: {
* subdocName?: string;
* }[];
* }
*/
type RawDocType = mongoose.InferRawDocType<typeof schemaDefinition>;
How is InferRawDocType
different from Mongoose's existing InferSchemaType
helper?
First, InferSchemaType
takes the typeof
the schema as an argument, not the schema definition object.
Second, InferSchemaType
returns the document with document arrays as Mongoose document arrays, rather than vanilla JavaScript arrays of POJOs.
import mongoose from 'mongoose';
const schemaDefinition = {
name: {
type: String,
required: true
},
myId: {
type: mongoose.Schema.Types.ObjectId
},
subdocs: [new mongoose.Schema({ subdocName: String })]
};
const schema = new mongoose.Schema(schemaDefinition);
// InferredSchemaType will have `subdocs` as `mongoose.Types.DocumentArray`
type InferredSchemaType = mongoose.InferSchemaType<typeof schema>;
In general, InferSchemaType
is more for Mongoose's internal use for automatic type inference.
The result type from InferSchemaType
doesn't line up with the raw document type or the hydrated document type.
If you need the raw document type, you should use InferRawDocType
.
Moving On
transactionAsyncLocalStorage
and InferRawDocType
are just two of the 9 new features in Mongoose 8.4.
There's also an officially supported omitUndefined()
helper function, a listDatabases()
function, schema-level default readConcern
, and more.
Make sure you upgrade to take advantage of the new features!
Want to become your team's MongoDB expert? "Mastering Mongoose" distills 10 years of hard-earned lessons building Mongoose apps at scale into 153 pages. That means you can learn what you need to know to build production-ready full-stack apps with Node.js and MongoDB in a few days. Get your copy!