A polystore ORM for Node.js and the browser

Install

npm install backbone-orm

Get the client JavaScript

Download Latest (0.7.14)

Introduction

BackboneORM was designed to provide a consistent, polystore ORM across Node.js and the browser.

It was inspired by other great software and provides:

  • Node.js-style callbacks and streams for a familiar asynchronous programming style
  • MongoDB-like query language to easily slice-and-dice your data
  • a REST controller enabling browser search bar queries and an optional paging format like CouchDB

Other great things:

  • it provides a JSON-rendering DSL
  • it solves the dreaded Node.js circular dependencies problem for related models
  • it is compatible with Knockback.js
  • it parses ISO8601 dates automatically
  • BackboneMongo provides a CouchDB-like '_rev' versioning solution
  • BackboneREST provides authorization middleware hooks and emits REST events

Getting Started

BackboneORM lets you use basic backbone models. To make them all ORM-y, we override the Backbone sync property with a BackboneORM Sync. We pass the Sync our model class.

BackboneORM will patch the provided model through the Sync constructor. Here we're just using the MemorySync for in-memory models, so we don't need to specify an urlRoot. Usually we will, so it's here for clarity.

class Project extends Backbone.Model
  urlRoot: '/projects'
  sync: require('backbone-orm').sync(Project)
var Project = Backbone.Model.extend({
  urlRoot: '/projects'
});
Project.prototype.sync = require('backbone-orm').sync(Project);

Persisting models

Now, in-memory models are all well and good, but that's not what we're here for. To persist our models we need to provide a bit more information, namely filling in the urlRoot property.

Here we set our models urlRoot to a connection string specifying our mongodb database and collection.

Since we're using MongoDB here, we'll use a Sync that knows how to persist to Mongo: MongoSync.

class Project extends Backbone.Model
  urlRoot: 'mongodb://localhost:27017/projects'
  sync: require('backbone-mongo').sync(Project)
var Project = Backbone.Model.extend({
  urlRoot: 'mongodb://localhost:27017/projects'
});
Project.prototype.sync = require('backbone-mongo').sync(Project);

Callbacks

Use Node or Backbone style callbacks as you please (even on the client).

save, destroy and fetch work just like you expect from plain Backbone.

# Node style
project.save (err, project) ->
  return console.log 'Oh no an error' if err
  console.log "We're saved!"

# Backbone style
project.save {
  success: (project) ->
    console.log "We're saved!"
  error: (project, err) ->
    console.log 'Oh no an error'
}
// Node style
project.save(function(err, project) {
  if (err) {
    return console.log('Oh no an error');
  }
  return console.log("We're saved!");
});

// Backbone style
project.save({
  success: function(project) {
    return console.log("We're saved!");
  },
  error: function(project, err) {
    return console.log('Oh no an error');
  }
});

Relations

Backbone-ORM allows you to create relations to use with any backend

Relation types

Naming conventions are as follows:

  • belongsTo: A foreign key to the related model will be placed on this model
  • hasOne: A foreign key to this model is placed on the related model
  • hasMany: A foreign key to this model is placed on the related model, and the relation will return a set of models when accessed
  • manyToMany: A join table is automatically created with foreign keys to each side of the relation.

Asynchronous get

When using get on relations, if the relation has not been loaded it will be returned asynchronously via a callback. If it is already loaded it will also be returned directly.

MongoSync = require('backbone-mongo').sync

class Project extends Backbone.Model
  urlRoot: 'mongodb://localhost:27017/projects'
  schema:
    tasks: -> ['hasMany', Task]
  sync: MongoSync(Project)

class Task extends Backbone.Model
  urlRoot: 'mongodb://localhost:27017/tasks'
  schema:
    project: -> ['belongsTo', Project]
  sync: MongoSync(Task)

# Asynchronous relation loading
Project.findOne {name: 'My project'}, (err, project) ->
  # Retrieve its tasks from mongo
  project.get 'tasks', (err, tasks) ->
    # Do things with tasks

# Find a project and its tasks with `$include`
Project.findOne {name: 'My project', $include: 'tasks'}, (err, project) ->
  # Tasks are already loaded, we can just use them
  tasks = project.get('tasks')

# Find a project and its tasks using cursor with include
Project.cursor({name: 'My project'}).include('tasks').toModel (err, project) ->
  # Tasks are already loaded, we can just use them
  tasks = project.get('tasks')
var MongoSync = require('backbone-mongo').sync;

var Project = Backbone.Model.extend({
  urlRoot: 'mongodb://localhost:27017/projects'
  schema: {
    tasks: function() { return ['hasMany', Task]; }
  }
});
Project.prototype.sync = MongoSync(Project);

var Task = Backbone.Model.extend({
  urlRoot: 'mongodb://localhost:27017/tasks'
  schema: {
    tasks: function() { return ['belongsTo', Project]; }
  }
});
Task.prototype.sync = MongoSync(Task);

// Find a project
Project.findOne({name: 'My project'}, function(err, project) {
  // Retrieve its tasks from mongo
  project.get('tasks', function(err, tasks) { /* do things with tasks */ });
});

