UNPKG

@expander/mongoose-tracker

Version:

Is a mongoose plugin that automatically keeps track of when the document has been created, updated and optionally when some fields have been modified

397 lines (306 loc) 12.3 kB
# 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](https://www.npmjs.com/package/@folhomee-public/mongoose-tracker) package, **mongooseTracker** offers improved functionality and customization to seamlessly integrate with your Mongoose schemas. ## Table of Contents - [Features](#features) - [Installation](#installation) - [Usage](#usage) - [Plugin Configuration](#plugin-configuration) - [Options](#options) - [Example Schema Usage](#example-schema-usage) - [Using _changedBy to Record Changes](#using-_changedby-to-record-changes) - [Importance of the _display Field](#importance-of-the-_display-field) - [Tracking Array Fields](#tracking-array-fields) - [Contributing](#contributing) - [Legal](#legal) --- ## 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: ```bash npm install @expander/mongoose-tracker ``` OR ``` yarn add @expander/mongoose-tracker ``` --- ## Usage ### Plugin Configuration ```js 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 : ```js 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. <br/> You can set this field directly before updating a document. <br/> It's recommended to use a **user ID**, but any string value can be assigned. #### Example ```js 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 ```js [ { 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: ```ts 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 ); ``` ```js 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: ```js { "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): ```js 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: ```js { "action": "added", "at": 1734955271622, "changedBy": null, "changes": [ { "field": "orders", "before": null, "after": 'ORD-TEST-002' // the name of _display. } ] } ``` #### Removing an element (Order): ```js purchaseDemand.orders.pop(); // we remove the last element that insert in orders. (ORD-TEST-002) await purchaseDemand.save(); ``` #### History Log Entry After Removal: ```js { "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