Ben Kong
Ben Kong

Reputation: 1

Mongoose pre.save() async middleware not working on record creation

I am using [email protected]. I would like to change the post category to a tree structure. The below code is running well except when I create a category, it goes into a deadlock:

var keystone = require('keystone'),
	Types = keystone.Field.Types;

/**
 * PostCategory Model
 * ==================
 */

var PostCategory = new keystone.List('PostCategory', {
	autokey: { from: 'name', path: 'key', unique: true }
});

PostCategory.add({
	name: { type: String, required: true },
	parent: { type: Types.Relationship, ref: 'PostCategory' },
	parentTree: { type: Types.Relationship, ref: 'PostCategory', many: true }
});

PostCategory.relationship({ ref: 'Post', path: 'categories' });

PostCategory.scanTree = function(item, obj, done) {
	if(item.parent){
		PostCategory.model.find().where('_id', item.parent).exec(function(err, cats) {
			if(cats.length){
				obj.parentTree.push(cats[0]);
				PostCategory.scanTree(cats[0], obj, done);
			}
		});
	}else{
		done();
	}
}

PostCategory.schema.pre('save', true, function (next, done) { //Parallel middleware, waiting done to be call
	if (this.isModified('parent')) {
        this.parentTree = [];
		if(this.parent != null){
			this.parentTree.push(this.parent);
			PostCategory.scanTree(this, this, done);
		}else
			process.nextTick(done);
    }else
		process.nextTick(done); //here is deadlock.

    next();
});

PostCategory.defaultColumns = 'name, parentTree';
PostCategory.register();

Thanks so much.

Upvotes: 0

Views: 1027

Answers (2)

Ben Kong
Ben Kong

Reputation: 1

I think it's a bug of keystone.js. I have changed schemaPlugins.js 104 line

from

this.schema.pre('save', function(next) {

to

this.schema.pre('save', true, function(next, done) {

and change from line 124 to the following,

		// if has a value and is unmodified or fixed, don't update it
		if ((!modified || autokey.fixed) && this.get(autokey.path)) {
			process.nextTick(done);
			return next();
		}

		var newKey = utils.slug(values.join(' ')) || this.id;

		if (autokey.unique) {
			r = getUniqueKey(this, newKey, done);
			next();
			return r;
		} else {
			this.set(autokey.path, newKey);
			process.nextTick(done);
			return next();
		}

It works.

Upvotes: 0

Jed Watson
Jed Watson

Reputation: 20378

As I explained on the issue you logged on Keystone here: https://github.com/keystonejs/keystone/issues/759

This appears to be a reproducible bug in mongoose that prevents middleware from resolving when:

  • Parallel middleware runs that executes a query, followed by
  • Serial middleware runs that executes a query

Changing Keystone's autokey middleware to run in parallel mode may cause bugs in other use cases, so cannot be done. The answer is to implement your parentTree middleware in serial mode instead of parallel mode.

Also, some other things I noticed:

  • There is a bug in your middleware, where the first parent is added to the array twice.
  • The scanTree method would be better implemented as a method on the schama
  • You can use the findById method for a simpler parent query

The schema method looks like this:

PostCategory.schema.methods.addParents = function(target, done) {
    if (this.parent) {
        PostCategory.model.findById(this.parent, function(err, parent) {
            if (parent) {
                target.parentTree.push(parent.id);
                parent.addParents(target, done);
            }
        });
    } else {
        done();
    }
}

And the fixed middleware looks like this:

PostCategory.schema.pre('save', function(done) {
    if (this.isModified('parent')) {
        this.parentTree = [];
        if (this.parent != null) {
            PostCategory.scanTree(this, this, done);
        } else {
            process.nextTick(done);
        }
    } else {
        process.nextTick(done);
    }
});

Upvotes: 1

Related Questions