/// import DbUtil = require("./DbUtil"); declare var window: any; type SqlStatementType = WebSqlSpi.SqlStatementType; interface SqlQuery extends WebSqlSpi.SqlQuery { } interface SqlTableInfo extends WebSqlSpi.SqlTableInfo { } /*! websql.js | MIT license | Stepan Riha | http://bitbucket.org/nonplus/websql-js * websql.js may be freely distributed under the MIT license. * converted to TypeScript at 2017-11-04 by TeamworkGuy2 */ /** Module that wraps asynchronous WebSQL calls with deferred promises and provides SQL utility methods. * * Promises are **resolved** when asynchronous database callback is finished. * Promises are **rejected** with an `Error` object that may contain one or more of the following: * - `message`: Describing what failed * - `exception`: Exception that was thrown * - `sqlError`: Error returned by WebSQL * - `sql`: statement that was executing * * ## Using the API * Example: * var wsdb = WebSqlSpi.newWebSqlDb(nameOrDbInst, _version_, _displayName_, _estimatedSize_, utilSettings); * wsdb.read({ sql: "SELECT * FROM ..." }).then(function(resultSet) { ... }); * * ## Public Methods ## * - `newWebSqlDb(nameOrDb, ...)` takes the same parameters as the `window.openDatabase` function, and used default values for unspecified parameters. * Returns: new a promise which resolves with the new `WebsqlDatabase` wrapper class. * Usage: * var wsdb = WebSqlSpi.newWebSqlDb("test", 1, "Test Database", 2 * 1024 * 1024, new DbUtil(...)); * wsdb.execute({ sql: "INSERT INTO ...", args: [...] }).then(function(resultSet) { ... }) */ class WebSqlSpi { public db!: Database; public util!: DbUtil; constructor(db: Database, util: DbUtil) { this.db = db; this.util = util; this.transaction = this.transaction.bind(this); this.readTransaction = this.readTransaction.bind(this); } /** Returns: promise that resolves once the database version has been changed * Usage: * wsdb.changeVersion(1, 2, function (xact) { * xact.executeSQL(...); * }).then(function() {...}); */ public changeVersion(oldVersion: number, newVersion: number, xactCallback: SQLTransactionCallback): PsPromise { var util = this.util; var dfd = util.defer(); if (!util.isDatabase(this.db)) { util.rejectError(dfd, "Database not specified (db='" + this.db + "')"); return dfd.promise; } util.log(util.DEBUG, "changeVersion", oldVersion, newVersion); try { this.db.changeVersion("" + oldVersion, "" + newVersion, xactCallback, function (sqlError) { util.rejectError(dfd, "Failed to change version", { sqlError: sqlError }); }, function () { dfd.resolve(null); }); } catch (ex) { util.rejectError(dfd, "Failed changeVersion(db, '" + oldVersion + "', '" + newVersion + "')", { exception: ex }); } return dfd.promise; } /** Queries the sqlite_master table for user tables * Returns: promise that resolves with an array of table information records * Usage: * wsdb.getTables().then(function(tables) { * for(var i = 0; i < tables.length; i++) { * var name = tables[i].name; * var sql = tables[i].sql; * ... * } * }); */ public getTables(): PsPromise { var sql = "SELECT name, type, sql FROM sqlite_master " + "WHERE type in ('table') AND name NOT LIKE '?_?_%' ESCAPE '?'"; return this.execSqlStatements(this.readTransaction, "read", { sql }, function (rs) { var tables: SqlTableInfo[] = []; var rows = rs.rows; for (var i = 0, size = rows.length; i < size; i++) { tables.push(rows.item(i)); } return tables; }); } /** Queries the sqlite_master for a table by name * Returns: promise that resolves with table info or with `undefined` if table * does not exist. * Usage: * wsdb.tableExists("person").then(function (table) { * alert("table " + (table ? "exists" : "does not exist")); * }); */ public tableExists(name: string): PsPromise { var sql = "SELECT * FROM sqlite_master WHERE name = ?"; return this.readRow([{ sql, args: [[name]] }], function (row) { return row || undefined; }); } /** Drops all the tables in the database. * Returns: promise that resolves with this `WebsqlDatabase` * Usage: * wsdb.destroyDatabase() * .then(function (wsdb) {...}); */ public destroyDatabase(): PsPromise { return this.changeVersion(this.db.version, "", function (xact) { var sql = "SELECT name FROM sqlite_master " + "WHERE type in ('table') AND name NOT LIKE '?_?_%' ESCAPE '?'"; xact.executeSql(sql, [], function (xact, rs) { var rows = rs.rows; for (var i = 0, size = rows.length; i < size; i++) { var sql = 'DROP TABLE "' + rows.item(i).name + '"'; xact.executeSql(sql); } }); }); } /** Calls xactCallback(xact) from within a database transaction * Returns: promise that resolves with the database * Usage: * wsdb.transaction(function (xact) { * xact.executeSQL(...); * }).then(function (wsdb) {...}); * * More usage: * var addressId; * var personId; * * function insertPerson(xact) { * return xact.executeSql("INSERT INTO person ...", [...], * function (xact, rs) { * personId = rs.insertId; * insertAddress(xact, personId); * } * ) * } * * function insertAddress(xact, personId) { * return wsdb.executeSql(xact, "INSERT INTO address (person, ...) VALUES (?, ...)", * [personId, ...], * function (xact, rs) { * addressId = rs.insertId; * } * ) * } * * wsdb.transaction(function (xact) { * insertPerson(xact); * }).then(function(wsdb) { * alert("Created person " + personId + " with address " + addressId); * }); */ public transaction(xactCallback: SQLTransactionCallback) { return this.executeTransaction("transaction", xactCallback); } /** Calls xactCallback(xact) from within a database read transaction * Returns: promise that resolves with the database * Usage: * wsdb.readTransaction(function (xact) { * xact.executeSQL(...); * }).then(function (wsdb) {...}); */ public readTransaction(xactCallback: SQLTransactionCallback) { return this.executeTransaction("readTransaction", xactCallback); } /** Call 'webSqlFunc' method on 'db' * Implements common behavior for 'wsdb.transaction' and 'wsdb.readTransaction' */ public executeTransaction(webSqlFuncName: ("transaction" | "readTransaction"), xactCallback: SQLTransactionCallback): PsPromise { var util = this.util; var dfd = util.defer(); if (!util.isDatabase(this.db)) { util.rejectError(dfd, "Database not specified (db='" + this.db + "')"); return dfd.promise; } if (this.db[webSqlFuncName] == null) { util.rejectError(dfd, "Database function '" + webSqlFuncName + "' does not exist"); return dfd.promise; } try { this.db[webSqlFuncName](function (xact) { try { xactCallback(xact); } catch (exception) { util.rejectError(dfd, webSqlFuncName + " callback threw an exception", { exception: exception }); } }, function (sqlError) { util.rejectError(dfd, "Failed executing " + webSqlFuncName.replace(/transaction/i, "") + " transaction", { sqlError: sqlError }); }, function () { dfd.resolve(null); }); } catch (exception) { util.rejectError(dfd, "Failed calling " + webSqlFuncName, { exception: exception }); } return dfd.promise; } /** Method for executing a transaction with a one or more `sqlStatement` * with the specified `args`, calling the `rsCallback` with the result set(s). * The `args` and `rsCallback` are optional. * * Passing a _single_ `sqlStatement` string with `args` that is an _array of arrays_, * the statement is executed with each row in the `args`. * Passing an array of `{ sql, args}` objects to `sqlStatement` * executes the `sql` in each row with the row's `args` (or the parameter `args`). * * Returns: promise that resolves with `rsCallback` result * or the resultSet, if no `rsCallback` specified. If an array of statements or arguments * is specified, the promise resolves with an array of results/resultSets. * * Basic Usage: * wsdb.execute("DELETE FROM person") * .then(function (resultSet) {...}); * * Other Usage: (single `sqlStatement` with multiple sets of `args`) * wsdb.execute("INSERT INTO person (first, last) VALUES (?, ?)", * [ * ["John", "Doe"], * ["Jane", "Doe"] * ], * // called for each row in args * function (rs) { * console.log("Inserted person", rs.insertId); * return rs.insertId; * } * ).then(function (insertIds) { * var personId1 = insertIds[0], personId2 = insertIds[1]; * ... * }); * * Other Usage: (multiple `sqlStatement` with multiple sets of `args`) * wsdb.execute( * [{ * sql: "UPDATE person SET (first=?, last=?) WHERE id=?", * args: ["Robert", "Smith", 23] * }, { * sql: "UPDATE address SET (street=?, city=?, zip=?) WHERE id=?", * args: ["Sesame St.", "Austin", "78758", 45] * }], * // called for each object in args * function (rs) { * console.log("Updated object: ", rs.rowsAffected); * return rs.rowsAffected; * } * ).then(function (results) { * var numPersons = results[0], numAddresses = results[1]; * ... * }); */ public executeQuery(sqlStatement: SqlQuery): PsPromise { return this.execSqlStatements(this.transaction, "execute", sqlStatement, null); } public executeQueries(sqlStatements: SqlQuery[]): PsPromise { return this.execSqlStatements(this.transaction, "execute", sqlStatements, null); } public execute(sqlStatements: SqlQuery | SqlQuery[], rsCallback?: (rs: SQLResultSet) => U): PsPromise { return this.execSqlStatements(this.transaction, "execute", sqlStatements, rsCallback); } /** Method for executing a readTransaction with a one or more `sqlStatement` * with the specified `args`, calling the `rsCallback` with the result set(s). * The `args` and `rsCallback` are optional. * Passing a _single_ `sqlStatement` string with `args` that is an _array of arrays_, * the statement is executed with each row in the `args`. * Passing an array of `{ sql, args}` objects to `sqlStatement` * executes the `sql` in each row with the row's `args` (or the parameter `args`). * Returns: promise that resolves with `rsCallback` result * or the resultSet, if no `rsCallback` specified. If an array of statements or arguments * is specified, the promise resolves with an array of results/resultSets. * Usage: * wsdb.read("SELECT * FROM person WHERE first = ?", * ["Bob"], * function (rs) { * var rows = rs.rows; * for(var i = 0; i < rows.length; i++) { * ... * } * return result; * } * ).then(function (result) {...}); * * Other Usage: (single `sqlStatement` with multiple sets of `args`) * wsdb.read("SELECT * FROM person WHERE first = ?", * [ ["Bob"], ["John"] ], * // called for each row in args * function (rs) { * return rs.rows; * } * ).then(function (results) { * var bobRows = results[0], johnRows = results[1]; * ... * }); * * Other Usage: (multiple `sqlStatement` with multiple sets of `args`) * wsdb.read([{ * sql: "SELECT * FROM person WHERE id=?", * args: [23] * }, { * sql: "SELECT * FROM address WHERE state in (?, ?, ?)", * args: ["CA", "FL", "TX"] * }], * // called for each object in args * function (rs) { * return rs.rows; * } * ).then(function (results) { * var person23rows = results[0], addressRows = results[1]; * ... * }); */ public read(sqlStatements: SqlQuery | SqlQuery[], rsCallback?: (rs: SQLResultSet) => U) { return this.execSqlStatements(this.readTransaction, "read", sqlStatements, rsCallback); } /** Method for executing a readTransaction with a single `sqlStatement` that's expected to return a single row. * The `rowCallback` function is called with the first row in the resultset * or with `undefined` if resultset contains no rows. * If the query does not return a row, the `defaultValue` is returned instead. * @returns promise that resolves with the `rowCallback` result or the row, if no `rowCallback` specified. * If no rows are selected and `rowCallback` isn't specified, the promise resolves with the `defaultRow`. * The promise is rejected if the query returns multiple rows or if it returns * zero rows and no `rowCallback` and `defaultRow` were specified. * Usage: * wsdb.readRow("SELECT * FROM person WHERE id = ?", [123], function (row) { * if(!row) { * // person not found * } * else { * ... * } * }).then(function (result) {...}); */ public readRow(sqlStatements: SqlQuery | SqlQuery[], rowCallback?: (row?: any) => void, defaultValue?: any) { var util = this.util; return util.pipe(>this.read(sqlStatements), function (rs: SQLResultSet | SQLResultSet[]) { var row: any; if (Array.isArray(rs) || rs.rows.length > 1) { return util.rejectError(util.defer(), new Error("Query returned " + (Array.isArray(rs) ? "array of " + rs.length + " result sets" : rs.rows.length + " rows"))); } else if (rs.rows.length === 0) { if (defaultValue) { row = defaultValue; } else if (rowCallback) { row = rowCallback(); } else { return util.rejectError(util.defer(), new Error("Query returned 0 rows")); } } else { row = rs.rows.item(0); if (rowCallback) { row = rowCallback(row); } } return row; }); } /** Execute sqlStatement in the context of `xactMethod` * Implements common behavior for `wsdb.execute` and `wsdb.read` */ execSqlStatements(xactMethod: (callback: SQLTransactionCallback) => PsPromise, xactMethodType: SqlStatementType, sqlStatements: SqlQuery[], rsCallback: null | undefined): PsPromise; execSqlStatements(xactMethod: (callback: SQLTransactionCallback) => PsPromise, xactMethodType: SqlStatementType, sqlStatements: SqlQuery[], rsCallback: (rs: SQLResultSet) => U): PsPromise; execSqlStatements(xactMethod: (callback: SQLTransactionCallback) => PsPromise, xactMethodType: SqlStatementType, sqlStatements: SqlQuery, rsCallback: (rs: SQLResultSet) => U): PsPromise; execSqlStatements(xactMethod: (callback: SQLTransactionCallback) => PsPromise, xactMethodType: SqlStatementType, sqlStatements: SqlQuery, rsCallback: null | undefined): PsPromise; execSqlStatements(xactMethod: (callback: SQLTransactionCallback) => PsPromise, xactMethodType: SqlStatementType, sqlStatements: SqlQuery[], rsCallback: null | undefined): PsPromise; execSqlStatements(xactMethod: (callback: SQLTransactionCallback) => PsPromise, xactMethodType: SqlStatementType, sqlStatements: SqlQuery | SqlQuery[], rsCallback: ((rs: SQLResultSet) => U) | null | undefined): PsPromise; execSqlStatements(xactMethod: (callback: SQLTransactionCallback) => PsPromise, xactMethodType: SqlStatementType, sqlStatements: SqlQuery | SqlQuery[], rsCallback: ((rs: SQLResultSet) => U) | null | undefined): PsPromise { var start = new Date().getTime(); if (typeof window !== "undefined" && !(window)["startQueriesTime"]) { (window)["startQueriesTime"] = start; } var util = this.util; var isAry = Array.isArray(sqlStatements); var sqls = (isAry ? sqlStatements : [sqlStatements]); var results: (SQLResultSet | U)[] = []; var pipeReturn = util.pipe(xactMethod(function (xact: SQLTransaction) { for (var i = 0; i < sqls.length; i++) { var cmnd: SqlQuery = sqls[i]; var params = (typeof cmnd.args === "undefined" ? null : cmnd.args); if (params == null || params.length === 0) { xact.executeSql(cmnd.sql, null, function (xact: SQLTransaction, rs: SQLResultSet) { results.push(rsCallback ? rsCallback(rs) : rs); }); } else { for (var j = 0, szJ = params.length; j < szJ; j++) { xact.executeSql(cmnd.sql, params[j], function (xact: SQLTransaction, rs: SQLResultSet) { results.push(rsCallback ? rsCallback(rs) : rs); }); } } } }), function () { return isAry ? results : results[0]; }, function (err: any) { err.sql = sqls; return err; }); if (util.logTimings) { pipeReturn.then(function () { var end = new Date().getTime(); var time = end - start; if (typeof window !== "undefined") { (window)["endQueriesTime"] = end; } util.log(util.DEBUG, "websql finish args: ", xactMethodType, sqls.length, sqls); util.log(util.DEBUG, "websql runtime: ", time); }); } return pipeReturn; } /** Calls window.openDatabase(). * - version defaults to `""` * - displayName defaults to `name` * - estimatedSize defaults to `2 * 1024 * 1024` * Returns: promise that resolves with this `WebsqlDatabase` instance * Usage: * wsdb.openDatabase("test", "Test Database", 2 * 1024 * 1024)) * .then(function(wsdb) {...}); * More usage: * wsdb.openDatabase("test")) * .then(function(wsdb) {...}); */ public static openDatabase(util: DbUtil, name: string, version?: number | null, displayName?: string | null, estimatedSize?: number | null): PsPromise { util.log(util.DEBUG, "openDatabase", name, version, displayName, estimatedSize); if (!displayName) displayName = name; if (!version) version = ""; if (!estimatedSize) { if (typeof window !== "undefined" && window.navigator.userAgent.match(/(iPad|iPhone);.*CPU.*OS 7_0/i)) { estimatedSize = 5 * 1024 * 1024; } else { estimatedSize = 50 * 1024 * 1024; } } var dfd = util.defer(); try { if (typeof window === "undefined" || !window.openDatabase) { util.rejectError(dfd, "WebSQL not implemented"); } else { // seems to synchronously open WebSQL, even though window.openDatabase is async var db: Database = window.openDatabase(name, version, displayName, estimatedSize); if (util.isDatabase(db)) { dfd.resolve(db); } else { util.rejectError(dfd, "Failed to open database"); } } } catch (ex) { util.rejectError(dfd, "Failed to open database " + name, { exception: ex }); } return dfd.promise; } public static newWebSqlDb(name: string | Database, version: number | null, displayName: string | null, estimatedSize: number | null, utilSettings: DataPersister.UtilConfig): PsPromise { var util = new DbUtil("WebSQL", "[object Database]", utilSettings); // Create WebSQL wrapper from native Database or by opening 'name' DB var pOpen: PsPromise; if (util.isDatabase(name)) { var dfd = util.defer(); dfd.resolve(name); pOpen = dfd.promise; } else { pOpen = WebSqlSpi.openDatabase(util, name, version, displayName, estimatedSize); } return pOpen.then((dbInst) => new WebSqlSpi(dbInst, util)); } } module WebSqlSpi { export type SqlStatementType = ("read" | "execute"); export interface SqlQuery { sql: string; args?: ObjectArray[]; } export interface SqlTableInfo { name: string; type: string; sql: string; } } export = WebSqlSpi;