Skip to content

Commit

Permalink
initial commit, adding slugify with unique slugs, and array of ancest…
Browse files Browse the repository at this point in the history
…ors tree.
  • Loading branch information
Marius Kubilius committed Jan 7, 2013
0 parents commit bc7b9ec
Show file tree
Hide file tree
Showing 9 changed files with 467 additions and 0 deletions.
40 changes: 40 additions & 0 deletions .gitignore
@@ -0,0 +1,40 @@
# Numerous always-ignore extensions
*.diff
*.err
*.orig
*.log
*.rej
*.swo
*.swp
*.vi
*~

# OS or Editor folders
.DS_Store
.cache
.project
.settings
nbproject
thumbs.db

# Logs
.log
.pid
.sock
.monitor

# Dreamweaver added files
_notes
dwsync.xml

# Komodo
*.komodoproject
.komodotools

# Folders to ignore
node_modules
.hg
.svn
publish
.idea
_dev
3 changes: 3 additions & 0 deletions index.js
@@ -0,0 +1,3 @@
require('directory')(__dirname + '/lib/', function (fn, filename) {
module.exports[filename] = fn
})
87 changes: 87 additions & 0 deletions lib/ancestorTree.js
@@ -0,0 +1,87 @@
/**
* Mongoose extension implementing ancestor tree as defined in the link below.
* @url http://docs.mongodb.org/manual/tutorial/model-tree-structures-with-ancestors-array/
* @author Marius Kubilius
* @todo add methods and statics dealing with the tree.
*/
var ObjectId = require('mongoose').Schema.ObjectId;

ancestorTree = function(schema){

var fields = {};
if (!schema.paths.parent) {
fields.parent = {
type: ObjectId,
set : function(val) {
if(typeof(val) === "object" && val._id) {
return val._id;
}
return val;
},
index: true
}
}

if (!schema.paths.ancestors) {
fields.ancestors = [{type: ObjectId, index: true}];
}

schema.add(fields);

var createAncestors = function(ancestors, parent) {
return ancestors.push(parent);
}

schema.pre('save', function (next) {
var parentModified = this.isDirectModified('parent');
var self = this;

if (self.isNew || parentModified) {
if(!self.parent && self.isNew){
return next();
}
self.collection.findOne({ _id : self.parent }, function(err, doc) {
if(err) return next(err);
var oldAncestorsLength = self.ancestors.length;
if(doc) {
self.ancestors = doc.ancestors;
// @todo find atomic operation for that.
self.ancestors.nonAtomicPush(self.parent);
//self.markModified('ancestors');
}
else {
self.ancestors = [];
self.markModified('ancestors');
}
if(!self.isNew && parentModified){

self.collection.find({ ancestors : self._id }, function(err, cursor) {
if(err) return next(err);
var stream = cursor.stream();
stream.on('data', function (doc) {
//console.log(self.ancestors.concat(doc.ancestors.slice(oldAncestorsLength)));
var newPath = self.ancestors.concat(doc.ancestors.slice(oldAncestorsLength));
self.collection.update({ _id : doc._id }, { $set : { ancestors : newPath } }, function(err){
if(err) return next();
});
});
stream.on('close', function() {
next();
});
stream.on('error', function(err) {
next(err);
});
});
}
else{
next();
}
});
}
else{
next();
}
});
}

module.exports = ancestorTree;
86 changes: 86 additions & 0 deletions lib/slugify.js
@@ -0,0 +1,86 @@
/**
* Mongoose extension which makes sure that the slugs are unique no matter what.
* Has minimum configuration operations, as it is suposed to be used for in house
* developement.
* @author Marius Kubilius <marius.kubilius@gmail.com>
* @param schema
* @todo add lithuanian accents.
*/
slugify = function(schema) {

//define defaults
var fr = 'àáâãäåçèéêëìíîïñðóòôõöøùúûüýÿ' // Accent chars to find
var to = 'aaaaaaceeeeiiiinooooooouuuuyy' // Accent replacement
var fields = {};

//if not defined define schema for title and slug.

if (!schema.paths.slug) {
fields.slug = {
type: String
, index:{unique: true, sparse: true}
}
}

if (!schema.paths.title) {
fields.title = String;
}

schema.add(fields);

['static', 'method'].forEach(function (method) {
schema[method]('slugify', function (str) {
str = str
.replace(/^\s+|\s+$/g, '')
.toLowerCase();

//replace all illegal characters and accents
for (var i=0; i<fr.length; i++) {
str = str.replace(new RegExp(fr.charAt(i), 'g'), to.charAt(i));
}
return str
.replace(/[^a-z0-9 -]/g, '')
.replace(/\s+/g, '-');
})
})

// pre save check whether slug is modified ;)
// Extract the slug on save, optionally overriding a previous value
schema.pre('save', function (next) {
var self = this;
var slugChanged = self.isDirectModified('slug');
//check for duplicated slugs.
var checkDupes = function(self, oldSlug, i){
self.collection.count({slug: self.slug}, function(err, count) {
if (err) return next(err);
if(count > 0) {
self.slug = oldSlug + '-' + i;
i++
checkDupes(self, oldSlug, i);
}
else {
next();
}
});
}
if (slugChanged) {
self.slug = self.slugify(self.slug);
checkDupes(self, self.slug, 1);
}
if (!self.slug) {
self.slug = self.slugify(self.title);
checkDupes(self, self.slug, 1);
}
else {
next();
}


});



}

