Add Cloudant read and write actions (#62)

diff --git a/actions/database-actions/write-document.js b/actions/database-actions/write-document.js
new file mode 100755
index 0000000..b96b6af
--- /dev/null
+++ b/actions/database-actions/write-document.js
@@ -0,0 +1,122 @@
+function main(message) {
+
+    var cloudantOrError = getCloudantAccount(message);
+
+    if (typeof cloudantOrError !== 'object') {
+        return Promise.reject(cloudantOrError);
+    }
+
+    var cloudant = cloudantOrError;
+    var dbName = message.dbname;
+    var doc = message.doc;
+    var overwrite;
+
+    if (!dbName) {
+        return Promise.reject('dbname is required.');
+    }
+    if (!doc) {
+        return Promise.reject('doc is required.');
+    }
+
+    if (typeof message.doc === 'object') {
+        doc = message.doc;
+    } else if (typeof message.doc === 'string') {
+        try {
+            doc = JSON.parse(message.doc);
+        } catch (e) {
+            return Promise.reject('doc field cannot be parsed. Ensure it is valid JSON.');
+        }
+    } else {
+        return Promise.reject('doc field is ' + (typeof doc) + ' and should be an object or a JSON string.');
+    }
+
+
+    if (typeof message.overwrite === 'boolean') {
+        overwrite = message.overwrite;
+    } else if (typeof message.overwrite === 'string') {
+        overwrite = message.overwrite.trim().toLowerCase() === 'true';
+    } else {
+        overwrite = false;
+    }
+
+    var cloudantDb = cloudant.use(dbName);
+    return insertOrUpdate(cloudantDb, overwrite, doc);
+}
+
+/**
+ * If id defined and overwrite is true, checks if doc exists to retrieve version
+ * before insert. Else inserts.
+ */
+function insertOrUpdate(cloudantDb, overwrite, doc) {
+    if (doc._id) {
+        if (overwrite) {
+            return new Promise(function(resolve, reject) {
+                cloudantDb.get(doc._id, function(error, body) {
+                    if (!error) {
+                        doc._rev = body._rev;
+                        insert(cloudantDb, doc)
+                            .then(function (response) {
+                                resolve(response);
+                            })
+                            .catch(function (err) {
+                               reject(err);
+                            });
+                    } else {
+                        console.error('error', error);
+                        reject(error);
+                    }
+                });
+            });
+        } else {
+            // May fail due to conflict.
+            return insert(cloudantDb, doc);
+        }
+    } else {
+        // There is no option of overwrite because id is not defined.
+        return insert(cloudantDb, doc);
+    }
+}
+
+/**
+ * Inserts updated document into database.
+ */
+function insert(cloudantDb, doc) {
+    return new Promise(function(resolve, reject) {
+        cloudantDb.insert(doc, function(error, response) {
+            if (!error) {
+                console.log('success', response);
+                resolve(response);
+            } else {
+                console.log('error', error);
+                reject(error);
+            }
+        });
+    });
+}
+
+function getCloudantAccount(message) {
+    // full cloudant URL - Cloudant NPM package has issues creating valid URLs
+    // when the username contains dashes (common in Bluemix scenarios)
+    var cloudantUrl;
+
+    if (message.url) {
+        // use bluemix binding
+        cloudantUrl = message.url;
+    } else {
+        if (!message.host) {
+            return 'cloudant account host is required.';
+        }
+        if (!message.username) {
+            return 'cloudant account username is required.';
+        }
+        if (!message.password) {
+            return 'cloudant account password is required.';
+        }
+
+        cloudantUrl = "https://" + message.username + ":" + message.password + "@" + message.host;
+    }
+
+    return require('cloudant')({
+        url: cloudantUrl
+    });
+}
diff --git a/installCatalog.sh b/installCatalog.sh
index 19bb768..e895f36 100755
--- a/installCatalog.sh
+++ b/installCatalog.sh
@@ -93,12 +93,24 @@
     -a description 'Create document in database' \
     -a parameters '[ {"name":"dbname", "required":true}, {"name":"doc", "required":true, "description": "The JSON document to insert"}, {"name":"params", "required":false} ]' \
 
+$WSK_CLI -i --apihost "$APIHOST" action update --auth "$AUTH" cloudant/read \
+    "$PACKAGE_HOME/actions/database-actions/read-document.js" \
+    -a description 'Read document from database' \
+    -a parameters '[ {"name":"dbname", "required":true}, {"name":"id", "required":true, "description": "The Cloudant document id to fetch"}, {"name":"params", "required":false}]' \
+    -p id ''
+
 $WSK_CLI -i --apihost "$APIHOST" action update --auth "$AUTH" cloudant/read-document \
     "$PACKAGE_HOME/actions/database-actions/read-document.js" \
     -a description 'Read document from database' \
     -a parameters '[ {"name":"dbname", "required":true}, {"name":"docid", "required":true, "description": "The Cloudant document id to fetch"}, {"name":"params", "required":false}]' \
     -p docid ''
 
+$WSK_CLI -i --apihost "$APIHOST" action update --auth "$AUTH" cloudant/write \
+    "$PACKAGE_HOME/actions/database-actions/write-document.js" \
+    -a description 'Write document in database' \
+    -a parameters '[ {"name":"dbname", "required":true}, {"name":"doc", "required":true} ]' \
+    -p doc '{}'
+
 $WSK_CLI -i --apihost "$APIHOST" action update --auth "$AUTH" cloudant/update-document \
     "$PACKAGE_HOME/actions/database-actions/update-document.js" \
     -a description 'Update document in database' \
diff --git a/tests/src/catalog/cloudant/CloudantDatabaseActionsTests.scala b/tests/src/catalog/cloudant/CloudantDatabaseActionsTests.scala
index a248350..69b3875 100644
--- a/tests/src/catalog/cloudant/CloudantDatabaseActionsTests.scala
+++ b/tests/src/catalog/cloudant/CloudantDatabaseActionsTests.scala
@@ -124,6 +124,49 @@
             }
     }
 
