@@ -0,0 +1,345 @@
/**
* Interceptor
* Implements functionality to catch various requests and fire events when they happen. This is generally to ensure
* that responses from the server are handled in a uniform fashion across the application. Also, by firing events
* it allows to have any number of handlers attach to the response.
*
* @author Kirk Bushell
* @date 28th March 2013
*/
var module = angular . module ( 'core.interceptor' , [ ] ) ;
module . service ( 'Messages' , [ '$filter' , function ( $filter ) {
var service = {
/**
* Store all the messages here. The default (base) or the custom messages.
*
* @type {Object }
*/
messages : {
base : {
error : {
create : 'Could not create :resource. Please try again.' ,
read : 'Could not load :resource. Please try again.' ,
update : 'Could not update :resource. Please try again.' ,
'delete' : 'Could not delete :resource. Please try again.'
} ,
success : {
create : ':resource created.' ,
read : 'Loaded :resource successfully.' ,
update : ':resource saved.' ,
'delete' : ':resource deleted.'
}
} ,
// Custom messages are stored here.
custom : { }
} ,
get : function ( resource , action , type ) {
resource . split ( '-' ) . join ( ' ' ) ;
resource = $filter ( 'ucfirst' ) ( resource ) ;
var customResource = get ( service . messages . custom [ resource . toLowerCase ( ) ] ) ;
if ( customResource != null ) {
var custom = get ( customResource [ type ] [ action ] ) ;
if ( custom != null ) return custom ;
}
// Get the default message, if available, and perform the replacement.
var msg = get ( service . messages . base [ type ] [ action ] ) ;
if ( msg && msg . length > 0 ) {
msg = msg . split ( ':resource' ) . join ( resource . singularize ( ) ) ;
}
return msg ;
} ,
/**
* Registers a resource and all its custom messages.
*
* @param {String } resource Resource name
* @param {Object } messages An object with create, read, update, delete keys
*
* @return {void }
*/
register : function ( resource , messages ) {
service . messages . custom [ resource ] = messages ;
}
} ;
return service ;
} ] ) ;
module . config ( [ '$httpProvider' , function ( $httpProvider ) {
var interceptor = [ '$rootScope' , '$q' , 'Notify' , 'Messages' , 'Analytics' , function ( $rootScope , $q , Notify , Messages , Analytics ) {
/**
* Parses the resource based on the url that was sent.
*
* @param {object } response Response object
*
* @return {string }
*/
var getResourceFromResponse = function ( response ) {
var urlParts = response . config . url . split ( '?' ) ,
url = urlParts [ 0 ] . replace ( / ^ \/ | \/ $ / g, '' ) , // strip first and last slash.
base = $rootScope . config . app . base . replace ( / ^ \/ | \/ $ / g, '' ) ; // strip first and last slash.
// If there's a base, let's strip it out.
if ( base . length ) {
url = url . replace ( base , '' ) ;
}
urlParts = url . split ( '/' ) ;
url = urlParts [ 0 ] ;
return url ;
}
/**
* Extracts an ID from a URL, if it's available.
*
* @param {string } resource The name of the resource.
* @param {string } url The URL to parse.
*
* @return {mixed } Returns the extracted ID or null.
*/
var getIDFromURL = function ( resource , url ) {
var urlParts = url . split ( resource ) ,
possibleID = urlParts [ 1 ] ,
id ;
// If there is nothing, we know we're creating and not updating.
// Therefore, there will be no ID to return.
if ( ! possibleID ) return null ;
// Remove the first occurance of a slash.
possibleID = possibleID . replace ( '/' , '' ) ;
// Check if there are any more slashes, so we know whether we should split
// the possible ID variable or just return it.
if ( possibleID . indexOf ( '/' ) === - 1 ) {
return isNaN ( possibleID ) ? null : possibleID ;
}
// Since at this point we know that the url still has a slash in it, we will
// split on that slash and get the first part of the array.
id = possibleID . split ( '/' ) [ 0 ] ;
// Now we want to check if the value is a number or not.
return isNaN ( id ) ? null : id ;
} ;
/**
* Returns a custom resource action, if it has been supplied in the URL. This is defined
* by a string representation AFTER the resource id. Eg.
*
* /entries/1/submit
*
* @param string resource
* @param string url
* @return mixed string on success, null on failure
*/
var getActionFromUrl = function ( resource , url ) {
url = url . replace ( config . app . base , '' ) . split ( '/' ) ;
url . shift ( ) ;
// Could be dealing with an integer or extra action
if ( url . length > 1 ) {
if ( isNaN ( url [ 1 ] ) ) {
// custom action
return url [ 1 ] ;
}
if ( url . length > 2 && isNaN ( url [ 2 ] ) ) {
return url [ 2 ] ;
}
}
return null ;
} ;
/**
* Based on the data returned from the server whenever there's a validation error
* we will construct a single validation message that is displayed in an alert.
*
* @param {Object } data The data object returned from the server.
*
* @return {String }
*/
var getValidationMessages = function ( response ) {
var errors = [ ] ;
// Check if the response is empty, meaning there are no validation errors.
if ( getResponseType ( response ) != 'validation' || $ . isEmptyObject ( response . data ) || $ . isEmptyObject ( response . data . errors ) ) return errors ;
// Put all the errors from all the fields into 1 array. Basically flatenning the array.
angular . forEach ( response . data . errors , function ( issues ) {
angular . forEach ( issues , function ( error ) {
errors . push ( error ) ;
} ) ;
} ) ;
return errors ;
}
var getResponseType = function ( response ) {
return get ( response . headers ( ) [ 'x-response-type' ] ) ;
}
var notificationate = function ( response ) {
var method = response . config . method . toLowerCase ( ) ,
action = '' ,
status = response . status ,
type = 'Error' , // notification type. Error, Success, Info or Warning.
responseType = getResponseType ( response ) , // custom response type.
message = null ,
resource ,
possibleAction ;
// Ignore GET requests.
if ( method == 'get' && status == 200 ) return ;
// Any status 200 responses at this point are for successful operations.
// So we will change the notification type to Success.
if ( status == 200 ) type = 'Success' ;
// Parse the resource name.
resource = getResourceFromResponse ( response ) ;
// Do not display any message for exceptions.
if ( resource == 'exceptions' ) return ;
// Let's determine what action is taken based on the request method.
switch ( method ) {
case 'post' : action = 'create' ; break ;
case 'get' : action = 'read' ; break ;
case 'put' : action = 'update' ; break ;
case 'delete' : action = 'delete' ; break ;
}
// Check if there's an ID in the URL whenever we send a post request because the action
// could be update instead of create.
// This is done because ngResource sends a POST request for updates instead of PUT.
if ( method == 'post' ) {
if ( getIDFromURL ( resource , response . config . url ) ) {
action = 'update' ;
}
}
// Set up our action based on whether or not a custom one has been defined in the URL
possibleAction = getActionFromUrl ( resource , response . config . url ) ;
if ( possibleAction ) {
action = possibleAction ;
}
// Based on the response status.
switch ( status ) {
case 400 :
if ( responseType == 'validation' ) {
if ( typeof response . data . message == 'string' ) {
message = response . data . message ;
}
}
else {
message = response . data ;
}
break ;
case 401 :
message = 'Your current session has expired. Please log in.' ;
break ;
case 403 :
message = 'You do not have sufficient permission to access this resource.' ;
break ;
case 500 :
if ( angular . isString ( response . data ) && response . data . length ) {
message = response . data ;
}
break ;
}
if ( ! message ) {
message = Messages . get ( resource , action , type . toLowerCase ( ) ) ;
}
// Send an update to the validation-errors directive to show/hide validation errors.
$rootScope . $broadcast ( 'validation.errors' , getValidationMessages ( response ) ) ;
if ( message ) {
Notify [ type ] ( message ) ;
// Google Analytics event tracking.
Analytics . trackEvent ( resource , type , message ) ;
}
}
/**
* Broadcasts an event that any part of the app can listen to
* and perform custom actions.
*
* @param string name The event name
* @param mixed response The response that comes back from the server
*
* @return void
*/
var broadcast = function ( name , response ) {
$rootScope . $broadcast ( name , response ) ;
notificationate ( response ) ;
}
/**
* Successful response handler.
*
* @param mixed response The response th at comes back from the server
*
* @return mixed
*/
var success = function ( response ) {
broadcast ( 'app.success' , response ) ;
return response ;
}
/**
* Invalid response handler.
*
* @param mixed response The response th at comes back from the server
*
* @return mixed
*/
var error = function ( response ) {
// This is the default error event to broadcast.
// It may be overwritten depending on the response status.
var event = 'app.unknown-error' ;
switch ( response . status ) {
case 400 : event = 'app.error' ; break ; // Bad requests (validation errors.etc.)
case 401 : event = 'app.unauthorised' ; break ; // Unauthorised, should require login
case 403 : event = 'app.forbidden' ; break ; // Forbidden, user is simply not allowed access
case 500 : event = 'app.failure' ; break ; // Critical error on the server, catch and display
}
broadcast ( event , response ) ;
return $q . reject ( response ) ;
}
return function ( promise ) {
return promise . then ( success , error ) ;
} ;
} ] ;
$httpProvider . responseInterceptors . push ( interceptor ) ;
} ] ) ;