February 05, 2015
Adam Kelso

Enola Labs creates custom strategy and products for mobile and web.

Let’s Talk

Subscribe to our blog:

This is the second part of our series on wrapping third-party libraries. To read the first part, click here.

3. Translating Parse Getters and Setters to Angular

Parse’s SDK, and Backbone for that matter, uses explicit setters and getters on most objects while Angular uses POJOs (Plain, Old JavaScript Objects).

 // Parse objects explicit getters and setters employee.set('firstName', 'Adam'); employee.set('lastName', 'Kelso'); var firstname = employee.get('firstName'); var lastname = employee.get('lastName'); // Angular objects use implicit getters and setters employee.firstName = 'Adam'; employee.lastName = 'Kelso'; var firstName = employee.firstName; var lastName = employee.lastName; 

This difference presents the same problem calling digest because of Parse promises did; without an automated mechanism to deal with the translation, our Angular code ends up requiring more knowledge about the Parse SDK than it should. Thankfully, there is a native JavaScript solution to this problem. Object.defineProperty to the rescue!

 // In the returned object of the parseInit service GettersAndSetters: function(classObject, attributesArray) { for(var i = 0; i < attributesArray.length; i++) { eval( 'Object.defineProperty(classObject.prototype, "' + attributesArray.angular + '", {' + 'get: function() {' + 'return this.get("' + attributesArray.parse + '");' + '},' + 'set: function(aValue) {' + 'this.set("' + attributesArray.parse + '", aValue);' + '}' + '});' ); } } 

This new method creates implicit looking setters and getters for any Parse objects that are used. Object.defineProperty creates a little bit of magic in that we can provide a get and set function - among other things it can do - as interceptors on objects and under the hood execute Parse’s methods.

You may notice, and potentially frown, at the use of eval. It’s not a mistake. The eval method is required because it forces the code it contains to not be executed until the method is called. Without eval, the Getters and Setters method is hoisted when the browser compiles the page’s javascript, causing lots of problems and prevents Angular from compiling correctly. eval delays execution until after any repository using the parseInit service is injected.

