Skip to content

Brainstem API adapter for Backbone complete with relational models

License

Notifications You must be signed in to change notification settings

mavenlink/brainstem-js

Repository files navigation

Brainstem.js

Brainstem.js is a companion library for Backbone.js that makes integration with Brainstem APIs a breeze. Brainstem.js adds an identity map and relational models to Backbone.

Brainstem is designed to power rich APIs in Rails. The Brainstem gem provides a presenter library that handles converting ActiveRecord objects into structured JSON and a set of API abstractions that allow users to request sorts, filters, and association loads, allowing for simpler implementations, fewer requests, and smaller responses.

build status npm version npm downloads Gitter

Why Brainstem.js?

  • Speaks natively to Brainstem APIs
  • Provides a caching layer to avoid loading already-available records
  • Adds relational models in Backbone, allowing you to setup has_one, has_many, and belongs_to relationships
  • Supports Brainstem association side-loading of multiple objects for fast, single-request workflows
  • Interprets the Brainstem server API results array and hashes for you, abstracting away the Brainstem JSON protocol

Installation

NPM

npm install --save brainstem-js

Usage

Models and Collections

Brainstem.js models and collections behave very similarly to Backbone models and collections. However, in Brainstem.js models have the ability to specify associations that map to other Brainstem models in the StorageManager. These associations leverage the power of the Brainstem server API to facilitate side-loading related data in a single fetch.

Sub-class Brainstem.js collections and models for each Brainstem server endpoint to map client-side Brainstem.js models to your Brainstem server-side models.

Model associations

Assocations are defined as a map in the class property associations to declare the mapping between association names and StorageManager collections where the data can be located.

  • Strings inidicate has_one or belongs_to relationships
  • Arrays with a single item indicate has_many relationships
  • Arrays with multiple items indicate polymorphic belongs_to relationships

Examples

CommonJS

Model:

BrainstemModel = require('brainstem-js').Model;

Post = BrainstemModel.extend({
  paramRoot: 'post',
  brainstemKey: 'posts',
  urlRoot: '/api/v1/posts'
}, {
  associations: {
    user: 'users', // has_one
    comments: ['comments'], // has_many
    account: 'accounts', // belongs_to
    parent: ['category', 'post'] // belongs_to (polymorphic)
  }
});

module.exports = Post;

Collection:

BrainstemCollection = require('brainstem-js').Collection;
Post = require('./models/post');

Posts = BrainstemCollection.extend({
  model: Post,
  url: '/api/v1/posts'
});

module.exports = Posts;
Vanilla JavaScript

Model:

Application.Models.Post = Brainstem.Model.extend({
  paramRoot: 'post',
  brainstemKey: 'posts',
  urlRoot: '/api/v1/posts'
}, {
  associations: {
    user: 'users', // has_one
    comments: ['comments'], // has_many
    account: 'accounts', // belongs_to
    parent: ['category', 'post'] // belongs_to (polymorphic)
  }
});

Collection:

Application.Collections.Posts = Brainstem.Collection.extend({
  model: Application.Models.Post,
  url: '/api/v1/posts'
});

StorageManager

The Brainstem.js StorageManager is the data store in charge of loading data from a Brainstem API as well as managing cached data. The StorageManager should be set up when your application starts.

Use the StorageManager addCollection API to register Brainstem.js collections that map to your Brainstem server API endpoints.

storageManager.addCollection([brainstem key], [collection class])

Note: The StorageManager is implemented as a singleton. The StorageManager instance can be obtained using StorageManager.get(). Once instantiated the manager instance will maintain state and cache throughout the duration of your application's runtime.

Examples

CommonJS
StorageManager = require('brainstem-js').StorageManager;
Users = require('./collections/users');
Posts = require('./collections/posts');

storageManager = StorageManager.get();
storageManager.addCollection('users', Users);
storageManager.addCollection('posts', Posts);
storageManager.addCollection('comments', Comments);
Vanilla JavaScript
Application = {};

Application.storageManager = Brainstem.StorageManager.get();
Application.storageManager.addCollection('users', Application.Collections.Users);
Application.storageManager.addCollection('posts', Application.Collections.Posts);
Application.storageManager.addCollection('comments', Application.Collections.Comments);

Note: all following examples assume a CommonJS environment, however the same functionality applies to vanilla JavaScript environments


Fetching data

Brainstem.js extends the Backbone fetch API so requesting data from a Brainstem API should be familiar to fetching data from any RESTful API using just Backbone.

Models

In addition to basic REST requests, the Brainstem.js model fetch method supports an include option to side-load associated model data.

Example
Post = require('./models/post');

new Post({ id: 1 })
  .fetch({ include: ['user', 'comments'] })
  .done(/* handle result */)
  .fail(/* handle error */);

Brainstem also supports requesting deeply nested associations. Under the hood, Brainstem makes recursive requests for each layer of associations. There are three ways to make this kind of request, using JSON-like Objects, Backbone Collections or BrainstemParams. Using Backbone Collections or BrainstemParams enables expressing additional options for the nested includes / associations.

JSON-like Object Example
Post = require('./models/post');

new Post({ id: 1 })
  .fetch({
    include: [
      'user',
      { comments: ['replies', 'ratings'] }
    ]
  })
  .done(/* handle result */)
  .fail(/* handle error */);
Backbone Collection Example
Post = require('./models/post');
Comments = require('./collections/comments')

