Skip to content

Commit b1b0c13

Browse files
committed
small fixes and updates from other contributors
1 parent 10acb97 commit b1b0c13

File tree

5 files changed

+461
-275
lines changed

5 files changed

+461
-275
lines changed

.gitignore

Lines changed: 0 additions & 2 deletions
This file was deleted.

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
language: node_js
2+
node_js:
3+
- "0.11"
4+
- "0.10"
5+
- "0.8"

lib/tree.js

Lines changed: 272 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,283 @@
1-
21
var Schema = require('mongoose').Schema;
32

43
module.exports = exports = tree;
54

5+
/**
6+
* @class tree
7+
* Tree Behavior for Mongoose
8+
*
9+
* Implements the materialized path strategy with cascade child re-parenting
10+
* on delete for storing a hierarchy of documents with mongoose
11+
*
12+
* @param {mongoose.Schema} schema
13+
* @param {Object} options
14+
*/
615
function tree(schema, options) {
7-
var pathSeparator = options && options.pathSeparator || '#';
8-
9-
schema.add({
10-
parent : {
11-
type : Schema.ObjectId,
12-
set : function(val) {
13-
if(typeof(val) === "object" && val._id) {
14-
return val._id;
16+
var pathSeparator = options && options.pathSeparator || '#'
17+
, onDelete = options && options.onDelete || 'DELETE'; //'REPARENT'
18+
19+
/**
20+
* Add parent and path properties
21+
*
22+
* @property {ObjectID} parent
23+
* @property {String} path
24+
*/
25+
schema.add({
26+
parent: {
27+
type: Schema.ObjectId,
28+
set: function (val) {
29+
return (val instanceof Object && val._id) ? val._id : val;
30+
},
31+
index: true
32+
},
33+
path: {
34+
type: String,
35+
index: true
1536
}
16-
return val;
17-
},
18-
index: true
19-
},
20-
path : {
21-
type : String,
22-
index: true
23-
}
24-
});
25-
26-
schema.pre('save', function(next) {
27-
var isParentChange = this.isModified('parent');
28-
29-
if(this.isNew || isParentChange) {
30-
if(!this.parent) {
31-
this.path = this._id.toString();
32-
return next();
33-
}
34-
35-
var self = this;
36-
this.collection.findOne({ _id : this.parent }, function(err, doc) {
37-
if(err) return next(err);
38-
39-
var previousPath = self.path;
40-
self.path = doc.path + pathSeparator + self._id.toString();
41-
42-
if(isParentChange) {
43-
// When the parent is changed we must rewrite all children paths as well
44-
self.collection.find({ path : { '$regex' : '^' + previousPath + pathSeparator } }, function(err, cursor) {
45-
if(err) return next(err);
46-
47-
var stream = cursor.stream();
48-
stream.on('data', function (doc) {
49-
var newPath = self.path+doc.path.substr(previousPath.length);
50-
self.collection.update({ _id : doc._id }, { $set : { path : newPath } }, function(err) {
51-
if(err) return next(err);
52-
});
53-
});
54-
stream.on('close', function() {
55-
next();
37+
});
38+
39+
/**
40+
* Pre-save middleware
41+
* Build or rebuild path when needed
42+
*
43+
* @param {Function} next
44+
*/
45+
schema.pre('save', function preSave(next) {
46+
var isParentChange = this.isModified('parent');
47+
48+
if (this.isNew || isParentChange) {
49+
if (!this.parent) {
50+
this.path = this._id.toString();
51+
return next();
52+
}
53+
54+
var self = this;
55+
this.collection.findOne({ _id: this.parent }, function (err, doc) {
56+
if (err) return next(err);
57+
58+
var previousPath = self.path;
59+
self.path = doc.path + pathSeparator + self._id.toString();
60+
61+
if (isParentChange) {
62+
// When the parent is changed we must rewrite all children paths as well
63+
self.collection.find({ path: { '$regex': '^' + previousPath + pathSeparator } }, function (err, cursor) {
64+
if (err) return next(err);
65+
66+
var stream = cursor.stream();
67+
stream.on('data', function (doc) {
68+
var newPath = self.path + doc.path.substr(previousPath.length);
69+
self.collection.update({ _id: doc._id }, { $set: { path: newPath } }, function (err) {
70+
if (err) return next(err);
71+
});
72+
});
73+
stream.on('close', next);
74+
stream.on('error', next);
75+
});
76+
} else {
77+
next();
78+
}
5679
});
57-
stream.on('error', function(err) {
58-
next(err);
80+
} else {
81+
next();
82+
}
83+
});
84+
85+
/**
86+
* Pre-remove middleware
87+
*
88+
* @param {Function} next
89+
*/
90+
schema.pre('remove', function preRemove(next) {
91+
if (!this.path)
92+
return next();
93+
94+
if (onDelete == 'DELETE') {
95+
this.collection.remove({ path: { '$regex': '^' + this.path + pathSeparator } }, next);
96+
}
97+
else {
98+
var self = this,
99+
newParent = this.parent,
100+
previousParent = this._id;
101+
102+
// Update parent property from children
103+
this.collection.find({ parent: previousParent }, function (err, cursor) {
104+
if (err) return next(err);
105+
var stream = cursor.stream();
106+
stream.on('data', function streamOnData(doc) {
107+
self.collection.update({ _id: doc._id }, { $set: { parent: newParent } }, function (err) {
108+
if (err) return next(err);
109+
});
110+
});
111+
stream.on('close', function streamOnClose() {
112+
// Cascade update Path
113+
self.collection.find({ path: { $regex: previousParent + pathSeparator} }, function (err, cursor) {
114+
115+
var subStream = cursor.stream();
116+
subStream.on('data', function subStreamOnData(doc) {
117+
var newPath = doc.path.replace(previousParent + pathSeparator, '');
118+
self.collection.update({ _id: doc._id }, { $set: { path: newPath } }, function (err) {
119+
if (err) return next(err);
120+
});
121+
});
122+
subStream.on('close', next);
123+
subStream.on('error', next);
124+
});
125+
});
126+
stream.on('error', next);
59127
});
60-
});
128+
//this.collection.update({})
129+
}
130+
});
131+
132+
/**
133+
* @method getChildren
134+
*
135+
* @param {[type]} recursive
136+
* @param {Function} next
137+
* @return {Model}
138+
*/
139+
schema.method('getChildren', function getChildren(recursive, next) {
140+
if (typeof(recursive) === "function") {
141+
next = recursive;
142+
recursive = false;
143+
}
144+
var filter = recursive ? { path: { $regex: '^' + this.path + pathSeparator } } : { parent: this._id };
145+
return this.model(this.constructor.modelName).find(filter, next);
146+
});
147+
148+
/**
149+
* @method getParent
150+
*
151+
* @param {Function} next
152+
* @return {Model}
153+
*/
154+
schema.method('getParent', function getParent(next) {
155+
return this.model(this.constructor.modelName).findOne({ _id: this.parent }, next);
156+
});
157+
158+
/**
159+
* @method getAncestors
160+
*
161+
* @param {Function} next
162+
* @return {Model}
163+
*/
164+
schema.method('getAncestors', function getAncestors(next) {
165+
if (this.path) {
166+
var ids = this.path.split(pathSeparator);
167+
ids.pop();
61168
} else {
62-
next();
169+
var ids = [];
63170
}
64-
});
65-
} else {
66-
next();
67-
}
68-
});
69-
70-
schema.pre('remove', function(next) {
71-
if(!this.path) {
72-
return next();
73-
}
74-
this.collection.remove({ path : { '$regex' : '^' + this.path + pathSeparator } }, next);
75-
});
76-
77-
schema.method('getChildren', function(recursive, cb) {
78-
if(typeof(recursive) === "function") {
79-
cb = recursive;
80-
recursive = false;
81-
}
82-
var filter = recursive ? { path : { $regex : '^' + this.path + pathSeparator } } : { parent : this._id };
83-
return this.model(this.constructor.modelName).find(filter, cb);
84-
});
85-
86-
schema.method('getParent', function(cb) {
87-
return this.model(this.constructor.modelName).findOne({ _id : this.parent }, cb);
88-
});
89-
90-
var getAncestors = function(cb) {
91-
if(this.path) {
92-
var ids = this.path.split(pathSeparator);
93-
ids.pop();
94-
} else {
95-
var ids = [];
96-
}
97-
var filter = { _id : { $in : ids } };
98-
return this.model(this.constructor.modelName).find(filter, cb);
99-
};
100-
101-
schema.method('getAnsestors', getAncestors);
102-
schema.method('getAncestors', getAncestors);
103-
104-
schema.virtual('level').get(function() {
105-
return this.path ? this.path.split(pathSeparator).length : 0;
106-
});
171+
var filter = { _id: { $in: ids } };
172+
return this.model(this.constructor.modelName).find(filter, next);
173+
});
174+
175+
176+
177+
/**
178+
* @method getChildrenTree
179+
*
180+
* @param {Object} filters (like for find)
181+
* @param {Object} fields (like for find)
182+
* @param {Object} options (like for find})
183+
* @param {Number} minLevel, default 1
184+
* @param {Boolean} recursive
185+
* @param {Boolean} allowEmptyChildren
186+
* @param {Function} next
187+
* @return {Model}
188+
*/
189+
schema.method('getChildrenTree', function (args, cb) {
190+
var self = this;
191+
192+
var rargs = args;
193+
if (typeof(args) === "function") {
194+
rargs = JSON.parse(JSON.stringify(args));
195+
cb = args;
196+
}
197+
198+
var filters = rargs.filters || {};
199+
var fields = rargs.fields || null;
200+
var options = rargs.options || {};
201+
var minLevel = rargs.minLevel || 1;
202+
var recursive = rargs.recursive != undefined ? rargs.recursive : true;
203+
var allowEmptyChildren = rargs.allowEmptyChildren != undefined ? rargs.allowEmptyChildren : true;
204+
205+
if (!cb) throw new Error('no callback defined when calling getChildrenTree');
206+
207+
// filters: Add recursive path filter or not
208+
if (recursive) {
209+
filters.path = { $regex: '^' + this.path + pathSeparator };
210+
if (filters.parent === null) delete filters.parent;
211+
} else {
212+
filters.parent = this._id;
213+
}
214+
215+
// columns: Add path and parent in the result if not already specified
216+
if (fields) {
217+
if (fields instanceof Object) {
218+
if (!fields.hasOwnProperty('path'))
219+
fields['path'] = 1;
220+
if (!fields.hasOwnProperty('parent'))
221+
fields['parent'] = 1;
222+
}
223+
else {
224+
if (!fields.match(/path/))
225+
fields += ' path';
226+
if (!fields.match(/parent/))
227+
fields += ' parent';
228+
}
229+
}
230+
231+
// options:sort , path sort is mandatory
232+
if (!options.sort) options.sort = {};
233+
options.sort.path = 1;
234+
235+
return this.model(this.constructor.modelName).find(filters, columns, options, function (err, results) {
236+
if (err) throw err;
237+
238+
var copyOf = function (obj) {
239+
var o = JSON.parse(JSON.stringify(obj));
240+
if (allowEmptyChildren)
241+
o.children = [];
242+
return o;
243+
}
244+
245+
var getLevel = function (path) {
246+
return path ? path.split(pathSeparator).length : 0;
247+
}
248+
249+
var createChildren = function (arr, node, level) {
250+
var rootLevel = getLevel(self.path) + 1;
251+
if (minLevel < rootLevel) {
252+
minLevel = rootLevel
253+
}
254+
if (level == minLevel) {
255+
return arr.push(copyOf(node));
256+
}
257+
var nextIndex = arr.length - 1
258+
var myNode = arr[nextIndex];
259+
if (!myNode) {
260+
//console.log("Tree node " + node.name + " filtered out. Level: " + level + " minLevel: " + minLevel);
261+
return []
262+
} else {
263+
createChildren(myNode.children, node, level - 1);
264+
}
265+
}
266+
var finalResults = [];
267+
for (var r in results) {
268+
var level = getLevel(results[r].path);
269+
createChildren(finalResults, results[r], level);
270+
}
271+
272+
cb(err, finalResults);
273+
274+
});
275+
});
276+
277+
/**
278+
* @property {Number} level <virtual>
279+
*/
280+
schema.virtual('level').get(function virtualPropLevel() {
281+
return this.path ? this.path.split(pathSeparator).length : 0;
282+
});
107283
}

0 commit comments

Comments
 (0)