Efficient tree structures in Mongoose using materialized paths.
- One-query retrieval of ancestors
- Automatically updates ancestors and children on change to parent
- Reparent children to grandparent on delete
- Get ancestors for multiple documents in a single query
Install with npm
:
$ npm install mongoose-path-tree
Use it:
var tree = require('mongoose-path-tree');
var UserSchema = new Schema({
name : String
});
UserSchema.plugin(tree);
var User = mongoose.model('User', UserSchema);
var adam = new User({ name : 'Adam' });
var bob = new User({ name : 'Bob' });
var carol = new User({ name : 'Carol' });
// Set the parent relationships
bob.parent = adam;
carol.parent = bob;
adam.save(function() {
bob.save(function() {
carol.save();
});
});
At this point in mongoDB you will have documents similar to
{
"_id" : ObjectId("50136e40c78c4b9403000001"),
"name" : "Adam",
"path" : "50136e40c78c4b9403000001"
}
{
"_id" : ObjectId("50136e40c78c4b9403000002"),
"name" : "Bob",
"parent" : ObjectId("50136e40c78c4b9403000001"),
"path" : "50136e40c78c4b9403000001#50136e40c78c4b9403000002"
}
{
"_id" : ObjectId("50136e40c78c4b9403000003"),
"name" : "Carol",
"parent" : ObjectId("50136e40c78c4b9403000002"),
"path" : "50136e40c78c4b9403000001#50136e40c78c4b9403000002#50136e40c78c4b9403000003"
}
The path is used for recursive methods and is kept up to date by the plugin if the parent is changed
Model.plugin(tree, {
pathSeparator: '#' // Default path separator
onDelete: 'REPARENT' // Can be set to 'DELETE' or 'REPARENT'. Default: 'DELETE'
numWorkers: 5 // Number of stream workers
idType: Schema.ObjectId // Type used for _id. Can be, for example, String generated by shortid module
parentExists: true // Whether `parent` property is already defined in your schema. Default `false`
pathExists: true // Whether `path` property is already defined in your schema. Default `false`
levelExists: true // Whether `level` property is already defined in your schema. Default `false`
})
If parentExists === true
, your schema must, at minumum, define type
and index
on parent
. The same applies to path
. level
requires a Number
type, and an index is recommended if you plan to query on it, but is not required. This library defines an index on level
.
var treeOpts = {
parentExists: true,
pathExists: true,
levelExists: true
};
var schemaDef = {
parent: {
type: ObjectId,
index: true
},
path: {
type: String,
index: true
},
level: {
type: Number,
index: true
}
};
Signature:
getChildren([filters], [fields], [options], [recursive], cb);
args are additional filters if needed. if recursive is supplied and true, subchildren are returned
Based on the above hierarchy:
adam.getChildren(function(err, users) {
// users is an array of with the bob document
});
adam.getChildren(true, function(err, users) {
// users is an array with both bob and carol documents
});
Signature as method:
getChildrenTree([args], cb);
Signature as static:
getChildrenTree([rootDoc], [args], cb);
return a recursive tree of sub-children.
args is an object you can defined with theses properties :
filters: mongoose query filter, optional, default null
example: filters: {owner:myId}
fields: mongoose fields, optional, default null (all fields)
example: fields: "_id name owner"
options: mongoose query option, optional, default null
example: options:{{sort:'-name'}}
minLevel: level at which will start the search, default 1
example: minLevel:2
recursive: boolean, default true
make the search recursive or only fetch children for the specified level
example: recursive:false
allowEmptyChildren: boolean, default true
if true, every child not having children will have 'children' attribute (empty array)
if false, every child not having children will not have 'children' attribute
Example :
var args = {
filters: {owner:myId},
fields: "_id name owner",
minLevel:2,
recursive:true,
allowEmptyChildren:false
}
getChildrenTree(args,myCallback);
Based on the above hierarchy:
adam.getChildrenTree( function(err, users) {
/* if you dump users, you will have something like this :
{
"_id" : ObjectId("50136e40c78c4b9403000001"),
"name" : "Adam",
"path" : "50136e40c78c4b9403000001"
"children" : [{
"_id" : ObjectId("50136e40c78c4b9403000002"),
"name" : "Bob",
"parent" : ObjectId("50136e40c78c4b9403000001"),
"path" : "50136e40c78c4b9403000001#50136e40c78c4b9403000002"
"children" : [{
"_id" : ObjectId("50136e40c78c4b9403000003"),
"name" : "Carol",
"parent" : ObjectId("50136e40c78c4b9403000002"),
"path" : "50136e40c78c4b9403000001#50136e40c78c4b9403000002#50136e40c78c4b9403000003"
}]
}]
}
*/
});
Signature:
getAncestors([filters], [fields], [options], cb);
Based on the above hierarchy:
carol.getAncestors(function(err, users) {
// users is an array of adam and bob
})
Get all ancestors for a collection of documents. Optimized to execute in a single query, rather than one per document.
Signature:
Model.getAncestors(documents, [filters], [fields], [options], cb);
Based on the above hierarchy:
User.getAncestors([carol, bob], function(err, users) {
console.log(users);
})
// [
// [{name: 'Adam'}, {'name': 'Bob'}],
// [{name: 'Adam'}]
// ]
The depth of the node. Root is 1
carol.level; // equals 3
Run with npm
:
$ npm run test