Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adapter for native SQLite via plugin on Cordova/Phonegap platform #221

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion doc/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ Adapters expose a consistent interface to a persistent storage implementation. A
<td>memory</td>
<td class="subdue">in memory reference implementation</td>
</tr>
<tr>
<td>cordova-native-sqlite</td>
<td class="subdue">Cordova-specific to interface with native SQLite on mobile via a plugin</td>
</tr>
</table>
</div>

Expand Down Expand Up @@ -86,4 +90,4 @@ If you require an adapter thats not listed here it is trivial to implement your
nuke (callback)


The tests ensure adapters are consistent no matter what the underlying store is. If you are writing an adapter check out `./tests/lawnchair-spec.js`. The memory adaptor is probably the simplest implementation to learn from. Note, all `Lawnchair` methods accept a callback as a last parameter. This is deliberate, most modern clientside storages only have async style interfaces, for a good reason, your code won't block the main thread aiding in the perception of performance. That callback will be scoped to the `Lawnchair` instance. Make use of `fn` and `lambda` methods to allow for terse callbacks.
The tests ensure adapters are consistent no matter what the underlying store is. If you are writing an adapter check out `./tests/lawnchair-spec.js`. The memory adaptor is probably the simplest implementation to learn from. Note, all `Lawnchair` methods accept a callback as a last parameter. This is deliberate, most modern clientside storages only have async style interfaces, for a good reason, your code won't block the main thread aiding in the perception of performance. That callback will be scoped to the `Lawnchair` instance. Make use of `fn` and `lambda` methods to allow for terse callbacks.
41 changes: 39 additions & 2 deletions lib/lawnchair.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var Lawnchair = function (options, callback) {
// default configuration
this.record = options.record || 'record' // default for records
this.name = options.name || 'records' // default name for underlying store
this.keyPath = options.keyPath || 'key' // default identifier property

// mixin first valid adapter
var adapter
Expand Down Expand Up @@ -90,8 +91,8 @@ Lawnchair.adapter = function (id, obj) {
if (indexOf(implementing, i) === -1) throw 'Invalid adapter! Nonstandard method: ' + i
}
// if we made it this far the adapter interface is valid
// insert the new adapter as the preferred adapter
Lawnchair.adapters.splice(0,0,obj)
// insert the new adapter as the preferred adapter
Lawnchair.adapters.splice(0,0,obj)
}

Lawnchair.plugins = []
Expand Down Expand Up @@ -158,6 +159,42 @@ Lawnchair.prototype = {
})
}
return this
},
keyEmbellish:function(object) {
var value=null;
if('key' in object) {value=object['key'];}
else {value=this.uuid();object['key']=value;}
return value;
},
keyExtraction:function(object, key_path) {
var value=null;
if('key' in object) {value=object['key'];}
if(!value) {value=this.keyEmbellish(object);}
return value;
},
keyIsValid:function(key_value, every_index, every_array) {
var key_is_valid = false;
if(key_value instanceof Date) {key_is_valid = !window.isNaN(key_value.getTime());}
else if(typeof( key_value) === 'number') {key_is_valid = !window.isNaN(key_value);}
else if(typeof( key_value) === 'string') {key_is_valid = true;}
else if(key_value instanceof Array) {key_is_valid = key_value.every(this.keyIsValid, key_value);}
return key_is_valid;
},
keyObjectComparator:function(key_path) {
var self = this;
return function key_object_comparator(leftObj, rightObj) {
var left_key = self.keyExtraction(leftObj);
var right_key = self.keyExtraction(rightObj);
var comparison = self.keyValueComparator()(left_key, right_key);
return comparison;
};
},
keyValueComparator:function()
{
return function key_value_comparator(left_key, right_key) {
var comparison = ((left_key < right_key)?(-1):((left_key > right_key)?(+1):(0)));
return comparison;
};
}
// --
};
Expand Down
6 changes: 5 additions & 1 deletion makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ build:
cp ./src/adapters/webkit-sqlite.js ./lib/lawnchair-adapter-webkit-sqlite-$(VERSION).js
cp ./src/adapters/html5-filesystem.js ./lib/lawnchair-adapter-html5-filesystem-$(VERSION).js
cp ./src/adapters/touchdb-couchdb.js ./lib/lawnchair-adapter-touchdb-couchdb-$(VERSION).js
cp ./src/adapters/cordova-native-sqlite.js ./lib/lawnchair-adapter-cordova-native-sqlite-$(VERSION).js

