Model Schema - gregerolsson/booster GitHub Wiki

It is not uncommon for models to have nested models and collections which are based on attributes in the model that are received from the server. Consider the following data structure:

post = {
  id: 123,
  title: "A blog post",
  comments: [{
    by: 123,
    at: "2011-12-16T13:27:11.146Z"
    text: "Nice post"
  }]
}

Likely we have a Backbone.Model representing blog posts, a Backbone.Model representing comments, and a Backbone.Collection representing collections of comments.

When instantiating a blog post model over the post data, and invoking get('comments') you will get the actual array of objects stored in the comments attribute, not a Backbone.Collection. There are several ways of dealing with this outside of Backbone, and in Booster we have chosen to supply a mixin that allows you to define a small schema describing how your model is built up:

app/assets/javascripts/models/post.js.booster

var schema  = require('booster/support/schema'),
    comment = require(./comment);

exports.Model = Backbone.Model.extend({
  schema: {
    comments: comment.Collection
  }
});

_.extend(exports.Model.prototype, schema.mixin());

Applying the schema mixin to the model will make the internal get and set methods aware of any schema definition in the model. When invoking get to fetch and attribute from the model, the schema is examined and if an entry for the requested attribute is found, it will perform a conversion, among other things.

So the following code will work:

app/assets/javascripts/examples/an_example.js.booster

var post  = require('models/post');

var data = {
  id: 123,
  title: "A blog post",
  comments: [{
    by: 123,
    at: "2011-12-16T13:27:11.146Z"
    text: "Nice post"
  }]
}

model = new post.Model(data);
console.log(model.get('comments').map(function(comment) {
  return comment.get('text');
}));

Here when invoking get('comments'), get recognizes that the schema says that the comments attribute should be converted into a comment collection. The comments array is automatically converted into a comments collection which is memoized inside the post instance -- invoking get('comments') again on the same post model will return the same collection instance.

Invoking model.toJSON() in the example above will also do the right thing; the comments array in the resulting object will come from the memoized comments collection, not the old comments attribute in the original model data. In fact, when the comments array is converted to a collection the actual attribute value is removed from the model since the collection takes ownership of the data and there is no need to keep a potentially stale copy of it around.

More Advanced Schemas

The simplest form of defining a schema is to simply list the direct conversion you want for an attribute, as in the example above:

schema: {
  comments: comment.Collection
}

The Booster schema mixin can do more than that, though, and mimics the API used in Mongoose for those that have used it together with Node.js to access MongoDB databases. Below are a few example of what is supported.

Instructing the toJSON Behavior

schema: {
  comments: { type: comment.Collection, serialize: false }
}

Instructs toJSON not to include the comments attribute.

Consider loading a blog post from a REST-API which is backed by a relational database where comments are stored in a separate table. It is not unlikely that the API exposes the resources similar to the following to deal with posts and comments:

  • GET /posts
  • POST /posts
  • GET /posts/:id
  • PUT /posts/:id
  • POST /posts/:id/comments
  • PUT /comments/:id

Fetching an individual post through GET /posts/123 will probably include all the comments for that post which are resolved in the API through a join. Adding a new comment to the post is here achieved by a POST /posts/123/comments, not by adding a comment model to the comments collection of the post and saving the whole post. In fact PUT /posts/123 is likely to only deal with the post attributes like title and body, but still GET /posts/123 includes the comments since that is probably what the clients of the API need from the request.

This is where the serialize attribute in the schema comes in. When performing a save on the post model (which performs a toJSON internally) the comments array will not be sent to the server which is what we want for this particular REST-API.

Another example:

exports.Model = Backbone.Model.extend({
  schema: {
    comments: { type: comment.Collection, serialize: id }
  }
);

function id(model) {
  return model.id;
}

Instructs toJSON to invokde the id function for every model in the collection.

If your REST-API is backed by a NoSQL database like MongoDB and you have a document that references other top-level documents through an array of ObjectIDs the example above will fit your bill. The comments collection nested in the post will be mapped using the given function id when toJSON is invoked, effectively transforming it to an array of IDs.

All of the above also works for nested models:

exports.Model = Backbone.Model.extend({
  schema: {
    by:       { type: user.Model, serialize: id },
    comments: { type: comment.Collection, serialize: id }
  }
);

function id(model) {
  return model.id;
}

Validation

To be defined.