Skip to content

Commit

Permalink
feat(deprecation): create deprecation function
Browse files Browse the repository at this point in the history
Create deprecateOptions wrapper to warn on using 
deprecated options of library methods.

Fixes NODE-1430
  • Loading branch information
rweinberger authored Jul 31, 2018
1 parent 666b8fa commit 4f907a0
Show file tree
Hide file tree
Showing 13 changed files with 627 additions and 1 deletion.
10 changes: 10 additions & 0 deletions lib/aggregation_cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,16 @@ AggregationCursor.prototype.unwind = function(field) {
return this;
};

/**
* Return the cursor logger
* @method
* @return {Logger} return the cursor logger
* @ignore
*/
AggregationCursor.prototype.getLogger = function() {
return this.logger;
};

AggregationCursor.prototype.get = AggregationCursor.prototype.toArray;

/**
Expand Down
10 changes: 10 additions & 0 deletions lib/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -2073,4 +2073,14 @@ Collection.prototype.initializeOrderedBulkOp = function(options) {
return ordered(this.s.topology, this, options);
};

/**
* Return the db logger
* @method
* @return {Logger} return the db logger
* @ignore
*/
Collection.prototype.getLogger = function() {
return this.s.db.s.logger;
};

module.exports = Collection;
10 changes: 10 additions & 0 deletions lib/command_cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,16 @@ CommandCursor.prototype.maxTimeMS = function(value) {
return this;
};

/**
* Return the cursor logger
* @method
* @return {Logger} return the cursor logger
* @ignore
*/
CommandCursor.prototype.getLogger = function() {
return this.logger;
};

CommandCursor.prototype.get = CommandCursor.prototype.toArray;

/**
Expand Down
10 changes: 10 additions & 0 deletions lib/cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,16 @@ Cursor.prototype._read = function() {
});
};

/**
* Return the cursor logger
* @method
* @return {Logger} return the cursor logger
* @ignore
*/
Cursor.prototype.getLogger = function() {
return this.logger;
};

