Skip to content

Instantly share code, notes, and snippets.

@martyphee
Forked from kirkbushell/interceptor.js
Created June 26, 2014 02:10
Show Gist options
  • Save martyphee/dbb67c12598f2ae3d59c to your computer and use it in GitHub Desktop.
Save martyphee/dbb67c12598f2ae3d59c to your computer and use it in GitHub Desktop.

Revisions

  1. @kirkbushell kirkbushell created this gist Jun 11, 2014.
    345 changes: 345 additions & 0 deletions interceptor.js
    Original file line number Diff line number Diff line change
    @@ -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 );
    }]);