Skip to content

Instantly share code, notes, and snippets.

@lbrenman
Last active August 29, 2015 14:24
Show Gist options
  • Save lbrenman/fda1620c08534e158324 to your computer and use it in GitHub Desktop.
Save lbrenman/fda1620c08534e158324 to your computer and use it in GitHub Desktop.
Financial Watch List in Arrow

#Stock Watch List using Appcelerator Arrow

See how Appcelerator Arrow can be used to create a mobile Stock Watch List.

The Appcelerator mBaaS is a powerful, scalable middleware platform for accelerating and optimizing mobile applications. In a prior post I described how to leverage the Appcelerator mBaaS, Node.ACS and ACS to create a Stock Watch List that could be utilized in a brokerage application. By storing the Watch List in the mBaaS, you are able to keep the Watch List synchronized among all your mobile devices. Furthermore, the mobile application is more performant since it does not need to make repeated calls to the stock quote service for each stock symbol in the Watch List; instead it can make a call to one API and the mBaaS does the rest. Refer to the blog post for more background on this.

This blog post will describe how to implement this mBaaS based Watch List using Arrow. It will also demonstrate field validation as well as describe methods of restricting what APIs can be executed.

An example of a Watch List is shown below:

##Creating the Watch List

We are going to use ArrowDB to store the Watch List for the users. Calls to the Watch List API will support the following operations:

  1. Create a Watch List entry (i.e. a stock symbol)
  2. Retreive a user's Watch List (i.e. list of symbols and current value/change)
  3. Delete a Watch List entry

My Watch List model is shown below:

var Arrow = require("arrow");

