split validation out of abstract
authorJustin Wind <justin.wind+git@gmail.com>
Mon, 15 Jan 2024 20:17:34 +0000 (12:17 -0800)
committerJustin Wind <justin.wind+git@gmail.com>
Mon, 15 Jan 2024 20:17:34 +0000 (12:17 -0800)
index.js
lib/abstract.js
lib/validation.js [new file with mode: 0644]
test/lib/abstract.js
test/lib/validation.js [new file with mode: 0644]

index 2aeb924a19a82398a1647b5658193bf690995c86..500890ced5e9da0f545b981077d8180dabac0bcc 100644 (file)
--- 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,
 };
index f8f5dc4a49eb2d2151b02f48c7a683010a5ef8e1..6e98137e857cd49d2ae84b78f1f9e0a9a2cb7f64 100644 (file)
@@ -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 (file)
index 0000000..caf1ae9
--- /dev/null
@@ -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
index 1e8a44f9131f37eebd7b8ca08428dd18e89120c3..a0011484699fc03f6e7f6754c1532658123664c0 100644 (file)
@@ -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 (file)
index 0000000..6291af3
--- /dev/null
@@ -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