├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── bower.json ├── dist └── schemaobject.js ├── gulpfile.babel.js ├── lib └── schemaobject.js ├── package-lock.json ├── package.json ├── test ├── browser.html ├── profile.js └── tests.js └── typescript └── schemaobject.d.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-class-properties", 4 | ["babel-plugin-transform-builtin-extend", { 5 | "globals": ["Error", "Array"] 6 | }] 7 | ], 8 | "presets": ["es2015"] 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | pids 10 | logs 11 | results 12 | npm-debug.log 13 | node_modules/ 14 | .idea/ 15 | bower_components/ 16 | !bower.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | 16 | node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "5" 5 | - "4" -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 scotthovestadt 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | UNMAINTAINED 2 | ================== 3 | 4 | Schema Object [![Build Status](https://travis-ci.org/scotthovestadt/schema-object.png?branch=master)](https://travis-ci.org/scotthovestadt/schema-object) 5 | ================== 6 | 7 | Designed to enforce schema on Javascript objects. Allows you to specify type, transformation and validation of values via a set of attributes. Support for sub-schemas included. 8 | 9 | ``` 10 | npm install schema-object 11 | ``` 12 | ``` 13 | bower install schema-object 14 | ``` 15 | 16 | **TypeScript typings included. Node and browser environments supported.** 17 | 18 | For older versions of Node, run node with the harmony proxies ````--harmony_proxies```` flag. This flag is on by default in newer NodeJS versions. 19 | 20 | 21 | # Very basic usage example 22 | ```js 23 | var SchemaObject = require('schema-object'); 24 | 25 | // Create User schema 26 | var User = new SchemaObject({ 27 | firstName: String, 28 | lastName: String, 29 | birthDate: Date 30 | }); 31 | 32 | // Initialize instance of user 33 | var user = new User({firstName: 'Scott', lastName: 'Hovestadt', birthDate: 'June 21, 1988'}); 34 | console.log(user); 35 | 36 | // Prints: 37 | { firstName: 'Scott', 38 | lastName: 'Hovestadt', 39 | birthDate: Tue Jun 21 1988 00:00:00 GMT-0700 (PDT) } 40 | ``` 41 | 42 | # Advanced example 43 | ```js 44 | var SchemaObject = require('schema-object'); 45 | 46 | // Create custom basic type 47 | // Type can be extended with more properties when defined 48 | var NotEmptyString = {type: String, minLength: 1}; 49 | 50 | // Create sub-schema for user's Company 51 | var Company = new SchemaObject({ 52 | // Any string will be magically parsed into date 53 | startDate: Date, 54 | endDate: Date, 55 | 56 | // String with properties 57 | name: NotEmptyString, 58 | 59 | // Typed array 60 | tags: [NotEmptyString] 61 | }); 62 | 63 | // Create User schema 64 | var User = new SchemaObject({ 65 | // Basic user information using properties 66 | firstName: NotEmptyString, 67 | lastName: NotEmptyString, 68 | 69 | // Extend "NotEmptyString" with enum property 70 | gender: {type: NotEmptyString, enum: ['m', 'f']}, 71 | 72 | // Sub-object with enforced type 73 | work: Company 74 | }, { 75 | // Add methods to User prototype 76 | methods: { 77 | getDisplayName: function() { 78 | return this.firstName + ' ' + this.lastName; 79 | } 80 | } 81 | }); 82 | 83 | // Create Account schema by extending User schema 84 | var Account = User.extend({ 85 | // Add username to schema 86 | username: NotEmptyString, 87 | 88 | // Special behavior will transform password to hash if necessary 89 | // https://www.npmjs.com/package/password-hash 90 | password: {type: String, stringTransform: function(string) { 91 | if(!passwordHash.isHashed(string)) { 92 | string = passwordHash.generate(string); 93 | } 94 | return string; 95 | }} 96 | }, { 97 | methods: { 98 | getDisplayName: function() { 99 | // If available, use username as display name 100 | // Otherwise fall back to first name and last name 101 | return this.username || this.super(); 102 | } 103 | } 104 | }); 105 | 106 | // Initialize a new instance of the User with a value 107 | var account = new Account({ 108 | username: 'scotthovestadt', 109 | password: 'hunter2', 110 | firstName: 'Scott', 111 | lastName: 'Hovestadt', 112 | gender: 'm', 113 | work: { 114 | name: 'My Company', 115 | startDate: 'June 1, 2010' 116 | } 117 | }); 118 | 119 | console.log(account.getDisplayName()); 120 | 121 | // Prints: 122 | "scotthovestadt" 123 | 124 | console.log(account); 125 | 126 | // Prints: 127 | { firstName: 'Scott', 128 | lastName: 'Hovestadt', 129 | gender: 'm', 130 | work: 131 | { startDate: Tue Jun 01 2010 00:00:00 GMT-0700 (PDT), 132 | name: 'My Company' }, 133 | username: 'scotthovestadt' } 134 | ``` 135 | 136 | # Static Methods 137 | 138 | ## extend 139 | 140 | Allows you to extend SchemaObject instance schema and options. 141 | 142 | ```js 143 | var Person = new SchemaObject({ 144 | firstName: String, 145 | lastName: String 146 | }, { 147 | constructors: { 148 | fromFullName: function(fullName) { 149 | fullName = fullName.split(' '); 150 | this.firstName = fullName[0]; 151 | this.lastName = fullName[1]; 152 | } 153 | }, 154 | methods: { 155 | getDisplayName: function() { 156 | return this.firstName + ' ' + this.lastName; 157 | } 158 | } 159 | }); 160 | 161 | var Employee = Person.extend({ 162 | id: Number 163 | }, { 164 | methods: { 165 | getDisplayName: function() { 166 | return '[Employee ID ' + this.id + '] ' + this.super(); 167 | } 168 | } 169 | }); 170 | 171 | var john = Employee.fromFullName('John Smith'); 172 | john.id = 1; 173 | 174 | console.log(john.getDisplayName()); 175 | 176 | // Prints: 177 | "[Employee ID 1] John Smith" 178 | ``` 179 | 180 | 181 | # Methods 182 | 183 | ## clone 184 | 185 | Clones SchemaObject and all sub-objects and sub-arrays into another SchemaObject container. Writes on any sub-objects or sub-arrays will not touch the original. 186 | ```js 187 | var User = new SchemaObject({ 188 | firstName: String, 189 | lastName: String 190 | }); 191 | 192 | var user = new User({firstName: 'Scott', lastName: 'Hovestadt'}); 193 | 194 | var anotherUser = user.clone(); 195 | anotherUser.firstName = 'John'; 196 | anotherUser.lastName = 'Smith'; 197 | 198 | console.log(user); 199 | console.log(anotherUser); 200 | 201 | 202 | // Prints: 203 | { firstName: 'Scott', 204 | lastName: 'Hovestadt' } 205 | { firstName: 'John', 206 | lastName: 'Smith' } 207 | ``` 208 | 209 | ## toObject 210 | 211 | toObject returns a cloned primitive object, stripped of all magic. Writes on any sub-objects or sub-arrays will not touch the original. All values will be typecasted and transformed, but future writes to the primitive object will not. The [invisible attribute](https://github.com/scotthovestadt/schema-object#invisible) can be used to ensure an index stored on the SchemaObject will not be written to the primitive object. toObject is automatically called if a SchemaObject is passed to JSON.stringify. 212 | ```js 213 | var User = new SchemaObject({ 214 | firstName: String, 215 | lastName: String, 216 | birthDate: Date 217 | }); 218 | 219 | var user = new User({firstName: 'Scott', lastName: 'Hovestadt', birthDate: 'June 21, 1988'}); 220 | console.log(user); 221 | 222 | // Prints: 223 | { firstName: 'Scott', 224 | lastName: 'Hovestadt', 225 | birthDate: Tue Jun 21 1988 00:00:00 GMT-0700 (PDT) } 226 | ``` 227 | 228 | ## populate 229 | 230 | populate will copy an object's values. 231 | ```js 232 | var User = new SchemaObject({ 233 | firstName: String, 234 | lastName: String 235 | }); 236 | 237 | var user = new User(); 238 | user.populate({firstName: 'Scott', lastName: 'Hovestadt'}); 239 | console.log(user); 240 | 241 | // Prints: 242 | { firstName: 'Scott', 243 | lastName: 'Hovestadt' } 244 | ``` 245 | 246 | ## clear 247 | 248 | clear removes all values. 249 | ```js 250 | var User = new SchemaObject({ 251 | firstName: String, 252 | lastName: String 253 | }); 254 | 255 | var user = new User({firstName: 'Scott', lastName: 'Hovestadt'}); 256 | console.log(user); 257 | 258 | // Prints: 259 | { firstName: 'Scott', 260 | lastName: 'Hovestadt' } 261 | 262 | user.clear(); 263 | console.log(user); 264 | 265 | // Prints: 266 | { firstName: undefined, 267 | lastName: undefined } 268 | ``` 269 | 270 | ## isErrors / getErrors / clearErrors 271 | 272 | See documentation on [Errors](https://github.com/scotthovestadt/schema-object#errors). 273 | 274 | 275 | # Options 276 | 277 | When you create the SchemaObject, you may pass a set of options as a second argument. These options allow you to fine-tune the behavior of your objects for specific needs. 278 | 279 | ## constructors 280 | 281 | The constructors option allows you to override the default or attach new constructors to your SchemaObject-created class. 282 | 283 | ```js 284 | var Person = new SchemaObject({ 285 | firstName: String, 286 | lastName: String 287 | }, { 288 | constructors: { 289 | // Override default constructor 290 | default: function(values) { 291 | // Will call this.populate 292 | this.super(values); 293 | 294 | // Populate default values with custom constructor 295 | if(this.firstName === undefined) { 296 | this.firstName = 'John'; 297 | } 298 | if(this.lastName === undefined) { 299 | this.lastName = 'Smith'; 300 | } 301 | }, 302 | 303 | // Create new constructor used by calling Person.fromFullName 304 | fromFullName: function(fullName) { 305 | // Will call default constructor 306 | this.super(); 307 | 308 | fullName = fullName.split(' '); 309 | if(fullName[0]) { 310 | this.firstName = fullName[0]; 311 | } 312 | if(fullName[1]) { 313 | this.lastName = fullName[1]; 314 | } 315 | } 316 | } 317 | }); 318 | 319 | var person = new Person({ firstName: 'Scott' }); 320 | // OR 321 | var person = Person.fromFullName('Scott'); 322 | 323 | console.log(person); 324 | 325 | // Prints: 326 | { firstName: 'Scott', 327 | lastName: 'Smith' } 328 | ``` 329 | 330 | ## methods 331 | 332 | The methods option allows you to attach new methods to your SchemaObject-created class. 333 | 334 | ```js 335 | var Person = new SchemaObject({ 336 | firstName: String, 337 | lastName: String 338 | }, { 339 | methods: { 340 | getFullName: function() { 341 | return this.firstName + ' ' + this.lastName; 342 | } 343 | } 344 | }); 345 | 346 | var person = new Person({ firstName: 'Scott', lastName: 'Hovestadt' }); 347 | console.log(person.getFullName()); 348 | 349 | // Prints: 350 | { 'Scott Hovestadt' } 351 | ``` 352 | 353 | ## toObject(object) 354 | 355 | toObject allows you to transform the model before the result of toObject() is passed back. 356 | 357 | This example shows how it could be used to ensure transform all strings to uppercase. 358 | ```js 359 | var Model = new SchemaObject({ 360 | string: String 361 | }, { 362 | toObject: function(object) { 363 | _.each(object, function(value, key) { 364 | if(_.isString(value)) { 365 | object[key] = value.toUpperCase(); 366 | } 367 | }); 368 | return object; 369 | } 370 | }); 371 | 372 | var model = new Model(); 373 | model.string = 'a string'; 374 | console.log(model.string); 375 | 376 | // Prints: 377 | { 'a string' } 378 | 379 | var simpleObject = model.toObject(); 380 | console.log(simpleObject.string); 381 | 382 | // Prints: 383 | { 'A STRING' } 384 | ``` 385 | 386 | ## inheritRootThis 387 | 388 | inheritRootThis (default: false) should be set to true if you want your nested SchemaObjects to have the "this" context of the root SchemaObject. SchemaObjects created with the shorthand syntax are considered a part of the parent object and have this enabled automatically. 389 | 390 | 391 | ## setUndefined 392 | 393 | setUndefined (default: false) allows you to specify if an unset value is written when toObject() is called. By default, the behavior is not to write unset values. This means if there is a null/undefined primitive, an empty array, or an empty object it will not be written to the object when toObject() is called. 394 | 395 | This value should set to true if: 396 | - You want your database (Mongo, etc) to write unset indexes and overwrite existing fields with empty values. 397 | - You want to write undefined values when exporting to JSON explicitly. 398 | - You want toObject() to contain empty arrays and objects. 399 | 400 | ## preserveNull 401 | 402 | preserveNull (default: false) allows you to set `null` to any field. The default behavior will treat null as unsetting the field. 403 | 404 | This value should set to true if you're intentionally using null and know the difference between null and undefined. 405 | 406 | ## strict 407 | 408 | strict (default: true) allows you to specify what happens when an index is set on your SchemaObject that does not exist in the schema. If strict mode is on, the index will be ignored. If strict mode is off, the index will automatically be created in the schema when it's set with type "any". 409 | 410 | With strict mode on (default): 411 | ```js 412 | var Profile = new SchemaObject({ 413 | id: String 414 | }, { 415 | strict: true 416 | }); 417 | 418 | var profile = new Profile(); 419 | profile.id = 'abc123'; 420 | profile.customField = 'hello'; 421 | 422 | // Prints: 423 | { id: 'abc123' } 424 | ``` 425 | 426 | With strict mode off: 427 | ```js 428 | var Profile = new SchemaObject({ 429 | id: String 430 | }, { 431 | strict: false 432 | }); 433 | 434 | var profile = new Profile(); 435 | profile.id = 'abc123'; 436 | profile.customField = 'hello'; 437 | 438 | // Prints: 439 | { id: 'abc123', customField: 'hello' } 440 | ``` 441 | 442 | ## dotNotation 443 | 444 | dotNotation (default: false) allows you to access deep fields in child objects using dot notation. If dot notation is on, getting or setting "profile.name" will look inside the object for a child object "profile" and then for key "name", instead of simply setting the index "profile.name" on the parent object. 445 | 446 | The following example turns off strict mode to demonstrate the differences when toggling dot notation on or off, although dot notation can be used with or without strict mode. 447 | 448 | With dot notation off (default): 449 | ```js 450 | var User = new SchemaObject({ 451 | }, { 452 | dotNotation: false, 453 | strict: false 454 | }); 455 | 456 | var user = new User(); 457 | user['profile.name'] = 'Scott'; 458 | 459 | // Prints: 460 | { 'profile.name': 'Scott' } 461 | ``` 462 | 463 | With dot notation on: 464 | ```js 465 | var User = new SchemaObject({ 466 | }, { 467 | dotNotation: true, 468 | strict: false 469 | }); 470 | 471 | var user = new User(); 472 | user['profile.name'] = 'Scott'; 473 | 474 | // Prints: 475 | { profile: { name: 'Scott' } } 476 | ``` 477 | 478 | ## keysIgnoreCase 479 | 480 | keysIgnoreCase (default: false) allows you to set indexes without worrying about the casing of the key. 481 | 482 | With keys ignore case off (default): 483 | ```js 484 | var User = new SchemaObject({ 485 | firstName: String 486 | }, { 487 | keysIgnoreCase: false 488 | }); 489 | 490 | var user = new User(); 491 | user.firstname = 'Scott'; 492 | 493 | // Prints: 494 | {} 495 | ``` 496 | 497 | With keys ignore case on: 498 | ```js 499 | var User = new SchemaObject({ 500 | firstName: String 501 | }, { 502 | keysIgnoreCase: true 503 | }); 504 | 505 | var user = new User(); 506 | user.firstname = 'Scott'; 507 | 508 | // Prints: 509 | { firstName: 'Scott' } 510 | ``` 511 | 512 | 513 | ## onBeforeValueSet(value, key) / onValueSet(value, key) 514 | 515 | onBeforeValueSet / onValueSet allow you to bind an event handler to all write operations on an object. Currently, it will only notify of write operations on the object itself and will not notify you when child objects are written to. If you return false or throw an error within the onBeforeValueSet handler, the write operation will be cancelled. Throwing an error will add the error to the error stack. 516 | ```js 517 | var User = new SchemaObject({ 518 | name: String 519 | }, { 520 | onBeforeValueSet: function(value, key) { 521 | if(key === 'name' && value === 'Scott') { 522 | return false; 523 | } 524 | } 525 | }); 526 | 527 | var user = new User(); 528 | 529 | user.name = 'Scott'; 530 | // Prints: 531 | { name: undefined } 532 | 533 | user.name = 'Scott Hovestadt'; 534 | // Prints: 535 | { name: 'Scott Hovestadt' } 536 | ``` 537 | 538 | ## allowFalsyValues 539 | 540 | allowFalsyValues (default: true) allows you to specify what happens when an index is required by the schema, but a falsy value is provided. If allowFalsyValues is true, all falsy values, such as empty strings are ignored. If false, falsy values other than booleans will result in an error. 541 | 542 | With allowFalsyValues mode on (default): 543 | ```js 544 | var Profile = new SchemaObject({ 545 | id: { 546 | type: String, 547 | required: true 548 | }, { 549 | allowFalsyValues: true 550 | }); 551 | 552 | var profile = new Profile(); 553 | profile.id = ''; 554 | 555 | console.log(profile.getErrors()); 556 | // Prints: 557 | [] 558 | ``` 559 | 560 | With allowFalsyValues mode off: 561 | ```js 562 | var Profile = new SchemaObject({ 563 | id: { 564 | type: String, 565 | required: true 566 | }, { 567 | allowFalsyValues: false 568 | }); 569 | 570 | var profile = new Profile(); 571 | profile.id = ''; 572 | 573 | console.log(profile.getErrors()); 574 | // Prints: 575 | [ SetterError { 576 | errorMessage: 'id is required but not provided' 577 | ... 578 | ``` 579 | 580 | ## useDecimalNumberGroupSeparator 581 | useDecimalNumberGroupSeparator (default: false) defines the digit group separator used for parsing numbers. When left false, numbers are expected to use `,` as a digit separator. For example 3,043,201.01. However when this options is enabled it swaps commas and decimals to allow parsing numbers like 3.043.201,01. This is to allow for usability in countries which use this format instead. 582 | 583 | With useDecimalNumberGroupSeparator mode off (default): 584 | ```js 585 | var Profile = new SchemaObject({ 586 | id: String, 587 | followers: Number 588 | }); 589 | var profile = new Profile({ followers: '124.423.123,87'}); 590 | console.log(profile.followers); //undefined 591 | 592 | profile = new Profile({ followers: '124,423,123.87'}); 593 | console.log(profile.followers); //124423123.87 594 | ``` 595 | 596 | With useDecimalNumberGroupSeparator mode on: 597 | ```js 598 | var Profile = new SchemaObject({ 599 | id: String, 600 | followers: Number 601 | }, { 602 | useDecimalNumberGroupSeparator: true 603 | }); 604 | var profile = new Profile({ followers: '124.423.123,87'}); 605 | console.log(profile.followers); //124423123.87 606 | 607 | profile = new Profile({ followers: '124,423,123.87'}); 608 | console.log(profile.followers); //undefined 609 | ``` 610 | 611 | # Errors 612 | 613 | When setting a value fails, an error is generated silently. Errors can be retrieved with getErrors() and cleared with clearErrors(). 614 | 615 | ```js 616 | var Profile = new SchemaObject({ 617 | id: {type: String, minLength: 5} 618 | }); 619 | 620 | var profile = new Profile(); 621 | profile.id = '1234'; 622 | 623 | console.log(profile.isErrors()); 624 | 625 | // Prints: 626 | true 627 | 628 | console.log(profile.getErrors()); 629 | 630 | // Prints: 631 | [ { errorMessage: 'String length too short to meet minLength requirement.', 632 | setValue: '1234', 633 | originalValue: undefined, 634 | fieldSchema: { name: 'id', type: 'string', minLength: 5 } } ] 635 | 636 | // Clear all errors. 637 | profile.clearErrors(); 638 | ``` 639 | ## Error codes 640 | 641 | Each error type has an error code. Here is a map of them, however this may be out of date if someone adds an error without updating this list. 642 | 643 | * [1000] SetterError 644 | * [1100] CastError 645 | * [1101] StringCastError 646 | * [1102] NumberCastError 647 | * [1103] ArrayCastError 648 | * [1104] ObjectCastError 649 | * [1105] DateCastError 650 | * [1200] ValidationError 651 | * [1210] StringValidationError 652 | * [1211] StringEnumValidationError 653 | * [1212] StringMinLengthValidationError 654 | * [1213] StringMaxLengthValidationError 655 | * [1214] StringRegexValidationError 656 | * [1220] NumberValidationError 657 | * [1221] NumberMinValidationError 658 | * [1222] NumberMaxValidationError 659 | * [1230] DateValidationError 660 | * [1231] DateParseValidationError 661 | 662 | ## Custom Errors 663 | 664 | You can also set custom errors for all validators. There are currently two supported formats for this. 665 | 666 | ### Array Error Format 667 | 668 | The array format expects the validator value as the first argument, and the error message as the second argument. 669 | Here is an example: 670 | ```js 671 | var Profile = new SchemaObject({ 672 | id: { 673 | type: String, 674 | minLength: [5, 'id length must be longer than 5 characters'] 675 | } 676 | }); 677 | ``` 678 | 679 | ### Object Error Format 680 | 681 | The object format expects an object with two keys, `value` is the validator value, and `errorMessage` is the custom error message. 682 | Here is an example: 683 | ```js 684 | var Profile = new SchemaObject({ 685 | id: { 686 | type: String, 687 | minLength: { 688 | value: 5, 689 | errorMessage: 'id length must be longer than 5 characters' 690 | } 691 | } 692 | }); 693 | ``` 694 | 695 | Both of these formats can be used interchangeably. 696 | 697 | # Types 698 | 699 | Supported types: 700 | - String 701 | - Number 702 | - Boolean 703 | - Date 704 | - Array (including types within Array) 705 | - Object (including typed SchemaObjects for sub-schemas) 706 | - 'alias' 707 | - 'any' 708 | 709 | When a type is specified, it will be enforced. Typecasting is enforced on all types except 'any'. If a value cannot be typecasted to the correct type, the original value will remain untouched. 710 | 711 | Types can be extended with a variety of attributes. Some attributes are type-specific and some apply to all types. 712 | 713 | Custom types can be created by defining an object with type properties. 714 | ```js 715 | var NotEmptyString = {type: String, minLength: 1}; 716 | country: {type: NotEmptyString, default: 'USA'} 717 | ``` 718 | 719 | ## General attributes 720 | 721 | ### transform 722 | Called immediately when value is set and before any typecast is done. 723 | ```js 724 | name: {type: String, transform: function(value) { 725 | // Modify the value here... 726 | return value; 727 | }} 728 | ``` 729 | 730 | ### default 731 | Provide default value. You may pass value directly or pass a function which will be executed when the object is initialized. The function is executed in the context of the object and can use "this" to access other properties (which . 732 | ```js 733 | country: {type: String, default: 'USA'} 734 | ``` 735 | 736 | ### getter 737 | Provide function to transform value when retrieved. Executed in the context of the object and can use "this" to access properties. 738 | ```js 739 | string: {type: String, getter: function(value) { return value.toUpperCase(); }} 740 | ``` 741 | 742 | ### required 743 | If true, a value must be provided. If a value is not provided, an error will be generated silently. If used in conjunction with default, this check will always pass. 744 | ```js 745 | fullName: {type: String, required: true} 746 | ``` 747 | Required can also be a function, you can use 'this' to reference the current object instance. Required will be based on what boolean value the function returns. 748 | ```js 749 | age: { 750 | type: Number, 751 | required: true 752 | }, 753 | employer: { 754 | type: String, 755 | required: function() { 756 | return this.age > 18; 757 | } 758 | } 759 | ``` 760 | You can also override the default error message for required fields by using an array and providing a string for the second value. 761 | ```js 762 | age: { 763 | type: Number, 764 | required: [ 765 | true, 766 | 'You must provide the age of this user' 767 | ] 768 | }, 769 | employer: { 770 | type: String, 771 | required: [ 772 | function() { 773 | return this.age > 18; 774 | }, 775 | 'An employer is required for all users over the age of 18' 776 | ] 777 | } 778 | ``` 779 | ### readOnly 780 | If true, the value can be read but cannot be written to. This can be useful for creating fields that reflect other values. 781 | ```js 782 | fullName: {type: String, readOnly: true, default: function(value) { 783 | return (this.firstName + ' ' + this.lastName).trim(); 784 | }} 785 | ``` 786 | 787 | ### invisible 788 | If true, the value can be written to but isn't outputted as an index when toObject() is called. This can be useful for creating aliases that redirect to other indexes but aren't actually present on the object. 789 | ```js 790 | zip: String, 791 | postalCode: {type: 'alias', invisible: true, index: 'zip'} 792 | // this.postalCode = 12345 -> this.toObject() -> {zip: '12345'} 793 | ``` 794 | 795 | 796 | ## String 797 | 798 | ### stringTransform 799 | Called after value is typecast to string **if** value was successfully typecast but called before all validation. 800 | ```js 801 | postalCode: {type: String, stringTransform: function(string) { 802 | // Type will ALWAYS be String, so using string prototype is OK. 803 | return string.toUpperCase(); 804 | }} 805 | ``` 806 | 807 | ### regex 808 | Validates string against Regular Expression. If string doesn't match, it's rejected. 809 | ```js 810 | memberCode: {type: String, regex: new RegExp('^([0-9A-Z]{4})$')} 811 | ``` 812 | 813 | ### enum 814 | Validates string against array of strings. If not present, it's rejected. 815 | ```js 816 | gender: {type: String, enum: ['m', 'f']} 817 | ``` 818 | 819 | ### minLength 820 | Enforces minimum string length. 821 | ```js 822 | notEmpty: {type: String, minLength: 1} 823 | ``` 824 | 825 | ### maxLength 826 | Enforces maximum string length. 827 | ```js 828 | stateAbbrev: {type: String, maxLength: 2} 829 | ``` 830 | 831 | ### clip 832 | If true, clips string to maximum string length instead of rejecting string. 833 | ```js 834 | bio: {type: String, maxLength: 255, clip: true} 835 | ``` 836 | 837 | 838 | ## Number 839 | 840 | ### min 841 | Number must be > min attribute or it's rejected. 842 | ```js 843 | positive: {type: Number, min: 0} 844 | ``` 845 | 846 | ### max 847 | Number must be < max attribute or it's rejected. 848 | ```js 849 | negative: {type: Number, max: 0} 850 | ``` 851 | 852 | 853 | ## Array 854 | 855 | ### arrayType 856 | Elements within the array will be typed to the attributes defined. 857 | ```js 858 | aliases: {type: Array, arrayType: {type: String, minLength: 1}} 859 | ``` 860 | 861 | An alternative shorthand version is also available -- wrap the properties within array brackets. 862 | ```js 863 | aliases: [{type: String, minLength: 1}] 864 | ``` 865 | 866 | ### unique 867 | Ensures duplicate-free array, using === to test object equality. 868 | ```js 869 | emails: {type: Array, unique: true, arrayType: String} 870 | ``` 871 | 872 | ### filter 873 | Reject any values where filter callback does not return truthy. 874 | ```js 875 | emails: {type: Array, arrayType: Person, filter: (person) => person.gender !== 'f'} 876 | ``` 877 | 878 | 879 | ## Object 880 | ### objectType 881 | Allows you to define a typed object. 882 | ```js 883 | company: {type: Object, objectType: { 884 | name: String 885 | }} 886 | ``` 887 | 888 | An alternative shorthand version is also available -- simply pass an instance of SchemaObject or a schema. 889 | ```js 890 | company: { 891 | name: String 892 | } 893 | ``` 894 | 895 | 896 | ## Alias 897 | 898 | ### index (required) 899 | The index key of the property being aliased. 900 | ```js 901 | zip: String, 902 | postalCode: {type: 'alias', alias: 'zip'} 903 | // this.postalCode = 12345 -> this.toObject() -> {zip: '12345'} 904 | ``` 905 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "schema-object", 3 | "version": "4.0.0", 4 | "homepage": "https://github.com/scotthovestadt/schema-object", 5 | "authors": [ 6 | "Scott Hovestadt " 7 | ], 8 | "description": "Enforce schema on JavaScript objects, including type, transformation, and validation. Supports extends, sub-schemas, and arrays.", 9 | "main": "dist/schemaobject.js", 10 | "moduleType": [ 11 | "globals" 12 | ], 13 | "keywords": [ 14 | "javascript", 15 | "schema", 16 | "extends", 17 | "type", 18 | "transformation", 19 | "validation", 20 | "object", 21 | "array" 22 | ], 23 | "license": "BSD", 24 | "ignore": [ 25 | "**/.*", 26 | "node_modules", 27 | "bower_components", 28 | "test", 29 | "lib" 30 | ], 31 | "dependencies": { 32 | "harmony-reflect": "1.4.6", 33 | "lodash": "4.11.1" 34 | } 35 | } -------------------------------------------------------------------------------- /dist/schemaobject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 4 | 5 | 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; }; }(); 6 | 7 | function _extendableBuiltin(cls) { 8 | function ExtendableBuiltin() { 9 | var instance = Reflect.construct(cls, Array.from(arguments)); 10 | Object.setPrototypeOf(instance, Object.getPrototypeOf(this)); 11 | return instance; 12 | } 13 | 14 | ExtendableBuiltin.prototype = Object.create(cls.prototype, { 15 | constructor: { 16 | value: cls, 17 | enumerable: false, 18 | writable: true, 19 | configurable: true 20 | } 21 | }); 22 | 23 | if (Object.setPrototypeOf) { 24 | Object.setPrototypeOf(ExtendableBuiltin, cls); 25 | } else { 26 | ExtendableBuiltin.__proto__ = cls; 27 | } 28 | 29 | return ExtendableBuiltin; 30 | } 31 | 32 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 33 | 34 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 35 | 36 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 37 | 38 | (function (_) { 39 | 'use strict'; 40 | 41 | var _isProxySupported = typeof Proxy !== 'undefined' && Proxy.toString().indexOf('proxies not supported on this platform') === -1; 42 | 43 | // Use require conditionally, otherwise assume global dependencies. 44 | if (typeof require !== 'undefined') { 45 | _ = require('lodash'); 46 | 47 | if (!global._babelPolyfill) { 48 | // Necessary to do this instead of runtime transformer for browser compatibility. 49 | require('babel-polyfill'); 50 | } 51 | 52 | // Patch the harmony-era (pre-ES6) Proxy object to be up-to-date with the ES6 spec. 53 | // Without the --harmony and --harmony_proxies flags, options strict: false and dotNotation: true will fail with exception. 54 | if (_isProxySupported === true) { 55 | require('harmony-reflect'); 56 | } 57 | } else { 58 | _ = window._; 59 | } 60 | 61 | // If reflection is being used, our traps will hide internal properties. 62 | // If reflection is not being used, Symbol will hide internal properties. 63 | var _privateKey = _isProxySupported === true ? '_private' : Symbol('_private'); 64 | 65 | // Reserved fields, map to internal property. 66 | var _reservedFields = ['super']; 67 | 68 | // Is a number (ignores type). 69 | function isNumeric(n) { 70 | return !isNaN(parseFloat(n)) && isFinite(n); 71 | } 72 | 73 | // Used to get real index name. 74 | function getIndex(index) { 75 | if (this[_privateKey]._options.keysIgnoreCase && typeof index === 'string') { 76 | var indexLowerCase = index.toLowerCase(); 77 | for (var key in this[_privateKey]._schema) { 78 | if (typeof key === 'string' && key.toLowerCase() === indexLowerCase) { 79 | return key; 80 | } 81 | } 82 | } 83 | 84 | return index; 85 | } 86 | 87 | // Used to fetch current values. 88 | function getter(value, properties) { 89 | // Most calculations happen within the typecast and the value passed is typically the value we want to use. 90 | // Typically, the getter just returns the value. 91 | // Modifications to the value within the getter are not written to the object. 92 | 93 | // Getter can transform value after typecast. 94 | if (properties.getter) { 95 | value = properties.getter.call(this[_privateKey]._root, value); 96 | } 97 | 98 | return value; 99 | } 100 | 101 | // Used to write value to object. 102 | function writeValue(value, fieldSchema) { 103 | // onBeforeValueSet allows you to cancel the operation. 104 | // It doesn't work like transform and others that allow you to modify the value because all typecast has already happened. 105 | // For use-cases where you need to modify the value, you can set a new value in the handler and return false. 106 | if (this[_privateKey]._options.onBeforeValueSet) { 107 | if (this[_privateKey]._options.onBeforeValueSet.call(this, value, fieldSchema.name) === false) { 108 | return; 109 | } 110 | } 111 | 112 | // Alias simply copies the value without actually writing it to alias index. 113 | // Because the value isn't actually set on the alias index, onValueSet isn't fired. 114 | if (fieldSchema.type === 'alias') { 115 | this[fieldSchema.index] = value; 116 | return; 117 | } 118 | 119 | // Write the value to the inner object. 120 | this[_privateKey]._obj[fieldSchema.name] = value; 121 | 122 | // onValueSet notifies you after a value has been written. 123 | if (this[_privateKey]._options.onValueSet) { 124 | this[_privateKey]._options.onValueSet.call(this, value, fieldSchema.name); 125 | } 126 | } 127 | 128 | // Represents an error encountered when trying to set a value. 129 | // Code 1xxx 130 | 131 | var SetterError = function () { 132 | function SetterError(errorMessage, setValue, originalValue, fieldSchema) { 133 | _classCallCheck(this, SetterError); 134 | 135 | this.errorMessage = errorMessage; 136 | this.setValue = setValue; 137 | this.originalValue = originalValue; 138 | this.fieldSchema = fieldSchema; 139 | this.errorCode = this.constructor.errorCode(); 140 | } 141 | 142 | _createClass(SetterError, null, [{ 143 | key: 'errorCode', 144 | value: function errorCode() { 145 | return 1000; 146 | } 147 | }]); 148 | 149 | return SetterError; 150 | }(); 151 | 152 | // Cast Error Base 153 | // Thrown when a value cannot be cast to the type specified by the schema 154 | // Code 11xx 155 | 156 | 157 | var CastError = function (_SetterError) { 158 | _inherits(CastError, _SetterError); 159 | 160 | function CastError(errorMessage, setValue, originalValue, fieldSchema) { 161 | _classCallCheck(this, CastError); 162 | 163 | var _this = _possibleConstructorReturn(this, (CastError.__proto__ || Object.getPrototypeOf(CastError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 164 | 165 | _this.errorType = 'CastError'; 166 | return _this; 167 | } 168 | 169 | _createClass(CastError, null, [{ 170 | key: 'errorCode', 171 | value: function errorCode() { 172 | return 1100; 173 | } 174 | }]); 175 | 176 | return CastError; 177 | }(SetterError); 178 | 179 | var StringCastError = function (_CastError) { 180 | _inherits(StringCastError, _CastError); 181 | 182 | function StringCastError(setValue, originalValue, fieldSchema) { 183 | _classCallCheck(this, StringCastError); 184 | 185 | var errorMessage = 'String type cannot typecast Object or Array types.'; 186 | return _possibleConstructorReturn(this, (StringCastError.__proto__ || Object.getPrototypeOf(StringCastError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 187 | } 188 | 189 | _createClass(StringCastError, null, [{ 190 | key: 'errorCode', 191 | value: function errorCode() { 192 | return 1101; 193 | } 194 | }]); 195 | 196 | return StringCastError; 197 | }(CastError); 198 | 199 | var NumberCastError = function (_CastError2) { 200 | _inherits(NumberCastError, _CastError2); 201 | 202 | function NumberCastError(sourceType, setValue, originalValue, fieldSchema) { 203 | _classCallCheck(this, NumberCastError); 204 | 205 | var errorMessage = 'Number could not be typecast from the provided ' + sourceType; 206 | return _possibleConstructorReturn(this, (NumberCastError.__proto__ || Object.getPrototypeOf(NumberCastError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 207 | } 208 | 209 | _createClass(NumberCastError, null, [{ 210 | key: 'errorCode', 211 | value: function errorCode() { 212 | return 1102; 213 | } 214 | }]); 215 | 216 | return NumberCastError; 217 | }(CastError); 218 | 219 | var ArrayCastError = function (_CastError3) { 220 | _inherits(ArrayCastError, _CastError3); 221 | 222 | function ArrayCastError(setValue, originalValue, fieldSchema) { 223 | _classCallCheck(this, ArrayCastError); 224 | 225 | var errorMessage = 'Array type cannot typecast non-Array types.'; 226 | return _possibleConstructorReturn(this, (ArrayCastError.__proto__ || Object.getPrototypeOf(ArrayCastError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 227 | } 228 | 229 | _createClass(ArrayCastError, null, [{ 230 | key: 'errorCode', 231 | value: function errorCode() { 232 | return 1103; 233 | } 234 | }]); 235 | 236 | return ArrayCastError; 237 | }(CastError); 238 | 239 | var ObjectCastError = function (_CastError4) { 240 | _inherits(ObjectCastError, _CastError4); 241 | 242 | function ObjectCastError(setValue, originalValue, fieldSchema) { 243 | _classCallCheck(this, ObjectCastError); 244 | 245 | var errorMessage = 'Object type cannot typecast non-Object types.'; 246 | return _possibleConstructorReturn(this, (ObjectCastError.__proto__ || Object.getPrototypeOf(ObjectCastError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 247 | } 248 | 249 | _createClass(ObjectCastError, null, [{ 250 | key: 'errorCode', 251 | value: function errorCode() { 252 | return 1104; 253 | } 254 | }]); 255 | 256 | return ObjectCastError; 257 | }(CastError); 258 | 259 | var DateCastError = function (_CastError5) { 260 | _inherits(DateCastError, _CastError5); 261 | 262 | function DateCastError(setValue, originalValue, fieldSchema) { 263 | _classCallCheck(this, DateCastError); 264 | 265 | var errorMessage = 'Date type cannot typecast Array or Object types.'; 266 | return _possibleConstructorReturn(this, (DateCastError.__proto__ || Object.getPrototypeOf(DateCastError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 267 | } 268 | 269 | _createClass(DateCastError, null, [{ 270 | key: 'errorCode', 271 | value: function errorCode() { 272 | return 1105; 273 | } 274 | }]); 275 | 276 | return DateCastError; 277 | }(CastError); 278 | 279 | // Validation error base 280 | // Thrown when a value does not meet the validation criteria set by the schema 281 | // Code 12xx 282 | 283 | 284 | var ValidationError = function (_SetterError2) { 285 | _inherits(ValidationError, _SetterError2); 286 | 287 | function ValidationError(errorMessage, setValue, originalValue, fieldSchema) { 288 | _classCallCheck(this, ValidationError); 289 | 290 | var _this7 = _possibleConstructorReturn(this, (ValidationError.__proto__ || Object.getPrototypeOf(ValidationError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 291 | 292 | _this7.errorType = 'ValidationError'; 293 | return _this7; 294 | } 295 | 296 | _createClass(ValidationError, null, [{ 297 | key: 'errorCode', 298 | value: function errorCode() { 299 | return 1200; 300 | } 301 | }]); 302 | 303 | return ValidationError; 304 | }(SetterError); 305 | 306 | /** 307 | * String Validation Errors 308 | * Codes 121x 309 | */ 310 | 311 | var StringValidationError = function (_ValidationError) { 312 | _inherits(StringValidationError, _ValidationError); 313 | 314 | function StringValidationError(errorMessage, setValue, originalValue, fieldSchema) { 315 | _classCallCheck(this, StringValidationError); 316 | 317 | return _possibleConstructorReturn(this, (StringValidationError.__proto__ || Object.getPrototypeOf(StringValidationError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 318 | } 319 | 320 | _createClass(StringValidationError, null, [{ 321 | key: 'errorCode', 322 | value: function errorCode() { 323 | return 1210; 324 | } 325 | }]); 326 | 327 | return StringValidationError; 328 | }(ValidationError); 329 | 330 | var StringEnumValidationError = function (_StringValidationErro) { 331 | _inherits(StringEnumValidationError, _StringValidationErro); 332 | 333 | function StringEnumValidationError(errorMessage, setValue, originalValue, fieldSchema) { 334 | _classCallCheck(this, StringEnumValidationError); 335 | 336 | errorMessage = errorMessage || 'String does not exist in enum list.'; 337 | return _possibleConstructorReturn(this, (StringEnumValidationError.__proto__ || Object.getPrototypeOf(StringEnumValidationError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 338 | } 339 | 340 | _createClass(StringEnumValidationError, null, [{ 341 | key: 'errorCode', 342 | value: function errorCode() { 343 | return 1211; 344 | } 345 | }]); 346 | 347 | return StringEnumValidationError; 348 | }(StringValidationError); 349 | 350 | var StringMinLengthValidationError = function (_StringValidationErro2) { 351 | _inherits(StringMinLengthValidationError, _StringValidationErro2); 352 | 353 | function StringMinLengthValidationError(errorMessage, setValue, originalValue, fieldSchema) { 354 | _classCallCheck(this, StringMinLengthValidationError); 355 | 356 | errorMessage = errorMessage || 'String length too short to meet minLength requirement.'; 357 | return _possibleConstructorReturn(this, (StringMinLengthValidationError.__proto__ || Object.getPrototypeOf(StringMinLengthValidationError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 358 | } 359 | 360 | _createClass(StringMinLengthValidationError, null, [{ 361 | key: 'errorCode', 362 | value: function errorCode() { 363 | return 1212; 364 | } 365 | }]); 366 | 367 | return StringMinLengthValidationError; 368 | }(StringValidationError); 369 | 370 | var StringMaxLengthValidationError = function (_StringValidationErro3) { 371 | _inherits(StringMaxLengthValidationError, _StringValidationErro3); 372 | 373 | function StringMaxLengthValidationError(errorMessage, setValue, originalValue, fieldSchema) { 374 | _classCallCheck(this, StringMaxLengthValidationError); 375 | 376 | errorMessage = errorMessage || 'String length too long to meet maxLength requirement.'; 377 | return _possibleConstructorReturn(this, (StringMaxLengthValidationError.__proto__ || Object.getPrototypeOf(StringMaxLengthValidationError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 378 | } 379 | 380 | _createClass(StringMaxLengthValidationError, null, [{ 381 | key: 'errorCode', 382 | value: function errorCode() { 383 | return 1213; 384 | } 385 | }]); 386 | 387 | return StringMaxLengthValidationError; 388 | }(StringValidationError); 389 | 390 | var StringRegexValidationError = function (_StringValidationErro4) { 391 | _inherits(StringRegexValidationError, _StringValidationErro4); 392 | 393 | function StringRegexValidationError(errorMessage, setValue, originalValue, fieldSchema) { 394 | _classCallCheck(this, StringRegexValidationError); 395 | 396 | errorMessage = errorMessage || 'String does not match regular expression pattern.'; 397 | return _possibleConstructorReturn(this, (StringRegexValidationError.__proto__ || Object.getPrototypeOf(StringRegexValidationError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 398 | } 399 | 400 | _createClass(StringRegexValidationError, null, [{ 401 | key: 'errorCode', 402 | value: function errorCode() { 403 | return 1214; 404 | } 405 | }]); 406 | 407 | return StringRegexValidationError; 408 | }(StringValidationError); 409 | 410 | /** 411 | * Number Validation Errors 412 | * Codes 122x 413 | */ 414 | 415 | var NumberValidationError = function (_ValidationError2) { 416 | _inherits(NumberValidationError, _ValidationError2); 417 | 418 | function NumberValidationError(errorMessage, setValue, originalValue, fieldSchema) { 419 | _classCallCheck(this, NumberValidationError); 420 | 421 | return _possibleConstructorReturn(this, (NumberValidationError.__proto__ || Object.getPrototypeOf(NumberValidationError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 422 | } 423 | 424 | _createClass(NumberValidationError, null, [{ 425 | key: 'errorCode', 426 | value: function errorCode() { 427 | return 1220; 428 | } 429 | }]); 430 | 431 | return NumberValidationError; 432 | }(ValidationError); 433 | 434 | var NumberMinValidationError = function (_NumberValidationErro) { 435 | _inherits(NumberMinValidationError, _NumberValidationErro); 436 | 437 | function NumberMinValidationError(errorMessage, setValue, originalValue, fieldSchema) { 438 | _classCallCheck(this, NumberMinValidationError); 439 | 440 | errorMessage = errorMessage || 'Number is too small to meet min requirement.'; 441 | return _possibleConstructorReturn(this, (NumberMinValidationError.__proto__ || Object.getPrototypeOf(NumberMinValidationError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 442 | } 443 | 444 | _createClass(NumberMinValidationError, null, [{ 445 | key: 'errorCode', 446 | value: function errorCode() { 447 | return 1221; 448 | } 449 | }]); 450 | 451 | return NumberMinValidationError; 452 | }(NumberValidationError); 453 | 454 | var NumberMaxValidationError = function (_NumberValidationErro2) { 455 | _inherits(NumberMaxValidationError, _NumberValidationErro2); 456 | 457 | function NumberMaxValidationError(errorMessage, setValue, originalValue, fieldSchema) { 458 | _classCallCheck(this, NumberMaxValidationError); 459 | 460 | errorMessage = errorMessage || 'Number is too big to meet max requirement.'; 461 | return _possibleConstructorReturn(this, (NumberMaxValidationError.__proto__ || Object.getPrototypeOf(NumberMaxValidationError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 462 | } 463 | 464 | _createClass(NumberMaxValidationError, null, [{ 465 | key: 'errorCode', 466 | value: function errorCode() { 467 | return 1222; 468 | } 469 | }]); 470 | 471 | return NumberMaxValidationError; 472 | }(NumberValidationError); 473 | 474 | /** 475 | * Date Validation Errors 476 | * Codes 123x 477 | */ 478 | 479 | var DateValidationError = function (_ValidationError3) { 480 | _inherits(DateValidationError, _ValidationError3); 481 | 482 | function DateValidationError(errorMessage, setValue, originalValue, fieldSchema) { 483 | _classCallCheck(this, DateValidationError); 484 | 485 | return _possibleConstructorReturn(this, (DateValidationError.__proto__ || Object.getPrototypeOf(DateValidationError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 486 | } 487 | 488 | _createClass(DateValidationError, null, [{ 489 | key: 'errorCode', 490 | value: function errorCode() { 491 | return 1230; 492 | } 493 | }]); 494 | 495 | return DateValidationError; 496 | }(ValidationError); 497 | 498 | var DateParseValidationError = function (_DateValidationError) { 499 | _inherits(DateParseValidationError, _DateValidationError); 500 | 501 | function DateParseValidationError(errorMessage, setValue, originalValue, fieldSchema) { 502 | _classCallCheck(this, DateParseValidationError); 503 | 504 | errorMessage = errorMessage || 'Could not parse date.'; 505 | return _possibleConstructorReturn(this, (DateParseValidationError.__proto__ || Object.getPrototypeOf(DateParseValidationError)).call(this, errorMessage, setValue, originalValue, fieldSchema)); 506 | } 507 | 508 | _createClass(DateParseValidationError, null, [{ 509 | key: 'errorCode', 510 | value: function errorCode() { 511 | return 1231; 512 | } 513 | }]); 514 | 515 | return DateParseValidationError; 516 | }(DateValidationError); 517 | 518 | // Returns typecasted value if possible. If rejected, originalValue is returned. 519 | 520 | 521 | function typecast(value, originalValue, properties) { 522 | var options = this[_privateKey]._options; 523 | 524 | // Allow transform to manipulate raw properties. 525 | if (properties.transform) { 526 | value = properties.transform.call(this[_privateKey]._root, value, originalValue, properties); 527 | } 528 | 529 | // Allow null to be preserved. 530 | if (value === null && options.preserveNull) { 531 | return null; 532 | } 533 | 534 | // Helper function designed to detect and handle usage of array-form custom error messages for validators 535 | function detectCustomErrorMessage(key) { 536 | if (_typeof(properties[key]) === 'object' && properties[key].errorMessage && properties[key].value) { 537 | return properties[key]; 538 | } else if (_.isArray(properties[key])) { 539 | return { 540 | value: properties[key][0], 541 | errorMessage: properties[key][1] 542 | }; 543 | } else { 544 | return { 545 | value: properties[key], 546 | errorMessage: undefined 547 | }; 548 | } 549 | } 550 | 551 | // Property types are always normalized as lowercase strings despite shorthand definitions being available. 552 | switch (properties.type) { 553 | case 'string': 554 | // Reject if object or array. 555 | if (_.isObject(value) || _.isArray(value)) { 556 | throw new StringCastError(value, originalValue, properties); 557 | } 558 | 559 | // If index is being set with null or undefined, set value and end. 560 | if (value === undefined || value === null) { 561 | return undefined; 562 | } 563 | 564 | // Typecast to String. 565 | value = value + ''; 566 | 567 | // If stringTransform function is defined, use. 568 | // This is used before we do validation checks (except to be sure we have a string at all). 569 | if (properties.stringTransform) { 570 | value = properties.stringTransform.call(this[_privateKey]._root, value, originalValue, properties); 571 | } 572 | 573 | // If clip property & maxLength properties are set, the string should be clipped. 574 | // This is basically a shortcut property that could be done with stringTransform. 575 | if (properties.clip !== undefined && properties.maxLength !== undefined) { 576 | value = value.substr(0, properties.maxLength); 577 | } 578 | 579 | var enumValidation = void 0; 580 | 581 | // Detect custom error message usage for enum (can't use function here as enum is expected to be an array) 582 | if (_typeof(properties.enum) === 'object' && properties.enum.errorMessage && properties.enum.value) { 583 | enumValidation = properties.enum; 584 | } else if (_.isArray(properties.enum) && _.isArray(properties.enum[0])) { 585 | enumValidation = { 586 | value: properties.enum[0], 587 | errorMessage: properties.enum[1] 588 | }; 589 | } else { 590 | enumValidation = { 591 | value: properties.enum, 592 | errorMessage: undefined 593 | }; 594 | } 595 | 596 | // If enum is being used, be sure the value is within definition. 597 | if (enumValidation.value !== undefined && _.isArray(enumValidation.value) && enumValidation.value.indexOf(value) === -1) { 598 | throw new StringEnumValidationError(enumValidation.errorMessage, value, originalValue, properties); 599 | } 600 | 601 | // Detect custom error message usage for minLength 602 | var minLength = detectCustomErrorMessage('minLength'); 603 | 604 | // If minLength is defined, check to be sure the string is > minLength. 605 | if (minLength.value !== undefined && value.length < minLength.value) { 606 | throw new StringMinLengthValidationError(minLength.errorMessage, value, originalValue, properties); 607 | } 608 | 609 | // Detect custom error message usage for maxLength 610 | var maxLength = detectCustomErrorMessage('maxLength'); 611 | 612 | // If maxLength is defined, check to be sure the string is < maxLength. 613 | if (maxLength.value !== undefined && value.length > maxLength.value) { 614 | throw new StringMaxLengthValidationError(maxLength.errorMessage, value, originalValue, properties); 615 | } 616 | 617 | // Detect custom error message usage for maxLength 618 | var regex = detectCustomErrorMessage('regex'); 619 | 620 | // If regex is defined, check to be sure the string matches the regex pattern. 621 | if (regex.value && !regex.value.test(value)) { 622 | throw new StringRegexValidationError(regex.errorMessage, value, originalValue, properties); 623 | } 624 | 625 | return value; 626 | 627 | case 'number': 628 | // If index is being set with null, undefined, or empty string: clear value. 629 | if (value === undefined || value === null || value === '') { 630 | return undefined; 631 | } 632 | 633 | // Set values for boolean. 634 | if (_.isBoolean(value)) { 635 | value = value ? 1 : 0; 636 | } 637 | 638 | // Remove/convert number group separators 639 | if (typeof value === 'string') { 640 | if (options.useDecimalNumberGroupSeparator) { 641 | // Remove decimals 642 | value = value.replace(/\./g, ''); 643 | // Replace commas with decimals for js parsing 644 | value = value.replace(/,/g, '.'); 645 | } else { 646 | //Remove commas 647 | value = value.replace(/,/g, ''); 648 | } 649 | 650 | // Reject if string was not a valid number 651 | if (isNaN(Number(value))) { 652 | throw new NumberCastError('String', value, originalValue, properties); 653 | } 654 | } 655 | 656 | // Reject if array, object, or not numeric. 657 | if (_.isArray(value)) { 658 | throw new NumberCastError('Array', value, originalValue, properties); 659 | } else if (_.isObject(value)) { 660 | throw new NumberCastError('Object', value, originalValue, properties); 661 | } else if (!isNumeric(value)) { 662 | throw new NumberCastError('Non-numeric', value, originalValue, properties); 663 | } 664 | 665 | // Typecast to number. 666 | value = Number(value); 667 | 668 | // Transformation after typecasting but before validation and filters. 669 | if (properties.numberTransform) { 670 | value = properties.numberTransform.call(this[_privateKey]._root, value, originalValue, properties); 671 | } 672 | 673 | // Detect custom error message usage for min 674 | var min = detectCustomErrorMessage('min'); 675 | 676 | if (min.value !== undefined && value < min.value) { 677 | throw new NumberMinValidationError(min.errorMessage, value, originalValue, properties); 678 | } 679 | 680 | // Detect custom error message usage for min 681 | var max = detectCustomErrorMessage('max'); 682 | 683 | if (max.value !== undefined && value > max.value) { 684 | throw new NumberMaxValidationError(max.errorMessage, value, originalValue, properties); 685 | } 686 | 687 | return value; 688 | 689 | case 'boolean': 690 | // If index is being set with null, undefined, or empty string: clear value. 691 | if (value === undefined || value === null || value === '') { 692 | return undefined; 693 | } 694 | 695 | // If is String and is 'false', convert to Boolean. 696 | if (value === 'false') { 697 | return false; 698 | } 699 | 700 | // If is Number, <0 is true and >0 is false. 701 | if (isNumeric(value)) { 702 | return value * 1 > 0; 703 | } 704 | 705 | // Use Javascript to eval and return boolean. 706 | value = !!value; 707 | 708 | // Transformation after typecasting but before validation and filters. 709 | if (properties.booleanTransform) { 710 | value = properties.booleanTransform.call(this[_privateKey]._root, value, originalValue, properties); 711 | } 712 | 713 | return value; 714 | 715 | case 'array': 716 | // If it's an object, typecast to an array and return array. 717 | if (_.isObject(value)) { 718 | value = _.toArray(value); 719 | } 720 | 721 | // Reject if not array. 722 | if (!_.isArray(value)) { 723 | throw new ArrayCastError(value, originalValue, properties); 724 | } 725 | 726 | // Arrays are never set directly. 727 | // Instead, the values are copied over to the existing SchemaArray instance. 728 | // The SchemaArray is initialized immediately and will always exist. 729 | originalValue.length = 0; 730 | _.each(value, function (arrayValue) { 731 | originalValue.push(arrayValue); 732 | }); 733 | 734 | return originalValue; 735 | 736 | case 'object': 737 | // If it's not an Object, reject. 738 | if (!_.isObject(value)) { 739 | throw new ObjectCastError(value, originalValue, properties); 740 | } 741 | 742 | // If object is schema object and an entirely new object was passed, clear values and set. 743 | // This preserves the object instance. 744 | if (properties.objectType) { 745 | // The object will usually exist because it's initialized immediately for deep access within SchemaObjects. 746 | // However, in the case of Array elements, it will not exist. 747 | var schemaObject = void 0; 748 | if (originalValue !== undefined) { 749 | // Clear existing values. 750 | schemaObject = originalValue; 751 | schemaObject.clear(); 752 | } else { 753 | // The SchemaObject doesn't exist yet. Let's initialize a new one. 754 | // This is used for Array types. 755 | schemaObject = new properties.objectType({}, this[_privateKey]._root); 756 | } 757 | 758 | // Copy value to SchemaObject and set value to SchemaObject. 759 | for (var key in value) { 760 | schemaObject[key] = value[key]; 761 | } 762 | value = schemaObject; 763 | } 764 | 765 | // Otherwise, it's OK. 766 | return value; 767 | 768 | case 'date': 769 | // If index is being set with null, undefined, or empty string: clear value. 770 | if (value === undefined || value === null || value === '') { 771 | return undefined; 772 | } 773 | 774 | // Reject if object, array or boolean. 775 | if (!_.isDate(value) && !_.isString(value) && !_.isNumber(value)) { 776 | throw new DateCastError(value, originalValue, properties); 777 | } 778 | 779 | // Attempt to parse string value with Date.parse (which returns number of milliseconds). 780 | if (_.isString(value)) { 781 | value = Date.parse(value); 782 | } 783 | 784 | // If is timestamp, convert to Date. 785 | if (isNumeric(value)) { 786 | value = new Date((value + '').length > 10 ? value : value * 1000); 787 | } 788 | 789 | // If the date couldn't be parsed, do not modify index. 790 | if (value == 'Invalid Date' || !_.isDate(value)) { 791 | throw new DateParseValidationError(null, value, originalValue, properties); 792 | } 793 | 794 | // Transformation after typecasting but before validation and filters. 795 | if (properties.dateTransform) { 796 | value = properties.dateTransform.call(this[_privateKey]._root, value, originalValue, properties); 797 | } 798 | 799 | return value; 800 | 801 | default: 802 | // 'any' 803 | return value; 804 | } 805 | } 806 | 807 | // Properties can be passed in multiple forms (an object, just a type, etc). 808 | // Normalize to a standard format. 809 | function normalizeProperties(properties, name) { 810 | // Allow for shorthand type declaration: 811 | 812 | // Check to see if the user passed in a raw type of a properties hash. 813 | if (properties) { 814 | // Raw type passed. 815 | // index: Type is translated to index: {type: Type} 816 | // Properties hash created. 817 | if (properties.type === undefined) { 818 | properties = { 819 | type: properties 820 | }; 821 | 822 | // Properties hash passed. 823 | // Copy properties hash before modifying. 824 | // Users can pass in their own custom types to the schema and we don't want to write to that object. 825 | // Especially since properties.name contains the index of our field and copying that will break functionality. 826 | } else { 827 | properties = _.cloneDeep(properties); 828 | } 829 | } 830 | 831 | // Type may be an object with properties. 832 | // If "type.type" exists, we'll assume it's meant to be properties. 833 | // This means that shorthand objects can't use the "type" index. 834 | // If "type" is necessary, they must be wrapped in a SchemaObject. 835 | if (_.isObject(properties.type) && properties.type.type !== undefined) { 836 | _.each(properties.type, function (value, key) { 837 | if (properties[key] === undefined) { 838 | properties[key] = value; 839 | } 840 | }); 841 | properties.type = properties.type.type; 842 | } 843 | 844 | // Null or undefined should be flexible and allow any value. 845 | if (properties.type === null || properties.type === undefined) { 846 | properties.type = 'any'; 847 | 848 | // Convert object representation of type to lowercase string. 849 | // String is converted to 'string', Number to 'number', etc. 850 | // Do not convert the initialized SchemaObjectInstance to a string! 851 | // Check for a shorthand declaration of schema by key length. 852 | } else if (_.isString(properties.type.name) && properties.type.name !== 'SchemaObjectInstance' && Object.keys(properties.type).length === 0) { 853 | properties.type = properties.type.name; 854 | } 855 | if (_.isString(properties.type)) { 856 | properties.type = properties.type.toLowerCase(); 857 | } 858 | 859 | // index: [Type] or index: [] is translated to index: {type: Array, arrayType: Type} 860 | if (_.isArray(properties.type)) { 861 | if (_.size(properties.type)) { 862 | // Properties will be normalized when array is initialized. 863 | properties.arrayType = properties.type[0]; 864 | } 865 | properties.type = 'array'; 866 | } 867 | 868 | // index: {} or index: SchemaObject is translated to index: {type: Object, objectType: Type} 869 | if (!_.isString(properties.type)) { 870 | if (_.isFunction(properties.type)) { 871 | properties.objectType = properties.type; 872 | properties.type = 'object'; 873 | } else if (_.isObject(properties.type)) { 874 | // When an empty object is passed, no schema is enforced. 875 | if (_.size(properties.type)) { 876 | // Options should be inherited by sub-SchemaObjects, except toObject. 877 | var options = _.clone(this[_privateKey]._options); 878 | delete options.toObject; 879 | 880 | // When we're creating a nested schema automatically, it should always inherit the root "this". 881 | options.inheritRootThis = true; 882 | 883 | // Initialize the SchemaObject sub-schema automatically. 884 | properties.objectType = new SchemaObject(properties.type, options); 885 | } 886 | 887 | // Regardless of if we created a sub-schema or not, the field is indexed as an object. 888 | properties.type = 'object'; 889 | } 890 | } 891 | 892 | // Set name if passed on properties. 893 | // It's used to show what field an error what generated on. 894 | if (name) { 895 | properties.name = name; 896 | } 897 | 898 | return properties; 899 | } 900 | 901 | // Add field to schema and initializes getter and setter for the field. 902 | function addToSchema(index, properties) { 903 | this[_privateKey]._schema[index] = normalizeProperties.call(this, properties, index); 904 | 905 | defineGetter.call(this[_privateKey]._getset, index, this[_privateKey]._schema[index]); 906 | defineSetter.call(this[_privateKey]._getset, index, this[_privateKey]._schema[index]); 907 | } 908 | 909 | // Defines getter for specific field. 910 | function defineGetter(index, properties) { 911 | var _this18 = this; 912 | 913 | // If the field type is an alias, we retrieve the value through the alias's index. 914 | var indexOrAliasIndex = properties.type === 'alias' ? properties.index : index; 915 | 916 | this.__defineGetter__(index, function () { 917 | // If accessing object or array, lazy initialize if not set. 918 | if (!_this18[_privateKey]._obj[indexOrAliasIndex] && (properties.type === 'object' || properties.type === 'array')) { 919 | // Initialize object. 920 | if (properties.type === 'object') { 921 | if (properties.default !== undefined) { 922 | writeValue.call(_this18[_privateKey]._this, _.isFunction(properties.default) ? properties.default.call(_this18) : properties.default, properties); 923 | } else { 924 | writeValue.call(_this18[_privateKey]._this, properties.objectType ? new properties.objectType({}, _this18[_privateKey]._root) : {}, properties); 925 | } 926 | 927 | // Native arrays are not used so that Array class can be extended with custom behaviors. 928 | } else if (properties.type === 'array') { 929 | writeValue.call(_this18[_privateKey]._this, new SchemaArray(_this18, properties), properties); 930 | } 931 | } 932 | 933 | try { 934 | return getter.call(_this18, _this18[_privateKey]._obj[indexOrAliasIndex], properties); 935 | } catch (error) { 936 | // This typically happens when the default value isn't valid -- log error. 937 | _this18[_privateKey]._errors.push(error); 938 | } 939 | }); 940 | } 941 | 942 | // Defines setter for specific field. 943 | function defineSetter(index, properties) { 944 | var _this19 = this; 945 | 946 | this.__defineSetter__(index, function (value) { 947 | // Don't proceed if readOnly is true. 948 | if (properties.readOnly) { 949 | return; 950 | } 951 | 952 | try { 953 | // this[_privateKey]._this[index] is used instead of this[_privateKey]._obj[index] to route through the public interface. 954 | writeValue.call(_this19[_privateKey]._this, typecast.call(_this19, value, _this19[_privateKey]._this[index], properties), properties); 955 | } catch (error) { 956 | // Setter failed to validate value -- log error. 957 | _this19[_privateKey]._errors.push(error); 958 | } 959 | }); 960 | } 961 | 962 | // Reset field to default value. 963 | function clearField(index, properties) { 964 | // Aliased fields reflect values on other fields and do not need to be cleared. 965 | if (properties.isAlias === true) { 966 | return; 967 | } 968 | 969 | // In case of object & array, they must be initialized immediately. 970 | if (properties.type === 'object') { 971 | this[properties.name].clear(); 972 | 973 | // Native arrays are never used so that toArray can be globally supported. 974 | // Additionally, other properties such as unique rely on passing through SchemaObject. 975 | } else if (properties.type === 'array') { 976 | this[properties.name].length = 0; 977 | 978 | // Other field types can simply have their value set to undefined. 979 | } else { 980 | writeValue.call(this[_privateKey]._this, undefined, properties); 981 | } 982 | } 983 | 984 | // Represents a basic array with typecasted values. 985 | 986 | var SchemaArray = function (_extendableBuiltin2) { 987 | _inherits(SchemaArray, _extendableBuiltin2); 988 | 989 | function SchemaArray(self, properties) { 990 | _classCallCheck(this, SchemaArray); 991 | 992 | // Store all internals. 993 | var _this20 = _possibleConstructorReturn(this, (SchemaArray.__proto__ || Object.getPrototypeOf(SchemaArray)).call(this)); 994 | 995 | var _private = _this20[_privateKey] = {}; 996 | 997 | // Store reference to self. 998 | _private._self = self; 999 | 1000 | // Store properties (arrayType, unique, etc). 1001 | _private._properties = properties; 1002 | 1003 | // Normalize our own properties. 1004 | if (properties.arrayType) { 1005 | properties.arrayType = normalizeProperties.call(self, properties.arrayType); 1006 | } 1007 | return _this20; 1008 | } 1009 | 1010 | _createClass(SchemaArray, [{ 1011 | key: 'push', 1012 | value: function push() { 1013 | var _this21 = this; 1014 | 1015 | // Values are passed through the typecast before being allowed onto the array if arrayType is set. 1016 | // In the case of rejection, the typecast returns undefined, which is not appended to the array. 1017 | var values = void 0; 1018 | 1019 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 1020 | args[_key] = arguments[_key]; 1021 | } 1022 | 1023 | if (this[_privateKey]._properties.arrayType) { 1024 | values = [].map.call(args, function (value) { 1025 | return typecast.call(_this21[_privateKey]._self, value, undefined, _this21[_privateKey]._properties.arrayType); 1026 | }, this); 1027 | } else { 1028 | values = args; 1029 | } 1030 | 1031 | // Enforce filter. 1032 | if (this[_privateKey]._properties.filter) { 1033 | values = _.filter(values, function (value) { 1034 | return _this21[_privateKey]._properties.filter.call(_this21, value); 1035 | }); 1036 | } 1037 | 1038 | // Enforce uniqueness. 1039 | if (this[_privateKey]._properties.unique) { 1040 | values = _.difference(values, _.toArray(this)); 1041 | } 1042 | 1043 | return Array.prototype.push.apply(this, values); 1044 | } 1045 | }, { 1046 | key: 'concat', 1047 | value: function concat() { 1048 | // Return new instance of SchemaArray. 1049 | var schemaArray = new SchemaArray(this[_privateKey]._self, this[_privateKey]._properties); 1050 | 1051 | // Create primitive array with all elements. 1052 | var array = this.toArray(); 1053 | 1054 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 1055 | args[_key2] = arguments[_key2]; 1056 | } 1057 | 1058 | for (var i in args) { 1059 | if (args[i].toArray) { 1060 | args[i] = args[i].toArray(); 1061 | } 1062 | array = array.concat(args[i]); 1063 | } 1064 | 1065 | // Push each value in individually to typecast. 1066 | for (var _i in array) { 1067 | schemaArray.push(array[_i]); 1068 | } 1069 | 1070 | return schemaArray; 1071 | } 1072 | }, { 1073 | key: 'toArray', 1074 | value: function toArray() { 1075 | // Create new Array to hold elements. 1076 | var array = []; 1077 | 1078 | // Loop through each element, clone if necessary. 1079 | _.each(this, function (element) { 1080 | // Call toObject() method if defined (this allows us to return primitive objects instead of SchemaObjects). 1081 | if (_.isObject(element) && _.isFunction(element.toObject)) { 1082 | element = element.toObject(); 1083 | 1084 | // If is non-SchemaType object, shallow clone so that properties modification don't have an affect on the original object. 1085 | } else if (_.isObject(element)) { 1086 | element = _.clone(element); 1087 | } 1088 | 1089 | array.push(element); 1090 | }); 1091 | 1092 | return array; 1093 | } 1094 | }, { 1095 | key: 'toJSON', 1096 | value: function toJSON() { 1097 | return this.toArray(); 1098 | } 1099 | 1100 | // Used to detect instance of SchemaArray internally. 1101 | 1102 | }, { 1103 | key: '_isSchemaArray', 1104 | value: function _isSchemaArray() { 1105 | return true; 1106 | } 1107 | }]); 1108 | 1109 | return SchemaArray; 1110 | }(_extendableBuiltin(Array)); 1111 | 1112 | // Represents an object FACTORY with typed indexes. 1113 | 1114 | 1115 | var SchemaObject = function SchemaObject(schema) { 1116 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 1117 | 1118 | _classCallCheck(this, SchemaObject); 1119 | 1120 | // Create object for options if doesn't exist and merge with defaults. 1121 | options = _.extend({ 1122 | // By default, allow only values in the schema to be set. 1123 | // When this is false, setting new fields will dynamically add the field to the schema as type "any". 1124 | strict: true, 1125 | 1126 | // Allow fields to be set via dotNotation; obj['user.name'] = 'Scott'; -> obj: { user: 'Scott' } 1127 | dotNotation: false, 1128 | 1129 | // Do not set undefined values to keys within toObject(). 1130 | // This is the default because MongoDB will convert undefined to null and overwrite existing values. 1131 | // If this is true, toObject() will output undefined for unset primitives and empty arrays/objects for those types. 1132 | // If this is false, toObject() will not output any keys for unset primitives, arrays, and objects. 1133 | setUndefined: false, 1134 | 1135 | // If this is set to true, null will NOT be converted to undefined automatically. 1136 | // In many cases, when people use null, they actually want to unset a value. 1137 | // There are rare cases where preserving the null is important. 1138 | // Set to true if you are one of those rare cases. 1139 | preserveNull: false, 1140 | 1141 | // Allow "profileURL" to be set with "profileUrl" when set to false 1142 | keysIgnoreCase: false, 1143 | 1144 | // Inherit root object "this" context from parent SchemaObject. 1145 | inheritRootThis: false, 1146 | 1147 | // If this is set to false, require will not allow falsy values such as empty strings 1148 | allowFalsyValues: true, 1149 | 1150 | // This defines the digit group separator used for parsing numbers, it defaults to ',' 1151 | // For example 3,043,201.01. However if enabled it swaps commas and decimals to allow parsing 1152 | // numbers like 3.043.201,01 1153 | useDecimalNumberGroupSeparator: false 1154 | 1155 | }, options); 1156 | 1157 | // Some of the options require reflection. 1158 | if (_isProxySupported === false) { 1159 | if (!options.strict) { 1160 | throw new Error('[schema-object] Turning strict mode off requires --harmony flag.'); 1161 | } 1162 | if (options.dotNotation) { 1163 | throw new Error('[schema-object] Dot notation support requires --harmony flag.'); 1164 | } 1165 | if (options.keysIgnoreCase) { 1166 | throw new Error('[schema-object] Keys ignore case support requires --harmony flag.'); 1167 | } 1168 | } 1169 | 1170 | // Used at minimum to hold default constructor. 1171 | if (!options.constructors) { 1172 | options.constructors = {}; 1173 | } 1174 | 1175 | // Default constructor can be overridden. 1176 | if (!options.constructors.default) { 1177 | // By default, populate runtime values as provided to this instance of object. 1178 | options.constructors.default = function (values) { 1179 | this.populate(values); 1180 | }; 1181 | } 1182 | 1183 | // Create SchemaObject factory. 1184 | var SO = SchemaObjectInstanceFactory(schema, options); 1185 | 1186 | // Add custom constructors. 1187 | _.each(options.constructors, function (method, key) { 1188 | SO[key] = function () { 1189 | // Initialize new SO. 1190 | var obj = new SO(); 1191 | 1192 | // Expose default constructor to populate defaults. 1193 | obj[_privateKey]._reservedFields.super = function () { 1194 | options.constructors.default.apply(obj, arguments); 1195 | }; 1196 | 1197 | // Call custom constructor. 1198 | method.apply(obj, arguments); 1199 | 1200 | // Cleanup and return SO. 1201 | delete obj[_privateKey]._reservedFields.super; 1202 | return obj; 1203 | }; 1204 | }); 1205 | 1206 | return SO; 1207 | }; 1208 | 1209 | // Represents an object INSTANCE factory with typed indexes. 1210 | 1211 | 1212 | function SchemaObjectInstanceFactory(schema, options) { 1213 | // Represents an actual instance of an object. 1214 | var SchemaObjectInstance = function () { 1215 | _createClass(SchemaObjectInstance, null, [{ 1216 | key: 'extend', 1217 | 1218 | // Extend instance factory. 1219 | value: function extend(extendSchema) { 1220 | var extendOptions = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 1221 | 1222 | // Extend requires reflection. 1223 | if (_isProxySupported === false) { 1224 | throw new Error('[schema-object] Extending object requires --harmony flag.'); 1225 | } 1226 | 1227 | // Merge schema and options together. 1228 | var mergedSchema = _.merge({}, schema, extendSchema); 1229 | var mergedOptions = _.merge({}, options, extendOptions); 1230 | 1231 | // Allow method and constructor to call `this.super()`. 1232 | var methodHomes = ['methods', 'constructors']; 1233 | var _iteratorNormalCompletion = true; 1234 | var _didIteratorError = false; 1235 | var _iteratorError = undefined; 1236 | 1237 | try { 1238 | var _loop = function _loop() { 1239 | var methodHome = _step.value; 1240 | 1241 | // Ensure object containing methods exists on both provided and original options. 1242 | if (_.size(options[methodHome]) && _.size(extendOptions[methodHome])) { 1243 | // Loop through each method in the original options. 1244 | // It's not necessary to bind `this.super()` for options that didn't already exist. 1245 | _.each(options[methodHome], function (method, name) { 1246 | // The original option may exist, but was it extended? 1247 | if (extendOptions[methodHome][name]) { 1248 | // Extend method by creating a binding that takes the `this` context given and adds `self`. 1249 | // `self` is a reference to the original method, also bound to the correct `this`. 1250 | mergedOptions[methodHome][name] = function () { 1251 | var _this22 = this, 1252 | _arguments = arguments; 1253 | 1254 | this[_privateKey]._reservedFields.super = function () { 1255 | return method.apply(_this22, _arguments); 1256 | }; 1257 | var ret = extendOptions[methodHome][name].apply(this, arguments); 1258 | delete this[_privateKey]._reservedFields.super; 1259 | return ret; 1260 | }; 1261 | } 1262 | }); 1263 | } 1264 | }; 1265 | 1266 | for (var _iterator = methodHomes[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 1267 | _loop(); 1268 | } 1269 | } catch (err) { 1270 | _didIteratorError = true; 1271 | _iteratorError = err; 1272 | } finally { 1273 | try { 1274 | if (!_iteratorNormalCompletion && _iterator.return) { 1275 | _iterator.return(); 1276 | } 1277 | } finally { 1278 | if (_didIteratorError) { 1279 | throw _iteratorError; 1280 | } 1281 | } 1282 | } 1283 | 1284 | return new SchemaObject(mergedSchema, mergedOptions); 1285 | } 1286 | 1287 | // Construct new instance pre-populated with values. 1288 | 1289 | }]); 1290 | 1291 | function SchemaObjectInstance(values, _root) { 1292 | var _this23 = this; 1293 | 1294 | _classCallCheck(this, SchemaObjectInstance); 1295 | 1296 | // Object used to store internals. 1297 | var _private = this[_privateKey] = {}; 1298 | 1299 | // 1300 | _private._root = options.inheritRootThis ? _root || this : this; 1301 | 1302 | // Object with getters and setters bound. 1303 | _private._getset = this; 1304 | 1305 | // Public version of ourselves. 1306 | // Overwritten with proxy if available. 1307 | _private._this = this; 1308 | 1309 | // Object used to store raw values. 1310 | var obj = _private._obj = {}; 1311 | 1312 | // Schema as defined by constructor. 1313 | _private._schema = schema; 1314 | 1315 | // Errors, retrieved with getErrors(). 1316 | _private._errors = []; 1317 | 1318 | // Options need to be accessible. Shared across ALL INSTANCES. 1319 | _private._options = options; 1320 | 1321 | // Reserved keys for storing internal properties accessible from outside. 1322 | _private._reservedFields = {}; 1323 | 1324 | // Normalize schema properties to allow for shorthand declarations. 1325 | _.each(schema, function (properties, index) { 1326 | schema[index] = normalizeProperties.call(_this23, properties, index); 1327 | }); 1328 | 1329 | // Define getters/typecasts based off of schema. 1330 | _.each(schema, function (properties, index) { 1331 | // Use getter / typecast to intercept and re-route, transform, etc. 1332 | defineGetter.call(_private._getset, index, properties); 1333 | defineSetter.call(_private._getset, index, properties); 1334 | }); 1335 | 1336 | // Proxy used as interface to object allows to intercept all access. 1337 | // Without Proxy we must register individual getter/typecasts to put any logic in place. 1338 | // With Proxy, we still use the individual getter/typecasts, but also catch values that aren't in the schema. 1339 | if (_isProxySupported === true) { 1340 | var proxy = this[_privateKey]._this = new Proxy(this, { 1341 | // Ensure only public keys are shown. 1342 | ownKeys: function ownKeys(target) { 1343 | return Object.keys(_this23.toObject()); 1344 | }, 1345 | 1346 | // Return keys to iterate. 1347 | enumerate: function enumerate(target) { 1348 | return Object.keys(_this23[_privateKey]._this)[Symbol.iterator](); 1349 | }, 1350 | 1351 | // Check to see if key exists. 1352 | has: function has(target, key) { 1353 | return !!_private._getset[key]; 1354 | }, 1355 | 1356 | // Ensure correct prototype is returned. 1357 | getPrototypeOf: function getPrototypeOf() { 1358 | return _private._getset; 1359 | }, 1360 | 1361 | // Ensure readOnly fields are not writeable. 1362 | getOwnPropertyDescriptor: function getOwnPropertyDescriptor(target, key) { 1363 | return { 1364 | value: proxy[key], 1365 | writeable: !schema[key] || schema[key].readOnly !== true, 1366 | enumerable: true, 1367 | configurable: true 1368 | }; 1369 | }, 1370 | 1371 | // Intercept all get calls. 1372 | get: function get(target, name, receiver) { 1373 | // First check to see if it's a reserved field. 1374 | if (_reservedFields.includes(name)) { 1375 | return _this23[_privateKey]._reservedFields[name]; 1376 | } 1377 | 1378 | // Support dot notation via lodash. 1379 | if (options.dotNotation && typeof name === 'string' && name.indexOf('.') !== -1) { 1380 | return _.get(_this23[_privateKey]._this, name); 1381 | } 1382 | 1383 | // Use registered getter without hitting the proxy to avoid creating an infinite loop. 1384 | return _this23[name]; 1385 | }, 1386 | 1387 | // Intercept all set calls. 1388 | set: function set(target, name, value, receiver) { 1389 | // Support dot notation via lodash. 1390 | if (options.dotNotation && typeof name === 'string' && name.indexOf('.') !== -1) { 1391 | return _.set(_this23[_privateKey]._this, name, value); 1392 | } 1393 | 1394 | // Find real keyname if case sensitivity is off. 1395 | if (options.keysIgnoreCase && !schema[name]) { 1396 | name = getIndex.call(_this23, name); 1397 | } 1398 | 1399 | if (!schema[name]) { 1400 | if (options.strict) { 1401 | // Strict mode means we don't want to deal with anything not in the schema. 1402 | // TODO: SetterError here. 1403 | return true; 1404 | } else { 1405 | // Add index to schema dynamically when value is set. 1406 | // This is necessary for toObject to see the field. 1407 | addToSchema.call(_this23, name, { 1408 | type: 'any' 1409 | }); 1410 | } 1411 | } 1412 | 1413 | // This hits the registered setter but bypasses the proxy to avoid an infinite loop. 1414 | _this23[name] = value; 1415 | 1416 | // Necessary for Node v6.0. Prevents error: 'set' on proxy: trap returned falsish for property 'string'". 1417 | return true; 1418 | }, 1419 | 1420 | // Intercept all delete calls. 1421 | deleteProperty: function deleteProperty(target, property) { 1422 | _this23[property] = undefined; 1423 | return true; 1424 | } 1425 | }); 1426 | } 1427 | 1428 | // Populate schema defaults into object. 1429 | _.each(schema, function (properties, index) { 1430 | if (properties.default !== undefined) { 1431 | // Temporarily ensure readOnly is turned off to prevent the set from failing. 1432 | var readOnly = properties.readOnly; 1433 | properties.readOnly = false; 1434 | _this23[index] = _.isFunction(properties.default) ? properties.default.call(_this23) : properties.default; 1435 | properties.readOnly = readOnly; 1436 | } 1437 | }); 1438 | 1439 | // Call default constructor. 1440 | _private._options.constructors.default.call(this, values); 1441 | 1442 | // May return actual object instance or Proxy, depending on harmony support. 1443 | return _private._this; 1444 | } 1445 | 1446 | // Populate values. 1447 | 1448 | 1449 | _createClass(SchemaObjectInstance, [{ 1450 | key: 'populate', 1451 | value: function populate(values) { 1452 | for (var key in values) { 1453 | this[_privateKey]._this[key] = values[key]; 1454 | } 1455 | } 1456 | 1457 | // Clone and return SchemaObject. 1458 | 1459 | }, { 1460 | key: 'clone', 1461 | value: function clone() { 1462 | return new SchemaObjectInstance(this.toObject(), this[_privateKey]._root); 1463 | } 1464 | 1465 | // Return object without getter/typecasts, extra properties, etc. 1466 | 1467 | }, { 1468 | key: 'toObject', 1469 | value: function toObject() { 1470 | var _this24 = this; 1471 | 1472 | var options = this[_privateKey]._options; 1473 | var getObj = {}; 1474 | 1475 | // Populate all properties in schema. 1476 | _.each(this[_privateKey]._schema, function (properties, index) { 1477 | // Do not write values to object that are marked as invisible. 1478 | if (properties.invisible) { 1479 | return; 1480 | } 1481 | 1482 | // Fetch value through the public interface. 1483 | var value = _this24[_privateKey]._this[index]; 1484 | 1485 | // Do not write undefined values to the object because of strange behavior when using with MongoDB. 1486 | // MongoDB will convert undefined to null and overwrite existing values in that field. 1487 | if (value === undefined && options.setUndefined !== true) { 1488 | return; 1489 | } 1490 | 1491 | // Clone objects so they can't be modified by reference. 1492 | if (_.isObject(value)) { 1493 | if (value._isSchemaObject) { 1494 | value = value.toObject(); 1495 | } else if (value._isSchemaArray) { 1496 | value = value.toArray(); 1497 | } else if (_.isArray(value)) { 1498 | value = value.splice(0); 1499 | } else if (_.isDate(value)) { 1500 | // https://github.com/documentcloud/underscore/pull/863 1501 | // _.clone doesn't work on Date object. 1502 | getObj[index] = new Date(value.getTime()); 1503 | } else { 1504 | value = _.clone(value); 1505 | } 1506 | 1507 | // Don't write empty objects or arrays. 1508 | if (!_.isDate(value) && !options.setUndefined && !_.size(value)) { 1509 | return; 1510 | } 1511 | } 1512 | 1513 | // Write to object. 1514 | getObj[index] = value; 1515 | }); 1516 | 1517 | // If options contains toObject, pass through before returning final object. 1518 | if (_.isFunction(options.toObject)) { 1519 | getObj = options.toObject.call(this, getObj); 1520 | } 1521 | 1522 | return getObj; 1523 | } 1524 | 1525 | // toJSON is an interface used by JSON.stringify. 1526 | // Return the raw object if called. 1527 | 1528 | }, { 1529 | key: 'toJSON', 1530 | value: function toJSON() { 1531 | return this.toObject(); 1532 | } 1533 | 1534 | // Clear all values. 1535 | 1536 | }, { 1537 | key: 'clear', 1538 | value: function clear() { 1539 | var _this25 = this; 1540 | 1541 | _.each(this[_privateKey]._schema, function (properties, index) { 1542 | clearField.call(_this25[_privateKey]._this, index, properties); 1543 | }); 1544 | } 1545 | 1546 | // Get all errors. 1547 | 1548 | }, { 1549 | key: 'getErrors', 1550 | value: function getErrors() { 1551 | var _this26 = this; 1552 | 1553 | var errors = []; 1554 | var _iteratorNormalCompletion2 = true; 1555 | var _didIteratorError2 = false; 1556 | var _iteratorError2 = undefined; 1557 | 1558 | try { 1559 | for (var _iterator2 = this[_privateKey]._errors[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 1560 | var error = _step2.value; 1561 | 1562 | error = _.cloneDeep(error); 1563 | error.schemaObject = this; 1564 | errors.push(error); 1565 | } 1566 | } catch (err) { 1567 | _didIteratorError2 = true; 1568 | _iteratorError2 = err; 1569 | } finally { 1570 | try { 1571 | if (!_iteratorNormalCompletion2 && _iterator2.return) { 1572 | _iterator2.return(); 1573 | } 1574 | } finally { 1575 | if (_didIteratorError2) { 1576 | throw _iteratorError2; 1577 | } 1578 | } 1579 | } 1580 | 1581 | _.each(this[_privateKey]._schema, function (properties, index) { 1582 | var required = properties.required; 1583 | var message = index + ' is required but not provided'; 1584 | 1585 | //If required is an array, set custom message 1586 | if (Array.isArray(required)) { 1587 | message = required[1] || message; 1588 | required = required[0]; 1589 | } 1590 | //Skip if required does not exist 1591 | if (!required) { 1592 | return; 1593 | } 1594 | //Skip if required is a function, but returns false 1595 | else if (typeof required === 'function' && !required.call(_this26)) { 1596 | return; 1597 | } 1598 | 1599 | //Skip if property has a value, is a boolean set to false, or if it's falsy and falsy values are allowed 1600 | if (_this26[index] || typeof _this26[index] === 'boolean' || _this26[_privateKey]._options.allowFalsyValues && _this26[index] !== undefined) { 1601 | return; 1602 | } 1603 | 1604 | var error = new SetterError(message, _this26[index], _this26[index], properties); 1605 | error.schemaObject = _this26; 1606 | errors.push(error); 1607 | }); 1608 | 1609 | // Look for sub-SchemaObjects. 1610 | for (var name in this[_privateKey]._schema) { 1611 | var field = this[_privateKey]._schema[name]; 1612 | if (field.type === 'object' && typeof field.objectType === 'function') { 1613 | var subErrors = this[name].getErrors(); 1614 | var _iteratorNormalCompletion3 = true; 1615 | var _didIteratorError3 = false; 1616 | var _iteratorError3 = undefined; 1617 | 1618 | try { 1619 | for (var _iterator3 = subErrors[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { 1620 | var subError = _step3.value; 1621 | 1622 | subError.fieldSchema.name = name + '.' + subError.fieldSchema.name; 1623 | subError.schemaObject = this; 1624 | errors.push(subError); 1625 | } 1626 | } catch (err) { 1627 | _didIteratorError3 = true; 1628 | _iteratorError3 = err; 1629 | } finally { 1630 | try { 1631 | if (!_iteratorNormalCompletion3 && _iterator3.return) { 1632 | _iterator3.return(); 1633 | } 1634 | } finally { 1635 | if (_didIteratorError3) { 1636 | throw _iteratorError3; 1637 | } 1638 | } 1639 | } 1640 | } 1641 | } 1642 | 1643 | return errors; 1644 | } 1645 | 1646 | // Clear all errors 1647 | 1648 | }, { 1649 | key: 'clearErrors', 1650 | value: function clearErrors() { 1651 | this[_privateKey]._errors.length = 0; 1652 | 1653 | // Look for sub-SchemaObjects. 1654 | for (var name in this[_privateKey]._schema) { 1655 | var field = this[_privateKey]._schema[name]; 1656 | if (field.type === 'object' && typeof field.objectType === 'function') { 1657 | this[name].clearErrors(); 1658 | } 1659 | } 1660 | } 1661 | 1662 | // Has errors? 1663 | 1664 | }, { 1665 | key: 'isErrors', 1666 | value: function isErrors() { 1667 | return this.getErrors().length > 0; 1668 | } 1669 | 1670 | // Used to detect instance of schema object internally. 1671 | 1672 | }, { 1673 | key: '_isSchemaObject', 1674 | value: function _isSchemaObject() { 1675 | return true; 1676 | } 1677 | }]); 1678 | 1679 | return SchemaObjectInstance; 1680 | }(); 1681 | 1682 | // Add custom methods to factory-generated class. 1683 | 1684 | 1685 | _.each(options.methods, function (method, key) { 1686 | if (SchemaObjectInstance.prototype[key]) { 1687 | throw new Error('Cannot overwrite existing ' + key + ' method with custom method.'); 1688 | } 1689 | SchemaObjectInstance.prototype[key] = method; 1690 | }); 1691 | 1692 | return SchemaObjectInstance; 1693 | } 1694 | 1695 | if ((typeof module === 'undefined' ? 'undefined' : _typeof(module)) === 'object') { 1696 | module.exports = SchemaObject; 1697 | } else if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object') { 1698 | window.SchemaObject = SchemaObject; 1699 | } else { 1700 | throw new Error('[schema-object] Error: module.exports and window are unavailable.'); 1701 | } 1702 | })(); -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const _gulp = require('load-plugins')('gulp-*'); 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | gulp.task('test-node', ['build'], () => { 6 | return gulp.src('test/tests.js', { read: false }) 7 | .pipe(_gulp.mocha({ 8 | reporter: 'spec', 9 | grep: argv.grep 10 | })); 11 | }); 12 | 13 | gulp.task('test-browser', ['build'], () => { 14 | return gulp.src('test/browser.html') 15 | .pipe(_gulp.mochaPhantomjs({ 16 | reporter: 'spec', 17 | mocha: { 18 | grep: argv.grep 19 | }, 20 | phantomjs: { 21 | useColors: true 22 | } 23 | })); 24 | }); 25 | 26 | gulp.task('build', () => { 27 | return gulp.src('lib/*.js') 28 | .pipe(_gulp.babel()) 29 | .pipe(gulp.dest('dist')); 30 | }); 31 | 32 | gulp.task('default', ['build']); -------------------------------------------------------------------------------- /lib/schemaobject.js: -------------------------------------------------------------------------------- 1 | (function (_) { 2 | 'use strict'; 3 | 4 | const _isProxySupported = typeof Proxy !== 'undefined' && 5 | Proxy.toString().indexOf('proxies not supported on this platform') === -1; 6 | 7 | // Use require conditionally, otherwise assume global dependencies. 8 | if (typeof require !== 'undefined') { 9 | _ = require('lodash'); 10 | 11 | if (!global._babelPolyfill) { 12 | // Necessary to do this instead of runtime transformer for browser compatibility. 13 | require('babel-polyfill'); 14 | } 15 | 16 | // Patch the harmony-era (pre-ES6) Proxy object to be up-to-date with the ES6 spec. 17 | // Without the --harmony and --harmony_proxies flags, options strict: false and dotNotation: true will fail with exception. 18 | if (_isProxySupported === true) { 19 | require('harmony-reflect'); 20 | } 21 | } else { 22 | _ = window._; 23 | } 24 | 25 | // If reflection is being used, our traps will hide internal properties. 26 | // If reflection is not being used, Symbol will hide internal properties. 27 | const _privateKey = _isProxySupported === true ? '_private' : Symbol('_private'); 28 | 29 | // Reserved fields, map to internal property. 30 | const _reservedFields = ['super']; 31 | 32 | // Is a number (ignores type). 33 | function isNumeric(n) { 34 | return !isNaN(parseFloat(n)) && isFinite(n); 35 | } 36 | 37 | // Used to get real index name. 38 | function getIndex(index) { 39 | if (this[_privateKey]._options.keysIgnoreCase && typeof index === 'string') { 40 | const indexLowerCase = index.toLowerCase(); 41 | for (const key in this[_privateKey]._schema) { 42 | if (typeof key === 'string' && key.toLowerCase() === indexLowerCase) { 43 | return key; 44 | } 45 | } 46 | } 47 | 48 | return index; 49 | } 50 | 51 | // Used to fetch current values. 52 | function getter(value, properties) { 53 | // Most calculations happen within the typecast and the value passed is typically the value we want to use. 54 | // Typically, the getter just returns the value. 55 | // Modifications to the value within the getter are not written to the object. 56 | 57 | // Getter can transform value after typecast. 58 | if (properties.getter) { 59 | value = properties.getter.call(this[_privateKey]._root, value); 60 | } 61 | 62 | return value; 63 | } 64 | 65 | // Used to write value to object. 66 | function writeValue(value, fieldSchema) { 67 | // onBeforeValueSet allows you to cancel the operation. 68 | // It doesn't work like transform and others that allow you to modify the value because all typecast has already happened. 69 | // For use-cases where you need to modify the value, you can set a new value in the handler and return false. 70 | if (this[_privateKey]._options.onBeforeValueSet) { 71 | if (this[_privateKey]._options.onBeforeValueSet.call(this, value, fieldSchema.name) === false) { 72 | return; 73 | } 74 | } 75 | 76 | // Alias simply copies the value without actually writing it to alias index. 77 | // Because the value isn't actually set on the alias index, onValueSet isn't fired. 78 | if (fieldSchema.type === 'alias') { 79 | this[fieldSchema.index] = value; 80 | return; 81 | } 82 | 83 | // Write the value to the inner object. 84 | this[_privateKey]._obj[fieldSchema.name] = value; 85 | 86 | // onValueSet notifies you after a value has been written. 87 | if (this[_privateKey]._options.onValueSet) { 88 | this[_privateKey]._options.onValueSet.call(this, value, fieldSchema.name); 89 | } 90 | } 91 | 92 | // Represents an error encountered when trying to set a value. 93 | // Code 1xxx 94 | class SetterError { 95 | constructor(errorMessage, setValue, originalValue, fieldSchema) { 96 | this.errorMessage = errorMessage; 97 | this.setValue = setValue; 98 | this.originalValue = originalValue; 99 | this.fieldSchema = fieldSchema; 100 | this.errorCode = this.constructor.errorCode(); 101 | } 102 | 103 | static errorCode() { 104 | return 1000; 105 | } 106 | } 107 | 108 | // Cast Error Base 109 | // Thrown when a value cannot be cast to the type specified by the schema 110 | // Code 11xx 111 | class CastError extends SetterError { 112 | constructor(errorMessage, setValue, originalValue, fieldSchema) { 113 | super(errorMessage, setValue, originalValue, fieldSchema); 114 | this.errorType = 'CastError'; 115 | } 116 | static errorCode() { 117 | return 1100; 118 | } 119 | } 120 | 121 | class StringCastError extends CastError { 122 | constructor(setValue, originalValue, fieldSchema) { 123 | let errorMessage = 'String type cannot typecast Object or Array types.'; 124 | super(errorMessage, setValue, originalValue, fieldSchema); 125 | } 126 | static errorCode() { 127 | return 1101; 128 | } 129 | } 130 | 131 | class NumberCastError extends CastError { 132 | constructor(sourceType, setValue, originalValue, fieldSchema) { 133 | let errorMessage = 'Number could not be typecast from the provided ' + sourceType; 134 | super(errorMessage, setValue, originalValue, fieldSchema); 135 | } 136 | static errorCode() { 137 | return 1102; 138 | } 139 | } 140 | 141 | class ArrayCastError extends CastError { 142 | constructor(setValue, originalValue, fieldSchema) { 143 | let errorMessage = 'Array type cannot typecast non-Array types.'; 144 | super(errorMessage, setValue, originalValue, fieldSchema); 145 | } 146 | static errorCode() { 147 | return 1103; 148 | } 149 | } 150 | 151 | class ObjectCastError extends CastError { 152 | constructor(setValue, originalValue, fieldSchema) { 153 | let errorMessage = 'Object type cannot typecast non-Object types.'; 154 | super(errorMessage, setValue, originalValue, fieldSchema); 155 | } 156 | static errorCode() { 157 | return 1104; 158 | } 159 | } 160 | 161 | class DateCastError extends CastError { 162 | constructor(setValue, originalValue, fieldSchema) { 163 | let errorMessage = 'Date type cannot typecast Array or Object types.'; 164 | super(errorMessage, setValue, originalValue, fieldSchema); 165 | } 166 | static errorCode() { 167 | return 1105; 168 | } 169 | } 170 | 171 | // Validation error base 172 | // Thrown when a value does not meet the validation criteria set by the schema 173 | // Code 12xx 174 | class ValidationError extends SetterError { 175 | constructor(errorMessage, setValue, originalValue, fieldSchema) { 176 | super(errorMessage, setValue, originalValue, fieldSchema); 177 | this.errorType = 'ValidationError'; 178 | } 179 | static errorCode() { 180 | return 1200; 181 | } 182 | } 183 | 184 | /** 185 | * String Validation Errors 186 | * Codes 121x 187 | */ 188 | 189 | class StringValidationError extends ValidationError { 190 | constructor(errorMessage, setValue, originalValue, fieldSchema) { 191 | super(errorMessage, setValue, originalValue, fieldSchema); 192 | } 193 | static errorCode() { 194 | return 1210; 195 | } 196 | } 197 | 198 | class StringEnumValidationError extends StringValidationError { 199 | constructor(errorMessage, setValue, originalValue, fieldSchema) { 200 | errorMessage = errorMessage || 'String does not exist in enum list.'; 201 | super(errorMessage, setValue, originalValue, fieldSchema); 202 | } 203 | static errorCode() { 204 | return 1211; 205 | } 206 | } 207 | 208 | class StringMinLengthValidationError extends StringValidationError { 209 | constructor(errorMessage, setValue, originalValue, fieldSchema) { 210 | errorMessage = errorMessage || 'String length too short to meet minLength requirement.'; 211 | super(errorMessage, setValue, originalValue, fieldSchema); 212 | } 213 | static errorCode() { 214 | return 1212; 215 | } 216 | } 217 | 218 | class StringMaxLengthValidationError extends StringValidationError { 219 | constructor(errorMessage, setValue, originalValue, fieldSchema) { 220 | errorMessage = errorMessage || 'String length too long to meet maxLength requirement.'; 221 | super(errorMessage, setValue, originalValue, fieldSchema); 222 | } 223 | static errorCode() { 224 | return 1213; 225 | } 226 | } 227 | 228 | class StringRegexValidationError extends StringValidationError { 229 | constructor(errorMessage, setValue, originalValue, fieldSchema) { 230 | errorMessage = errorMessage || 'String does not match regular expression pattern.'; 231 | super(errorMessage, setValue, originalValue, fieldSchema); 232 | } 233 | static errorCode() { 234 | return 1214; 235 | } 236 | } 237 | 238 | /** 239 | * Number Validation Errors 240 | * Codes 122x 241 | */ 242 | 243 | class NumberValidationError extends ValidationError { 244 | constructor(errorMessage, setValue, originalValue, fieldSchema) { 245 | super(errorMessage, setValue, originalValue, fieldSchema); 246 | } 247 | static errorCode() { 248 | return 1220; 249 | } 250 | } 251 | 252 | class NumberMinValidationError extends NumberValidationError { 253 | constructor(errorMessage, setValue, originalValue, fieldSchema) { 254 | errorMessage = errorMessage || 'Number is too small to meet min requirement.'; 255 | super(errorMessage, setValue, originalValue, fieldSchema); 256 | } 257 | static errorCode() { 258 | return 1221; 259 | } 260 | } 261 | 262 | class NumberMaxValidationError extends NumberValidationError { 263 | constructor(errorMessage, setValue, originalValue, fieldSchema) { 264 | errorMessage = errorMessage || 'Number is too big to meet max requirement.'; 265 | super(errorMessage, setValue, originalValue, fieldSchema); 266 | } 267 | static errorCode() { 268 | return 1222; 269 | } 270 | } 271 | 272 | /** 273 | * Date Validation Errors 274 | * Codes 123x 275 | */ 276 | 277 | class DateValidationError extends ValidationError { 278 | constructor(errorMessage, setValue, originalValue, fieldSchema) { 279 | super(errorMessage, setValue, originalValue, fieldSchema); 280 | } 281 | static errorCode() { 282 | return 1230; 283 | } 284 | } 285 | 286 | class DateParseValidationError extends DateValidationError { 287 | constructor(errorMessage, setValue, originalValue, fieldSchema) { 288 | errorMessage = errorMessage || 'Could not parse date.'; 289 | super(errorMessage, setValue, originalValue, fieldSchema); 290 | } 291 | static errorCode() { 292 | return 1231; 293 | } 294 | } 295 | 296 | 297 | // Returns typecasted value if possible. If rejected, originalValue is returned. 298 | function typecast(value, originalValue, properties) { 299 | const options = this[_privateKey]._options; 300 | 301 | // Allow transform to manipulate raw properties. 302 | if (properties.transform) { 303 | value = properties.transform.call(this[_privateKey]._root, value, originalValue, properties); 304 | } 305 | 306 | // Allow null to be preserved. 307 | if (value === null && options.preserveNull) { 308 | return null; 309 | } 310 | 311 | // Helper function designed to detect and handle usage of array-form custom error messages for validators 312 | function detectCustomErrorMessage(key) { 313 | if (typeof properties[key] === 'object' && properties[key].errorMessage && properties[key].value) { 314 | return properties[key]; 315 | } 316 | else if (_.isArray(properties[key])) { 317 | return { 318 | value: properties[key][0], 319 | errorMessage: properties[key][1] 320 | }; 321 | } 322 | else { 323 | return { 324 | value: properties[key], 325 | errorMessage: undefined 326 | }; 327 | } 328 | } 329 | 330 | // Property types are always normalized as lowercase strings despite shorthand definitions being available. 331 | switch (properties.type) { 332 | case 'string': 333 | // Reject if object or array. 334 | if (_.isObject(value) || _.isArray(value)) { 335 | throw new StringCastError(value, originalValue, properties); 336 | } 337 | 338 | // If index is being set with null or undefined, set value and end. 339 | if (value === undefined || value === null) { 340 | return undefined; 341 | } 342 | 343 | // Typecast to String. 344 | value = value + ''; 345 | 346 | // If stringTransform function is defined, use. 347 | // This is used before we do validation checks (except to be sure we have a string at all). 348 | if (properties.stringTransform) { 349 | value = properties.stringTransform.call(this[_privateKey]._root, value, originalValue, properties); 350 | } 351 | 352 | // If clip property & maxLength properties are set, the string should be clipped. 353 | // This is basically a shortcut property that could be done with stringTransform. 354 | if (properties.clip !== undefined && properties.maxLength !== undefined) { 355 | value = value.substr(0, properties.maxLength); 356 | } 357 | 358 | let enumValidation; 359 | 360 | // Detect custom error message usage for enum (can't use function here as enum is expected to be an array) 361 | if (typeof properties.enum === 'object' && properties.enum.errorMessage && properties.enum.value) { 362 | enumValidation = properties.enum; 363 | } 364 | else if (_.isArray(properties.enum) && _.isArray(properties.enum[0])) { 365 | enumValidation = { 366 | value: properties.enum[0], 367 | errorMessage: properties.enum[1] 368 | }; 369 | } 370 | else { 371 | enumValidation = { 372 | value: properties.enum, 373 | errorMessage: undefined 374 | }; 375 | } 376 | 377 | // If enum is being used, be sure the value is within definition. 378 | if ( 379 | enumValidation.value !== undefined && 380 | _.isArray(enumValidation.value) && 381 | enumValidation.value.indexOf(value) === -1 382 | ) { 383 | throw new StringEnumValidationError(enumValidation.errorMessage, value, originalValue, properties); 384 | } 385 | 386 | // Detect custom error message usage for minLength 387 | let minLength = detectCustomErrorMessage('minLength'); 388 | 389 | // If minLength is defined, check to be sure the string is > minLength. 390 | if (minLength.value !== undefined && value.length < minLength.value) { 391 | throw new StringMinLengthValidationError(minLength.errorMessage, value, originalValue, properties); 392 | } 393 | 394 | // Detect custom error message usage for maxLength 395 | let maxLength = detectCustomErrorMessage('maxLength'); 396 | 397 | // If maxLength is defined, check to be sure the string is < maxLength. 398 | if (maxLength.value !== undefined && value.length > maxLength.value) { 399 | throw new StringMaxLengthValidationError(maxLength.errorMessage, value, originalValue, properties); 400 | } 401 | 402 | // Detect custom error message usage for maxLength 403 | let regex = detectCustomErrorMessage('regex'); 404 | 405 | // If regex is defined, check to be sure the string matches the regex pattern. 406 | if (regex.value && !regex.value.test(value)) { 407 | throw new StringRegexValidationError(regex.errorMessage, value, originalValue, properties); 408 | } 409 | 410 | return value; 411 | 412 | case 'number': 413 | // If index is being set with null, undefined, or empty string: clear value. 414 | if (value === undefined || value === null || value === '') { 415 | return undefined; 416 | } 417 | 418 | // Set values for boolean. 419 | if (_.isBoolean(value)) { 420 | value = value ? 1 : 0; 421 | } 422 | 423 | // Remove/convert number group separators 424 | if (typeof value === 'string') { 425 | if (options.useDecimalNumberGroupSeparator) { 426 | // Remove decimals 427 | value = value.replace(/\./g, ''); 428 | // Replace commas with decimals for js parsing 429 | value = value.replace(/,/g, '.'); 430 | } 431 | else { 432 | //Remove commas 433 | value = value.replace(/,/g, ''); 434 | } 435 | 436 | // Reject if string was not a valid number 437 | if (isNaN(Number(value))) { 438 | throw new NumberCastError('String', value, originalValue, properties); 439 | } 440 | } 441 | 442 | // Reject if array, object, or not numeric. 443 | if (_.isArray(value)) { 444 | throw new NumberCastError('Array', value, originalValue, properties); 445 | } 446 | else if (_.isObject(value)) { 447 | throw new NumberCastError('Object', value, originalValue, properties); 448 | } 449 | else if (!isNumeric(value)) { 450 | throw new NumberCastError('Non-numeric', value, originalValue, properties); 451 | } 452 | 453 | // Typecast to number. 454 | value = Number(value); 455 | 456 | // Transformation after typecasting but before validation and filters. 457 | if (properties.numberTransform) { 458 | value = properties.numberTransform.call(this[_privateKey]._root, value, originalValue, properties); 459 | } 460 | 461 | // Detect custom error message usage for min 462 | let min = detectCustomErrorMessage('min'); 463 | 464 | if (min.value !== undefined && value < min.value) { 465 | throw new NumberMinValidationError(min.errorMessage, value, originalValue, properties); 466 | } 467 | 468 | // Detect custom error message usage for min 469 | let max = detectCustomErrorMessage('max'); 470 | 471 | if (max.value !== undefined && value > max.value) { 472 | throw new NumberMaxValidationError(max.errorMessage, value, originalValue, properties); 473 | } 474 | 475 | return value; 476 | 477 | case 'boolean': 478 | // If index is being set with null, undefined, or empty string: clear value. 479 | if (value === undefined || value === null || value === '') { 480 | return undefined; 481 | } 482 | 483 | // If is String and is 'false', convert to Boolean. 484 | if (value === 'false') { 485 | return false; 486 | } 487 | 488 | // If is Number, <0 is true and >0 is false. 489 | if (isNumeric(value)) { 490 | return (value * 1) > 0; 491 | } 492 | 493 | // Use Javascript to eval and return boolean. 494 | value = !!value; 495 | 496 | // Transformation after typecasting but before validation and filters. 497 | if (properties.booleanTransform) { 498 | value = properties.booleanTransform.call(this[_privateKey]._root, value, originalValue, properties); 499 | } 500 | 501 | return value; 502 | 503 | case 'array': 504 | // If it's an object, typecast to an array and return array. 505 | if (_.isObject(value)) { 506 | value = _.toArray(value); 507 | } 508 | 509 | // Reject if not array. 510 | if (!_.isArray(value)) { 511 | throw new ArrayCastError(value, originalValue, properties); 512 | } 513 | 514 | // Arrays are never set directly. 515 | // Instead, the values are copied over to the existing SchemaArray instance. 516 | // The SchemaArray is initialized immediately and will always exist. 517 | originalValue.length = 0; 518 | _.each(value, (arrayValue) => { 519 | originalValue.push(arrayValue); 520 | }); 521 | 522 | return originalValue; 523 | 524 | case 'object': 525 | // If it's not an Object, reject. 526 | if (!_.isObject(value)) { 527 | throw new ObjectCastError(value, originalValue, properties); 528 | } 529 | 530 | // If object is schema object and an entirely new object was passed, clear values and set. 531 | // This preserves the object instance. 532 | if (properties.objectType) { 533 | // The object will usually exist because it's initialized immediately for deep access within SchemaObjects. 534 | // However, in the case of Array elements, it will not exist. 535 | let schemaObject; 536 | if (originalValue !== undefined) { 537 | // Clear existing values. 538 | schemaObject = originalValue; 539 | schemaObject.clear(); 540 | } else { 541 | // The SchemaObject doesn't exist yet. Let's initialize a new one. 542 | // This is used for Array types. 543 | schemaObject = new properties.objectType({}, this[_privateKey]._root); 544 | } 545 | 546 | // Copy value to SchemaObject and set value to SchemaObject. 547 | for (const key in value) { 548 | schemaObject[key] = value[key]; 549 | } 550 | value = schemaObject; 551 | } 552 | 553 | // Otherwise, it's OK. 554 | return value; 555 | 556 | case 'date': 557 | // If index is being set with null, undefined, or empty string: clear value. 558 | if (value === undefined || value === null || value === '') { 559 | return undefined; 560 | } 561 | 562 | // Reject if object, array or boolean. 563 | if (!_.isDate(value) && !_.isString(value) && !_.isNumber(value)) { 564 | throw new DateCastError(value, originalValue, properties); 565 | } 566 | 567 | // Attempt to parse string value with Date.parse (which returns number of milliseconds). 568 | if (_.isString(value)) { 569 | value = Date.parse(value); 570 | } 571 | 572 | // If is timestamp, convert to Date. 573 | if (isNumeric(value)) { 574 | value = new Date((value + '').length > 10 ? value : value * 1000); 575 | } 576 | 577 | // If the date couldn't be parsed, do not modify index. 578 | if (value == 'Invalid Date' || !_.isDate(value)) { 579 | throw new DateParseValidationError(null, value, originalValue, properties); 580 | } 581 | 582 | // Transformation after typecasting but before validation and filters. 583 | if (properties.dateTransform) { 584 | value = properties.dateTransform.call(this[_privateKey]._root, value, originalValue, properties); 585 | } 586 | 587 | return value; 588 | 589 | default: // 'any' 590 | return value; 591 | } 592 | } 593 | 594 | // Properties can be passed in multiple forms (an object, just a type, etc). 595 | // Normalize to a standard format. 596 | function normalizeProperties(properties, name) { 597 | // Allow for shorthand type declaration: 598 | 599 | // Check to see if the user passed in a raw type of a properties hash. 600 | if (properties) { 601 | // Raw type passed. 602 | // index: Type is translated to index: {type: Type} 603 | // Properties hash created. 604 | if (properties.type === undefined) { 605 | properties = { 606 | type: properties 607 | }; 608 | 609 | // Properties hash passed. 610 | // Copy properties hash before modifying. 611 | // Users can pass in their own custom types to the schema and we don't want to write to that object. 612 | // Especially since properties.name contains the index of our field and copying that will break functionality. 613 | } else { 614 | properties = _.cloneDeep(properties); 615 | } 616 | } 617 | 618 | // Type may be an object with properties. 619 | // If "type.type" exists, we'll assume it's meant to be properties. 620 | // This means that shorthand objects can't use the "type" index. 621 | // If "type" is necessary, they must be wrapped in a SchemaObject. 622 | if (_.isObject(properties.type) && properties.type.type !== undefined) { 623 | _.each(properties.type, (value, key) => { 624 | if (properties[key] === undefined) { 625 | properties[key] = value; 626 | } 627 | }); 628 | properties.type = properties.type.type; 629 | } 630 | 631 | // Null or undefined should be flexible and allow any value. 632 | if (properties.type === null || properties.type === undefined) { 633 | properties.type = 'any'; 634 | 635 | // Convert object representation of type to lowercase string. 636 | // String is converted to 'string', Number to 'number', etc. 637 | // Do not convert the initialized SchemaObjectInstance to a string! 638 | // Check for a shorthand declaration of schema by key length. 639 | } else if (_.isString(properties.type.name) && properties.type.name !== 'SchemaObjectInstance' && 640 | Object.keys(properties.type).length === 0) { 641 | properties.type = properties.type.name; 642 | } 643 | if (_.isString(properties.type)) { 644 | properties.type = properties.type.toLowerCase(); 645 | } 646 | 647 | // index: [Type] or index: [] is translated to index: {type: Array, arrayType: Type} 648 | if (_.isArray(properties.type)) { 649 | if (_.size(properties.type)) { 650 | // Properties will be normalized when array is initialized. 651 | properties.arrayType = properties.type[0]; 652 | } 653 | properties.type = 'array'; 654 | } 655 | 656 | // index: {} or index: SchemaObject is translated to index: {type: Object, objectType: Type} 657 | if (!_.isString(properties.type)) { 658 | if (_.isFunction(properties.type)) { 659 | properties.objectType = properties.type; 660 | properties.type = 'object'; 661 | } else if (_.isObject(properties.type)) { 662 | // When an empty object is passed, no schema is enforced. 663 | if (_.size(properties.type)) { 664 | // Options should be inherited by sub-SchemaObjects, except toObject. 665 | const options = _.clone(this[_privateKey]._options); 666 | delete options.toObject; 667 | 668 | // When we're creating a nested schema automatically, it should always inherit the root "this". 669 | options.inheritRootThis = true; 670 | 671 | // Initialize the SchemaObject sub-schema automatically. 672 | properties.objectType = new SchemaObject(properties.type, options); 673 | } 674 | 675 | // Regardless of if we created a sub-schema or not, the field is indexed as an object. 676 | properties.type = 'object'; 677 | } 678 | } 679 | 680 | // Set name if passed on properties. 681 | // It's used to show what field an error what generated on. 682 | if (name) { 683 | properties.name = name; 684 | } 685 | 686 | return properties; 687 | } 688 | 689 | // Add field to schema and initializes getter and setter for the field. 690 | function addToSchema(index, properties) { 691 | this[_privateKey]._schema[index] = normalizeProperties.call(this, properties, index); 692 | 693 | defineGetter.call(this[_privateKey]._getset, index, this[_privateKey]._schema[index]); 694 | defineSetter.call(this[_privateKey]._getset, index, this[_privateKey]._schema[index]); 695 | } 696 | 697 | // Defines getter for specific field. 698 | function defineGetter(index, properties) { 699 | // If the field type is an alias, we retrieve the value through the alias's index. 700 | let indexOrAliasIndex = properties.type === 'alias' ? properties.index : index; 701 | 702 | this.__defineGetter__(index, () => { 703 | // If accessing object or array, lazy initialize if not set. 704 | if (!this[_privateKey]._obj[indexOrAliasIndex] && (properties.type === 'object' || properties.type === 'array')) { 705 | // Initialize object. 706 | if (properties.type === 'object') { 707 | if (properties.default !== undefined) { 708 | writeValue.call(this[_privateKey]._this, _.isFunction(properties.default) ? 709 | properties.default.call(this) : 710 | properties.default, properties); 711 | } else { 712 | writeValue.call(this[_privateKey]._this, 713 | properties.objectType ? new properties.objectType({}, this[_privateKey]._root) : {}, properties); 714 | } 715 | 716 | // Native arrays are not used so that Array class can be extended with custom behaviors. 717 | } else if (properties.type === 'array') { 718 | writeValue.call(this[_privateKey]._this, new SchemaArray(this, properties), properties); 719 | } 720 | } 721 | 722 | try { 723 | return getter.call(this, this[_privateKey]._obj[indexOrAliasIndex], properties); 724 | } catch (error) { 725 | // This typically happens when the default value isn't valid -- log error. 726 | this[_privateKey]._errors.push(error); 727 | } 728 | }); 729 | } 730 | 731 | // Defines setter for specific field. 732 | function defineSetter(index, properties) { 733 | this.__defineSetter__(index, (value) => { 734 | // Don't proceed if readOnly is true. 735 | if (properties.readOnly) { 736 | return; 737 | } 738 | 739 | try { 740 | // this[_privateKey]._this[index] is used instead of this[_privateKey]._obj[index] to route through the public interface. 741 | writeValue.call(this[_privateKey]._this, 742 | typecast.call(this, value, this[_privateKey]._this[index], properties), properties); 743 | } catch (error) { 744 | // Setter failed to validate value -- log error. 745 | this[_privateKey]._errors.push(error); 746 | } 747 | }); 748 | } 749 | 750 | // Reset field to default value. 751 | function clearField(index, properties) { 752 | // Aliased fields reflect values on other fields and do not need to be cleared. 753 | if (properties.isAlias === true) { 754 | return; 755 | } 756 | 757 | // In case of object & array, they must be initialized immediately. 758 | if (properties.type === 'object') { 759 | this[properties.name].clear(); 760 | 761 | // Native arrays are never used so that toArray can be globally supported. 762 | // Additionally, other properties such as unique rely on passing through SchemaObject. 763 | } else if (properties.type === 'array') { 764 | this[properties.name].length = 0; 765 | 766 | // Other field types can simply have their value set to undefined. 767 | } else { 768 | writeValue.call(this[_privateKey]._this, undefined, properties); 769 | } 770 | } 771 | 772 | // Represents a basic array with typecasted values. 773 | class SchemaArray extends Array { 774 | constructor(self, properties) { 775 | super(); 776 | 777 | // Store all internals. 778 | const _private = this[_privateKey] = {}; 779 | 780 | // Store reference to self. 781 | _private._self = self; 782 | 783 | // Store properties (arrayType, unique, etc). 784 | _private._properties = properties; 785 | 786 | // Normalize our own properties. 787 | if (properties.arrayType) { 788 | properties.arrayType = normalizeProperties.call(self, properties.arrayType); 789 | } 790 | } 791 | 792 | push(...args) { 793 | // Values are passed through the typecast before being allowed onto the array if arrayType is set. 794 | // In the case of rejection, the typecast returns undefined, which is not appended to the array. 795 | let values; 796 | if (this[_privateKey]._properties.arrayType) { 797 | values = [].map.call(args, (value) => { 798 | return typecast.call(this[_privateKey]._self, value, undefined, this[_privateKey]._properties.arrayType); 799 | }, this); 800 | } else { 801 | values = args; 802 | } 803 | 804 | // Enforce filter. 805 | if (this[_privateKey]._properties.filter) { 806 | values = _.filter(values, (value) => this[_privateKey]._properties.filter.call(this, value)); 807 | } 808 | 809 | // Enforce uniqueness. 810 | if (this[_privateKey]._properties.unique) { 811 | values = _.difference(values, _.toArray(this)); 812 | } 813 | 814 | return Array.prototype.push.apply(this, values); 815 | } 816 | 817 | concat(...args) { 818 | // Return new instance of SchemaArray. 819 | const schemaArray = new SchemaArray(this[_privateKey]._self, this[_privateKey]._properties); 820 | 821 | // Create primitive array with all elements. 822 | let array = this.toArray(); 823 | 824 | for (const i in args) { 825 | if (args[i].toArray) { 826 | args[i] = args[i].toArray(); 827 | } 828 | array = array.concat(args[i]); 829 | } 830 | 831 | // Push each value in individually to typecast. 832 | for (const i in array) { 833 | schemaArray.push(array[i]); 834 | } 835 | 836 | return schemaArray; 837 | } 838 | 839 | toArray() { 840 | // Create new Array to hold elements. 841 | const array = []; 842 | 843 | // Loop through each element, clone if necessary. 844 | _.each(this, (element) => { 845 | // Call toObject() method if defined (this allows us to return primitive objects instead of SchemaObjects). 846 | if (_.isObject(element) && _.isFunction(element.toObject)) { 847 | element = element.toObject(); 848 | 849 | // If is non-SchemaType object, shallow clone so that properties modification don't have an affect on the original object. 850 | } else if (_.isObject(element)) { 851 | element = _.clone(element); 852 | } 853 | 854 | array.push(element); 855 | }); 856 | 857 | return array; 858 | } 859 | 860 | toJSON() { 861 | return this.toArray(); 862 | } 863 | 864 | // Used to detect instance of SchemaArray internally. 865 | _isSchemaArray() { 866 | return true; 867 | } 868 | } 869 | 870 | // Represents an object FACTORY with typed indexes. 871 | class SchemaObject { 872 | constructor(schema, options = {}) { 873 | // Create object for options if doesn't exist and merge with defaults. 874 | options = _.extend({ 875 | // By default, allow only values in the schema to be set. 876 | // When this is false, setting new fields will dynamically add the field to the schema as type "any". 877 | strict: true, 878 | 879 | // Allow fields to be set via dotNotation; obj['user.name'] = 'Scott'; -> obj: { user: 'Scott' } 880 | dotNotation: false, 881 | 882 | // Do not set undefined values to keys within toObject(). 883 | // This is the default because MongoDB will convert undefined to null and overwrite existing values. 884 | // If this is true, toObject() will output undefined for unset primitives and empty arrays/objects for those types. 885 | // If this is false, toObject() will not output any keys for unset primitives, arrays, and objects. 886 | setUndefined: false, 887 | 888 | // If this is set to true, null will NOT be converted to undefined automatically. 889 | // In many cases, when people use null, they actually want to unset a value. 890 | // There are rare cases where preserving the null is important. 891 | // Set to true if you are one of those rare cases. 892 | preserveNull: false, 893 | 894 | // Allow "profileURL" to be set with "profileUrl" when set to false 895 | keysIgnoreCase: false, 896 | 897 | // Inherit root object "this" context from parent SchemaObject. 898 | inheritRootThis: false, 899 | 900 | // If this is set to false, require will not allow falsy values such as empty strings 901 | allowFalsyValues: true, 902 | 903 | // This defines the digit group separator used for parsing numbers, it defaults to ',' 904 | // For example 3,043,201.01. However if enabled it swaps commas and decimals to allow parsing 905 | // numbers like 3.043.201,01 906 | useDecimalNumberGroupSeparator: false 907 | 908 | }, options); 909 | 910 | // Some of the options require reflection. 911 | if (_isProxySupported === false) { 912 | if (!options.strict) { 913 | throw new Error('[schema-object] Turning strict mode off requires --harmony flag.'); 914 | } 915 | if (options.dotNotation) { 916 | throw new Error('[schema-object] Dot notation support requires --harmony flag.'); 917 | } 918 | if (options.keysIgnoreCase) { 919 | throw new Error('[schema-object] Keys ignore case support requires --harmony flag.'); 920 | } 921 | } 922 | 923 | // Used at minimum to hold default constructor. 924 | if (!options.constructors) { 925 | options.constructors = {}; 926 | } 927 | 928 | // Default constructor can be overridden. 929 | if (!options.constructors.default) { 930 | // By default, populate runtime values as provided to this instance of object. 931 | options.constructors.default = function (values) { 932 | this.populate(values); 933 | }; 934 | } 935 | 936 | // Create SchemaObject factory. 937 | const SO = SchemaObjectInstanceFactory(schema, options); 938 | 939 | // Add custom constructors. 940 | _.each(options.constructors, (method, key) => { 941 | SO[key] = function () { 942 | // Initialize new SO. 943 | const obj = new SO(); 944 | 945 | // Expose default constructor to populate defaults. 946 | obj[_privateKey]._reservedFields.super = function () { 947 | options.constructors.default.apply(obj, arguments); 948 | }; 949 | 950 | // Call custom constructor. 951 | method.apply(obj, arguments); 952 | 953 | // Cleanup and return SO. 954 | delete obj[_privateKey]._reservedFields.super; 955 | return obj; 956 | }; 957 | }); 958 | 959 | return SO; 960 | } 961 | } 962 | 963 | // Represents an object INSTANCE factory with typed indexes. 964 | function SchemaObjectInstanceFactory(schema, options) { 965 | // Represents an actual instance of an object. 966 | class SchemaObjectInstance { 967 | // Extend instance factory. 968 | static extend(extendSchema, extendOptions = {}) { 969 | // Extend requires reflection. 970 | if (_isProxySupported === false) { 971 | throw new Error('[schema-object] Extending object requires --harmony flag.'); 972 | } 973 | 974 | // Merge schema and options together. 975 | const mergedSchema = _.merge({}, schema, extendSchema); 976 | const mergedOptions = _.merge({}, options, extendOptions); 977 | 978 | // Allow method and constructor to call `this.super()`. 979 | const methodHomes = ['methods', 'constructors']; 980 | for (const methodHome of methodHomes) { 981 | // Ensure object containing methods exists on both provided and original options. 982 | if (_.size(options[methodHome]) && _.size(extendOptions[methodHome])) { 983 | // Loop through each method in the original options. 984 | // It's not necessary to bind `this.super()` for options that didn't already exist. 985 | _.each(options[methodHome], (method, name) => { 986 | // The original option may exist, but was it extended? 987 | if (extendOptions[methodHome][name]) { 988 | // Extend method by creating a binding that takes the `this` context given and adds `self`. 989 | // `self` is a reference to the original method, also bound to the correct `this`. 990 | mergedOptions[methodHome][name] = function () { 991 | this[_privateKey]._reservedFields.super = () => { 992 | return method.apply(this, arguments); 993 | }; 994 | const ret = extendOptions[methodHome][name].apply(this, arguments); 995 | delete this[_privateKey]._reservedFields.super; 996 | return ret; 997 | }; 998 | } 999 | }); 1000 | } 1001 | } 1002 | 1003 | return new SchemaObject(mergedSchema, mergedOptions); 1004 | } 1005 | 1006 | // Construct new instance pre-populated with values. 1007 | constructor(values, _root) { 1008 | // Object used to store internals. 1009 | const _private = this[_privateKey] = {}; 1010 | 1011 | // 1012 | _private._root = options.inheritRootThis ? _root || this : this; 1013 | 1014 | // Object with getters and setters bound. 1015 | _private._getset = this; 1016 | 1017 | // Public version of ourselves. 1018 | // Overwritten with proxy if available. 1019 | _private._this = this; 1020 | 1021 | // Object used to store raw values. 1022 | const obj = _private._obj = {}; 1023 | 1024 | // Schema as defined by constructor. 1025 | _private._schema = schema; 1026 | 1027 | // Errors, retrieved with getErrors(). 1028 | _private._errors = []; 1029 | 1030 | // Options need to be accessible. Shared across ALL INSTANCES. 1031 | _private._options = options; 1032 | 1033 | // Reserved keys for storing internal properties accessible from outside. 1034 | _private._reservedFields = {}; 1035 | 1036 | // Normalize schema properties to allow for shorthand declarations. 1037 | _.each(schema, (properties, index) => { 1038 | schema[index] = normalizeProperties.call(this, properties, index); 1039 | }); 1040 | 1041 | // Define getters/typecasts based off of schema. 1042 | _.each(schema, (properties, index) => { 1043 | // Use getter / typecast to intercept and re-route, transform, etc. 1044 | defineGetter.call(_private._getset, index, properties); 1045 | defineSetter.call(_private._getset, index, properties); 1046 | }); 1047 | 1048 | // Proxy used as interface to object allows to intercept all access. 1049 | // Without Proxy we must register individual getter/typecasts to put any logic in place. 1050 | // With Proxy, we still use the individual getter/typecasts, but also catch values that aren't in the schema. 1051 | if (_isProxySupported === true) { 1052 | const proxy = this[_privateKey]._this = new Proxy(this, { 1053 | // Ensure only public keys are shown. 1054 | ownKeys: (target) => { 1055 | return Object.keys(this.toObject()); 1056 | }, 1057 | 1058 | // Return keys to iterate. 1059 | enumerate: (target) => { 1060 | return Object.keys(this[_privateKey]._this)[Symbol.iterator](); 1061 | }, 1062 | 1063 | // Check to see if key exists. 1064 | has: (target, key) => { 1065 | return !!_private._getset[key]; 1066 | }, 1067 | 1068 | // Ensure correct prototype is returned. 1069 | getPrototypeOf: () => { 1070 | return _private._getset; 1071 | }, 1072 | 1073 | // Ensure readOnly fields are not writeable. 1074 | getOwnPropertyDescriptor: (target, key) => { 1075 | return { 1076 | value: proxy[key], 1077 | writeable: !schema[key] || schema[key].readOnly !== true, 1078 | enumerable: true, 1079 | configurable: true 1080 | }; 1081 | }, 1082 | 1083 | // Intercept all get calls. 1084 | get: (target, name, receiver) => { 1085 | // First check to see if it's a reserved field. 1086 | if (_reservedFields.includes(name)) { 1087 | return this[_privateKey]._reservedFields[name]; 1088 | } 1089 | 1090 | // Support dot notation via lodash. 1091 | if (options.dotNotation && typeof name === 'string' && name.indexOf('.') !== -1) { 1092 | return _.get(this[_privateKey]._this, name); 1093 | } 1094 | 1095 | // Use registered getter without hitting the proxy to avoid creating an infinite loop. 1096 | return this[name]; 1097 | }, 1098 | 1099 | // Intercept all set calls. 1100 | set: (target, name, value, receiver) => { 1101 | // Support dot notation via lodash. 1102 | if (options.dotNotation && typeof name === 'string' && name.indexOf('.') !== -1) { 1103 | return _.set(this[_privateKey]._this, name, value); 1104 | } 1105 | 1106 | // Find real keyname if case sensitivity is off. 1107 | if (options.keysIgnoreCase && !schema[name]) { 1108 | name = getIndex.call(this, name); 1109 | } 1110 | 1111 | if (!schema[name]) { 1112 | if (options.strict) { 1113 | // Strict mode means we don't want to deal with anything not in the schema. 1114 | // TODO: SetterError here. 1115 | return true; 1116 | } else { 1117 | // Add index to schema dynamically when value is set. 1118 | // This is necessary for toObject to see the field. 1119 | addToSchema.call(this, name, { 1120 | type: 'any' 1121 | }); 1122 | } 1123 | } 1124 | 1125 | // This hits the registered setter but bypasses the proxy to avoid an infinite loop. 1126 | this[name] = value; 1127 | 1128 | // Necessary for Node v6.0. Prevents error: 'set' on proxy: trap returned falsish for property 'string'". 1129 | return true; 1130 | }, 1131 | 1132 | // Intercept all delete calls. 1133 | deleteProperty: (target, property) => { 1134 | this[property] = undefined; 1135 | return true; 1136 | } 1137 | }); 1138 | } 1139 | 1140 | // Populate schema defaults into object. 1141 | _.each(schema, (properties, index) => { 1142 | if (properties.default !== undefined) { 1143 | // Temporarily ensure readOnly is turned off to prevent the set from failing. 1144 | const readOnly = properties.readOnly; 1145 | properties.readOnly = false; 1146 | this[index] = _.isFunction(properties.default) ? properties.default.call(this) : properties.default; 1147 | properties.readOnly = readOnly; 1148 | } 1149 | }); 1150 | 1151 | // Call default constructor. 1152 | _private._options.constructors.default.call(this, values); 1153 | 1154 | // May return actual object instance or Proxy, depending on harmony support. 1155 | return _private._this; 1156 | } 1157 | 1158 | // Populate values. 1159 | populate(values) { 1160 | for (const key in values) { 1161 | this[_privateKey]._this[key] = values[key]; 1162 | } 1163 | } 1164 | 1165 | // Clone and return SchemaObject. 1166 | clone() { 1167 | return new SchemaObjectInstance(this.toObject(), this[_privateKey]._root); 1168 | } 1169 | 1170 | // Return object without getter/typecasts, extra properties, etc. 1171 | toObject() { 1172 | const options = this[_privateKey]._options; 1173 | let getObj = {}; 1174 | 1175 | // Populate all properties in schema. 1176 | _.each(this[_privateKey]._schema, (properties, index) => { 1177 | // Do not write values to object that are marked as invisible. 1178 | if (properties.invisible) { 1179 | return; 1180 | } 1181 | 1182 | // Fetch value through the public interface. 1183 | let value = this[_privateKey]._this[index]; 1184 | 1185 | // Do not write undefined values to the object because of strange behavior when using with MongoDB. 1186 | // MongoDB will convert undefined to null and overwrite existing values in that field. 1187 | if (value === undefined && options.setUndefined !== true) { 1188 | return; 1189 | } 1190 | 1191 | // Clone objects so they can't be modified by reference. 1192 | if (_.isObject(value)) { 1193 | if (value._isSchemaObject) { 1194 | value = value.toObject(); 1195 | } else if (value._isSchemaArray) { 1196 | value = value.toArray(); 1197 | } else if (_.isArray(value)) { 1198 | value = value.splice(0); 1199 | } else if (_.isDate(value)) { 1200 | // https://github.com/documentcloud/underscore/pull/863 1201 | // _.clone doesn't work on Date object. 1202 | getObj[index] = new Date(value.getTime()); 1203 | } else { 1204 | value = _.clone(value); 1205 | } 1206 | 1207 | // Don't write empty objects or arrays. 1208 | if (!_.isDate(value) && !options.setUndefined && !_.size(value)) { 1209 | return; 1210 | } 1211 | } 1212 | 1213 | // Write to object. 1214 | getObj[index] = value; 1215 | }); 1216 | 1217 | // If options contains toObject, pass through before returning final object. 1218 | if (_.isFunction(options.toObject)) { 1219 | getObj = options.toObject.call(this, getObj); 1220 | } 1221 | 1222 | return getObj; 1223 | } 1224 | 1225 | // toJSON is an interface used by JSON.stringify. 1226 | // Return the raw object if called. 1227 | toJSON() { 1228 | return this.toObject(); 1229 | } 1230 | 1231 | // Clear all values. 1232 | clear() { 1233 | _.each(this[_privateKey]._schema, (properties, index) => { 1234 | clearField.call(this[_privateKey]._this, index, properties); 1235 | }); 1236 | } 1237 | 1238 | // Get all errors. 1239 | getErrors() { 1240 | const errors = []; 1241 | for (let error of this[_privateKey]._errors) { 1242 | error = _.cloneDeep(error); 1243 | error.schemaObject = this; 1244 | errors.push(error); 1245 | } 1246 | 1247 | _.each(this[_privateKey]._schema, (properties, index) => { 1248 | let required = properties.required; 1249 | let message = `${index} is required but not provided`; 1250 | 1251 | //If required is an array, set custom message 1252 | if (Array.isArray(required)) { 1253 | message = required[1] || message; 1254 | required = required[0]; 1255 | } 1256 | //Skip if required does not exist 1257 | if (!required) { 1258 | return; 1259 | } 1260 | //Skip if required is a function, but returns false 1261 | else if (typeof required === 'function' && !required.call(this)) { 1262 | return; 1263 | } 1264 | 1265 | //Skip if property has a value, is a boolean set to false, or if it's falsy and falsy values are allowed 1266 | if ( 1267 | this[index] || 1268 | typeof this[index] === 'boolean' || 1269 | this[_privateKey]._options.allowFalsyValues && this[index] !== undefined 1270 | ) { 1271 | return; 1272 | } 1273 | 1274 | const error = new SetterError(message, this[index], this[index], properties); 1275 | error.schemaObject = this; 1276 | errors.push(error); 1277 | }); 1278 | 1279 | // Look for sub-SchemaObjects. 1280 | for (const name in this[_privateKey]._schema) { 1281 | const field = this[_privateKey]._schema[name]; 1282 | if (field.type === 'object' && typeof field.objectType === 'function') { 1283 | const subErrors = this[name].getErrors(); 1284 | for (const subError of subErrors) { 1285 | subError.fieldSchema.name = `${name}.${subError.fieldSchema.name}`; 1286 | subError.schemaObject = this; 1287 | errors.push(subError); 1288 | } 1289 | } 1290 | } 1291 | 1292 | return errors; 1293 | } 1294 | 1295 | // Clear all errors 1296 | clearErrors() { 1297 | this[_privateKey]._errors.length = 0; 1298 | 1299 | // Look for sub-SchemaObjects. 1300 | for (const name in this[_privateKey]._schema) { 1301 | const field = this[_privateKey]._schema[name]; 1302 | if (field.type === 'object' && typeof field.objectType === 'function') { 1303 | this[name].clearErrors(); 1304 | } 1305 | } 1306 | } 1307 | 1308 | // Has errors? 1309 | isErrors() { 1310 | return this.getErrors().length > 0; 1311 | } 1312 | 1313 | // Used to detect instance of schema object internally. 1314 | _isSchemaObject() { 1315 | return true; 1316 | } 1317 | } 1318 | 1319 | // Add custom methods to factory-generated class. 1320 | _.each(options.methods, (method, key) => { 1321 | if (SchemaObjectInstance.prototype[key]) { 1322 | throw new Error(`Cannot overwrite existing ${key} method with custom method.`); 1323 | } 1324 | SchemaObjectInstance.prototype[key] = method; 1325 | }); 1326 | 1327 | return SchemaObjectInstance; 1328 | } 1329 | 1330 | if (typeof module === 'object') { 1331 | module.exports = SchemaObject; 1332 | } else if (typeof window === 'object') { 1333 | window.SchemaObject = SchemaObject; 1334 | } else { 1335 | throw new Error('[schema-object] Error: module.exports and window are unavailable.'); 1336 | } 1337 | 1338 | })(); 1339 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "schema-object", 3 | "version": "4.0.11", 4 | "description": "Enforce schema on JavaScript objects, including type, transformation, and validation. Supports extends, sub-schemas, and arrays.", 5 | "main": "dist/schemaobject.js", 6 | "scripts": { 7 | "test": "gulp test-node", 8 | "build": "gulp build", 9 | "prepublish": "in-publish && gulp build || not-in-publish" 10 | }, 11 | "types": "./typescript/schemaobject.d.ts", 12 | "directories": { 13 | "test": "test" 14 | }, 15 | "files": [ 16 | "dist", 17 | "typescript" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git@github.com:scotthovestadt/schema-object.git" 22 | }, 23 | "keywords": [ 24 | "javascript", 25 | "schema", 26 | "extends", 27 | "type", 28 | "transformation", 29 | "validation", 30 | "object", 31 | "array" 32 | ], 33 | "dependencies": { 34 | "babel-polyfill": "^6.26.0", 35 | "harmony-reflect": "^1.6.0", 36 | "lodash": "^4.17.10" 37 | }, 38 | "devDependencies": { 39 | "babel-core": "6.26.3", 40 | "babel-plugin-transform-builtin-extend": "1.1.2", 41 | "babel-plugin-transform-class-properties": "6.24.1", 42 | "babel-preset-es2015": "6.24.1", 43 | "gulp": "3.9.1", 44 | "gulp-babel": "7.0.1", 45 | "gulp-mocha": "6.0.0", 46 | "gulp-mocha-phantomjs": "0.12.2", 47 | "in-publish": "^2.0.0", 48 | "load-plugins": "2.1.2", 49 | "minimist": "1.2.0", 50 | "should": "13.x.x" 51 | }, 52 | "author": "Scott Hovestadt", 53 | "license": "BSD", 54 | "readmeFilename": "README.md", 55 | "engines": { 56 | "node": ">=0.11" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Schema Object 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/profile.js: -------------------------------------------------------------------------------- 1 | // Used to get rough benchmarks when trying to optimize the library. 2 | 3 | var SchemaObject = require('../dist/schemaobject'); 4 | 5 | var SO = new SchemaObject({ 6 | id: String, 7 | profile: { 8 | firstName: String, 9 | lastName: String 10 | }, 11 | identities: [{ 12 | providerUID: String, 13 | subArr: [String] 14 | }], 15 | otherStuff: { 16 | hello: String 17 | }, 18 | arr: [String] 19 | }); 20 | 21 | var data = { 22 | id: '123' 23 | }; 24 | var start = Date.now(); 25 | var o; 26 | for (var i = 0; i < 100000; i++) { 27 | o = new SO(data); 28 | } 29 | console.log((Date.now() - start) / 1000); -------------------------------------------------------------------------------- /typescript/schemaobject.d.ts: -------------------------------------------------------------------------------- 1 | interface SchemaObjectInstance { 2 | populate(values: T): void; 3 | toObject(): T; 4 | clone(): SchemaObjectInstance; 5 | clear(): void; 6 | getErrors(): Array<{ 7 | errorMessage: string; 8 | setValue: any; 9 | originalValue: any; 10 | fieldSchema: { 11 | name: string; 12 | index: string; 13 | } 14 | schemaObject: SchemaObjectInstance; 15 | }>; 16 | clearErrors(): void; 17 | isErrors(): boolean; 18 | } 19 | 20 | declare module 'schema-object' { 21 | 22 | interface SchemaObject { 23 | new (schema: { [key in keyof T]: any }, options?: any): { 24 | new (values?: T): T & SchemaObjectInstance; 25 | }; 26 | } 27 | const SO: SchemaObject; 28 | export = SO; 29 | 30 | } 31 | --------------------------------------------------------------------------------