A polystore ORM for Node.js and the browser
npm install backbone-orm
BackboneORM was designed to provide a consistent, polystore ORM across Node.js and the browser.
It was inspired by other great software and provides:
Other great things:
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);
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);
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');
}
});
Backbone-ORM allows you to create relations to use with any backend
Naming conventions are as follows:
belongsTo
: A foreign key to the related model will be placed on this modelhasOne
: A foreign key to this model is placed on the related modelhasMany
: A foreign key to this model is placed on the related model, and the relation will return a set of models when accessedmanyToMany
: A join table is automatically created with foreign keys to each side of the relation.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');
});
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}]; }
}
});
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) {});
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 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
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);
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'});
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}}
};