Neil Camara bio photo

Neil Camara

I'm a farmer

LinkedIn Github

Two months ago, a new team was built consisting of 2 backend developers and 1 frontend developer( it’s me :-) ). We had all the freedom to pick which technology we want to use. Since I was the only frontend developer, I was the only one to disagree with myself, LOL! The purpose of the team was to build a monitoring and analytics tool. I picked AngularJS, NodeJs, Primus+SockJS and Restangular(and there are more). We were asked to build a monitoring tool that will plot metric data generated by network switches and routers. I used Google Charts to display all the metrics that I retrieve from a specific endpoint using Restangular library.

I tested and experimented 3-4 different graphing libraries before I finally decided. I started first with FusionCharts library. It was great but customizing it was a bit of a pain. I also tried HighCharts. It was also nice but I was looking for something more flexible. I was planning to use D3 but I’m not sure why I didn’t choose it. All I remember is that it’s a huge library. Maybe that was the reason!

Fast forward, we were asked to generate a report and convert it to csv format. I wrote some PoC in the frontend that converts the json data into csv format. It works well with small sizes but when json data is about 20mb, it crashes Chrome but not Firefox. I decided and told the team that it’s a bad approach if we implement the conversion in the frontend. The backend JAVA devs, the folks who provides me the json data, wrote a csv converter. Oh, I forgot to mention that the source of data is Cassandra. However, I am using Restangular to retrieve json data via the API endpoints.

Back to csv conversion, the dev folks made 2 endpoints for retrieving the csv report. Yes I know, you’re going to ask why 2 endpoints. Well, I honestly like the reasons why they built it that way. The first REST call is a POST but query string is also used for including startdate and enddate of the report that I want to see. Once API receives my REST request, it responds back with 200 and a statusText of “Ok, I will generate your data” with an ephemeral resourceId that I will use for the next REST call. On the second REST request, a GET method, the API responds with either the report via octet-stream or a 404 with a message saying “Your report with resourceId 2DNA7YSNDEJTFHHPQNP and filename KWRWzcDhjrfcs2GK9G6p.csv is still being generated “. I hear yah! I know you’ll “WHAT THE …….!!!!” :). This is actually a very good approach especially when the report being generated is huge. It puts a lot of flexibility to the frontend. However, the coding on the frontend becomes a little more complicated … maybe! :) You can just keep on calling the second REST endpoint until you receive the file. This is what I’m actually doing but not via a for or while-loop. I’m exploting the power of Angulars’ $httpProvider.interceptors. The other way cooler approach would be to put the resourceId in collection and have a small window somewhere in your app which will display clickable resourceIds :)

Captured POST and multiple GET requests

The Nested Restangular Endpoint calls

OdometerRestFactory.one('csvexport').customPOST({}, path, { startdate : startHour, enddate : endHour, format : 'csv' }).then(function(data) {
    OdometerRestFactory.all('csvexport').get('csvreport', {
        format : 'csv',
        reportresourceid : $rootScope.downloadToken
    }).then(function(csvdata) {
            downloadCsvStream(csvdata, $rootScope.csvFilename);
            $rootScope.csvFilename = "";
    }, function(err) {
            $rootScope.modalDialogs.loadingSpinner = false;
            if (err.status == 404 && err.data.message.indexOf('still being generated') == -1) {
                alert('1');
                $scope.unknownError = true;
                $scope.serverError = err.data;
            }
            console.log('Reason for failure:', err);
        })
});

$httpProvider Interceptor

The magic is in the “if (rejection.data.status == 404 && rejection.data.message.indexOf(‘still being generated’) != -1) {“

cbApp.config(['$provide', '$httpProvider', function ($provide, $httpProvider) {

// Intercept http calls.

$provide.factory('HttpInterceptor', ['$q', '$injector', '$rootScope', function($q, $injector, $rootScope) {
    return {
        // On request success
        request: function (config) {
            //  console.log('$httpInterceptor1', config); // Contains the data about the request before it is sent.

            // Return the config or wrap it in a promise if blank.
            return config || $q.when(config);
        },

        // On request failure
        requestError: function (rejection) {
            //  console.log('$httpInterceptor2',rejection); // Contains the data about the error on the request.

            // Return the promise rejection.
            return $q.reject(rejection);
        },

        // On response success
        response: function (response) {
            //    console.log('$httpInterceptor3',response); // Contains the data from the response.

            // Return the response or promise.
            return response || $q.when(response);
        },

        // On response failture
        responseError: function (rejection) {
            $rootScope.downloadStatus = rejection.data.message;
            if (rejection.data.status == 401) {
                var $state = $injector.get('$state');
                var User = $injector.get('User');
                $rootScope.modalDialogs.loadingSpinner = false;
                $rootScope.modalDialogs.tokenExpired = true;
            }
            if (rejection.data.status == 404 && rejection.data.message.indexOf('still being generated') != -1) {
                var OdometerRestFactory = $injector.get('OdometerRestFactory');
                OdometerRestFactory.all('csvexport').get('csvreport', {
                    format : 'csv',
                    reportresourceid : $rootScope.downloadToken
                }).then(function(csvdata) {
                        downloadCsvStream(csvdata, $rootScope.csvFilename);
                        $rootScope.csvFilename = "";
                    }, function(errorResponse){
                        console.log('Reason for failure in Factory:', errorResponse.data.message);
                    })
            }
            // Return the promise rejection.
            return $q.reject(rejection);
        }
    };
}]);

// Add the interceptor to the $httpProvider.
$httpProvider.interceptors.push('HttpInterceptor');

}]);

As a bonus, I’ve also included the code that captures 401 HTTP error code. So if I access a REST endpoint from any part of the application and I get a 401 HTTP error code, this will redirect me to the login screen. I don’t have to write it multiple times! Isn’t it lovely?! :) Have a great day everyone! Peace!