This is a mixin that maps Redis hashes into user-defined objects and vice versa. Its intention is to provide a simple way to perform CRUD on Redis hashes with minimal interference; Redis is great, so details shouldn't be abstracted away to the point that direct client access is hacky.
TL;DR: RHOM tries to save stuff in Redis the same way you might.
Adding rhom functionality to a class:
var rhom = require('rhom');
var client = require('redis').createClient();
function MyUserModel() {} // Define however you want.
rhom(MyUserModel, ['id', 'name', 'email'], client); // Mix-in RHOM functionality
/* Create */
var user1 = new MyUserModel();
user1.name = "John Smith";
user1.email = "[email protected]";
user1.save(function(err, res) {
if (res) console.log("Saved");
}); // Saves a hash at key MyUserModel:id
/* Retrieve based on the autogenerated id from user1 */
var copy1;
MyUserModel.get(user1.id, function(err, res) {
if (res) copy1 = res;
});
/* Update */
copy1.email = "[email protected]";
copy1.save(function(err, res) {
if (res) console.log("Saved");
});
/* Delete */
copy1.delete(function(err, res) {
if (res) console.log("Deleted");
});
// Hash underlying user1 is also gone, because they're the same.
If you don't know the properties an object will have, you can store everything enumerable on the instance. This isn't recommended in most cases, as it may silently fail to save if any enumerable property can't be saved to Redis.
function KitchenSink() {}
rhom(KitchenSink, undefined, client);
var sink = new KitchenSink();
sink["prop" + Math.round(Math.random()*1000)] = "Doesn't matter";
/* Arbitrary properties like this can be saved and retrieved. */
Additional mixins can be applied on top of the base mapper functionality. These include but are not limited to caching and relationships.
Caching can be applied on top of base mapper functionality by applying the rhom.cache mixin:
function MyUserModel() {}
/* Method 1: Cache for 30 seconds */
rhom(MyUserModel, [/* properties */], client).cache(30000);
/* Method 2: Cache for 30 seconds */
//rhom(MyUserModel, [/* properties */], client);
//rhom.cache(MyUserModel, 30000);
While I'm not sure it's advisable (consider a relational database), it is possible to create relationships between mapped objects using the rhom.relates mixin. Relationships must be defined explicitly on every level, and relationships are limited. For example, a one-to-one relationship only creates method definitions on the source object. If the reverse is also desired, define that too - it isn't created automatically. Indirect relationships are also possible.
function User() {};
function Group() {};
function Permission() {};
/* Method 1: Relations with method chaining. */
var rhomUser = rhom(User, ['username'], client);
rhomUser.relates.toOne(Group);
rhomUser.relates.via(Group).toMany(Permission);
rhom(Group, ['name'], client).relates.toMany(Permission);
rhom(Permission, ['label'], client);
/* Method 2: Relations */
//rhom(User, ['username'], client);
//rhom(Group, ['name'], client);
//rhom(Permission, ['label'], client);
//rhom.relates(User).toOne(Group); // 1 to 1. Getter is get<Classname>
//rhom.relates(Group).toMany(Permission); // 1 to N. Getter is pluralized get<Classnames>.
//rhom.relates(User).via(Group).toMany(Permission); // Indirect
var user1 = /* retrieved instance of User */;
user1.getGroup(function(err, group) {
if (err) return;
if (!group) return; // if available, should be a related instance of group
groups.getPermissions(function(err, permissions) {
if (err) return;
permissions; // should be a list of related O3 instances.
});
});
user1.getPermissions(function(err, permissions) {
permissions; // should be a list of indirectly prelated permission instances.
});
/* Ridiculous amounts of chaining should be possible, but only two levels is tested. */
// If these were defined models.
// rhom.relates(User).via(Group).via(Permission).via(x).via(y)toOne(Something);
// toMany also works, but all intermediaries must be defined as one-to-one.
// or
// rhom.relates(User).via([Group, Permission, x, y]).toOne(Something); // Same thing.
/* In case you're curious */
Group.getUser(); // Reverse relationship is not automatically defined.
User.getPermission(); // Singular would not be defined; toMany is pluralized.
Notes:
Adds an equality index so that fields can be searched quickly.
function User() {};
/* Method 1: Method chaining */
rhom(User, ['username', 'password', 'name', 'email'], client)
.index("username") // Defines User.getByUsername()
.index("email"); // Defines User.getByEmail()
/* Method 2 */
//rhom(User, ['username', 'password', 'name', 'email'], client);
//rhom.index(User, "username", client); // Defines User.getByUsername();
//rhom.index(User, "email", client); // Defines User.getByEmail();
User.getByUsername('jsmith', function(err, users) {
if (err) return;
if (!users) return; // If available, should be a list of users with the given username.
});
All the getters on the base object should return promises.
Cls.get('foo').then(function(obj) {
// Do something with the retreived object.
}, function(err) {
// Do something with the error
});
// Should work for all asynchronous calls: get/all/purge/save/delete.
Classes must be defined using a named function definition, not an anonymous function assigned to a variable. The named function makes the .name property available, on which some functions rely.
// Do this:
function MyClass() { /* ... */ }
MyClass.prototype = { /* ... */ }
// Not this:
var MyClass = function() { /* ... */ }
MyClass.prototype = { /* ... */ }
Currently, some things don't clean up after themselves and may leave you with a dirty Redis database. Relations and indexes come to mind. If you delete the target, the keys that reference the deleted item may remain until the missing item is detected by a getter.
Some things I've thought about adding:
Class.all(callback); // Normal
Class.all(100, callback); // With Limit 100
Class.all(100, 500, callback); // With limit 100, offset 500
Class.getRelatedItems(callback); // Normal
Class.getRelatedItems(100, callback); // With limit 100
Class.getRelatedItems(100, 500, callback); // With Limit 100, offset 500