diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ba5de8864..34bfff72f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.2.23 + +- fix: upgrade to @hapi/joi to version 16.1.8 + ## 3.2.22 - fix: add appName as label in usage metrics diff --git a/lib/error/validation-error.js b/lib/error/validation-error.js deleted file mode 100644 index 43995b34c4..0000000000 --- a/lib/error/validation-error.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -class ValidationError extends Error { - constructor(message) { - super(); - Error.captureStackTrace(this, this.constructor); - - this.name = this.constructor.name; - this.message = message; - } -} - -module.exports = ValidationError; diff --git a/lib/routes/admin-api/feature-schema.js b/lib/routes/admin-api/feature-schema.js index 85b75eb827..1e883448d3 100644 --- a/lib/routes/admin-api/feature-schema.js +++ b/lib/routes/admin-api/feature-schema.js @@ -1,6 +1,6 @@ 'use strict'; -const joi = require('joi'); +const joi = require('@hapi/joi'); const { nameType } = require('./util'); const nameSchema = joi.object().keys({ name: nameType }); diff --git a/lib/routes/admin-api/feature-schema.test.js b/lib/routes/admin-api/feature-schema.test.js index 4e6d553540..3701624adb 100644 --- a/lib/routes/admin-api/feature-schema.test.js +++ b/lib/routes/admin-api/feature-schema.test.js @@ -2,7 +2,6 @@ const test = require('ava'); const { featureShema } = require('./feature-schema'); -const joi = require('joi'); test('should require URL firendly name', t => { const toggle = { @@ -11,7 +10,7 @@ test('should require URL firendly name', t => { strategies: [{ name: 'default' }], }; - const { error } = joi.validate(toggle, featureShema); + const { error } = featureShema.validate(toggle); t.deepEqual(error.details[0].message, '"name" must be URL friendly'); }); @@ -22,7 +21,7 @@ test('should be valid toggle name', t => { strategies: [{ name: 'default' }], }; - const { value } = joi.validate(toggle, featureShema); + const { value } = featureShema.validate(toggle); t.deepEqual(value, toggle); }); @@ -40,7 +39,7 @@ test('should strip extra variant fields', t => { ], }; - const { value } = joi.validate(toggle, featureShema); + const { value } = featureShema.validate(toggle); t.notDeepEqual(value, toggle); t.falsy(value.variants[0].unkown); }); @@ -64,7 +63,7 @@ test('should be possible to define variant overrides', t => { ], }; - const { value, error } = joi.validate(toggle, featureShema); + const { value, error } = featureShema.validate(toggle); t.deepEqual(value, toggle); t.falsy(error); }); @@ -88,9 +87,12 @@ test('variant overrides must have corect shape', async t => { }; try { - await joi.validate(toggle, featureShema); + await featureShema.validateAsync(toggle); } catch (error) { - t.is(error.details[0].message, '"overrides" must be an array'); + t.is( + error.details[0].message, + '"variants[0].overrides" must be an array' + ); } }); @@ -112,7 +114,7 @@ test('should keep constraints', t => { ], }; - const { value, error } = joi.validate(toggle, featureShema); + const { value, error } = featureShema.validate(toggle); t.deepEqual(value, toggle); t.falsy(error); }); diff --git a/lib/routes/admin-api/feature.js b/lib/routes/admin-api/feature.js index b0ac05337c..fc1def07b7 100644 --- a/lib/routes/admin-api/feature.js +++ b/lib/routes/admin-api/feature.js @@ -1,7 +1,6 @@ 'use strict'; const Controller = require('../controller'); -const joi = require('joi'); const { FEATURE_CREATED, @@ -56,7 +55,7 @@ class FeatureController extends Controller { const name = req.body.name; try { - await joi.validate({ name }, nameSchema); + await nameSchema.validateAsync({ name }); await this.validateUniqueName(name); res.status(201).end(); } catch (error) { @@ -87,11 +86,11 @@ class FeatureController extends Controller { try { await this.validateUniqueName(toggleName); - const featureToggle = await joi.validate(req.body, featureShema); + const value = await featureShema.validateAsync(req.body); await this.eventStore.store({ type: FEATURE_CREATED, createdBy: userName, - data: featureToggle, + data: value, }); res.status(201).end(); } catch (error) { @@ -108,7 +107,7 @@ class FeatureController extends Controller { try { await this.featureToggleStore.getFeature(featureName); - await joi.validate(updatedFeature, featureShema); + await featureShema.validateAsync(updatedFeature); await this.eventStore.store({ type: FEATURE_UPDATED, createdBy: userName, diff --git a/lib/routes/admin-api/metrics-schema.js b/lib/routes/admin-api/metrics-schema.js index e6ca5b57f7..087d812af1 100644 --- a/lib/routes/admin-api/metrics-schema.js +++ b/lib/routes/admin-api/metrics-schema.js @@ -1,6 +1,6 @@ 'use strict'; -const joi = require('joi'); +const joi = require('@hapi/joi'); const { nameType } = require('./util'); const applicationSchema = joi diff --git a/lib/routes/admin-api/metrics.js b/lib/routes/admin-api/metrics.js index 1bd3d0e6e5..d4c04b0efe 100644 --- a/lib/routes/admin-api/metrics.js +++ b/lib/routes/admin-api/metrics.js @@ -1,6 +1,5 @@ 'use strict'; -const joi = require('joi'); const Controller = require('../controller'); const ClientMetrics = require('../../client-metrics'); const schema = require('./metrics-schema'); @@ -80,7 +79,7 @@ class MetricsController extends Controller { const input = Object.assign({}, req.body, { appName: req.params.appName, }); - const { value: applicationData, error } = joi.validate(input, schema); + const { value: applicationData, error } = schema.validate(input); if (error) { this.logger.warn('Invalid application data posted', error); diff --git a/lib/routes/admin-api/strategy-schema.js b/lib/routes/admin-api/strategy-schema.js index 7175e7bce9..c7052be919 100644 --- a/lib/routes/admin-api/strategy-schema.js +++ b/lib/routes/admin-api/strategy-schema.js @@ -1,6 +1,6 @@ 'use strict'; -const joi = require('joi'); +const joi = require('@hapi/joi'); const { nameType } = require('./util'); const strategySchema = joi.object().keys({ diff --git a/lib/routes/admin-api/strategy.js b/lib/routes/admin-api/strategy.js index 185fa24e03..6fce18c8bc 100644 --- a/lib/routes/admin-api/strategy.js +++ b/lib/routes/admin-api/strategy.js @@ -1,7 +1,6 @@ 'use strict'; const Controller = require('../controller'); -const joi = require('joi'); const eventType = require('../../event-type'); const NameExistsError = require('../../error/name-exists-error'); @@ -65,12 +64,12 @@ class StrategyController extends Controller { async createStrategy(req, res) { try { - const newStrategy = await joi.validate(req.body, strategySchema); - await this._validateStrategyName(newStrategy); + const value = await strategySchema.validateAsync(req.body); + await this._validateStrategyName(value); await this.eventStore.store({ type: eventType.STRATEGY_CREATED, createdBy: extractUser(req), - data: newStrategy, + data: value, }); res.status(201).end(); } catch (error) { @@ -82,14 +81,14 @@ class StrategyController extends Controller { const input = req.body; try { - const updatedStrategy = await joi.validate(input, strategySchema); + const value = await strategySchema.validateAsync(input); const strategy = await this.strategyStore.getStrategy(input.name); await this._validateEditable(strategy); await this.eventStore.store({ type: eventType.STRATEGY_UPDATED, createdBy: extractUser(req), - data: updatedStrategy, + data: value, }); res.status(200).end(); } catch (error) { diff --git a/lib/routes/admin-api/strategy.test.js b/lib/routes/admin-api/strategy.test.js index af451261e5..ae591d26d8 100644 --- a/lib/routes/admin-api/strategy.test.js +++ b/lib/routes/admin-api/strategy.test.js @@ -59,7 +59,7 @@ test('require a name when creating a new stratey', t => { .send({}) .expect(400) .expect(res => { - t.true(res.body.name === 'ValidationError'); + t.true(res.body.details[0].message === '"name" is required'); }); }); @@ -73,7 +73,10 @@ test('require parameters array when creating a new stratey', t => { .send({ name: 'TestStrat' }) .expect(400) .expect(res => { - t.true(res.body.name === 'ValidationError'); + t.deepEqual( + res.body.details[0].message, + '"parameters" is required' + ); }); }); diff --git a/lib/routes/admin-api/util.js b/lib/routes/admin-api/util.js index a476ecd0d3..3be0b71b3a 100644 --- a/lib/routes/admin-api/util.js +++ b/lib/routes/admin-api/util.js @@ -1,34 +1,23 @@ 'use strict'; -const joi = require('joi'); +const joi = require('@hapi/joi'); const customJoi = joi.extend(j => ({ + type: 'isUrlFriendly', base: j.string(), - name: 'string', - language: { - isUrlFriendly: 'must be URL friendly', + messages: { + 'isUrlFriendly.base': '"{{#label}}" must be URL friendly', + }, + validate(value, helpers) { + // Base validation regardless of the rules applied + if (encodeURIComponent(value) !== value) { + // Generate an error, state and options need to be passed + return { value, errors: helpers.error('isUrlFriendly.base') }; + } }, - rules: [ - { - name: 'isUrlFriendly', - validate(params, value, state, options) { - if (encodeURIComponent(value) !== value) { - // Generate an error, state and options need to be passed - return this.createError( - 'string.isUrlFriendly', - { v: value }, - state, - options - ); - } - return value; // Everything is OK - }, - }, - ], })); const nameType = customJoi - .string() .isUrlFriendly() .min(2) .max(100) @@ -41,6 +30,7 @@ const handleErrors = (res, logger, error) => { return res.status(404).end(); case 'NameExistsError': case 'ValidationError': + error.isJoi = true; return res .status(400) .json(error) diff --git a/lib/routes/client-api/metrics-schema.js b/lib/routes/client-api/metrics-schema.js index 425b9b754a..5884705e0c 100644 --- a/lib/routes/client-api/metrics-schema.js +++ b/lib/routes/client-api/metrics-schema.js @@ -1,6 +1,6 @@ 'use strict'; -const joi = require('joi'); +const joi = require('@hapi/joi'); const countSchema = joi .object() diff --git a/lib/routes/client-api/metrics.js b/lib/routes/client-api/metrics.js index de1d46b055..6b53650358 100644 --- a/lib/routes/client-api/metrics.js +++ b/lib/routes/client-api/metrics.js @@ -1,7 +1,5 @@ 'use strict'; -const joi = require('joi'); - const Controller = require('../controller'); const { clientMetricsSchema } = require('./metrics-schema'); @@ -19,7 +17,7 @@ class ClientMetricsController extends Controller { const data = req.body; const clientIp = req.ip; - const { error, value } = joi.validate(data, clientMetricsSchema); + const { error, value } = clientMetricsSchema.validate(data); if (error) { this.logger.warn('Invalid metrics posted', error); diff --git a/lib/routes/client-api/metrics.test.js b/lib/routes/client-api/metrics.test.js index 1e59792242..9d06913707 100644 --- a/lib/routes/client-api/metrics.test.js +++ b/lib/routes/client-api/metrics.test.js @@ -2,7 +2,6 @@ const test = require('ava'); const supertest = require('supertest'); -const joi = require('joi'); const store = require('./../../../test/fixtures/store'); const getLogger = require('../../../test/fixtures/no-logger'); const getApp = require('../../app'); @@ -132,7 +131,7 @@ test('shema allow empty strings', t => { stop: '2019-05-06T09:30:50.515Z', }, }; - const { error, value } = joi.validate(data, clientMetricsSchema); + const { error, value } = clientMetricsSchema.validate(data); t.falsy(error); t.is(value.bucket.toggles.Demo2.yes, 0); t.is(value.bucket.toggles.Demo2.no, 0); @@ -148,7 +147,7 @@ test('shema allow yes=', t => { stop: '2019-05-06T09:30:50.515Z', }, }; - const { error, value } = joi.validate(data, clientMetricsSchema); + const { error, value } = clientMetricsSchema.validate(data); t.falsy(error); t.is(value.bucket.toggles.Demo2.yes, 12); t.is(value.bucket.toggles.Demo2.no, 256); diff --git a/lib/routes/client-api/register-schema.js b/lib/routes/client-api/register-schema.js index 9d0997cd11..4a90f8089c 100644 --- a/lib/routes/client-api/register-schema.js +++ b/lib/routes/client-api/register-schema.js @@ -1,6 +1,6 @@ 'use strict'; -const joi = require('joi'); +const joi = require('@hapi/joi'); const clientRegisterSchema = joi .object() diff --git a/lib/routes/client-api/register.js b/lib/routes/client-api/register.js index 2c759e5392..788dcf53f6 100644 --- a/lib/routes/client-api/register.js +++ b/lib/routes/client-api/register.js @@ -1,7 +1,5 @@ 'use strict'; -const joi = require('joi'); - const Controller = require('../controller'); const { clientRegisterSchema: schema } = require('./register-schema'); @@ -17,19 +15,19 @@ class RegisterController extends Controller { async handleRegister(req, res) { const data = req.body; - const { value: clientRegistration, error } = joi.validate(data, schema); + const { value, error } = schema.validate(data); if (error) { this.logger.warn('Invalid client data posted', error); return res.status(400).json(error); } - clientRegistration.clientIp = req.ip; + value.clientIp = req.ip; try { - await this.clientApplicationsStore.upsert(clientRegistration); - await this.clientInstanceStore.insert(clientRegistration); - const { appName, instanceId } = clientRegistration; + await this.clientApplicationsStore.upsert(value); + await this.clientInstanceStore.insert(value); + const { appName, instanceId } = value; this.logger.info( `New client registration: appName=${appName}, instanceId=${instanceId}` ); diff --git a/lib/state-service.js b/lib/state-service.js index 7c5a42211e..f19de4f138 100644 --- a/lib/state-service.js +++ b/lib/state-service.js @@ -1,6 +1,6 @@ 'use strict'; -const joi = require('joi'); +const joi = require('@hapi/joi'); const fs = require('fs'); const mime = require('mime'); const { featureShema } = require('./routes/admin-api/feature-schema'); @@ -52,7 +52,7 @@ class StateService { async import({ data, userName, dropBeforeImport }) { const { eventStore } = this.config.stores; - const importData = await joi.validate(data, dataSchema); + const importData = await dataSchema.validateAsync(data); if (importData.features) { this.logger.info( diff --git a/lib/user.js b/lib/user.js index ea0d24e983..601dc6c60a 100644 --- a/lib/user.js +++ b/lib/user.js @@ -1,7 +1,7 @@ 'use strict'; const gravatar = require('gravatar'); -const Joi = require('joi'); +const Joi = require('@hapi/joi'); module.exports = class User { constructor({ name, email, permissions, imageUrl } = {}) { diff --git a/package.json b/package.json index 8c7180c0b9..c48b600620 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "errorhandler": "^1.5.1", "express": "^4.17.1", "gravatar": "^1.8.0", - "joi": "^14.3.1", + "@hapi/joi": "^16.1.8", "js-yaml": "^3.12.2", "knex": "^0.20.0", "log4js": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index 08b22bcd3c..0542b22e74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -439,6 +439,44 @@ dependencies: arrify "^1.0.1" +"@hapi/address@^2.1.2": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" + integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ== + +"@hapi/formula@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-1.2.0.tgz#994649c7fea1a90b91a0a1e6d983523f680e10cd" + integrity sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA== + +"@hapi/hoek@^8.2.4", "@hapi/hoek@^8.3.0": + version "8.5.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.0.tgz#2f9ce301c8898e1c3248b0a8564696b24d1a9a5a" + integrity sha512-7XYT10CZfPsH7j9F1Jmg1+d0ezOux2oM2GfArAzLwWe4mE2Dr3hVjsAL6+TFY49RRJlCdJDMw3nJsLFroTc8Kw== + +"@hapi/joi@^16.1.8": + version "16.1.8" + resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-16.1.8.tgz#84c1f126269489871ad4e2decc786e0adef06839" + integrity sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg== + dependencies: + "@hapi/address" "^2.1.2" + "@hapi/formula" "^1.2.0" + "@hapi/hoek" "^8.2.4" + "@hapi/pinpoint" "^1.0.2" + "@hapi/topo" "^3.1.3" + +"@hapi/pinpoint@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-1.0.2.tgz#025b7a36dbbf4d35bf1acd071c26b20ef41e0d13" + integrity sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ== + +"@hapi/topo@^3.1.3": + version "3.1.6" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29" + integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ== + dependencies: + "@hapi/hoek" "^8.3.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b" @@ -2801,10 +2839,6 @@ hasha@^5.0.0: is-stream "^1.1.0" type-fest "^0.3.0" -hoek@6.x.x: - version "6.1.2" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.2.tgz#99e6d070561839de74ee427b61aa476bd6bddfd6" - homedir-polyfill@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" @@ -3253,12 +3287,6 @@ isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" -isemail@3.x.x: - version "3.2.0" - resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.2.0.tgz#59310a021931a9fb06bbb51e155ce0b3f236832c" - dependencies: - punycode "2.x.x" - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -3350,14 +3378,6 @@ jest-docblock@^21.0.0: version "21.2.0" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414" -joi@^14.3.1: - version "14.3.1" - resolved "https://registry.yarnpkg.com/joi/-/joi-14.3.1.tgz#164a262ec0b855466e0c35eea2a885ae8b6c703c" - dependencies: - hoek "6.x.x" - isemail "3.x.x" - topo "3.x.x" - js-string-escape@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" @@ -4853,15 +4873,15 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@2.x.x, punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" @@ -5879,12 +5899,6 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -topo@3.x.x: - version "3.0.3" - resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.3.tgz#d5a67fb2e69307ebeeb08402ec2a2a6f5f7ad95c" - dependencies: - hoek "6.x.x" - tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"