1

I would prevent any sub document from being removed, thus I added an error to the pre('remove') middleware of each sub document Schema.

When calling the .remove() function, it effectively calls the middleware. But when it is deleted without calling remove(), the middleware doesn't check if it has been removed.

The cases where is matters is when I receive an object from a remote source, I'd like to perform all the integrity checks via mongoose middlewares to keep everything at the same place. The remote source can have, by mistake or not, deleted one of the sub docs. So when Mongoose is checking the whole doc, the sub doc has already been removed without triggering the .remove() function.

Here is the minimal working example of my problem:

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var subDateSchema = new Schema({
    date_add: {type: Date, default: Date.now},
    date_remove: {type: Date, default: null}
});

var ResourceSchema = new Schema({
    activation_dates: [subDateSchema]
});

subDateSchema.pre('remove', function(next){
    next(new Error("YOU CAN'T DELETE ANY ACTIVATION DATE"));
});

var Resource = mongoose.model('Resource', ResourceSchema);

var newresource = new Resource({
    activation_dates: [{
        date_add: Date.now()
    }]
});

newresource.save(function(err){
    if(err) throw err;
    newresource.activation_dates.splice(0, 1);
    /**
      * Here I tried
      * newresource.markModified('activation_dates');
      * On update it *DOES* trigger pre save and pre validate
      * But it does nothing to deleted content
    **/ 
    newresource.save(function(err){
        if(err) throw err;
    });
});

So my question is: Is there a clean way to call sub doc removing middleware without proceeding to check for all the previous elements and compare with the new ones to see which ones are being deleted ?

Musinux
  • 31
  • 6
  • "Sub-documents" and therefore hence "members of arrays" are **never** "removed" as per the action hook suggests. They are only ever "pulled" from the array. This is why your code fails. – Blakes Seven Aug 15 '15 at 13:50
  • @BlakesSeven alright, so is there a way to check for sub documents being pulled from an array ? – Musinux Aug 15 '15 at 13:52
  • Come to think of it, I actually do not think mongoose models support this at all. The ability to even "support" the "validation" process on "atomic" operations such as `$pull` within an `.update()` or similar is also relatively a very "new" thing in the codebase. Therefore I would suggest "validation" hooks rather than the `.pre()` middleware to be more appropriate. Have not tried your process myself. Might try during the weekend. – Blakes Seven Aug 15 '15 at 13:56

1 Answers1

1

After a little bit of researches, I found this:

A workaround can be to hook an event to the whole array of Sub Documents, and to have a copy of the previous array of data.

This is a complete working example on how to be sure an array element hasn't been deleted or pulled out. To check for modifications, you'll need further modifications.

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var ResourceSchema = new Schema({
    activation_dates: [subDateSchema]
});

// This virtual permits to store the original array accross the middlewares
ResourceSchema.virtual("original").set(function(item){
    this._original = item;
}).get(function(){
    return this._original;
});

// This middleware checks for a previous version of the "Resource"
ResourceSchema.pre("validate", function(next){
    var self = this;
    mongoose.model("Resource").findById(this._id, function(err, doc){
        if(err) throw err;
        self.original = doc;
        next();
    });
});

// This validation block checks for any modification of the array of sub documents
ResourceSchema.path("activation_dates").validate(function(value){
    var j;
    if(this.original){
        // if the new array is smaller than the original, no need to go further
        if(this.original.activation_dates.length > value.length){
            return false;
        }

        for(var i=0; i < this.original.activation_dates.length; i++){
            j=0;
            // if the array element has been deleted but not pulled out, we need to check it
            if(typeof value[j] == "undefined" || typeof value[j]._id == "undefined"){
                return false;
            }

            while(value.length > j && this.original.activation_dates[i]._id.toString() != value[j]._id.toString()){
                j++;
            }
            if(j == value.length){
                return false;
            }
        }
    }
    return true;
}, "You deleted at least one element of the array");

var Resource = mongoose.model('Resource', ResourceSchema);

var newresource = new Resource({
    activation_dates: [{
        date_add: Date.now()
    }]
});

newresource.save(function(err){
    if(err) throw err;

    newresource.activation_dates.splice(0, 1);
    // OR:
    //delete newresource.activation_dates[0];
    // this line is essential in the case you *delete* but not pull out
    newresource.markModified('activation_dates');

    newresource.save(function(err){
        if(err) throw err;
    });
});

Unfortunately I couldn't find another solution than doing a loop over all elements and retrieving the original document.

Musinux
  • 31
  • 6