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 });