1 // ========================================================================== 2 // Project: The M-Project - Mobile HTML5 Application Framework 3 // Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved. 4 // (c) 2011 panacoda GmbH. All rights reserved. 5 // Creator: Sebastian 6 // Date: 28.10.2010 7 // License: Dual licensed under the MIT or GPL Version 2 licenses. 8 // http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE 9 // http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE 10 // ========================================================================== 11 12 M.STATE_UNDEFINED = 'state_undefined'; 13 M.STATE_NEW = 'state_new'; 14 M.STATE_INSYNCPOS = 'state_insyncpos'; 15 M.STATE_INSYNCNEG = 'state_insyncneg'; 16 M.STATE_LOCALCHANGED = 'state_localchange'; 17 M.STATE_VALID = 'state_valid'; 18 M.STATE_INVALID = 'state_invalid'; 19 M.STATE_DELETED = 'state_deleted'; 20 21 /** 22 * @class 23 * 24 * M.Model is the prototype for every model and for every model record (a model itself is the blueprint for a model record). 25 * Models hold the business data of an application respectively the application's state. It's usually the part of an application that is persisted to storage. 26 * M.Model acts as the gatekeeper to storage. It uses data provider for persistence and validators to validate its records. 27 * 28 * @extends M.Object 29 */ 30 M.Model = M.Object.extend( 31 /** @scope M.Model.prototype */ { 32 /** 33 * The type of this object. 34 * 35 * @type String 36 */ 37 type: 'M.Model', 38 39 /** 40 * The name of the model. 41 * 42 * @type String 43 */ 44 name: '', 45 46 /** 47 * Unique identifier for the model record. 48 * 49 * It's as unique as it needs to be: four digits, each digits can be one of 32 chars 50 * 51 * @type String 52 */ 53 m_id: null, 54 55 /** 56 * The model's record defines the properties that are semantically bound to this model: 57 * e.g. a person's record is (in simplest case): firstname, lastname, age. 58 * 59 * @type Object record 60 */ 61 record: null, 62 63 /** 64 * Object containing all meta information for the object's properties 65 * @type Object 66 */ 67 __meta: {}, 68 69 /** 70 * Manages records of this model 71 * @type Object 72 */ 73 recordManager: null, 74 75 /** 76 * List containing all models in application 77 * @type Object|Array 78 */ 79 modelList: {}, 80 81 /** 82 * A constant defining the model's state. Important e.g. for syncing storage 83 * @type String 84 */ 85 state: M.STATE_UNDEFINED, 86 87 /** 88 * 89 * @type String 90 */ 91 state_remote: M.STATE_UNDEFINED, 92 93 /** 94 * determines whether model shall be validated before saving to storage or not. 95 * @type Boolean 96 */ 97 usesValidation: NO, 98 99 /** 100 * The model's data provider. A data provider persists the model to a certain storage. 101 * 102 * @type Object 103 */ 104 dataProvider: null, 105 106 getUniqueId: function() { 107 return M.UniqueId.uuid(4); 108 }, 109 110 /** 111 * Creates a new record of the model, means an instance of the model based on the blueprint. 112 * You pass the object's specific attributes to it as an object. 113 * 114 * @param {Object} obj The properties object, e.g. {firstname: 'peter', lastname ='fox'} 115 * @returns {Object} The model record with the passed properties set. State depends on newly creation or fetch from storage: if 116 * from storage then state is M.STATE_NEW or 'state_new', if fetched from database then it is M.STATE_VALID or 'state_valid' 117 */ 118 createRecord: function(obj) { 119 120 var rec = this.extend({ 121 m_id: obj.m_id ? obj.m_id : this.getUniqueId(), 122 record: obj /* properties that are added to record here, but are not part of __meta, are deleted later (see below) */ 123 }); 124 delete obj.m_id; 125 rec.state = obj.state ? obj.state : M.STATE_NEW; 126 delete obj.state; 127 128 /* set timestamps if new */ 129 if(rec.state === M.STATE_NEW) { 130 rec.record[M.META_CREATED_AT] = +new Date(); 131 rec.record[M.META_UPDATED_AT] = +new Date(); 132 } 133 134 for(var i in rec.record) { 135 136 if(i === M.META_CREATED_AT || i === M.META_UPDATED_AT) { 137 continue; 138 } 139 140 /* if record contains properties that are not part of __meta (means that are not defined in the model blueprint) delete them */ 141 if(!rec.__meta.hasOwnProperty(i)) { 142 M.Logger.log('Deleting "' + i + '" property. It\'s not part of ' + this.name + ' definition.', M.WARN); 143 delete rec.record[i]; 144 continue; 145 } 146 147 /* if reference to a record entity is in param obj, assign it like in set. */ 148 if(rec.__meta[i].dataType === 'Reference' && rec.record[i] && rec.record[i].type && rec.record[i].type === 'M.Model') { 149 // call set of model 150 rec.set(i, rec.record[i]); 151 } 152 153 if(rec.__meta[i]) { 154 rec.__meta[i].isUpdated = NO; 155 } 156 } 157 158 this.recordManager.add(rec); 159 return rec; 160 }, 161 162 /** 163 * Create defines a new model blueprint. It is passed an object with the model's attributes and the model's business logic 164 * and after it the type of data provider to use. 165 * 166 * @param {Object} obj An object defining the model's 167 * @param {Object} dp The data provider to use, e. g. M.LocalStorageProvider 168 * @returns {Object} The model blueprint: acts as blueprint to all records created with @link M.Model#createRecord 169 */ 170 create: function(obj, dp) { 171 var model = M.Model.extend({ 172 __meta: {}, 173 name: obj.__name__, 174 dataProvider: dp, 175 recordManager: {}, 176 usesValidation: obj.usesValidation ? obj.usesValidation : this.usesValidation 177 }); 178 delete obj.__name__; 179 delete obj.usesValidation; 180 181 for(var prop in obj) { 182 if(typeof(obj[prop]) === 'function') { 183 model[prop] = obj[prop]; 184 } else if(obj[prop].type === 'M.ModelAttribute') { 185 model.__meta[prop] = obj[prop]; 186 } 187 } 188 189 /* add ID, _createdAt and _modifiedAt properties in meta for timestamps */ 190 model.__meta[M.META_CREATED_AT] = this.attr('String', { // could be 'Date', too 191 isRequired:YES 192 }); 193 model.__meta[M.META_UPDATED_AT] = this.attr('String', { // could be 'Date', too 194 isRequired:YES 195 }); 196 197 model.recordManager = M.RecordManager.extend({records:[]}); 198 199 /* save model in modelList with model name as key */ 200 this.modelList[model.name] = model; 201 202 return model; 203 }, 204 205 /** 206 * Defines a to-one-relationship. 207 * @param refName 208 * @param refEntity 209 */ 210 hasOne: function(refEntity, obj) { 211 var relAttr = this.attr('Reference', { 212 refType: 'toOne', 213 reference: refEntity, 214 validators: obj.validators, 215 isRequired: obj.isRequired 216 }); 217 return relAttr; 218 }, 219 220 /** 221 * Defines a to-many-relationship 222 * @param colName 223 * @param refEntity 224 * @param invRel 225 */ 226 hasMany: function(colName, refEntity, invRel) { 227 var relAttr = this.attr('Reference', { 228 refType: 'toMany', 229 reference: refEntity 230 }); 231 return relAttr; 232 }, 233 234 /** 235 * Returns a M.ModelAttribute object to map an attribute in our record. 236 * 237 * @param {String} type type of the attribute 238 * @param {Object} opts options for the attribute, like required flag and validators array 239 * @returns {Object} An M.ModelAttribute object configured with the type and options passed to the function. 240 */ 241 attr: function(type, opts) { 242 return M.ModelAttribute.attr(type, opts); 243 }, 244 245 /* 246 * get and set methods for encapsulated attribute access 247 */ 248 249 /** 250 * Get attribute propName from model, if async provider is used, get on references does not return the property value but a boolean indicating 251 * the load status. get must then be called again in onSuccess callback to retrieve the value 252 * @param {String} propName the name of the property whose value shall be returned 253 * @param {Object} obj optional parameter containing the load force flag and callbacks, e.g.: 254 * { 255 * force: YES, 256 * onSuccess: function() { console.log('yeah'); } 257 * } 258 * @returns {Boolean|Object|String} value of property or boolean indicating the load status 259 */ 260 get: function(propName, obj) { 261 var metaProp = this.__meta[propName]; 262 var recProp = this.record[propName]; 263 /* return ref entity if property is a reference */ 264 if(metaProp && metaProp.dataType === 'Reference') { 265 if(metaProp.refEntity) {// if entity is already loaded and assigned here in model record 266 return metaProp.refEntity; 267 } else if(recProp) { // if model record has a reference set, but it is not loaded yet 268 if(obj && obj.force) { // if force flag was set 269 /* custom call to deepFind with record passed only being the one property that needs to be filled, type of dp checked in deepFind */ 270 var callback = this.dataProvider.isAsync ? obj.onSuccess : null 271 this.deepFind([{ 272 prop: propName, 273 name: metaProp.reference, 274 model: this.modelList[metaProp.reference], 275 m_id: recProp 276 }], callback); 277 if(!this.dataProvider.isAsync) { // if data provider acts synchronous, we can now return the fetched entity 278 return metaProp.refEntity; 279 } 280 return YES; 281 } else { // if force flag was not set, and object is not in memory and record manager load is not done and we return NO 282 var r = this.recordManager.getRecordById(recProp); 283 if(r) { /* if object is already loaded and in record manager don't access storage */ 284 return r; 285 } else { 286 return NO; // return 287 } 288 } 289 } else { // if reference has not been set yet 290 return null; 291 } 292 } 293 /* if propName is not a reference, but a "simple" property, just return it */ 294 return recProp; 295 }, 296 297 /** 298 * Set attribute propName of model with value val, sets' property to isUpdated (=> will be included in UPDATE call) 299 * and sets a new timestamp to _updatedAt. Will not do anything, if newVal is the same as the current prop value. 300 * @param {String} propName the name of the property whose value shall be set 301 * @param {String|Object} val the new value 302 */ 303 set: function(propName, val) { 304 if(this.__meta[propName].dataType === 'Reference' && val.type && val.type === 'M.Model') { // reference set 305 /* first check if new value is passed */ 306 if(this.record[propName] !== val.m_id) { 307 /* set m_id of reference in record */ 308 this.record[propName] = val.m_id; 309 this.__meta[propName].refEntity = val; 310 } 311 return; 312 } 313 314 if(this.record[propName] !== val) { 315 this.record[propName] = val; 316 this.__meta[propName].isUpdated = YES; 317 /* mark record as updated with new timestamp*/ 318 this.record[M.META_UPDATED_AT] = +new Date(); 319 } 320 }, 321 322 /** 323 * Returns the records array of the model's record manager. 324 * @returns {Object|Array} The records array of record manager. 325 */ 326 records: function() { 327 if(this.recordManager && this.recordManager.records) { 328 return this.recordManager.records; 329 } 330 }, 331 332 /** 333 * Validates the model, means calling validate for each property. 334 * @returns {Boolean} Indicating whether this record is valid (YES|true) or not (NO|false). 335 */ 336 validate: function() { 337 var isValid = YES; 338 var validationErrorOccured = NO; 339 /* clear validation error buffer before validation */ 340 M.Validator.clearErrorBuffer(); 341 342 /* 343 * validationBasis depends on the state of the model: if the model is in state NEW, all properties (__meta includes all) 344 * shall be considered for validation. if model is in another state, the model's record is used. example: the model is loaded from 345 * a database with only two properties included (select name, age FROM...). record now only contains these two properties but __meta 346 * still has all properties listed. models are valid if loaded from database so when saved again only the loaded properties need to get 347 * validated because all others have not been touched. that's why then record is used. 348 * */ 349 var validationBasis = this.state === M.STATE_NEW ? this.__meta : this.record; 350 351 for (var i in validationBasis) { 352 if(i === 'ID') { // skip property ID 353 continue; 354 } 355 var prop = this.__meta[i]; 356 var obj = { 357 value: this.record[i], 358 modelId: this.name + '_' + this.m_id, 359 property: i 360 }; 361 if (!prop.validate(obj)) { 362 isValid = NO; 363 } 364 } 365 /* set state of model */ 366 /*if(!isValid) { 367 this.state = M.STATE_INVALID; 368 } else { 369 this.state = M.STATE_VALID; 370 }*/ 371 return isValid; 372 }, 373 374 /* CRUD Methods below */ 375 /** 376 * Calls the corresponding find() of the data provider to fetch data based on the passed query or key. 377 * 378 * @param {Object} obj The param object with query or key and callbacks. 379 * @returns {Boolean|Object} Depends on data provider used. When WebSQL used, a boolean is returned, the find result is returned asynchronously, 380 * because the call itself is asynchronous. If LocalStorage is used, the result of the query is returned. 381 */ 382 find: function(obj){ 383 if(!this.dataProvider) { 384 M.Logger.log('No data provider given.', M.ERR); 385 } 386 obj = obj ? obj : {}; 387 /* check if the record list shall be cleared (default) before new found model records are appended to the record list */ 388 obj.deleteRecordList = obj.deleteRecordList ? obj.deleteRecordList : YES; 389 if(obj.deleteRecordList) { 390 this.recordManager.removeAll(); 391 } 392 if(!this.dataProvider) { 393 M.Logger.log('No data provider given.', M.ERR); 394 } 395 396 /* extends the given obj with self as model property in obj */ 397 return this.dataProvider.find( $.extend(obj, {model: this}) ); 398 }, 399 400 /** 401 * Create or update a record in storage if it is valid (first check this). 402 * If param obj includes cascade:YES then save is cascadaded through all references recursively. 403 * 404 * @param {Object} obj The param object with query, cascade flag and callbacks. 405 * @returns {Boolean} The result of the data provider function call. Is a boolean. With LocalStorage used, it indicates if the save operation was successful. 406 * When WebSQL is used, the result of the save operation returns asynchronously. The result then is just the standard result returned by the web sql provider's save method 407 * which does not necessarily indicate whether the operation was successful, because the operation is asynchronous, means the operation's result is not predictable. 408 */ 409 save: function(obj) { 410 if(!this.dataProvider) { 411 M.Logger.log('No data provider given.', M.ERR); 412 } 413 obj = obj ? obj: {}; 414 if(!this.m_id) { 415 return NO; 416 } 417 var isValid = YES; 418 419 if(this.usesValidation) { 420 isValid = this.validate(); 421 } 422 423 if(obj.cascade) { 424 for(var prop in this.__meta) { 425 if(this.__meta[prop] && this.__meta[prop].dataType === 'Reference' && this.__meta[prop].refEntity) { 426 this.__meta[prop].refEntity.save({cascade:YES}); // cascades recursively through all referenced model records 427 } 428 } 429 } 430 431 if(isValid) { 432 return this.dataProvider.save($.extend(obj, {model: this})); 433 } 434 }, 435 436 /** 437 * Delete a record in storage. 438 * @returns {Boolean} Indicating whether deletion was successful or not (only with synchronous data providers, e.g. LocalStorage). When asynchronous data providers 439 * are used, e.g. WebSQL provider the real result comes asynchronous and here just the result of the del() function call of the @link M.WebSqlProvider is used. 440 */ 441 del: function(obj) { 442 if(!this.dataProvider) { 443 M.Logger.log('No data provider given.', M.ERR); 444 } 445 obj = obj ? obj : {}; 446 if(!this.m_id) { 447 return NO; 448 } 449 450 var isDel = this.dataProvider.del($.extend(obj, {model: this})); 451 if(isDel) { 452 this.state = M.STATE_DELETED; 453 return YES; 454 } 455 }, 456 457 /** 458 * completes the model record by loading all referenced entities. 459 * 460 * @param {Function | Object} obj The param object with query, cascade flag and callbacks. 461 */ 462 complete: function(callback) { 463 //console.log('complete...'); 464 var records = []; 465 for(var i in this.record) { 466 if(this.__meta[i].dataType === 'Reference') { 467 //records.push(this.__meta[i].refEntity); 468 records.push({ 469 prop:i, 470 name: this.__meta[i].reference, 471 model: this.modelList[this.__meta[i].reference], 472 m_id: this.record[i] 473 }); 474 } 475 } 476 this.deepFind(records, callback); 477 }, 478 479 deepFind: function(records, callback) { 480 //console.log('deepFind...'); 481 //console.log('### records.length: ' + records.length); 482 if(records.length < 1) { // recursion end constraint 483 if(callback) { 484 callback(); 485 } 486 return; 487 } 488 var curRec = records.pop(); // delete last element, decreases length of records by 1 => important for recursion constraint above 489 var cb = this.bindToCaller(this, this.deepFind,[records, callback]); // cb is callback for find in data provider 490 var that = this; 491 492 493 switch(this.dataProvider.type) { 494 case 'M.DataProviderLocalStorage': 495 var ref = this.modelList[curRec.name].find({ 496 key: curRec.m_id 497 }); 498 this.__meta[curRec.prop].refEntity = ref; 499 500 this.deepFind(records, callback); // recursion 501 break; 502 503 default: 504 break; 505 } 506 }, 507 508 setReference: function(result, that, prop, callback) { 509 that.__meta[prop].refEntity = result[0]; // set reference in source model defined by that 510 callback(); 511 } 512 513 });