POSTS

Node Active Record Callbacks

One of the design patterns that we’ve been using throughout the New Visions’ Data Portal API is modeled after Rails’ Active Record Callbacks.

The basic idea is that when instances of a given model are created, updated, deleted, etc. you have these hooks that allow you to take additional actions:

Active Record callbacks allow you to attach code to certain events in the life-cycle of your models. This enables you to add behavior to your models by transparently executing code when those events occur

Some of the available callbacks include:

  • before_update
  • after_update
  • after_create
  • after_save

So you can hook into the before_update callback, before the model instance is updated in the database. And you can hook into the after_update callback after it is updated.

Get it?

The after_save callback is called both after an object is updated and after it is created.

Note: There are many other active record callbacks in Rails, but we’ve only adapted this small subset to suit our purposes. And if you’ve never developed in Rails (like me), the documentation on Active Record is worth a quick read.

How we are using it

I’ve found this pattern to be incredibly useful as our application has grown and become increasingly complex. It’s often necessary to run a bunch of different business logic in response to documents of one type being created, updated, deleted, etc.

For example, we were recently asked to implement a change-log feature, whereby whenever any document across a subset of collections is edited we capture who made the edit, when they made it, and what the change was (old and new values).

NOTE: Because we are using MongoDB we don’t yet have the equivalent of a SQL Trigger (thought it may be coming very soon).

Generating change logs via afterUpdate callback

Say we have a ChangeLog class responsible for generating logs that looks something like:

class ChangeLog extends BaseModel {
  static async createChangeLog( { diffs, editedBy, collectionName, docId } ) {
    const changeLog = new this({
      diffs,
      editedBy,
      editedAt: new Date().toISOString(),
      collectionName
      docId
    });
    await changeLog.validateAndUpdate();
  }  
}

Then, in our Student class we can do something like this:

class Student extends BaseModel {
  constructor(studentPOJO) {
    super(studentPOJO);
    this.registerAfterUpdateCallback({
      callbackName: 'createChangelog',
      callback: this.createChangelog
    });
  }

  createChangelog (patch, { updatingUser }) {
    const ChangeLog = require('./models/ChangeLog');
    try {
      await ChangeLog.createChangeLog({ 
        diffs: this.generateDiffs(), 
        editedBy: updatingUser, 
        collectionName: this.collectionName, 
        docId: this._id
      });
    } catch (err) {
      LogError(err);
    }
  }
}

Our BaseClass does the work of calling any callbacks that have been registered. It looks like this:

class BaseModel {
  constructor(pojo) {
    Object.assign(this, pojo);
    this._beforeUpdateCallbacks = [];
    this._afterUpdateCallbacks = [];
  }

  async update(patch, options) {
    await this.runBeforeUpdateCallbacks(patch, options);
    // Mock DB update
    const result = await mockDbUpdate({ _id: this._id }, patch);
    await this.runAfterUpdateCallbacks(patch, options);
    return result;
  }

  registerAfterUpdateCallback({ callbackName, callback }) {
    // Require callbackName to help with debugging
    this._afterUpdateCallbacks.push({ callbackName, callback });
  }

  async runAfterUpdateCallbacks(patch, options) {
    for (const { callbackName, callback } of this._afterUpdateCallbacks ) {
      try {
        await callback(patch, options);
      } catch (err) {
        // Append the callback name to help with debugging
        err.callbackName = callbackName;
        throw err;
      }
    }
  }

And that’s it!

What’s great about this pattern is that it’s super extensible, and we can abstract everything that we want to do before/after a given action into the class we’re acting upon. Our endpoint handlers, for example, just need to make the update and don’t need to know anything about change logs, etc.

A big thanks to Andy Fiedler for suggesting this pattern and helping with its early implementation.

And here is a github repo with a working code example.

comments powered by Disqus