├── .gitignore ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug* 4 | test -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "mysql-activerecord", 3 | "version": "0.8.6", 4 | "author": "Martin Tajur ", 5 | "description": "A lightweight MySQL query builder on top of the node-mysql module.", 6 | "licence": [ 7 | "MIT", 8 | "GPL" 9 | ], 10 | "homepage": "https://github.com/martintajur/node-mysql-activerecord", 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/martintajur/node-mysql-activerecord.git" 14 | }, 15 | "contributors": [ 16 | "Daniel Bretoi ", 17 | "Kyle Farris ", 18 | "Daehyub Kim " 19 | ], 20 | "dependencies": { 21 | "mysql": "2.5.2" 22 | }, 23 | "main" : "./", 24 | "engines": { 25 | "node": "*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Note from author: I don't actively maintain this repository anymore. Huge thanks to Kyle Farris who has a more actively maintained version of the same thing available at (https://github.com/kylefarris/node-querybuilder) Please use that instead.** 2 | 3 | MySQL ActiveRecord Adapter for Node.js 4 | ====================================== 5 | 6 | Query builder on top of node-mysql module (https://github.com/felixge/node-mysql). 7 | 8 | [![npm version](https://badge.fury.io/js/mysql-activerecord.svg)](https://badge.fury.io/js/mysql-activerecord) 9 | 10 | To me, the main benefit of is the ability to direct JavaScript objects straight to MySQL query components without having to worry about constructing the query itself. Although this query builder is a tiny step towards an ORM, I see a lot of value in the query builder as it allows more control over database queries than traditional ORM where queries are hidden behind the business logic and may become executed in an unoptimized way. (It is named after a popular PHP framework CodeIgniter's "Active Record" class, and thus the whole library does not have much in common with the active record pattern as such.) 11 | 12 | This query builder is 13 | 14 | * Light-weight 15 | * Supports all basic MySQL commands 16 | * Supports method chaining 17 | * Automatically escapes field values 18 | * Has no dependencies (it already includes the node-mysql module) 19 | * Supports raw queries 20 | 21 | How to install 22 | ============== 23 | 24 | npm install mysql-activerecord 25 | 26 | 27 | Get started 28 | ----------- 29 | 30 | var Db = require('mysql-activerecord'); 31 | var db = new Db.Adapter({ 32 | server: 'localhost', 33 | username: 'root', 34 | password: '12345', 35 | database: 'test', 36 | reconnectTimeout: 2000 37 | }); 38 | 39 | * `server`: the IP address or hostname to connect to 40 | * `username`: MySQL username to connect with 41 | * `password`: MySQL password to connect with 42 | * `database`: database to switch to initially (optional). If omitted, no database will be selected. 43 | * `port`: which port to connect to (optional). If omitted, 3306 will be used. 44 | * `reconnectTimeout`: milliseconds after which to try to reconnect to the MySQL server if a disconnect happens (optional). If omitted, the default value of 2000 will be used. If set to `false`, no reconnecting will take place. 45 | 46 | Support of MySQL commands 47 | ========================= 48 | 49 | * SELECT 50 | * UPDATE 51 | * INSERT (single-row and multi-row) 52 | * INSERT IGNORE 53 | * DELETE 54 | * JOIN 55 | * LIMIT and OFFSET 56 | * ORDER BY 57 | * GROUP BY 58 | * COUNT 59 | * HAVING 60 | 61 | Methods 62 | ======= 63 | 64 | # .select() 65 | 66 | ## .select(selectFieldName) 67 | Specifies the field(s) to use in the SELECT query as a atring. 68 | 69 | db.select("id, CONCAT(first_name, ' ', last_name) as full_name, email"); 70 | // This would produce: SELECT id, CONCAT(first_name, ' ', last_name) as full_name, email … 71 | 72 | You can call .select() multiple times within the scope of one query — all parameters will be used in the final query. E.g. 73 | 74 | db.select('id'); 75 | // do some advanced checking and calculations here (only synchronous work, though!) 76 | db.select('first_name, last_name'); 77 | // This would procude: SELECT id, first_name, last_name … 78 | 79 | ## .select([selectFieldName, selectFieldName, … ]) 80 | Same as above, with a difference of taking in fields list as an array. 81 | 82 | db.select(['id', 'first_name', 'last_name']); 83 | // This would produce: SELECT id, first_name, last_name … 84 | 85 | # .where() 86 | 87 | ## .where(rawClause) 88 | Specifies a where clause component. 89 | 90 | db.where('add_time is null'); 91 | // This would produce: … WHERE add_time is null … 92 | 93 | You can call .where() multiple times within the scope of one query — all parameters will be used in the final query. 94 | 95 | ## .where(fieldName, [possibleWhereInValue, possibleWhereInValue]) 96 | Specifies a WHERE IN structure to use in the query. 97 | 98 | db.where('first_name', ['John', 'Maria', 'Jason', 'Herbert']); 99 | // This would produce: … WHERE first_name in ('John', 'Maria', 'Jason', 'Herbert') … 100 | 101 | ## .where(fieldName, fieldValue) 102 | Specifies a single WHERE condition to use in the query. 103 | 104 | db.where('first_name', 'John'); 105 | // This would produce: … WHERE first_name = 'John' … 106 | 107 | ## .where({ fieldName: fieldValue, fieldName: fieldValue, … }) 108 | Specifies multiple WHERE conditions to use in the query. 109 | 110 | var conditions = { 111 | first_name: 'John', 112 | last_name: 'Smith' 113 | }; 114 | db.where(conditions); 115 | // This would produce: … WHERE first_name = 'John' AND last_name = 'Smith' … 116 | 117 | # .order_by() 118 | 119 | ## .order_by(orderByCondition) 120 | Specifies the ORDER BY condition as a full string. 121 | 122 | db.order_by('name asc'); 123 | // This would produce: … ORDER BY name asc … 124 | 125 | You can call .order_by() multiple times within the scope of one query — all parameters will be used in the final query. 126 | 127 | ## .order_by([orderByCondition, orderByCondition, … ]) 128 | Specifies multiple ORDER BY conditions as an array. 129 | 130 | db.order_by(['name asc', 'last_name desc']); 131 | // This would produce: … ORDER BY name asc, last_name desc … 132 | 133 | # .group_by() 134 | 135 | ## .group_by(groupByCondition) 136 | Specifies the GROUP BY condition as a full string. 137 | 138 | db.group_by('name asc'); 139 | // This would produce: … GROUP BY name asc … 140 | 141 | You can call .group_by() multiple times within the scope of one query — all parameters will be used in the final query. 142 | 143 | ## .group_by([groupByCondition, groupByCondition, … ]) 144 | Specifies the GROUP BY condition as a full string. 145 | 146 | db.group_by(['name asc', 'last_name desc']); 147 | // This would produce: … GROUP BY name asc, last_name desc … 148 | 149 | # .join() 150 | 151 | ## .join(tableName, joinCondition, joinDirection) 152 | Join additional tables to the query. 153 | 154 | db.join('pets', 'pets.owner_id = people.id', 'LEFT'); 155 | // This would produce: … LEFT JOIN pets ON pets.owner_id = people.id … 156 | 157 | db.join('pets', 'pets.owner_id = people.id'); 158 | // This would produce: … JOIN pets ON pets.owner_id = people.id … 159 | 160 | # .limit() 161 | 162 | ## .limit(limitNumber) 163 | Adds a row limit to query results. 164 | 165 | db.limit(10); 166 | // Limits query results to 10 rows. 167 | 168 | ## .limit(limitNumber, offsetNumber) 169 | Adds a row limit with an offset pointer position to query results. 170 | 171 | db.limit(10, 30); 172 | // Limits query results to 10 rows, starting from the 30th row in the full matching set. 173 | 174 | # Query execution commands 175 | 176 | After execution of a query, all query conditions are cleared. Results are passed down to responseCallback function. The parameters handed over to responseCallback match exactly what the underlying node-mysql module produces. See documentation from https://github.com/felixge/node-mysql 177 | 178 | ## .update(tableName, newData, responseCallback) 179 | Produces and executes UPDATE query. 180 | 181 | db.update('people', { first_name: 'John', last_name: 'Smith' }, function(err) { ... }); 182 | // This would produce: … UPDATE people SET first_name = 'John', last_name = 'Smith' … 183 | 184 | ## .delete(tableName, responseCallback) 185 | Produces and executes DELETE query. Be sure to specify some WHERE clause components using .where() not to truncate an entire table. ✌ 186 | 187 | db.delete('people', function(err) { ... }); 188 | 189 | ## .insert(tableName, newData, responseCallback) 190 | Produces and executes a single-row INSERT query. 191 | 192 | db.insert('people', { first_name: 'John', last_name: 'Smith' }, function(err, info) { ... }); 193 | // This would produce: … INSERT INTO people SET first_name = 'John', last_name = 'Smith' … 194 | 195 | ## .insert(tableName, [newData, newData, newData, …], responseCallback) 196 | Produces and executes a multi-row INSERT query. 197 | 198 | var person1 = { first_name: 'John', last_name: 'Smith' }; 199 | var person2 = { first_name: 'Jason', last_name: 'Binder' }; 200 | var person3 = { first_name: 'Herbert', last_name: 'von Kellogg' }; 201 | db.insert('people', [person1, person2, person3], function(err, info) { ... }); 202 | // This would produce: … INSERT INTO people (first_name, last_name) VALUES (('John','Smith'),('Jason','Binder'),('Herbert','von Kellogg')) … 203 | 204 | ## .insert_ignore(tableName, newData, responseCallback, onDuplicateKeyClause) 205 | Produces and executes an INSERT IGNORE query. Note that the newData parameter can be either a string (produces single-row INSERT) or an array (produces multi-row INSERT). You can also specify an optional onDuplicateKeyClause, e.g. 206 | 207 | db.insert_ignore('people', { first_name: 'John', last_name: 'Smith' }, function(err, info) { ... }, 'ON DUPLICATE KEY UPDATE duplicate_count = duplicate_count + 1'); 208 | // This would produce: … INSERT IGNORE INTO people SET first_name = 'John', last_name = 'Smith' … ON DUPLICATE KEY UPDATE duplicate_count = duplicate_count + 1 209 | 210 | ## .get(tableName, responseCallback) 211 | Produces and executes a SELECT query. 212 | 213 | db.get('people', function(err, rows, fields) { ... }); 214 | // This would produce: SELECT … FROM people … 215 | 216 | ## .count(tableName, responseCallback) 217 | Produces and executes a SELECT query with count. 218 | 219 | db.get('people', function(err, rows, fields) { ... }); 220 | // This would produce: SELECT count(*) FROM people … 221 | 222 | ## .query(sqlQueryString, responseCallback) 223 | Produces and executes a raw query. Note that while no set query conditions will be used in this query, they will all be reset nevertheless with the execution. 224 | 225 | db.query('SHOW TABLES FROM test_database', function(err, results) { ... }); 226 | 227 | ## .ping() 228 | Pings the connection. This is useful when extending idle timeouts. 229 | 230 | ## ._last_query() 231 | Returns the last executed query as a string. 232 | 233 | ## .connection() 234 | Returns the underlying database connection object, ultimately what https://github.com/felixge/node-mysql .createConnection() returns. 235 | 236 | Pooling connections 237 | =================== 238 | 239 | Single or multiple connections can be pooled with the Pool object. 240 | 241 | var Db = require('mysql-activerecord'); 242 | 243 | var pool = new Db.Pool({ 244 | server: 'localhost', 245 | username: 'root', 246 | password: '12345', 247 | database: 'test' 248 | }); 249 | 250 | pool.getNewAdapter(function(db) { 251 | db 252 | .where({ name: 'Martin' }) 253 | .get('people', function(err, results, fields) { 254 | console.log(results); 255 | db.releaseConnection(); 256 | // do not do anything with db that has been released. 257 | }); 258 | }); 259 | 260 | Some more usage examples 261 | ======================== 262 | 263 | Establishing a connection 264 | ------------------------- 265 | 266 | var Db = require('mysql-activerecord'); 267 | var db = new Db.Adapter({ 268 | server: 'localhost', 269 | username: 'root', 270 | password: '12345', 271 | database: 'test' 272 | }); 273 | 274 | Basic SELECT query 275 | ------------------ 276 | 277 | db.get('people', function(err, results, fields) { 278 | console.log(results); 279 | }); 280 | 281 | INSERT query 282 | ------------ 283 | 284 | var data = { 285 | name: 'Martin', 286 | email: 'martin@example.com' 287 | }; 288 | 289 | db.insert('people', data, function(err, info) { 290 | console.log('New row ID is ' + info.insertId); 291 | }); 292 | 293 | INSERT IGNORE query with ON DUPLICATE KEY clause 294 | ------------------------------------------------ 295 | 296 | var data = { 297 | name: 'Martin', 298 | email: 'martin@example.com' 299 | }; 300 | 301 | db.insert_ignore('people', data, function(err, info) { 302 | console.log('New row ID is ' + info.insertId); 303 | }, 'ON DUPLICATE KEY SET counter = counter + 1'); 304 | 305 | SELECT query with WHERE clause 306 | ------------------------------ 307 | 308 | db 309 | .where({ name: 'Martin' }) 310 | .get('people', function(err, results, fields) { 311 | console.log(results); 312 | }); 313 | 314 | SELECT query with custom fields, WHERE, JOIN and LIMIT 315 | ------------------------------------------------------ 316 | 317 | db 318 | .select(['people.id', 'people.name', 'people.email', 'songs.title']) 319 | .join('songs', 'people.favorite_song_id', 'left') 320 | .where({ 321 | 'people.name': 'Martin', 322 | 'songs.title': 'Yesterday' 323 | }) 324 | .limit(5, 10) 325 | .order_by('people.name asc') 326 | .get('people', function(err, results, fields) { 327 | console.log(results); 328 | }); 329 | 330 | Basic counting 331 | ------------------------------------------------------ 332 | 333 | db 334 | .where({ 335 | 'people.name': 'Martin', 336 | 'songs.title': 'Yesterday' 337 | }) 338 | .count('people', function(err, results, fields) { 339 | console.log(results); 340 | }); 341 | 342 | SELECT query with custom fields and GROUP BY 343 | -------------------------------------------- 344 | 345 | db 346 | .select('name, COUNT(name) AS name_count') 347 | .group_by('name') 348 | .order_by('name_count DESC') 349 | .get('people', function(err, results, fields) { 350 | console.log(results); 351 | }); 352 | 353 | Basic UPDATE query 354 | ------------------ 355 | 356 | var newData = { 357 | name: 'John', 358 | email: 'john@doe.com' 359 | }; 360 | 361 | db 362 | .where({ id: 1 }) 363 | .update('people', newData, function(err) { 364 | if (!err) { 365 | console.log('Updated!'); 366 | } 367 | }); 368 | 369 | Basic DELETE query 370 | ------------------ 371 | 372 | db 373 | .where({ id: 1 }) 374 | .delete('people', function(err) { 375 | if (!err) { 376 | console.log('Deleted!') 377 | } 378 | }); 379 | 380 | 381 | Advanced WHERE conditions 382 | ------------------------- 383 | 384 | db 385 | .where("title not like '%Jackson%'") 386 | .where("date_created > '2012-03-10'") 387 | .where({ owner_id: 32 }) 388 | .delete('records', function(err) { 389 | if (!err) { 390 | console.log('Deleted!') 391 | } 392 | }); 393 | 394 | 395 | Contribute 396 | ========== 397 | 398 | Got a missing feature you'd like to use? Found a bug? Go ahead and fork this repo, build the feature and issue a pull request. 399 | 400 | 401 | Licence info 402 | ============ 403 | 404 | Licensed under the GPL license and MIT: 405 | 406 | * http://www.opensource.org/licenses/GPL-license.php 407 | * http://www.opensource.org/licenses/mit-license.php 408 | 409 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MySQL ActiveRecord Adapter for Node.js 3 | * (C) Martin Tajur 2011-2014 4 | * martin@tajur.ee 5 | * 6 | * Active Record Database Pattern implementation for use with node-mysql as MySQL connection driver. 7 | * 8 | * Dual licensed under the MIT and GPL licenses. 9 | * 10 | * Permission is hereby granted, free of charge, to any person obtaining a 11 | * copy of this software and associated documentation files (the 12 | * "Software"), to deal in the Software without restriction, including 13 | * without limitation the rights to use, copy, modify, merge, publish, 14 | * distribute, sublicense, and/or sell copies of the Software, and to 15 | * permit persons to whom the Software is furnished to do so, subject to 16 | * the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included 19 | * in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | * IN NO EVENT SHALL KEVIN VAN ZONNEVELD BE LIABLE FOR ANY CLAIM, DAMAGES 25 | * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 26 | * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | * 29 | **/ 30 | 31 | var Adapter = function(settings) { 32 | 33 | var mysql = require('mysql'); 34 | 35 | var initializeConnectionSettings = function () { 36 | if(settings.server) { 37 | settings.host = settings.server; 38 | } 39 | if(settings.username) { 40 | settings.user = settings.username; 41 | } 42 | 43 | if (!settings.host) { 44 | throw new Error('Unable to start mysql-activerecord - no server given.'); 45 | } 46 | if (!settings.port) { 47 | settings.port = 3306; 48 | } 49 | if (!settings.user) { 50 | settings.user = ''; 51 | } 52 | if (!settings.password) { 53 | settings.password = ''; 54 | } 55 | if (!settings.database) { 56 | throw new Error('Unable to start mysql-activerecord - no database given.'); 57 | } 58 | 59 | return settings; 60 | }; 61 | 62 | var connection; 63 | var connectionSettings; 64 | var pool; 65 | 66 | if (settings && settings.pool) { 67 | pool = settings.pool.pool; 68 | connection = settings.pool.connection; 69 | } else { 70 | connectionSettings = initializeConnectionSettings(); 71 | connection = new mysql.createConnection(connectionSettings); 72 | } 73 | 74 | if (settings.charset) { 75 | connection.query('SET NAMES ' + settings.charset); 76 | } 77 | 78 | var whereClause = {}, 79 | selectClause = [], 80 | orderByClause = '', 81 | groupByClause = '', 82 | havingClause = '', 83 | limitClause = -1, 84 | offsetClause = -1, 85 | joinClause = [], 86 | lastQuery = ''; 87 | 88 | var resetQuery = function(newLastQuery) { 89 | whereClause = {}; 90 | selectClause = []; 91 | orderByClause = ''; 92 | groupByClause = ''; 93 | havingClause = '', 94 | limitClause = -1; 95 | offsetClause = -1; 96 | joinClause = []; 97 | lastQuery = (typeof newLastQuery === 'string' ? newLastQuery : ''); 98 | rawWhereClause = {}; 99 | rawWhereString = {}; 100 | }; 101 | 102 | var rawWhereClause = {}; 103 | var rawWhereString = {}; 104 | 105 | var escapeFieldName = function(str) { 106 | return (typeof rawWhereString[str] === 'undefined' && typeof rawWhereClause[str] === 'undefined' ? '`' + str.replace('.','`.`') + '`' : str); 107 | }; 108 | 109 | var buildDataString = function(dataSet, separator, clause) { 110 | if (!clause) { 111 | clause = 'WHERE'; 112 | } 113 | var queryString = '', y = 1; 114 | if (!separator) { 115 | separator = ', '; 116 | } 117 | var useSeparator = true; 118 | 119 | var datasetSize = getObjectSize(dataSet); 120 | 121 | for (var key in dataSet) { 122 | useSeparator = true; 123 | 124 | if (dataSet.hasOwnProperty(key)) { 125 | if (clause == 'WHERE' && rawWhereString[key] == true) { 126 | queryString += key; 127 | } 128 | else if (dataSet[key] === null) { 129 | queryString += escapeFieldName(key) + (clause == 'WHERE' ? " is NULL" : "=NULL"); 130 | } 131 | else if (typeof dataSet[key] !== 'object') { 132 | queryString += escapeFieldName(key) + "=" + connection.escape(dataSet[key]); 133 | } 134 | else if (typeof dataSet[key] === 'object' && Object.prototype.toString.call(dataSet[key]) === '[object Array]' && dataSet[key].length > 0) { 135 | queryString += escapeFieldName(key) + ' in ("' + dataSet[key].join('", "') + '")'; 136 | } 137 | else { 138 | useSeparator = false; 139 | datasetSize = datasetSize - 1; 140 | } 141 | 142 | if (y < datasetSize && useSeparator) { 143 | queryString += separator; 144 | y++; 145 | } 146 | } 147 | } 148 | if (getObjectSize(dataSet) > 0) { 149 | queryString = ' ' + clause + ' ' + queryString; 150 | } 151 | return queryString; 152 | }; 153 | 154 | var buildJoinString = function() { 155 | var joinString = ''; 156 | 157 | for (var i = 0; i < joinClause.length; i++) { 158 | joinString += (joinClause[i].direction !== '' ? ' ' + joinClause[i].direction : '') + ' JOIN ' + escapeFieldName(joinClause[i].table) + ' ON ' + joinClause[i].relation; 159 | } 160 | 161 | return joinString; 162 | }; 163 | 164 | var mergeObjects = function() { 165 | for (var i = 1; i < arguments.length; i++) { 166 | for (var key in arguments[i]) { 167 | if (arguments[i].hasOwnProperty(key)) { 168 | arguments[0][key] = arguments[i][key]; 169 | } 170 | } 171 | } 172 | return arguments[0]; 173 | }; 174 | 175 | var getObjectSize = function(object) { 176 | var size = 0; 177 | for (var key in object) { 178 | if (object.hasOwnProperty(key)) { 179 | size++; 180 | } 181 | } 182 | return size; 183 | }; 184 | 185 | var trim = function (s) { 186 | var l = 0, r = s.length - 1; 187 | while (l < s.length && s[l] == ' ') { 188 | l++; 189 | } 190 | while (r > l && s[r] == ' ') { 191 | r-=1; 192 | } 193 | return s.substring(l, r + 1); 194 | }; 195 | 196 | this.connectionSettings = function() { return connectionSettings; }; 197 | this.connection = function() { return connection; }; 198 | 199 | this.where = function(whereSet, whereValue, isRaw) { 200 | if (typeof whereSet === 'object' && typeof whereValue === 'undefined') { 201 | whereClause = mergeObjects(whereClause, whereSet); 202 | } 203 | else if ((typeof whereSet === 'string' || typeof whereSet === 'number') && typeof whereValue != 'undefined') { 204 | if (isRaw) { 205 | rawWhereClause[whereSet] = true; 206 | } 207 | whereClause[whereSet] = whereValue; 208 | } 209 | else if ((typeof whereSet === 'string' || typeof whereSet === 'number') && typeof whereValue === 'object' && Object.prototype.toString.call(whereValue) === '[object Array]' && whereValue.length > 0) { 210 | whereClause[whereSet] = whereValue; 211 | } 212 | else if (typeof whereSet === 'string' && typeof whereValue === 'undefined') { 213 | rawWhereString[whereSet] = true; 214 | whereClause[whereSet] = whereValue; 215 | } 216 | return that; 217 | }; 218 | 219 | this.count = function(tableName, responseCallback) { 220 | if (typeof tableName === 'string') { 221 | var combinedQueryString = 'SELECT COUNT(*) as count FROM ' + escapeFieldName(tableName) 222 | + buildJoinString() 223 | + buildDataString(whereClause, ' AND ', 'WHERE'); 224 | 225 | connection.query(combinedQueryString, function(err, res) { 226 | if (err) 227 | responseCallback(err, null); 228 | else 229 | responseCallback(null, res[0]['count']); 230 | }); 231 | resetQuery(combinedQueryString); 232 | } 233 | 234 | return that; 235 | }; 236 | 237 | this.join = function(tableName, relation, direction) { 238 | joinClause.push({ 239 | table: tableName, 240 | relation: relation, 241 | direction: (typeof direction === 'string' ? trim(direction.toUpperCase()) : '') 242 | }); 243 | return that; 244 | }; 245 | 246 | this.select = function(selectSet) { 247 | if (Object.prototype.toString.call(selectSet) === '[object Array]') { 248 | for (var i = 0; i < selectSet.length; i++) { 249 | selectClause.push(selectSet[i]); 250 | } 251 | } 252 | else { 253 | if (typeof selectSet === 'string') { 254 | var selectSetItems = selectSet.split(','); 255 | for (var i = 0; i < selectSetItems.length; i++) { 256 | selectClause.push(trim(selectSetItems[i])); 257 | } 258 | } 259 | } 260 | return that; 261 | }; 262 | 263 | this.comma_separated_arguments = function(set) { 264 | var clause = ''; 265 | if (Object.prototype.toString.call(set) === '[object Array]') { 266 | clause = set.join(', '); 267 | } 268 | else if (typeof set === 'string') { 269 | clause = set; 270 | } 271 | return clause; 272 | }; 273 | 274 | this.group_by = function(set) { 275 | groupByClause = this.comma_separated_arguments(set); 276 | return that; 277 | }; 278 | 279 | this.having = function(set) { 280 | havingClause = this.comma_separated_arguments(set); 281 | return that; 282 | }; 283 | 284 | this.order_by = function(set) { 285 | orderByClause = this.comma_separated_arguments(set); 286 | return that; 287 | }; 288 | 289 | this.limit = function(newLimit, newOffset) { 290 | if (typeof newLimit === 'number') { 291 | limitClause = newLimit; 292 | } 293 | if (typeof newOffset === 'number') { 294 | offsetClause = newOffset; 295 | } 296 | return that; 297 | }; 298 | 299 | this.ping = function() { 300 | connection.ping(); 301 | return that; 302 | }; 303 | 304 | this.insert = function(tableName, dataSet, responseCallback, verb, querySuffix) { 305 | if (typeof verb === 'undefined') { 306 | var verb = 'INSERT'; 307 | } 308 | if (Object.prototype.toString.call(dataSet) !== '[object Array]') { 309 | if (typeof querySuffix === 'undefined') { 310 | var querySuffix = ''; 311 | } 312 | else if (typeof querySuffix !== 'string') { 313 | var querySuffix = ''; 314 | } 315 | 316 | if (typeof tableName === 'string') { 317 | var combinedQueryString = verb + ' into ' + escapeFieldName(tableName) 318 | + buildDataString(dataSet, ', ', 'SET'); 319 | 320 | if (querySuffix != '') { 321 | combinedQueryString = combinedQueryString + ' ' + querySuffix; 322 | } 323 | 324 | connection.query(combinedQueryString, responseCallback); 325 | resetQuery(combinedQueryString); 326 | } 327 | } 328 | else { 329 | doBatchInsert(verb, tableName, dataSet, responseCallback); 330 | } 331 | return that; 332 | }; 333 | 334 | this.insert_ignore = function(tableName, dataSet, responseCallback, querySuffix) { 335 | return this.insert(tableName, dataSet, responseCallback, 'INSERT IGNORE', querySuffix); 336 | }; 337 | 338 | var doBatchInsert = function(verb, tableName, dataSet, responseCallback) { 339 | if (Object.prototype.toString.call(dataSet) !== '[object Array]') { 340 | throw new Error('Array of objects must be provided for batch insert!'); 341 | } 342 | 343 | if (dataSet.length === 0) return false; 344 | 345 | var map = []; 346 | var columns = []; 347 | var escColumns = []; 348 | 349 | for (var aSet in dataSet) { 350 | for (var key in dataSet[aSet]) { 351 | if (columns.indexOf(key) == -1) { 352 | columns.push(key); 353 | escColumns.push(escapeFieldName(key)); 354 | } 355 | } 356 | } 357 | 358 | for (var i = 0; i < dataSet.length; i++) { 359 | (function(i) { 360 | var row = []; 361 | 362 | for (var key in columns) { 363 | if (dataSet[i].hasOwnProperty(columns[key])) { 364 | row.push(that.escape(dataSet[i][columns[key]])); 365 | } else { 366 | row.push('NULL'); 367 | } 368 | } 369 | 370 | if (row.length != columns.length) { 371 | throw new Error('Cannot use batch insert into ' + tableName + ' - fields must match on all rows (' + row.join(',') + ' vs ' + columns.join(',') + ').'); 372 | } 373 | map.push('(' + row.join(',') + ')'); 374 | })(i); 375 | } 376 | 377 | that.query(verb + ' INTO ' + escapeFieldName(tableName) + ' (' + escColumns.join(', ') + ') VALUES' + map.join(','), responseCallback); 378 | return that; 379 | }; 380 | 381 | this.get = function(tableName, responseCallback) { 382 | if (typeof tableName === 'string') { 383 | var combinedQueryString = 'SELECT ' + (selectClause.length === 0 ? '*' : selectClause.join(',')) 384 | + ' FROM ' + escapeFieldName(tableName) 385 | + buildJoinString() 386 | + buildDataString(whereClause, ' AND ', 'WHERE') 387 | + (groupByClause !== '' ? ' GROUP BY ' + groupByClause : '') 388 | + (havingClause !== '' ? ' HAVING ' + havingClause : '') 389 | + (orderByClause !== '' ? ' ORDER BY ' + orderByClause : '') 390 | + (limitClause !== -1 ? ' LIMIT ' + limitClause : '') 391 | + (offsetClause !== -1 ? ' OFFSET ' + offsetClause : ''); 392 | 393 | connection.query(combinedQueryString, responseCallback); 394 | resetQuery(combinedQueryString); 395 | } 396 | 397 | return that; 398 | }; 399 | 400 | this.update = function(tableName, newData, responseCallback) { 401 | if (typeof tableName === 'string') { 402 | var combinedQueryString = 'UPDATE ' + escapeFieldName(tableName) 403 | + buildDataString(newData, ', ', 'SET') 404 | + buildDataString(whereClause, ' AND ', 'WHERE') 405 | + (limitClause !== -1 ? ' LIMIT ' + limitClause : ''); 406 | 407 | connection.query(combinedQueryString, responseCallback); 408 | resetQuery(combinedQueryString); 409 | } 410 | 411 | return that; 412 | }; 413 | 414 | this.escape = function(str) { 415 | return connection.escape(str); 416 | }; 417 | 418 | this.delete = function(tableName, responseCallback) { 419 | if (typeof tableName === 'string') { 420 | var combinedQueryString = 'DELETE FROM ' + escapeFieldName(tableName) 421 | + buildDataString(whereClause, ' AND ', 'WHERE') 422 | + (limitClause !== -1 ? ' LIMIT ' + limitClause : ''); 423 | 424 | connection.query(combinedQueryString, responseCallback); 425 | resetQuery(combinedQueryString); 426 | } 427 | 428 | return that; 429 | }; 430 | 431 | this._last_query = function() { 432 | return lastQuery; 433 | }; 434 | 435 | this.query = function(sqlQueryString, responseCallback) { 436 | connection.query(sqlQueryString, responseCallback); 437 | resetQuery(sqlQueryString); 438 | return that; 439 | }; 440 | 441 | this.disconnect = function() { 442 | return connection.end(); 443 | }; 444 | 445 | this.forceDisconnect = function() { 446 | return connection.destroy(); 447 | }; 448 | 449 | this.releaseConnection = function() { 450 | pool.releaseConnection(connection); 451 | }; 452 | 453 | this.releaseConnection = function() { 454 | pool.releaseConnection(connection); 455 | }; 456 | 457 | var reconnectingTimeout = false; 458 | 459 | function handleDisconnect(connectionInstance) { 460 | connectionInstance.on('error', function(err) { 461 | if (!err.fatal || reconnectingTimeout) { 462 | return; 463 | } 464 | 465 | if (err.code !== 'PROTOCOL_CONNECTION_LOST' && err.code !== 'ECONNREFUSED') { 466 | throw err; 467 | } 468 | 469 | if (settings.reconnectTimeout === false) return; 470 | 471 | var reconnectingTimeout = setTimeout(function() { 472 | connection = mysql.createConnection(connectionInstance.config); 473 | handleDisconnect(connection); 474 | connection.connect(); 475 | }, settings.reconnectTimeout || 2000); 476 | }); 477 | } 478 | 479 | if (!pool) { 480 | handleDisconnect(connection); 481 | } 482 | 483 | var that = this; 484 | 485 | return this; 486 | }; 487 | 488 | var mysqlPool; // this should be initialized only once. 489 | var mysqlCharset; 490 | 491 | var Pool = function (settings) { 492 | if (!mysqlPool) { 493 | var mysql = require('mysql'); 494 | 495 | var poolOption = { 496 | createConnection: settings.createConnection, 497 | waitForConnections: settings.waitForConnections, 498 | connectionLimit: settings.connectionLimit, 499 | queueLimit: settings.queueLimit 500 | }; 501 | Object.keys(poolOption).forEach(function (element) { 502 | // Avoid pool option being used by mysql connection. 503 | delete settings[element]; 504 | // Also remove undefined elements from poolOption 505 | if (!poolOption[element]) { 506 | delete poolOption[element]; 507 | } 508 | }); 509 | 510 | // Confirm settings with Adapter. 511 | var db = new Adapter(settings); 512 | var connectionSettings = db.connectionSettings(); 513 | 514 | Object.keys(connectionSettings).forEach(function (element) { 515 | poolOption[element] = connectionSettings[element]; 516 | }); 517 | 518 | mysqlPool = mysql.createPool(poolOption); 519 | mysqlCharset = settings.charset; 520 | } 521 | 522 | this.pool = function () { 523 | return mysqlPool; 524 | }; 525 | 526 | this.getNewAdapter = function (responseCallback) { 527 | mysqlPool.getConnection(function (err, connection) { 528 | if (err) { 529 | throw err; 530 | } 531 | var adapter = new Adapter({ 532 | pool: { 533 | pool: mysqlPool, 534 | enabled: true, 535 | connection: connection 536 | }, 537 | charset: mysqlCharset 538 | }); 539 | responseCallback(adapter); 540 | }); 541 | }; 542 | 543 | this.disconnect = function (responseCallback) { 544 | this.pool().end(responseCallback); 545 | }; 546 | 547 | return this; 548 | }; 549 | 550 | exports.Adapter = Adapter; 551 | exports.Pool = Pool; 552 | --------------------------------------------------------------------------------