Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Modeling relational data from REST api via angularjs

I'm building an app, that is backed with node-mysql combo, and angularjs on the frontend part. The backend REST service is ready, but I'm struggling with modeling my relational data. There are some questions regarding this like : $resource relations in Angular.js or $resource relations in Angular.js [updated] . Are those approaches still the best approaches, or were there any significant changes in $resource ? Or maybe Restangular is the way to go?

like image 222
mike_hornbeck Avatar asked Dec 25 '22 06:12

mike_hornbeck


2 Answers

Here is my technique:

I declare a factory called dataService, which is a wrapper around Restangular, extended with some other features.

First let me gave some code and then explain:

.factory('identityMap',

      var identityMap = {};

      return {
        insert: function(className, object) {
          if (object) {
            var mappedObject;
            if (identityMap[className]) {
              mappedObject = identityMap[className][object.id];
              if (mappedObject) {
                extend(mappedObject, object);
              } else {
                identityMap[className][object.id] = object;
                mappedObject = object;
              }
            } else {
              identityMap[className] = {};
              identityMap[className][object.id] = object;
              mappedObject = object;
            }
            return mappedObject;
          }
        },
        remove: function(className, object) {
          if (identityMap[className] && identityMap[className][id]) delete identityMap[className][id];
        },
        get: function(className, id) {
          return identityMap[className] && identityMap[className][id] ? identityMap[className][id] : null;
        },
        flush: function(){
          identityMap = {};
        }
      };
    }

.factory('modelService', ['Restangular', 'identityMap', '$rootScope', '$log', function(Restangular, identityMap, $rootScope, $log) {
            var ENUM1 = {STATE:0, OTHER_STATE:1, OTHER_STATE2: 2},
                ENUM2 = {OK:0, ERROR:1, UNKNOWN:2};

            function extendModel(obj, modelExtension, modelName){
                angular.extend(obj, modelExtension);
                obj.initExtension();
                obj = identityMap.insert(modelName, obj);        
            }

            function broadcastRestEvent(resourceName, operation, data){
                $rootScope.$broadcast(resourceName + $filter('capitalize')(operation), data);
            }

            var resource1Extension = {
                _extensionFunction1: function() {
                    // ... do something internally ...
                    if (this.something){
                        // this.newValue ....
                        ; 
                    }
                    else {
                        // ....;
                    }
                },
                publicExtensionFunction: function(param1) {
                    // return something
                },
                initExtension: function() {
                    this._extensionFunction2();
                    extendModel(this.resource2, resource2Extension, 'resource2');
                }
            };

            var resorce2Extension = {
                _extensionFunction1: function() {
                    // do something internally
                },
                publicExtensionFunction = function(param1) {
                    // return something
                },
                initExtension: function(){
                    this._extensionFunction1;

                }
            };

            var modelExtensions = {
                'resource1': resource1Extension,
                'resource2': resorce2Extension
            };

            var rest = Restangular.withConfig(function(RestangularConfigurer) {
                RestangularConfigurer.setBaseUrl('/api');

                RestangularConfigurer.setOnElemRestangularized(function(obj, isCollection, what, Restangular){
                    if (!isCollection) {
                        if (modelExtensions.hasOwnProperty(what)) {
                            extendModel(obj, modelExtensions[what], what);
                        }
                        else {
                            identityMap.insert(what, obj);
                        }
                        if (obj.metadata && obj.metadata.operation) {
                            broadcastRestEvent(what, obj.metadata.operation, obj);
                        }
                    }
                    return obj;
                });

                RestangularConfigurer.addResponseInterceptor(function(data, operation, what, url, response, deferred) {
                    var newData;

                    if (operation === 'getList') {
                        newData = data.objects;
                        newData.metadata = {
                            numResults: data.num_results,
                            page: data.page,
                            totalPages: data.total_pages,
                            operation: operation
                        };
                        data = newData;
                    } 
                    else if (operation === 'remove') {
                        var splittedUrl =  url.split('/');
                        var id = splittedUrl.pop();
                        var resource = splittedUrl.pop();  
                        identityMap.remove(resource, id);
                        broadcastRestEvent(resource, operation, id);
                    }
                    else {
                        data.metadata = {operation: operation};
                    }
                    return data;
                });
            });


            return {
                rest: rest,
                enums: {
                    ENUM1: ENUM1,
                    ENUM2: ENUM2
                },
                flush: identityMap.flush,
                get: identityMap.get
            }

        }]);

1) Let me explain identityMap (it's the code from this blog post with some extended features):

  • Let's consider a REST model which looks like this (each resource represents a database table):

resource 1:

id = Integer
field1 = String
field2 = String
resource2s = [] (List of resources2 which points to this resource with their foreign key)

resource 2:

id = Integer
field1 = String
field2 = String
...
resource1_idfk = Foreign Key to resource 1
  • Resource API is so smart that it returns resource1 relationships with resources2 with GET /api/resource1/1 to save the overhead that you would get with GET to resource2 with some query parameters to resource1_idfk...

  • The problem is that if your app is doing the GET to resource1 and then somewhere later GET to resource2 and edits the resource2, the object representing the resource2 which is nested in resource1 would not know about the change (because it is not the same Javascript object reference)

  • The identity map solves this issue, so you hold only one reference to each resource's instance

  • So, for example, when you are doing an update in your controller the values automatically updates in the other object where this resource is nested

  • The drawback is that you have to do memory management yourself and flush the identity map content when you no longer need it. I personally use Angular Router UI, and define this in a controller which is the root of other nested states:

    $scope.$on("$destroy", function() { modelService.flush(); });

  • The other approach I use within the Angular Router UI is that I give the id of the resource which i want to edit/delete within that controller as the parameter of nested state and within the nested state i use:

    $scope.resource1instance = modelService.get('resource1', $stateParams.id);

You can than use

resource1.put(...).then(
    function(){
        // you don't need to edit resource1 in list of resources1
        $state.go('^');  
    }    
    function(error){
        handleError(error);    
    });

2) When I need to use some new functionality over resources I use `Restangular's setOnElemRestangularized. I think the code above is self explanatory and very similar to the one mentioned in blog post I have mentioned above. My approach is slightly different from the one in that post, that I don't use the mixin initialization before, but after I mix it to the object, so one could reference the new functions in initializer. The other thing I don't use, for example, he creates single factory for every resource, for example Proposal for extended logic and the other factory ProposalSvc for manipulating the instances. For me that's a lot of code you don't have to write and personally I think that Javascript is not suited very well for this object oriented approach, so I return just the whole Restangular object and do operations with it.

3) Another thing I have there is the broadcast of events when something in my model changes with Restangular, this is something I needed when I used ng-table. For example, when the model changed and rows in my table needed to be updated to reference the changes, so in the controller which manages the table I use $scope.on('eventName') and then change appropriate row. These events are also great when you have a multiuser live application and you use websockets for server notifications (code not included here in modelService). For example somebody deletes something in a database, so the server sends a notification to everyone who is alive through websocket about the change, you then broadcast the same event as used in Restangular and the controller does the same edits on its data.

like image 126
Peter Pristas Avatar answered Jan 11 '23 11:01

Peter Pristas


This blog post should help you make your choice http://sauceio.com/index.php/2014/07/angularjs-data-models-http-vs-resource-vs-restangular/

I agree that there are a lot of good practices using http headers in Restangular, but you can pick them in the source and use them directly.

What you have to wonder is, will you be able to wrap your nested resources within a $resource and make instance calls while modifying the parent object. And that's not a given.

like image 20
Gepsens Avatar answered Jan 11 '23 11:01

Gepsens