Source: api.js

/********************************************************************
 * Libraries - Why we need 'em
 ********************************************************************/
// Centralized Configs
var config = require('../config/config.js');
// Controllable Log Levels
var log = require('iphb-logs');
// To create random strings
var crypto = require('crypto');
// KV Store
var kvstore = require('./kvstore.js');

/********************************************************************
 * Guards
 ********************************************************************/

if (!config.kvstoreKeyPrefix) {
  var _error = [
    "Environment Variables Required:",
    "   KV_KEY_PREFIX"
  ].join('\n');
  throw new Error(_error);
}

/********************************************************************
 * Privates
 ********************************************************************/

/**
 * @function _getUniqueKey
 * @description Generate a unique key not already in use by the KV Store
 * @private
 * @param       {string}      prefix        The prefix we use for this app
 *                                          when storing unique keys in the
 *                                          kv store for this url.  this
 *                                          prefix needs to be unique for each
 *                                          domain we might use with this shortner
 *                                          code.
 * @param       {int}         length        The approximate length we want
 *                                          our unique url tokens to be.
 *                                          http://glg.link/$token
 * @param       {Function}    callback      Callback format (err,res)
 */
var _getUniqueKey = function(prefix, length, callback) {
  /************
   * Guards
   ***********/
  if (typeof(callback) !== "function") {
    return log.error("_getUniqueKey didn't get a callback");
  }

  if (!prefix) {
    prefix = "";
  }

  // Let's begin
  try {
    // Create a random string at the specified length,
    // then yoink out the equals sign (=)
    var _randomString = (crypto.randomBytes(length - 1)).toString('base64');
    var _keyWithoutEquals = _randomString.replace(/=/g, "").replace(/\+/g, "");
    // Now we double check if this key already exists in the KV store and
    // if it doesn't we have a winner.  We wrap this in an enclosure incase
    // we are called from a service or something that would possibly rapid fire
    var _testKey = function(key) {
      // We store the key with a prefix so we need to test it with the prefix
      // By doing this it allows us to use relatively small tokens lengths per domain
      // but guarantees uniqueness in the KV store
      kvstore.get([prefix, key].join(''), function(err, res) {
        // Errors are still no good.  Even though we don't want the key
        // to exist we should get a valid response from the KV store
        if (err) {
          throw new Error(err);
        }
        // We didn't get an error but there's already 'something' for this key
        // so we just try our entire process again
        if (res && res.message !== "no data for key") {
          return _getUniqueKey(prefix, length, callback);
        }
        // We must have a decent key
        callback(null, key);
      });
    };

    // Test that the key isn't already used
    _testKey(_keyWithoutEquals);

  } catch (e) {
    callback(e.toString());
  }
};

/********************************************************************
 * Main Exports
 ********************************************************************/

/**
 * @module Api
 * @description Public Exports for this module
 * @type {Object}
 */
var api = {
  /**
   * @function getUrlByKey
   * @description Get the URL for a key
   * @param    {string}    key       A key for the URL you wish to retrieve
   * @param    {Function}  callback  callback(err,url); the $url is a string
   */
  getUrlByKey: function(urlKey, callback) {
    /************
     * Guards
     ***********/
    // Callback must be a function
    if (typeof(callback) !== "function") {
      return log.error("getUrlByKey didn't get a callback");
    }

    try {
      // Prefix the keys we generate in the KV store with 'something' unique
      // to prevent collisions with any other app.  It doesn't matter what the
      // prefix is as long as its supported in the KV Store
      var _kvstoreKey = [config.kvstoreKeyPrefix, urlKey].join('');
      log.verbose("KVK:", _kvstoreKey);
      kvstore.get(_kvstoreKey, function(err, urlObject) {
        if (err || !urlObject || !urlObject.url) {
          var _error = ["Something went wrong getting urlKey:", err, urlObject.url].join(' ');
          log.verbose("Something went wrong getting urlKey:", err, urlObject.url);
          return callback(_error);
        }
        // return the url
        callback(null, urlObject.url);
      });
    } catch (e) {
      callback(e.toString());
    }

  },
  /**
   * @function    getKeyByUrl
   * @description Get a key for a URL.  When the shortener is presented with
   *              the returned key it will redirect to the URL passed here.
   * @param       {string}    url         The full URL you intend to shorten
   * @param       {Function}  callback    callback(err,key); the $key is a string
   *                                      that should be passed to the shortner url.
   *                                      For example:
   *                                        http://glg.link/$key
   */
  getKeyByUrl: function(url, callback) {
    /************
     * Guards
     ***********/
    if (typeof(callback) !== "function") {
      return log.error("getKeyByUrl didn't get a callback");
    }
    try {
      // Get a unique key.  The token length returned here will
      // be $prefix+$key.length.  IE, if you set a length of 4
      // the unique part will be approx 4 chars.  The prefix is in addition
      // to the unique part.  Also, the key length isn't exact.  We generate
      // a random string of the specified length and then base64 encode it.  Then,
      // we strip weird chars.  SO, it will be approx the length specified.  For
      // the purposes of this app.. meh
      var _keyLength = 4;
      _getUniqueKey(config.kvstoreKeyPrefix, _keyLength, function(err, key) {
        if (err) {
          throw new Error(err);
        }

        // Set our new key in the KV Store with the URL passed to us.  We
        //   - Join the unique key returned with the 'prefix'
        //       Example:
        //         _uniqueKey = "$prefix_$key"
        //   - Use the above key as a token in the kvstore
        //   - Pass an object to be posted into the KV store that
        //     contains the URL we want associated with this key
        //   - Callback with our result
        kvstore.set([config.kvstoreKeyPrefix, key].join(''), {
          url: url
        }, function(err, result) {
          if (err) {
            throw new Error(err);
          }
          callback(null, key);
        });
      });
    } catch (e) {
      callback(e.toString());
    }
  },
  /**
   * @function    shorten
   * @description A drop-in replacement for bit.ly's "shorten"
   *              method with no guarantees it works correctly.  =)
   * @param       {string}      url           The URL we want to shorten
   * @param       {Function}    callback      callback(err,res):
   *                                            res = {
   *                                              data: {
   *                                                url: $short_url
   *                                              }
   *                                            }
   */
  shorten: function(url, callback) {
    /************
     * Guards
     ***********/
    if (!config.glgShortUrl) {
      var _error = [
        "You must specify environment var:",
        "   GLG_SHORT_URL"
      ].join('\n');
      return callback(_error);
    }

    // Call our normal method but format the result in such a way
    // that it would be compatible with the Bitly node module
    try {
      api.getKeyByUrl(url, function(err, key) {
        if (err) {
          throw new Error(err);
        }

        var _dataObject = {};
        _dataObject.data = {};
        _dataObject.data.url = [config.glgShortUrl, '/', key].join('');

        return callback(null, _dataObject);

      });
    } catch (e) {
      return callback(e.toString());
    }
  }
};

// Export our api
module.exports = api;

/********************************************************************
 * Tests or Run Examples
 ********************************************************************/

if (module.parent === null) {

  log.enable.tests = true;

  api.getKeyByUrl("https://glg.it", function(err, key) {
    if (err) {
      return log.fail("getKeyByUrl:", err);
    }

    api.getUrlByKey(key, function(err, url) {
      if (err) {
        return log.fail("getUrlByKey:", err);
      }
      return log.success("getUrlByKey:", url);
    });

    return log.success("getKeyByUrl:", key);
  });

  api.shorten("https://www.google.com", function(err, res) {
    if (err) {
      return log.fail("shorten:", err);
    }
    log.success("shorten:", res.data.url);
  });
}