├── README.markdown ├── javascripts ├── database-enyo.js ├── database-mojo.js └── database-standalone.js └── samples ├── example_data.json └── schema.json /README.markdown: -------------------------------------------------------------------------------- 1 | The Database class provides methods for working with HTML 5 SQLite 2 | databases in Palm's WebOS. It offers versions for both Enyo and 3 | Mojo (Enyo version is a Component, Mojo version a Prototype class). 4 | You can find full reference documentation within the inline comments 5 | or at http://onecrayon.github.com/database-webos 6 | 7 | ## Installation 8 | 9 | To use the class, download either database-mojo.js or database-enyo.js 10 | and put it somewhere in your app (I usually use a top-level javascripts 11 | folder for this kind of generic script). 12 | 13 | ### Enyo (webOS 3.0+, Enyo 2-compatible) 14 | 15 | Add a reference to the file in your appropriate depends.js file: 16 | 17 | enyo.depends( 18 | "javascripts/database-enyo.js" 19 | ); 20 | 21 | And then in your kind add the onecrayon.Database kind to your components: 22 | 23 | components: [ 24 | { 25 | name: "db", 26 | kind: "onecrayon.Database", 27 | database: 'ext:MyDatabase', 28 | version: "", 29 | debug: false 30 | } 31 | ] 32 | 33 | After your `create` method has been called, you will then be able to access 34 | the database methods using `this.$.db` (in this case; may be different if you 35 | named it differently). So for instance: 36 | 37 | this.$.db.setSchemaFromURL('schemas/schema.json', { 38 | onSuccess: enyo.bind(this, this.finishFirstRun) 39 | }); 40 | 41 | ### Mojo (webOS 1.x-2.x) 42 | 43 | Add the following line to your `sources.json` file: 44 | 45 | {"source": "javascripts/database.js"} 46 | 47 | And then in any of your assistants or other Javascript files you 48 | should be able to instantiate the class like so: 49 | 50 | var db = new Database('ext:my_database', {version: '1', estimatedSize: 1048576}); 51 | 52 | ## Documentation 53 | 54 | Currently all documentation for the class is inline in the source code. 55 | In particular, you should read the comments for `setSchema`, `query`, 56 | and `queries` as these are the main methods you will need in everyday usage. 57 | 58 | ## Changelog 59 | 60 | **2.1** 61 | 62 | - Now supports Enyo 2.0; thanks Scott Miles! 63 | 64 | **2.0** 65 | 66 | - Rewritten from the ground up for Enyo 67 | 68 | ## In the wild 69 | 70 | The Database class is developed for and used by [TapNote][1]. 71 | 72 | Let me know if you are using it, and I will note it here. 73 | 74 | [1]: http://onecrayon.com/tapnote/ 75 | 76 | ## Released under an MIT license 77 | 78 | Copyright (c) 2010-2012 Ian Beck 79 | 80 | Permission is hereby granted, free of charge, to any person obtaining a copy 81 | of this software and associated documentation files (the "Software"), to deal 82 | in the Software without restriction, including without limitation the rights 83 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 84 | copies of the Software, and to permit persons to whom the Software is 85 | furnished to do so, subject to the following conditions: 86 | 87 | The above copyright notice and this permission notice shall be included in 88 | all copies or substantial portions of the Software. 89 | 90 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 91 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 92 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 93 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 94 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 95 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 96 | THE SOFTWARE. -------------------------------------------------------------------------------- /javascripts/database-enyo.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | 4 | script: database.js 5 | 6 | description: Provides an interface to HTML 5 database objects for WebOS using an Enyo component 7 | 8 | license: MIT license 9 | 10 | authors: 11 | - Ian Beck 12 | - Scott J. Miles 13 | 14 | version: 2.1.0 15 | 16 | Core class design based on the Mootools Database class by André Fiedler: 17 | http://github.com/SunboX/mootools-database/ 18 | 19 | Schema definitions based on Mojo Database Helper Objects by Dave Freeman: 20 | http://webos101.com/Mojo_Database_Helper_Objects 21 | 22 | ... 23 | */ 24 | 25 | /** 26 | * onecrayon.Database (component) 27 | * 28 | * This is the component you'll be using in your own code. Provides shortcuts 29 | * to common HTML 5 SQLite database operations. 30 | * 31 | * Parameters: 32 | * - database (string, required): name of your database; prefix with ext: to allow >1 MB sizes 33 | * - version (string): version of the database you want to open/create 34 | * - estimatedSize (int): estimated size in bytes 35 | * - debug (bool): if true, outputs verbose debugging messages (mainly SQL that's being run) 36 | * 37 | * USAGE: 38 | * components: [ 39 | * { 40 | * name: "myDatabase", kind: "onecrayon.Database", 41 | * database: 'ext:my_database', 42 | * version: '1', estimatedSize: 1048576, debug: true 43 | * } 44 | * ] 45 | */ 46 | enyo.kind({ 47 | name: "onecrayon.Database", 48 | kind: enyo.Component, 49 | published: { 50 | database: '', 51 | version: '1', 52 | estimatedSize: null, 53 | debug: false 54 | }, 55 | 56 | /** @protected */ 57 | db: undefined, 58 | dbVersion: null, 59 | lastInsertRowId: 0, 60 | 61 | // === Constructor and creation logic === 62 | 63 | /** @protected */ 64 | constructor: function() { 65 | // Run our default construction 66 | this.inherited(arguments); 67 | 68 | // Setup listing of bound methods 69 | // Cuts down on memory usage spikes, since bind() creates a new method every call, but causes more initial memory to be allocated 70 | this.bound = { 71 | setSchema: enyo.bind(this, this.setSchema), 72 | insertData: enyo.bind(this, this.insertData), 73 | _errorHandler: enyo.bind(this, this._errorHandler) 74 | }; 75 | 76 | // @deprecated methods and properties; provided for backwards compatibility 77 | this.lastInsertID = this.lastInsertId; 78 | this.setSchemaFromURL = this.setSchemaFromUrl; 79 | this.insertDataFromURL = this.insertDataFromUrl; 80 | this.changeVersionWithSchemaFromURL = this.changeVersionWithSchemaFromUrl; 81 | }, 82 | 83 | /** @protected */ 84 | create: function() { 85 | this.inherited(arguments); 86 | 87 | // Database name is required; enforce it 88 | if (this.database === '') { 89 | this.error('Database: you must define a name for your database when instantiating the kind using the `database` property.'); 90 | return null; 91 | } 92 | // Just to make sure people are aware... 93 | if (this.database.indexOf('ext:') !== 0) { 94 | this.warn('Database: you are working with an internal database, which will limit its size to 1 MB. Prepend `ext:` to your database name to remove this restriction.'); 95 | } 96 | // Open our database connection 97 | // parameters: name, version, displayName [unused in WebOS], target size 98 | this.db = openDatabase(this.database, this.version, '', this.estimatedSize); 99 | // Make sure everything is peachy 100 | if (!this.db) { 101 | this.error('Database: failed to open database named ' + this.database); 102 | return null; 103 | } 104 | // Save the database version, in case it differs from options 105 | this.dbVersion = this.db.version; 106 | }, 107 | 108 | // === Standard database methods === 109 | 110 | /** 111 | * Fetch the version of the database 112 | */ 113 | getVersion: function() { 114 | return this.dbVersion; 115 | }, 116 | 117 | /** 118 | * Exposes the last ID inserted 119 | */ 120 | lastInsertId: function() { 121 | return this.lastInsertRowId; 122 | }, 123 | 124 | /** 125 | * Close the database connection 126 | * 127 | * Why you'd want to do this, I don't know; may as well support it, though 128 | */ 129 | close: function() { 130 | this.db.close(); 131 | }, 132 | 133 | /** 134 | * Destroy the entire database for the given version (if passed) 135 | * 136 | * If only there were a way to actually do this... 137 | */ 138 | remove: function(version) { 139 | this.error('Database: there is currently no way to destroy a database. Hopefully we will be able to add this in the future.'); 140 | }, 141 | 142 | /** 143 | * Execute an arbitrary SQL command on the database. 144 | * 145 | * If you need to execute multiple commands in a transaction, use queries() 146 | * 147 | * Parameters: 148 | * - sql (string or query object, required) 149 | * - options (object): 150 | * * values (array): replacements for '?' placeholders in SQL 151 | * (only use if not passing a DatabaseQuery object) 152 | * * onSuccess (function): method to call on successful query 153 | * + receives single argument: results as an array of objects 154 | * * onError (function): method to call on error; defaults to logging 155 | */ 156 | query: function(sql, options) { 157 | // Possible that the user closed the connection already, so double check 158 | if (!this.db) { 159 | this._db_lost(); 160 | return; 161 | } 162 | // Merge in user options (if any) to defaults 163 | var options = (typeof options !== 'undefined' ? options : {}); 164 | // Check to see if they passed in a query object 165 | if (!enyo.isString(sql)) { 166 | // Translate into options object and SQL string 167 | options.values = sql.values; 168 | sql = sql.sql; 169 | } 170 | // Run the actual merge for our options, making sure there's a default values array 171 | options = this._getOptions(options, {"values": []}); 172 | // Trim whitespace to make sure we can accurately check character positions 173 | sql = sql.replace(/^\s+|\s+$/g, ""); 174 | if (sql.lastIndexOf(';') !== sql.length - 1) { 175 | sql = sql + ';'; 176 | } 177 | // Run the transaction 178 | var self = this; 179 | this.db.transaction(function(transaction) { 180 | if (self.debug) { 181 | // Output the query to the log for debugging 182 | self.log(sql, ' ==> ', options.values); 183 | } 184 | transaction.executeSql(sql, options.values, function(transaction, results) { 185 | // We use this anonymous function to format the results 186 | // Just passing the SQLResultSet object would require SQLite-specific code on the part of the callback 187 | 188 | // Try to snag the last insert ID, if available 189 | try { 190 | self.lastInsertRowId = results.insertId; 191 | } catch(e) {} 192 | // Call the onSuccess with formatted results 193 | if (options.onSuccess) { 194 | options.onSuccess(self._convertResultSet(results)); 195 | } 196 | }, options.onError); 197 | }); 198 | }, 199 | 200 | /** 201 | * Execute multiple arbitrary SQL queries on the database as a single 202 | * transaction (group of inserts, for instance) 203 | * 204 | * Notes: 205 | * - Not appropriate for SELECT or anything with returned rows 206 | * - The last inserted ID will NOT be set when using this method 207 | * - onSuccess and onError are only for the transaction! NOT individual queries 208 | * 209 | * Parameters: 210 | * - queries (array, required): 211 | * * SQL strings or DatabaseQuery objects 212 | * - options (object): 213 | * * onSuccess: function to execute on LAST QUERY success 214 | * * onError: function to execute on TRANSACTION error 215 | */ 216 | queries: function(queries, options) { 217 | // Possible that the user closed the connection already, so double check 218 | if (!this.db) { 219 | this._db_lost(); 220 | return; 221 | } 222 | // Merge in user options (if any) to defaults 223 | var options = (typeof options !== 'undefined' ? options : {}); 224 | options = this._getOptions(options); 225 | // Run the transaction 226 | var DEBUG = this.debug; 227 | var self = this; 228 | this.db.transaction(function(transaction) { 229 | // Loop over each query and execute it 230 | var length = queries.length; 231 | var query = null; 232 | // Init variables for tracking SQL and values 233 | var sql = ''; 234 | var values = []; 235 | for (var i = 0; i < length; i++) { 236 | query = queries[i]; 237 | // If query isn't a string, it's an object 238 | if (enyo.isString(query)) { 239 | sql = query; 240 | } else { 241 | sql = query.sql; 242 | values = query.values; 243 | } 244 | if (DEBUG) { 245 | // Output query to the log for debugging 246 | self.log(sql, " ==> ", values); 247 | } 248 | if (i === length - 1) { 249 | // Last call 250 | transaction.executeSql(sql, values, options.onSuccess); 251 | } else { 252 | transaction.executeSql(sql, values); 253 | } 254 | } 255 | }, options.onError); 256 | }, 257 | 258 | 259 | // === JSON methods === 260 | 261 | /** 262 | * A core goal of the Database class is to enable you to easily port data 263 | * into your database using JSON. 264 | * 265 | * setSchema defines/inserts a table layout (if it doesn't already exist) 266 | * and inserts any data that you've provided inline 267 | * 268 | * Parameters: 269 | * - schema (object): see advanced description below 270 | * - options (object): 271 | * * onSuccess (function): called after successful transactions 272 | * * onError (function): called on error for transactions 273 | * 274 | * PLEASE NOTE: the onSuccess and onError functions may be called multiple 275 | * times if you are inserting data as well as defining a table schema. 276 | * 277 | * Schema Description 278 | * ================== 279 | * 280 | * An array of table objects, which each contain an array of columns objects 281 | * and an optional array of data to insert 282 | * 283 | * Array of table objects (optional if single table) => 284 | * table Object => 285 | * table (text, required; name of the table) 286 | * columns (array) => 287 | * column (text, required; name of the column) 288 | * type (text, required) 289 | * constraints (array of strings) 290 | * data (array) => 291 | * Object (keys are the names of the columns) 292 | * string (executed as a straight SQL query) 293 | * 294 | * Both columns and data are optionally; you can use setSchema to 295 | * define the table schema, populate with data, or both. 296 | * 297 | * Obviously, it's better practice to populate with data only when you 298 | * need to, whereas you'll likely be defining tables every time you 299 | * instantiate the Database class. 300 | * 301 | * You may also use an SQL string instead of a table object if you desire. 302 | * This is useful for running batch updates to modify existing schema, for 303 | * instance, as you can mix and match new tables with ALTER TABLE statements. 304 | * 305 | * JSON example 306 | * ============ 307 | * 308 | * [ 309 | * { 310 | * "table": "table1", 311 | * "columns": [ 312 | * { 313 | * "column": "entry_id", 314 | * "type": "INTEGER", 315 | * "constraints": ["PRIMARY_KEY"] 316 | * }, 317 | * { 318 | * "column": "title", 319 | * "type": "TEXT" 320 | * } 321 | * ], 322 | * "data": [ 323 | * { "entry_id": "1", "title": "My first entry" }, 324 | * { "entry_id": "2", "title": "My second entry" } 325 | * ] 326 | * }, 327 | * "ALTER TABLE table1 ADD COLUMN category TEXT" 328 | * ] 329 | */ 330 | setSchema: function(schema, options) { 331 | // Check to see if it's a single table, make array for convenience 332 | if (!enyo.isArray(schema)) { 333 | schema = [schema]; 334 | } 335 | // Merge in user options (if any) to defaults 336 | var options = (typeof options !== 'undefined' ? options : {}); 337 | options = this._getOptions(options); 338 | // Setup array to track table creation SQL 339 | var tableQueries = []; 340 | // Setup array to track data (just in case) 341 | var data = []; 342 | // Loop over the tables 343 | var length = schema.length; 344 | var table = null; 345 | for (var i = 0; i < length; i++) { 346 | table = schema[i]; 347 | // Check to see if we have an SQL string 348 | if (enyo.isString(table)) { 349 | tableQueries.push(table); 350 | } else { 351 | // Check for and save columns object 352 | if (typeof table.columns !== 'undefined') { 353 | tableQueries.push(this.getCreateTable(table.table, table.columns)); 354 | } 355 | // Check for and save data array 356 | if (typeof table.data !== 'undefined') { 357 | data.push({"table": table.table, "data": table.data}); 358 | } 359 | } 360 | } 361 | if (data.length > 0) { 362 | var dataInsertFollowup = enyo.bind(this, this.insertData, data, options); 363 | // Execute the queries 364 | this.queries(tableQueries, { 365 | onSuccess: dataInsertFollowup, 366 | onError: options.onError 367 | }); 368 | } else { 369 | this.queries(tableQueries, options); 370 | } 371 | }, 372 | 373 | 374 | /** 375 | * Allows you to set your schema using an arbitrary JSON file. 376 | * 377 | * Parameters: 378 | * - url (string, required): local or remote URL for JSON file 379 | * - options (object): same as setSchema options (above) 380 | */ 381 | setSchemaFromUrl: function(url, options) { 382 | this._readUrl(url, this.bound.setSchema, options); 383 | }, 384 | 385 | /** 386 | * Inserts arbitrary data from a Javascript object 387 | * 388 | * Parameters: 389 | * - data (array or object): 390 | * * table (string, required): name of the table to insert into 391 | * * data (array, required): array of objects whose keys are the column 392 | * names to insert into 393 | * - options (object): 394 | * * onSuccess (function): success callback 395 | * * onError (function): error callback 396 | * 397 | * The formatting is the same as for the schema, just without the columns. 398 | * Note that data can be a single object if only inserting into one table. 399 | */ 400 | insertData: function(data, options) { 401 | // Check to see if it's a single table 402 | if (!enyo.isArray(data)) { 403 | data = [data]; 404 | } 405 | // Merge in user options (if any) to defaults 406 | var options = (typeof options !== 'undefined' ? options : {}); 407 | options = this._getOptions(options); 408 | // Setup array to track queries 409 | var dataQueries = []; 410 | var length = data.length; 411 | var table = null; 412 | var i, j; 413 | var insertsLength = 0; 414 | var row = null; 415 | for (i = 0; i < length; i++) { 416 | table = data[i]; 417 | // Make sure there's actually a data array 418 | if (typeof table.data !== 'undefined') { 419 | var tableName = table.table; 420 | // Check to see if we have more than one row of data 421 | var inserts = null; 422 | if (!enyo.isArray(table.data)) { 423 | inserts = [table.data] 424 | } else { 425 | inserts = table.data; 426 | } 427 | // Nested loop to fetch the data inserts 428 | insertsLength = inserts.length; 429 | for (j = 0; j < insertsLength; j++) { 430 | row = inserts[j]; 431 | dataQueries.push(this.getInsert(tableName, row)); 432 | } 433 | } 434 | } 435 | // Execute that sucker! 436 | this.queries(dataQueries, options); 437 | }, 438 | 439 | /** 440 | * Allows you to populate data using arbitrary JSON file. 441 | * 442 | * Parameters: 443 | * - url (string, required): local or remote URL for JSON file 444 | * - options (object): same as insertData options (above) 445 | */ 446 | insertDataFromUrl: function(url, options) { 447 | this._readUrl(url, this.bound.insertData, options); 448 | }, 449 | 450 | 451 | // === VERSIONING METHODS === 452 | 453 | /** 454 | * Change the version of the database; allows porting data when 455 | * upgrading schema 456 | * 457 | * WARNING: you must have NO other database connections active when you 458 | * do this, and remember that afterward you will need to use the new 459 | * version in your `new Database()` calls. 460 | */ 461 | changeVersion: function(newVersion) { 462 | // Backwards compatibility with previous incarnation which was changeVersion(from, to) 463 | if (arguments.length > 1) { 464 | newVersion = arguments[1]; 465 | } 466 | var self = this; 467 | this.db.changeVersion(this.dbVersion, newVersion, function() {}, function() { 468 | if (self.debug) { 469 | self.error("DATABASE VERSION UPDATE FAILED: " + newVersion); 470 | } 471 | }, function() { 472 | if (self.debug) { 473 | self.log("DATABASE VERSION UPDATE SUCCESS: " + newVersion); 474 | } 475 | }); 476 | this.dbVersion = newVersion; 477 | }, 478 | 479 | /** 480 | * Change the version of the database and apply any schema updates 481 | * specified in the `schema` object 482 | * 483 | * NOTE: You cannot insert data with this call. Instead, run your schema 484 | * update and then use insertData in your success callback 485 | * 486 | * Parameters: 487 | * - newVersion (string or int) 488 | * - schema (object or string): same as setSchema (documented above), 489 | * minus any data insertion support 490 | * - options (object): same as setSchema options 491 | */ 492 | changeVersionWithSchema: function(newVersion, schema, options) { 493 | // Check to see if it's a single table, make array for convenience 494 | if (!enyo.isArray(schema)) { 495 | schema = [schema]; 496 | } 497 | // Merge in user options (if any) to defaults 498 | var options = (typeof options !== 'undefined' ? options : {}); 499 | options = this._getOptions(options); 500 | 501 | // Run the changeVersion update! 502 | this.db.changeVersion(this.dbVersion, newVersion, enyo.bind(this, function(transaction) { 503 | // Loop over the items in the schema 504 | var length = schema.length; 505 | var item = null, query = null, sql = null, values = null; 506 | for (var i = 0; i < length; i++) { 507 | item = schema[i]; 508 | // Check to see if we have an SQL string or table definition 509 | if (enyo.isString(item)) { 510 | query = item; 511 | } else if (typeof item.columns !== 'undefined') { 512 | query = this.getCreateTable(item.table, item.columns); 513 | } 514 | 515 | // Run the query 516 | sql = (enyo.isString(query) ? query : query.sql); 517 | values = (typeof query.values !== 'undefined' ? query.values : null); 518 | if (this.debug) { 519 | // Output the query to the log for debugging 520 | this.log(sql, ' ==> ', values); 521 | } 522 | if (values !== null) { 523 | transaction.executeSql(sql, values); 524 | } else { 525 | transaction.executeSql(sql); 526 | } 527 | } 528 | }), options.onError, enyo.bind(this, this._versionChanged, newVersion, options.onSuccess)); 529 | }, 530 | 531 | /** 532 | * Change the version of the database and apply any schema updates 533 | * specified in the schema JSON file located at `url` 534 | */ 535 | changeVersionWithSchemaFromUrl: function(newVersion, url, options) { 536 | this._readUrl(url, enyo.bind(this, this.changeVersionWithSchema, newVersion)); 537 | }, 538 | 539 | 540 | // === SQL Methods === 541 | 542 | /** 543 | * SQL to Insert records (create) 544 | * 545 | * Parameters: 546 | * - tableName (string, required) 547 | * - data (object, required): 548 | * * key: value pairs to be updated as column: value (same format as data 549 | * objects in schema) 550 | * 551 | * Returns DatabaseQuery object 552 | */ 553 | getInsert: function(tableName, data) { 554 | var sql = 'INSERT INTO ' + tableName + ' ('; 555 | var valueString = ' VALUES ('; 556 | // Set up our tracker array for value placeholders 557 | var colValues = []; 558 | // Loop over the keys in our object 559 | for (var key in data) { 560 | // Add the value to the valueString 561 | colValues.push(data[key]); 562 | // Add the placeholders 563 | sql += key; 564 | valueString += '?'; 565 | // Append commas 566 | sql += ', '; 567 | valueString += ', '; 568 | } 569 | // Remove extra commas and insert closing parentheses 570 | sql = sql.substr(0, sql.length - 2) + ')'; 571 | valueString = valueString.substr(0, valueString.length - 2) + ')'; 572 | // Put together the full SQL statement 573 | sql += valueString; 574 | // At long last, we've got our SQL; return it 575 | return new onecrayon.DatabaseQuery({'sql': sql, 'values': colValues}); 576 | }, 577 | 578 | /** 579 | * SQL for a very simple select 580 | * 581 | * Parameters: 582 | * - tableName (string, required) 583 | * - columns (string, array, or null): names of the columns to return 584 | * - where (object): {key: value} is equated to column: value 585 | * 586 | * Returns DatabaseQuery object 587 | */ 588 | getSelect: function(tableName, columns, where) { 589 | var sql = 'SELECT '; 590 | // Setup our targeted columns 591 | var colStr = ''; 592 | if (columns === null || columns === '') { 593 | colStr = '*'; 594 | } else if (enyo.isArray(columns)) { 595 | // Cut down on memory needs with a straight for loop 596 | var length = columns.length; 597 | var colStr = []; 598 | for (var i = 0; i < length; i++) { 599 | colStr.push(columns[i]); 600 | } 601 | // Join the column string together with commas 602 | colStr = colStr.join(', '); 603 | } 604 | sql += colStr + ' FROM ' + tableName; 605 | // Parse the WHERE object if we have one 606 | if (typeof where !== 'undefined') { 607 | sql += ' WHERE '; 608 | var sqlValues = []; 609 | var whereStrings = []; 610 | // Loop over the where object to populate 611 | for (var key in where) { 612 | sqlValues.push(where[key]); 613 | whereStrings.push(key + ' = ?'); 614 | } 615 | // Add the WHERE strings to the sql 616 | sql += whereStrings.join(' AND '); 617 | } 618 | return new onecrayon.DatabaseQuery({'sql': sql, 'values': sqlValues}); 619 | }, 620 | 621 | /** 622 | * SQL to update a particular row 623 | * 624 | * Parameters: 625 | * - tableName (string, required) 626 | * - data (object, required): 627 | * * key: value pairs to be updated as column: value (same format as 628 | * data objects in schema) 629 | * - where (object): key: value translated to 'column = value' 630 | * 631 | * Returns DatabaseQuery object 632 | */ 633 | getUpdate: function(tableName, data, where) { 634 | var sql = 'UPDATE ' + tableName + ' SET '; 635 | var sqlValues = []; 636 | var sqlStrings = []; 637 | // Loop over data object 638 | for (var key in data) { 639 | sqlStrings.push(key + ' = ?'); 640 | sqlValues.push(data[key]); 641 | } 642 | // Collapse sqlStrings into SQL 643 | sql += sqlStrings.join(', '); 644 | // Parse the WHERE object 645 | sql += ' WHERE '; 646 | var whereStrings = []; 647 | // Loop over the where object to populate 648 | for (var key in where) { 649 | whereStrings.push(key + ' = ?'); 650 | sqlValues.push(where[key]); 651 | } 652 | // Add the WHERE strings to the sql 653 | sql += whereStrings.join(' AND '); 654 | return new onecrayon.DatabaseQuery({'sql': sql, 'values': sqlValues}); 655 | }, 656 | 657 | /** 658 | * SQL to delete records 659 | * 660 | * Parameters: 661 | * - tableName (string, required) 662 | * - where (object, required): key: value mapped to 'column = value' 663 | * 664 | * Returns DatabaseQuery object 665 | */ 666 | getDelete: function(tableName, where) { 667 | var sql = 'DELETE FROM ' + tableName + ' WHERE '; 668 | var sqlValues = []; 669 | var whereStrings = []; 670 | // Loop over the where object to populate 671 | for (var key in where) { 672 | whereStrings.push(key + ' = ?'); 673 | sqlValues.push(where[key]); 674 | } 675 | // Add the WHERE strings to the sql 676 | sql += whereStrings.join(' AND '); 677 | return new onecrayon.DatabaseQuery({'sql': sql, 'values': sqlValues}); 678 | }, 679 | 680 | /** 681 | * SQL to create a new table 682 | * 683 | * Parameters: 684 | * - tableName (string, required) 685 | * - columns (array, required): uses syntax from setSchema (see above) 686 | * - ifNotExists (bool, defaults to true) 687 | * 688 | * Returns string, since value substitution isn't supported for this 689 | * statement in SQLite 690 | */ 691 | getCreateTable: function(tableName, columns, ifNotExists) { 692 | var ifNotExists = (typeof ifNotExists !== 'undefined' ? ifNotExists : true); 693 | // Setup the basic SQL 694 | var sql = 'CREATE TABLE '; 695 | if (ifNotExists) { 696 | sql += 'IF NOT EXISTS '; 697 | } 698 | sql += tableName + ' ('; 699 | // Add the column definitions to the SQL 700 | var length = columns.length; 701 | var col = null; 702 | var colStr = []; 703 | var colDef = ''; 704 | for (var i = 0; i < length; i++) { 705 | col = columns[i]; 706 | // Construct the string for the column definition 707 | colDef = col.column + ' ' + col.type; 708 | if (col.constraints) { 709 | colDef += ' ' + col.constraints.join(' '); 710 | } 711 | // Add to SQL 712 | colStr.push(colDef); 713 | } 714 | sql += colStr.join(', ') + ')'; 715 | return sql; 716 | }, 717 | 718 | /** 719 | * SQL for dropping a table 720 | * 721 | * Returns string 722 | */ 723 | getDropTable: function(tableName) { 724 | return 'DROP TABLE IF EXISTS ' + tableName; 725 | }, 726 | 727 | 728 | // === Private methods === 729 | 730 | /** 731 | * @protected 732 | * Sets the local tracking variable for the DB version 733 | * 734 | * PRIVATE FUNCTION; use the changeVersion* functions to modify 735 | * your database's version information 736 | */ 737 | _versionChanged: function(newVersion, callback) { 738 | this.dbVersion = newVersion; 739 | callback(); 740 | }, 741 | 742 | /** 743 | * @protected 744 | * Merge user options into the standard set 745 | * 746 | * Parameters: 747 | * - userOptions (object, required): options passed by the user 748 | * - extraOptions (object, optional) any default options beyond onSuccess 749 | * and onError 750 | */ 751 | _getOptions: function(userOptions, extraOptions) { 752 | var opts = { 753 | "onSuccess": this._emptyFunction, 754 | "onError": this.bound._errorHandler 755 | }; 756 | if (typeof extraOptions !== 'undefined') { 757 | opts = enyo.mixin(opts, extraOptions); 758 | } 759 | if (typeof userOptions === 'undefined') { 760 | var userOptions = {}; 761 | } 762 | return enyo.mixin(opts, userOptions); 763 | }, 764 | 765 | /** @protected */ 766 | _emptyFunction: function() {}, 767 | 768 | /** 769 | * @protected 770 | * Used to read in external JSON files 771 | */ 772 | _readUrl: function(url, callback, options) { 773 | var callbackBound = enyo.bind(this, function(responseText, response) { 774 | // I have no idea why status can be zero when reading file locally, but it can 775 | if (response.status === 200 || response.status === 0) { 776 | try { 777 | var json = enyo.json.parse(responseText); 778 | callback(json, options); 779 | } catch (e) { 780 | this.error('JSON request error:', e); 781 | } 782 | } else { 783 | this.error('Database: failed to read JSON at URL `' + url + '`'); 784 | } 785 | }); 786 | if (typeof enyo.xhrGet !== 'undefined') { 787 | // We're working with Enyo 1 788 | enyo.xhrGet({ 789 | 'url': url, 790 | load: callbackBound 791 | }); 792 | } else { 793 | // Working with Enyo 2, so use its xhr object instead 794 | enyo.xhr.request({ 795 | 'url': url, 796 | callback: callbackBound 797 | }); 798 | } 799 | }, 800 | 801 | /** 802 | * @protected 803 | * Converts an SQLResultSet into a standard Javascript array of results 804 | */ 805 | _convertResultSet: function(rs) { 806 | var results = []; 807 | if (rs.rows) { 808 | for (var i = 0; i < rs.rows.length; i++) { 809 | results.push(rs.rows.item(i)); 810 | } 811 | } 812 | return results; 813 | }, 814 | 815 | /** 816 | * @protected 817 | * Used to report generic database errors 818 | */ 819 | _errorHandler: function(transaction, error) { 820 | // If a transaction error (rather than an executeSQL error) there might only be one parameter 821 | if (typeof error === 'undefined') { 822 | var error = transaction; 823 | } 824 | this.error('Database error (' + error.code + '): ' + error.message); 825 | }, 826 | 827 | /** 828 | * @protected 829 | * Used to output "database lost" error 830 | */ 831 | _db_lost: function() { 832 | this.error('Database: connection has been closed or lost; cannot execute SQL'); 833 | } 834 | }); 835 | 836 | /** 837 | * onecrayon.DatabaseQuery (object) 838 | * 839 | * This is a helper that, at the moment, is basically just an object 840 | * with standard properties. 841 | * 842 | * Maybe down the road I'll add some helper methods for working with queries. 843 | * 844 | * USAGE: 845 | * var myQuery = new onecrayon.DatabaseQuery({ 846 | * sql: 'SELECT * FROM somewhere WHERE id = ?', 847 | * values: ['someID'] 848 | * }); 849 | */ 850 | onecrayon.DatabaseQuery = function(inProps) { 851 | this.sql = (typeof inProps.sql !== 'undefined' ? inProps.sql : ''); 852 | this.values = (typeof inProps.values !== 'undefined' ? inProps.values : []); 853 | }; 854 | -------------------------------------------------------------------------------- /javascripts/database-mojo.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | 4 | script: database.js 5 | 6 | description: Provides an interface to HTML 5 database objects for WebOS using Prototype.js classes 7 | 8 | license: MIT license 9 | 10 | authors: 11 | - Ian Beck 12 | 13 | version: 1.2.1 14 | 15 | Core class design based on the Mootools Database class by André Fiedler: 16 | http://github.com/SunboX/mootools-database/ 17 | 18 | Schema definitions based on Mojo Database Helper Objects by Dave Freeman: 19 | http://webos101.com/Mojo_Database_Helper_Objects 20 | 21 | ... 22 | */ 23 | 24 | /** 25 | * Database (class) 26 | * 27 | * This is the class you'll be using in your own code. Provides shortcuts 28 | * to common HTML 5 SQLite database operations. 29 | * 30 | * Parameters: 31 | * - name (string, required): prefix with ext: to allow >1 MB sizes 32 | * - options (object): 33 | * * version (string): version of the database you want to open/create 34 | * * estimatedSize (int): estimated size in bytes 35 | * 36 | * USAGE: 37 | * var db = new Database('ext:my_database', {version: '1', estimatedSize: 1048576}); 38 | */ 39 | 40 | var DATABASE_DEBUG = false; 41 | 42 | var Database = Class.create({ 43 | // === Class constructor === 44 | initialize: function(name, options, debug) { 45 | // Name is required, enforce that 46 | if (Object.isUndefined(name) || name == '') { 47 | Mojo.Log.error('Database: you must define a name for your database when instantiating the class.'); 48 | return; 49 | } else { 50 | this.dbName = name; 51 | } 52 | // Just to make sure people are aware... 53 | if (this.dbName.indexOf('ext:') != 0) { 54 | Mojo.Log.warn('Database: you are working with an internal database, which will limit its size to 1 MB. Prepend `ext:` to your database name to remove this restriction.'); 55 | } 56 | // Make sure there's something available as options 57 | var options = (!Object.isUndefined(options) ? options : {}); 58 | // Default options; oh, Mootools, how I miss thee 59 | this.options = new Hash({ 60 | version: '1', 61 | estimatedSize: null 62 | }); 63 | // Merge our passed options into the default options 64 | this.options = this.options.merge(options).toObject(); 65 | // Set the debug flag 66 | if (Object.isUndefined(debug)) { 67 | DATABASE_DEBUG = false; 68 | } else { 69 | DATABASE_DEBUG = debug; 70 | } 71 | // Open our database connection 72 | // parameters: name, version, displayName [unused in WebOS], target size 73 | this.db = openDatabase(this.dbName, this.options.version, '', this.options.estimatedSize); 74 | // Make sure everything is peachy 75 | if (!this.db) { 76 | Mojo.Log.error('Database: failed to open database named ' + this.dbName); 77 | return null; 78 | } 79 | // Save the database version, in case it differs from options 80 | this.dbVersion = this.db.version; 81 | // Setup a dummy last insert row ID 82 | this.lastInsertRowId = 0; 83 | // Setup listing of bound methods 84 | // Cuts down on memory usage spikes, since bind() creates a new method every call, but causes more initial memory to be allocated 85 | this.bound = { 86 | setSchema: this.setSchema.bind(this), 87 | insertData: this.insertData.bind(this), 88 | _errorHandler: this._errorHandler.bind(this) 89 | }; 90 | }, 91 | 92 | 93 | // === Standard database methods === 94 | 95 | /** 96 | * Fetch the version of the database 97 | */ 98 | getVersion: function() { 99 | return this.dbVersion; 100 | }, 101 | 102 | /** 103 | * Exposes the last ID inserted 104 | */ 105 | lastInsertID: function() { 106 | return this.lastInsertRowId; 107 | }, 108 | 109 | /** 110 | * Close the database connection 111 | * 112 | * Why you'd want to do this, I don't know; may as well support it, though 113 | */ 114 | close: function() { 115 | this.db.close(); 116 | }, 117 | 118 | /** 119 | * Destroy the entire database for the given version (if passed) 120 | * 121 | * If only there were a way to actually do this... 122 | */ 123 | destroy: function(version) { 124 | Mojo.Log.error('Database: there is currently no way to destroy a database. Hopefully we will be able to add this in the future.'); 125 | }, 126 | 127 | /** 128 | * Execute an arbitrary SQL command on the database. 129 | * 130 | * If you need to execute multiple commands in a transaction, use queries() 131 | * 132 | * Parameters: 133 | * - sql (string or query object, required) 134 | * - options (object): 135 | * * values (array): replacements for '?' placeholders in SQL 136 | * (only use if not passing a DatabaseQuery object) 137 | * * onSuccess (function): method to call on successful query 138 | * + receives single argument: results as an array of objects 139 | * * onError (function): method to call on error; defaults to logging 140 | */ 141 | query: function(sql, options) { 142 | // Possible that the user closed the connection already, so double check 143 | if (!this.db) { 144 | this._db_lost(); 145 | return; 146 | } 147 | // Merge in user options (if any) to defaults 148 | var options = (!Object.isUndefined(options) ? options : {}); 149 | // Check to see if they passed in a query object 150 | if (!Object.isString(sql)) { 151 | // Translate into options object and SQL string 152 | options.values = sql.values; 153 | sql = sql.sql; 154 | } 155 | // Run the actual merge for our options, making sure there's a default values array 156 | options = this._getOptions(options, {"values": []}); 157 | // SQL won't be executed unless we append the `GO;` command 158 | // Trim whitespace to make sure we can accurately check character positions 159 | sql = sql.strip(); 160 | if (sql.lastIndexOf(';') != sql.length - 1) { 161 | sql = sql + ';'; 162 | } 163 | if (sql.indexOf('GO;') == -1) { 164 | sql = sql + ' GO;'; 165 | } 166 | // Run the transaction 167 | this.db.transaction(function(transaction) { 168 | if (DATABASE_DEBUG) { 169 | // Output the query to the log for debugging 170 | Mojo.Log.info(sql, ' ==> ', options.values); 171 | } 172 | transaction.executeSql(sql, options.values, function(transaction, results) { 173 | // We use this anonymous function to format the results 174 | // Just passing the SQLResultSet object would require SQLite-specific code on the part of the callback 175 | 176 | // Try to snag the last insert ID, if available 177 | try { 178 | this.lastInsertRowId = results.insertId; 179 | } catch(e) {} 180 | // Call the onSuccess with formatted results 181 | if (options.onSuccess) { 182 | options.onSuccess(this._convertResultSet(results)); 183 | } 184 | }.bind(this), options.onError); 185 | }.bind(this)); 186 | }, 187 | 188 | /** 189 | * Execute multiple arbitrary SQL queries on the database as a single 190 | * transaction (group of inserts, for instance) 191 | * 192 | * Notes: 193 | * - Not appropriate for SELECT or anything with returned rows 194 | * - The last inserted ID will NOT be set when using this method 195 | * - onSuccess and onError are only for the transaction! NOT individual queries 196 | * 197 | * Parameters: 198 | * - queries (array, required): 199 | * * SQL strings or DatabaseQuery objects 200 | * - options (object): 201 | * * onSuccess: function to execute on LAST QUERY success 202 | * * onError: function to execute on TRANSACTION error 203 | */ 204 | queries: function(queries, options) { 205 | // Possible that the user closed the connection already, so double check 206 | if (!this.db) { 207 | this._db_lost(); 208 | return; 209 | } 210 | // Merge in user options (if any) to defaults 211 | var options = (!Object.isUndefined(options) ? options : {}); 212 | options = this._getOptions(options); 213 | // Run the transaction 214 | this.db.transaction(function(transaction) { 215 | // Loop over each query and execute it 216 | // Avoiding each saves on memory usage 217 | var length = queries.length; 218 | var query = null; 219 | // Init variables for tracking SQL and values 220 | var sql = ''; 221 | var values = []; 222 | for (var i = 0; i < length; i++) { 223 | query = queries[i]; 224 | // If query isn't a string, it's an object 225 | if (Object.isString(query)) { 226 | sql = query; 227 | } else { 228 | sql = query.sql; 229 | values = query.values; 230 | } 231 | if (DATABASE_DEBUG) { 232 | // Ouput query to the log for debugging 233 | Mojo.Log.info(sql, " ==> ", values); 234 | } 235 | if (i == length - 1) { 236 | // Last call 237 | transaction.executeSql(sql, values, options.onSuccess); 238 | } else { 239 | transaction.executeSql(sql, values); 240 | } 241 | } 242 | }, options.onError); 243 | }, 244 | 245 | 246 | // === JSON methods === 247 | 248 | /** 249 | * A core goal of the Database class is to enable you to easily port data 250 | * into your database using JSON. 251 | * 252 | * setSchema defines/inserts a table layout (if it doesn't already exist) 253 | * and inserts any data that you've provided inline 254 | * 255 | * Parameters: 256 | * - schema (object): see advanced description below 257 | * - options (object): 258 | * * onSuccess (function): called after successful transactions 259 | * * onError (function): called on error for transactions 260 | * 261 | * PLEASE NOTE: the onSuccess and onError functions may be called multiple 262 | * times if you are inserting data as well as defining a table schema. 263 | * 264 | * Schema Description 265 | * ================== 266 | * 267 | * An array of table objects, which each contain an array of columns objects 268 | * and an optional array of data to insert 269 | * 270 | * Array of table objects (optional if single table) => 271 | * table Object => 272 | * table (text, required; name of the table) 273 | * columns (array) => 274 | * column (text, required; name of the column) 275 | * type (text, required) 276 | * constraints (array of strings) 277 | * data (array) => 278 | * Object (keys are the names of the columns) 279 | * string (executed as a straight SQL query) 280 | * 281 | * Both columns and data are optionally; you can use setSchema to 282 | * define the table schema, populate with data, or both. 283 | * 284 | * Obviously, it's better practice to populate with data only when you 285 | * need to, whereas you'll likely be defining tables every time you 286 | * instantiate the Database class. 287 | * 288 | * You may also use an SQL string instead of a table object if you desire. 289 | * This is useful for running batch updates to modify existing schema, for 290 | * instance, as you can mix and match new tables with ALTER TABLE statements. 291 | * 292 | * JSON example 293 | * ============ 294 | * 295 | * [ 296 | * { 297 | * "table": "table1", 298 | * "columns": [ 299 | * { 300 | * "column": "entry_id", 301 | * "type": "INTEGER", 302 | * "constraints": ["PRIMARY_KEY"] 303 | * }, 304 | * { 305 | * "column": "title", 306 | * "type": "TEXT" 307 | * } 308 | * ], 309 | * "data": [ 310 | * { "entry_id": "1", "title": "My first entry" }, 311 | * { "entry_id": "2", "title": "My second entry" } 312 | * ] 313 | * }, 314 | * "ALTER TABLE table1 ADD COLUMN category TEXT" 315 | * ] 316 | */ 317 | setSchema: function(schema, options) { 318 | // Check to see if it's a single table, make array for convenience 319 | if (!Object.isArray(schema)) { 320 | schema = [schema]; 321 | } 322 | // Merge in user options (if any) to defaults 323 | var options = (!Object.isUndefined(options) ? options : {}); 324 | options = this._getOptions(options); 325 | // Setup array to track table creation SQL 326 | var tableQueries = []; 327 | // Setup array to track data (just in case) 328 | var data = []; 329 | // Loop over the tables 330 | var length = schema.length; 331 | var table = null; 332 | for (var i = 0; i < length; i++) { 333 | table = schema[i]; 334 | // Check to see if we have an SQL string 335 | if (Object.isString(table)) { 336 | tableQueries.push(table); 337 | } else { 338 | // Check for and save columns object 339 | if (!Object.isUndefined(table.columns)) { 340 | tableQueries.push(this.getCreateTable(table.table, table.columns)); 341 | } 342 | // Check for and save data array 343 | if (!Object.isUndefined(table.data)) { 344 | data.push({"table": table.table, "data": table.data}); 345 | } 346 | } 347 | } 348 | if (data.length > 0) { 349 | // Setup a synchronizer to allow the data insertion to proceed after table creation 350 | var synchronizer = new Mojo.Function.Synchronize({ 351 | syncCallback: this.insertData.bind(this, data, options) 352 | }); 353 | var wrapTrigger = synchronizer.wrap(function() {}); 354 | // Execute the queries 355 | this.queries(tableQueries, { 356 | onSuccess: wrapTrigger, 357 | onError: options.onError 358 | }); 359 | } else { 360 | this.queries(tableQueries, options); 361 | } 362 | }, 363 | 364 | /** 365 | * Allows you to set your schema using an arbitrary JSON file. 366 | * 367 | * Parameters: 368 | * - url (string, required): local or remote URL for JSON file 369 | * - options (object): same as setSchema options (above) 370 | */ 371 | setSchemaFromURL: function(url, options) { 372 | this._readURL(url, this.bound.setSchema, options); 373 | }, 374 | 375 | /** 376 | * Inserts arbitrary data from a Javascript object 377 | * 378 | * Parameters: 379 | * - data (array or object): 380 | * * table (string, required): name of the table to insert into 381 | * * data (array, required): array of objects whose keys are the column 382 | * names to insert into 383 | * - options (object): 384 | * * onSuccess (function): success callback 385 | * * onError (function): error callback 386 | * 387 | * The formatting is the same as for the schema, just without the columns. 388 | * Note that data can be a single object if only inserting into one table. 389 | */ 390 | insertData: function(data, options) { 391 | // Check to see if it's a single table 392 | if (!Object.isArray(data)) { 393 | data = [data]; 394 | } 395 | // Merge in user options (if any) to defaults 396 | var options = (!Object.isUndefined(options) ? options : {}); 397 | options = this._getOptions(options); 398 | // Setup array to track queries 399 | var dataQueries = []; 400 | var length = data.length; 401 | var table = null; 402 | var i, j; 403 | var insertsLength = 0; 404 | var row = null; 405 | for (i = 0; i < length; i++) { 406 | table = data[i]; 407 | // Make sure there's actually a data array 408 | if (!Object.isUndefined(table.data)) { 409 | var tableName = table.table; 410 | // Check to see if we have more than one row of data 411 | var inserts = null; 412 | if (!Object.isArray(table.data)) { 413 | inserts = [table.data] 414 | } else { 415 | inserts = table.data; 416 | } 417 | // Nested loop to fetch the data inserts 418 | insertsLength = inserts.length; 419 | for (j = 0; j < insertsLength; j++) { 420 | row = inserts[j]; 421 | dataQueries.push(this.getInsert(tableName, row)); 422 | } 423 | } 424 | } 425 | // Execute that sucker! 426 | this.queries(dataQueries, options); 427 | }, 428 | 429 | /** 430 | * Allows you to populate data using arbitrary JSON file. 431 | * 432 | * Parameters: 433 | * - url (string, required): local or remote URL for JSON file 434 | * - options (object): same as insertData options (above) 435 | */ 436 | insertDataFromURL: function(url, options) { 437 | this._readURL(url, this.bound.insertData, options); 438 | }, 439 | 440 | 441 | // === VERSIONING METHODS === 442 | 443 | /** 444 | * Change the version of the database; allows porting data when 445 | * upgrading schema 446 | * 447 | * WARNING: you must have NO other database connections active when you 448 | * do this, and remember that afterward you will need to use the new 449 | * version in your `new Database()` calls. 450 | */ 451 | changeVersion: function(newVersion) { 452 | // Backwards compatibility with previous incarnation which was changeVersion(from, to) 453 | if (arguments.length > 1) { 454 | newVersion = arguments[1]; 455 | } 456 | this.db.changeVersion(this.dbVersion, newVersion, function() {}, function() { 457 | if (DATABASE_DEBUG) { 458 | Mojo.Log.error("DATABASE VERSION UPDATE FAILED: " + newVersion); 459 | } 460 | }, function() { 461 | if (DATABASE_DEBUG) { 462 | Mojo.Log.info("DATABASE VERSION UPDATE SUCCESS: " + newVersion); 463 | } 464 | }); 465 | this.dbVersion = newVersion; 466 | }, 467 | 468 | /** 469 | * Change the version of the database and apply any schema updates 470 | * specified in the `schema` object 471 | * 472 | * NOTE: You cannot insert data with this call. Instead, run your schema 473 | * update and then use insertData in your success callback 474 | * 475 | * Parameters: 476 | * - newVersion (string or int) 477 | * - schema (object or string): same as setSchema (documented above), 478 | * minus any data insertion support 479 | * - options (object): same as setSchema options 480 | */ 481 | changeVersionWithSchema: function(newVersion, schema, options) { 482 | // Check to see if it's a single table, make array for convenience 483 | if (!Object.isArray(schema)) { 484 | schema = [schema]; 485 | } 486 | // Merge in user options (if any) to defaults 487 | var options = (!Object.isUndefined(options) ? options : {}); 488 | options = this._getOptions(options); 489 | 490 | // Run the changeVersion update! 491 | this.db.changeVersion(this.dbVersion, newVersion, function(transaction) { 492 | // Loop over the items in the schema 493 | var length = schema.length; 494 | var item = null, query = null, sql = null, values = null; 495 | for (var i = 0; i < length; i++) { 496 | item = schema[i]; 497 | // Check to see if we have an SQL string or table definition 498 | if (Object.isString(item)) { 499 | query = item; 500 | } else if (!Object.isUndefined(item.columns)) { 501 | query = this.getCreateTable(item.table, item.columns); 502 | } 503 | 504 | // Run the query 505 | sql = (Object.isString(query) ? query : query.sql); 506 | values = (!Object.isUndefined(query.values) ? query.values : null); 507 | if (DATABASE_DEBUG) { 508 | // Output the query to the log for debugging 509 | Mojo.Log.info(sql, ' ==> ', values); 510 | } 511 | if (values !== null) { 512 | transaction.executeSql(sql, values); 513 | } else { 514 | transaction.executeSql(sql); 515 | } 516 | } 517 | }.bind(this), options.onError, this._versionChanged.bind(this, newVersion, options.onSuccess)); 518 | }, 519 | 520 | /** 521 | * Change the version of the database and apply any schema updates 522 | * specified in the schema JSON file located at `url` 523 | */ 524 | changeVersionWithSchemaFromURL: function(newVersion, url, options) { 525 | this._readURL(url, this.changeVersionWithSchema.bind(this, newVersion)); 526 | }, 527 | 528 | 529 | // === SQL Methods === 530 | 531 | /** 532 | * SQL to Insert records (create) 533 | * 534 | * Parameters: 535 | * - tableName (string, required) 536 | * - data (object, required): 537 | * * key: value pairs to be updated as column: value (same format as data 538 | * objects in schema) 539 | * 540 | * Returns DatabaseQuery object 541 | */ 542 | getInsert: function(tableName, data) { 543 | var sql = 'INSERT INTO ' + tableName + ' ('; 544 | var valueString = ' VALUES ('; 545 | // Set up our tracker array for value placeholders 546 | var colValues = []; 547 | // Loop over the keys in our object 548 | for (var key in data) { 549 | // Add the value to the valueString 550 | colValues.push(data[key]); 551 | // Add the placeholders 552 | sql += key; 553 | valueString += '?'; 554 | // Append commas 555 | sql += ', '; 556 | valueString += ', '; 557 | } 558 | // Remove extra commas and insert closing parentheses 559 | sql = sql.substr(0, sql.length - 2) + ')'; 560 | valueString = valueString.substr(0, valueString.length - 2) + ')'; 561 | // Put together the full SQL statement 562 | sql += valueString; 563 | // At long last, we've got our SQL; return it 564 | return new DatabaseQuery(sql, colValues); 565 | }, 566 | 567 | /** 568 | * SQL for a very simple select 569 | * 570 | * Parameters: 571 | * - tableName (string, required) 572 | * - columns (string, array, or null): names of the columns to return 573 | * - where (object): {key: value} is equated to column: value 574 | * 575 | * Returns DatabaseQuery object 576 | */ 577 | getSelect: function(tableName, columns, where) { 578 | var sql = 'SELECT '; 579 | // Setup our targeted columns 580 | var colStr = ''; 581 | if (columns == null || columns == '') { 582 | colStr = '*'; 583 | } else if (Object.isArray(columns)) { 584 | // Cut down on memory needs with a straight for loop 585 | var length = columns.length; 586 | var colStr = new Array(); 587 | for (var i = 0; i < length; i++) { 588 | colStr.push(columns[i]); 589 | } 590 | // Join the column string together with commas 591 | colStr = colStr.join(', '); 592 | } 593 | sql += colStr + ' FROM ' + tableName; 594 | // Parse the WHERE object if we have one 595 | if (!Object.isUndefined(where)) { 596 | sql += ' WHERE '; 597 | var sqlValues = []; 598 | var whereStrings = []; 599 | // Loop over the where object to populate 600 | for (var key in where) { 601 | sqlValues.push(where[key]); 602 | whereStrings.push(key + ' = ?'); 603 | } 604 | // Add the WHERE strings to the sql 605 | sql += whereStrings.join(' AND '); 606 | } 607 | return new DatabaseQuery(sql, sqlValues); 608 | }, 609 | 610 | /** 611 | * SQL to update a particular row 612 | * 613 | * Parameters: 614 | * - tableName (string, required) 615 | * - data (object, required): 616 | * * key: value pairs to be updated as column: value (same format as 617 | * data objects in schema) 618 | * - where (object): key: value translated to 'column = value' 619 | * 620 | * Returns DatabaseQuery object 621 | */ 622 | getUpdate: function(tableName, data, where) { 623 | var sql = 'UPDATE ' + tableName + ' SET '; 624 | var sqlValues = []; 625 | var sqlStrings = []; 626 | // Loop over data object 627 | for (var key in data) { 628 | sqlStrings.push(key + ' = ?'); 629 | sqlValues.push(data[key]); 630 | } 631 | // Collapse sqlStrings into SQL 632 | sql += sqlStrings.join(', '); 633 | // Parse the WHERE object 634 | sql += ' WHERE '; 635 | var whereStrings = []; 636 | // Loop over the where object to populate 637 | for (var key in where) { 638 | whereStrings.push(key + ' = ?'); 639 | sqlValues.push(where[key]); 640 | } 641 | // Add the WHERE strings to the sql 642 | sql += whereStrings.join(' AND '); 643 | return new DatabaseQuery(sql, sqlValues); 644 | }, 645 | 646 | /** 647 | * SQL to delete records 648 | * 649 | * Parameters: 650 | * - tableName (string, required) 651 | * - where (object, required): key: value mapped to 'column = value' 652 | * 653 | * Returns DatabaseQuery object 654 | */ 655 | getDelete: function(tableName, where) { 656 | var sql = 'DELETE FROM ' + tableName + ' WHERE '; 657 | var sqlValues = []; 658 | var whereStrings = []; 659 | // Loop over the where object to populate 660 | for (var key in where) { 661 | whereStrings.push(key + ' = ?'); 662 | sqlValues.push(where[key]); 663 | } 664 | // Add the WHERE strings to the sql 665 | sql += whereStrings.join(' AND '); 666 | return new DatabaseQuery(sql, sqlValues); 667 | }, 668 | 669 | /** 670 | * SQL to create a new table 671 | * 672 | * Parameters: 673 | * - tableName (string, required) 674 | * - columns (array, required): uses syntax from setSchema (see above) 675 | * - ifNotExists (bool, defaults to true) 676 | * 677 | * Returns string, since value substitution isn't supported for this 678 | * statement in SQLite 679 | */ 680 | getCreateTable: function(tableName, columns, ifNotExists) { 681 | var ifNotExists = (!Object.isUndefined(ifNotExists) ? ifNotExists : true); 682 | // Setup the basic SQL 683 | var sql = 'CREATE TABLE '; 684 | if (ifNotExists) { 685 | sql += 'IF NOT EXISTS '; 686 | } 687 | sql += tableName + ' ('; 688 | // Add the column definitions to the SQL 689 | var length = columns.length; 690 | var col = null; 691 | var colStr = new Array(); 692 | var colDef = ''; 693 | for (var i = 0; i < length; i++) { 694 | col = columns[i]; 695 | // Construct the string for the column definition 696 | colDef = col.column + ' ' + col.type; 697 | if (col.constraints) { 698 | colDef += ' ' + col.constraints.join(' '); 699 | } 700 | // Add to SQL 701 | colStr.push(colDef); 702 | } 703 | sql += colStr.join(', ') + ')'; 704 | return sql; 705 | }, 706 | 707 | /** 708 | * SQL for dropping a table 709 | * 710 | * Returns string 711 | */ 712 | getDropTable: function(tableName) { 713 | return 'DROP TABLE IF EXISTS ' + tableName; 714 | }, 715 | 716 | 717 | // === Private methods === 718 | 719 | /** 720 | * Sets the local tracking variable for the DB version 721 | * 722 | * PRIVATE FUNCTION; use the changeVersion* functions to modify 723 | * your database's version information 724 | */ 725 | _versionChanged: function(newVersion, callback) { 726 | this.dbVersion = newVersion; 727 | callback(); 728 | }, 729 | 730 | /** 731 | * Merge user options into the standard set 732 | * 733 | * Parameters: 734 | * - userOptions (object, required): options passed by the user 735 | * - extraOptions (object, optional) any default options beyond onSuccess 736 | * and onError 737 | */ 738 | _getOptions: function(userOptions, extraOptions) { 739 | var opts = new Hash({ 740 | "onSuccess": Prototype.emptyFunction, 741 | "onError": this.bound._errorHandler 742 | }); 743 | if (!Object.isUndefined(extraOptions)) { 744 | opts.merge(extraOptions); 745 | } 746 | if (Object.isUndefined(userOptions)) { 747 | var userOptions = {}; 748 | } 749 | return opts.merge(userOptions).toObject(); 750 | }, 751 | 752 | /* Used to read in external JSON files */ 753 | _readURL: function(url, callback, options) { 754 | new Ajax.Request(url, { 755 | method: 'get', 756 | onSuccess: function(response) { 757 | try { 758 | var json = response.responseText.evalJSON(true); 759 | callback(json, options); 760 | } catch (e) { 761 | Mojo.Log.error('JSON request error:', e); 762 | } 763 | }, 764 | onFailure: function(failure) { 765 | Mojo.Log.error('Database: failed to read JSON at URL `' + url + '`'); 766 | } 767 | }); 768 | }, 769 | 770 | /* Converts an SQLResultSet into a standard Javascript array of results */ 771 | _convertResultSet: function(rs) { 772 | var results = []; 773 | if (rs.rows) { 774 | for (var i = 0; i < rs.rows.length; i++) { 775 | results.push(rs.rows.item(i)); 776 | } 777 | } 778 | return results; 779 | }, 780 | 781 | /* Used to report generic database errors */ 782 | _errorHandler: function(transaction, error) { 783 | // If a transaction error (rather than an executeSQL error) there might only be one parameter 784 | if (Object.isUndefined(error)) { 785 | var error = transaction; 786 | } 787 | Mojo.Log.error('Database error (' + error.code + '): ' + error.message); 788 | }, 789 | 790 | /* Used to output "database lost" error */ 791 | _db_lost: function() { 792 | Mojo.Log.error('Database: connection has been closed or lost; cannot execute SQL'); 793 | } 794 | }); 795 | 796 | /** 797 | * DatabaseQuery (class) 798 | * 799 | * This is a helper class that, at the moment, is basically just an object 800 | * with standard properties. 801 | * 802 | * Maybe down the road I'll add some helper methods for working with queries. 803 | * 804 | * USAGE: 805 | * var myQuery = new DatabaseQuery('SELECT * FROM somwehere WHERE id = ?', ['someID']); 806 | * console.log(myQuery.sql); 807 | * consol.log(myQuery.values); 808 | */ 809 | 810 | var DatabaseQuery = Class.create({ 811 | initialize: function(sql, values) { 812 | this.sql = sql; 813 | this.values = values; 814 | } 815 | }); 816 | -------------------------------------------------------------------------------- /javascripts/database-standalone.js: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | 4 | script: database.js 5 | 6 | description: Provides a simplified interface to HTML 5 database objects 7 | 8 | license: MIT license 9 | 10 | authors: 11 | - Ian Beck 12 | 13 | version: 2.0.0 14 | 15 | Core class design based on the Mootools Database class by André Fiedler: 16 | http://github.com/SunboX/mootools-database/ 17 | 18 | Schema definitions based on Mojo Database Helper Objects by Dave Freeman: 19 | http://webos101.com/Mojo_Database_Helper_Objects 20 | 21 | ... 22 | */ 23 | 24 | /** 25 | * Database (class) 26 | * 27 | * This is the class you'll be using in your own code. Provides shortcuts 28 | * to common HTML 5 SQLite database operations. 29 | * 30 | * Parameters: 31 | * - name (string, required): name of your database; prefix with ext: to allow >1 MB sizes 32 | * - version (string): version of the database you want to open/create 33 | * - estimatedSize (int): estimated size in bytes 34 | * - debug (bool): if true, outputs verbose debugging messages (mainly SQL that's being run) 35 | * 36 | * USAGE: 37 | * var db = new Database('database-name', '1', null, false); 38 | */ 39 | var Database = function(name, version, estimatedSize, debug) { 40 | if (typeof name !== 'string') { 41 | throw new Error('Database class constructor requires name argument'); 42 | return undefined; 43 | } 44 | // Setup public properties 45 | this.name = name; 46 | this.version = (arguments.length >= 2 ? version : '1'); 47 | this.estimatedSize = (arguments.length >= 3 ? estimatedSize : null); 48 | this.debug = (arguments.length >= 4 ? debug : false); 49 | this.debug = (this.debug); 50 | 51 | // Open connection to database, and setup protected properties 52 | // parameters: name, version, displayName [unused anywhere that I know of], target size 53 | this._db = openDatabase(this.name, this.version, '', this.estimatedSize); 54 | // Make sure everything is peachy 55 | if (!this._db) { 56 | throw new Error('Database: failed to open database named ' + this.name); 57 | return undefined; 58 | } 59 | // Save the database version, in case it differs from options 60 | this._dbVersion = this._db.version; 61 | // Init lastInsertRowId 62 | this._lastInsertRowId = 0; 63 | 64 | // Setup bound functions; increases memory footprint, but speeds performance 65 | this.bound = { 66 | setSchema: this._bind(this, this.setSchema), 67 | insertData: this._bind(this, this.insertData), 68 | _errorHandler: this._bind(this, this._errorHandler) 69 | }; 70 | } 71 | 72 | // === Standard database methods === 73 | 74 | /** 75 | * Fetch the version of the database 76 | */ 77 | Database.prototype.getVersion = function() { 78 | return this._dbVersion; 79 | } 80 | 81 | /** 82 | * Exposes the last ID inserted 83 | */ 84 | Database.prototype.lastInsertId = function() { 85 | return this._lastInsertRowId; 86 | } 87 | 88 | /** 89 | * Close the database connection 90 | * 91 | * Why you'd want to do this, I don't know; may as well support it, though 92 | */ 93 | Database.prototype.close = function() { 94 | this._db.close(); 95 | } 96 | 97 | /** 98 | * Destroy the entire database for the given version (if passed) 99 | * 100 | * If only there were a way to actually do this... 101 | */ 102 | Database.prototype.destroy = function(version) { 103 | if (console && console.log) { 104 | console.log('Database: there is currently no way to destroy a database. Hopefully we will be able to add this in the future.'); 105 | } 106 | } 107 | 108 | /** 109 | * Execute an arbitrary SQL command on the database. 110 | * 111 | * If you need to execute multiple commands in a transaction, use queries() 112 | * 113 | * Parameters: 114 | * - sql (string or query object, required) 115 | * - options (object): 116 | * * values (array): replacements for '?' placeholders in SQL 117 | * (only use if not passing a DatabaseQuery object) 118 | * * onSuccess (function): method to call on successful query 119 | * + receives single argument: results as an array of objects 120 | * * onError (function): method to call on error; defaults to logging 121 | */ 122 | Database.prototype.query = function(sql, options) { 123 | // Possible that the user closed the connection already, so double check 124 | if (!this._db) { 125 | this._db_lost(); 126 | return; 127 | } 128 | // Merge in user options (if any) to defaults 129 | var options = (typeof options !== 'undefined' ? options : {}); 130 | // Check to see if they passed in a query object 131 | if (typeof sql !== 'string') { 132 | // Translate into options object and SQL string 133 | options.values = sql.values; 134 | sql = sql.sql; 135 | } 136 | // Run the actual merge for our options, making sure there's a default values array 137 | options = this._getOptions(options, {"values": []}); 138 | // Trim whitespace to make sure we can accurately check character positions 139 | sql = sql.replace(/(^\s*|\s*$)/g, ''); 140 | if (sql.lastIndexOf(';') !== sql.length - 1) { 141 | sql = sql + ';'; 142 | } 143 | // Run the transaction 144 | var self = this; 145 | this._db.transaction(function(transaction) { 146 | if (self.debug) { 147 | // Output the query to the log for debugging 148 | console.log(sql, ' ==> ', options.values); 149 | } 150 | transaction.executeSql(sql, options.values, function(transaction, results) { 151 | // We use this anonymous function to format the results 152 | // Just passing the SQLResultSet object would require SQLite-specific code on the part of the callback 153 | 154 | // Try to snag the last insert ID, if available 155 | try { 156 | self._lastInsertRowId = results.insertId; 157 | } catch(e) {} 158 | // Call the onSuccess with formatted results 159 | if (options.onSuccess) { 160 | options.onSuccess(self._convertResultSet(results)); 161 | } 162 | }, options.onError); 163 | }); 164 | } 165 | 166 | /** 167 | * Execute multiple arbitrary SQL queries on the database as a single 168 | * transaction (group of inserts, for instance) 169 | * 170 | * Notes: 171 | * - Not appropriate for SELECT or anything with returned rows 172 | * - The last inserted ID will NOT be set when using this method 173 | * - onSuccess and onError are only for the transaction! NOT individual queries 174 | * 175 | * Parameters: 176 | * - queries (array, required): 177 | * * SQL strings or DatabaseQuery objects 178 | * - options (object): 179 | * * onSuccess: function to execute on LAST QUERY success 180 | * * onError: function to execute on TRANSACTION error 181 | */ 182 | Database.prototype.queries = function(queries, options) { 183 | // Possible that the user closed the connection already, so double check 184 | if (!this._db) { 185 | this._db_lost(); 186 | return; 187 | } 188 | // Merge in user options (if any) to defaults 189 | var options = (typeof options !== 'undefined' ? options : {}); 190 | options = this._getOptions(options); 191 | // Run the transaction 192 | var DEBUG = this.debug; 193 | this._db.transaction(function(transaction) { 194 | // Loop over each query and execute it 195 | var length = queries.length; 196 | var query = null; 197 | // Init variables for tracking SQL and values 198 | var sql = ''; 199 | var values = []; 200 | for (var i = 0; i < length; i++) { 201 | query = queries[i]; 202 | // If query isn't a string, it's an object 203 | if (typeof query === 'string') { 204 | sql = query; 205 | } else { 206 | sql = query.sql; 207 | values = query.values; 208 | } 209 | if (debug) { 210 | // Output query to the log for debugging 211 | console.log(sql, " ==> ", values); 212 | } 213 | if (i === length - 1) { 214 | // Last call 215 | transaction.executeSql(sql, values, options.onSuccess); 216 | } else { 217 | transaction.executeSql(sql, values); 218 | } 219 | } 220 | }, options.onError); 221 | } 222 | 223 | 224 | // === JSON methods === 225 | 226 | /** 227 | * A core goal of the Database class is to enable you to easily port data 228 | * into your database using JSON. 229 | * 230 | * setSchema defines/inserts a table layout (if it doesn't already exist) 231 | * and inserts any data that you've provided inline 232 | * 233 | * Parameters: 234 | * - schema (object): see advanced description below 235 | * - options (object): 236 | * * onSuccess (function): called after successful transactions 237 | * * onError (function): called on error for transactions 238 | * 239 | * PLEASE NOTE: the onSuccess and onError functions may be called multiple 240 | * times if you are inserting data as well as defining a table schema. 241 | * 242 | * Schema Description 243 | * ================== 244 | * 245 | * An array of table objects, which each contain an array of columns objects 246 | * and an optional array of data to insert 247 | * 248 | * Array of table objects (optional if single table) => 249 | * table Object => 250 | * table (text, required; name of the table) 251 | * columns (array) => 252 | * column (text, required; name of the column) 253 | * type (text, required) 254 | * constraints (array of strings) 255 | * data (array) => 256 | * Object (keys are the names of the columns) 257 | * string (executed as a straight SQL query) 258 | * 259 | * Both columns and data are optionally; you can use setSchema to 260 | * define the table schema, populate with data, or both. 261 | * 262 | * Obviously, it's better practice to populate with data only when you 263 | * need to, whereas you'll likely be defining tables every time you 264 | * instantiate the Database class. 265 | * 266 | * You may also use an SQL string instead of a table object if you desire. 267 | * This is useful for running batch updates to modify existing schema, for 268 | * instance, as you can mix and match new tables with ALTER TABLE statements. 269 | * 270 | * JSON example 271 | * ============ 272 | * 273 | * [ 274 | * { 275 | * "table": "table1", 276 | * "columns": [ 277 | * { 278 | * "column": "entry_id", 279 | * "type": "INTEGER", 280 | * "constraints": ["PRIMARY_KEY"] 281 | * }, 282 | * { 283 | * "column": "title", 284 | * "type": "TEXT" 285 | * } 286 | * ], 287 | * "data": [ 288 | * { "entry_id": "1", "title": "My first entry" }, 289 | * { "entry_id": "2", "title": "My second entry" } 290 | * ] 291 | * }, 292 | * "ALTER TABLE table1 ADD COLUMN category TEXT" 293 | * ] 294 | */ 295 | Database.prototype.setSchema = function(schema, options) { 296 | // Check to see if it's a single table, make array for convenience 297 | if (!this._isArray(schema)) { 298 | schema = [schema]; 299 | } 300 | // Merge in user options (if any) to defaults 301 | var options = (typeof options !== 'undefined' ? options : {}); 302 | options = this._getOptions(options); 303 | // Setup array to track table creation SQL 304 | var tableQueries = []; 305 | // Setup array to track data (just in case) 306 | var data = []; 307 | // Loop over the tables 308 | var length = schema.length; 309 | var table = null; 310 | for (var i = 0; i < length; i++) { 311 | table = schema[i]; 312 | // Check to see if we have an SQL string 313 | if (typeof table === 'string') { 314 | tableQueries.push(table); 315 | } else { 316 | // Check for and save columns object 317 | if (typeof table.columns !== 'undefined') { 318 | tableQueries.push(this.getCreateTable(table.table, table.columns)); 319 | } 320 | // Check for and save data array 321 | if (typeof table.data !== 'undefined') { 322 | data.push({"table": table.table, "data": table.data}); 323 | } 324 | } 325 | } 326 | if (data.length > 0) { 327 | var dataInsertFollowup = this._bind(this, this.insertData, data, options); 328 | // Execute the queries 329 | this.queries(tableQueries, { 330 | onSuccess: dataInsertFollowup, 331 | onError: options.onError 332 | }); 333 | } else { 334 | this.queries(tableQueries, options); 335 | } 336 | } 337 | 338 | 339 | /** 340 | * Allows you to set your schema using an arbitrary JSON file. 341 | * 342 | * Parameters: 343 | * - url (string, required): local or remote URL for JSON file 344 | * - options (object): same as setSchema options (above) 345 | */ 346 | Database.prototype.setSchemaFromUrl = function(url, options) { 347 | this._readUrl(url, this.bound.setSchema, options); 348 | } 349 | 350 | /** 351 | * Inserts arbitrary data from a Javascript object 352 | * 353 | * Parameters: 354 | * - data (array or object): 355 | * * table (string, required): name of the table to insert into 356 | * * data (array, required): array of objects whose keys are the column 357 | * names to insert into 358 | * - options (object): 359 | * * onSuccess (function): success callback 360 | * * onError (function): error callback 361 | * 362 | * The formatting is the same as for the schema, just without the columns. 363 | * Note that data can be a single object if only inserting into one table. 364 | */ 365 | Database.prototype.insertData = function(data, options) { 366 | // Check to see if it's a single table 367 | if (!this._isArray(data)) { 368 | data = [data]; 369 | } 370 | // Merge in user options (if any) to defaults 371 | var options = (typeof options !== 'undefined' ? options : {}); 372 | options = this._getOptions(options); 373 | // Setup array to track queries 374 | var dataQueries = []; 375 | var length = data.length; 376 | var table = null; 377 | var i, j; 378 | var insertsLength = 0; 379 | var row = null; 380 | for (i = 0; i < length; i++) { 381 | table = data[i]; 382 | // Make sure there's actually a data array 383 | if (typeof table.data !== 'undefined') { 384 | var tableName = table.table; 385 | // Check to see if we have more than one row of data 386 | var inserts = null; 387 | if (!this._isArray(table.data)) { 388 | inserts = [table.data] 389 | } else { 390 | inserts = table.data; 391 | } 392 | // Nested loop to fetch the data inserts 393 | insertsLength = inserts.length; 394 | for (j = 0; j < insertsLength; j++) { 395 | row = inserts[j]; 396 | dataQueries.push(this.getInsert(tableName, row)); 397 | } 398 | } 399 | } 400 | // Execute that sucker! 401 | this.queries(dataQueries, options); 402 | } 403 | 404 | /** 405 | * Allows you to populate data using arbitrary JSON file. 406 | * 407 | * Parameters: 408 | * - url (string, required): local or remote URL for JSON file 409 | * - options (object): same as insertData options (above) 410 | */ 411 | Database.prototype.insertDataFromUrl = function(url, options) { 412 | this._readUrl(url, this.bound.insertData, options); 413 | } 414 | 415 | 416 | // === VERSIONING METHODS === 417 | 418 | /** 419 | * Change the version of the database; allows porting data when 420 | * upgrading schema 421 | * 422 | * WARNING: you must have NO other database connections active when you 423 | * do this, and remember that afterward you will need to use the new 424 | * version in your `new Database()` calls. 425 | */ 426 | Database.prototype.changeVersion = function(newVersion) { 427 | // Backwards compatibility with previous incarnation which was changeVersion(from, to) 428 | if (arguments.length > 1) { 429 | newVersion = arguments[1]; 430 | } 431 | var self = this; 432 | this._db.changeVersion(this._dbVersion, newVersion, function() {}, function() { 433 | if (self.debug) { 434 | console.log("DATABASE VERSION UPDATE FAILED: " + newVersion); 435 | } 436 | }, function() { 437 | if (self.debug) { 438 | console.log("DATABASE VERSION UPDATE SUCCESS: " + newVersion); 439 | } 440 | }); 441 | this._dbVersion = newVersion; 442 | } 443 | 444 | /** 445 | * Change the version of the database and apply any schema updates 446 | * specified in the `schema` object 447 | * 448 | * NOTE: You cannot insert data with this call. Instead, run your schema 449 | * update and then use insertData in your success callback 450 | * 451 | * Parameters: 452 | * - newVersion (string or int) 453 | * - schema (object or string): same as setSchema (documented above), 454 | * minus any data insertion support 455 | * - options (object): same as setSchema options 456 | */ 457 | Database.prototype.changeVersionWithSchema = function(newVersion, schema, options) { 458 | // Check to see if it's a single table, make array for convenience 459 | if (!this._isArray(schema)) { 460 | schema = [schema]; 461 | } 462 | // Merge in user options (if any) to defaults 463 | var options = (typeof options !== 'undefined' ? options : {}); 464 | options = this._getOptions(options); 465 | 466 | // Run the changeVersion update! 467 | this._db.changeVersion(this._dbVersion, newVersion, this._bind(this, function(transaction) { 468 | // Loop over the items in the schema 469 | var length = schema.length; 470 | var item = null, query = null, sql = null, values = null; 471 | for (var i = 0; i < length; i++) { 472 | item = schema[i]; 473 | // Check to see if we have an SQL string or table definition 474 | if (typeof item === 'string') { 475 | query = item; 476 | } else if (typeof item.columns !== 'undefined') { 477 | query = this.getCreateTable(item.table, item.columns); 478 | } 479 | 480 | // Run the query 481 | sql = (typeof query === 'string' ? query : query.sql); 482 | values = (typeof query.values !== 'undefined' ? query.values : null); 483 | if (this.debug) { 484 | // Output the query to the log for debugging 485 | console.log(sql, ' ==> ', values); 486 | } 487 | if (values !== null) { 488 | transaction.executeSql(sql, values); 489 | } else { 490 | transaction.executeSql(sql); 491 | } 492 | } 493 | }), options.onError, this._bind(this, this._versionChanged, newVersion, options.onSuccess)); 494 | } 495 | 496 | /** 497 | * Change the version of the database and apply any schema updates 498 | * specified in the schema JSON file located at `url` 499 | */ 500 | Database.prototype.changeVersionWithSchemaFromUrl = function(newVersion, url, options) { 501 | this._readUrl(url, this._bind(this, this.changeVersionWithSchema, newVersion)); 502 | } 503 | 504 | 505 | // === SQL Methods === 506 | 507 | /** 508 | * SQL to Insert records (create) 509 | * 510 | * Parameters: 511 | * - tableName (string, required) 512 | * - data (object, required): 513 | * * key: value pairs to be updated as column: value (same format as data 514 | * objects in schema) 515 | * 516 | * Returns DatabaseQuery object 517 | */ 518 | Database.prototype.getInsert = function(tableName, data) { 519 | var sql = 'INSERT INTO ' + tableName + ' ('; 520 | var valueString = ' VALUES ('; 521 | // Set up our tracker array for value placeholders 522 | var colValues = []; 523 | // Loop over the keys in our object 524 | for (var key in data) { 525 | // Add the value to the valueString 526 | colValues.push(data[key]); 527 | // Add the placeholders 528 | sql += key; 529 | valueString += '?'; 530 | // Append commas 531 | sql += ', '; 532 | valueString += ', '; 533 | } 534 | // Remove extra commas and insert closing parentheses 535 | sql = sql.substr(0, sql.length - 2) + ')'; 536 | valueString = valueString.substr(0, valueString.length - 2) + ')'; 537 | // Put together the full SQL statement 538 | sql += valueString; 539 | // At long last, we've got our SQL; return it 540 | return new DatabaseQuery({'sql': sql, 'values': colValues}); 541 | } 542 | 543 | /** 544 | * SQL for a very simple select 545 | * 546 | * Parameters: 547 | * - tableName (string, required) 548 | * - columns (string, array, or null): names of the columns to return 549 | * - where (object): {key: value} is equated to column: value 550 | * 551 | * Returns DatabaseQuery object 552 | */ 553 | Database.prototype.getSelect = function(tableName, columns, where) { 554 | var sql = 'SELECT '; 555 | // Setup our targeted columns 556 | var colStr = ''; 557 | if (columns === null || columns === '') { 558 | colStr = '*'; 559 | } else if (this._isArray(columns)) { 560 | // Cut down on memory needs with a straight for loop 561 | var length = columns.length; 562 | var colStr = []; 563 | for (var i = 0; i < length; i++) { 564 | colStr.push(columns[i]); 565 | } 566 | // Join the column string together with commas 567 | colStr = colStr.join(', '); 568 | } 569 | sql += colStr + ' FROM ' + tableName; 570 | // Parse the WHERE object if we have one 571 | if (typeof where !== 'undefined') { 572 | sql += ' WHERE '; 573 | var sqlValues = []; 574 | var whereStrings = []; 575 | // Loop over the where object to populate 576 | for (var key in where) { 577 | sqlValues.push(where[key]); 578 | whereStrings.push(key + ' = ?'); 579 | } 580 | // Add the WHERE strings to the sql 581 | sql += whereStrings.join(' AND '); 582 | } 583 | return new DatabaseQuery({'sql': sql, 'values': sqlValues}); 584 | } 585 | 586 | /** 587 | * SQL to update a particular row 588 | * 589 | * Parameters: 590 | * - tableName (string, required) 591 | * - data (object, required): 592 | * * key: value pairs to be updated as column: value (same format as 593 | * data objects in schema) 594 | * - where (object): key: value translated to 'column = value' 595 | * 596 | * Returns DatabaseQuery object 597 | */ 598 | Database.prototype.getUpdate = function(tableName, data, where) { 599 | var sql = 'UPDATE ' + tableName + ' SET '; 600 | var sqlValues = []; 601 | var sqlStrings = []; 602 | // Loop over data object 603 | for (var key in data) { 604 | sqlStrings.push(key + ' = ?'); 605 | sqlValues.push(data[key]); 606 | } 607 | // Collapse sqlStrings into SQL 608 | sql += sqlStrings.join(', '); 609 | // Parse the WHERE object 610 | sql += ' WHERE '; 611 | var whereStrings = []; 612 | // Loop over the where object to populate 613 | for (var key in where) { 614 | whereStrings.push(key + ' = ?'); 615 | sqlValues.push(where[key]); 616 | } 617 | // Add the WHERE strings to the sql 618 | sql += whereStrings.join(' AND '); 619 | return new DatabaseQuery({'sql': sql, 'values': sqlValues}); 620 | } 621 | 622 | /** 623 | * SQL to delete records 624 | * 625 | * Parameters: 626 | * - tableName (string, required) 627 | * - where (object, required): key: value mapped to 'column = value' 628 | * 629 | * Returns DatabaseQuery object 630 | */ 631 | Database.prototype.getDelete = function(tableName, where) { 632 | var sql = 'DELETE FROM ' + tableName + ' WHERE '; 633 | var sqlValues = []; 634 | var whereStrings = []; 635 | // Loop over the where object to populate 636 | for (var key in where) { 637 | whereStrings.push(key + ' = ?'); 638 | sqlValues.push(where[key]); 639 | } 640 | // Add the WHERE strings to the sql 641 | sql += whereStrings.join(' AND '); 642 | return new DatabaseQuery({'sql': sql, 'values': sqlValues}); 643 | } 644 | 645 | /** 646 | * SQL to create a new table 647 | * 648 | * Parameters: 649 | * - tableName (string, required) 650 | * - columns (array, required): uses syntax from setSchema (see above) 651 | * - ifNotExists (bool, defaults to true) 652 | * 653 | * Returns string, since value substitution isn't supported for this 654 | * statement in SQLite 655 | */ 656 | Database.prototype.getCreateTable = function(tableName, columns, ifNotExists) { 657 | var ifNotExists = (typeof ifNotExists !== 'undefined' ? ifNotExists : true); 658 | // Setup the basic SQL 659 | var sql = 'CREATE TABLE '; 660 | if (ifNotExists) { 661 | sql += 'IF NOT EXISTS '; 662 | } 663 | sql += tableName + ' ('; 664 | // Add the column definitions to the SQL 665 | var length = columns.length; 666 | var col = null; 667 | var colStr = []; 668 | var colDef = ''; 669 | for (var i = 0; i < length; i++) { 670 | col = columns[i]; 671 | // Construct the string for the column definition 672 | colDef = col.column + ' ' + col.type; 673 | if (col.constraints) { 674 | colDef += ' ' + col.constraints.join(' '); 675 | } 676 | // Add to SQL 677 | colStr.push(colDef); 678 | } 679 | sql += colStr.join(', ') + ')'; 680 | return sql; 681 | } 682 | 683 | /** 684 | * SQL for dropping a table 685 | * 686 | * Returns string 687 | */ 688 | Database.prototype.getDropTable = function(tableName) { 689 | return 'DROP TABLE IF EXISTS ' + tableName; 690 | } 691 | 692 | 693 | // === Private methods === 694 | 695 | /** 696 | * @protected 697 | * Sets the local tracking variable for the DB version 698 | * 699 | * PRIVATE FUNCTION; use the changeVersion* functions to modify 700 | * your database's version information 701 | */ 702 | Database.prototype._versionChanged = function(newVersion, callback) { 703 | this._dbVersion = newVersion; 704 | callback(); 705 | } 706 | 707 | /** 708 | * @protected 709 | * Merge user options into the standard set 710 | * 711 | * Parameters: 712 | * - userOptions (object, required): options passed by the user 713 | * - extraOptions (object, optional) any default options beyond onSuccess 714 | * and onError 715 | */ 716 | Database.prototype._getOptions = function(userOptions, extraOptions) { 717 | var opts = { 718 | "onSuccess": this._emptyFunction, 719 | "onError": this.bound._errorHandler 720 | }; 721 | if (typeof extraOptions !== 'undefined') { 722 | opts = this._mixin(opts, extraOptions); 723 | } 724 | if (typeof userOptions === 'undefined') { 725 | var userOptions = {}; 726 | } 727 | return this._mixin(opts, userOptions); 728 | } 729 | 730 | /** @protected */ 731 | Database.prototype._emptyFunction = function() {} 732 | 733 | /** 734 | * @protected 735 | * Used to read in external JSON files 736 | */ 737 | Database.prototype._readUrl = function(url, callback, options) { 738 | // Send our request 739 | // We cannot use a Prototype request, because Prototype injects a bunch of useless crap that fucks up Dropbox's OAuth parsing 740 | var transport = new XMLHttpRequest(); 741 | transport.open("get", url, true); 742 | transport.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); 743 | var self = this; 744 | transport.onreadystatechange = function() { 745 | // Only respond once the request is complete 746 | if (transport.readyState === 4) { 747 | // Shorten that thing up 748 | var status = transport.status; 749 | if (!status || (status >= 200 && status < 300) || status === 304) { 750 | try { 751 | var json = JSON.parse(transport.responseText); 752 | callback(json, options); 753 | } catch (e) { 754 | if (console && console.log) { 755 | console.log('JSON request error:', e); 756 | } 757 | } 758 | } else { 759 | throw new Error('Database: failed to read JSON at URL `' + url + '`'); 760 | } 761 | } 762 | }; 763 | // Launch 'er! 764 | transport.send(); 765 | } 766 | 767 | /** 768 | * @protected 769 | * Converts an SQLResultSet into a standard Javascript array of results 770 | */ 771 | Database.prototype._convertResultSet = function(rs) { 772 | var results = []; 773 | if (rs.rows) { 774 | for (var i = 0; i < rs.rows.length; i++) { 775 | results.push(rs.rows.item(i)); 776 | } 777 | } 778 | return results; 779 | } 780 | 781 | /** 782 | * @protected 783 | * Used to report generic database errors 784 | */ 785 | Database.prototype._errorHandler = function(transaction, error) { 786 | // If a transaction error (rather than an executeSQL error) there might only be one parameter 787 | if (typeof error === 'undefined') { 788 | var error = transaction; 789 | } 790 | if (console && console.log) { 791 | console.log('Database error (' + error.code + '): ' + error.message); 792 | } 793 | } 794 | 795 | /** 796 | * @protected 797 | * Used to output "database lost" error 798 | */ 799 | Database.prototype._db_lost = function() { 800 | throw new Error('Database: connection has been closed or lost; cannot execute SQL'); 801 | } 802 | 803 | /** 804 | * @protected 805 | * Detects if the variable is an array or not 806 | */ 807 | Database.prototype._isArray = function(testIt) { 808 | return Object.prototype.toString.apply(it) === '[object Array]'; 809 | } 810 | 811 | /** 812 | * @protected 813 | * Returns bound version of the function 814 | */ 815 | Database.prototype._bind = function(scope, method/*, bound arguments*/) { 816 | return function(){ return method.apply(scope, arguments || []); } 817 | } 818 | 819 | Database.prototype._mixin = function(target, source) { 820 | target = target || {}; 821 | if (source) { 822 | var name; 823 | for (name in source) { 824 | target[name] = source[name]; 825 | } 826 | } 827 | return target; 828 | } 829 | 830 | /** 831 | * DatabaseQuery (object) 832 | * 833 | * This is a helper that, at the moment, is basically just an object 834 | * with standard properties. 835 | * 836 | * Maybe down the road I'll add some helper methods for working with queries. 837 | * 838 | * USAGE: 839 | * var myQuery = new DatabaseQuery({ 840 | * sql: 'SELECT * FROM somewhere WHERE id = ?', 841 | * values: ['someID'] 842 | * }); 843 | */ 844 | DatabaseQuery = function(inProps) { 845 | this.sql = (typeof inProps.sql !== 'undefined' ? inProps.sql : ''); 846 | this.values = (typeof inProps.values !== 'undefined' ? inProps.values : []); 847 | }; 848 | -------------------------------------------------------------------------------- /samples/example_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "table": "favorite_books", 4 | "data": [ 5 | { 6 | "rowID": 0, 7 | "author": "F. Scott Fitzgerald", 8 | "title": "The Great Gatsby" 9 | }, 10 | { 11 | "rowID": 1, 12 | "author": "J.R.R. Tolkien", 13 | "title": "The Lord of the Rings" 14 | }, 15 | { 16 | "rowID": 2, 17 | "author": "Orson Scott Card", 18 | "title": "Ender's Game" 19 | } 20 | ] 21 | }, 22 | { 23 | "table": "awesome_video_games", 24 | "data": [ 25 | { "rowID": 0, "title": "Dark Forces" }, 26 | { "rowID": 1, "title": "Earthworm Jim" }, 27 | { "rowID": 2, "title": "Nethergate" } 28 | ] 29 | } 30 | ] -------------------------------------------------------------------------------- /samples/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "table": "favorite_books", 4 | "columns": [ 5 | { 6 | "column": "rowID", 7 | "type": "INTEGER", 8 | "constraints": ["PRIMARY KEY"] 9 | }, 10 | { 11 | "column": "author", 12 | "type": "TEXT" 13 | }, 14 | { 15 | "column": "title", 16 | "type": "TEXT" 17 | } 18 | ] 19 | }, 20 | { 21 | "table": "awesome_video_games", 22 | "columns": [ 23 | { 24 | "column": "rowID", 25 | "type": "INTEGER", 26 | "constraints": ["PRIMARY KEY"] 27 | }, 28 | { "column": "title", "type": "TEXT" } 29 | ] 30 | } 31 | ] --------------------------------------------------------------------------------