Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

bugfixes and additions:

- made backbone-redis.js work on server without sockets
- added sorting solution
- added foreign key solution
  • Loading branch information...
commit 2bc5f44abca32adde7042d25ecd1aecf13ed8723 1 parent cc79e54
Maurice Faber authored
246 browser/backbone.redis.js
View
@@ -4,7 +4,7 @@
// For all details and documentation:
// https://github.com/sorensen/backbone-redis
-(function() {
+(function () {
// Save a reference to the global object.
var root = this;
@@ -17,7 +17,7 @@
var socket;
// Default socket event listener
- var listener = 'backbone';
+ var listener = 'message';
// Storage container for subscribed models, allowing the returning method
// calls from the server know where and how to find the model in question
@@ -31,22 +31,33 @@
var Backbone = root.Backbone;
if (!Backbone && (typeof require !== 'undefined')) Backbone = require('backbone');
- var useOnceRegistry = {};
- var useOnce = function (options) {
- var model = useOnceRegistry[JSON.stringify(options)];
+ var modelRegistry = {}, callbacksRegistry = {};
+ var useModelOnce = function (channel) {
+ var model = modelRegistry[channel];
if (!model) return;
- delete useOnceRegistry[JSON.stringify(options)];
+ delete modelRegistry[channel];
return model;
}
+ var useCallbacksOnce = function (channel) {
+ var callbacks = callbacksRegistry[channel];
+ if (!callbacks) return;
+ delete callbacksRegistry[channel];
+ return callbacks;
+ }
var registerOnce = function (model, options) {
+ var channel = options.channel;
// use the entire options hash as key
- useOnceRegistry[JSON.stringify(options)] = model;
- }
-
+ modelRegistry[channel] = model;
+ callbacksRegistry[channel] = {
+ success: options.success,
+ error: options.error
+ };
+ }
+
core = {
- //###config
- config : function(options, next) {
+ // ###config
+ config: function (options, next) {
options.io && (socket = options.io);
options.listener && (listener = options.listener);
@@ -56,100 +67,116 @@
next && next();
},
- //###process
- process : function(packet) {
- var model = packet.model,
- options = packet.options;
-
- if (!options || !options.method) {
- return;
- }
- switch(options.method) {
- case 'published' : core.published(packet); break;
- case 'subscribed' : core.subscribed(packet); break;
- case 'unsubscribed' : core.unsubscribed(packet); break;
- case 'created' : core.created(packet); break;
- case 'read' : core.read(packet); break;
- case 'updated' : core.updated(packet); break;
- case 'destroyed' : core.destroyed(packet); break;
+ // ###process
+ process: function (packet) {
+ var model = packet.model, options = packet.options;
+
+ if (!options || !options.method) { return; }
+ switch (options.method)
+ {
+ case 'published':
+ core.published(packet);
+ break;
+ case 'subscribed':
+ core.subscribed(packet);
+ break;
+ case 'unsubscribed':
+ core.unsubscribed(packet);
+ break;
+ case 'created':
+ core.created(packet);
+ break;
+ case 'read':
+ core.read(packet);
+ break;
+ case 'updated':
+ core.updated(packet);
+ break;
+ case 'destroyed':
+ core.destroyed(packet);
+ break;
}
},
// Pubsub routines
- //----------------
+ // ----------------
- //###subscribed
+ // ###subscribed
// Someone has subscribed to a channel
// Note: This method is not required to run the
// application, it may prove as a useful way to
// update clients, and it may prove to be an added
// security risk, when private channels are involved
- subscribed : function(packet) {
+ subscribed: function (packet) {
var options = packet.options;
options.finished && options.finished(packet);
},
- //###unsubscribed
+ // ###unsubscribed
// Someone has unsubscribed from a channel, see the
// note above, as it applies to this method as well
- unsubscribed : function(packet) {
+ unsubscribed: function (packet) {
var options = packet.options;
options.finished && options.finished(packet);
},
- //###published
+ // ###published
// Data has been published by another client, this serves
// as the main entry point for server to client communication.
// Events are delegated based on the original method passed,
// and are sent to 'crud.dnode.js' for completion
- published : function(packet) {
+ published: function (packet) {
var options = packet.options;
- if (!options.method) {
- return;
+ if (!options.method) { return; }
+ switch (options.method)
+ {
+ case 'create':
+ core.created(packet);
+ break;
+ case 'read':
+ core.read(packet);
+ break;
+ case 'update':
+ core.updated(packet);
+ break;
+ case 'delete':
+ core.destroyed(packet);
+ break;
}
- switch (options.method) {
- case 'create' : core.created(packet); break;
- case 'read' : core.read(packet); break;
- case 'update' : core.updated(packet); break;
- case 'delete' : core.destroyed(packet); break;
- };
+ ;
},
// CRUD routines
- //--------------
+ // --------------
- //###created
+ // ###created
// A model has been created on the server,
// get the model or collection based on channel
// name or channel to set or add the new data
- created : function(packet) {
- var data = packet.model,
- options = packet.options,
- model = Store[options.channel];
+ created: function (packet) {
+ var data = packet.model, options = packet.options, model = Store[options.channel];
// Model processing
if (model instanceof Backbone.Model) {
model.set(model.parse(data));
- // Collection processing
+ // Collection processing
} else if (model instanceof Backbone.Collection) {
if (!model.get(data.id)) model.add(model.parse(data));
}
options.finished && options.finished(data);
},
- //###read
+ // ###read
// The server has responded with data from a
// model or collection read event, set or add
// the data to the model based on channel
- read : function(packet) {
- var data = packet.model,
- options = packet.options,
- model = Store[options.channel] || useOnce(options);
+ read: function (packet) {
+ var data = packet.model, options = packet.options, channel = options.channel, model = Store[channel] || useModelOnce(channel), callbacks = callbacksRegistry[channel];
// Model Processing
if (model instanceof Backbone.Model) {
model.set(model.parse(data));
- // Collection processing
+ // Collection processing
} else if (model instanceof Backbone.Collection) {
if (_.isArray(data)) {
model.reset(model.parse(data));
@@ -157,33 +184,32 @@
model.add(model.parse(data));
}
}
+ var resp = model instanceof Backbone.Collection ? model.models : model;
+ // we still have to do the success callback
+ callbacks.success && callbacks.success(resp);
options.finished && options.finished(data);
},
- //###updated
+ // ###updated
// A model has been updated with new data from the
// server, set the appropriate model or collection
- updated : function(packet) {
- var data = packet.model,
- options = packet.options,
- model = Store[options.channel];
+ updated: function (packet) {
+ var data = packet.model, options = packet.options, model = Store[options.channel];
// Collection processing
if (model.get(data.id)) {
model.get(data.id).set(model.parse(data));
- // Model processing
+ // Model processing
} else {
model.set(model.parse(data));
}
options.finished && options.finished(data);
},
- //###destroyed
+ // ###destroyed
// A model has been destroyed
- destroyed : function(packet) {
- var data = packet.model,
- options = packet.options,
- model = Store[options.channel];
+ destroyed: function (packet) {
+ var data = packet.model, options = packet.options, model = Store[options.channel];
Store[options.channel].remove(data) || delete Store[options.channel];
options.finished && options.finished(data);
@@ -193,39 +219,39 @@
// Extend default Backbone functionality
_.extend(Backbone.Model.prototype, {
- //###publish
+ // ###publish
// Publish model data to the server for processing, this serves as
- // the main entry point for client to server communications. If no
+ // the main entry point for client to server communications. If no
// method is provided, it defaults to an 'update', which is the least
// conflicting method when returned to the client for processing
- publish : function(options, next) {
+ publish: function (options, next) {
if (!socket) return (options.error && options.error(503, model, options));
var model = this;
- options || (options = {});
+ options || (options = {});
options.channel || (options.channel = model.getChannel());
- options.method = 'publish';
- options.type = model.type;
+ options.method = 'publish';
+ options.type = model.type;
var packet = {
- model : model.toJSON(),
- options : options
+ model: model.toJSON(),
+ options: options
};
- socket.emit(listener, packet, function(response){
+ socket.emit(listener, packet, function (response) {
if (!options.silent) model.trigger('publish', model, options);
next && next(response);
});
return this;
},
-
+
// used to generate channel name from type
- getChannel : function() {
+ getChannel: function () {
var type = this.type || (this.collection ? this.collection.type : null);
return this.type + (this.id ? ':' + this.id : '');
}
});
-
+
_.extend(Backbone.Collection.prototype, {
- getChannel : function() {
+ getChannel: function () {
return this.type || (this.model && this.model.type ? this.model.type : null);
}
});
@@ -233,17 +259,19 @@
// Common extention object for both models and collections
var common = {
- //###connection
+ // ###connection
// Setting a reference to the DNode/socket connection to allow direct
// server communication without the need of a global object
- connection : socket,
+ connection: socket,
- //###subscribe
- // Subscribe to the 'Server' for model changes, if 'override' is set to true
+ // ###subscribe
+ // Subscribe to the 'Server' for model changes, if 'override' is set to
+ // true
// in the options, this model will replace any other models in the local
- // 'Store' which holds the reference for future updates. Uses Backbone 'channel'
+ // 'Store' which holds the reference for future updates. Uses Backbone
+ // 'channel'
// for subscriptions, relabeled to 'channel' for clarity
- subscribe : function(options, next) {
+ subscribe: function (options, next) {
if (!socket) return (options.error && options.error(503, model, options));
var model = this;
options || (options = {});
@@ -252,15 +280,15 @@
options.type = model.type;
var packet = {
- model : model.toJSON(),
- options : options
+ model: model.toJSON(),
+ options: options
};
// Add the model to a local object container so that other methods
// called from the 'Server' have access to it
if (!Store[options.channel] || options.override) {
Store[options.channel] = model;
- socket.emit(listener, packet, function(response) {
+ socket.emit(listener, packet, function (response) {
if (!options.silent) model.trigger('subscribe', model, options);
next && next(response);
});
@@ -271,22 +299,24 @@
return this;
},
- //###unsubscribe
- // Stop listening for published model data, removing the reference in the local
- // subscription 'Store', will trigger an unsubscribe event unless 'silent'
+ // ###unsubscribe
+ // Stop listening for published model data, removing the reference in
+ // the local
+ // subscription 'Store', will trigger an unsubscribe event unless
+ // 'silent'
// is passed in the options
- unsubscribe : function(options, next) {
+ unsubscribe: function (options, next) {
if (!socket) return (options.error && options.error(503, model, options));
var model = this;
- options || (options = {});
+ options || (options = {});
options.channel || (options.channel = model.getChannel());
options.method = 'unsubscribe';
var packet = {
- model : {},
- options : options
+ model: {},
+ options: options
}
- socket.emit(listener, packet, function(response) {
+ socket.emit(listener, packet, function (response) {
if (!options.silent) model.trigger('unsubscribe', model, options);
next && next(response);
});
@@ -298,16 +328,17 @@
return this;
}
};
-
+
// Add to underscore utility functions to allow optional usage
// This will allow other storage options easier to manage, such as
// 'localStorage'. This must be set on the model and collection to
// be used on directly. Defaults to 'Backbone.sync' otherwise.
_.mixin({
- //###sync
- // Set the model or collection's sync method to communicate through DNode
- sync : function(method, model, options) {
+ // ###sync
+ // Set the model or collection's sync method to communicate through
+ // DNode
+ sync: function (method, model, options) {
if (!socket) return (options.error && options.error(503, model, options));
// Remove the Backbone id from the model as not to conflict with
@@ -316,16 +347,21 @@
if (model.attributes && model.attributes._id) delete model.attributes.id;
// Set the RPC options for model interaction
- options.type || (options.type = model.type || model.collection.type);
+ options.type || (options.type = model.type || model.collection.type);
options.channel || (options.channel = model.getChannel());
- options.method || (options.method = method);
+ options.method || (options.method = method);
+ options.indexProps = model.indexProps || (model.model && model.model.prototype.indexProps ? model.model.prototype.indexProps : []);
+ options.extKeys = model.extKeys || (model.model && model.model.prototype.extKeys ? model.model.prototype.extKeys : []);
+ if (typeof model == Backbone.Collection && model.extKey) {
+ options.extKey = model.extKey;
+ }
registerOnce(model, options);
-
+
// Create the packet to send over the wire
var packet = {
- model : model.toJSON(),
- options : options
+ model: model.toJSON(),
+ options: options
}
if (method === 'read') {
var lookupModel = {};
6 browser/index.js
View
@@ -1,7 +1,7 @@
-// backbone-redis
+// Backbone-Redis
// (c) 2011 Beau Sorensen
-// backbone-redis may be freely distributed under the MIT license.
+// Backbone-Redis may be freely distributed under the MIT license.
// For all details and documentation:
// https://github.com/sorensen/backbone-redis
-module.exports = require('./backbone.redis');
+module.exports = require('./backbone-redis');
92 examples/todos/server.js
View
@@ -6,7 +6,7 @@ require.paths.unshift('../../lib');
// Project dependencies
var express = require('express'),
Redis = require('redis'),
- support = require('../../'),
+ middleware = require('../../'),
browserify = require('browserify'),
io = require('socket.io'),
server = module.exports = express.createServer(),
@@ -47,84 +47,18 @@ server.get('/', function(req, res) {
res.render(__dirname + '/index.html');
});
-//db.flushall();
-
-support.config({
- io : io,
- database : db,
- publish : pub,
- subscribe : sub,
- listener : 'backbone',
- safeMode : true,
- showDebug : true,
- showError : true
-});
-
-
-model = support
- .schema({
- content : '',
- order : '',
- done : ''
- })
- .pre('create', function(next, sock, data, cb) {
- console.log('todo-pre-create');
- next(sock, data, cb);
- })
- .pre('read', function(next, sock, data, cb) {
- console.log('todo-pre-read');
- next(sock, data, cb);
- })
- .pre('update', function(next, sock, data, cb) {
- console.log('todo-pre-update');
- next(sock, data, cb);
- })
- .pre('delete', function(next, sock, data, cb) {
- console.log('todo-pre-delete');
- next(sock, data, cb);
- })
- .pre('subscribe', function(next, sock, data, cb) {
- console.log('todo-pre-subscribe');
- next(sock, data, cb);
- })
- .pre('unsubscribe', function(next, sock, data, cb) {
- console.log('todo-pre-unsubscribe');
- next(sock, data, cb);
- })
- .pre('publish', function(next, sock, data, cb) {
- console.log('todo-pre-publish');
- next(sock, data, cb);
- })
- .post('create', function(next, sock, data, cb) {
- console.log('todo-post-create');
- next(sock, data, cb);
- })
- .post('read', function(next, sock, data, cb) {
- console.log('todo-post-read');
- next(sock, data, cb);
- })
- .post('update', function(next, sock, data, cb) {
- console.log('todo-post-update');
- next(sock, data, cb);
- })
- .post('delete', function(next, sock, data, cb) {
- console.log('todo-post-delete');
- next(sock, data, cb);
- })
- .post('subscribe', function(next, sock, data, cb) {
- console.log('todo-post-subscribe');
- next(sock, data, cb);
- })
- .post('unsubscribe', function(next, sock, data, cb) {
- console.log('todo-post-unsubscribe');
- next(sock, data, cb);
- })
- .post('publish', function(next, sock, data, cb) {
- console.log('todo-post-publish');
- next(sock, data, cb);
+// Start up the application
+if (!module.parent) {
+ middleware({
+ io : io,
+ db : db,
+ publish : pub,
+ subscribe : sub,
+ listener : 'backbone'
});
-support.model('todo', model);
-
+ middleware.pre('room:save', function(model, options, next) {
-server.listen(8080);
+ });
+ server.listen(8080);
+}
3  examples/todos/todos.js
View
@@ -26,8 +26,7 @@ $(function(){
// Default attributes for the todo.
defaults: {
content: "empty todo...",
- done: false,
- dirty: "dirty data",
+ done: false
},
// Ensure that each todo created has `content`.
4 index.js
View
@@ -1,6 +1,6 @@
-// backbone-redis
+// Backbone-Redis
// (c) 2011 Beau Sorensen
-// backbone-redis may be freely distributed under the MIT license.
+// Backbone-Redis may be freely distributed under the MIT license.
// For all details and documentation:
// https://github.com/sorensen/backbone-redis
534 lib/backbone-redis.js
View
@@ -35,38 +35,46 @@ var models = {},
var _ = this._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._;
-//Require Backbone, if we're on the server, and it's not already present.
+// Require Backbone, if we're on the server, and it's not already present.
var Backbone = this.Backbone;
-if (!Backbone && (typeof require !== 'undefined')) Backbone = require('Backbone');
+if (!Backbone && (typeof require !== 'undefined')) Backbone = require('backbone');
_.extend(Backbone.Model.prototype, {
- // used to generate channel name from type
- channel : function() {
- var type = this.type || (this.collection ? this.collection.type : null);
- return this.type + (this.id ? ':' + this.id : '');
- }
+ indexProps: {},
+ getChannel : function() {
+ var type = this.type || (this.collection ? this.collection.type : null);
+ if (!type) throw new Error("No type found on model or related collection.");
+ return this.type + ':' + this[this.idAttribute];
+ },
+ subscribe: function () {return this},
+ unsubscribe: function () {return this}
});
_.extend(Backbone.Collection.prototype, {
- channel : function() {
- return this.type || (this.model && this.model.type ? this.model.type : null);
- }
+ getChannel : function() {
+ var type = this.type || (this.model && this.model.type ? this.model.type : null);
+ if (!type) throw new Error("No type found on collection or related model.");
+ // @TODO: use conditions for channel creation?
+ return type;
+ },
+ unsubscribe: function () {return this},
+ subscribe: function () {return this}
});
// Server side dependencies
if (typeof exports !== 'undefined') {
- var hooks = require('hooks');
+ var hooks = require('hooks');
}
function Message(opt) {
- this.model = opt.model;
- this.options = opt.options;
- this.options.type && (this.type = this.options.type);
+ this.model = opt.model;
+ this.options = opt.options;
+ this.options.type && (this.type = this.options.type);
}
// Error and debug handlers
-//-------------------------
+// -------------------------
-//###errorMessage
+// ###errorMessage
// Simple error helper messages
function errorMessage(err, packet) {
if (!showError) return;
@@ -75,7 +83,7 @@ function errorMessage(err, packet) {
return this;
};
-//###debugMessage
+// ###debugMessage
// Simple debug helper messages
function debugMessage(msg, packet) {
if (!showDebug) return;
@@ -84,12 +92,23 @@ function debugMessage(msg, packet) {
return this;
}
-module.exports = {
+var clientRegistry = {};
+function useClientOnce (channel) {
+ var use = clientRegistry[channel];
+ if (!use) return;
+ delete clientRegistry[channel];
+ return use;
+}
+function registerClientOnce (channel) {
+ clientRegistry[channel] = 1;
+}
+
+var Sync = module.exports = {
// Configuration and setup
- //------------------------
+ // ------------------------
- //###config
+ // ###config
config : function(opt, cb) {
opt.io && (conn = opt.io);
opt.database && (db = opt.database);
@@ -107,7 +126,7 @@ module.exports = {
return this;
},
- //###_configSocket
+ // ###_configSocket
// Set the incomming socket messages handler
_configSocket : function() {
if (!conn) return this;
@@ -120,7 +139,7 @@ module.exports = {
return this;
},
- //###_configRedis
+ // ###_configRedis
// Redis publish subscribe event handling
_configRedis : function() {
if (!sub) return this;
@@ -150,18 +169,71 @@ module.exports = {
return this;
},
- //###filter
- filter : function(type, data) {
- var filtered = {};
- for (attr in this.model(type)) {
- filtered[attr] = doc[attr]
- ? doc[attr]
- : schemas[type][attr];
- }
- return filtered;
+ _getSortKey: function (channel, sortBy) {
+ return channel + '::' + sortBy + '::';
+ },
+
+ _getExtKey: function (channel, extKey, id) {
+ return channel + '::' + extKey + ':' + id + '::';
},
- //###process
+ filter: function (records, conditions) {
+ if (!conditions) {
+ return records;
+ }
+ if (!_.isArray(conditions)) {
+ conditions = [conditions];
+ }
+ var single = false;
+ if (!_.isArray(records)) {
+ records = [records];
+ single = true;
+ }
+ // validate conditions first
+ _.each(conditions, function (condition, key){
+ if (_.isNumber(key)) {
+ // signature: {prop: 'foo', val: 'bar', op: '!='}
+ var prop = condition.prop
+ , val = _.condition.val
+ , op = condition.op;
+ // sanitize value
+ conditions[key].val = sanitize(val).xss();
+ } else {
+ // signature: {foo: 'bar'} gets operator '=='
+ var prop = key
+ , val = condition
+ , op = '==';
+ // sanitize value
+ conditions[key] = sanitize(val).xss();
+ }
+ // and check for property format and operator
+ if(!(prop.test(/[a-zA-Z_][a-zA-Z0-9_]*/) &&
+ op && op.test(/(==|===|!=|!==|>|<|>=|<=)/)))
+ {
+ throw new Error("Invalid condition: '" + prop + op + val + "'");
+ }
+
+ });
+ // alrighty, lets filter!
+ collection = _.select(records, function (record) {
+ _.each(conditions, function (condition, key){
+ var prop = condition.prop
+ , val = condition.val
+ , op = condition.op;
+ // after all our checks we should now be safe to use eval
+ if (eval("record[prop] " + op + " value;") === false) {
+ return false;
+ }
+ });
+ return true;
+ });
+ if (single) {
+ return _.first(collection);
+ }
+ return collection;
+ },
+
+ // ###process
process : function(socket, packet, fn) {
var model = packet.model,
options = packet.options,
@@ -184,7 +256,7 @@ module.exports = {
this[options.method](socket, packet, fn);
},
- //###schema
+ // ###schema
// Get or set a model schema, add hooks' methods for
// `hook`, `pre`, and `post`, as well as all hookable
// methods for pubsub and crud routines.
@@ -198,7 +270,7 @@ module.exports = {
return obj;
},
- //###model
+ // ###model
// Get or set a model schema, add hooks' methods for
// `hook`, `pre`, and `post`, as well as all hookable
// methods for pubsub and crud routines.
@@ -210,31 +282,31 @@ module.exports = {
},
// Pubsub routines
- //----------------
+ // ----------------
- //###subscribe
+ // ###subscribe
// Channel subscription, add the client to the internal
// subscription object, creating a container for the channel
// if one does not exist, then subscribe to the Redis client
subscribe : function(socket, packet, cb) {
var chan = packet.options.channel;
- socket && socket.join(chan)
+ socket && socket.join(chan);
sub && sub.subscribe(chan);
cb && cb(true);
return this;
},
- //###unsubscribe
+ // ###unsubscribe
// Unsubscribe from model changes via channel
unsubscribe : function(socket, packet, cb) {
var chan = packet.options.channel;
- socket && socket.leave(chan)
+ socket && socket.leave(chan);
sub && sub.unsubscribe(chan);
cb && cb(true);
return this;
},
- //###publish
+ // ###publish
// Publish to redis if a connection has been supplied,
// otherwise send through to clients on this thread
publish : function(socket, packet, cb) {
@@ -242,32 +314,43 @@ module.exports = {
type = packet.options.type;
var str = JSON.stringify(packet);
if (pub) {
+ // publish to redis
pub.publish(chan, str);
if (chan !== type) {
// also publish to the collection channel
pub.publish(type, str);
}
- }
- else return this._pushed(packet, cb);
+ } else return this._pushed(packet, cb);
cb && cb(true);
return this;
},
- //###pushed
+ // ###pushed
// Push a message to application clients based on channels, used
// as the delivery method for redis published events, but can be
// used by itself on a single thread basis
_pushed : function(packet, cb) {
- var chan = packet.options.channel;
- conn && conn.sockets.in(chan).json.emit(listener, packet);
+ var chan = packet.options.channel,
+ type = packet.options.type;
+ if (conn) {
+ conn.sockets.in(chan).json.emit(listener, packet);
+ if (chan !== type) {
+ // also publish to the collection channel
+ conn.sockets.in(type).json.emit(listener, packet);
+ }
+ }
cb && cb(true);
return this;
},
+ _getTypeId: function (type, id) {
+ return type + ':' + id;
+ },
+
// CRUD Routines
- //--------------
+ // --------------
- //###create
+ // ###create
// Create a new model with the givin data, publishing the
// event to the pub/sub middleware, builds upon Backbone
// options, if the option 'silent' is true, the event will
@@ -278,75 +361,236 @@ module.exports = {
model = packet.model,
options = packet.options,
type = options.type,
- chan = options.channel;
+ channel = options.channel,
+ index = options.indexProps
+ extKeys = options.extKeys;
packet.options.method = 'created';
// Generate the next redis id by model type to allow set transfers
db.incr('next.' + type + '.id', function(err, rid) {
- if (err) return (errorMessage(err, packet));
-
- var id = model.id = rid,
- data = JSON.stringify(model);
-
- db.set(id, data, function(err, isset) {
- if (err) return (errorMessage(packet, err));
- if (!isset) return (debugMessage('set', packet));
+ if (err) {
+ options.error && options.error(err);
+ return (errorMessage(err, packet));
+ }
- db.sadd(type, id, function(err, added) {
- if (err) return (errorMessage(err, packet));
- if (!added) return (debugMessage('sadd', packet));
- });
- options.silent || self.publish(socket, packet, cb);
- cb && cb(true);
- });
+ model.dateCreated = model.dateModified = new Date().getTime();
+ var id = model.id = rid,
+ data = JSON.stringify(model);
+
+ var multi = db.multi();
+ // first set the object itself with the type id as key
+ var typeId = Sync._getTypeId(type, id);
+ multi.set(typeId, data);
+ // then go over all our index props and set those too
+ Sync._setIndexProps(model, type, index, multi);
+ // we always want to store the date created index
+ multi.set(Sync._getSortKey(type, 'dateModified') + id, model.dateModified)
+ multi.set(Sync._getSortKey(type, 'dateCreated') + id, model.dateCreated)
+ // then go over all our external keys and set those too
+ Sync._setExtKeys(model, null, type, extKeys, multi);
+ // and add to the set
+ multi.sadd(type, typeId);
+ // execute
+ multi.exec(function(err, isset) {
+ if (err) return (errorMessage(packet, err));
+ if (!isset) return (debugMessage('set', packet));
+
+ options.silent || self.publish(socket, packet);
+ cb && cb(true);
+ })
});
},
+
+ _setIndexProps: function (model, type, index, multi) {
+ _.each(index, function(prop) {
+ // convert booleans to 1 or 0 for sorting
+ var val = _.isBoolean(model[prop]) ? (model[prop] ? 1 : 0) : model[prop];
+ var identifier = Sync._getSortKey(type, prop) + model.id;
+ multi.set(identifier, val);
+ });
+ },
+
+ _unsetIndexProps: function (model, type, index, multi) {
+ _.each(index, function(prop) {
+ var identifier = Sync._getSortKey(type, prop) + model.id;
+ multi.del(identifier);
+ });
+ },
+
+ /**
+ * Set external keys:
+ * - all keys if no oldModel provided
+ * - if oldModel, only keys that have a different value compared to oldModel
+ */
+ _setExtKeys: function (model, oldModel, type, keys, multi) {
+ var workingModel = oldModel || model;
+ _.each(keys, function(key) {
+ // skip if no work
+ if (!model[key] && (!oldModel || !workingModel[key])) return;
+
+ var val = model[key];
+ if (!_.isArray(val)) {
+ val = [val];
+ }
+ var oldVal = workingModel[key];
+ if (typeof oldVal !== 'undefined' && !_.isArray(oldVal)) {
+ oldVal = [oldVal];
+ }
- //###read
+ // lets make an index for each item in the array
+ // this enables us to find has many relations
+ _.each(val, function(v){
+ if (v && (!oldModel || _.indexOf(v, oldVal) === -1)) { // only set when necessary
+ var identifier = Sync._getExtKey(type, key, v) + model.id;
+ multi.set(identifier, v);
+ }
+ })
+ });
+ },
+
+ /**
+ * Unset external keys:
+ * - all keys if no oldModel provided
+ * - if oldModel, only keys from oldModel that are different from the new model
+ */
+ _unsetExtKeys: function (model, oldModel, type, keys, multi) {
+ var workingModel = oldModel || model;
+ _.each(keys, function(key) {
+ // skip if no work
+ if (!model[key] && (!oldModel || !workingModel[key])) return;
+
+ var val = model[key];
+ if (typeof val !== 'undefined' && !_.isArray(val)) {
+ val = [val];
+ }
+ var oldVal = workingModel[key];
+ if (typeof oldVal !== 'undefined' && !_.isArray(oldVal)) {
+ oldVal = [oldVal];
+ }
+ _.each(oldVal, function(v){
+ if (v && (!oldModel || _.indexOf(v, val) === -1)) { // only unset when necessary
+ var identifier = Sync._getExtKey(type, key, v) + model.id;
+ multi.del(identifier);
+ }
+ })
+ });
+ },
+
+ // ###read
// Retrieve either a single model or collection of models
- read : function(socket, packet, cb) {
- var self = this,
- model = packet.model,
- options = packet.options,
- type = options.type,
- chan = options.channel
-
- // Check to see if a specific model was requested based on 'id',
- // otherwise search the collection with the given parameters
- if (model.id) {
- db.get(model.id, function(err, doc) {
- if (err) return (errorMessage(err, packet));
- if (!doc) return (debugMessage('get', packet));
-
- // Set the data and write to client
- packet.model = JSON.parse(doc);
- socket.json.emit(listener, packet);
- });
- cb && cb(true);
- return;
+ read: function (socket, packet) {
+ var model = packet.model,
+ options = packet.options,
+ type = options.type,
+ index = options.indexProps,
+ extKey = options.extKey,
+ channel = options.channel,
+ sort = options.sort || {},
+ limit = sort.limit;
+
+ // Check to see if a specific model was requested based on 'id',
+ // otherwise search the collection with the given parameters
+ if (model.id) {
+ var typeId = Sync._getTypeId(type, model.id);
+ redisClient.get(typeId, function (err, record) {
+ if (err) {
+ errorMessage(err, packet);
+ }
+ if (!record) {
+ debugMessage('get', packet);
+ options.error && options.error(record);
+ } else {
+ record = JSON.parse(record);
+ options.success && options.success(record);
+ }
+ if (socket) {
+ packet.model = record;
+ socket.json.emit(listener, packet);
+ }
+ });
+ return;
+ }
+ var processRecords = function (err, records) {
+ if (err || ! records) {
+ err = err || 'no collection items found for list: ' + JSON.stringify(list);
+ errorMessage(err, packet);
+ options.error && options.error(err);
+ if (socket) {
+ packet.model = null;
+ socket.json.emit(listener, packet);
+ }
+ return;
}
- db.smembers(type, function(err, list) {
- if (err) return (errorMessage(err, packet));
- if (!list) return (debugMessage('smembers', packet));
-
- if (list.length === 0) {
- packet.model = [];
- socket.json.emit(listener, packet);
- cb && cb(true);
- }
- db.mget(list, function(err, result) {
- if (err) return (errorMessage(err, packet));
- if (!result) return (debugMessage('mget', packet));
-
- // Send client the model data
- packet.model = _.map(result, function(record) {
- return JSON.parse(record);
- });
- socket.json.emit(listener, packet);
- cb && cb(true);
- });
+
+ // create json objects from the strings
+ var collection = _.map(records, function (record) {
+ return JSON.parse(record);
});
+
+ // filter by conditions?
+// conditions && (collection = Sync.filter(collection, conditions));
+
+ if (socket) {
+ packet.model = collection;
+ socket.json.emit(listener, packet);
+ }
+ options.success && options.success(collection);
+ };
+ var args = [channel, 'by'];
+ if (extKey) {
+ // get collection by extKey
+ var prop, v;
+ for (prop in extKey) {
+ v = extKey[prop];
+ }
+ var key = Sync._getExtKey(type, prop, v) + '*';
+ args = [key];
+ } else {
+ var key = sort.by ? Sync._getSortKey(type, sort.by) + '*' : 'nosort';
+ args.push(key);
+ // add limit?
+ if (limit) {
+ limit && (limit = limit.split('-'));
+ var limitStart = limit[0] - 1 < 0 ? 0 : limit[0] - 1;
+ args.push('limit', limitStart, limit[1]);
+ }
+ // add type hint for string prop?
+ index && index[sort.by] && _.isString(index[sort.by]) && args.push('alpha');
+ // add dir?
+ sort.dir && args.push(sort.dir);
+ }
+ var handle = function (err, list) {
+ if (err) {
+ errorMessage(err, packet);
+ options.error && options.error(err);
+ if (socket) {
+ packet.model = null;
+ socket.json.emit(listener, packet);
+ }
+ return;
+ }
+ if (list.length == 0) {
+ options.success && options.success([]);
+ if (socket) {
+ packet.model = [];
+ socket.json.emit(listener, packet);
+ }
+ return;
+ }
+ // if we got a list by external key, fix the list
+ if (extKey) {
+ _.each(list, function (val, i) {
+ list[i] = type + ':' + val.substr(val.lastIndexOf('::') + 2);
+ });
+ }
+ redisClient.mget(list, processRecords);
+ }
+ if (extKey) {
+ redisClient.keys(args, handle);
+ } else {
+ redisClient.sort(args, handle);
+ }
},
update : function(socket, packet, cb) {
@@ -356,29 +600,51 @@ module.exports = {
model = packet.model,
options = packet.options,
type = options.type,
- chan = options.channel;
- id = model.id,
- data = JSON.stringify(model);
+ channel = options.channel,
+ index = options.indexProps,
+ extKeys = options.extKeys,
+ id = model.id;
if (!id) {
console.log('update no id:');
return;
}
- db.get(id, function(err, exists) {
- if (err) return (errorMessage(err, packet));
- if (!exists) return (debugMessage('get', packet));
-
- db.set(id, data, function(err, isset) {
- if (err) return (errorMessage(err, packet));
- if (!isset) return (debugMessage('set', packet));
+ var typeId = Sync._getTypeId(type, id);
+ db.get(typeId, function(err, exists) {
+ if (err || !exists) {
+ options.error && options.error(err);
+ return (errorMessage(err, packet));
+ }
+ model.dateModified = new Date().getTime();
+ var data = JSON.stringify(model);
+ var multi = db.multi();
+ multi.set(typeId, data);
+ // lets remove old indexes
+ var oldModel = JSON.parse(exists);
+ Sync._unsetIndexProps(model, type, index, multi);
+ // and remove old external keys
+ Sync._unsetExtKeys(model, oldModel, type, extKeys, multi);
+ // then go over all our new index props and set them
+ Sync._setIndexProps(model, type, index, multi);
+ // and set new external keys
+ Sync._setExtKeys(model, oldModel, type, extKeys, multi);
+ // we always want to store the date indexes, but won't touch dateCreated
+ multi.set(Sync._getSortKey(type, 'dateModified') + id, model.dateModified)
+ multi.exec(function(err, isset) {
+ if (err) {
+ options.error && options.error(err);
+ return (errorMessage(err, packet));
+ }
options.silent || self.publish(socket, packet, cb);
+ options.success && options.success(packet.model);
cb && cb(true);
});
+
});
},
- //###destroy
+ // ###destroy
// Remove the specified model from the database, only one model may be
// removed at a time, passing a 'temporary' option will publish the change
// without persisting to the database
@@ -388,25 +654,39 @@ module.exports = {
var self = this,
model = packet.model,
options = packet.options,
+ index = options.indexProps,
+ extKeys = options.extKeys,
id = model.id,
type = options.type,
chan = options.channel;
- db.sismember(type, id, function(err, member) {
- if (err) return (errorMessage(err, packet));
+ var typeId = Sync._getTypeId(type, id);
+ db.sismember(type, typeId, function(err, member) {
+ if (err) {
+ options.error && options.error(err);
+ return (errorMessage(err, packet));
+ }
+ var multi = db.multi();
if (member) {
// Remove model from collection set
- db.srem(type, id, function(err, removed) {
- if (err) return (errorMessage(err, packet));
- if (!removed) return (debugMessage('srem', packet));
- });
+ multi.srem(type, id);
}
- db.del(id, function(err, destroyed) {
- if (err) return (errorMessage(err, packet));
- if (!destroyed) return (debugMessage('del', packet));
-
- options.silent || self.publish(socket, packet, cb);
- cb && cb(true);
+ multi.del(typeId);
+ // then go over all our index props and delete those too
+ index.push('dateCreated');
+ index.push('dateModified');
+ Sync._unsetIndexProps(model, type, index, multi);
+ // and don't forget the external keys
+ Sync._unsetExtKeys(model, null, type, extKeys, multi);
+ // execute
+ multi.exec(function(err, result){
+ if (err) {
+ options.error && options.error(err);
+ return (errorMessage(err, packet));
+ }
+ options.silent || self.publish(socket, packet, cb);
+ options.success && options.success(packet.model);
+ cb && cb(true);
});
});
}
174 lib/redis-store-sync.js
View
@@ -1,118 +1,120 @@
/**
- * Server only override for Backbone.sync, enabling us to directly talk to redis without a socket.
+ * Server only override for Backbone.sync, enabling us to directly talk to redis
+ * without a socket.
*/
var _ = require('underscore');
var Sync = require('backbone-redis');
var Backbone = require('backbone');
-//Error and debug settings
+// Error and debug settings
var showError = false,
- showDebug = false;
+ showDebug = false;
-//Simple error helper messages
+// Simple error helper messages
function errorMessage(err, packet) {
- if (!showError) return;
- console.error('Error!', err);
- console.trace();
- return this;
+ if (!showError) return;
+ console.error('Error!', err);
+ console.trace();
+ return this;
};
-//###debugMessage
+// ###debugMessage
// Simple debug helper messages
function debugMessage(msg, packet) {
- if (!showDebug) return;
- packet.options || (packet.options = {});
- console.log('Debug: Method: ' + packet.options.method + 'Msg: ', msg);
- return this;
+ if (!showDebug) return;
+ packet.options || (packet.options = {});
+ console.log('Debug: Method: ' + packet.options.method + 'Msg: ', msg);
+ return this;
}
var myRedis = {
- read: function (socket, packet, options) {
- var model = packet.model,
- options = packet.options,
- type = model.type,
- chan = options.channel;
+ read: function (socket, packet, options) {
+ var model = packet.model,
+ options = packet.options,
+ type = model.type,
+ chan = options.channel;
- // Check to see if a specific model was requested based on 'id',
- // otherwise search the collection with the given parameters
- if (model.id) {
- redisClient.get(model.id, function (err, doc) {
- if (err) {
- errorMessage(err, packet);
- options.error(err);
- return;
- }
- if (!doc) {
- debugMessage('get', packet);
- }
- options.success(JSON.parse(doc));
- });
- return;
+ // Check to see if a specific model was requested based on 'id',
+ // otherwise search the collection with the given parameters
+ if (model.id) {
+ redisClient.get(model.id, function (err, doc) {
+ if (err) {
+ errorMessage(err, packet);
+ options.error(err);
+ return;
}
- redisClient.smembers(type, function (err, list) {
- if (err) {
- errorMessage(err, packet);
- options.error(err);
- return;
- }
- if (list.length == 0) {
- options.success([]);
- return debugMessage('smembers', packet);
- }
+ if (!doc) {
+ debugMessage('get', packet);
+ }
+ options.success(JSON.parse(doc));
+ });
+ return;
+ }
+ redisClient.smembers(type, function (err, list) {
+ if (err) {
+ errorMessage(err, packet);
+ options.error(err);
+ return;
+ }
+ if (list.length == 0) {
+ options.success([]);
+ return debugMessage('smembers', packet);
+ }
- redisClient.mget(list, function (err, result) {
- if (err) {
- errorMessage(err, packet);
- options.error(err);
- return;
- }
- if (!result) {
- errorMessage('no collection items found for list: ' + JSON.stringify(list));
- return debugMessage('mget', packet);
- }
+ redisClient.mget(list, function (err, result) {
+ if (err) {
+ errorMessage(err, packet);
+ options.error(err);
+ return;
+ }
+ if (!result) {
+ errorMessage('no collection items found for list: ' + JSON.stringify(list));
+ return debugMessage('mget', packet);
+ }
- // Send client the model data
- var collection = _.map(result, function (record) {
- return JSON.parse(record);
- });
- options.success(collection);
- });
+ // Send client the model data
+ var collection = _.map(result, function (record) {
+ return JSON.parse(record);
});
- }
+ options.success(collection);
+ });
+ });
+ }
}
var sync = function (method, model, options) {
- if (typeof redisClient == 'undefined') { throw new Error("redis client must be configured!"); }
- var resp;
+ if (typeof redisClient == 'undefined') { throw new Error("redis client must be configured!"); }
+ var resp;
- options || (options = {});
- options.channel = model.getChannel();
- options.type = model.type;
+ options || (options = {});
+ options.channel = model.getChannel();
+ options.type = model.type;
- var data = {
- model: model,
- options: options
- }
-
- switch (method)
- {
- // only reading will need to be overwritten, as it's the only method writing it's result to a socket
- case "read":
- return myRedis.read(null, data, options);
- // the rest we simply plug onto backbone-redis
- case "create":
- return Sync.create(null, data, options.success);
- case "update":
- return Sync.update(null, data, options.success);
- case "delete":
- return Sync.delete(null, data, options.success);
- }
- var err = 'method: ' + method + ' does not exist!';
- console.log(err);
- options.error(err);
+ var data = {
+ model: model,
+ options: options
+ }
+
+ switch (method)
+ {
+ // only reading will need to be overwritten, as it's the only method
+ // writing it's result to a socket
+ case "read":
+ return myRedis.read(null, data, options);
+ // the rest we simply plug onto backbone-redis
+ case "create":
+ return Sync.create(null, data, options.success);
+ case "update":
+ return Sync.update(null, data, options.success);
+ case "delete":
+ return Sync.delete(null, data, options.success);
+ }
+ var err = 'method: ' + method + ' does not exist!';
+ console.log(err);
+ options.error(err);
};
module.exports = sync;
10 package.json
View
@@ -1,7 +1,7 @@
{
"name" : "backbone-redis",
"description" : "Persistant backbone storage through redis pub/sub and socket.io",
- "version" : "0.0.3",
+ "version" : "0.0.2",
"homepage" : "https://github.com/sorensen/backbone-redis",
"repository" : {
"type" : "git",
@@ -18,13 +18,13 @@
],
"main" : "./index",
"dependencies" : {
- "backbone" : "0.5.1",
- "underscore" : "1.1.7",
- "socket.io" : "0.7.0"
+ "backbone" : ">= 0.5.1",
+ "underscore" : ">= 1.1.7",
+ "socket.io" : ">= 0.7.0"
},
"browserify" : "browser/index.js",
"engines" : {
- "node" : ">= 0.4.1 < 0.4.10"
+ "node" : ">= 0.4.1 < 0.5.0"
},
"author" : {
"name" : "Beau Sorensen",
Please sign in to comment.
Something went wrong with that request. Please try again.