1 /* 2 * Geddy JavaScript Web development framework 3 * Copyright 2112 Matthew Eernisse (mde@fleegix.org) 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 /* 20 Example model file, would be app/models/user.js: 21 22 var User = function () { 23 this.property('login', 'string', {required: true}); 24 this.property('password', 'string', {required: true}); 25 this.property('lastName', 'string'); 26 this.property('firstName', 'string'); 27 28 this.validatesPresent('login'); 29 this.validatesFormat('login', /[a-z]+/, {message: 'Subdivisions!'}); 30 this.validatesLength('login', {min: 3}); 31 this.validatesConfirmed('password', 'confirmPassword'); 32 this.validatesWithFunction('password', 33 function (s) { return s.length > 0; // Could be anything 34 }); 35 }; 36 37 User.prototype.someMethod = function () { 38 // Do some stuff on a User instance 39 }; 40 41 geddy.model.register('User', User); 42 */ 43 44 var model = {} 45 , utils = require('../utils'); 46 47 model.datatypes = require('./datatypes.js'); 48 model.validators = require('./validators.js'); 49 model.formatters = require('./formatters.js'); 50 51 utils.mixin(model, new (function () { 52 53 var _createModelItemConstructor = function (def) { 54 // Base constructor function for all model items 55 var ModelItemConstructor = function (params) { 56 this.type = def.name; 57 58 this.isValid = function () { 59 return !this.errors; 60 }; 61 62 this.toString = function () { 63 var obj = {}; 64 obj.id = this.id; 65 obj.type = this.type; 66 var props = this.properties; 67 var formatter; 68 for (var p in props) { 69 formatter = model.formatters[props[p].datatype]; 70 obj[p] = typeof formatter == 'function' ? formatter(this[p]) : this[p]; 71 } 72 return JSON.stringify(obj); 73 }; 74 75 this.toJson = this.toString; 76 }; 77 78 return ModelItemConstructor; 79 }; 80 81 var _createStaticMethodsMixin = function (name) { 82 var obj = {}; 83 84 obj.create = function () { 85 var args = Array.prototype.slice.call(arguments); 86 args.unshift(name); 87 return model.createItem.apply(model, args); 88 }; 89 90 obj.load = function () { 91 var args = Array.prototype.slice.call(arguments); 92 if (!model.adapter) { 93 throw new Error('adapter is not defined.'); 94 } 95 args.unshift(name); 96 return model.adapter.load.apply(model.adapter, args); 97 }; 98 99 return obj; 100 }; 101 102 103 this.reg = {}; 104 this.useTimestamps = false; 105 this.adapter = {}; 106 107 this.register = function (name, ModelDefinition) { 108 var origProto = ModelDefinition.prototype 109 , defined 110 , ModelCtor; 111 112 // Create the place to store the metadata about the model structure 113 // to use to do validations, etc. when constructing 114 model.reg[name] = new model.ModelDescription(name); 115 // Execute all the definition methods to create that metadata 116 ModelDefinition.prototype = new model.ModelDefinitionBase(name); 117 defined = new ModelDefinition(); 118 119 // Create the constructor function to use when calling static 120 // ModelCtor.create. Gives them the proper instanceof value, 121 // and .valid, etc. instance-methods. 122 ModelCtor = _createModelItemConstructor(defined); 123 124 // Mix in the static methods like .create and .load 125 utils.mixin(ModelCtor, _createStaticMethodsMixin(name)); 126 // Same with statics 127 utils.mixin(ModelCtor, defined); 128 129 // Mix any functions defined directly in the model-item definition 130 // 'constructor' into the original prototype, and set it as the prototype of the 131 // actual constructor 132 utils.mixin(origProto, defined); 133 134 ModelCtor.prototype = origProto; 135 136 model[name] = ModelCtor; 137 }; 138 139 this.createItem = function (name, params) { 140 var item = new model[name](params); 141 item = this.validateAndUpdateFromParams(item, params); 142 143 if (this.useTimestamps && !item.createdAt) { 144 item.createdAt = new Date(); 145 } 146 147 // After-create hook 148 if (typeof item.afterCreate == 'function') { 149 item.afterCreate(); 150 } 151 return item; 152 }; 153 154 this.updateItem = function (item, params) { 155 item = this.validateAndUpdateFromParams(item, params); 156 157 // After-update hook 158 if (typeof item.afterUpdate == 'function') { 159 item.afterUpdate(); 160 } 161 return item; 162 }; 163 164 this.validateAndUpdateFromParams = function (item, params) { 165 var type = model.reg[item.type] 166 , properties = type.properties 167 , validated = null 168 , errs = null 169 , val 170 171 // May be revalidating, clear errors 172 delete item.errors; 173 174 // User-input should never contain these -- but we still want 175 // to validate them 176 if (typeof item.createdAt != 'undefined') { 177 params.createdAt = item.createdAt; 178 } 179 if (typeof item.updatedAt != 'undefined') { 180 params.updatedAt = item.updatedAt; 181 } 182 183 for (var p in properties) { 184 validated = this.validateProperty(properties[p], params); 185 // If there are any failed validations, the errs param 186 // contains an Object literal keyed by field name, and the 187 // error message for the first failed validation for that 188 // property 189 if (validated.err) { 190 errs = errs || {}; 191 errs[p] = validated.err; 192 } 193 // Otherwise add this property the the return item 194 else { 195 item[p] = validated.val; 196 } 197 } 198 199 // Should never be incuded in user input 200 delete params.createdAt; 201 delete params.updatedAt; 202 203 if (errs) { 204 item.errors = errs; 205 } 206 207 return item; 208 }; 209 210 this.validateProperty = function (prop, params) { 211 var name = prop.name 212 , val = params[name]; 213 214 // Validate for the base datatype only if there actually is a value -- 215 // e.g., undefined will fail the validation for Number, even if the 216 // field is optional 217 if (val) { 218 var result = model.datatypes[prop.datatype.toLowerCase()](prop.name, val); 219 if (result.err) { 220 return { 221 err: result.err, 222 val: null 223 }; 224 } 225 // Value may have been modified in the datatype check -- e.g., 226 // 'false' changed to false, '8.0' changed to 8, '2112' changed to 227 // 2112, etc. 228 val = result.val; 229 } 230 231 // Now go through all the base validations for this property 232 var validations = prop.validations; 233 var validator; 234 var err; 235 for (var p in validations) { 236 validator = model.validators[p] 237 if (typeof validator != 'function') { 238 throw new Error(p + ' is not a valid validator'); 239 } 240 err = validator(name, val, params, validations[p]); 241 // If there's an error for a validation, don't bother 242 // trying to continue with more validations -- just return 243 // this first error message 244 if (err) { 245 return { 246 err: err, 247 val: null 248 }; 249 } 250 } 251 252 // If there weren't any errors, return the value for this property 253 // and no error 254 return { 255 err: null, 256 val: val 257 }; 258 }; 259 260 })()); 261 262 model.ModelDefinitionBase = function (name) { 263 var self = this 264 , _getValidator = function (p) { 265 return function () { 266 var args = Array.prototype.slice.call(arguments); 267 args.unshift(p); 268 return self.validates.apply(self, args); 269 }; 270 }; 271 272 this.name = name; 273 this.property = function (name, datatype, o) { 274 model.reg[this.name].properties[name] = 275 new model.PropertyDescription(name, datatype, o); 276 }; 277 278 this.validates = function (condition, name, qualifier, opts) { 279 var rule = utils.mixin({}, opts, true); 280 rule.qualifier = qualifier; 281 model.reg[this.name].properties[name] 282 .validations[condition] = rule; 283 }; 284 285 // For each of the validators, create a validatesFooBar from 286 // validates('fooBar' ... 287 for (var p in model.validators) { 288 this['validates' + utils.string.capitalize(p)] = _getValidator(p); 289 } 290 291 // Add the base model properties -- these should not be handled by user input 292 if (model.useTimestamps) { 293 this.property('createdAt', 'datetime'); 294 this.property('updatedAt', 'datetime'); 295 } 296 297 }; 298 299 model.ModelDescription = function (name) { 300 this.name = name; 301 this.properties = {}; 302 }; 303 304 model.PropertyDescription = function (name, datatype, o) { 305 var opts = o || {}; 306 this.name = name; 307 this.datatype = datatype; 308 this.options = opts; 309 var validations = {}; 310 for (var p in opts) { 311 if (opts.required || opts.length) { 312 validations.present = true; 313 } 314 if (opts.length) { 315 validations.length = opts.length; 316 } 317 if (opts.format) { 318 validations.format = opts.format; 319 } 320 } 321 this.validations = validations; 322 }; 323 324 module.exports = model; 325 326