├── test ├── fixtures │ └── a.txt └── test.js ├── .eslintrc ├── .gitignore ├── binary.js ├── utils.js ├── cursor.js ├── package.json ├── .github └── workflows │ └── main.yml ├── index.js ├── db.js ├── README.md ├── mongo-client.js ├── CHANGELOG.md └── collection.js /test/fixtures/a.txt: -------------------------------------------------------------------------------- 1 | AAA 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "apostrophe" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /binary.js: -------------------------------------------------------------------------------- 1 | module.exports = function (BaseClass) { 2 | function TinselBinary() { 3 | if (!(this instanceof BaseClass)) { 4 | return new BaseClass(...arguments); 5 | } 6 | } 7 | 8 | return TinselBinary; 9 | }; 10 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | const toTinsel = Symbol.for('@@mdb.callbacks.toTinsel'); 2 | 3 | const omit = function (obj, keys) { 4 | const n = {}; 5 | Object.keys(obj).forEach(function(key) { 6 | if (keys.indexOf(key) === -1) { 7 | n[key] = obj[key]; 8 | } 9 | }); 10 | 11 | return n; 12 | }; 13 | 14 | module.exports = { 15 | toTinsel, 16 | omit 17 | }; 18 | -------------------------------------------------------------------------------- /cursor.js: -------------------------------------------------------------------------------- 1 | const { toTinsel } = require('./utils.js'); 2 | 3 | module.exports = function (baseClass) { 4 | class TinselCursor extends baseClass { 5 | get __emulated() { 6 | return true; 7 | } 8 | 9 | nextObject(callback) { 10 | return super.next(callback); 11 | } 12 | } 13 | 14 | Object.defineProperty( 15 | baseClass.prototype, 16 | toTinsel, 17 | { 18 | enumerable: false, 19 | value: function () { 20 | return Object.setPrototypeOf(this, TinselCursor.prototype); 21 | } 22 | } 23 | ); 24 | 25 | return TinselCursor; 26 | }; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emulate-mongo-2-driver", 3 | "version": "1.3.7", 4 | "description": "Emulate the Mongo 2.x nodejs driver on top of the Mongo 3.x nodejs driver, for bc", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npx eslint . && mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/apostrophecms/emulate-mongo-2-driver.git" 12 | }, 13 | "keywords": [ 14 | "mongodb", 15 | "mongo", 16 | "apostrophecms" 17 | ], 18 | "author": "Apostrophe Technologies", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/apostrophecms/emulate-mongo-2-driver/issues" 22 | }, 23 | "homepage": "https://github.com/apostrophecms/emulate-mongo-2-driver#readme", 24 | "dependencies": { 25 | "@apostrophecms/emulate-mongo-3-driver": "^1.0.6" 26 | }, 27 | "devDependencies": { 28 | "eslint": "^8.57.0", 29 | "eslint-config-apostrophe": "^4.3.0", 30 | "eslint-config-standard": "^17.1.0", 31 | "eslint-plugin-import": "^2.18.2", 32 | "eslint-plugin-node": "^11.1.0", 33 | "eslint-plugin-promise": "^6.1.1", 34 | "mocha": "^6.2.2" 35 | } 36 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: tests 4 | 5 | # Controls when the action will run. 6 | on: 7 | push: 8 | branches: [ '*' ] 9 | pull_request: 10 | branches: [ '*' ] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 16 | jobs: 17 | # This workflow contains a single job called "build" 18 | build: 19 | # The type of runner that the job will run on 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | node-version: [16, 18, 20] 24 | mongodb-version: [4.4, 5.0, 6.0] 25 | 26 | # Steps represent a sequence of tasks that will be executed as part of the job 27 | steps: 28 | - name: Git checkout 29 | uses: actions/checkout@v2 30 | 31 | - name: Use Node.js ${{ matrix.node-version }} 32 | uses: actions/setup-node@v1 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | 36 | - name: Start MongoDB 37 | uses: supercharge/mongodb-github-action@1.3.0 38 | with: 39 | mongodb-version: ${{ matrix.mongodb-version }} 40 | 41 | - run: npm install 42 | 43 | - run: npm test 44 | env: 45 | CI: true 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const mongodb = require('@apostrophecms/emulate-mongo-3-driver'); 2 | const binary = require('./binary.js'); 3 | const collection = require('./collection.js'); 4 | const cursor = require('./cursor.js'); 5 | const db = require('./db.js'); 6 | const mongoClient = require('./mongo-client.js'); 7 | 8 | const emulateClasses = new Map([ 9 | [ 'Binary', binary ], 10 | [ 'FindCursor', cursor ], 11 | [ 'AggregationCursor', cursor ], 12 | [ 'Collection', collection ], 13 | [ 'Db', db ], 14 | [ 'MongoClient', mongoClient ] 15 | ]); 16 | 17 | const entries = Object.entries(mongodb); 18 | for (const [ mongodbExportName, mongodbExportValue ] of entries) { 19 | const emulateClass = emulateClasses.get(mongodbExportName); 20 | if (emulateClass != null) { 21 | const patchedClass = emulateClass(mongodbExportValue); 22 | Object.defineProperty( 23 | module.exports, 24 | mongodbExportName, 25 | { 26 | enumerable: true, 27 | get: function () { 28 | return patchedClass; 29 | } 30 | } 31 | ); 32 | } else { 33 | Object.defineProperty( 34 | module.exports, 35 | mongodbExportName, 36 | { 37 | enumerable: true, 38 | get: function () { 39 | return mongodbExportValue; 40 | } 41 | } 42 | ); 43 | } 44 | } 45 | 46 | // TODO: https://github.com/mongodb/node-mongodb-native/blob/master/CHANGES_3.0.0.md#bulkwriteresult--bulkwriteerror (we don't use it) 47 | // https://github.com/mongodb/node-mongodb-native/blob/master/CHANGES_3.0.0.md#mapreduce-inlined-results (we don't use it) 48 | // See others on that page 49 | -------------------------------------------------------------------------------- /db.js: -------------------------------------------------------------------------------- 1 | const { toTinsel } = require('./utils.js'); 2 | 3 | module.exports = function (baseClass) { 4 | class TinselDb extends baseClass { 5 | get __emulated() { 6 | return true; 7 | } 8 | 9 | collection(name, options, callback) { 10 | if (arguments.length === 1) { 11 | return super.collection(name)[toTinsel](); 12 | } 13 | if (arguments.length === 2) { 14 | if ((typeof options) !== 'function') { 15 | return super.collection(name, options)[toTinsel](); 16 | } else { 17 | callback = options; 18 | return super.collection(name, {}, function(err, collection) { 19 | if (err) { 20 | return callback(err); 21 | } 22 | const tinselled = collection[toTinsel](); 23 | return callback(null, tinselled); 24 | }); 25 | } 26 | } 27 | 28 | return super.collection(name, options, function(err, collection) { 29 | if (err) { 30 | return callback(err); 31 | } 32 | return callback(null, collection[toTinsel]()); 33 | }); 34 | }; 35 | 36 | // Reintroduce the "db" method of db objects, for talking to a second 37 | // database via the same connection 38 | db(name) { 39 | return this.client.db(name); 40 | }; 41 | 42 | // Reintroduce the "close" method of db objects, yes it closes 43 | // the entire client, did that before too 44 | close(force, callback) { 45 | return this.client.close(force, callback); 46 | }; 47 | } 48 | 49 | Object.defineProperty( 50 | baseClass.prototype, 51 | toTinsel, 52 | { 53 | enumerable: false, 54 | value: function () { 55 | return Object.setPrototypeOf(this, TinselDb.prototype); 56 | } 57 | } 58 | ); 59 | 60 | return TinselDb; 61 | }; 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # emulate-mongo-2-driver 2 | 3 | ## Purpose 4 | 5 | You have legacy code that depends on the 2.x version of the MongoDB Node.js driver. You don't want to upgrade to the 3.x driver because of [backwards compability problems](https://github.com/mongodb/node-mongodb-native/blob/master/CHANGES_3.0.0.md), but you don't have a choice because of reported vulnerabilities such as those detected by `npm audit`. 6 | 7 | `emulate-mongo-2-driver` aims to be a highly compatible emulation of the 2.x version of the MongoDB Node.js driver, implemented as a wrapper for the 3.x driver. 8 | 9 | It was created for long term support of [ApostropheCMS 2.x](https://apostrophecms.com). Of course, ApostropheCMS 3.x will use the MongoDB 3.x driver directly. 10 | 11 | ## Usage 12 | 13 | If you are using ApostropheCMS, this is **standard** beginning with version 2.101.0. You don't have to do anything. The example below is for those who wish to use this driver in non-ApostropheCMS projects. 14 | 15 | ``` 16 | npm install emulate-mongo-2-driver 17 | ``` 18 | 19 | ```javascript 20 | const mongo = require('emulate-mongo-2-driver'); 21 | 22 | // Use it here as if it were the 2.x driver 23 | ``` 24 | 25 | ## Goals 26 | 27 | This module aims for complete compatibility with the [2.x features mentioned as obsolete or changed here](https://github.com/mongodb/node-mongodb-native/blob/master/CHANGES_3.0.0.md) but there may be omissions. An emphasis has been placed on features used by ApostropheCMS but PRs for further compatibility are welcome. 28 | 29 | ## What about those warnings? 30 | 31 | "What about the warnings re: insert, update and ensureIndex operations being obsolete?" 32 | 33 | Although deprecated, these operations are supported by the 3.x driver and work just fine. 34 | 35 | However, since the preferred newer operations were also supported by the 2.x driver, the path forward is clear. We will migrate away from using them gradually, and you should do the same. It doesn't make sense to provide "deprecation-free" wrappers when doing the right thing is in easy reach. 36 | -------------------------------------------------------------------------------- /mongo-client.js: -------------------------------------------------------------------------------- 1 | const URL = require('url').URL; 2 | const { toTinsel, omit } = require('./utils.js'); 3 | 4 | function reencode(s) { 5 | return encodeURIComponent(decodeURIComponent(s)); 6 | } 7 | 8 | function parseUri(uri) { 9 | let parsed; 10 | try { 11 | parsed = new URL(uri); 12 | } catch (e) { 13 | // MongoDB driver tolerates URIs that the WHATWG parser will not, 14 | // deal with the common cases 15 | // eslint-disable-next-line no-useless-escape 16 | const matches = uri.match(/mongodb:\/\/(([^:]+):([^@]+)@)?([^\/]+)(\/([^?]+))?(\?(.*))?$/); 17 | const newUri = 'mongodb://' + (matches[1] ? (reencode(matches[2]) + ':' + reencode(matches[3]) + '@') : '') + reencode(matches[4]) + (matches[5] ? ('/' + matches[6]) : '') + (matches[7] ? ('?' + matches[8]) : ''); 18 | parsed = new URL(newUri); 19 | } 20 | return parsed; 21 | }; 22 | 23 | module.exports = function (baseClass) { 24 | class TinselMongoClient extends baseClass { 25 | get __emulated() { 26 | return true; 27 | } 28 | 29 | static connect(uri, options, callback) { 30 | if ((!callback) && ((typeof options) === 'function')) { 31 | callback = options; 32 | options = {}; 33 | } 34 | if (!options) { 35 | options = {}; 36 | } 37 | if (options.useUnifiedTopology) { 38 | // Per warnings these three options have no meaning with the 39 | // unified topology. Swallow them so that apostrophe 2.x doesn't 40 | // need to directly understand a mongodb 3.x driver option 41 | options = omit(options, [ 'autoReconnect', 'reconnectTries', 'reconnectInterval' ]); 42 | } 43 | if ((typeof callback) === 'function') { 44 | return super.connect(uri, options, function(err, client) { 45 | if (err) { 46 | return callback(err); 47 | } 48 | const parsed = parseUri(uri); 49 | try { 50 | return callback(null, client.db(parsed.pathname.substr(1))); 51 | } catch (e) { 52 | return callback(e); 53 | } 54 | }); 55 | } 56 | return super.connect(uri, options).then(function(client) { 57 | const parsed = parseUri(uri); 58 | return client.db(parsed.pathname.substr(1)); 59 | }); 60 | } 61 | 62 | db(dbName, options) { 63 | return super.db(dbName, options)[toTinsel](); 64 | } 65 | } 66 | 67 | return TinselMongoClient; 68 | }; 69 | 70 | // Convert (err, client) back to (err, db) in both callback driven 71 | // and promisified flavors 72 | 73 | // TODO: also wrap legacy db.open? We never used it. See: 74 | // See https://github.com/mongodb/node-mongodb-native/blob/3.0/CHANGES_3.0.0.md 75 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.3.7 (2025-08-28) 4 | 5 | * Work around a sudden change in the upstream mongodb driver's `findOne` support by reimplementing `findOne()` calls in terms of `find()`. 6 | 7 | ## 1.3.6 8 | 9 | ### Changes 10 | 11 | * Use `@apostrophecms/emulate-mongo-3-driver` `1.0.6`. 12 | 13 | ## 1.3.5 14 | 15 | ### Changes 16 | 17 | * Use `@apostrophecms/emulate-mongo-3-driver` `1.0.5`. 18 | 19 | ## 1.3.4 20 | 21 | ### Changes 22 | 23 | * Use `@apostrophecms/emulate-mongo-3-driver` `1.0.4`. 24 | 25 | ## 1.3.3 26 | 27 | ### Fix 28 | 29 | * Add support for creating `Binary` instances without using `new`. 30 | 31 | ## 1.3.2 32 | 33 | ### Changes 34 | 35 | * Replace `mongodb@3` with `@apostrophecms/emulate-mongo-3-driver` with `mongodb@6`. 36 | 37 | ## 1.3.1 38 | 39 | Temporarily reverted to reliance on the official mongodb driver version 3.x. 40 | 41 | ## 1.3.0 42 | 43 | Released version based on emulate-mongo-3-driver, causing a temporary regression. 44 | 45 | ## 1.2.3 46 | 47 | * Fixed issue that caused `count` to disregard its criteria object in at least some cases. 48 | * The `.db` method of a database object now works as it does with the 2.x driver, even if 49 | used to create a third generation of object. 50 | 51 | ## 1.2.2 52 | 53 | * Fixed issue when calling `aggregate` with a `cursor` option. Thanks to Mohamad Yusri for identifying the issue. 54 | * Fixed issue when calling `aggregate` with no callback and no cursor option. Thanks again to Mohamad Yusri for identifying the issue. 55 | 56 | ## 1.2.1 57 | 58 | * Fixed incompatible default behavior for `aggregate()`. Although the 2.x mongodb driver documentation suggests it would not return a cursor when the cursor option is not present, it actually does so at any time when there is no callback provided. This broke certain workflow scenarios in Apostrophe. 59 | 60 | ## 1.2.0 61 | 62 | * Deprecation warning eliminated for `count` via use of `countDocuments` where possible, and where not (use of `$near` with `count` for instance), fetching of all `_id` properties as a minimal projection to arrive at a count. 63 | * Fixed bugs in wrapper for `aggregate()`. 64 | * Support for `cursor: true` option to `aggregate()`. 65 | 66 | ## 1.1.0 67 | 68 | * Deprecation warnings eliminated through emulation of all of the common deprecated methods. Test coverage included. A warning will still appear if you do not pass `useUnifiedTopology: true`. See the README for details on how to do that in ApostropheCMS. 69 | * Three options passed by Apostrpohe that are neither valid nor meaningful with `useUnifiedTopology: true` are automatically discarded when it is present, to prevent more deprecation warnings. 70 | 71 | ## 1.0.4 72 | 73 | Node 13.x compatibility. Resolves [this bug report](https://github.com/apostrophecms/apostrophe/issues/2120). 74 | 75 | ## 1.0.3 76 | 77 | URI tolerance change from 1.0.2 now covers connect() with a promise as well. 78 | 79 | ## 1.0.2 80 | 81 | Tolerate MongoDB URIs that break the WHATWG URL parser. 82 | 83 | ## 1.0.1 84 | 85 | We no longer set `useNewUrlParser` and `useUnifiedTopology` by default. These caused bc breaks for some and while the old parser and topology generate deprecation warnings they work correctly with 2.x code. We shouldn't default these on again unless we've added measures to mitigate incompatibilities with 2.x code. 86 | 87 | ## 1.0.0 88 | 89 | Initial release. 90 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const mongo = require('../index.js'); 2 | const assert = require('assert'); 3 | const fs = require('fs'); 4 | 5 | describe('use mongodb 3 driver in a 2.x style', function() { 6 | let db; 7 | let trees; 8 | 9 | after(function(done) { 10 | if (trees) { 11 | trees.remove({}, function(err) { 12 | assert.ifError(err); 13 | db.close(function(err) { 14 | assert.ifError(err); 15 | done(); 16 | }); 17 | }); 18 | } 19 | }); 20 | 21 | it('binary', function(done) { 22 | try { 23 | const binary = mongo.Binary(fs.readFileSync('./test/fixtures/a.txt')); 24 | 25 | const actual = binary.toString(); 26 | const expected = 'AAA\n'; 27 | assert.deepEqual(actual, expected); 28 | done(); 29 | } catch (error) { 30 | done(error); 31 | } 32 | }); 33 | it('binary with new', function(done) { 34 | try { 35 | const binary = new mongo.Binary(fs.readFileSync('./test/fixtures/a.txt')); 36 | 37 | const actual = binary.toString(); 38 | const expected = 'AAA\n'; 39 | assert.deepEqual(actual, expected); 40 | done(); 41 | } catch (error) { 42 | done(error); 43 | } 44 | }); 45 | 46 | it('connects', function(done) { 47 | return mongo.MongoClient.connect('mongodb://localhost:27017/testdb', function (err, _db) { 48 | assert.ifError(err); 49 | assert(_db.__emulated); 50 | assert(_db); 51 | assert(_db.collection); 52 | db = _db; 53 | done(); 54 | }); 55 | }); 56 | it('gets collection', function(done) { 57 | return db.collection('trees', function(err, collection) { 58 | assert.ifError(err); 59 | assert(collection); 60 | assert(collection.__emulated); 61 | trees = collection; 62 | done(); 63 | }); 64 | }); 65 | it('indexes collection', function() { 66 | return trees.ensureIndex({ 67 | location: '2dsphere' 68 | }); 69 | }); 70 | it('inserts', function(done) { 71 | return trees.insert([ 72 | { 73 | kind: 'spruce', 74 | leaves: 5 75 | }, 76 | { 77 | kind: 'pine', 78 | leaves: 10 79 | } 80 | ], function(err) { 81 | assert.ifError(err); 82 | done(); 83 | }); 84 | }); 85 | it('finds without projection', function(done) { 86 | const cursor = trees.find({}); 87 | assert(cursor.__emulated); 88 | cursor.sort({ leaves: 1 }).toArray(function(err, result) { 89 | assert.ifError(err); 90 | assert(result); 91 | assert(result[0]); 92 | assert(result[1]); 93 | assert.equal(result[0].kind, 'spruce'); 94 | assert.equal(result[0].leaves, 5); 95 | assert.equal(result[1].kind, 'pine'); 96 | assert.equal(result[1].leaves, 10); 97 | done(); 98 | }); 99 | }); 100 | it('finds with projection', function(done) { 101 | return trees.find({}, { kind: 1 }).sort({ leaves: 1 }).toArray(function(err, result) { 102 | assert.ifError(err); 103 | assert(result); 104 | assert(result[0]); 105 | assert.equal(result[0].kind, 'spruce'); 106 | assert(!result[0].leaves); 107 | done(); 108 | }); 109 | }); 110 | it('findOne', function(done) { 111 | return trees.findOne({ leaves: 5 }, function(err, result) { 112 | assert.ifError(err); 113 | assert(result); 114 | assert.equal(result.kind, 'spruce'); 115 | done(); 116 | }); 117 | }); 118 | it('findOne with projection', function(done) { 119 | return trees.findOne({ leaves: 5 }, { kind: 1 }, function(err, result) { 120 | assert.ifError(err); 121 | assert(result); 122 | assert(result.kind); 123 | assert(!result.leaves); 124 | assert.equal(result.kind, 'spruce'); 125 | done(); 126 | }); 127 | }); 128 | it('find with nextObject', function(done) { 129 | const cursor = trees.find({}).sort({ leaves: 1 }); 130 | cursor.nextObject(function(err, result) { 131 | assert.ifError(err); 132 | assert(result); 133 | assert(result.leaves === 5); 134 | cursor.nextObject(function(err, result) { 135 | assert.ifError(err); 136 | assert(result); 137 | assert(result.leaves === 10); 138 | done(); 139 | }); 140 | }); 141 | }); 142 | it('updates one with an atomic operator', function(done) { 143 | return trees.update({ kind: 'spruce' }, { $set: { kind: 'puce' } }, function(err, status) { 144 | assert(!err); 145 | assert(status.result.nModified === 1); 146 | return trees.findOne({ kind: 'puce' }, function(err, obj) { 147 | assert(!err); 148 | assert(obj); 149 | done(); 150 | }); 151 | }); 152 | }); 153 | it('updates one without an atomic operator', function(done) { 154 | return trees.update({ kind: 'puce' }, { 155 | kind: 'truce', 156 | leaves: 70 157 | }, function(err, status) { 158 | assert(!err); 159 | assert(status.result.nModified === 1); 160 | return trees.findOne({ kind: 'truce' }, function(err, obj) { 161 | assert(!err); 162 | assert(obj); 163 | done(); 164 | }); 165 | }); 166 | }); 167 | it('updates many with an atomic operator', function(done) { 168 | return trees.update( 169 | { leaves: { $gte: 1 } }, 170 | { $set: { age: 50 } }, 171 | { multi: true }, 172 | function(err, status) { 173 | assert(!err); 174 | assert(status.result.nModified === 2); 175 | return trees.find({}).toArray(function(err, trees) { 176 | assert(!err); 177 | assert(trees.length > 1); 178 | assert(!trees.find(tree => (tree.leaves > 0) && (tree.age !== 50))); 179 | done(); 180 | }); 181 | } 182 | ); 183 | }); 184 | it('updates many without an atomic operator', function(done) { 185 | return trees.update({ leaves: { $gte: 1 } }, { 186 | leaves: 1, 187 | kind: 'boring' 188 | }, { multi: true }, function(err, status) { 189 | assert(!err); 190 | assert(status.result.nModified === 2); 191 | return trees.find({}).toArray(function(err, trees) { 192 | assert(!err); 193 | assert(trees.length > 1); 194 | assert(!trees.find(tree => (tree.leaves !== 1) || (tree.kind !== 'boring') || tree.age)); 195 | done(); 196 | }); 197 | }); 198 | }); 199 | it('updates many without an atomic operator, using promises', function() { 200 | return trees.update( 201 | { leaves: { $gte: 1 } }, 202 | { ohmy: true }, 203 | { multi: true } 204 | ) 205 | .then(function(status) { 206 | return trees.find({}).toArray(); 207 | }) 208 | .then(function(trees) { 209 | assert(trees.length > 1); 210 | assert(!trees.find(tree => (tree.ohmy !== true) || tree.leaves)); 211 | }); 212 | }); 213 | it('aggregation query works', function() { 214 | return trees.aggregate([ 215 | { 216 | $match: { 217 | ohmy: true 218 | } 219 | } 220 | ]).toArray().then(function(result) { 221 | assert(result); 222 | assert(Array.isArray(result)); 223 | assert(result.length); 224 | }); 225 | }); 226 | it('aggregation query works with callback', function(done) { 227 | return trees.aggregate([ 228 | { 229 | $match: { 230 | ohmy: true 231 | } 232 | } 233 | ], function(err, result) { 234 | assert(!err); 235 | assert(result); 236 | assert(Array.isArray(result)); 237 | assert(result.length); 238 | done(); 239 | }); 240 | }); 241 | it('aggregation query works with cursor: { batchSize: 1 }', function() { 242 | return trees.aggregate([ 243 | { 244 | $match: { 245 | ohmy: true 246 | } 247 | } 248 | ], { 249 | cursor: { batchSize: 1 } 250 | }).toArray().then(function(result) { 251 | assert(result); 252 | assert(Array.isArray(result)); 253 | assert(result.length); 254 | }); 255 | }); 256 | it('aggregation with aggregation pipeline with cursor', async function() { 257 | const cursor = await trees.aggregate([ 258 | { $match: { ohmy: true } }, 259 | { 260 | $group: { 261 | _id: null, 262 | count: { $sum: 1 } 263 | } 264 | } 265 | ]); 266 | const result = await cursor.toArray(); 267 | assert(result); 268 | assert.equal(result.length, 1); 269 | assert(result[0].count >= 0); 270 | }); 271 | it('count query works', function() { 272 | return trees.count().then(function(result) { 273 | assert(result > 0); 274 | }); 275 | }); 276 | it('count query works with callback', function(done) { 277 | return trees.count(function(err, count) { 278 | assert(!err); 279 | assert(count > 0); 280 | done(); 281 | }); 282 | }); 283 | it('insert test data for count with criteria', function() { 284 | return trees.insert([ 285 | { 286 | wiggy: true 287 | }, 288 | { 289 | wiggy: true 290 | }, 291 | { 292 | wiggy: false 293 | } 294 | ]); 295 | }); 296 | it('count works with criteria and callback', function(done) { 297 | return trees.find({ 298 | wiggy: true 299 | }).count(function(err, count) { 300 | assert(!err); 301 | assert(count === 2); 302 | done(); 303 | }); 304 | }); 305 | it('count works with criteria and callback', function(done) { 306 | return trees.find({ 307 | wiggy: false 308 | }).count(function(err, count) { 309 | assert(!err); 310 | assert(count === 1); 311 | done(); 312 | }); 313 | }); 314 | it('count works with $near', function() { 315 | return trees.insert({ 316 | location: { 317 | type: 'Point', 318 | coordinates: [ -73.9667, 40.78 ] 319 | } 320 | }).then(function() { 321 | return trees.count({ 322 | location: { 323 | $near: { 324 | $geometry: { 325 | type: 'Point', 326 | coordinates: [ -73.9667, 40.78 ], 327 | maxDistance: 100 328 | } 329 | } 330 | } 331 | }); 332 | }).then(function(count) { 333 | assert(count === 1); 334 | }); 335 | }); 336 | it('count works with $near and a callback', function(done) { 337 | trees.count({ 338 | location: { 339 | $near: { 340 | $geometry: { 341 | type: 'Point', 342 | coordinates: [ -73.9667, 40.78 ], 343 | maxDistance: 100 344 | } 345 | } 346 | } 347 | }, function(err, count) { 348 | assert(!err); 349 | assert(count === 1); 350 | done(); 351 | }); 352 | }); 353 | it('client.db works twice to get another connection', function() { 354 | const db2 = db.db('testdb2'); 355 | return db2.collection('trees').count({}).then(function(trees) { 356 | assert(!trees.length); 357 | }).then(function() { 358 | const db3 = db2.db('testdb3'); 359 | return db3.collection('trees').count({}).then(function(trees) { 360 | assert(!trees.length); 361 | }); 362 | }); 363 | }); 364 | }); 365 | -------------------------------------------------------------------------------- /collection.js: -------------------------------------------------------------------------------- 1 | const { toTinsel, omit } = require('./utils.js'); 2 | 3 | function hasNestedProperties(object, properties) { 4 | for (const key of Object.keys(object)) { 5 | if (properties.indexOf(key) !== -1) { 6 | return true; 7 | } 8 | if (object[key] && ((typeof object[key]) === 'object')) { 9 | if (hasNestedProperties(object[key], properties)) { 10 | return true; 11 | } 12 | } 13 | } 14 | return false; 15 | } 16 | 17 | module.exports = function (baseClass) { 18 | class TinselCollection extends baseClass { 19 | get __emulated() { 20 | return true; 21 | } 22 | 23 | // conversion APIs 24 | aggregate(op1 /* , op2... */, callback) { 25 | const last = arguments.length && arguments[arguments.length - 1]; 26 | // Bring back support for operations as a variable number of 27 | // parameters rather than as an array 28 | if (Array.isArray(op1)) { 29 | const options = arguments[1]; 30 | if (options && ((typeof options) === 'object')) { 31 | if (options.cursor) { 32 | // Behaves 100% like 3.x, so pass straight through 33 | return super.aggregate(...Array.prototype.slice(arguments)); 34 | } 35 | } 36 | // Normal: array of aggregate stages 37 | if ((typeof last) === 'function') { 38 | // 2.x driver took a callback or returned a promise for results directly, 39 | // 3.x driver always returns a cursor so convert back to results 40 | return super.aggregate(op1).toArray(last); 41 | } else { 42 | // Both 2.x and 3.x return a cursor in the absence of a callback, 43 | // despite documentation implying you must explicitly ask 44 | // for a cursor 45 | return super.aggregate(op1); 46 | } 47 | } else { 48 | // Positional arguments as aggregate stages (2.x supports, 3.x does not) 49 | if ((typeof last) === 'function') { 50 | // 2.x driver supported passing a callback rather than 51 | // returning a cursor, 3.x driver does not 52 | return super.aggregate( 53 | ...Array.prototype.slice.call(arguments, 0, arguments.length - 1) 54 | ) 55 | .toArray(last); 56 | } else { 57 | // Both 2.x and 3.x return a cursor in the absence of a callback, 58 | // despite documentation implying you must explicitly ask 59 | // for a cursor 60 | return super.aggregate(...Array.prototype.slice.call(arguments)); 61 | } 62 | } 63 | }; 64 | 65 | // initializeUnorderedBulkOp(options) { 66 | // return super.initializeUnorderedBulkOp(options)[toTinsel](); 67 | // } 68 | // 69 | // initializeOrderedBulkOp(options) { 70 | // return super.initializeOrderedBulkOp(options)[toTinsel](); 71 | // } 72 | 73 | find(filter, projection) { 74 | const cursor = super.find(filter)[toTinsel](); 75 | if (projection) { 76 | return cursor.project(projection); 77 | } 78 | 79 | return cursor; 80 | } 81 | 82 | // listIndexes(options) { 83 | // return super.listIndexes(options)[toTinsel](); 84 | // } 85 | // 86 | // watch(pipeline, options) { 87 | // return super.watch(pipeline, options)[toTinsel](); 88 | // } 89 | 90 | // Before this module existed, Apostrophe patched this into 91 | // the mongodb collection prototype 92 | findWithProjection(filter, projection) { 93 | return this.find(filter, projection); 94 | } 95 | 96 | // Reimplement findOne in terms of find(), because find() works. findOne broke 97 | // here most likely due to a recent update of the mongodb driver, even though it 98 | // works fine in meulate-mongo-3-driver alone. A big hint is that 99 | // emulate-mongo-3-driver never touches findOne 100 | 101 | findOne(criteria, projection, callback) { 102 | if ((arguments.length === 1) && ((typeof criteria) === 'function')) { 103 | callback = arguments[0]; 104 | projection = null; 105 | criteria = {}; 106 | } else if (arguments.length === 0) { 107 | criteria = {}; 108 | projection = null; 109 | callback = null; 110 | } 111 | if (projection && ((typeof projection) === 'object')) { 112 | if (callback) { 113 | // Mustn't return the promise here, mocha gets mad 114 | this.find(criteria, projection).limit(1).toArray().then(docs => { 115 | return callback(null, docs[0]); 116 | }).catch(e => { 117 | return callback(e); 118 | }); 119 | } else { 120 | return this.find(criteria, projection).limit(1).toArray().then(docs => { 121 | return docs[0]; 122 | }); 123 | } 124 | } else { 125 | callback = projection; 126 | projection = null; 127 | if (callback) { 128 | // Mustn't return the promise here, mocha gets mad 129 | this.find(criteria).limit(1).toArray().then(docs => { 130 | return callback(null, docs[0]); 131 | }).catch(e => { 132 | return callback(e); 133 | }); 134 | } else { 135 | return this.find(criteria).limit(1).toArray().then(docs => { 136 | return docs[0]; 137 | }); 138 | } 139 | } 140 | }; 141 | 142 | // ensureIndex is deprecated but createIndex has exactly the 143 | // same behavior 144 | 145 | ensureIndex(fieldOrSpec, options, callback) { 146 | return this.createIndex(fieldOrSpec, options, callback); 147 | }; 148 | 149 | insert(docs, options, callback) { 150 | return super.insert(docs, options, callback); 151 | }; 152 | 153 | remove(selector, options, callback) { 154 | return super.remove(selector, options, callback); 155 | }; 156 | 157 | update(selector, doc, _options, callback) { 158 | const takesCallback = (typeof arguments[arguments.length - 1]) === 'function'; 159 | const options = _options && ((typeof _options) === 'object') ? _options : {}; 160 | let multi; 161 | let atomic; 162 | multi = options.multi; 163 | if (doc._id) { 164 | // Cannot match more than one, and would confuse our 165 | // don't-repeat-the-ids algorithm if we tried to use it 166 | multi = false; 167 | } 168 | let i; 169 | const keys = Object.keys(doc); 170 | let _ids; 171 | let nModified; 172 | for (i = 0; (i < keys.length); i++) { 173 | if (keys[i].substring(0, 1) === '$') { 174 | atomic = true; 175 | break; 176 | } 177 | } 178 | if (atomic) { 179 | // Undeprecated equivalents 180 | if (multi) { 181 | arguments[2] = omit(arguments[2], [ 'multi' ]); 182 | return this.updateMany(...Array.prototype.slice.call(arguments)); 183 | } else { 184 | return this.updateOne(...Array.prototype.slice.call(arguments)); 185 | } 186 | } else { 187 | 188 | if (multi) { 189 | 190 | arguments[2] = omit(arguments[2], [ 'multi' ]); 191 | 192 | // There is no replaceMany, so we have to do this repeatedly until 193 | // we run out of matching documents. We also have to get all of the 194 | // relevant _ids up front so we don't repeat them. It is a royal 195 | // pain in the tuckus. 196 | // 197 | // Fortunately it is rarely used. 198 | 199 | const collection = this; 200 | const promise = getIds(collection).then(function(docs) { 201 | _ids = docs.map(function(doc) { 202 | return doc._id; 203 | }); 204 | nModified = 0; 205 | return attemptMulti(collection); 206 | }).then(function() { 207 | return completeMulti(null, { 208 | result: { 209 | nModified, 210 | ok: 1 211 | } 212 | }); 213 | }).catch(completeMulti); 214 | 215 | if (takesCallback) { 216 | return null; 217 | } else { 218 | return promise; 219 | } 220 | 221 | } else { 222 | return this.replaceOne(...Array.prototype.slice.call(arguments)); 223 | } 224 | } 225 | 226 | function getIds(collection) { 227 | return collection.find(selector).project({ _id: 1 }).toArray(); 228 | } 229 | 230 | function attemptMulti(collection) { 231 | if (!_ids.length) { 232 | return null; 233 | } 234 | const _selector = Object.assign({}, selector, { 235 | _id: _ids.shift() 236 | }); 237 | return collection.replaceOne(_selector, doc, options).then(function(status) { 238 | nModified += status.result.nModified; 239 | return attemptMulti(collection); 240 | }).catch(function(err) { 241 | return completeMulti(err); 242 | }); 243 | } 244 | 245 | function completeMulti(err, response) { 246 | if (takesCallback) { 247 | return callback(err, response); 248 | } else { 249 | if (err) { 250 | throw err; 251 | } else { 252 | return response; 253 | } 254 | } 255 | } 256 | 257 | }; 258 | 259 | count(query, options, callback) { 260 | if (arguments.length === 2) { 261 | if ((typeof options) === 'function') { 262 | callback = options; 263 | options = {}; 264 | } 265 | } else if (arguments.length === 1) { 266 | if ((typeof query) === 'function') { 267 | callback = query; 268 | options = {}; 269 | query = {}; 270 | } else { 271 | options = {}; 272 | } 273 | } else if (!arguments.length) { 274 | options = {}; 275 | query = {}; 276 | } 277 | if (hasNestedProperties(query, [ '$where', '$near', '$nearSphere' ])) { 278 | // Queries not supported by countDocuments must be turned into a 279 | // find() that actually fetches the ids (minimum projection) 280 | // and returns the number of documents 281 | const cursor = this.find(query); 282 | if (options.limit !== undefined) { 283 | cursor.limit(options.limit); 284 | } 285 | if (options.skip !== undefined) { 286 | cursor.skip(options.skip); 287 | } 288 | if (options.hint !== undefined) { 289 | cursor.hint(options.hint); 290 | } 291 | const p = cursor.project({ _id: 1 }).toArray().then(function(objects) { 292 | if (callback) { 293 | callback(null, objects.length); 294 | return null; 295 | } else { 296 | return objects.length; 297 | } 298 | }).catch(function(e) { 299 | if (callback) { 300 | callback(e); 301 | return null; 302 | } else { 303 | throw e; 304 | } 305 | }); 306 | if (!callback) { 307 | return p; 308 | } 309 | } else { 310 | const p = this.countDocuments(query, options).then(function(count) { 311 | if (callback) { 312 | callback(null, count); 313 | return null; 314 | } else { 315 | return count; 316 | } 317 | }).catch(function(e) { 318 | if (callback) { 319 | callback(e); 320 | return null; 321 | } else { 322 | throw e; 323 | } 324 | }); 325 | if (!callback) { 326 | return p; 327 | } 328 | } 329 | }; 330 | } 331 | 332 | Object.defineProperty( 333 | baseClass.prototype, 334 | toTinsel, 335 | { 336 | enumerable: false, 337 | value: function () { 338 | return Object.setPrototypeOf(this, TinselCollection.prototype); 339 | } 340 | } 341 | ); 342 | 343 | return TinselCollection; 344 | }; 345 | --------------------------------------------------------------------------------