├── .gitignore ├── tsconfig.json ├── typings └── tsd.d.ts ├── tsd.json ├── package.json ├── index.ts ├── README.md ├── index.js └── test ├── test.ts └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | typings 3 | !typings/tsd.d.ts 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": false, 6 | "sourceMap": false 7 | } 8 | } -------------------------------------------------------------------------------- /typings/tsd.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "node/node.d.ts": { 9 | "commit": "7dc3b09e3ad2a8de14330d6c84efe3e6d76fd79b" 10 | }, 11 | "chai/chai.d.ts": { 12 | "commit": "7dc3b09e3ad2a8de14330d6c84efe3e6d76fd79b" 13 | }, 14 | "mocha/mocha.d.ts": { 15 | "commit": "7dc3b09e3ad2a8de14330d6c84efe3e6d76fd79b" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoose-ajv-plugin", 3 | "version": "2.0.0", 4 | "description": "AJV plugin for Mongoose", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "author": "Jason Thorpe", 10 | "license": "ISC", 11 | "dependencies": { 12 | "ajv": "^5.2.0", 13 | "mongoose": "^4.10.8", 14 | "semver": "^5.3.0" 15 | }, 16 | "devDependencies": { 17 | "bluebird": "^3.4.6", 18 | "chai": "^3.5.0", 19 | "chai-as-promised": "^6.0.0", 20 | "mocha": "^3.1.2" 21 | }, 22 | "directories": { 23 | "test": "test" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/jdthorpe/mongoose-ajv-plugin.git" 28 | }, 29 | "keywords": [ 30 | "mongoose", 31 | "ajv", 32 | "json-schema", 33 | "json", 34 | "schema", 35 | "JSONschema" 36 | ], 37 | "bugs": { 38 | "url": "https://github.com/jdthorpe/mongoose-ajv-plugin/issues" 39 | }, 40 | "homepage": "https://github.com/jdthorpe/mongoose-ajv-plugin#readme" 41 | } 42 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------- 2 | // Programmer: Jason Thorpe 3 | // Language: typescript 4 | // Purpose: 5 | // Comments: 6 | // -------------------------------------------------------------------------------- 7 | 8 | /// 9 | 10 | var semver = require('semver'); 11 | var mongoose = require('mongoose'); 12 | var AJV = require('ajv'); 13 | var ValidationError = mongoose.Error.ValidationError; 14 | var ValidatorError = mongoose.Error.ValidatorError; 15 | 16 | export = function (schema, options) { 17 | 18 | // GET THE AJV INSTANCE 19 | var ajv = (options && options.ajv) || new AJV(); 20 | if(options && options.plugins){ 21 | for(let key in options.plugins){ 22 | if(!options.plugins.hasOwnProperty(key)) 23 | continue; 24 | if(options.plugins[key].version){ 25 | var version = require(key+"/package.json").version; 26 | if(!semver.satisfies(version, options.plugins[key].version)) 27 | throw new Error(`Installed version "${version}" of plugin "${key}" does not satisify "${options.plugins[key].version}"`); 28 | } 29 | if(options.plugins[key].options){ 30 | require(key)(ajv,options.plugins[key].options) 31 | }else{ 32 | require(key)(ajv) 33 | } 34 | } 35 | } 36 | 37 | // COMPILE THE ATTRIBUTE SCHEMA 38 | var schemata = {},SCHEMA; 39 | // COMPILE THE OVERALL DOCUMENT SCHEMA 40 | if(schema.path("ajv-schema")){ 41 | try{ 42 | var data = schema.paths["ajv-schema"].options; 43 | SCHEMA = ajv.compile(data) 44 | schema.remove("ajv-schema"); 45 | }catch(err){ 46 | throw new Error(`Failed to compile document schema with error message: ${err.message} `); 47 | } 48 | } 49 | 50 | schema.eachPath((key) => { 51 | if(! schema.path(key).options["ajv-schema"]){ 52 | return ; 53 | } 54 | try{ 55 | console.log(`creating schema for path: "${key}" ... `) 56 | var $schema = ajv.compile(schema.path(key).options["ajv-schema"]) 57 | console.log(`SUCCESS!\n`) 58 | }catch(err){ 59 | console.log(`FAILED.\n`) 60 | throw new Error(`Failed to compile schema for path "${key}" with error message: ${err.message} `); 61 | } 62 | schemata[key] = $schema; 63 | }); 64 | 65 | schema.post('validate', function (data,next) { 66 | // APPLY THE OVERALL DOCUMENT SCHEMA 67 | if(SCHEMA && !SCHEMA(data)){ 68 | var error = new ValidationError(data); 69 | error.message += "; "; 70 | console.log("SCHEMA.errors.length: ",SCHEMA.errors.length) 71 | error.message += JSON.stringify(SCHEMA.errors.map((x) => `'${x.schemaPath}' ${x.message}`)); 72 | error.errors.record = new ValidatorError('record', 'Overall object does not match JSON-schema', 'notvalid', data); 73 | error.errors.record.errors = SCHEMA.errors; 74 | return next(error); 75 | } 76 | try{ 77 | // APPLY THE ATTRIBUTE SCHEMA 78 | var $schema; 79 | for(var key in schemata){ 80 | if(data[key] === undefined){ 81 | // use the Mongoose `required` validator for validating the presence of the attribute 82 | continue 83 | } 84 | console.log("validating schema path", key); 85 | $schema = schemata[key]; 86 | if(!$schema(data[key])){ 87 | var error = new ValidationError(data); 88 | error.message += `; '${key}' attribute does not match it's JSON-schema: `; 89 | error.message += JSON.stringify($schema.errors.map((x) => `'${x.schemaPath}' ${x.message}`)); 90 | error.errors[key] = new ValidatorError(key, key+' does not match JSON-schema', 'notvalid', data); 91 | error.errors[key].errors = schemata[key].errors; 92 | return next(error); 93 | } 94 | } 95 | return next(); 96 | }catch(err){ 97 | return next(err); 98 | } 99 | }) 100 | 101 | } 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## The Problem 3 | 4 | You love Mongoose for all it's convenience methods and 5 | valiate-before-saving logic, but but you store complex objects using 6 | `Schema.Types.Mixed` which lacks validation in Mongoose, or you just wish 7 | you could validate objects, strings, etc. using a richer 8 | [JSON-schema](http://json-schema.org/) vocabulary than is included with 9 | Mongoose. 10 | 11 | ## The Solution 12 | 13 | The `mongoose-ajv-plugin` lets you use the awesome [AJV JSON-Schema 14 | validation library][ajv], to validate individual attributes or entire 15 | documents, giving you access to it's rich extensible schema vocabulary and convenience 16 | formats like [email][formats], [Date][formats], [hostname][formats], ect. 17 | 18 | [ajv]: https://github.com/epoberezkin/ajv "AJV" 19 | [formats]: https://github.com/epoberezkin/ajv#formats "String Formats" 20 | [validate]: http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate "Validation" 21 | 22 | ## Getting Started 23 | 24 | Import mongoose and add in the `mongoose-ajv-plugin`: 25 | 26 | ```JavaScript 27 | var mongoose = require("mongoose"); 28 | mongoose.plugin(require("mongoose-ajv-plugin")) 29 | ``` 30 | 31 | Now use your favorite AJV Schema, such as the `ajv_contact_schema` defined 32 | below, to validate entire documents using the `"ajv-schema"` keyword, like 33 | so: 34 | 35 | ```JavaScript 36 | var Contact_schema = new mongoose.Schema({ 37 | "name": String , 38 | "email": String, 39 | "birthday": String, 40 | // let AJV validate this entire document 41 | "ajv-schema": ajv_contact_schema 42 | }); 43 | ``` 44 | 45 | Or use AJV to validate one or more attributes of a document using the `"ajv-schema"` option: 46 | 47 | ```JavaScript 48 | // use AJV to validate fields within a document 49 | var Player_schema = new Schema({ 50 | "user_name": String, 51 | "rank": Number, 52 | "ip_address": { 53 | "type": String, 54 | // let AJV validate this string attribute 55 | "ajv-schema": { 56 | "type": 'string', 57 | "format": 'ipv4' / 58 | } 59 | }, 60 | "contact-info": { 61 | "type": Schema.Types.Mixed , 62 | // let AJV validate this nested object 63 | "ajv-schema": contact_json_schema 64 | }, 65 | }); 66 | ``` 67 | 68 | ## Using AJV Extensions 69 | 70 | If you wish to extend the Ajv instance used for validation with additional 71 | [schemata](https://github.com/epoberezkin/ajv#addschemaarrayobjectobject-schema--string-key), 72 | [formats](https://github.com/epoberezkin/ajv#addformatstring-name-stringregexpfunctionobject-format), 73 | or [keywords](https://github.com/epoberezkin/ajv#api-addkeyword), you can 74 | pass your own (extended) ajv instance to the plugin, like so: 75 | 76 | ```JavaScript 77 | // create an Ajv instance 78 | var Ajv = require("ajv"); 79 | var ajv = new Ajv(); 80 | 81 | // add custom schema, keywords, or formats 82 | ajv.addSchema(...); 83 | // or 84 | ajv.addKewword(...) 85 | // or 86 | ajv.addFormat(...) 87 | // or 88 | require("my-ajv-plugin")(ajv) 89 | 90 | // use this ajv instance to compile every new validator 91 | mongoose.plugin(require("mongoose-ajv-plugin",{"ajv":ajv}) 92 | 93 | // or use this ajv instance to compile validators for an individual 94 | // mongoose schema 95 | var my_schema = new mongoose.Schema({...}); 96 | my_schema.plugin(require("mongoose-ajv-plugin",{"ajv":ajv}) 97 | ``` 98 | 99 | ## Contact JSON schema 100 | 101 | And finally, here's the definition of `ajv_contact_schema` used in the 102 | above examples: 103 | 104 | ```JavaScript 105 | var ajv_contact_schema = { 106 | "type":"object", 107 | "properties":{ 108 | "name": { 109 | "type":"string" 110 | }, 111 | "email": { 112 | "type":"string", 113 | "fomrat":"email" 114 | }, 115 | "birthday": { 116 | "oneOf":[ 117 | {"$ref":"#/definitions/date"}, 118 | {"$ref":"#/definitions/date-time"} 119 | ] 120 | } 121 | }, 122 | "required":[ 123 | "name", 124 | "email" 125 | ], 126 | "definitions":{ 127 | "date":{ 128 | "type":"string", 129 | "format":"date" 130 | }, 131 | "date-time":{ 132 | "type":"string", 133 | "format":"date-time" 134 | } 135 | } 136 | }; 137 | ``` 138 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // -------------------------------------------------------------------------------- 3 | // Programmer: Jason Thorpe 4 | // Language: typescript 5 | // Purpose: 6 | // Comments: 7 | // -------------------------------------------------------------------------------- 8 | /// 9 | var semver = require('semver'); 10 | var mongoose = require('mongoose'); 11 | var AJV = require('ajv'); 12 | var ValidationError = mongoose.Error.ValidationError; 13 | var ValidatorError = mongoose.Error.ValidatorError; 14 | module.exports = function (schema, options) { 15 | // GET THE AJV INSTANCE 16 | var ajv = (options && options.ajv) || new AJV(); 17 | if (options && options.plugins) { 18 | for (var key in options.plugins) { 19 | if (!options.plugins.hasOwnProperty(key)) 20 | continue; 21 | if (options.plugins[key].version) { 22 | var version = require(key + "/package.json").version; 23 | if (!semver.satisfies(version, options.plugins[key].version)) 24 | throw new Error("Installed version \"" + version + "\" of plugin \"" + key + "\" does not satisify \"" + options.plugins[key].version + "\""); 25 | } 26 | if (options.plugins[key].options) { 27 | require(key)(ajv, options.plugins[key].options); 28 | } 29 | else { 30 | require(key)(ajv); 31 | } 32 | } 33 | } 34 | // COMPILE THE ATTRIBUTE SCHEMA 35 | var schemata = {}, SCHEMA; 36 | // COMPILE THE OVERALL DOCUMENT SCHEMA 37 | if (schema.path("ajv-schema")) { 38 | try { 39 | var data = schema.paths["ajv-schema"].options; 40 | SCHEMA = ajv.compile(data); 41 | schema.remove("ajv-schema"); 42 | } 43 | catch (err) { 44 | throw new Error("Failed to compile document schema with error message: " + err.message + " "); 45 | } 46 | } 47 | schema.eachPath(function (key) { 48 | if (!schema.path(key).options["ajv-schema"]) { 49 | return; 50 | } 51 | try { 52 | console.log("creating schema for path: \"" + key + "\" ... "); 53 | var $schema = ajv.compile(schema.path(key).options["ajv-schema"]); 54 | console.log("SUCCESS!\n"); 55 | } 56 | catch (err) { 57 | console.log("FAILED.\n"); 58 | throw new Error("Failed to compile schema for path \"" + key + "\" with error message: " + err.message + " "); 59 | } 60 | schemata[key] = $schema; 61 | }); 62 | schema.post('validate', function (data, next) { 63 | // APPLY THE OVERALL DOCUMENT SCHEMA 64 | if (SCHEMA && !SCHEMA(data)) { 65 | var error = new ValidationError(data); 66 | error.message += "; "; 67 | console.log("SCHEMA.errors.length: ", SCHEMA.errors.length); 68 | error.message += JSON.stringify(SCHEMA.errors.map(function (x) { return "'" + x.schemaPath + "' " + x.message; })); 69 | error.errors.record = new ValidatorError('record', 'Overall object does not match JSON-schema', 'notvalid', data); 70 | error.errors.record.errors = SCHEMA.errors; 71 | return next(error); 72 | } 73 | try { 74 | // APPLY THE ATTRIBUTE SCHEMA 75 | var $schema; 76 | for (var key in schemata) { 77 | if (data[key] === undefined) { 78 | // use the Mongoose `required` validator for validating the presence of the attribute 79 | continue; 80 | } 81 | console.log("validating schema path", key); 82 | $schema = schemata[key]; 83 | if (!$schema(data[key])) { 84 | var error = new ValidationError(data); 85 | error.message += "; '" + key + "' attribute does not match it's JSON-schema: "; 86 | error.message += JSON.stringify($schema.errors.map(function (x) { return "'" + x.schemaPath + "' " + x.message; })); 87 | error.errors[key] = new ValidatorError(key, key + ' does not match JSON-schema', 'notvalid', data); 88 | error.errors[key].errors = schemata[key].errors; 89 | return next(error); 90 | } 91 | } 92 | return next(); 93 | } 94 | catch (err) { 95 | return next(err); 96 | } 97 | }); 98 | }; 99 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------- 2 | // Programmer: Jason Thorpe 3 | // Language: typescript 4 | // Purpose: mocha testing 5 | // Comments: 6 | // -------------------------------------------------------------------------------- 7 | 8 | /// 9 | /// 10 | /// 11 | 12 | import chai = require('chai'); 13 | var expect = chai.expect; 14 | 15 | var mongoose = require('mongoose'); 16 | mongoose.Promise = require('bluebird'); 17 | var Schema = mongoose.Schema; 18 | 19 | var ajv_plugin = require('../index') 20 | mongoose.plugin(ajv_plugin) 21 | 22 | var AJV = require('ajv'), 23 | ajv = new AJV(); 24 | 25 | ajv.validate({"type":"string","format":"ipv4"},"123.4.5.789") 26 | 27 | //------------------------------ 28 | // define an AJV JSONschema: 29 | //------------------------------ 30 | 31 | var contact_json_schema = { 32 | "type":"object", 33 | "properties":{ 34 | "name": { 35 | "type":"string" 36 | }, 37 | "email": { 38 | "type":"string", 39 | "fomrat":"email" 40 | }, 41 | "birthday": { 42 | "oneOf":[ 43 | {"$ref":"#/definitions/date"}, 44 | {"$ref":"#/definitions/date-time"} 45 | ] 46 | } 47 | }, 48 | "required":[ 49 | "name", 50 | "email" 51 | ], 52 | "definitions":{ 53 | "date":{ 54 | "type":"string", 55 | "format":"date" 56 | }, 57 | "date-time":{ 58 | "type":"string", 59 | "format":"date-time" 60 | } 61 | } 62 | }; 63 | 64 | var team_json_schema = { 65 | "type":"object", 66 | "properties": { 67 | "team_name": { 68 | "type": "string", 69 | // team names must be between 5 and 30 characters 70 | "minLength": 5, 71 | "maxLength": 30 72 | }, 73 | "players": { 74 | "type": "array", 75 | "minItems": 2, 76 | "maxItems": 10, 77 | "items": { 78 | "type": "string" 79 | } 80 | } 81 | } 82 | }; 83 | 84 | describe("Schemas",function(){ 85 | it("should be valid",function(){ 86 | expect(ajv.validateSchema(contact_json_schema)).to.be.true; 87 | expect(ajv.validateSchema(team_json_schema)).to.be.true; 88 | }) 89 | }) 90 | 91 | // ---------------------------------------- 92 | // build the models 93 | // ---------------------------------------- 94 | 95 | // use AJV to validate fields within a document 96 | var Player_schema = new Schema({ 97 | "user_name": String, 98 | "rank": Number, 99 | "ip_address": { 100 | "type": String, // the mongoose type 101 | "ajv-schema": { // use AJV to validtae this string 102 | "type": 'string', // the JSON schema Type 103 | "format": 'ipv4' // AJV convenience String Format 104 | } 105 | }, 106 | "contact": { 107 | "type": Schema.Types.Mixed , 108 | "ajv-schema": contact_json_schema // use AJV to validate this object 109 | }, 110 | }); 111 | 112 | // add the AJV plugin to the schema 113 | //-- Player_schema.plugin(ajv_plugin); 114 | // Create a model from the schema 115 | var Player = mongoose.model('Player', Player_schema); 116 | 117 | var Team_schema = new Schema({ 118 | "team_name": String, 119 | "players": [String], 120 | "ajv-schema":team_json_schema, 121 | }); 122 | //-- Team_schema.plugin(ajv_plugin); 123 | var Team = mongoose.model('Team', Team_schema); 124 | 125 | // ---------------------------------------- 126 | // build the model instances 127 | // ---------------------------------------- 128 | 129 | var valid_attrs = new Player({ 130 | 131 | "user_name": "Felix", 132 | "rank": 5, 133 | "ip_address": "123.45.67.89", 134 | "contact": { 135 | "name":"Jack" , 136 | "email":"plaza626@email.com", 137 | "birthday": "1925-02-08" 138 | } 139 | 140 | }); 141 | 142 | var invalid_string_attribute = new Player({ 143 | "user_name": "Oscar", 144 | "rank": 7, 145 | "ip_address": "123.4.5.678", // invalid ip_address 146 | "contact": { 147 | "name":"Walter" , 148 | "email":"RedWingsFan@poker.com", 149 | "birthday": "1920-10-01" // invalid date format format 150 | }, 151 | }) 152 | 153 | var invalid_object_attribute = new Player({ 154 | "user_name": "Oscar", 155 | "rank": 7, 156 | "ip_address": "123.45.67.89", 157 | "contact": { 158 | "name":"Walter" , 159 | "email":"RedWingsFan@poker.com", 160 | "birthday": "October 1, 1920" // invalid date format format 161 | }, 162 | }) 163 | 164 | var invalid_doc = new Team({ 165 | "team_name": "Just Me", 166 | "players": ["Bridget"] // too few players 167 | }) 168 | 169 | var valid_doc = new Team({ 170 | "team_name": "ThursdayNightPoker", 171 | "players": ["Oscar","Felix","Speed","Vinnie","Roy","Murray"] 172 | }) 173 | 174 | // ---------------------------------------- 175 | // testing 176 | // ---------------------------------------- 177 | 178 | describe("Invalid nested object",function(){ 179 | it("should not validate",function(done){ 180 | invalid_object_attribute.validate(function(err,doc){ 181 | if(err){ 182 | done(); 183 | }else{ 184 | done(new Error("Failed to throw an error")); 185 | } 186 | }) 187 | }) 188 | }) 189 | 190 | describe("Invalid string attribute",function(){ 191 | it("should not validate",function(done){ 192 | invalid_string_attribute.validate(function(err,doc){ 193 | if(err){ 194 | done(); 195 | }else{ 196 | done(new Error("Failed to throw an error")); 197 | } 198 | }) 199 | }) 200 | }) 201 | 202 | describe("Valid attributes",function(){ 203 | it("should not throw",function(done){ 204 | valid_attrs.validate(function(err,doc){ 205 | if(err){ 206 | done(err); 207 | }else{ 208 | done(); 209 | } 210 | }) 211 | }) 212 | }) 213 | 214 | describe("Invalid document",function(){ 215 | it("should not validate",function(done){ 216 | invalid_doc.validate(function(err,doc){ 217 | if(err){ 218 | done(); 219 | }else{ 220 | done(new Error("Failed to throw an error")); 221 | } 222 | }) 223 | }) 224 | }) 225 | 226 | describe("Valid document",function(){ 227 | it("should not throw",function(done){ 228 | valid_doc.validate(function(err,doc){ 229 | if(err){ 230 | done(err); 231 | }else{ 232 | done(); 233 | } 234 | }) 235 | }) 236 | }) 237 | 238 | 239 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // -------------------------------------------------------------------------------- 3 | // Programmer: Jason Thorpe 4 | // Language: typescript 5 | // Purpose: mocha testing 6 | // Comments: 7 | // -------------------------------------------------------------------------------- 8 | Object.defineProperty(exports, "__esModule", { value: true }); 9 | /// 10 | /// 11 | /// 12 | var chai = require("chai"); 13 | var expect = chai.expect; 14 | var mongoose = require('mongoose'); 15 | mongoose.Promise = require('bluebird'); 16 | var Schema = mongoose.Schema; 17 | var ajv_plugin = require('../index'); 18 | mongoose.plugin(ajv_plugin); 19 | var AJV = require('ajv'), ajv = new AJV(); 20 | ajv.validate({ "type": "string", "format": "ipv4" }, "123.4.5.789"); 21 | //------------------------------ 22 | // define an AJV JSONschema: 23 | //------------------------------ 24 | var contact_json_schema = { 25 | "type": "object", 26 | "properties": { 27 | "name": { 28 | "type": "string" 29 | }, 30 | "email": { 31 | "type": "string", 32 | "fomrat": "email" 33 | }, 34 | "birthday": { 35 | "oneOf": [ 36 | { "$ref": "#/definitions/date" }, 37 | { "$ref": "#/definitions/date-time" } 38 | ] 39 | } 40 | }, 41 | "required": [ 42 | "name", 43 | "email" 44 | ], 45 | "definitions": { 46 | "date": { 47 | "type": "string", 48 | "format": "date" 49 | }, 50 | "date-time": { 51 | "type": "string", 52 | "format": "date-time" 53 | } 54 | } 55 | }; 56 | var team_json_schema = { 57 | "type": "object", 58 | "properties": { 59 | "team_name": { 60 | "type": "string", 61 | // team names must be between 5 and 30 characters 62 | "minLength": 5, 63 | "maxLength": 30 64 | }, 65 | "players": { 66 | "type": "array", 67 | "minItems": 2, 68 | "maxItems": 10, 69 | "items": { 70 | "type": "string" 71 | } 72 | } 73 | } 74 | }; 75 | describe("Schemas", function () { 76 | it("should be valid", function () { 77 | expect(ajv.validateSchema(contact_json_schema)).to.be.true; 78 | expect(ajv.validateSchema(team_json_schema)).to.be.true; 79 | }); 80 | }); 81 | // ---------------------------------------- 82 | // build the models 83 | // ---------------------------------------- 84 | // use AJV to validate fields within a document 85 | var Player_schema = new Schema({ 86 | "user_name": String, 87 | "rank": Number, 88 | "ip_address": { 89 | "type": String, 90 | "ajv-schema": { 91 | "type": 'string', 92 | "format": 'ipv4' // AJV convenience String Format 93 | } 94 | }, 95 | "contact": { 96 | "type": Schema.Types.Mixed, 97 | "ajv-schema": contact_json_schema // use AJV to validate this object 98 | }, 99 | }); 100 | // add the AJV plugin to the schema 101 | //-- Player_schema.plugin(ajv_plugin); 102 | // Create a model from the schema 103 | var Player = mongoose.model('Player', Player_schema); 104 | var Team_schema = new Schema({ 105 | "team_name": String, 106 | "players": [String], 107 | "ajv-schema": team_json_schema, 108 | }); 109 | //-- Team_schema.plugin(ajv_plugin); 110 | var Team = mongoose.model('Team', Team_schema); 111 | // ---------------------------------------- 112 | // build the model instances 113 | // ---------------------------------------- 114 | var valid_attrs = new Player({ 115 | "user_name": "Felix", 116 | "rank": 5, 117 | "ip_address": "123.45.67.89", 118 | "contact": { 119 | "name": "Jack", 120 | "email": "plaza626@email.com", 121 | "birthday": "1925-02-08" 122 | } 123 | }); 124 | var invalid_string_attribute = new Player({ 125 | "user_name": "Oscar", 126 | "rank": 7, 127 | "ip_address": "123.4.5.678", 128 | "contact": { 129 | "name": "Walter", 130 | "email": "RedWingsFan@poker.com", 131 | "birthday": "1920-10-01" // invalid date format format 132 | }, 133 | }); 134 | var invalid_object_attribute = new Player({ 135 | "user_name": "Oscar", 136 | "rank": 7, 137 | "ip_address": "123.45.67.89", 138 | "contact": { 139 | "name": "Walter", 140 | "email": "RedWingsFan@poker.com", 141 | "birthday": "October 1, 1920" // invalid date format format 142 | }, 143 | }); 144 | var invalid_doc = new Team({ 145 | "team_name": "Just Me", 146 | "players": ["Bridget"] // too few players 147 | }); 148 | var valid_doc = new Team({ 149 | "team_name": "ThursdayNightPoker", 150 | "players": ["Oscar", "Felix", "Speed", "Vinnie", "Roy", "Murray"] 151 | }); 152 | // ---------------------------------------- 153 | // testing 154 | // ---------------------------------------- 155 | describe("Invalid nested object", function () { 156 | it("should not validate", function (done) { 157 | invalid_object_attribute.validate(function (err, doc) { 158 | if (err) { 159 | done(); 160 | } 161 | else { 162 | done(new Error("Failed to throw an error")); 163 | } 164 | }); 165 | }); 166 | }); 167 | describe("Invalid string attribute", function () { 168 | it("should not validate", function (done) { 169 | invalid_string_attribute.validate(function (err, doc) { 170 | if (err) { 171 | done(); 172 | } 173 | else { 174 | done(new Error("Failed to throw an error")); 175 | } 176 | }); 177 | }); 178 | }); 179 | describe("Valid attributes", function () { 180 | it("should not throw", function (done) { 181 | valid_attrs.validate(function (err, doc) { 182 | if (err) { 183 | done(err); 184 | } 185 | else { 186 | done(); 187 | } 188 | }); 189 | }); 190 | }); 191 | describe("Invalid document", function () { 192 | it("should not validate", function (done) { 193 | invalid_doc.validate(function (err, doc) { 194 | if (err) { 195 | done(); 196 | } 197 | else { 198 | done(new Error("Failed to throw an error")); 199 | } 200 | }); 201 | }); 202 | }); 203 | describe("Valid document", function () { 204 | it("should not throw", function (done) { 205 | valid_doc.validate(function (err, doc) { 206 | if (err) { 207 | done(err); 208 | } 209 | else { 210 | done(); 211 | } 212 | }); 213 | }); 214 | }); 215 | --------------------------------------------------------------------------------