Giter VIP home page Giter VIP logo

nohm's Introduction

Nohm

Known Vulnerabilities (Snyk) Coverage Status

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

  • redis >= 2.4

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';
// or if your environment does not support module import
// const NohmModule = require('nohm'); // access NohmModule.Nohm, NohmModule.NohmModel and NohmModule.ValidationError

// This is the parent object where you set redis connection, create your models and some other configuration stuff
const nohm = Nohm;

nohm.setPrefix('example'); // This prefixes all redis keys. By default the prefix is "nohm", you probably want to change it to your applications name or something similar

// This is a class that you can extend to create nohm models. Not needed when using nohm.model()
const Model = NohmModel;

const existingCountries = ['Narnia', 'Gondor', 'Tatooine'];

// Using ES6 classes here, but you could also use the old nohm.model definition
class UserModel extends Model {
  getCountryFlag() {
    return `http://example.com/flag_${this.property('country')}.png`;
  }
}
// Define the required static properties
UserModel.modelName = 'User';
UserModel.definitions = {
  email: {
    type: 'string',
    unique: true,
    validations: ['email'],
  },
  country: {
    type: 'string',
    defaultValue: 'Narnia',
    index: true,
    validations: [
      // the function name will be part of the validation error messages, so for this it would be "custom_checkCountryExists"
      async function checkCountryExists(value) {
        // needs to return a promise that resolves to a bool - async functions take care of the promise part
        return existingCountries.includes(value);
      },
      {
        name: 'length',
        options: { min: 3 },
      },
    ],
  },
  visits: {
    type: function incrVisitsBy(value, key, old) {
      // arguments are always string here since they come from redis.
      // in behaviors (type functions) you are responsible for making sure they return in the type you want them to be.
      return parseInt(old, 10) + parseInt(value, 10);
    },
    defaultValue: 0,
    index: true,
  },
};

// register our model in nohm and returns the resulting Class, do not use the UserModel directly!
const UserModelClass = nohm.register(UserModel);

