Skip to content
Browse files

initial commit, adding slugify with unique slugs, and array of ancest…

…ors tree.
  • Loading branch information...
0 parents commit bc7b9ecd70687cc11dd0e4f95a7e3cef3a75310f Marius Kubilius committed
Showing with 467 additions and 0 deletions.
  1. +40 −0 .gitignore
  2. +3 −0 index.js
  3. +87 −0 lib/ancestorTree.js
  4. +86 −0 lib/slugify.js
  5. +33 −0 package.json
  6. +1 −0 readme.md
  7. +109 −0 test/ancestorTree.test.js
  8. +100 −0 test/slugify.test.js
  9. +8 −0 test/utils/common.js
40 .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 index.js
@@ -0,0 +1,3 @@
+require('directory')(__dirname + '/lib/', function (fn, filename) {
+ module.exports[filename] = fn
+})
87 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 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 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 readme.md
@@ -0,0 +1 @@
+A small collection of mongoose extensions to be used in my projects.
109 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();
+ });
+ });
+ })
+ });
+ });
+ });
+ });
+ });
+ });
+
+ });
+
+ });
+
+});
100 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();
+ });
+ });
+ });
+ });
+ });
+
+});
8 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
+}
+

0 comments on commit bc7b9ec

Please sign in to comment.
Something went wrong with that request. Please try again.