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: 18.11.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_require('core/detox/data_provider.js'); 12 13 /** 14 * @class 15 * 16 * Encapsulates access to WebSQL (in-browser sqlite storage). All CRUD operations are asynchronous. That means that onSuccess 17 * and onError callbacks have to be passed to the function calls to have the result returned when operation finished. 18 * 19 * @extends M.DataProvider 20 */ 21 M.WebSqlProvider = M.DataProvider.extend( 22 /** @scope M.WebSqlProvider.prototype */ { 23 24 /** 25 * The type of this object. 26 * @type String 27 */ 28 type: 'M.WebSqlProvider', 29 30 /** 31 * Configuration object 32 * @type Object 33 */ 34 config: {}, 35 36 /** 37 * Is set to YES when initialization ran successfully, means when {@link M.WebSqlProvider#init} was called, db and table created. 38 * @type Boolean 39 */ 40 isInitialized: NO, 41 42 /** 43 * Object containing all rules for mapping JavaScript data types to SQLite data types. 44 * @type Object 45 */ 46 typeMapping: { 47 'String': 'varchar(255)', 48 'Text': 'text', 49 'Float': 'float', 50 'Integer': 'integer', 51 'Number': 'interger', 52 'Date': 'date', 53 'Boolean': 'boolean' 54 }, 55 56 /** 57 * Is set when database "opened". Acts as handler for all db operations => transactions, queries, etc. 58 * @type Object 59 */ 60 dbHandler: null, 61 62 /** 63 * Saves the internal callback function. Is needed when provider/db is not initialized and init() must be executed first to have the return point again after 64 * initialization. 65 * @type Function 66 */ 67 internalCallback: null, 68 69 /** 70 * Used internally. External callback for success case. 71 * @private 72 */ 73 onSuccess: null, 74 75 /** 76 * Used internally. External callback for error case. 77 * @private 78 */ 79 onError: null, 80 81 /** 82 * Opens a database and creates the appropriate table for the model record. 83 * 84 * @param {Object} obj The param obj, includes model. Not used here, just passed through. 85 * @param {Function} callback The function that called init as callback bind to this. 86 * @private 87 */ 88 init: function(obj, callback) { 89 this.openDb(); 90 this.createTable(obj, callback); 91 }, 92 93 /* 94 * CRUD methods 95 */ 96 97 /** 98 * Saves a model in the database. Constructs the sql query from the model record. Prepares an INSERT or UPDATE depending on the state 99 * of the model. If M.STATE_NEW then prepares an INSERT, if M.STATE_VALID then prepares an UPDATE. The operation itself 100 * is done by {@link M.WebSqlProvider#performOp} that is called 101 * 102 * @param {Object} obj The param obj, includes: 103 * * onSuccess callback 104 * * onError callback 105 * * the model 106 */ 107 save: function(obj) { 108 console.log('save() called.'); 109 110 this.onSuccess = obj.onSuccess; 111 this.onError = obj.onError; 112 113 /** 114 * if not already done, initialize db/table first 115 */ 116 if(!this.isInitialized) { 117 this.internalCallback = this.save; 118 this.init(obj.model); 119 return; 120 } 121 122 123 if(obj.model.state === M.STATE_NEW) { // perform an INSERT 124 125 var sql = 'INSERT INTO ' + obj.model.name + ' ('; 126 for(var prop in obj.model.record) { 127 sql += prop + ', '; 128 } 129 130 sql = sql.substring(0, sql.lastIndexOf(',')) + ') '; 131 sql += 'VALUES ('; 132 133 for(var prop in obj.model.record) { 134 /* if property is string or text write value in quotes */ 135 var pre_suffix = obj.model.__meta[prop].dataType === 'String' || obj.model.__meta[prop].dataType === 'Text' ? '"' : ''; 136 sql += pre_suffix + obj.model.record[prop] + pre_suffix + ', '; 137 } 138 sql = sql.substring(0, sql.lastIndexOf(',')) + '); '; 139 140 console.log(sql); 141 142 this.performOp(sql, obj, 'INSERT'); 143 144 } else { // perform an UPDATE with id of model 145 146 var sql = 'UPDATE ' + obj.model.name + ' SET '; 147 148 for(var prop in obj.model.record) { 149 var pre_suffix = obj.model.__meta[prop].dataType === 'String' || obj.model.__meta[prop].dataType === 'Text' ? '"' : ''; 150 sql += prop + ' = ' + pre_suffix + obj.model.record[prop] + pre_suffix + ', '; 151 } 152 sql = sql.substring(0, sql.lastIndexOf(',')); 153 sql += ' WHERE ' + 'ID = ' + obj.model.record.ID; 154 155 console.log(sql); 156 157 this.performOp(sql, obj, 'UPDATE'); 158 } 159 }, 160 161 /** 162 * Performs operation on WebSQL storage: INSERT, UPDATE or DELETE. Is used by {@link M.WebSqlProvider#save} and {@link M.WebSqlProvider#del}. 163 * Calls is made asynchronously, means that result is just available in callback. 164 * @param {String} sql The query. 165 * @param {Object} obj The param object. Contains e.g. callbacks (onError & onSuccess) 166 * @param {String} opType The String identifying the operation: 'INSERT', 'UPDATE' oder 'DELETE' 167 * @private 168 */ 169 performOp: function(sql, obj, opType) { 170 var that = this; 171 this.dbHandler.transaction(function(t) { 172 t.executeSql(sql, null, function() { 173 if(opType === 'INSERT') { /* after INSERT operation set the assigned DB ID to the model records id */ 174 that.queryDbForId(obj.model); 175 } 176 }, function() { // error callback for SQLStatementTransaction 177 M.Logger.log('Incorrect statement: ' + sql, M.ERROR); 178 }); 179 }, 180 function() { // errorCallback 181 /* bind error callback */ 182 if (obj.onError && obj.onError.target && obj.onError.action) { 183 obj.onError = this.bindToCaller(obj.onError.target, obj.onError.target[obj.onError.action]); 184 obj.onError(); 185 } 186 }, 187 188 function() { // voidCallback (success) 189 190 /* add or delete to the model blueprints record list */ 191 switch(opType) { 192 case 'INSERT': 193 obj.model.recordManager.add(obj.model); 194 break; 195 case 'DELETE': 196 obj.model.recordManager.remove(obj.model.id); 197 break; 198 default: 199 // do nothing 200 break; 201 } 202 203 /* bind success callback */ 204 if (obj.onSuccess && obj.onSuccess.target && obj.onSuccess.action) { 205 obj.onSuccess = that.bindToCaller(obj.onSuccess.target, obj.onSuccess.target[obj.onSuccess.action]); 206 obj.onSuccess(); 207 } else if (typeof(obj.onError) !== 'function') { 208 M.Logger.log('Target and action in onSuccess not defined.', M.INFO); 209 } 210 }); 211 }, 212 213 /** 214 * Prepares delete query for a model record. Operation itself is performed by {@link M.WebSqlProvider#performOp}. 215 * Tuple is identified by ID (not the internal model id, but the ID provided by the DB in record). 216 * 217 * @param {Object} obj The param obj, includes: 218 * * onSuccess callback 219 * * onError callback 220 * * the model 221 */ 222 del: function(obj) { 223 console.log('del() called.'); 224 if(!this.isInitialized) { 225 this.internalCallback = this.del; 226 this.init(obj, this.bindToCaller(this, this.del)); 227 return; 228 } 229 230 var sql = 'DELETE FROM ' + obj.model.name + ' WHERE ID=' + obj.model.record.ID + ';'; 231 232 console.log(sql); 233 234 this.performOp(sql, obj, 'DELETE'); 235 }, 236 237 238 /** 239 * Finds model records in the database. If a constraint is given, result is filtered. 240 * @param {Object} obj The param object. Includes: 241 * * model: the model blueprint 242 * * onSuccess: 243 * * onError: 244 * * columns: Array of strings naming the properties to be selected: ['name', 'age'] => SELECT name, age FROM... 245 * * constraint: Object containing itself two properties: 246 * * statement: a string with the statement, e.g. 'WHERE ID = ?' 247 * * parameters: array of strings with the parameters, length array must be the same as the number of ? in statement 248 * => ? are substituted with the parameters 249 * * order: String with the ORDER expression: e.g. 'ORDER BY price ASC' 250 * * limit: Number defining the number of max. result items 251 */ 252 find: function(obj) { 253 console.log('find() called.'); 254 255 this.onSuccess = obj.onSuccess; 256 this.onError = obj.onError; 257 258 if(!this.isInitialized) { 259 this.internalCallback = this.find; 260 this.init(obj, this.bindToCaller(this, this.find)); 261 return; 262 } 263 264 var sql = 'SELECT '; 265 266 if(obj.columns) { 267 /* ID column always needs to be in de result relation */ 268 if(!(_.include(obj.columns, 'ID'))) { 269 obj.columns.push('ID'); 270 } 271 272 if(obj.columns.length > 1) { 273 sql += obj.columns.join(', '); 274 } else if(obj.columns.length == 1) { 275 sql += obj.columns[0] + ' '; 276 } 277 } else { 278 sql += '* '; 279 } 280 281 sql += ' FROM ' + obj.model.name; 282 283 var stmtParameters = []; 284 285 /* now process constraint */ 286 if(obj.constraint) { 287 288 var n = obj.constraint.statement.split("?").length - 1; 289 290 /* if parameters are passed we assign them to stmtParameters, the array that is passed for prepared statement substitution*/ 291 if(obj.constraint.parameters) { 292 293 if(n === obj.constraint.parameters.length) { /* length of parameters list must match number of ? in statement */ 294 sql += obj.constraint.statement; 295 stmtParameters = obj.constraint.parameters; 296 } else { 297 M.Logger.log('Not enough parameters provided for statement.', M.ERROR); 298 return NO; 299 } 300 /* if no ? are in statement, we handle it as a non-prepared statement 301 * => developer needs to take care of it by himself regarding 302 * sql injection => all statements that are constructed with dynamic 303 * input should be done as prepared statements 304 */ 305 } else if(n === 0) { 306 sql += obj.constraint.statement; 307 } 308 } 309 310 /* now attach order */ 311 if(obj.order) { 312 sql += ' ORDER BY ' + obj.order 313 } 314 315 /* now attach limit */ 316 if(obj.limit) { 317 sql += ' LIMIT ' + obj.limit 318 } 319 320 console.log(sql); 321 322 var result = []; 323 var that = this; 324 this.dbHandler.readTransaction(function(t) { 325 t.executeSql(sql, stmtParameters, function (tx, res) { 326 var len = res.rows.length, i; 327 for (i = 0; i < len; i++) { 328 /* create model record from result with state valid */ 329 /* $.extend merges param1 object with param2 object*/ 330 result.push(obj.model.createRecord($.extend(res.rows.item(i), {state: M.STATE_VALID}), this)); 331 } 332 }, function(){M.Logger.log('Incorrect statement: ' + sql, M.ERROR)}) // callbacks: SQLStatementErrorCallback 333 }, function(){ // errorCallback 334 /* bind error callback */ 335 if(obj.onError && obj.onError.target && obj.onError.action) { 336 obj.onError = this.bindToCaller(obj.onError.target, obj.onError.target[obj.onError.action]); 337 obj.onError(); 338 } else if (typeof(obj.onError) !== 'function') { 339 M.Logger.log('Target and action in onError not defined.', M.ERROR); 340 } 341 }, function() { // voidCallback (success) 342 /* add to model blueprint's recordmanager record list */ 343 /* first reset record list */ 344 obj.model.recordManager.removeAll(); 345 obj.model.recordManager.addMany(result); 346 347 /* bind success callback */ 348 if(obj.onSuccess && obj.onSuccess.target && obj.onSuccess.action) { 349 /* [result] is a workaround for bindToCaller: bindToCaller uses call() instead of apply() if an array is given. 350 * result is an array, but we what call is doing with it is wrong in this case. call maps each array element to one method 351 * parameter of the function called. Our callback only has one parameter so it would just pass the first value of our result set to the 352 * callback. therefor we now put result into an array (so we have an array inside an array) and result as a whole is now passed as the first 353 * value to the callback. 354 * */ 355 obj.onSuccess = that.bindToCaller(obj.onSuccess.target, obj.onSuccess.target[obj.onSuccess.action], [result]); 356 obj.onSuccess(); 357 } 358 }); 359 }, 360 361 362 /** 363 * @private 364 */ 365 openDb: function() { 366 console.log('openDb() called.'); 367 /* openDatabase(db_name, version, description, estimated_size, callback) */ 368 this.dbHandler = openDatabase(this.config.dbName, '2.0', 'Database for M app', this.config.size); 369 }, 370 371 372 /** 373 * Creates the table corresponding to the model record. 374 * @private 375 */ 376 createTable: function(obj, callback) { 377 console.log('createTable() called.'); 378 var sql = 'CREATE TABLE IF NOT EXISTS ' + obj.model.name 379 + ' (ID INTEGER PRIMARY KEY ASC AUTOINCREMENT UNIQUE'; 380 381 for(var r in obj.model.__meta) { 382 sql += ', ' + this.buildDbAttrFromProp(obj.model, r); 383 } 384 385 sql += ');'; 386 387 console.log(sql); 388 389 if(this.dbHandler) { 390 var that = this; 391 try { 392 /* transaction has 3 parameters: the transaction callback, the error callback and the success callback */ 393 this.dbHandler.transaction(function(t) { 394 t.executeSql(sql); 395 }, null, that.bindToCaller(that, that.handleDbReturn, [obj, callback])); 396 } catch(e) { 397 M.Logger.log('Error code: ' + e.code + ' msg: ' + e.message, M.ERROR); 398 return; 399 } 400 } else { 401 M.Logger.log('dbHandler does not exist.', M.ERROR); 402 } 403 }, 404 405 /** 406 * Creates a new data provider instance with the passed configuration parameters 407 * @param {Object} obj Includes dbName 408 */ 409 configure: function(obj) { 410 console.log('configure() called.'); 411 // maybe some value checking 412 return this.extend({ 413 config:obj 414 }); 415 }, 416 417 /* Helper methods */ 418 419 /** 420 * @private 421 * Creates the column definitions from the properties of the model record with help of the meta information that 422 * the {@link M.ModelAttribute} objects provide. 423 * @param {Object} 424 * @returns {String} The string used for db create to represent this property. 425 */ 426 buildDbAttrFromProp: function(model, prop) { 427 console.log(model); 428 console.log(prop); 429 var type = this.typeMapping[model.__meta[prop].dataType].toUpperCase(); 430 431 var isReqStr = model.__meta[prop].isRequired ? ' NOT NULL' : ''; 432 433 return prop + ' ' + type + isReqStr; 434 }, 435 436 437 /** 438 * Queries the WebSQL storage for the maximum db ID that was provided for a table that is defined by model.name. Delegates to 439 * {@link M.WebSqlProvider#setDbIdOfModel}. 440 * 441 * @param {Object} model The table's model 442 */ 443 queryDbForId: function(model) { 444 var that = this; 445 this.dbHandler.readTransaction(function(t) { 446 var r = t.executeSql('SELECT seq as ID FROM sqlite_sequence WHERE name="' + model.name + '"', [], function (tx, res) { 447 that.setDbIdOfModel(model, res.rows.item(0).ID); 448 }); 449 }); 450 }, 451 452 /** 453 * @private 454 * Is called when creating table successfully returned and therefor sets the initialization flag of the provider to YES. 455 * Then calls the internal callback => the function that called init(). 456 */ 457 handleDbReturn: function(obj, callback) { 458 console.log('handleDbReturn() called.'); 459 this.isInitialized = YES; 460 this.internalCallback(obj, callback); 461 }, 462 463 /** 464 * @private 465 * Is called from queryDbForId, sets the model record's ID to the latest value of ID in the database. 466 */ 467 setDbIdOfModel: function(model, id) { 468 model.record.ID = id; 469 } 470 471 });