balderdashy/waterline

View on GitHub
lib/waterline/utils/query/private/normalize-pk-value.js

Summary

Maintainability
C
1 day
Test Coverage
/**
 * Module dependencies
 */

var util = require('util');
var _ = require('@sailshq/lodash');
var flaverr = require('flaverr');
var isSafeNaturalNumber = require('./is-safe-natural-number');


/**
 * normalizePkValue()
 *
 * Validate and normalize the provided pk value.
 *
 * > This ensures the provided pk value is a string or a number.
 * > • If a string, it also validates that it is not the empty string ("").
 * > • If a number, it also validates that it is a base-10, non-zero, positive integer
 * >   that is not larger than the maximum safe integer representable by JavaScript.
 * >   Also, if we are expecting numbers, numeric strings are tolerated, so long as they
 * >   can be parsed as valid numeric pk values.
 *
 * ------------------------------------------------------------------------------------------
 * @param  {String|Number} pkValue
 * @param  {String} expectedPkType   [either "number" or "string"]
 * ------------------------------------------------------------------------------------------
 * @returns {String|Number}
 *          A valid primary key value, guaranteed to match the specified `expectedPkType`.
 * ------------------------------------------------------------------------------------------
 * @throws {Error} if invalid
 *         @property {String} code (=== "E_INVALID_PK_VALUE")
 * ------------------------------------------------------------------------------------------
 * @throws {Error} If anything unexpected happens, e.g. bad usage, or a failed assertion.
 * ------------------------------------------------------------------------------------------
 */

