npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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

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

  1. Adds a History Field: Adds a field called history (by default) to your schema, storing the history of changes.

  2. 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.

  3. 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 the city field inside user.address.
  • A dollar sign ($) matches “any array index.”
    • e.g. contacts.$.phone tracks changes to the phone field for any element in the contacts array.

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

  1. 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.
  2. 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"
  3. Handling ObjectId References:

    • If the _display field contains an ObjectId 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.

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