You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
321 lines
12 KiB
321 lines
12 KiB
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; |
|
} |
|
|
|
}); |
|
});
|
|
|