diff --git a/lib/collection.js b/lib/collection.js index 9d129f3753b..29f7c87450b 100644 --- a/lib/collection.js +++ b/lib/collection.js @@ -378,14 +378,14 @@ Collection.prototype.find = function(query, options, callback) { // Decorate find command with collation options decorateWithCollation(findCommand, this, options); - // Create the cursor - if (typeof callback === 'function') - return handleCallback( - callback, - null, - this.s.topology.cursor(this.s.namespace, findCommand, newOptions) - ); - return this.s.topology.cursor(this.s.namespace, findCommand, newOptions); + const cursor = this.s.topology.cursor(this.s.namespace, findCommand, newOptions); + + // automatically call map on the cursor if the map option is set + if (typeof this.s.options.map === 'function') { + cursor.map(this.s.options.map); + } + + return typeof callback === 'function' ? handleCallback(callback, null, cursor) : cursor; }; /** @@ -491,19 +491,7 @@ Collection.prototype.insertMany = function(docs, options, callback) { // If keep going set unordered options['serializeFunctions'] = options['serializeFunctions'] || self.s.serializeFunctions; - // Set up the force server object id - var forceServerObjectId = - typeof options.forceServerObjectId === 'boolean' - ? options.forceServerObjectId - : self.s.db.options.forceServerObjectId; - - // Do we want to force the server to assign the _id key - if (forceServerObjectId !== true) { - // Add _id if not specified - for (var i = 0; i < docs.length; i++) { - if (docs[i]._id == null) docs[i]._id = self.s.pkFactory.createPk(); - } - } + docs = prepareDocs(this, docs, options); // Generate the bulk write operations var operations = [ @@ -683,18 +671,7 @@ var insertDocuments = function(self, docs, options, callback) { if (finalOptions.keepGoing === true) finalOptions.ordered = false; finalOptions['serializeFunctions'] = options['serializeFunctions'] || self.s.serializeFunctions; - // Set up the force server object id - var forceServerObjectId = - typeof options.forceServerObjectId === 'boolean' - ? options.forceServerObjectId - : self.s.db.options.forceServerObjectId; - - // Add _id if not specified - if (forceServerObjectId !== true) { - for (var i = 0; i < docs.length; i++) { - if (docs[i]._id === void 0) docs[i]._id = self.s.pkFactory.createPk(); - } - } + docs = prepareDocs(self, docs, options); // File inserts self.s.topology.insert(self.s.namespace, docs, finalOptions, function(err, result) { @@ -909,6 +886,10 @@ Collection.prototype.replaceOne = function(filter, doc, options, callback) { options.ignoreUndefined = this.s.options.ignoreUndefined; } + if (typeof this.s.options.unmap === 'function') { + doc = this.s.options.unmap(doc); + } + return executeOperation(this.s.topology, replaceOne, [this, filter, doc, options, callback]); }; @@ -2253,6 +2234,11 @@ var findAndModify = function(self, query, sort, doc, options, callback) { // Execute the command self.s.db.command(queryObject, finalOptions, function(err, result) { if (err) return handleCallback(callback, err, null); + + if (result && result.value && typeof self.s.options.map === 'function') { + result.value = self.s.options.map(result.value); + } + return handleCallback(callback, null, result); }); }; @@ -3028,4 +3014,28 @@ var getReadPreference = function(self, options, db) { return options; }; +// modifies documents before being inserted or updated +const prepareDocs = function(self, docs, options) { + const forceServerObjectId = + typeof options.forceServerObjectId === 'boolean' + ? options.forceServerObjectId + : self.s.db.options.forceServerObjectId; + + const unmap = typeof self.s.options.unmap === 'function' ? self.s.options.unmap : false; + + // no need to modify the docs if server sets the ObjectId + // and unmap collection option is unset + if (forceServerObjectId === true && !unmap) { + return docs; + } + + return docs.map(function(doc) { + if (forceServerObjectId !== true && doc._id == null) { + doc._id = self.s.pkFactory.createPk(); + } + + return unmap ? unmap(doc) : doc; + }); +}; + module.exports = Collection; diff --git a/lib/db.js b/lib/db.js index 937efcd6352..38ad88305b6 100644 --- a/lib/db.js +++ b/lib/db.js @@ -400,6 +400,8 @@ var collectionKeys = [ * @param {(ReadPreference|string)} [options.readPreference=null] The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST). * @param {boolean} [options.serializeFunctions=false] Serialize functions on any object. * @param {boolean} [options.strict=false] Returns an error if the collection does not exist + * @param {function} [options.map] Function to map documents returned in find, findOne, and findAndModify commands. + * @param {function} [options.unmap] Function to unmap documents passed to insertOne, insertMany, and replaceOne commands. * @param {object} [options.readConcern=null] Specify a read concern for the collection. (only MongoDB 3.2 or higher supported) * @param {object} [options.readConcern.level='local'] Specify a read concern level for the collection operations, one of [local|majority]. (only MongoDB 3.2 or higher supported) * @param {Db~collectionResultCallback} [callback] The collection result callback diff --git a/test/functional/collection_mapping_tests.js b/test/functional/collection_mapping_tests.js new file mode 100644 index 00000000000..6ee957f8622 --- /dev/null +++ b/test/functional/collection_mapping_tests.js @@ -0,0 +1,365 @@ +'use strict'; +const ObjectID = require('bson').ObjectID; +const expect = require('chai').expect; +const setupDatabase = require('./shared').setupDatabase; + +class User { + constructor(doc) { + doc = doc || {}; + + this._id = doc._id || new ObjectID(); + this.firstName = doc.firstName; + this.lastName = doc.lastName; + } + + getFullName() { + return `${this.firstName} ${this.lastName}`; + } + + static map(doc) { + return new User(doc); + } + + static unmap(user) { + return { + _id: user._id, + firstName: user.firstName, + lastName: user.lastName, + internalField: true + }; + } +} + +describe('Collection Mapping', function() { + before(function() { + return setupDatabase(this.configuration); + }); + + it('should map find', { + metadata: { + requires: { topology: ['single'] } + }, + + // The actual test we wish to run + test: function(done) { + const configuration = this.configuration; + const ObjectID = configuration.require.ObjectID; + + const client = configuration.newClient(configuration.writeConcernMax(), { + poolSize: 1 + }); + + client.connect(function(err, client) { + expect(err).to.be.null; + const db = client.db(configuration.db); + + const collection = db.collection('collection_mapping_find', { + map: User.map + }); + + const docs = [ + { + _id: new ObjectID(), + firstName: 'John', + lastName: 'Doe' + }, + { + _id: new ObjectID(), + firstName: 'Mongo', + lastName: 'DB' + } + ]; + + collection.insertMany(docs, configuration.writeConcernMax(), function(err) { + expect(err).to.be.null; + + collection + .find({}) + .sort({ firstName: 1 }) + .toArray(function(err, users) { + expect(err).to.be.null; + expect(users[0]).to.be.an.instanceof(User); + expect(users[0].firstName).to.equal('John'); + expect(users[0].lastName).to.equal('Doe'); + expect(users[0].getFullName()).to.equal('John Doe'); + client.close(); + done(); + }); + }); + }); + } + }); + + it('should map findOne', { + metadata: { + requires: { topology: ['single'] } + }, + + // The actual test we wish to run + test: function(done) { + const configuration = this.configuration; + const ObjectID = configuration.require.ObjectID; + + const client = configuration.newClient(configuration.writeConcernMax(), { + poolSize: 1 + }); + + client.connect(function(err, client) { + const db = client.db(configuration.db); + expect(err).to.be.null; + + const collection = db.collection('collection_mapping_findOne', { + map: User.map + }); + + const doc = { + _id: new ObjectID(), + firstName: 'John', + lastName: 'Doe' + }; + + //insert new user + collection.insertOne(doc, configuration.writeConcernMax(), function(err) { + expect(err).to.be.null; + + collection.findOne({}, function(err, user) { + expect(err).to.be.null; + expect(user).to.be.an.instanceof(User); + expect(user.getFullName()).to.equal('John Doe'); + client.close(); + done(); + }); + }); + }); + } + }); + + it('should map findAndModify commands', { + metadata: { + requires: { topology: ['single'] } + }, + + // The actual test we wish to run + test: function(done) { + const configuration = this.configuration; + + const client = configuration.newClient(configuration.writeConcernMax(), { + poolSize: 1 + }); + + client.connect(function(err, client) { + const db = client.db(configuration.db); + expect(err).to.be.null; + + const collection = db.collection('collection_mapping_findAndModify', { + map: User.map + }); + + const doc = { firstName: 'John', lastName: 'Doe' }; + + collection.insertOne(doc, configuration.writeConcernMax(), function(err) { + expect(err).to.be.null; + + const opts = { upsert: true, returnOriginal: false }; + + collection.findOneAndUpdate({}, { $set: { firstName: 'Johnny' } }, opts, function( + err, + result + ) { + expect(err).to.be.null; + expect(result.value).to.be.an.instanceof(User); + expect(result.value.getFullName()).to.equal('Johnny Doe'); + + // Execute findOneAndReplace + collection.findOneAndReplace( + {}, + { firstName: 'Johnny Boy', lastName: 'Doey' }, + opts, + function(err, result) { + expect(err).to.be.null; + expect(result.value).to.be.an.instanceof(User); + expect(result.value.getFullName()).to.equal('Johnny Boy Doey'); + + // Execute findOneAndReplace + collection.findOneAndDelete({}, function(err, result) { + expect(err).to.be.null; + expect(result.value).to.be.an.instanceof(User); + expect(result.value.getFullName()).to.equal('Johnny Boy Doey'); + + client.close(); + done(); + }); + } + ); + }); + }); + }); + } + }); + + it('should unmap insertOne', { + metadata: { + requires: { topology: ['single'] } + }, + + // The actual test we wish to run + test: function(done) { + const configuration = this.configuration; + + const client = configuration.newClient(configuration.writeConcernMax(), { + poolSize: 1 + }); + + client.connect(function(err, client) { + const db = client.db(configuration.db); + expect(err).to.be.null; + + const collection = db.collection('collection_mapping_insertOne', { + unmap: User.unmap + }); + + const user = new User(); + user.firstName = 'John'; + user.lastName = 'Doe'; + + collection.insertOne(user, function(err) { + expect(err).to.be.null; + + collection.findOne({}, function(err, doc) { + expect(err).to.be.null; + + expect(doc).to.deep.equal({ + _id: user._id, + firstName: 'John', + lastName: 'Doe', + internalField: true + }); + + client.close(); + done(); + }); + }); + }); + } + }); + + it('should unmap insertMany', { + metadata: { + requires: { topology: ['single'] } + }, + + // The actual test we wish to run + test: function(done) { + const configuration = this.configuration; + + const client = configuration.newClient(configuration.writeConcernMax(), { + poolSize: 1 + }); + + client.connect(function(err, client) { + const db = client.db(configuration.db); + expect(err).to.be.null; + + const collection = db.collection('collection_mapping_insertMany', { + unmap: User.unmap + }); + + const daenerys = new User(); + daenerys.firstName = 'Daenerys'; + daenerys.lastName = 'Targaryen'; + + const jon = new User(); + jon.firstName = 'Jon'; + jon.lastName = 'Snow'; + + collection.insertMany([daenerys, jon], function(err) { + expect(err).to.be.null; + + collection + .find({}) + .sort({ firstName: 1 }) + .toArray(function(err, docs) { + expect(err).to.be.null; + + expect(docs).to.deep.equal([ + { + _id: daenerys._id, + firstName: 'Daenerys', + lastName: 'Targaryen', + internalField: true + }, + { + _id: jon._id, + firstName: 'Jon', + lastName: 'Snow', + internalField: true + } + ]); + + client.close(); + + done(); + }); + }); + }); + } + }); + + it('should unmap replaceOne', { + metadata: { + requires: { topology: ['single'] } + }, + + // The actual test we wish to run + test: function(done) { + const configuration = this.configuration; + + const client = configuration.newClient(configuration.writeConcernMax(), { + poolSize: 1 + }); + + client.connect(function(err, client) { + const db = client.db(configuration.db); + expect(err).to.be.null; + + const collection = db.collection('collection_mapping_replaceOne', { + unmap: User.unmap + }); + + const unmappedCollection = db.collection('collection_mapping_replaceOne'); + + const doc = { + _id: new ObjectID(), + firstName: 'John', + lastName: 'Doe' + }; + + unmappedCollection.insertOne(doc, function(err) { + expect(err).to.be.null; + + const user = new User(doc); + user.firstName = 'Johnny'; + user.lastName = 'Doey'; + + collection.replaceOne({}, user, function(err) { + expect(err).to.be.null; + + collection.findOne({}, function(err, doc) { + expect(err).to.be.null; + + expect(doc).to.deep.equal({ + _id: user._id, + firstName: 'Johnny', + lastName: 'Doey', + internalField: true + }); + + client.close(); + done(); + }); + }); + }); + }); + } + }); +});