mongoscope-client
Version:
245 lines (223 loc) • 6.45 kB
JavaScript
/**
* ## Backbone.js
*
* Objects you can extend Backbone Models and Collections from that
* makes using mongoscope ridiculously simple.
* ```javascript
* var mongoscope = require('mongoscope-client'),
* Backbone = require('backbone'),
* Mackbone = mongoscope.adapters.Backbone,
* Model = Backbone.Model.extend(Mackbone.Model),
* Collection = Backbone.Model.extend(Mackbone.Collection);
* ```
*/
var getOrCreateClient = require('../client'),
types = {ns: require('mongodb-ns'), uri: require('mongodb-uri')},
debug = require('debug')('mongoscope-client:backbone');
/**
* @ignore
*
* Shim for `_.result()`.
*/
var _result = function(resource, key){
return (typeof resource[key] === 'function') ? resource[key]() : resource[key];
};
/**
* ### property: `mongodb`
*
* Allows models to be fully self contained. As per the rest of the Backbone
* conventions, can be a literal value or function that returns the value.
* Supports extended URI scheme so you can include a collection name.
*
* ```javascript
* var Ticket = Mackbone.Model.extend({
* mongodb: 'localhost:27017/xgen.jira'
* });
*
* var Ticket = Mackbone.Model.extend({
* mongodb: function(){
* return [
* Config.MONGO_HOST, ':', Config.MONGO_PORT, '/',
* Config.JIRA_DB, '.', Config.JIRA_COLLECTION
* ];
* }
* });
* ```
*
* Also allows getting crazy with the cheesewiz:
*
* ```javascript
* // Read from my local copy of jira for searching because it's expensive.
* var Tickets = Collection.extend({
* mongodb: 'mongodb://localhost:27017/xgen.jira',
* model: Ticket
* });
*
* // CRUD to the kernel cluster though.
* var Ticket = Model.extend({
* idAttr: '_id',
* mongodb: 'mongodb://kernel-jira.mongo-internal.com/xgen.jira-crud'
* });
* ```
*/
var _mongodb = function(resource){
var uri = _result(resource, 'mongodb');
if(!uri) return undefined;
if(uri.indexOf('mongodb://') !== 0) uri = 'mongodb://' + uri;
var info = types.uri.parse(uri), ns = null;
var mongodb = info.hosts[0].host + ':' + info.hosts[0].port;
if(info.database && !resource.url){
ns = types.ns(info.database);
if(ns.collection){
resource.url = '/collections/' + ns.toString() + '/find';
}
}
return mongodb;
};
function getClient(model){
var opts = {};
if(model) opts.mongodb = _mongodb(model);
return getOrCreateClient(opts);
}
/**
* `Backbone.sync` compatible handler to do the right thing with
* `mongoscope-client`.
*/
function sync(method, model, options){
var url = options.url || _result(this, 'url'),
client = options.client || this.mongoscope(options),
ender = function(err, res){
if(err){
return options.error(err);
}
options.success(res);
};
// @todo: CRUD is now possible so should add it. Something like:
// ```javascript
// if(client.isResource(url)){
// var resource = client.getResource(url);
// debug('Calling ' + resource.type + ' ' + method);
// if(method === 'read' && options.all){
// return resource.createReadStream(params).pipe(concat()).on('end', ender);
// }
// return resource[method](params, ender); // Client side error if unsupported method.
// }
// ```
if(method !== 'read'){
throw new Error('mongoscope is readonly, so create, update, ' +
'patch, and delete sync methods are not available.');
}
if(!url){
throw new Error('A "url" property or function must be specified');
}
var params = {}, docs = [];
// Filter backbone specific options out to get params to pass
// to the mongoscope-client call.
Object.keys(options).map(function(k){
if(['error', 'success', 'client', 'parse'].indexOf(k) === -1){
params[k] = options[k];
}
});
if(method === 'read'){
if(!options.all){
// Normal api call.
return client.get(url, params, ender);
}
// Some calls support read streams but aren't really resources
// and they have a slightly different api. Create a read stream,
// concat all of its data, hit the callback.
return client.get(url, params)
.on('error', ender)
.on('data', function(doc){docs.push(doc);})
.on('end', function(){
ender(null, docs);
});
}
}
/**
* @ignore
*/
var Base = {
mongoscope: function(opts){
opts = opts || {};
if(!opts.mongodb && !opts.seed){
opts.mongodb = _mongodb(this);
}
var _page;
if(typeof window !== 'undefined'){
_page = 'http://' + window.location.hostname + ':' + window.location.port;
}
opts.endpoint = opts.endpoint || opts.instanceId || _page;
return getOrCreateClient(opts);
}
};
/**
* Mixins for `Backbone.Model`.
*/
module.exports.Model = {
sync: sync,
mongoscope: Base.mongoscope
};
/**
* Mixins for `Backbone.Collection`.
*/
module.exports.Collection = {
sync: sync,
mongoscope: Base.mongoscope
};
/**
* Mixins for streams like log. Impress your friends with this one simple
* trick to tail a mongo log:
*
* ```javascript
* var Log = Backbone.Collection.extend({ReadableStream}).extend({
* url: '/log'
* });
* var LogView = Backbone.View.extend({
* initialize: function(){
* this.log = new Log().on('sync', this.update, this).subscribe();
* },
* update: function(newLines){
* newLines.map(this.render.bind(this));
* },
* render: function(lineData){
* this.$el.append(this.tpls.line(lineData));
* }
* });
* ```
*/
module.exports.ReadableStream = {
mongoscope: Base.mongoscope,
subscription: null,
subscribe: function(){
if(this.subscription !== null){
debug('already have a subscription', this.subscription);
return this;
}
var self = this,
client = this.mongoscope(),
url = _result(this, 'url');
this.subscription = client.get(url)
.on('error', function(err){
self.trigger('error', err, self);
})
.on('data', function(data){
if (!self.set(data)) return false;
self.trigger('sync', self, data);
});
// If the client context changes, move our subscription.
this.subscription.client.on('change', function(){
self.unsubscribe().subscribe();
});
return this;
},
unsubscribe: function(){
if(this.subscription === null){
debug('already unsubscribed');
return this;
}
this.subscription.close();
this.subscription = null;
return this;
}
};