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