UNPKG

dojox

Version:

Dojo eXtensions, a rollup of many useful sub-projects and varying states of maturity – from very stable and robust, to alpha and experimental. See individual projects contain README files for details.

322 lines (315 loc) 11.8 kB
define(['dojo/_base/declare', 'dojo/Deferred', 'dojo/when', 'dojo/store/util/QueryResults', 'dojo/_base/lang', 'dojo/promise/all'], function(declare, Deferred, when, QueryResults, lang, all) { // module: // ./store/db/SQL // summary: // This module implements the Dojo object store API using the WebSQL database var wildcardRe = /(.*)\*$/; function convertExtra(object){ // converts the 'extra' data on sql rows that can contain expando properties outside of the defined column return object && lang.mixin(object, JSON.parse(object.__extra)); } return declare([], { constructor: function(config){ var dbConfig = config.dbConfig; // open the database and get it configured // args are short_name, version, display_name, and size this.database = openDatabase(config.dbName || "dojo-db", '1.0', 'dojo-db', 4*1024*1024); var indexPrefix = this.indexPrefix = config.indexPrefix || "idx_"; var storeName = config.table || config.storeName; this.table = (config.table || config.storeName).replace(/[^\w]/g, '_'); var promises = []; // for all the structural queries // the indices for this table this.indices = dbConfig.stores[storeName]; this.repeatingIndices = {}; for(var index in this.indices){ // we support multiEntry property to simulate the similar behavior in IndexedDB, we track these because we use the if(this.indices[index].multiEntry){ this.repeatingIndices[index] = true; } } if(!dbConfig.available){ // the configuration where we create any necessary tables and indices for(var storeName in dbConfig.stores){ var storeConfig = dbConfig.stores[storeName]; var table = storeName.replace(/[^\w]/g, '_'); // the __extra property contains any expando properties in JSON form var idConfig = storeConfig[this.idProperty]; var indices = ['__extra', this.idProperty + ' ' + ((idConfig && idConfig.autoIncrement) ? 'INTEGER PRIMARY KEY AUTOINCREMENT' : 'PRIMARY KEY')]; var repeatingIndices = [this.idProperty]; for(var index in storeConfig){ if(index != this.idProperty){ indices.push(index); } } promises.push(this.executeSql("CREATE TABLE IF NOT EXISTS " + table+ ' (' + indices.join(',') + ')')); for(var index in storeConfig){ if(index != this.idProperty){ if(storeConfig[index].multiEntry){ // it is "repeating" property, meaning that we expect it to have an array, and we want to index each item in the array // we will search on it using a nested select repeatingIndices.push(index); var repeatingTable = table+ '_repeating_' + index; promises.push(this.executeSql("CREATE TABLE IF NOT EXISTS " + repeatingTable + ' (id,value)')); promises.push(this.executeSql("CREATE INDEX IF NOT EXISTS idx_" + repeatingTable + '_id ON ' + repeatingTable + '(id)')); promises.push(this.executeSql("CREATE INDEX IF NOT EXISTS idx_" + repeatingTable + '_value ON ' + repeatingTable + '(value)')); }else{ promises.push(this.executeSql("ALTER TABLE " + table + ' ADD ' + index).then(null, function(){ /* suppress failed alter table statements*/ })); // otherwise, a basic index will do if(storeConfig[index].indexed !== false){ promises.push(this.executeSql("CREATE INDEX IF NOT EXISTS " + indexPrefix + table + '_' + index + ' ON ' + table + '(' + index + ')')); } } } } } dbConfig.available = all(promises); } this.available = dbConfig.available; }, idProperty: "id", selectColumns: ["*"], get: function(id){ // basic get() operation, query by id property return when(this.executeSql("SELECT " + this.selectColumns.join(",") + " FROM " + this.table + " WHERE " + this.idProperty + "=?", [id]), function(result){ return result.rows.length > 0 ? convertExtra(result.rows.item(0)) : undefined; }); }, getIdentity: function(object){ return object[this.idProperty]; }, remove: function(id){ return this.executeSql("DELETE FROM " + this.table + " WHERE " + this.idProperty + "=?", [id]); // Promise // TODO: remove from repeating rows too }, identifyGeneratedKey: true, add: function(object, directives){ // An add() wiill translate to an INSERT INTO in SQL var params = [], vals = [], cols = []; var extra = {}; var actionsWithId = []; var store = this; for(var i in object){ if(object.hasOwnProperty(i)){ if(i in this.indices || i == this.idProperty){ if(this.repeatingIndices[i]){ // we will need to add to the repeating table for the given field/column, // but it must take place after the insert, so we know the id actionsWithId.push(function(id){ var array = object[i]; return all(array.map(function(value){ return store.executeSql('INSERT INTO ' + store.table + '_repeating_' + i + ' (value, id) VALUES (?, ?)', [value, id]); })); }); }else{ // add to the columns and values for SQL statement cols.push(i); vals.push('?'); params.push(object[i]); } }else{ extra[i] = object[i]; } } } // add the "extra" expando data as well cols.push('__extra'); vals.push('?'); params.push(JSON.stringify(extra)); var idColumn = this.idProperty; if(this.identifyGeneratedKey){ params.idColumn = idColumn; } var sql = "INSERT INTO " + this.table + " (" + cols.join(',') + ") VALUES (" + vals.join(',') + ")"; return when(this.executeSql(sql, params), function(results) { var id = results.insertId; object[idColumn] = id; // got the id now, perform the insertions for the repeating data return all(actionsWithId.map(function(func){ return func(id); })).then(function(){ return id; }); }); }, put: function(object, directives){ // put, if overwrite is not specified, we have to do a get() to determine if we need to do an INSERT INTO (via add), or an UPDATE directives = directives || {}; var id = directives.id || object[this.idProperty]; var overwrite = directives.overwrite; if(overwrite === undefined){ // can't tell if we need to do an INSERT or UPDATE, do a get() to find out var store = this; return this.get(id).then(function(previous){ if((directives.overwrite = !!previous)){ directives.overwrite = true; return store.put(object, directives); }else{ return store.add(object, directives); } }); } if(!overwrite){ return store.add(object, directives); } var sql = "UPDATE " + this.table + " SET "; var params = []; var cols = []; var extra = {}; var promises = []; for(var i in object){ if(object.hasOwnProperty(i)){ if(i in this.indices || i == this.idProperty){ if(this.repeatingIndices[i]){ // update the repeating value tables this.executeSql('DELETE FROM ' + this.table + '_repeating_' + i + ' WHERE id=?', [id]); var array = object[i]; for(var j = 0; j < array.length; j++){ this.executeSql('INSERT INTO ' + this.table + '_repeating_' + i + ' (value, id) VALUES (?, ?)', [array[j], id]); } }else{ cols.push(i + "=?"); params.push(object[i]); } }else{ extra[i] = object[i]; } } } cols.push("__extra=?"); params.push(JSON.stringify(extra)); // create the SETs for the SQL sql += cols.join(',') + " WHERE " + this.idProperty + "=?"; params.push(object[this.idProperty]); return when(this.executeSql(sql, params), function(result){ return id; }); }, query: function(query, options){ options = options || {}; var from = 'FROM ' + this.table; var condition; var addedWhere; var store = this; var table = this.table; var params = []; if(query.forEach){ // a set of OR'ed conditions condition = query.map(processObjectQuery).join(') OR ('); if(condition){ condition = '(' + condition + ')'; } }else{ // regular query condition = processObjectQuery(query); } if(condition){ condition = ' WHERE ' + condition; } function processObjectQuery(query){ // we are processing an object query, that needs to be translated to WHERE conditions, AND'ed var conditions = []; for(var i in query){ var filterValue = query[i]; var convertWildcard = function convertWildcard(value){ // convert to LIKE if it ends with a * var wildcard = value && value.match && value.match(wildcardRe); if(wildcard){ params.push(wildcard[1] + '%'); return ' LIKE ?'; } params.push(value); return '=?'; }; if(filterValue){ if(filterValue.contains){ // search within the repeating table var repeatingTable = store.table + '_repeating_' + i; conditions.push(filterValue.contains.map(function(value){ return store.idProperty + ' IN (SELECT id FROM ' + repeatingTable + ' WHERE ' + 'value' + convertWildcard(value) + ')'; }).join(' AND ')); continue; }else if(typeof filterValue == 'object' && ("from" in filterValue || "to" in filterValue)){ // a range object, convert to appropriate SQL operators var fromComparator = filterValue.excludeFrom ? '>' : '>='; var toComparator = filterValue.excludeTo ? '<' : '<='; if('from' in filterValue){ params.push(filterValue.from); if('to' in filterValue){ params.push(filterValue.to); conditions.push('(' + table + '.' + i + fromComparator + '? AND ' + table + '.' + i + toComparator + '?)'); }else{ conditions.push(table + '.' + i + fromComparator + '?'); } }else{ params.push(filterValue.to); conditions.push(table + '.' + i + toComparator + '?'); } continue; } } // regular value equivalence conditions.push(table + '.' + i + convertWildcard(filterValue)); } return conditions.join(' AND '); } if(options.sort){ condition += ' ORDER BY ' + options.sort.map(function(sort){ return table + '.' + sort.attribute + ' ' + (sort.descending ? 'desc' : 'asc'); }); } var limitedCondition = condition; if(options.count){ limitedCondition += " LIMIT " + options.count; } if(options.start){ limitedCondition += " OFFSET " + options.start; } var results = lang.delegate(this.executeSql('SELECT * ' + from + limitedCondition, params).then(function(sqlResults){ // get the results back and do any conversions on it var results = []; for(var i = 0; i < sqlResults.rows.length; i++){ results.push(convertExtra(sqlResults.rows.item(i))); } return results; })); var store = this; results.total = { then: function(callback,errback){ // lazily do a total, using the same query except with a COUNT(*) and without the limits return store.executeSql('SELECT COUNT(*) ' + from + condition, params).then(function(sqlResults){ return sqlResults.rows.item(0)['COUNT(*)']; }).then(callback,errback); } }; return new QueryResults(results); }, executeSql: function(sql, parameters){ // send it off to the DB var deferred = new Deferred(); var result, error; this.database.transaction(function(transaction){ transaction.executeSql(sql, parameters, function(transaction, value){ deferred.resolve(result = value); }, function(transaction, e){ deferred.reject(error = e); }); }); // return synchronously if the data is already available. if(result){ return result; } if(error){ throw error; } return deferred.promise; } }); });