Skip to content

Latest commit

 

History

History
482 lines (373 loc) · 24 KB

readme.markdown

File metadata and controls

482 lines (373 loc) · 24 KB

node.js actionHero API Framework

Build Status

Who is the actionHero?

actionHero is a node.js API framework for both tcp sockets and http clients. The goals of actionHero are to create an easy-to-use toolkit to get started making combination http and socket APIs as quickly as possible. An actionHero can work as a stand-alone server or in concert with other actionHero servers in a cluster to handle larger load.

The actionHero API defines a single access point and accepts GET and POST input. You define "Actions" which handle input and response, such as "userAdd" or "geoLocate". The actionHero API is not "RESTful" (which is meaningless for persistent socket connections). This was chosen to make it as simple as possible for devices/users to access the actions, including low-level embedded devices which may have trouble with all the HTTP verbs.

Actions

The core of actionHero is the Action framework, actions are the basic units of a request and work for HTTP and socket responses. The goal of an action is to set the connection.response ( and connection.error when needed) value to build the response to the client

Here's an example of a simple action which will return a random number to the client:

var action = {};

/////////////////////////////////////////////////////////////////////
// metadata
action.name = "randomNumber";
action.description = "I am an API method which will generate a random number";
action.inputs = {
	"required" : [],
	"optional" : []
};
action.outputExample = {
	randomNumber: 123
}

/////////////////////////////////////////////////////////////////////
// functional
action.run = function(api, connection, next){
	connection.response.randomNumber = Math.random();
	next(connection, true);
};

/////////////////////////////////////////////////////////////////////
// exports
exports.action = action;

Notes:

  • Actions are asynchronous, and require in the API object, the connection object, and the callback function. Completing an action is as simple as calling next(connection, true). The second param in the callback is a boolean to let the framework know if it needs to render anything else to the client (default = true). There are some actions where you may have already sent the user output (see the file.js action for an example) where you would not want to render the default messages.
  • The metadata is used in reflexive and self-documenting actions in the API, such as actionsView. actions.inputs.required and actions.inputs.required are used for both documentation and for building the whitelist of allowed GET and POST variables the API will accept (in addition to your schema/models).

Connecting

HTTP

General

You can visit the API in a browser, Curl, etc. {url}?action or {url}/{action} is how you would access an action. For example, using the default ports in config.json you could reach the status action with both http://127.0.0.1/status or http://127.0.0.1/?action=status The only action which doesn't return the default JSON format would be file, as it should return files with the appropriate headers if they are found, and a 404 error if they are not.

HTTP responses follow the format:

{
	hello: "world"
	serverInformation: {
		serverName: "actionHero API",
		apiVerson: 1,
		requestDuration: 14
	},
	requestorInformation: {
		remoteAddress: "127.0.0.1",
		RequestsRemaining: 989,
		recievedParams: {
			action: "",
			limit: 100,
			offset: 0
		}
	},
	error: "OK"
}

HTTP Example:

