├── .babelrc ├── lib ├── createGetterAndSetter.js ├── typeBuilders │ └── objectsByKey.js ├── ReactEntityCollection.js └── ReactEntity.js ├── .gitignore ├── .travis.yml ├── jasmine.json ├── spec ├── jasmine.json ├── jasmine-runner.js ├── typeBuilders │ └── objectsByKey.spec.js ├── fixtures │ └── fakerClasses.js └── ReactEntity.spec.js ├── .editorconfig ├── src ├── typeBuilders │ └── objectsByKey.js ├── ReactEntityCollection.js └── ReactEntity.js ├── .eslintrc ├── package.json ├── LICENSE └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 1 3 | } 4 | -------------------------------------------------------------------------------- /lib/createGetterAndSetter.js: -------------------------------------------------------------------------------- 1 | "use strict"; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.2.1" 4 | -------------------------------------------------------------------------------- /jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": ".", 3 | "spec_files": [ 4 | "./spec/**/*.spec.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /spec/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": ".", 3 | "spec_files": [ 4 | "./spec/**/*.spec.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | #http://EditorConfig.org 2 | root = true 3 | 4 | # Indentation override for all JS under lib directory 5 | [{src/**.js}] 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /spec/jasmine-runner.js: -------------------------------------------------------------------------------- 1 | require("babel/register")({ 2 | stage: 1 3 | }); 4 | 5 | var Jasmine = require('jasmine'); 6 | var SpecReporter = require('jasmine-spec-reporter'); 7 | 8 | var jrunner = new Jasmine(); 9 | jasmine.getEnv().addReporter(new SpecReporter()); // add jasmine-spec-reporter 10 | jrunner.loadConfigFile('spec/jasmine.json'); // load jasmine.json configuration 11 | jrunner.execute(); 12 | -------------------------------------------------------------------------------- /src/typeBuilders/objectsByKey.js: -------------------------------------------------------------------------------- 1 | const objectsByKey = (Type) => { 2 | return { 3 | type: Type, 4 | //TODO validate to use the schema of Type 5 | validator: () => undefined, 6 | builder: (data) => { 7 | return Object.keys(data).reduce((result, key) => { 8 | result[key] = new Type(data[key]); 9 | return result; 10 | },{}) 11 | } 12 | } 13 | }; 14 | 15 | export default objectsByKey; 16 | -------------------------------------------------------------------------------- /lib/typeBuilders/objectsByKey.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var objectsByKey = function objectsByKey(Type) { 7 | return { 8 | type: Type, 9 | //TODO validate to use the schema of Type 10 | validator: function validator() { 11 | return undefined; 12 | }, 13 | builder: function builder(data) { 14 | return Object.keys(data).reduce(function (result, key) { 15 | result[key] = new Type(data[key]); 16 | return result; 17 | }, {}); 18 | } 19 | }; 20 | }; 21 | 22 | exports["default"] = objectsByKey; 23 | module.exports = exports["default"]; -------------------------------------------------------------------------------- /spec/typeBuilders/objectsByKey.spec.js: -------------------------------------------------------------------------------- 1 | import objectsByKey from '../../src/typeBuilders/objectsByKey'; 2 | 3 | import { ChildrenEntity, ProductEntity } from '../fixtures/fakerClasses'; 4 | 5 | describe('objectsByKey', () => { 6 | it('should create the type based on parameter', () => { 7 | const newType = objectsByKey(ChildrenEntity); 8 | 9 | expect(newType.type).toBe(ChildrenEntity); 10 | }); 11 | 12 | it('should build objects by its type', () => { 13 | const newType = objectsByKey(ChildrenEntity); 14 | 15 | const result = newType.builder({ sample: {}, other: {} }); 16 | 17 | expect(result.sample.constructor).toBe(ChildrenEntity); 18 | expect(result.other.constructor).toBe(ChildrenEntity); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true, 4 | "modules": true 5 | }, 6 | "env": { 7 | "browser": true, 8 | "es6": true, 9 | "node": true 10 | }, 11 | "parser": "babel-eslint", 12 | "rules": { 13 | "eol-last": 2, 14 | "max-len": [2, 120, 4], 15 | "no-underscore-dangle": [0], 16 | "no-var": 2, 17 | "react/display-name": 0, 18 | "react/jsx-boolean-value": 2, 19 | "react/jsx-quotes": 2, 20 | "react/jsx-no-undef": 2, 21 | "react/jsx-sort-props": 0, 22 | "react/jsx-uses-react": 2, 23 | "react/jsx-uses-vars": 2, 24 | "react/no-did-mount-set-state": 2, 25 | "react/no-did-update-set-state": 2, 26 | "react/no-multi-comp": 0, 27 | "react/no-unknown-property": 2, 28 | "react/prop-types": 2, 29 | "react/react-in-jsx-scope": 2, 30 | "react/self-closing-comp": 2, 31 | "react/wrap-multilines": 2, 32 | "quotes": [2, "single"], 33 | "space-before-function-paren": [2, { 34 | "anonymous": "always", 35 | "named": "never" 36 | }], 37 | "strict": [2, "global"] 38 | }, 39 | "plugins": [ 40 | "react" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-entity", 3 | "version": "1.3.4", 4 | "description": "Create entities base on React propTypes", 5 | "main": "lib/ReactEntity.js", 6 | "scripts": { 7 | "test": "node spec/jasmine-runner", 8 | "coverage": "babel-istanbul cover 'spec/jasmine-runner.js' -x '**/spec/**' && open ./coverage/lcov-report/index.html" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/scup/react-entity" 13 | }, 14 | "keywords": [ 15 | "Javascript", 16 | "Entities", 17 | "React", 18 | "Entities" 19 | ], 20 | "author": "Scup", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/scup/react-entity/issues" 24 | }, 25 | "homepage": "https://github.com/scup/react-entity", 26 | "dependencies": { 27 | "babel": "^5.8.23", 28 | "babel-core": "^6.3.15", 29 | "babel-istanbul": "^0.5.9", 30 | "lodash": "^4.6.1" 31 | }, 32 | "devDependencies": { 33 | "faker": "^3.0.1", 34 | "jasmine": "^2.3.2", 35 | "jasmine-spec-reporter": "^2.4.0", 36 | "react": "^0.14.3", 37 | "sinon": "^1.17.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Scup 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/ReactEntityCollection.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const LODASH_METHODS = [ 4 | 'chunk', 'compact', 'concat', 'countBy', 'difference', 5 | 'differenceBy', 'differenceWith', 'drop', 'dropRight', 6 | 'dropRightWhile', 'dropWhile', 'each', 'eachRight', 7 | 'every', 'fill', 'filter', 'find', 'findIndex', 8 | 'findLast', 'findLastIndex', 'first', 'flatMap', 9 | 'flatten', 'flattenDeep', 'flattenDepth', 'forEach', 10 | 'forEachRight', 'fromPairs', 'groupBy', 'head', 11 | 'includes', 'indexOf', 'initial', 'intersection', 12 | 'intersectionBy', 'intersectionWith', 'invokeMap', 13 | 'join', 'keyBy', 'last', 'lastIndexOf', 'map', 'orderBy', 14 | 'partition', 'pull', 'pullAll', 'pullAllBy', 'pullAllWith', 15 | 'pullAt', 'reduce', 'reduceRight', 'reject', 'remove', 16 | 'reverse', 'sample', 'sampleSize', 'shuffle', 'size', 17 | 'slice', 'some', 'sortBy', 'sortedIndex', 'sortedIndexBy', 18 | 'sortedIndexOf', 'sortedLastIndex', 'sortedLastIndexBy', 19 | 'sortedLastIndexOf', 'sortedUniq', 'sortedUniqBy', 'tail', 'take', 20 | 'takeRight', 'takeRightWhile', 'takeWhile', 'union', 'unionBy', 21 | 'unionWith', 'uniq', 'uniqBy', 'uniqWith', 'unzip', 'unzipWith', 22 | 'without', 'xor', 'xorBy', 'xorWith', 'zip', 'zipObject', 23 | 'zipObjectDeep', 'zipWith' 24 | ]; 25 | 26 | class ReactEntityCollection { 27 | constructor(data) { 28 | this.items = _.map(data, ( item ) => { 29 | if( _.isNil(item) || _.isPlainObject(item) ) { 30 | return new this.constructor.TYPE(item); 31 | } 32 | 33 | return item; 34 | }); 35 | } 36 | 37 | result() { 38 | return this.items; 39 | } 40 | } 41 | 42 | const reduceToNewItem = (all, arg) => { 43 | all.push(arg); 44 | return all; 45 | }; 46 | 47 | _.each(LODASH_METHODS, (method) => { 48 | ReactEntityCollection.prototype[method] = function () { 49 | const args = _.reduce(arguments, reduceToNewItem, [ this.items ]) ; 50 | 51 | const result = _[method].apply(_, args); 52 | 53 | if ( !_.isArray(result) ) { return result; } 54 | 55 | return new this.constructor(result); 56 | } 57 | }); 58 | 59 | export default ReactEntityCollection; 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## react-entity - Create entities base on [React](https://github.com/facebook/react) [propTypes](https://facebook.github.io/react/docs/reusable-components.html) 2 | 3 | [![Build Status](https://travis-ci.org/scup/react-entity.svg?branch=master)](https://travis-ci.org/scup/react-entity) 4 | 5 | This package let you create entities with schema validator like React PropTypes. 6 | 7 | * [Installing](#installing) 8 | * [Using](#using) 9 | * [Examples](#examples) 10 | 11 | ### Installing 12 | $ npm install react-entity 13 | 14 | ### Using 15 | 16 | #### Sample Entities 17 | ```javascript 18 | import { PropTypes } from 'react'; 19 | 20 | class MyEntity extends ReactEntity { 21 | static SCHEMA = { 22 | field: PropTypes.string, 23 | otherField: { 24 | type: PropTypes.number, 25 | defaultValue: 10 26 | } 27 | } 28 | } 29 | 30 | class FatherEntity extends ReactEntity { 31 | static SCHEMA = { 32 | children: { 33 | validator: PropTypes.arrayOf(PropTypes.instanceOf(MyEntity)), 34 | type: MyEntity 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | #### Get default values 41 | ```javascript 42 | const niceInstance = new MyEntity(); 43 | console.log(niceInstance.fetch()); // { field: undefined, otherField: 10 } 44 | console.log(niceInstance.errors); // {} 45 | ``` 46 | 47 | #### Validations 48 | ```javascript 49 | const buggedInstance = new MyEntity({ field: 10, otherField: 'value' }); 50 | console.log(buggedInstance.fetch()); // { field: 10, otherField: 'value' } 51 | console.log(buggedInstance.errors); /* or buggedInstance.getErrors() -- but... getErrors also includes children errors 52 | { 53 | field: { 54 | errors: [ 'Invalid undefined `field` of type `number` supplied to `MyEntityEntity`, expected `string`.' ] 55 | }, 56 | otherField: { 57 | errors: [ 'Invalid undefined `otherField` of type `string` supplied to `MyEntityEntity`, expected `number`.' ] 58 | } 59 | } 60 | */ 61 | ``` 62 | 63 | #### Validate on change value 64 | ```javascript 65 | const otherInstance = new MyEntity({ field: 'myString' }); 66 | console.log(otherInstance.errors); // {} 67 | console.log(otherInstance.valid); // true 68 | 69 | otherInstance.field = 1; 70 | console.log(otherInstance.errors); // {field: { errors: [ 'Invalid undefined `field` of type `number` supplied to `MyEntityEntity`, expected `string`.' ] }} 71 | console.log(otherInstance.valid); // false 72 | ``` 73 | 74 | #### Parse children to Entity 75 | ```javascript 76 | const fatherInstance = new FatherEntity({ 77 | children: [{ 78 | field: 'A', 79 | otherField: 2 80 | }, { 81 | field: 'B', 82 | otherField: 3 83 | }] 84 | }) 85 | console.log(fatherInstance.children[0]); //An instance of MyEntity 86 | console.log(fatherInstance.children[1].fetch()); 87 | //{ field: 'B', otherField: 3 } 88 | ``` 89 | 90 | #### Clean unexpected values 91 | ```javascript 92 | const anotherInstance = new MyEntity({ field: 'myString', fake: 'fake' }); 93 | console.log(anotherInstance.fetch()); // { field: 'myString', otherField: 10 } 94 | ``` 95 | To understand the validators [React PropTypes](https://facebook.github.io/react/docs/reusable-components.html) 96 | 97 | ### Well known issues 98 | - Create helpers for relationships validations(Like, mininum, maximum) 99 | - Create identifier and equal comparison 100 | -------------------------------------------------------------------------------- /lib/ReactEntityCollection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 10 | 11 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 12 | 13 | var _lodash = require('lodash'); 14 | 15 | var _lodash2 = _interopRequireDefault(_lodash); 16 | 17 | var LODASH_METHODS = ['chunk', 'compact', 'concat', 'countBy', 'difference', 'differenceBy', 'differenceWith', 'drop', 'dropRight', 'dropRightWhile', 'dropWhile', 'each', 'eachRight', 'every', 'fill', 'filter', 'find', 'findIndex', 'findLast', 'findLastIndex', 'first', 'flatMap', 'flatten', 'flattenDeep', 'flattenDepth', 'forEach', 'forEachRight', 'fromPairs', 'groupBy', 'head', 'includes', 'indexOf', 'initial', 'intersection', 'intersectionBy', 'intersectionWith', 'invokeMap', 'join', 'keyBy', 'last', 'lastIndexOf', 'map', 'orderBy', 'partition', 'pull', 'pullAll', 'pullAllBy', 'pullAllWith', 'pullAt', 'reduce', 'reduceRight', 'reject', 'remove', 'reverse', 'sample', 'sampleSize', 'shuffle', 'size', 'slice', 'some', 'sortBy', 'sortedIndex', 'sortedIndexBy', 'sortedIndexOf', 'sortedLastIndex', 'sortedLastIndexBy', 'sortedLastIndexOf', 'sortedUniq', 'sortedUniqBy', 'tail', 'take', 'takeRight', 'takeRightWhile', 'takeWhile', 'union', 'unionBy', 'unionWith', 'uniq', 'uniqBy', 'uniqWith', 'unzip', 'unzipWith', 'without', 'xor', 'xorBy', 'xorWith', 'zip', 'zipObject', 'zipObjectDeep', 'zipWith']; 18 | 19 | var ReactEntityCollection = (function () { 20 | function ReactEntityCollection(data) { 21 | var _this = this; 22 | 23 | _classCallCheck(this, ReactEntityCollection); 24 | 25 | this.items = _lodash2['default'].map(data, function (item) { 26 | if (_lodash2['default'].isNil(item) || _lodash2['default'].isPlainObject(item)) { 27 | return new _this.constructor.TYPE(item); 28 | } 29 | 30 | return item; 31 | }); 32 | } 33 | 34 | _createClass(ReactEntityCollection, [{ 35 | key: 'result', 36 | value: function result() { 37 | return this.items; 38 | } 39 | }]); 40 | 41 | return ReactEntityCollection; 42 | })(); 43 | 44 | var reduceToNewItem = function reduceToNewItem(all, arg) { 45 | all.push(arg); 46 | return all; 47 | }; 48 | 49 | _lodash2['default'].each(LODASH_METHODS, function (method) { 50 | ReactEntityCollection.prototype[method] = function () { 51 | var args = _lodash2['default'].reduce(arguments, reduceToNewItem, [this.items]); 52 | 53 | var result = _lodash2['default'][method].apply(_lodash2['default'], args); 54 | 55 | if (!_lodash2['default'].isArray(result)) { 56 | return result; 57 | } 58 | 59 | return new this.constructor(result); 60 | }; 61 | }); 62 | 63 | exports['default'] = ReactEntityCollection; 64 | module.exports = exports['default']; -------------------------------------------------------------------------------- /spec/fixtures/fakerClasses.js: -------------------------------------------------------------------------------- 1 | import Faker from 'faker'; 2 | import { PropTypes } from 'react'; 3 | 4 | import ReactEntity, { ReactEntityCollection } from '../../src/ReactEntity'; 5 | 6 | const defaultField = Faker.name.firstName(); 7 | const defaultValue = Faker.name.firstName(); 8 | 9 | const fooValidator = function (data, propName){ 10 | if(data[propName] !== 'bar'){ 11 | return `${propName} accepts just 'bar' as value`; 12 | } 13 | }; 14 | 15 | class FakeEntityWithDefault extends ReactEntity { 16 | static SCHEMA = { 17 | [defaultField]: { 18 | validator: function (){}, 19 | defaultValue: defaultValue 20 | }, 21 | [`_${defaultField}`]: { 22 | validator: function (){}, 23 | defaultValue: `_${defaultValue}` 24 | }, 25 | } 26 | } 27 | 28 | function alwaysTruth(){ 29 | return true; 30 | } 31 | 32 | class ProductEntity extends ReactEntity { 33 | static SCHEMA = { 34 | name: alwaysTruth, 35 | price: alwaysTruth 36 | } 37 | } 38 | 39 | class ProductEntityCollection extends ReactEntityCollection { 40 | static TYPE = ProductEntity; 41 | 42 | getSortedItemsByName() { 43 | return this.sortBy('name'); 44 | } 45 | } 46 | 47 | class Validatable extends ReactEntity { 48 | static SCHEMA = { 49 | field: function (data, propName, entityName){ 50 | if(data[propName] !== 'valid'){ 51 | return `${propName} wrong on ${entityName}`; 52 | } 53 | }, 54 | otherField: { 55 | validator: function (data, propName, entityName){ 56 | if(data[propName] !== 'valid'){ 57 | return new Error(`${propName} wrong on ${entityName}`); 58 | } 59 | }, 60 | defaultValue: 'bla' 61 | } 62 | } 63 | } 64 | 65 | class ChildrenEntity extends ReactEntity { 66 | static SCHEMA = { 67 | foo: fooValidator 68 | } 69 | } 70 | 71 | class FatherEntity extends ReactEntity { 72 | static SCHEMA = { 73 | foo: { 74 | validator: fooValidator, 75 | defaultValue: 'bar' 76 | }, children: { 77 | validator: function (){}, 78 | type: ChildrenEntity 79 | } 80 | } 81 | } 82 | 83 | class FatherWithObjectEntity extends ReactEntity { 84 | static SCHEMA = { 85 | children: { 86 | type: ChildrenEntity, 87 | validator: alwaysTruth, 88 | builder: (data, Type) => { 89 | return Object.keys(data).reduce((result, key) => { 90 | result[key] = new Type(data[key]); 91 | return result; 92 | },{}) 93 | } 94 | } 95 | } 96 | } 97 | 98 | class ChildWithChildArray extends ReactEntity { 99 | static SCHEMA = { 100 | name: PropTypes.string, 101 | children: { 102 | validator: PropTypes.arrayOf(PropTypes.instanceOf(ChildWithChildArray)), 103 | type: ChildWithChildArray 104 | } 105 | } 106 | } 107 | 108 | class WithBooleanFields extends ReactEntity { 109 | static SCHEMA = { 110 | fieldA: PropTypes.bool, 111 | fieldB: { 112 | type: Boolean, 113 | validator: PropTypes.bool 114 | }, 115 | fieldWithDefault: { 116 | validator: PropTypes.bool, 117 | defaultValue: false 118 | } 119 | 120 | } 121 | } 122 | 123 | export default { 124 | defaultField, 125 | defaultValue, 126 | FakeEntityWithDefault, 127 | ProductEntity, 128 | ProductEntityCollection, 129 | Validatable, 130 | ChildrenEntity, 131 | FatherEntity, 132 | FatherWithObjectEntity, 133 | ChildWithChildArray, 134 | WithBooleanFields 135 | } 136 | -------------------------------------------------------------------------------- /src/ReactEntity.js: -------------------------------------------------------------------------------- 1 | import ReactEntityCollection from './ReactEntityCollection'; 2 | import objectsByKey from './typeBuilders/objectsByKey'; 3 | 4 | const isPrimitiveType = function(type) { 5 | if (type === Boolean || type === Number || type === String) { 6 | return true; 7 | } 8 | return false; 9 | }; 10 | 11 | const createGetterAndSetter = function (instance, field){ 12 | return { 13 | set: function (value){ 14 | if(instance.data[field] !== value) { 15 | instance.data[field] = value; 16 | return instance._validate(); 17 | } 18 | }, 19 | get: function (){ return instance.data[field]; }, 20 | enumerable: true 21 | } 22 | } 23 | 24 | class ReactEntity { 25 | constructor(data) { 26 | Object.defineProperty(this, 'schema', { 27 | value: this.constructor.SCHEMA, 28 | enumerable: false 29 | }); 30 | 31 | Object.defineProperty(this, 'childrenEntities', { 32 | value: Object.keys(this.constructor.SCHEMA).filter((field) => !!this.constructor.SCHEMA[field].type), 33 | enumerable: false 34 | }); 35 | 36 | this.errors = {}; 37 | Object.defineProperty(this, 'data', { 38 | value: this._mergeDefault(data || {}), 39 | enumerable: false 40 | }); 41 | 42 | this._validate(); 43 | } 44 | 45 | applyEntityConstructor(field, data) { 46 | if (data === undefined || data === null) return data; 47 | 48 | const Type = field.type; 49 | 50 | if(field.builder) { 51 | return field.builder(data, Type); 52 | } 53 | 54 | if (Array.isArray(data)) { 55 | if (isPrimitiveType(Type)) { 56 | return data.map(instance => Type(instance)); 57 | } 58 | return data.map(instance => new Type(instance)); 59 | } 60 | 61 | if (isPrimitiveType(Type)) { 62 | return Type(data); 63 | } 64 | 65 | return new Type(data); 66 | } 67 | 68 | _mergeDefault(data) { 69 | const newData = {}; 70 | let field; 71 | for(field in this.schema){ 72 | newData[field] = data[field] === undefined ?this.schema[field].defaultValue : data[field]; 73 | 74 | if (this.schema[field].type) { 75 | newData[field] = this.applyEntityConstructor(this.schema[field], newData[field]); 76 | } 77 | 78 | Object.defineProperty(this, field, createGetterAndSetter(this, field)); 79 | } 80 | return newData; 81 | } 82 | 83 | _fetchChild(fieldValue){ 84 | if (Array.isArray(fieldValue)){ 85 | return fieldValue.map(this._fetchChild) 86 | } 87 | if (fieldValue) 88 | if (fieldValue.fetch){ 89 | return fieldValue.fetch(); 90 | } 91 | 92 | return fieldValue 93 | } 94 | 95 | __validateField(field) { 96 | const validator = typeof(this.schema[field]) === 'function' ? 97 | this.schema[field] : 98 | this.schema[field].validator; 99 | 100 | const error = validator(this.data, field, this.constructor.name + 'Entity'); 101 | 102 | if (error) { 103 | if (!this.errors[field]) { 104 | this.errors[field] = { errors : [] } 105 | } 106 | 107 | this.errors[field].errors.push(error.message || error); 108 | } 109 | } 110 | 111 | _validate() { 112 | this.errors = {}; 113 | 114 | let field; 115 | for(field in this.schema){ 116 | this.__validateField(field); 117 | } 118 | this.valid = Object.keys(this.errors).length === 0; 119 | 120 | if(!this.valid) { 121 | return this.errors; 122 | } 123 | } 124 | 125 | fetch() { 126 | let rawData = {}; 127 | for(let field in this.data){ 128 | rawData[field] = this._fetchChild(this.data[field]); 129 | } 130 | 131 | return rawData; 132 | } 133 | 134 | getErrors() { 135 | this._validate(); 136 | const errors = Object.assign({}, this.errors); 137 | 138 | for(let field of this.childrenEntities) { 139 | const children = Array.isArray(this[field]) ? this[field] : [this[field]]; 140 | 141 | children.forEach((entity, index) => { 142 | if(!entity.valid) { 143 | if(errors[field] === undefined) { errors[field] = {} } 144 | 145 | errors[field][index] = entity.getErrors(); 146 | } 147 | }) 148 | } 149 | 150 | return errors; 151 | } 152 | 153 | } 154 | 155 | ReactEntity.ReactEntityCollection = ReactEntityCollection; 156 | ReactEntity.Types = { objectsByKey } 157 | 158 | export default ReactEntity; 159 | -------------------------------------------------------------------------------- /lib/ReactEntity.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 10 | 11 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 12 | 13 | var _ReactEntityCollection = require('./ReactEntityCollection'); 14 | 15 | var _ReactEntityCollection2 = _interopRequireDefault(_ReactEntityCollection); 16 | 17 | var _typeBuildersObjectsByKey = require('./typeBuilders/objectsByKey'); 18 | 19 | var _typeBuildersObjectsByKey2 = _interopRequireDefault(_typeBuildersObjectsByKey); 20 | 21 | var isPrimitiveType = function isPrimitiveType(type) { 22 | if (type === Boolean || type === Number || type === String) { 23 | return true; 24 | } 25 | return false; 26 | }; 27 | 28 | var createGetterAndSetter = function createGetterAndSetter(instance, field) { 29 | return { 30 | set: function set(value) { 31 | if (instance.data[field] !== value) { 32 | instance.data[field] = value; 33 | return instance._validate(); 34 | } 35 | }, 36 | get: function get() { 37 | return instance.data[field]; 38 | }, 39 | enumerable: true 40 | }; 41 | }; 42 | 43 | var ReactEntity = (function () { 44 | function ReactEntity(data) { 45 | var _this = this; 46 | 47 | _classCallCheck(this, ReactEntity); 48 | 49 | Object.defineProperty(this, 'schema', { 50 | value: this.constructor.SCHEMA, 51 | enumerable: false 52 | }); 53 | 54 | Object.defineProperty(this, 'childrenEntities', { 55 | value: Object.keys(this.constructor.SCHEMA).filter(function (field) { 56 | return !!_this.constructor.SCHEMA[field].type; 57 | }), 58 | enumerable: false 59 | }); 60 | 61 | this.errors = {}; 62 | Object.defineProperty(this, 'data', { 63 | value: this._mergeDefault(data || {}), 64 | enumerable: false 65 | }); 66 | 67 | this._validate(); 68 | } 69 | 70 | _createClass(ReactEntity, [{ 71 | key: 'applyEntityConstructor', 72 | value: function applyEntityConstructor(field, data) { 73 | if (data === undefined || data === null) return data; 74 | 75 | var Type = field.type; 76 | 77 | if (field.builder) { 78 | return field.builder(data, Type); 79 | } 80 | 81 | if (Array.isArray(data)) { 82 | if (isPrimitiveType(Type)) { 83 | return data.map(function (instance) { 84 | return Type(instance); 85 | }); 86 | } 87 | return data.map(function (instance) { 88 | return new Type(instance); 89 | }); 90 | } 91 | 92 | if (isPrimitiveType(Type)) { 93 | return Type(data); 94 | } 95 | 96 | return new Type(data); 97 | } 98 | }, { 99 | key: '_mergeDefault', 100 | value: function _mergeDefault(data) { 101 | var newData = {}; 102 | var field = undefined; 103 | for (field in this.schema) { 104 | newData[field] = data[field] === undefined ? this.schema[field].defaultValue : data[field]; 105 | 106 | if (this.schema[field].type) { 107 | newData[field] = this.applyEntityConstructor(this.schema[field], newData[field]); 108 | } 109 | 110 | Object.defineProperty(this, field, createGetterAndSetter(this, field)); 111 | } 112 | return newData; 113 | } 114 | }, { 115 | key: '_fetchChild', 116 | value: function _fetchChild(fieldValue) { 117 | if (Array.isArray(fieldValue)) { 118 | return fieldValue.map(this._fetchChild); 119 | } 120 | if (fieldValue) if (fieldValue.fetch) { 121 | return fieldValue.fetch(); 122 | } 123 | 124 | return fieldValue; 125 | } 126 | }, { 127 | key: '__validateField', 128 | value: function __validateField(field) { 129 | var validator = typeof this.schema[field] === 'function' ? this.schema[field] : this.schema[field].validator; 130 | 131 | var error = validator(this.data, field, this.constructor.name + 'Entity'); 132 | 133 | if (error) { 134 | if (!this.errors[field]) { 135 | this.errors[field] = { errors: [] }; 136 | } 137 | 138 | this.errors[field].errors.push(error.message || error); 139 | } 140 | } 141 | }, { 142 | key: '_validate', 143 | value: function _validate() { 144 | this.errors = {}; 145 | 146 | var field = undefined; 147 | for (field in this.schema) { 148 | this.__validateField(field); 149 | } 150 | this.valid = Object.keys(this.errors).length === 0; 151 | 152 | if (!this.valid) { 153 | return this.errors; 154 | } 155 | } 156 | }, { 157 | key: 'fetch', 158 | value: function fetch() { 159 | var rawData = {}; 160 | for (var field in this.data) { 161 | rawData[field] = this._fetchChild(this.data[field]); 162 | } 163 | 164 | return rawData; 165 | } 166 | }, { 167 | key: 'getErrors', 168 | value: function getErrors() { 169 | var _this2 = this; 170 | 171 | this._validate(); 172 | var errors = Object.assign({}, this.errors); 173 | 174 | var _iteratorNormalCompletion = true; 175 | var _didIteratorError = false; 176 | var _iteratorError = undefined; 177 | 178 | try { 179 | var _loop = function () { 180 | var field = _step.value; 181 | 182 | var children = Array.isArray(_this2[field]) ? _this2[field] : [_this2[field]]; 183 | 184 | children.forEach(function (entity, index) { 185 | if (!entity.valid) { 186 | if (errors[field] === undefined) { 187 | errors[field] = {}; 188 | } 189 | 190 | errors[field][index] = entity.getErrors(); 191 | } 192 | }); 193 | }; 194 | 195 | for (var _iterator = this.childrenEntities[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 196 | _loop(); 197 | } 198 | } catch (err) { 199 | _didIteratorError = true; 200 | _iteratorError = err; 201 | } finally { 202 | try { 203 | if (!_iteratorNormalCompletion && _iterator['return']) { 204 | _iterator['return'](); 205 | } 206 | } finally { 207 | if (_didIteratorError) { 208 | throw _iteratorError; 209 | } 210 | } 211 | } 212 | 213 | return errors; 214 | } 215 | }]); 216 | 217 | return ReactEntity; 218 | })(); 219 | 220 | ReactEntity.ReactEntityCollection = _ReactEntityCollection2['default']; 221 | ReactEntity.Types = { objectsByKey: _typeBuildersObjectsByKey2['default'] }; 222 | 223 | exports['default'] = ReactEntity; 224 | module.exports = exports['default']; -------------------------------------------------------------------------------- /spec/ReactEntity.spec.js: -------------------------------------------------------------------------------- 1 | import Faker from 'faker'; 2 | import ReactEntity, { ReactEntityCollection } from '../src/ReactEntity'; 3 | 4 | import { 5 | defaultField, 6 | defaultValue, 7 | FakeEntityWithDefault, 8 | ProductEntity, 9 | ProductEntityCollection, 10 | Validatable, 11 | ChildrenEntity, 12 | FatherEntity, 13 | FatherWithObjectEntity, 14 | ChildWithChildArray, 15 | WithBooleanFields 16 | } from './fixtures/fakerClasses'; 17 | 18 | describe('ReactEntity', function (){ 19 | it('should merge with default data', function (){ 20 | const fakeEntity = new FakeEntityWithDefault(); 21 | 22 | expect(fakeEntity[defaultField]).toBe(defaultValue); 23 | }); 24 | 25 | it('should clean data on fetch', function (){ 26 | const fakeEntity = new FakeEntityWithDefault({ 27 | fakeAttribute: 'should not come' 28 | }); 29 | 30 | expect(fakeEntity.fetch()).toEqual({ 31 | [defaultField]: defaultValue, 32 | [`_${defaultField}`]: `_${defaultValue}` 33 | }); 34 | }); 35 | 36 | it('should create set for property and call validate when change', function (){ 37 | const fakeEntity = new FakeEntityWithDefault(); 38 | spyOn(fakeEntity, '_validate'); 39 | 40 | fakeEntity[`_${defaultField}`] = `_${defaultValue}`; 41 | expect(fakeEntity._validate).not.toHaveBeenCalled(); 42 | 43 | fakeEntity[`_${defaultField}`] = defaultValue; 44 | expect(fakeEntity._validate).toHaveBeenCalled(); 45 | }); 46 | 47 | it('should not use defaultValue when a value is passed', function (){ 48 | const newValue = Faker.name.findName(); 49 | const fakeEntity = new FakeEntityWithDefault({ 50 | [defaultField]: newValue 51 | }); 52 | 53 | expect(fakeEntity[`_${defaultField}`]).toBe(`_${defaultValue}`); 54 | expect(fakeEntity[defaultField]).toBe(newValue); 55 | }); 56 | 57 | it('should validate when build', function (){ 58 | // given 59 | spyOn(Validatable.SCHEMA, 'field').and.returnValue(null) 60 | spyOn(Validatable.SCHEMA.otherField, 'validator').and.returnValue(null) 61 | 62 | // when 63 | new Validatable({ 64 | field: 'value', 65 | noField: 'should not go' 66 | }); 67 | 68 | // then 69 | expect(Validatable.SCHEMA.field).toHaveBeenCalledWith( 70 | { field: 'value', otherField: 'bla' }, 71 | 'field', 72 | 'ValidatableEntity' 73 | ); 74 | expect(Validatable.SCHEMA.otherField.validator).toHaveBeenCalledWith( 75 | { field: 'value', otherField: 'bla' }, 76 | 'otherField', 77 | 'ValidatableEntity' 78 | ); 79 | }); 80 | 81 | it('should auto validate', function (){ 82 | // when 83 | const entity = new Validatable({ field: 'invalid', otherField: 'invalid'}); 84 | 85 | expect(entity.valid).toBe(false); 86 | entity.field = 'valid'; 87 | 88 | expect(entity.valid).toBe(false); 89 | entity.otherField = 'valid'; 90 | expect(entity.valid).toBe(true); 91 | }); 92 | 93 | describe('Boolean', function (){ 94 | let entity; 95 | 96 | beforeEach(() => { 97 | entity = new WithBooleanFields({ 98 | fieldA: false, 99 | fieldB: false, 100 | fieldWithDefault: undefined 101 | }); 102 | }) 103 | 104 | it('returns a boolean "false" when is set "false" to the field', function(){ 105 | expect(entity.valid).toBe(true); 106 | expect(entity.fetch().fieldA).toBe(false); 107 | expect(entity.fetch().fieldB).toBe(false); 108 | }); 109 | 110 | it('returns a default boolean "false" when is set "undefined" to the field', function(){ 111 | expect(entity.valid).toBe(true); 112 | expect(entity.fetch().fieldWithDefault).toBe(false); 113 | }); 114 | }); 115 | 116 | describe('children', function (){ 117 | it('should auto build child entities of array', function (){ 118 | const father = new FatherEntity({ 119 | children: [ 120 | {}, 121 | {} 122 | ] 123 | }); 124 | 125 | expect(father.children[0].constructor).toBe(ChildrenEntity); 126 | expect(father.children[1].constructor).toBe(ChildrenEntity); 127 | }); 128 | 129 | it('should auto build using the parameter builder', () => { 130 | const father = new FatherWithObjectEntity({ 131 | children: { 132 | content: { field: 'foo' }, 133 | tweet: { field: 'bar' } 134 | } 135 | }); 136 | 137 | expect(father.children.content.constructor).toBe(ChildrenEntity); 138 | expect(father.children.tweet.constructor).toBe(ChildrenEntity); 139 | }); 140 | 141 | it('should include errors of children', function (){ 142 | const father = new FatherEntity({ 143 | foo: 'test', 144 | children: [{ foo: 'bar' }] 145 | }); 146 | 147 | expect(father.getErrors()).toEqual({ foo: { errors: [ 'foo accepts just \'bar\' as value' ] } }); 148 | 149 | const lee = new ChildrenEntity({ foo: 'bar invalid '}); 150 | father.children.push(lee); 151 | 152 | expect(father.getErrors()).toEqual({ 153 | foo: { errors: [ 'foo accepts just \'bar\' as value' ] }, 154 | children: { 1: { foo: { errors: [ 'foo accepts just \'bar\' as value' ] } } } 155 | }); 156 | }); 157 | }); 158 | 159 | 160 | describe('collection', function (){ 161 | 162 | it('should return a collection of object', function (){ 163 | 164 | const products = [ 165 | { 166 | name: 'A', 167 | price: 10 168 | }, 169 | { 170 | name: 'B', 171 | price: 2 172 | }, 173 | ]; 174 | 175 | const collection = new ProductEntityCollection(products); 176 | const results = collection.filter({name: 'A'}).result(); 177 | 178 | expect(results[0].fetch()).toEqual({ name: 'A', price: 10 }); 179 | }); 180 | 181 | it('should return a collection similar with keyBy/lodash ', function (){ 182 | const products = [ 183 | { 184 | name: 'A', 185 | price: 1 186 | }, 187 | { 188 | name: 'B', 189 | price: 2 190 | }, 191 | ]; 192 | 193 | const collection = new ProductEntityCollection(products); 194 | const product = collection 195 | .filter({ name: 'B' }) 196 | .keyBy('name'); 197 | 198 | expect(!!product.B).toBe(true); 199 | expect(product.B.name).toEqual(products[1].name); 200 | expect(product.B.price).toEqual(products[1].price); 201 | }); 202 | 203 | it('should return a collection ordered by name ', function (){ 204 | 205 | const products = [ 206 | { 207 | name: 'B' 208 | }, 209 | { 210 | name: 'C', 211 | price: 2 212 | }, 213 | { 214 | name: 'A' 215 | } 216 | ]; 217 | 218 | const collection = new ProductEntityCollection(products); 219 | const results = collection.getSortedItemsByName().result(); 220 | 221 | expect(results[0].fetch()).toEqual({ name: 'A', price: undefined }); 222 | expect(results[1].fetch()).toEqual({ name: 'B', price: undefined }); 223 | expect(results[2].fetch()).toEqual({ name: 'C', price: 2 }); 224 | }); 225 | 226 | it('concat a list with another list ', function (){ 227 | 228 | const listA = [ 229 | { 230 | name: 'AAA' 231 | } 232 | ]; 233 | 234 | const listB = [ 235 | { 236 | name: 'BBB' 237 | } 238 | ]; 239 | 240 | const collection = new ProductEntityCollection(listA); 241 | const results = collection.concat(listB).result(); 242 | }); 243 | 244 | it('it should build itself along with childs which is of type itself', () => { 245 | const childWithChildArray = new ChildWithChildArray({ 246 | name: 'Node1', 247 | children: [{ 248 | name: 'Node1.1', 249 | children: [{ 250 | name: "Node 1.1.1" 251 | }] 252 | }] 253 | }); 254 | 255 | expect(childWithChildArray.constructor).toBe(ChildWithChildArray); 256 | expect(childWithChildArray.children[0].constructor).toBe(ChildWithChildArray); 257 | }); 258 | 259 | }); 260 | }); 261 | --------------------------------------------------------------------------------