diff --git a/README.md b/README.md new file mode 100644 index 0000000..8fc3e4a --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +## `shape` +#### Isomorphic and Async object validation library. + +##### `npm install --save @krab/shape'` +##### `import Shape from '@krab/shape'` +##### `const Shape = require('@krab/shape/common')` + +`shape` is used to vaidate data in an async manner. Doesn't matter if your checks are all synchronous, `shape` will check them in an async manner. This is very useful for validating data against external data sources. Example below: + +```js +import Shape from '@krab/shape'; + +// using async-await +async function createUser(data={}) { + const validation = new Shape({ + username: async (username) => { + if (isString(username) && username.length > 0) { + const existing = await findUserByUsername(username); + return existing ? 'username taken' : null; + } else { + return 'invalid'; + } + }, + + password: (password) => { + return isString(password) && password.length >= 7 ? null : 'invalid'; + }, + + repeat_password: (repeat_password, {password}) => { + return repeat_password === password ? null : 'invalid'; + } + }); + + const errors = await validation.errors(data); + // Errors will be 'null' if all checks pass. + // If any check fails, errors will be an object that contains error messages. + // Validation will also fail for keys in data which are not specified when + // defining a validation + + if (errors) { + throw errors; + } else { + return await insertNewPostInDB(data); + } +} + +// using promises +function createPost(data={}) { + const validation = new Shape({ + author_id: async (authorId) => { + const author = await findUserById(authorId); + return author ? null : `invalid author_id: ${authorId}`; + }, + + title: (title) => { + return isString(title) ? findPostByTitle(title).then((existing) => { + return ( + existing ? 'title already taken' : ( + title.length === 0 ? 'title cannot be empty' : null + ) + ); + }) : 'title must be string'; + }, + + body: (body) => { + if (!isArray(body) || body.filter((p) => isString(p)).length < body.length) { + return 'body must be a list of paragraph strings'; + } else if (body.length === 0) { + return 'body cannot be empty'; + } else { + return null; + } + } + }); + + return validation.errors(data).then((errors) => { + if (errors) { + return Promise.reject(errors); + } else { + return insertNewPostInDB(data); + } + }); +} +``` diff --git a/common.js b/common.js new file mode 100644 index 0000000..6f34a51 --- /dev/null +++ b/common.js @@ -0,0 +1,5 @@ +'use strict'; + +var Shape = require('./Shape').default; + +module.exports = Shape; \ No newline at end of file diff --git a/lib/Shape.js b/lib/Shape.js new file mode 100644 index 0000000..26df3e7 --- /dev/null +++ b/lib/Shape.js @@ -0,0 +1,142 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +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; }; }(); /** + * Promise based validation of input + * + + const Shape = require('shape-errors'); + const s = new Shape({ + user_id: (userId) => userExists(userId).then((exists) => exists ? null : 'invalid'), + name: (name, {user_id}) => findUserByName(name).then( + (user) => user.id === user_id ? null : 'invalid' + ) + }) + + s.errors(data).then(({result, errors}) => {}) + */ + + +var _lodash = require('lodash'); + +var _isuseableobject = require('@krab/isuseableobject'); + +var _isuseableobject2 = _interopRequireDefault(_isuseableobject); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Shape = function () { + function Shape() { + var validations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; + + _classCallCheck(this, Shape); + + this.validations = new Map(); + + this.addValidations(validations); + } + + _createClass(Shape, [{ + key: 'addValidations', + value: function addValidations() { + var _this = this; + + var validations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; + + if ((0, _isuseableobject2.default)(validations)) { + validations = (0, _lodash.toPlainObject)(validations); + validations = Object.keys(validations).map(function (k) { + return { key: k, validation: validations[k] }; + }); + } + + validations.forEach(function (_ref) { + var key = _ref.key, + validation = _ref.validation; + + _this.validations.set(key, validation); + }); + + return this; + } + }, { + key: 'addValidation', + value: function addValidation(_ref2) { + var key = _ref2.key, + validation = _ref2.validation; + + this.validations.set(key, validation); + return this; + } + }, { + key: 'merge', + value: function merge(validator) { + var _this2 = this; + + Array.from(validator.validation.keys()).forEach(function (k) { + _this2.validations.set(k, validator.validations.get(k)); + }); + + return this; + } + }, { + key: 'errors', + value: function errors() { + var _this3 = this; + + var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var invalidInputKeysErr = Object.keys(input).filter(function (k) { + return Array.from(_this3.validations.keys()).indexOf(k) === -1; + }).reduce(function (err, k) { + return _extends({}, err, _defineProperty({}, k, 'invalid key')); + }, {}); + + return Promise.all(Array.from(this.validations.keys()).map(function (key) { + var err = _this3.validations.get(key)(input[key], input, key); + + if (err instanceof Promise) { + return err.then(function (err) { + return { key: key, err: err }; + }); + } else if (err instanceof Shape) { + return err.errors(input[key]).then(function (err) { + return { key: key, err: err }; + }); + } else { + return { key: key, err: err }; + } + })).then(function (checks) { + var checksFailed = checks.filter(function (_ref3) { + var err = _ref3.err; + return !!err; + }); + var numInvalidInputKeysError = Object.keys(invalidInputKeysErr).length; + + if (checksFailed.length === 0 && numInvalidInputKeysError === 0) { + return null; + } else { + return checks.reduce(function (all, _ref4) { + var key = _ref4.key, + err = _ref4.err; + + return (0, _lodash.assign)(all, _defineProperty({}, key, err)); + }, invalidInputKeysErr); + } + }); + } + }]); + + return Shape; +}(); + +exports.default = Shape; \ No newline at end of file diff --git a/lib/common.js b/lib/common.js new file mode 100644 index 0000000..6f34a51 --- /dev/null +++ b/lib/common.js @@ -0,0 +1,5 @@ +'use strict'; + +var Shape = require('./Shape').default; + +module.exports = Shape; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1814646..895bd96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -209,6 +209,14 @@ } } }, + "@krab/isuseableobject": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@krab/isuseableobject/-/isuseableobject-1.0.1.tgz", + "integrity": "sha1-t1SBFH3Hlhjm4akRmx7TciN8ivs=", + "requires": { + "lodash": "4.17.5" + } + }, "acorn": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", diff --git a/package.json b/package.json index 71428dc..62eaa74 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "vent", "version": "1.0.0", - "description": "An isomorphic event-aggregator", - "main": "index.js", + "description": "An isomorphic & async object-validation library", + "main": "lib/Shape.js", "scripts": { "test": "babel-node test/main.js", "repl": "babel-node", @@ -12,18 +12,20 @@ }, "repository": { "type": "git", - "url": "git+ssh://git@github.com/kapv89/vent.git" + "url": "git+ssh://git@github.com/kapv89/shape.git" }, "keywords": [ - "events", - "isomorphic" + "async", + "isomorphic", + "object-validation", + "validation" ], "author": "Kapil Verma ", "license": "MIT", "bugs": { - "url": "https://github.com/kapv89/vent/issues" + "url": "https://github.com/kapv89/shape/issues" }, - "homepage": "https://github.com/kapv89/vent#readme", + "homepage": "https://github.com/kapv89/shape#readme", "devDependencies": { "babel-cli": "^6.26.0", "babel-eslint": "^8.2.3", @@ -35,6 +37,7 @@ "eslint-plugin-react": "^7.7.0" }, "dependencies": { + "@krab/isuseableobject": "^1.0.1", "lodash": "^4.17.5" } } diff --git a/src/Shape.js b/src/Shape.js new file mode 100644 index 0000000..2e7815b --- /dev/null +++ b/src/Shape.js @@ -0,0 +1,91 @@ +/** + * Promise based validation of input + * + +const Shape = require('shape-errors'); +const s = new Shape({ + user_id: (userId) => userExists(userId).then((exists) => exists ? null : 'invalid'), + name: (name, {user_id}) => findUserByName(name).then( + (user) => user.id === user_id ? null : 'invalid' + ) +}) + +s.errors(data).then(({result, errors}) => {}) +*/ +import {assign, toPlainObject} from 'lodash'; +import isuseableobject from '@krab/isuseableobject'; + +export default class Shape { + constructor(validations=[]) { + this.validations = new Map(); + + this.addValidations(validations); + } + + addValidations(validations=[]) { + if (isuseableobject(validations)) { + validations = toPlainObject(validations); + validations = Object.keys(validations).map((k) => ({key: k, validation: validations[k]})); + } + + validations.forEach(({key, validation}) => { + this.validations.set(key, validation); + }); + + return this; + } + + addValidation({key, validation}) { + this.validations.set(key, validation); + return this; + } + + merge(validator) { + Array.from(validator.validation.keys()).forEach((k) => { + this.validations.set(k, validator.validations.get(k)); + }); + + return this; + } + + errors(input={}) { + const invalidInputKeysErr = Object.keys(input) + .filter((k) => { + return Array.from(this.validations.keys()).indexOf(k) === -1; + }) + .reduce((err, k) => ({ + ...err, + [k]: 'invalid key' + }), {}) + ; + + return Promise.all( + Array.from(this.validations.keys()).map((key) => { + const err = this.validations.get(key)(input[key], input, key); + + if (err instanceof Promise) { + return err.then((err) => { + return {key, err}; + }); + } else if (err instanceof Shape) { + return err.errors(input[key]).then((err) => { + return {key, err}; + }); + } else { + return {key, err}; + } + }) + ).then((checks) => { + const checksFailed = checks.filter(({err}) => !!err); + const numInvalidInputKeysError = Object.keys(invalidInputKeysErr).length; + + if (checksFailed.length === 0 && numInvalidInputKeysError === 0) { + return null; + } else { + return checks.reduce((all, {key, err}) => { + return assign(all, {[key]: err}); + }, invalidInputKeysErr); + } + }); + } +} diff --git a/src/common.js b/src/common.js new file mode 100644 index 0000000..63d0c7e --- /dev/null +++ b/src/common.js @@ -0,0 +1,3 @@ +const Shape = require('./Shape').default; + +module.exports = Shape; diff --git a/test/main.js b/test/main.js index 13cc953..3f4d933 100644 --- a/test/main.js +++ b/test/main.js @@ -1,3 +1,48 @@ +import {ok} from 'assert'; +import Shape from '../src/Shape'; + +async function run() { + console.log('testing shape'); + + const validData = { + x: 1, + y: 2 + }; + + const dataWithExtraKey = { + x: 1, + y: 2, + z: 3 + }; + + const invalidData = { + x: 1, + y: 1 + }; + + const validation = new Shape({ + x: (x) => x === 1 ? null : 'invalid', + y: (y) => new Promise((resolve) => { + setTimeout(() => resolve(2), 300); + }).then((val) => y === val ? null : 'invalid') + }); + + console.log('testing valid data'); + const noErr = await validation.errors(validData); + ok(noErr === null); + + console.log('testing data with extra key'); + const extraKeyErr = await validation.errors(dataWithExtraKey); + ok(extraKeyErr.x === null); + ok(extraKeyErr.y === null); + ok(extraKeyErr.z === 'invalid key'); + + console.log('testing invalid data'); + const invalidDataErr = await validation.errors(invalidData); + ok(invalidDataErr.x === null); + ok(invalidDataErr.y === 'invalid'); +} + if (require.main === module) { - console.log('here'); + run(); }