> curl localhost:8080/api -v
* About to connect() to localhost port 8080 (#0)
*   Trying ::1... Connection refused
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api HTTP/1.1
> User-Agent: curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5
> Host: localhost:8080
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: application/json
< X-Powered-By: actionHero API
< Connection: keep-alive
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
* Closing connection #0
{"serverInformation":{"serverName":"actionHero API","apiVerson":"0.1.5","requestDuration":16},"requestorInformation":{"remoteAddress":"127.0.0.1","RequestsRemaining":905,"recievedParams":{"limit":100,"offset":0}},"error":"undefined is not a known action."}
  • you can provide the ?callback=myFunc param to initiate a JSON-p response which will wrap the returned JSON in your callback function.
  • unless otherwise provided, the api will set default values of limit and offset to help with paginating long lists of response objects (default: limit=100, offset=0).
  • the error if everything is OK will be "OK", otherwise, you should set a string error within your action
  • to build the response for "hello" above, the action would have set connection.response.hello = "world";

Files and Routes

actionHero can also serve up flat files. There is an action, file which is used to do this and a file server is part of the core framework (check out initFileserver for more information).

  • /file and /api are routes which expose the 'directories' of those types. These top level paths can be configured in config.json with api.configData.urlPathForActions and api.configData.urlPathForFiles.
  • the root of the web server "/" can be toggled to serve the content between /file or /api actions per your needs api.configData.rootEndpointType. The default is api.
  • actionHero will serve up flat files (html, images, etc) as well from your ./public folder. This is accomplished via a file action or via the 'file' route as described above. http://{baseUrl}/file/{pathToFile} is equivalent to http://{baseUrl}?action=file&fileName={pathToFile}.
  • Errors will result in a 404 (file not found).
  • proper mime-type headers will be set when possible.

Sockets

General

You can also access actionHero's methods via a persistent socket connection rather than http. The default port for this type of communication is 5000. There are a few special actions which set and keep parameters bound to your session (so they don't need to be re-posted). These special methods are:

  • quit. disconnect from the session
  • paramAdd - save a singe variable to your connection. IE: 'addParam screenName=evan'
  • paramView - returns the details of a single param. IE: 'viewParam screenName'
  • paramDelete - deletes a single param. IE: 'deleteParam screenName'
  • paramsView - returns a JSON object of all the params set to this connection
  • paramsDelete - deletes all params set to this session
  • roomChange - change the room you are connected to. By default all socket connections are in the api.configData.defaultSocketRoom room.
  • roomView - show you the room you are connected to, and information about the members currently in that room.
  • say [message]

Please note that any params set using the above method will be 'sticky' to the connection and sent for all subsequent requests. Be sure to delete or update your params!

All socket connections are also joined to a room. Rooms are used to broadcast messages from the system or other users. Rooms can be created on the fly and don't require any special setup. In this way. you can push messages to your users with a special function: api.socketRoomBroadcast(api, connection, message). connection can be null if you want the message to come from the server itself. The final special action socket connections have is say which will tell a message to all other users in the room, IE: say Hello World.

API Functions for helping with room communications are:

  • api.socketRoomBroadcast(api, connection, message): tell a message to all members in a room
  • api.socketRoomStatus(api, room): return the status object which contains information about a room and its members
  • api.sendSocketMessage(api, connection, message): send a message directly to a socket connection

Every socket action (including the special param methods above) will return a single line denoted by \r\n in JSON. If the Action was executed successfully, the response will be {"status":"OK"}.

To help sort out the potential stream of messages a socket user may receive, it is best to set a "context" as part of the JSON response. For example, by default all actions set a context of "response" indicating that the message being sent to the client is response to a request they sent. Messages sent by a user via the 'say' command have the context of user indicating they came form a user. Every minute a ping is sent from the server to keep the TCP connection alive and send the current time. This message has the context of api. Messages resulting from data sent to the api (like an action) will have the response context.

Socket Example:

> telnet localhost 5000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
{"welcome":"Hello! Welcome to the actionHero api","room":"defaultRoom","context":"api","messageCount":0}
randomNumber
{"context":"response","randomNumber":0.6138995781075209,"messageCount":1}
cacheTest
{"context":"response","error":"key is a required parameter for this action","messageCount":2}
paramAdd key=myKey
{"status":"OK","context":"response","messageCount":3}
paramAdd value=myValue
{"status":"OK","context":"response","messageCount":4}
paramsView
{"context":"response","params":{"action":"cacheTest","limit":100,"offset":0,"key":"myKey","value":"myValue"},"messageCount":5}
cacheTest
{"cacheTestResults":{"key":"myKey","value":"myValue","saveResp":"new record","loadResp":"myValue","deleteResp":true},"messageCount":6}
say hooray!
{"context":"response","status":"OK","messageCount":7}
{"context":"api","status":"keep-alive","serverTime":"2012-01-03T19:48:40.136Z","messageCount":8}

Files and Routes

