├── test ├── fixtures_with_subdir │ ├── extra │ │ └── .gitkeep │ └── archer.js ├── fixtures │ ├── southpark2.js │ ├── archer.js │ └── southpark.js ├── connect_load_close.test.js └── index.test.js ├── .gitignore ├── .travis.yml ├── bin └── mongofixtures ├── package.json ├── LICENSE ├── npm-shrinkwrap.json ├── README.md └── src └── index.js /test/fixtures_with_subdir/extra/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .npmrc 3 | 4 | # IDE 5 | .idea 6 | *.iml 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.10" 5 | - "iojs" 6 | services: 7 | - mongodb -------------------------------------------------------------------------------- /test/fixtures/southpark2.js: -------------------------------------------------------------------------------- 1 | exports.southpark = { 2 | stan: { name: 'Stan' }, 3 | towelie: { name: 'Towelie' } 4 | }; 5 | -------------------------------------------------------------------------------- /test/fixtures/archer.js: -------------------------------------------------------------------------------- 1 | exports.archer = [ 2 | { name: 'Sterling' }, 3 | { name: 'Lana' }, 4 | { name: 'Cheryl' } 5 | ]; 6 | -------------------------------------------------------------------------------- /test/fixtures/southpark.js: -------------------------------------------------------------------------------- 1 | exports.southpark = [ 2 | { name: 'Eric' }, 3 | { name: 'Butters' }, 4 | { name: 'Kenny' } 5 | ]; 6 | -------------------------------------------------------------------------------- /test/fixtures_with_subdir/archer.js: -------------------------------------------------------------------------------- 1 | exports.archer = [ 2 | { name: 'Sterling' }, 3 | { name: 'Lana' }, 4 | { name: 'Cheryl' } 5 | ]; 6 | -------------------------------------------------------------------------------- /test/connect_load_close.test.js: -------------------------------------------------------------------------------- 1 | var fixtures = require('../src/index.js'), 2 | dbName = 'pow-mongodb-fixtures-test'; 3 | 4 | exports['closeDb'] = function(test) { 5 | var db = fixtures.connect(dbName); 6 | db.load({}, function(){ 7 | db.close(function() { 8 | test.done(); 9 | }); 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /bin/mongofixtures: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var argv = require('optimist').argv, 4 | path = require('path'); 5 | 6 | var dbName = argv._[0], 7 | file = path.resolve(process.cwd(), argv._[1]); 8 | 9 | var fixtures = require('pow-mongodb-fixtures').connect(dbName); 10 | 11 | fixtures.load(file, function(err) { 12 | if (err) { 13 | console.error(err); 14 | return process.exit(-1); 15 | } 16 | 17 | process.exit(0); 18 | }); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Charles Davison ", 3 | "name": "pow-mongodb-fixtures", 4 | "description": "Easy JSON fixture loading for MongoDB. Makes managing document relationships easier.", 5 | "version": "0.13.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "http://github.com/powmedia/pow-mongodb-fixtures.git" 9 | }, 10 | "bin": { 11 | "mongofixtures": "./bin/mongofixtures" 12 | }, 13 | "engines": { 14 | "node": ">= 0.4.1" 15 | }, 16 | "dependencies": { 17 | "async": "0.1.15", 18 | "bson": ">=0.0.4", 19 | "mongodb": "~2.0.36", 20 | "nodeunit": "^0.9.1", 21 | "optimist": "0.3.5", 22 | "lodash": "~3.10.0" 23 | }, 24 | "devDependencies": { 25 | "nodeunit": "^0.9.1" 26 | }, 27 | "main": "src/index", 28 | "scripts": { 29 | "test": "node_modules/.bin/nodeunit test" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Charles Davison 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pow-mongodb-fixtures", 3 | "version": "0.11.0", 4 | "dependencies": { 5 | "async": { 6 | "version": "0.1.22", 7 | "from": "async@>=0.1.15 <0.2.0", 8 | "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz" 9 | }, 10 | "mongodb": { 11 | "version": "2.0.34", 12 | "from": "mongodb@2.0.34", 13 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.0.34.tgz", 14 | "dependencies": { 15 | "mongodb-core": { 16 | "version": "1.2.0", 17 | "from": "mongodb-core@1.2.0", 18 | "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-1.2.0.tgz", 19 | "dependencies": { 20 | "bson": { 21 | "version": "0.4.6", 22 | "from": "bson@>=0.4.0 <0.5.0", 23 | "resolved": "https://registry.npmjs.org/bson/-/bson-0.4.6.tgz" 24 | }, 25 | "kerberos": { 26 | "version": "0.0.12", 27 | "from": "kerberos@>=0.0.0 <0.1.0", 28 | "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.12.tgz", 29 | "dependencies": { 30 | "nan": { 31 | "version": "1.8.4", 32 | "from": "nan@>=1.8.0 <1.9.0", 33 | "resolved": "https://registry.npmjs.org/nan/-/nan-1.8.4.tgz" 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | "readable-stream": { 40 | "version": "1.0.31", 41 | "from": "readable-stream@1.0.31", 42 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz", 43 | "dependencies": { 44 | "core-util-is": { 45 | "version": "1.0.1", 46 | "from": "core-util-is@>=1.0.0 <1.1.0", 47 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz" 48 | }, 49 | "isarray": { 50 | "version": "0.0.1", 51 | "from": "isarray@0.0.1", 52 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" 53 | }, 54 | "string_decoder": { 55 | "version": "0.10.31", 56 | "from": "string_decoder@>=0.10.0 <0.11.0", 57 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" 58 | }, 59 | "inherits": { 60 | "version": "2.0.1", 61 | "from": "inherits@>=2.0.1 <2.1.0", 62 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" 63 | } 64 | } 65 | } 66 | } 67 | }, 68 | "optimist": { 69 | "version": "0.3.7", 70 | "from": "optimist@>=0.3.5 <0.4.0", 71 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", 72 | "dependencies": { 73 | "wordwrap": { 74 | "version": "0.0.3", 75 | "from": "wordwrap@>=0.0.2 <0.1.0", 76 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" 77 | } 78 | } 79 | }, 80 | "underscore": { 81 | "version": "1.8.3", 82 | "from": "underscore@>=1.8.3 <2.0.0", 83 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz" 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/powmedia/pow-mongodb-fixtures.svg?branch=master)](https://travis-ci.org/powmedia/pow-mongodb-fixtures) 2 | 3 | pow-mongodb-fixtures 4 | ================= 5 | 6 | Simple fixture loader for MongoDB on NodeJS. Makes managing relationships between documents easier. 7 | 8 | Fixtures can be in one file, or divided up into separate files for organisation 9 | (e.g. one file per model) 10 | 11 | The fixture files must export objects which are keyed by the MongoDB collection name, each 12 | containing the data for documents within that. 13 | 14 | FOR EXAMPLE: 15 | With the file below, 3 documents will be inserted into the 'users' collection and 2 into the 'businesses' collection: 16 | 17 | //fixtures.js 18 | exports.users = [ 19 | { name: 'Gob' }, 20 | { name: 'Buster' }, 21 | { name: 'Steve Holt' } 22 | ]; 23 | 24 | exports.businesses = [ 25 | { name: 'The Banana Stand' }, 26 | { name: 'Bluth Homes' } 27 | ]; 28 | 29 | 30 | You can also load fixtures as an object where each document is keyed, in case you want to reference another document. This example uses the included `createObjectId` helper: 31 | 32 | //users.js 33 | var id = require('pow-mongodb-fixtures').createObjectId; 34 | 35 | var users = exports.users = { 36 | user1: { 37 | _id: id(), 38 | name: 'Michael' 39 | }, 40 | user2: { 41 | _id: id(), 42 | name: 'George Michael', 43 | father: users.user1._id 44 | }, 45 | user3: { 46 | _id: id('4ed2b809d7446b9a0e000014'), 47 | name: 'Tobias' 48 | } 49 | } 50 | 51 | 52 | CLI usage 53 | ========= 54 | 55 | A CLI program is included for quickly loading fixture files. To use it install the module globally: 56 | 57 | npm install pow-mongodb-fixtures -g 58 | 59 | Then use the program to install a file or directory: 60 | 61 | mongofixtures 62 | 63 | mongofixtures appdb fixtures/users.js 64 | 65 | 66 | API 67 | === 68 | 69 | connect(dbname, options) 70 | ------------------------ 71 | 72 | Returns a new Loader instance, configured to interact with a certain database. 73 | 74 | Options: 75 | 76 | - host (Default: localhost) 77 | - port (Default: 27017) 78 | - user 79 | - pass 80 | - safe (Default: false) 81 | 82 | Usage: 83 | 84 | var fixtures = require('pow-mongodb-fixtures').connect('dbname'); 85 | 86 | var fixtures2 = require('pow-mongodb-fixtures').connect('dbname', { 87 | host: 'http://dbhost.com/', 88 | port: 1234 89 | }); 90 | 91 | 92 | load(data, callback) 93 | -------------------- 94 | 95 | Adds documents to the relevant collection. If the collection doesn't exist it will be created first. 96 | 97 | var fixtures = require('pow-mongodb-fixtures').connect('mydb'); 98 | 99 | //Objects 100 | fixtures.load({ 101 | users: [ 102 | { name: 'Maeby' }, 103 | { name: 'George Michael' } 104 | ] 105 | }, callback); 106 | 107 | //Files 108 | fixtures.load(__dirname + '/fixtures/users.js', cb); 109 | 110 | //Directories (loads all files in the directory) 111 | fixtures.load(__dirname + '/fixtures', callback); 112 | 113 | 114 | clear(callback) 115 | --------------- 116 | 117 | Clears existing data. 118 | 119 | fixtures.clear(function(err) { 120 | //Drops the database 121 | }); 122 | 123 | fixtures.clear('foo', function(err) { 124 | //Clears the 'foo' collection 125 | }); 126 | 127 | fixtures.clear(['foo', 'bar'], function(err) { 128 | //Clears the 'foo' and 'bar' collections 129 | }); 130 | 131 | 132 | clearAllAndLoad(data, callback) 133 | ---------------------------- 134 | 135 | Drops the database (clear all collections) and loads data. 136 | 137 | 138 | clearAndLoad(data, callback) 139 | ---------------------------- 140 | 141 | Clears the collections that have documents in the `data` that is passed in, and then loads data. 142 | 143 | var data = { users: [...] }; 144 | 145 | fixtures.clearAndLoad(data, function(err) { 146 | //Clears only the 'users' collection then loads data 147 | }); 148 | 149 | 150 | addModifier(callback) 151 | ---------------------------- 152 | 153 | Adds a modifier (function) which gets called for each document that is to be inserted. The signature of this function 154 | should be: 155 | 156 | (collectionName, document, callback) 157 | 158 | * collectionName - name of collection 159 | * document - the document which is to be inserted 160 | * callback - function with signature (err, modifiedDocument). This should be called with the modified document. 161 | 162 | Modifiers are chained in the order in which they're added. For example: 163 | 164 | 165 | var data = { users: [...] }; 166 | 167 | // this modifier will get called first 168 | fixtures.addModifier(function(collectionName, doc, cb) { 169 | doc.createdAt = new Date(); 170 | 171 | cb(null, doc); 172 | }); 173 | 174 | // this modifier will get called second with the result from the first modifier call 175 | fixtures.addModifier(function(collectionName, doc, cb) { 176 | doc.updatedAt = new Date(); 177 | 178 | cb(null, doc); 179 | }); 180 | 181 | fixtures.load(data, function(err) { 182 | // each loaded data item will have the createdAt and updatedAt keys set. 183 | }); 184 | 185 | 186 | Installation 187 | ------------ 188 | 189 | npm install pow-mongodb-fixtures 190 | 191 | 192 | Changelog 193 | --------- 194 | 195 | ###0.13.0 196 | - Update mongodb driver to 2.0.x 197 | - Updated `collection.insert` with `collection.insertMany` - the former is marked for deprecation in version 3.x 198 | - Move to Lo-Dash from Underscore 199 | 200 | ###0.10.0 201 | - Update mongodb driver to 1.3.x 202 | - Add ability to connect with URI 203 | - Make safe mode the default 204 | 205 | ###0.8.1 206 | - Add mongofixtures CLI program 207 | 208 | ###0.7.1 209 | - Add 'safe' option (donnut) 210 | 211 | ###0.7.0 212 | - Add user and password options for connecting to authenticated/remote DBs 213 | 214 | ###0.6.4 215 | - Add username and password connect options 216 | 217 | ###0.6.3 218 | - Make clear be safe 219 | 220 | ###0.6.2 221 | - Windows fixes (samitny) 222 | 223 | ###0.6.1 224 | - Ignore subdirectories (hiddentao) 225 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | //Dependencies 2 | var fs = require('fs'), 3 | url = require('url'), 4 | path = require('path'), 5 | mongo = require('mongodb'), 6 | ObjectID = mongo.ObjectId, 7 | async = require('async'), 8 | _ = require('lodash'), 9 | basePath = path.dirname(module.parent.filename); 10 | 11 | 12 | /** 13 | * Helper function that creates a MongoDB ObjectID given a hex string 14 | * @param {String|ObjectId} Optional hard-coded Object ID as string 15 | */ 16 | exports.createObjectId = function(id) { 17 | if (!id) return new ObjectID(); 18 | 19 | //Allow cloning ObjectIDs 20 | if (id.constructor.name == 'ObjectID') id = id.toString(); 21 | 22 | return new ObjectID(id); 23 | }; 24 | 25 | 26 | 27 | /** 28 | * Main method for connecting to the database and returning the fixture loader (Loader) 29 | * 30 | * @param {String} dbOrUri Database name or connection URI 31 | * @param {Object} [options] Connection options: host ('localhost'), port (27017) 32 | */ 33 | exports.connect = function(db, options) { 34 | return new Loader(db, options); 35 | } 36 | 37 | 38 | 39 | /** 40 | * Loader constructor 41 | * 42 | * @param {String} dbOrUri Database name or connection URI 43 | * @param {Object} [options] Connection options 44 | * @param {String} [options.host] Default: 'localhost' 45 | * @param {Number} [options.port] Default: 27017 46 | * @param {String} [options.user] Username 47 | * @param {String} [options.pass] Password 48 | * @param {Boolean} [options.safe] Default: false 49 | */ 50 | var Loader = exports.Loader = function(dbOrUri, options) { 51 | //Try parsing uri 52 | var parts = url.parse(dbOrUri); 53 | 54 | //Using connection URI 55 | if (parts.protocol) { 56 | options = _.extend({ 57 | db: parts.path.replace('/', ''), 58 | host: parts.hostname, 59 | port: parseInt(parts.port, 10), 60 | user: parts.auth ? parts.auth.split(':')[0] : null, 61 | pass: parts.auth ? parts.auth.split(':')[1] : null, 62 | safe: true 63 | }, options); 64 | } 65 | 66 | //Using DB name 67 | else { 68 | options = _.extend({ 69 | db: dbOrUri, 70 | host: 'localhost', 71 | port: 27017, 72 | user: null, 73 | pass: null, 74 | safe: true 75 | }, options); 76 | } 77 | 78 | this.options = options; 79 | this.modifiers = []; 80 | }; 81 | 82 | 83 | /** 84 | * Inserts data 85 | * 86 | * @param {Mixed} The data to load. This parameter accepts either: 87 | * String: Path to a file or directory to load 88 | * Object: Object literal in the form described in docs 89 | * @param {Function} Callback(err) 90 | */ 91 | Loader.prototype.load = function(fixtures, cb) { 92 | var self = this; 93 | 94 | _mixedToObject(fixtures, function(err, data) { 95 | if (err) return cb(err); 96 | 97 | _loadData(self, data, cb); 98 | }); 99 | }; 100 | 101 | 102 | 103 | /** 104 | * Add a modifier function. 105 | * 106 | * Modifier functions get called (in the order in which they were added) for each document, prior to it being loaded. 107 | * The result from each modifier is fed into the next modifier as its input, and so on until the final result which is 108 | * then inserted into the db. 109 | * 110 | * @param {Function} cb The modifier callback function with signature (collectionName, document, callback). 111 | */ 112 | Loader.prototype.addModifier = function(cb) { 113 | this.modifiers.push(cb); 114 | }; 115 | 116 | 117 | 118 | /** 119 | * loader.clear(cb) : Clears all collections 120 | * 121 | * loader.clear(collectionNames, cb) : Clears only the given collection(s) 122 | * 123 | * @param {String|Array} Optional. Name of collection to clear or an array of collection names 124 | * @param {Function} Callback(err) 125 | */ 126 | Loader.prototype.clear = function(collectionNames, cb) { 127 | //Normalise arguments 128 | if (arguments.length == 1) { //cb 129 | cb = collectionNames; 130 | collectionNames = null; 131 | } 132 | 133 | var self = this; 134 | 135 | var results = {}; 136 | 137 | async.series([ 138 | function connect(cb) { 139 | _connect(self, function(err, db) { 140 | if (err) return cb(err); 141 | 142 | results.db = db; 143 | cb(); 144 | }) 145 | }, 146 | 147 | function getCollectionNames(cb) { 148 | //If collectionNames not passed, clear all of them 149 | if (!collectionNames) { 150 | results.db.listCollections().toArray(function(err, names) { 151 | if (err) return cb(err); 152 | 153 | //Get the real collection names 154 | names = _.map(names, function(nameObj) { 155 | var fullName = nameObj.name, 156 | parts = fullName.split('.'); 157 | 158 | //Skip system collections 159 | if (parts[0] == 'system' || parts[0] == 'local') return; 160 | 161 | return parts.join('.'); 162 | }); 163 | 164 | results.collectionNames = _.compact(names); 165 | 166 | cb(); 167 | }) 168 | } else { 169 | //Convert single collection as string to array 170 | if (!_.isArray(collectionNames)) collectionNames = [collectionNames]; 171 | 172 | async.map(collectionNames, function (collectionName, cbForEachCollection) { 173 | results.db.listCollections({ name: collectionName }).toArray(cbForEachCollection); 174 | }, function (err, result) { 175 | if (err) { return cb(err); } 176 | 177 | result = _.flatten(result); 178 | 179 | if (_.isEmpty(result)) { 180 | results.collectionNames = null; 181 | return cb(); 182 | } 183 | 184 | results.collectionNames = collectionNames; 185 | cb(); 186 | }); 187 | } 188 | }, 189 | 190 | function clearCollections() { 191 | if (results.collectionNames) { 192 | async.forEach(results.collectionNames, function(name, cb) { 193 | var collection = results.db.collection(name); 194 | collection.remove({},null,cb); 195 | }, cb); 196 | } else { cb(); } 197 | } 198 | ], cb) 199 | }; 200 | 201 | 202 | /** 203 | * Clears all collections and inserts data 204 | * 205 | * @param {Mixed} The data to load. This parameter accepts either: 206 | * String: Path to a file or directory to load 207 | * Object: Object literal in the form described in docs 208 | * @param {Function} Callback(err) 209 | */ 210 | Loader.prototype.clearAllAndLoad = function(fixtures, cb) { 211 | var self = this; 212 | 213 | self.clear(function(err) { 214 | if (err) return cb(err); 215 | 216 | self.load(fixtures, function(err) { 217 | cb(err); 218 | }); 219 | }); 220 | }; 221 | 222 | 223 | /** 224 | * Clears only the collections that have documents to be inserted, then inserts data 225 | * 226 | * @param {Mixed} The data to load. This parameter accepts either: 227 | * String: Path to a file or directory to load 228 | * Object: Object literal in the form described in docs 229 | * @param {Function} Callback(err) 230 | */ 231 | Loader.prototype.clearAndLoad = function(fixtures, cb) { 232 | var self = this; 233 | 234 | _mixedToObject(fixtures, function(err, objData) { 235 | if (err) return cb(err); 236 | 237 | var collections = Object.keys(objData); 238 | 239 | self.clear(collections, function(err) { 240 | if (err) return cb(err); 241 | 242 | _loadData(self, objData, cb); 243 | }); 244 | }); 245 | }; 246 | 247 | /** 248 | * Close the connection to the DB 249 | * 250 | * @param {Function} Callback(err) 251 | */ 252 | Loader.prototype.close = function(cb) { 253 | var self = this; 254 | 255 | _close(self, function (err) { 256 | if (err) return cb(err); 257 | cb(); 258 | }); 259 | }; 260 | 261 | 262 | //PRIVATE METHODS 263 | 264 | var noop = function() {}; 265 | 266 | /** 267 | * Connects to the database and returns the client. If a connection has already been established it is used. 268 | * 269 | * @param {Loader} The configured loader 270 | * @param {Function} Callback(err, client) 271 | */ 272 | var _connect = function(loader, cb) { 273 | if (loader.client) return cb(null, loader.client); 274 | 275 | var options = loader.options; 276 | 277 | var db = new mongo.Db(options.db, new mongo.Server(options.host, options.port, {}), {safe: options.safe}); 278 | 279 | db.open(function(err, db) { 280 | if (err) return cb(err); 281 | 282 | loader.client = db; 283 | 284 | //Authenticate if required 285 | if (!options.user) return cb(null, db); 286 | 287 | db.authenticate(options.user, options.pass, function(err, result) { 288 | if (err) return cb(err); 289 | 290 | cb(null, db); 291 | }); 292 | }); 293 | }; 294 | 295 | /** 296 | * Close the connection to the database, if it exists 297 | * 298 | * @param {Function} Callback(err) 299 | */ 300 | 301 | var _close = function(loader, cb) { 302 | var db = loader.client; 303 | if (db) { 304 | db.close(function (err, results) { 305 | if (err) return cb(err); 306 | cb(null); 307 | }); 308 | } else { 309 | cb(new Error("No connection found!")); 310 | } 311 | }; 312 | 313 | /** 314 | * Inserts the given data (object or array) as new documents 315 | * 316 | * @param {Loader} The configured loader 317 | * @param {Object|Array} The data to load 318 | * @param {Function} Callback(err) 319 | * @api private 320 | */ 321 | var _loadData = function(loader, data, cb) { 322 | cb = cb || noop; 323 | 324 | var collectionNames = Object.keys(data); 325 | 326 | _connect(loader, function(err, db) { 327 | if (err) return cb(err); 328 | 329 | async.forEach(collectionNames, function(collectionName, cbForEachCollection) { 330 | var collectionData = data[collectionName]; 331 | 332 | //Convert object to array 333 | var items; 334 | if (Array.isArray(collectionData)) { 335 | items = collectionData.slice(); 336 | } else { 337 | items = _.values(collectionData); 338 | } 339 | 340 | var modifiedItems = []; 341 | 342 | async.forEach(items, function(item, cbForEachItem) { 343 | // apply modifiers 344 | async.forEach(loader.modifiers, function(modifier, cbForEachModifier) { 345 | modifier.call(modifier, collectionName, item, function(err, modifiedDoc) { 346 | if (err) return cbForEachModifier(err); 347 | 348 | item = modifiedDoc; 349 | 350 | cbForEachModifier(); 351 | }); 352 | }, function(err) { 353 | if (err) return cbForEachItem(err); 354 | 355 | modifiedItems.push(item); 356 | 357 | cbForEachItem(); 358 | }); 359 | }, function(err) { 360 | if (err) return cbForEachCollection(err); 361 | 362 | db.collection(collectionName, function(err, collection) { 363 | if (err) return cbForEachCollection(err); 364 | 365 | collection.insertMany(modifiedItems, { safe: true }, cbForEachCollection); 366 | }); 367 | }); 368 | }, cb); 369 | }); 370 | }; 371 | 372 | 373 | /** 374 | * Determine the type of fixtures being passed in (object, array, file, directory) and return 375 | * an object keyed by collection name. 376 | * 377 | * @param {Object|String} Fixture data (object, filename or dirname) 378 | * @param {Function} Optional callback(err, data) 379 | * @api private 380 | */ 381 | var _mixedToObject = function(fixtures, cb) { 382 | if (typeof fixtures == 'object') return cb(null, fixtures); 383 | 384 | //As it's not an object, it should now be a file or directory path (string) 385 | if (typeof fixtures != 'string') { 386 | return cb(new Error('Data must be an object, array or string (file or dir path)')); 387 | } 388 | 389 | // Resolve relative paths if necessary. 390 | fixtures = path.resolve(basePath, fixtures); 391 | 392 | //Determine if fixtures is pointing to a file or directory 393 | fs.stat(fixtures, function(err, stats) { 394 | if (err) return cb(err); 395 | 396 | if (stats.isDirectory()) { 397 | _dirToObject(fixtures, cb); 398 | } else { //File 399 | _fileToObject(fixtures, cb); 400 | } 401 | }); 402 | } 403 | 404 | 405 | /** 406 | * Get data from one file as an object 407 | * 408 | * @param {String} The full path to the file to load 409 | * @param {Function} Optional callback(err, data) 410 | * @api private 411 | */ 412 | var _fileToObject = function(file, cb) { 413 | cb = cb || noop; 414 | 415 | // Resolve relative paths if necessary. 416 | file = path.resolve(basePath, file); 417 | 418 | var data = require(file); 419 | 420 | cb(null, data); 421 | } 422 | 423 | 424 | /** 425 | * Get and compile data from all files in a directory, as an object 426 | * 427 | * @param {String} The directory path to load e.g. 'data/fixtures' or '../data' 428 | * @param {Function} Optional callback(err) 429 | * @api private 430 | */ 431 | var _dirToObject = function(dir, cb) { 432 | cb = cb || noop; 433 | 434 | // Resolve relative paths if necessary. 435 | dir = path.resolve(basePath, dir); 436 | 437 | async.waterfall([ 438 | function readDir(cb) { 439 | fs.readdir(dir, cb) 440 | }, 441 | 442 | function filesToObjects(files, cb) { 443 | async.map(files, function processFile(file, cb) { 444 | var path = dir + '/' + file; 445 | 446 | // Determine if it's a file or directory 447 | fs.stat(path, function(err, stats) { 448 | if (err) return cb(err); 449 | 450 | if (stats.isDirectory()) { 451 | cb(null, {}); 452 | } else { //File 453 | _fileToObject(path, cb); 454 | } 455 | }); 456 | }, cb); 457 | }, 458 | 459 | function combineObjects(results, cb) { 460 | //Where all combined data will be kept, keyed by collection name 461 | var collections = {}; 462 | 463 | results.forEach(function(fileObj) { 464 | _.each(fileObj, function(docs, name) { 465 | //Convert objects to array 466 | if (_.isObject(docs)) { 467 | docs = _.values(docs); 468 | } 469 | 470 | //Create array for collection if it doesn't exist yet 471 | if (!collections[name]) collections[name] = []; 472 | 473 | //Add docs to collection 474 | collections[name] = collections[name].concat(docs); 475 | }); 476 | }); 477 | 478 | cb(null, collections) 479 | } 480 | ], function(err, combinedData) { 481 | if (err) return cb(err); 482 | 483 | cb(null, combinedData); 484 | }); 485 | }; 486 | 487 | 488 | /** 489 | * Builds the full connection URI 490 | * 491 | * @param {Object} options 492 | * 493 | * @return {String} 494 | */ 495 | var _buildConnectionUri = function(options) { 496 | var parts = ['mongodb://']; 497 | 498 | if (options.user) parts.push(options.user); 499 | 500 | if (options.pass) { 501 | parts.push(':'); 502 | parts.push(options.pass); 503 | } 504 | 505 | if (options.user) { 506 | parts.push('@'); 507 | } 508 | 509 | parts.push(options.host); 510 | parts.push(':'); 511 | parts.push(options.port); 512 | parts.push('/'); 513 | parts.push(options.db); 514 | 515 | return parts.join(''); 516 | } 517 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | //Nodeunit tests 2 | 3 | var fixtures = require('../src/index.js'), 4 | id = fixtures.createObjectId, 5 | mongo = require('mongodb'), 6 | fs = require('fs'), 7 | async = require('async'), 8 | _ = require('underscore'); 9 | 10 | var dbName = 'pow-mongodb-fixtures-test', 11 | loader = fixtures.connect(dbName), 12 | server = new mongo.Db(dbName, new mongo.Server('127.0.0.1', 27017, {})), 13 | db; 14 | 15 | 16 | function loadCollection(name, cb) { 17 | db.collection(name, function(err, collection) { 18 | if (err) return cb(err); 19 | 20 | collection.find(function(err, cursor) { 21 | if (err) return cb(err); 22 | 23 | cursor.toArray(function(err, docs) { 24 | if (err) return cb(err); 25 | 26 | cb(null, docs); 27 | }); 28 | }); 29 | }); 30 | } 31 | 32 | exports['init'] = function(test) { 33 | server.open(function(err, openDb) { 34 | if (err) return test.done(err); 35 | 36 | db = openDb; 37 | 38 | db.dropDatabase(test.done); 39 | }); 40 | }; 41 | 42 | 43 | exports['createObjectId'] = { 44 | 'with ID as string': function(test) { 45 | var objId = id('4eca80fae4af59f55d000020'); 46 | 47 | test.same(objId.constructor.name, 'ObjectID'); 48 | test.same(objId.toString(), '4eca80fae4af59f55d000020'); 49 | 50 | test.done(); 51 | }, 52 | 53 | 'with existing ID': function(test) { 54 | var id1 = id(); 55 | 56 | var id2 = id(id1); 57 | 58 | test.same(id2.constructor.name, 'ObjectID'); 59 | test.same(id2.toString(), id1.toString()); 60 | 61 | test.done(); 62 | }, 63 | 64 | 'without ID': function(test) { 65 | var objId = id(); 66 | 67 | test.same(objId.constructor.name, 'ObjectID'); 68 | test.same(objId.toString().length, 24); 69 | 70 | test.done(); 71 | } 72 | }; 73 | 74 | 75 | exports['connect with dbName'] = function(test) { 76 | var loader = fixtures.connect(dbName); 77 | 78 | var options = loader.options; 79 | 80 | test.same(options.db, 'pow-mongodb-fixtures-test'); 81 | test.same(options.host, 'localhost'); 82 | test.same(options.port, 27017); 83 | test.same(options.user, null); 84 | test.same(options.pass, null); 85 | test.same(options.safe, true); 86 | 87 | test.done(); 88 | } 89 | 90 | 91 | exports['connect with uri'] = function(test) { 92 | var loader = fixtures.connect('mongodb://username:password@example.com:9191/dbname'); 93 | 94 | var options = loader.options; 95 | 96 | test.same(options.db, 'dbname'); 97 | test.same(options.host, 'example.com'); 98 | test.same(options.port, 9191); 99 | test.same(options.user, 'username'); 100 | test.same(options.pass, 'password'); 101 | test.same(options.safe, true); 102 | 103 | test.done(); 104 | } 105 | 106 | 107 | exports['works when referencing an objectID in different scope'] = { 108 | 'when using this': function(test) { 109 | var todo = 'TODO: Havent been able to replicate error yet:'; 110 | todo += 'Sometimes when referencing an ID that was generated elsewhere, it gets encoded incorrectly.'; 111 | todo += 'Test needs to fail first so it can be fixed.'; 112 | 113 | console.log(todo); 114 | return test.done(); 115 | 116 | var self = this; 117 | 118 | var data = {}; 119 | 120 | var users = data.users = { 121 | sterling: { 122 | _id: id(), 123 | name: 'Sterling' 124 | } 125 | }; 126 | 127 | var posts = data.posts = [ 128 | { 129 | _id: id(), 130 | author: users.sterling._id, 131 | text: 'Danger Zone!' 132 | } 133 | ]; 134 | 135 | loader.load(data, function(err) { 136 | if (err) return test.done(err); 137 | 138 | //TODO: Try to replicate problem from before. Maybe have to load setup fixture into DB 139 | test.same(users.sterling._id.toString(), posts[0].author.toString()); 140 | 141 | process.exit(); 142 | test.done(); 143 | }); 144 | } 145 | }; 146 | 147 | 148 | exports['add modifier'] = function(test) { 149 | var l = fixtures.connect(dbName); 150 | 151 | test.same([], l.modifiers); 152 | 153 | var modifier = function(col, doc, cb) { cb(null) }; 154 | 155 | l.addModifier(modifier); 156 | 157 | test.same([modifier], l.modifiers); 158 | 159 | test.done(); 160 | }; 161 | 162 | 163 | exports['load'] = { 164 | setUp: function(done) { 165 | db.dropDatabase(done); 166 | }, 167 | 168 | 'array': function(test) { 169 | test.expect(2); 170 | 171 | var data = { 172 | southpark: [ 173 | { name: 'Eric' }, 174 | { name: 'Butters' }, 175 | { name: 'Kenny' } 176 | ], 177 | boredToDeath: [ 178 | { name: 'Jonathan' }, 179 | { name: 'Ray' }, 180 | { name: 'George' } 181 | ] 182 | }; 183 | 184 | loader.load(data, function(err) { 185 | if (err) return test.done(err); 186 | 187 | async.parallel([ 188 | function(next) { 189 | loadCollection('southpark', function(err, docs) { 190 | if (err) return next(err); 191 | 192 | var names = _.pluck(docs, 'name'); 193 | 194 | test.same(names.sort(), ['Eric', 'Butters', 'Kenny'].sort()); 195 | 196 | next(); 197 | }); 198 | }, 199 | function(next) { 200 | loadCollection('boredToDeath', function(err, docs) { 201 | if (err) return next(err); 202 | 203 | var names = _.pluck(docs, 'name'); 204 | 205 | test.same(names.sort(), ['Jonathan', 'Ray', 'George'].sort()); 206 | 207 | next(); 208 | }); 209 | } 210 | ], test.done); 211 | }); 212 | }, 213 | 214 | 'object': function(test) { 215 | test.expect(2); 216 | 217 | var data = { 218 | southpark: { 219 | eric: { name: 'Eric' }, 220 | butters: { name: 'Butters' }, 221 | kenny: { name: 'Kenny' } 222 | }, 223 | boredToDeath: { 224 | jonathan: { name: 'Jonathan' }, 225 | ray: { name: 'Ray' }, 226 | george: { name: 'George' } 227 | } 228 | }; 229 | 230 | loader.load(data, function(err) { 231 | if (err) return test.done(err); 232 | 233 | async.parallel([ 234 | function(next) { 235 | loadCollection('southpark', function(err, docs) { 236 | if (err) return next(err); 237 | 238 | var names = _.pluck(docs, 'name'); 239 | 240 | test.same(names.sort(), ['Eric', 'Butters', 'Kenny'].sort()); 241 | 242 | next(); 243 | }); 244 | }, 245 | function(next) { 246 | loadCollection('boredToDeath', function(err, docs) { 247 | if (err) return next(err); 248 | 249 | var names = _.pluck(docs, 'name'); 250 | 251 | test.same(names.sort(), ['Jonathan', 'Ray', 'George'].sort()); 252 | 253 | next(); 254 | }); 255 | } 256 | ], test.done); 257 | }); 258 | }, 259 | 260 | 'file': function(test) { 261 | loader.load('./fixtures/archer.js', function(err) { 262 | if (err) return test.done(err); 263 | 264 | loadCollection('archer', function(err, docs) { 265 | if (err) return next(err); 266 | 267 | var names = _.pluck(docs, 'name'); 268 | 269 | test.same(names.sort(), ['Sterling', 'Lana', 'Cheryl'].sort()); 270 | 271 | test.done(); 272 | }); 273 | }); 274 | }, 275 | 276 | 'directory': { 277 | 'default' : function(test) { 278 | loader.load('./fixtures', function(err) { 279 | if (err) return test.done(err); 280 | 281 | async.parallel([ 282 | function(next) { 283 | loadCollection('archer', function(err, docs) { 284 | if (err) return next(err); 285 | 286 | var names = _.pluck(docs, 'name'); 287 | 288 | test.same(names.sort(), ['Sterling', 'Lana', 'Cheryl'].sort()); 289 | 290 | next(); 291 | }); 292 | }, 293 | function(next) { 294 | loadCollection('southpark', function(err, docs) { 295 | if (err) return next(err); 296 | 297 | var names = _.pluck(docs, 'name'); 298 | 299 | var expected = ['Eric', 'Butters', 'Kenny', 'Stan', 'Towelie']; 300 | 301 | test.same(names.sort(), expected.sort()); 302 | 303 | next(); 304 | }); 305 | } 306 | ], test.done); 307 | }); 308 | }, 309 | 'ignore sub directories' : function (test) { 310 | loader.load('./fixtures_with_subdir', function(err) { 311 | if (err) return test.done(err); 312 | 313 | async.parallel([ 314 | function(next) { 315 | loadCollection('archer', function(err, docs) { 316 | if (err) return next(err); 317 | 318 | var names = _.pluck(docs, 'name'); 319 | 320 | test.same(names.sort(), ['Sterling', 'Lana', 'Cheryl'].sort()); 321 | 322 | next(); 323 | }); 324 | } 325 | ], test.done); 326 | }); 327 | } 328 | }, 329 | 330 | 'with modifiers' : function(test) { 331 | var l = fixtures.connect(dbName); 332 | 333 | l.addModifier(function(collection, doc, cb) { 334 | doc.x = doc.name + 'X'; 335 | 336 | cb(null, doc); 337 | }); 338 | 339 | l.addModifier(function(collection, doc, cb) { 340 | doc.name += 'Y'; 341 | 342 | cb(null, doc); 343 | }); 344 | 345 | l.load('./fixtures/archer.js', function(err) { 346 | if (err) return test.done(err); 347 | 348 | loadCollection('archer', function(err, docs) { 349 | if (err) return test.done(err); 350 | 351 | var x = _.pluck(docs, 'x'); 352 | 353 | test.same(x.sort(), ['SterlingX', 'LanaX', 'CherylX'].sort()); 354 | 355 | var y = _.pluck(docs, 'name'); 356 | 357 | test.same(y.sort(), ['SterlingY', 'LanaY', 'CherylY'].sort()); 358 | 359 | test.done(); 360 | }); 361 | }); 362 | } 363 | }; 364 | 365 | exports['clear'] = { 366 | setUp: function(done) { 367 | db.dropDatabase(function(err) { 368 | if (err) return done(err); 369 | 370 | loader.load('./fixtures', done); 371 | }); 372 | }, 373 | 374 | 'drops the db if collections not specified': function(test) { 375 | async.series([ 376 | function(cb) { 377 | loader.clear(cb); 378 | }, 379 | 380 | function(cb) { 381 | loadCollection('archer', function(err, docs) { 382 | if (err) return cb(err); 383 | 384 | test.same(0, docs.length); 385 | 386 | cb(); 387 | }); 388 | }, 389 | 390 | function(cb) { 391 | loadCollection('southpark', function(err, docs) { 392 | if (err) return cb(err); 393 | 394 | test.same(0, docs.length); 395 | 396 | cb(); 397 | }); 398 | } 399 | ], test.done); 400 | }, 401 | 402 | 'clears a collection if called with a string': function(test) { 403 | async.series([ 404 | function(cb) { 405 | loader.clear('archer', cb); 406 | }, 407 | 408 | function(cb) { 409 | loadCollection('archer', function(err, docs) { 410 | if (err) return cb(err); 411 | 412 | test.same(0, docs.length); 413 | 414 | cb(); 415 | }); 416 | }, 417 | 418 | function(cb) { 419 | loadCollection('southpark', function(err, docs) { 420 | if (err) return cb(err); 421 | 422 | test.same(5, docs.length); 423 | 424 | cb(); 425 | }); 426 | } 427 | ], test.done); 428 | }, 429 | 430 | 'clears multiple collections if called with an array': function(test) { 431 | async.series([ 432 | function(cb) { 433 | loader.clear(['archer', 'southpark'], cb); 434 | }, 435 | 436 | function(cb) { 437 | loadCollection('archer', function(err, docs) { 438 | if (err) return cb(err); 439 | 440 | test.same(0, docs.length); 441 | 442 | cb(); 443 | }); 444 | }, 445 | 446 | function(cb) { 447 | loadCollection('southpark', function(err, docs) { 448 | if (err) return cb(err); 449 | 450 | test.same(0, docs.length); 451 | 452 | cb(); 453 | }); 454 | } 455 | ], test.done); 456 | }, 457 | 458 | 'clearing non-existent collections shouldn\'t error': function(test) { 459 | loader.clear('fheruas', function(err) { 460 | test.ifError(err); 461 | 462 | test.done(); 463 | }) 464 | } 465 | }; 466 | 467 | 468 | exports['clearAllAndLoad'] = { 469 | setUp: function(done) { 470 | db.dropDatabase(function(err) { 471 | if (err) return done(err); 472 | 473 | loader.load('./fixtures', done); 474 | }); 475 | }, 476 | 477 | 'drops the db and loads data': function(test) { 478 | var data = {}; 479 | data.southpark = [ 480 | { name: 'Kyle' } 481 | ]; 482 | 483 | async.series([ 484 | function(cb) { 485 | loader.clearAllAndLoad(data, cb); 486 | }, 487 | 488 | function(cb) { 489 | loadCollection('archer', function(err, docs) { 490 | if (err) return cb(err); 491 | 492 | test.same(0, docs.length); 493 | 494 | cb(); 495 | }); 496 | }, 497 | 498 | function(cb) { 499 | loadCollection('southpark', function(err, docs) { 500 | if (err) return cb(err); 501 | 502 | test.same(1, docs.length); 503 | 504 | cb(); 505 | }); 506 | } 507 | ], test.done); 508 | } 509 | }; 510 | 511 | 512 | exports['clearAndLoad'] = { 513 | setUp: function(done) { 514 | db.dropDatabase(function(err) { 515 | if (err) return done(err); 516 | 517 | loader.load('./fixtures', done); 518 | }); 519 | }, 520 | 521 | 'drops only referenced collections; with object data': function(test) { 522 | var data = {}; 523 | data.southpark = [ 524 | { name: 'Kyle' } 525 | ]; 526 | 527 | async.series([ 528 | function(cb) { 529 | loader.clearAndLoad(data, cb); 530 | }, 531 | 532 | function(cb) { 533 | loadCollection('archer', function(err, docs) { 534 | if (err) return cb(err); 535 | 536 | test.same(3, docs.length); 537 | 538 | cb(); 539 | }); 540 | }, 541 | 542 | function(cb) { 543 | loadCollection('southpark', function(err, docs) { 544 | if (err) return cb(err); 545 | 546 | test.same(1, docs.length); 547 | 548 | cb(); 549 | }); 550 | } 551 | ], test.done); 552 | }, 553 | 554 | 'drops only referenced collections; with a file': function(test) { 555 | async.series([ 556 | function(cb) { 557 | loader.clearAndLoad(__dirname + '/fixtures/southpark2.js', cb); 558 | }, 559 | 560 | function(cb) { 561 | loadCollection('archer', function(err, docs) { 562 | if (err) return cb(err); 563 | 564 | test.same(3, docs.length); 565 | 566 | cb(); 567 | }); 568 | }, 569 | 570 | function(cb) { 571 | loadCollection('southpark', function(err, docs) { 572 | if (err) return cb(err); 573 | 574 | var names = _.pluck(docs, 'name'); 575 | 576 | test.same(names, ['Stan', 'Towelie']); 577 | 578 | cb(); 579 | }); 580 | } 581 | ], test.done); 582 | } 583 | 584 | }; 585 | 586 | 587 | //Close DB connection and end process when done 588 | exports['exit'] = { 589 | 'exit': function(test) { 590 | test.done(); 591 | setTimeout(function() { 592 | db.close(function() { 593 | process.exit(); 594 | }); 595 | }); 596 | } 597 | } 598 | --------------------------------------------------------------------------------