1 // ==========================================================================
  2 // Project:   The M-Project - Mobile HTML5 Application Framework
  3 // Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved.
  4 // Creator:   Sebastian
  5 // Date:      28.10.2010
  6 // License:   Dual licensed under the MIT or GPL Version 2 licenses.
  7 //            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
  8 //            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
  9 // ==========================================================================
 10 
 11 M.STATE_UNDEFINED = 'state_undefined';
 12 M.STATE_NEW = 'state_new';
 13 M.STATE_INSYNCPOS = 'state_insyncpos';
 14 M.STATE_INSYNCNEG = 'state_insyncneg';
 15 M.STATE_LOCALCHANGED = 'state_localchange';
 16 M.STATE_VALID = 'state_valid';
 17 M.STATE_INVALID = 'state_invalid';
 18 M.STATE_DELETED = 'state_deleted';
 19 
 20 m_require('core/foundation/model_registry.js');
 21 
 22 /**
 23  * @class
 24  * 
 25  * M.Model is the prototype for every model and for every model record (a model itself is the blueprint for a model record).
 26  * 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.
 27  * M.Model acts as the gatekeeper to storage. It uses data provider for persistence and validators to validate its records.
 28  * 
 29  * @extends M.Object
 30  */
 31 M.Model = M.Object.extend(
 32 /** @scope M.Model.prototype */ { 
 33     /**
 34      * The type of this object.
 35      *
 36      * @type String
 37      */
 38     type: 'M.Model',
 39 
 40     /**
 41      * The name of the model.
 42      *
 43      * @type String
 44      */
 45     name: '',
 46 
 47     /**
 48      * Unique identifier for the model record.
 49      *
 50      * Note: Unique doesn't mean that this id is a global unique ID, it is just unique
 51      * for records of this type of model.
 52      *
 53      * @type Number
 54      */
 55     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      *
 79      * @param obj
 80      */
 81     records: null,
 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      * determines whether model shall be validated before saving to storage or not.
 92      * @type Boolean
 93      */
 94     usesValidation: YES,
 95 
 96     /**
 97      * The model's data provider.
 98      *
 99      * Needs a refactoring, also in connection with storageEngine.
100      *
101      * @type Object
102      */
103     dataProvider: null,
104 
105     /**
106      * Creates a new record of the model, means an instance of the model based on the blueprint.
107      * You pass the object's specific attributes to it as an object.
108      *
109      * @param {Object} obj The specific attributes as an object, e.g. {firstname: 'peter', lastname ='fox'}
110      * @returns {Object} The model record with the passed properties set. State depends on newly creation or fetch from storage: if
111      * from storage then state is M.STATE_NEW or 'state_new', if fetched from database then it is M.STATE_VALID or 'state_valid'
112      */
113     createRecord: function(obj) {
114         var modelRecord = this.extend({
115             id: obj.id ? obj.id : M.Application.modelRegistry.getNextId(this.name),
116             record: obj
117         });
118         modelRecord.state = obj.state ? obj.state : M.STATE_NEW;
119         delete obj.state;
120         delete modelRecord.record.id;
121         return modelRecord;
122     },
123 
124     /** 
125      * Create defines a new model blueprint. It is passed an object with the model's attributes and the model's business logic
126      * and after it the type of data provider to use.
127      *
128      * @param {Object} obj An object defining the model's  
129      * @param {Object} dp The data provider to use, e. g. M.LocalStorageProvider
130      * @returns {Object} The model blueprint: acts as blueprint to all records created with @link M.Model#createRecord
131      */
132     create: function(obj, dp) {
133         var model = M.Model.extend({
134             __meta: {},
135             name: obj.__name__,
136             dataProvider: dp,
137             usesValidation: obj.usesValidation === null || obj.usesValidation === undefined ? this.usesValidation : obj.usesValidation
138         });
139         delete obj.__name__;
140         delete obj.usesValidation;
141 
142         for(var prop in obj) {
143             if(typeof(obj[prop]) === 'function') {
144                 model[prop] = obj[prop];
145             } else if(obj[prop].type === 'M.ModelAttribute') {
146                 model.__meta[prop] = obj[prop];    
147             }
148         }
149 
150         model.recordManager = M.RecordManager.extend({});
151         model.records = model.recordManager.records;
152 
153         M.Application.modelRegistry.register(model.name);
154 
155         /* Re-set the just registered model's id, if there is a value stored */
156         /* Model Registry stores the current id of a model type into localStorage */
157         var id = localStorage.getItem(model.name);
158         if(id) {
159             M.Application.modelRegistry.setId(model.name, parseInt(id));
160         }
161         return model;
162     }, 
163 
164     /**
165      * Returns a M.ModelAttribute object to map an attribute in our record.
166      *
167      * @param {String} type type of the attribute
168      * @param {Object} opts options for the attribute, like required flag and validators array
169      * @returns {Object} An M.ModelAttribute object configured with the type and options passed to the function.
170      */
171     attr: function(type, opts) {
172         return M.ModelAttribute.attr(type, opts); 
173     },
174 
175     /*
176      * get and set methods for encapsulated attribute access
177      */
178 
179     /**
180      * Get attribute propName from model
181      * @param {String} propName the name of the property whose value shall be returned
182      * @returns {Object|String} value of property
183      */
184     get: function(propName) {
185         return this.record[propName];
186     },
187 
188     /**
189      * Set attribute propName of model with value val
190      * @param {String} propName the name of the property whose value shall be set
191      * @param {String|Object} val the new value
192      */
193     set: function(propName, val) {
194         this.record[propName] = val;
195     },
196 
197     /**
198      * Validates the model, means calling validate for each property.
199      * @returns {Boolean} Indicating whether this record is valid (YES|true) or not (NO|false).
200      */
201     validate: function() {
202         var isValid = YES;
203         var validationErrorOccured = NO;
204         /* clear validation error buffer before validation */
205         M.Validator.clearErrorBuffer();
206 
207         /*
208         * validationBasis depends on the state of the model: if the model is in state NEW, all properties (__meta includes all)
209         * shall be considered for validation. if model is in another state, the model's record is used. example: the model is loaded from
210         * a database with only two properties included (select name, age FROM...). record now only contains these two properties but __meta
211         * still has all properties listed. models are valid if loaded from database so when saved again only the loaded properties need to get
212         * validated because all others have not been touched. that's why then record is used.
213         * */
214         var validationBasis = this.state === M.STATE_NEW ? this.__meta : this.record;
215 
216         for (var i in validationBasis) {
217             var prop = this.__meta[i];
218             var obj = {
219                 value: this.record[i],
220                 modelId: this.name + '_' + this.id,
221                 property: i
222             };
223             if (!prop.validate(obj)) {
224                 isValid = NO;
225             }
226         }
227         /* set state of model */
228         /*if(!isValid) {
229             this.state = M.STATE_INVALID;
230         } else {
231             this.state = M.STATE_VALID;   
232         }*/
233         return isValid;
234     },
235 
236     /* CRUD Methods below */
237     /**
238      * Calls the corresponding find() of the data provider to fetch data based on the passed query or key.
239      *
240      * @param {Object} obj The param object with query or key and callbacks.
241      * @returns {Boolean|Object} Depends on data provider used. When WebSQL used, a boolean is returned, the find result is returned asynchronously,
242      * because the call itself is asynchronous. If LocalStorage is used, the result of the query is returned.
243      */
244     find: function(obj){
245         /* extends the given obj with self as model property in obj */
246         return this.dataProvider.find( $.extend(obj, {model: this}) );
247     },
248 
249     /**
250      * Create or update a record in storage if it is valid (first check this).
251      *
252      * @param {Object} obj The param object with query and callbacks.
253      * @returns {Boolean} The result of the data provider function call. Is a boolean. With LocalStorage used, it indicates if the save operation was successful.
254      * 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
255      * which does not necessarily indicate whether the operation was successful, because the operation is asynchronous, means the operation's end is not predictable. 
256      */
257     save: function(obj) {
258         obj = obj ? obj: {};
259         if(!this.id) {
260             return NO;
261         }
262         var isValid = YES;
263 
264         if(this.usesValidation) {
265             isValid = this.validate();
266         }
267 
268         if(isValid) {
269             return this.dataProvider.save($.extend(obj, {model: this}));
270         }
271     },
272 
273     /**
274      * Delete a record in storage.
275      * @returns {Boolean} Indicating whether deletion was successful or not (only with synchronous data providers, e.g. LocalStorage). When asynchronous data providers
276      * 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.
277      */
278     del: function() {
279         if(!this.id) {
280             return NO;
281         }
282 
283        var isDel = this.dataProvider.del({model: this});
284         if(isDel) {
285             this.state = M.STATE_DELETED;
286             return YES
287         }
288         
289     }
290 
291 });