├── .gitignore ├── .npmignore ├── README.md ├── karma.conf.js ├── package.json ├── specs ├── example_store_spec.js └── validations_spec.js └── src ├── define.js ├── index.js ├── types.js └── validations.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # local_orm 2 | [![CodeShip](https://codeship.com/projects/0876cb80-2453-0134-45d7-7a46f2e0a594/status?branch=master)](https://codeship.com/projects/0876cb80-2453-0134-45d7-7a46f2e0a594/status?branch=master) 3 | [![Code Climate](https://codeclimate.com/github/hiquest/local_orm/badges/gpa.svg)](https://codeclimate.com/github/hiquest/local_orm) 4 | 5 | A simple ORM-like wrapper around localStorage with functional interface, types and validations. 6 | 7 | Installation 8 | ------ 9 | ``` 10 | npm install local_orm --save 11 | ``` 12 | 13 | Usage 14 | ------ 15 | Define a schema (yes, we call it a schema): 16 | 17 | ```javascript 18 | const { define: define, types: t, validations: v } = require("local_orm"); 19 | 20 | const Store = define({ 21 | name: "books_schema", 22 | schema: { 23 | books: { 24 | title: { 25 | type: t.string, 26 | validations: [v.present, v.maxLength(32)] 27 | }, 28 | year: { 29 | type: t.integer, 30 | validations: [v.min(1900), v.max(2999)] 31 | }, 32 | genre: { 33 | type: t.string, 34 | validations: [v.present, v.oneOf('fiction', 'non-fiction')], 35 | defaultVal: 'fiction' 36 | } 37 | } 38 | } 39 | }); 40 | 41 | ``` 42 | 43 | Let's save some books. 44 | 45 | ```javascript 46 | 47 | // Create a book 48 | let [err, book] = Store.books.save({ title: "War And Peace" }); 49 | console.log(book); 50 | // => { id: "0326d5ce-d3db-4bf7-853f-37d4d5adf6a8", title: "War And Peace", genre: 'fiction' } 51 | // ( Note that we have an id now, and that genre was populated with a default value ) 52 | 53 | // Let's try another one 54 | let [err, book] = Store.books.save({ year: "1984" }); 55 | console.log(book); // => null 56 | 57 | // Was there some errors? 58 | console.log(err); // => { 'year': ['should be an integer'], 'title': ['should be present'] } 59 | 60 | // Oh, I see now... 61 | let [err, book] = Store.books.save({ title: "So Long, and Thanks for all the Fish", year: 1984 }); 62 | ``` 63 | 64 | We can load books from localStorage now. 65 | 66 | ```javascript 67 | // Find a book by id 68 | let book = Store.books.find(id); 69 | 70 | // Load all books 71 | let books = Store.books.all(); 72 | 73 | // Filter by title 74 | let books = Store.books.where({title: 'War And Peace'}); 75 | 76 | // Any function is also accepted 77 | let books = Store.books.where((b) => b.year > 1980); 78 | ``` 79 | 80 | Store API 81 | ------ 82 | * `build` builds a new entity with set default values. Returns a new `entity` 83 | 84 | * `validate` validates an entity. Returns an array `[errors, isValid]`, where errors is an object like this one: 85 | ```javascript 86 | { 87 | "title": [ "should be present", "should have more than 5 characters" ], 88 | "year": [ "should be less of equal to 2999" ] 89 | } 90 | ``` 91 | 92 | * `save` creates a new entity or updates the existing one (if it has an id). Returns an array `[errors, entity]` 93 | 94 | * `find` finds an entity by id. Throws an error if it does not exist. Returns an `entity` 95 | 96 | * `destroy` destroys an entity by id. Throws an error if it does not exist. Returns `true` 97 | 98 | * `all` loads all entities. Returns an `array of entities` 99 | 100 | * `where` filters out entities. Accepts `object` or `function`. Returns an `array of entities` 101 | 102 | Validations 103 | ------ 104 | Validation is a simple plain JavaScript function that takes a value and returns an array like this one [error, valid]. Local_orm comes with several predefined validations. 105 | 106 | * `present` requires a value to be present (not undefined and not null) 107 | ```javascript 108 | validations: [v.present] 109 | ``` 110 | 111 | * `min`, `max` define a range for integer types (inclusive) 112 | ```javascript 113 | validations: [v.min(0), v.max(127)] 114 | ``` 115 | 116 | * `minLength`, `maxLength` define a length range for strings or arrays (really, anything that has length) 117 | ```javascript 118 | validations: [v.maxLength(32)] 119 | ``` 120 | 121 | * `oneOf` require a value to be in a particular set of values (like enum) 122 | ```javascript 123 | validations: [v.oneOf("sun", "moon")] 124 | ``` 125 | 126 | Also, note that when you define a type, under the curtains the corresponding validation is added to the list. 127 | 128 | As noted earlier, validations are just functions, so it is easy to define your own: 129 | ```javascript 130 | const positive = (val) => { 131 | if (val > 0) { 132 | return [null, true]; 133 | } else { 134 | return ["should be positive", false]; 135 | } 136 | }; 137 | 138 | validations: [ positive ] 139 | ``` 140 | 141 | Contributing 142 | ------ 143 | Feel free to fork, add features and send pull requests. Please make sure to add corresponding tests. 144 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: '', 4 | frameworks: ['jasmine', 'browserify'], 5 | files: [ 6 | './specs/**/*.js' 7 | ], 8 | exclude: [ ], 9 | preprocessors: { 10 | './specs/**/*.js': ['browserify'] 11 | }, 12 | browserify: { 13 | debug: true, 14 | transform: [], 15 | extensions: ['.js'] 16 | }, 17 | reporters: ['spec'], 18 | port: 9876, 19 | colors: true, 20 | logLevel: config.LOG_INFO, 21 | browsers: ['Chrome'], 22 | autoWatch: true, 23 | singleRun: false, 24 | concurrency: Infinity 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "local_orm", 3 | "version": "0.6.1", 4 | "description": "A simple ORM-like layer on top of localStorage ", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "build": "babel src --presets babel-preset-es2015 --out-dir dist", 8 | "prepublish": "npm run build", 9 | "test": "karma start --single-run", 10 | "test-watch": "karma start" 11 | }, 12 | "keywords": [ 13 | "orm", 14 | "localStorage" 15 | ], 16 | "author": "Yanis", 17 | "license": "ISC", 18 | "dependencies": { 19 | "node-uuid": "^1.4.7", 20 | "underscore": "^1.8.3" 21 | }, 22 | "devDependencies": { 23 | "babel-cli": "^6.10.1", 24 | "babel-preset-es2015": "^6.9.0", 25 | "browserify": "^13.0.1", 26 | "jasmine-core": "^2.4.1", 27 | "karma": "^1.1.0", 28 | "karma-browserify": "^5.0.5", 29 | "karma-chrome-launcher": "^1.0.1", 30 | "karma-jasmine": "^1.0.2", 31 | "karma-spec-reporter": "0.0.26", 32 | "watchify": "^3.7.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /specs/example_store_spec.js: -------------------------------------------------------------------------------- 1 | const { define: define, types: t, validations: v } = require("../src/index"); 2 | 3 | describe("An example store",() => { 4 | const Store = define({ 5 | name: "books_schema", 6 | schema: { 7 | books: { 8 | title: { 9 | type: t.string, 10 | validations: [v.present, v.maxLength(32)] 11 | }, 12 | year: { 13 | type: t.integer, 14 | validations: [v.min(1900), v.max(2999)] 15 | }, 16 | genre: { 17 | type: t.string, 18 | validations: [v.present, v.oneOf('fiction', 'non-fiction')], 19 | defaultVal: 'fiction' 20 | }, 21 | readersCount: { 22 | type: t.integer, 23 | validations: [v.present], 24 | defaultVal: () => 0 25 | } 26 | }, 27 | authors: { 28 | name: { 29 | type: t.string 30 | } 31 | } 32 | } 33 | }); 34 | 35 | beforeEach(() => { 36 | window.localStorage.clear(); 37 | }); 38 | 39 | describe("#build", () => { 40 | it("populates model with default values", () => { 41 | const book = Store.books.build(); 42 | expect(book.genre).toEqual("fiction"); 43 | expect(book.readersCount).toEqual(0); 44 | }); 45 | }); 46 | 47 | describe("#validate", () => { 48 | it("allows valid entities", () => { 49 | let book = { title: "War And Peace" }; 50 | let [errors, valid] = Store.books.validate(book); 51 | expect(valid).toBe(true); 52 | expect(errors).toEqual({}); 53 | }); 54 | 55 | it("not allows invalid entities", () => { 56 | let book = {}; 57 | let [errors, valid] = Store.books.validate(book); 58 | expect(valid).toBe(false); 59 | expect(errors['title']).toEqual( 60 | [ "should be present" ] 61 | ); 62 | 63 | book = { 64 | year: "1995", 65 | title: 'This is a way too long title, you really should not call the books like that', 66 | genre: 'sci-fi' 67 | }; 68 | [errors, valid] = Store.books.validate(book); 69 | expect(valid).toBe(false); 70 | expect(errors['year']).toEqual( 71 | [ "should be an integer" ] 72 | ); 73 | expect(errors['title']).toEqual( 74 | [ "max length exceeded" ] 75 | ); 76 | expect(errors['genre']).toEqual( 77 | [ "should be one of [fiction,non-fiction]" ] 78 | ); 79 | }); 80 | }); 81 | 82 | describe("#save", () => { 83 | it("refuse to save the enity if it is invalid", () => { 84 | let input = {} 85 | let [errors, book] = Store.books.save(input); 86 | expect(input).toEqual({}); 87 | expect(book).toBe(null); 88 | expect(errors['title']).toEqual( 89 | [ "should be present" ] 90 | ); 91 | }); 92 | 93 | it("successfully save the entity if it is valid", () => { 94 | let [errors, book] = Store.books.save({title: "Test Title"}); 95 | expect(book.id).toBeDefined(); 96 | expect(book.title).toEqual("Test Title"); 97 | expect(book.genre).toEqual("fiction"); 98 | expect(errors).toBe(null); 99 | }); 100 | }); 101 | 102 | describe("#find", () => { 103 | it("throws an error if entity doesn't exist", () => { 104 | expect(() => { 105 | let ent = Store.books.find("not-exists"); 106 | }).toThrow(); 107 | }); 108 | 109 | it("returns an entity if it exists", () => { 110 | let [errors, book] = Store.books.save({title: "Test Title"}); 111 | let ent = Store.books.find(book.id); 112 | expect(ent.title).toBe("Test Title"); 113 | }); 114 | }); 115 | 116 | describe("#destroy", () => { 117 | it("throws an error if entity doesn't exist", () => { 118 | expect(() => { 119 | let ent = Store.books.destroy("not-exists"); 120 | }).toThrow(); 121 | }); 122 | 123 | it("deletes the entity if it exists", () => { 124 | let [errors, book] = Store.books.save({title: "Test Title"}); 125 | 126 | let success = Store.books.destroy(book.id); 127 | expect(success).toBe(true); 128 | 129 | let entities = Store.books.where({id: book.id}); 130 | expect(entities.length).toEqual(0); 131 | }); 132 | }); 133 | 134 | describe("#where", () => { 135 | beforeEach(() => { 136 | let [e, b] = Store.books.save({year: 1996, title: "X"}); 137 | [e, b] = Store.books.save({year: 2005, title: "Y"}); 138 | }); 139 | 140 | it("loads all entities when no arguments provided", () => { 141 | let books = Store.books.where(); 142 | expect(books.length).toEqual(2); 143 | }); 144 | 145 | it("filters every item with function", () => { 146 | let books = Store.books.where((x) => x.year > 2000); 147 | expect(books.length).toEqual(1); 148 | expect(books[0].title).toEqual('Y'); 149 | }); 150 | 151 | it("filters when object provided", () => { 152 | let books = Store.books.where({year: 1996}); 153 | expect(books.length).toEqual(1); 154 | expect(books[0].title).toEqual('X'); 155 | }); 156 | 157 | it("prevents from using not defined key when object provided", () => { 158 | expect(() => { 159 | let ent = Store.books.where({some: 'something'}); 160 | }).toThrow(); 161 | }); 162 | }); 163 | 164 | describe("Integrity", () => { 165 | it("don't mix up tables", () => { 166 | [_, author] = Store.authors.save({name: "Author"}); 167 | [_, book] = Store.books.save({title: "Title"}); 168 | expect(Store.authors.all().length).toEqual(1); 169 | expect(Store.books.all().length).toEqual(1); 170 | }); 171 | }) 172 | }); 173 | -------------------------------------------------------------------------------- /specs/validations_spec.js: -------------------------------------------------------------------------------- 1 | const { validations: v } = require("../src/index"); 2 | 3 | describe("Validations",() => { 4 | describe("requireBoolean", () => { 5 | it("should work correctly", () => { 6 | const testBool = (val) => { 7 | [err, valid] = v.requireBoolean(val); 8 | return valid; 9 | } 10 | expect(testBool(true)).toBe(true); 11 | expect(testBool(false)).toBe(true); 12 | expect(testBool(1)).toBe(false); 13 | expect(testBool({})).toBe(false); 14 | expect(testBool([])).toBe(false); 15 | expect(testBool("str")).toBe(false); 16 | expect(testBool(null)).toBe(false); 17 | expect(testBool(undefined)).toBe(true); 18 | }); 19 | }); 20 | 21 | describe("requireInteger", () => { 22 | it("should work correctly", () => { 23 | const test = (val) => { 24 | [err, valid] = v.requireInteger(val); 25 | return valid; 26 | } 27 | expect(test(true)).toBe(false); 28 | expect(test(false)).toBe(false); 29 | expect(test(1)).toBe(true); 30 | expect(test(-1)).toBe(true); 31 | expect(test({})).toBe(false); 32 | expect(test([])).toBe(false); 33 | expect(test("str")).toBe(false); 34 | expect(test(null)).toBe(false); 35 | expect(test(undefined)).toBe(true); 36 | }); 37 | }); 38 | 39 | describe("requireString", () => { 40 | it("should work correctly", () => { 41 | const test = (val) => { 42 | [err, valid] = v.requireString(val); 43 | return valid; 44 | } 45 | expect(test(true)).toBe(false); 46 | expect(test(false)).toBe(false); 47 | expect(test(1)).toBe(false); 48 | expect(test(-1)).toBe(false); 49 | expect(test({})).toBe(false); 50 | expect(test([])).toBe(false); 51 | expect(test("str")).toBe(true); 52 | expect(test("")).toBe(true); 53 | expect(test(null)).toBe(false); 54 | expect(test(undefined)).toBe(true); 55 | }); 56 | }); 57 | 58 | describe("present", () => { 59 | it("should work correctly", () => { 60 | let [err, valid] = v.present(""); 61 | expect(valid).toBe(true); 62 | [err, valid] = v.present("hello"); 63 | expect(valid).toBe(true); 64 | [err, valid] = v.present(1); 65 | expect(valid).toBe(true); 66 | [err, valid] = v.present(null); 67 | expect(valid).toBe(false); 68 | }); 69 | }); 70 | 71 | describe("min", () => { 72 | it("should work correctly", () => { 73 | const min5 = v.min(5); 74 | let [err, valid] = min5(0); 75 | expect(valid).toBe(false); 76 | [err, valid] = min5(10); 77 | expect(valid).toBe(true); 78 | [err, valid] = min5(5); 79 | expect(valid).toBe(true); 80 | }); 81 | }); 82 | 83 | describe("max", () => { 84 | it("should work correctly", () => { 85 | const max5 = v.max(5); 86 | let [err, valid] = max5(0); 87 | expect(valid).toBe(true); 88 | [err, valid] = max5(10); 89 | expect(valid).toBe(false); 90 | [err, valid] = max5(5); 91 | expect(valid).toBe(true); 92 | }); 93 | }); 94 | 95 | describe("maxLength", () => { 96 | it("should work correctly", () => { 97 | const max10 = v.maxLength(10); 98 | let [err, valid] = max10("hello"); 99 | expect(valid).toBe(true); 100 | expect(err).toBeFalsy(); 101 | 102 | [err, valid] = max10("this is a too long string"); 103 | expect(valid).toBe(false); 104 | expect(err).toBeTruthy(); 105 | 106 | [err, valid] = max10("0123456789"); 107 | expect(valid).toBe(true); 108 | expect(err).toBeFalsy(); 109 | 110 | [err, valid] = max10(123); 111 | expect(valid).toBe(false); 112 | }); 113 | }); 114 | 115 | describe("minLength", () => { 116 | it("should work correctly", () => { 117 | const min10 = v.minLength(10); 118 | let [err, valid] = min10("hello"); 119 | expect(valid).toBe(false); 120 | expect(err).toBeTruthy(); 121 | 122 | [err, valid] = min10("this is a too long string"); 123 | expect(valid).toBe(true); 124 | 125 | [err, valid] = min10("0123456789"); 126 | expect(valid).toBe(true); 127 | 128 | [err, valid] = min10(123); 129 | expect(valid).toBe(false); 130 | }); 131 | }); 132 | 133 | describe("oneOf", () => { 134 | it("should work correctly", () => { 135 | const oneOf = v.oneOf('test', 'live'); 136 | let [err, valid] = oneOf("hello"); 137 | expect(valid).toBe(false); 138 | expect(err).toBeTruthy(); 139 | [err, valid] = oneOf('test'); 140 | expect(valid).toBe(true); 141 | [err, valid] = oneOf('live'); 142 | expect(valid).toBe(true); 143 | }); 144 | }); 145 | }); 146 | 147 | describe("Composite or validation", () => { 148 | it("should work correctly", () => { 149 | outerRange = v.or(v.minLength(10), v.maxLength(4)); 150 | 151 | let [err, valid] = outerRange("hello"); 152 | expect(valid).toBe(false); 153 | expect(err).toBeTruthy(); 154 | 155 | [err, valid] = outerRange("this is a too long string"); 156 | expect(valid).toBe(true); 157 | }); 158 | }); 159 | 160 | describe("#check", () => { 161 | it("should work correctly", () => { 162 | const validations = [v.requireString, v.maxLength(10)]; 163 | 164 | let [errors, valid] = v.check(4, validations); 165 | expect(valid).toBe(false); 166 | expect(errors.length).toBe(2); 167 | 168 | [errors, valid] = v.check("this is a too long string", validations); 169 | expect(valid).toBe(false); 170 | expect(errors.length).toBe(1); 171 | 172 | [errors, valid] = v.check("pass", validations); 173 | expect(errors.length).toBe(0); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /src/define.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | const uuid = require('node-uuid'); 3 | const _ = require('underscore'); 4 | 5 | const t = require('./types'); 6 | const v = require('./validations'); 7 | 8 | const PREFIX = "LOCAL_ORM"; 9 | 10 | const define = ({name: schemaName, schema: schema}) => { 11 | 12 | let table_key = (table_name) => `${PREFIX}_${schemaName}_${table_name}`; 13 | 14 | const loadTable = (table_name) => { 15 | let s = window.localStorage[table_key(table_name)]; 16 | return s ? JSON.parse(s) : []; 17 | } 18 | 19 | const commitTable = (table_name, entities) => { 20 | return window.localStorage[table_key(table_name)] = JSON.stringify(entities); 21 | } 22 | 23 | return _.reduce(Object.keys(schema), (memo, tableName) => { 24 | const tableConfig = schema[tableName]; 25 | 26 | const fields = Object.keys(tableConfig); 27 | 28 | const fetchAll = () => loadTable(tableName); 29 | 30 | const setDefaultValues = (oldEntity) => { 31 | let entity = _.clone(oldEntity); 32 | fields.forEach( (k) => { 33 | if (!entity[k] && !_.isUndefined(tableConfig[k].defaultVal)) { 34 | let dv = tableConfig[k].defaultVal; 35 | entity[k] = _.isFunction(dv) ? dv() : dv 36 | }; 37 | }); 38 | return entity; 39 | }; 40 | 41 | const addTypeValidation = (validations, type) => { 42 | let out = _.clone(validations); 43 | if (type === t.string) { 44 | out.unshift(v.requireString); 45 | } else if (type === t.integer) { 46 | out.unshift(v.requireInteger); 47 | } else if (type === t.boolean) { 48 | out.unshift(v.requireBoolean); 49 | } else { 50 | throw "Unsupported type"; 51 | } 52 | return out; 53 | }; 54 | 55 | // Exposed 56 | 57 | const validate = (oldEnt) => { 58 | let ent = setDefaultValues(oldEnt); 59 | 60 | const errors = _.reduce(fields, (memo, field) => { 61 | let validations = tableConfig[field]['validations'] || []; 62 | validations = addTypeValidation(validations, tableConfig[field]['type']); 63 | let fieldErrors = validations.map((validator) => { 64 | let [err, valid] = validator(ent[field]); 65 | return err; 66 | }); 67 | fieldErrors = _.filter(fieldErrors, (x) => x !== null); 68 | if (fieldErrors.length > 0 ) { 69 | memo[field] = fieldErrors; 70 | } 71 | return memo; 72 | }, {}); 73 | 74 | const valid = _.reduce( 75 | _.pairs(errors), 76 | (memo, val) => { return memo && (val[1].length === 0) } 77 | , true 78 | ); 79 | 80 | return [errors, valid]; 81 | } 82 | 83 | const create = (ent) => { 84 | const [err, valid] = validate(ent); 85 | if (valid) { 86 | ent.id = uuid.v1(); 87 | let entities = fetchAll(); 88 | entities.push(ent); 89 | commitTable(tableName, entities); 90 | return [null, ent]; 91 | } else { 92 | return [err, null] 93 | } 94 | }; 95 | 96 | const update = (ent) => { 97 | const [err, valid] = validate(ent); 98 | if (valid) { 99 | let entities = fetchAll(); 100 | const ind = _.findIndex(entities, { id: ent.id }); 101 | if (ind > -1) { 102 | entities[ind] = ent; 103 | commitTable(tableName, entities); 104 | return [null, ent]; 105 | } else { 106 | return ['Not Found', null]; 107 | } 108 | } else { 109 | return [err, null]; 110 | } 111 | }; 112 | 113 | const build = (opts = {}) => { 114 | let ent = setDefaultValues(opts); 115 | return ent; 116 | }; 117 | 118 | const save = (oldEnt) => { 119 | let ent = setDefaultValues(oldEnt); 120 | if (ent.id) { 121 | return update(ent); 122 | } else { 123 | return create(ent); 124 | } 125 | }; 126 | 127 | const find = (id) => { 128 | const ent = _.find(fetchAll(), {id: id}); 129 | if (ent) { 130 | return ent; 131 | } else { 132 | throw `Entity with id ${id} does not exist`; 133 | } 134 | }; 135 | 136 | const destroy = (id) => { 137 | let ent = find(id); 138 | let entities = fetchAll(); 139 | let e = _.find(entities, { id: ent.id }); 140 | let updatedEntities = _.without(entities, e); 141 | commitTable(tableName, updatedEntities); 142 | return true; 143 | }; 144 | 145 | const all = (filterWith) => { 146 | return fetchAll(); 147 | }; 148 | 149 | const where = (filterWith) => { 150 | let allFields = _.union(fields, ['id']); 151 | if (_.isObject(filterWith)) { 152 | let keys = Object.keys(filterWith); 153 | _.each(keys, (key) => { 154 | if(allFields.indexOf(key) < 0) { 155 | throw `Key ${key} doesn't exists`; 156 | } 157 | }); 158 | }; 159 | if (!filterWith) { 160 | return fetchAll(); 161 | } else { 162 | return _.filter(fetchAll(), filterWith); 163 | } 164 | }; 165 | 166 | memo[tableName] = { save, find, destroy, all, where, validate, build }; 167 | return memo; 168 | }, {}); 169 | }; 170 | 171 | module.exports = define; 172 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | const validations = require('./validations'); 3 | const define = require('./define'); 4 | const types = require('./types'); 5 | 6 | module.exports = { 7 | define: define, 8 | validations: validations, 9 | types: types 10 | }; 11 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | const TYPE_PREFIX = "RELATIVE_STORE"; 2 | 3 | const types = { 4 | string: `${TYPE_PREFIX}_STRING`, 5 | integer: `${TYPE_PREFIX}_INTEGER`, 6 | boolean: `${TYPE_PREFIX}_BOOLEAN` 7 | }; 8 | 9 | module.exports = types; 10 | -------------------------------------------------------------------------------- /src/validations.js: -------------------------------------------------------------------------------- 1 | // Validation is a function that takes a single value, 2 | // and returns an array of errors and a boolean validity. 3 | 4 | const _ = require('underscore'); 5 | 6 | const wrap = (fn, err) => { 7 | return (val) => { 8 | if (_.isUndefined(val) || fn(val)) { 9 | return [null, true]; 10 | } else { 11 | return [err, false]; 12 | } 13 | }; 14 | }; 15 | 16 | const requireBoolean = wrap(_.isBoolean, "should be a boolean"); 17 | const requireString = wrap(_.isString, "should be a string"); 18 | 19 | const isInt = (value) => { 20 | return typeof value === "number" && 21 | isFinite(value) && 22 | Math.floor(value) === value; 23 | }; 24 | 25 | const requireInteger = wrap(isInt, "should be an integer"); 26 | 27 | const present = (val) => { 28 | if (_.isUndefined(val) || _.isNull(val)) { 29 | return [ "should be present", false]; 30 | } else { 31 | return [null, true]; 32 | } 33 | }; 34 | 35 | const maxLength = (max) => { 36 | return (val) => { 37 | if (_.isUndefined(val)) { 38 | return [null, true]; 39 | } 40 | 41 | if (val.length) { 42 | if (val.length > max) { 43 | return ["max length exceeded", false]; 44 | } else { 45 | return [null, true]; 46 | } 47 | } else { 48 | return ["can't limit a max length: length is undefined", false]; 49 | } 50 | }; 51 | }; 52 | 53 | const minLength = (min) => { 54 | return (val) => { 55 | if (_.isUndefined(val)) { 56 | return [null, true]; 57 | } 58 | 59 | if (val && val.length) { 60 | if (val.length < min) { 61 | return ["min length exceeded", false]; 62 | } else { 63 | return [null, true]; 64 | } 65 | } else { 66 | return ["can't limit a min length: length is undefined", false]; 67 | }; 68 | }; 69 | }; 70 | 71 | const min = (min) => { 72 | return (val) => { 73 | if (val < min) { 74 | return [`should be more or equal to ${min}`, false] 75 | } else { 76 | return [null, true]; 77 | }; 78 | }; 79 | }; 80 | 81 | const max = (max) => { 82 | return (val) => { 83 | if (val > max) { 84 | return [`should be less or equal to ${max}`, false] 85 | } else { 86 | return [null, true]; 87 | }; 88 | }; 89 | }; 90 | 91 | const oneOf = (...args) => { 92 | return (val) => { 93 | if ((args || []).indexOf(val) > -1) { 94 | return [null, true]; 95 | } else { 96 | return [`should be one of [${args}]`, false] 97 | }; 98 | }; 99 | }; 100 | 101 | // Composite or 102 | // Usage: 103 | // let outerRange = or(minLength(10), maxLength(2)); 104 | const or = (v1, v2) => { 105 | return (val) => { 106 | let [err1, valid1] = v1(val); 107 | if (valid1) { 108 | return [null, true]; 109 | } 110 | 111 | let [err2, valid2] = v2(val); 112 | if (valid2) { 113 | return [null, true]; 114 | } 115 | 116 | return [`${err1}, ${err2}`, false]; 117 | }; 118 | }; 119 | 120 | // Checks a value over a list of validations 121 | const check = (val, validations) => { 122 | return _.reduce(validations, (memo, validation) => { 123 | const [err, valid] = validation(val); 124 | if (!valid) { 125 | memo[0].push(err); 126 | memo[1] = false; 127 | } 128 | return memo; 129 | }, [ [], true ]); 130 | }; 131 | 132 | module.exports = { 133 | requireBoolean, 134 | requireInteger, 135 | requireString, 136 | present, 137 | min, 138 | max, 139 | maxLength, 140 | minLength, 141 | oneOf, 142 | or, 143 | check 144 | }; 145 | --------------------------------------------------------------------------------