1 var mysql = require("mysql"), 2 comb = require("comb"), 3 hitch = comb.hitch, 4 Promise = comb.Promise, 5 PromiseList = comb.PromiseList, 6 ConnectionPool = require("./ConnectionPool"); 7 //MySQLPool = require("mysql-pool").MySQLPool; 8 9 10 var closedCount = 0; 11 function throwCmdError(cmd, action) { 12 cmd.on('error', function(err) { 13 throw new Error('Failed to ' + action + 14 (err && err.message ? ': ' + err.message : '')); 15 }); 16 } 17 18 function throwError(action, err) { 19 throw new Error('Failed to ' + action + 20 (err && err.message ? ': ' + err.message : '')); 21 } 22 23 var LOGGER = comb.logging.Logger.getLogger("comb.adapters.mysql.client"); 24 25 /** 26 * @class MySQL Client wrapper to standardize queries. 27 * 28 * <b>NOTE this class is not publicly exposed, but returned from calling getConnection on {@link moose.getConnection}</b> 29 * 30 * @name Query 31 * @memberOf moose.adapters.client.mysql 32 * 33 * @param {moose.adapters.mysql.ConnectionPool} pool the pool to retrieve connections from. 34 * @param {Boolean} forceClose=false whether or not to force close the connection 35 * @param {String} [database] the database to connect to when querying. 36 * 37 * @property {String} database the name of the database the query is currently using. 38 */ 39 var Query = comb.define(null, { 40 instance : { 41 /**@lends moose.adapters.client.mysql.Query.prototype*/ 42 43 constructor : function(pool, forceClose, database) { 44 if (!pool) throw "Query : Pool required to query"; 45 if(database){ 46 LOGGER.debug("QUERY DB = " + database) 47 this.database = database; 48 } 49 this.pool = pool; 50 this.forceClose = forceClose; 51 this._ended = false; 52 }, 53 54 __query : function(query) { 55 var ret = new Promise(); 56 this.connection.query(query, hitch(this, function(err, results, info) { 57 var conn = this.connection; 58 this.connection = null; 59 if (this.forceClose) { 60 conn.end(hitch(this, function() { 61 if (err) { 62 ret.errback(err); 63 } else { 64 ret.callback(results, info); 65 } 66 this.pool.removeConnection(conn); 67 })); 68 } else { 69 this.pool.returnConnection(conn); 70 if (err) { 71 ret.errback(err); 72 } else { 73 ret.callback(results, info); 74 } 75 } 76 })); 77 return ret; 78 }, 79 80 /** 81 * Queries the database. 82 * 83 * @param {String} query query to perform 84 * 85 * @return {comb.Promise} promise that is called back with the results, or error backs with an error. 86 */ 87 query : function(query) { 88 if (!this._ended) { 89 var ret = new Promise(); 90 if (!this.connection) { 91 this.pool.getConnection().then(hitch(this, function(conn) { 92 this.connection = conn; 93 this.query(query).then(hitch(ret, "callback"), hitch(ret, "errback")); 94 })); 95 } else { 96 var conn = this.connection; 97 if (this.__setDatabase && this.__database != conn.database) { 98 this.connection.useDatabase(this.__database, hitch(this, function(err) { 99 this.__setDatabase = false; 100 this.connection.database = this.__database; 101 if (err) { 102 ret.errback(err); 103 } else { 104 this.__query(query).then(hitch(ret, "callback"), hitch(ret, "errback")); 105 } 106 })); 107 } else { 108 this.__query(query).then(hitch(ret, "callback"), hitch(ret, "errback")); 109 } 110 } 111 } else { 112 throw new Error("Query has already been closed"); 113 } 114 return ret; 115 }, 116 117 /**@ignore*/ 118 setters : { 119 database : function(database) { 120 if (comb.isString(database) && database != this.__database && !this._ended) { 121 this.__setDatabase = true; 122 this.__database = database; 123 } 124 } 125 }, 126 127 /**@ignore*/ 128 getters : { 129 database : function(){ 130 return this.__database; 131 } 132 } 133 } 134 }); 135 136 137 /** 138 * @class Sub class of query to handle transaction based queries. 139 * 140 * <b>NOTE this class is not publicly exposed, but returned from calling getConnection on {@link moose.transaction}</b> 141 * 142 * @name TransactionQuery 143 * @augemtns moose.adapters.client.mysql.Query 144 * @memberOf moose.adapters.client.mysql 145 * 146 * @param {moose.adapters.mysql.ConnectionPool} pool the pool to retrieve connections from. 147 * @param {Boolean} forceClose=false whether or not to force close the connection 148 * @param {String} [database] the database to connect to when querying. 149 */ 150 var TransactionQuery = comb.define(Query, { 151 instance : { 152 /**@lends moose.adapters.client.mysql.TransactionQuery.prototype*/ 153 154 __query : function(query) { 155 var ret = new Promise(); 156 this.connection.query(query, hitch(this, function(err, results, info) { 157 if (err) { 158 this.connection.clean = false; 159 ret.errback(err); 160 } else { 161 ret.callback(results, info); 162 } 163 })); 164 return ret; 165 }, 166 167 query : function(query, args) { 168 if (this.connection && !this.connection.clean) { 169 throw new Error("Cannot commit a transaction with an error"); 170 return; 171 } 172 return this.super(arguments); 173 }, 174 175 /** 176 * Call to commit a transaction. 177 * 178 * @return {comb.Promise} called back after the commit has finished. 179 */ 180 commit: function() { 181 var ret = new Promise(); 182 this.query("COMMIT").then(hitch(this, function(res) { 183 this.connection.end(); 184 this.pool.removeConnection(this.connection); 185 this.connection = null; 186 ret.callback(res); 187 }), hitch(ret, "errback")); 188 return ret; 189 }, 190 191 abort: function() { 192 var ret = new Promise(); 193 this.query("ROLLBACK").then(hitch(this, function() { 194 this.connection.end(); 195 this.pool.removeConnection(this.connection); 196 this.connection = null; 197 ret.callback(); 198 }), hitch(ret, "errback")); 199 return ret; 200 }, 201 202 /** 203 * Call to rollback a transaction. 204 * 205 * @return {comb.Promise} called back after the rollback has finished. 206 */ 207 rollback : function() { 208 return this.abort(); 209 } 210 } 211 }); 212 213 /** 214 * @class MySQL connection pool class. This class is not used directly. 215 * 216 * 217 * @name ConnectionPool 218 * @augments moose.adapters.client.ConnectionPool 219 * @memberOf moose.adapters.client.mysql 220 */ 221 var MysqlConnectionPool = (exports.ConnectionPool = comb.define(ConnectionPool, { 222 instance : { 223 224 createConnection : function() { 225 var conn = new mysql.Client(this._options); 226 conn.clean = true; 227 conn.connect(); 228 return conn; 229 }, 230 231 closeConnection : function(conn) { 232 var ret = new Promise(); 233 conn.end(function() { 234 ret.callback(); 235 }); 236 237 return ret; 238 }, 239 240 validate : function(conn) { 241 var ret = new Promise(); 242 if (!conn.clean || conn.ending) { 243 ret.callback(false); 244 } else if (!conn.database || conn.database != this.database) { 245 //reset to the default database. 246 conn.useDatabase(this.database, function(err) { 247 if (err) { 248 ret.callback(false); 249 } else { 250 conn.database = this.database; 251 ret.callback(true); 252 } 253 }); 254 } else { 255 ret.callback(true); 256 } 257 return ret; 258 } 259 } 260 })); 261 262 /** 263 * @class Manages mysql connections. This class manages the creation of a connection pool, 264 * and is the class that retrieves/creates connections to be used my moose. 265 * 266 * @name Client 267 * @memberOf moose.adapters.client 268 */ 269 exports.Client = comb.define(null, { 270 instance : { 271 /**@lends moose.adapters.client.Client.prototype*/ 272 273 minConnections : null, 274 275 constructor : function(options) { 276 this.options = options || {}; 277 this.database = options.database; 278 this.maxConnections = typeof options.maxConnections == "number" ? options.maxConnections : 10; 279 this.pool = new MysqlConnectionPool(options); 280 }, 281 282 /** 283 * Executes a query with the given sql and database. 284 * @param {String} sql the sql to execute 285 * @param {String} [database] the name of the database to execute the query on. 286 * 287 * @return {comb.Promise} a promise that is called back with the results. 288 */ 289 query : function(sql, database) { 290 var q = new Query(this.pool, false, database || this.database); 291 return q.query(sql); 292 }, 293 294 /** 295 * Retrieces a connection from the connection pool. 296 * 297 * @param {boolean} [forceClose=false] whether or not to close the connection after the query is done. 298 * this is typically not needed. 299 * @param [String] [database] the name of the database to connection to. 300 * 301 * @return {Query} the query object to perform queries on. 302 */ 303 getConnection : function(forceClose, database) { 304 LOGGER.debug("GET CONNECTION DB = " + database + " " + this.database); 305 return new Query(this.pool, forceClose, (database || this.database)); 306 }, 307 308 /** 309 * Retrieves a transaction query to perform a transaction on. 310 * 311 * @param {String} database the name of the database to perform the transaction on. 312 */ 313 transaction: function(database) { 314 var conn = new TransactionQuery(this.pool, false, database || this.database); 315 conn.query('START TRANSACTION').addErrback(hitch(null, throwError, 'start transaction')); 316 return conn; 317 }, 318 319 /** 320 * Closes all connections. 321 * 322 * @return {comb.Promise} called back once all queries are done, or calls errback if an error occurs. 323 */ 324 close : function() { 325 var ret = new Promise(); 326 this.pool.endAll(hitch(this, function(err) { 327 this.pool = null; 328 if (err) { 329 LOGGER.error("Error closing connections"); 330 ret.errback(err); 331 } else { 332 ret.callback(); 333 } 334 })); 335 return ret; 336 }, 337 338 /**@ignore*/ 339 setters : { 340 database : function(database) { 341 if (comb.isString(database)) { 342 this.__defaultDatabase = database; 343 } else { 344 throw "moose.adapters.mysql.Client : Database required"; 345 } 346 } 347 }, 348 349 /**@ignore*/ 350 getters : { 351 database : function() { 352 return this.__defaultDatabase; 353 } 354 } 355 } 356 }); 357