module.exports = function normalizePkValue (pkValue, expectedPkType){

  // Check usage
  if (expectedPkType !== 'string' && expectedPkType !== 'number') {
    throw new Error('Consistency violation: The internal normalizePkValue() utility must always be called with a valid second argument ("string" or "number").  But instead, got: '+util.inspect(expectedPkType, {depth:5})+'');
  }


  // If explicitly expecting strings...
  if (expectedPkType === 'string') {
    if (!_.isString(pkValue)) {
      // > Note that we DO NOT tolerate non-strings being passed in, even though it
      // > would be possible to cast them into strings automatically.  While this would
      // > be useful for key/value adapters like Redis, or in SQL databases when using
      // > a string primary key, it can lead to bugs when querying against a database
      // > like MongoDB that uses special hex or uuid strings.
      throw flaverr('E_INVALID_PK_VALUE', new Error('Instead of a string (the expected pk type), the provided value is: '+util.inspect(pkValue,{depth:5})+''));
    }//-•

    // Empty string ("") is never a valid primary key value.
    if (pkValue === '') {
      throw flaverr('E_INVALID_PK_VALUE', new Error('Cannot use empty string ('+util.inspect(pkValue,{depth:5})+') as a primary key value.'));
    }//-•

  }//‡
  // Else if explicitly expecting numbers...
  else if (expectedPkType === 'number') {
    if (!_.isNumber(pkValue)) {

      // If this is not even a _string_ either, then reject it.
      // (Note that we handle this case separately in order to support a more helpful error message.)
      if (!_.isString(pkValue)) {
        throw flaverr('E_INVALID_PK_VALUE', new Error(
          'Instead of a number (the expected pk type), got: '+util.inspect(pkValue,{depth:5})+''
        ));
      }//-•



      // Tolerate strings that _look_ like base-10, non-zero, positive integers;
      // and that wouldn't be too big to be a safe JavaScript number.
      // (Cast them into numbers automatically.)

      var GOT_STRING_FOR_NUMERIC_PK_SUFFIX =
      'To resolve this error, pass in a valid base-10, non-zero, positive integer instead.  '+
      '(Or if you must use strings, then change the relevant model\'s pk attribute from '+
      '`type: \'number\'` to `type: \'string\'`.)';

      var canPrblyCoerceIntoValidNumber = _.isString(pkValue) && pkValue.match(/^[0-9]+$/);
      if (!canPrblyCoerceIntoValidNumber) {
        throw flaverr('E_INVALID_PK_VALUE', new Error(
          'Instead of a number, the provided value (`'+util.inspect(pkValue,{depth:5})+'`) is a string, '+
          'and it cannot be coerced into a valid primary key value automatically (contains characters other '+
          'than numerals 0-9).  '+
          GOT_STRING_FOR_NUMERIC_PK_SUFFIX
        ));
      }//-•

      var coercedNumber = +pkValue;
      if (coercedNumber > (Number.MAX_SAFE_INTEGER||9007199254740991)) {
        throw flaverr('E_INVALID_PK_VALUE', new Error(
          'Instead of a valid number, the provided value (`'+util.inspect(pkValue,{depth:5})+'`) is '+
          'a string that looks like a number.  But it cannot be coerced automatically because, despite '+
          'its "numbery" appearance, it\'s just too big!  '+
          GOT_STRING_FOR_NUMERIC_PK_SUFFIX
        ));
      }//-•

      pkValue = coercedNumber;

    }//>-•  </ if !_.isNumber(pkValue) >


    //-•
    // IWMIH, then we know that `pkValue` is now a number.
    // (But it might be something like `NaN` or `Infinity`!)
    //
    // `pkValue` should be provided as a safe, positive, non-zero, finite integer.
    //
    // > We do a few explicit checks below for better error messages, and then finally
    // > do one last check as a catchall, at the very end.

    // NaN is never valid as a primary key value.
    if (_.isNaN(pkValue)) {
      throw flaverr('E_INVALID_PK_VALUE', new Error('Cannot use `NaN` as a primary key value.'));
    }//-•

    // Zero is never a valid primary key value.
    if (pkValue === 0) {
      throw flaverr('E_INVALID_PK_VALUE', new Error('Cannot use zero ('+util.inspect(pkValue,{depth:5})+') as a primary key value.'));
    }//-•

    // A negative number is never a valid primary key value.
    if (pkValue < 0) {
      throw flaverr('E_INVALID_PK_VALUE', new Error('Cannot use a negative number ('+util.inspect(pkValue,{depth:5})+') as a primary key value.'));
    }//-•

    // A floating point number is never a valid primary key value.
    if (Math.floor(pkValue) !== pkValue) {
      throw flaverr('E_INVALID_PK_VALUE', new Error('Cannot use a floating point number ('+util.inspect(pkValue,{depth:5})+') as a primary key value.'));
    }//-•

    // Neither Infinity nor -Infinity are ever valid as primary key values.
    if (Infinity === pkValue || -Infinity === pkValue) {
      throw flaverr('E_INVALID_PK_VALUE', new Error('Cannot use `Infinity` or `-Infinity` (`'+util.inspect(pkValue,{depth:5})+'`) as a primary key value.'));
    }//-•

    // Numbers greater than the maximum safe JavaScript integer are never valid as a primary key value.
    // > Note that we check for `Infinity` above FIRST, before we do this comparison.  That's just so that
    // > we can display a tastier error message.
    if (pkValue > (Number.MAX_SAFE_INTEGER||9007199254740991)) {
      throw flaverr('E_INVALID_PK_VALUE', new Error('Cannot use the provided value (`'+util.inspect(pkValue,{depth:5})+'`), because it is too large to safely fit into a JavaScript integer (i.e. `> Number.MAX_SAFE_INTEGER`)'));
    }//-•

    // Now do one last check as a catch-all, w/ a generic error msg.
    if (!isSafeNaturalNumber(pkValue)) {
      throw flaverr('E_INVALID_PK_VALUE', new Error('Cannot use the provided value (`'+util.inspect(pkValue,{depth:5})+'`) as a primary key value -- it is not a "safe", natural number (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isSafeInteger).'));
    }

  } else { throw new Error('Consistency violation: Should not be possible to make it here in the code!  If you are seeing this error, there\'s a bug in Waterline!'); }
  //>-•

  // Return the normalized pk value.
  return pkValue;

};