@expander/mongoose-tracker
v1.6.4
Published
Is a mongoose plugin that automatically keeps track of when the document has been created, updated and optionally when some fields have been modified
Downloads
758
Maintainers
Readme
mongooseTracker
mongooseTracker is a versatile Mongoose plugin that automatically tracks the creation and updates of your documents. It meticulously logs changes to specified fields, including nested fields, arrays, and references to other documents, providing a comprehensive history of modifications. This plugin enhances data integrity and auditability within your MongoDB collections.
Inspired by the mongoose-trackable package, mongooseTracker offers improved functionality and customization to seamlessly integrate with your Mongoose schemas.
Table of Contents
Features
- Tracks changes to fields in your Mongoose documents.
- Supports nested objects.
- Supports array elements (detecting added/removed items).
- Supports references (
ObjectId
) to other Mongoose documents (will store a “display” value if available). - Allows ignoring certain fields (e.g.
_id
,__v
, etc.). - Keeps a configurable maximum length of history entries.
Installation
Install mongooseTracker via npm:
npm install @expander/mongoose-tracker
OR
yarn add @expander/mongoose-tracker
Usage
Plugin Configuration
import mongoose, { Schema } from "mongoose";
import mongooseTracker from "@expander/mongoose-tracker"; // Adjust import based on your actual package name
const YourSchema = new Schema({
title: String,
orders: [
{
orderId: String,
timestamp: Date,
items: [ { name: String, price:Number, .... }, ],
// ...other fields...
}
],
user: {
firstName: String,
lastName:String,
// ...other fields...
}
// ...other fields...
});
// Apply the plugin with options
YourSchema.plugin(mongooseTracker, {
name: "history",
fieldsToTrack: [
"title",
"user.firstName",
"user.lastName",
"orders.$.items.$.price",
"orders.$.items.$.name",
"orders.$.timestamp",
],
fieldsNotToTrack: ["history", "_id", "__v", "createdAt", "updatedAt"],
limit: 50,
instanceMongoose: mongoose, //optional.
});
export default mongoose.model("YourModel", YourSchema);
What It Does
Adds a History Field: Adds a field called
history
(by default) to your schema, storing the history of changes.Monitors Document Changes: Monitors changes during
save
operations and on specific query-based updates (findOneAndUpdate
,updateOne
,updateMany
).Note: Currently, the plugin works best with the
save
method for tracking changes. We are actively working on enhancing support for other update hooks to ensure comprehensive change tracking across all update operations.Logs Detailed Changes: Logs an entry each time changes occur, storing the user/system who made the change (
_changedBy
) if provided.
Options
| Option | Type | Default | Description |
| ---------------------- | ---------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| name
| string
| 'history'
| The name of the array field in which the history records will be stored. |
| fieldsToTrack
| string[]
| []
(empty) | A list of field patterns to track. If empty, all fields (except those in fieldsNotToTrack
) are tracked. |
| fieldsNotToTrack
| string[]
| ['history', '_id', '_v', '__v', 'createdAt', 'updatedAt', 'deletedAt', '_display']
| Fields/paths to exclude from tracking. |
| limit
| number
| 50
| Maximum number of history entries to keep in the history array. |
| instanceMongoose
| mongoose
| The default imported mongoose
instance | Override if you have a separate Mongoose instance. |
Field Patterns
- A dot (
.
) matches subfields.- e.g.
user.address.city
tracks changes to thecity
field insideuser.address
.
- e.g.
- A dollar sign (
$
) matches “any array index.”- e.g.
contacts.$.phone
tracks changes to thephone
field for any element in thecontacts
array.
- e.g.
Usage
Use as you would any Mongoose plugin :
const mongoose = require("mongoose");
const mongooseTracker = require("@expander/mongoose-tracker");
const { Schema } = mongoose.Schema;
const CarsSchema = new Schema({
tags: [String],
description: String,
price: { type: Number, default: 0 },
});
CarsSchema.plugin(mongooseTracker, {
limit: 50,
name: "metaDescriptions",
fieldsToTrack: ["price", "description"],
});
module.exports = mongoose.model("Cars", CarsSchema);
Using _changedBy
to Record Changes
The _changedBy
field allows tracking who made specific changes to a document.
You can set this field directly before updating a document.
It's recommended to use a user ID, but any string value can be assigned.
Example
async function foo() {
// Create a new document
const doc = await SomeModel.find({ name: "Initial Name" });
doc.name = "New Name";
// Set the user or system responsible for the creation
doc._changedBy = "creator"; // Replace 'creator' with the user's ID or identifier
await doc.save();
}
Resulting History Log
[
{
action: "updated",
at: 1734955271622,
changedBy: "creator",
changes: [
{
field: "name",
before: "Initial Name",
after: "New Name",
},
],
},
];
Key Notes
The _changedBy field is optional but highly recommended for accountability.
You can dynamically set _changedBy based on the current user's ID, username, or other unique identifiers.
Importance of the _display
Field
The _display
field is crucial for enhancing the readability of history logs. Instead of logging raw field paths with array indices (e.g., orders.0.items.1.price
), the plugin utilizes the _display
field from the respective object to present a more meaningful identifier.
How It Works
Presence of
_display
:- Ensure that each subdocument (e.g., items within orders) includes a
_display
field. - This field should contain a string value that uniquely identifies the object, such as a name or a readable label.
- Ensure that each subdocument (e.g., items within orders) includes a
Concatenation Mechanism:
- When a tracked field is updated (e.g.,
orders.$.items.$.price
), the plugin retrieves the_display
value of the corresponding item. - It then concatenates this
_display
value with the changed field name to form a readable string for the history log. - Example:
- Raw Field Path:
orders.0.items.1.price
- With
_display
:"Test Item 2 price"
- Raw Field Path:
- When a tracked field is updated (e.g.,
Handling ObjectId References:
- If the
_display
field contains anObjectId
referencing another document, the plugin will traverse the reference to fetch the_display
value of the parent document. - This recursive resolution continues until a string value is obtained, ensuring that the history log remains informative.
- If the
Benefits
- Clarity: Provides a clear and concise representation of changes, making it easier to understand what was modified.
- Readability: Avoids confusion that can arise from array indices, especially in documents with multiple nested arrays.
- Relevance: Focuses on meaningful identifiers that are significant within the application's context.
Example
- Consider the following schema snippet:
interface Item extends Document {
name: string;
price: number;
_display: string;
}
const ItemSchema = new Schema<Item>({
name: { type: String, required: true },
price: { type: Number, required: true },
_display: { type: String, required: true },
});
interface Order extends Document {
orderNumber: string;
date: Date;
items: Item[];
_display:string;
}
const OrderSchema = new Schema<Order>({
orderNumber: { type: String, required: true, unique: true },
date: { type: Date, required: true, default: Date.now },
items: { type: [ItemSchema], required: true },
_display: { type: String },
});
interface PurchaseDemand extends Document {
pdNumber: string;
orders: Order[];
}
const PurchaseDemandSchema = new Schema<PurchaseDemand>({
pdNumber: { type: String, required: true, unique: true },
orders: [OrderSchema],
});
PurchaseDemandSchema.plugin(mongooseTracker, {
fieldsToTrack: ["orders.$.date", "orders.$.items.$.price"], //The Fields I want to track.
});
const PurchaseDemandModel = mongoose.model<PurchaseDemand>(
"PurchaseDemand",
PurchaseDemandSchema
);
const purchaseDemand = new PurchaseDemand({
pdNumber: "PD-001",
orders: [
{
orderNumber: "ORD-001",
items: [
{ name: "Test Item 1", price: 100, _display: "Test Item 1" },
{ name: "Test Item 2", price: 200, _display: "Test Item 2" },
],
_display: "Order 1",
},
],
});
// Update an item's price
purchaseDemand._changedBy = 'system';
purchaseDemand.orders[0].items[1].price = 250;
await purchaseDemand.save();
History Log Entry:
{
"action": "updated",
"at": 1734955271622,
"changedBy": "system",
"changes": [
{
"field": "Test Item 2 price", // instead of "orders.0.items.1.price"
"before": 200,
"after": 250
}
]
}
Tracking Array Fields
When specifying an array field in fieldsToTrack, such as "orders", mongooseTracker will monitor for any additions or deletions within that array. This means that:
- Additions: When a new element is added to the array, the plugin logs this change in the history array.
- Deletions: When an existing element is removed from the array, the plugin logs this removal in the history array.
Operations:
Adding an element (Order):
PurchaseDemandSchema.plugin(mongooseTracker, {
fieldsToTrack: ["orders"],
});
const purchaseDemand = await PurchaseDemandModel.create({
pdNumber: "PD-TEST-002",
orders: [],
});
// Adding a new order
purchaseDemand.orders.push({
orderNumber: "ORD-TEST-002",
date: new Date(),
items: [{ name: "Test Item 3", price: 300, _display: "Test Item 3" }],
_display: "ORD-TEST-002",
});
await purchaseDemand.save();
History Log Entry After Addition:
{
"action": "added",
"at": 1734955271622,
"changedBy": null,
"changes": [
{
"field": "orders",
"before": null,
"after": 'ORD-TEST-002' // the name of _display.
}
]
}
Removing an element (Order):
purchaseDemand.orders.pop(); // we remove the last element that insert in orders. (ORD-TEST-002)
await purchaseDemand.save();
History Log Entry After Removal:
{
"action": "removed",
"at": 1734955271622,
"changedBy": null,
"changes": [
{
"field": "orders",
"before": 'ORD-TEST-002'
"after": null
}
]
}
Contributing
- Use eslint to lint your code.
- Add tests for any new or changed functionality.
- Update the readme with an example if you add or change any functionality.
Legal
- Author: Roni Jack Vituli
- License: Apache-2.0