diff --git a/lib/document.js b/lib/document.js index 3e0e42cc1ef..a8614bbb749 100644 --- a/lib/document.js +++ b/lib/document.js @@ -31,6 +31,7 @@ const inspect = require('util').inspect; const internalToObjectOptions = require('./options').internalToObjectOptions; const mpath = require('mpath'); const utils = require('./utils'); +const isPromise = require('./helpers/isPromise'); const clone = utils.clone; const deepEqual = utils.deepEqual; @@ -3075,7 +3076,7 @@ Document.prototype.$toObject = function(options, json) { // we need to adjust options.transform to be the child schema's transform and // not the parent schema's if (transform) { - applySchemaTypeTransforms(this, ret, gettersOptions, options); + applySchemaTypeTransforms(this, ret); } if (transform === true || (schemaOptions.toObject && transform)) { @@ -3414,13 +3415,17 @@ function applySchemaTypeTransforms(self, json) { const schematype = schema.paths[path]; if (typeof schematype.options.transform === 'function') { const val = self.get(path); - json[path] = schematype.options.transform.call(self, val); + const transformedValue = schematype.options.transform.call(self, val); + throwErrorIfPromise(path, transformedValue); + json[path] = transformedValue; } else if (schematype.$embeddedSchemaType != null && typeof schematype.$embeddedSchemaType.options.transform === 'function') { const vals = [].concat(self.get(path)); const transform = schematype.$embeddedSchemaType.options.transform; for (let i = 0; i < vals.length; ++i) { - vals[i] = transform.call(self, vals[i]); + const transformedValue = transform.call(self, vals[i]); + vals[i] = transformedValue; + throwErrorIfPromise(path, transformedValue); } json[path] = vals; @@ -3430,6 +3435,12 @@ function applySchemaTypeTransforms(self, json) { return json; } +function throwErrorIfPromise(path, transformedValue) { + if (isPromise(transformedValue)) { + throw new Error('`transform` function must be synchronous, but the transform on path `' + path + '` returned a promise.'); + } +} + /** * The return value of this method is used in calls to JSON.stringify(doc). * diff --git a/lib/helpers/isPromise.js b/lib/helpers/isPromise.js new file mode 100644 index 00000000000..d6db2608cc9 --- /dev/null +++ b/lib/helpers/isPromise.js @@ -0,0 +1,6 @@ +'use strict'; +function isPromise(val) { + return !!val && (typeof val === 'object' || typeof val === 'function') && typeof val.then === 'function'; +} + +module.exports = isPromise; \ No newline at end of file diff --git a/test/document.test.js b/test/document.test.js index 8020a362b36..9ef1d2d3e00 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -8976,6 +8976,28 @@ describe('document', function() { assert.equal(axl.fullName, 'Axl Rose'); }); + it('throws an error when `transform` returns a promise (gh-9163)', function() { + const userSchema = new Schema({ + name: { + type: String, + transform: function() { + return new Promise(() => {}); + } + } + }); + + const User = db.model('User', userSchema); + + const user = new User({ name: 'Hafez' }); + assert.throws(function() { + user.toJSON(); + }, /`transform` option has to be synchronous, but is returning a promise on path `name`./); + + assert.throws(function() { + user.toObject(); + }, /`transform` option has to be synchronous, but is returning a promise on path `name`./); + }); + it('uses strict equality when checking mixed paths for modifications (gh-9165)', function() { const schema = Schema({ obj: {} }); const Model = db.model('gh9165', schema);