min:
java -jar ./util/compiler.jar --js ./lib/lawnchair-$(VERSION).js > ./lib/lawnchair-$(VERSION).min.js
Expand All @@ -59,7 +60,10 @@ test:
cp ./src/adapters/indexed-db.js ./test/lib/lawnchair-adapter-indexed-db.js
cp ./src/adapters/webkit-sqlite.js ./test/lib/lawnchair-adapter-webkit-sqlite.js
cp ./src/adapters/html5-filesystem.js ./test/lib/lawnchair-adapter-html5-filesystem.js
cp ./src/adapters/touchdb-couchdb.js ./test/lib/lawnchair-adapter-touchdb-couchdb.js
cp ./src/adapters/touchdb-couchdb.js ./test/lib/lawnchair-adapter-touchdb-couchdb.js

#will not pass qunit tests as dependent on cordova/native mobile env
#cp ./src/adapters/cordova-native-sqlite.js ./test/lib/lawnchair-adapter-cordova-native-sqlite.js

open ./test/index.html
#open ./test/plugin/aggregation.html
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"license": "MIT",
"description": "simple json storage",
"author": "Brian Leroux",
"version": "0.6.4",
"version": "0.6.5",
"repository": {
"type": "git",
"url": "https://github.com/brianleroux/lawnchair.git"
Expand Down
225 changes: 225 additions & 0 deletions src/adapters/cordova-native-sqlite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
Lawnchair.adapter('cordova-native-sqlite', (function () {
// private methods
var fail = function (e, i) { console.error('error in sqlite adaptor!', e, i) }
, now = function () { return new Date() } // FIXME need to use better date fn
// not entirely sure if this is needed...
if (!Function.prototype.bind) {
Function.prototype.bind = function( obj ) {
var slice = [].slice
, args = slice.call(arguments, 1)
, self = this
, nop = function () {}
, bound = function () {
return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments)))
}
nop.prototype = self.prototype
bound.prototype = new nop()
return bound
}
}

function stringifyKey(row_key){
return ((typeof(row_key)==='string')?(row_key):(JSON.stringify(row_key)));
}

// public methods
return {

valid: function() {
if(!window.cordova){
console.error("Cordova environment not found!");
return false;
}
if(!window.sqlitePlugin){
// Plugin init is async, so cannot determine it's missing now, but will cause error if missing on Lawnchair.init()
console.warn("cordova-sqlite-storage plugin not found during 'cordova-native-sqlite' adapter bootstrap");
}
return true;
},

init: function (options, callback) {
function onError(err){
console.error('Open database ERROR: ' + JSON.stringify(err));
if(options.error){
options.error(err);
}
}
if(!window.sqlitePlugin){
onError("cordova-sqlite-storage plugin not found during Lawnchair.init()! Make sure plugin is installed");
return;
}
var that = this;
// Wait for Cordova to load
document.addEventListener("deviceready", function(){
var cb = that.fn(that.name, callback)
, create = "CREATE TABLE IF NOT EXISTS " + that.record + " (id NVARCHAR(32) UNIQUE PRIMARY KEY, value TEXT, timestamp REAL)"
, win = function(){ if(cb) return cb.call(that, that); }

if (cb && typeof cb != 'function') throw 'callback not valid';

// open a connection and create the db if it doesn't exist
that.db = window.sqlitePlugin.openDatabase({name: that.name+".db", location: options.location},
function(){
that.db.transaction(function (t) {
t.executeSql(create, [])
}, fail, win);
}, onError);
}, false);
},

keys: function (callback) {
var cb = this.lambda(callback)
, that = this
, keys = "SELECT id FROM " + this.record + " ORDER BY timestamp DESC"

this.db.readTransaction(function(t) {
var win = function (xxx, results) {
if (results.rows.length == 0 ) {
cb.call(that, [])
} else {
var r = [];
for (var i = 0, l = results.rows.length; i < l; i++) {
var row_key = results.rows.item(i).id;
try{row_key = JSON.parse(row_key);}catch(exc){}
r.push(row_key);
}
cb.call(that, r)
}
}
t.executeSql(keys, [], win, fail)
})
return this
},
// you think thats air you're breathing now?
save: function (obj, callback, error) {
var that = this
, objs = (this.isArray(obj) ? obj : [obj]).map(function(o){var row_key = that.keyEmbellish(o); return o})
, ins = "INSERT OR REPLACE INTO " + this.record + " (value, timestamp, id) VALUES (?,?,?)"
, win = function () { if (callback) { that.lambda(callback).call(that, that.isArray(obj)?objs:objs[0]) }}
, error= error || function() {}
, insvals = []
, ts = now()

try {
for (var i = 0, l = objs.length; i < l; i++) {
insvals[i] = [JSON.stringify(objs[i]), ts, stringifyKey(that.keyExtraction(objs[i]))];
}
} catch (e) {
fail(e)
throw e;
}

that.db.transaction(function(t) {
for (var i = 0, l = objs.length; i < l; i++)
t.executeSql(ins, insvals[i])
}, function(e,i){fail(e,i)}, win)

return this
},


batch: function (objs, callback) {
return this.save(objs, callback)
},

get: function (keyOrArray, cb) {
var that = this
, sql = ''
, args = (this.isArray(keyOrArray) ? keyOrArray : [keyOrArray])
.map(function(row_key) {
return stringifyKey(row_key);
});
// batch selects support
sql = 'SELECT id, value FROM ' + this.record + " WHERE id IN (" +
args.map(function(){return '?'}).join(",") + ")"
// FIXME
// will always loop the results but cleans it up if not a batch return at the end..
// in other words, this could be faster
var win = function (xxx, results) {
var o
, r
, lookup = {}
// map from results to keys
for (var i = 0, l = results.rows.length; i < l; i++) {
var rowObj = results.rows.item(i);
o = JSON.parse(rowObj.value)
var row_key = rowObj.id;
lookup[row_key] = o;
try{row_key = JSON.parse(row_key);}catch(exc){}
//o.key = row_key;
o.timestamp = rowObj.timestamp;
}
r = args.map(function(row_key) {return lookup[row_key];});
if (!that.isArray(keyOrArray)) r = r.length ? r[0] : null
if (cb) that.lambda(cb).call(that, r)
}
this.db.readTransaction(function(t){ t.executeSql(sql, args, win, fail) })
return this
},

exists: function (row_key, cb) {
var is = "SELECT * FROM " + this.record + " WHERE id = ?"
, that = this
, win = function(xxx, results) { if (cb) that.fn('exists', cb).call(that, (results.rows.length > 0)) }
this.db.readTransaction(function(t){ t.executeSql(is, [stringifyKey(row_key)], win, fail) })
return this
},

all: function (callback) {
var that = this
, all = "SELECT * FROM " + this.record
, r = []
, cb = this.fn(this.name, callback) || undefined
, win = function (xxx, results) {
if (results.rows.length != 0) {
for (var i = 0, l = results.rows.length; i < l; i++) {
var obj = JSON.parse(results.rows.item(i).value)
//obj.key = results.rows.item(i).id
r.push(obj)
}
}
if (cb) cb.call(that, r)
}

this.db.readTransaction(function (t) {
t.executeSql(all, [], win, fail)
})
return this
},

remove: function (keyOrArray, cb) {
var that = this
, args
, sql = "DELETE FROM " + this.record + " WHERE id "
, win = function () { if (cb) that.lambda(cb).call(that) }
if (!this.isArray(keyOrArray)) {
sql += '= ?';
args = [keyOrArray];
} else {
args = keyOrArray;
sql += "IN (" +
args.map(function(){return '?'}).join(',') +
")";
}
args = args.map(function(obj) {
return stringifyKey(typeof(obj) !== "string" ? that.keyExtraction(obj) : obj)
});

this.db.transaction( function (t) {
t.executeSql(sql, args, win, fail);
});

return this;
},

nuke: function (cb) {
var nuke = "DELETE FROM " + this.record
, that = this
, win = cb ? function() { that.lambda(cb).call(that) } : function(){}
this.db.transaction(function (t) {
t.executeSql(nuke, [], win, fail)
})
return this
}
//////
}})());