Nohm is an ORM for redis. This How-To is intended to give you a good understanding of how nohm works.
To make nohm more modern many breaking changes were introduced. Among them a switch from callbacks to promises, but also many small breaking changes.
For a full list of all breaking changes see the CHANGELOG.md
There are some things you need to do before you can use nohm. If you just want to know how to actually use nohm models, skip to the next part Models.
Note: Almost all code examples here assume the following code:
const nohm = require('nohm').Nohm;
const redisClient = require('redis').createClient();
redis.on('connect', () => {
nohm.setClient(redisClient);
// example code goes here!
});
The nohm
module exports several objects/classes:
require('nohm').Nohm
is the default instance of the NohmClass. In normal use, this is the only instance of NohmClass you should use.require('nohm').NohmClass
There are usecases where you would want multiple NohmClasses, for example if you want different prefixes for different parts of your application or you have multiple different databases, each containing some models. TODO: add more documentation for these special use casesrequire('nohm').NohmModel
This is the Extendable Model class you should use for ES6/Typescript class definitionsThe first thing you should do is set a prefix for the redis keys. This should be unique on the redis database you’re using since you may run into conflicts otherwise. You can do this with the following command:
nohm.setPrefix('yourAppPrefixForRedis');
You need to set a redis client for nohm. You should connect and select the appropriate database before you set it.
The standard method would be to use node_redis:
const redis = require('redis');
const redisClient = redis.createClient();
// wait for redis to connect, to make sure everything will work as expected
redis.on('connect', () => {
nohm.setClient(redisClient);
});
(available in v2.2.0 onwards) You can also use ioredis instead of node_redis.
const Redis = require('ioredis');
const redisClient = new Redis();
// wait for ioredis to be ready, to make sure everything will work as expected
redis.on('ready', () => {
nohm.setClient(redisClient);
});
In the simple stress tests using ioredis shows more than a 20% decrease in performance.
If you require ioredis for other parts of your application and performance is important to you, the best option is probably to use ioredis for that and node*redis for nohm. There should be no conflicts when using both in the same application. Tread with caution though and test and benchmark it yourself for your use-cases.
Note: Clustering and sharding is not yet tested or officially supported by nohm.
By default nohm just logs errors it encounters to the console. However you can overwrite the logError method with anything you want:
TODO: once v2 has proper logging support, change this.
// this will throw all errors nohm encounters - not recommended
nohm.logError = function(err) {
throw new Error({
name: 'Nohm Error',
message: err,
});
};
You start by defining a Model. A model needs a name and properties and can optionally have custom methods and a per-model redis client.
There are 2 ways to define a model: Using plain objects or using classes.
When using plain objects you use the nohm.model() when using classes (in ES6 or Typescript) you use nohm.register().
const someModel = nohm.model('YourModelName', {
properties: {
// ... here you'll define your properties
},
methods: {
// optional
// ... here you'll define your custom methods
},
client: someRedisClient, // optional
});
Strictly speaking the properties are optional as well, but a model really doesn't make sense without them.
The first parameter is the name of the model that is used internally by nohm for the redis keys and relations. This should be unique across your application (prefix/db). The second is an object containing properties, methods and the client.
If you want your model to have custom methods you can define them in the methods
object. They will be bound to the model and thus inside them the ‘this’ keyword is the instance of a model.
You can optionally set a redis client for a model by passing it as the property client
.
Important: Currently relations between models on different DBs do NOT work!
If you can use ES6+ or Typescript classes the preferred way to define your models is by using the nohm.register()
function.
Note: At the time of writing the current EcmaScript does not support static properties in class definitions yet. The workaround is to define them afterwards on the class object as seen below.
ES6+ Example:
const NohmModel = require('nohm').NohmModel;
class SomeModel extends NohmModel {
public someMethod() {
console.log(this.property('name'));
}
}
SomeModel.modelName = 'SomeModelName';
SomeModel.definitions = {
name: {
type: 'string',
unique: true
}
};
Defining the properties is the most important part of a model. A property can have the following options: (explained in more detail later)
string, integer, float, boolean, timestamp and json
Here’s an example of a very basic user model:
const User = nohm.model('User', {
properties: {
name: {
type: 'string',
unique: true,
validations: ['notEmpty'],
},
email: {
type: 'string',
unique: true,
validations: ['notEmpty', 'email'],
},
password: {
defaultValue: '',
type: (value) => {
return `${value}someSeed`; // and hash it of course, but to make this short that is omitted in this example
},
validations: [
{
name: 'length',
options: {
min: 6,
},
},
],
},
visits: {
type: 'integer',
index: true,
},
},
});
Normal javascript string.
The value is parsed to an Int(base 10) or Float and defaults to 0 if NaN.
Casts to boolean - except ‘false’ (string) which will be cast to false (boolean).
Converts a Date(-time) to a timestamp (base 10 integer of miliseconds from 1970). This takes two different formats as inputs:
If a valid JSON string is entered nothing is done, anything else will get put through JSON.stringify. Note that properties with the type of JSON will be returned as parsed objects!
This can be any function you want.
Its this
keyword is the instance of the model and it receives the arguments new_value, name and old_value.
The return value of the function will be the new value of the property.
Note that the redis client will convert everything to strings before storing!
A simple example:
const User = nohm.model('User', {
properties: {
balance: {
defaultValue: 0,
type: function changeBalance(value, key, old) {
return old + value;
},
},
},
});
const test = new User();
test.p('balance'); // 0
test.p('balance', 5);
test.p('balance'); // 5
test.p('balance', 10);
test.p('balance'); // 15
test.p('balance', -6);
test.p('balance'); // 9
A property can have multiple validators. These are invoked whenever a model is saved or manually validated. Validations of a property are defined as an array of strings, objects and functions.
Functions take 2 arguments and must return a promise that resolves to true or false, depending on whether the new value is valid.
The arguments are the new value and options. Options can be any object you define but gets the following values mixed in: old (old value), optional (bool, Default: false) and trim (bool indicating whether the value should be trimmed before validating. Default: true)
Note: Functions included like this cannot be exported to the browser!
Note: The function name is used in the error fields if validation fails. Thus using arrow functions here might not be desirable.
Here’s an example with all three ways:
const validatorModel = nohm.model('validatorModel', {
properties: {
builtIns: {
type: 'string',
validations: [
'notEmpty',
{
name: 'length',
options: {
max: 20,
},
},
],
},
optionalEmail: {
type: 'string',
unique: true,
validations: [
{
name: 'email',
options: {
optional: true, // every built-in validation supports the optional option
},
},
],
},
customValidation: {
type: 'integer',
validations: [
function checkIsFour(value, options) {
return Promise.resolve(value === 4);
},
],
},
},
});
You can find the documentation of the built-in validations by looking directly at the source code.
// TODO: re-write/change once custom validations are back in
If you need to define custom validations as functions and want them to be exportable for the browser validations, you need to include them from an external file.
Example customValidation.js:
exports.usernameIsAnton = function(value, options) {
if (options.revert) {
callback(value !== 'Anton');
} else {
callback(value === 'Anton');
}
};
This is then included like this:
Nohm.setExtraValidations('customValidation.js');
Now you can use this validation in your model definitions like this:
nohm.model('validatorModel', {
properties: {
customValidation: {
type: 'string',
validations: [
'usernameIsAnton',
// or
[
'usernameIsAnton',
{
revert: true,
},
],
],
},
},
});
By default the ids of instances are uuid v1 and generated at the time of the first save call. You can however either choose an incremental id scheme or provide a custom function for generating ids.
nohm.model('incrementalIdModel', {
idGenerator: 'increment',
properties: {
// ...
},
});
//ids of incremental will be 1, 2, 3 ...
const prefix = 'bob';
const step = 50;
let counter = 200;
Nohm.model('customIdModel', {
idGenerator: async () => {
// ids of custom will be bob250, bob300, bob350 ...
counter += step;
return prefix + counter;
},
properties: {
// ...
},
});
// this is just an example and not a good implementation,
// because it keeps the counter purely in memory
There are two basic ways to create an instance of a model.
Using the new keyword on the return of .model() or .register().
const UserModel = nohm.model('UserModel', {});
const user = new UserModel();
This has the drawback that you need to keep track of your models.
Alternatively you can use the factory method of a NohmClass instance.
// define your model in a userModel.js or similar
nohm.model('UserModel', {});
// anywhere else in your code
const user = await nohm.factory('UserModel');
// note the await, because factory always returns a promise
You can also pass an id as the 2nd argument to immediately load the data from db.
const user = await nohm.factory('UserModel', 123);
user.id === 123; // true
user.isLoaded === true; // true
The method property() gets and sets properties of an instance.
const user = new User();
user.property('name', 'test');
user.property('name'); // returns 'test'
user.property({
name: 'test2',
email: 'someMail@example.com',
});
user.property('name'); // returns 'test2'
user.property('email'); // returns 'someMail@example.com'
The convenience short versions .p() and .prop() still exist, but are deprecated and cause a deprecation warning.
There are several other methods for dealing with properties:
Your model instance is automatically validated on save but you can manually validate it as well.
In the following code examples we assume the model of the valitators section is defined and instanced as user
.
user.property({
builtIns: 'teststringlongerthan20chars',
optionalEmail: 'hurgs',
customValidation: 3,
});
const valid = await user.validate(undefined, false);
if (!valid) {
user.errors; // { builtIns: ['length'], optionalEmail: ['email'], customValidation: ['custom'] }
} else {
// valid! YEHAA!
}
There are a few things to note here:
You can also do most validations in the browser by using the nohm-middleware.
This is useful if you don’t want to do the ajax round trip for every small validation you might have or for sharing the same validation code between frontend and backend.
There is one exception for this: Custom validations (aka. functions) defined in the model definition itself will not be available through the middleware. Instead, if you want to share custom validations with frontend and backend, they have to be explicitly defined in custom validation files and registered in nohm as such.
TODO: Check/fix/update this once the custom validation files are ported in v2.
nohm.middleware(options);
The middleware takes an argument containing the following options:
url
- Url under which the js file will be available. Default: ‘/nohmValidations.js’exclusions
- Object containing exclusions for the validations export - see example for detailsnamespace
- Namespace to be used by the js file in the browser. Default: ‘nohmValidations’extraFiles
- Extra files containing validations. You should only use this if they are not already set via Nohm.setExtraValidations as nohm.connect automatically includes those.maxAge
- Cache control (in seconds)server.use(
nohm.middleware(
// options object
{
url: '/nohm.js',
namespace: 'nohm',
exclusions: {
User: {
// modelName
name: [0], // this will ignore the first validation in the validation definition array for name in the model definition
salt: true, // this will completely ignore all validations for the salt property
},
Privileges: true, // this will completely ignore the Priviledges model
},
},
),
);
If you now include /nohm.js (or default /nohmValidations.js) in your page, you can validate any model in the browser like this:
// using defined namespace from above. default would be nohmValidations
const { valid, errors } = await nohm.validate('User', {
name: 'test123',
email: 'test@test.de',
password: '******',
});
if (valid) {
alert('User is valid!');
} else {
alert('Oh no, your user data was not accepted!');
// errors is the same format as on the server model.errors
}
Of course you have to make sure your supported browsers can handle Async/Await, most likely with a transformation step and Promise shims.
Saving an instance automatically decides whether it needs to be created or updated on the base of checking for user.id.
This means that if you haven’t either manually set the id or load()ed the instance from the database, it will try to create a new instance.
Saving automatically validates the entire instance. If it is not valid, nothing will be saved.
try {
await user.save();
// it's in the db :)
} catch (error) {
if (error instanceof nohm.ValidationError) {
// erros = the errors in validation
}
}
Save can take an optional object containing options, which defaults to this:
user.save({
// If true, no events from this save are published
silent: false,
// By default if user was linked to two objects before saving and the first linking fails, the second link will not be saved either.
// Set this to true to try saving all relations, regardless of previous linking errors.
continue_on_link_error: false,
// Set to true to skip validation entirely.
// _WARNING_: This can cause severe problems. Think hard before using this.
// It skips checking _and setting_ unique indexes.
// It is also NOT passed to linked objects that have to be saved.
skip_validation_and_unique_indexes: false,
});
Calling remove() completely removes the instance from the db, including realtions - but not the related instances - so it is not a cascading remove. This only works on instances where the id is set (manually or from load()).
const user = await nohm.factory('User');
user.id = 123;
try {
await user.remove({
// options object can be omitted
silent: true, // whether remove event is published. defaults to false.
});
} catch (error) {
// removal failed.
}
To populate the properties of an existing instance you have to load it via ID.
If the instance does not exist a new Error(‘not found’) is thrown.
try {
const properties = await user.load(1234);
} catch (err) {
if (err && err.message === 'not found') {
// user does not exist
} else {
// unknown error
}
}
To find an ID of an instance Nohm offers a few simple search functionalities. The function to do so is always .find(), but what it does depends on the arguments given.
Simply calling find() without arguments will retrieve all IDs.
const ids = await SomeModel.find();
// ids = array of ids
To specify indexes to search for you have to pass in an object as the first parameter. There are three kinds of indexes: unique, simple and numeric. Unique is the fastest and if you look for a property that is unqiue all other search criteria is ignored. You can mix the three search queries within one find call. After all search queries of a find() have been processed the intersection of the found IDs is returned.
Simple indexes are created for all properties that have index
set to true and are of the type ‘string’, ‘boolean’, ‘json’ or custom (behavior).
Example:
const ids = await SomeModel.find({
someString: 'hurg',
someBoolean: false,
});
// ids = array of all instances that have (somestring === 'hurg' && someBoolean === false)
// if no instances match the search an empty array is returned
Numeric indexes are created for all properties that have index
set to true and are of the type ‘integer’, ‘float’ or ‘timestamp’.
The search needs to be an object that optionaly contains further filters: min, max, offset and limit.
This uses the redis command zrangebyscore and the filters are the same as the arguments passed to that command. (limit = count)
They default to this:
{
min: '-inf',
max: '+inf',
offset: '+inf', // only used if a limit is defined
limit: undefined
}
To specify an infinite limit while using an offset use limit: 0.
Example:
const ids = await SomeModel.find({
someInteger: {
min: 10,
max: 40,
offset: 15, // this in combination with the limit would work as a kind of pagination where only five results are returned, starting from result 15
limit: 5,
},
SomeTimestamp: {
max: +new Date(), // timestamp before now
},
});
Important: The limit is only specific to the index you are searching for. In this example it will limit the someInteger search to 5 results, but the someTimestamp search is unlimited. Since the overall result will be an intersection of all searches, there can only be as many ids as the limit of the smallest search has.
If you limit multiple searches you might also end up with 0 results even though each search resulted in more ids because there may be no intersection.
It is simpler and recommended to either only limit one search or manually limit the result array in the callback.
You can also search for exact numeric values by using the syntax of a simple index search.
Zrangebyscore Quote:
By default, the interval specified by min and max is closed (inclusive). It is possible to specify an open interval (exclusive) by prefixing the score with the character (.
In nohm you can do this by specifying an endpoints option. The default is ‘[]’ which creates the redis default: inclusive queries.
Example:
const ids = await SomeModel.find({
someInteger: {
min: 10,
max: 20,
endpoints: '(]', // exclude models that have someInteger === 10, but include 20
// endpoints: '(' short form for the same as above
// endpoints: '[)' would mean include 10, but exclude 20
// endpoints: '()' would excludes 10 and 20
},
});
You can sort your models in a few basic ways with the build-in .sort() method. However it might be a good idea to do more complex sorts manually.
let ids = await SomeModel.sort({
// options object
// ids is an array of the first 100 ids of SomeModel instances in the db, sorted alphabetically ascending by name
field: 'name', // field is mandatory
});
ids = await SomeModel.sort({
// ids is an array of the first 100 ids of SomeModel instances in the db, sorted alphabetically descending by name
field: 'name',
direction: 'DESC',
});
ids = await SomeModel.sort({
// ids is an array of 100 ids of SomeModel instances in the db, sorted alphabetically descending by name - starting at the 50th
field: 'name',
direction: 'DESC',
limit: [50],
});
ids = await SomeModel.sort({
// ids is an array of 25 ids of SomeModel instances in the db, sorted alphabetically descending by name - starting at the 50th
field: 'name',
direction: 'DESC',
limit: [50, 25],
});
// this
ids = await SomeModel.sort({
// ids is an array of the 10 last edited instances in the model (provided last_edit is filled properly on edit)
field: 'last_edit',
limit: [-10, 10],
});
// would have the same result as:
ids = await SomeModel.sort({
// ids is an array of the 10 last edited instances in the model (provided last_edit is filled properly on edit)
field: 'last_edit',
direction: 'DESC',
limit: [0, 10],
});
If you have an array of IDs and want them in a sorted order, you can use the same method with the same options but giving the array as the second argument. This is especially useful if combined with find().
// assuming car model
const ferrariIDs = await Car.find({
manufacturer: 'ferrari',
});
const sortedFerariIDs = Car.sort(
{
field: 'build_year',
},
ferrariIDs, // array of found ferrari car ids
);
// sortedFerrariIDs = oldest 100 ferrari cars
Note; If performance is very important it might be a good idea to do this kind of find/sort combination yourself in a multi query to the redis DB or as a lua script in redis, depending on complexity.
Relations (links) are dynamically defined for each instance and not for a model. This differs from traditional ORMs that use RDBMS and thus need a predefined set of tables or columns to maintain these relations. In nohm this is not necessary making it possible for one instance of a model to have relations to models that other instances of the same model do not have.
A simple example: We have 2 instances of the UserModel: User1, User2 We have 3 instances the RoleModel: AdminRole, AuthorRole, UserManagerRole A user can have 0-3 roles. This creates an N:M relationship. In a traditional DB you’d now need a pivot table and then you’d have to somehow tell your ORM that it should use that table to map these relations. In nohm this step is not needed. Instead we just tell every UserModel instance whatever relationships it has.
This has the upside of more flexibility in relations, but the downside of more complexity maintaining these relations.
In nohm all relations have a name pair. By default this pair is “default” and “defaultForeign”. The instance that initiated the relationship is the “default” the one that is linked to it is the “defaultForeign” (“Foreign” is attached to custom link names for this). This again has the upside of more flexibility in relations, but the downside of more complexity maintaining these relations.
Some Examples:
User1.link(AdminRole);
User1.link(AuthorRole);
User2.link(UserManagerRole, 'createdBy');
User2.link(UserManagerRole, 'temp');
Now (after saving) these relations exist:
Tip: Be careful with naming and don’t overuse it!
Usage: instance.link(otherInstance, [options])
This creates a relation (link) to another instance. The most basic usage is to just use the first argument:
User1.link(AdminRole);
The relation is only written to the DB when User1 is saved. (not when saving Admin!)
const User = await nohm.factory('User');
User.link(AdminRole);
try {
await User.save();
// User1 and Admin are saved and the relation is in the DB
} catch (err) {
if (err instanceof Nohm.LinkError) {
// error occured during linking, in this case probably AdminRole validation
// err.errors is an Array of these objects:
/*
{
success: boolean;
child: NohmModel;
parent: NohmModel;
error: null | Error | LinkError | ValidationError;
}
*/
} else if (err instanceof Nohm.ValidationError) {
// User failed to validate
} else {
// unknown error
}
}
There are several things that happen here:
This process works infinitely deep. However this process is not atomic, thus it might be a better idea to save the elements individually and then link them!
link can take an optional options object or link name as the second argument. If it is a string, it’s assumed to be the link name. The options object has 2 available options:
User1.link(ManagerRole, {
name: 'hasRole', // otherwise defaults to "default"
error: function(error, linkedInstance) {
// this is called if there was an error while saving the linked object (ManagerRole in this case)
// error is the error ManagerRole.save() rejected with
// linkedInstance is ManagerRole
},
});
Usage: instance.unlink(otherInstance, [options])
Removes the relation to another instance and otherwise works the same as link.
Usage: instance.belongsTo(otherInstance, [relationName])
This checks if an instance has a relationship to another relationship.
const isManager = await User.belongsTo(ManagerRole);
This requires that User as well as ManagerRole have an id set.
Usage: instance.numLinks(modelName, [relationName])
This checks how many relations of one name pair an Instance has to another Model.
// assuming the relation definitions from above
let num = await User1.numLinks('RoleModel');
// num will be 2
// note that it is not 3, because the default link name is used
// get the amount of links that are named 'createdBy':
num = await User1.numLinks('RoleModel', 'createdBy');
// num will be 1
// get the amount of links that are named 'temp':
num = await User1.numLinks('RoleModel', 'temp');
// num will be 0
Usage: instance.getAll(modelName, [relationName])
This gets the IDs of all linked instances.
let roleIds = await User1.getAll('RoleModel');
// roleIds = [1,2]
roleIds = await User1.getAll('RoleModel', 'createdBy');
// roleIds = [3]
roleIds = await User2.getAll('RoleModel', 'temp');
// roleIds = [3]
Nohm supports a way for seperate clients to get notified of nohm actions in other clients, if they are connected to the same redis database and PubSub is activated.
To use PubSub 2 steps are required:
const secondClient = require('redis').createClient();
await nohm.setPubSubClient(secondClient);
// Yey, we can start subscribing :)
// to close the pubsub connection and make the redis client available for normal commands again:
const client = nohm.closePubSub();
// client == secondClient
// Note: this doesn't stop nohm from publishing, just subscribing
nohm.setPublish(true); // this client will publish on all modelsby default
nohm.setPublish(false); // this client will not publish by default
// This model will publish no matter what the global publish setting is.
nohm.model('Publish', {
properties: {},
publish: true
}):
// This model will only publish if the global setting is set to true.
nohm.model('No_publish', {
properties: {}
});
const model = await nohm.factory('someModelName');
model.getPublish(); // returns whether the model someModelName will publish
There are 6 events that get published:
All* these event callbacks get an object containing these properties:
{
target: {
id: 'id_of_the_instance',
modelName: 'name_of_the_model',
properties: {} // instance.allProperties() from where the event was fired
// only in save/update:
diff: {} // instance.propertyDiff()
}
}
*The Exceptions are link and unlink:
{
child: {
id: 'id_of_the_child_instance',
modelName: 'name_of_the_child_model',
properties: {} // child.allProperties() from where the event was fired
},
parent: {
id: 'id_of_the_parent_instance',
modelName: 'name_of_the_parent_model',
properties: {} // parent.allProperties() from where the event was fired
},
relation: 'child' // relation name
}
To handle subscribing to these events there are 3 functions to use: model.subscribe, model.subscribeOnce and model.unsubscribe.
Subscribe to all actions of a specified event type on a model.
Example:
const model = await nohm.factory('someModel');
model.subscribe('update', function(event) {
console.log(
'someModel with id %s was updated and now looks like this:',
event.target.id,
event.target.properties,
);
});
Subscribe and after it is fired once, unsubcsribe.
Example:
let updates = 0;
const model = await nohm.factory('someModel');
model.subscribeOnce('update', function(event) {
// will only be called once no matter how many updates happen after 1 has published
updates++;
console.log(
'someModel with id %s was updated and now looks like this:',
event.target.id,
event.target.properties,
);
console.log(updates);
});
Unsubscribe one or all listeners.
Example:
const model = await nohm.factory('someModel');
const callback = function(event) {
console.log(
'someModel with id %s was updated and now looks like this:',
event.target.id,
event.target.properties,
);
};
model.subscribe('update', callback);
// to unsubscribe only one:
model.unsubscribe('update', callback);
// or unsubscribe all
model.unsubscribe('update');
Some things that don’t really fit anywhere else in this documentation.
For some functions there are short forms.
Instead of having to do something like:
const user = new User();
await user.load(1);
user.property('name', 'test');
You can do this:
const user = await User.load(1);
user.property('name', 'test');
});
This currently works for the following functions: load, find, save and remove. It is really only a shortcut.
A shortcut for loading all instances of ids in an Array.
Contrary to .load() this does not throw on non existant ids and instead just resolves with all models that existed.
const ids = [1, 2, 1593];
const cars = await CarModel.loadMany(ids);
cars.forEach((car) => {
// if for example id 2 is not found, only car.id 1 and 1593 will be in cars
console.log('A car was found: ', car.allProperties());
});
A shortcut for find() and load().
const cars = await CarModel.findAndLoad({
manufacturer: 'ferrari'
});
// cars = array of nohm instances of ferraris
cars.forEach((car) => {
if (car.property('build_year') < 1990)) {
console.log('You should probably check out', car.id);
}
});