Skip to content

Commit

Permalink
Added pre.save middleware that fixes tree after new node insertion
Browse files Browse the repository at this point in the history
  • Loading branch information
luccastera committed Dec 30, 2011
1 parent acf7691 commit 31f0b78
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 0 deletions.
Binary file added docs/test_tree_after_leaf_insertion.dia
Binary file not shown.
Binary file added docs/test_tree_after_leaf_insertion.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions lib/nested_set.js
Expand Up @@ -13,6 +13,53 @@ var NestedSetPlugin = function(schema, options) {
schema.add({ rgt: {type: Number, min: 0} });
schema.add({ parentId: {type: Schema.ObjectId} });

schema.pre('save', function(next) {
var self = this;
var model = mongoose.model(modelName);
if (self.parentId) {
self.parent(function(err, parentNode) {
if (!err && parentNode && parentNode.lft && parentNode.rgt) {

// find siblings and check if they have lft and rgt values set
self.siblings(function(err, nodes) {
if (nodes.every(function(node) { return node.lft && node.rgt;})) {
var maxRgt = 0;
nodes.forEach(function(node) {
if (node.rgt > maxRgt) {
maxRgt = node.rgt;
}
});
if (nodes.length === 0) {
// if it is a leaf node, the maxRgt should be the lft value of the parent
maxRgt = parentNode.lft;
}
model.update({lft: { $gt: maxRgt}}, {$inc: {lft: 2}}, {multi: true}, function(err, updatedCount) {
model.update({rgt: { $gt: maxRgt}}, {$inc: {rgt: 2}}, {multi: true}, function(err, updatedCount2) {
self.lft = maxRgt + 1;
self.rgt = maxRgt + 2;
next();
});
});
} else {
// the siblings do not have lft and rgt set. This means tree was not build.
// warn on console and move on.
// console.log('WARNING: tree is not built for ' + modelName + ' nodes. Siblings does not have lft/rgt');
next();
}
});
} else {
// parent node does not have lft and rgt set. This means tree was not built.
// warn on console and move on.
// console.log('WARNING: tree is not built for ' + modelName + ' nodes. Parent does not have lft/rgt');
next();
}
});
} else {
// no parentId is set, so ignore
next();
}
});


// Builds the tree by populating lft and rgt using the parentIds
schema.static('rebuildTree', function(parent, left, callback) {
Expand Down Expand Up @@ -61,6 +108,13 @@ var NestedSetPlugin = function(schema, options) {
return self.lft < other.lft && other.lft < self.rgt;
});

// returns the parent node
schema.method('parent', function(callback) {
var self = this;
var model = mongoose.model(modelName);
model.findOne({_id: self.parentId}, callback);
});

// Returns the list of ancestors + current node
schema.method('selfAndAncestors', function(callback) {
var self = this;
Expand Down
85 changes: 85 additions & 0 deletions tests/nested_set_test.js
Expand Up @@ -167,6 +167,22 @@ var tests = testCase({
});
});
},
'parent should return parent node': function(test) {
test.expect(4);
User.findOne({username: 'michael'}, function(err, user) {
User.rebuildTree(user, 1, function() {
User.findOne({username: 'kelly'}, function(err, kelly) {
test.ok(!err)
test.ok(kelly)
kelly.parent(function(err, node) {
test.ok(!err);
test.equal('meredith',node.username)
test.done()
});
});
});
});
},
'selfAndAncestors should return all ancestors higher up in tree + current node': function(test) {
test.expect(2);
User.findOne({username: 'michael'}, function(err, user) {
Expand Down Expand Up @@ -344,6 +360,75 @@ var tests = testCase({
});
});
});
},
'pre save middleware should not set lft and rgt if there is no parentId': function(test) {
test.expect(4);
var user = new User({
username: 'joe'
})
user.save(function(err, joe) {
test.ok(!err);
test.equal('joe', joe.username);
test.ok(!joe.lft);
test.ok(!joe.rgt);
test.done();
});
},
'adding a new leaf node the a built tree should re-arrange the tree correctly': function(test) {
test.expect(22)
User.findOne({username: 'michael'}, function(err, michael) {
User.rebuildTree(michael, 1, function() {
User.findOne({username: 'creed'}, function(err, creed) {
var newUser = new User({
username: 'joe',
parentId: creed._id
})
newUser.save(function(err, joe) {
User.find(function(err, users) {
// see docs/test_tree_after_leaf_insertion.png for the graphical representation of this tree
// with lft/rgt values after the insertion
users.forEach(function(person) {
if (person.username === 'michael') {
test.equal(1, person.lft);
test.equal(22, person.rgt);
} else if (person.username === 'meredith') {
test.equal(2, person.lft);
test.equal(9, person.rgt);
} else if (person.username === 'jim') {
test.equal(10, person.lft);
test.equal(17, person.rgt);
} else if (person.username === 'angela') {
test.equal(18, person.lft);
test.equal(21, person.rgt);
} else if (person.username === 'kelly') {
test.equal(3, person.lft);
test.equal(4, person.rgt);
} else if (person.username === 'creed') {
test.equal(5, person.lft);
test.equal(8, person.rgt);
} else if (person.username === 'phyllis') {
test.equal(11, person.lft);
test.equal(12, person.rgt);
} else if (person.username === 'stanley') {
test.equal(13, person.lft);
test.equal(14, person.rgt);
} else if (person.username === 'dwight') {
test.equal(15, person.lft);
test.equal(16, person.rgt);
} else if (person.username === 'oscar') {
test.equal(19, person.lft);
test.equal(20, person.rgt);
} else if (person.username === 'joe') {
test.equal(6, person.lft);
test.equal(7, person.rgt);
}
});
test.done();
});
});
});
});
});
}
});

Expand Down

0 comments on commit 31f0b78

Please sign in to comment.