Connections over socket can also use the file action.

  • errors are returned in the normal way {error: someError} rather than setting headers. That wouldn't make sense in this context
  • a successful file transfer will return the raw file data in a single send().

Cache

actionHero ships with the functions needed for an in-memory key-value cache. Check the cacheTest action to see how to use it. You can cache strings, numbers, arrays and objects (as long as they contain only strings, numbers, and arrays). Cache functions:

  • api.cache.save(api, key, value, expireTimeSeconds, next)
  • api.cache.load(api, key, next)
  • api.cache.destroy(api, key, next)

api.cache.save is used to both create new entires or update existing cache entires. If you don't define an expireTimeSeconds, the default will be used from api.configData.cache.defaultExpireTimeSeconds. A task will periodically go though and delete expired cache entries. As this is an in-memory cache, the maximum ram refined for the entire application is set with api.configData.cache.maxMemoryBytes. If your application is consuming more than the defined amount of ram, the cache will not save any new objects, and api.cache.save will return false. No other part of actionHero will halt due to this memory limitation.

Note: that the keys starting with an "_" should not be used, as they are in use by core parts of the system.

api.cache.save: Response

  • true / false
    • will be true unless the object could not be saved (perhaps out of ram or a bad object type).
    • overwriting an existing object will return true

api.cache.load: Response

  • (value, expireTimestamp, createdAt, readAt)
    • value will be the object which was saved
    • expireTimestamp(ms) is when the object is set to expire in system time
    • createdAt(ms) is when the object was created
    • readAt(ms) is the timestamp at which the object was last read with api.cache.load

api.cache.destroy: Response

  • true / false
    • will be false if the object cannot be found

actionCluster

actionHero can be run either as a stand-alone server or as part of a cluster. When running in cluster mode, the api will make use of the actionCluster methods. Features of an actionCluster:

  • ring-based communication. Lists of peers are shared among all members of the actionCluster, and each member communicates directly with all other members. This allows any member of the cluster to fail and the cluster to continue.
  • reconnection. Peers will always attempt to reconnect to disconnected peers
  • cluster security. Each actionCluster has a unique membership phrase api.configData.actionCluster.Key defined by you
  • shared messaging: the say socket command will message all clients in the same room in all peers. roomView will aggregate information for all peer's connections
  • shared or individual configuration for each peer.
  • Shared memory objects!
    • actionHero's single-node cache, api.cache is extended when operating in an actionCluster to allow for you to simply create redundant in-memory objects which can be accessed by any member of the cluster, even a peer which doesn't hold any of the data being accessed.
    • Object duplication. Using api.configData.actionCluster.nodeDuplication, you can ensure that your cached objects will be present on n peers to survive the crash of a peer. In the event a peer goes down, the remaining peers will reduplicate the object in question. actionHero will not delete extra copies of an object across peers, with the assumption they all might be different.

actionCluster.cache functions:

  • api.actionCluster.cache.save(api, key, value, expireTimeSeconds, next)
  • api.actionCluster.cache.load(api, key, next)
  • api.actionCluster.cache.destroy(api, key, remotePeer, next)

Just like the local version, api.actionCluster.cache.save will default to api.configData.cache.defaultExpireTimeSeconds if expireTimeSeconds is not set.

If you do not provide a remotePeer, api.actionCluster.cache.destroy will delete the object on all peers.

api.actionCluster.cache actions differ from their local cousins in their responses. As peers may differ in their value for a given object, this method returns an array of objets which contain the values and which peer they came from. In this way, you can handle differing responses however you like. Responses from all peers will be collected, and many may be null if they aren't holding the object. Examples:

api.actionCluster.cache.save: response object

Note that only those peers which the object was saved to are returned. In this case, nodeDuplication was set to 2.

[
   {
       "remotePeer": {
           "host": "127.0.0.1", 
           "port": "5000"
       }, 
       "value": true
   }, 
   {
       "remotePeer": {
           "host": "127.0.0.1", 
           "port": "5002"
       }, 
       "value": true
   }
]

