diff --git a/CHANGELOG.md b/CHANGELOG.md index 79cd3add6d..a8b6accab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,6 +119,7 @@ ___ - LDAP: Properly unbind client on group search error (Diamond Lewis) [#7265](https://github.com/parse-community/parse-server/pull/7265) - Improve data consistency in Push and Job Status update (Diamond Lewis) [#7267](https://github.com/parse-community/parse-server/pull/7267) - Excluding keys that have trailing edges.node when performing GraphQL resolver (Chris Bland) [#7273](https://github.com/parse-community/parse-server/pull/7273) +- Added centralized feature deprecation with standardized warning logs (Manuel Trezza) [#7303](https://github.com/parse-community/parse-server/pull/7303) ___ ## 4.5.0 [Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0) diff --git a/spec/Deprecator.spec.js b/spec/Deprecator.spec.js new file mode 100644 index 0000000000..7e0e28df3d --- /dev/null +++ b/spec/Deprecator.spec.js @@ -0,0 +1,36 @@ +'use strict'; + +const Deprecator = require('../lib/Deprecator/Deprecator'); + +describe('Deprecator', () => { + let deprecations = []; + + beforeEach(async () => { + deprecations = [{ optionKey: 'exampleKey', changeNewDefault: 'exampleNewDefault' }]; + }); + + it('deprecations are an array', async () => { + expect(Deprecator._getDeprecations()).toBeInstanceOf(Array); + }); + + it('logs deprecation for new default', async () => { + deprecations = [{ optionKey: 'exampleKey', changeNewDefault: 'exampleNewDefault' }]; + + spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations); + const logger = require('../lib/logger').logger; + const logSpy = spyOn(logger, 'warn').and.callFake(() => {}); + + await reconfigureServer(); + expect(logSpy.calls.all()[0].args[0]).toContain(deprecations[0].optionKey); + expect(logSpy.calls.all()[0].args[0]).toContain(deprecations[0].changeNewDefault); + }); + + it('does not log deprecation for new default if option is set manually', async () => { + deprecations = [{ optionKey: 'exampleKey', changeNewDefault: 'exampleNewDefault' }]; + + spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations); + const logSpy = spyOn(Deprecator, '_log').and.callFake(() => {}); + await reconfigureServer({ [deprecations[0].optionKey]: 'manuallySet' }); + expect(logSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/Deprecator/Deprecations.js b/src/Deprecator/Deprecations.js new file mode 100644 index 0000000000..5c7cc3630f --- /dev/null +++ b/src/Deprecator/Deprecations.js @@ -0,0 +1,14 @@ +/** + * The deprecations. + * + * Add deprecations to the array using the following keys: + * - `optionKey`: The option key incl. its path, e.g. `security.enableCheck`. + * - `envKey`: The environment key, e.g. `PARSE_SERVER_SECURITY`. + * - `changeNewKey`: Set the new key name if the current key will be replaced, + * or set to an empty string if the current key will be removed without replacement. + * - `changeNewDefault`: Set the new default value if the key's default value + * will change in a future version. + * + * If there are no deprecations this must return an empty array anyway. + */ +module.exports = []; diff --git a/src/Deprecator/Deprecator.js b/src/Deprecator/Deprecator.js new file mode 100644 index 0000000000..53036fcb1f --- /dev/null +++ b/src/Deprecator/Deprecator.js @@ -0,0 +1,71 @@ +import logger from '../logger'; +import Deprecations from './Deprecations'; + +/** + * The deprecator class. + */ +class Deprecator { + /** + * Scans the Parse Server for deprecated options. + * This needs to be called before setting option defaults, otherwise it + * becomes indistinguishable whether an option has been set manually or + * by default. + * @param {any} options The Parse Server options. + */ + static scanParseServerOptions(options) { + // Scan for deprecations + for (const deprecation of Deprecator._getDeprecations()) { + // Get deprecation properties + const optionKey = deprecation.optionKey; + const changeNewDefault = deprecation.changeNewDefault; + + // If default will change, only throw a warning if option is not set + if (changeNewDefault != null && options[optionKey] == null) { + Deprecator._log({ optionKey, changeNewDefault }); + } + } + } + + /** + * Returns the deprecation definitions. + * @returns {Array} The deprecations. + */ + static _getDeprecations() { + return Deprecations; + } + + /** + * Logs a deprecation warning for a Parse Server option. + * @param {String} optionKey The option key incl. its path, e.g. `security.enableCheck`. + * @param {String} envKey The environment key, e.g. `PARSE_SERVER_SECURITY`. + * @param {String} changeNewKey Set the new key name if the current key will be replaced, + * or set to an empty string if the current key will be removed without replacement. + * @param {String} changeNewDefault Set the new default value if the key's default value + * will change in a future version. + * @param {String} [solution] The instruction to resolve this deprecation warning. This + * message must not include the warning that the parameter is deprecated, that is + * automatically added to the message. It should only contain the instruction on how + * to resolve this warning. + */ + static _log({ optionKey, envKey, changeNewKey, changeNewDefault, solution }) { + const type = optionKey ? 'option' : 'environment key'; + const key = optionKey ? optionKey : envKey; + const keyAction = + changeNewKey == null + ? undefined + : changeNewKey.length > 0 + ? `renamed to '${changeNewKey}'` + : `removed`; + + // Compose message + let output = `DeprecationWarning: The Parse Server ${type} '${key}' `; + output += changeNewKey ? `is deprecated and will be ${keyAction} in a future version.` : ''; + output += changeNewDefault + ? `default will change to '${changeNewDefault}' in a future version.` + : ''; + output += solution ? ` ${solution}` : ''; + logger.warn(output); + } +} + +module.exports = Deprecator; diff --git a/src/ParseServer.js b/src/ParseServer.js index 1ee653b62f..43996ac751 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -43,6 +43,7 @@ import * as controllers from './Controllers'; import { ParseGraphQLServer } from './GraphQL/ParseGraphQLServer'; import { SecurityRouter } from './Routers/SecurityRouter'; import CheckRunner from './Security/CheckRunner'; +import Deprecator from './Deprecator/Deprecator'; // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -55,6 +56,9 @@ class ParseServer { * @param {ParseServerOptions} options the parse server initialization options */ constructor(options: ParseServerOptions) { + // Scan for deprecated Parse Server options + Deprecator.scanParseServerOptions(options); + // Set option defaults injectDefaults(options); const { appId = requiredParameter('You must provide an appId!'), diff --git a/src/cli/utils/commander.js b/src/cli/utils/commander.js index d5a8208253..8b8826fe69 100644 --- a/src/cli/utils/commander.js +++ b/src/cli/utils/commander.js @@ -1,6 +1,8 @@ /* eslint-disable no-console */ import { Command } from 'commander'; import path from 'path'; +import Deprecator from '../../Deprecator/Deprecator'; + let _definitions; let _reverseDefinitions; let _defaults; @@ -40,7 +42,7 @@ Command.prototype.loadDefinitions = function (definitions) { }, {}); _defaults = Object.keys(definitions).reduce((defs, opt) => { - if (_definitions[opt].default) { + if (_definitions[opt].default !== undefined) { defs[opt] = _definitions[opt].default; } return defs; @@ -119,6 +121,8 @@ Command.prototype.parse = function (args, env) { this.setValuesIfNeeded(envOptions); // Load from file to override this.setValuesIfNeeded(fromFile); + // Scan for deprecated Parse Server options + Deprecator.scanParseServerOptions(this); // Last set the defaults this.setValuesIfNeeded(_defaults); };