const redis = require('redis').createClient();
// wait for redis to connect, otherwise we might try to write to a non-existent redis server
redis.on('connect', async () => {
  nohm.setClient(redis);

  // factory returns a promise, resolving to a fresh instance (or a loaded one if id is provided, see below)
  const user = await nohm.factory('User');

  // set some properties
  user.property({
    email: '[email protected]',
    country: 'Gondor',
    visits: 1,
  });

  try {
    await user.save();
  } catch (err) {
    if (err instanceof ValidationError) {
      // validation failed
      for (const key in err.errors) {
        const failures = err.errors[key].join(`', '`);
        console.log(
          `Validation of property '${key}' failed in these validators: '${failures}'.`,
        );

        // in a real app you'd probably do something with the validation errors (like make an object for the client)
        // and then return or rethrow some other error
      }
    }
    // rethrow because we didn't recover from the error.
    throw err;
  }
  console.log(`Saved user with id ${user.id}`);

  const id = user.id;

  // somewhere else we could then load the user again
  const loadedUser = await UserModelClass.load(id); // this will throw an error if the user cannot be found

  // alternatively you can use nohm.factory('User', id)

  console.log(`User loaded. His properties are %j`, loadedUser.allProperties());
  const newVisits = loadedUser.property('visits', 20);
  console.log(`User visits set to ${newVisits}.`); // Spoiler: it's 21

  // or find users by country
  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';

// We're gonna assume the basics are clear and the connection is set up etc. - look at the ES6 example otherwise.
// This example highlights some of the typing capabilities in nohm.

interface IUserProperties {
  email: string;
  visits: number;
}

class UserModel extends NohmModel<IUserProperties> {
  public static modelName = 'User';

  protected static definitions: TTypedDefinitions<IUserProperties> = {
    // because of the TTypedDefinitions we can only define properties keys here that match our interface keys
    // the structure of the definitions is also typed
    email: {
      type: 'string', // the type value is currently not checked. If you put a wrong type here, no compile error will appear.
      unique: true,
      validations: ['email'],
    },
    visits: {
      defaultValue: 0,
      index: true,
      type: function incrVisitsBy(value, _key, old): number {
        return old + value; // TS Error: arguments are all strings, not assignable to number
      },
    },
  };

  public getVisitsAsString(): string {
    return this.property('visits'); // TS Error: visits is number and thus not assignable to string
  }

  public static async loadTyped(id: string): Promise<UserModel> {
    // see main() below for explanation
    return userModelStatic.load<UserModel>(id);
  }
}

const userModelStatic = nohm.register(UserModel);

async function main() {
  // currently you still have to pass the generic if you want typing for class methods
  const user = await userModelStatic.load<UserModel>('some id');
  // you can use the above defined loadTyped method to work around that.

  const props = user.allProperties();
  props.email; // string
  props.id; // any
  props.visits; // number
  props.foo; // TS Error: Property foo does not exist
  user.getVisitsAsString(); // string
}

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

nohm's People

Contributors

brysgo avatar dependabot[bot] avatar displague avatar index0h avatar johngeorgewright avatar karlbohlmark avatar lionello avatar maritz avatar maritz-ergo avatar midworld avatar mkuklis avatar nfyfamr avatar roxasshadow avatar samccone avatar tarkus avatar yuchi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

nohm's Issues

clarify delete() docs

delete() deletes the links themselves but not the related objects. this is not clear from the documentation.

Find() with limit argument

Hello !

It will be very useful for me to have the limit argument with find() :

User.find({
limit: 10 // maximum of 10 keys in the result
}, function(err, ids) {
// the maximum possible length of ids is 10
});

Is it possible ?
Thanks :-)

TypeError: Object #<Nohm> has no method 'has'

Hi!

I am getting this issue constantly when I try and detect a relationship between two objects.

Here is an example of the code :

var profile = Nohm.factory( 'Profile', id, function ( err )
{
if ( err )
return res.json( { result : 'error', message : err } );

    profile.has( image, 'images', function ( err, has )
            {
                                  ....

image is an instance of Nohm.factory( 'Image', id, ....

Can anyone please provide an example to this, or any details on where I am going wrong?

Thank you :)

What is the preferred way of doing migrations/schema changes?

I have started building an application on top of nohm, and recently needed to add an index to a model. This of course leaves all the entities already added of this model type unindexed.

What is your workflow for doing this, or other model changes on existing data? Maybe something to mention in the readme?

Unique check does not invalidate default values

Instead the unique id is just overwritten. (but only if the instance has an id)

To work around this it would probably be good to rename "value" to "default" (or "defaultValue", or both since we'd have to hide the default from accidental changes anyways) and keep the current actual value in "value". This way we'd be able to know the difference between "not updated" and "still default".

TODO: implement methods!

Hi, I'm very interested in your project. Below is a patch with the implementation methods for the models.

Nohm.model = function (name, options) { // TODO: implement methods!
    var obj = function (id) {
        this.init(name, options);
        if(typeof(id) === 'number') {
            this.id = id;
            this.load(id);
        }
    };
    obj.prototype = new Nohm();
    if(options.methods != null){
        try{
            for(var method in options.methods){
                if(typeof(options.methods[method]) == 'function'
                    & typeof(obj.prototype[method]) == 'undefined'){
                    obj.prototype[method] = options.methods[method];
                }
            }
        } catch(err){
            console.log(err);
        }
    }
    return obj;
};

empty unique

empty strings should not be lockable as uniques.

way to get the parameters of a validation for the error message needed

Currently if you get a validation error like "length", that doesn't really help the user, even if you have some i18n lookup, because you can't get the parameters of the validation.

You'd need to be able to get them and then you can tell the user "input needs to be between 2 and 418 characters".

Introducing Nohm.relations

Something that dries to:

Nohm.relations = {
  'child' : 'parent'
};

Nohm.__reversedRelations = function () {
  var reversed = {};
  for (var child in Nohm.relations) {
    if (Nohm.relations.hasOwnProperty(child)) {
      reversed[Nohm.relations[child]] = child;
    }
  }
  return reversed;
};

Nohm.getRelationParentName = function (name) {
  var parent = Nohm.relations[name];
  return parent || Nohm.toDefaultParentName(name);
};

Nohm.getRelationChildName = function (name) {
  return Nohm.__reversedRelations()[name] || Nohm.toDefaultChildName(name);
};

Nohm.toDefaultParentName = function (name) {
  return name + 'Parent';
};

Nohm.toDefaultChildName = function (name) {
  // regexp? :(
};

This way I could be able to write:

Nohm.relations[ 'grandson' ] = 'grandpa';

And I'm not forced to use name and nameParent, that sometimes is confusing.

Permit `defaultValue`s to be dynamic (functions)

If defaultValues can be functions then

Todo = nohm.model 'Todo',
  properties:
    # ...
    creationDate:
      type: 'timestamp',
      defaultValue: ->
        + new Date()
    # ...

sets the creationDate in its place, for example.

Model sort by DESC

Hello ;)

In the documentation, you say

"Note that the ID array is sorted by default from lowest to highest."

Is it possible to sort the key by a field directly in redis, maybe when I define the model ? My object has a timestamp property. I want to have each "hash" sorted from newest to oldest.

Thanks !

validation

valid() arguments handling needs to be improved by using getCallback

valid() user.errors needs to be reset if key is not set

user.errors needs to have better custom error descriptons (maybe count all custom validators and add count to the string)

Pub/Sub on saves/links/removes

Saves, links and removes (possibly loads) should publish to a special channel on redis.

Optional: Way to do beforeXXXX on all three where a subscriber can message the publisher back to not do the action it was supposed to do. This might become too complicated for simple maintainability very soon.

deeplinking save errors

Currently there is no way to know if a validation failed in an instance or one of its links/deeplinks.
It should be somehow possible to know which instance failed from withing the save callback (other than going through all the instances and checking for their errors array).

ability to search for exact numerics

find() should automatically decide whether to use the zset or set by looking at the search criteria: object is zset, simple (! isNaN(parseInt(search))) is set.

errors array should be cleared

The errors array should be cleared on valid() call without a key specified. (and the errors[key] when key is specified)

unable to save data, or use logger in save call

Hi,

I'm setting up Nohm at the moment, and I'm finding that a call to model.save() swallows all logging output, and doesn't seem to execute. It's probably a mistake on my part, as I'm in the early stages of learning Node, but I've worked through your documentation (and the github examples) several times.

This is my model:

Look = nohm.model 'Look',
    properties:
        name:
            type: 'string'

This is my nohm config:

nohm = require('nohm').Nohm
winston =  require 'winston'

exports.setup = (client) ->
    nohm.setPrefix 'etuii'
    nohm.setClient client

    nohm.logError = (err) ->
        if err
            winston.error err
            console.trace()
    nohm

This is my test to save the model:

    it "creates a model", ->
        look = nohm.factory('Look')
        look.p({'name': "test1"})

        look.save (e) -> 
            console.log "...."
            if e
                look.errors
                                winston.error e
                console.log "error: ", e.toString(), look.errors
            else
                console.log ">>>"
                winston.info look.id, " <<<<<<<"
                should.exist look.id

etc ...

No logging occurs in the save call. Nor is the object written to redis.

Can you see anything obvious about what I might be doing wrong or should it be working?

Cheers,
Nicholas

Foo.load(id), Foo.find, Foo.remove(id)

Hi!

The methods in the subj would be good to have as model static methods:

Foo.load(1, function(err, foo1) { foo1 loaded })
instead of, or in addition to
var foo1 = new Foo; foo1.load(1, function(err) { foo1 loaded})

Foo.remove(1, function(err) { foo#1 deleted })
instead of, or in addition to
var foo1 = new Foo; foo1.load(1, function(err) { foo1.remove(1), function(err) { foo#1 deleted }) })

etc.

What do you think?

TIA,
--Vladimir

'ni' or 'nohm' error?

Hi Moritz,

I can't start 'admin' application with the following error:

alexo@aozerov:~/Projects/git/nohm/admin$ node app.js 
TypeErrorCannot call method 'extend' of undefinedTypeError: Cannot call method 'extend' of undefined
    at Object. (/home/alexo/Projects/git/nohm/admin/models/User.js:4:29)
    at Module._compile (module.js:380:26)
    at Object..js (module.js:386:10)
    at Module.load (module.js:312:31)
    at Function._load (module.js:273:12)
    at require (module.js:324:19)
    at /home/alexo/Projects/git/nohm/admin/ni.js:436:25
    at /home/alexo/Projects/git/nohm/admin/ni.js:468:33
    at Array.forEach (native)
    at Function.readFiles (/home/alexo/Projects/git/nohm/admin/ni.js:464:31)ErrorEISDIR, Is a directoryError: EISDIR, Is a directory

What can be the reason of the error?

And by the way, there is missed comma at line 5 of the file 'admin/app.js':
https://github.com/maritz/nohm/blob/master/admin/app.js#L5

Nohm to browser

Hi, I'm trying to port your module to use it in browser with localStorage (in future Gears/Air/WebSQL and browser-server syncronization) and I have some problems with saving models with unique fields. There is no updating, just creating new objects in storage.
I changed all Object.forEach functions to underscore's _.each() and all require path to work with node-browserify Could you help me to find the problem? Thanks in advance.

connect middleware

a connect middleware to serve a js file with the model validations to be used in the browser.

Failing to return factory methods.

I'm trying to import a model from another module, but I can't seem to get/use it's properties and methods. For example,

This is the module

var nohm = require('../lib/nohm').Nohm;                     
var redis = require('../lib/nohm/node_modules/redis');                      

nohm.setClient(redis);                      
nohm.model('user',{ /*-- snip --*/ });                      
var user = '';                      
exports.user = nohm.factory('user');

Loading the module works fine and I can even view the values returned by the factory. The error is when I try create and save. Here's the follow error when calling user.p({ username: 'first last' });
Object # has no method 'p'

'this' in the load method refers to the global scope

When I call this.allProperties() from inside a load method, I receive the following error:
TypeError: Object # has no method 'allProperties'

If I run console.log(this) inside the load method, it logs the global object to console.
My fix is to change line 53 of retrieve.js to callback.call(self, err);

full example

is there a complete (working) example that shows everything together? would be nice to have.

thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.