11
\$\begingroup\$

We record software builds from our build machine into a database and for practice purposes I'm building a little web dashboard for it.

The API is REST (WebApi) and provides access to query for Products, Branches, Profiles and specific Builds.

The controller queries all products (each product contains a list of branches and profiles it has been build for) as well as all branches and profiles anything was ever built for.

It then goes on to query the last 10 builds for each of the branches and profiles a product was built for.

This is my first attempt at anything related to web-programming so I thought I'll get someone to look at it. Here is the code:

(function (angular) {
    var app = angular.module("app", ["ngResource"]);

    app.factory("Product", ["$resource", function ($resource) {
        return $resource("/api/Product");
    }]);

    app.factory("Build", ["$resource", function ($resource) {
        return $resource("/api/Build");
    }]);

    app.factory("BasicResource", ["$q", "$http", function ($q, $http) {
        var resourceFactory = {
            async: function(type) {
                var d = $q.defer();
                var result = $http.get("/api/" + type).success(function() {
                    d.resolve(result);
                });
                return d.promise;
            }
        };
        return resourceFactory;
    }]);

    app.controller("MainCtrl", ["$rootScope", "$scope", "$q", "$http", "BasicResource", "Product", "Build", function ($rootScope, $scope, $q, $http, BasicResource, Product, Build) {
        $rootScope.title = "Build Dashboard";
        $scope.Builds = [];

        $q.all([
            Product.query().$promise,
            BasicResource.async("Branch"),
            BasicResource.async("Profile")
        ]).then(function(data) {
            $scope.Products = data[0];
            $scope.Branches = data[1].data;
            $scope.Profiles = data[2].data;
        }).then(function() {

            var allBuildQueries = [];
            $scope.Products.forEach(function (prod) {
                prod.branches.forEach(function(b) {
                    prod.profiles.forEach(function(prof) {
                        allBuildQueries.push(Build.query({ product: prod.name, branch: b, profile: prof, limit: 10 }).$promise);
                    });
                });
            });

            $q.all(allBuildQueries).then(function (allBuilds) {
                allBuilds.forEach(function(builds) {
                    builds.forEach(function(build) {
                        $scope.Builds.push(build);
                    });
                });
            });
        });
    }]);

})(angular);

There are a few things which I don't really like about it so far but don't know how to improve it:

  1. The /api/Branch and /api/Profile resources return a list of strings. This causes the issue described in the SO question. The two basic workarounds are to use $http or transform the response. I have used $http because it seemed the easiest and I have not figured out how to use transformResponse properly so far (documentation on that is somewhat light). However it results in the factories (Branch and Profile vs Product and Build) having slightly different interfaces which bugs me.

  2. The second issue is the async query of the builds. Currently it queries Number of Branches x Number of Profiles per product for all products. At the moment this results in about 90 queries and really is no problem. However I'm wondering how this should be done properly in case the number of queries can grow into the hundreds or thousands.

  3. The way how I combine all the build query results seems somewhat inelegant to me. In C# I'd use a LINQ query like allBuilds.SelectMany(x => x.builds).ToList(). Would strike me as weird if there wouldn't be a more elegant javascript way of doing it.
    Since asking the question I have started using linqjs so that solves this issue.

\$\endgroup\$
2
+25
\$\begingroup\$
  1. You can use this unexplodeWord function:

     var explodedWords = [{"0":"h","1":"e","2":"l","3":"l","4":"o"},    
                           {"0":"w","1":"o","2":"r","3":"l","4":"d"}]
    
     var words = explodedWords.map(unexplodeWord);
    
     // Converts {"0":"h","1":"e"} to "he".
     function unexplodeWord(explodedWord) {
       var chars = [];
       angular.forEach(explodedWord, function(char) { 
         chars.push(char);
       });
       return chars.join('');
     }
    
     words.forEach(function(item) { console.log("word: " + JSON.stringify(item)); });
    

    You would call it as such:

        $scope.Products = data[0];
        $scope.Branches = data[1].map(unexplodeWord);
        $scope.Profiles = data[2].map(unexplodeWord);  
    

    You would still have some asymmetry relative to Products, but not as much.

  2. If I understand correctly, you want some kind of advice about pagination. If you really get too much data, maybe you could print only the branches and profiles for one product at a time.

    If you have too many branches and profiles for a single product, you should probably only output either branches or profiles, and paginate those.

\$\endgroup\$
  • \$\begingroup\$ For 1) that looks like an acceptable compromise. For 2) No I don't particularly look for pagination advise. My concern is that I issue one query per product-branch-profile - and these are all issued in parallel (and the waited on through the promises). Out in the wild (the internet) I'd probably kill my backend server fairly quickly with that so I'm interested what better solutions would exist. \$\endgroup\$ – ChrisWue Jul 26 '14 at 6:41
2
\$\begingroup\$

Your "BasicResource" resource factory can be greatly simplified:

before:

app.factory("BasicResource", ["$q", "$http", function ($q, $http) {
    var resourceFactory = {
        async: function(type) {
            var d = $q.defer();
            var result = $http.get("/api/" + type).success(function() {
                d.resolve(result);
            });
            return d.promise;
        }
    };
    return resourceFactory;
}]);

after:

app.factory("basicResource", ["$http", function ($http) {
    return {
        get: function(type) {
            return $http.get("/api/" + type)
                .then(function(response) {
                    return response.data;
                });
        }
    };
}]);

You don't need to call the method async because consumers will expect a promise from such a method. Also resource is probably not the best name for a service which only provides get functionality.

Regarding response transformation, there are several ways to do it. Per request, or by registering interceptors with $httpProvider.

Personally, my preference is to avoid $resource entirely as it behaves oddly, transforming a promise into its result for example. $http meets my needs.

Also note that .success and .error are deprecated and you should use .then and .catch in their place. This will improve interop and avoid some quirks.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.