initial commit of cloudant trigger provider
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c95e0ad
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/node_modules/
+*.log
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..05efda4
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,26 @@
+FROM ubuntu:14.04
+
+ENV DEBIAN_FRONTEND noninteractive
+
+# Initial update and some basics.
+# This odd double update seems necessary to get curl to download without 404 errors.
+RUN apt-get update --fix-missing && \
+  apt-get install -y wget && \
+  apt-get update && \
+  apt-get install -y curl && \
+  apt-get update && \
+  apt-get remove -y nodejs && \
+  curl -sL https://deb.nodesource.com/setup_0.12 | bash - && \
+  apt-get install -y nodejs
+
+# only package.json
+ADD package.json /cloudantTrigger/
+RUN cd /cloudantTrigger; npm install
+
+# App
+ADD . /cloudantTrigger
+
+EXPOSE 8080
+
+# Run the app
+CMD ["/bin/bash", "-c", "node /cloudantTrigger/app.js >> /logs/cloudantTrigger_logs.log 2>&1"]
diff --git a/Logger.js b/Logger.js
new file mode 100644
index 0000000..1ee6ca3
--- /dev/null
+++ b/Logger.js
@@ -0,0 +1,51 @@
+var _ = require('lodash');
+var moment = require('moment');
+var winston = require('winston');
+
+var emailRegex = /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/g;
+
+var logger = new winston.Logger({
+    transports: [
+        new winston.transports.Console({
+            timestamp: function() {
+                return moment.utc().format("YYYY-MM-DDTHH:mm:ss.SSS") + 'Z';
+            },
+            formatter: function(options) {
+                // Return string will be passed to logger.
+                return '[' + options.timestamp() +'] ['+ options.level.toUpperCase() +'] '+  options.message;
+            }
+        })
+    ],
+    filters: [
+        function maskEmails(level, msg) {
+            return msg.replace(emailRegex, 'xxxxxxxx');
+        }
+    ]
+});
+
+function getMessage(argsObject) {
+    var args = Array.prototype.slice.call(argsObject);
+    args.splice(0, 2);
+    args.forEach(function(arg, i) {
+        if (_.isObject(args[i])) {
+            args[i] = JSON.stringify(args[i]);
+        }
+    });
+    return args.join(' ');
+}
+
+// FORMAT: s"[$time] [$category] [$id] [$componentName] [$name] $message"
+module.exports = {
+    info: function(tid, name) {
+        logger.info('['+tid+']', '['+name+']', getMessage(arguments));
+    },
+    warn: function(tid, name) {
+        logger.warn('['+tid+']', '['+name+']', getMessage(arguments));
+    },
+    error: function(tid, name) {
+        logger.error('['+tid+']', '['+name+']', getMessage(arguments));
+    },
+    debug: function(tid, name) {
+        logger.debug('['+tid+']', '['+name+']', getMessage(arguments));
+    }
+};
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c146d66
--- /dev/null
+++ b/README.md
@@ -0,0 +1,26 @@
+RESTful service to listen for changes to a Cloudant database.
+
+Changes to a Cloudant database can trigger the invocation of an action, passing the action the body of the updated document.
+
+See the scripts directory for examples on how to register a new Cloudant trigger.
+
+## Testing the Cloudant trigger service
+
+The following commands will test the Cloudant trigger service:
+
+# Build 
+cd <bluewhisk_home>
+gradle distDocker
+
+# Deploy
+Follow the instructions in [ansible/README.md][../../../ansible/README.md]
+
+# Register an action and listen for changes to a test Cloudant database.
+./bin/wsk create HELLOCLOUDANT ./actions/hellocloudant.js
+./catalog/providers/cloudantTrigger/scripts/addTestTriggers.sh
+
+# Insert a new document to the test Cloudant database.
+./catalog/providers/cloudantTrigger/scripts/addNewDocument.sh
+
+You should see the output from invoking the HELLOCLOUDANT action in the ELK logs.
+
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..5670383
--- /dev/null
+++ b/app.js
@@ -0,0 +1,144 @@
+'use strict';
+/**
+ * Service which can be configured to listen for triggers from a provider.
+ * The Provider will store, invoke, and POST whisk events appropriately.
+ */
+var http = require('http');
+var express = require('express');
+var request = require('request');
+var bodyParser = require('body-parser');
+var logger = require('./Logger');
+
+var ProviderUtils = require('./lib/utils.js');
+var ProviderRAS = require('./lib/ras.js');
+var ProviderUpdate = require('./lib/update.js');
+var ProviderCreate = require('./lib/create.js');
+var ProviderDelete = require('./lib/delete.js');
+var constants = require('./lib/constants.js');
+
+// Initialize the Express Application
+var app = express();
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: false }));
+app.set('port', process.env.PORT || 8080);
+
+// TODO: Setup a proper Transaction ID
+var tid = "??";
+
+// Whisk API Router Host
+var routerHost = process.env.ROUTER_HOST || 'localhost';
+
+// This is the maximum times a single trigger is allow to fire.
+// Trigger should not be allow to be created with a value higher than this value
+// Trigger can be created with a value lower than this between 1 and this value
+var triggerFireLimit = 10000;
+
+// Maximum number of times to retry the invocation of an action
+// before deleting the associated trigger
+var retriesBeforeDelete = 5;
+
+// Allow invoking servers with self-signed certificates.
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
+
+// If it does not already exist, create the triggers database.  This is the database that will
+// store the managed triggers.
+//
+var dbProvider = process.env.DB_PROVIDER;
+var dbUsername = process.env.DB_USERNAME;
+var dbPassword = process.env.DB_PASSWORD;
+var dbHost = process.env.DB_HOST;
+var dbPort = process.env.DB_PORT;
+var dbProtocol = process.env.DB_PROTOCOL;
+var dbPrefix = process.env.DB_PREFIX;
+var databaseName = dbPrefix + constants.TRIGGER_DB_SUFFIX;
+
+// Create the Provider Server
+var server = http.createServer(app);
+server.listen(app.get('port'), function(){
+    logger.info(tid, 'server.listen', 'Express server listening on port ' + app.get('port'));
+});
+
+function createDatabase (nanop) {
+
+  if (nanop !== null) {
+    nanop.db.create(databaseName, function(err, body, header) {
+        if (!err) {
+          logger.info(tid, databaseName, ' database for triggers was created.');
+        } else {
+          logger.info(tid, databaseName, err);
+        }
+    });
+    var chDb = nanop.db.use(databaseName);
+    return chDb;
+  } else {
+    return null;
+  }
+
+}
+
+function createTriggerDb () {
+
+  var nanop = null;
+
+  var promise = new Promise(function(resolve, reject) {
+
+    nanop = require('nano')(dbProtocol + '://' + dbHost + ':' + dbPort);
+    logger.info('url is ' +  dbProtocol + '://' + dbHost + ':' + dbPort);
+    nanop.auth(dbUsername, dbPassword, function (err, body, headers) {
+      if (err) {
+        reject(err);
+      } else {
+        nanop = require('nano')({url: dbProtocol + '://' + dbHost + ':' + dbPort, cookie: headers['set-cookie']});
+        resolve(createDatabase (nanop));
+      }
+    });
+
+  });
+
+  return promise;
+
+}
+
+// Initialize the Provider Server
+function init(server) {
+    if (server !== null) {
+        var address = server.address();
+        if (address === null) {
+            logger.error(tid, 'init', 'Error initializing server. Perhaps port is already in use.');
+            process.exit(-1);
+        }
+    }
+
+    ///
+    var triggerDBPromise = createTriggerDb();
+    triggerDBPromise.then(function (nanoDb) {
+
+      logger.info(tid, 'init', 'trigger storage database details: ', nanoDb);
+
+      var providerUtils = new ProviderUtils (tid, logger, app, retriesBeforeDelete, nanoDb, dbProvider, triggerFireLimit, routerHost);
+      var providerRAS = new ProviderRAS (tid, logger, providerUtils);
+      var providerUpdate = new ProviderUpdate (tid, logger, providerUtils);
+      var providerCreate = new ProviderCreate (tid, logger, providerUtils);
+      var providerDelete = new ProviderDelete (tid, logger, providerUtils);
+
+      // RAS Endpoint
+      app.get(providerRAS.endPoint, providerRAS.ras);
+
+      // Endpoint for Update OR Create a Trigger
+      app.put(providerUpdate.endPoint, providerUtils.authorize, providerUpdate.update);
+
+      // Endpoint for Creating a new Trigger
+      app.post(providerCreate.endPoint, providerUtils.authorize, providerCreate.create);
+
+      // Endpoint for Deleting an existing Trigger.
+      app.delete(providerDelete.endPoint, providerUtils.authorize, providerDelete.delete);
+
+      providerUtils.initAllTriggers();
+    }, function(err) {
+      logger.info(tid, 'init', 'found an error creating database: ', err);
+    })
+    ///
+
+}
+
+init(server);
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..cb83ee0
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,2 @@
+ext.dockerImageName = 'catalog_cloudanttrigger'
+apply from: '../../../gradle/docker.gradle'
diff --git a/lib/constants.js b/lib/constants.js
new file mode 100644
index 0000000..2ca1292
--- /dev/null
+++ b/lib/constants.js
@@ -0,0 +1,5 @@
+const TRIGGER_DB_SUFFIX = 'cloudanttrigger';
+
+module.exports = {
+    TRIGGER_DB_SUFFIX: TRIGGER_DB_SUFFIX
+};
diff --git a/lib/create.js b/lib/create.js
new file mode 100644
index 0000000..99ee1bf
--- /dev/null
+++ b/lib/create.js
@@ -0,0 +1,46 @@
+module.exports = function(tid, logger, utils) {
+
+  // Test Endpoint
+  this.endPoint = '/cloudanttriggers/:id';
+
+  // Create Logic
+  this.create = function (req, res) {
+
+    var method = 'PUT /cloudanttriggers';
+
+    logger.info(tid, method);
+    var args = typeof req.body === 'object' ? req.body : JSON.parse(req.body);
+    if(args.maxTriggers > utils.triggersLimit) {
+        // TODO: update error code to indicate that content provided is not correct
+        logger.warn(tid, method, 'maxTriggers > ' + utils.triggersLimit + ' is not allowed');
+        res.status(400).json({
+            error: 'maxTriggers > ' + utils.triggersLimit + ' is not allowed'
+        });
+        return;
+    } else if (!args.callback || !args.callback.action || !args.callback.action.name) {
+        // TODO: update error code to indicate that content provided is not correct
+        logger.warn(tid, method, 'Your callback is unknown for cloudant trigger:', args.callback);
+        res.status(400).json({
+            error: 'You callback is unknown for cloudant trigger.'
+        });
+        return;
+    }
+    var id = req.params.id;
+    var trigger = utils.initTrigger(args, id);
+    // 10 is number of retries to create a trigger.
+    var promise = utils.createTrigger(trigger, 10);
+    promise.then(function(newTrigger) {
+        logger.info(tid, method, "Trigger was added and database is confirmed.", newTrigger);
+        utils.addTriggerToDB(newTrigger, res);
+    }, function(err) {
+        logger.error(tid, method, "Trigger could not be created.", err);
+        utils.deleteTrigger(id);
+        res.status(400).json({
+            message: "Trigger could not be created.",
+            error: err
+        });
+    });
+
+  };
+
+}
diff --git a/lib/delete.js b/lib/delete.js
new file mode 100644
index 0000000..72bb592
--- /dev/null
+++ b/lib/delete.js
@@ -0,0 +1,15 @@
+module.exports = function(tid, logger, utils) {
+
+  // Test Endpoint
+  this.endPoint = '/cloudanttriggers/:id';
+
+  // Delete Logic
+  this.delete = function (req, res) {
+
+    var method = 'DELETE /cloudanttriggers';
+    logger.info(tid, method);
+    utils.deleteTriggerFromDB(req.params.id, res);
+
+  };
+
+}
diff --git a/lib/ras.js b/lib/ras.js
new file mode 100644
index 0000000..35bef02
--- /dev/null
+++ b/lib/ras.js
@@ -0,0 +1,11 @@
+module.exports = function(logger) {
+
+  // Test Endpoint
+  this.endPoint = '/ping';
+
+  // Test Logic
+  this.ras = function (req, res) {
+      res.send({msg: 'pong'});
+  };
+
+}
diff --git a/lib/update.js b/lib/update.js
new file mode 100644
index 0000000..8f87950
--- /dev/null
+++ b/lib/update.js
@@ -0,0 +1,46 @@
+module.exports = function(tid, logger, utils) {
+
+  // Test Endpoint
+  this.endPoint = '/cloudanttriggers/:id';
+
+  // Update Logic
+  this.update = function (req, res) {
+
+    var method = 'PUT /cloudanttriggers';
+
+    logger.info(tid, method);
+    var args = typeof req.body === 'object' ? req.body : JSON.parse(req.body);
+    if(args.maxTriggers > utils.triggersLimit) {
+        // TODO: update error code to indicate that content provided is not correct
+        logger.warn(tid, method, 'maxTriggers > ' + utils.triggersLimit + ' is not allowed');
+        res.status(400).json({
+            error: 'maxTriggers > ' + utils.triggersLimit + ' is not allowed'
+        });
+        return;
+    } else if (!args.callback || !args.callback.action || !args.callback.action.name) {
+        // TODO: update error code to indicate that content provided is not correct
+        logger.warn(tid, method, 'Your callback is unknown for cloudant trigger:', args.callback);
+        res.status(400).json({
+            error: 'You callback is unknown for cloudant trigger.'
+        });
+        return;
+    }
+    var id = req.params.id;
+    var trigger = utils.initTrigger(args, id);
+    // 10 is number of retries to create a trigger.
+    var promise = utils.createTrigger(trigger, 10);
+    promise.then(function(newTrigger) {
+        logger.info(tid, method, "Trigger was added and database is confirmed.", newTrigger);
+        utils.addTriggerToDB(newTrigger, res);
+    }, function(err) {
+        logger.error(tid, method, "Trigger could not be created.", err);
+        utils.deleteTrigger(id);
+        res.status(400).json({
+            message: "Trigger could not be created.",
+            error: err
+        });
+    });
+
+  };
+
+}
diff --git a/lib/utils.js b/lib/utils.js
new file mode 100644
index 0000000..e10daf0
--- /dev/null
+++ b/lib/utils.js
@@ -0,0 +1,339 @@
+var _ = require('lodash');
+var request = require('request');
+var Agent = require('agentkeepalive');
+var when = require('when');
+
+module.exports = function(
+  tid,
+  logger,
+  app,
+  retriesBeforeDelete,
+  triggerDB,
+  dbProvider,
+  triggersFireLimit,
+  routerHost
+) {
+
+    this.tid = tid;
+    this.logger = logger;
+    this.app = app;
+    this.retriesBeforeDelete = retriesBeforeDelete;
+    this.triggerDB = triggerDB;
+    this.dbProvider = dbProvider;
+    this.triggersLimit = triggersFireLimit;
+    this.routerHost = routerHost;
+
+    // Log HTTP Requests
+    app.use(function(req, res, next) {
+        if (req.url.indexOf('/cloudanttriggers') === 0)
+            logger.info(tid, 'HttpRequest',req.method, req.url);
+        next();
+    });
+
+    this.module = 'utils';
+    this.triggers = {};
+
+    var that = this;
+
+    // Add a trigger: listen for changes and dispatch.
+    this.createTrigger = function(dataTrigger, retryCount) {
+
+      var method = 'createTrigger';
+
+      // Cleanup connection when trigger is deleted.
+      var sinceToUse = dataTrigger.since ? dataTrigger.since : "now";
+      var nanoConnection;
+      var dbURL;
+      
+      if (dataTrigger.accounturl.indexOf('cloudant.com') !== -1) {
+        // contruct cloudant URL
+        dbURL = 'https://' + dataTrigger.user + ':' + dataTrigger.pass + '@' + dataTrigger.user + '.cloudant.com';
+      } else {
+        // construct couchDB URL
+        dbURL = dataTrigger.accounturl;
+        if (dataTrigger.protocol) {
+          dbURL = dataTrigger.protocol + '://' + dataTrigger.host;
+        } 
+      }
+      
+      // add port if specified
+      if (dataTrigger.port) {
+        dbURL = dbURL + ':' + dataTrigger.port
+      }
+          
+      logger.info(tid, 'found trigger accounturl: ', dbURL);
+      nanoConnection = require('nano')(dbURL);
+      
+      return new Promise(function(resolve, reject) {
+
+        nanoConnection.auth(dataTrigger.user, dataTrigger.pass, function (err, body, headers) {
+
+          if (err) {
+            logger.info(tid, method,'Error happened during the db connection process: ');
+            logger.info(tid, method,'dbURL: ' + dbURL);
+            logger.info(tid, method,'dataTrigger.user: ' + dataTrigger.user);
+            logger.info(tid, method,'dataTrigger.pass: ' + dataTrigger.pass);
+            reject(err);
+          } else {
+            nanoConnection = require('nano')({url: dbURL, cookie: headers['set-cookie']});
+            var triggeredDB = nanoConnection.use(dataTrigger.dbname);
+            // Listen for changes on this database.
+            var feed = triggeredDB.follow({since: sinceToUse, include_docs: dataTrigger.includeDoc});
+
+            dataTrigger.feed = feed;
+            that.triggers[dataTrigger.id] = dataTrigger;
+
+            feed.on('change', function (change) {
+                var triggerHandle = that.triggers[dataTrigger.id];
+                logger.info(tid, method, 'Got change from', dataTrigger.dbname, change);
+                if(triggerHandle && triggerHandle.triggersLeft > 0 && triggerHandle.retriesLeft > 0) {
+                    try {
+                        that.invokeWhiskAction(dataTrigger.id, change);
+                    } catch (e) {
+                        logger.error(tid, method, 'Exception occurred in callback', e);
+                    }
+                }
+            });
+
+            feed.follow();
+
+            resolve(feed);
+          }
+
+        });
+
+      }).then(function(feed) {
+
+        return new Promise(function(resolve, reject) {
+
+          feed.on('error', function (err) {
+              logger.error(tid, method,'Error occurred for trigger', dataTrigger.id, '(db ' + dataTrigger.dbname + '):', err);
+              // revive the feed if an error ocurred for now
+              // the user should be in charge of removing the feeds
+              logger.info(tid, "attempting to recreate trigger", dataTrigger.id);
+              that.deleteTrigger(dataTrigger.id);
+              dataTrigger.since = "now";
+              if (retryCount > 0) {
+                  var addTriggerPromise = that.createTrigger(dataTrigger, (retryCount - 1));
+                  addTriggerPromise.then(function(trigger) {
+                      logger.error(tid, method, "Retry Count:", (retryCount - 1));
+                      resolve(trigger);
+                  }, function(err) {
+                      reject(err);
+                  });
+              } else {
+                  logger.error(tid, method, "Trigger's feed produced too many errors. Deleting the trigger", dataTrigger.id, '(db ' + dataTrigger.dbname + ')');
+                  reject({
+                      error: err,
+                      message: "Trigger's feed produced too many errors. Deleting the trigger " + dataTrigger.id
+                  });
+              }
+          });
+
+          feed.on('confirm', function (dbObj) {
+              logger.info(tid, method, 'Added cloudant data trigger', dataTrigger.id, 'listening for changes in database', dataTrigger.dbname);
+              resolve(dataTrigger);
+          });
+
+        });
+      }, function (err) {
+        logger.info('caught an exception: ' + err);
+        return Promise.reject(err);
+      }).catch(function (err) {
+          logger.info('caught an exception: ' + err);
+          return Promise.reject(err);
+      });
+
+    };
+
+    this.initTrigger = function (obj, id) {
+
+        logger.info(tid, 'initTrigger', obj);
+        var includeDoc = ((obj.includeDoc == true || obj.includeDoc.toString().trim().toLowerCase() == 'true')) || "false";
+        var trigger = {
+            id: id,
+            accounturl: obj.accounturl,
+            dbname: obj.dbname,
+            user: obj.user,
+            pass: obj.pass,
+            host: obj.host,
+            port: obj.port,
+            protocol: obj.protocol,
+            includeDoc: includeDoc,
+            apikey: obj.apikey,
+            since: obj.since,
+            callback: obj.callback,
+            maxTriggers: obj.maxTriggers,
+            triggersLeft: obj.maxTriggers,
+            retriesLeft: retriesBeforeDelete
+        };
+
+        return trigger;
+
+    };
+
+    this.initAllTriggers = function () {
+
+        var method = 'initAllTriggers';
+        logger.info(tid, that.module, method, 'Initializing all cloudant triggers from database.');
+
+        triggerDB.list({include_docs: true}, function(err, body) {
+            if(!err) {
+                body.rows.forEach(function(trigger) {
+                    var cloudantTrigger = that.initTrigger(trigger.doc, trigger.doc.id);
+                    // check here for triggers left if none left end here, and dont create
+                    if (cloudantTrigger.triggersLeft > 0) {
+                      that.createTrigger(cloudantTrigger, 10);
+                    } else {
+                      logger.info(tid, method, 'found a trigger with no triggers left to fire off.');
+                    }
+                });
+            } else {
+                logger.error(tid, method, 'could not get latest state from database');
+            }
+        });
+
+    };
+
+    // Delete a trigger: stop listening for changes and remove it.
+    this.deleteTrigger = function (id) {
+        var method = 'deleteTrigger';
+        var trigger = that.triggers[id];
+        if (trigger) {
+            logger.info(tid, method, 'Stopped cloudant trigger',
+                id, 'listening for changes in database', trigger.dbname);
+            trigger.feed.stop();
+            delete that.triggers[id];
+        } else {
+            logger.info(tid, method, 'trigger', id, 'could not be found in the trigger list.');
+            return false;
+        }
+    };
+
+    this.addTriggerToDB = function (trigger, res) {
+
+        var method = 'addTriggerToDB';
+        triggerDB.insert(_.omit(trigger, 'feed'), trigger.id, function(err, body) {
+            if(!err) {
+                logger.info(tid, method, 'trigger', trigger.id, 'was inserted into db.');
+                res.status(200).json(_.omit(trigger, 'feed'));
+            } else {
+                that.deleteTrigger(trigger.id);
+                res.status(err.statusCode).json({error: 'Cloudant trigger cannot be created.'});
+            }
+        });
+
+    };
+
+    this.deleteTriggerFromDB = function (id, res) {
+
+        var method = 'deleteTriggerFromDB';
+
+        triggerDB.get(id, function(err, body) {
+            if(!err) {
+                triggerDB.destroy(body._id, body._rev, function(err, body) {
+                    if(err) {
+                        logger.error(tid, method, 'there was an error while deleting', id, 'from database');
+                        if (res) {
+                            res.status(err.statusCode).json({ error: 'Cloudant data trigger ' + id  + 'cannot be deleted.' } );
+                        }
+                    } else {
+                        that.deleteTrigger(id);
+                        logger.info(tid, method, 'cloudant trigger', id, ' is successfully deleted');
+                        if (res) {
+                            res.send('Deleted cloudant trigger ' + id);
+                        }
+                    }
+                });
+            } else {
+                if (err.statusCode === 404) {
+                    logger.info(tid, method, 'there was no trigger with id', id, 'in database.', err.error);
+                    if (res) {
+                        res.status(200).json({ message: 'there was no trigger with id ' + id + ' in database.' } );
+                        res.end();
+                    }
+                } else {
+                    logger.error(tid, method, 'there was an error while getting', id, 'from database', err);
+                    if (res) {
+                        res.status(err.statusCode).json({ error: 'Cloudant data trigger ' + id  + ' cannot be deleted.' } );
+                    }
+                }
+            }
+        });
+
+    };
+
+    this.invokeWhiskAction = function (id, change) {
+        var method = 'invokeWhiskAction';
+
+        var dataTrigger = that.triggers[id];
+        var apikey = dataTrigger.apikey;
+        var triggerName = dataTrigger.callback.action.name;
+        var triggerObj = that.parseQName(triggerName);
+        logger.info(tid, method, 'invokeWhiskAction: change =', change);
+        var form = change.hasOwnProperty('doc') ? change.doc : change;
+        logger.info(tid, method, 'invokeWhiskAction: form =', form);
+        logger.info(tid, method, 'for trigger', id, 'invoking action', triggerName, 'with db update', JSON.stringify(form));
+
+        var host = 'https://'+routerHost+':'+443;
+        var uri = host+'/api/v1/namespaces/' + triggerObj.namespace +'/triggers/'+triggerObj.name;
+        var auth = apikey.split(':');
+        logger.info(tid, method, uri, auth, form);
+
+        dataTrigger.triggersLeft--;
+
+        request({
+            method: 'post',
+            uri: uri,
+            auth: {
+                user: auth[0],
+                pass: auth[1]
+            },
+            json: form
+        }, function(error, response, body) {
+            if(dataTrigger) {
+                logger.info(tid, method, 'done http request, STATUS', response ? response.statusCode : response);
+                logger.info(tid, method, 'done http request, body', body);
+                if(error || response.statusCode >= 400) {
+                    dataTrigger.retriesLeft--;
+                    dataTrigger.triggersLeft++; // setting the counter back to where it used to be
+                    logger.error(tid, method, 'there was an error invoking', id, response ? response.statusCode : response, error, body);
+                } else {
+                    dataTrigger.retriesLeft = that.retriesBeforeDelete; // reset retry counter
+                    logger.info(tid, method, 'fired', id, 'with body', body, dataTrigger.triggersLeft, 'triggers left');
+                }
+
+                if(dataTrigger.triggersLeft === 0 || dataTrigger.retriesLeft === 0) {
+                    if(dataTrigger.triggersLeft === 0)
+                        logger.info(tid, 'onTick', 'no more triggers left, deleting');
+                    if(dataTrigger.retriesLeft === 0)
+                        logger.info(tid, 'onTick', 'too many retries, deleting');
+
+                    that.deleteTriggerFromDB(dataTrigger.id);
+                }
+            } else {
+                logger.info(tid, method, 'trigger', id, 'was deleted between invocations');
+            }
+        });
+    };
+
+    this.parseQName = function (qname) {
+        var parsed = {};
+        var delimiter = '/';
+        var defaultNamespace = '_';
+        if (qname && qname.charAt(0) === delimiter) {
+            var parts = qname.split(delimiter);
+            parsed.namespace = parts[1];
+            parsed.name = parts.length > 2 ? parts.slice(2).join(delimiter) : '';
+        } else {
+            parsed.namespace = defaultNamespace;
+            parsed.name = qname;
+        }
+        return parsed;
+    };
+
+    this.authorize = function(req, res, next) {
+        next();
+    }
+
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..afa0a94
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "cloudantTrigger",
+  "version": "0.0.1",
+  "private": true,
+  "scripts": {
+    "start": "node app.js"
+  },
+  "dependencies": {
+    "body-parser": "^1.12.0",
+    "cradle": "^0.6.7",
+    "agentkeepalive": "^2.2.0",
+    "express": "^4.12.2",
+    "winston": "^2.1.1",
+    "moment": "^2.11.1",
+    "lodash": "^3.10.1",
+    "request": "2.69.0",
+    "when": "^3.7.4",
+    "nano": "^6.2.0"
+  }
+}
\ No newline at end of file
diff --git a/scripts/addNewDocument.sh b/scripts/addNewDocument.sh
new file mode 100755
index 0000000..4ae9f4e
--- /dev/null
+++ b/scripts/addNewDocument.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# Add a new document to the test Cloudant database.
+
+DBUSER="cusinatest"
+DBPASS="hal4you!"
+DBNAME="foo"
+
+curl -v -X POST -H 'Content-Type: application/json' \
+	-u "$DBUSER:$DBPASS" \
+        -d "{
+	  \"message\": \"This is a test document.\",
+	  \"date\": \"$(date)\"
+        }" \
+        "https://$DBUSER.cloudant.com/$DBNAME/"
+
+
diff --git a/scripts/addTestTriggers.sh b/scripts/addTestTriggers.sh
new file mode 100755
index 0000000..2af2b1c
--- /dev/null
+++ b/scripts/addTestTriggers.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# Register test cloudant triggers.
+
+# The service location (can be specified as command line parameter.)
+SERVICEURL="http://${CLOUDANTTRIGGER_HOST:-"localhost"}:4001" # default
+[ $# -eq 1 ] && SERVICEURL="$1" # override in command line arg
+
+# Delete all registry entries.
+#curl -X DELETE "$SERVICEURL/cloudanttriggers"; echo
+
+curl -X PUT -H 'Content-Type: application/json' \
+        -d '{
+          "accounturl": "https://cusinatest.cloudant.com",
+          "dbname": "foo",
+          "user": "cusinatest",
+          "pass": "hal4you!",
+	      "callback": { "action": {"name":"HELLOCLOUDANT"} }
+        }' \
+        "$SERVICEURL/cloudanttriggers/foo_db"; echo