A mongoose plugin to log changes in MongoDB documents made using mongoose
methods. If used on Mongoose models, any changes made to that model will be logged as a document of the _Footprint
model. It will contain an array that shows every change made to the document, along with the old and new document bodies.
Changes in referenced/populated documents will not be logged unless the referenced Object ID changes entirely to a different one. However changes in nested documents or subdocuments will be logged.
The plugin also supports sessions so queries in aborted transactions won't be logged.
Currently it supports the following operations:
- Update
findOneAndUpdate
findByIdAndUpdate
document.save
- Create
create
document.save
- Delete
findOneAndDelete
findByIdAndDelete
findOneAndRemove
findByIdAndRemove
Note: The update operations will set new: true
as the default so the returned object will always be the updated object.
Install the package
npm i mongoose-footprints
Then, just use the plugin on the schema before you make a model from it.
Book.js
const footprints = require('mongoose-footprints');
const mongoose = require('mongoose');
const bookSchema = mongoose.Schema({
name: String,
author: String,
});
const options = {
operations: ['update', 'delete'],
logUser: false,
};
bookSchema.plugin(footprints.plugin, options);
const Book = mongoose.model('Book', bookSchema);
Possible plugin options you can pass are given below. They are passed inside an object.
logUser
:true
/false
- To log the user who updated the document. If set tofalse
, the default user logged isSystem
. If set totrue
, the user has to be passed in the options of the query. Otherwise, it will be recorded asUnknown
. Default isfalse
.operations
:['update', 'create', 'delete']
- The operations that will be logged. Default is['update']
.storeDocuments
:true/false
- Store the entire old document and updated document inside the footprint along with the list of changes. Set it tofalse
if it causes you to exceed the maximum document size of MongoDB. Default istrue
.
You can pass in extra options during mongoose queries that are specific to that operation.
footprint
:true
/false
- Set it to false to not create a footprint of this query. Default istrue
.user
:mongoose.Schema.Types.Mixed
- Can be any data type. This will log the user who made this change. Default isSystem
iflogUser
is set tofalse
, otherwise it is simplyUnknown
.
findOneAndUpdate()
await Book.findOneAndUpdate(filter, updates, {
user: req.user,
session: session,
});
create()
const bookObject = {
name: 'Angels & Demons',
author: 'Dan Brown',
};
// to use Model.create() with options, the document has to be passed in an array
// see https://mongoosejs.com/docs/api/model.html#model_Model-create
const doc = (
await Book.create([bookObject], {
footprint: false, // setting to false will not log this creation
})
)[0];
save() when updating
let savedBook = await Book.findById(doc._id);
savedBook.name = 'The Da Vinci Code';
await savedBook.save({
footprint: true, // already true by default though
});
save() when creating
const book = new Book({ name: 'Angels & Demons', author: 'Dan Brown' });
await book.save({ user: req.user });
findOneAndDelete()
await Book.findOneAndDelete(filter, {
session: session,
});
There are also some utility methods that can be used to query for footprints. The parameters are similar to that of the find()
method of mongoose:
- getFootprints(filter, projection, options, callback)
- getCreations(filter, projection, options, callback)
- getDeletions(filter, projection, options, callback)
- getUpdates(filter, projection, options, callback)
const footprints = require('mongoose-footprints');
await footprint.getFootprints({ documentId: doc._id });
{
modelName: String,
documentId: mongoose.Schema.Types.ObjectId,
oldDocument: {},
newDocument: {},
user: mongoose.Schema.Types.Mixed,
changes: [String],
typeOfChange: {
type: String,
enum: ['Create', 'Update', 'Delete'],
default: 'Update',
}
}
- Not satisfied with how changes in arrays are documented. I have plans to make the logging much more extensive for this kind of data type.
- Test cases don't cover all sorts of scenarios at the moment, so I'll need to think of more. Currently only the core features are covered.