From bc7b9ecd70687cc11dd0e4f95a7e3cef3a75310f Mon Sep 17 00:00:00 2001 From: Marius Kubilius Date: Mon, 7 Jan 2013 19:27:53 +0200 Subject: [PATCH] initial commit, adding slugify with unique slugs, and array of ancestors tree. --- .gitignore | 40 ++++++++++++++ index.js | 3 ++ lib/ancestorTree.js | 87 ++++++++++++++++++++++++++++++ lib/slugify.js | 86 ++++++++++++++++++++++++++++++ package.json | 33 ++++++++++++ readme.md | 1 + test/ancestorTree.test.js | 109 ++++++++++++++++++++++++++++++++++++++ test/slugify.test.js | 100 ++++++++++++++++++++++++++++++++++ test/utils/common.js | 8 +++ 9 files changed, 467 insertions(+) create mode 100644 .gitignore create mode 100644 index.js create mode 100644 lib/ancestorTree.js create mode 100644 lib/slugify.js create mode 100644 package.json create mode 100644 readme.md create mode 100644 test/ancestorTree.test.js create mode 100644 test/slugify.test.js create mode 100644 test/utils/common.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58fd79e --- /dev/null +++ b/.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 \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..b01c3a9 --- /dev/null +++ b/index.js @@ -0,0 +1,3 @@ +require('directory')(__dirname + '/lib/', function (fn, filename) { + module.exports[filename] = fn +}) diff --git a/lib/ancestorTree.js b/lib/ancestorTree.js new file mode 100644 index 0000000..f803c11 --- /dev/null +++ b/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; diff --git a/lib/slugify.js b/lib/slugify.js new file mode 100644 index 0000000..fa2812a --- /dev/null +++ b/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 + * @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 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; + diff --git a/package.json b/package.json new file mode 100644 index 0000000..6c031b2 --- /dev/null +++ b/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" +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..cf40e36 --- /dev/null +++ b/readme.md @@ -0,0 +1 @@ +A small collection of mongoose extensions to be used in my projects. diff --git a/test/ancestorTree.test.js b/test/ancestorTree.test.js new file mode 100644 index 0000000..27c7003 --- /dev/null +++ b/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(); + }); + }); + }) + }); + }); + }); + }); + }); + }); + + }); + + }); + +}); diff --git a/test/slugify.test.js b/test/slugify.test.js new file mode 100644 index 0000000..dc03e9d --- /dev/null +++ b/test/slugify.test.js @@ -0,0 +1,100 @@ +//dependencies +var assert = require('assert'); +var mongoose = require('mongoose'); +var slugify = require('../lib/slugify'); +var common = require('./utils/common'); +var db = common.db; +var Schema = mongoose.Schema; +var ObjectId = Schema.ObjectId; + +describe('slugify', function () { + var MockSchema = new Schema(); + + describe('#default()', function() { + MockSchema.plugin(slugify); + var MockModel = db.model('slugMock', MockSchema); + var mock = new MockModel({ title: 'I will & be @ Slugiffied'}); + + before(function() { + MockModel.remove(function(err) { + assert.strictEqual(err, null); + }); + }); + + it('should have custom properties set', function (done) { + assert.strictEqual(typeof MockSchema.paths.title, 'object'); + assert.strictEqual(typeof MockSchema.paths.slug, 'object'); + assert.strictEqual(typeof MockSchema.methods.slugify, 'function'); + assert.strictEqual(typeof MockSchema.statics.slugify, 'function'); + done(); + }); + + it('should correctly format a slug', function(done) { + mock.save(function(err, doc) { + assert.strictEqual(err, null); + assert.strictEqual(typeof doc.slug, 'string'); + assert.strictEqual(doc.slug, 'i-will-be-slugiffied'); + done(); + }); + }); + + it('should keep the slug the same', function(done) { + mock.title = 'My new nice title'; + mock.save(function(err,doc) { + assert.strictEqual(err,null); + assert.strictEqual(typeof doc.slug, 'string'); + assert.strictEqual(doc.slug, 'i-will-be-slugiffied'); + done(); + }); + }); + + it('should change the slug', function(done) { + mock.slug = 'new-nice slug'; + mock.save(function(err,doc) { + assert.strictEqual(err, null); + assert.strictEqual(typeof doc.slug, 'string'); + assert.strictEqual(doc.slug, 'new-nice-slug'); + done(); + }); + }); + + it('should manually slugify', function (done) { + var str = 'one two three'; + assert.strictEqual(mock.slugify(str), 'one-two-three'); + assert.strictEqual(MockModel.slugify(str), 'one-two-three'); + done(); + }); + + }); + + describe('#duplicates()', function() { + MockSchema.plugin(slugify); + var MockModel = db.model('slugMock', MockSchema); + var mock1 = new MockModel({ title: 'duplicated slug'}); + var mock2 = new MockModel({ title: 'duplicated slug'}); + var mock3 = new MockModel({ title: 'duplicated slug'}); + + before(function() { + MockModel.remove(function(err) { + assert.strictEqual(err, null); + }); + }); + + it('should deal gracefully with duplicates', function(done) { + mock1.save(function(err, doc) { + assert.strictEqual(err,null); + assert.strictEqual(doc.slug, 'duplicated-slug'); + mock2.save(function(err, doc) { + assert.strictEqual(err, null); + assert.strictEqual(doc.slug, 'duplicated-slug-1'); + mock3.save(function(err, doc) { + assert.strictEqual(err, null); + assert.strictEqual(doc.slug, 'duplicated-slug-2'); + done(); + }); + }); + }); + }); + }); + +}); diff --git a/test/utils/common.js b/test/utils/common.js new file mode 100644 index 0000000..ddfcf01 --- /dev/null +++ b/test/utils/common.js @@ -0,0 +1,8 @@ +//define connection +var mongoose = require('mongoose'); +var db = mongoose.connect(process.env.MONGO_DB_URI || 'mongodb://localhost/mongoose_utils') + +module.exports = { + db: db +} +