muesli

3.0.3 • Public • Published

Muesli

Simple, ORM-agnostic domain driven model management

npm version Greenkeeper badge Build Status Code Climate CircleCI dependencies:? devDependencies:?

Installation

npm install muesli

or

yarn add muesli

Optional packages(in TODO)

npm install muesli-filters
npm install muesli-validators
npm install muesli-constraints

Usage

Import Model class from muesli package.

import Model from 'muesli';
// or
const Model = require('muesli');

Define your model's props

class Book extends Model {
  
}
Book.props = {
  ISBN: {
    filter: 'string',
    value: '',
    constraints: [],
    validate: (value) => {},
    json: {},
  },
  title: {
    filter: 'string',
  },
  author: {
    filter: (value) => Author.fromJSON(value),
  },
  reference: {
    filter: (ref) => CustomModelStore.findByRef(ref),
  },
};

Add settings for your model

class Book extends Model {
  
}
Book.propsSettings = {
  nameStrategy: 'camel', // currently affected only during json serialization for attributes' keys
  
  strict: false, // if true is set, then model won't accept non schema attributes. It won't throw an error
  throwOnStrictError: false, // if true it will throw an error when `strict` is true and you are trying to set non schema attribute
  
  useGetters: true, // default to true -- give access to direct access to props via model.values object
  useSetters: true, // default to true
 
  validators: [ // model validators that can validate all object's values
    MuesliValidators.equalProps(['password', 'password-confirmation'], ['validation-group1']),
    MuesliValidators.validDates(['birthdate'], 'validation-group2')
  ]
};
Setting name Default value Possible values Description
nameStrategy <empty string> (camel OR pascal OR title OR snake OR lower OR upper OR constant) Props names' serialization strategy. Look inside package https://www.npmjs.com/package/change-case
strict false true OR false if true is set, then model won't accept non schema props.
throwOnStrictError false true OR false if true is set and strict === true, model will throw an error when model tries to set non schema prop
useGetters true true OR false gives direct access to props values via model.values object
useSetters true true OR false
validators [] array of model validators that can validate through whole model

Model validation

const book = Book.fromJSON({ ISBN: '1232412-123' });
 
book.validate()
  .then((validationErrors) => {
    if (validationErrors.length) {
      // Model doesn't pass validation constraints
    } else {
      // everything is good
    }
  })
  .catch((error) => {
    // FATAL errors occurred during validation process
    console.error(error);
  });

There is static method to make the same operation quicker

Book.validate({ ISBN: '12345123-123' })
  .then((validationErrors) => {
    if (validationErrors.length) {
      // Model doesn't pass validation constraints
    } else {
      // everything is good
    }
  })
  .catch((error) => {
    // FATAL errors occurred during validation process
    console.error(error);
  });

You can also validate only part of the model using validation group

book.validate(['group1'])
  .then((validationErrors) => {
    if (validationErrors.length) {
      // Model doesn't pass validation constraints
    } else {
      // everything is good
    }
  })
  .catch((error) => {
    // FATAL errors occurred during validation process
    console.error(error);
  });

Custom constraints and validators

Constraints and validators are asynchronous by default, but you not required to use Promises if you don't need to.

const customConstraint = (groups = []) => {
  return (propValue, propName, currentGroup) => {
    return new Promise((resolve, reject) => {
      if (!groups.includes(currentGroup)) {
        // It is very important that you handle validation groups inside custom constraint 
        resolve();
        return;
      }
      if (propValue !== 'custom value') {
        reject(new Error(`${propName} must be equals 'custom value'`));
        return;
      }
      resolve();
    });
  };
};
const customValidator = (groups = []) => {
  return (values, currentGroup) => {
    if (!groups.includes(currentGroup)) {
      // It is very important that you handle validation groups inside custom constraint 
      return;
    }
    if (values.password !== values.password_confirmation) {
      throw new Error('Password and password confirmation must be equal');
    }
    // You don't need to return anything if everything is fine
  };
};

Computed props

class Author extends Model {}
Author.props = {
  firstName: {
    filter: String,
    value: 'Dmytro',
  },
  lastName: {
    filter: String,
  },
  fullName: {
    filter: function (deps = []) {
      return deps.filter((v) => v).join(' ');
    },
    computed: ['firstName', 'lastName'],
  },
};
const author = new Author();
 
console.log(author.get('fullName')); // output -> 'Dmytro'
author.set('lastName', 'Zelenetskyi');
console.log(author.get('fullName')); // output -> 'Dmytro Zelenetskyi'

Deserializing and serializing model(fromJSON/toJSON)

class Author extends Model {}
 
Author.props = {
 name: {
   filter: 'string',
   constraints: [MusliConstraints.required()],
 },
 lastName: {
   filter: 'string',
 },
}; 
 
const horror = Author.fromJSON({
  name: 'Stephen',
  lastName: 'King',
});
console.log(horror instanceof Author);
// outputs `true`
 
console.log(horror.toJSON());
// outputs `{ name: "Stephen", lastName: "King" }`

Using useGetters and useSetters options

if useGetters or/and useSetters options are set to true, then you can use props' values directly via model.values object.

Example:

class Author extends Model {}
Author.props = {
 name: {
   filter: 'string',
   value: 'Default value',
 },
 age: {
   filter: 'number',
   value: 0,
 },
};
Author.propsSettings = {
  useGetters: true,
  useSetters: true,
};
 
const model = new Author();
console.log(model.values.name); // outputs: 'Default value'
console.log(model.values.age); // outputs: 0
 
// or you can use setter
model.values.age = 30;
console.log(model.values.age); // outputs: 30

model.values object can't be extended. Only props from schema are available. Setters won't be provided for computed properties.

Model version

With each model mutation model increases it's version, so you can track it.

const author = Author.fromJSON({ name: 'Stephen' });
console.log(model.version); // output: 1
model.set('name', 'Dmytro');
console.log(model.version); // output: 2

Creating ORM-like entities

const pg = require('pg');
const SQL = require('sql-template-strings')
 
class Entity extends Model {
  static get source() {
    throw new Error('Implement me');
  }
  
  static async findById(id) {
    const rows = await pool.query(SQL`SELECT * FROM "`.append(this.source).append(SQL`" WHERE id=${id}`));
    return this.fromJSON(rows[0]);
  }
}
 
class UserEntity extends Entity {
  static get source() {
    return 'users';
  }
  
  static get props() {
    return {
      first_name: {
        filter: 'string',
      },
      last_name: {
        filter: 'string',
      },
      password: {
        filter: 'string',
      },
    };
  }
}
 
const user = UserEntity.findById(1);
console.log(user.get('first_name'));
console.log(user.values);

Events

Model inherits event system from rx-emitter(github link) package that uses rxjs and handles all events as observables.

const book = new Book();
 
Rx.Observable.combineLatest(
  book.subject('chapter1'),
  book.subject('chapter2'),
)
.subscribe(() => {
  console.log('Two first chapters are ready');
});
 
book.publish('chapter1');
book.publish('chapter2');

License

Muesli is released under the MIT license.

Donate

Package Sidebar

Install

npm i muesli

Weekly Downloads

2

Version

3.0.3

License

MIT

Last publish

Collaborators

  • hunterman