diff --git a/websql/websql-tests.ts b/websql/websql-tests.ts new file mode 100644 index 0000000000..131883c243 --- /dev/null +++ b/websql/websql-tests.ts @@ -0,0 +1,303 @@ +/// + +interface DbState { + database: Database; + version: DOMString; + transaction: (callback: SQLTransactionCallback, errorCallback?: SQLTransactionErrorCallback, successCallback?: SQLVoidCallback) => void; + readTransaction: (callback: SQLTransactionCallback, errorCallback?: SQLTransactionErrorCallback, successCallback?: SQLVoidCallback) => void; + changeVersion: (oldVersion: DOMString, newVersion: DOMString, callback?: SQLTransactionCallback, errorCallback?: SQLTransactionErrorCallback, successCallback?: SQLVoidCallback) => void +} + +interface SqlQuery { + sql: DOMString; + args?: ObjectArray; +} + +interface Results { + data: any[]; + sqlResultSet: SQLResultSet; +} + + +(function () { + var dbName: string = "websql-tests"; + + // run the test + createDatabase(dbName); + + + function log(...msg: any[]) { + if (console && console.log) { + console.log.apply(console, msg); + } + } + + + function createDatabase(name: string) { + var tableOne = { + name: "tableOne", + columns: ["i_id", "s_name", "d_last"], + }; + var tableTwo = { + name: "tableTwo", + columns: ["i_thing", "s_description", "d_decimal"], + }; + + openDatabase(dbName, "1", dbName, 1024 * 1024, function (dbState) { + createTables(dbState, [ + { name: tableOne.name, columns: tableOne.columns }, + { name: tableTwo.name, columns: tableTwo.columns }, + ], function (res) { + writeRecords(dbState, [ + { name: tableOne.name, data: createMockRecord(tableOne.columns) }, + { name: tableOne.name, data: createMockRecord(tableOne.columns) }, + { name: tableTwo.name, data: createMockRecord(tableTwo.columns) }, + { name: tableTwo.name, data: createMockRecord(tableTwo.columns) }, + { name: tableTwo.name, data: createMockRecord(tableTwo.columns) }, + ], function (ress) { + log(ress); + readRecords(dbState, [tableOne.name, tableTwo.name], function (resss) { + log(resss); + }); + }); + }); + }); + } + + + function initDb(db: Database): DbState { + return { + database: db, + version: db.version, + transaction: db.transaction.bind(db), + readTransaction: db.readTransaction.bind(db), + changeVersion: db.changeVersion.bind(db) + }; + } + + + function openDatabase(name: string, version: string, displayName: string, estimatedSize: number, callback: (dbState: DbState) => void) { + estimatedSize = estimatedSize || (5 * 1024 * 1024); + + try { + if (!window.openDatabase) { + log("WebSQL not implemented"); + } else { + // seems to synchronously open WebSQL, even though window.openDatabase is async + var db: Database = window.openDatabase(name, version, displayName, estimatedSize); + var dbState = initDb(db); + if (db) { + callback(dbState); + } else { + log("Failed to open database"); + } + } + } catch (ex) { + log("Failed to open database '" + name + "': " + (JSON && JSON.stringify ? JSON.stringify(ex) : ex)); + } + } + + + function createTables(dbState: DbState, tableDefs: { name: string; columns: string[]; }[], callback: (rec: SQLResultSet[][]) => void) { + var res: SQLResultSet[][] = []; + var resI = 0, count = tableDefs.length; + for (var i = 0; i < count; i++) { + createTable(dbState, tableDefs[i].name, tableDefs[i].columns, function (rec: SQLResultSet[]) { + res.push(rec); + resI++; + if (resI == count) { + callback(res); + } + }); + } + } + + + function createTable(dbState: DbState, name: string, columns: string[], callback: (rec: SQLResultSet[]) => void) { + var sqls: SqlQuery[] = []; + var colDefs = columns.map((col) => { + var colType: string = null; + var type = col.charAt(0); + switch (type) { + case 'i': + case 'l': + colType = "INTEGER"; + break; + case 'f': + case 'd': + colType = "REAL"; + break; + case 'b': + case 'z': + colType = "INTEGER"; + break; + case 's': + colType = "TEXT"; + break; + default: + colType = "TEXT"; + } + return col + " " + colType; + }); + sqls.push({ sql: "CREATE TABLE IF NOT EXISTS " + name + " (" + colDefs.join(",") + ")", args: [] }); + execSqlStatements(dbState.transaction, sqls, callback); + } + + + /** column name prefixes + * 'i', 'l' - integer + * 'f', 'd' - float + * 'b', 'z' - boolean + * 's' - string + * other - null + */ + function createMockRecord(columns: string[]): any { + var rec: { [id: string]: any } = {}; + for (var i = 0, size = columns.length; i < size; i++) { + var col = columns[i]; + var type = col.charAt(0); + switch (type) { + case 'i': + case 'l': + rec[col] = parseInt((Math.random() * 1000000).toString()); + break; + case 'f': + case 'd': + rec[col] = (Math.random() - 0.5) * 2000000; + break; + case 'b': + case 'z': + rec[col] = Math.random() > 0.5; + break; + case 's': + rec[col] = String.fromCharCode.apply(String, new Array(Math.round(Math.random() * 21)).join(".").split("").map((k) => 65 + Math.round(Math.random() * 25))); + break; + default: + rec[col] = null; + } + } + return rec; + } + + + function writeRecords(dbState: DbState, tableDefs: { name: string; data: any; }[], callback: (rec: SQLResultSet[][]) => void) { + var res: SQLResultSet[][] = []; + var resI = 0, count = tableDefs.length; + for (var i = 0; i < count; i++) { + writeRecord(dbState, tableDefs[i].name, tableDefs[i].data, function (rec: SQLResultSet[]) { + res.push(rec); + resI++; + if (resI == count) { + callback(res); + } + }); + } + } + + + function writeRecord(dbState: DbState, tableName: string, record: any, callback: (rec: SQLResultSet[]) => void) { + var props = Object.keys(record); + var propCount = props.length; + var args: ObjectArray = []; + var sqlStatement = { + sql: "INSERT INTO " + tableName + " values(" + (new Array(propCount + 1).join("?").split("").join(",")) + ")", + args: args + }; + for (var i = 0; i < propCount; i++) { + var prop = props[i]; + args.push(record[prop]); + } + + execSqlStatements(dbState.transaction, [sqlStatement], callback); + } + + + function readRecords(dbState: DbState, tableNames: string[], callback: (rec: Results[]) => void) { + var res: Results[] = []; + var resI = 0, count = tableNames.length; + for (var i = 0; i < count; i++) { + readRecord(dbState, tableNames[i], function (rec: Results) { + res.push(rec); + resI++; + if (resI == count) { + callback(res); + } + }); + } + } + + + function readRecord(dbState: DbState, tableName: string, callback: (rec: Results) => void) { + var args: ObjectArray = []; + var sqlStatement = { + sql: "SELECT * FROM " + tableName, + args: args + }; + + execSqlStatements(dbState.transaction, [sqlStatement], function (rec: SQLResultSet[]) { + var resObjs: Results[] = resultSetToResults(rec); + callback(resObjs[0]); + }); + } + + + function resultSetToResults(results: SQLResultSet[]) { + var resObjs: Results[] = []; + for (var i = 0, count = results.length; i < count; i++) { + var resAry: any[] = []; + var sqlRsI = results[i]; + var resObj = { + data: resAry, + sqlResultSet: sqlRsI + }; + resObjs.push(resObj); + var rows = sqlRsI.rows; + var rI = 0; + for (var rI = 0, rCount = sqlRsI.rows.length; rI < rCount; rI++) { + var r = rows.item(rI); + resAry.push(r); + } + } + return resObjs; + } + + + function compareRecords(rec1: any, rec2: any): boolean { + var props = Object.keys(rec1); + for (var i = 0, size = props.length; i < size; i++) { + var prop = props[i]; + if (rec1[prop] !== rec2[prop]) { + return false; + } + } + return true; + } + + + function execSqlStatements(xactMethod: (callback: SQLTransactionCallback) => void, sqlStatement: SqlQuery[], + callback: (rs: SQLResultSet[]) => void) { + var results: SQLResultSet[] = []; + var sqlCount = sqlStatement.length; + var resI = 0; + + function execCommand(xact: SQLTransaction, sql: DOMString, args: ObjectArray) { + xact.executeSql(sql, args || [], function (xact: SQLTransaction, rs: SQLResultSet): void { + results.push(rs); + resI++; + if (resI === sqlCount) { + callback(results); + } + }, function (transaction, err) { + log(err, transaction); + return false; + }); + } + + xactMethod(function (xact: SQLTransaction): void { + for (var i = 0; i < sqlCount; i++) { + execCommand(xact, sqlStatement[i].sql, sqlStatement[i].args); + } + }); + } + +} ()); diff --git a/websql/websql.d.ts b/websql/websql.d.ts new file mode 100644 index 0000000000..7a9a248ea1 --- /dev/null +++ b/websql/websql.d.ts @@ -0,0 +1,216 @@ +// Type definitions for websql +// Project: http://www.w3.org/TR/webdatabase/ +// Definitions by: TeamworkGuy2 +// Definitions: https://github.com/borisyankov/DefinitelyTyped + +// W3C spec: http://www.w3.org/TR/webdatabase/#database +// Spec revision: 2010-11-18 +// NOTE: the W3C websql spec has been deprecated + +// uncomment to integrate with Window global object +interface Window extends WindowDatabase { } +interface WorkerUtils extends WorkerUtilsDatabase { } + +// util interfaces +interface DOMString extends String { } +interface ObjectArray extends Array { } + + +//[Supplemental, NoInterfaceObject] +interface WindowDatabase { + openDatabase(name: DOMString, version: DOMString, displayName: DOMString, estimatedSize: number, + creationCallback?: DatabaseCallback): Database; +} + +//[Supplemental, NoInterfaceObject] +interface WorkerUtilsDatabase { + openDatabase(name: DOMString, version: DOMString, displayName: DOMString, estimatedSize: number, + creationCallback?: DatabaseCallback): Database; + + openDatabaseSync(name: DOMString, version: DOMString, displayName: DOMString, estimatedSize: number, + creationCallback?: DatabaseCallback): DatabaseSync; +} + +//[Callback = FunctionOnly, NoInterfaceObject] +interface DatabaseCallback { + /*handleEvent*/(database: Database): void; +} + + +/** 4.3 Asynchronous database API - The transaction() and readTransaction() methods takes + * one to three arguments. When called, these methods must immediately return and then + * asynchronously run the transaction steps with the transaction callback being the + * first argument, the error callback being the second argument, if any, the success + * callback being the third argument, if any, and with no preflight operation or + * postflight operation + */ +interface Database { + /*readonly/const*/version: DOMString; + + transaction(callback: SQLTransactionCallback, errorCallback?: SQLTransactionErrorCallback, + successCallback?: SQLVoidCallback): void; + + readTransaction(callback: SQLTransactionCallback, errorCallback?: SQLTransactionErrorCallback, + successCallback?: SQLVoidCallback): void; + + /** The changeVersion() method allows scripts to atomically verify the version number and change + * it at the same time as doing a schema update. When the method is invoked, it must immediately + * return, and then asynchronously run the transaction steps with the transaction callback being + * the third argument, the error callback being the fourth argument, the success callback being + * the fifth argument + */ + changeVersion(oldVersion: DOMString, newVersion: DOMString, callback?: SQLTransactionCallback, + errorCallback?: SQLTransactionErrorCallback, successCallback?: SQLVoidCallback): void; +} + +//[Callback = FunctionOnly, NoInterfaceObject] +interface SQLVoidCallback { + /*handleEvent*/(): void; +} + +//[Callback = FunctionOnly, NoInterfaceObject] +interface SQLTransactionCallback { + /*handleEvent*/(transaction: SQLTransaction): void; +} + +//[Callback = FunctionOnly, NoInterfaceObject] +interface SQLTransactionErrorCallback { + /*handleEvent*/(error: SQLError): void; +} + +/** 4.3.1 Executing SQL statements + */ +interface SQLTransaction { + executeSql(sqlStatement: DOMString, arguments?: ObjectArray, callback?: SQLStatementCallback, + errorCallback?: SQLStatementErrorCallback): void; +} + +//[Callback = FunctionOnly, NoInterfaceObject] +interface SQLStatementCallback { + /*handleEvent*/(transaction: SQLTransaction, resultSet: SQLResultSet): void; +} + +//[Callback = FunctionOnly, NoInterfaceObject] +interface SQLStatementErrorCallback { + /*handleEvent*/(transaction: SQLTransaction, error: SQLError): boolean; +} + + +/** 4.4 Synchronous database API + */ +interface DatabaseSync { + /*readonly/const*/version: DOMString; + + transaction(callback: SQLTransactionSyncCallback): void; + + readTransaction(callback: SQLTransactionSyncCallback): void; + + changeVersion(oldVersion: DOMString, newVersion: DOMString, callback: SQLTransactionSyncCallback): void; +} + +//[Callback = FunctionOnly, NoInterfaceObject] +interface SQLTransactionSyncCallback { + /*handleEvent*/(transaction: SQLTransactionSync): void; +} + +/** 4.4.1 Executing SQL statements + */ +interface SQLTransactionSync { + executeSql(sqlStatement: DOMString, arguments?: ObjectArray): SQLResultSet; +} + + +/** 4.5 Database query results + * The insertId attribute must return the row ID of the row that the SQLResultSet + * object's SQL statement inserted into the database, if the statement inserted a row. + * If the statement inserted multiple rows, the ID of the last row must be the one returned. + * If the statement did not insert a row, then the attribute must instead raise an INVALID_ACCESS_ERR exception. + * + * The rowsAffected attribute must return the number of rows that were changed by the SQL statement. + * If the statement did not affected any rows, then the attribute must return zero. + * For "SELECT" statements, this returns zero (querying the database doesn't affect any rows). + * + * The rows attribute must return a SQLResultSetRowList representing the rows returned, + * in the order returned by the database. The same object must be returned each time. + * If no rows were returned, then the object will be empty (its length will be zero) + */ +interface SQLResultSet { + insertId: number; + rowsAffected: number; + rows: SQLResultSetRowList; +} + +/** SQLResultSetRowList objects have a length attribute that must return the number of + * rows it represents (the number of rows returned by the database). This is the length. + * Fetching the length might be expensive, and authors are thus encouraged to avoid using + * it (or enumerating over the object, which implicitly uses it) where possible. + * The object's supported property indices are the numbers in the range zero to length-1, + * unless the length is zero, in which case there are no supported property indices. + * The item(index) attribute must return the row with the given index index. + * If there is no such row, then the method must return null. + * + * Each row must be represented by a native ordered dictionary data type. In the + * JavaScript binding, this must be Object. Each row object must have one property + * (or dictionary entry) per column, with those properties enumerating in the order + * that these columns were returned by the database. Each property must have the + * name of the column and the value of the cell, as they were returned by the database + */ +interface SQLResultSetRowList { + length: number; + item(index: number): any; +} + + +/** 4.6 Errors and exceptions - asynchronous database API error + */ +declare class SQLError { + static UNKNOWN_ERR: number; // = 0; + static DATABASE_ERR: number; // = 1; + static VERSION_ERR: number; // = 2; + static TOO_LARGE_ERR: number; // = 3; + static QUOTA_ERR: number; // = 4; + static SYNTAX_ERR: number; // = 5; + static CONSTRAINT_ERR: number; // = 6; + static TIMEOUT_ERR: number; // = 7; + + code: number; + message: DOMString; +} + +// synchronous database API error +declare class SQLException { + /** Code 0 - The transaction failed for reasons unrelated to the database itself + * and not covered by any other error code. + */ + static UNKNOWN_ERR: number; // = 0; + /** Code 1 - The statement failed for database reasons not covered by any other error code. */ + static DATABASE_ERR: number; // = 1; + /** Code 2 - The operation failed because the actual database version was not what it should be. + * For example, a statement found that the actual database version no longer matched the + * expected version of the Database or DatabaseSync object, or the Database.changeVersion() + * or DatabaseSync.changeVersion() methods were passed a version that doesn't match the actual database version. + */ + static VERSION_ERR: number; // = 2; + /** Code 3 - The statement failed because the data returned from the database was too large. + * The SQL "LIMIT" modifier might be useful to reduce the size of the result set. + */ + static TOO_LARGE_ERR: number; // = 3; + /** Code 4 - The statement failed because there was not enough remaining storage space, + * or the storage quota was reached and the user declined to give more space to the database. + */ + static QUOTA_ERR: number; // = 4; + /** Code 5 - The statement failed because of a syntax error, or the number of arguments did + * not match the number of ? placeholders in the statement, or the statement tried to use a + * statement that is not allowed, such as BEGIN, COMMIT, or ROLLBACK, or the statement tried + * to use a verb that could modify the database but the transaction was read-only. */ + static SYNTAX_ERR: number; // = 5; + /** Code 6 - An INSERT, UPDATE, or REPLACE statement failed due to a constraint failure. + * For example, because a row was being inserted and the value given for the primary + * key column duplicated the value of an existing row. */ + static CONSTRAINT_ERR: number; // = 6; + /** Code 7 - A lock for the transaction could not be obtained in a reasonable time. */ + static TIMEOUT_ERR: number; // = 7; + + code: number; + message: DOMString; +}