Skip to content

Commit 4f907a0

Browse files
authored
feat(deprecation): create deprecation function
Create deprecateOptions wrapper to warn on using deprecated options of library methods. Fixes NODE-1430
1 parent 666b8fa commit 4f907a0

13 files changed

+627
-1
lines changed

lib/aggregation_cursor.js

+10
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,16 @@ AggregationCursor.prototype.unwind = function(field) {
281281
return this;
282282
};
283283

284+
/**
285+
* Return the cursor logger
286+
* @method
287+
* @return {Logger} return the cursor logger
288+
* @ignore
289+
*/
290+
AggregationCursor.prototype.getLogger = function() {
291+
return this.logger;
292+
};
293+
284294
AggregationCursor.prototype.get = AggregationCursor.prototype.toArray;
285295

286296
/**

lib/collection.js

+10
Original file line numberDiff line numberDiff line change
@@ -2073,4 +2073,14 @@ Collection.prototype.initializeOrderedBulkOp = function(options) {
20732073
return ordered(this.s.topology, this, options);
20742074
};
20752075

2076+
/**
2077+
* Return the db logger
2078+
* @method
2079+
* @return {Logger} return the db logger
2080+
* @ignore
2081+
*/
2082+
Collection.prototype.getLogger = function() {
2083+
return this.s.db.s.logger;
2084+
};
2085+
20762086
module.exports = Collection;

lib/command_cursor.js

+10
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,16 @@ CommandCursor.prototype.maxTimeMS = function(value) {
216216
return this;
217217
};
218218

219+
/**
220+
* Return the cursor logger
221+
* @method
222+
* @return {Logger} return the cursor logger
223+
* @ignore
224+
*/
225+
CommandCursor.prototype.getLogger = function() {
226+
return this.logger;
227+
};
228+
219229
CommandCursor.prototype.get = CommandCursor.prototype.toArray;
220230