+    it should """read cloudant document with read action""" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            implicit val wskprops = wp
+            val packageName = "dummyCloudantPackage"
+
+            try {
+                CloudantUtil.setUp(credential)
+
+                val packageGetResult = wsk.pkg.get("/whisk.system/cloudant")
+                println("Fetching cloudant package.")
+                packageGetResult.stdout should include("ok")
+
+                println("Creating cloudant package binding.")
+                assetHelper.withCleaner(wsk.pkg, packageName) {
+                    (pkg, name) =>
+                        pkg.bind("/whisk.system/cloudant", name,
+                            Map("username" -> credential.user.toJson,
+                                "password" -> credential.password.toJson,
+                                "host" -> credential.host().toJson,
+                                "dbname" -> credential.dbname.toJson))
+                }
+                //Create test doc
+                val doc = CloudantUtil.createDocParameterForWhisk.get("doc").getAsString
+                val response = CloudantUtil.createDocument(credential, doc)
+                response.get("ok").getAsString shouldBe "true"
+
+                println("Invoking the read action.")
+                withActivation(wsk.activation, wsk.action.invoke(s"${packageName}/read",
+                    Map(
+                        "docid" -> response.get("id").getAsString.toJson,
+                        "params" -> JsObject("revs_info" -> JsBoolean(true))))) {
+                    activation =>
+                        activation.response.success shouldBe true
+                        activation.response.result.get.fields.get("date") shouldBe defined
+                        activation.response.result.get.fields.get("_rev") shouldBe defined
+                        activation.response.result.get.fields.get("_revs_info") shouldBe defined
+                }
+            }
+            finally {
+                CloudantUtil.unsetUp(credential)
+            }
+    }
+
     it should """read cloudant document with undefined docid""" in withAssetCleaner(wskprops) {
         (wp, assetHelper) =>
             implicit val wskprops = wp
@@ -160,6 +203,91 @@
             }
     }
 
+    it should """write cloudant document""" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            implicit val wskprops = wp
+            val packageName = "dummyCloudantPackage"
+
+            try {
+                CloudantUtil.setUp(credential)
+
+                val packageGetResult = wsk.pkg.get("/whisk.system/cloudant")
+                println("Fetching cloudant package.")
+                packageGetResult.stdout should include("ok")
+
+                println("Creating cloudant package binding.")
+                assetHelper.withCleaner(wsk.pkg, packageName) {
+                    (pkg, name) =>
+                        pkg.bind("/whisk.system/cloudant", name,
+                            Map("username" -> credential.user.toJson,
+                                "password" -> credential.password.toJson,
+                                "host" -> credential.host().toJson,
+                                "dbname" -> credential.dbname.toJson))
+                }
+
+                val doc = CloudantUtil.createDocParameterForWhisk().get("doc").getAsString
+                val docJSObj = doc.parseJson.asJsObject
+
+                println("Invoking the write action.")
+                withActivation(wsk.activation, wsk.action.invoke(s"${packageName}/write",
+                    Map("doc" -> docJSObj))) {
+                    activation =>
+                        activation.response.success shouldBe true
+                        activation.response.result.get.fields.get("id") shouldBe defined
+                }
+                val getResponse = CloudantUtil.getDocument(credential, "testId")
+                Some(JsString(getResponse.get("date").getAsString)) shouldBe docJSObj.fields.get("date")
+            }
+            finally {
+                CloudantUtil.unsetUp(credential)
+            }
+    }
+
+    it should """write cloudant document with overwrite""" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            implicit val wskprops = wp
+            val packageName = "dummyCloudantPackage"
+
+            try {
+                CloudantUtil.setUp(credential)
+
+                val packageGetResult = wsk.pkg.get("/whisk.system/cloudant")
+                println("Fetching cloudant package.")
+                packageGetResult.stdout should include("ok")
+
+                println("Creating cloudant package binding.")
+                assetHelper.withCleaner(wsk.pkg, packageName) {
+                    (pkg, name) =>
+                        pkg.bind("/whisk.system/cloudant", name,
+                            Map("username" -> credential.user.toJson,
+                                "password" -> credential.password.toJson,
+                                "host" -> credential.host().toJson,
+                                "dbname" -> credential.dbname.toJson))
+                }
+
+                //Create test doc
+                val doc = CloudantUtil.createDocParameterForWhisk.get("doc").getAsString
+                val response = CloudantUtil.createDocument(credential, doc)
+                response.get("ok").getAsString shouldBe "true"
+
+                val docJSObj = doc.parseJson.asJsObject
+
+                println("Invoking the write action.")
+                withActivation(wsk.activation, wsk.action.invoke(s"${packageName}/write",
+                    Map("doc" -> docJSObj,
+                        "overwrite" -> "true".toJson))) {
+                    activation =>
+                        activation.response.success shouldBe true
+                        activation.response.result.get.fields.get("id") shouldBe defined
+                }
+                val getResponse = CloudantUtil.getDocument(credential, "testId")
+                Some(JsString(getResponse.get("date").getAsString)) shouldBe docJSObj.fields.get("date")
+            }
+            finally {
+                CloudantUtil.unsetUp(credential)
+            }
+    }
+
     it should """update cloudant document""" in withAssetCleaner(wskprops) {
         (wp, assetHelper) =>
             implicit val wskprops = wp