nativescript-toolbox-sw
Version:
Fork of nativescript-toolbox - A NativeScript module that is a composition of useful classes, tools and helpers.
765 lines (686 loc) • 24.9 kB
JavaScript
/************************************************************************************
* (c) 2015, 2016 Master Technology
* Licensed under the MIT license or contact me for a support, changes, enhancements,
* and/or if you require a commercial licensing
*
* Any questions please feel free to email me or put a issue up on github
* Version 0.1.1 - iOS Nathan@master-technology.com
***********************************************************************************/
"use strict";
var fs = require('file-system');
/* jshint undef: true, camelcase: false */
/* global Promise, NSFileManager, NSBundle, NSString, interop, sqlite3_open_v2, sqlite3_close, sqlite3_prepare_v2, sqlite3_step,
sqlite3_finalize, sqlite3_bind_null, sqlite3_bind_text, sqlite3_column_type, sqlite3_column_int,
sqlite3_column_double, sqlite3_column_text, sqlite3_column_count, sqlite3_column_name */
/***
* Creates a Cursor Tracking Statement for reading result sets
* @param statement
* @param resultType
* @param valuesType
* @constructor
*/
function CursorStatement(statement, resultType, valuesType) {
this.statement = statement;
this.resultType = resultType;
this.valuesType = valuesType;
this.built = false;
this.columns = [];
}
//noinspection JSValidateJSDoc
/***
* Database Constructor
* @param dbname - Database Name
* @param options - options
* @param callback - Callback when Done
* @returns {Promise} object
* @constructor
*/
function Database(dbname, options, callback) {
if (!this instanceof Database) { // jshint ignore:line
//noinspection JSValidateTypes
return new Database(dbname, options, callback);
}
this._isOpen = false;
this._resultType = Database.RESULTSASARRAY;
this._valuesType = Database.VALUESARENATIVE;
if (typeof options === 'function') {
callback = options;
options = {};
} else {
options = options || {};
}
// Check to see if it has a path, or if it is a relative dbname
// DBNAME = "" - is a Temporary Database
// DBNAME = ":memory:" - is a Memory only database
if (dbname !== "" && dbname !== ":memory:") {
var path;
if (dbname.indexOf('/') === -1) {
path = fs.knownFolders.documents().path;
dbname = path + '/' + dbname;
} else {
path = dbname.substr(0, dbname.lastIndexOf('/') + 1);
}
// Create "databases" folder if it is missing. This causes issues on Emulators if it is missing
// So we create it if it is missing
try {
if (!fs.File.exists(path)) {
//noinspection JSUnresolvedFunction
var fileManager = NSFileManager.defaultManager();
if (!fileManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError(path, true, null, null)) {
console.warn("SQLITE.CONSTRUCTOR - Creating DB Folder Error");
}
}
}
catch (err) {
console.warn("SQLITE.CONSTRUCTOR - Creating DB Folder Error", err);
}
}
var self = this;
//noinspection JSUnresolvedFunction
return new Promise(function (resolve, reject) {
var error;
try {
self._db = new interop.Reference();
// SQLITE_OPEN_FULLMUTEX = 65536, SQLITE_OPEN_CREATE = 4, SQLITE_OPEN_READWRITE = 2 --- 4 | 2 | 65536 = 65542
if (options && options.readOnly) {
error = sqlite3_open_v2(dbname, self._db, 65536, null);
} else {
error = sqlite3_open_v2(dbname, self._db, 65542, null);
}
self._db = self._db.value;
} catch (err) {
if (callback) { callback(err, null); }
reject(err);
return;
}
if (error) {
if (callback) { callback(error, null); }
reject(error);
return;
}
self._isOpen = true;
if (callback) { callback(null, self); }
resolve(self);
});
}
/***
* Constant that this structure is a sqlite structure
* @type {boolean}
*/
Database.prototype._isSqlite = true;
/***
* This gets or sets the database version
* @param valueOrCallback to set or callback(err, version)
* @returns Promise
*/
Database.prototype.version = function(valueOrCallback) {
if (typeof valueOrCallback === 'function') {
return this.get('PRAGMA user_version', function (err, data) {
valueOrCallback(err, data && data[0]);
}, Database.RESULTSASARRAY);
} else if (!isNaN(valueOrCallback+0)) {
return this.execSQL('PRAGMA user_version='+(valueOrCallback+0).toString());
} else {
return this.get('PRAGMA user_version', Database.RESULTSASARRAY);
}
};
/***
* Is the database currently open
* @returns {boolean} - true if the db is open
*/
Database.prototype.isOpen = function() {
return this._isOpen;
};
/***
* Gets/Sets whether you get Arrays or Objects for the row values
* @param value - Database.RESULTSASARRAY or Database.RESULTSASOBJECT
* @returns {number} - Database.RESULTSASARRAY or Database.RESULTSASOBJECT
*/
Database.prototype.resultType = function(value) {
if (value === Database.RESULTSASARRAY) {
this._resultType = Database.RESULTSASARRAY;
} else if (value === Database.RESULTSASOBJECT) {
this._resultType = Database.RESULTSASOBJECT;
}
return this._resultType;
};
/***
* Gets/Sets whether you get Native or Strings for the row values
* @param value - Database.VALUESARENATIVE or Database.VALUESARESTRINGS
* @returns {number} - Database.VALUESARENATIVE or Database.VALUESARESTRINGS
*/
Database.prototype.valueType = function(value) {
if (value === Database.VALUESARENATIVE) {
this._valuesType = Database.VALUESARENATIVE;
} else if (value === Database.VALUESARESTRINGS) {
this._valuesType = Database.VALUESARESTRINGS;
}
return this._resultType;
};
/***
* Closes this database, any queries after this will fail with an error
* @param callback
* @returns Promise
*/
Database.prototype.close = function(callback) {
var self = this;
return new Promise( function (resolve, reject) {
if (!self._isOpen) {
if (callback) {
callback('SQLITE.CLOSE - Database is already closed');
}
reject('SQLITE.CLOSE - Database is already closed');
return;
}
sqlite3_close(self._db);
self._db = null;
self._isOpen = false;
if (callback) {
callback(null, null);
}
resolve();
});
};
/***
* Exec SQL
* @param sql - sql to use
* @param params - optional array of parameters
* @param callback - (err, result) - can be last_row_id for insert, and rows affected for update/delete
* @returns Promise
*/
Database.prototype.execSQL = function(sql, params, callback) {
if (typeof params === 'function') {
callback = params;
params = undefined;
}
var self = this;
return new Promise( function(resolve, reject) {
var hasCallback = true;
if (typeof callback !== 'function') {
callback = reject;
hasCallback = false;
}
if (!self._isOpen) {
callback("SQLITE.EXECSQL - Database is not open");
return;
}
// Need to see if we have to run any status queries afterwords
var flags = 0;
var test = sql.trim().substr(0, 7).toLowerCase();
if (test === 'insert ') {
flags = 1;
} else if (test === 'update ' || test === 'delete ') {
flags = 2;
}
var res;
try {
var statement = new interop.Reference();
res = sqlite3_prepare_v2(self._db, sql, -1, statement, null);
statement = statement.value;
if (res) {
callback("SQLITE.ExecSQL Failed Prepare: " + res);
return;
}
if (params !== undefined) {
if (!self._bind(statement, params)) {
callback("SQLITE.ExecSQL Bind Error");
return;
}
}
var result = sqlite3_step(statement);
sqlite3_finalize(statement);
if (result && result !== 100 && result !== 101) {
callback("SQLITE.ExecSQL Failed " + res);
return;
}
} catch (Err) {
callback(Err, null);
return;
}
switch (flags) {
case 0:
if (hasCallback) {
callback();
}
resolve();
break;
case 1:
self.get('select last_insert_rowid()', function (err, data) {
if (hasCallback) {
callback(err, data && data[0]);
}
if (err) {
reject(err);
} else {
resolve(data && data[0]);
}
}, Database.RESULTSASARRAY | Database.VALUESARENATIVE);
break;
case 2:
self.get('select changes()', function (err, data) {
if (hasCallback) {
callback(err, data && data[0]);
}
if (err) {
reject(err);
} else {
resolve(data && data[0]);
}
}, Database.RESULTSASARRAY | Database.VALUESARENATIVE);
break;
default:
if (hasCallback) {
callback();
}
resolve();
}
});
};
/***
* Get the first record result set
* @param sql - sql to run
* @param params - optional
* @param callback - callback (error, results)
* @param mode - allows you to manually override the results set to be a array or object
* @returns Promise
*/
Database.prototype.get = function(sql, params, callback, mode) {
if (typeof params === 'function') {
mode = callback;
callback = params;
params = undefined;
}
var self = this;
return new Promise( function (resolve, reject) {
var hasCallback = true;
if (typeof callback !== 'function') {
callback = reject;
hasCallback = false;
}
if (!self._isOpen) {
callback("SQLITE.GET - Database is not open");
return;
}
var cursor;
try {
var statement = new interop.Reference();
var res = sqlite3_prepare_v2(self._db, sql, -1, statement, null);
var cursorStatement = new CursorStatement(statement.value, self._resultType, self._valuesType);
statement = statement.value;
if (res) {
callback("SQLITE.GET Failed Prepare: " + res);
return;
}
if (params !== undefined) {
if (!self._bind(statement, params)) {
callback("SQLITE.GET Bind Error");
return;
}
}
var result = sqlite3_step(statement);
if (result === 100) {
cursor = self._getResults(cursorStatement, mode);
}
sqlite3_finalize(statement);
if (result && result !== 100 && result !== 101) {
callback("SQLITE.GET - Step Error" + result);
return;
}
} catch (err) {
callback(err);
return;
}
// No Records
if (!cursor) {
if (hasCallback) {
callback(null, null);
}
resolve(null);
return;
}
if (hasCallback) {
callback(null, cursor);
}
resolve(cursor);
});
};
/***
* This returns the entire result set in a array of rows
* @param sql - Sql to run
* @param params - optional
* @param callback - (err, results)
* @returns Promise
*/
Database.prototype.all = function(sql, params, callback) {
if (typeof params === 'function') {
callback = params;
params = undefined;
}
var self = this;
return new Promise(function(resolve, reject) {
var hasCallback = true;
if (typeof callback !== 'function') {
callback = reject;
hasCallback = false;
}
if (!self._isOpen) {
callback("SQLITE.ALL - Database is not open");
return;
}
var rows = [], res;
try {
var statement = new interop.Reference();
res = sqlite3_prepare_v2(self._db, sql, -1, statement, null);
var cursorStatement = new CursorStatement(statement.value, self._resultType, self._valuesType);
statement = statement.value;
if (res) {
callback("SQLITE.ALL - Prepare Error " + res);
return;
}
if (params !== undefined) {
if (!self._bind(statement, params)) {
callback("SQLITE.ALL Bind Error");
return;
}
}
var result;
do {
result = sqlite3_step(statement);
if (result === 100) {
var cursor = self._getResults(cursorStatement);
if (cursor) {
rows.push(cursor);
}
} else if (result && result !== 101) {
sqlite3_finalize(statement);
callback("SQLITE.ALL - Database Error" + result);
return;
}
} while (result === 100);
sqlite3_finalize(statement);
} catch (err) {
callback(err, null);
return;
}
// No Records
if (rows.length === 0) {
if (hasCallback) {
callback(null, []);
}
resolve([]);
return;
}
if (hasCallback) {
callback(null, rows);
}
resolve(rows);
});
};
/***
* This sends each row of the result set to the "Callback" and at the end calls the complete callback upon completion
* @param sql - sql to run
* @param params - optional
* @param callback - callback (err, rowsResult)
* @param complete - callback (err, recordCount)
* @returns {Promise}
*/
Database.prototype.each = function(sql, params, callback, complete) {
if (typeof params === 'function') {
complete = callback;
callback = params;
params = undefined;
}
// Callback is required
if (typeof callback !== 'function') {
throw new Error("SQLITE.EACH - requires a callback");
}
var self = this;
return new Promise (function(resolve, reject) {
// Set the error Callback
var errorCB = complete || callback;
var count = 0, res;
try {
var statement = new interop.Reference();
res = sqlite3_prepare_v2(self._db, sql, -1, statement, null);
var cursorStatement = new CursorStatement(statement.value, self._resultType, self._valuesType);
statement = statement.value;
if (res) {
errorCB("SQLITE.EACH Error in Prepare" + res);
reject("SQLITE.EACH Error in Prepare" + res);
return;
}
if (params !== undefined) {
if (!self._bind(statement, params)) {
errorCB("SQLITE.EACH Bind Error");
reject("SQLITE.EACH Bind Error");
return;
}
}
var result;
do {
result = sqlite3_step(statement);
if (result === 100) {
var cursor = self._getResults(cursorStatement);
if (cursor) {
count++;
callback(null, cursor);
}
} else if (result && result !== 101) {
sqlite3_finalize(statement);
errorCB("SQLITE.EACH - Database Error " + result);
reject("SQLITE.EACH - Database Error " + result);
return;
}
} while (result === 100);
sqlite3_finalize(statement);
} catch (err) {
errorCB(err, null);
reject(err);
return;
}
if (complete) {
complete(null, count);
}
resolve(count);
});
};
/**
* Binds the Parameters in a Statement
* @param statement
* @param params
* @private
*/
Database.prototype._bind = function(statement, params) {
var param, res;
if (Array.isArray(params)) {
var count = params.length;
for (var i=0; i<count; ++i) {
if (params[i] == null) { // jshint ignore:line
res = sqlite3_bind_null(statement, i+1);
} else {
param = params[i].toString();
res = sqlite3_bind_text(statement, i+1, param, -1, null );
}
if (res) {
console.error("SQLITE.Binding Error ", res);
return false;
}
}
} else {
if (params == null) { // jshint ignore:line
res = sqlite3_bind_null(statement, 1);
} else {
param = params.toString();
res = sqlite3_bind_text(statement, 1, param, -1, null );
}
if (res) {
console.error("SQLITE.Binding Error ", res);
return false;
}
}
return true;
};
Database.prototype._getNativeResult = function(statement, column) {
var resultType = sqlite3_column_type(statement, column);
switch (resultType) {
case 1: // Int
return sqlite3_column_int(statement, column);
case 2: // Float
return sqlite3_column_double(statement, column);
case 3: // Text
//noinspection JSUnresolvedFunction
return NSString.stringWithUTF8String(sqlite3_column_text(statement, column)).toString();
case 4: // Blob
return null; // TODO: We don't currently support Blobs on iOS
case 5: // Null
return null;
default:
//noinspection JSUnresolvedFunction
return NSString.stringWithUTF8String(sqlite3_column_text(statement, column)).toString();
}
};
Database.prototype._getStringResult = function(statement, column) {
var resultType = sqlite3_column_type(statement, column);
switch (resultType) {
case 1: // Int
//return sqlite3_column_int(statement, column).toString();
return NSString.stringWithUTF8String(sqlite3_column_text(statement, column)).toString();
case 2: // Float
//return sqlite3_column_double(statement, column).toString();
return NSString.stringWithUTF8String(sqlite3_column_text(statement, column)).toString();
case 3: // Text
//noinspection JSUnresolvedFunction
return NSString.stringWithUTF8String(sqlite3_column_text(statement, column)).toString();
case 4: // Blob
return null; // TODO: We don't currently support Blobs on iOS
case 5: // Null
return null;
default:
//noinspection JSUnresolvedFunction
return NSString.stringWithUTF8String(sqlite3_column_text(statement, column)).toString();
}
};
Database.prototype._getResults = function(cursorStatement, mode) {
var resultType, valueType;
var statement = cursorStatement.statement;
var i;
if (!mode) {
resultType = cursorStatement.resultType;
valueType = cursorStatement.valuesType;
} else {
resultType = (mode & (Database.RESULTSASARRAY | Database.RESULTSASOBJECT));
valueType = (mode & (Database.VALUESARENATIVE | Database.VALUESARESTRINGS));
if (resultType <= 0) {
resultType = cursorStatement.resultType;
}
if (valueType <= 0) {
valueType = cursorStatement.valuesType;
}
}
// Track this statements information so we don't have to build it each time
if (!cursorStatement.built) {
cursorStatement.count = sqlite3_column_count(statement);
if (resultType === Database.RESULTSASOBJECT) {
for (i=0;i<cursorStatement.count;i++) {
//noinspection JSUnresolvedFunction
var cn = NSString.stringWithUTF8String(sqlite3_column_name(statement, i)).toString();
if (!cn || cursorStatement.columns.indexOf(cn) >= 0) {
cn = "column"+i;
}
cursorStatement.columns.push(cn);
}
}
cursorStatement.built=true;
}
var cnt = cursorStatement.count, data;
if (cnt === 0) { return null; }
if (resultType === Database.RESULTSASARRAY) {
data = [];
if (valueType === Database.VALUESARESTRINGS) {
for (i = 0; i < cnt; i++) {
data.push(this._getStringResult(statement, i));
}
} else {
for (i = 0; i < cnt; i++) {
data.push(this._getNativeResult(statement, i));
}
}
return data;
} else {
var colName = cursorStatement.columns;
data = {};
if (valueType === Database.VALUESARESTRINGS) {
for (i = 0; i < cnt; i++) {
data[colName[i]] = this._getStringResult(statement, i);
}
} else {
for (i = 0; i < cnt; i++) {
data[colName[i]] = this._getNativeResult(statement, i);
}
}
return data;
}
};
/***
* Is this a SQLite object
* @param obj - possible sqlite object to check
* @returns {boolean}
*/
Database.isSqlite = function(obj) {
return obj && obj._isSqlite;
};
Database.exists = function(name) {
if (name.indexOf('/') === -1) {
name = fs.knownFolders.documents().path + '/' + name;
}
//noinspection JSUnresolvedFunction
var fileManager = NSFileManager.defaultManager();
return fileManager.fileExistsAtPath(name);
};
Database.deleteDatabase = function(name) {
//noinspection JSUnresolvedFunction
var fileManager = NSFileManager.defaultManager();
var path;
if (name.indexOf('/') === -1) {
path = fs.knownFolders.documents().path + '/';
} else {
path = name.substr(0, name.lastIndexOf('/') + 1);
name = name.substr(path.length);
}
if (!fileManager.fileExistsAtPath(path + name)) { return; }
// Need to remove the trailing .sqlite
var idx = name.lastIndexOf('.');
if (idx) {
name = name.substr(0,idx);
}
var files = fileManager.contentsOfDirectoryAtPathError(path, null);
if (!files) {
return;
}
for (var i = 0; i < files.count; i++) {
var fileName = files.objectAtIndex(i);
if (fileName.indexOf(name) !== -1) {
fileManager.removeItemAtPathError(path + fileName, null);
}
}
};
Database.copyDatabase = function(name) {
//noinspection JSUnresolvedFunction
var fileManager = NSFileManager.defaultManager();
var path;
if (name.indexOf('/') === -1) {
path = fs.knownFolders.documents().path + '/';
} else {
path = name.substr(0, name.lastIndexOf('/') + 1);
name = name.substr(path.length);
}
var source = fs.knownFolders.currentApp().path + '/' + name;
var destination = path + name;
fileManager.copyItemAtPathToPathError(source, destination, null);
};
// Literal Defines
Database.RESULTSASARRAY = 1;
Database.RESULTSASOBJECT = 2;
Database.VALUESARENATIVE = 4;
Database.VALUESARESTRINGS = 8;
module.exports = Database;