├── .gitignore ├── .travis.yml ├── .editorconfig ├── sift.js ├── mongo-mock.js ├── package.json ├── LICENSE ├── lib ├── find_options.js ├── mongo_client.js ├── bulk.js ├── cursor.js ├── db.js └── collection.js ├── README.md └── test ├── projection.test.js ├── options.test.js └── mock.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | scratch.js 3 | mongo.json 4 | mongo.js 5 | node_modules 6 | yarn.lock 7 | .vscode 8 | .npmrc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/boron" 4 | - "lts/carbon" 5 | - "lts/dubnium" 6 | - "lts/erbium" 7 | - "lts/*" 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | # insert_final_newline = true 9 | end_of_line = lf 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /sift.js: -------------------------------------------------------------------------------- 1 | var sift = require('sift'); 2 | 3 | //use a custom compare function so we can search on ObjectIDs 4 | var compare = sift.compare; 5 | sift.compare = function(a, b) { 6 | if(a && b && a._bsontype && b._bsontype) { 7 | return a.equals(b)? 0 : (compare(time(a), time(b)) || compare(a.toHexString(), b.toHexString())); 8 | } 9 | return compare(a,b); 10 | }; 11 | function time(id) { 12 | return id.getTimestamp().getTime() 13 | } 14 | 15 | module.exports = sift; 16 | -------------------------------------------------------------------------------- /mongo-mock.js: -------------------------------------------------------------------------------- 1 | var delay = 400; 2 | 3 | module.exports = { 4 | get max_delay() { return delay; }, 5 | set max_delay(n) { delay = Number(n) || 0; }, 6 | // pretend we are doing things async 7 | asyncish: function asyncish(callback) { 8 | setTimeout(callback, Math.random()*(delay)); 9 | }, 10 | get find_options() { return require('./lib/find_options.js') }, 11 | get MongoClient() { return require('./lib/mongo_client.js') }, 12 | get ObjectId() { return require('bson-objectid') }, 13 | get ObjectID() { return require('bson-objectid') } 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "William Kapke", 3 | "name": "mongo-mock", 4 | "version": "4.2.0", 5 | "description": "Let's pretend we have a real MongoDB", 6 | "main": "mongo-mock.js", 7 | "scripts": { 8 | "test": "mocha" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/williamkapke/mongo-mock.git" 13 | }, 14 | "keywords": [ 15 | "mongo", 16 | "mock" 17 | ], 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/williamkapke/mongo-mock/issues" 21 | }, 22 | "homepage": "https://github.com/williamkapke/mongo-mock", 23 | "dependencies": { 24 | "bson-objectid": "^2.0.1", 25 | "debug": "^2.6.9", 26 | "lodash": "^4.17.13", 27 | "modifyjs": "^0.3.1", 28 | "object-assign-deep": "^0.4.0", 29 | "sift": "^11.1.8" 30 | }, 31 | "devDependencies": { 32 | "mocha": "^5", 33 | "should": "^5" 34 | }, 35 | "engines": { 36 | "node": ">=6" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | 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, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /lib/find_options.js: -------------------------------------------------------------------------------- 1 | var ObjectID = require('../').ObjectID; 2 | 3 | module.exports = function find_options(args) { 4 | if(!args) args = []; 5 | var signature = Array.prototype.map.call(args, function(arg){ return Array.isArray(arg)? "array" : typeof arg }).join(); 6 | var options = { 7 | query: args[0], 8 | fields: {}, 9 | skip: 0, 10 | limit: 0, 11 | callback: /function$/.test(signature)? args[args.length-1] : undefined 12 | }; 13 | switch(signature) { 14 | //callback? 15 | case "": 16 | case "undefined": 17 | case "function": 18 | options.query = {}; 19 | break; 20 | //selector, callback?, 21 | case "object": 22 | case "object,undefined": 23 | case "object,function": 24 | if (ObjectID.isValid(options.query)) 25 | options.query = { _id: options.query }; 26 | break; 27 | //selector, fields, callback? 28 | //selector, options, callback? 29 | case "object,object": 30 | case "object,undefined,function": 31 | case "object,object,function": 32 | //sniff for a 1 or -1 to detect fields object 33 | if(!args[1] || Math.abs(args[1][Object.keys(args[1])[0]])===1) { 34 | options.fields = args[1]; 35 | } 36 | else { 37 | if(args[1].skip) options.skip = args[1].skip; 38 | if(args[1].sort) options.sort = args[1].sort; 39 | if(args[1].limit) options.limit = args[1].limit; 40 | if(args[1].fields) options.fields = args[1].fields; 41 | if(args[1].projection) options.fields = args[1].projection; 42 | } 43 | break; 44 | //selector, fields, options, callback? 45 | case "object,object,object": 46 | case "object,object,object,function": 47 | options.fields = args[1]; 48 | if(args[2].skip) options.skip = args[2].skip; 49 | if(args[2].sort) options.sort = args[2].sort; 50 | if(args[2].limit) options.limit = args[2].limit; 51 | if(args[2].fields) options.fields = args[2].fields; 52 | if(args[2].projection) options.fields = args[2].projection; 53 | break; 54 | //selector, fields, skip, limit, timeout, callback? 55 | case "object,object,number,number,number": 56 | case "object,object,number,number,number,function": 57 | options.fields = args[1]; 58 | options.timeout = args[4]; 59 | //selector, fields, skip, limit, callback? 60 | case "object,object,number,number": 61 | case "object,object,number,number,function": 62 | options.fields = args[1]; 63 | options.skip = args[2]; 64 | options.limit = args[3]; 65 | //if(typeof args[4]==="number") options.timeout = args[4]; 66 | break; 67 | default: 68 | throw new Error("unknown signature: "+ signature); 69 | } 70 | return options; 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mongo-mock     [![Build Status](https://travis-ci.org/williamkapke/mongo-mock.svg?branch=master)](https://travis-ci.org/williamkapke/mongo-mock) 2 | ======================================================================================================================================================================= 3 | 4 | This is an in-memory _'pretend'_ mongodb. The goal is to make the interface compatible with 5 | [the real mongodb](https://github.com/mongodb/node-mongodb-native) module so they are interchangeable. 6 | 7 | There are a TON of features for mongo and I can't write them all myself- so **pull requests are encouraged!** 8 | My initial goal was to provide _basic_ CRUD operations to enable this to work as a throw-something-together tool. 9 | 10 | ## Why? 11 | Maybe you don't want to (or can't) connect to a MongoDB instance for your tests?
12 | Maybe you want to throw together a quick example app? 13 | 14 | ## Demo code 15 | ```javascript 16 | var mongodb = require('mongo-mock'); 17 | mongodb.max_delay = 0;//you can choose to NOT pretend to be async (default is 400ms) 18 | var MongoClient = mongodb.MongoClient; 19 | MongoClient.persist="mongo.js";//persist the data to disk 20 | 21 | // Connection URL 22 | var url = 'mongodb://localhost:27017/myproject'; 23 | // Use connect method to connect to the Server 24 | MongoClient.connect(url, {}, function(err, client) { 25 | var db = client.db(); 26 | // Get the documents collection 27 | var collection = db.collection('documents'); 28 | // Insert some documents 29 | var docs = [ {a : 1}, {a : 2}, {a : 3}]; 30 | collection.insertMany(docs, function(err, result) { 31 | console.log('inserted',result); 32 | 33 | collection.updateOne({ a : 2 }, { $set: { b : 1 } }, function(err, result) { 34 | console.log('updated',result); 35 | 36 | collection.findOne({a:2}, {b:1}, function(err, doc) { 37 | console.log('foundOne', doc); 38 | 39 | collection.removeOne({ a : 3 }, function(err, result) { 40 | console.log('removed',result); 41 | 42 | collection.find({}, {_id:-1}).toArray(function(err, docs) { 43 | console.log('found',docs); 44 | 45 | function cleanup(){ 46 | var state = collection.toJSON(); 47 | // Do whatever you want. It's just an Array of Objects. 48 | state.documents.push({a : 2}); 49 | 50 | // truncate 51 | state.documents.length = 0; 52 | 53 | // closing connection 54 | db.close(); 55 | } 56 | 57 | setTimeout(cleanup, 1000); 58 | }); 59 | }); 60 | }); 61 | }); 62 | }); 63 | }); 64 | ``` 65 | 66 | ## Install 67 | Well, you know.. the usual: 68 | ``` 69 | $ npm install mongo-mock 70 | ``` 71 | -------------------------------------------------------------------------------- /test/projection.test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | var Cursor = require('../lib/cursor.js'); 3 | var docs = [ 4 | { _id: '1', a:11, b: 111 }, 5 | { _id: '2', a:22, b: 222 }, 6 | { _id: '3', a:33, b: 333 } 7 | ]; 8 | 9 | describe('Cursor tests', function () { 10 | 11 | describe('getProjectionType', function() { 12 | it('should identify a "pick"', function(){ 13 | var type = Cursor._getProjectionType({ a:1, b:1, c:1 }); 14 | type.should.equal('pick') 15 | }); 16 | it('should identify a "pick" with an explicit _id exclusion', function(){ 17 | var type = Cursor._getProjectionType({ a:1, _id:0, b:1, c:1 }); 18 | type.should.equal('pick') 19 | }); 20 | it('should identify an "omit"', function(){ 21 | var type = Cursor._getProjectionType({ a:0, b:0, c:0 }); 22 | type.should.equal('omit') 23 | }); 24 | it('should identify an "omit with an explicit _id inclusion"', function(){ 25 | var type = Cursor._getProjectionType({ a:0, _id:1, b:0, c:0 }); 26 | type.should.equal('omit') 27 | }); 28 | it('should default to "pick" if no fields given', function(){ 29 | var type = Cursor._getProjectionType({ }); 30 | type.should.equal('pick') 31 | }); 32 | it('should be a "pick" for { _id:1 }', function(){ 33 | var type = Cursor._getProjectionType({ _id:1 }); 34 | type.should.equal('pick') 35 | }); 36 | it('should be a "omit" for { _id:0 }', function(){ 37 | var type = Cursor._getProjectionType({ _id:0 }); 38 | type.should.equal('omit') 39 | }); 40 | }); 41 | 42 | describe('applyProjection', function () { 43 | it('should include fields specified', function () { 44 | var results = Cursor._applyProjection(docs, { a:1, _id:1, b:1, c:1 }); 45 | results.should.eql(docs) 46 | }); 47 | it('should include fields and _id', function () { 48 | var results = Cursor._applyProjection(docs, { a:1 }); 49 | results.should.eql([ 50 | { _id: '1', a:11 }, 51 | { _id: '2', a:22 }, 52 | { _id: '3', a:33 } 53 | ]) 54 | }); 55 | it('should include fields and explicitly exclude _id', function () { 56 | var results = Cursor._applyProjection(docs, { _id:0, a:1 }); 57 | results.should.eql([ 58 | { a:11 }, 59 | { a:22 }, 60 | { a:33 } 61 | ]) 62 | }); 63 | it('should exclude fields', function () { 64 | var results = Cursor._applyProjection(docs, { a:0 }); 65 | results.should.eql([ 66 | { _id: '1', b:111 }, 67 | { _id: '2', b:222 }, 68 | { _id: '3', b:333 } 69 | ]) 70 | }); 71 | it('should exclude the _id field too', function () { 72 | var results = Cursor._applyProjection(docs, { _id:0, a:0 }); 73 | results.should.eql([ 74 | { b:111 }, 75 | { b:222 }, 76 | { b:333 } 77 | ]) 78 | }); 79 | it('should not exclude the _id field if explicitly asking for it', function () { 80 | var results = Cursor._applyProjection(docs, { _id:1, b:0 }); 81 | results.should.eql([ 82 | { _id: '1', a:11 }, 83 | { _id: '2', a:22 }, 84 | { _id: '3', a:33 } 85 | ]) 86 | }); 87 | 88 | // addition edge case test 89 | it('should handle { _id:1 }', function () { 90 | var results = Cursor._applyProjection(docs, { _id:1 }); 91 | results.should.eql([ 92 | { _id: '1' }, 93 | { _id: '2' }, 94 | { _id: '3' } 95 | ]) 96 | }); 97 | it('should handle { _id:0 }', function () { 98 | var results = Cursor._applyProjection(docs, { _id:1 }); 99 | results.should.eql([ 100 | { _id: '1' }, 101 | { _id: '2' }, 102 | { _id: '3' } 103 | ]) 104 | }); 105 | }); 106 | 107 | }); 108 | -------------------------------------------------------------------------------- /lib/mongo_client.js: -------------------------------------------------------------------------------- 1 | var Db = require('./db.js'); 2 | var servers = {}; 3 | var urlparse = require('url').parse; 4 | var ObjectId = require('bson-objectid'); 5 | var debug = require('debug')('mongo-mock:mongo_client'); 6 | var fs = require('fs'); 7 | 8 | module.exports = MongoClient; 9 | function MongoClient() { 10 | this.connect = MongoClient.connect; 11 | } 12 | 13 | MongoClient.connect = function(url, options, callback) { 14 | callback = arguments[arguments.length - 1]; 15 | if(!options || callback===options) options = {}; 16 | url = urlparse(url); 17 | 18 | var server = servers[url.host] || (servers[url.host] = { databases:{}, persist:MongoClient._persist }); 19 | debug('connecting %s%s', url.host, url.pathname); 20 | 21 | var dbname = url.pathname.replace(/^\//, ''); 22 | return new Db(dbname, server).open(callback); 23 | }; 24 | 25 | MongoClient._persist = persist; 26 | function persist() { 27 | var filename = MongoClient.persist; 28 | if(typeof filename!=='string') return; 29 | debug('persisting to %s', filename); 30 | 31 | var out = "var ObjectID = require('bson-objectid');\n\nmodule.exports = "; 32 | 33 | var inspect = (Symbol && Symbol.for('nodejs.util.inspect.custom')) || 'inspect'; 34 | 35 | ObjectId.prototype.toJSON = ObjectId.prototype[inspect] || ObjectId.prototype.inspect; 36 | out += JSON.stringify(servers, null, 2).replace(/"ObjectID\(([0-9a-f]{24})\)"/g, 'ObjectID("$1")'); 37 | ObjectId.prototype.toJSON = ObjectId.prototype.toHexString; 38 | 39 | fs.writeFileSync(filename, out); 40 | } 41 | 42 | MongoClient.load = function (filename, callback) { 43 | filename = filename || MongoClient.persist || './mongo.json'; 44 | var p = MongoClient.persist; 45 | if(p) MongoClient.persist = false;//disable while loading 46 | 47 | debug('loading data from %s', filename); 48 | try{ 49 | var data = require(filename); 50 | } 51 | catch(e) { 52 | debug('Error loading data: %s', e); 53 | } 54 | 55 | var servers_names = Object.keys(data); 56 | 57 | function create_server(server_name) { 58 | if(!server_name) { 59 | MongoClient.persist = p; 60 | if(callback) callback(); 61 | return; 62 | } 63 | 64 | var database_names = Object.keys(data[server_name].databases); 65 | 66 | function create_database(database_name) { 67 | if(!database_name) 68 | return create_server(servers_names.pop()); 69 | 70 | var collections = data[server_name].databases[database_name].collections; 71 | MongoClient.connect('mongodb://'+server_name+'/'+database_name, function (err, db) { 72 | if(err) throw err; 73 | 74 | function create_collection(collection) { 75 | if(!collection) { 76 | db.close(); 77 | return create_database(servers_names.pop()); 78 | } 79 | 80 | db.createCollection(collection.name, function (err, instance) { 81 | if(err) throw err; 82 | 83 | function insert(doc) { 84 | if(!doc) return create_collection(collections.pop()); 85 | 86 | if(doc._id) doc._id = ObjectId(doc._id); 87 | instance.update(doc, doc, {upsert:true}, function (err, result) { 88 | if(err) throw err; 89 | process.nextTick(function () { 90 | insert(collection.documents.pop()); 91 | }); 92 | }); 93 | } 94 | insert(collection.documents && collection.documents.pop()); 95 | }) 96 | } 97 | create_collection(collections.pop()); 98 | }); 99 | } 100 | create_database(database_names.pop()); 101 | } 102 | 103 | if(typeof callback!=='function') { 104 | return new Promise(function (resolve) { 105 | callback = resolve; 106 | create_server(servers_names.pop()); 107 | }) 108 | } else { 109 | create_server(servers_names.pop()); 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /lib/bulk.js: -------------------------------------------------------------------------------- 1 | var asyncish = require('../').asyncish; 2 | 3 | module.exports = function Bulk(collection, ordered) { 4 | var ops = []; 5 | var executedOps = []; 6 | var iface = { 7 | insert: function (docs, options, callback) { 8 | ops.push({ 9 | args: Array.from(arguments), 10 | fnc: collection.insert, 11 | }); 12 | 13 | return this; 14 | }, 15 | find: function() { 16 | return new FindOperators(collection, arguments[0], ops); 17 | }, 18 | getOperations: NotImplemented, 19 | tojson: NotImplemented, 20 | toString: NotImplemented, 21 | execute: function(callback) { 22 | if (ordered) { 23 | return executeOperations(ops, callback); 24 | } else { 25 | return executeOperationsParallel(ops, callback); 26 | } 27 | }, 28 | }; 29 | 30 | //Runs operations only one at a time 31 | function executeOperations(operations, callback) { 32 | callback = arguments[arguments.length - 1]; 33 | asyncish(() => { 34 | operations.reduce((promiseChain, operation) => { 35 | return promiseChain.then(() => { 36 | executedOps.push(operation); 37 | return operation.fnc.apply(this, operation.args) 38 | }); 39 | }, Promise.resolve([])) 40 | .then(() => { 41 | callback(null, executedOps); 42 | }) 43 | .catch(callback) 44 | }); 45 | if (typeof callback !== 'function') { 46 | return new Promise(function (resolve, reject) { 47 | callback = function (e, r) { e ? reject(e) : resolve(r) }; 48 | }) 49 | } 50 | } 51 | 52 | //Exhibits a more "fire and forget" behavior 53 | function executeOperationsParallel(operations, callback) { 54 | callback = arguments[arguments.length - 1]; 55 | var promises = []; 56 | for (var i = 0; i < operations.length; i++) { 57 | var operation = operations[i]; 58 | promises.push(operation.fnc.apply(this, operation.args)); 59 | } 60 | 61 | asyncish(() => { 62 | Promise.all(promises) 63 | .then((operations) => { 64 | callback(null, operations) 65 | }) 66 | .catch(callback); 67 | }); 68 | 69 | if (typeof callback !== 'function') { 70 | return new Promise(function (resolve, reject) { 71 | callback = function (e, r) { e ? reject(e) : resolve(r) }; 72 | }) 73 | } 74 | 75 | } 76 | 77 | return iface; 78 | }; 79 | 80 | function FindOperators(collection, query, ops) { 81 | var cursor = collection.find(query); 82 | var upsert = false; 83 | 84 | var iface = { 85 | remove: function() { 86 | var process = (doc) => { 87 | cursor = getLatestCursor(); 88 | if (!doc) { 89 | return Promise.resolve(); 90 | } 91 | 92 | return collection.remove({_id: doc._id}).then(() => { 93 | return cursor.next().then((d) => process(d)); 94 | }); 95 | }; 96 | 97 | ops.push({ 98 | args: [], 99 | fnc: () => { 100 | return cursor.next().then((d) => { 101 | return process(d); 102 | }); 103 | }, 104 | }); 105 | 106 | return this; 107 | }, 108 | removeOne: function() { 109 | ops.push({ 110 | args: [], 111 | fnc: () => { 112 | cursor = getLatestCursor(); 113 | return cursor.next().then((doc) => { 114 | if (!doc) { 115 | return; 116 | } 117 | return collection.deleteOne({_id: doc._id}); 118 | }); 119 | }, 120 | }); 121 | 122 | return this; 123 | }, 124 | replaceOne: NotImplemented, 125 | update: function(updateSpec) { 126 | ops.push({ 127 | args: [], 128 | fnc: () => { 129 | return collection.update(query, updateSpec, { 130 | multi: true, 131 | upsert: upsert, 132 | }); 133 | } 134 | }); 135 | 136 | return this; 137 | }, 138 | updateOne: function(updateSpec) { 139 | ops.push({ 140 | args: [], 141 | fnc: () => { 142 | return collection.updateOne(query, updateSpec, { 143 | upsert: upsert, 144 | }); 145 | } 146 | }); 147 | 148 | return this; 149 | }, 150 | upsert: function() { 151 | upsert = true; 152 | return this; 153 | }, 154 | collation: NotImplemented, 155 | arrayFilters: NotImplemented, 156 | }; 157 | 158 | function getLatestCursor() { 159 | return collection.find(query); 160 | } 161 | 162 | return iface; 163 | } 164 | 165 | function NotImplemented(){ 166 | throw Error('Not Implemented'); 167 | } 168 | -------------------------------------------------------------------------------- /test/options.test.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var mongo = require('../'); 3 | var fo = require('../lib/find_options.js'); 4 | function find_options() { 5 | return fo(arguments); 6 | } 7 | 8 | describe('options tests', function () { 9 | var cb = function(){}; 10 | 11 | it('should accept signature: empty arguments', function(){ 12 | var options = find_options(); 13 | options.should.eql({ callback:undefined, fields:{}, limit:0, query:{}, skip:0 }); 14 | }); 15 | it('should accept signature: "callback"', function(){ 16 | var options = find_options(cb); 17 | options.should.eql({ callback:cb, fields:{}, limit:0, query:{}, skip:0 }); 18 | }); 19 | it('should accept signature: "selector"', function(){ 20 | var options = find_options({_id:"ABC123"}); 21 | options.should.eql({ callback:undefined, fields:{}, limit:0, query:{_id:"ABC123"}, skip:0 }); 22 | }); 23 | it('should handle ObjectIds', function(){ 24 | var id = mongo.ObjectID(); 25 | var options = find_options(id); 26 | options.should.eql({ callback:undefined, fields:{}, limit:0, query:{_id:id}, skip:0 }); 27 | }); 28 | it('should accept signature: "selector, callback"', function(){ 29 | var options = find_options({_id:"ABC123"}, cb); 30 | options.should.eql({ callback:cb, fields:{}, limit:0, query:{_id:"ABC123"}, skip:0 }); 31 | }); 32 | it('should accept signature: "selector, fields"', function(){ 33 | var options = find_options({_id:"ABC123"}, {_id:-1}); 34 | options.should.eql({ callback:undefined, fields:{_id:-1}, limit:0, query:{_id:"ABC123"}, skip:0 }); 35 | }); 36 | it('should accept signature: "selector, fields, callback"', function(){ 37 | var options = find_options({_id:"ABC123"}, {_id:-1}, cb); 38 | options.should.eql({ callback:cb, fields:{_id:-1}, limit:0, query:{_id:"ABC123"}, skip:0 }); 39 | }); 40 | it('should accept signature: "selector, undefined, callback"', function(){ 41 | var options = find_options({_id:"ABC123"}, undefined, cb); 42 | options.should.eql({ callback:cb, fields:undefined, limit:0, query:{_id:"ABC123"}, skip:0 }); 43 | }); 44 | it('should accept signature: "selector, options"', function(){ 45 | var options = find_options({_id:"ABC123"}, {fields:{_id:-1}, skip:100}); 46 | options.should.eql({ callback:undefined, fields:{_id:-1}, limit:0, query:{_id:"ABC123"}, skip:100 }); 47 | }); 48 | it('should accept signature: "selector, options"', function(){ 49 | var options = find_options({_id:"ABC123"}, {projection:{_id:-1}, skip:100}); 50 | options.should.eql({ callback:undefined, fields:{_id:-1}, limit:0, query:{_id:"ABC123"}, skip:100 }); 51 | }); 52 | it('should accept signature: "selector, options, callback"', function(){ 53 | var options = find_options({_id:"ABC123"}, {fields:{_id:-1}, skip:100}, cb); 54 | options.should.eql({ callback:cb, fields:{_id:-1}, limit:0, query:{_id:"ABC123"}, skip:100 }); 55 | }); 56 | it('should accept signature: "selector, options, callback"', function(){ 57 | var options = find_options({_id:"ABC123"}, {projection:{_id:-1}, skip:100}, cb); 58 | options.should.eql({ callback:cb, fields:{_id:-1}, limit:0, query:{_id:"ABC123"}, skip:100 }); 59 | }); 60 | it('should accept signature: "selector, fields, options"', function(){ 61 | var options = find_options({_id:"ABC123"}, {_id:-1}, {skip:100}); 62 | options.should.eql({ callback:undefined, fields:{_id:-1}, limit:0, query:{_id:"ABC123"}, skip:100 }); 63 | }); 64 | it('should accept signature: "selector, fields, options, callback"', function(){ 65 | var options = find_options({_id:"ABC123"}, {_id:-1}, {skip:100}, cb); 66 | options.should.eql({ callback:cb, fields:{_id:-1}, limit:0, query:{_id:"ABC123"}, skip:100 }); 67 | }); 68 | it('should accept signature: "selector, fields, skip, limit"', function(){ 69 | var options = find_options({_id:"ABC123"}, {_id:-1}, 200, 100); 70 | options.should.eql({ callback:undefined, fields:{_id:-1}, limit:100, query:{_id:"ABC123"}, skip:200 }); 71 | }); 72 | it('should accept signature: "selector, fields, skip, limit, callback"', function(){ 73 | var options = find_options({_id:"ABC123"}, {_id:-1}, 200, 100, cb); 74 | options.should.eql({ callback:cb, fields:{_id:-1}, limit:100, query:{_id:"ABC123"}, skip:200 }); 75 | }); 76 | it('should accept signature: "selector, fields, skip, limit, timeout"', function(){ 77 | var options = find_options({_id:"ABC123"}, {_id:-1}, 200, 100, 600000); 78 | options.should.eql({ callback:undefined, fields:{_id:-1}, limit:100, query:{_id:"ABC123"}, skip:200, timeout:600000 }); 79 | }); 80 | it('should accept signature: "selector, fields, skip, limit, timeout, callback"', function(){ 81 | var options = find_options({_id:"ABC123"}, {_id:-1}, 200, 100, 600000, cb); 82 | options.should.eql({ callback:cb, fields:{_id:-1}, limit:100, query:{_id:"ABC123"}, skip:200, timeout:600000 }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /lib/cursor.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var debug = require('debug')('mongo-mock:cursor'); 3 | var asyncish = require('../').asyncish; 4 | var sift = require('../sift.js'); 5 | var _ = require('lodash'); 6 | var ObjectId = require('bson-objectid'); 7 | 8 | 9 | var Cursor = module.exports = function(documents, opts) { 10 | debug('initializing cursor'); 11 | var i = 0; 12 | var state = Cursor.INIT; 13 | if(!documents) documents = []; 14 | 15 | function notUndefined(x) { 16 | return typeof x === 'undefined' ? null : x; 17 | } 18 | 19 | function getDocs(applySkipLimit) { 20 | state = Cursor.OPEN; 21 | var docs = documents.filter(sift(opts.query)); 22 | if (opts.sort) { 23 | // partial implementation of mongodb sorting 24 | // https://docs.mongodb.com/manual/reference/bson-type-comparison-order/ 25 | // TODO: Fully implement this (somehow) 26 | docs = docs.sort(function(a,b) { 27 | var retVal = 0; 28 | for (var field in opts.sort) { 29 | var aVal = notUndefined(_.get(a, field)); 30 | var bVal = notUndefined(_.get(b, field)); 31 | 32 | retVal = sortByType(aVal,bVal) || sortByValue(aVal,bVal); 33 | 34 | // apply the order modifier 35 | retVal *= opts.sort[field]; 36 | 37 | if (retVal !== 0) break; // no need to continue; 38 | } 39 | 40 | return retVal; 41 | }); 42 | } 43 | if (opts.each) { 44 | docs.forEach(opts.each); 45 | } 46 | if (opts.map) { 47 | docs = docs.map(opts.map); 48 | } 49 | if (applySkipLimit) { 50 | docs = docs.slice(opts.skip||0, opts.skip+(opts.limit||docs.length)); 51 | } 52 | docs = _.cloneDeepWith(docs, cloneObjectIDs); 53 | 54 | return applyProjection(docs, opts.fields); 55 | } 56 | 57 | var iface = { 58 | cmd: opts, 59 | 60 | batchSize: NotImplemented, 61 | 62 | clone: NotImplemented, 63 | 64 | close: function (callback) { 65 | state = Cursor.CLOSED; 66 | docs = []; 67 | debug('closing cursor'); 68 | iface.emit('close'); 69 | if(callback) return callback(null, iface); 70 | }, 71 | 72 | count: function (applySkipLimit, callback) { 73 | callback = arguments[arguments.length-1]; 74 | applySkipLimit = (applySkipLimit === callback) ? false : applySkipLimit; 75 | if(typeof callback !== 'function') 76 | return Promise.resolve(getDocs(applySkipLimit).length); 77 | 78 | asyncish(function () { 79 | callback(null, getDocs(applySkipLimit).length) 80 | }); 81 | }, 82 | 83 | project: function (toProject) { 84 | _.assign(opts, { 85 | fields: toProject, 86 | }); 87 | return this; 88 | }, 89 | 90 | each: function(fn) { 91 | if(state !== Cursor.INIT) 92 | throw new Error('MongoError: Cursor is closed'); 93 | opts.each = fn; 94 | return this; 95 | }, 96 | 97 | limit: function (n) { 98 | if(state !== Cursor.INIT) 99 | throw new Error('MongoError: Cursor is closed'); 100 | opts.limit = n; 101 | return this; 102 | }, 103 | 104 | next: function (callback) { 105 | var docs = getDocs(true); 106 | var limit = Math.min(opts.limit || Number.MAX_VALUE, docs.length); 107 | var next_idx = i { 119 | _.forEach(filteredDocuments, (document) => { 120 | this.emit('data', document); 121 | }); 122 | this.emit('end'); 123 | }, 1); 124 | }, 125 | 126 | rewind: function () { 127 | i = 0; 128 | }, 129 | 130 | size: function(callback) { 131 | return this.count(true, callback); 132 | }, 133 | 134 | skip: function (n) { 135 | if(state !== Cursor.INIT) 136 | throw new Error('MongoError: Cursor is closed'); 137 | opts.skip = n; 138 | return this; 139 | }, 140 | 141 | sort: function(fields) { 142 | if(state !== Cursor.INIT) 143 | throw new Error('MongoError: Cursor is closed'); 144 | opts.sort = fields; 145 | return this; 146 | }, 147 | 148 | map: function(fn) { 149 | if(state !== Cursor.INIT) 150 | throw new Error('MongoError: Cursor is closed'); 151 | opts.map = fn; 152 | return this; 153 | }, 154 | 155 | toArray: function (callback) { 156 | debug('cursor.toArray()'); 157 | 158 | function done() { 159 | iface.rewind(); 160 | return getDocs(true); 161 | } 162 | 163 | if(!callback) 164 | return Promise.resolve(done()); 165 | 166 | asyncish(function () { 167 | callback(null, done()) 168 | }); 169 | }, 170 | 171 | forEach: function (iterator, callback) { 172 | debug('cursor.forEach()'); 173 | 174 | function done() { 175 | iface.rewind(); 176 | var docs = getDocs(true); 177 | for (var i = 0; i < docs.length; i += 1) { 178 | iterator(docs[i]); 179 | } 180 | } 181 | 182 | if(!callback) 183 | return Promise.resolve(done()); 184 | 185 | asyncish(function () { 186 | callback(null, done()) 187 | }); 188 | }, 189 | 190 | on: function (event, fn) { 191 | debug('cursor.on()'); 192 | switch (event) { 193 | case 'data': { 194 | iface.rewind(); 195 | var documentsToStream = getDocs(true); 196 | this._triggerStream(documentsToStream); 197 | break; 198 | } 199 | } 200 | this.addListener(event, fn); 201 | return this; 202 | }, 203 | 204 | stream: function (options) { 205 | debug('cursor.stream()'); 206 | this.streamOptions = options || {}; 207 | return this; 208 | }, 209 | }; 210 | 211 | iface.__proto__ = EventEmitter.prototype; 212 | 213 | return iface; 214 | }; 215 | 216 | Cursor.INIT = 0; 217 | Cursor.OPEN = 1; 218 | Cursor.CLOSED = 2; 219 | Cursor._applyProjection = applyProjection; //expose for testing, do not reference! 220 | Cursor._getProjectionType = getProjectionType; //expose for testing, do not reference! 221 | 222 | function getProjectionType(fields) { 223 | var values = _.values(_.omit(fields, '_id')); 224 | if (!values.length) return fields._id === 0 ? 'omit' : 'pick'; 225 | 226 | var sum = _.sum(values); 227 | if (sum !== 0 && sum !== values.length) 228 | throw new Error('Mixed projections types not allowed'); 229 | return sum > 0 ? 'pick' : 'omit' 230 | } 231 | function applyProjection(docs, fields) { 232 | if(!docs.length || _.isEmpty(fields)) 233 | return docs; 234 | 235 | var props = Object.keys(fields); 236 | var type = getProjectionType(fields); 237 | var _id = fields._id; 238 | // handle special rules for _id 239 | if ((type === 'pick' && _id === 0) || (type === 'omit' && _id === 1)) { 240 | props = _.without(props, '_id') 241 | } 242 | else if (type === 'pick' && !('_id' in fields)) { 243 | props.push('_id'); 244 | } 245 | return docs.map(function (doc) { 246 | //only supports simple projections. Lodash v4 supports it. PRs welcome! :) 247 | return _[type](doc, props); 248 | }); 249 | } 250 | 251 | 252 | function NotImplemented(){ 253 | throw Error('Not Implemented'); 254 | } 255 | 256 | function cloneObjectIDs(value) { 257 | return value instanceof ObjectId? ObjectId(value) : undefined; 258 | } 259 | 260 | function sortByType(a, b) { 261 | return guessTypeSort(a) - guessTypeSort(b); 262 | } 263 | 264 | function sortByValue(a, b) { 265 | if (a < b) return -1; 266 | else if (b < a) return 1; 267 | return 0; 268 | } 269 | 270 | // https://docs.mongodb.com/manual/reference/bson-type-comparison-order/ 271 | function guessTypeSort(value) { 272 | if (value === null || value === undefined) return 2; 273 | 274 | var type = typeof value; 275 | switch (type) { 276 | case 'number': return 3; 277 | case 'string': return 4; 278 | case 'boolean': return 9; 279 | case 'object': 280 | if (Array.isArray(value)) { 281 | // A comparison of an empty array (e.g. [ ]) treats the empty array as less than null or a missing field. 282 | if (value.length === 0) return 1; 283 | else return 6; 284 | } else if (value instanceof Date) return 10; 285 | else if (value instanceof RegExp) return 12; 286 | else if (value instanceof ObjectId) return 8; 287 | else return 5; 288 | } 289 | 290 | return 13; 291 | } 292 | -------------------------------------------------------------------------------- /lib/db.js: -------------------------------------------------------------------------------- 1 | var asyncish = require('../').asyncish; 2 | var EventEmitter = require('events').EventEmitter; 3 | var debug = require('debug')('mongo-mock:db'); 4 | var Collection = require('./collection.js'); 5 | var ObjectId = require('bson-objectid'); 6 | var _ = require('lodash'); 7 | 8 | var Db = module.exports = function(dbname, server) { 9 | var badguy = /[ .$\/\\]/.exec(dbname); 10 | if(badguy) throw new Error("database names cannot contain the character '" + badguy[0] + "'"); 11 | var open = false; 12 | 13 | var iface = { 14 | get databaseName() { return dbname; }, 15 | addUser: NotImplemented, 16 | admin: NotImplemented, 17 | authenticate: NotImplemented, 18 | close: function(force, callback) { 19 | callback = arguments[arguments.length-1]; 20 | if(typeof callback !== "function") callback = undefined; 21 | //else if(callback===force) force = false; 22 | 23 | iface.emit('close'); 24 | iface.removeAllListeners('close'); 25 | 26 | debug('closing %s', dbname); 27 | clearInterval(open); 28 | open = false; 29 | 30 | if(callback) callback(); 31 | else return Promise.resolve(); 32 | }, 33 | collection: function(name, options, callback) { 34 | callback = arguments[arguments.length - 1]; 35 | if(typeof callback !== 'function') callback = undefined; 36 | if(!options || callback===options) options = {}; 37 | 38 | if(options.strict && !callback) 39 | throw Error("A callback is required in strict mode. While getting collection " + name); 40 | 41 | var collection = getCollection(name); 42 | 43 | if(options.strict && !collection.documents) 44 | return callback(Error("Collection "+name+" does not exist. Currently in strict mode.")); 45 | 46 | if(callback) callback(null, collection); 47 | return collection; 48 | }, 49 | collections: NotImplemented, 50 | command: NotImplemented, 51 | createCollection: function (name, options, callback) { 52 | if(!name) throw Error('name is mandatory'); 53 | callback = arguments[arguments.length - 1]; 54 | if(typeof options !== 'object') options = {}; 55 | 56 | debug('createCollection("%s")', name); 57 | asyncish(function () { 58 | var collection = getCollection(name); 59 | if(collection.documents) { 60 | debug('createCollection("%s") - collection exists', name); 61 | if(options.strict) 62 | return callback && callback(new Error("Collection " + name + " already exists. Currently in strict mode.")); 63 | return collection; 64 | } 65 | 66 | debug('createCollection("%s") - materializing collection', name); 67 | collection.persist(options.autoIndexId); 68 | 69 | callback(null, collection); 70 | }); 71 | 72 | if(typeof callback!=='function') { 73 | return new Promise(function (resolve) { 74 | callback = function (e, r) { resolve(r) }; 75 | }) 76 | } 77 | }, 78 | createIndex: function (name, keys, options, callback) { 79 | callback = arguments[arguments.length-1]; 80 | if(typeof options !== 'object') options = {}; 81 | if(typeof callback!=='function') { 82 | var promise = new Promise(function(resolve){ 83 | callback = function(e,r){ resolve(r) }; 84 | }); 85 | } 86 | if(typeof keys === 'string') keys = keyify(keys); 87 | 88 | debug('createIndex("%s", %j, %j)', name, keys, options); 89 | var ns = dbname+'.'+name; 90 | var index = _.find(indexes, {key:keys, ns:ns}) || (options.name && _.find(indexes, {name:options.name})); 91 | if(index) { 92 | //the behavior is to ignore if it exists 93 | callback(null, index.name || options.name); 94 | return promise; 95 | } 96 | 97 | index = _.extend({}, options); 98 | if(index.v && index.v!==1) throw new Error("`v` not supported"); 99 | if(index.dropDups) throw new Error("`dropDups` not supported. PR welcome!"); 100 | if(index.unique!==true && name !== '_id_') { 101 | index.unique = false; 102 | } 103 | if(!index.name) 104 | index.name = Object.keys(keys).map(function(k){return k+'_'+keys[k]}).join('_'); 105 | index.v = 1; 106 | index.key = keys; 107 | index.ns = ns; 108 | 109 | iface.createCollection(name, {}, function (err, collection) { 110 | if(index.name !== '_id_' && !_.isEqual(keys, {_id:1})) 111 | indexes.push(index); 112 | collection.persist(); 113 | callback(null, index.name); 114 | }); 115 | return promise; 116 | }, 117 | db: function (newDbName, opts) { 118 | var otherDb = new Db(newDbName, server); 119 | // start the interval and just ignore it 120 | otherDb.open(noop); 121 | return otherDb; 122 | }, 123 | dropCollection: function(name, callback) { 124 | iface.collection(Db.SYSTEM_NAMESPACE_COLLECTION).deleteOne({name:name}, function (err, result) { 125 | if (!result.deletedCount) return callback(new Error('ns not found')); 126 | 127 | var ns = dbname+'.'+name; 128 | iface.collection(Db.SYSTEM_INDEX_COLLECTION).deleteMany({ns:ns}, function (err, result) { 129 | var removed = !!_.remove(backingstore.collections || [], {name:name} ).length; 130 | if(removed) { 131 | server.persist(); 132 | } 133 | callback(null, removed); 134 | }); 135 | }); 136 | if(typeof callback!=='function') { 137 | return new Promise(function(resolve, reject){ 138 | callback = function(e, r){ 139 | if(e) return reject(e); 140 | return resolve(r); 141 | }; 142 | }); 143 | } 144 | }, 145 | dropDatabase: NotImplemented, 146 | ensureIndex: NotImplemented, 147 | eval: NotImplemented, 148 | executeDbAdminCommand: NotImplemented, 149 | indexInformation: function (name, options, callback) { 150 | callback = arguments[arguments.length-1]; 151 | if(typeof options !== 'object') options = {}; 152 | if(!options.full) throw Error('only `options.full` is supported. PR welcome!'); 153 | 154 | if(typeof callback!=='function') { 155 | return new Promise(function (resolve) { 156 | callback = function (e, r) { resolve(r) }; 157 | }) 158 | } 159 | callback(null, _.filter(indexes, { ns:dbname+'.'+name })); 160 | }, 161 | isConnected: function(options) { 162 | return true; 163 | }, 164 | listCollections: function(filter, options) { 165 | debug('listCollections(%j)', filter); 166 | return iface.collection(Db.SYSTEM_NAMESPACE_COLLECTION).find(filter); 167 | }, 168 | collectionNames: function() { 169 | return _.map(backingstore.collections, function(c) { return c.name; }); 170 | }, 171 | logout: NotImplemented, 172 | open: function(callback) { 173 | asyncish(function () { 174 | if(!open) { 175 | debug('%s open', dbname); 176 | //keep the process running like a live connection would 177 | open = setInterval(function () {}, 600000); 178 | } 179 | callback(null, iface); 180 | }); 181 | if(typeof callback!=='function') { 182 | return new Promise(function (resolve) { 183 | callback = function (e, r) { resolve(r) }; 184 | }) 185 | } 186 | }, 187 | removeUser: NotImplemented, 188 | renameCollection: NotImplemented, 189 | stats: NotImplemented, 190 | toJSON: function () { 191 | return backingstore; 192 | } 193 | }; 194 | iface.__proto__ = EventEmitter.prototype; 195 | 196 | function clearCollections() { 197 | backingstore.collections.clear(); 198 | } 199 | 200 | function getCollection(name) { 201 | var instance = _.find(backingstore.collections, {name:name} ); 202 | if(instance) return instance; 203 | 204 | var state = new CollectionState(name); 205 | state.persist = function materialize(autoIndexId) { 206 | if(!state.documents) state.documents = []; 207 | if(autoIndexId !== false) { 208 | debug('%s persist() - creating _id index', name); 209 | indexes.push({ v:1, key:{_id:1}, ns:dbname+'.'+name, name:"_id_", unique:true }); 210 | } 211 | //registering it in the namespaces makes it legit 212 | namespaces.push({ name:name }); 213 | 214 | //now that it is materialized, remove this function 215 | delete state.persist; 216 | 217 | //call the prototype's version 218 | state.persist(); 219 | }; 220 | 221 | instance = Collection(iface, state); 222 | backingstore.collections.push(instance); 223 | return instance; 224 | } 225 | 226 | function CollectionState(name, documents, pk) { 227 | this.name = name; 228 | this.documents = documents; 229 | this.pkFactory = pk || ObjectId; 230 | } 231 | CollectionState.prototype = { 232 | persist: server.persist, 233 | findConflict: function (data, original) { 234 | var documents = this.documents; 235 | if(!documents) return; 236 | 237 | var ns = dbname+'.'+this.name; 238 | var idxs = _.filter(indexes, {ns:ns, unique:true}); 239 | for (var i = 0; i < idxs.length; i++) { 240 | var index = idxs[i]; 241 | var keys = Object.keys(index.key); 242 | keys.forEach(function (key) { 243 | if(!data.hasOwnProperty(key)) data[key] = undefined; 244 | }); 245 | var query = _.pick(data, keys); 246 | var conflict = _.find(documents, query); 247 | if(conflict && conflict!==original) 248 | return indexError(index, i); 249 | } 250 | }, 251 | toJSON: function () { 252 | if(!this.documents) return; 253 | return { name:this.name, documents:this.documents }; 254 | } 255 | }; 256 | 257 | function create_backingstore(db) { 258 | return { 259 | collections: [ 260 | Collection(db, new CollectionState(Db.SYSTEM_NAMESPACE_COLLECTION, [{name:Db.SYSTEM_INDEX_COLLECTION}], noop)), 261 | Collection(db, new CollectionState(Db.SYSTEM_INDEX_COLLECTION, [], noop)) 262 | ] 263 | }; 264 | } 265 | 266 | 267 | var open = false; 268 | var backingstore = server.databases[dbname] || (server.databases[dbname] = create_backingstore(iface)); 269 | var namespaces = getCollection(Db.SYSTEM_NAMESPACE_COLLECTION).toJSON().documents; 270 | var indexes = getCollection(Db.SYSTEM_INDEX_COLLECTION).toJSON().documents; 271 | return iface; 272 | }; 273 | function noop(){} 274 | 275 | 276 | function NotImplemented(){ 277 | throw Error('Not Implemented. PR welcome!'); 278 | } 279 | function indexError(index, i) { 280 | var err = new Error('E11000 duplicate key error index: ' + index.ns +'.$'+ index.name); 281 | err.name = 'MongoError'; 282 | err.ok = 1; 283 | err.n = 1; 284 | err.code = 11000; 285 | err.errmsg = err.message; 286 | err.writeErrors = [{ 287 | index: i, 288 | code: 11000, 289 | errmsg: err.message 290 | }]; 291 | return err; 292 | } 293 | function keyify(key) { 294 | var out = {}; 295 | out[key] = 1; 296 | return out; 297 | } 298 | 299 | // Constants 300 | Db.SYSTEM_NAMESPACE_COLLECTION = "system.namespaces"; 301 | Db.SYSTEM_INDEX_COLLECTION = "system.indexes"; 302 | Db.SYSTEM_PROFILE_COLLECTION = "system.profile"; 303 | Db.SYSTEM_USER_COLLECTION = "system.users"; 304 | -------------------------------------------------------------------------------- /lib/collection.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var _ = require('lodash'); 3 | var objectAssignDeep = require('object-assign-deep'); 4 | 5 | var ObjectId = require('bson-objectid'); 6 | var debug = require('debug')('mongo-mock:collection'); 7 | var asyncish = require('../').asyncish; 8 | var cursor = require('./cursor.js'); 9 | var modifyjs = require('modifyjs'); 10 | var bulk = require('./bulk.js'); 11 | var find_options = require('./find_options.js'); 12 | 13 | var sift = require('../sift.js'); 14 | 15 | function addToSet(array, other) { 16 | other = _.isArray(other) ? other : [other]; 17 | 18 | var index = -1, 19 | length = array.length, 20 | othIndex = -1, 21 | othLength = other.length, 22 | result = Array(length); 23 | 24 | while (++index < length) { 25 | result[index] = array[index]; 26 | } 27 | while (++othIndex < othLength) { 28 | if (_.indexOf(array, other[othIndex]) < 0) { 29 | result.push(other[othIndex]); 30 | } 31 | } 32 | return result; 33 | }; 34 | 35 | module.exports = function Collection(db, state) { 36 | var name = state.name; 37 | var pk = state.pkFactory || ObjectId; 38 | debug('initializing instance of `%s` with %s documents', name, state.documents ? state.documents.length : undefined); 39 | 40 | var iface = { 41 | get collectionName() { return name; }, 42 | get hit() { NotImplemented() }, 43 | get name() { return name; }, 44 | get namespace() { return db.databaseName + '.' + name; }, 45 | get writeConcern() { NotImplemented() }, 46 | 47 | aggregate: NotImplemented, 48 | bulkWrite: function (operations, options, callback) { 49 | const promises = [] 50 | 51 | for (const operation of operations) { 52 | let promise 53 | 54 | // Determine which operation to forward to 55 | if (operation.insertOne) { 56 | const { document } = operation.insertOne 57 | promise = this.insertOne(document, options) 58 | } else if (operation.updateOne) { 59 | const { filter, update, ...opts } = operation.updateOne 60 | promise = this.updateOne(filter, update, { ...options, ...opts }) 61 | } else if (operation.updateMany) { 62 | const { filter, update, ...opts } = operation.updateMany 63 | promise = this.updateMany(filter, update, { ...options, ...opts }) 64 | } else if (operation.deleteOne) { 65 | const { filter } = operation.deleteOne 66 | promise = this.deleteOne(filter, options) 67 | } else if (operation.deleteMany) { 68 | const { filter } = operation.deleteMany 69 | promise = this.deleteMany(filter, options) 70 | } else if (operation.replaceOne) { 71 | const { filter, replacement, ...opts } = operation.replaceOne 72 | promise = this.replaceOne(filter, replacement, { ...options, ...opts }) 73 | } else { 74 | throw Error('bulkWrite only supports insertOne, updateOne, updateMany, deleteOne, deleteMany') 75 | } 76 | 77 | // Add the operation results to the list 78 | promises.push(promise) 79 | } 80 | 81 | Promise.all(promises).then(function(values) { 82 | // Loop through all operation results, and aggregate 83 | // the result object 84 | let ops = [] 85 | let n = 0 86 | for (const value of values) { 87 | if (value.insertedId || value.insertedIds) { 88 | ops = [...ops, ...value.ops] 89 | } 90 | n += value.result.n 91 | } 92 | 93 | callback(null, { 94 | ops, 95 | connection: db, 96 | result: { 97 | ok: 1, 98 | n 99 | } 100 | }) 101 | }).catch(function (error) { 102 | callback(error, null) 103 | }) 104 | 105 | if (typeof callback !== 'function') { 106 | return new Promise(function(resolve, reject) { 107 | callback = function(e, r) { e ? reject(e) : resolve(r); }; 108 | }); 109 | } 110 | }, 111 | count: count, 112 | countDocuments: count, 113 | estimatedDocumentCount: function(options, callback){ 114 | return this.find({}, options).count(callback); 115 | }, 116 | createIndex: function (keys, options, callback) { 117 | return db.createIndex(name, keys, options, callback); 118 | }, 119 | createIndexes: NotImplemented, 120 | deleteMany: function (filter, options, callback) { 121 | callback = arguments[arguments.length - 1]; 122 | debug('deleteMany %j', filter); 123 | 124 | const opts = find_options(arguments); 125 | asyncish(function () { 126 | cursor(state.documents || [], opts).toArray((err, docsToRemove) => { 127 | debug('docs', docsToRemove); 128 | if (docsToRemove.length) { 129 | debug(state.documents.length); 130 | debug(docsToRemove.length); 131 | const idsToRemove = _.map(docsToRemove, '_id'); 132 | _.remove(state.documents || [], document => _.includes(idsToRemove, document._id)); 133 | debug(state.documents.length); 134 | 135 | // debug(documentsLeft); 136 | if (debug.enabled) debug("removed: " + docsToRemove.map(function (doc) { return doc._id; })); 137 | state.persist(); 138 | } 139 | callback(null, { result: { n: docsToRemove.length, ok: 1 }, deletedCount: docsToRemove.length, connection: db }); 140 | }); 141 | }); 142 | 143 | if (typeof callback !== 'function') { 144 | return new Promise(function(resolve, reject) { 145 | callback = function(e, r) { e ? reject(e) : resolve(r); }; 146 | }); 147 | } 148 | }, 149 | deleteOne: function (filter, options, callback) { 150 | callback = arguments[arguments.length - 1]; 151 | 152 | debug('deleteOne %j', filter); 153 | 154 | asyncish(function () { 155 | var deletionIndex = _.findIndex(state.documents || [], filter); 156 | var docs = deletionIndex === -1 ? [] : state.documents.splice(deletionIndex, 1); 157 | 158 | if (deletionIndex > -1) { 159 | if (debug.enabled) debug("removed: " + docs.map(function (doc) { return doc._id; })); 160 | state.persist(); 161 | } 162 | callback(null, { result: { n: docs.length, ok: 1 }, deletedCount: docs.length, connection: db }); 163 | }); 164 | if (typeof callback !== 'function') { 165 | return new Promise(function (resolve, reject) { 166 | callback = function (e, r) { e ? reject(e) : resolve(r) }; 167 | }) 168 | } 169 | }, 170 | distinct: NotImplemented, 171 | drop: function (callback) { 172 | return db.dropCollection(name, callback); 173 | }, 174 | dropIndex: NotImplemented, 175 | dropIndexes: NotImplemented, 176 | ensureIndex: function (fieldOrSpec, options, callback) { return this.createIndex(fieldOrSpec, options, callback); }, 177 | find: function () { 178 | var opts = find_options(arguments); 179 | debug('find %j callback=%s', opts, typeof opts.callback); 180 | 181 | var crsr = cursor(state.documents, opts); 182 | 183 | if (!opts.callback) 184 | return crsr; 185 | 186 | asyncish(function () { 187 | opts.callback(null, crsr); 188 | }); 189 | }, 190 | findAndModify: NotImplemented, 191 | findAndRemove: NotImplemented, 192 | findOne: function () { 193 | var opts = find_options(arguments); 194 | debug('findOne %j callback=%s', opts, typeof opts.callback); 195 | 196 | var crsr = cursor(state.documents, opts); 197 | 198 | if (!opts.callback) 199 | return crsr.next(); 200 | 201 | crsr.next().then(function (doc) { 202 | opts.callback(null, doc); 203 | }); 204 | }, 205 | findOneAndDelete: NotImplemented, 206 | findOneAndReplace: NotImplemented, 207 | findOneAndUpdate: function (selector, data, options, callback) { 208 | callback = arguments[arguments.length-1]; 209 | if(typeof options!=='object') options = {}; 210 | var self = this; 211 | this.updateOne(selector, data, options) 212 | .then(function (opResult) { 213 | let findResult = { 214 | ok: 1, 215 | lastErrorObject: null 216 | }; 217 | var findQuery = { _id: opResult.upsertedId }; 218 | if (options.upsert === true && opResult.upsertedId._id) findQuery._id = opResult.upsertedId._id; 219 | self.findOne(findQuery) 220 | .catch(callback) 221 | .then(function (doc) { 222 | findResult.value = doc; 223 | if (options.upsert) { 224 | findResult.lastErrorObject = { 225 | n: 1, 226 | updatedExisting: !opResult.upsertedCount, 227 | }; 228 | if (!!opResult.upsertedCount) findResult.lastErrorObject.upserted = opResult.upsertedId._id; 229 | } 230 | callback(null, findResult); 231 | }); 232 | }) 233 | .catch(callback); 234 | 235 | if (typeof callback!=='function') { 236 | return new Promise(function (resolve,reject) { 237 | callback = function (e, r) { e? reject(e) : resolve(r) }; 238 | }) 239 | } 240 | }, 241 | geoHaystackSearch: NotImplemented, 242 | geoNear: NotImplemented, 243 | group: NotImplemented, 244 | indexExists: NotImplemented, 245 | indexInformation: function (options, callback) { 246 | return db.indexInformation(name, options, callback); 247 | }, 248 | indexes: NotImplemented, 249 | initializeOrderedBulkOp: function () { 250 | return new bulk(this, true); 251 | }, 252 | initializeUnorderedBulkOp: function () { 253 | return new bulk(this, false); 254 | }, 255 | insert: function (docs, options, callback) { 256 | debug('insert %j', docs); 257 | callback = arguments[arguments.length - 1]; 258 | //if(callback===options) options = {};//ignored when mocking 259 | if (!Array.isArray(docs)) 260 | docs = [docs]; 261 | if (name === 'system.indexes') return iface.createIndexes(docs, callback) 262 | 263 | //make copies to break refs to the persisted docs 264 | docs = _.cloneDeepWith(docs, cloneObjectIDs); 265 | 266 | //The observed behavior of `mongodb` is that documents 267 | // are committed until the first error. No information 268 | // about the successful inserts are return :/ 269 | asyncish(function () { 270 | var insertedIds = []; 271 | for (var i = 0; i < docs.length; i++) { 272 | var doc = docs[i]; 273 | if (!doc._id) doc._id = pk(); 274 | 275 | var conflict = state.findConflict(doc); 276 | if (conflict) { 277 | state.persist(); 278 | return callback(conflict); 279 | } 280 | 281 | if (!state.documents) state.documents = [doc]; 282 | else state.documents.push(doc); 283 | 284 | insertedIds.push(doc._id) 285 | } 286 | 287 | state.persist(); 288 | callback(null, { 289 | insertedIds: insertedIds, 290 | insertedCount: docs.length, 291 | result: { ok: 1, n: docs.length }, 292 | connection: {}, 293 | ops: _.cloneDeepWith(docs, cloneObjectIDs) 294 | }); 295 | }); 296 | if (typeof callback !== 'function') { 297 | return new Promise(function (resolve, reject) { 298 | callback = function (e, r) { e ? reject(e) : resolve(r) }; 299 | }) 300 | } 301 | }, 302 | get insertMany() { return this.insert; }, 303 | insertOne: function (doc, options, callback) { 304 | callback = arguments[arguments.length - 1]; 305 | 306 | this.insert([doc], options, function (e, r) { 307 | if (e) return callback(e) 308 | callback(null, { 309 | insertedId: r.insertedIds[0], 310 | insertedCount: r.result.n, 311 | result: r.result, 312 | connection: r.connection, 313 | ops: r.ops 314 | }) 315 | }) 316 | 317 | if (typeof callback !== 'function') { 318 | return new Promise(function (resolve, reject) { 319 | callback = function (e, r) { e ? reject(e) : resolve(r) }; 320 | }) 321 | } 322 | }, 323 | isCapped: NotImplemented, 324 | listIndexes: NotImplemented, 325 | mapReduce: NotImplemented, 326 | options: NotImplemented, 327 | parallelCollectionScan: NotImplemented, 328 | persist: function () { 329 | //this is one of the very few functions that are unique 330 | // to the `mock-mongo` interface. It causes a collection 331 | // to be materialized and the data to be persisted to disk. 332 | state.persist(); 333 | }, 334 | // reIndex: NotImplemented, 335 | remove: function (selector, options, callback) { 336 | callback = arguments[arguments.length - 1]; 337 | 338 | debug('remove %j', selector); 339 | 340 | asyncish(function () { 341 | var docs = _.remove(state.documents || [], selector); 342 | if (docs.length) { 343 | if (debug.enabled) debug("removed: " + docs.map(function (doc) { return doc._id; })); 344 | state.persist(); 345 | } 346 | callback(null, {result:{n:docs.length,ok:1}, ops:docs, connection:db}); 347 | }); 348 | if (typeof callback !== 'function') { 349 | return new Promise(function (resolve, reject) { 350 | callback = function (e, r) { e ? reject(e) : resolve(r) }; 351 | }) 352 | } 353 | }, 354 | rename: NotImplemented, 355 | replaceOne: NotImplemented, 356 | save: function (doc, options, callback) { 357 | callback = arguments[arguments.length-1]; 358 | if(typeof options!=='object') options = {}; 359 | return this.update({ _id: doc._id }, doc, Object.assign({}, options, { upsert: true }), callback) 360 | }, 361 | stats: NotImplemented, 362 | update: function (selector, data, options, callback) { 363 | callback = arguments[arguments.length - 1]; 364 | if (typeof options !== 'object') options = {}; 365 | 366 | var opResult = { 367 | result: { 368 | ok: 1, 369 | nModified: 0, 370 | n: 0 371 | }, 372 | connection:db 373 | }; 374 | var action = (options.upsert?"upsert: ":"update: "); 375 | debug('%s.%s %j', name, action, selector); 376 | 377 | asyncish(function () { 378 | var docs = state.documents || []; 379 | if (options.multi) { 380 | docs = docs.filter(sift(selector)) 381 | } 382 | else { 383 | docs = first(selector, docs) || [] 384 | } 385 | if (!Array.isArray(docs)) docs = [docs]; 386 | debug('%s.%s %j', name, action, docs); 387 | 388 | if(!docs.length && options.upsert) { 389 | var cloneData = upsertClone(selector, data); 390 | var cloned = _.cloneDeepWith(cloneData, cloneObjectIDs); 391 | cloned._id = selector._id || pk(); 392 | 393 | debug('%s.%s checking for index conflict', name, action); 394 | var conflict = state.findConflict(cloned); 395 | if (conflict) { 396 | debug('conflict found %j', conflict); 397 | return callback(conflict); 398 | } 399 | 400 | if (!state.documents) state.documents = [cloned]; 401 | else state.documents.push(cloned); 402 | 403 | opResult.result.n = 1; 404 | opResult.result.nModified = 1; 405 | opResult.ops = [cloned]; 406 | } 407 | else { 408 | debug('%s.%s checking for index conflicts', name, action); 409 | for (var i = 0; i < docs.length; i++) { 410 | var conflict = modify(docs[i], data, state); 411 | if (conflict) return callback(conflict); 412 | } 413 | opResult.result.n = docs.length; 414 | opResult.result.nModified = docs.length; 415 | } 416 | 417 | state.persist(); 418 | callback(null, opResult); 419 | }); 420 | 421 | if (typeof callback !== 'function') { 422 | return new Promise(function (resolve, reject) { 423 | callback = function (e, r) { e ? reject(e) : resolve(r) }; 424 | }) 425 | } 426 | }, 427 | updateMany: function (selector, data, options, callback) { 428 | callback = arguments[arguments.length - 1]; 429 | if (typeof options !== 'object') options = {}; 430 | 431 | var opResult = { 432 | result: { 433 | ok: 1, 434 | nModified: 0, 435 | n: 0 436 | }, 437 | connection: db, 438 | matchedCount: 0, 439 | modifiedCount: 0, 440 | upsertedCount: 0, 441 | upsertedId: null 442 | }; 443 | var action = (options.upsert ? "upsert: " : "update: "); 444 | debug('%s.%s %j', name, action, selector); 445 | 446 | asyncish(function () { 447 | var docs = (state.documents || []).filter(sift(selector)); 448 | if (!Array.isArray(docs)) docs = [docs]; 449 | debug('%s.%s %j', name, action, docs); 450 | 451 | if(!docs.length && options.upsert) { 452 | var cloneData = upsertClone(selector, data); 453 | var cloned = _.cloneDeepWith(cloneData, cloneObjectIDs); 454 | cloned._id = selector._id || pk(); 455 | 456 | debug('%s.%s checking for index conflict', name, action); 457 | var conflict = state.findConflict(cloned); 458 | if (conflict) { 459 | debug('conflict found %j', conflict); 460 | return callback(conflict); 461 | } 462 | 463 | if (!state.documents) state.documents = [cloned]; 464 | else state.documents.push(cloned); 465 | 466 | opResult.matchedCount = opResult.result.n = 1; 467 | opResult.upsertedCount = opResult.result.nModified = 1; 468 | opResult.upsertedId = { _id: cloned._id }; 469 | } 470 | else { 471 | debug('%s.%s checking for index conflicts', name, action); 472 | for (var i = 0; i < docs.length; i++) { 473 | var conflict = modify(docs[i], data, state); 474 | if (conflict) return callback(conflict); 475 | } 476 | opResult.matchedCount = opResult.result.n = docs.length; 477 | opResult.modifiedCount = opResult.result.nModified = docs.length; 478 | } 479 | 480 | state.persist(); 481 | callback(null, opResult); 482 | }); 483 | 484 | if (typeof callback !== 'function') { 485 | return new Promise(function (resolve, reject) { 486 | callback = function (e, r) { e ? reject(e) : resolve(r) }; 487 | }) 488 | } 489 | }, 490 | updateOne: function (selector, data, options, callback) { 491 | callback = arguments[arguments.length - 1]; 492 | if (typeof options !== 'object') options = {}; 493 | 494 | var opResult = { 495 | result: { 496 | ok: 1, 497 | nModified: 0, 498 | n: 0 499 | }, 500 | connection: db, 501 | matchedCount: 0, 502 | modifiedCount: 0, 503 | upsertedCount: 0, 504 | upsertedId: null 505 | }; 506 | var action = (options.upsert ? "upsert: " : "update: "); 507 | debug('%s.%s %j', name, action, selector); 508 | 509 | asyncish(function () { 510 | var docs = first(selector, state.documents || []) || []; 511 | if (!Array.isArray(docs)) docs = [docs]; 512 | debug('%s.%s %j', name, action, docs); 513 | 514 | 515 | if(!docs.length && options.upsert) { 516 | var cloneData = upsertClone(selector, data); 517 | var cloned = _.cloneDeepWith(cloneData, cloneObjectIDs); 518 | cloned._id = cloned._id || pk(); 519 | 520 | debug('%s.%s checking for index conflict', name, action); 521 | var conflict = state.findConflict(cloned); 522 | if (conflict) { 523 | debug('conflict found %j', conflict); 524 | return callback(conflict); 525 | } 526 | 527 | if (!state.documents) state.documents = [cloned]; 528 | else state.documents.push(cloned); 529 | 530 | opResult.matchedCount = opResult.result.n = 1; 531 | opResult.upsertedCount = opResult.result.nModified = 1; 532 | opResult.upsertedId = { _id: cloned._id }; 533 | } 534 | else if (docs.length > 0) { 535 | debug('%s.%s checking for index conflicts', name, action); 536 | var conflict = modify(docs[0], data, state); 537 | if (conflict) return callback(conflict); 538 | opResult.matchedCount = opResult.result.n = docs.length; 539 | opResult.modifiedCount = opResult.result.nModified = docs.length; 540 | opResult.upsertedId = docs[0]._id; 541 | } 542 | 543 | state.persist(); 544 | callback(null, opResult); 545 | }); 546 | 547 | if (typeof callback !== 'function') { 548 | return new Promise(function (resolve, reject) { 549 | callback = function (e, r) { e ? reject(e) : resolve(r) }; 550 | }) 551 | } 552 | }, 553 | 554 | toJSON: function () { 555 | return state; 556 | } 557 | }; 558 | iface.removeOne = iface.deleteOne; 559 | iface.removeMany = iface.deleteMany; 560 | iface.dropAllIndexes = iface.dropIndexes; 561 | return iface; 562 | }; 563 | function modify(original, updates, state) { 564 | var updated = modifyjs(original, updates); 565 | updated._id = original._id; 566 | var conflict = state.findConflict(updated, original); 567 | if (conflict) { 568 | debug('conflict found %j', conflict); 569 | return conflict; 570 | } 571 | // remove unset properties 572 | if (typeof updates.$unset === "object") { 573 | Object.keys(updates.$unset).forEach(function (k) { 574 | _.unset(original, k); 575 | }); 576 | } 577 | _.assign(original, updated); 578 | } 579 | function NotImplemented() { 580 | throw Error('Not Implemented'); 581 | } 582 | function cloneObjectIDs(value) { 583 | return value instanceof ObjectId ? ObjectId(value) : undefined; 584 | } 585 | function restoreObjectIDs(originalValue, updatedValue) { 586 | return updatedValue && updatedValue.constructor.name === 'ObjectID' && updatedValue.id ? ObjectId(updatedValue.id) : undefined; 587 | } 588 | 589 | function isOperator(key) { 590 | return key.length > 0 && key[0] === '$'; 591 | } 592 | 593 | function isPlainObject(value) { 594 | return value !== null && typeof value === 'object' && !Array.isArray(value); 595 | } 596 | 597 | function isProducingEmptyObject(obj) { 598 | assert(isPlainObject(obj), 'Invalid "obj" argument. Must be a plain object.'); 599 | 600 | for (const key of Object.keys(obj)) { 601 | if (key === '$and' || !isOperator(key)) { 602 | return false; 603 | } 604 | } 605 | 606 | return true; 607 | } 608 | 609 | function operatorArrayToNormalizedObject(array, result) { 610 | assert(Array.isArray(array), 'Invalid "array" argument. Must be an array.'); 611 | 612 | for (const item of array) { 613 | assert(isPlainObject(item), 'MongoError: $or/$and/$nor entries need to be full objects.'); 614 | 615 | for (const itemKey of Object.keys(item)) { 616 | assert(!Boolean(result[itemKey]), `MongoError: cannot infer query fields to set, path '${itemKey}' is matched twice.`); 617 | } 618 | 619 | normalizeSelectorToData(item, result); 620 | } 621 | } 622 | 623 | /* 624 | Normalizing a selector object to data object here means flattening $and operators, 625 | getting rid of $or and $nor operators, conserving the structure when needed. 626 | */ 627 | function normalizeSelectorToData(obj, result) { 628 | result = result || {}; 629 | 630 | assert(isPlainObject(obj), 'Invalid "obj" argument. Must be a plain object.'); 631 | 632 | for (const key of Object.keys(obj)) { 633 | const val = obj[key]; 634 | 635 | // Normalize the $and operator array. 636 | if (key === '$and') { 637 | operatorArrayToNormalizedObject(val, result); // Merge into result. 638 | continue; 639 | } 640 | 641 | // Skip other operators ($or and $nor). 642 | if (isOperator(key)) { 643 | continue; 644 | } 645 | 646 | // Process non plain objects as is. 647 | if (!isPlainObject(val)) { 648 | result[key] = val; 649 | continue; 650 | } 651 | 652 | // Ensure processed object would still be meaningful. 653 | if (!isProducingEmptyObject(val)) { 654 | result[key] = normalizeSelectorToData(val); 655 | } 656 | } 657 | 658 | return result; 659 | } 660 | 661 | function upsertClone (selector, data) { 662 | if (data.$setOnInsert) { 663 | var dataToClone = {}; 664 | dataToClone.$set = objectAssignDeep({}, data.$set, data.$setOnInsert); 665 | selector = normalizeSelectorToData(selector); 666 | return objectAssignDeep({}, modifyjs({}, selector || {}), modifyjs({}, dataToClone)); 667 | } 668 | selector = normalizeSelectorToData(selector); 669 | return objectAssignDeep({}, modifyjs({}, data || {}), modifyjs({}, selector || {})); 670 | } 671 | 672 | function first(query, collection) { 673 | return collection[collection.findIndex(sift(query))]; 674 | } 675 | 676 | function count() { 677 | var opts = find_options(arguments); 678 | return this.find(opts.query || {}, opts).count(opts.callback); 679 | } 680 | 681 | -------------------------------------------------------------------------------- /test/mock.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var should = require('should'); 3 | var _ = require('lodash'); 4 | var mongo = require('../'); 5 | var MongoClient = mongo.MongoClient; 6 | var ObjectID = mongo.ObjectID; 7 | var id = ObjectID(); 8 | MongoClient.persist = "mongo.js"; 9 | 10 | // this number is used in all the query/find tests, so it's easier to add more docs 11 | var EXPECTED_TOTAL_TEST_DOCS = 13; 12 | 13 | describe('mock tests', function () { 14 | var connected_db; 15 | var collection; 16 | 17 | before(function (done) { 18 | MongoClient.connect("mongodb://someserver/mock_database", function(err, db) { 19 | connected_db = db; 20 | collection = connected_db.collection("users"); 21 | done(); 22 | }); 23 | }); 24 | after(function(done) { 25 | connected_db.close().then(done).catch(done) 26 | }); 27 | 28 | 29 | describe('databases', function() { 30 | it('should list collections', function(done) { 31 | var listCollectionName = "test_databases_listCollections_collection"; 32 | connected_db.createCollection(listCollectionName, function(err, listCollection) { 33 | if(err) return done(err); 34 | connected_db.listCollections().toArray(function(err, items) { 35 | if(err) return done(err); 36 | var instance = _.find(items, {name:listCollectionName} ); 37 | instance.should.not.be.undefined; 38 | done(); 39 | }); 40 | }); 41 | }); 42 | it('should list collections names', function(done) { 43 | var collectionName1 = "test_databases_collectionNames_collection_1"; 44 | var collectionName2 = "test_databases_collectionNames_collection_2"; 45 | var collectionName3 = "test_databases_collectionNames_collection_3"; 46 | connected_db.createCollection(collectionName1, function(err, listCollection) { 47 | if(err) return done(err); 48 | connected_db.createCollection(collectionName3, function(err, listCollection) { 49 | if(err) return done(err); 50 | connected_db.createCollection(collectionName2, function(err, listCollection) { 51 | if(err) return done(err); 52 | var items = connected_db.collectionNames(); 53 | items.should.containEql(collectionName1); 54 | items.should.containEql(collectionName2); 55 | items.should.containEql(collectionName3); 56 | done(); 57 | }); 58 | }); 59 | }); 60 | }); 61 | it('should drop collection', function (done) { 62 | var dropCollectionName = "test_databases_dropCollection_collection"; 63 | connected_db.createCollection(dropCollectionName, function (err, dropCollection){ 64 | if(err) return done(err); 65 | connected_db.dropCollection(dropCollectionName, function (err, result) { 66 | if(err) return done(err); 67 | connected_db.listCollections().toArray(function(err, items) { 68 | var instance = _.find(items, {name:dropCollectionName} ); 69 | (instance === undefined).should.be.true; 70 | done(); 71 | }); 72 | }); 73 | }); 74 | }); 75 | 76 | it('should drop collection by promise', function (done) { 77 | var dropCollectionName = "test_databases_dropCollection_collection_promise"; 78 | connected_db.createCollection(dropCollectionName) 79 | .then( collection => { 80 | return connected_db.dropCollection(dropCollectionName); 81 | }).then( result => { 82 | return connected_db.listCollections().toArray(); 83 | }).then( items => { 84 | var instance = _.find(items, {name:dropCollectionName} ); 85 | (instance === undefined).should.be.true; 86 | done(); 87 | }) 88 | }); 89 | 90 | it('should load another db', function (done) { 91 | var otherCollectionName = 'someOtherCollection'; 92 | var otherDb = connected_db.db('some_other_mock_database'); 93 | otherDb.createCollection(otherCollectionName, function (err, otherCollection) { 94 | if(err) return done(err); 95 | connected_db.listCollections().toArray(function(err, mainCollections) { 96 | if(err) return done(err); 97 | otherDb.listCollections().toArray(function(err, otherCollections) { 98 | // otherDb should have a separate list of collections 99 | if(err) return done(err); 100 | var otherInstance = _.find(otherCollections, {name:otherCollectionName} ); 101 | otherInstance.should.not.be.undefined; 102 | var mainInstance = _.find(mainCollections, {name:otherCollectionName} ); 103 | (mainInstance === undefined).should.be.true; 104 | otherDb.close().then(done).catch(done); 105 | }); 106 | }); 107 | }); 108 | }); 109 | }); 110 | 111 | describe('indexes', function () { 112 | it('should create a unique index', function (done) { 113 | collection.createIndex({test:1}, {unique:true}, function (err, name) { 114 | if(err) return done(err); 115 | name.should.equal('test_1'); 116 | done(); 117 | }); 118 | }); 119 | 120 | it('should deny unique constraint violations on insert', function (done) { 121 | collection.insertMany([{test:333},{test:444},{test:555, baz:1},{test:555,baz:2}], function (err, result) { 122 | (!!err).should.be.true; 123 | (!!result).should.be.false; 124 | err.message.should.equal('E11000 duplicate key error index: mock_database.users.$test_1'); 125 | 126 | //the first one should succeed 127 | collection.findOne({test:555}, function (err, doc) { 128 | if(err) return done(err); 129 | (!!doc).should.be.true; 130 | doc.should.have.property('baz', 1); 131 | done(); 132 | }); 133 | }); 134 | }); 135 | it('should deny unique constraint violations on update', function (done) { 136 | collection.update({test:333},{$set:{test:444,baz:2}}, function (err, result) { 137 | (!!err).should.be.true; 138 | (!!result).should.be.false; 139 | err.message.should.equal('E11000 duplicate key error index: mock_database.users.$test_1'); 140 | 141 | //make sure it didn't update the data 142 | collection.findOne({test:333}, function (err, doc) { 143 | if(err) return done(err); 144 | (!!doc).should.be.true; 145 | doc.should.not.have.property('baz'); 146 | done(); 147 | }); 148 | }); 149 | }); 150 | 151 | it('should create a non-unique index', function (done) { 152 | collection.createIndex({test_nonunique:1}, {unique:false}, function (err, name) { 153 | if(err) return done(err); 154 | name.should.equal('test_nonunique_1'); 155 | done(); 156 | }); 157 | }); 158 | 159 | it('should create a non-unique index by default', function (done) { 160 | collection.createIndex({test_nonunique_default:1}, {}, function (err, name) { 161 | if(err) return done(err); 162 | collection.indexInformation({full:true}, function (err, indexes) { 163 | if(err) return done(err); 164 | var index = _.filter(indexes, {name: 'test_nonunique_default_1'})[0]; 165 | index.unique.should.be.false; 166 | done(); 167 | }); 168 | }); 169 | }); 170 | 171 | it('should allow insert with same non-unique index property', function (done) { 172 | collection.insertMany([ 173 | {test:3333, test_nonunique:3333}, 174 | {test:4444, test_nonunique:4444}, 175 | {test:5555, test_nonunique:3333}], function (err, result) { 176 | (!!err).should.be.false; 177 | result.result.ok.should.be.eql(1); 178 | result.result.n.should.eql(3); 179 | done(); 180 | }); 181 | }); 182 | it('should allow update with same non-unique index property', function (done) { 183 | collection.update({test:4444}, {$set:{test_nonunique:3333}}, function (err, result) { 184 | (!!err).should.be.false; 185 | result.result.n.should.eql(1); 186 | done(); 187 | }); 188 | }); 189 | }); 190 | 191 | describe('collections', function () { 192 | 'drop,insert,findOne,findOneAndUpdate,update,updateOne,updateMany,remove,deleteOne,deleteMany,save'.split(',').forEach(function(key) { 193 | it("should have a '"+key+"' function", function () { 194 | collection.should.have.property(key); 195 | collection[key].should.be.type('function'); 196 | }); 197 | }); 198 | 199 | it('should insert data', function (done) { 200 | collection.insertOne({test:123}, function (err, result) { 201 | if(err) return done(err); 202 | (!!result.ops).should.be.true; 203 | (!!result.ops[0]).should.be.true; 204 | (!!result.ops[0]._id).should.be.true; 205 | result.ops[0]._id.toString().should.have.length(24); 206 | result.ops[0].should.have.property('test', 123); 207 | result.should.have.property('insertedId'); 208 | result.should.have.property('insertedCount'); 209 | done(); 210 | }); 211 | }); 212 | it('should allow _id to be defined', function (done) { 213 | collection.insert({_id:id, test:456, foo:true}, function (err, result) { 214 | if(err) return done(err); 215 | (!!result.ops).should.be.true; 216 | (!!result.ops[0]).should.be.true; 217 | (!!result.ops[0]._id).should.be.true; 218 | result.ops[0]._id.toString().should.have.length(24); 219 | result.ops[0].should.have.property('test', 456); 220 | done(); 221 | }); 222 | }); 223 | 224 | it('should findOne by a property', function (done) { 225 | collection.findOne({test:123}, function (err, doc) { 226 | if(err) return done(err); 227 | (!!doc).should.be.true; 228 | doc.should.have.property('_id'); 229 | doc._id.toString().should.have.length(24);//auto generated _id 230 | doc.should.have.property('test', 123); 231 | done(); 232 | }); 233 | }); 234 | it('should return only the fields specified by field projection', () => 235 | collection.findOne({test:456}, {projection: {foo:1}}) 236 | .then(doc => { 237 | (!!doc).should.be.true; 238 | Object.keys(doc).should.eql(['foo', '_id']); 239 | }) 240 | ); 241 | it('should return only the fields specified', () => 242 | collection.findOne({test:456}, {foo:1}) 243 | .then(doc => { 244 | (!!doc).should.be.true; 245 | Object.keys(doc).should.eql(['foo', '_id']); 246 | }) 247 | ); 248 | it('should accept undefined fields', function (done) { 249 | collection.findOne({test:456}, undefined, function (err, doc) { 250 | if(err) return done(err); 251 | (!!doc).should.be.true; 252 | doc.should.have.property('_id'); 253 | doc._id.toString().should.have.length(24);//auto generated _id 254 | doc.should.have.property('test', 456); 255 | doc.should.have.property('foo', true); 256 | done(); 257 | }); 258 | }); 259 | it('should find by an ObjectID', function (done) { 260 | collection.find({_id:ObjectID(id.toHexString())}).toArray(function (err, results) { 261 | if(err) return done(err); 262 | (!!results).should.be.true; 263 | results.should.have.length(1); 264 | var doc = results[0]; 265 | doc.should.have.property('_id'); 266 | id.toHexString().should.eql(doc._id.toHexString()); 267 | doc.should.have.property('test', 456); 268 | doc.should.have.property('foo', true); 269 | done(); 270 | }); 271 | }); 272 | it('should findOne by an ObjectID', function (done) { 273 | collection.findOne({_id:id}, function (err, doc) { 274 | if(err) return done(err); 275 | (!!doc).should.be.true; 276 | doc.should.have.property('_id'); 277 | id.toHexString().should.eql(doc._id.toHexString()); 278 | doc.should.have.property('test', 456); 279 | done(); 280 | }); 281 | }); 282 | it('should NOT findOne if it does not exist', function (done) { 283 | collection.findOne({_id:"asdfasdf"}, function (err, doc) { 284 | if(err) return done(err); 285 | (!!doc).should.be.false; 286 | done(); 287 | }); 288 | }); 289 | 290 | it('should NOT findOne if the collection has just been created', function (done) { 291 | var collection = connected_db.collection('some_brand_new_collection'); 292 | collection.findOne({_id:"asdfasdf"}, function (err, doc) { 293 | if(err) return done(err); 294 | (!!doc).should.be.false; 295 | done(); 296 | }); 297 | }) 298 | 299 | it('should find document where nulled property exists', function (done) { 300 | collection.insert({_id: 'g5f6h2df6g46j', a: true, b: null}, function (errInsert, resultInsert) { 301 | if(errInsert) return done(errInsert); 302 | (!!resultInsert.ops).should.be.true; 303 | collection.find({b:{$exists: true}}).toArray(function (errFind, resultFind) { 304 | if (errFind) return done(errFind); 305 | collection.remove({_id: 'g5f6h2df6g46j'}, function (errRemove) { 306 | if (errRemove) return done(errRemove); 307 | resultFind.length.should.equal(1); 308 | resultFind[0].should.have.property("b", null); 309 | done(); 310 | }); 311 | }); 312 | }); 313 | }) 314 | 315 | it('should not find document where property does not exists', function (done) { 316 | collection.insert({_id: 'weg8h7rt6h5weg69', a: 37}, function (errInsert, resultInsert) { 317 | if(errInsert) return done(errInsert); 318 | (!!resultInsert.ops).should.be.true; 319 | collection.find({_id: 'weg8h7rt6h5weg69', b:{$exists: false}}).toArray(function (errFind, resultFind) { 320 | if (errFind) return done(errFind); 321 | collection.remove({_id: 'weg8h7rt6h5weg69'}, function (errRemove) { 322 | if (errRemove) return done(errRemove); 323 | resultFind.length.should.equal(1); 324 | resultFind[0].should.have.property("a", 37); 325 | done(); 326 | }); 327 | }); 328 | }); 329 | }) 330 | 331 | it('should find document with nulled property and exists false', function (done) { 332 | collection.insert({_id: 'iuk51hf34', a: true, b: null}, function (errInsert, resultInsert) { 333 | if(errInsert) return done(errInsert); 334 | (!!resultInsert.ops).should.be.true; 335 | collection.find({_id: 'iuk51hf34', b:{$exists: false}}).toArray(function (errFind, resultFind) { 336 | if (errFind) return done(errFind); 337 | collection.remove({_id: 'iuk51hf34'}, function (errRemove) { 338 | if (errRemove) return done(errRemove); 339 | resultFind.length.should.equal(0); 340 | done(); 341 | }); 342 | }); 343 | }); 344 | }) 345 | 346 | it('should update one (updateOne)', function (done) { 347 | //query, data, options, callback 348 | collection.updateOne({test:123}, { $set: { foo: { bar: "buzz", fang: "dang" } } }, function (err, opResult) { 349 | if(err) return done(err); 350 | opResult.result.n.should.equal(1); 351 | 352 | collection.findOne({test:123}, function (err, doc) { 353 | if(err) return done(err); 354 | (!!doc).should.be.true; 355 | doc.should.have.property("foo", { bar: "buzz", fang: "dang" }); 356 | done(); 357 | }); 358 | }); 359 | }); 360 | 361 | it('should update one (updateOne) with shallow overwrite', function (done) { 362 | //query, data, options, callback 363 | collection.updateOne({ test: 123 }, { $set: { foo: { newValue: "bar" } } }, function (err, opResult) { 364 | if (err) return done(err); 365 | opResult.result.n.should.equal(1); 366 | 367 | collection.findOne({ test: 123 }, function (err, doc) { 368 | if (err) return done(err); 369 | (!!doc).should.be.true; 370 | doc.should.have.property("foo", { newValue: "bar" }); 371 | done(); 372 | }); 373 | }); 374 | }); 375 | 376 | it('should update one (findOneAndUpdate)', function (done) { 377 | //query, data, options, callback 378 | collection.findOneAndUpdate({test:123}, {$set:{foo:"john"}}, function (err, opResult) { 379 | if(err) return done(err); 380 | opResult.should.have.properties("ok", "lastErrorObject", "value"); 381 | opResult.ok.should.equal(1); 382 | opResult.value.should.have.property("foo", "john"); 383 | 384 | collection.findOne({test:123}, function (err, doc) { 385 | if(err) return done(err); 386 | (!!doc).should.be.true; 387 | doc.should.have.property("foo", "john"); 388 | done(); 389 | }); 390 | }); 391 | }); 392 | 393 | it('should create one (findOneAndUpdate) with upsert and no document found', function (done) { 394 | //query, data, options, callback 395 | collection.findOneAndUpdate({ test: 1689 }, { $set: { foo: "john" }, $setOnInsert: { bar: "dang" } }, { upsert: true }, function (err, opResult) { 396 | if (err) return done(err); 397 | opResult.should.have.properties("ok", "lastErrorObject", "value"); 398 | opResult.lastErrorObject.should.have.property("upserted"); 399 | opResult.lastErrorObject.should.have.property("updatedExisting", false); 400 | opResult.lastErrorObject.should.have.property("n", 1); 401 | 402 | opResult.value.should.have.property("foo", "john"); 403 | opResult.value.should.have.property("bar", "dang"); 404 | 405 | collection.findOne({ test: 1689 }, function (err, doc) { 406 | if (err) return done(err); 407 | (!!doc).should.be.true; 408 | doc.should.have.property("foo", "john"); 409 | doc.should.have.property("bar", "dang"); 410 | done(); 411 | }); 412 | }); 413 | }); 414 | 415 | it('should update one (findOneAndUpdate) with upsert and matching document found', function (done) { 416 | //query, data, options, callback 417 | collection.findOneAndUpdate({ test: 1689 }, { $set: { foo: "john" }, $setOnInsert: { bar: "dang" } }, { upsert: true }, function (err, opResult) { 418 | if (err) return done(err); 419 | opResult.should.have.properties("ok", "lastErrorObject", "value"); 420 | opResult.lastErrorObject.should.have.property("updatedExisting", true); 421 | opResult.lastErrorObject.should.have.property("n", 1); 422 | 423 | opResult.value.should.have.property("foo", "john"); 424 | 425 | collection.findOne({ test: 1689 }, function (err, doc) { 426 | if (err) return done(err); 427 | (!!doc).should.be.true; 428 | doc.should.have.property("foo", "john"); 429 | done(); 430 | }); 431 | }); 432 | }); 433 | 434 | it('should create one (findOneAndUpdate) with upsert and no document found, complex filter', function (done) { 435 | //query, data, options, callback 436 | collection.findOneAndUpdate({ $and: [{ test: 1690 }, { another_test: 1691 }] }, { $set: { foo: "alice" }, $setOnInsert: { bar: "bob" } }, { upsert: true }, function (err, opResult) { 437 | if (err) return done(err); 438 | opResult.should.have.properties("ok", "lastErrorObject", "value"); 439 | opResult.lastErrorObject.should.have.property("upserted"); 440 | opResult.lastErrorObject.should.have.property("updatedExisting", false); 441 | opResult.lastErrorObject.should.have.property("n", 1); 442 | 443 | opResult.value.should.have.property("foo", "alice"); 444 | opResult.value.should.have.property("bar", "bob"); 445 | 446 | collection.findOne({ test: 1690, another_test: 1691 }, function (err, doc) { 447 | if (err) return done(err); 448 | (!!doc).should.be.true; 449 | doc.should.have.property("foo", "alice"); 450 | doc.should.have.property("bar", "bob"); 451 | done(); 452 | }); 453 | }); 454 | }); 455 | 456 | it('should update one (findOneAndUpdate) with upsert and matching document found, complex filter', function (done) { 457 | //query, data, options, callback 458 | collection.findOneAndUpdate({ $and: [{ test: 1690 }, { another_test: 1691 }] }, { $set: { foo: "alice2" }, $setOnInsert: { bar: "bob2" } }, { upsert: true }, function (err, opResult) { 459 | if (err) return done(err); 460 | 461 | opResult.should.have.properties("ok", "lastErrorObject", "value"); 462 | opResult.lastErrorObject.should.have.property("updatedExisting", true); 463 | opResult.lastErrorObject.should.have.property("n", 1); 464 | 465 | opResult.value.should.have.property("foo", "alice2"); 466 | opResult.value.should.have.property("bar", "bob"); // Update here, no insertion. 467 | 468 | function cleanup(cb) { 469 | collection.remove({ test: 1690, another_test: 1691 }, cb); 470 | } 471 | 472 | collection.findOne({ test: 1690, another_test: 1691 }, function (err, doc) { 473 | if (err) { 474 | return cleanup(function () { 475 | done(err); 476 | }); 477 | } 478 | (!!doc).should.be.true; 479 | doc.should.have.property("foo", "alice2"); 480 | 481 | cleanup(done); 482 | }); 483 | }); 484 | }); 485 | 486 | it('should create one (findOneAndUpdate) with upsert and no document found, more complex filter', function (done) { 487 | //query, data, options, callback 488 | collection.findOneAndUpdate({ $and: [{ test: 1790 }, { timestamp: { $lt: 1 } }] }, { $set: { foo: "alice", timestamp: 1 }, $setOnInsert: { bar: "bob" } }, { upsert: true }, function (err, opResult) { 489 | if (err) return done(err); 490 | opResult.should.have.properties("ok", "lastErrorObject", "value"); 491 | opResult.lastErrorObject.should.have.property("upserted"); 492 | opResult.lastErrorObject.should.have.property("updatedExisting", false); 493 | opResult.lastErrorObject.should.have.property("n", 1); 494 | 495 | opResult.value.should.have.property("test"); 496 | opResult.value.should.have.property("foo", "alice"); 497 | opResult.value.should.have.property("bar", "bob"); 498 | 499 | collection.findOne({ test: 1790 }, function (err, doc) { 500 | if (err) return done(err); 501 | (!!doc).should.be.true; 502 | doc.should.have.property("foo", "alice"); 503 | doc.should.have.property("bar", "bob"); 504 | doc.should.have.property("timestamp", 1); 505 | done(); 506 | }); 507 | }); 508 | }); 509 | 510 | it('should not update one (findOneAndUpdate) with upsert and no matching document found, more complex filter', function (done) { 511 | //query, data, options, callback 512 | collection.findOneAndUpdate({ $and: [{ test: 1790 }, { timestamp: { $lt: 1 } }] }, { $set: { foo: "alice2", timestamp: 1 }, $setOnInsert: { bar: "bob2" } }, { upsert: true }, function (err, opResult) { 513 | if (err) { 514 | if (err.code !== 11000) { 515 | return done(err); 516 | } 517 | } 518 | 519 | done(); 520 | }); 521 | }); 522 | 523 | it('should update one (findOneAndUpdate) with upsert and document found, more complex filter', function (done) { 524 | //query, data, options, callback 525 | collection.findOneAndUpdate({ $and: [{ test: 1790 }, { timestamp: { $lt: 2 } }] }, { $set: { foo: "alice3", timestamp: 2 }, $setOnInsert: { bar: "bob3" } }, { upsert: true }, function (err, opResult) { 526 | if (err) return done(err); 527 | opResult.should.have.properties("ok", "lastErrorObject", "value"); 528 | opResult.lastErrorObject.should.have.property("updatedExisting", true); 529 | opResult.lastErrorObject.should.have.property("n", 1); 530 | 531 | opResult.value.should.have.property("test"); 532 | opResult.value.should.have.property("foo", "alice3"); 533 | opResult.value.should.have.property("bar", "bob"); 534 | 535 | function cleanup(cb) { 536 | collection.remove({ test: 1790 }, cb); 537 | } 538 | 539 | collection.findOne({ test: 1790 }, function (err, doc) { 540 | if (err) { 541 | return cleanup(function () { 542 | done(err); 543 | }); 544 | } 545 | (!!doc).should.be.true; 546 | doc.should.have.property("foo", "alice3"); 547 | doc.should.have.property("bar", "bob"); 548 | doc.should.have.property("timestamp", 2); 549 | cleanup(done); 550 | }); 551 | }); 552 | }); 553 | 554 | it('should create one (findOneAndUpdate) with upsert and no document found, using correct _id', function (done) { 555 | //query, data, options, callback 556 | collection.findOneAndUpdate({ $and: [{ _id: 123 }, { timestamp: 1 }] }, { $set: { foo: "alice" } }, { upsert: true }, function (err, opResult) { 557 | if (err) return done(err); 558 | opResult.value.should.have.property("_id", 123); 559 | opResult.value.should.have.property("foo", "alice"); 560 | 561 | collection.remove({ _id: 123 }, function () { 562 | done(); 563 | }); 564 | }); 565 | }); 566 | 567 | it('should update nothing (findOneAndUpdate) without upsert and no document found', function (done) { 568 | //query, data, options, callback 569 | collection.findOneAndUpdate({ $and: [{ _id: 123 }, { timestamp: 1 }] }, { $set: { foo: "alice" } }, { upsert: false }, function (err, opResult) { 570 | if (err) return done(err); 571 | opResult.should.have.property("value", null); 572 | done(); 573 | }); 574 | }); 575 | 576 | it('should update one (default)', function (done) { 577 | //query, data, options, callback 578 | collection.update({test:123}, {$set:{foo:"bar"}}, function (err, opResult) { 579 | if(err) return done(err); 580 | opResult.result.n.should.equal(1); 581 | 582 | collection.findOne({test:123}, function (err, doc) { 583 | if(err) return done(err); 584 | (!!doc).should.be.true; 585 | doc.should.have.property("foo", "bar"); 586 | done(); 587 | }); 588 | }); 589 | }); 590 | it('should update multi', function (done) { 591 | collection.update({}, {$set:{foo:"bar"}}, {multi:true}, function (err, opResult) { 592 | if(err) return done(err); 593 | opResult.result.n.should.equal(9); 594 | 595 | collection.find({foo:"bar"}).count(function (err, n) { 596 | if(err) return done(err); 597 | n.should.equal(9); 598 | done(); 599 | }); 600 | }); 601 | }); 602 | it('should updateMany', function (done) { 603 | collection.updateMany({}, {$set:{updateMany:"bar"}}, function (err, opResult) { 604 | if(err) return done(err); 605 | opResult.result.n.should.equal(9); 606 | opResult.result.nModified.should.equal(9); 607 | opResult.matchedCount.should.equal(9); 608 | opResult.modifiedCount.should.equal(9); 609 | 610 | collection.find({updateMany:"bar"}).count(function (err, n) { 611 | if(err) return done(err); 612 | n.should.equal(9); 613 | done(); 614 | }); 615 | }); 616 | }); 617 | it('should update subdocs in dot notation', function (done) { 618 | collection.update({}, {$set:{"update.subdocument":true}}, function (err, opResult) { 619 | if(err) return done(err); 620 | opResult.result.n.should.equal(1); 621 | 622 | collection.find({"update.subdocument":true}).count(function (err, n) { 623 | if(err) return done(err); 624 | n.should.equal(1); 625 | done(); 626 | }); 627 | }); 628 | }); 629 | it('should update subdoc arrays in dot notation', function (done) { 630 | collection.update({}, {$set:{"update.arr.0": true}}, function (err, opResult) { 631 | if(err) return done(err); 632 | opResult.result.n.should.equal(1); 633 | 634 | collection.find({"update.arr.0": true}).count(function (err, n) { 635 | if(err) return done(err); 636 | n.should.equal(1); 637 | done(); 638 | }); 639 | }); 640 | }); 641 | it('should $unset', function (done) { 642 | var original = { test: 237, parent0 :999, parent1 :{ child1 :111, child2 :222, child3 :333, child4 :{ child5 :555}}}; 643 | var expected = '{"test":237,"parent1":{"child1":111,"child3":333,"child4":{}}}'; 644 | 645 | collection.insert(original) 646 | .then(r1 => 647 | collection.update({test: 237}, {$unset: { "parent0": 1, "parent1.child2": 1, "parent1.child4.child5": 1 }}) 648 | .then(r2 => 649 | collection.findOne({test: 237}) 650 | .then(doc => { 651 | let copy = _.clone(doc); 652 | delete copy._id; 653 | JSON.stringify(copy).should.eql(expected); 654 | }) 655 | ) 656 | ) 657 | .then(done) 658 | .catch(done) 659 | }); 660 | it('should upsert', function (done) { 661 | //prove it isn't there... 662 | collection.findOne({test:1}, function (err, doc) { 663 | if(err) return done(err); 664 | (!!doc).should.be.false; 665 | 666 | collection.update({test:1}, {test:1,bar:"none"}, {upsert:true}, function (err, opResult) { 667 | if(err) return done(err); 668 | opResult.result.n.should.equal(1); 669 | 670 | collection.find({test:1}).count(function (err, n) { 671 | if(err) return done(err); 672 | n.should.equal(1); 673 | done(); 674 | }); 675 | }); 676 | }); 677 | }); 678 | it('should upsert (updateMany)', function (done) { 679 | //prove it isn't there... 680 | collection.findOne({upsertMany:1}, function (err, doc) { 681 | if(err) return done(err); 682 | (!!doc).should.be.false; 683 | 684 | collection.updateMany({upsertMany:1}, { $set:{upsertMany:1,bar:"none"} }, {upsert:true}, function (err, opResult) { 685 | if(err) return done(err); 686 | opResult.result.n.should.equal(1); 687 | 688 | collection.find({upsertMany:1}).count(function (err, n) { 689 | if(err) return done(err); 690 | n.should.equal(1); 691 | done(); 692 | }); 693 | }); 694 | }); 695 | }); 696 | it('should save (no _id)', function (done) { 697 | //prove it isn't there... 698 | collection.findOne({test:2}, function (err, doc) { 699 | if(err) return done(err); 700 | (!!doc).should.be.false; 701 | 702 | collection.save({test:2,bar:"none"}, function (err, opResult) { 703 | if(err) return done(err); 704 | opResult.result.n.should.equal(1); 705 | 706 | collection.find({test:2}).count(function (err, n) { 707 | if(err) return done(err); 708 | n.should.equal(1); 709 | done(); 710 | }); 711 | }); 712 | }); 713 | }); 714 | it('should save (with _id)', function (done) { 715 | //prove it isn't there... 716 | collection.findOne({test:2}, function (err, doc) { 717 | if(err) return done(err); 718 | (!doc).should.be.false; 719 | 720 | collection.save({_id: doc._id,test:3,bar:"none"}, function (err, opResult) { 721 | if(err) return done(err); 722 | opResult.result.n.should.equal(1); 723 | 724 | collection.find({test:3}).count(function (err, n) { 725 | if(err) return done(err); 726 | n.should.equal(1); 727 | done(); 728 | }); 729 | }); 730 | }); 731 | }); 732 | /***************/ 733 | it('should delete one', function (done) { 734 | //query, data, options, callback 735 | collection.insert({test:967, delete: true}, function (err, result) { 736 | if(err) return done(err); 737 | (!!result.ops).should.be.true; 738 | 739 | collection.deleteOne({test:967}, function (err, result) { 740 | if (err) return done(err); 741 | result.result.n.should.equal(1); 742 | 743 | collection.findOne({test: 967}, function (err, doc) { 744 | if (err) return done(err); 745 | (!!doc).should.be.false; 746 | done(); 747 | }); 748 | }); 749 | }); 750 | }); 751 | it('should delete one and only one', function (done) { 752 | //query, data, options, callback 753 | collection.insertMany([{test:967, delete: true}, {test:5309, delete: true}], function (err, result) { 754 | if(err) return done(err); 755 | (!!result.ops).should.be.true; 756 | 757 | collection.deleteOne({delete: true}, function (err, result) { 758 | if (err) return done(err); 759 | result.result.n.should.equal(1); 760 | 761 | collection.find({delete: true}).count(function (err, n) { 762 | if (err) return done(err); 763 | n.should.equal(1); 764 | done(); 765 | }); 766 | }); 767 | }); 768 | }); 769 | it('should delete many', function (done) { 770 | //query, data, options, callback 771 | collection.insertOne({test:967, delete: true}, function (err, result) { 772 | if(err) return done(err); 773 | (!!result.ops).should.be.true; 774 | collection.find({ delete: true }).count(function (err, n) { 775 | if (err) return done(err); 776 | n.should.equal(2); 777 | 778 | collection.deleteMany({delete: true}, function (err, result) { 779 | if (err) return done(err); 780 | result.result.n.should.equal(2); 781 | 782 | collection.find({delete: true}).count(function (err, n) { 783 | if (err) return done(err); 784 | n.should.equal(0); 785 | done(); 786 | }); 787 | }); 788 | }); 789 | }); 790 | }); 791 | it('should return a promise for deleteMany', function (done) { 792 | const prom = collection.deleteMany({ shouldNeverMatchAnythingImportant: true}); 793 | prom.should.be.instanceOf(Promise); 794 | done(); 795 | }); 796 | it('should delete many using the $in symbol', function (done) { 797 | //query, data, options, callback 798 | collection.insertMany([{ test: 967, delete: true }, { test: 418, delete: true }], function (err, result) { 799 | if (err) return done(err); 800 | (!!result.ops).should.be.true; 801 | collection.find({ test: { $in: [418, 967] } }).count(function (err, n) { 802 | if (err) return done(err); 803 | n.should.equal(2); 804 | 805 | collection.deleteMany({ test: { $in: [418, 967] } }, function (err, result) { 806 | if (err) return done(err); 807 | result.result.n.should.equal(2); 808 | 809 | collection.find({ delete: true }).count(function (err, n) { 810 | if (err) return done(err); 811 | n.should.equal(0); 812 | done(); 813 | }); 814 | }); 815 | }); 816 | }); 817 | }); 818 | it('should add to set (default)', function (done) { 819 | collection.update({test:123}, {$addToSet:{ boo:"bar"}}, function (err, opResult) { 820 | if(err) return done(err); 821 | opResult.result.n.should.equal(1); 822 | collection.findOne({test:123}, function (err, doc) { 823 | if(err) return done(err); 824 | doc.should.have.property("boo", ["bar"]); 825 | done(); 826 | }); 827 | }); 828 | }); 829 | it('should add to set', function (done) { 830 | collection.update({test:123}, {$addToSet:{ boo:"foo"}}, function (err, opResult) { 831 | if(err) return done(err); 832 | opResult.result.n.should.equal(1); 833 | collection.findOne({test:123}, function (err, doc) { 834 | if(err) return done(err); 835 | doc.should.have.property("boo", ["bar", "foo"]); 836 | done(); 837 | }); 838 | }); 839 | }); 840 | it('should not add to set already existing item', function (done) { 841 | collection.update({test:123}, {$addToSet:{ boo:"bar"}}, function (err, opResult) { 842 | if(err) return done(err); 843 | opResult.result.n.should.equal(1); 844 | collection.findOne({test:123}, function (err, doc) { 845 | if(err) return done(err); 846 | doc.should.have.property("boo", ["bar", "foo"]); 847 | done(); 848 | }); 849 | }); 850 | }); 851 | it('should increment a number', function(done) { 852 | // add some fields to increment 853 | collection.update({test:333}, {$set: {incTest: 1, multiIncTest: { foo: 1 }}}, function (err, result) { 854 | if (err) done(err); 855 | collection.update({test:333}, { $inc: { incTest: 1, 'multiIncTest.foo': 2}}, function (err, opResult) { 856 | if (err) done(err); 857 | opResult.result.n.should.equal(1); 858 | collection.findOne({test:333}, function (err, doc) { 859 | if (err) done(err); 860 | doc.incTest.should.equal(2); 861 | doc.multiIncTest.foo.should.equal(3); 862 | done(); 863 | }); 864 | }); 865 | }); 866 | }); 867 | it('should decrement a number', function(done) { 868 | collection.update({test:333}, { $inc: { incTest: -1, 'multiIncTest.foo': -2, 'some.new.key': 42}}, function (err, opResult) { 869 | if (err) done(err); 870 | opResult.result.n.should.equal(1); 871 | collection.findOne({test:333}, function (err, doc) { 872 | if (err) done(err); 873 | doc.incTest.should.equal(1); 874 | doc.multiIncTest.foo.should.equal(1); 875 | doc.some.new.key.should.equal(42); 876 | done(); 877 | }); 878 | }); 879 | }); 880 | it('should push item into array', function (done) { 881 | collection.update({test:333}, { $set: {pushTest: []}}, function (err, result) { 882 | if (err) done(err); 883 | collection.update({test:333}, { $push:{ pushTest: {$each: [ 2 ]} }} ,function (err, opResult) { 884 | if (err) done(err); 885 | opResult.result.n.should.equal(1); 886 | collection.findOne({test:333}, function (err, doc) { 887 | if (err) done(err); 888 | doc.pushTest.should.have.length(1); 889 | doc.pushTest.should.containEql(2); 890 | done(); 891 | }); 892 | }); 893 | }); 894 | }); 895 | it('should push item into array that does not yet exist on the doc', function (done) { 896 | collection.update({test:333}, { $push:{ newPushTest: {$each: [ 2 ]} }} ,function (err, opResult) { 897 | if (err) done(err); 898 | opResult.result.n.should.equal(1); 899 | collection.findOne({test: 333}, function (err, doc) { 900 | if (err) done(err); 901 | doc.newPushTest.should.have.length(1); 902 | doc.newPushTest.should.containEql(2); 903 | done(); 904 | }); 905 | }); 906 | }); 907 | it('should push item into array + $slice', function (done) { 908 | collection.update({test:333}, { $set: {pushTest: []}}, function (err, result) { 909 | if (err) done(err); 910 | collection.update({test:333}, { $push:{ pushTest: {$each: [ 1, 2, 3, 4 ], $slice: -2 } }} ,function (err, opResult) { 911 | if (err) done(err); 912 | opResult.result.n.should.equal(1); 913 | collection.findOne({test:333}, function (err, doc) { 914 | if (err) done(err); 915 | doc.pushTest.should.have.length(2); 916 | doc.pushTest.should.containEql(3, 4); 917 | done(); 918 | }); 919 | }); 920 | }); 921 | }); 922 | it('should count the number of items in the collection - count method', function(done) { 923 | collection.should.have.property('count'); 924 | collection.count({}, function(err, cnt) { 925 | if (err) done(err); 926 | cnt.should.equal(EXPECTED_TOTAL_TEST_DOCS); 927 | 928 | collection.count({ test:333 }, function(err, singleCnt) { 929 | if (err) done(err); 930 | singleCnt.should.equal(1); 931 | done(); 932 | }); 933 | }); 934 | }); 935 | it('should count the number of items in the collection - countDocuments method', function(done) { 936 | collection.should.have.property('countDocuments'); 937 | collection.countDocuments({}, function(err, cnt) { 938 | if (err) done(err); 939 | cnt.should.equal(EXPECTED_TOTAL_TEST_DOCS); 940 | 941 | collection.countDocuments({ test:333 }, function(err, singleCnt) { 942 | if (err) done(err); 943 | singleCnt.should.equal(1); 944 | done(); 945 | }); 946 | }); 947 | }); 948 | it('should count the number of items in the collection - estimatedDocumentCount method', function(done) { 949 | collection.should.have.property('estimatedDocumentCount'); 950 | collection.estimatedDocumentCount({}, function(err, cnt) { 951 | if (err) done(err); 952 | cnt.should.equal(EXPECTED_TOTAL_TEST_DOCS); 953 | done(); 954 | }); 955 | }); 956 | it('should drop themselves', function(done) { 957 | var dropCollectionName = "test_collections_drop_collection"; 958 | connected_db.createCollection(dropCollectionName, function(err, dropCollection) { 959 | if(err) return done(err); 960 | dropCollection.drop(function(err, reply) { 961 | if(err) return done(err); 962 | connected_db.listCollections().toArray(function(err, items) { 963 | if(err) return done(err); 964 | var instance = _.find(items, {name:dropCollectionName} ); 965 | (instance === undefined).should.be.true; 966 | done(); 967 | }); 968 | }); 969 | }); 970 | }); 971 | it('should drop themselves by promise', function(done) { 972 | var dropCollectionName = "test_collections_drop_collection_promise"; 973 | connected_db.createCollection(dropCollectionName) 974 | .then(dropCollection => { 975 | return dropCollection.drop(); 976 | }).then(() => { 977 | return connected_db.listCollections().toArray(); 978 | }).then(items => { 979 | var instance = _.find(items, {name:dropCollectionName} ); 980 | (instance === undefined).should.be.true; 981 | done(); 982 | }); 983 | }); 984 | 985 | it('should have bulk operations', function(done) { 986 | collection.should.have.property('initializeOrderedBulkOp'); 987 | collection.should.have.property('initializeUnorderedBulkOp'); 988 | 989 | done(); 990 | }); 991 | 992 | it('should have bulk find', function(done) { 993 | var bulk = collection.initializeOrderedBulkOp(); 994 | bulk.should.have.property('find'); 995 | done(); 996 | }); 997 | 998 | it('should have bulk upsert', function(done) { 999 | var bulk = collection.initializeOrderedBulkOp(); 1000 | var findOps = bulk.find({}); 1001 | 1002 | findOps.should.have.property('upsert'); 1003 | done(); 1004 | }); 1005 | 1006 | it('should bulk updateOne', function(done) { 1007 | var bulk = collection.initializeOrderedBulkOp(); 1008 | bulk.find({test: {$exists: true}}).updateOne({ 1009 | $set: { 1010 | bulkUpdate: true, 1011 | } 1012 | }); 1013 | bulk.execute().then(() => { 1014 | collection.findOne({bulkUpdate: true}) 1015 | .then((doc) => { 1016 | if (doc && doc.bulkUpdate) { 1017 | done(); 1018 | } else { 1019 | done(new Error('Bulk operation did not updateOne')); 1020 | } 1021 | }); 1022 | }); 1023 | }).timeout(0); 1024 | 1025 | it('should bulk update', function(done) { 1026 | var bulk = collection.initializeOrderedBulkOp(); 1027 | 1028 | bulk.find({test: {$exists: true}}).update({ 1029 | $set: { 1030 | bulkUpdate: true, 1031 | } 1032 | }); 1033 | bulk.execute().then(() => { 1034 | collection.find({bulkUpdate: true}).toArray() 1035 | .then((docs) => { 1036 | if (docs.every((val) => val.bulkUpdate)) { 1037 | done(); 1038 | } else { 1039 | done(new Error('Bulk operation did not update')); 1040 | } 1041 | }); 1042 | }); 1043 | }).timeout(0); 1044 | 1045 | it('should bulk insert', function(done) { 1046 | var bulk = collection.initializeOrderedBulkOp(); 1047 | 1048 | bulk.insert([{ 1049 | test: 5353, 1050 | bulkTest: true, 1051 | }, { 1052 | test: 5454, 1053 | bulkTest: true, 1054 | }]); 1055 | 1056 | bulk.execute().then(() => { 1057 | collection.findOne({test: 5353}) 1058 | .then((doc) => { 1059 | if (doc.bulkTest) { 1060 | done(); 1061 | } else { 1062 | done(new Error('Doc didn\'t get inserted')); 1063 | } 1064 | }); 1065 | }); 1066 | }).timeout(0); 1067 | 1068 | it('should bulk removeOne', function(done) { 1069 | var bulk = collection.initializeOrderedBulkOp(); 1070 | 1071 | bulk.find({bulkTest: true}).removeOne(); 1072 | 1073 | bulk.execute().then(() => { 1074 | collection.findOne({test: 5353}) 1075 | .then((doc) => { 1076 | if (doc) { 1077 | done(new Error('Doc didn\'t get removed')); 1078 | } else { 1079 | done(); 1080 | } 1081 | }); 1082 | }); 1083 | }).timeout(0); 1084 | 1085 | it('should bulk remove', function(done) { 1086 | var bulk = collection.initializeOrderedBulkOp(); 1087 | 1088 | bulk.find({bulkTest: true}).remove(); 1089 | 1090 | bulk.execute().then(() => { 1091 | collection.find({bulkTest: true}).toArray() 1092 | .then((docs) => { 1093 | if (docs.length > 0) { 1094 | done(new Error('Docs didn\'t get removed')); 1095 | } else { 1096 | done(); 1097 | } 1098 | }); 1099 | }); 1100 | }).timeout(0); 1101 | 1102 | it('should bulk write', function(done) { 1103 | const setup = Promise.all([ 1104 | collection.insertOne({ test: 1989, delete: true, many: false }), 1105 | collection.insertOne({ test: 1989, delete: true, many: true }), 1106 | collection.insertOne({ test: 1989, delete: true, many: true }), 1107 | collection.insertOne({ test: 1989, update: true, many: false }), 1108 | collection.insertOne({ test: 1989, update: true, many: true }), 1109 | collection.insertOne({ test: 1989, update: true, many: true }) 1110 | ]) 1111 | 1112 | setup 1113 | .then(() => { 1114 | return collection.bulkWrite([ 1115 | { 1116 | insertOne: { 1117 | document: { test: 1989, inserted: true } 1118 | } 1119 | }, 1120 | { updateOne: { 1121 | filter: { test: 1989, update: true, many: false }, 1122 | update: { 1123 | $set: { foo: 'bar' } 1124 | } 1125 | } }, 1126 | { updateMany: { 1127 | filter: { test: 1989, update: true, many: true }, 1128 | update: { 1129 | $set: { baz: 'bing' } 1130 | } 1131 | } }, 1132 | { deleteOne: { 1133 | filter: { test: 1989, delete: true, many: false } 1134 | } }, 1135 | { deleteMany: { 1136 | filter: { test: 1989, delete: true, many: true } 1137 | } }, 1138 | ]).then(results => { 1139 | results.result.n.should.equal(7) 1140 | 1141 | // Clean up, and assert that there are 4 records left 1142 | return collection.deleteMany({test: 1989}) 1143 | .then(results => { 1144 | results.result.n.should.equal(4) 1145 | done() 1146 | }) 1147 | }) 1148 | }) 1149 | .catch(err => done(err)) 1150 | }).timeout(0); 1151 | }); 1152 | 1153 | describe('cursors', function() { 1154 | it('should return a count of found items', function (done) { 1155 | var crsr = collection.find({}); 1156 | crsr.should.have.property('count'); 1157 | crsr.count(function(err, cnt) { 1158 | cnt.should.equal(EXPECTED_TOTAL_TEST_DOCS); 1159 | done(); 1160 | }); 1161 | }); 1162 | 1163 | it('should limit the fields in the documents using project', function (done) { 1164 | var crsr = collection.find({}); 1165 | crsr.should.have.property('project'); 1166 | crsr.project({ _id: 1 }).toArray(function(err, res) { 1167 | res.length.should.equal(EXPECTED_TOTAL_TEST_DOCS); 1168 | res.forEach(function(doc) { 1169 | Object.keys(doc).should.eql(['_id']); 1170 | }); 1171 | done(); 1172 | }); 1173 | }); 1174 | 1175 | it('should remove property/properites from the documents', function (done) { 1176 | var crsr = collection.find({}); 1177 | crsr.should.have.property('project'); 1178 | crsr.project({ _id: 0, foo: 0 }).toArray(function(err, res) { 1179 | res.length.should.equal(EXPECTED_TOTAL_TEST_DOCS); 1180 | res.forEach(function(doc) { 1181 | doc.should.not.have.keys('_id', 'foo'); 1182 | }); 1183 | done(); 1184 | }); 1185 | }); 1186 | 1187 | it('should skip 1 item', function (done) { 1188 | var crsr = collection.find({}); 1189 | crsr.should.have.property('skip'); 1190 | crsr.skip(1).toArray(function(err, res) { 1191 | res.length.should.equal(EXPECTED_TOTAL_TEST_DOCS - 1); 1192 | done(); 1193 | }); 1194 | }); 1195 | 1196 | it('should limit to 3 items', function (done) { 1197 | var crsr = collection.find({}); 1198 | crsr.should.have.property('limit'); 1199 | crsr.limit(3).toArray(function(err, res) { 1200 | res.length.should.equal(3); 1201 | done(); 1202 | }); 1203 | }); 1204 | 1205 | it('should skip 1 item, limit to 3 items', function (done) { 1206 | var crsr = collection.find({}); 1207 | crsr.should.have.property('limit'); 1208 | crsr.skip(1).limit(3).toArray(function(err, res) { 1209 | res.length.should.equal(3); 1210 | done(); 1211 | }); 1212 | }); 1213 | 1214 | it('should count all items regardless of skip/limit', function (done) { 1215 | var crsr = collection.find({}); 1216 | crsr.skip(1).limit(3).count(function(err, cnt) { 1217 | cnt.should.equal(EXPECTED_TOTAL_TEST_DOCS); 1218 | done(); 1219 | }); 1220 | }); 1221 | 1222 | it('should count only skip/limit results', function (done) { 1223 | var crsr = collection.find({}); 1224 | crsr.skip(1).limit(3).count(true, function(err, cnt) { 1225 | cnt.should.equal(3); 1226 | done(); 1227 | }); 1228 | }); 1229 | 1230 | it('should toggle count applySkipLimit and not', function (done) { 1231 | var crsr = collection.find({}).skip(1).limit(3); 1232 | crsr.count(true, function(err, cnt) { 1233 | cnt.should.equal(3); 1234 | crsr.count(function(err, cnt) { 1235 | cnt.should.equal(EXPECTED_TOTAL_TEST_DOCS); 1236 | done(); 1237 | }); 1238 | }); 1239 | }); 1240 | 1241 | it('should count only skip/limit results but return actual count if less than limit', function (done) { 1242 | var crsr = collection.find({}); 1243 | crsr.skip(4).limit(6).count(true, function(err, cnt) { 1244 | cnt.should.equal(6); 1245 | done(); 1246 | }); 1247 | }); 1248 | 1249 | it('should count only skip/limit results for size', function (done) { 1250 | var crsr = collection.find({}); 1251 | crsr.skip(2).limit(3).size(function(err, cnt) { 1252 | cnt.should.equal(3); 1253 | done(); 1254 | }); 1255 | }); 1256 | 1257 | describe('each', function() { 1258 | var each_db; 1259 | var each_collection; 1260 | var docs = [ 1261 | { a: 1 }, 1262 | { b: 2 } 1263 | ]; 1264 | 1265 | function stripIds(result) { 1266 | return result.map(function(x) { delete x._id; return x; }); 1267 | } 1268 | 1269 | before(function (done) { 1270 | MongoClient.connect("mongodb://somesortserver/sort_mock_database", function(err, db) { 1271 | each_db = db; 1272 | each_collection = connected_db.collection("eaching"); 1273 | each_collection.insertMany(docs).then(function() { done() }); 1274 | }); 1275 | }); 1276 | after(function(done) { 1277 | each_db.close().then(done).catch(done) 1278 | }); 1279 | 1280 | it('should manipulate with each and return modified array', function (done) { 1281 | var crsr = each_collection.find({}); 1282 | crsr.should.have.property('each'); 1283 | crsr.each(function(item) { 1284 | item._test = true; 1285 | }).toArray(function(err, res) { 1286 | if (err) done(err); 1287 | 1288 | stripIds(res); 1289 | res.should.containEql({ a: 1, _test: true }); 1290 | res.should.containEql({ b: 2, _test: true }); 1291 | res.should.not.containEql({ c: 3, _test: true }); 1292 | done(); 1293 | }); 1294 | }); 1295 | }); 1296 | 1297 | describe('sort', function() { 1298 | var sort_db; 1299 | var sort_collection; 1300 | var date = new Date(); 1301 | var docs = [ 1302 | { sortField: null, otherField: 6}, // null 1303 | { sortField: [ 1 ], otherField: 4}, // array 1304 | { sortField: 42, otherField: 1}, // number 1305 | { sortField: true, otherField: 5}, // boolean 1306 | { sortField: 'foo', otherField: 2}, // string 1307 | { sortField: /foo/, otherField: 7}, // regex 1308 | { sortField: { foo: 'bar' }, otherField: 8}, // object 1309 | { sortField: date, otherField: 3} // date 1310 | ]; 1311 | 1312 | function stripIds(result) { 1313 | return result.map(function(x) { delete x._id; return x; }); 1314 | } 1315 | 1316 | before(function (done) { 1317 | MongoClient.connect("mongodb://somesortserver/sort_mock_database", function(err, db) { 1318 | sort_db = db; 1319 | sort_collection = connected_db.collection("sorting"); 1320 | sort_collection.insertMany(docs).then(function() { done() }); 1321 | }); 1322 | }); 1323 | after(function(done) { 1324 | sort_db.close().then(done).catch(done) 1325 | }); 1326 | 1327 | it('should sort results by type order', function (done) { 1328 | var sortedDocs = [ 1329 | { sortField: null, otherField: 6}, // null 1330 | { sortField: 42, otherField: 1}, // number 1331 | { sortField: 'foo', otherField: 2}, // string 1332 | { sortField: { foo: 'bar' }, otherField: 8}, // object 1333 | { sortField: [ 1 ], otherField: 4}, // array 1334 | { sortField: true, otherField: 5}, // boolean 1335 | { sortField: date, otherField: 3}, // date 1336 | { sortField: /foo/, otherField: 7} // regex 1337 | ] 1338 | 1339 | var crsr = sort_collection.find({}); 1340 | crsr.should.have.property('sort'); 1341 | crsr.sort({sortField: 1}).toArray(function(err, sortRes) { 1342 | if(err) done(err); 1343 | stripIds(sortRes).should.eql(sortedDocs); 1344 | done(); 1345 | }); 1346 | }); 1347 | 1348 | it('should sort results by type order (reversed)', function (done) { 1349 | var sortedDocs = [ 1350 | { sortField: /foo/, otherField: 7}, // regex 1351 | { sortField: date, otherField: 3}, // date 1352 | { sortField: true, otherField: 5}, // boolean 1353 | { sortField: [ 1 ], otherField: 4}, // array 1354 | { sortField: { foo: 'bar' }, otherField: 8}, // object 1355 | { sortField: 'foo', otherField: 2}, // string 1356 | { sortField: 42, otherField: 1}, // number 1357 | { sortField: null, otherField: 6}, // null 1358 | ] 1359 | 1360 | var crsr = sort_collection.find({}); 1361 | crsr.should.have.property('sort'); 1362 | crsr.sort({sortField: -1}).toArray(function(err, sortRes) { 1363 | if(err) done(err); 1364 | stripIds(sortRes).should.eql(sortedDocs); 1365 | done(); 1366 | }); 1367 | }); 1368 | 1369 | it('should sort results by value', function (done) { 1370 | var sortedDocs = [ 1371 | { sortField: 42, otherField: 1}, // number 1372 | { sortField: 'foo', otherField: 2}, // string 1373 | { sortField: date, otherField: 3}, // date 1374 | { sortField: [ 1 ], otherField: 4}, // array 1375 | { sortField: true, otherField: 5}, // boolean 1376 | { sortField: null, otherField: 6}, // null 1377 | { sortField: /foo/, otherField: 7}, // regex 1378 | { sortField: { foo: 'bar' }, otherField: 8}, // object 1379 | ] 1380 | 1381 | var crsr = sort_collection.find({}); 1382 | crsr.should.have.property('sort'); 1383 | crsr.sort({otherField: 1}).toArray(function(err, sortRes) { 1384 | if(err) done(err); 1385 | stripIds(sortRes).should.eql(sortedDocs); 1386 | done(); 1387 | }); 1388 | }); 1389 | }); 1390 | 1391 | it('should map results', function (done) { 1392 | var crsr = collection.find({}); 1393 | crsr.should.have.property('map'); 1394 | crsr.map(c => c.test).toArray(function(err, res) { 1395 | if (err) done(err); 1396 | var sampleTest = 333; 1397 | res.should.containEql(sampleTest); 1398 | done(); 1399 | }); 1400 | }); 1401 | it('should return stream of documents', function (done) { 1402 | var results = []; 1403 | var crsr = collection.find({}); 1404 | crsr.should.have.property('on'); 1405 | crsr.on('data', function (data) { 1406 | results.push(data); 1407 | }) 1408 | .on('end', function () { 1409 | results.length.should.equal(EXPECTED_TOTAL_TEST_DOCS); 1410 | return done(); 1411 | }); 1412 | }); 1413 | 1414 | it('should compute total with forEach', function (done) { 1415 | collection.insertMany([ 1416 | { testForEach: 1111111, value: 1 }, 1417 | { testForEach: 1111111, value: 2 }, 1418 | { testForEach: 1111111, value: 3 }, 1419 | { testForEach: 1111111, value: 4 } 1420 | ], function (err) { 1421 | if (err) done(err); 1422 | var crsr = collection.find({ testForEach: 1111111 }); 1423 | crsr.should.have.property('forEach'); 1424 | 1425 | var total = 0; 1426 | crsr.forEach(function (doc) { 1427 | total += doc.value; 1428 | }, function() { 1429 | total.should.equal(10); 1430 | done(); 1431 | }); 1432 | }); 1433 | }); 1434 | }); 1435 | 1436 | it('should handle issue #144', () => { 1437 | var productStateCollection = connected_db.collection('ProductState'); 1438 | var productCollection = connected_db.collection('Product'); 1439 | 1440 | var product, state; 1441 | 1442 | return productCollection.insert({ 1443 | name: 'Test' 1444 | }).then((result) => { 1445 | product = result.ops[0]; 1446 | 1447 | assert.ok(product._id); 1448 | assert.ok(product._id.toHexString()); 1449 | assert.strictEqual(product.name, 'Test'); 1450 | 1451 | return productStateCollection.insert({ 1452 | price: 4900 1453 | }); 1454 | }).then((result) => { 1455 | state = result.ops[0]; 1456 | 1457 | assert.ok(state._id); 1458 | assert.ok(state._id.toHexString()); 1459 | assert.strictEqual(state.price, 4900); 1460 | 1461 | return productCollection.findOneAndUpdate( 1462 | { _id: product._id }, 1463 | { $set: { currentState: state } }, 1464 | { returnOriginal: false } 1465 | ); 1466 | }).then((result) => { 1467 | product = result.value; 1468 | 1469 | assert.ok(product._id); 1470 | assert.ok(product._id.toHexString()); 1471 | assert.strictEqual(product.name, 'Test'); 1472 | assert.ok(product.currentState._id); 1473 | assert.ok(product.currentState._id.toHexString()); 1474 | assert.strictEqual(product.currentState.price, 4900); 1475 | }); 1476 | }); 1477 | }); 1478 | --------------------------------------------------------------------------------