rhom

Redis Hash-Object Mapper mixin

Redis Hash to Object Mapper

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 = "jsmith@gmail.com";
user1.save(function(errres) {
    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(errres) {
    if (res) copy1 = res;
});
 
/* Update */
copy1.email = "jsmith@yahoo.com";
copy1.save(function(errres) {
    if (res) console.log("Saved");
});
 
/* Delete */
copy1.delete(function(errres) {
    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, , 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

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(errgroup) {
    if (err) return;
 
    if (!group) return; // if available, should be a related instance of group 
 
    groups.getPermissions(function(errpermissions) {
        if (err) return;
 
        permissions; // should be a list of related O3 instances.  
    });
});
 
user1.getPermissions(function(errpermissions) {
    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:

  • Relationships will let you shoot yourself in the foot. If you don't define a relationship and try to call it (e.g. intermediaries), it will blow up.
  • Indirect relationships don't populate writer functions. This is partially because setting an indirect relation would also implicitly set a direct relationship on two other classes that may not have intended it. To give an example based on the sample code above, if user.addPermission() existed you would logically think it would add a permission for that user. It would, but it would also add it for the user's entire group - which isn't obvious. user.getGroup().addPermission() is much clearer.
  • Relations are not automatically cleaned up. If you delete a related object, the other end may still think it is related. Functionally, this makes no difference; The result will still be that the relation no longer exists. The relation key is cleaned up when detected by the relation getter. (E.g. user.getRole() where the role has been deleted will return nothing, and internally deletes the relation's internal tracking key.)

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(errusers) {
    if (err) return;
 
    if (!users) return; // If available, should be a list of users with the given username. 
});

Promises

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. 

Caveats

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.

Possible Additions

Some things I've thought about adding:

  • Local storage with event or pubsub based change tracking. This would be useful for multi-process node classes. (Something like keeping local copies that are automatically updated when changed in redis.)
  • Getters that can return limit/offset if integer arguments are provided. Maybe:
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