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