var Model = Arrow.createModel("WatchList",{
	"fields": {
		"username": {
			"type": "String",
			"required": true,
		},
		"symbol": {
			"type": "String",
			"required": true,
		},
		"LastPrice": {
			"type": "String",
			"custom": true
		},
		"Change": {
			"type": "String",
			"custom": true
		}
	},
	"connector": "appc.arrowdb",

  ...


module.exports = Model;

The symbol field stores the stock symbol. The username field will be used to filter the Watch List for each user. The LastPrice and Change fields are custom and will be retreived in a post block using the MarkitOndemand Stock Quote REST API.

##Restricting the APIs

The APIs that will be created off of this model can be controlled using the model actions property. The list of actions are:

  1. create
  2. read
  3. update
  4. delete
  5. deleteAll

In my Watch List model, I would like to prevent the deleteAll and update since I don't want one user to delete another users Watch List items and there is little need to update a Watch List item, since it only stores a symbol.

Therefore, in my model above, I add the following:

"actions": [
  "create",
  "read",
  "delete"
]

This is nice but not sufficient as I have further restrictions I would like to place on my Watch List APIs. Namely, I would like to restrict GETs to "GET by ID" and "GET by query". Recall that in order to store all users Watch List items in the same database, I added a username. Each Watch List entry will contain the symbol and the user that it belongs to. So, for a user to fetch their Watch List, the would use a Query as follows:

curl -u l51s0f0FKIAc4m7on5lv9vSk5Vy8vHTt: "http://127.0.0.1:8080/api/watchlist/query?where=%7B%22username%22:%22lbrenman%22%7D"

The where query above is where={"username":"lbrenman"}.

This can also be achieved using ACL (Access Control List) but for this demo, I am keeping it simple and generic. The application developer will be responsible for managing the unique username for the Watch List.

In order to prevent the plain GET (e.g. findAll) from being generated, I will use the before$METHOD$ to call a pre-block on beforeFindAll by adding the following to my model:

"beforeFindAll": "unsupportedAPI"

The code for unsupportedAPI pre-block is show below:

var Arrow = require('arrow');

var PreBlock = Arrow.Block.extend({
	name: 'unsupportedAPI',
	description: 'will block API from being called',

	action: function (req, resp, next) {
		resp.response.status(500);
		resp.send({"success": false,"error":"unsupported API"});
		next(false);
	}
});

module.exports = PreBlock;

The response to the following GET:

curl -u l51s0f0FKIAc4m7on5lv9vSk5Vy8vHTt: "http://127.0.0.1:8080/api/watchlist"

is shown below:

{
    "success": false,
    "error": "unsupported API"
}

Note that restricting APIs can also be achieved as described in the online docs by disabling autogen and then manually implementing the APIs you would like to expose.

##Adding the Watch List Stock Quote Data

In a prior Arrow blog post, i described how to implement a post-block to add GPS data to Accounts data returned from Salesforce. I will use this exact same idea to add the LastPrice and Change values from the MarkitOnDemand stockQuote REST API to my Watch List data.

My post-block, addstockquote_ is shown below:

var Arrow = require('arrow');
var request = require('request');

var PostBlock = Arrow.Block.extend({
	name: 'addstockquote_',
	description: 'add stock quote info for each symbol in watch list',

	action: function (req, resp, next) {
		if(req.method==="GET") {
			var body = JSON.parse(resp.body);
			var data = body[body.key];
			var dataLen = data.length;
			var replies = 0;

			if(dataLen === 0) {
				next();
				return;
			}

			if(dataLen){
				data.forEach(function (_row, _index) {
					var url = "http://dev.markitondemand.com/Api/Quote/json?symbol="+_row.symbol;
					request(url, function (error, response, body) {
						if (!error && response.statusCode == 200) {
							var res = JSON.parse(body);
							if(res.Data) {
								_row.LastPrice = res.Data.LastPrice;
								_row.Change = res.Data.Change;
							} else {
								_row.LastPrice = "---";
								_row.Change = "---";
							}
							replies++;
							if(replies == dataLen) {
								setReply();
							}
						} else {
							if(replies == dataLen) {
								setReply();
							}
						}
					});
				});
			} else {

				var url = "http://dev.markitondemand.com/Api/Quote/json?symbol="+data.symbol;

				request(url, function (error, response, body) {
					if (!error && response.statusCode == 200) {
						var res = JSON.parse(body);
						if(res.Data) {
								data.LastPrice = res.Data.Change;
						} else {
								data.LastPrice = "---";
						}
					} else {
						console.log("markitondemand request error");
					}
					setReply();
				});
			}
		} else {
			next();
		}
		function setReply() {
			resp.success(body[body.key], next);
		}
	}
});

module.exports = PostBlock;

The block above simply loops through the Watch List data and calls the following web service:

http://dev.markitondemand.com/Api/Quote/json?symbol=

and populates the Watch List LastPrice and Change custom fields with the reply from the Quote web service call.

This post block should be called after a Query and after a findOne, so I add the follwoing to my model:

"afterQuery": "addstockquote_",
"afterFindOne": "addstockquote_",

A response to the Watch List GET Query API is shown below:

{
    "success": true,
    "request-id": "58bba659-2851-4024-9b32-59bf80cc8c65",
    "key": "watchlists",
    "watchlists": [
        {
            "id": "559aa1127ed7bbfaaf1c166c",
            "username": "lbrenman",
            "symbol": "INTC",
            "LastPrice": 30.16,
            "Change": -0.395
        },
        {
            "id": "5595d78c7ed7bbfaaf0c749f",
            "username": "lbrenman",
            "symbol": "AAPL",
            "LastPrice": 125.76,
            "Change": -0.679999999999993
        },
        {
            "id": "5595d6f9e0140446250cc002",
            "username": "lbrenman",
            "symbol": "TXN",
            "LastPrice": 51.77,
            "Change": -0.199999999999996
        }
    ]
}

##Validating API Requests

In addition to restricting the APIs, I would like to perform the following validations:

  1. validate that on create, the symbol and username fields are not empty
  2. validate that on create a duplicate symbol is not being added to the user's Watch List
  3. validate that a GET is a where query

This will be accomplished using a combination of a model field validator property and pre-blocks.

To make sure that the symbol and username are not empty on create, I modified the Watch List model's fields and added validators as follows:

...
"username": {
  "type": "String",
  "required": true,
  "validator": function(val) {
    if(val == "") {
      return 'username must not be empty'
    }
  }
},
"symbol": {
  "type": "String",
  "required": true,
  "validator": function(val) {
    if(val == "") {
      return 'symbol must not be empty'
    }
  }
},
...

Note that since the fields above also have required property set to true, Arrow will make sure that they are provided and the validator will make sure that the fields are not emtpty.

To make sure that the symbol provided is not a duplicate, the following validatewatchlistcreate pre-block is used:

var Arrow = require('arrow');

var PreBlock = Arrow.Block.extend({
	name: 'validatewatchlistcreate',
	description: 'validate watch list create',

	action: function (req, resp, next) {
		var model = Arrow.getModel('WatchList');
		model.query({where:{symbol:req.params.symbol, username:req.params.username}}, function(err, coll) {
			if(err) {
					resp.response.status(500);
					resp.send({"success": false,"error":"ArrowDB error"});
					next(false);
			} else {
				if(coll.length>0) {
						resp.response.status(500);
						resp.send({"success": false,"error":"duplicate symbol"});
					next(false);
				} else {
					next();
				}
			}
		});
	}
});

module.exports = PreBlock;

and the following is added to the Watch List model:

"beforeCreate": "validatewatchlistcreate",

The validatewatchlistcreate pre-block above performs a query on the Watch List data to see if there is an entry for the particular user for the symbol provided. If so, then this is a duplicate symbol and the API returns an error. Otherwise, the API proceeds and the new Watch List entry is created.

Finally, to make sure that the GET is a where query of the form where={"username":""}, the validatewatchlistquery pre-block is used:

var Arrow = require('arrow');

var PreBlock = Arrow.Block.extend({
	name: 'validatewatchlistquery',
	description: 'validate watch list query',

	action: function (req, resp, next) {
		if(req.params.where && req.params.where != "" && req.params.where != null) {
			next();
		} else {
			resp.response.status(500);
			resp.send({"success": false,"error":"incorrect query, use where={'username':'abc'}"});
			next(false);
		}
	}
});

module.exports = PreBlock;

and the following is added to the Watch List model:

"beforeQuery": "validatewatchlistquery",

##Summary

In this tutorial we saw how easy Arrow makes it to implement a robust, mobile-optimized Watch List API for a brokerage application, making the mobile application more performant and modern since the Watch List will follow the user from device to device since the symbols are stored in the mBaaS instead of on the mobile device. We also saw how Arrow makes it easy to perform validations and implement fine grained control over the APIs that are exposed.

The code for this sample project can be found here: https://gist.github.com/lbrenman/687053cc9bdc6b802e1d

Read more about Arrow here:

http://docs.appcelerator.com/platform/latest/#!/guide/Appcelerator_Arrow

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment