UNPKG

emulate-mongo-2-driver

Version:

Emulate the Mongo 2.x nodejs driver on top of the Mongo 3.x nodejs driver, for bc

345 lines (316 loc) 10.4 kB
const { toTinsel, omit } = require('./utils.js'); function hasNestedProperties(object, properties) { for (const key of Object.keys(object)) { if (properties.indexOf(key) !== -1) { return true; } if (object[key] && ((typeof object[key]) === 'object')) { if (hasNestedProperties(object[key], properties)) { return true; } } } return false; } module.exports = function (baseClass) { class TinselCollection extends baseClass { get __emulated() { return true; } // conversion APIs aggregate(op1 /* , op2... */, callback) { const last = arguments.length && arguments[arguments.length - 1]; // Bring back support for operations as a variable number of // parameters rather than as an array if (Array.isArray(op1)) { const options = arguments[1]; if (options && ((typeof options) === 'object')) { if (options.cursor) { // Behaves 100% like 3.x, so pass straight through return super.aggregate(...Array.prototype.slice(arguments)); } } // Normal: array of aggregate stages if ((typeof last) === 'function') { // 2.x driver took a callback or returned a promise for results directly, // 3.x driver always returns a cursor so convert back to results return super.aggregate(op1).toArray(last); } else { // Both 2.x and 3.x return a cursor in the absence of a callback, // despite documentation implying you must explicitly ask // for a cursor return super.aggregate(op1); } } else { // Positional arguments as aggregate stages (2.x supports, 3.x does not) if ((typeof last) === 'function') { // 2.x driver supported passing a callback rather than // returning a cursor, 3.x driver does not return super.aggregate( ...Array.prototype.slice.call(arguments, 0, arguments.length - 1) ) .toArray(last); } else { // Both 2.x and 3.x return a cursor in the absence of a callback, // despite documentation implying you must explicitly ask // for a cursor return super.aggregate(...Array.prototype.slice.call(arguments)); } } }; // initializeUnorderedBulkOp(options) { // return super.initializeUnorderedBulkOp(options)[toTinsel](); // } // // initializeOrderedBulkOp(options) { // return super.initializeOrderedBulkOp(options)[toTinsel](); // } find(filter, projection) { const cursor = super.find(filter)[toTinsel](); if (projection) { return cursor.project(projection); } return cursor; } // listIndexes(options) { // return super.listIndexes(options)[toTinsel](); // } // // watch(pipeline, options) { // return super.watch(pipeline, options)[toTinsel](); // } // Before this module existed, Apostrophe patched this into // the mongodb collection prototype findWithProjection(filter, projection) { return this.find(filter, projection); } // Reimplement findOne in terms of find(), because find() works. findOne broke // here most likely due to a recent update of the mongodb driver, even though it // works fine in meulate-mongo-3-driver alone. A big hint is that // emulate-mongo-3-driver never touches findOne findOne(criteria, projection, callback) { if ((arguments.length === 1) && ((typeof criteria) === 'function')) { callback = arguments[0]; projection = null; criteria = {}; } else if (arguments.length === 0) { criteria = {}; projection = null; callback = null; } if (projection && ((typeof projection) === 'object')) { if (callback) { // Mustn't return the promise here, mocha gets mad this.find(criteria, projection).limit(1).toArray().then(docs => { return callback(null, docs[0]); }).catch(e => { return callback(e); }); } else { return this.find(criteria, projection).limit(1).toArray().then(docs => { return docs[0]; }); } } else { callback = projection; projection = null; if (callback) { // Mustn't return the promise here, mocha gets mad this.find(criteria).limit(1).toArray().then(docs => { return callback(null, docs[0]); }).catch(e => { return callback(e); }); } else { return this.find(criteria).limit(1).toArray().then(docs => { return docs[0]; }); } } }; // ensureIndex is deprecated but createIndex has exactly the // same behavior ensureIndex(fieldOrSpec, options, callback) { return this.createIndex(fieldOrSpec, options, callback); }; insert(docs, options, callback) { return super.insert(docs, options, callback); }; remove(selector, options, callback) { return super.remove(selector, options, callback); }; update(selector, doc, _options, callback) { const takesCallback = (typeof arguments[arguments.length - 1]) === 'function'; const options = _options && ((typeof _options) === 'object') ? _options : {}; let multi; let atomic; multi = options.multi; if (doc._id) { // Cannot match more than one, and would confuse our // don't-repeat-the-ids algorithm if we tried to use it multi = false; } let i; const keys = Object.keys(doc); let _ids; let nModified; for (i = 0; (i < keys.length); i++) { if (keys[i].substring(0, 1) === '$') { atomic = true; break; } } if (atomic) { // Undeprecated equivalents if (multi) { arguments[2] = omit(arguments[2], [ 'multi' ]); return this.updateMany(...Array.prototype.slice.call(arguments)); } else { return this.updateOne(...Array.prototype.slice.call(arguments)); } } else { if (multi) { arguments[2] = omit(arguments[2], [ 'multi' ]); // There is no replaceMany, so we have to do this repeatedly until // we run out of matching documents. We also have to get all of the // relevant _ids up front so we don't repeat them. It is a royal // pain in the tuckus. // // Fortunately it is rarely used. const collection = this; const promise = getIds(collection).then(function(docs) { _ids = docs.map(function(doc) { return doc._id; }); nModified = 0; return attemptMulti(collection); }).then(function() { return completeMulti(null, { result: { nModified, ok: 1 } }); }).catch(completeMulti); if (takesCallback) { return null; } else { return promise; } } else { return this.replaceOne(...Array.prototype.slice.call(arguments)); } } function getIds(collection) { return collection.find(selector).project({ _id: 1 }).toArray(); } function attemptMulti(collection) { if (!_ids.length) { return null; } const _selector = Object.assign({}, selector, { _id: _ids.shift() }); return collection.replaceOne(_selector, doc, options).then(function(status) { nModified += status.result.nModified; return attemptMulti(collection); }).catch(function(err) { return completeMulti(err); }); } function completeMulti(err, response) { if (takesCallback) { return callback(err, response); } else { if (err) { throw err; } else { return response; } } } }; count(query, options, callback) { if (arguments.length === 2) { if ((typeof options) === 'function') { callback = options; options = {}; } } else if (arguments.length === 1) { if ((typeof query) === 'function') { callback = query; options = {}; query = {}; } else { options = {}; } } else if (!arguments.length) { options = {}; query = {}; } if (hasNestedProperties(query, [ '$where', '$near', '$nearSphere' ])) { // Queries not supported by countDocuments must be turned into a // find() that actually fetches the ids (minimum projection) // and returns the number of documents const cursor = this.find(query); if (options.limit !== undefined) { cursor.limit(options.limit); } if (options.skip !== undefined) { cursor.skip(options.skip); } if (options.hint !== undefined) { cursor.hint(options.hint); } const p = cursor.project({ _id: 1 }).toArray().then(function(objects) { if (callback) { callback(null, objects.length); return null; } else { return objects.length; } }).catch(function(e) { if (callback) { callback(e); return null; } else { throw e; } }); if (!callback) { return p; } } else { const p = this.countDocuments(query, options).then(function(count) { if (callback) { callback(null, count); return null; } else { return count; } }).catch(function(e) { if (callback) { callback(e); return null; } else { throw e; } }); if (!callback) { return p; } } }; } Object.defineProperty( baseClass.prototype, toTinsel, { enumerable: false, value: function () { return Object.setPrototypeOf(this, TinselCollection.prototype); } } ); return TinselCollection; };