api.actionCluster.cache.load: response object

Note that responses from all peers are listed, and those that do not hold the object return null.

[
   {
       "remotePeer": {
           "host": "127.0.0.1", 
           "port": "5002"
       }, 
       "value": "myValue",
	   "expireTimestamp": 1327726850689,
	   "createdAt": 1327723250689,
       "readAt": 1327723250890,
       "key": "ActionClusterCacheTest_myKey"
   }, 
   {
       "remotePeer": {
           "host": "127.0.0.1", 
           "port": "5001"
       }, 
       "value": null,
	   "expireTimestamp": null,
	   "createdAt": null,
       "readAt": null,
       "key": "ActionClusterCacheTest_myKey"
   }, 
   {
       "remotePeer": {
           "host": "127.0.0.1", 
           "port": "5000"
       }, 
       "value": "myValue",
	   "expireTimestamp": 1327726850689,
	   "createdAt": 1327723250690,
       "readAt": 1327723250890,
       "key": "ActionClusterCacheTest_myKey"
   }
]

api.actionCluster.cache.destroy: response object

Note that responses from all peers are listed, and those that successfully deleted are true.

[
   {
       "remotePeer": {
           "host": "127.0.0.1", 
           "port": "5002"
       }, 
       "value": true
   }, 
   {
       "remotePeer": {
           "host": "127.0.0.1", 
           "port": "5000"
       }, 
       "value": true
   }, 
   {
       "remotePeer": {
           "host": "127.0.0.1", 
           "port": "5001"
       }, 
       "value": false
   }
]

All actionCluster.cache actions will also include the local peer in their operations. All actionCluster.cache actions will also only wait api.configData.actionCluster.remoteTimeoutWaitMS to collect responses from peers, and will then return whatever information they have collected so far. This helps ensure that clients get the data they need even if a peer becomes unresponsive (some data is better than no data).

Tasks

Tasks are special periodically run actions the server will do at a set interval. Tasks can be run on every node in the actionCluster or just one. There are a few tasks which are core to actionHero which include:

  • calculateStats
    • Polls all other members in the actionCluster to build up statistics
    • Runs every 10 seconds
  • cleanLogFiles
    • removes all files in ./log/ if they are larger than api.configData.maxLogFileSize
    • runs every 60 seconds
  • cleanOldCacheObjects
    • removes expired objects in api.cache.data
    • runs every 10 seconds
  • pingSocketClients
    • sends a keep-alive message to all TCP socket clients
    • runs every 60 seconds
  • runAction
    • a wrapper to run an action as a task
    • will not run automatically
  • saveCacheToDisk
    • will save the contents of api.cache.data to disc
    • runs every 60 seconds

