├── .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 [](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 |
--------------------------------------------------------------------------------