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