├── .travis.yml ├── .gitignore ├── CHANGES.md ├── test ├── setup.js ├── db.js ├── tenancy.js ├── hooks.js └── util.js ├── package.json ├── LICENSE ├── .jshintrc ├── README.md └── mongojs-hooks.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | - "0.8" 6 | 7 | services: 8 | - mongodb 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # mongojs-hooks Change Log 2 | 3 | ## Unreleased 4 | 5 | 6 | ## 0.1.0 7 | - db() connection helper function 8 | - tenant() function to separate collection definitions in a multi-tenancy application 9 | - mongojs-hooks.collection() to define pre() and post() hooks -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | chai.config.includeStack = true; 3 | global.should = chai.should(); 4 | 5 | var mongo = require("../mongojs-hooks"); 6 | 7 | module.exports = { 8 | db: "mongojs-hooks-tdd" 9 | }; 10 | 11 | before(function () { 12 | mongo.db("test", module.exports.db); 13 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongojs-hooks", 3 | "description": "Wrap monogjs in pre and post hooks", 4 | "version": "0.1.8", 5 | "main": "./mongojs-hooks", 6 | "author": { 7 | "name": "David N. Johnson" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/clear/mongojs-hooks.git" 12 | }, 13 | "dependencies": { 14 | "mongojs": "0.18.x", 15 | "fn-hooks": "0.1.x", 16 | "lodash": "2.4.x" 17 | }, 18 | "devDependencies": { 19 | "mocha": "1.17.x", 20 | "chai": "1.10.x", 21 | "sinon": "1.8.x" 22 | }, 23 | "engines": { 24 | "node": ">= 0.8.0" 25 | }, 26 | "scripts": { 27 | "test": "node_modules/mocha/bin/mocha --reporter spec" 28 | } 29 | } -------------------------------------------------------------------------------- /test/db.js: -------------------------------------------------------------------------------- 1 | var mongo = require("../mongojs-hooks"); 2 | 3 | describe("db()", function () { 4 | beforeEach(function () { 5 | mongo.db("test key", undefined); 6 | }); 7 | 8 | afterEach(function () { 9 | mongo.db("test key", undefined); 10 | }); 11 | 12 | it("with key and no existing connection should return undefined", function () { 13 | var db = mongo.db("test key"); 14 | (db === undefined).should.be.ok; 15 | }); 16 | 17 | it("with key and name should return a new database connection", function () { 18 | var db = mongo.db("test key", "mongojs-hooks-tdd"); 19 | db.should.be.ok; 20 | db._name.should.equal("mongojs-hooks-tdd"); 21 | }); 22 | 23 | it("with key and an existing connection should return the database connection", function () { 24 | var dbFirst = mongo.db("test key", "mongojs-hooks-tdd"); 25 | 26 | var db = mongo.db("test key"); 27 | db.should.be.ok; 28 | db._name.should.equal("mongojs-hooks-tdd"); 29 | db.should.equal(dbFirst); 30 | }); 31 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Clear 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/tenancy.js: -------------------------------------------------------------------------------- 1 | var mongo = require("../mongojs-hooks"); 2 | var config = require("./setup"); 3 | 4 | describe("tenancy", function () { 5 | var db; 6 | 7 | before(function () { 8 | db = mongo.db("test"); 9 | }); 10 | 11 | it("collection() with collection name should return a collection with supplied name", function () { 12 | var collection = db.collection("tests"); 13 | collection.should.be.ok; 14 | collection._name.should.equal(config.db + ".tests"); 15 | }); 16 | 17 | it("tenant() with tenant name should return a collection with supplied name", function () { 18 | var collection = db.tenant("tenant"); 19 | collection.should.be.ok; 20 | collection._name.should.equal(config.db + ".tenant"); 21 | }); 22 | 23 | it("tenant().collection() with tenant name should prepend collection name with tenant name", function () { 24 | var collection = db.tenant("tenant").collection("tests"); 25 | collection.should.be.ok; 26 | collection._name.should.equal(config.db + ".tenant.tests"); 27 | }); 28 | 29 | // This test doesn't need to assert anything, because if we mock out the call and test to 30 | // see if it executes, then we don't evaluate the lazy loading and won't know if it called 31 | // correctly. Instead, this test will fail by throwing an exception. 32 | it("tenant().collection().find() with tenant name should not throw an exception", function (done) { 33 | var collection = db.tenant("tenant").collection("tests"); 34 | 35 | collection.find(function () { 36 | done(); 37 | }); 38 | }); 39 | }); -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | /*** Globals ***/ 3 | // To ignore any custom global variables, enable the `predef` option and list 4 | // your variables within it. 5 | "predef": [ 6 | "describe", 7 | "should", 8 | "it", 9 | "before", 10 | "after", 11 | "beforeEach", 12 | "afterEach" 13 | ], 14 | 15 | /*** Enforcing options ***/ 16 | // Set these to `true` to enforce, or `false` to relax. 17 | "bitwise": true, 18 | "camelcase": false, 19 | "curly": false, 20 | "eqeqeq": true, 21 | "forin": false, 22 | "immed": true, 23 | "latedef": false, 24 | "newcap": true, 25 | "noarg": true, 26 | "noempty": true, 27 | "nonew": false, 28 | "plusplus": false, 29 | "regexp": false, 30 | "undef": true, 31 | "unused": true, 32 | "strict": false, 33 | "trailing": true, 34 | 35 | /*** Relaxing options ***/ 36 | // Set these to `true` to relax, or `false` to enforce. 37 | "asi": false, 38 | "boss": false, 39 | "debug": false, 40 | "eqnull": true, 41 | "esnext": true, 42 | "evil": false, 43 | "expr": true, 44 | "funcscope": false, 45 | "globalstrict": false, 46 | "iterator": false, 47 | "lastsemic": false, 48 | "laxbreak": false, 49 | "laxcomma": false, 50 | "loopfunc": true, 51 | "multistr": false, 52 | "onecase": false, 53 | "proto": false, 54 | "regexdash": false, 55 | "scripturl": false, 56 | "shadow": true, 57 | "smarttabs": false, 58 | "sub": false, 59 | "supernew": false, 60 | "validthis": true, 61 | 62 | /*** Environments ***/ 63 | // Set each environment that you're using to `true`. 64 | "browser": false, 65 | "couch": false, 66 | "devel": false, 67 | "dojo": false, 68 | "jquery": true, 69 | "mootools": false, 70 | "node": true, 71 | "nonstandard": false, 72 | "prototypejs": false, 73 | "rhino": false, 74 | "wsh": false, 75 | 76 | /*** Legacy from JSLint ***/ 77 | // Set these to `true` to enforce, or `false` to relax. 78 | "nomen": false, 79 | "onevar": false, 80 | "passfail": false, 81 | "white": true 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mongojs-hooks 2 | 3 | A lightweight module that wraps [mongojs](https://github.com/mafintosh/mongojs) in pre() and post() hooks with support for multi-tenancy. 4 | 5 | [![Build Status](https://travis-ci.org/clear/mongojs-hooks.png)](https://travis-ci.org/clear/mongojs-hooks) 6 | [![NPM version](https://badge.fury.io/js/mongojs-hooks.png)](http://badge.fury.io/js/mongojs-hooks) 7 | 8 | ## Installation 9 | 10 | $ npm install mongojs-hooks 11 | 12 | ## Usage 13 | 14 | To use hooks, you need to define the collection they will be applied to. 15 | 16 | var mongo = require("mongojs-hooks"); 17 | var myCollection = mongo.collection("my.collection"); 18 | 19 | myCollection.pre("save", function (next, object, callback) { 20 | //Do whatever you want with your object here, validation, mutation, etc. 21 | 22 | //Now call next() to propogate to save() 23 | next(object, callback); 24 | }); 25 | 26 | myCollection.post("save", function (next) { 27 | //Will execute after Mongo's save() 28 | 29 | next(); 30 | }); 31 | 32 | The hook will run for that collection anytime you call save(). 33 | 34 | var db = mongo("my.database"); 35 | db.collection("my.collection").save({ my: "object" }); 36 | 37 | In a multi-tenancy application, your collections will typically be uniquely named but you probably still want to bind the same hooks irrespesctive of the tenant. **mongojs-hooks** introduces a helper `tenant()` function to separate collections from their tenant: 38 | 39 | //Will call pre()/post() hooks and save { my: "object" } to the collection client_name.my.collection 40 | db.tenant("client_name").collection("my.collection").save({ my: "object" }); 41 | 42 | **mongojs-hooks** adds one final helper function to assist in sharing a database connection around your application in the form of `db()`. 43 | 44 | //Connects and stores the database connection 45 | var db1 = mongo.db("db", "my.database"); 46 | 47 | //Can be used in a separate file to retrieve the connection above 48 | var db2 = mongo.db("db"); 49 | 50 | //db1 === db2 51 | 52 | Aside from the added functions above, **mongojs-hooks** is a transparent wrapper around [mongojs](https://github.com/mafintosh/mongojs) so refer to its documentation for more information. 53 | 54 | 55 | ## Tests 56 | 57 | $ npm test 58 | 59 | ## Contributing 60 | 61 | All contributions are welcome! I'm happy to accept pull requests as long as they conform to the following guidelines: 62 | 63 | - Keep the API clean, we prefer ease-of-use over extra features 64 | - Don't break the build and add tests where necessary 65 | - Keep the coding style consistent, we follow [JSHint's Coding Style](http://www.jshint.com/hack/) 66 | 67 | Otherwise, please [open an issue](https://github.com/clear/mongojs-hooks/issues/new) if you have any suggestions or find a bug. 68 | 69 | ## License 70 | 71 | [The MIT License (MIT)](https://github.com/clear/mongojs-hooks/blob/master/LICENSE) - Copyright (c) 2013 Clear Learning Systems -------------------------------------------------------------------------------- /mongojs-hooks.js: -------------------------------------------------------------------------------- 1 | var mongojs = require("mongojs"); 2 | var fnhooks = require("fn-hooks"); 3 | var util = require("util"); 4 | var _ = require("lodash"); 5 | 6 | // DB connection helper 7 | var dbConns = { }; 8 | 9 | mongojs.db = function (key, dbConnectionString) { 10 | if (dbConnectionString !== undefined) 11 | dbConns[key] = mongojs(dbConnectionString); 12 | 13 | return dbConns[key]; 14 | }; 15 | 16 | // Tenancy helper 17 | mongojs.Database.prototype.tenant = function (tenantName) { 18 | var db = this; 19 | var tenant = mongojs.Database.prototype.collection.apply(this, arguments); 20 | 21 | tenant.collection = function (collName) { 22 | // 'this' currently refers to the collection returned as 'tenant' and not the db 23 | return mongojs.Database.prototype.collection.call(db, tenantName + "." + collName, collName); 24 | }; 25 | 26 | return tenant; 27 | }; 28 | 29 | // Define collection hooks 30 | var colls = { }; 31 | 32 | mongojs.reset = function () { 33 | colls = { }; 34 | }; 35 | 36 | mongojs.collection = function (name) { 37 | // Use already existing collection if previously created 38 | if (colls[name] !== undefined) 39 | return colls[name].prototype; 40 | 41 | // Create retrievable collection 42 | colls[name] = function () { 43 | colls[name].super_.apply(this, arguments); 44 | }; 45 | 46 | // Inherit from parent mongojs collection and add pre() and post() hooks 47 | util.inherits(colls[name], mongojs.Collection); 48 | fnhooks(colls[name].prototype); 49 | 50 | return colls[name].prototype; 51 | }; 52 | 53 | mongojs.Database.prototype.collection = _.wrap(mongojs.Database.prototype.collection, function (fn, name, collName) { 54 | // Create a parent mongojs collection to setup _name and _get fields 55 | var collection = fn.call(this, name); 56 | 57 | // Now if a pre-defined collection exists (use tenant-less name if it exists), call it with the internal fields retrieved above 58 | if (colls[collName || name]) 59 | collection = new colls[collName || name](collection._name, collection._get); 60 | 61 | return collection; 62 | }); 63 | 64 | mongojs.util = { 65 | // Converts any . and $ characters in key names with the full-length unicode characters. 66 | // This is necessary since these two characters are illegal in Mongo key names. 67 | sanitise: function (object) { 68 | if (!object || object._bsontype) 69 | return object; 70 | 71 | if (_.isString(object)) 72 | return object.replace(/\./g, "U+FF0E").replace(/\$/g, "U+FF04"); 73 | 74 | var newObject = _.isArray(object) ? [ ] : { }; 75 | 76 | Object.keys(object).forEach(function (key) { 77 | var sKey = key; 78 | 79 | if (/[.|$]/.test(key)) { 80 | sKey = mongojs.util.sanitise(key); 81 | newObject[sKey] = object[key]; 82 | } else { 83 | newObject[key] = object[key]; 84 | } 85 | 86 | // Recurse 87 | if (isObject(newObject[sKey])) 88 | newObject[sKey] = this.sanitise(newObject[sKey]); 89 | }.bind(this)); 90 | 91 | return newObject; 92 | }, 93 | 94 | // Will convert full-length unicode chatacters back to their ASCII form. 95 | unsanitise: function (object) { 96 | if (!object || object._bsontype) 97 | return object; 98 | 99 | if (_.isString(object)) 100 | return object.replace(/U\+FF0E/g, ".").replace(/U\+FF04/g, "$"); 101 | 102 | var newObject = _.isArray(object) ? [ ] : { }; 103 | 104 | Object.keys(object).forEach(function (sKey) { 105 | var key = sKey; 106 | 107 | if (/(U\+FF0E|U\+FF04)/.test(sKey)) { 108 | key = mongojs.util.unsanitise(sKey); 109 | newObject[key] = object[sKey]; 110 | } else { 111 | newObject[key] = object[key]; 112 | } 113 | 114 | // Recurse 115 | if (isObject(newObject[key])) 116 | newObject[key] = this.unsanitise(newObject[key]); 117 | }.bind(this)); 118 | 119 | return newObject; 120 | }, 121 | 122 | // Flattens an object into dot-notation 123 | flatten: function (object) { 124 | var result = { }; 125 | 126 | for (var i in object) { 127 | if (!object.hasOwnProperty(i)) continue; 128 | 129 | if (isObject(object[i])) { 130 | var flatObject = this.flatten(object[i]); 131 | for (var x in flatObject) { 132 | if (!flatObject.hasOwnProperty(x)) continue; 133 | 134 | result[i + '.' + x] = flatObject[x]; 135 | } 136 | } else { 137 | result[i] = object[i]; 138 | } 139 | } 140 | 141 | return result; 142 | } 143 | }; 144 | 145 | var isObject = function (obj) { 146 | if (typeof obj !== "object" || obj === null) 147 | return false; 148 | 149 | var ObjProto = obj; 150 | while (Object.getPrototypeOf(ObjProto = Object.getPrototypeOf(ObjProto)) !== null) ; 151 | 152 | return Object.getPrototypeOf(obj) === ObjProto; 153 | }; 154 | 155 | module.exports = mongojs; -------------------------------------------------------------------------------- /test/hooks.js: -------------------------------------------------------------------------------- 1 | var sinon = require("sinon"); 2 | var mongo = require("../mongojs-hooks"); 3 | var mongojs = require("mongojs"); 4 | var config = require("./setup"); 5 | 6 | describe("hooks", function () { 7 | var db; 8 | 9 | before(function () { 10 | db = mongo.db("test"); 11 | }); 12 | 13 | describe("defining collection", function () { 14 | it("mongo.collection() with collection name should define a retrievable collection on db", function () { 15 | mongo.collection("tests"); 16 | 17 | db.collection("tests").should.be.ok; 18 | db.collection("tests")._name.should.equal(config.db + ".tests"); 19 | }); 20 | 21 | it("mongo.collection() with collection name should define a retrievable collection on tenant", function () { 22 | mongo.collection("tests"); 23 | 24 | db.tenant("tenant").collection("tests").should.be.ok; 25 | db.tenant("tenant").collection("tests")._name.should.equal(config.db + ".tenant.tests"); 26 | }); 27 | 28 | it("mongo.collection() with collection name should have pre(), post(), etc. hook functions", function () { 29 | var tests = mongo.collection("tests"); 30 | tests.should.have.property("pre"); 31 | tests.should.have.property("post"); 32 | tests.should.have.property("removePre"); 33 | tests.should.have.property("removePost"); 34 | }); 35 | }); 36 | 37 | describe("save()", function () { 38 | afterEach(function () { 39 | // Clean-out all hooks from previous tests 40 | mongo.reset(); 41 | }); 42 | 43 | describe("when single collection", function () { 44 | var saveStub; 45 | 46 | beforeEach(function () { 47 | saveStub = sinon.stub(mongojs.Collection.prototype, "save").callsArg(1); 48 | }); 49 | 50 | afterEach(function () { 51 | saveStub.restore(); 52 | }); 53 | 54 | it("with object should save that object to the database", function (done) { 55 | db.collection("tests").save({ test: "property" }, function () { 56 | mongojs.Collection.prototype.save.calledWith({ test: "property" }).should.be.ok; 57 | done(); 58 | }); 59 | }); 60 | 61 | it("on defined collection should save that object to the database", function (done) { 62 | mongo.collection("tests"); 63 | 64 | db.collection("tests").save({ test: "property" }, function () { 65 | mongojs.Collection.prototype.save.calledWith({ test: "property" }).should.be.ok; 66 | done(); 67 | }); 68 | }); 69 | 70 | it("with pre() hook on defined collection should call pre() hook before save()", function (done) { 71 | var tests = mongo.collection("tests"); 72 | var preStub = sinon.stub(); 73 | 74 | tests.pre("save", function (next, object, callback) { 75 | preStub.callCount.should.equal(0); 76 | preStub(); 77 | object.should.have.property("test"); 78 | object.test.should.equal("property"); 79 | 80 | next(object, callback); 81 | }); 82 | 83 | db.collection("tests").save({ test: "property" }, function () { 84 | preStub.callCount.should.equal(1); 85 | mongojs.Collection.prototype.save.calledWith({ test: "property" }).should.be.ok; 86 | 87 | done(); 88 | }); 89 | }); 90 | 91 | it("with post() hook on defined collection should call post() hook after save()", function (done) { 92 | var tests = mongo.collection("tests"); 93 | var postStub = sinon.stub(); 94 | 95 | tests.post("save", function (next) { 96 | mongojs.Collection.prototype.save.calledWith({ test: "property" }).should.be.ok; 97 | postStub(); 98 | next(); 99 | }); 100 | 101 | db.collection("tests").save({ test: "property" }, function () { 102 | postStub.callCount.should.equal(1); 103 | 104 | done(); 105 | }); 106 | }); 107 | 108 | it("when pre() and post() hooks added to different instances of same collection should call both hooks", function (done) { 109 | var preStub = sinon.stub(); 110 | var postStub = sinon.stub(); 111 | 112 | mongo.collection("tests").pre("save", function (next, object, callback) { 113 | preStub(); 114 | next(object, callback); 115 | }); 116 | 117 | mongo.collection("tests").post("save", function (next) { 118 | postStub(); 119 | next(); 120 | }); 121 | 122 | db.collection("tests").save({ test: "property" }, function () { 123 | preStub.callCount.should.equal(1); 124 | postStub.callCount.should.equal(1); 125 | 126 | done(); 127 | }); 128 | }); 129 | 130 | it("with pre() hook on defined collection and accessed via tenant should call pre() hook before save()", function (done) { 131 | var tests = mongo.collection("tests"); 132 | var preStub = sinon.stub(); 133 | 134 | tests.pre("save", function (next, object, callback) { 135 | preStub.callCount.should.equal(0); 136 | preStub(); 137 | object.should.have.property("test"); 138 | object.test.should.equal("property"); 139 | 140 | next(object, callback); 141 | }); 142 | 143 | db.tenant("tenant").collection("tests").save({ test: "property" }, function () { 144 | preStub.callCount.should.equal(1); 145 | mongojs.Collection.prototype.save.calledWith({ test: "property" }).should.be.ok; 146 | 147 | done(); 148 | }); 149 | }); 150 | }); 151 | 152 | describe("when multiple collections", function () { 153 | var saveStub; 154 | 155 | beforeEach(function () { 156 | saveStub = sinon.stub(mongojs.Collection.prototype, "save").callsArg(1); 157 | }); 158 | 159 | afterEach(function () { 160 | saveStub.restore(); 161 | }); 162 | 163 | it("with single defined collection and pre() hook called twice should call hook on collection twice", function (done) { 164 | var tests = mongo.collection("tests"); 165 | var preStub = sinon.stub(); 166 | 167 | tests.pre("save", function (next, object, callback) { 168 | preStub(); 169 | next(object, callback); 170 | }); 171 | 172 | db.collection("tests").save({ test1: "property1" }, function () { 173 | db.collection("tests").save({ test2: "property2" }, function () { 174 | preStub.callCount.should.equal(2); 175 | 176 | mongojs.Collection.prototype.save.callCount.should.equal(2); 177 | mongojs.Collection.prototype.save.getCall(0).calledWith({ test1: "property1" }).should.be.ok; 178 | mongojs.Collection.prototype.save.getCall(1).calledWith({ test2: "property2" }).should.be.ok; 179 | 180 | done(); 181 | }); 182 | }); 183 | }); 184 | 185 | it("with two defined collections and pre() hook one one should only call hook on the collection defined", function (done) { 186 | mongo.collection("test1"); 187 | var test2 = mongo.collection("test2"); 188 | var preStub = sinon.stub(); 189 | 190 | test2.pre("save", function (next, object, callback) { 191 | preStub.callCount.should.equal(0); 192 | preStub(); 193 | 194 | object.should.have.property("test2"); 195 | object.test2.should.equal("property2"); 196 | 197 | next(object, callback); 198 | }); 199 | 200 | db.collection("test1").save({ test1: "property1" }, function () { 201 | db.collection("test2").save({ test2: "property2" }, function () { 202 | // It's important that this is only 1 since the 2nd collection doesn't have a hook 203 | preStub.callCount.should.equal(1); 204 | 205 | mongojs.Collection.prototype.save.callCount.should.equal(2); 206 | mongojs.Collection.prototype.save.getCall(0).calledWith({ test1: "property1" }).should.be.ok; 207 | mongojs.Collection.prototype.save.getCall(1).calledWith({ test2: "property2" }).should.be.ok; 208 | 209 | done(); 210 | }); 211 | }); 212 | }); 213 | }); 214 | }); 215 | }); -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | var mongo = require("../mongojs-hooks"); 2 | 3 | describe("util", function () { 4 | describe("sanitise()", function () { 5 | describe("when null", function () { 6 | it("should return null", function () { 7 | should.not.exist(mongo.util.sanitise(null)); 8 | }); 9 | }); 10 | 11 | describe("when a string", function () { 12 | it("contains a '.' should replace with 'U+FF0E'", function () { 13 | mongo.util.sanitise("hey.there").should.equal("heyU+FF0Ethere"); 14 | }); 15 | }); 16 | 17 | describe("when an object", function () { 18 | it("contains plain keys should not change the key", function () { 19 | var object = { 20 | abcdefghiklmnopqrstuvwxyz: "just fine" 21 | }; 22 | 23 | mongo.util.sanitise(object).should.have.property("abcdefghiklmnopqrstuvwxyz"); 24 | }); 25 | 26 | it("with a key containing a '.' should replace with 'U+FF0E'", function () { 27 | var object = { 28 | "test.this": "not good" 29 | }; 30 | 31 | mongo.util.sanitise(object).should.have.property("testU+FF0Ethis"); 32 | }); 33 | 34 | it("with a key containing multiple '.' chars should replace all with 'U+FF0E'", function () { 35 | var object = { 36 | "test.this.another.here": "not good" 37 | }; 38 | 39 | mongo.util.sanitise(object).should.have.property("testU+FF0EthisU+FF0EanotherU+FF0Ehere"); 40 | }); 41 | 42 | it("with a key containing a '$' should replace with 'U+FF04'", function () { 43 | var object = { 44 | "test$this": "not good" 45 | }; 46 | 47 | mongo.util.sanitise(object).should.have.property("testU+FF04this"); 48 | }); 49 | 50 | it("with a key containing multiple '$' chars should replace all with 'U+FF04'", function () { 51 | var object = { 52 | "test$this$another$here": "not good" 53 | }; 54 | 55 | mongo.util.sanitise(object).should.have.property("testU+FF04thisU+FF04anotherU+FF04here"); 56 | }); 57 | 58 | it("with a key containing both '.' and '$' should replace them respectively", function () { 59 | var object = { 60 | "here's a $ and now a .": "what an unlikely key!" 61 | }; 62 | 63 | mongo.util.sanitise(object).should.have.property("here's a U+FF04 and now a U+FF0E"); 64 | }); 65 | 66 | it("with a sub-object key contains a '.' should replace with 'U+FF0E'", function () { 67 | var object = { 68 | "test.one": { 69 | "test.two": "sub-object" 70 | } 71 | }; 72 | 73 | var sanitised = mongo.util.sanitise(object); 74 | sanitised.should.have.property("testU+FF0Eone"); 75 | sanitised["testU+FF0Eone"].should.have.property("testU+FF0Etwo"); 76 | }); 77 | 78 | it("should not mutate original object", function () { 79 | var object = { 80 | "test.this": "not good" 81 | }; 82 | 83 | mongo.util.sanitise(object); 84 | object.should.not.have.property("testU+FF0Ethis"); 85 | object.should.have.property("test.this"); 86 | }); 87 | 88 | it("should skip object if it contains a _bsontype key", function () { 89 | // These are special Mongo objects so don't malform them 90 | var object = { 91 | _bsontype: "ObjectID", 92 | "test.this": "ignore" 93 | }; 94 | 95 | var sanitised = mongo.util.sanitise(object); 96 | sanitised.should.not.have.property("testU+FF0Ethis"); 97 | sanitised.should.have.property("test.this"); 98 | }); 99 | }); 100 | 101 | describe("when an Array", function () { 102 | it("should maintain all elements in the array", function () { 103 | var array = [ 104 | { key: "value" }, 105 | { second: "object" } 106 | ]; 107 | 108 | var sanitised = mongo.util.sanitise(array); 109 | (sanitised instanceof Array).should.be.true; 110 | sanitised.length.should.equal(2); 111 | sanitised.should.deep.equal(array); 112 | }); 113 | 114 | it("should sanitise keys in any elements", function () { 115 | var array = [ 116 | { "test.one": "value" }, 117 | { "test.two": "object" } 118 | ]; 119 | 120 | var sanitised = mongo.util.sanitise(array); 121 | sanitised[0].should.have.property("testU+FF0Eone"); 122 | sanitised[1].should.have.property("testU+FF0Etwo"); 123 | }); 124 | }); 125 | 126 | describe("when includes a Date", function () { 127 | it("should not alter date", function () { 128 | var date = new Date("2015-03-02"); 129 | 130 | var sanitised = mongo.util.sanitise({ date: date }); 131 | (sanitised.date instanceof Date).should.be.true; 132 | sanitised.date.should.deep.equal(new Date("2015-03-02")); 133 | }); 134 | }); 135 | }); 136 | 137 | describe("unsanitise()", function () { 138 | describe("when null", function () { 139 | it("should return null", function () { 140 | should.not.exist(mongo.util.unsanitise(null)); 141 | }); 142 | }); 143 | 144 | describe("when a string", function () { 145 | it("contains a '.' should replace with 'U+FF0E'", function () { 146 | mongo.util.unsanitise("heyU+FF0Ethere").should.equal("hey.there"); 147 | }); 148 | }); 149 | 150 | describe("when an object", function () { 151 | it("containing plain keys should not change the key", function () { 152 | var object = { 153 | abcdefghiklmnopqrstuvwxyz: "just fine" 154 | }; 155 | 156 | mongo.util.unsanitise(object).should.have.property("abcdefghiklmnopqrstuvwxyz"); 157 | }); 158 | 159 | it("with a key containing a 'U+FF0E' should replace with '.'", function () { 160 | var object = { 161 | "testU+FF0Ethis": "not good" 162 | }; 163 | 164 | mongo.util.unsanitise(object).should.have.property("test.this"); 165 | }); 166 | 167 | it("with a key containing multiple 'U+FF0E' codes should replace all with '.'", function () { 168 | var object = { 169 | "testU+FF0EthisU+FF0EanotherU+FF0Ehere": "not good" 170 | }; 171 | 172 | mongo.util.unsanitise(object).should.have.property("test.this.another.here"); 173 | }); 174 | 175 | it("with a key containing a 'U+FF04' should replace with '$'", function () { 176 | var object = { 177 | "testU+FF04this": "not good" 178 | }; 179 | 180 | mongo.util.unsanitise(object).should.have.property("test$this"); 181 | }); 182 | 183 | it("with a key containing multiple 'U+FF04' codes should replace all with '$'", function () { 184 | var object = { 185 | "testU+FF04thisU+FF04anotherU+FF04here": "not good" 186 | }; 187 | 188 | mongo.util.unsanitise(object).should.have.property("test$this$another$here"); 189 | }); 190 | 191 | it("with a key containing both 'U+FF0E' and 'U+FF04' should replace them respectively", function () { 192 | var object = { 193 | "here's a U+FF04 and now a U+FF0E": "what an unlikely key!" 194 | }; 195 | 196 | mongo.util.unsanitise(object).should.have.property("here's a $ and now a ."); 197 | }); 198 | 199 | it("with a sub-object key contains a 'U+FF0E' code should replace with '.'", function () { 200 | var object = { 201 | "testU+FF0Eone": { 202 | "testU+FF0Etwo": "sub-object" 203 | } 204 | }; 205 | 206 | var unsanitised = mongo.util.unsanitise(object); 207 | unsanitised.should.have.property("test.one"); 208 | unsanitised["test.one"].should.have.property("test.two"); 209 | }); 210 | 211 | it("should not mutate original object", function () { 212 | var object = { 213 | "testU+FF0Ethis": "not good" 214 | }; 215 | 216 | mongo.util.unsanitise(object); 217 | object.should.not.have.property("test.this"); 218 | object.should.have.property("testU+FF0Ethis"); 219 | }); 220 | 221 | it("should skip object if it contains a _bsontype key", function () { 222 | // These are special Mongo objects so don't malform them 223 | var object = { 224 | _bsontype: "ObjectID", 225 | "testU+FF0Ethis": "ignore" 226 | }; 227 | 228 | var unsanitised = mongo.util.unsanitise(object); 229 | unsanitised.should.not.have.property("test.this"); 230 | unsanitised.should.have.property("testU+FF0Ethis"); 231 | }); 232 | }); 233 | 234 | describe("when an Array", function () { 235 | it("should maintain all elements in the array", function () { 236 | var array = [ 237 | { key: "value" }, 238 | { second: "object" } 239 | ]; 240 | 241 | var unsanitised = mongo.util.unsanitise(array); 242 | (unsanitised instanceof Array).should.be.true; 243 | unsanitised.length.should.equal(2); 244 | unsanitised.should.deep.equal(array); 245 | }); 246 | 247 | it("should unsanitise keys in any elements", function () { 248 | var array = [ 249 | { "testU+FF0Eone": "value" }, 250 | { "testU+FF0Etwo": "object" } 251 | ]; 252 | 253 | var unsanitised = mongo.util.unsanitise(array); 254 | unsanitised[0].should.have.property("test.one"); 255 | unsanitised[1].should.have.property("test.two"); 256 | }); 257 | }); 258 | 259 | describe("when includes a Date", function () { 260 | it("should not alter date", function () { 261 | var date = new Date("2015-03-02"); 262 | 263 | var unsanitised = mongo.util.unsanitise({ date: date }); 264 | (unsanitised.date instanceof Date).should.be.true; 265 | unsanitised.date.should.deep.equal(new Date("2015-03-02")); 266 | }); 267 | }); 268 | }); 269 | 270 | describe("flatten()", function () { 271 | describe("when an object", function () { 272 | it("and nested", function () { 273 | var object = { 274 | first: { 275 | second: "value" 276 | } 277 | }; 278 | 279 | var flattened = mongo.util.flatten(object); 280 | flattened.should.deep.equal({ 281 | "first.second": "value" 282 | }); 283 | }); 284 | 285 | it("with multiple values", function () { 286 | var object = { 287 | first: { 288 | second: "value", 289 | third: "val" 290 | } 291 | }; 292 | 293 | var flattened = mongo.util.flatten(object); 294 | flattened.should.deep.equal({ 295 | "first.second": "value", 296 | "first.third": "val" 297 | }); 298 | }); 299 | 300 | it("and contains an Array", function () { 301 | var object = { 302 | first: { 303 | second: [ "el1", "el2", "el3" ] 304 | } 305 | }; 306 | 307 | var flattened = mongo.util.flatten(object); 308 | flattened.should.deep.equal({ 309 | "first.second": [ "el1", "el2", "el3" ] 310 | }); 311 | }); 312 | 313 | it("should not mutate original object", function () { 314 | var object = { 315 | first: { 316 | second: "value" 317 | } 318 | }; 319 | 320 | mongo.util.flatten(object); 321 | object.should.not.have.property("first.second"); 322 | object.should.deep.equal({ 323 | first: { 324 | second: "value" 325 | } 326 | }); 327 | }); 328 | }); 329 | }); 330 | }); --------------------------------------------------------------------------------