<file_sep>
# Validating request payload and body

[![npm version](https://badge.fury.io/js/goabela-schema-validator.svg)](https://badge.fury.io/js/goabela-schema-validator)

This module can validate the request's payload and body by using a schema JSON file.

## Installation
Install it by using the `npm install` command in the node shell:
```sh
$ npm install goabela-schema-validator
```
\
Import the module in your `.js` file:
```js
var schemaValidator = require("goabela-schema-validator")
```

## Features
### Validate
_Method: `schemaValidator.validate()`._

The `validate` method will do the validation. It will return a promise so you can call `then` and `catch` on the method. Required arguements are the body/payload as an object and the schema object.

First you have to create a schema object. A good solution could be that creating a schema file such as `schema.json`.

After that in the NodeJS file (e.g. under the Express controller), you can call the `validate` method with the schema and the body object.

The prototype of the method:
```js
schemaValidator.validate(body, schema)
  .then(() => {
    // The body is OK
  })
  .catch(err => {
    // The body is invalid
  })
```

The `err` variable will contain an object with all the errors that the body has. You can check the possible errors at the [errors section](#errors).

_For a full example please check the [example section](#example)._

## Schema structure
### Properties
The available properties are the followings:
- `type`: The type of the element. Valid values are `string`, `number` and `array`. Default is `string`.
- `required`: Wether the given element is required or not. Can be `true` or `false`. Default is `false`.
- `children`: The sub-elements.
- `enum`: The list of the possible values collected into an array.
- ~`length`~: The length settings of the element. Can have a `min` and a `max` value.
- ~`range`~: When the `type` is `number`, you can specify a range by adding `min` and `max`.
- ~`pattern`~: A regex script that you want to run on the given value.

### Prototype
```js
{
  "username": {
    "type": "string",
    "required": true
  }
}
```

## Errors
By calling the `catch` on the `validate` function, you can get the errors.

```js
.catch(err => {
  console.log(err) // Output: { "internalIssue": "schemaNotSet" }
})
```

### Internal errors
_Error key: `internalIssue`._

The internal issues can be the followings:
- `schemaNotSet`: The schema has not been set properly.
- `bodyNotSet`: The body has not been set properly.
- `bodyNotEmpty`: The schema is empty which means no body should have been received, but it is not empty.

### Missing keys
_Error key: `missingKeys`._

If there are any keys which are required but the body has no such key, then this error will be returned.

It will be an array which contains the missing keys with all its parents, for instance `born.date.year`.

### Invalid keys
_Error key: `invalidKeys`._

If there are any keys in the body which are not in the schema, then this error will be returned.

It will be an array which contains the invalid keys with all its parents, for instance `born.date.foo`.

### Invalid types
_Error key: `invalidTypes`._

This error object will be returned once there are any type errors.

For instance, in the schema if you set type of `year` to be `number`, and the body's key has different type, then this error will be returned.

### Out of enum
_Error key: `outOfEnum`._

If you set an enum on a property, the validator will force the body's value to be one of the enum's value.

## Example

The `schema.json` file:
```json
{
  "userId": {
    "type": "string",
    "required": true
  },
  "userRole": {
    "type": "string",
    "enum": ["owner", "writer"]
  }
}
```
\
The `app.js` file:
```js
// Other part of the app.js

var schemaValidator = require("goabela-schema-validator")
var bodySchema = require("./schema.json")

app.post("/user", (req, res) => {
  schemaValidator.validate(req.body, bodySchema)
    .then(() => {
      // Do something
    })
    .catch(err => {
      res.json({
        "message": "The request body is not correct.",
        "errors": err
      })
    })
})
```

## TO-DOs
- ~Migrating the script from my app into a single node package.~ :heavy_check_mark:
- ~Deploying unit-testing.~ :heavy_check_mark:
- Creating all the features stated above.
- Migrating the validator core loops into iterators.

## Tests
Test files are located in the `test` directory.

Use the `test` `npm` script to run the tests:
```sh
$ npm run test
```

## License
[MIT](LICENSE)
<file_sep>var assert = require("chai").assert
var expect = require("chai").expect

var schemaValidator = require("../index.js")


/**
 * Defining the schema and the body.
 *
 * Resetting before each test.
 * Later this will be modified for testing.
 */
let schema = null
let body = null

beforeEach(done => {
	schema = {
		username: {
			type: "string"
		},
		name: {
			children: {
				last: {
					type: "string"
				},
				first: {
					type: "string",
					required: true
				},
				nick: {
					type: "string"
				}
			}
		},
		born: {
			children: {
				place: {
					type: "string"
				},
				date: {
					children: {
						year: {
							type: "number"
						},
						month: {
							type: "string",
							enum: ["Jan", "Feb", "Marc"]
						},
						day: {
							type: "number",
							required: true
						}
					}
				}
			}
		},
		skills: {
			type: "array"
		}
	}

	body = {
		username: "johndoe",
		name: {
			last: "Doe",
			first: "John",
			nick: "Johnny"
		},
		born: {
			place: "Colorado, Denver",
			date: {
				year: 1985,
				month: "Jan",
				day: 15
			}
		},
		skills: [
			"SEO",
			"HTML",
			"CSS"
		]
	}

	done()
})

// Internal issue tests
describe("Internal tests", () => {

	// Testing with empty body
	it("Should throw EmptyBody error", () => {
		return schemaValidator.validate(null, schema)
		.then(() => {
			assert.fail(0, 1, "Validate should have thrown Error")
		})
		.catch(err => {
			expect(err).to.be.an.instanceof(Object)
			expect(Object.keys(err).length).to.equal(1)

			expect(err).to.have.property("internalIssue")
			expect(err.internalIssue).to.equal("bodyNotSet")
		})
	})

	// Testing with empty schema
	it("Should throw EmptySchema error", () => {
		return schemaValidator.validate(body, null)
		.then(() => {
			assert.fail(0, 1, "Validate should have thrown Error")
		})
		.catch(err => {
			expect(err).to.be.an.instanceof(Object)
			expect(Object.keys(err).length).to.equal(1)

			expect(err).to.have.property("internalIssue")
			expect(err.internalIssue).to.equal("schemaNotSet")
		})
	})
})

// Basic tests
describe("Basic tests", () => {

	// Testing for success validation
	it("Should be OK", () => {
		return schemaValidator.validate(body, schema)
		.then(() => {
			assert
		})
		.catch(err => {
			assert.fail(0, 1, "Validate thrown Error")
		})
	})

	// Testing for `invalidKeys` error
	it("Should throw InvalidKey Error", () => {

		// Adding an foreign key
		body.testingForInvalidKey1 = 123
		body.testingForInvalidKey2 = { subKey2: null }
		body.testingForInvalidKey3 = { subKey3: { subSubKey3: 123 } }
		body.testingForInvalidKey4 = { subKey4: { subSubKey4: { subSubSubKey4: 123 } } }

		return schemaValidator.validate(body, schema)
		.then(() => {
			assert.fail(0, 1, "Should have thrown an Error")
		})
		.catch(err => {
			expect(err).to.be.an.instanceof(Object)
			expect(Object.keys(err).length).to.equal(1)
			expect(err).to.have.property("invalidKeys")

			expect(err.invalidKeys).to.be.an.instanceof(Array)
			expect(err.invalidKeys).to.have.lengthOf(4)

			expect(err.invalidKeys[0]).to.equal("testingForInvalidKey1")
			expect(err.invalidKeys[1]).to.equal("testingForInvalidKey2")
			expect(err.invalidKeys[2]).to.equal("testingForInvalidKey3")
			expect(err.invalidKeys[3]).to.equal("testingForInvalidKey4")
		})
	})

	// Testing for `missingKey` error
	it("Should throw MissingKey Error", () => {

		// Adding extra keys to the schema
		schema.requiredTest1 = { type: "string", required: true }
		schema.requiredTest2 = { type: "string", required: false }
		schema.requiredTest3 = { children: { subItem3: { type: "string", required: true } } }
		schema.requiredTest4 = { children: { subItem4: { children: { subSubItem4: { type: "string", required: true } } } } }

		return schemaValidator.validate(body, schema)
		.then(() => {
			assert.fail(0, 1, "Should have thrown an Error")
		})
		.catch(err => {
			expect(err).to.be.an.instanceof(Object)
			expect(Object.keys(err).length).to.equal(1)
			expect(err).to.have.property("missingKeys")

			expect(err.missingKeys).to.be.an.instanceof(Array)
			expect(err.missingKeys).to.have.lengthOf(3)

			expect(err.missingKeys[0]).to.equal("requiredTest1")
			expect(err.missingKeys[1]).to.equal("requiredTest3.subItem3")
			expect(err.missingKeys[2]).to.equal("requiredTest4.subItem4.subSubItem4")
		})
	})
})

// Type tests
describe("Type tests", () => {

	it("Should expect `string` instead of `number` and `array`", () => {

		// Setting up invalid types

		body.testWithNumber = { subItem: { subSubItem: 47576458 } }
		schema.testWithNumber = { children: { subItem: { children: { subSubItem: { type: "string" } } } } }

		body.testWithArray = { subItem: [ "one", "two", "three" ] }
		schema.testWithArray = { children: { subItem: { type: "string" } } }

		return schemaValidator.validate(body, schema)
		.then(() => {
			assert.fail(0, 1, "Should have thrown an Error")
		})
		.catch(err => {
			expect(err).to.be.an.instanceof(Object)
			expect(Object.keys(err).length).to.equal(1)
			expect(err).to.have.property("invalidTypes")

			expect(err.invalidTypes).to.be.an.instanceof(Array)
			expect(err.invalidTypes).to.have.lengthOf(2)

			expect(err.invalidTypes[0].key).to.equal("testWithNumber.subItem.subSubItem")
			expect(err.invalidTypes[0].expected).to.equal("string")
			expect(err.invalidTypes[0].received).to.equal("number")

			expect(err.invalidTypes[1].key).to.equal("testWithArray.subItem")
			expect(err.invalidTypes[1].expected).to.equal("string")
			expect(err.invalidTypes[1].received).to.equal("array")
		})
	})

	it("Should expect `number` instead of `string` and `array`", () => {

		// Setting up invalid types
		body.testWithString = { subItem: { subSubItem: "two" } }
		schema.testWithString = { children: { subItem: { children: { subSubItem: { type: "number" } } } } }

		body.testWithArray = { subItem: [ "one", "two", "three" ] }
		schema.testWithArray = { children: { subItem: { type: "number" } } }

		return schemaValidator.validate(body, schema)
		.then(() => {
			assert.fail(0, 1, "Should have thrown an Error")
		})
		.catch(err => {
			expect(err).to.be.an.instanceof(Object)
			expect(Object.keys(err).length).to.equal(1)
			expect(err).to.have.property("invalidTypes")

			expect(err.invalidTypes).to.be.an.instanceof(Array)
			expect(err.invalidTypes).to.have.lengthOf(2)

			expect(err.invalidTypes[0].key).to.equal("testWithString.subItem.subSubItem")
			expect(err.invalidTypes[0].expected).to.equal("number")
			expect(err.invalidTypes[0].received).to.equal("string")

			expect(err.invalidTypes[1].key).to.equal("testWithArray.subItem")
			expect(err.invalidTypes[1].expected).to.equal("number")
			expect(err.invalidTypes[1].received).to.equal("array")
		})
	})

	it("Should expect `array` instead of `string` and `number`", () => {

		// Setting up invalid types
		body.testWithString = { subItem: { subSubItem: "two" } }
		schema.testWithString = { children: { subItem: { children: { subSubItem: { type: "array" } } } } }

		body.testWithNumber = { subItem: 123 }
		schema.testWithNumber = { children: { subItem: { type: "array" } } }

		return schemaValidator.validate(body, schema)
		.then(() => {
			assert.fail(0, 1, "Should have thrown an Error")
		})
		.catch(err => {
			expect(err).to.be.an.instanceof(Object)
			expect(Object.keys(err).length).to.equal(1)
			expect(err).to.have.property("invalidTypes")

			expect(err.invalidTypes).to.be.an.instanceof(Array)
			expect(err.invalidTypes).to.have.lengthOf(2)

			expect(err.invalidTypes[0].key).to.equal("testWithString.subItem.subSubItem")
			expect(err.invalidTypes[0].expected).to.equal("array")
			expect(err.invalidTypes[0].received).to.equal("string")

			expect(err.invalidTypes[1].key).to.equal("testWithNumber.subItem")
			expect(err.invalidTypes[1].expected).to.equal("array")
			expect(err.invalidTypes[1].received).to.equal("number")
		})
	})
})

describe("Property tests", () => {

	it("Should be out of enum", () => {

		// Setting up a out-of-enum body property
		body.born.date.month = "Apr"

		return schemaValidator.validate(body, schema)
		.then(() => {
			assert.fail(0, 1, "Should have thrown an Error")
		})
		.catch(err => {
			expect(err).to.be.an.instanceof(Object)
			expect(Object.keys(err).length).to.equal(1)
			expect(err).to.have.property("outOfEnum")

			expect(err.outOfEnum).to.be.an.instanceof(Array)
			expect(err.outOfEnum).to.have.lengthOf(1)

			expect(err.outOfEnum[0].key).to.equal("born.date.month")
			expect(err.outOfEnum[0].received).to.equal("Apr")
			expect(err.outOfEnum[0]).to.have.property("enum")
			expect(err.outOfEnum[0].enum).to.be.an.instanceof(Array)
		})
	})
})
