balderdashy/waterline

View on GitHub
lib/waterline/utils/query/forge-adapter-error.js

Summary

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

var assert = require('assert');
var util = require('util');
var _ = require('@sailshq/lodash');
var flaverr = require('flaverr');
var getModel = require('../ontology/get-model');


/**
 * forgeAdapterError()
 *
 * Given a raw error from the adapter, convert it into a normalized, higher-level Error instance
 * with a better stack trace.
 *
 * > This includes potentially examining its `footprint` property.
 * > For more info on the lower-level driver specification, from whence this error originates, see:
 * > https://github.com/treelinehq/waterline-query-docs/blob/a0689b6a6536a3c196dff6a9528f2ef72d4f6b7d/docs/errors.md#notunique
 * >
 * > Note that after calling this utility, the provided `omen` must NEVER BE USED AGAIN!
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 *
 * @param {Ref} originalError        [The original error from the adapter]
 * @param {Ref} omen                 [Used purely for improving the quality of the stack trace.  Should be an error instance w/ its stack trace already adjusted.]
 * @param {String} adapterMethodName [The name of the adapter method]
 * @param {String} modelIdentity     [The identity of the originating model]
 * @param {Ref} orm                  [The current ORM instance]
 *
 * @returns {Error} the new error
 *                      @property {Ref} raw               [The original error, just as it came]
 *                      @property {String} modelIdentity  [The identity of the originating model]
 *                      @property {Function?} toJSON      [Might be included, but only if this is a recognized error]
 *                      @property {String?} code          [Might be included, but only if this is a recognized error (e.g. "E_UNIQUE")]
 *                      @property {Array?} attrNames      [Might be included if this is an E_UNIQUE error]
 *                                @of {String}
 *
 * > Note that if any internal error occurs, this utility still returns an Error
 * > instance rather than throwing.  Just note that the Error will not necessarily
 * > have any of the standard properties above.  (This is purely to avoid the burden
 * > of an extra try/catch in code that calls this utility.)
 *
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */
module.exports = function forgeAdapterError(err, omen, adapterMethodName, modelIdentity, orm){

  try {
    // Sanity checks
    assert(err, 'Should never call `forgeAdapterError` with a falsy first argument!');
    assert(_.isError(omen), 'An already-set-up, generic uniqueness error should be provided (in the second argument) to this utility.  This is for use as an omen, to improve the quality of the stack trace.');
    assert(_.isString(adapterMethodName) && adapterMethodName, 'Unexpected third argument to`forgeAdapterError`!  Expecting non-empty string.');

    // Look up model.
    var WLModel = getModel(modelIdentity, orm);

    // If this is an Error-like object (e.g. from bluebird) but not technically
    // valid for _.isError(), then parse it to obtain the underlying Error.
    err = flaverr.parseError(err) || err;

    // Call a self-invoking function which determines the customizations that we'll need
    // to fold into this particular adapter error below.
    //
    // > Note that it is NOT THE RESPONSIBILITY OF THIS SELF-INVOKING FUNCTION to new up an
    // > Error instance, and also that OTHER PROPERTIES ARE FOLDED IN AFTERWARDS!  The only
    // > reason this code is extrapolated is to reduce the likelihood of accidentally using
    // > the wrong stack trace as adapter errors are added on in the future.
    var customizations = (function(){

      //  ███╗   ██╗ ██████╗ ████████╗     █████╗ ███╗   ██╗    ███████╗██████╗ ██████╗  ██████╗ ██████╗
      //  ████╗  ██║██╔═══██╗╚══██╔══╝    ██╔══██╗████╗  ██║    ██╔════╝██╔══██╗██╔══██╗██╔═══██╗██╔══██╗
      //  ██╔██╗ ██║██║   ██║   ██║       ███████║██╔██╗ ██║    █████╗  ██████╔╝██████╔╝██║   ██║██████╔╝
      //  ██║╚██╗██║██║   ██║   ██║       ██╔══██║██║╚██╗██║    ██╔══╝  ██╔══██╗██╔══██╗██║   ██║██╔══██╗
      //  ██║ ╚████║╚██████╔╝   ██║       ██║  ██║██║ ╚████║    ███████╗██║  ██║██║  ██║╚██████╔╝██║  ██║
      //  ╚═╝  ╚═══╝ ╚═════╝    ╚═╝       ╚═╝  ╚═╝╚═╝  ╚═══╝    ╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝ ╚═════╝ ╚═╝  ╚═╝
      //
      // If the incoming `err` is not an error instance, then handle it as a special case.
      // (this should never happen)
      if (!_.isError(err)) {
        return {

          message: 'Malformed error from adapter: Should always be an Error instance, '+
          'but instead, got:\n'+
          '```\n'+
          util.inspect(err, {depth:5})+'\n'+
          '```'

        };
      }//-•


      // IWMIH, it's a valid Error instance.

      //  ███╗   ███╗██╗███████╗███████╗██╗███╗   ██╗ ██████╗
      //  ████╗ ████║██║██╔════╝██╔════╝██║████╗  ██║██╔════╝
      //  ██╔████╔██║██║███████╗███████╗██║██╔██╗ ██║██║  ███╗
      //  ██║╚██╔╝██║██║╚════██║╚════██║██║██║╚██╗██║██║   ██║
      //  ██║ ╚═╝ ██║██║███████║███████║██║██║ ╚████║╚██████╔╝
      //  ╚═╝     ╚═╝╚═╝╚══════╝╚══════╝╚═╝╚═╝  ╚═══╝ ╚═════╝
      //
      //  ███████╗ ██████╗  ██████╗ ████████╗██████╗ ██████╗ ██╗███╗   ██╗████████╗
      //  ██╔════╝██╔═══██╗██╔═══██╗╚══██╔══╝██╔══██╗██╔══██╗██║████╗  ██║╚══██╔══╝
      //  █████╗  ██║   ██║██║   ██║   ██║   ██████╔╝██████╔╝██║██╔██╗ ██║   ██║
      //  ██╔══╝  ██║   ██║██║   ██║   ██║   ██╔═══╝ ██╔══██╗██║██║╚██╗██║   ██║
      //  ██║     ╚██████╔╝╚██████╔╝   ██║   ██║     ██║  ██║██║██║ ╚████║   ██║
      //  ╚═╝      ╚═════╝  ╚═════╝    ╚═╝   ╚═╝     ╚═╝  ╚═╝╚═╝╚═╝  ╚═══╝   ╚═╝
      //
      // If it doesn't have a footprint, then this is some miscellaneous error from the adapter.
      // Still, wrap it up before sending it back.
      if (!err.footprint) {
        return {

          message: 'Unexpected error from database adapter: '+err.message

        };
      }//-•


      //  ██╗███╗   ██╗██╗   ██╗ █████╗ ██╗     ██╗██████╗
      //  ██║████╗  ██║██║   ██║██╔══██╗██║     ██║██╔══██╗
      //  ██║██╔██╗ ██║██║   ██║███████║██║     ██║██║  ██║
      //  ██║██║╚██╗██║╚██╗ ██╔╝██╔══██║██║     ██║██║  ██║
      //  ██║██║ ╚████║ ╚████╔╝ ██║  ██║███████╗██║██████╔╝
      //  ╚═╝╚═╝  ╚═══╝  ╚═══╝  ╚═╝  ╚═╝╚══════╝╚═╝╚═════╝
      //
      //  ███████╗ ██████╗  ██████╗ ████████╗██████╗ ██████╗ ██╗███╗   ██╗████████╗
      //  ██╔════╝██╔═══██╗██╔═══██╗╚══██╔══╝██╔══██╗██╔══██╗██║████╗  ██║╚══██╔══╝
      //  █████╗  ██║   ██║██║   ██║   ██║   ██████╔╝██████╔╝██║██╔██╗ ██║   ██║
      //  ██╔══╝  ██║   ██║██║   ██║   ██║   ██╔═══╝ ██╔══██╗██║██║╚██╗██║   ██║
      //  ██║     ╚██████╔╝╚██████╔╝   ██║   ██║     ██║  ██║██║██║ ╚████║   ██║
      //  ╚═╝      ╚═════╝  ╚═════╝    ╚═╝   ╚═╝     ╚═╝  ╚═╝╚═╝╚═╝  ╚═══╝   ╚═╝
      //
      // If it has an invalid footprint (not a dictionary, or missing the fundamentals),
      // then handle it as a special case.  This should never happen.
      if (!_.isObject(err.footprint) || !_.isString(err.footprint.identity) || err.footprint.identity === '') {
        return {

          message: 'Malformed error from adapter: If Error has a `footprint`, it should be a dictionary '+
          'with a valid `identity`.  But instead, the error\'s `footprint` is:\n'+
          '```\n'+
          util.inspect(err.footprint, {depth:5})+'\n'+
          '```'

        };
      }//-•



      // IWMIH, it's an Error instance with a superficially-valid footprint.
      switch (err.footprint.identity) {

        //  ███╗   ██╗ ██████╗ ████████╗    ██╗   ██╗███╗   ██╗██╗ ██████╗ ██╗   ██╗███████╗
        //  ████╗  ██║██╔═══██╗╚══██╔══╝    ██║   ██║████╗  ██║██║██╔═══██╗██║   ██║██╔════╝
        //  ██╔██╗ ██║██║   ██║   ██║       ██║   ██║██╔██╗ ██║██║██║   ██║██║   ██║█████╗
        //  ██║╚██╗██║██║   ██║   ██║       ██║   ██║██║╚██╗██║██║██║▄▄ ██║██║   ██║██╔══╝
        //  ██║ ╚████║╚██████╔╝   ██║       ╚██████╔╝██║ ╚████║██║╚██████╔╝╚██████╔╝███████╗
        //  ╚═╝  ╚═══╝ ╚═════╝    ╚═╝        ╚═════╝ ╚═╝  ╚═══╝╚═╝ ╚══▀▀═╝  ╚═════╝ ╚══════╝
        //
        // If this appears to be a uniqueness constraint violation error, then...
        case 'notUnique': return (function(){

          //  ┌─┐┌─┐┌─┐┌┬┐┌─┐┬─┐┬┌┐┌┌┬┐  ┬┌─┐  ┌┬┐┬┌─┐┌─┐┬┌┐┌┌─┐  ╦╔═╔═╗╦ ╦╔═╗
          //  ├┤ │ ││ │ │ ├─┘├┬┘││││ │   │└─┐  ││││└─┐└─┐│││││ ┬  ╠╩╗║╣ ╚╦╝╚═╗
          //  └  └─┘└─┘ ┴ ┴  ┴└─┴┘└┘ ┴   ┴└─┘  ┴ ┴┴└─┘└─┘┴┘└┘└─┘  ╩ ╩╚═╝ ╩ ╚═╝
          if (!_.isArray(err.footprint.keys)) {
            return {

              message: 'Malformed error from adapter: Since `footprint.identity` is "notUnique", '+
              'this error\'s footprint should have an array of `keys`!  But instead, the error\'s '+
              '`footprint.keys` is:\n'+
              '```\n'+
              util.inspect(err.footprint.keys, {depth:5})+'\n'+
              '```'

            };
          }//-•

          // But otherwise, it looks good, so we'll go on to forge it into a uniqueness error.


          //  ┌─┐┌─┐┌─┐┌┬┐┌─┐┬─┐┬┌┐┌┌┬┐  ┬┌─┐  ┌─┐┬─┐┌─┐┌─┐┌─┐┬─┐┬ ┬ ┬  ┌─┐┌─┐┬─┐┌┬┐┌─┐┌┬┐┌┬┐┌─┐┌┬┐
          //  ├┤ │ ││ │ │ ├─┘├┬┘││││ │   │└─┐  ├─┘├┬┘│ │├─┘├┤ ├┬┘│ └┬┘  ├┤ │ │├┬┘│││├─┤ │  │ ├┤  ││
          //  └  └─┘└─┘ ┴ ┴  ┴└─┴┘└┘ ┴   ┴└─┘  ┴  ┴└─└─┘┴  └─┘┴└─┴─┘┴   └  └─┘┴└─┴ ┴┴ ┴ ┴  ┴ └─┘─┴┘
          // Determine the standard customizations for this kind of error, mapping the `footprint.keys`
          // (~=column names) back to attribute names, and attaching a `toJSON()` function.

          // Format the `attrNames` property of our error by parsing `footprint.keys`.
          // Along the way, also track any unmatched keys.
          var namesOfOffendingAttrs = [];
          var unmatchedKeys = [];
          _.each(err.footprint.keys, function(key){

            // Find matching attr name.
            var matchingAttrName;
            _.any(WLModel.schema, function(wlsAttr, attrName) {

              var attrDef = WLModel.attributes[attrName];
              assert(attrDef, 'Attribute (`'+attrName+'`) is corrupted!  This attribute exists as a WLS attr in `schema`, so it should always exist in `attributes` as well-- but it does not!  If you are seeing this message, it probably means your model (`'+modelIdentity+'`) has become corrupted.');

              // If this is a plural association, then skip it.
              // (it is impossible for a key from this error to match up with one of these-- they don't even have column names)
              if (attrDef.collection) { return; }

              // Otherwise, we can expect a valid column name to exist.
              assert(wlsAttr.columnName, 'The normalized `schema` of model `'+modelIdentity+'` has an attribute (`'+attrName+'`) with no `columnName`.  But at this point, every WLS-normalized attribute should have a column name!  (If you are seeing this error, the model definition may have been corrupted in-memory-- or there might be a bug in WL schema.)');

              if (wlsAttr.columnName === key) {
                matchingAttrName = attrName;
                return true;
              }
            });//</_.any>

            // Push it on, if it could be found.
            if (matchingAttrName) {
              namesOfOffendingAttrs.push(matchingAttrName);
            }
            // Otherwise track this as an unmatched key.
            else {
              unmatchedKeys.push(key);
            }

          });//</_.each()>


          // If there were any unmatched keys, log a warning and silently ignore them.
          if (unmatchedKeys.length > 0) {
            console.warn('\n'+
              'Warning: Adapter sent back a uniqueness error, but that error references key(s) ('+unmatchedKeys+') which cannot\n'+
              'be matched up with the column names of any attributes in this model (`'+modelIdentity+'`).  This probably\n'+
              'means there is a bug in this adapter.\n'+
              '(Note for adapter implementors: If your adapter doesn\'t support granular reporting of the keys violated\n'+
              'in uniqueness errors, then just use an empty array for the `keys` property of this error.)\n'+
              '(Proceeding anyway as if these keys weren\'t included...)\n'
            );
          }//>-


          // Build the customizations for our uniqueness error.
          return {
            message: 'Would violate uniqueness constraint-- a record already exists with conflicting value(s).',
            code: 'E_UNIQUE',
            attrNames: namesOfOffendingAttrs,
            toJSON: function (){
              return {
                code: this.code,
                message: this.message,
                modelIdentity: this.modelIdentity,
                attrNames: this.attrNames,
              };
            }
          };

        })();

        //  ███╗   ██╗ ██████╗     ███████╗██╗   ██╗ ██████╗██╗  ██╗
        //  ████╗  ██║██╔═══██╗    ██╔════╝██║   ██║██╔════╝██║  ██║
        //  ██╔██╗ ██║██║   ██║    ███████╗██║   ██║██║     ███████║
        //  ██║╚██╗██║██║   ██║    ╚════██║██║   ██║██║     ██╔══██║
        //  ██║ ╚████║╚██████╔╝    ███████║╚██████╔╝╚██████╗██║  ██║
        //  ╚═╝  ╚═══╝ ╚═════╝     ╚══════╝ ╚═════╝  ╚═════╝╚═╝  ╚═╝
        //
        //  ██████╗ ██╗  ██╗██╗   ██╗███████╗██╗ ██████╗ █████╗ ██╗
        //  ██╔══██╗██║  ██║╚██╗ ██╔╝██╔════╝██║██╔════╝██╔══██╗██║
        //  ██████╔╝███████║ ╚████╔╝ ███████╗██║██║     ███████║██║
        //  ██╔═══╝ ██╔══██║  ╚██╔╝  ╚════██║██║██║     ██╔══██║██║
        //  ██║     ██║  ██║   ██║   ███████║██║╚██████╗██║  ██║███████╗
        //  ╚═╝     ╚═╝  ╚═╝   ╚═╝   ╚══════╝╚═╝ ╚═════╝╚═╝  ╚═╝╚══════╝
        //
        //  ███╗   ███╗ ██████╗ ██████╗ ███████╗██╗
        //  ████╗ ████║██╔═══██╗██╔══██╗██╔════╝██║
        //  ██╔████╔██║██║   ██║██║  ██║█████╗  ██║
        //  ██║╚██╔╝██║██║   ██║██║  ██║██╔══╝  ██║
        //  ██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗███████╗
        //  ╚═╝     ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝
        //
        case 'noSuchPhysicalModel': return (function(){
          return {
            message: 'Database says there is no such table/collection/etc.  '+err.message,
            code: 'E_NO_SUCH_PHYSICAL_MODEL',
            toJSON: function (){
              return {
                code: this.code,
                message: this.message,
                modelIdentity: this.modelIdentity,
              };
            }
          };
        })();


        //   ██████╗ █████╗ ████████╗ ██████╗██╗  ██╗ █████╗ ██╗     ██╗
        //  ██╔════╝██╔══██╗╚══██╔══╝██╔════╝██║  ██║██╔══██╗██║     ██║
        //  ██║     ███████║   ██║   ██║     ███████║███████║██║     ██║
        //  ██║     ██╔══██║   ██║   ██║     ██╔══██║██╔══██║██║     ██║
        //  ╚██████╗██║  ██║   ██║   ╚██████╗██║  ██║██║  ██║███████╗███████╗
        //   ╚═════╝╚═╝  ╚═╝   ╚═╝    ╚═════╝╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝╚══════╝
        //
        case 'catchall': return (function(){
          return {
            message: 'Unexpected error from database adapter: '+err.message
          };
        })();


        //  ██╗   ██╗███╗   ██╗██████╗ ███████╗ ██████╗ ██████╗  ██████╗ ███╗   ██╗██╗███████╗███████╗██████╗
        //  ██║   ██║████╗  ██║██╔══██╗██╔════╝██╔════╝██╔═══██╗██╔════╝ ████╗  ██║██║╚══███╔╝██╔════╝██╔══██╗
        //  ██║   ██║██╔██╗ ██║██████╔╝█████╗  ██║     ██║   ██║██║  ███╗██╔██╗ ██║██║  ███╔╝ █████╗  ██║  ██║
        //  ██║   ██║██║╚██╗██║██╔══██╗██╔══╝  ██║     ██║   ██║██║   ██║██║╚██╗██║██║ ███╔╝  ██╔══╝  ██║  ██║
        //  ╚██████╔╝██║ ╚████║██║  ██║███████╗╚██████╗╚██████╔╝╚██████╔╝██║ ╚████║██║███████╗███████╗██████╔╝
        //   ╚═════╝ ╚═╝  ╚═══╝╚═╝  ╚═╝╚══════╝ ╚═════╝ ╚═════╝  ╚═════╝ ╚═╝  ╚═══╝╚═╝╚══════╝╚══════╝╚═════╝
        //
        //  ███████╗ ██████╗  ██████╗ ████████╗██████╗ ██████╗ ██╗███╗   ██╗████████╗
        //  ██╔════╝██╔═══██╗██╔═══██╗╚══██╔══╝██╔══██╗██╔══██╗██║████╗  ██║╚══██╔══╝
        //  █████╗  ██║   ██║██║   ██║   ██║   ██████╔╝██████╔╝██║██╔██╗ ██║   ██║
        //  ██╔══╝  ██║   ██║██║   ██║   ██║   ██╔═══╝ ██╔══██╗██║██║╚██╗██║   ██║
        //  ██║     ╚██████╔╝╚██████╔╝   ██║   ██║     ██║  ██║██║██║ ╚████║   ██║
        //  ╚═╝      ╚═════╝  ╚═════╝    ╚═╝   ╚═╝     ╚═╝  ╚═╝╚═╝╚═╝  ╚═══╝   ╚═╝
        //
        // Handle unrecognized footprint identity as a special case.  (This should never happen.)
        default: return {

          message:
          'Malformed error from adapter: If Error has a `footprint`, it should be a dictionary with a recognized `identity`.  '+
          'But this error\'s footprint identity (`'+err.footprint.identity+'`) is not recognized.'

        };

      }//</switch>

    })();//</self-invoking function that builds `customizations`>

    assert(_.isObject(customizations) && !_.isError(customizations), 'At this point, `customizations` should be a dictionary, but it should not be an Error instance!');


    //  ██████╗ ██╗   ██╗██╗██╗     ██████╗        ██╗
    //  ██╔══██╗██║   ██║██║██║     ██╔══██╗       ██║
    //  ██████╔╝██║   ██║██║██║     ██║  ██║    ████████╗
    //  ██╔══██╗██║   ██║██║██║     ██║  ██║    ██╔═██╔═╝
    //  ██████╔╝╚██████╔╝██║███████╗██████╔╝    ██████║
    //  ╚═════╝  ╚═════╝ ╚═╝╚══════╝╚═════╝     ╚═════╝
    //
    //  ██████╗ ███████╗████████╗██╗   ██╗██████╗ ███╗   ██╗    ███████╗██╗███╗   ██╗ █████╗ ██╗
    //  ██╔══██╗██╔════╝╚══██╔══╝██║   ██║██╔══██╗████╗  ██║    ██╔════╝██║████╗  ██║██╔══██╗██║
    //  ██████╔╝█████╗     ██║   ██║   ██║██████╔╝██╔██╗ ██║    █████╗  ██║██╔██╗ ██║███████║██║
    //  ██╔══██╗██╔══╝     ██║   ██║   ██║██╔══██╗██║╚██╗██║    ██╔══╝  ██║██║╚██╗██║██╔══██║██║
    //  ██║  ██║███████╗   ██║   ╚██████╔╝██║  ██║██║ ╚████║    ██║     ██║██║ ╚████║██║  ██║███████╗
    //  ╚═╝  ╚═╝╚══════╝   ╚═╝    ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═══╝    ╚═╝     ╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝╚══════╝
    //
    //  ███████╗██████╗ ██████╗  ██████╗ ██████╗
    //  ██╔════╝██╔══██╗██╔══██╗██╔═══██╗██╔══██╗
    //  █████╗  ██████╔╝██████╔╝██║   ██║██████╔╝
    //  ██╔══╝  ██╔══██╗██╔══██╗██║   ██║██╔══██╗
    //  ███████╗██║  ██║██║  ██║╚██████╔╝██║  ██║
    //  ╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝ ╚═════╝ ╚═╝  ╚═╝
    //
    // Tack on the baseline customizations that are used in every adapter error.
    _.extend(customizations, {
      name: 'AdapterError',
      adapterMethodName: adapterMethodName,
      modelIdentity: modelIdentity,
      raw: err
    });

    // Then build and return the final error.
    //
    // > Remember: This cannibalizes the `omen` that was passed in!
    return flaverr(customizations, omen);

  } catch (e) {
    return new Error('Consistency violation: Waterline encountered an unexpected internal error: '+e.stack);
  }

};