// Find a project and its tasks with `$include`
Project.findOne({name: 'My project', $include: 'tasks'}, function(err, project) {
  // Tasks are already loaded, we can just use them
  var tasks = project.get('tasks');
});

// Find a project and its tasks using cursor with include
Project.cursor({name: 'My project'}).include('tasks').toModel function(err, project) {
  // Tasks are already loaded, we can just use them
  var tasks = project.get('tasks');
});

Relation options

Sometimes we'll want to treat certain relations differently, the following options are available:

  • manual_fetch: By default BackbonORM will load related models whenever they are accessed. To turn off this behavior and manually retrieve related models, pass manual_fetch: true as an option to the relation.
  • virtual: By default, BackbonORM will ensure that changes to relations in memory are persisted to the respective store. This may result in hasOne, hasMany and manyToMany relations being loaded in order to update their foreign keys. To turn off this behavior and manually manage model relations, pass virtual: true as an option to the relation. This is particularly useful when you wish to use models that do not have a store representation (e.g. they may be on the client).
  • embed: By default, relations are given their own storage representation (collection/table/endpoint). To indicate that a model should only be saved as embedded json in a parent model pass embed: true as an option to the relation.
MongoSync = require('backbone-mongo').Sync

# manual_fetch: Tasks will not be loaded when accessing a projects tasks property
class Project extends Backbone.Model
  urlRoot: 'mongodb://localhost:27017/projects'
  schema:
    tasks: -> ['hasMany', Task, manual_fetch: true]
  sync: MongoSync(Project)

# virtual: Tasks will not have their foreign keys updated automatically if a project is deleted
#          or it has its tasks changed while some are not loaded to memory
class Project extends Backbone.Model
  urlRoot: 'mongodb://localhost:27017/projects'
  schema:
    tasks: -> ['hasMany', Task, virtual: true]
  sync: MongoSync(Project)

# embed: Tasks will be saved as an embedded document in each project
class Project extends Backbone.Model
  urlRoot: 'mongodb://localhost:27017/projects'
  schema:
    tasks: -> ['hasMany', Task, embed: true]
  sync: MongoSync(Project)
var MongoSync = require('backbone-mongo').Sync;

// manual_fetch: Tasks will not be loaded when accessing a projects tasks property
var Project = Backbone.Model.extend({
  urlRoot: 'mongodb://localhost:27017/projects',
  schema: {
    tasks: function() { return ['hasMany', Task, {manual_fetch: true}]; }
  }
});
Project.prototype.sync = MongoSync(Project);

// virtual: Tasks will not have their foreign keys updated automatically if a project is deleted
//          or it has its tasks changed while some are not loaded to memory
var Project = Backbone.Model.extend({
  urlRoot: 'mongodb://localhost:27017/projects',
  schema: {
    tasks: function() { return ['hasMany', Task, {virtual: true}]; }
  }
});

// embed: Tasks will be saved as an embedded document in each project
var Project = Backbone.Model.extend({
  urlRoot: 'mongodb://localhost:27017/projects',
  schema: {
    tasks: function() { return ['hasMany', Task, {embed: true}]; }
  }
});

MongoDB, SQL, and HTTP

Regardless of the BackboneORM variant and whether you are in the browser or on the server, you can query your models using an identical syntax.

# Find the Project with id = 123
Project.findOne {id: 123}, (err, project) ->

# Find the first Project named 'my kickass project'
Project.findOne {name: 'my kickass project'}, (err, project) ->

# Find all items with is_active = true
Project.find {is_active: true}, (err, projects) ->

# Find the items with an id of 1, 2 or 3
Project.find {id: {$in: [1, 2, 3]}}, (err, projects) ->

# A shortcut for `$in` when we're working with ids
Project.find {$ids: [1, 2, 3]}, (err, projects) ->

# Find active items in pages
Project.find {is_active: true, $limit: 10, $offset: 20}, (err, projects) ->

# Select named properties from each model
Project.find {$select: ['created_at', 'name']}, (err, array_of_json) ->

# Select values in the specified order
Project.find {$values: ['created_at', 'status']}, (err, array_of_arrays) ->

# Find active items in pages using cursor syntax (Models or JSON)
Project.cursor({is_active: true}).limit(10).offset(20).toModels (err, projects) ->
Project.cursor({is_active: true}).limit(10).offset(20).toJSON (err, projects_json) ->

# Find completed tasks in a project
project.cursor('tasks', {status: 'completed'}).sort('name').toModels (err, tasks) ->
// Find the Project with id = 123
Project.findOne({id: 123}, function(err, project) {});

// Find the first Project named 'my kickass project'
Project.findOne({name: 'my kickass project'}, function(err, project) {});

// Find all items with is_active = true
Project.find({is_active: true}, function(err, projects) {});

// Find the items with an id of 1, 2 or 3
Project.find({id: {$in: [1, 2, 3]}}, function(err, projects) {});