module.exports = slugify;

33 changes: 33 additions & 0 deletions package.json
@@ -0,0 +1,33 @@
{
"name" : "lbg-mongoose-utils",
"version" : "0.0.1",
"description" : "a collection of plugins used in my projects for mongoose",
"main" : "index.js",
"bin" : {},
"directories" : {
"test" : "test"
},
"scripts" : {
"test": "make test && make clean"
},
"dependencies" : {
"mongoose": "~3.5.x"
},
"devDependencies" : {
"mocha": "~0.10.0"
},
"keywords" : [
"mongoose",
"mongoose 3.5.x",
"slugs",
"unique-slugs",
"array of ancestors",
"tree"
],
"author" : {
"name" : "Marius Kubilius",
"email" : "marius.kubilius@gmail.com",
"url" : "https://www.littleboygenius.com"
},
"license" : "MIT"
}
1 change: 1 addition & 0 deletions readme.md
@@ -0,0 +1 @@
A small collection of mongoose extensions to be used in my projects.
109 changes: 109 additions & 0 deletions test/ancestorTree.test.js
@@ -0,0 +1,109 @@
//dependencies
var assert = require('assert');
var mongoose = require('mongoose');
//mongoose.set('debug', true);
var slugify = require('../lib/ancestorTree');
var common = require('./utils/common');
var db = common.db;
var Schema = mongoose.Schema;
var ObjectId = Schema.ObjectId;

describe('ancestorTree', function () {
var MockSchema = new Schema();

describe('#default()', function() {
MockSchema.plugin(ancestorTree);
var MockModel = db.model('ancestorMock', MockSchema);
var parent = new MockModel({});
var child = new MockModel({parent: parent._id});
var subChild = new MockModel({parent: child._id});

before(function() {
MockModel.remove(function(err) {
assert.strictEqual(err, null);
});
});

it('should have custom properties set', function (done) {
assert.strictEqual(typeof MockSchema.paths.parent, 'object');
assert.strictEqual(typeof MockSchema.paths.ancestors, 'object');
done();
});

it('should save and set hierarchy where aplicable', function (done) {
parent.save(function(err, doc){
assert.strictEqual(err, null);
assert.strictEqual(typeof doc.parent, 'undefined');
assert.strictEqual(typeof doc.ancestors, 'object');
assert.strictEqual(doc.parent, undefined);
assert.strictEqual(doc.ancestors.length, 0);
child.save(function(err,doc){
assert.strictEqual(err, null);
assert.strictEqual(typeof doc.parent, 'object');
assert.strictEqual(doc.parent, parent._id);
assert.strictEqual(typeof doc.ancestors, 'object');
assert.strictEqual(doc.ancestors.length, 1);
assert.strictEqual(doc.ancestors[0], parent._id);
subChild.save(function(err, doc){
assert.strictEqual(err, null, 'should be no errors thrown');
assert.strictEqual(doc.ancestors.length, 2, 'should be length of 2');
assert.strictEqual(doc.ancestors[1], child._id, 'second item should be last added');
done();
});
});
});
});

});

describe('#update()', function(){
MockSchema.plugin(ancestorTree);
var MockModel = db.model('ancestorMock', MockSchema);
var p = new MockModel({});
var c = new MockModel({parent: p._id});
var sc = new MockModel({parent: c._id});
var ap = new MockModel({});
var ac = new MockModel({parent: ap._id});
var asc = new MockModel({parent: ac._id});

before(function() {
MockModel.remove(function(err) {
assert.strictEqual(err, null);
});
});

it ('should save and update ancestors.', function (done) {
p.save(function(err,doc){
assert.strictEqual(err, null, 'item without parent should save.');
c.save(function(err, doc){
sc.save(function(err,doc) {
ap.save(function(err,doc) {
ac.save(function(err,doc) {
asc.save(function(err,doc) {
assert.strictEqual(err, null, 'should be no errors while saving.');
ac.parent = c;
ac.save(function(err,doc){
assert.strictEqual(err, null, 'should be no errors while saving.');
MockModel.findOne({_id: asc._id}, function(err, doc){
assert.strictEqual(doc.ancestors[0].toString(), p._id.toString(), 'hierarchy should be updated to the new tree');
assert.strictEqual(doc.ancestors[1].toString(), c._id.toString(), 'hierarchy should be updated in children');
assert.strictEqual(doc.ancestors.length, 3)
c.parent = undefined;
c.save(function(err,doc){
assert.strictEqual(err, null);
done();
});
});
})
});
});
});
});
});
});

});

});

});

0 comments on commit bc7b9ec

Please sign in to comment.