Search code examples
javascriptnode.jsmongodbmongoosemongoose-plugins

How to Capture Stack Trace of Actual Update in Mongoose Plugin Instead of Plugin Call?


I’m working on a Mongoose plugin to log database updates, including the stack trace. Currently, I can only capture the stack trace of the plugin itself, but what I need is the stack trace where the actual update query is triggered in the application code, not in the plugin.

Here’s a simplified version of my Mongoose plugin:

schema.pre(['updateOne', 'findOneAndUpdate', 'updateMany'], async function (next) {
    const stackTrace = new Error().stack;  // This gives the plugin's stack trace
    // I need the stack trace where the update was called
    console.log("Stack Trace:", stackTrace);
    next();
});

I can achieve this saving stack trace in the query options and then get it from there in the plugin. Something like this:

// Application code
await dbnative.orders.updateOne({'order_id': order_id, 'status': 2}, {'pending': true}, { stack_trace: captureStackTrace() });



// Plugin code
schema.pre(['updateOne', 'findOneAndUpdate', 'updateMany'], async function (next) {
    const stackTrace = this.options && this.options.stack_trace
    console.log("Stack Trace:", stackTrace);
    next();
});

However, in this scenario I need to refactor the all update queries. Is there a better way to achieve this?


Solution

  • Found a solution. We can override the "update" methods and save the stack trace in the options:

    function overwriteUpdateMethods() {
        const queryProto = mongoose.Query.prototype;
        const modelProto = mongoose.Model.prototype;
    
        // Overwrite updateOne
        const originalUpdateOne = queryProto.updateOne;
        queryProto.updateOne = function (...args) {
            // Add stack trace to options
            const options = args[2] || {};
            options.stack_trace = captureStackTrace();
            args[2] = options;  // Assign the modified options back to args
            return originalUpdateOne.apply(this, args);
        };
    
        // Overwrite findOneAndUpdate
        const originalFindOneAndUpdate = queryProto.findOneAndUpdate;
        queryProto.findOneAndUpdate = function (...args) {
            const options = args[2] || {};
            options.stack_trace = captureStackTrace();
            args[2] = options;
            return originalFindOneAndUpdate.apply(this, args);
        };
    
        // Overwrite updateMany
        const originalUpdateMany = queryProto.updateMany;
        queryProto.updateMany = function (...args) {
            const options = args[2] || {};
            options.stack_trace = captureStackTrace();
            args[2] = options;
            return originalUpdateMany.apply(this, args);
        };
    
        // Overwrite save method (only for updates)
        const originalSave = modelProto.save;
    
        modelProto.save = async function (options = {}) {
            const isNewDocument = this.isNew;
    
            // adding stack trace to options only for updates
            if (!isNewDocument) {
                options.stack_trace = captureStackTrace();
                console.log('stacktrace', options.stack_trace);
            }
    
            return originalSave.apply(this, [options]);
        };
    }
    

    Hope this will help someone.