Nohm
Description
Nohm is an object relational mapper (ORM) written for node.js and redis written in Typescript.
Features
- Standard ORM features (validate, store, search, sort, link/unlink, delete)
- Share validations with browser.
Allows using the same code for client validations that is used for backend. Includes filtering which validations are shared. - Subscribe to orm events (save, delete, link, unlink)
With this you can do things like socket connections to get live updates from stored models.
Since it uses redis PUBSUB you can scale your node app and clients can connect to separate node app instances but will still get the same live updates. - Typescript typings
nohm is written in Typescript and thus provides first-class typings for most things, including the option to type your model properties. This means if you use Typescript you don't have to remember every single property name of each model anymore, your IDE can tell you. - Dynamic relations
This is a double-edged sword. Usually ORMs describe relations statically and you have to do database changes before you can add new relations.
In nohm all relations are defined and used at run-time, since there are no schemas stored in the database.
Requirements
Documentation
v2 documentation
API docs
v1 documentation
v1 to v2 migration guide
Example
The examples/rest-user-server is running as a demo on https://nohm-example.maritz.space. It showcases most features on a basic level, including the shared validation and PubSub.
Example ES6 code (click to expand)
import { Nohm, NohmModel, ValidationError } from 'nohm';
const nohm = Nohm;
nohm.setPrefix('example');
const Model = NohmModel;
const existingCountries = ['Narnia', 'Gondor', 'Tatooine'];
class UserModel extends Model {
getCountryFlag() {
return `http://example.com/flag_${this.property('country')}.png`;
}
}
UserModel.modelName = 'User';
UserModel.definitions = {
email: {
type: 'string',
unique: true,
validations: ['email'],
},
country: {
type: 'string',
defaultValue: 'Narnia',
index: true,
validations: [
async function checkCountryExists(value) {
return existingCountries.includes(value);
},
{
name: 'length',
options: { min: 3 },
},
],
},
visits: {
type: function incrVisitsBy(value, key, old) {
return parseInt(old, 10) + parseInt(value, 10);
},
defaultValue: 0,
index: true,
},
};
const UserModelClass = nohm.register(UserModel);
const redis = require('redis').createClient();
redis.on('connect', async () => {
nohm.setClient(redis);
const user = await nohm.factory('User');
user.property({
email: 'mark13@example.com',
country: 'Gondor',
visits: 1,
});
try {
await user.save();
} catch (err) {
if (err instanceof ValidationError) {
for (const key in err.errors) {
const failures = err.errors[key].join(`', '`);
console.log(
`Validation of property '${key}' failed in these validators: '${failures}'.`,
);
}
}
throw err;
}
console.log(`Saved user with id ${user.id}`);
const id = user.id;
const loadedUser = await UserModelClass.load(id);
console.log(`User loaded. His properties are %j`, loadedUser.allProperties());
const newVisits = loadedUser.property('visits', 20);
console.log(`User visits set to ${newVisits}.`);
const gondorians = await UserModelClass.findAndLoad({
country: 'Gondor',
});
console.log(
`Here are all users from Gondor: %j`,
gondorians.map((u) => u.property('email')),
);
await loadedUser.remove();
console.log(`User deleted from database.`);
});
Example Typescript code (click to expand)
import { Nohm, NohmModel, TTypedDefinitions } from 'nohm';
interface IUserProperties {
email: string;
visits: number;
}
class UserModel extends NohmModel<IUserProperties> {
public static modelName = 'User';
protected static definitions: TTypedDefinitions<IUserProperties> = {
email: {
type: 'string',
unique: true,
validations: ['email'],
},
visits: {
defaultValue: 0,
index: true,
type: function incrVisitsBy(value, _key, old): number {
return old + value;
},
},
};
public getVisitsAsString(): string {
return this.property('visits');
}
public static async loadTyped(id: string): Promise<UserModel> {
return userModelStatic.load<UserModel>(id);
}
}
const userModelStatic = nohm.register(UserModel);
async function main() {
const user = await userModelStatic.load<UserModel>('some id');
const props = user.allProperties();
props.email;
props.id;
props.visits;
props.foo;
user.getVisitsAsString();
}
main();
More detailed examples
Do you have code that should/could be listed here? Message me!
Add it to your project
npm install --save nohm
Debug
Nohm uses the debug module under the namespace "nohm". To see detailed debug logging set the environment variable DEBUG accordingly:
DEBUG="nohm:*" node yourApp.js
Available submodule debug namespaces are nohm:index
, nohm:model
, nohm:middleware
, nohm:pubSub
and nohm:idGenerator
.
Developing nohm
If you want to make changes to nohm, you can fork or clone it. Then install the dependencies:
npm install
and run the development scripts (compile & watch & tests):
npm run dev
When submitting PRs, please make sure that you run the linter and that everything still builds fine.
The easiest way to do that is to run the prepublishOnly
script:
npm run prepublishOnly
Running tests
Build the javascript files:
npm run build
Then run the tests:
npm run test
# or
npm run test:watch
This requires a running redis server. (you can configure host/port with the command line arguments --redis-host 1.1.1.1 --redis-port 1234)
WARNING: The tests also create a lot of temporary keys in your database that look something like this:
nohmtestsuniques:something:something
After the tests have run all keys that match the pattern nohmtests* are deleted!
You can change the prefix ("nohmtests") part doing something like
node test/tests.js --nohm-prefix YourNewPrefix
Now the keys will look like this:
YourNewPrefixuniques:something:something