Atomic updates with AngularFire

I’ve been working lately with Firebase and I find it impressive. But I know from my experience when you are new to a framework or library it’s not easy to piece it together.

Firebase introduced atomic multi-location writes, which play really well with the concept of denormalization as we can update multiple locations at the same time.

So I want to share here how I’ve implemented atomic updates using AngularFire.

Data Structure

The first step working with Firebase is thinking about the data structure.

In this case we are going to model a blog. When we write a blog post we want to add the content and tag it. So when a user visits our blog and clicks on a tag, we only display the tagged posts.

I see at least two ways of implementing this functionality:

  • Go through all the posts and filter them, retrieving only those that contain the tag that we are looking for.
  • Keep a list of tagged posts under each tag and retrieve all of them at once.

The second approach seems to me more scalable and the data would look as seen in the following image.

Firebase_posts_tags

In this scenario, when we create a new blog post we need to add the reference of the new post to each tag where it belongs to.

Implementation

I’m sure there are a thousand ways to do it but I decided to extend the $firebaseArray so I can create my custom atomic methods and transform the data before is saved, while keeping my code clean and organized.

Why do I need to transform the data? Because when the user fills the form and select one or multiple tags, we have an array of tag objects. But what we need to save to Firebase is a list of tag indices defining the relationship.

function parsePostData(postData) {
  var post = {
    name: postData.name,
    content: postData.content
  };
  post.tags = postData.tags.reduce(function(tags, tag) {
    tags[tag.$id] = {
      name: tag.name
    };
    return tags;
  }, {});
  return post;
}

Then we need to do what David East calls client-side fan-out, creating an object with all the paths that we need to update.

As in Firebase setting a path to null removes the data, I can reuse this same method for deletions.

function fanOutPost(postId, post, isDelete) {
  var fanOutObj = {};
  fanOutObj['posts/' + postId] = (isDelete) ? null : post;
  angular.forEach(post.tags, function(tag, tagId) {
    fanOutObj['tags/' + tagId + '/posts/' + postId] = (isDelete) ? null : true;
  });
  return fanOutObj;
}

Finally we can implement the atomic save method in our custom service extending $firebaseArray.

Notice we need to push first an empty object so Firebase creates a reference to the new post that we can use as a key for the fan-out.

function PostFactory($firebaseArray) {
  return $firebaseArray.$extend({
  
    atomicSave: function(postData) {
      var ref = this.$ref().parent();
      var newPostRef = this.$ref().ref().push();
      var newPostKey = newPostRef.key();
      
      var post = parsePostData(postData);
      var postFanOut = fanOutPost(newPostKey, post);
      ref.update(postFanOut, function(error) {       
        if (error) {
          console.log(error);
        }
      });
    }
    
  });
}

As code is usually easier to understand when seen, I’ve created a demo app where you can play adding and deleting posts. There I also use the NormalizedCollection of the fantastic Firebase-util to join posts and tags.

Although I haven’t included updates because I consider it really depends of the use case, in mine I’m issuing an atomic delete followed by an atomic save with the old key. What would you use?

Leave a comment