var comments = new Comments(null, {
  include: ['replies', 'ratings'],
  filters: { only_by_user_id: 1 }
});

new Post({ id: 1 })
  .fetch({ include: ['user', { comments: comments }] })
  .done(/* handle result */)
  .fail(/* handle error */);

In this example we are able to specify a filter for the comments association. With the filter, we can request comments for the post filtered by user_id 1. Here, comments needs to be the name of the association

brainstemParams Example
Post = require('./models/post');

var commentsParams = {
  brainstemParams: true,
  include: ['replies', 'ratings'],
  filters: { only_by_user_id: 1 }
};

new Post({ id: 1 })
  .fetch({ include: ['user', { comments: commentsParams }] })
  .done(/* handle result */)
  .fail(/* handle error */);

Similar to the Backbone Collection example, again with brainstemParams, we can include additional options for any association.

Collections

In addition to basic REST requests, the Brainstem.js model fetch method supports additional Brainstem options:

  • Pagination using page and perPage, or offset and limit
  • Filtering using filters object
  • Ordering user order string
Example
Posts = require('./collections/posts');

new Posts().fetch({
  page: 1,
  perPage: 10,
  order: 'date:desc',
  filters: {
      title: 'collections',
      description: 'fetching'
    }
  })
  .done(/* handle result */)
  .fail(/* handle error */);

Accessing Model Associations

Example
Post = require('./models/post');
User = require('./models/user');

var user;
var comments;
var reviews;
var userCollection = new User([], { include: 'reviews' });

new Post({ id: 1 }).fetch({ include: ['comments', { user: userCollection }] })
  .done(function (post) {
  	user = post.get('user');
  	reviews = user.get('reviews');
  	comments = post.get('comments');
  });

console.log(user);
// User [BackboneModel]

console.log(comments);
// Comments [BackboneCollection]

console.log(reviews);
// Reviews [BackboneCollection]

Manipulating Collections

Filter Scoping

Brainstem.js collections provide a filter scoping mechanism that allows a base scope to be defined either by providing base filter and order options to the Brainstem.js Collection constructor, or by passing said options to the first fetch call.

The collection can be restored to the original base scope by simply invoking fetch on the collection without passing any options.

The base scope is stored in the firstFetchOptions property on the collection and the current filter scope is stored in the lastFetchOptions property on the collection.

Example
Posts = require('./collections/posts');

posts = new Posts([], { filters: { account_id: 1 } });

console.log(posts.firstFetchOptions);
// { filters: { account_id: 1 } }

// Base scope fetch

posts.fetch()
  .done(function (posts) {
  	console.log(posts);
  	// Posts [Brainstem Collection] – all posts filtered by `account_id`
  });

// Further scoped fetch

posts.fetch({ filters: { user_id: 1 }, order: 'updated_at:desc' })
  .done(function (posts) {
  	console.log(posts);
  	// Posts [Brainstem Collection] – all posts filtered by `account_id` and `user_id` ordered by `updated_at`
  });

// Restoring base scope

posts.fetch()
  .done(function (posts) {
  	console.log(posts);
  	// Posts [Brainstem Collection] – all posts filtered by `account_id` in default order
  });

Pagination

Backbone.js collections support pagination natively. The default page size is 20.

As mentioned, collections support both page and perPage options or offset and limit options. If no pagination options are specified, collections will default to page and perPage options. The offset and limit paginations options can be substitued in any of the following examples.

Supported pagination methods:

  • getNextPage()
  • getPreviousPage()
  • getFirstPage()
  • getLastPage()
  • getPage([page number])
Example
Posts = require('./collections/posts');

posts = new Posts([], page: 1, perPage: 10);

console.log(posts.firstFetchOptions);
// { page: 1, perPage: 10 }

posts.fetch()
  .done(function (posts) {
  	console.log(posts.pluck('id');
  	// [1, 2, 3, 4, 5, 6, 7, 8, 9 , 10]
  });

posts.getNextPage()
  .done(function (posts) {
  	console.log(posts.pluck('id');
  	// [11, 12, 13, 14, 15, 16, 17, 18, 19 , 20]
  });

posts.getPreviousPage()
  .done(function (posts) {
  	console.log(posts.pluck('id');
  	// [1, 2, 3, 4, 5, 6, 7, 8, 9 , 10]
  });

// The Backbone.Collection `add` option can be utilized for "load more" style pagination

posts.getNextPage({ add: true })
  .done(function (posts) {
  	console.log(posts.pluck('id');
  	// [1, 2, 3, 4, 5, 6, 7, 8, 9 , 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 , 20]
  });

Development

We're always open to pull requests!

Dependencies

Development Environment

yarn install

yarn link usages

To use this module as a symbolic link, run:

yarn install

which runs yarn prepublish.

Running Specs

To run the specs on the command line, run:

yarn test

To run the specs in a server with live code reloading and compilation:

yarn test-watch

Publishing

  1. Use an NPM account (with 2FA enabled) on the Mavenlink organization
  2. Run yarn install to update dependencies
  3. Modify the semantic version appropriately as a commit
  4. Make a pull request with said changes
  5. If using a pre-release tag, run yarn publish to publish the alpha version to NPM
  6. Request a review (and merge accordingly)
  7. Run yarn publish on master to publish the latest release version to NPM

Note: it might be worthwhile to publish a prerelease tag during normal development to test changes sooner rather than later.

License

Brainstem and Brainstem.js were created by Mavenlink, Inc. and are available under the MIT License.