From c78031f7ea79acec450b445b98f08c99ada6bf8d Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Mon, 15 Jan 2024 12:17:34 -0800 Subject: [PATCH] split validation out of abstract --- index.js | 2 + lib/abstract.js | 122 +++-------------------------------------- lib/validation.js | 117 +++++++++++++++++++++++++++++++++++++++ test/lib/abstract.js | 90 ------------------------------ test/lib/validation.js | 99 +++++++++++++++++++++++++++++++++ 5 files changed, 225 insertions(+), 205 deletions(-) create mode 100644 lib/validation.js create mode 100644 test/lib/validation.js diff --git a/index.js b/index.js index 2aeb924..500890c 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ const Factory = require('./lib/factory'); const SchemaVersionHelper = require('./lib/schema-version-helper'); const PostgresCreator = require('./lib/postgres-creator'); const SQLiteCreator = require('./lib/sqlite-creator'); +const { validate } = require('./lib/validation'); module.exports = { Abstract, @@ -14,4 +15,5 @@ module.exports = { SchemaVersionHelper, SQLiteCreator, PostgresCreator, + validate, }; diff --git a/lib/abstract.js b/lib/abstract.js index f8f5dc4..6e98137 100644 --- a/lib/abstract.js +++ b/lib/abstract.js @@ -4,27 +4,9 @@ const { fileScope } = require('@squeep/log-helper'); const DatabaseErrors = require('./errors'); const { schemaVersionObjectToNumber } = require('./schema-version-helper'); -const uuid = require('uuid'); const _fileScope = fileScope(__filename); -const supportedValidationTypes = [ - 'array', - 'bigint', - 'boolean', - 'buffer', - 'date', - 'function', - 'infinites', - 'null', - 'number', - 'object', - 'string', - 'symbol', - 'undefined', - 'uuid', -]; - /** * An abstract database class skeleton. */ @@ -42,7 +24,9 @@ class Database { this._isProduction = process.env['NODE_ENV'] === 'production'; // istanbul ignore next if (!this._isProduction) { - // List of methods an engine needs to implement, used for engine test coverage. + // Configure metadata used by tests and extra validation when not in production. + + // List of generic methods an engine needs to implement to reify this model. this._abstractMethods = [ 'schemaVersionsSupported', '_currentSchema', @@ -51,6 +35,10 @@ class Database { 'context', 'transaction', ]; + + // List of methods specific to database interface. + this._interfaceMethods = []; + // List of tables for tests to purge during integration tests. this._tableNames = []; } @@ -166,102 +154,6 @@ class Database { } - /** @typedef {[String[], String[]]} ValidPropertiesTypes */ - /** - * Check property types on an object. - * @param {Object} object - * @param {ValidPropertiesTypes[]} validations - */ - _validate(object, validations) { - validations.forEach(([properties, types]) => this._ensureTypes(object, properties, types)); - } - - - /** - * Sanity check for validation args. - * @param {Object} object - * @param {String[]} properties - * @param {String[]} types - */ - _ensureTypesArgsCheck(object, properties, types) { - const _scope = _fileScope('_ensureTypesArgsCheck'); - const problemArguments = []; - if (!object || typeof object !== 'object') { - problemArguments.push('object'); - } - if (!properties || !Array.isArray(properties)) { - problemArguments.push('properties'); - } - if (!types || !Array.isArray(types)) { - problemArguments.push('types'); - } - if (problemArguments.length) { - throw new TypeError(`invalid argument ${problemArguments}`); - } - types.forEach((t) => { - if (!this.constructor._supportedValidationTypes.includes(t)) { - this.logger.error(_scope, 'unsupported type', { object, properties, types, unsupportedType: t }); - throw new TypeError(`unsupported type '${t}'`); - } - }); - } - - - static get _supportedValidationTypes() { - return supportedValidationTypes; - } - - /** - * Basic type checking of object properties. - * - * Types may be any of the built-in types: - * - boolean - * - bigint (also allowed with 'number') - * - function - * - number (this will also allow 'bigint') - * - object - * - string - * - symbol - * - undefined - * - * Types may also be any of the following: - * - array - * - buffer - * - date - * - infinites - * - null - * - uuid - * @param {Object} object - * @param {String[]} properties - * @param {String[]} types - */ - _ensureTypes(object, properties, types) { - const _scope = _fileScope('_ensureTypes'); - - // Only sanity-check args outside of prod - this._isProduction || this._ensureTypesArgsCheck(object, properties, types); - - properties.forEach((p) => { - // eslint-disable-next-line security/detect-object-injection - const pObj = object[p]; - const pType = typeof pObj; - if (!types.includes(pType) - && !(types.includes('array') && Array.isArray(pObj)) - && !(types.includes('buffer') && pObj instanceof Buffer) - && !(types.includes('date') && pObj instanceof Date) - && !(types.includes('infinites') && Math.abs(pObj) === Infinity) - && !(types.includes('null') && pObj === null) - && !(types.includes('number') && pType === 'bigint') - && !(types.includes('uuid') && uuid.validate(pObj)) - ) { - const reason = `'${p}' is '${pType}', but must be ${types.length > 1 ? 'one of ' : ''}'${types}'`; - this.logger.error(_scope, reason, {}); - throw new DatabaseErrors.DataValidation(reason); - } - }); - } - - /** * Interface methods need implementations. Ensure the db-interaction * methods on the base class call this, so they may be overridden by diff --git a/lib/validation.js b/lib/validation.js new file mode 100644 index 0000000..caf1ae9 --- /dev/null +++ b/lib/validation.js @@ -0,0 +1,117 @@ +'use strict'; + +const { DataValidation } = require('./errors'); +const uuid = require('uuid'); + +const supportedValidationTypes = [ + 'array', + 'bigint', + 'boolean', + 'buffer', + 'date', + 'function', + 'infinites', + 'null', + 'number', + 'object', + 'string', + 'symbol', + 'undefined', + 'uuid', +]; + +/** @typedef {[String[], String[]]} ValidPropertiesTypes */ +/** + * Check property types on an object. + * @param {Object} object + * @param {ValidPropertiesTypes[]} validations + */ +function validate(object, validations) { + validations.forEach(([properties, types]) => ensureTypes(object, properties, types)); +} + + +/** + * Sanity check for validation args. + * @param {Object} object + * @param {String[]} properties + * @param {String[]} types + */ +function _ensureTypesArgsCheck(object, properties, types) { + const problemArguments = []; + if (!object || typeof object !== 'object') { + problemArguments.push('object'); + } + if (!properties || !Array.isArray(properties)) { + problemArguments.push('properties'); + } + if (!types || !Array.isArray(types)) { + problemArguments.push('types'); + } + if (problemArguments.length) { + throw new TypeError(`invalid argument ${problemArguments}`); + } + types.forEach((t) => { + if (!supportedValidationTypes.includes(t)) { + throw new TypeError(`unsupported type '${t}'`); + } + }); +} + + +/** + * Basic type checking of object properties. + * + * Types may be any of the built-in types: + * - boolean + * - bigint (also allowed with 'number') + * - function + * - number (this will also allow 'bigint') + * - object + * - string + * - symbol + * - undefined + * + * Types may also be any of the following: + * - array + * - buffer + * - date + * - infinites + * - null + * - uuid + * @param {Object} object + * @param {String[]} properties + * @param {String[]} types + */ +function _ensureTypes(object, properties, types) { + properties.forEach((p) => { + const pObj = object[p]; // eslint-disable-line security/detect-object-injection + const pType = typeof pObj; + if (!types.includes(pType) + && !(types.includes('array') && Array.isArray(pObj)) + && !(types.includes('buffer') && pObj instanceof Buffer) + && !(types.includes('date') && pObj instanceof Date) + && !(types.includes('infinites') && Math.abs(pObj) === Infinity) + && !(types.includes('null') && pObj === null) + && !(types.includes('number') && pType === 'bigint') + && !(types.includes('uuid') && uuid.validate(pObj)) + ) { + const reason = `'${p}' is '${pType}', but must be ${types.length > 1 ? 'one of ' : ''}'${types}'`; + throw new DataValidation(reason); + } + }); +} + +// istanbul ignore next +const ensureTypes = (process.env['NODE_ENV'] === 'production') ? _ensureTypes : (...args) => { + _ensureTypesArgsCheck(...args); + _ensureTypes(...args); +}; + +module.exports = { + supportedValidationTypes, + ensureTypes, + _ensureTypes, + _ensureTypesArgsCheck, + validate, +}; \ No newline at end of file diff --git a/test/lib/abstract.js b/test/lib/abstract.js index 1e8a44f..a001148 100644 --- a/test/lib/abstract.js +++ b/test/lib/abstract.js @@ -77,96 +77,6 @@ describe('Abstract', function () { }); // _camelfy - describe('_ensureTypesArgsCheck', function () { - it('fails for missing fields', function () { - assert.throws(() => db._ensureTypesArgsCheck(), TypeError); - }); - it('fails for invalid type', function () { - const object = {}; - const properties = ['prop']; - const types = ['bad type']; - assert.throws(() => db._ensureTypesArgsCheck(object, properties, types), TypeError); - }); - it('passes valid args', function () { - const object = {}; - const properties = ['prop']; - const types = ['string']; - db._ensureTypesArgsCheck(object, properties, types); - }); - }); // _ensureTypesArgsCheck - - describe('_ensureTypes / _validate', function () { - let object; - beforeEach(function () { - object = { - array: ['foo', 'bar'], - bignum: BigInt(456), - buf: Buffer.from('foop'), - date: new Date(), - infP: Infinity, - infN: -Infinity, - num: 123, - obj: {}, - str: 'some words', - uuid: 'a4dd5106-2d64-11ed-a2ba-0025905f714a', - veryNull: null, - }; - }); - it('succeeds', function () { - db._ensureTypes(object, ['array'], ['array']); - db._ensureTypes(object, ['bignum'], ['bigint']); - db._ensureTypes(object, ['bignum', 'num'], ['number']); - db._ensureTypes(object, ['buf'], ['buffer']); - db._ensureTypes(object, ['date'], ['date']); - db._ensureTypes(object, ['infP', 'infN'], ['infinites']); - db._ensureTypes(object, ['str', 'veryNull'], ['string', 'null']); - db._ensureTypes(object, ['uuid'], ['uuid']); - }); - it('data failure', function () { - assert.throws(() => db._ensureTypes(object, ['missingField'], ['string', 'null']), DataValidation); - }); - it('failure covers singular', function () { - try { - db._ensureTypes(object, ['missingField'], ['string']); - assert.fail('validation should have failed'); - } catch (e) { - assert(e instanceof DataValidation); - } - }); - it('parameter failure', function () { - try { - db._ensureTypes(object, ['missingField'], undefined); - assert.fail('validation should have failed'); - } catch (e) { - assert(e instanceof TypeError); - } - }); - it('_validate succeeds', function () { - db._validate(object, [ - [['array'], ['array']], - [['bignum'], ['bigint']], - [['bignum', 'num'], ['number']], - [['buf'], ['buffer']], - [['date'], ['date']], - [['infP', 'infN'], ['infinites']], - [['str', 'veryNull'], ['string', 'null']], - [['uuid'], ['uuid']], - ]); - }); - it('_validate rejects', function () { - assert.throws(() => db._validate({}, [ - [['array'], ['array']], - [['bignum'], ['bigint']], - [['bignum', 'num'], ['number']], - [['buf'], ['buffer']], - [['date'], ['date']], - [['infP', 'infN'], ['infinites']], - [['str', 'veryNull'], ['string', 'null']], - [['uuid'], ['uuid']], - ]), DataValidation); - }); - }); // _ensureTypes / _validate - describe('_notImplemented', function () { it('does what it says', function () { assert.throws(() => db._notImplemented('method', []), NotImplemented); diff --git a/test/lib/validation.js b/test/lib/validation.js new file mode 100644 index 0000000..6291af3 --- /dev/null +++ b/test/lib/validation.js @@ -0,0 +1,99 @@ +/* eslint-env mocha */ +/* eslint-disable sonarjs/no-duplicate-string */ +'use strict'; + +const assert = require('node:assert'); +const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require +const { StubLogger } = require('../helpers'); +const Validation = require('../../lib/validation'); +const { DataValidation } = require('../../lib/errors'); + +describe('Validation', function () { + + describe('_ensureTypesArgsCheck', function () { + it('fails for missing fields', function () { + assert.throws(() => Validation._ensureTypesArgsCheck(), TypeError); + }); + it('fails for invalid type', function () { + const object = {}; + const properties = ['prop']; + const types = ['bad type']; + assert.throws(() => Validation._ensureTypesArgsCheck(object, properties, types), TypeError); + }); + it('fails for wrong args', function () { + const object = 'foop'; + const properties = 'prop'; + const types = 'bad type'; + assert.throws(() => Validation._ensureTypesArgsCheck(object, properties, types), TypeError); + }); + it('passes valid args', function () { + const object = {}; + const properties = ['prop']; + const types = ['string']; + Validation._ensureTypesArgsCheck(object, properties, types); + }); + }); // _ensureTypesArgsCheck + + describe('_ensureTypes / validate', function () { + let object; + beforeEach(function () { + object = { + array: ['foo', 'bar'], + bignum: BigInt(456), + buf: Buffer.from('foop'), + date: new Date(), + infP: Infinity, + infN: -Infinity, + num: 123, + obj: {}, + str: 'some words', + uuid: 'a4dd5106-2d64-11ed-a2ba-0025905f714a', + veryNull: null, + }; + }); + it('succeeds', function () { + Validation._ensureTypes(object, ['array'], ['array']); + Validation._ensureTypes(object, ['bignum'], ['bigint']); + Validation._ensureTypes(object, ['bignum', 'num'], ['number']); + Validation._ensureTypes(object, ['buf'], ['buffer']); + Validation._ensureTypes(object, ['date'], ['date']); + Validation._ensureTypes(object, ['infP', 'infN'], ['infinites']); + Validation._ensureTypes(object, ['str', 'veryNull'], ['string', 'null']); + Validation._ensureTypes(object, ['uuid'], ['uuid']); + }); + it('data failure', function () { + assert.throws(() => Validation._ensureTypes(object, ['missingField'], ['string', 'null']), DataValidation); + }); + it('failure covers singular', function () { + assert.throws(() => Validation._ensureTypes(object, ['missingField'], ['string']), DataValidation); + }); + it('parameter failure', function () { + assert.throws(() => Validation._ensureTypes(object, ['missingField'], undefined), TypeError); + }); + it('validate succeeds', function () { + Validation.validate(object, [ + [['array'], ['array']], + [['bignum'], ['bigint']], + [['bignum', 'num'], ['number']], + [['buf'], ['buffer']], + [['date'], ['date']], + [['infP', 'infN'], ['infinites']], + [['str', 'veryNull'], ['string', 'null']], + [['uuid'], ['uuid']], + ]); + }); + it('validate rejects', function () { + assert.throws(() => Validation.validate({}, [ + [['array'], ['array']], + [['bignum'], ['bigint']], + [['bignum', 'num'], ['number']], + [['buf'], ['buffer']], + [['date'], ['date']], + [['infP', 'infN'], ['infinites']], + [['str', 'veryNull'], ['string', 'null']], + [['uuid'], ['uuid']], + ]), DataValidation); + }); + }); // _ensureTypes / validate + +}); // Validation -- 2.49.0