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