justapis-javascript-sdk v0.2.3
justapis-javascript-sdk
A lightweight JavaScript SDK to connect to a JustAPIs gateway in Node or the browser.
Dependencies
- Native Promise Only
- Tiny Emitter
- xml2js (Node-only)
- xmlserializer (Node-only)
Features
- Browser & Node.js support
- HTTP request/response connection
- HTTP Public Key Pinning
- Per-request caching
- Pausable/Resumable asynchronous request queue
Install via NPM
$ npm install justapis-javascript-sdk
Setup
The SDK is built with browserify. If you would like to add a single bundled file
you can find it in the dist
folder.
<script src="justapis-javascript-sdk/dist/justapis-javascript-sdk.min.js"></script>
Else, if you are using browserify in your project you probably prefer to use
require
to load the dependency.
var APGateway = require("justapis-javascript-sdk");
Creating a Gateway
The main object in the SDK is APGateway, you can think of it as an http client. To make a request to an endpoint you just need to do
var APGateway = require("justapis-javascript-sdk");
var options = {
url: "http://my.gateway.domain.org/users",
method: "GET",
headers: {
"Foo": "Bar"
},
data: {
"name": "john"
}
};
// If you prefer objects you can do:
var gw = new APGateway(options);
// Or you can create it like this
var gw = APGateway.create(options);
// Now executing a request is as easy as
gw.execute().then(function(response) {
// Response came back ok...
console.log(response.data);
}).catch(function(error) {
// An error occured :(
console.log(error.message);
});
// Also you can reuse your gateway as many times as you like.
// It will send the same request as before...
gw.execute();
// Or you can change only the pieces that you want and keep using it...
gw.method("POST").execute();
Copying a Gateway
A nice feature of gateways is that they can be copied. By copying a gateway you get all the configuration from the original, so you don't have to repeat yourself.
var gw = new APGateway({
headers: {
"Foo": "Bar"
}
});
// just call the copy method
var gwCopy = gw.copy();
gwCopy.headers({
"Foo": "Hello World!"
});
gw.headers(); // will return { "Foo": "Bar" }
gwCopy.headers(); // will return { "Foo": "Hello World!" }
Caching Requests
Note: The Caching service behaves differently in Node than in the browser
APGateway
allow for request caching per request. Only responses to GET
requests are cached, and this is enabled by default.
var gateway = new APGateway();
gateway.cache(false); // Disable caching from now on...
gateway.execute(); // Send request without caching the response
gateway.cache(true) // Enable caching from now on...
When using it in Node, responses will be cached in-memory only by default.
In the browser however, cached responses will be saved to localStorage
if available.
Since localStorage
is persistent, you might want to flush it at some point.
// This will only remove localStorage entries set by
// APGateway's request cache
APGateway.RequestCache.flush();
Cached responses have a TTL (time to live) of 1 week (604800000 milliseconds). Any response older than that will be ignored and removed from the cache. If you would like to use a different TTL you can set it like so:
// ttl is in milliseconds
APGateway.RequestCache.ttl = 60000; // set ttl to 1 minute
Custom Persistence
In some cases you may want to persist the cache in a different way. In the case
of Node, for example, you may want to persist cached instances through a
database or external service. In order to do that you can replace APGateway.RequestCache.storage
with your own implementation. Here is a small
example of how to do just that:
// The storage object MUST have the following methods
APGateway.RequestCache.storage = {
/**
* Set key/value pair in storage
*
* key -> string key identifying the value
* record -> an Object containing two attributes:
* 'value' -> the Object being cached
* 'timestamp' -> already serialized Date string
*
* returns -> a Promise to be resolved when set is finished (no resolve value needed)
*/
set: function(key, record) {...},
/**
* Get a value from storage
*
* key -> string identifying the value to retrieve
*
* returns -> a Promise that resolves with the retrieved Object
*/
get: function(key) {...},
/**
* Get all values in the storage. A prefix is passed that identifies the entire cache.
* This prefix is prepended to every key and used to differentiate one cache instance from another.
* It is only passed as a convenience, it is not required to use it internally.
*
* prefix -> string prefix identifying the cache
*
* returns -> a Promise that resolves with an Array of the retrieved objects (or empty Array if none)
*/
getAll: function(prefix) {...},
/**
* Removes a single record from the storage
*
* key -> string key identifying the record
*
* returns -> a Promise that resolves when the record has been removed (no resolve value needed)
*/
remove: function(key) {...},
/**
* Removes all records from storage
*
* prefix -> Same as with 'getAll()'
*
* returns -> a Promise that resolves when flushing is complete (no resolve value needed)
*/
flush: function(prefix) {...}
};
You may have noticed that all the required methods to override return a Promise,
this is meant as a convenience so you can easily work with async operations when
persisting records. Any Promises/A+ compliant implementation can be used
(or even native Promises if available), but in case you do not want to add a
promise package just for this, APGateway uses an implementation internally
that you can find in APGateway.Promise
.
Async request queue
APGateway instances use an async queue internally to send requests. This queue is shared across instances and can be paused/resumed to avoid sending further requests at any time. If your application goes offline you can pause the queue, wait for reconnection, and resume it without loosing requests.
APGateway.Queue.pause();
var gateway = new APGateway();
gateway
.url('http://localhost:1337/resource')
.execute() // This adds the request to the queue
.then(function(response) { /* Got response back */ })
.catch(function(error) { /* Got an error */ });
// The queue will continue to build up until resumed
APGateway.Queue.resume();
Whenever the queue is resumed it will start sending pending requests
asynchronously. Because the queue can get pretty big while paused, the queue
will throttle the flow of requests being sent to avoid flooding the server.
The default throttle time is 300 milliseconds, but you can adjust this by doing APGateway.Queue.throttleBy(amountInMilliseconds)
.
Persisting the Queue
In some cases you might want to save the state of the queue either to
localStorage
or a database. For that purpose there's an export method on the
queue you can use.
Note: The queue must be paused before calling export, otherwise an Error will be thrown
// Pause the queue
APGateway.Queue.pause();
// requests will be an Array of requests
var requests = APGateway.Queue.export();
// persists requests...
Now when get your persisted requests you can just resend them.
// Get the saved requests from your storage of choice
var gateway = new APGateway();
persistedRequests.forEach(function(requestData) {
// First we need to recreate the APRequest object
var request = Object.create(APGateway.APRequest, requestData);
gateway
.sendRequest(request)
.then(function(res) {
// Do something else
})
.catch(function(error) {
// There was an error
});
});
APGateway Instance Methods
Default Properties
url: {
href: "http://localhost:5000",
protocol: "http:",
hostname: "localhost",
port: "5000",
pathname: "/",
search: null,
hash: null
},
method: "GET",
silentFail: true,
cache: true,
dataType: "json",
data: {},
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
parsers: {
json: JSONParser,
form: FormDataParser,
xml: XMLParser
},
transformations: {
request: [ EncodeTransformation ],
response: [ DecodeTransformation, CacheResponse ]
}
Methods
url( url )
Returns the current url or the APGateway instance for quick chaining
url
:string
* Ifurl
is undefined the method will act as a getter, else it will set the value and returnthis
.
method( method )
Returns the current http method or the APGateway instance for quick chaining
method
:string
Accepted valuesGET
,POST
,PUT
,PATCH
,DELETE
Ifmethod
is undefined the method will act as a getter, else it will set the value and returnthis
.
data( data )
Returns the current request data or the APGateway instance for quick chaining
data
:object
* Ifdata
is undefined the method will act as a getter, else it will set the value and returnthis
.
dataType( dataType )
Returns the current response data type or the APGateway instance for quick chaining
dataType
:string
* IfdataType
is undefined the method will act as a getter, else it will set the value and returnthis
.json
andxml
dataTypes are parsed automatically, any other dataType will be returned as a string.
contentType( contentType )
Returns the current content type or the APGateway instance for quick chaining
contentType
:string
* IfcontentType
is undefined the method will act as a getter, else it will set the value and returnthis
.
headers( headers )
Returns the current headers or the APGateway instance for quick chaining
headers
:object
The key-value pairs inheaders
will be appended to the current ones. Ifheaders
is undefined the method will act as a getter, else it will set the value and returnthis
.
withCredentials( withCredentials )
Returns the current value of withCredentials or the APGateway instance for quick chaining
Enabling withCredentials
will cause any cookies to be included in the request
to the server. The server needs to be configured to enable credentials as well
by adding Access-Control-Allow-Credentials: true
as a response header.
withCredentials
:boolean
Default value:false
IfwithCredentials
is undefined the method will act as a getter, else it will set the value and returnthis
.
silentFail( silent )
Returns the current value of silentFail or the APGateway instance for quick chaining
When a request is not successful APGateway
will throw an error.
Setting silentFail
to true
will cause the gateway to ignore those errors.
silent
:boolean
* Default value:true
. Ifsilent
is undefined the method will act as a getter, else it will set the value and returnthis
.
cache( active )
Returns the current value of cache or the APGateway instance for quick chaining
active
:boolean
* Default value:true
. Ifactive
is undefined the method will act as a getter, else it will set the value and returnthis
.
copy()
Returns a shallow copy of the APGateway instance.
requestTransformations( transformations )
Returns the current request transformations or the APGateway instance for quick chaining
transformations
:[function]
* Array of functions to transform the request configuration before it is sent to the server```javascript gw.requestTransformations([ // transformations must ALWAYS return "request" // in order for the entire chain to work properly function(request) {...}, function(request) {...} ]); ``` If undefined the method will act as a getter, else it will set the value and return `this`.
responseTransformations( transformations )
Returns the current response transformations or the APGateway instance for quick chaining
transformations
:[function]
* Array of functions to transform the response object after it returns from the server```javascript gw.responseTransformations([ // transformations must ALWAYS return "response" // in order for the entire chain to work properly function(response) {...}, function(response) {...} ]); ``` If undefined the method will act as a getter, else it will set the value and return `this`.
addRequestTransformation( transformation )
Returns the APGateway instance for quick chaining
transformation
:function
* Adds the transformation at the end of the request transformation chain
addResponseTransformation( transformation )
Returns the APGateway instance for quick chaining
transformation
:function
* Adds the transformation at the end of the response transformation chain
hpkp( options )
Sets up HTTP Public Key Pinning for the APGateway instance.
options
:object
sha256s
:[string]
(required)- Array of two encoded public key information hashes. One is actually used, the other is kept as backup.
maxAge
:number
(required)- The time in seconds that the pinned key will be remembered for.
includeSubdomains
:boolean
(optional)- Applies the pinned key to subdomains also.
reportOnly
:boolean
(optional)- Specifies if pin validation failures should be reported to the given
URL (if true
reportUri
must be present as well).
- Specifies if pin validation failures should be reported to the given
URL (if true
reportUri
:string
(optional)- URL to send pin validation failure reports to.
execute()
Returns a Promise
- Executes a request with the current configuration
Framework Integration
React
The SDK will work with React.js out of the box since its plain JavaScript.
Angular
Integrating with Angular.js is not a problem, here is an example of how you would use it.
angular.module('MyModule')
.controller('MyModuleController', ['$scope', function($scope) {
// Declare a default message to show
$scope.message = "Default message";
// Create the gateway as usual...
var gateway = new APGateway();
gateway
.url('http://my.service/message')
.execute()
.then(function(response) {
// Keep in mind, when updating the $scope, to use $apply
// so angular is made aware of the change
$scope.$apply(function() {
$scope.message = response.data;
});
});
});
Ember
Like React or Angular, there is no restriction to use APGateway in an Ember application. If you're using Ember Data however you might want to integrate APGateway so you can load Models from it.
Ember Data
In order to integrate with Ember Data you will want to create an Adapter.
This example code shows the basic principle of how to integrate the two.
NOTE: For simplicity's sake the example only shows implementations of findRecord
and createRecord
but when extending DS.Adapter
you must implement the other methods too.
// url of your endpoint
var URL = "http://localhost:5000/todos";
var gateway = new APGateway();
// This helper will make sure that the response of the gateway runs
// inside Ember's run loop.
function runRequestToGateway(gateway) {
return Ember.RSVP.Promise(function(resolve, reject) {
gateway
.execute()
.then(function(response) {
Ember.run(null, resolve, response.data);
})
.catch(function(error) {
Ember.run(null, reject, error);
});
});
}
// Register an ApplicationAdapter that uses APGateway internally...
Todos.ApplicationAdapter = DS.Adapter.extend({
findRecord: function(store, type, id, snapshot) {
gateway
.method("GET")
.url(URL + "/" + id)
.silentFail(false);
return runRequestToGateway(gateway);
},
createRecord: function(store, type, snapshot) {
var data = this.serialize(snapshot, { includeId: true });
gateway
.url(URL)
.method("POST")
.data(data)
.silentFail(false);
return runRequestToGateway(gateway);
},
updateRecord: function(store, type, snapshot) {...},
deleteRecord: function(store, type, snapshot) {...},
findAll: function(store, type, sinceToken, snapshotRecordArray) {...},
query: function(store, type, query, recordArray) {...}
});
Development
If you would like to develop in the SDK you can just download the repository and do
npm install
Once that finishes, just use grunt to start the watch
process
grunt
Contribution Guidelines
This repository relies on semantic-release-cli for automated releases and commitizen and cz-conventional-changelog for standardized commit messages and automated changelogs.
Committing
All work should be committed to a new branch off develop
, never to a mainline
branch directly. When ready to commit changes, stage them as usual with:
git add .
Committing changes is a little different. In order to ensure standardized commit messages across time and contributors, always use the npm script:
npm run commit
and follow the instructions on screen.
Note: for an overview of commitizen and the conventional changelog tool, watch the video.
Pull Requests
When a branch is ready to merge, submit a pull request to develop
via GitHub.
Pull requests are automatically tested on Travis CI and may not be merged until
all tests pass. At that time, a team member may safely merge the PR
into develop
.
Releasing
Releases are made from the master
branch automatically. To initiate a
release, create a PR from develop
to master
. Since releasing is automated,
the timing and extent of these PR's is at the discretion of team members.
Releasing is automated with semantic-release
. An automatic release is tagged
and published to npm upon a successful merge into the master
branch only after
all tests successfully pass. No human intervention is required to create
releases other than merging a PR.