// A shortcut for `$in` when we're working with ids
Project.find({$ids: [1, 2, 3]}, function(err, projects) {});

// Find all items with is_active = true
Project.find({is_active: true, $limit: 10, $offset: 20}, function(err, projects) {});

// Select named properties from each model
Project.find({$select: ['created_at', 'name']}, function(err, array_of_json) {});

// Select values in the specified order
Project.find({$values: ['created_at', 'status']}, function(err, array_of_arrays) {});

// Find active items in pages using cursor syntax (Models or JSON)
Project.cursor({is_active: true}).limit(10).offset(20).toModels function(err, projects) {});
Project.cursor({is_active: true}).limit(10).offset(20).toJSON function(err, projects_json) {});

// Find completed tasks in a project sorted by name
project.cursor('tasks', {status: 'completed'}).sort('name').toModels function(err, tasks) {});

Iteration

Using each, stream, and interval, you can iterate over your models in a way that suits the problem you are trying to solve.

# Iterate through all items with is_active = true in batches of 200
Project.each {is_active: true, $each: {fetch: 200}},
  ((project, callback) -> console.log "project: #{project.get('name')}"; callback()),
  (err) -> console.log 'Done'

# Stream all items with is_active = true in batches of 200
Project.stream({is_active: true, $each: {fetch: 200}})
  .pipe(new ModelStringifier())
  .on('finish', -> console.log 'Done')

# Collect the status of tasks over days
stats = []
Task.interval {$interval: {key: 'created_at', type: 'days', length: 1}},
  ((query, info, callback) ->
    histogram = new Histogram()
    Task.stream(_.extend(query, {$select: ['created_at', 'status']}))
      .pipe(histogram)
      .on('finish', -> stats.push(histogram.summary()); callback())
  ),
  (err) -> console.log 'Done'
// Iterate through all items with is_active = true in batches of 200
Project.each({is_active: true, $each: {fetch: 200}},
  function(project, callback) {console.log('project: ' + project.get('name')); callback()},
  function(err) {return console.log('Done');}
);

// Stream all items with is_active = true in batches of 200
Project.stream({is_active: true, $each: {fetch: 200}})
  .pipe(new ModelStringifier())
  .on('finish', function() {return console.log('Done');});

var stats = [];
Task.interval({$interval: {key: 'created_at', type: 'days', length: 1}},
  function(query, info, callback) {
    var histogram = new Histogram()
    Task.stream(_.extend(query, {$select: ['created_at', 'status']}))
      .pipe(histogram)
      .on('finish', function() {stats.push(histogram.summary()); return callback();});
  },
  function(err) { return console.log('Done'); }
);

BackboneHTTP and BackboneRest

BackboneHTTP provides an interface for consuming JSON APIs through HTTP. BackboneRest provides a RESTful controller for JSON APIs from Node.js. Together, they rock!

With this simple example setup, you can iterate through Tasks on the server or in the browser and can even make ad-hoc queries from the Brower's address bar:

Assuming you Node.js app in on port 5000, you can request tasks by name:

localhost:5000/tasks?name='Bob'

or even ask for the first 10 names of names of tasks:

localhost:5000/tasks?$sort=name&$values=name&$limit=10

In the browser:
class Task extends Backbone.Model
  urlRoot: '/tasks'
  sync: require('backbone-http').sync(Task)
var Task = Backbone.Model.extend({
  urlRoot: '/tasks'
});
Task.prototype.sync = require('backbone-http').sync(Task);
On the server:
class Task extends Backbone.Model
  urlRoot: 'mongodb://localhost:27017/tasks'
  sync: require('backbone-mongo').sync(Task)

new RestController(app, {model_type: Task, route: '/tasks'})
var Task = Backbone.Model.extend({
  urlRoot: 'mongodb://localhost:27017/tasks'
});
Task.prototype.sync = require('backbone-mongo').sync(Task);

new RestController(app, {model_type: Task, route: '/tasks'});

JSON Rendering DSL

BackboneORM provides an asynchronous JSON rendering DSL that is consistent with the unified query language.

When using BackboneREST and BackboneHTTP, you can select the template by passing the $template parameter to your query.

module.exports =
  # select id and name from the task
  {$select: ['id', 'name']}

  # use a render function
  task_custom:   (model, options, callback) ->
    callback(null, _.pick(model.attributes, 'id', 'name'))

  # select properties from relationship
  project:          {$select: ['id', 'name']}

  # relationship inferred with query
  commits:          {query: {active: false}}

  # relationship with operation
  total_commits:    {key: 'commits', query: {$count: true}}
module.exports = {
  // select id and name from the task
  {$select: ['id', 'name']}

  // use a render function
  task_custom:   function(model, options, callback) {
    callback(null, _.pick(model.attributes, 'id', 'name'))
  }

  // select properties from relationship
  project:          {$select: ['id', 'name']},

  // relationship inferred with query
  commits:          {query: {active: false}},

  // relationship with operation
  total_commits:    {key: 'commits', query: {$count: true}}
};