npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

rhom

v0.2.10

Published

Redis Hash-Object Mapper mixin

Downloads

21

Readme

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.

Example

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

Additional mixins can be applied on top of the base mapper functionality. These include but are not limited to caching and relationships.

Transparent Caching

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);

Relationships

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:

  • 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.)

Indexing

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.
});

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

Function Definitions

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 = { /* ... */ }

Cleanup

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