├── .gitignore ├── package.json ├── LICENSE ├── README.md ├── mongoose-simple-random.js └── test └── mongoose-simple-random.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoose-simple-random", 3 | "version": "0.4.1", 4 | "description": "Simple and easy-to-use NodeJS Mongoose Schema plugin to pull random documents", 5 | "main": "mongoose-simple-random.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "mocha" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/larryprice/mongoose-simple-random.git" 15 | }, 16 | "keywords": [ 17 | "mongoose", 18 | "random" 19 | ], 20 | "author": "Larry Price (http://larry-price.com)", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/larryprice/mongoose-simple-random/issues" 24 | }, 25 | "homepage": "https://github.com/larryprice/mongoose-simple-random", 26 | "devDependencies": { 27 | "async": "^2.1.4", 28 | "chai": "^3.5.0", 29 | "mocha": "^3.2.0", 30 | "mockgoose": "^6.0.8", 31 | "mongoose": "^4.7.7" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### mongoose-simple-random 2 | 3 | Author: Larry Price 4 | Website: [larry-price.com](https://larry-price.com) 5 | Email: 6 | 7 | #### Description 8 | 9 | Simple and easy-to-use NodeJS Mongoose Schema plugin to find random documents. 10 | 11 | #### Usage 12 | 13 | ``` javascript 14 | var random = require('mongoose-simple-random'); 15 | 16 | var s = new Schema({ 17 | message: String 18 | }); 19 | s.plugin(random); 20 | 21 | Test = mongoose.model('Test', s); 22 | 23 | // Find a single random document 24 | Test.findOneRandom(function(err, result) { 25 | if (!err) { 26 | console.log(result); // 1 element 27 | } 28 | }); 29 | 30 | // Find "limit" random documents (defaults to array of 1) 31 | Test.findRandom({}, {}, {limit: 5}, function(err, results) { 32 | if (!err) { 33 | console.log(results); // 5 elements 34 | } 35 | }); 36 | 37 | // Parameters match parameters for "find" 38 | var filter = { genre: { $in: ['adventure', 'point-and-click'] } }; 39 | var fields = { name: 1, description: 0 }; 40 | var options = { skip: 10, limit: 10, populate: 'mySubDoc' }; 41 | Test.findRandom(filter, fields, options, function(err, results) { 42 | if (!err) { 43 | console.log(results); // 10 elements, name only, in genres "adventure" and "point-and-click" 44 | } 45 | }); 46 | ``` 47 | 48 | ## Tests 49 | 50 | ``` 51 | $ npm test 52 | ``` 53 | 54 | ## Contributing 55 | 56 | In lieu of a formal styleguide, take care to maintain the existing coding style. 57 | 58 | Add unit tests for any new or changed functionality. Lint and test your code. 59 | 60 | ## Release History 61 | 62 | * 0.1.0 Initial release 63 | * 0.2.0 API change - findRandom always returns array, findOneRandom returns single item 64 | * 0.2.1 README update 65 | * 0.3.0 API change - flip-flopping on "count", use "limit" to tell findByRandom how many elements to return 66 | * 0.4.0 Update dependencies and use a new random algorithm 67 | * 0.4.1 Fix hang on 0 results and clean up callback syntax. 68 | -------------------------------------------------------------------------------- /mongoose-simple-random.js: -------------------------------------------------------------------------------- 1 | // Utility methods - some were pulled partially from other MIT-licensed projects 2 | var utils = (function () { 3 | var random = function(max) { return Math.floor(Math.random() * (max+1)); }; 4 | var shuffle = function(a) { 5 | var length = a.length, 6 | shuffled = Array(length); 7 | for (var index = 0, rand; index < length; ++index) { 8 | rand = random(index); 9 | if (rand !== index) shuffled[index] = shuffled[rand]; 10 | shuffled[rand] = a[index]; 11 | } 12 | return shuffled; 13 | }; 14 | 15 | var range = function(length) {return Array(length).fill(null).map(function(cv, i) {return i}); }; 16 | var sample = function(a, n) { return shuffle(a).slice(0, Math.max(0, n)); }; 17 | var randomMap = function(count, limit, next, callback) { return asyncMap(sample(range(count), limit), next, callback); }; 18 | 19 | var asyncMap = function(items, next, callback) { 20 | var transformed = new Array(items.length), 21 | count = 0, 22 | halt = false; 23 | 24 | if (items.length === 0) { 25 | return callback() 26 | } 27 | 28 | items.forEach(function(item, index) { 29 | next(item, function(error, transformedItem) { 30 | if (halt) return; 31 | if (error) { 32 | halt = true; 33 | return callback(error); 34 | } 35 | transformed[index] = transformedItem; 36 | if (++count === items.length) return callback(undefined, transformed); 37 | }); 38 | }); 39 | }; 40 | 41 | var checkParams = function (conditions, fields, options, callback) { 42 | if (typeof conditions === 'function') { 43 | callback = conditions; 44 | conditions = {}; 45 | fields = {}; 46 | options = {}; 47 | } else if (typeof fields === 'function') { 48 | callback = fields; 49 | fields = {}; 50 | options = {}; 51 | } else if (typeof options === 'function') { 52 | callback = options; 53 | options = {}; 54 | } 55 | 56 | if (options.skip) { 57 | delete options.skip; 58 | } 59 | 60 | return { 61 | conditions: conditions, 62 | fields: fields, 63 | options: options, 64 | callback: callback 65 | } 66 | }; 67 | 68 | return { 69 | randomMap: randomMap, 70 | checkParams: checkParams 71 | }; 72 | }()); 73 | 74 | module.exports = exports = function (schema) { 75 | schema.statics.findRandom = function (conditions, fields, options, callback) { 76 | var args = utils.checkParams(conditions, fields, options, callback), 77 | _this = this; 78 | 79 | return _this.estimatedDocumentCount(args.conditions, function(err, count) { 80 | var limit = 1, 81 | populate = null; 82 | if (err) { 83 | return args.callback(err, undefined); 84 | } 85 | if (args.options.limit) { 86 | limit = args.options.limit; 87 | delete args.options.limit; 88 | } 89 | if (limit > count) { 90 | limit = count; 91 | } 92 | if (args.options.populate) { 93 | populate = args.options.populate; 94 | delete args.options.populate; 95 | } 96 | return utils.randomMap(count, limit, (item, next) => { 97 | args.options.skip = item; 98 | var find = _this.findOne(args.conditions, args.fields, args.options); 99 | if (populate) { 100 | find.populate(populate); 101 | } 102 | find.exec(next); 103 | }, args.callback); 104 | }); 105 | }; 106 | 107 | schema.statics.findOneRandom = function (conditions, fields, options, callback) { 108 | var args = utils.checkParams(conditions, fields, options, callback); 109 | 110 | args.options.limit = 1; 111 | this.findRandom(args.conditions, args.fields, args.options, function(err, docs) { 112 | if (docs && docs.length === 1) { 113 | return args.callback(err, docs[0]); 114 | } 115 | 116 | if (!err) { 117 | err = "findOneRandom yielded no results."; 118 | } 119 | 120 | return args.callback(err); 121 | }); 122 | }; 123 | }; 124 | -------------------------------------------------------------------------------- /test/mongoose-simple-random.test.js: -------------------------------------------------------------------------------- 1 | var random = require('../mongoose-simple-random'), 2 | mockgoose = require('mockgoose'), 3 | mongoose = require('mongoose'), 4 | async = require('async'), 5 | should = require('chai').should(); 6 | 7 | mockgoose(mongoose); 8 | 9 | describe('mongoose-simple-random', function () { 10 | var Test; 11 | 12 | describe('multiple documents', function () { 13 | before(function (done) { 14 | mockgoose(mongoose).then(function() { 15 | mongoose.connect('mongodb://localhost/mydb', function() { 16 | var s = new mongoose.Schema({ 17 | message: String, 18 | urls: [ { type: mongoose.Schema.Types.ObjectId, ref: "Url" } ] 19 | }); 20 | s.plugin(random); 21 | Test = mongoose.model('Test', s); 22 | 23 | Test.create({ 24 | message: "this" 25 | }); 26 | Test.create({ 27 | message: "is" 28 | }); 29 | Test.create({ 30 | message: "not" 31 | }); 32 | Test.create({ 33 | message: "a" 34 | }); 35 | Test.create({ 36 | message: "drill" 37 | }); 38 | 39 | done(); 40 | }); 41 | }); 42 | }); 43 | 44 | after(function(done) { 45 | mockgoose.reset(function() {done();}) 46 | }); 47 | 48 | it('gets a single doc at random', function (done) { 49 | Test.findOneRandom(function (err, result) { 50 | should.not.exist(err); 51 | result.should.have.property('message'); 52 | result.should.have.property('_id'); 53 | result.should.have.property('__v'); 54 | done(); 55 | }); 56 | }); 57 | 58 | it('gets 3 docs at random', function (done) { 59 | Test.findRandom({}, {}, { 60 | limit: 3 61 | }, function (err, result) { 62 | should.not.exist(err); 63 | result.should.have.length(3); 64 | for (var i = 0; i < 3; ++i) { 65 | result[i].should.have.property('message'); 66 | result[i].should.have.property('_id'); 67 | result[i].should.have.property('__v'); 68 | } 69 | // check that they're distinct 70 | for (var i = 0; i < 3; i++) { 71 | for (var j = 0; j < 3; j++) { 72 | if (i == j) continue; 73 | result[i].should.not.be.equal(result[j]); 74 | } 75 | } 76 | done(); 77 | }); 78 | }); 79 | }); 80 | 81 | describe('document population', function () { 82 | var urls = ['https://www.npmjs.com','https://github.com']; 83 | 84 | before(function (done){ 85 | mockgoose.reset(function() { 86 | mockgoose(mongoose).then(function() { 87 | mongoose.connect('mongodb://localhost/mydb', function() { 88 | var u = new mongoose.Schema({ url: String }); 89 | var Url = mongoose.model('Url', u); 90 | var urlIds = []; 91 | var tasks = []; 92 | 93 | urls.forEach(function (url) { 94 | tasks.push(function (cb) { 95 | Url.create({ url: url }, function (err, url) { 96 | urlIds.push(url._id); 97 | cb(); 98 | }); 99 | }); 100 | }); 101 | 102 | tasks.push(function (cb) { 103 | Test.create({ message: 'stuff', urls: urlIds }, function () { cb(); }); 104 | }); 105 | 106 | async.waterfall(tasks, function () { done(); }); 107 | }); 108 | }); 109 | }); 110 | }); 111 | 112 | after(function(done) { 113 | mockgoose.reset(function() {done();}) 114 | }); 115 | 116 | it('gets ids without populating document', function (done) { 117 | Test.findOneRandom(function (err, result) { 118 | result.urls.length.should.equal(2); 119 | for (var i = 0; i < result.urls.length; i++) { 120 | result.urls[i].should.be.instanceOf(mongoose.Types.ObjectId); 121 | } 122 | done(); 123 | }); 124 | }); 125 | 126 | it('populates document with url objects', function (done) { 127 | Test.findOneRandom({}, {}, { 128 | populate: 'urls' 129 | }, function (err, result) { 130 | result.urls.length.should.equal(2); 131 | result.urls.forEach(function (urlObject, i) { 132 | urlObject.url.should.equal(urls[i]); 133 | }); 134 | done(); 135 | }); 136 | }); 137 | }); 138 | 139 | describe('no results', function () { 140 | before(function (done) { 141 | mockgoose.reset(function () { 142 | mockgoose(mongoose).then(function() { 143 | done(); 144 | }); 145 | }); 146 | }); 147 | 148 | it('doesnt freak out when no documents available', function (done) { 149 | Test.findOneRandom(function (err, result) { 150 | err.should.equal("findOneRandom yielded no results."); 151 | should.not.exist(result); 152 | done(); 153 | }); 154 | }); 155 | }); 156 | 157 | describe('edge cases', function () { 158 | before(function (done) { 159 | mockgoose.reset(function () { 160 | mockgoose(mongoose).then(function() { 161 | Test.create({ 162 | message: "this" 163 | }); 164 | 165 | done(); 166 | }); 167 | }); 168 | }); 169 | 170 | after(function(done) { 171 | mockgoose.reset(function() {done();}) 172 | }); 173 | 174 | it('gets the only document with findOne', function (done) { 175 | Test.findOneRandom(function (err, result) { 176 | should.not.exist(err); 177 | should.exist(result); 178 | result.should.have.property('message'); 179 | result.message.should.equal("this"); 180 | result.should.have.property('_id'); 181 | result.should.have.property('__v'); 182 | done(); 183 | }); 184 | }); 185 | 186 | it('gets the only document with findMany', function (done) { 187 | Test.findRandom(function (err, result) { 188 | should.not.exist(err); 189 | should.exist(result); 190 | result.should.have.length(1); 191 | result[0].should.have.property('message'); 192 | result[0].message.should.equal("this"); 193 | result[0].should.have.property('_id'); 194 | result[0].should.have.property('__v'); 195 | done(); 196 | }); 197 | }); 198 | 199 | it('doesnt freak out when limit is huge', function (done) { 200 | Test.findRandom({}, {}, { 201 | limit: 1000 202 | }, function (err, result) { 203 | should.not.exist(err); 204 | should.exist(result); 205 | result.should.have.length(1); 206 | result[0].should.have.property('message'); 207 | result[0].message.should.equal("this"); 208 | result[0].should.have.property('_id'); 209 | result[0].should.have.property('__v'); 210 | done(); 211 | }); 212 | }); 213 | }); 214 | }); 215 | --------------------------------------------------------------------------------