/** * * A SecureSQL database transaction. See `Ext.space.securesql.Database.createTransaction` * * Queries added to a transaction will be executed as part of a single transaction. * Each query returns a promise with the data for the query. The `execute` statement * returns a promise that will resolve when the transaction is complete. * * If any of the queries generate an error then the transaction will be rolled back * and any data mutations will not be applied to the database. * The promise of the query that failed will reject. And the promise returned by * `execute` will also reject. * * var tx = testDatabase.createTransaction(); * * tx.query("select * from test where id = ?",[2]).then(function(rows){ * // * }).error(function(e){ * Ext.space.Logger.error("Error with query?",e); * }); * * tx.query("select * from test where id = ?",[1]).then(function(rows){ * // * }).error(function(e){ * Ext.space.Logger.error("Error with query?",e); * }); * * tx.execute().then(function(){ * // * }).error(function(e){ * Ext.space.Logger.error("Error with TX?",e); * }); */Ext.define("Ext.space.securesql.Transaction", { /** * @private */ database: null, /** * @private */ queries: null, /** * @private */ id: null, /** * @private */ began: false, /** * @private */ skipDatabaseLoaded: false, /** * @private */ executePromise: null, constructor: function(database, options) { this.database = database; this.queries = []; // allow for moving ahead even if the database hasn't declared itself to // be loaded (because in order to load it, we have to create a version table, // so we need a transaction first in an unfortunate chicken and egg scenario) if (options && options.immediate) { this.skipDatabaseLoaded = true; } }, /** * Add a query this transaction. * * The returned promise will provide an array of records that match the query. * Each array value will be an object; the object properties will match field * names in the query. If the query uses column aliases then the alias will be * returned as the field name. * * testDatabase.query("select id, name from test where id = ?",[1]).then(function(rows) { * var row = rows[0]; * Ext.space.Logger.log("record ",row.id, row.name); * }).error(function(e) { * Ext.space.Logger.error("Error with query?",e); * }); * * testDatabase.query("select count(*) as countOfTest from test").then(function(rows) { * var row = rows[0]; * Ext.space.Logger.log("Count of rows in test ", row.countOfTest); * }).error(function(e) { * Ext.space.Logger.error("Error with query?",e); * }); * * @param {String} query The SQL statement * @param {Array} fields The fields of the statement. * @return {Ext.space.Promise} promise that will resolve when the query completes */ query: function(query, fields){ if (this.began) { throw new Error("Ext.space.securesql.Transaction#query: Cannot add queries to a transaction that has already begun."); } var me = this; return this.sql(query, fields).then(function(rs){ return me.database.convertResults(rs); }); }, /** * Insert a single record into a table. Pass this function an array or object: * - If an array, then the order of elements in the array must match the order * of the fields. * - If an object, then the field names will be extracted from the properties * * Array Example: * * tx.insert('test', ["id", "name"], [1, "one"]).then(function(insertId) { * //done with insert * //insertId will be 1 or the auto increment value of the id of the table if the table has one. * }).error(function(e) { * Ext.space.Logger.error("Error with insert",e); * }); * * would be equivalent to * * INSERT INTO TABLE test (id, name) VALUES (1, 'one'); * * Object Example: * * tx.insert('test', ["id", "name"], {id: 2, name:"two"}).then(function() { * //done with insert * //insertId will be 2 or the auto increment value of the id of the table if the table has one. * }).error(function(e) { * Ext.space.Logger.error("Error with insert",e); * }); * * would be equivalent to * * INSERT INTO TABLE test (id, name) VALUES (2, 'two'); * * @param {String} table the name of the table the record will be inserted into * @param {Array} fields an array of field names to insert. * @param {Array|Object} values either an array or object to insert. * @return {Ext.space.Promise} promise that will resolve when the insert is complete. */ insert: function(table, fields, values){ if (this.began) { throw new Error("Ext.space.securesql.Transaction#insert: Cannot add queries to a transaction that has already begun."); } var query = this.database._buildInsertStatement(table, fields, values); return this.sql(query.statement, query.values).then(function(rs){ return rs.insertId; }); }, /** * Execute the queries in the transaction. * * @return {Ext.space.Promise} Promise that resolves if all the queries are successful * or rejects if any query fails. */ execute: function() { if (!this.executePromise) { this.executePromise = new Ext.space.Promise(); if (this.skipDatabaseLoaded) { this.run(); } else { this.database.queueTransaction(this, this.executePromise); } } return this.executePromise; }, // // private native bridge interface methods follow // /** * Add a raw SQL statement to this transaction. * * This differs from `query()` in that it will not do any processing on the * results returned by the query. * * @private * @param {String} sql The SQL statement to execute. This is required. * @param {Array} args The arguments array to bind each "?" placeholder in the Sql statement. This is optional. * @return {Ext.space.Promise} The promise that is resolved when the SQL statement has finished executing. */ sql: function(sql, args) { if (this.began) { throw new Error("Ext.space.securesql.Transaction#sql: Cannot add SQL queries to a transaction that has already begun."); } var queryPromise = new Ext.space.Promise(); this.queries.push({statement: sql, values: args, promise: queryPromise}); return queryPromise; }, /** * Create the transaction in the native client. * * @param {Object} config Transaction options * @param {Boolean} config.readOnly `true` if this is a read-only transaction * @return {Ext.space.Promise} Promise that resolves when the transaction is created. */ create: function(config) { config = config || {}; var promise = new Ext.space.Promise(), me = this; Ext.space.Communicator.send({ command: "Sqlite#createTransaction", databaseId: me.database.id, readOnly: config.readOnly, queue: true, callbacks: { success: function(id) { me.id = id; promise.fulfill(); }, failure: function(e) { promise.reject(e); } } }); return promise; }, /** * Begin executing the transaction. * * @private * @return {Ext.space.Promise} Promise that resolves when the transaction is ready to begin executing SQL queries. */ begin: function() { var me = this, promise = new Ext.space.Promise(); if (!me.began) { me.began = true; Ext.space.Communicator.send({ command: "Sqlite#beginTransaction", transactionId: me.id, callbacks: { success: function() { promise.fulfill(); }, failure: function(e) { promise.reject(e); me.rollback(e); } } }); } else { promise.reject("Ext.space.securesql.Transaction#begin: Cannot begin a transaction that has already been started"); } return promise; }, /** * Execute the SQL statements in this transaction in sequence. * * @private */ executeStatements: function() { var me = this; function next(query) { if (!query) { me.commit(); return; } Ext.space.Communicator.send({ command: "Sqlite#executeStatement", transactionId: me.id, sqlStatement: query.statement, arguments: JSON.stringify(query.values), callbacks: { success: function(rs) { // Protect against a DB deadlock in case promise handler throws an exception. try { query.promise.fulfill(rs); next(me.queries.shift()); } catch(e) { try { query.promise.reject(e); } catch(ex) { // no-op } me.rollback(e); } }, failure: function(e) { // Protect against a DB deadlock in case promise handler throws an exception. try { query.promise.reject(e); } catch(ex) { // no-op } me.rollback(e); } } }); } next(me.queries.shift()); }, /** * Commit the transaction. * * @private */ commit: function() { var me = this; Ext.space.Communicator.send({ command: "Sqlite#commitTransaction", transactionId: me.id, callbacks: { success: function() { me.executePromise.fulfill(); }, failure: function() { me.rollback(); } } }); }, /** * Roll back the transaction * * @private */ rollback: function(e) { Ext.space.Communicator.send({ command: "Sqlite#rollbackTransaction", transactionId: this.id }); this.executePromise.reject(e); }, /** * Batch executes all queued Sql statements inside a transaction, handling errors and commit/rollback automatically. * * @private * @return {Ext.space.Promise} * The promise that is resolved when the transaction has been committed or rolled back. */ run: function() { var start, create = this.create.bind(this), begin = this.begin.bind(this), executeStatements = this.executeStatements.bind(this); if (this.skipDatabaseLoaded) { start = create(); } else { start = this.database.loaded.then(function() { return create(); }); } return start.then(begin).then(executeStatements); }});