This new code allows us to set up repositories in a way in which entities also have a second layer of abstraction between the server and the browser. You can pass an array of property names that should correlate with table columns on Parse like so so:

 angular.module('cms') .factory('employeeService', ['parseInit', function(parseInit){ var Employee = parseInit.CreateRepository('Employees'); parseInit.GettersAndSetters(Employee, [ {angular:'firstName', parse:'firstName'}, {angular:'lastName', parse:'lastName'}, {angular:'title', parse:'position'}, {angular:'startDate', parse:'dateOfHire'} // Any extra columns / properties needed ]); return Employee; }]); 

And then use object properties like normal.

 angular.module('cms') .controller('EmployeeCtrl', ['$scope', '$routeParams', '$location', 'employeeService', function($scope, $route, $location, $employees) { $scope.employee = {}; // Initialization $employees.get($route.employeeId).then( function(result) { $scope.employee = result; } ); // Assume this is called when a new employee is created $scope.submitForm = function() { // Most properties on the employee object can be updated from the form via two-way binding. // Here's the implicit setting, and the view uses implicit getting $scope.employee.startDate = new Date(); $employees.save($scope.employee).then( function() { $scope.emit('userMessage', {status:'success', text:'Employee data successfully updated.', timeout:true}); $location.path('/employees'); }, function(e) { // handle the error } ); } }]); 

An additional bonus to translating properties is that changing the name of a property on either the front or back end does not have to affect the other. You’ll notice in the above GettersAndSetters call, the title property syncs with the position column on the server, and the startDate syncs with the dateOfHire column. If at some time in the future, the team decides to rename a column in a Parse table, you don’t have to update any of your code on the front end using this service, but instead only update the GettersAndSetters array to the new column name. Likewise, if you prefer to name the property something special on the front end, you don’t need to change the actual column name.

4. Customizable hooks

Though our repository adds some really great, basic methods, it’s not truly powerful until it can allow a developer to customize the exact way each of our queries runs and allow a couple layers where custom code can be inserted before or after CRUD methods are run or instead of the basic CRUD functionality.

There are infinitely many ways to handle this basic idea, but here’s the basics we specifically wanted to cover.

Hook Does What Run Where Information Needed
queries Create an array of custom query parts. all(), count(), get() Needs an array of query strings.
beforeSave Allow a mutation of data on an object before it is saved to the server. save() A closure to run before the entity is saved.
afterSave Allow a closure of functionality to be run after saving an entity. save() A closure to run after the entity is saved.
softDelete When an object is softDeleted, the record is not actually deleted, but designated as “deleted”. delete() (executed), all() (recognized), count() (recognized) A boolean of whether or not to use soft deleting, and a column to save to
logMessage An activity record is saved, using the message provided. save(), delete() A string message to pass into the new activity entity.

A few things here:

  1. The reason we opted to use a local version of hooks like beforeSave and afterSave rather than using Parse’s cloud code is because there were often times in which we didn’t want objects to be globally changed on save. Because we were building a CMS, there were certain mutations that should only be made because the user would always be an admin. Additionally, this technique allowed us to create these hooks without touching cloud code. Though cloud code is still JavaScript, it is a separate “application” and may not necessarily share a codebase. It’s better to control what we can than to assume the cloud code will do exactly what we want. To have a proper separation of concerns, our repository shouldn’t even know cloud code functions exist.
  2. Soft deleting is the practice in which you don’t want to actually delete a record, but you still want to recognize when an entity is no longer supposed to be aggregated in normal queries. We needed this in several situations because the team members working on the Android and iOS platforms wanted a quick and easy way to determine if any entities of a given class were deleted via the CMS without having to compare every entity. By default, our softDelete feature saves a new DateTime to the column specified as the softDeleteColumn. Look ahead at the code sample for exact details.
  3. For our specific app, we wanted to keep track of user activity that changed data. An easy way to handle this is to create a UserActivity table in Parse in which we can describe a given action, the class on which it occurred, the entity class type, and the user to did the action. This polymorphic relation is inserted with a separate activityService. This additional service is injected into the parseInit service and contains a single method, log that saves a new entry every time a save() or delete() method is called. Outside of the repositories, we also use this activity service to save a new entry whenever a user logs into the CMS.
  4. None of the methods listed above are required for a repository. Any point that can use a hook checks if one exists, and if it doesn’t find one it executes as normal.

The proposed way to handle hooks was to pass in a config object when creating a repository with properties named after each method wanted to be hooked into. Below is an example usage.

 var Employees = parseInt.CreateRepository('Employees', { 'all':{ 'queries':['query.ascending("lastName");', 'query.limit(1000)', 'query.include("insurance");'] }, 'save':{ 'logMessage':'Updated employee', 'beforeSave': function(obj) { // Format phone numbers to (###) ###-#### var s = obj.phone.replace(/\D/g, ''); obj.phone = '(' + s.substring(0, 3) + ') ' + s.substring(3, 3) + '-' + s.substring(6); } }, 'delete':{ 'soft':true, 'softDeleteColumn':'deletedAt', 'logMessage':'Deleted employee' } }); 

Implementing these hooks was fairly easy and straightforward.

 all: function() { var defer = $q.defer(); var query = new Parse.Query(objectClass); if(options.hasOwnProperty('all') && options.all.hasOwnProperty('queries')) { for(var i = 0; i < options.all.queries.length; i ++) { eval(options.all.queries); } }else { query.limit(1000); } query.find({ success: function(results){ defer.resolve(results); }, error: function(arError) { defer.reject(arError); } }); return defer.promise; }, count: function() { var defer = $q.defer(); var query = new Parse.Query(objectClass); query.ascending('objectId'); if(options.hasOwnProperty('delete') && options.delete.hasOwnProperty('softDeleteColumn')) { query.doesNotExist(options.delete.softDeleteColumn); } if(options.hasOwnProperty('count') && options.count.hasOwnProperty('queries')) { for(var i = 0; i < options.count.queries.length; i++) { eval(options.count.queries); } } query.count({ success: function(result) { defer.resolve(result); }, error: function(e) { defer.reject(e); } }); return defer.promise; }, save: function(obj) { var defer = $q.defer(); if(options.hasOwnProperty('save') && options.save.hasOwnProperty('beforeSave')) { options.save.beforeSave(obj); } obj.save(null, { success: function(result) { defer.resolve(result); if(options.hasOwnProperty('save') && options.save.hasOwnProperty('afterSave')) { options.save.afterSave(result); } if(options.hasOwnProperty('save') && options.save.hasOwnProperty('logMessage')) { $activity.log( options.save.logMessage, className, result.id ); } }, error: function(object, error) { defer.reject(error); } }); return defer.promise; }, delete: function(obj) { var defer = $q.defer(); if(options.hasOwnProperty('delete') && options.delete.hasOwnProperty('soft') && options.delete.soft === true) { if(!options.delete.hasOwnProperty('softDeleteColumn')) { console.log('No soft delete column defined for the ' + className + ' class.'); } obj.set(options.delete.softDeleteColumn, new Date()); obj.save(null, { success: function(result) { defer.resolve(result); if(options.delete.hasOwnProperty('logMessage')) { $activity.log( options.delete.logMessage, className, result.id ); } }, error: function(error) { defer.reject(error); } }); }else{ obj.destroy(null, { success: function(result) { defer.resolve(); if(options.hasOwnProperty('delete') && options.delete.hasOwnProperty('logMessage')) { $activity.log( options.delete.logMessage, className, result.id ); } }, error: function(error) { defer.reject(error); } }); } return defer.promise; } 

Conclusion

When initially tackling the problem of building an Angular app to run off Parse, it seemed like a very taunting task. Yet, with some thorough planning and code abstraction with the repository pattern, we were able to craft a codebase that not only works with Parse, but allows a repository level that can be created for any back-end store. Check out our public repository for final code and examples here.