├── seed.js ├── index.js ├── test └── fakegoose.js ├── .gitignore ├── examples └── chat-message.js ├── LICENSE ├── package.json ├── bin └── fakegoose.js ├── README.md └── fakegoose.js /seed.js: -------------------------------------------------------------------------------- 1 | 2 | function seed(Model, count) { 3 | 4 | } 5 | 6 | module.exports = seed; 7 | 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var fakegoose = require('./fakegoose'); 3 | 4 | function fakegoosePlugin(schema, options) { 5 | 6 | schema.static('fake', function() { 7 | return fakegoose.find(this, arguments); 8 | }); 9 | 10 | schema.static('fakeOne', function() { 11 | return fakegoose.findOne(this, arguments); 12 | }); 13 | 14 | schema.static('seed', function(count, forceAppend, callback) { 15 | return fakegoose.seed(this, count, forceAppend, callback); 16 | }); 17 | 18 | } 19 | 20 | module.exports = fakegoosePlugin; 21 | 22 | -------------------------------------------------------------------------------- /test/fakegoose.js: -------------------------------------------------------------------------------- 1 | 2 | const assert = require('assert'); 3 | const fakegoose = require('../fakegoose'); 4 | 5 | describe('fakegoose', () => { 6 | describe('fake(...[, callback:(error, results)])', () => { 7 | it('does not have any tests yet', () => {}); 8 | }); 9 | 10 | describe('fakeOne(...[, callback:(error, result)])', () => { 11 | it('does not have any tests yet', () => {}); 12 | }); 13 | 14 | describe('seed(count[, forceAppend=false], callback:(error))', () => { 15 | it('does not have any tests yet', () => {}); 16 | }); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /examples/chat-message.js: -------------------------------------------------------------------------------- 1 | 2 | var mongoose = require('mongoose'); 3 | var fakegoose = require('..'); // "fakegoose" outside of this project 4 | 5 | var chatMessageSchema = new mongoose.Schema({ 6 | first: { 7 | type: String, 8 | fake: 'name.firstName' // calls faker.name.firstName() 9 | }, 10 | last: { 11 | type: String, 12 | fake: 'name.lastName' // calls faker.name.lastName() 13 | }, 14 | text: { 15 | type: String, 16 | fake: 'lorem.paragraph' // calls fake.lorem.paragraph() 17 | }, 18 | date: { 19 | type: Date, 20 | fake: 'date.past' // you get the pattern 21 | } 22 | }); 23 | 24 | chatMessageSchema.plugin(fakegoose); 25 | module.exports = mongoose.model('ChatMessage', chatMessageSchema); 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Chris Andrejewski 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fakegoose", 3 | "version": "0.0.3", 4 | "description": "Faker + Mongoose", 5 | "main": "index.js", 6 | "bin": { 7 | "fakegoose": "bin/fakegoose.js" 8 | }, 9 | "scripts": { 10 | "test": "mocha" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/andrejewski/fakegoose.git" 15 | }, 16 | "keywords": [ 17 | "faker", 18 | "mongoose", 19 | "fake", 20 | "schema", 21 | "data" 22 | ], 23 | "author": "Chris Andrejewski (chrisandrejewski.com)", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/andrejewski/fakegoose/issues" 27 | }, 28 | "homepage": "https://github.com/andrejewski/fakegoose#readme", 29 | "devDependencies": { 30 | "mocha": "^2.5.3" 31 | }, 32 | "dependencies": { 33 | "async": "^2.0.0-rc.6", 34 | "commander": "^2.9.0", 35 | "faker": "^3.1.0", 36 | "mongoose": "^4.5.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bin/fakegoose.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const fakegoose = require('..'); 5 | const package = require('../package'); 6 | const mongoose = require('mongoose'); 7 | const commander = require('commander'); 8 | 9 | const defaultCount = 1; 10 | const defaultSeed = 'mongodb://localhost:27017/test'; 11 | 12 | commander 13 | .version(package.version) 14 | .arguments('') 15 | .option('--count [int]', 'number of documents to create', parseInt) 16 | .option('--seed [mongo_url]', 'mongodb database url') 17 | .parse(process.argv); 18 | 19 | const modelFile = commander.args.pop(); 20 | if(!modelFile) { 21 | throw new Error('Please specify a Mongoose model file'); 22 | } 23 | 24 | const Model = require(path.join(process.cwd(), modelFile)); 25 | if(!(Model && Model.schema)) { 26 | throw new Error(`"${modelFile}" does not export a Mongoose Model: 27 | EXAMPLE: module.exports = mongoose.model('MyModel', myModelSchema);\n\n`); 28 | } 29 | 30 | Model.schema.plugin(fakegoose); 31 | const count = commander.count || defaultCount; 32 | 33 | function printRecords(error, records) { 34 | if(error) throw error; 35 | console.log(JSON.stringify(records, null, 2)); 36 | } 37 | 38 | if(commander.seed) { 39 | var forceAppend = true; 40 | mongoose.connect(commander.seed || defaultSeed); 41 | Model.seed(count, forceAppend, function(error) { 42 | if(error) throw error; 43 | }); 44 | } else if(count === 1) { 45 | Model 46 | .fakeOne() 47 | .lean() 48 | .exec(printRecords); 49 | } else { 50 | Model 51 | .fake() 52 | .lean() 53 | .limit(count) 54 | .exec(printRecords); 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fakegoose 2 | 3 | Fakegoose is a plugin for simulating queries and seeding collections in 4 | [Mongoose](https://github.com/Automattic/mongoose) using the 5 | [Faker](https://github.com/Marak/faker.js) contextual data generation tool. 6 | Fakegoose takes Mongoose schemas and generates the proper dummy data based 7 | on the types and defaults described for on-the-fly queries or seeding 8 | collections for testing. 9 | 10 | ```sh 11 | npm install fakegoose 12 | ``` 13 | 14 | ## Command-line Usage 15 | 16 | ### Seeding the database 17 | 18 | ```sh 19 | fakegoose examples/chat-message.js --count 42 --seed mongodb://localhost:27017/test 20 | ``` 21 | 22 | ### Quick JSON documents 23 | 24 | ```sh 25 | fakegoose examples/chat-message.js --count 42 26 | ``` 27 | 28 | Without the `--seed [mongo_url]` argument, generated documents will be 29 | printed with `console.log()`. 30 | 31 | **Note:** Fakegoose must be installed globally `install --global` to be used from the command-line. 32 | 33 | ## Programmatic Usage 34 | 35 | Fakegoose works like other Mongoose plugins and only effects the 36 | models of schemas it is applied to. Inside the schema, the `fake` 37 | property instructs Fakegoose how to generate fake data. See [Faker](https://github.com/Marak/faker.js) 38 | for a list of all methods. If Faker does not have the generator 39 | you need, `fake` can also be a function that takes no arguments. 40 | 41 | ```js 42 | // models/chat-message.js 43 | var mongoose = require('mongoose'); 44 | var fakegoose = require('fakegoose'); 45 | 46 | var chatMessageSchema = new mongoose.Schema({ 47 | first: { 48 | type: String, 49 | fake: 'name.firstName' // calls faker.name.firstName() 50 | }, 51 | last: { 52 | type: String, 53 | fake: 'name.lastName' // calls faker.name.lastName() 54 | }, 55 | text: { 56 | type: String, 57 | fake: 'lorem.paragraph' // calls faker.lorem.paragraph() 58 | }, 59 | date: { 60 | type: Date, 61 | fake: 'date.past' // you get the pattern 62 | } 63 | }); 64 | 65 | chatMessageSchema.plugin(fakegoose); 66 | module.exports = mongoose.model('ChatMessage', chatMessageSchema); 67 | ``` 68 | 69 | Fakegoose adds static methods `fake` (`find` variant) and 70 | `fakeOne` (`findOne` variant) for querying, and `seed` for 71 | database population. 72 | 73 | ### Querying 74 | 75 | The `fake` and `fakeOne` methods are drop-in replacements for 76 | Mongoose's `find` and `findOne` accepting the same arguments 77 | and using the same chaining interface. 78 | 79 | `Model.fake` 80 | - `fake([conditions]) Query` 81 | - `fake(conditions[, options]) Query` 82 | - `fake(conditions[, options], callback:(error, results)) Query` 83 | 84 | `Model.fakeOne` 85 | - `fakeOne([conditions]) Query` 86 | - `fakeOne(conditions[, options]) Query` 87 | - `fakeOne(conditions[, options], callback:(error, results)) Query` 88 | 89 | ```js 90 | // elsewhere 91 | var assert = require('assert'); 92 | var mongoose = require('mongoose'); 93 | var ChatMessage = mongoose.model('ChatMessage'); 94 | 95 | ChatMessage.fakeOne({first: 'Chris'}, function(error, message) { 96 | if(error) { 97 | // this won't be called ever but is good 98 | // to include if #fakeOne is ever going 99 | // to be changed to #findOne. 100 | } 101 | assert.equal(message.first, 'Chris'); 102 | }); 103 | ``` 104 | 105 | Fakegoose queries will conform to simple conditions, but don't yet 106 | interpret `$[g|l]t[e]`, `$in`, or other expressions, but can with 107 | help from viewers like you. Options like `select`, `limit`, `skip`, 108 | and `lean` work properly, however complicated features such as 109 | aggregation and `populate` do not..yet. 110 | 111 | ### Seeding 112 | 113 | `Model.seed` 114 | - `Model.seed(count:number[, forceAppend=false], callback:(error))` 115 | 116 | `Model.seed` adds `count` documents to the model's collection, passing 117 | an error to the completion `callback` if there was a Mongoose error. 118 | By default if you specify a `count` Fakegoose will only seed **at a maximum** 119 | the number of documents necessary to reach the count. So if your collection 120 | has 42 records and you call `Model.seed(69, ...)` only 27 documents will be 121 | added to the collection. This is done because seeding generally is safe to 122 | perform multiple times without overfilling the database. To add exactly 123 | `count` documents, use `Model.seed(420, true, myCallback)`. 124 | 125 | ## Contributing 126 | 127 | Contributions are incredibly welcome as long as they are standardly applicable 128 | and pass the tests (or break bad ones). Tests are written in Mocha and 129 | assertions are done with the Node.js core `assert` module. 130 | 131 | ```bash 132 | # running tests 133 | npm run test 134 | ``` 135 | 136 | Follow me on [Twitter](https://twitter.com/compooter) for updates or just for 137 | the lolz and please check out my other [repositories](https://github.com/andrejewski) 138 | if I have earned it. I thank you for reading. 139 | -------------------------------------------------------------------------------- /fakegoose.js: -------------------------------------------------------------------------------- 1 | 2 | var async = require('async'); 3 | var faker = require('faker'); 4 | 5 | function fake(model, rawArgs) { 6 | var args = Array.prototype.slice.call(rawArgs, 0); 7 | return fakeMethod('find', model, args); 8 | } 9 | 10 | function fakeOne(model, rawArgs) { 11 | var args = Array.prototype.slice.call(rawArgs, 0); 12 | return fakeMethod('findOne', model, args); 13 | } 14 | 15 | function fakeMethod(findMethod, model, args) { 16 | var callback; 17 | if(typeof args[args.length - 1] === 'function') { 18 | callback = args.pop(); 19 | } 20 | var query = makeFakeQuery(model[findMethod].apply(model, args)); 21 | if(callback) { 22 | query.exec(callback); 23 | } 24 | return query; 25 | } 26 | 27 | function makeFakeQuery(mongooseQuery) { 28 | mongooseQuery.exec = function(callback) { 29 | process.nextTick(function() { 30 | var result = generateResult(mongooseQuery); 31 | callback(null, result); 32 | }); 33 | } 34 | 35 | mongooseQuery.then = function(resolve, reject) { 36 | process.nextTick(function() { 37 | var result = generateResult(mongooseQuery); 38 | resolve(result); 39 | }); 40 | } 41 | 42 | return mongooseQuery; 43 | } 44 | 45 | function generateResult(query) { 46 | if(query.op === 'findOne') { 47 | return makeFakeDocument(query); 48 | } else { 49 | var count = query.options.limit || 100; 50 | var results = Array(count); 51 | for(var i = 0; i < count; i++) { 52 | results[i] = makeFakeDocument(query); 53 | } 54 | return results; 55 | } 56 | } 57 | 58 | function makeFakeDocument(query) { 59 | var model = query.model; // check 60 | var doc = selectFields(query) 61 | .reduce(function(doc, path) { 62 | var value = fakePathParam(model, path); 63 | doc[path] = typeof value === 'function' 64 | ? value() 65 | : value; 66 | return doc; 67 | }, {}); 68 | 69 | Object.assign(doc, conditionalProps(query)); 70 | 71 | var isLean = query._mongooseOptions.lean; 72 | if(isLean) return doc; 73 | return new model(doc); 74 | } 75 | 76 | function selectFields(query) { 77 | var fields = query._fields || allPaths(query); 78 | return Object.keys(fields) 79 | .filter(function(key) { 80 | return fields[key]; 81 | }); 82 | } 83 | 84 | function allPaths(query) { 85 | // Mongoose does not explicit require paths unless 86 | // specified, leaving it to Mongo -- we can't. 87 | var paths = query.model.schema.paths; 88 | return paths; 89 | } 90 | 91 | function fakePathParam(model, pathName) { 92 | var path = model.schema.paths[pathName]; 93 | if(!path) return null; 94 | path.options = path.options || {}; 95 | var prop = path.options.fake; 96 | if(prop) { 97 | if(typeof prop === 'function') return prop; 98 | return nestedProp(faker, prop); 99 | } 100 | return path.options.default 101 | || path.defaultValue 102 | || makeNew(path.options.type); 103 | } 104 | 105 | function makeNew(constructor) { 106 | if(typeof constructor === 'function') { 107 | return constructor(); 108 | } 109 | } 110 | 111 | function nestedProp(obj, prop) { 112 | var nestedProps = prop.split('.'); 113 | var key; 114 | while(key = nestedProps.shift()) { 115 | obj = obj[key]; 116 | } 117 | return obj; 118 | } 119 | 120 | function conditionalProps(query) { 121 | var conds = query._conditions; 122 | var props = {}; 123 | Object.keys(conds).forEach(function(key) { 124 | var value = conds[key]; 125 | if(typeof value !== 'object') { 126 | return props[key] = value; 127 | } 128 | var $conds = Object.keys(value) 129 | .filter(function(key) {return key.charAt(0) === '$';}); 130 | if(!$conds.length) return props[key] = value; 131 | $conds.forEach(function($cond) { 132 | console.log('[fakegoose]: "' + $cond + '" is not supported..yet'); 133 | }); 134 | }); 135 | return props; 136 | } 137 | 138 | function seed(model, count, forceAppend, callback) { 139 | if(!callback) { 140 | callback = forceAppend; 141 | forceAppend = false; 142 | } 143 | if(forceAppend) return _seed(model, count, callback); 144 | model.count(function(error, total) { 145 | if(error) return callback(error); 146 | var remainder = Math.max(0, total - count); 147 | if(!remainder) return callback(null); 148 | _seed(model, remainder, callback); 149 | }); 150 | } 151 | 152 | function _seed(model, count, callback) { 153 | var records = Array(count); 154 | for(var i = 0; i < count; i++) { 155 | records[i] = makeJsonDocument(model); 156 | } 157 | async.each(records, function(doc, next) { 158 | new model(doc).save(next); 159 | }); 160 | } 161 | 162 | function makeJsonDocument(model) { 163 | var paths = Object.keys(model.schema.paths); 164 | return paths.reduce(function(doc, path) { 165 | var value = fakePathParam(model, path); 166 | doc[path] = typeof value === 'function' 167 | ? value() 168 | : value; 169 | return doc; 170 | }, {}); 171 | } 172 | 173 | module.exports = { 174 | find: fake, 175 | findOne: fakeOne, 176 | seed: seed 177 | }; 178 | 179 | --------------------------------------------------------------------------------