221231
/**

lib/cursor.js

+10
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,16 @@ Cursor.prototype._read = function() {
10391039
});
10401040
};
10411041

1042+
/**
1043+
* Return the cursor logger
1044+
* @method
1045+
* @return {Logger} return the cursor logger
1046+
* @ignore
1047+
*/
1048+
Cursor.prototype.getLogger = function() {
1049+
return this.logger;
1050+
};
1051+
10421052
Object.defineProperty(Cursor.prototype, 'readPreference', {
10431053
enumerable: true,
10441054
get: function() {

lib/db.js

+10
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,16 @@ Db.prototype.watch = function(pipeline, options) {
900900
return new ChangeStream(this, pipeline, options);
901901
};
902902

903+
/**
904+
* Return the db logger
905+
* @method
906+
* @return {Logger} return the db logger
907+
* @ignore
908+
*/
909+
Db.prototype.getLogger = function() {
910+
return this.s.logger;
911+
};
912+
903913
/**
904914
* Db close event
905915
*

lib/gridfs-stream/index.js

+10
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,16 @@ GridFSBucket.prototype.drop = function(callback) {
322322
});
323323
};
324324

325+
/**
326+
* Return the db logger
327+
* @method
328+
* @return {Logger} return the db logger
329+
* @ignore
330+
*/
331+
GridFSBucket.prototype.getLogger = function() {
332+
return this.s.db.s.logger;
333+
};
334+
325335
/**
326336
* @ignore
327337
*/

lib/mongo_client.js

+10
Original file line numberDiff line numberDiff line change
@@ -458,4 +458,14 @@ MongoClient.prototype.watch = function(pipeline, options) {
458458
return new ChangeStream(this, pipeline, options);
459459
};
460460

461+
/**
462+
* Return the mongo client logger
463+
* @method
464+
* @return {Logger} return the mongo client logger
465+
* @ignore
466+
*/
467+
MongoClient.prototype.getLogger = function() {
468+
return this.s.options.logger;
469+
};
470+
461471
module.exports = MongoClient;

lib/utils.js

+78-1
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,82 @@ function decorateWithReadConcern(command, coll) {
606606
}
607607
}
608608

609+
const emitProcessWarning = msg => process.emitWarning(msg, 'DeprecationWarning');
610+
const emitConsoleWarning = msg => console.error(msg);
611+
const emitDeprecationWarning = process.emitWarning ? emitProcessWarning : emitConsoleWarning;
612+
613+
/**
614+
* Default message handler for generating deprecation warnings.
615+
*
616+
* @param {string} name function name
617+
* @param {string} option option name
618+
* @return {string} warning message
619+
* @ignore
620+
* @api private
621+
*/
622+
function defaultMsgHandler(name, option) {
623+
return `${name} option [${option}] is deprecated and will be removed in a later version.`;
624+
}
625+
626+
/**
627+
* Deprecates a given function's options.
628+
*
629+
* @param {object} config configuration for deprecation
630+
* @param {string} config.name function name
631+
* @param {Array} config.deprecatedOptions options to deprecate
632+
* @param {number} config.optionsIndex index of options object in function arguments array
633+
* @param {function} [config.msgHandler] optional custom message handler to generate warnings
634+
* @param {function} fn the target function of deprecation
635+
* @return {function} modified function that warns once per deprecated option, and executes original function
636+
* @ignore
637+
* @api private
638+
*/
639+
function deprecateOptions(config, fn) {
640+
if (process.noDeprecation === true) {
641+
return fn;
642+
}
643+
644+
const msgHandler = config.msgHandler ? config.msgHandler : defaultMsgHandler;
645+
646+
const optionsWarned = new Set();
647+
function deprecated() {
648+
const options = arguments[config.optionsIndex];
649+
650+
// ensure options is a valid, non-empty object, otherwise short-circuit
651+
if (!isObject(options) || Object.keys(options).length === 0) {
652+
return fn.apply(this, arguments);
653+
}
654+
655+
config.deprecatedOptions.forEach(deprecatedOption => {
656+
if (options.hasOwnProperty(deprecatedOption) && !optionsWarned.has(deprecatedOption)) {
657+
optionsWarned.add(deprecatedOption);
658+
const msg = msgHandler(config.name, deprecatedOption);
659+
emitDeprecationWarning(msg);
660+
if (this && this.getLogger) {
661+
const logger = this.getLogger();
662+
if (logger) {
663+
logger.warn(msg);
664+
}
665+
}
666+
}
667+
});
668+
669+
return fn.apply(this, arguments);
670+
}
671+
672+
// These lines copied from https://github.com/nodejs/node/blob/25e5ae41688676a5fd29b2e2e7602168eee4ceb5/lib/internal/util.js#L73-L80
673+
// The wrapper will keep the same prototype as fn to maintain prototype chain
674+
Object.setPrototypeOf(deprecated, fn);
675+
if (fn.prototype) {
676+
// Setting this (rather than using Object.setPrototype, as above) ensures
677+
// that calling the unwrapped constructor gives an instanceof the wrapped
678+
// constructor.
679+
deprecated.prototype = fn.prototype;
680+
}
681+
682+
return deprecated;
683+
}
684+
609685
module.exports = {
610686
filterOptions,
611687
mergeOptions,
@@ -629,5 +705,6 @@ module.exports = {
629705
resolveReadPreference,
630706
isPromiseLike,
631707
decorateWithCollation,
632-
decorateWithReadConcern
708+
decorateWithReadConcern,
709+
deprecateOptions
633710
};

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"istanbul": "^0.4.5",
2929
"jsdoc": "3.5.5",
3030
"lodash.camelcase": "^4.3.0",
31+
"mocha-sinon": "^2.1.0",
3132
"mongodb-extjson": "^2.1.1",
3233
"mongodb-mock-server": "^1.0.0",
3334
"mongodb-test-runner": "^1.1.18",
+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
'use strict';
2+
const exec = require('child_process').exec;
3+
const chai = require('chai');
4+
const expect = chai.expect;
5+
const sinon = require('sinon');
6+
const sinonChai = require('sinon-chai');
7+
require('mocha-sinon');
8+
chai.use(sinonChai);
9+
10+
const utils = require('../tools/utils');
11+
const ClassWithLogger = utils.ClassWithLogger;
12+
const ClassWithoutLogger = utils.ClassWithoutLogger;
13+
const ClassWithUndefinedLogger = utils.ClassWithUndefinedLogger;
14+
const ensureCalledWith = utils.ensureCalledWith;
15+
16+
describe('Deprecation Warnings', function() {
17+
beforeEach(function() {
18+
this.sinon.stub(console, 'error');
19+
});
20+
21+
const defaultMessage = ' is deprecated and will be removed in a later version.';
22+
23+
it('node --no-deprecation flag should suppress all deprecation warnings', {
24+
metadata: { requires: { node: '>=6.0.0' } },
25+
test: function(done) {
26+
exec(
27+
'node --no-deprecation ./test/tools/deprecate_warning_test_program.js',
28+
(err, stdout, stderr) => {
29+
expect(err).to.be.null;
30+
expect(stdout).to.be.empty;
31+
expect(stderr).to.be.empty;
32+
done();
33+
}
34+
);
35+
}
36+
});
37+
38+
it('node --trace-deprecation flag should print stack trace to stderr', {
39+
metadata: { requires: { node: '>=6.0.0' } },
40+
test: function(done) {
41+
exec(
42+
'node --trace-deprecation ./test/tools/deprecate_warning_test_program.js',
43+
(err, stdout, stderr) => {
44+
expect(err).to.be.null;
45+
expect(stdout).to.be.empty;
46+
expect(stderr).to.not.be.empty;
47+
48+
// split stderr into separate lines, trimming the first line to just the warning message
49+
const split = stderr.split('\n');
50+
const warning = split
51+
.shift()
52+
.split(')')[1]
53+
.trim();
54+
55+
// ensure warning message matches expected
56+
expect(warning).to.equal(
57+
'DeprecationWarning: testDeprecationFlags option [maxScan]' + defaultMessage
58+
);
59+
60+
// ensure each following line is from the stack trace, i.e. 'at config.deprecatedOptions.forEach.deprecatedOption'
61+
split.pop();
62+
split.forEach(s => {
63+
expect(s.trim()).to.match(/^at/);
64+
});
65+
66+
done();
67+
}
68+
);
69+
}
70+
});
71+
72+
it('node --throw-deprecation flag should throw error when deprecated function is called', {
73+
metadata: { requires: { node: '>=6.0.0' } },
74+
test: function(done) {
75+
exec(
76+
'node --throw-deprecation ./test/tools/deprecate_warning_test_program.js this_arg_should_never_print',
77+
(err, stdout, stderr) => {
78+
expect(stderr).to.not.be.empty;
79+
expect(err).to.not.be.null;
80+
expect(err)
81+
.to.have.own.property('code')
82+
.that.equals(1);
83+
84+
// ensure stdout is empty, i.e. that the program threw an error before reaching the console.log statement
85+
expect(stdout).to.be.empty;
86+
done();
87+
}
88+
);
89+
}
90+
});
91+
92+
it('test behavior for classes with an associated logger', function() {
93+
const fakeClass = new ClassWithLogger();
94+
const logger = fakeClass.getLogger();
95+
const stub = sinon.stub(logger, 'warn');
96+
97+
fakeClass.f({ maxScan: 5, snapshot: true });
98+
fakeClass.f({ maxScan: 5, snapshot: true });
99+
expect(stub).to.have.been.calledTwice;
100+
ensureCalledWith(stub, [
101+
'f option [maxScan] is deprecated and will be removed in a later version.',
102+
'f option [snapshot] is deprecated and will be removed in a later version.'
103+
]);
104+
});
105+
106+
it('test behavior for classes without an associated logger', function() {
107+
const fakeClass = new ClassWithoutLogger();
108+
109+
function func() {
110+
fakeClass.f({ maxScan: 5, snapshot: true });
111+
}
112+
113+
expect(func).to.not.throw();
114+
});
115+
116+
it('test behavior for classes with an undefined logger', function() {
117+
const fakeClass = new ClassWithUndefinedLogger();
118+
119+
function func() {
120+
fakeClass.f({ maxScan: 5, snapshot: true });
121+
}
122+
123+
expect(func).to.not.throw();
124+
});
125+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
// prevent this file from being imported; it is only for use in functional/deprecate_warning_tests.js
4+
if (require.main !== module) {
5+
return;
6+
}
7+
8+
const deprecateOptions = require('../../lib/utils.js').deprecateOptions;
9+
10+
const testDeprecationFlags = deprecateOptions(
11+
{
12+
name: 'testDeprecationFlags',
13+
deprecatedOptions: ['maxScan', 'snapshot', 'fields'],
14+
optionsIndex: 0
15+
},
16+
options => {
17+
if (options) options = null;
18+
}
19+
);
20+
21+
testDeprecationFlags({ maxScan: 0 });
22+
23+
// for tests that throw error on calling deprecated fn - this should never happen; stdout should be empty
24+
if (process.argv[2]) {
25+
console.log(process.argv[2]);
26+
}
27+
28+
process.nextTick(() => process.exit());

0 commit comments

Comments
 (0)