You can create you own tasks by placing them in a ./tasks/ folder at the root of your application. Like actions, all tasks have some required metadata:

  • task.name: The unique name of your task
  • task.description: a description
  • task.scope: "any" or "all". Should a single actionCluster server (any) run this task, or should all of them? For example, calculateStats is run by all peers in the action cluster (because we want to know all peer's status), but if you had a task to clean old sessions from your database or send an email, you would only want a single node to do that.
  • task.frequency: In milliseconds, how often should I run?. Setting me to 0 will cause me not to run automatically, but I can still be run with api.task.run

As stated above, any task can also be called programmatically with api.tasks.run(api, taskName, params, next).

An example Task:

var task = {};

/////////////////////////////////////////////////////////////////////
// metadata
task.name = "sayHello";
task.description = "I am a demo task which should only be on one node";
task.scope = "any";
task.frequency = 1000;

/////////////////////////////////////////////////////////////////////
// functional
task.run = function(api, params, next){
	api.log("----- Hi There! ----", "green");
	next(true);
};

/////////////////////////////////////////////////////////////////////
// exports
exports.task = task;

Requirements

  • node.js server
  • npm
  • mySQL (other ORMs coming soon?)

Install & Quickstart

  • npm install actionHero
  • Create a new file called index.js

The contents of index.js should look something like this:

// load in the actionHero class
var actionHero = require("actionHero").actionHero;

// if there is no config.js file in the application's root, then actionHero will load in a collection of default params.  You can overwrite them with params.configChanges
var params = {};
params.configChanges = {
	"webServerPort" : 8080,
	"socketServerPort" : 5000
}

// start the server!
actionHero.start(params);
  • Start up the server: node index.js

You will notice that you will be getting warning messages about how actionHero is using default files contained within the NPM package. This is normal until you replace those files with your own versions. Visit http://127.0.0.1:8080 in your browser and telnet to telnet localhost 5000 to see the actionHero in action!

You can pragmatically control an actionHero server with actionHero.start(params, callback), actionHero.stop(callback) and actionHero.restart(callback)

var timer = 5000;
actionHero.start(params, function(api){
	
	api.log(" >> Boot Sucessful!");
	setTimeout(function(){
		
		api.log(" >> restarting server...");
		actionHero.restart(function(){
			
			api.log(" >> Restarted!");
			setTimeout(function(){
				
				api.log(" >> stopping server...");
				actionHero.stop(function(){
					
					api.log(" >> Stopped!");
					process.exit();
					
				});
			}, timer);
		})
	}, timer);
});

Extending actionHero

The first thing to do is to make your own ./actions (and ./models) folder. If you like the default actions, feel free to copy them in. You should also make you own tasks as defined above.

A common practice to extend the API is to add new classes which are not actions, but useful to the rest of the api. The api variable is globally accessible to all actions within the API, so if you want to define something everyone can use, add it to the api object. In the quickstart example, if we wanted to create a method to generate a random number, we could do the following:

function initFunction(api, next){
	api.utils.randomNumber = function(){
		return Math.random() * 100;
	};
};

var actionHero = require("actionHero").actionHero;
actionHero.start({initFunction: initFunction}, function(api){
	api.log("Loading complete!", ['green', 'bold']);
});

Now api.utils.randomNumber() is available for any action to use! It is important to define extra methods in a setter function which is passed to the API on boot via ``params.initFunction`. This allows all threads in an cluster to access the methods. Even though the api object is returned to you, setting globally-available functions after initialization way may not propagate to the parts of actionHero.

Default Content

Actions:

  • cacheTest - a test of the DB-based key-value cache system
  • actionClusterCacheTest - another version of a cache test, but this one works across many nodes in the actionCluster. This version also persists an object after the test is compete.
  • actionsView - returns a list of available actions on the server and their metadata
  • randomNumber - generates a random number
  • status - returns server status and stats
  • say - sends messages via http to clients connected via socket (in the room you specify)

Files:

There are also some static files (index.html and associate files for a test) included in /public/ which you can check with the file action.

Other Goodies

Safe Params

Params (GET and POST) provided by the user will be checked against a whitelist. Any column headers in your tables (like firstName, lastName) will be accepted and additional params you define as required or optional in your actions action.inputs.required and action.inputs.optional. Special params which the api will always accept are: [ "callback", "action", "limit", "offset", "outputType" ]; Params are loaded in this order GET -> POST (normal) -> POST (multipart). This means that if you have {url}?key=getValue and you post a variable key=postValue as well, the postValue will be the one used. The only exception to this is if you use the URL method of defining your action.

Logging

The api.log() method is available to you throughout the application. api.log() will both write these log messages to file, but also display them on the console. There are formatting options you can pass to ``api.log(yourMessage, options=[]). The options array can be many colors and formatting types, IE: ['blue','bold']`. Check out `/initializers/initLog.js` to see the options.

Remember that one of the default actions will delete the log file if it gets over 10MB.

Versions of this API

see versions.markdown to see what's new in each version

Who?

  • The primary creator of the actionHero framework is Evan Tahler
  • If you want to contribute to actionHero, contribute to the conversation on github