Object.defineProperty(Cursor.prototype, 'readPreference', {
enumerable: true,
get: function() {
Expand Down
10 changes: 10 additions & 0 deletions lib/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,16 @@ Db.prototype.watch = function(pipeline, options) {
return new ChangeStream(this, pipeline, options);
};

/**
* Return the db logger
* @method
* @return {Logger} return the db logger
* @ignore
*/
Db.prototype.getLogger = function() {
return this.s.logger;
};

/**
* Db close event
*
Expand Down
10 changes: 10 additions & 0 deletions lib/gridfs-stream/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,16 @@ GridFSBucket.prototype.drop = function(callback) {
});
};

/**
* Return the db logger
* @method
* @return {Logger} return the db logger
* @ignore
*/
GridFSBucket.prototype.getLogger = function() {
return this.s.db.s.logger;
};

/**
* @ignore
*/
Expand Down
10 changes: 10 additions & 0 deletions lib/mongo_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -458,4 +458,14 @@ MongoClient.prototype.watch = function(pipeline, options) {
return new ChangeStream(this, pipeline, options);
};

/**
* Return the mongo client logger
* @method
* @return {Logger} return the mongo client logger
* @ignore
*/
MongoClient.prototype.getLogger = function() {
return this.s.options.logger;
};

module.exports = MongoClient;
79 changes: 78 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,82 @@ function decorateWithReadConcern(command, coll) {
}
}

const emitProcessWarning = msg => process.emitWarning(msg, 'DeprecationWarning');
const emitConsoleWarning = msg => console.error(msg);
const emitDeprecationWarning = process.emitWarning ? emitProcessWarning : emitConsoleWarning;

/**
* Default message handler for generating deprecation warnings.
*
* @param {string} name function name
* @param {string} option option name
* @return {string} warning message
* @ignore
* @api private
*/
function defaultMsgHandler(name, option) {
return `${name} option [${option}] is deprecated and will be removed in a later version.`;
}

/**
* Deprecates a given function's options.
*
* @param {object} config configuration for deprecation
* @param {string} config.name function name
* @param {Array} config.deprecatedOptions options to deprecate
* @param {number} config.optionsIndex index of options object in function arguments array
* @param {function} [config.msgHandler] optional custom message handler to generate warnings
* @param {function} fn the target function of deprecation
* @return {function} modified function that warns once per deprecated option, and executes original function
* @ignore
* @api private
*/
function deprecateOptions(config, fn) {
if (process.noDeprecation === true) {
return fn;
}

const msgHandler = config.msgHandler ? config.msgHandler : defaultMsgHandler;

const optionsWarned = new Set();
function deprecated() {
const options = arguments[config.optionsIndex];

// ensure options is a valid, non-empty object, otherwise short-circuit
if (!isObject(options) || Object.keys(options).length === 0) {
return fn.apply(this, arguments);
}

config.deprecatedOptions.forEach(deprecatedOption => {
if (options.hasOwnProperty(deprecatedOption) && !optionsWarned.has(deprecatedOption)) {
optionsWarned.add(deprecatedOption);
const msg = msgHandler(config.name, deprecatedOption);
emitDeprecationWarning(msg);
if (this && this.getLogger) {
const logger = this.getLogger();
if (logger) {
logger.warn(msg);
}
}
}
});

return fn.apply(this, arguments);
}

// These lines copied from https://github.com/nodejs/node/blob/25e5ae41688676a5fd29b2e2e7602168eee4ceb5/lib/internal/util.js#L73-L80
// The wrapper will keep the same prototype as fn to maintain prototype chain
Object.setPrototypeOf(deprecated, fn);
if (fn.prototype) {
// Setting this (rather than using Object.setPrototype, as above) ensures
// that calling the unwrapped constructor gives an instanceof the wrapped
// constructor.
deprecated.prototype = fn.prototype;
}

return deprecated;
}

module.exports = {
filterOptions,
mergeOptions,
Expand All @@ -629,5 +705,6 @@ module.exports = {
resolveReadPreference,
isPromiseLike,
decorateWithCollation,
decorateWithReadConcern
decorateWithReadConcern,
deprecateOptions
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"istanbul": "^0.4.5",
"jsdoc": "3.5.5",
"lodash.camelcase": "^4.3.0",
"mocha-sinon": "^2.1.0",
"mongodb-extjson": "^2.1.1",
"mongodb-mock-server": "^1.0.0",
"mongodb-test-runner": "^1.1.18",
Expand Down
125 changes: 125 additions & 0 deletions test/functional/deprecate_warning_tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
'use strict';
const exec = require('child_process').exec;
const chai = require('chai');
const expect = chai.expect;
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
require('mocha-sinon');
chai.use(sinonChai);

const utils = require('../tools/utils');
const ClassWithLogger = utils.ClassWithLogger;
const ClassWithoutLogger = utils.ClassWithoutLogger;
const ClassWithUndefinedLogger = utils.ClassWithUndefinedLogger;
const ensureCalledWith = utils.ensureCalledWith;

describe('Deprecation Warnings', function() {
beforeEach(function() {
this.sinon.stub(console, 'error');
});

const defaultMessage = ' is deprecated and will be removed in a later version.';

it('node --no-deprecation flag should suppress all deprecation warnings', {
metadata: { requires: { node: '>=6.0.0' } },
test: function(done) {
exec(
'node --no-deprecation ./test/tools/deprecate_warning_test_program.js',
(err, stdout, stderr) => {
expect(err).to.be.null;
expect(stdout).to.be.empty;
expect(stderr).to.be.empty;
done();
}
);
}
});

it('node --trace-deprecation flag should print stack trace to stderr', {
metadata: { requires: { node: '>=6.0.0' } },
test: function(done) {
exec(
'node --trace-deprecation ./test/tools/deprecate_warning_test_program.js',
(err, stdout, stderr) => {
expect(err).to.be.null;
expect(stdout).to.be.empty;
expect(stderr).to.not.be.empty;

// split stderr into separate lines, trimming the first line to just the warning message
const split = stderr.split('\n');
const warning = split
.shift()
.split(')')[1]
.trim();

// ensure warning message matches expected
expect(warning).to.equal(
'DeprecationWarning: testDeprecationFlags option [maxScan]' + defaultMessage
);

// ensure each following line is from the stack trace, i.e. 'at config.deprecatedOptions.forEach.deprecatedOption'
split.pop();
split.forEach(s => {
expect(s.trim()).to.match(/^at/);
});

done();
}
);
}
});

it('node --throw-deprecation flag should throw error when deprecated function is called', {
metadata: { requires: { node: '>=6.0.0' } },
test: function(done) {
exec(
'node --throw-deprecation ./test/tools/deprecate_warning_test_program.js this_arg_should_never_print',
(err, stdout, stderr) => {
expect(stderr).to.not.be.empty;
expect(err).to.not.be.null;
expect(err)
.to.have.own.property('code')
.that.equals(1);

// ensure stdout is empty, i.e. that the program threw an error before reaching the console.log statement
expect(stdout).to.be.empty;
done();
}
);
}
});

it('test behavior for classes with an associated logger', function() {
const fakeClass = new ClassWithLogger();
const logger = fakeClass.getLogger();
const stub = sinon.stub(logger, 'warn');

fakeClass.f({ maxScan: 5, snapshot: true });
fakeClass.f({ maxScan: 5, snapshot: true });
expect(stub).to.have.been.calledTwice;
ensureCalledWith(stub, [
'f option [maxScan] is deprecated and will be removed in a later version.',
'f option [snapshot] is deprecated and will be removed in a later version.'
]);
});

it('test behavior for classes without an associated logger', function() {
const fakeClass = new ClassWithoutLogger();

function func() {
fakeClass.f({ maxScan: 5, snapshot: true });
}

expect(func).to.not.throw();
});

it('test behavior for classes with an undefined logger', function() {
const fakeClass = new ClassWithUndefinedLogger();

function func() {
fakeClass.f({ maxScan: 5, snapshot: true });
}

expect(func).to.not.throw();
});
});
28 changes: 28 additions & 0 deletions test/tools/deprecate_warning_test_program.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

// prevent this file from being imported; it is only for use in functional/deprecate_warning_tests.js
if (require.main !== module) {
return;
}

const deprecateOptions = require('../../lib/utils.js').deprecateOptions;

const testDeprecationFlags = deprecateOptions(
{
name: 'testDeprecationFlags',
deprecatedOptions: ['maxScan', 'snapshot', 'fields'],
optionsIndex: 0
},
options => {
if (options) options = null;
}
);

testDeprecationFlags({ maxScan: 0 });

// for tests that throw error on calling deprecated fn - this should never happen; stdout should be empty
if (process.argv[2]) {
console.log(process.argv[2]);
}

process.nextTick(() => process.exit());
Loading

0 comments on commit 4f907a0

Please sign in to comment.