├── .gitignore ├── .jshintrc ├── ComputedProperty.js ├── LICENSE ├── Model.js ├── Property.js ├── README.md ├── docs ├── Design.md └── Testing.md ├── extensions ├── HiddenProperties.js └── jsonSchema.js ├── package.js ├── package.json ├── store └── Validating.js ├── tests ├── Model.js ├── Store.js ├── all.js ├── extensions │ └── validating-jsonSchema.js ├── intern.js ├── runTests.html ├── validating.js ├── validators.js └── validators │ ├── NumericValidator.js │ ├── StringValidator.js │ └── UniqueValidator.js └── validators ├── NumericValidator.js ├── StringValidator.js └── UniqueValidator.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": false, 3 | "bitwise": false, 4 | "boss": false, 5 | "browser": true, 6 | "camelcase": true, 7 | "couch": false, 8 | "curly": true, 9 | "debug": false, 10 | "devel": true, 11 | "dojo": false, 12 | "eqeqeq": false, 13 | "eqnull": true, 14 | "es3": true, 15 | "esnext": false, 16 | "evil": false, 17 | "expr": true, 18 | "forin": false, 19 | "funcscope": true, 20 | "gcl": false, 21 | "globalstrict": false, 22 | "immed": true, 23 | "iterator": false, 24 | "jquery": false, 25 | "lastsemic": false, 26 | "latedef": false, 27 | "laxbreak": true, 28 | "laxcomma": false, 29 | "loopfunc": true, 30 | "mootools": false, 31 | "moz": false, 32 | "multistr": false, 33 | "newcap": true, 34 | "noarg": true, 35 | "node": false, 36 | "noempty": false, 37 | "nonew": true, 38 | "nonstandard": false, 39 | "nomen": false, 40 | "onecase": false, 41 | "onevar": false, 42 | "passfail": false, 43 | "phantom": false, 44 | "plusplus": false, 45 | "proto": true, 46 | "prototypejs": false, 47 | "regexdash": true, 48 | "regexp": false, 49 | "rhino": false, 50 | "scripturl": true, 51 | "shadow": true, 52 | "shelljs": false, 53 | "smarttabs": true, 54 | "strict": false, 55 | "sub": false, 56 | "supernew": false, 57 | "trailing": true, 58 | "undef": true, 59 | "unused": true, 60 | "validthis": true, 61 | "withstmt": false, 62 | "worker": false, 63 | "wsh": false, 64 | "yui": false, 65 | 66 | "maxlen": 140, 67 | "indent": 4, 68 | "maxerr": 250, 69 | "predef": [ "require", "define" ], 70 | "quotmark": "single", 71 | "maxcomplexity": 20 72 | } -------------------------------------------------------------------------------- /ComputedProperty.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | './Property' 4 | ], function (declare, Property) { 5 | return declare(Property, { 6 | // dependsOn: Array 7 | // This property declares the properties that this property it is computed 8 | // from. 9 | dependsOn: [], 10 | 11 | getValue: function () { 12 | // summary: 13 | // This function should be implemented to provide custom computed properties 14 | // When the corresponding property is accessed, this will be called with the 15 | // the values of the properties listed in dependsOn as the arguments 16 | }, 17 | // indicate that we have custom get functionality 18 | hasCustomGet: true, 19 | _get: function () { 20 | var dependsOn = this.dependsOn || [this.name]; 21 | var args = []; 22 | var parentObject = this._parent; 23 | for (var i = 0; i < dependsOn.length; i++) { 24 | var dependency = dependsOn[i]; 25 | args[i] = typeof dependency === 'object' ? 26 | // the dependency is another reactive object 27 | dependency.valueOf() : 28 | // otherwise, treat it as a propery 29 | dependency === this.name ? 30 | // don't recursively go through getters on our own property 31 | parentObject[this.name] : 32 | // another property 33 | parentObject.get(dependency); 34 | } 35 | return (this.value = this.getValue.apply(this, args)); 36 | }, 37 | _has: function () { 38 | return true; 39 | }, 40 | _addListener: function (listener) { 41 | // TODO: do we want to wait on computed properties that return a promise? 42 | var property = this; 43 | var dependsOn = this.dependsOn || [this.name]; 44 | var handles = []; 45 | function changeListener() { 46 | // recompute the value of this property. we could use when() here to wait on promised results 47 | property._queueChange(listener); 48 | } 49 | for (var i = 0; i < dependsOn.length; i++) { 50 | // listen to each dependency 51 | var dependency = dependsOn[i]; 52 | handles.push(typeof dependency === 'object' ? 53 | // it is another reactive object 54 | dependency.observe(changeListener, true) : 55 | // otherwise treat as property 56 | dependency === this.name ? 57 | // setup the default listener for our own name 58 | this.inherited(arguments, [changeListener]) : 59 | // otherwise get the other property and listen 60 | this._parent.property(dependsOn[i]).observe(changeListener, {onlyFutureUpdates: true})); 61 | } 62 | return { 63 | remove: function () { 64 | for (var i = 0; i < dependsOn.length; i++) { 65 | handles[i].remove(); 66 | } 67 | } 68 | }; 69 | } 70 | }); 71 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | dmodel is available under *either* the terms of the modified BSD license *or* the 2 | Academic Free License version 2.1. As a recipient of dmodel, you may choose which 3 | license to receive this code under. 4 | 5 | The text of the AFL and BSD licenses is reproduced below. 6 | 7 | ------------------------------------------------------------------------------- 8 | The "New" BSD License: 9 | ********************** 10 | 11 | Copyright (c) 2010-2015, The Dojo Foundation 12 | All rights reserved. 13 | 14 | Redistribution and use in source and binary forms, with or without 15 | modification, are permitted provided that the following conditions are met: 16 | 17 | * Redistributions of source code must retain the above copyright notice, this 18 | list of conditions and the following disclaimer. 19 | * Redistributions in binary form must reproduce the above copyright notice, 20 | this list of conditions and the following disclaimer in the documentation 21 | and/or other materials provided with the distribution. 22 | * Neither the name of the Dojo Foundation nor the names of its contributors 23 | may be used to endorse or promote products derived from this software 24 | without specific prior written permission. 25 | 26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 27 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 28 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 29 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 30 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 31 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 32 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 33 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 34 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | 37 | ------------------------------------------------------------------------------- 38 | The Academic Free License, v. 2.1: 39 | ********************************** 40 | 41 | This Academic Free License (the "License") applies to any original work of 42 | authorship (the "Original Work") whose owner (the "Licensor") has placed the 43 | following notice immediately following the copyright notice for the Original 44 | Work: 45 | 46 | Licensed under the Academic Free License version 2.1 47 | 48 | 1) Grant of Copyright License. Licensor hereby grants You a world-wide, 49 | royalty-free, non-exclusive, perpetual, sublicenseable license to do the 50 | following: 51 | 52 | a) to reproduce the Original Work in copies; 53 | 54 | b) to prepare derivative works ("Derivative Works") based upon the Original 55 | Work; 56 | 57 | c) to distribute copies of the Original Work and Derivative Works to the 58 | public; 59 | 60 | d) to perform the Original Work publicly; and 61 | 62 | e) to display the Original Work publicly. 63 | 64 | 2) Grant of Patent License. Licensor hereby grants You a world-wide, 65 | royalty-free, non-exclusive, perpetual, sublicenseable license, under patent 66 | claims owned or controlled by the Licensor that are embodied in the Original 67 | Work as furnished by the Licensor, to make, use, sell and offer for sale the 68 | Original Work and Derivative Works. 69 | 70 | 3) Grant of Source Code License. The term "Source Code" means the preferred 71 | form of the Original Work for making modifications to it and all available 72 | documentation describing how to modify the Original Work. Licensor hereby 73 | agrees to provide a machine-readable copy of the Source Code of the Original 74 | Work along with each copy of the Original Work that Licensor distributes. 75 | Licensor reserves the right to satisfy this obligation by placing a 76 | machine-readable copy of the Source Code in an information repository 77 | reasonably calculated to permit inexpensive and convenient access by You for as 78 | long as Licensor continues to distribute the Original Work, and by publishing 79 | the address of that information repository in a notice immediately following 80 | the copyright notice that applies to the Original Work. 81 | 82 | 4) Exclusions From License Grant. Neither the names of Licensor, nor the names 83 | of any contributors to the Original Work, nor any of their trademarks or 84 | service marks, may be used to endorse or promote products derived from this 85 | Original Work without express prior written permission of the Licensor. Nothing 86 | in this License shall be deemed to grant any rights to trademarks, copyrights, 87 | patents, trade secrets or any other intellectual property of Licensor except as 88 | expressly stated herein. No patent license is granted to make, use, sell or 89 | offer to sell embodiments of any patent claims other than the licensed claims 90 | defined in Section 2. No right is granted to the trademarks of Licensor even if 91 | such marks are included in the Original Work. Nothing in this License shall be 92 | interpreted to prohibit Licensor from licensing under different terms from this 93 | License any Original Work that Licensor otherwise would have a right to 94 | license. 95 | 96 | 5) This section intentionally omitted. 97 | 98 | 6) Attribution Rights. You must retain, in the Source Code of any Derivative 99 | Works that You create, all copyright, patent or trademark notices from the 100 | Source Code of the Original Work, as well as any notices of licensing and any 101 | descriptive text identified therein as an "Attribution Notice." You must cause 102 | the Source Code for any Derivative Works that You create to carry a prominent 103 | Attribution Notice reasonably calculated to inform recipients that You have 104 | modified the Original Work. 105 | 106 | 7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that 107 | the copyright in and to the Original Work and the patent rights granted herein 108 | by Licensor are owned by the Licensor or are sublicensed to You under the terms 109 | of this License with the permission of the contributor(s) of those copyrights 110 | and patent rights. Except as expressly stated in the immediately proceeding 111 | sentence, the Original Work is provided under this License on an "AS IS" BASIS 112 | and WITHOUT WARRANTY, either express or implied, including, without limitation, 113 | the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR 114 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. 115 | This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No 116 | license to Original Work is granted hereunder except under this disclaimer. 117 | 118 | 8) Limitation of Liability. Under no circumstances and under no legal theory, 119 | whether in tort (including negligence), contract, or otherwise, shall the 120 | Licensor be liable to any person for any direct, indirect, special, incidental, 121 | or consequential damages of any character arising as a result of this License 122 | or the use of the Original Work including, without limitation, damages for loss 123 | of goodwill, work stoppage, computer failure or malfunction, or any and all 124 | other commercial damages or losses. This limitation of liability shall not 125 | apply to liability for death or personal injury resulting from Licensor's 126 | negligence to the extent applicable law prohibits such limitation. Some 127 | jurisdictions do not allow the exclusion or limitation of incidental or 128 | consequential damages, so this exclusion and limitation may not apply to You. 129 | 130 | 9) Acceptance and Termination. If You distribute copies of the Original Work or 131 | a Derivative Work, You must make a reasonable effort under the circumstances to 132 | obtain the express assent of recipients to the terms of this License. Nothing 133 | else but this License (or another written agreement between Licensor and You) 134 | grants You permission to create Derivative Works based upon the Original Work 135 | or to exercise any of the rights granted in Section 1 herein, and any attempt 136 | to do so except under the terms of this License (or another written agreement 137 | between Licensor and You) is expressly prohibited by U.S. copyright law, the 138 | equivalent laws of other countries, and by international treaty. Therefore, by 139 | exercising any of the rights granted to You in Section 1 herein, You indicate 140 | Your acceptance of this License and all of its terms and conditions. 141 | 142 | 10) Termination for Patent Action. This License shall terminate automatically 143 | and You may no longer exercise any of the rights granted to You by this License 144 | as of the date You commence an action, including a cross-claim or counterclaim, 145 | against Licensor or any licensee alleging that the Original Work infringes a 146 | patent. This termination provision shall not apply for an action alleging 147 | patent infringement by combinations of the Original Work with other software or 148 | hardware. 149 | 150 | 11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this 151 | License may be brought only in the courts of a jurisdiction wherein the 152 | Licensor resides or in which Licensor conducts its primary business, and under 153 | the laws of that jurisdiction excluding its conflict-of-law provisions. The 154 | application of the United Nations Convention on Contracts for the International 155 | Sale of Goods is expressly excluded. Any use of the Original Work outside the 156 | scope of this License or after its termination shall be subject to the 157 | requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et 158 | seq., the equivalent laws of other countries, and international treaty. This 159 | section shall survive the termination of this License. 160 | 161 | 12) Attorneys Fees. In any action to enforce the terms of this License or 162 | seeking damages relating thereto, the prevailing party shall be entitled to 163 | recover its costs and expenses, including, without limitation, reasonable 164 | attorneys' fees and costs incurred in connection with such action, including 165 | any appeal of such action. This section shall survive the termination of this 166 | License. 167 | 168 | 13) Miscellaneous. This License represents the complete agreement concerning 169 | the subject matter hereof. If any provision of this License is held to be 170 | unenforceable, such provision shall be reformed only to the extent necessary to 171 | make it enforceable. 172 | 173 | 14) Definition of "You" in This License. "You" throughout this License, whether 174 | in upper or lower case, means an individual or a legal entity exercising rights 175 | under, and complying with all of the terms of, this License. For legal 176 | entities, "You" includes any entity that controls, is controlled by, or is 177 | under common control with you. For purposes of this definition, "control" means 178 | (i) the power, direct or indirect, to cause the direction or management of such 179 | entity, whether by contract or otherwise, or (ii) ownership of fifty percent 180 | (50%) or more of the outstanding shares, or (iii) beneficial ownership of such 181 | entity. 182 | 183 | 15) Right to Use. You may use the Original Work in all ways not otherwise 184 | restricted or conditioned by this License or by law, and Licensor promises not 185 | to interfere with or be responsible for such uses by You. 186 | 187 | This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved. 188 | Permission is hereby granted to copy and distribute this license without 189 | modification. This license may not be modified without the express written 190 | permission of its copyright owner. 191 | -------------------------------------------------------------------------------- /Model.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dojo/_base/lang', 4 | 'dojo/Deferred', 5 | 'dojo/aspect', 6 | 'dojo/when' 7 | ], function (declare, lang, Deferred, aspect, when) { 8 | 9 | function getSchemaProperty(object, key) { 10 | // this function will retrieve the individual property definition 11 | // from the schema, for the provided object and key 12 | var definition = object.schema[key]; 13 | if (definition !== undefined && !(definition instanceof Property)) { 14 | definition = new Property(definition); 15 | definition._parent = object; 16 | } 17 | if (definition) { 18 | definition.name = key; 19 | } 20 | return definition; 21 | } 22 | 23 | function validate(object, key) { 24 | // this performs validation, delegating validation, and coercion 25 | // handling to the property definitions objects. 26 | var hasOwnPropertyInstance, 27 | property = object.hasOwnProperty('_properties') && object._properties[key]; 28 | 29 | hasOwnPropertyInstance = property; 30 | 31 | if (!property) { 32 | // or, if we don't our own property object, we inherit from the schema 33 | property = getSchemaProperty(object, key); 34 | if (property && property.validate) { 35 | property = lang.delegate(property, { 36 | _parent: object, 37 | key: key 38 | }); 39 | } 40 | } 41 | 42 | if (property && property.validate) { 43 | return when(property.validate(), function (isValid) { 44 | if (!isValid) { 45 | // errors, so don't perform set 46 | if (!hasOwnPropertyInstance) { 47 | // but we do need to store our property 48 | // instance if we don't have our own 49 | (object.hasOwnProperty('_properties') ? 50 | object._properties : 51 | object._properties = new Hidden())[key] = property; 52 | } 53 | } 54 | return isValid; 55 | }); 56 | } 57 | return true; 58 | } 59 | 60 | function whenEach(iterator) { 61 | // this is responsible for collecting values from an iterator, 62 | // and waiting for the results if promises are returned, returning 63 | // a new promise represents the eventual completion of all the promises 64 | // this will consistently preserve a sync (non-promise) return value if all 65 | // sync values are provided 66 | var deferred; 67 | var remaining = 1; 68 | // start the iterator 69 | iterator(function (value, callback, key) { 70 | if (value && value.then) { 71 | // it is a promise, have to wait for it 72 | remaining++; 73 | if (!deferred) { 74 | // make sure we have a deferred 75 | deferred = new Deferred(); 76 | } 77 | value.then(function (value) { 78 | // result received, call callback, and then indicate another item is done 79 | doneItem(callback(value, key)); 80 | }).then(null, deferred.reject); 81 | } else { 82 | // not a promise, just a direct sync callback 83 | callback(value, key); 84 | } 85 | }); 86 | if (deferred) { 87 | // if we have a deferred, decrement one more time 88 | doneItem(); 89 | return deferred.promise; 90 | } 91 | function doneItem() { 92 | // called for each promise as it is completed 93 | remaining--; 94 | if (!remaining) { 95 | // all done 96 | deferred.resolve(); 97 | } 98 | } 99 | } 100 | var slice = [].slice; 101 | 102 | var Model = declare(null, { 103 | // summary: 104 | // A base class for modelled data objects. 105 | 106 | // schema: Object | dstore/Property 107 | // A hash map where the key corresponds to a property definition. 108 | // This can be a string corresponding to a JavaScript 109 | // primitive values (string, number, boolean), a constructor, a 110 | // null (to allow any type), or a Property object with more advanced 111 | // definitions. 112 | schema: {}, 113 | 114 | // additionalProperties: boolean 115 | // This indicates whether properties are allowed that are not 116 | // defined in the schema. 117 | additionalProperties: true, 118 | 119 | // _scenario: string 120 | // The scenario that is used to determine which validators should 121 | // apply to this model. There are two standard values for _scenario, 122 | // "insert" and "update", but it can be set to any arbitrary value 123 | // for more complex validation scenarios. 124 | _scenario: 'update', 125 | 126 | constructor: function (options) { 127 | this.init(options); 128 | }, 129 | 130 | init: function (values) { 131 | // if we are being constructed, we default to the insert scenario 132 | this._scenario = 'insert'; 133 | // copy in the default values 134 | values = this._setValues(values); 135 | 136 | // set any defaults 137 | for (var key in this.schema) { 138 | var definition = this.schema[key]; 139 | if (definition && typeof definition === 'object' && 'default' in definition && 140 | !values.hasOwnProperty(key)) { 141 | var defaultValue = definition['default']; 142 | values[key] = typeof defaultValue === 'function' ? defaultValue.call(this) : defaultValue; 143 | } 144 | } 145 | 146 | }, 147 | 148 | _setValues: function (values) { 149 | return lang.mixin(this, values); 150 | }, 151 | 152 | _getValues: function () { 153 | return this._values || this; 154 | }, 155 | 156 | save: function (/*Object*/ options) { 157 | // summary: 158 | // Saves this object, calling put or add on the attached store. 159 | // options.skipValidation: 160 | // Normally, validation is performed to ensure that the object 161 | // is not invalid before being stored. Set `skipValidation` to 162 | // true to skip it. 163 | // returns: any 164 | 165 | var object = this; 166 | return when((options && options.skipValidation) ? true : this.validate(), function (isValid) { 167 | if (!isValid) { 168 | throw object.createValidationError(object.errors); 169 | } 170 | var scenario = object._scenario; 171 | // suppress any non-date from serialization output 172 | object.prepareForSerialization(); 173 | return object._store && when(object._store[scenario === 'insert' ? 'add' : 'put'](object), 174 | function (returned) { 175 | // receive any updates from the server 176 | object.set(returned); 177 | object._scenario = 'update'; 178 | return object; 179 | }); 180 | }); 181 | }, 182 | 183 | remove: function () { 184 | var store = this._store; 185 | return store.remove(store.getIdentity(this)); 186 | }, 187 | 188 | prepareForSerialization: function () { 189 | // summary: 190 | // This method is responsible for cleaing up any properties on the instance 191 | // object to ensure it can easily be serialized (by JSON.stringify at least) 192 | this._scenario = undefined; 193 | if (this._inherited) { 194 | this._inherited.toJSON = toJSONHidden; 195 | } 196 | }, 197 | 198 | createValidationError: function (errors) { 199 | // summary: 200 | // This is called when a save is attempted and a validation error was found. 201 | // This can be overriden with locale-specific messages 202 | // errors: 203 | // Errors that were found in validation 204 | return new Error('Validation error'); 205 | }, 206 | 207 | property: function (/*String...*/ key, nextKey) { 208 | // summary: 209 | // Gets a new reactive property object, representing the present and future states 210 | // of the provided property. The returned property object gives access to methods for changing, 211 | // retrieving, and observing the property value, any validation errors, and property metadata. 212 | // key: String... 213 | // The name of the property to retrieve. Multiple key arguments can be provided 214 | // nested property access. 215 | 216 | // create the properties object, if it doesn't exist yet 217 | var properties = this.hasOwnProperty('_properties') ? this._properties : 218 | (this._properties = new Hidden()); 219 | var property = properties[key]; 220 | // if it doesn't exist, create one, delegated from the schema's property definition 221 | // (this gives an property instance, owning the current property value and listeners, 222 | // while inheriting metadata from the schema's property definitions) 223 | if (!property) { 224 | property = getSchemaProperty(this, key); 225 | // delegate, or just create a new instance if no schema definition exists 226 | property = properties[key] = property ? lang.delegate(property) : new Property(); 227 | property.name = key; 228 | // give it the correct initial value 229 | property._parent = this; 230 | } 231 | if (nextKey) { 232 | // go to the next property, if there are multiple 233 | return property.property.apply(property, slice.call(arguments, 1)); 234 | } 235 | return property; 236 | }, 237 | 238 | get: function (/*string*/ key) { 239 | // TODO: add listener parameter back in 240 | // summary: 241 | // Standard get() function to retrieve the current value 242 | // of a property, augmented with the ability to listen 243 | // for future changes 244 | 245 | var property, definition = this.schema[key]; 246 | // now we need to see if there is a custom get involved, or if we can just 247 | // shortcut to retrieving the property value 248 | definition = property || this.schema[key]; 249 | if (definition && definition.valueOf && 250 | (definition.valueOf !== simplePropertyValueOf || definition.hasCustomGet)) { 251 | // we have custom get functionality, need to create at least a temporary property 252 | // instance 253 | property = property || (this.hasOwnProperty('_properties') && this._properties[key]); 254 | if (!property) { 255 | // no property instance, so we create a temporary one 256 | property = lang.delegate(getSchemaProperty(this, key), { 257 | name: key, 258 | _parent: this 259 | }); 260 | } 261 | // let the property instance handle retrieving the value 262 | return property.valueOf(); 263 | } 264 | // default action of just retrieving the property value 265 | return this._getValues()[key]; 266 | }, 267 | 268 | set: function (/*string*/ key, /*any?*/ value) { 269 | // summary: 270 | // Only allows setting keys that are defined in the schema, 271 | // and remove any error conditions for the given key when 272 | // its value is set. 273 | if (typeof key === 'object') { 274 | startOperation(); 275 | try { 276 | for (var i in key) { 277 | value = key[i]; 278 | if (key.hasOwnProperty(i) && !(value && value.toJSON === toJSONHidden)) { 279 | this.set(i, value); 280 | } 281 | } 282 | } finally { 283 | endOperation(); 284 | } 285 | return; 286 | } 287 | var definition = this.schema[key]; 288 | if (!definition && !this.additionalProperties) { 289 | // TODO: Shouldn't this throw an error instead of just giving a warning? 290 | return console.warn('Schema does not contain a definition for', key); 291 | } 292 | var property = this.hasOwnProperty('_properties') && this._properties[key]; 293 | if (!property && 294 | // we need a real property instance if it is an object or if we have a custom put method 295 | ((value && typeof value === 'object') || 296 | (definition && definition.put !== simplePropertyPut))) { 297 | property = this.property(key); 298 | } 299 | if (property) { 300 | // if the property instance exists, use this to do the set 301 | property.put(value); 302 | } else { 303 | if (definition && definition.coerce) { 304 | // if a schema definition exists, and has a coerce method, 305 | // we can use without creating a new instance 306 | value = definition.coerce(value); 307 | } 308 | // we can shortcut right to just setting the object property 309 | this._getValues()[key] = value; 310 | // check to see if we should do validation 311 | if (definition && definition.validateOnSet !== false) { 312 | validate(this, key); 313 | } 314 | } 315 | 316 | return value; 317 | }, 318 | 319 | observe: function (/*string*/ key, /*function*/ listener, /*object*/ options) { 320 | // summary: 321 | // Registers a listener for any changes in the specified property 322 | // key: 323 | // The name of the property to listen to 324 | // listener: 325 | // Function to be called for each change 326 | // options.onlyFutureUpdates 327 | // If this is true, it won't call the listener for the current value, 328 | // just future updates. If this is true, it also won't return 329 | // a new reactive object 330 | return this.property(key).observe(listener, options); 331 | }, 332 | 333 | validate: function (/*string[]?*/ fields) { 334 | // summary: 335 | // Validates the current object. 336 | // fields: 337 | // If provided, only the fields listed in the array will be 338 | // validated. 339 | // returns: boolean | dojo/promise/Promise 340 | // A boolean or a promise that resolves to a boolean indicating whether 341 | // or not the model is in a valid state. 342 | 343 | var object = this, 344 | isValid = true, 345 | errors = [], 346 | fieldMap; 347 | 348 | if (fields) { 349 | fieldMap = {}; 350 | for (var i = 0; i < fields.length; i++) { 351 | fieldMap[i] = true; 352 | } 353 | } 354 | return when(whenEach(function (whenItem) { 355 | // iterate through the keys in the schema. 356 | // note that we will always validate every property, regardless of when it fails, 357 | // and we will execute all the validators immediately (async validators will 358 | // run in parallel) 359 | for (var key in object.schema) { 360 | // check to see if we are allowed to validate this key 361 | if (!fieldMap || (fieldMap.hasOwnProperty(key))) { 362 | // run validation 363 | whenItem(validate(object, key), function (isValid, key) { 364 | if (!isValid) { 365 | notValid(key); 366 | } 367 | }, key); 368 | } 369 | } 370 | }), function () { 371 | object.set('errors', isValid ? undefined : errors); 372 | // it wasn't async, so we just return the synchronous result 373 | return isValid; 374 | }); 375 | function notValid(key) { 376 | // found an error, mark valid state and record the errors 377 | isValid = false; 378 | errors.push.apply(errors, object.property(key).errors); 379 | } 380 | }, 381 | 382 | isValid: function () { 383 | // summary: 384 | // Returns whether or not there are currently any errors on 385 | // this model due to validation failures. Note that this does 386 | // not run validation but merely returns the result of any 387 | // prior validation. 388 | // returns: boolean 389 | 390 | var isValid = true, 391 | key; 392 | 393 | for (key in this.schema) { 394 | var property = this.hasOwnProperty('_properties') && this._properties[key]; 395 | if (property && property.errors && property.errors.length) { 396 | isValid = false; 397 | } 398 | } 399 | return isValid; 400 | } 401 | }); 402 | 403 | // define the start and end markers of an operation, so we can 404 | // fire notifications at the end of the operation, by default 405 | function startOperation() { 406 | setCallDepth++; 407 | } 408 | function endOperation() { 409 | // if we are ending this operation, start executing the queue 410 | if (setCallDepth < 2 && onEnd) { 411 | onEnd(); 412 | onEnd = null; 413 | } 414 | setCallDepth--; 415 | } 416 | var setCallDepth = 0; 417 | var callbackQueue; 418 | var onEnd; 419 | // the default nextTurn executes at the end of the current operation 420 | // The intent with this function is that it could easily be replaced 421 | // with something like setImmediate, setTimeout, or nextTick to provide 422 | // next turn handling 423 | (Model.nextTurn = function (callback) { 424 | // set the callback for the end of the current operation 425 | onEnd = callback; 426 | }).atEnd = true; 427 | 428 | var Reactive = declare([Model], { 429 | // summary: 430 | // A reactive object is a data model that can contain a value, 431 | // and notify listeners of changes to that value, in the future. 432 | observe: function (/*function*/ listener, /*object*/ options) { 433 | // summary: 434 | // Registers a listener for any changes in the current value 435 | // listener: 436 | // Function to be called for each change 437 | // options.onlyFutureUpdates 438 | // If this is true, it won't call the listener for the current value, 439 | // just future updates. If this is true, it also won't return 440 | // a new reactive object 441 | 442 | var reactive; 443 | if (typeof listener === 'string') { 444 | // a property key was provided, use the Model's method 445 | return this.inherited(arguments); 446 | } 447 | if (!options || !options.onlyFutureUpdates) { 448 | // create a new reactive to contain the results of the execution 449 | // of the provided function 450 | reactive = new Reactive(); 451 | if (this._has()) { 452 | // we need to notify of the value of the present (as well as future) 453 | reactive.value = listener(this.valueOf()); 454 | } 455 | } 456 | // add to the listeners 457 | var handle = this._addListener(function (value, oldValue, propertyName) { 458 | var result = listener(value, oldValue, propertyName); 459 | if (reactive) { 460 | // TODO: once we have a real notification API again, call that, instead 461 | // of requesting a change 462 | reactive.put(result); 463 | } 464 | }); 465 | if (reactive) { 466 | reactive.remove = handle.remove; 467 | return reactive; 468 | } else { 469 | return handle; 470 | } 471 | }, 472 | 473 | // validateOnSet: boolean 474 | // Indicates whether or not to perform validation when properties 475 | // are modified. 476 | // This can provided immediate feedback and on the success 477 | // or failure of a property modification. And Invalid property 478 | // values will be rejected. However, if you are 479 | // using asynchronous validation, invalid property values will still 480 | // be set. 481 | validateOnSet: true, 482 | 483 | // validators: Array 484 | // An array of additional validators to apply to this property 485 | validators: null, 486 | 487 | _addListener: function (listener) { 488 | // add a listener for the property change event 489 | return aspect.after(this, 'onchange', listener, true); 490 | }, 491 | 492 | valueOf: function () { 493 | return this._get(); 494 | }, 495 | 496 | _get: function () { 497 | return this.value; 498 | }, 499 | 500 | _has: function () { 501 | return this.hasOwnProperty('value'); 502 | }, 503 | setValue: function (value) { 504 | // summary: 505 | // This method is responsible for storing the value. This can 506 | // be overriden to define a custom setter 507 | // value: any 508 | // The value to be stored 509 | // parent: Object 510 | // The parent object of this propery 511 | this.value = value; 512 | }, 513 | 514 | put: function (/*any*/ value) { 515 | // summary: 516 | // Indicates a new value for this reactive object 517 | 518 | // notify all the listeners of this object, that the value has changed 519 | var oldValue = this._get(); 520 | value = this.coerce(value); 521 | if (this.errors) { 522 | // clear any errors 523 | this.set('errors', undefined); 524 | } 525 | var property = this; 526 | // call the setter and wait for it 527 | startOperation(); 528 | return when(this.setValue(value, this._parent), function (result) { 529 | if (result !== undefined) { 530 | // allow the setter to change the value 531 | value = result; 532 | } 533 | // notify listeners 534 | if (property.onchange) { 535 | // queue the callback 536 | property._queueChange(property.onchange, oldValue, property.name); 537 | } 538 | // if this was set to an object (or was an object), we need to notify. 539 | // update all the sub-property objects, so they can possibly notify their 540 | // listeners 541 | var key, 542 | hasOldObject = oldValue && typeof oldValue === 'object' && !(oldValue instanceof Array), 543 | hasNewObject = value && typeof value === 'object' && !(value instanceof Array); 544 | if (hasOldObject || hasNewObject) { 545 | // we will iterate through the properties recording the changes 546 | var changes = {}; 547 | if (hasOldObject) { 548 | oldValue = oldValue._getValues ? oldValue._getValues() : oldValue; 549 | for (key in oldValue) { 550 | changes[key] = {old: oldValue[key]}; 551 | } 552 | } 553 | if (hasNewObject) { 554 | value = value._getValues ? value._getValues() : value; 555 | for (key in value) { 556 | (changes[key] = changes[key] || {}).value = value[key]; 557 | } 558 | } 559 | property._values = hasNewObject && value; 560 | for (key in changes) { 561 | // now for each change, we can notify the property object 562 | var change = changes[key]; 563 | var subProperty = property._properties && property._properties[key]; 564 | if (subProperty && subProperty.onchange) { 565 | // queue the callback 566 | subProperty._queueChange(subProperty.onchange, change.old, subProperty.name); 567 | } 568 | } 569 | } 570 | if (property.validateOnSet) { 571 | property.validate(); 572 | } 573 | endOperation(); 574 | }); 575 | }, 576 | 577 | coerce: function (value) { 578 | // summary: 579 | // Given an input value, this method is responsible 580 | // for converting it to the appropriate type for storing on the object. 581 | 582 | var type = this.type; 583 | if (type) { 584 | if (type === 'string') { 585 | value = '' + value; 586 | } 587 | else if (type === 'number') { 588 | value = +value; 589 | } 590 | else if (type === 'boolean') { 591 | // value && value.length check is because dijit/_FormMixin 592 | // returns an array for checkboxes; an array coerces to true, 593 | // but an empty array should be set as false 594 | value = (value === 'false' || value === '0' || value instanceof Array && !value.length) ? 595 | false : !!value; 596 | } 597 | else if (typeof type === 'function' && !(value instanceof type)) { 598 | /* jshint newcap: false */ 599 | value = new type(value); 600 | } 601 | } 602 | return value; 603 | }, 604 | 605 | addError: function (error) { 606 | // summary: 607 | // Add an error to the current list of validation errors 608 | // error: String 609 | // Error to add 610 | this.set('errors', (this.errors || []).concat([error])); 611 | }, 612 | 613 | checkForErrors: function (value) { 614 | // summary: 615 | // This method can be implemented to simplify validation. 616 | // This is called with the value, and this method can return 617 | // an array of any errors that were found. It is recommended 618 | // that you call this.inherited(arguments) to permit any 619 | // other validators to perform validation 620 | // value: 621 | // This is the value to validate. 622 | var errors = []; 623 | if (this.type && !(typeof this.type === 'function' ? (value instanceof this.type) : 624 | (this.type === typeof value))) { 625 | errors.push(value + ' is not a ' + this.type); 626 | } 627 | 628 | if (this.required && !(value != null && value !== '')) { 629 | errors.push('required, and it was not present'); 630 | } 631 | return errors; 632 | }, 633 | 634 | validate: function () { 635 | // summary: 636 | // This method is responsible for validating this particular 637 | // property instance. 638 | var property = this; 639 | var model = this._parent; 640 | var validators = this.validators; 641 | var value = this.valueOf(); 642 | var totalErrors = []; 643 | 644 | return when(whenEach(function (whenItem) { 645 | // iterator through any validators (if we have any) 646 | if (validators) { 647 | for (var i = 0; i < validators.length; i++) { 648 | whenItem(validators[i].checkForErrors(value, property, model), addErrors); 649 | } 650 | } 651 | // check our own validation 652 | whenItem(property.checkForErrors(value, property, model), addErrors); 653 | function addErrors(errors) { 654 | if (errors) { 655 | // if we have an array of errors, add it to the total of all errors 656 | totalErrors.push.apply(totalErrors, errors); 657 | } 658 | } 659 | }), function () { 660 | if (totalErrors.length) { 661 | // errors exist 662 | property.set('errors', totalErrors); 663 | return false; 664 | } 665 | // no errors, valid value, if there were errors before, remove them 666 | if(property.get('errors') !== undefined){ 667 | property.set('errors', undefined); 668 | } 669 | return true; 670 | }); 671 | }, 672 | _queueChange: function (callback, oldValue, propertyName) { 673 | // queue up a notification callback 674 | if (!callback._queued) { 675 | // make sure we only queue up once before it is called by flagging it 676 | callback._queued = true; 677 | var reactive = this; 678 | // define a function for when it is called that will clear the flag 679 | // and provide the correct args 680 | var dispatch = function () { 681 | callback._queued = false; 682 | callback.call(reactive, reactive._get(), oldValue, propertyName); 683 | }; 684 | 685 | if (callbackQueue) { 686 | // we already have a waiting queue of callbacks, add our callback 687 | callbackQueue.push(dispatch); 688 | } 689 | if (!callbackQueue) { 690 | // no waiting queue, check to see if we have a custom nextTurn 691 | // or we are in an operation 692 | if (!Model.nextTurn.atEnd || setCallDepth > 0) { 693 | // create the queue (starting with this callback) 694 | callbackQueue = [dispatch]; 695 | // define the callback executor for the next turn 696 | Model.nextTurn(function () { 697 | // pull out all the callbacks 698 | for (var i = 0; i < callbackQueue.length; i++) { 699 | // call each one 700 | callbackQueue[i](); 701 | } 702 | // clear it 703 | callbackQueue = null; 704 | }); 705 | } else { 706 | // no set call depth, so just immediately execute 707 | dispatch(); 708 | } 709 | } 710 | } 711 | }, 712 | toJSON: function () { 713 | return this._values || this; 714 | } 715 | }); 716 | // a function that returns a function, to stop JSON serialization of an 717 | // object 718 | function toJSONHidden() { 719 | return toJSONHidden; 720 | } 721 | // An object that will be hidden from JSON serialization 722 | var Hidden = function () { 723 | }; 724 | Hidden.prototype.toJSON = toJSONHidden; 725 | 726 | var Property = Model.Property = declare(Reactive, { 727 | // summary: 728 | // A Property represents a time-varying property value on an object, 729 | // along with meta-data. One can listen to changes in this value (through 730 | // receive), as well as access and monitor metadata, like default values, 731 | // validation information, required status, and any validation errors. 732 | 733 | // value: any 734 | // This represents the value of this property, which can be 735 | // monitored for changes and validated 736 | 737 | init: function (options) { 738 | // handle simple definitions 739 | if (typeof options === 'string' || typeof options === 'function') { 740 | options = {type: options}; 741 | } 742 | // and/or mixin any provided properties 743 | if (options) { 744 | declare.safeMixin(this, options); 745 | } 746 | }, 747 | 748 | _get: function () { 749 | return this._parent._getValues()[this.name]; 750 | }, 751 | _has: function () { 752 | return this.name in this._parent._getValues(); 753 | }, 754 | setValue: function (value, parent) { 755 | parent._getValues()[this.name] = value; 756 | } 757 | }); 758 | var simplePropertyValueOf = Property.prototype.valueOf; 759 | var simplePropertyPut = Property.prototype.put; 760 | return Model; 761 | }); -------------------------------------------------------------------------------- /Property.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | './Model' 4 | ], function (declare, Model) { 5 | return declare(Model.Property, { 6 | }); 7 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data Modelling 2 | 3 | dmodel provides robust data modeling capabilities for managing individual objects themselves. dmodel provides a data model class that includes multiple methods on data objects, for saving, validating, and monitoring objects for changes. 4 | 5 | dmodel can be used with [dstore](https://github.com/SitePen/dstore), such that objects that are returned from a store (whether it be from iterating over a collection, or performing a get()) can be set to be 6 | an instance of the store's data model by setting the `model` property of the store to a model class, such as `dmodel/Model`. 7 | With this setting, the objects are instances of this model, and they all inherit the following properties and methods: 8 | 9 | ### Property Summary 10 | 11 | Property | Description 12 | -------- | ----------- 13 | `schema` | The schema is an object with property definitions that define various metadata about the instance objects' properties. 14 | `additionalProperties` | This indicates whether or not to allow additional properties outside of those defined by the schema. This defaults to true. 15 | 16 | Method | Description 17 | ------ | ----------- 18 | `get(name)` | This returns the property value with the given name. 19 | `set(name, value)` | This sets the value of a property. 20 | `property(name)` | This returns a property object instance for the given name. 21 | `observe(name, listener, options)` | This will listen for any changes to the value of the given property. See the Property's `observe` method for the options. 22 | `validate()` | This will validate the object, determining if there are any errors on the object. The errors can be checked on the `errors` property. 23 | `save()` | This will save the object, validating and then storing the object in the store. This will return the saved object (or a promise if it is saved asynchronously). 24 | `remove()` | This will delete the object from the object store. 25 | 26 | ## Property Objects 27 | 28 | One of the key ideas in the dmodel object model is the concept of property objects. A property object is a representation of single property on an object. The property object not only can provide the current value of a property, but can track meta-data about the property, such as property-specific validation information and whether or not the property is required. With the property object we can also monitor the property for changes, and modify the value of the property. A property object represents an encapsulation of a property that can easily be passed to different input components. 29 | 30 | Property objects actually extend the data model class, so the methods listed for data objects above are available on property objects. The following additional methods are defined on property objects: 31 | 32 | Method | Description 33 | ------ | ----------- 34 | `observe(listener, options)` | This registers a listener for any changes to the value of this property. The listener will be called with the current value (if it exists), and will be called with any future changes. The optional `options` object argument may include a `onlyFutureUpdates` set to true if the callback should not be called for the current value (only future updates). This will return an observe handle, with a remove() that can be used to stop listening. The listener will be called with the the new value as the first argument, and the old value as the second argument. 35 | `put(value)` | This requests a change in the value of this property. This may be coerced before being stored, and/or validated. 36 | `valueOf()` | This returns the current value of the property. 37 | `validate()` | Called to validate the current property value. This should return a boolean indicating whether or not validation was successful, or a promise to a boolean. This should also result in the `errors` property being set, if any errors were found in the validation process, and `errors` property being cleared (to null) if no errors were found. 38 | `addError(error)` | This can be called to add an error to the list of validation errors for a property. 39 | 40 | Property | Description 41 | ------ | ----------- 42 | `type` | This is a string that indicates the primitive type of the property value (string, number, boolean, or object). 43 | `required` | This is a boolean that indicates whether a (non-empty) value is required for this property. 44 | `default` | This defines the default value for the property. When a new model object is created, this will be used as the initial value, if no other value is provided. 45 | `errors` | This is an array of errors from the last validation of this property. This may be null to indicate no errors. 46 | `name` | This is the name of the property. 47 | `validateOnSet` | This indicates whether or not to validate a property when a new value is set on it. 48 | `validators` | This is an array of validators that can be applied on validation. 49 | 50 | To get a property object from an data object, we simply call the property method: 51 | 52 | var nameProperty = object.property('name'); 53 | 54 | Once we have the property object, we can access meta-data, watch, and modify this property: 55 | 56 | 57 | nameProperty.required -> is it required? 58 | nameProperty.observe(function(newValue){ 59 | // called with original value and each change 60 | }); 61 | nameProperty.put("Mark"); 62 | object.name -> "Mark" 63 | 64 | ## Schema 65 | 66 | A data model is largely defined through the schema. The model object has a `schema` property to define the schema object and the schema object has properties with definitions that correspond to the properties of model instances that they describe. Each property's value is a property definition. A property definition can be a simple string, defining the primitive type to be accepted, or it can be a property definition object. The property definition can have the following properties and/or methods: 67 | 68 | Property | Description 69 | ------ | ----------- 70 | `type` | This indicates the primitive type of the property value (string, number, boolean, or object). 71 | `required` | This indicates whether a (non-empty) value is required for this property. 72 | `default` | This defines the default value for the property. 73 | 74 | The property definition defines the type, structure, metadata, and behavior of the properties on the model. If the property definition object is an instance of `dmodel/Property`, it will be used as the direct prototype for the instance property objects, as well. If not, the property definition will be used to construct a `dmodel/Property` instance, (properties are copied over), to use as the prototype of the instance property objects. 75 | 76 | You can also define your own methods, to override the normal validation, access, and modification functionality of properties, by subclassing `dmodel/Property` or by directly defining methods in the property definition. The following methods can be defined or overriden: 77 | 78 | Method | Description 79 | ------ | ----------- 80 | `checkForErrors(valueToValidate)` | This method can be overriden to provide custom validation functionality. This method should return an array of errors property. This can return an empty array to indicate no errors were found. 81 | `coerce(value)` | This method is responsible for coercing input values. The default implementation coerces to the provided type (for example, if the type was a `string`, any input values would be converted to a string). 82 | `setValue(value, parent)` | This method can be called by a put() method to set the value of the underlying property. This can be overriden to define a custom setter. 83 | 84 | Here is an example of creating a model using a schema: 85 | 86 | MyModel = declare(Model, { 87 | schema: { 88 | firstName: 'string', // simple definition 89 | lastName: { 90 | type: 'string', 91 | required: true 92 | } 93 | } 94 | }); 95 | 96 | We can then define our model as the model to be used for a store: 97 | 98 | myStore = new Rest({ 99 | model: MyModel 100 | }); 101 | 102 | It is important to note that each store should have its own distinct model class. 103 | 104 | ### Computed Property Values 105 | 106 | A computed property may be defined on the schema, by using the the `dmodel/ComputedProperty` class. With a computed property, we can define a `getValue()` method to compute the value to be returned when a property is accessed. We can also define a `dependsOn` array to specify which properties we depend on. When the property is accessed or any of the dependent property changes, the property's value will be recomputed. The `getValue` is called with the values of the properties defined in the `dependsOn` array. 107 | 108 | With a computed property, we may also want to write a custom `setValue()` method if we wish to support assignments to the computed property. A `setValue()` method may need to interact with the parent object to compute values and determine behavior. The parent is provided as the second argument. 109 | 110 | Here is an example of a schema that with a computed property, `fullName`, that represents the concatenation of the `firstName` and the `lastName`: 111 | 112 | schema: { 113 | firstName: 'string' 114 | lastName: 'string' 115 | fullName: { 116 | dependsOn: ['firstName', 'lastName'], 117 | getValue: function (firstName, lastName) { 118 | // compute the full name 119 | return firstName + ' ' + lastName; 120 | }, 121 | setValue: function(value, parent){ 122 | // support setting this property as well 123 | var parts = value.split(' '); 124 | parent.set('firstName', parts[0]); 125 | parent.set('lastName', parts[1]); 126 | } 127 | } 128 | } 129 | 130 | The items in the `dependsOn' on array can be property names, or they can be other property objects. If other property objects are used, the computed property can be used as a standalone entity (it can be observed and values directly retrieved from it), without having to be attached to another parent object. The items in this array can be mixed, and include the property's own value as well (using its own name). 131 | 132 | Note, that traditional getters and setters can effectively be defined by creating `valueOf()` and `put()` methods on the property definition. However, this is generally eschewed in dmodel, since the primary use cases for getters and setters are better served by defining validation or creating a computed property. 133 | 134 | ### Validators 135 | 136 | Validators are `Property` subclasses with more advanced validation capabilities. dmodel includes several validators, that can be used, extended, or referenced for creating your own custom validators. To use a single validator, we can use it as the constructor for a property definition. For example, we could use the StringValidator to enforce the size of a string and acceptable characters: 137 | 138 | schema: { 139 | username: new StringValidator({ 140 | // must be at least 4 characters 141 | minimumLength: 4, 142 | // and max of 20 characters 143 | maximumLength: 20, 144 | // and only letters or numbers 145 | pattern: /^\w+$/ 146 | }) 147 | } 148 | 149 | dmodel include several pre-built validators. These are the available validators, and their properties: 150 | * StringValidator - Enforces string length and patterns. 151 | * minimumLength - Minimum length of the string 152 | * maximumLength - Maximum length of the string 153 | * pattern - Regular expression to test against the string 154 | * minimumLengthError - Error message when minimum length fails 155 | * maximumLengthError - Error message when maximum length fails 156 | * patternMatchError - The error when a pattern does not match 157 | * NumericValidator - Enforces numbers and ranges of numbers. 158 | * minimum - The minimum value for the value 159 | * maximum - The maximum value for the value 160 | * minimumError - The error message for values that are too low 161 | * maximumError - The error message for values that are too high 162 | * notANumberError - The error message for values that are not a number 163 | * UniqueValidator - Enforces uniqueness of values, testing against a store. 164 | * uniqueStore - The store that will be accessed to determine if a value is unique 165 | * uniqueError - The error message for when the value is not unique 166 | 167 | We can also combine validators. We can do this by using Dojo's `declare()` to mixin additional validators. For example, if we wanted to use the StringValidator in combination with the UniqueValidator, we could write: 168 | 169 | schema: { 170 | username: new (declare([StringValidator, UniqueValidator]))({ 171 | pattern: /^\w+$/, 172 | // the store to do lookups for uniqueness 173 | uniqueStore: userStore 174 | }) 175 | } 176 | 177 | Or we can use the validators array to provide a set of validators that should be applied. For example, we could alternately write this: 178 | 179 | schema: { 180 | username: { 181 | validators: [ 182 | new StringValidator({pattern: /^\w+$/}), 183 | new UniqueValidator({uniqueStore: userStore}) 184 | ] 185 | } 186 | } 187 | 188 | This can be particularly useful in our validators may have properties that collide with each other, or we generally just want to keep them distinct from the property. 189 | 190 | ### Extensions 191 | 192 | #### JSON Schema Based Models 193 | 194 | Models can be defined through [JSON Schema](http://json-schema.org/) (v3). A store with a Model based on a JSON Schema can be created with the `dmodel/extensions/jsonSchema` module. For example: 195 | 196 | define(['dmodel/extensions/jsonSchema', ...], function (jsonSchema, ...) { 197 | var myStore = new Memory({ 198 | model: jsonSchema({ 199 | properties: { 200 | someProperty: { 201 | type: "number", 202 | minimum: 0, 203 | maximum: 10 204 | }, 205 | } 206 | }) 207 | }) 208 | 209 | ### Queue Notifications 210 | 211 | dmodel will queue notifications so that multiple changes to a property can be delivered in a single notification. This is done for you automatically, to provide efficient notification of changes to listeners. 212 | 213 | The queuing mechanism is beneficial in that it avoids repetitive or intermediate notifications that occur during multiple changes to a model object. For example, if we had the computed property for `fullName` as described above, and we change multiple properties in a single `set()`: 214 | 215 | person.set({ 216 | firstName: 'John', 217 | lastName: 'Doe' 218 | }); 219 | 220 | 221 | Without notification queuing, the notification listener might be called for each intermediate step in the process (once for the change to `firstName`, once for the change to `lastName`), but the queuing means that the computed property listener for `fullName` would only be called once, for the resulting change, after both the dependent properties have changed. 222 | 223 | However, dmodel can be configured to use different strategies for when the queued notifications will be fired. By default, the notifications will be fired after the highest level `set()` operation completes. However, we can alternately configure dmodel to wait for the next event turn to fire notifications. This can be done by setting the `Model.nextTurn` property to another function that can defer the callback. For example, in NodeJS, we could use `process.nextTick`, or in the browser we could set it to `setImmediate` or `setTimeout`: 224 | 225 | Model.nextTurn = window.setImmediate || setTimeout; 226 | 227 | This would queue up all the notifications that occur before the next event turn, before calling the callbacks. 228 | 229 | ### HiddenProperties Model 230 | 231 | The `dmodel/extensions/HiddenProperties` module provides an extension of `dmodel/Model` where all the model properties are stored on a separate objects, rather than the model instance itself. This can provide a couple of advantages. First, model instances can be restored from persistence quicker since, the model instance simply needs to be instantiated with a reference to an existing object, rather than modifying the prototype chain. Second, this can be useful if you wish to protect properties from being directly accessed on the model object. Since the property values are stored on a separate object, this encourages property access through `get`, `set`, and `property`. This can be used as a model for stores, although you may want to use a custom query engine, depending on how you want property access to function during querying. 232 | 233 | ## Stores 234 | 235 | ### Validating 236 | 237 | The `dmodel/store/Validating` module is a store mixin that adds functionality for validating any objects that are saved through `put()` or `add()`. The validation relies on the `Model` for the objects, so any property constraints that should be applied should be defined on the model's schema. If validation fails on `put()` or `add()` than a validation `TypeError` will be thrown, with an `errors` property that lists any validation errors. 238 | -------------------------------------------------------------------------------- /docs/Design.md: -------------------------------------------------------------------------------- 1 | The purpose of this document is to explain the following goals and design decisions in dmodel: 2 | 3 | * Validation and properties - The goal of dmodel is to make declaration of validated properties as easy as possible. There are a couple of approaches we considered, including keeping validators in a separate array, and mixing in the functionality in properties. We opted to allow both approaches because each has some key advantages. Validation in arrays provides: 4 | * Clear distinct list of validators 5 | * No conflicts between validators 6 | * Multiple asynchronous validators can easily be combined 7 | Validators also extend properties, can be mixed in, can be used directly as properties or mixed in to property classes, with the following benefits: 8 | * A single constructor for a property with validation provided 9 | * Encapsulate all the property concerns in a single class/instance 10 | * Metadata about validation (required, type, min, max, etc.), is directly available as metadata properties that can easily be accessed by consumers 11 | * Property reuse between validators (that are mixed in) 12 | * Full inheritance capabilities provide more complex possibilities of validators, including integration with other aspects of a property (like coercion) 13 | 14 | * binding - While dmodel was still part of dstore, we added a bindTo method to the Model class in the bindTo branch (https://github.com/SitePen/dstore/tree/bindTo) based on the idea of giving a target to bind a property to. After implementing this, I am not sure I really like this API though. The fact that the bindTo has a potential side effects if an existing binding exists seems like it makes for an unpredictable and awkward interface. 15 | -------------------------------------------------------------------------------- /docs/Testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | dmodel uses [Intern](http://theintern.io/) as its test runner. Tests can 4 | either be run using the browser, or using [Sauce Labs](https://saucelabs.com/). 5 | More information on writing your own tests with Intern can be found in the 6 | [Intern wiki](https://github.com/theintern/intern/wiki). 7 | 8 | ## Setting up 9 | 10 | **Note:** Commands listed in this section are all written assuming they are 11 | run in the `dmodel` directory. 12 | 13 | Install dependencies for testing. 14 | 15 | ``` 16 | npm install 17 | ``` 18 | 19 | ## Running via the browser 20 | 21 | 1. Open a browser to http://hostname/path_to_dmodel/tests/runTests.html 22 | 2. View the console 23 | 24 | ## Running in the console 25 | 26 | Run the tests with the following command: 27 | 28 | ``` 29 | node node_modules/intern-geezer/client config=tests/intern 30 | ``` 31 | 32 | ## Running via Sauce Labs 33 | 34 | Make sure the proper Sauce Labs credentials are set in the environment: 35 | 36 | ``` 37 | export SAUCE_USERNAME= 38 | export SAUCE_ACCESS_KEY= 39 | ``` 40 | 41 | Then kick off the runner with the following command: 42 | 43 | ``` 44 | node node_modules/intern-geezer/runner config=tests/intern 45 | ``` 46 | -------------------------------------------------------------------------------- /extensions/HiddenProperties.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dojo/_base/lang', 4 | 'dojo/json', 5 | '../Model' 6 | ], function (declare, lang, JSON, Model) { 7 | // summary: 8 | // Extends the Model to keep properties on a '_values' sub-object 9 | // This can provide the benefits of keeping properties only 10 | // (publicly) accessible through getters and setters and can 11 | // also be faster to instantiate 12 | return declare(Model, { 13 | _setValues: function (values) { 14 | return this._values = values || {}; 15 | }, 16 | 17 | _getValues: function () { 18 | return this._values; 19 | }, 20 | 21 | _restore: function (model) { 22 | // we nest our properties 23 | var instance = lang.delegate(model.prototype); 24 | instance._values = this; 25 | return instance; 26 | }, 27 | 28 | toJSON: function () { 29 | return this._values; 30 | } 31 | }); 32 | }); -------------------------------------------------------------------------------- /extensions/jsonSchema.js: -------------------------------------------------------------------------------- 1 | define(['../Property', '../Model', 'dojo/_base/declare', 'json-schema/lib/validate'], 2 | function (Property, Model, declare, jsonSchemaValidator) { 3 | // module: 4 | // dstore/extensions/JsonSchema 5 | // summary: 6 | // This module generates a dstore schema from a JSON Schema to enabled validation of objects 7 | // and property changes with JSON Schema 8 | return function (jsonSchema) { 9 | // create the schema that can be used by dstore/Model 10 | var modelSchema = {}; 11 | var properties = jsonSchema.properties || jsonSchema; 12 | 13 | // the validation function, this can be used for all the properties 14 | function checkForErrors() { 15 | var value = this.valueOf(); 16 | var key = this.name; 17 | // get the current value and test it against the property's definition 18 | var validation = jsonSchemaValidator.validate(value, properties[key]); 19 | // set any errors 20 | var errors = validation.errors; 21 | if (errors) { 22 | // assign the property names to the errors 23 | for (var i = 0; i < errors.length; i++) { 24 | errors[i].property = key; 25 | } 26 | } 27 | return errors; 28 | } 29 | 30 | // iterate through the schema properties, creating property validators 31 | for (var i in properties) { 32 | var jsDefinition = properties[i]; 33 | var definition = modelSchema[i] = new Property({ 34 | checkForErrors: checkForErrors 35 | }); 36 | if (typeof jsDefinition.type === 'string') { 37 | // copy the type so it can be used for coercion 38 | definition.type = jsDefinition.type; 39 | } 40 | if (typeof jsDefinition['default'] === 'string') { 41 | // and copy the default 42 | definition['default'] = jsDefinition['default']; 43 | } 44 | } 45 | return declare(Model, { 46 | schema: modelSchema 47 | }); 48 | }; 49 | }); -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | var miniExcludes = { 2 | 'dmodel/README.md': 1, 3 | 'dmodel/package': 1 4 | }, 5 | isTestRe = /\/test\//; 6 | 7 | var packages = {}; 8 | try { 9 | // retrieve the set of packages for determining which modules to include 10 | require(['util/build/buildControl'], function (buildControl) { 11 | packages = buildControl.packages; 12 | }); 13 | } catch (error) { 14 | console.error('Unable to retrieve packages for determining optional package support in dmodel'); 15 | } 16 | var profile = { 17 | resourceTags: { 18 | test: function (filename, mid) { 19 | return isTestRe.test(filename); 20 | }, 21 | 22 | miniExclude: function (filename, mid) { 23 | return /\/(?:tests|demos|docs)\//.test(filename) || mid in miniExcludes; 24 | }, 25 | 26 | amd: function (filename, mid) { 27 | return /\.js$/.test(filename); 28 | }, 29 | 30 | copyOnly: function (filename, mid) { 31 | // conditionally omit modules dependent on json-schema packages 32 | return (!packages['json-schema'] && /jsonSchema\.js/.test(filename)); 33 | } 34 | } 35 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dmodel", 3 | "author": "Kris Zyp", 4 | "version": "0.1.0", 5 | "description": "A reactive object data modelling library", 6 | "licenses": [ 7 | { 8 | "type": "AFLv2.1", 9 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L43" 10 | }, 11 | { 12 | "type": "BSD", 13 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L13" 14 | } 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "http://github.com/SitePen/dstore" 19 | }, 20 | "dependencies": { 21 | "dojo": "1.8.6" 22 | }, 23 | "devDependencies": { 24 | "intern-geezer": "1.6.*", 25 | "dstore": "1.0.0", 26 | "json-schema": "0.2.2" 27 | }, 28 | "directories": { 29 | "lib": "." 30 | }, 31 | "main": "./Model", 32 | "dojoBuild": "package.js" 33 | } 34 | -------------------------------------------------------------------------------- /store/Validating.js: -------------------------------------------------------------------------------- 1 | define(['dstore/Store', 'dojo/when', 'dojo/_base/declare'], function (Store, when, declare) { 2 | // module: 3 | // dstore/Validating 4 | // summary: 5 | // This module provides a store mixin that enforces validation of objects on put and add 6 | return declare(Store, { 7 | validate: function (object) { 8 | // summary: 9 | // Validates the given object (by making it an instance of the 10 | // current model, and calling validate() on it) 11 | // object: Object 12 | // The object to validate 13 | // isNew: Boolean 14 | // Indicates whether or not to assume the object is new or not 15 | if (!(object instanceof this.Model)) { 16 | object = this._restore(object); 17 | } 18 | return when(object.validate()).then(function (isValid) { 19 | if (!isValid) { 20 | // create and throw a validation error 21 | var validationError = new TypeError('Invalid property'); 22 | validationError.errors = object.errors; 23 | throw validationError; 24 | } 25 | // return the object since it has had its prototype assigned 26 | return object; 27 | }); 28 | }, 29 | put: function (object, options) { 30 | var inheritedPut = this.getInherited(arguments); 31 | var store = this; 32 | return when(this.validate(object), function (object) { 33 | return inheritedPut.call(store, object, options); 34 | }); 35 | }, 36 | add: function (object, options) { 37 | var inheritedAdd = this.getInherited(arguments); 38 | var store = this; 39 | return when(this.validate(object, true), function (object) { 40 | return inheritedAdd.call(store, object, options); 41 | }); 42 | } 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/Model.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'intern!object', 3 | 'intern/chai!assert', 4 | 'dojo/json', 5 | 'dojo/_base/declare', 6 | 'dojo/_base/lang', 7 | 'dojo/Deferred', 8 | '../Model', 9 | '../Property', 10 | '../ComputedProperty', 11 | '../extensions/HiddenProperties', 12 | 'dstore/Memory' 13 | ], function (registerSuite, assert, JSON, declare, lang, Deferred, Model, Property, ComputedProperty, HiddenProperties, Memory) { 14 | function createPopulatedModel() { 15 | var model = new (declare(Model, { 16 | schema: { 17 | string: 'string', 18 | number: 'number', 19 | boolean: 'boolean', 20 | object: Object, 21 | array: Array, 22 | any: {}, 23 | accessor: { 24 | put: function (value) { 25 | return this._parent._accessor = value; 26 | }, 27 | valueOf: function () { 28 | return this._parent._accessor; 29 | } 30 | } 31 | }, 32 | additionalProperties: false 33 | 34 | }))(); 35 | 36 | model.set({ 37 | string: 'foo', 38 | number: 1234, 39 | 'boolean': true, 40 | object: { foo: 'foo' }, 41 | array: [ 'foo', 'bar' ], 42 | any: 'foo', 43 | invalid: 'foo', 44 | accessor: 'foo' 45 | }); 46 | 47 | return model; 48 | } 49 | 50 | 51 | var modelTests = { 52 | name: 'Model', 53 | 54 | '#get and #set': function () { 55 | var model = createPopulatedModel(); 56 | assert.strictEqual(model.get('string'), 'foo', 'string schema properties should be mutable as strings from an object'); 57 | assert.strictEqual(model.get('number'), 1234, 'number schema properties should be mutable as numbers from an object'); 58 | assert.strictEqual(model.get('boolean'), true, 'boolean schema properties should be mutable as booleans from an object'); 59 | assert.deepEqual(model.get('object'), { foo: 'foo' }, 'Object schema properties should be mutable as objects from an object'); 60 | assert.deepEqual(model.get('array'), [ 'foo', 'bar' ], 'Array schema properties should be mutable as arrays from an object'); 61 | assert.strictEqual(model.get('any'), 'foo', 'null schema properties should be mutable as any value from an object'); 62 | assert.strictEqual(model.get('invalid'), undefined, 'non-existant schema properties should not be mutable from an object'); 63 | assert.strictEqual(model.get('accessor'), 'foo', 'accessors and mutators should work normally'); 64 | 65 | model.set('number', 'not-a-number'); 66 | assert.typeOf(model.get('number'), 'number', 67 | 'number schema properties should still be numbers even if passed a non-number value'); 68 | assert.isTrue(isNaN(model.get('number')), 69 | 'number schema properties should still be set even if passed a non-number value'); 70 | 71 | model.set('string', 1234); 72 | assert.typeOf(model.get('string'), 'string', 73 | 'string schema properties should still be strings even if passed a non-string value'); 74 | assert.strictEqual(model.get('string'), '1234', 75 | 'string schema properties should still be set even if passed a non-string value'); 76 | 77 | model.set('boolean', 'foo'); 78 | assert.typeOf(model.get('boolean'), 'boolean', 79 | 'boolean schema properties should still be booleans even if passed a non-boolean value'); 80 | assert.strictEqual(model.get('boolean'), true, 81 | 'boolean schema properties should still be set even if passed a non-boolean value'); 82 | 83 | model.set('boolean', 'false'); 84 | assert.strictEqual(model.get('boolean'), false, 'setting "false" string to boolean property should set it to false'); 85 | 86 | model.set('boolean', '0'); 87 | assert.strictEqual(model.get('boolean'), false, 'setting "0" string to boolean property should set it to false'); 88 | 89 | model.set('boolean', []); 90 | assert.strictEqual(model.get('boolean'), false, 'setting an empty array to boolean property should set it to false'); 91 | 92 | model.set('array', 'foo'); 93 | assert.instanceOf(model.get('array'), Array, 'Array schema properties should still be Arrays even if passed a non-Array value'); 94 | assert.deepEqual(model.get('array'), [ 'foo' ], 'Array schema properties should still be set even if passed a non-Array value'); 95 | 96 | model.set('any', 1234); 97 | assert.typeOf(model.get('any'), 'number', 'any-type schema properties should be the type of the value passed'); 98 | assert.strictEqual(model.get('any'), 1234, 'any-type schema properties should be set regardless of the type of value'); 99 | 100 | model.set('invalid', 'foo'); 101 | assert.strictEqual(model.get('invalid'), undefined, 'non-existant schema properties should not be mutable'); 102 | }, 103 | '#property and #receive': function () { 104 | var model = createPopulatedModel(); 105 | function assertReceived (expected, test) { 106 | var receivedCount = 0; 107 | test(function (value) { 108 | assert.strictEqual(expected, value); 109 | receivedCount++; 110 | }); 111 | assert.strictEqual(receivedCount, 1); 112 | } 113 | assertReceived('foo', function (callback) { 114 | model.property('string').observe(callback); 115 | }); 116 | assertReceived('foo', function (callback) { 117 | model.property('string').observe(callback); 118 | }); 119 | assertReceived('foo', function (callback) { 120 | callback(model.property('string').valueOf()); 121 | }); 122 | assertReceived(1234, function (callback) { 123 | model.property('number').observe(callback); 124 | }); 125 | assertReceived(true, function (callback) { 126 | model.property('boolean').observe(callback); 127 | }); 128 | // make sure coercion works 129 | assert.strictEqual(model.property('number') + 1234, 2468); 130 | // reset the model, so don't have to listeners 131 | model = createPopulatedModel(); 132 | var string = model.property('string'); 133 | string.put(1234); 134 | assertReceived('1234', function (callback) { 135 | string.observe(callback); 136 | }); 137 | assertReceived('1234', function (callback) { 138 | model.property('string').observe(callback); 139 | }); 140 | assertReceived(true, function (callback) { 141 | model.property('boolean').observe(callback, {onlyFutureUpdates: true}); 142 | model.set('boolean', true); 143 | }); 144 | var number = model.property('number'); 145 | number.put(0); 146 | var order = []; 147 | number.observe(function (newValue) { 148 | order.push(newValue); 149 | }); 150 | number.put(1); 151 | model.set('number', 2); 152 | model.set('number', '3'); 153 | model.property('number').put(4); 154 | number.put('5'); 155 | 156 | assert.deepEqual(order, [0, 1, 2, 3, 4, 5]); 157 | model.property('object').set('foo', 'bar'); 158 | 159 | model.prepareForSerialization(); 160 | assert.strictEqual(JSON.stringify(model), '{"string":"1234","number":5,' + 161 | '"boolean":true,"object":{"foo":"bar"},"array":["foo","bar"],"any":"foo"' + 162 | (model instanceof HiddenProperties ? '' : ',"_accessor":"foo"') + '}'); 163 | }, 164 | 165 | 'property definitions': function () { 166 | var model = new (declare(Model, { 167 | schema: { 168 | requiredString: new Property({ 169 | type: 'string', 170 | required: true 171 | }), 172 | range: new Property({ 173 | checkForErrors: function (value) { 174 | var errors = this.inherited(arguments); 175 | if (value < 10 || value > 20) { 176 | errors.push('not in range'); 177 | } 178 | return errors; 179 | } 180 | }), 181 | hasDefault: { 182 | 'default': 'beginning value' 183 | }, 184 | hasDefaultFunction: { 185 | 'default': function () { 186 | return 'start with this'; 187 | } 188 | } 189 | }, 190 | validateOnSet: false 191 | }))(); 192 | assert.strictEqual(model.get('hasDefault'), 'beginning value'); 193 | assert.strictEqual(model.get('hasDefaultFunction'), 'start with this'); 194 | model.set('requiredString', 'a string'); 195 | model.set('range', 15); 196 | assert.isTrue(model.validate()); 197 | var lastReceivedErrors; 198 | model.property('range').property('errors').observe(function (errors) { 199 | lastReceivedErrors = errors; 200 | }); 201 | model.set('requiredString', ''); 202 | assert.isFalse(model.validate()); 203 | model.set('range', 33); 204 | assert.isFalse(model.validate()); 205 | assert.deepEqual(model.property('requiredString').get('errors'), ['required, and it was not present']); 206 | assert.deepEqual(model.property('range').get('errors'), ['not in range']); 207 | assert.deepEqual(lastReceivedErrors, ['not in range']); 208 | model.set('requiredString', 'a string'); 209 | model.set('range', 15); 210 | assert.isTrue(model.validate()); 211 | assert.strictEqual(lastReceivedErrors, undefined); 212 | model.property('range').addError('manually added error'); 213 | assert.deepEqual(lastReceivedErrors, ['manually added error']); 214 | }, 215 | defaults: function () { 216 | var model = new (declare(Model, { 217 | schema: { 218 | toOverride: { 219 | 'default': 'beginning value' 220 | }, 221 | hasDefault: { 222 | 'default': 'beginning value' 223 | } 224 | } 225 | }))({ 226 | toOverride: 'new value' 227 | }); 228 | assert.strictEqual(model.get('toOverride'), 'new value'); 229 | assert.strictEqual(model.get('hasDefault'), 'beginning value'); 230 | }, 231 | 'computed properties': function () { 232 | var model = new (declare(Model, { 233 | schema: { 234 | firstName: 'string', 235 | lastName: 'string', 236 | name: new ComputedProperty({ 237 | dependsOn: ['firstName', 'lastName'], 238 | getValue: function (firstName, lastName) { 239 | return firstName + ' ' + lastName; 240 | }, 241 | setValue: function (value, parent) { 242 | var parts = value.split(' '); 243 | parent.set('firstName', parts[0]); 244 | parent.set('lastName', parts[1]); 245 | } 246 | }), 247 | birthDate: new ComputedProperty({ 248 | dependsOn: ['birthDate'], 249 | getValue: function (birthDate) { 250 | return new Date(birthDate); 251 | }, 252 | setValue: function (value, parent) { 253 | return parent[this.name] = value.getTime(); 254 | } 255 | }) 256 | }, 257 | validateOnSet: false 258 | }))({ 259 | firstName: 'John', 260 | lastName: 'Doe' 261 | }); 262 | var updatedName; 263 | var nameProperty = model.property('name'); 264 | var nameUpdateCount = 0; 265 | nameProperty.observe(function (name) { 266 | updatedName = name; 267 | nameUpdateCount++; 268 | }); 269 | assert.strictEqual(nameProperty.valueOf(), 'John Doe'); 270 | assert.strictEqual(nameUpdateCount, 1); 271 | model.set('firstName', 'Jane'); 272 | assert.strictEqual(updatedName, 'Jane Doe'); 273 | assert.strictEqual(nameUpdateCount, 2); 274 | var updatedName2; 275 | var handle = model.observe('name', function (name) { 276 | updatedName2 = name; 277 | }); 278 | assert.strictEqual(updatedName2, 'Jane Doe'); 279 | 280 | model.set('lastName', 'Smith'); 281 | assert.strictEqual(updatedName, 'Jane Smith'); 282 | assert.strictEqual(updatedName2, 'Jane Smith'); 283 | assert.strictEqual(nameUpdateCount, 3); 284 | handle.remove(); 285 | 286 | model.set({ 287 | firstName: 'John', 288 | lastName: 'Doe' 289 | }); 290 | assert.strictEqual(updatedName, 'John Doe'); 291 | assert.strictEqual(updatedName2, 'Jane Smith'); 292 | assert.strictEqual(nameUpdateCount, 4); 293 | model.set('name', 'Adam Smith'); 294 | assert.strictEqual(updatedName, 'Adam Smith'); 295 | assert.strictEqual(model.get('firstName'), 'Adam'); 296 | assert.strictEqual(model.get('lastName'), 'Smith'); 297 | assert.strictEqual(nameUpdateCount, 5); 298 | assert.strictEqual(updatedName2, 'Jane Smith'); 299 | var then = new Date(1000000); 300 | model.set('birthDate', then); 301 | assert.strictEqual(model.get('birthDate').getTime(), 1000000); 302 | var updatedDate, now = new Date(); 303 | model.observe('birthDate', function (newDate) { 304 | updatedDate = newDate; 305 | }); 306 | model.set('birthDate', now); 307 | assert.strictEqual(updatedDate.getTime(), now.getTime()); 308 | 309 | var standaloneComputed = new ComputedProperty({ 310 | dependsOn: [nameProperty, model.property('birthDate')], 311 | getValue: function (name, birthDate) { 312 | return name + ' is ' + ((now - birthDate > 315360000000) ? 'older' : 'younger') + 313 | ' than ten years old'; 314 | } 315 | }); 316 | var updatedComputed; 317 | standaloneComputed.observe(function (newValue) { 318 | updatedComputed = newValue; 319 | }); 320 | assert.strictEqual(standaloneComputed.valueOf(), 'Adam Smith is younger than ten years old'); 321 | assert.strictEqual(updatedComputed, 'Adam Smith is younger than ten years old'); 322 | model.set('birthDate', then); 323 | assert.strictEqual(standaloneComputed.valueOf(), 'Adam Smith is older than ten years old'); 324 | assert.strictEqual(updatedComputed, 'Adam Smith is older than ten years old'); 325 | model.set('firstName', 'John'); 326 | assert.strictEqual(standaloneComputed.valueOf(), 'John Smith is older than ten years old'); 327 | assert.strictEqual(updatedComputed, 'John Smith is older than ten years old'); 328 | }, 329 | '#save async': function () { 330 | var model = new Model(); 331 | 332 | // If there is an exception in the basic save logic, it will be used to fail the test 333 | return model.save(); 334 | }, 335 | 336 | '#validate async': function () { 337 | var AsyncStringIsBValidator = declare(null, { 338 | checkForErrors: function (value) { 339 | var errors = this.inherited(arguments); 340 | var dfd = new Deferred(); 341 | setTimeout(function () { 342 | if (value !== 'b') { 343 | errors.push('it is not b'); 344 | } 345 | dfd.resolve(errors); 346 | }, 0); 347 | return dfd.promise; 348 | } 349 | }); 350 | var PropertyAsyncStringIsBValidator = declare([Property, AsyncStringIsBValidator]); 351 | 352 | 353 | var model = new (declare(Model, { 354 | schema: { 355 | test: new PropertyAsyncStringIsBValidator(), 356 | test2: new PropertyAsyncStringIsBValidator({ 357 | 'default': 'b' 358 | }) 359 | } 360 | }))(); 361 | model.set('test', 'a'); 362 | 363 | return model.validate().then(function (isValid) { 364 | assert.isFalse(isValid, 'Invalid model should validate to false'); 365 | 366 | var errors = model.property('test').get('errors'); 367 | assert.strictEqual(errors.length, 1, 'Invalid model field should have only one error'); 368 | assert.strictEqual(errors[0], 'it is not b', 'Invalid model error should be set properly from validator'); 369 | 370 | errors = model.property('test2').get('errors'); 371 | assert.isNotArray(errors, 'Valid model field should have zero errors'); 372 | }); 373 | }, 374 | 375 | '#isFieldRequired': function () { 376 | var model = new (declare(Model, { 377 | schema: { 378 | requiredField: new Property({ 379 | required: true 380 | }), 381 | optionalField: {} 382 | } 383 | }))(); 384 | 385 | assert.isTrue(model.property('requiredField').required, 'Field should be required'); 386 | assert.isFalse(!!model.property('optionalField').required, 'Field should not be required'); 387 | }, 388 | 389 | chaining: function () { 390 | var model = createPopulatedModel(), 391 | order = []; 392 | 393 | model.set('number', 1); 394 | var doubled = model.property('number').observe(function (value) { 395 | return value * 2; 396 | }); 397 | doubled.observe(function (value) { 398 | order.push(value); 399 | }); 400 | model.set('number', 2); 401 | model.set('number', 3); 402 | doubled.remove(); 403 | model.set('number', 4); 404 | assert.deepEqual(order, [2, 4, 6]); 405 | }, 406 | 407 | 'object values': function () { 408 | var model = new Model(), 409 | order = []; 410 | model.set('object', { 411 | number: 1 412 | }); 413 | var number = model.property('object', 'number'); 414 | number.observe(function (value) { 415 | order.push(value); 416 | }); 417 | model.set('object', { 418 | number: 2 419 | }); 420 | model.property('object').set('number', 3); 421 | assert.deepEqual(order, [1, 2, 3]); 422 | }, 423 | 424 | 'with store': function () { 425 | var store = new Memory({ 426 | data: [ 427 | {id: 1, num: 1, str: 'hi', bool: true} 428 | ], 429 | Model: declare(Model, { 430 | schema: { 431 | str: { 432 | type: 'string', 433 | required: true 434 | }, 435 | num: 'number' 436 | } 437 | }) 438 | }); 439 | var myObject = store.getSync(1); 440 | assert.strictEqual(myObject.get('num'), 1); 441 | myObject.set('num', 5); 442 | myObject.set('str', ''); 443 | assert['throw'](function () { 444 | return myObject.save(); 445 | }, Error); 446 | assert.isFalse(myObject.isValid()); 447 | myObject.set('str', 'hellow'); 448 | myObject.save(); 449 | myObject = store.getSync(1); 450 | assert.strictEqual(myObject.get('num'), 5); 451 | } 452 | 453 | }; 454 | registerSuite(modelTests); 455 | registerSuite(lang.mixin({}, modelTests, { 456 | name: 'HiddenProperties', 457 | setup: function(){ 458 | Model = HiddenProperties; 459 | } 460 | })); 461 | 462 | }); 463 | -------------------------------------------------------------------------------- /tests/Store.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'intern!object', 3 | 'intern/chai!assert', 4 | 'dojo/_base/declare', 5 | 'dmodel/Model', 6 | 'dstore/Memory' 7 | ], function (registerSuite, assert, declare, Model, Memory) { 8 | 9 | var store; 10 | 11 | registerSuite({ 12 | name: 'dmodel Store Interaction', 13 | 14 | beforeEach: function () { 15 | store = new Memory({ 16 | data: [ 17 | { id: 1, name: 'one', prime: false, mappedTo: 'E' }, 18 | { id: 2, name: 'two', even: true, prime: true, mappedTo: 'D' }, 19 | { id: 3, name: 'three', prime: true, mappedTo: 'C' }, 20 | { id: 4, name: 'four', even: true, prime: false, mappedTo: null }, 21 | { id: 5, name: 'five', prime: true, mappedTo: 'A' } 22 | ], 23 | Model: Model, 24 | filterFunction: function (object) { 25 | return object.name === 'two'; 26 | } 27 | }); 28 | 29 | // add a method to the model prototype 30 | store.Model.prototype.describe = function () { 31 | return this.name + ' is ' + (this.prime ? '' : 'not ') + 'a prime'; 32 | }; 33 | }, 34 | 35 | 'Model': function () { 36 | assert.strictEqual(store.getSync(1).describe(), 'one is not a prime'); 37 | assert.strictEqual(store.getSync(3).describe(), 'three is a prime'); 38 | assert.strictEqual(store.filter({even: true}).fetchSync()[1].describe(), 'four is not a prime'); 39 | }, 40 | 41 | save: function () { 42 | var four = store.getSync(4); 43 | four.square = true; 44 | four.save(); 45 | four = store.getSync(4); 46 | assert.isTrue(four.square); 47 | }, 48 | 49 | filter: function () { 50 | return store.filter({even: false}).forEach(function (object) { 51 | // the intrinsic methods 52 | assert.equal(typeof object.save, 'function'); 53 | assert.equal(typeof object.remove, 'function'); 54 | }); 55 | }, 56 | 'get and save': function () { 57 | var expectedObject = { id: 1, name: 'one', prime: false, mappedTo: 'E' }; 58 | 59 | return store.get(1).then(function (object) { 60 | expectedObject._scenario = 'update'; 61 | return object.save().then(function (result) { 62 | assert.deepEqual(JSON.stringify(result), JSON.stringify(expectedObject)); 63 | }); 64 | }); 65 | } 66 | 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/all.js: -------------------------------------------------------------------------------- 1 | define([ 2 | './Model', 3 | './extensions/validating-jsonSchema', 4 | './validating', 5 | './Store', 6 | './validators' 7 | ], function () { 8 | }); 9 | -------------------------------------------------------------------------------- /tests/extensions/validating-jsonSchema.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'intern!object', 3 | 'intern/chai!assert', 4 | 'dojo/_base/lang', 5 | 'dojo/json', 6 | 'dojo/_base/declare', 7 | 'dstore/Memory', 8 | 'dmodel/store/Validating', 9 | 'dmodel/extensions/jsonSchema' 10 | ], function (registerSuite, assert, lang, JSON, declare, Memory, Validating, jsonSchema) { 11 | 12 | var validatingMemory = (declare([Memory, Validating]))({ 13 | Model: jsonSchema({ 14 | properties: { 15 | prime: { 16 | type: 'boolean' 17 | }, 18 | number: { 19 | type: 'number', 20 | minimum: 1, 21 | maximum: 10 22 | }, 23 | name: { 24 | type: 'string', 25 | required: true 26 | } 27 | } 28 | }) 29 | }); 30 | validatingMemory.setData([ 31 | {id: 1, name: 'one', number: 1, prime: false, mappedTo: 'E'}, 32 | {id: 2, name: 'two', number: 2, prime: true, mappedTo: 'D'}, 33 | {id: 3, name: 'three', number: 3, prime: true, mappedTo: 'C'}, 34 | {id: 4, name: 'four', number: 4, even: true, prime: false, mappedTo: null}, 35 | {id: 5, name: 'five', number: 5, prime: true, mappedTo: 'A'} 36 | ]); 37 | 38 | registerSuite({ 39 | name: 'dstore validating jsonSchema', 40 | 41 | 'get': function () { 42 | assert.strictEqual(validatingMemory.getSync(1).name, 'one'); 43 | }, 44 | 45 | 'model errors': function () { 46 | validatingMemory.allowErrors = true; 47 | var four = validatingMemory.getSync(4); 48 | four.set('number', 33); 49 | assert.strictEqual(JSON.stringify(four.property('number').get('errors')), JSON.stringify([ 50 | {'property': 'number', 'message': 'must have a maximum value of 10'} 51 | ])); 52 | four.set('number', 3); 53 | assert.strictEqual(four.property('number').get('errors'), undefined); 54 | }, 55 | 56 | 'put update': function () { 57 | var four = lang.delegate(validatingMemory.getSync(4)); 58 | four.prime = 'not a boolean'; 59 | four.number = 34; 60 | four.name = 33; 61 | return validatingMemory.put(four).then(function () { 62 | assert.fail('should not pass validation'); 63 | }, function (validationError) { 64 | assert.strictEqual(JSON.stringify(validationError.errors), JSON.stringify([ 65 | {'property': 'prime', 'message': 'string value found, but a boolean is required'}, 66 | {'property': 'number', 'message': 'must have a maximum value of 10'}, 67 | {'property': 'name', 'message': 'number value found, but a string is required'} 68 | ])); 69 | }); 70 | } 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/intern.js: -------------------------------------------------------------------------------- 1 | // Learn more about configuring this file at . 2 | // These default settings work OK for most people. The options that *must* be changed below are the 3 | // packages, suites, excludeInstrumentation, and (if you want functional tests) functionalSuites. 4 | 5 | define({ 6 | // The port on which the instrumenting proxy will listen 7 | proxyPort: 9000, 8 | 9 | // A fully qualified URL to the Intern proxy 10 | proxyUrl: 'http://localhost:9000/', 11 | 12 | // Default desired capabilities for all environments. Individual capabilities can be overridden by any of the 13 | // specified browser environments in the `environments` array below as well. See 14 | // https://code.google.com/p/selenium/wiki/DesiredCapabilities for standard Selenium capabilities and 15 | // https://saucelabs.com/docs/additional-config#desired-capabilities for Sauce Labs capabilities. 16 | // Note that the `build` capability will be filled in with the current commit ID from the Travis CI environment 17 | // automatically 18 | capabilities: { 19 | 'selenium-version': '2.33.0' 20 | }, 21 | 22 | // Browsers to run integration testing against. Note that version numbers must be strings if used with Sauce 23 | // OnDemand. Options that will be permutated are browserName, version, platform, and platformVersion; any other 24 | // capabilities options specified for an environment will be copied as-is 25 | environments: [ 26 | { browserName: 'internet explorer', version: '10', platform: 'Windows 8' }, 27 | { browserName: 'internet explorer', version: '9', platform: 'Windows 7' }, 28 | { browserName: 'firefox', version: '22', platform: [ 'Linux', 'Windows 7' ] }, 29 | { browserName: 'firefox', version: '21', platform: 'Mac 10.6' }, 30 | { browserName: 'chrome', platform: [ 'Linux', 'Mac 10.8', 'Windows 7' ] }, 31 | { browserName: 'safari', version: '6', platform: 'Mac 10.8' } 32 | ], 33 | 34 | // Maximum number of simultaneous integration tests that should be executed on the remote WebDriver service 35 | maxConcurrency: 3, 36 | 37 | // Whether or not to start Sauce Connect before running tests 38 | useSauceConnect: true, 39 | 40 | // Connection information for the remote WebDriver service. If using Sauce Labs, keep your username and password 41 | // in the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables unless you are sure you will NEVER be 42 | // publishing this configuration file somewhere 43 | webdriver: { 44 | host: 'localhost', 45 | port: 4444 46 | }, 47 | 48 | // Configuration options for the module loader; any AMD configuration options supported by the Dojo loader can be 49 | // used here 50 | loader: { 51 | baseUrl: typeof process === 'undefined' ? 52 | // if we are using the full path to dmodel, we assume we are running 53 | // in a sibling path configuration 54 | location.search.indexOf('config=dmodel') > -1 ? '../..' : '..' : 55 | './node_modules', 56 | 57 | // Packages that should be registered with the loader in each testing environment 58 | requestProvider: 'dojo/request/registry', 59 | packages: [ 60 | { name: 'dojo', location: 'dojo' }, 61 | { 62 | name: 'dmodel', 63 | location: typeof process === 'undefined' ? 64 | location.search.indexOf('config=dmodel') > -1 ? 'dmodel' : '..' : 65 | '..' 66 | }, 67 | { name: 'dstore', location: 'dstore' }, 68 | { name: 'json-schema', location: 'json-schema' } 69 | ] 70 | }, 71 | 72 | // Non-functional test suite(s) to run in each browser 73 | suites: [ 'dmodel/tests/all' ], 74 | 75 | // Functional test suite(s) to run in each browser once non-functional tests are completed 76 | functionalSuites: [], 77 | 78 | // A regular expression matching URLs to files that should not be included in code coverage analysis 79 | excludeInstrumentation: /^dojox?|^tests?\// 80 | }); 81 | -------------------------------------------------------------------------------- /tests/runTests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dmodel Intern Unit Test Runner 5 | 6 | 7 | Redirecting to Intern runner. 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/validating.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'intern!object', 3 | 'intern/chai!assert', 4 | 'dojo/_base/lang', 5 | 'dojo/json', 6 | 'dojo/_base/declare', 7 | 'dstore/Memory', 8 | 'dmodel/Model', 9 | 'dmodel/store/Validating', 10 | 'dmodel/validators/NumericValidator' 11 | ], function (registerSuite, assert, lang, JSON, declare, Memory, Model, Validating, NumericValidator) { 12 | 13 | var validatingMemory = new (declare([Memory, Validating]))({ 14 | Model: declare(Model, { 15 | schema: { 16 | prime: 'boolean', 17 | number: new NumericValidator({ 18 | minimum: 1, 19 | maximum: 10 20 | }), 21 | name: { 22 | type: 'string', 23 | required: true 24 | } 25 | } 26 | }) 27 | }); 28 | validatingMemory.setData([ 29 | {id: 1, name: 'one', number: 1, prime: false, mappedTo: 'E'}, 30 | {id: 2, name: 'two', number: 2, prime: true, mappedTo: 'D'}, 31 | {id: 3, name: 'three', number: 3, prime: true, mappedTo: 'C'}, 32 | {id: 4, name: 'four', number: 4, even: true, prime: false, mappedTo: null}, 33 | {id: 5, name: 'five', number: 5, prime: true, mappedTo: 'A'} 34 | ]); 35 | 36 | registerSuite({ 37 | name: 'dstore validatingMemory', 38 | 39 | 'get': function () { 40 | assert.strictEqual(validatingMemory.getSync(1).name, 'one'); 41 | }, 42 | 43 | 'put update': function () { 44 | var four = lang.delegate(validatingMemory.get(4)); 45 | four.prime = 'not a boolean'; 46 | four.number = 34; 47 | four.name = 33; 48 | return validatingMemory.put(four).then(function () { 49 | assert.fail('should not pass validation'); 50 | }, function (validationError) { 51 | assert.strictEqual(JSON.stringify(validationError.errors), JSON.stringify([ 52 | 'not a boolean is not a boolean', 53 | 'The value is too high', 54 | '33 is not a string' 55 | ])); 56 | }); 57 | }, 58 | 'add update': function () { 59 | var four = { 60 | prime: 'not a boolean', 61 | number: 34, 62 | name: 33 63 | }; 64 | return validatingMemory.add(four).then(function () { 65 | assert.fail('should not pass validation'); 66 | }, function (validationError) { 67 | assert.strictEqual(JSON.stringify(validationError.errors), JSON.stringify([ 68 | 'not a boolean is not a boolean', 69 | 'The value is too high', 70 | '33 is not a string' 71 | ])); 72 | }); 73 | } 74 | 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /tests/validators.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dmodel/tests/validators/NumericValidator', 3 | 'dmodel/tests/validators/StringValidator', 4 | 'dmodel/tests/validators/UniqueValidator' 5 | ], function () {}); -------------------------------------------------------------------------------- /tests/validators/NumericValidator.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'intern!object', 3 | 'intern/chai!assert', 4 | 'dojo/_base/declare', 5 | 'dmodel/Model', 6 | 'dstore/Memory', 7 | 'dmodel/validators/NumericValidator' 8 | ], function (registerSuite, assert, declare, Model, Memory, NumericValidator) { 9 | 10 | registerSuite({ 11 | name: 'NumericValidator', 12 | 13 | 'NumericValidator as array': function () { 14 | var model = new Model({ 15 | schema: { 16 | foo: { 17 | validators: [new NumericValidator({ 18 | minimum: 10, 19 | maximum: 20 20 | })] 21 | } 22 | } 23 | }); 24 | var foo = model.property('foo'); 25 | foo.put(30); 26 | assert.deepEqual(foo.get('errors'), ['The value is too high']); 27 | foo.put(1); 28 | assert.deepEqual(foo.get('errors'), ['The value is too low']); 29 | foo.put('fd'); 30 | assert.deepEqual(foo.get('errors'), ['The value is not a number']); 31 | foo.put(15); 32 | assert.strictEqual(foo.get('errors'), undefined); 33 | }, 34 | 'NumericValidator as subclass': function () { 35 | var model = new Model({ 36 | schema: { 37 | foo: new NumericValidator({ 38 | minimum: 10, 39 | maximum: 20 40 | }) 41 | } 42 | }); 43 | var foo = model.property('foo'); 44 | foo.put(30); 45 | assert.deepEqual(foo.get('errors'), ['The value is too high']); 46 | foo.put(1); 47 | assert.deepEqual(foo.get('errors'), ['The value is too low']); 48 | foo.put('fd'); 49 | assert.deepEqual(foo.get('errors'), ['The value is not a number']); 50 | foo.put(15); 51 | assert.strictEqual(foo.get('errors'), undefined); 52 | } 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/validators/StringValidator.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'intern!object', 3 | 'intern/chai!assert', 4 | 'dojo/_base/declare', 5 | 'dmodel/Model', 6 | 'dstore/Memory', 7 | 'dmodel/validators/StringValidator' 8 | ], function (registerSuite, assert, declare, Model, Memory, StringValidator) { 9 | 10 | registerSuite({ 11 | name: 'StringValidator', 12 | 'StringValidator in array': function () { 13 | var model = new Model({ 14 | schema: { 15 | foo: { 16 | validators: [new StringValidator({ 17 | minimumLength: 1, 18 | maximumLength: 10, 19 | pattern: /\w+/ 20 | })] 21 | } 22 | } 23 | }); 24 | var foo = model.property('foo'); 25 | foo.put(''); 26 | assert.deepEqual(foo.get('errors'), ['This is too short', 'The pattern did not match']); 27 | foo.put('this is just too long of string to allow'); 28 | assert.deepEqual(foo.get('errors'), ['This is too long']); 29 | foo.put('???'); 30 | assert.deepEqual(foo.get('errors'), ['The pattern did not match']); 31 | foo.put('hello'); 32 | assert.strictEqual(foo.get('errors'), undefined); 33 | }, 34 | 'StringValidator direct': function () { 35 | var model = new Model({ 36 | schema: { 37 | foo: new StringValidator({ 38 | minimumLength: 1, 39 | maximumLength: 10, 40 | pattern: /\w+/ 41 | }) 42 | } 43 | }); 44 | var foo = model.property('foo'); 45 | foo.put(''); 46 | assert.deepEqual(foo.get('errors'), ['This is too short', 'The pattern did not match']); 47 | foo.put('this is just too long of string to allow'); 48 | assert.deepEqual(foo.get('errors'), ['This is too long']); 49 | foo.put('???'); 50 | assert.deepEqual(foo.get('errors'), ['The pattern did not match']); 51 | foo.put('hello'); 52 | assert.strictEqual(foo.get('errors'), undefined); 53 | } 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/validators/UniqueValidator.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'intern!object', 3 | 'intern/chai!assert', 4 | 'dojo/_base/declare', 5 | 'dmodel/Model', 6 | 'dstore/Memory', 7 | 'dmodel/validators/NumericValidator', 8 | 'dmodel/validators/UniqueValidator' 9 | ], function (registerSuite, assert, declare, Model, Memory, NumericValidator, UniqueValidator) { 10 | 11 | registerSuite({ 12 | name: 'UniqueValidator', 13 | 'UniqueValidator in array with NumericValidator': function () { 14 | var store = new Memory({ 15 | data: [{id: 1}, {id: 2}] 16 | }); 17 | var model = new Model({ 18 | schema: { 19 | foo: { 20 | validators: [new UniqueValidator({ 21 | uniqueStore: store 22 | })] 23 | }, 24 | bar: { 25 | validators: [ 26 | new NumericValidator({ 27 | maximum: 10 28 | }), 29 | new UniqueValidator({ 30 | uniqueStore: store 31 | }) 32 | ] 33 | } 34 | } 35 | }); 36 | var foo = model.property('foo'); 37 | foo.put(1); 38 | assert.deepEqual(foo.get('errors'), ['The value is not unique']); 39 | foo.put(100); 40 | assert.deepEqual(foo.get('errors'), undefined); 41 | var bar = model.property('bar'); 42 | bar.put(1); 43 | assert.deepEqual(bar.get('errors'), ['The value is not unique']); 44 | bar.put(100); 45 | assert.deepEqual(bar.get('errors'), ['The value is too high']); 46 | bar.put(3); 47 | assert.deepEqual(bar.get('errors'), undefined); 48 | }, 49 | 'UniqueValidator direct and mixed in with NumericValidator': function () { 50 | var store = new Memory({ 51 | data: [{id: 1}, {id: 2}] 52 | }); 53 | var model = new Model({ 54 | schema: { 55 | foo: new UniqueValidator({ 56 | uniqueStore: store 57 | }), 58 | bar: new (declare([NumericValidator, UniqueValidator]))({ 59 | uniqueStore: store, 60 | maximum: 10 61 | }) 62 | } 63 | }); 64 | var foo = model.property('foo'); 65 | foo.put(1); 66 | assert.deepEqual(foo.get('errors'), ['The value is not unique']); 67 | foo.put(100); 68 | assert.deepEqual(foo.get('errors'), undefined); 69 | var bar = model.property('bar'); 70 | bar.put(1); 71 | assert.deepEqual(bar.get('errors'), ['The value is not unique']); 72 | bar.put(100); 73 | assert.deepEqual(bar.get('errors'), ['The value is too high']); 74 | bar.put(3); 75 | assert.deepEqual(bar.get('errors'), undefined); 76 | } 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /validators/NumericValidator.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | '../Property' 4 | ], function (declare, Property) { 5 | return declare(Property, { 6 | // summary: 7 | // A validator for enforcing numeric values 8 | checkForErrors: function (value) { 9 | var errors = this.inherited(arguments); 10 | if (isNaN(value)) { 11 | errors.push(this.notANumberError); 12 | } 13 | if (this.minimum >= value) { 14 | errors.push(this.minimumError); 15 | } 16 | if (this.maximum <= value) { 17 | errors.push(this.maximumError); 18 | } 19 | return errors; 20 | }, 21 | // minimum: Number 22 | // The minimum value for the value 23 | minimum: -Infinity, 24 | // maximum: Number 25 | // The maximum value for the value 26 | maximum: Infinity, 27 | // minimumError: String 28 | // The error message for values that are too low 29 | minimumError: 'The value is too low', 30 | // maximumError: String 31 | // The error message for values that are too high 32 | maximumError: 'The value is too high', 33 | // notANumberError: String 34 | // The error message for values that are not a number 35 | notANumberError: 'The value is not a number' 36 | }); 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /validators/StringValidator.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | '../Property' 4 | ], function (declare, Property) { 5 | return declare(Property, { 6 | // summary: 7 | // A validator for enforcing string values 8 | checkForErrors: function (value) { 9 | var errors = this.inherited(arguments); 10 | if (this.minimumLength >= value.length) { 11 | errors.push(this.minimumLengthError); 12 | } 13 | if (this.maximumLength < value.length) { 14 | errors.push(this.maximumLengthError); 15 | } 16 | if (this.pattern && !this.pattern.test(value)) { 17 | errors.push(this.patternMatchError); 18 | } 19 | return errors; 20 | }, 21 | // minimumLength: Number 22 | // The minimum length of the string 23 | minimumLength: 0, 24 | // maximum: Number 25 | // The maximum length of the string 26 | maximumLength: Infinity, 27 | // pattern: Regex 28 | // A regular expression that the string must match 29 | pattern: null, 30 | // minimumError: String 31 | // The error message for values that are too low 32 | minimumLengthError: 'This is too short', 33 | // maximumError: String 34 | // The error message for values that are too high 35 | maximumLengthError: 'This is too long', 36 | // patternMatchError: String 37 | // The error when a pattern does not match 38 | patternMatchError: 'The pattern did not match' 39 | }); 40 | }); 41 | 42 | -------------------------------------------------------------------------------- /validators/UniqueValidator.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'dojo/_base/declare', 3 | 'dojo/when', 4 | '../Property' 5 | ], function (declare, when, Property) { 6 | return declare(Property, { 7 | // summary: 8 | // A validator for enforcing unique values. This will 9 | // check a value to see if exists as an id in a store (by 10 | // calling get() on the store), and if get() returns a value, 11 | // validation will fail. 12 | checkForErrors: function (value) { 13 | var property = this; 14 | return when(this.inherited(arguments), function (errors) { 15 | return when(property.uniqueStore.get(value), function (object) { 16 | if (object) { 17 | errors.push(property.uniqueError); 18 | } 19 | return errors; 20 | }); 21 | }); 22 | }, 23 | // TODO: Once we define relationships with properties, we may want the 24 | // store to be coordinated 25 | // uniqueStore: Store 26 | // The store that will be accessed to determine if a value is unique 27 | uniqueStore: null, 28 | // uniqueError: String 29 | // The error message for when the value is not unique 30 | uniqueError: 'The value is not unique' 31 | }); 32 | }); 33 | 34 | --------------------------------------------------------------------------------