├── .gitignore ├── History.md ├── LICENSE ├── README.md ├── examples └── generate.js ├── index.js ├── lib └── builder.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.tmp 3 | *~ 4 | node_modules 5 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2.0.0 / 2015-04-08 2 | ================== 3 | 4 | * Database 1.10.6 support: 5 | ** Deprecated ```fnRowFormatter``` and ```oRowFormatterParams``` (use DataTables callbacks instead). 6 | ** Deprecated ```aoColumnDefs``` (this information now comes from the DataTables AJAX request). 7 | * ```buildQuery``` now returns an object with ```use```, ```data```, ```recordsTotal```, and ```recordsFiltered keys```, and ```parseResponse``` consumes it. 8 | * Added Oracle support 9 | 10 | 1.1.2 / 2013-12-10 11 | ================== 12 | 13 | * Cast sEcho to int to prevent xss exploits 14 | 15 | 1.1.1 / 2013-09-02 16 | ================== 17 | 18 | * Fixed to use ```sAjaxDataProp``` in debug method 19 | 20 | 1.1.0 / 2013-08-22 21 | ================== 22 | 23 | * Added PostgreSQL support, courtesy Eric Chauty 24 | * Added ```sAjaxDataProp``` property to constructor options to allow result property name to be set (defaults to 'aaData') 25 | * Fixed issue where return was incorrectly limited when iDisplayLength is set to -1 26 | 27 | 1.0.2 / 2013-08-01 28 | ================== 29 | 30 | * Sanitize sort order to produce ASC or DESC 31 | 32 | 1.0.1 / 2013-02-01 33 | ================== 34 | 35 | * Do more thorough sanitization of input parameters 36 | 37 | 1.0.0 / 2013-01-25 38 | ================== 39 | 40 | * Bumped version to reflect stability. There were no changes from v0.0.6. 41 | 42 | 0.0.6 / 2012-10-14 43 | ================== 44 | 45 | * Commented out debug statements 46 | 47 | 0.0.5 / 2012-10-12 48 | ================== 49 | 50 | * Added oRowFormatterParams to fnRowFormatter function 51 | 52 | 0.0.4 / 2012-10-08 53 | ================== 54 | 55 | * Fix bug where LIMIT initial 0 not set if iDisplayStart is 0 56 | 57 | 0.0.1 / 2012-10-06 58 | ================== 59 | 60 | * Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Jim Pravetz 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-datatable 2 | ============== 3 | 4 | A Node.js implementation of a server-side processor for the jQuery DataTables plug-in. 5 | 6 | The node-datatable module provides backend query generation and result parsing to support 7 | [DataTables](https://www.datatables.net/manual/server-side) server-side processing for SQL databases. 8 | This module does not connect to nor query databases, instead leaving those tasks to the calling application. 9 | SQL querying has been separated so that the caller can leverage his or her existing module choices for connection pools, 10 | database interfaces, and the like. This module has been used with [node-mysql](https://github.com/felixge/node-mysql), 11 | [sequelize](http://sequelizejs.com), and [strong-oracle](https://github.com/strongloop/strong-oracle). 12 | 13 | An incomplete code example: 14 | 15 | ```javascript 16 | var async = require('async'), 17 | QueryBuilder = require('datatable'); 18 | 19 | var tableDefinition = { 20 | sTableName: 'Orgs' 21 | }; 22 | 23 | var queryBuilder = new QueryBuilder(tableDefinition); 24 | 25 | // requestQuery is normally provided by the DataTables AJAX call 26 | var requestQuery = { 27 | iDisplayStart: 0, 28 | iDisplayLength: 5 29 | }; 30 | 31 | // Build an object of SQL statements 32 | var queries = queryBuilder.buildQuery(requestQuery); 33 | 34 | // Connect to the database 35 | var myDbObject = ...; 36 | 37 | // Execute the SQL statements generated by queryBuilder.buildQuery 38 | myDbObject.query(queries.changeDatabaseOrSchema, function(err){ 39 | if (err) { res.error(err); } 40 | else{ 41 | async.parallel( 42 | { 43 | recordsFiltered: function(cb) { 44 | myDbObject.query(queries.recordsFiltered, cb); 45 | }, 46 | recordsTotal: function(cb) { 47 | myDbObject.query(queries.recordsTotal, cb); 48 | }, 49 | select: function(cb) { 50 | myDbObject.query(queries.select, cb); 51 | } 52 | }, 53 | function(err, results) { 54 | if (err) { res.error(err); } 55 | else { 56 | res.json(queryBuilder.parseResponse(results)); 57 | } 58 | } 59 | ); 60 | } 61 | }); 62 | ``` 63 | 64 | ## API ## 65 | 66 | The source code contains comments that may help you understand this module. 67 | 68 | ### Constructor ### 69 | 70 | Construct a QueryBuilder object. 71 | 72 | #### Parameters #### 73 | 74 | The node-datatable constructor takes an object parameter containing 75 | the following properties. In the simplest case, only the first 76 | two options will be necessary. 77 | 78 | - ```dbType``` - (Default: ```"mysql"```) The database language to use for queries 79 | (```"mysql"```, ```"postgres"```, or ```"oracle"```). The default value is ```mysql```. 80 | 81 | - ```sTableName``` - The name of the table in the database where a 82 | JOIN is not used. If a JOIN is used, set ```sSelectSql```. 83 | 84 | - ```sCountColumnName``` (Default: ```"id"```) The name of the column on which to 85 | do a SQL COUNT(). This is overridden with ```*``` when ```sSelectSql``` is set. 86 | 87 | - ```sDatabaseOrSchema``` - If set, ```buildQuery``` will add a ```changeDatabaseOrSchema``` 88 | property to the object returned containing a _USE sDatabaseOrSchema_ statement for 89 | MySQL / Postgres or an _ALTER SESSION SET CURRENT_SCHEMA = sDatabaseOrSchema_ statement for Oracle. 90 | 91 | - ```aSearchColumns``` - In database queries where JOIN is used, you may wish to specify an 92 | alternate array of column names that the search string will be applied against. Example: 93 | 94 | ```javascript 95 | aSearchColumns: ["table3.username", "table1.timestamp", "table1.urlType", "table1.mimeType", "table1.url", "table2.description"], 96 | ``` 97 | 98 | - ```sSelectSql``` - (Default: ```"id"```) A list of columns that will be 99 | selected. This can be used in combination with joins (see ```sFromSql```). 100 | 101 | - ```sFromSql``` - If set, this is used as the FROM clause of the SELECT 102 | statement. If not set, ```sTableName``` is used. Use this for more complex 103 | queries, for example when using JOIN. Example when using a double JOIN: 104 | 105 | ```javascript 106 | "table1 LEFT JOIN table2 ON table1.errorId=table2.errorId LEFT JOIN table3 ON table1.sessionId=table3.sessionId" 107 | ``` 108 | 109 | - ```sWhereAndSql``` - Use this to specify arbitrary SQL that you 110 | wish to append to the generated WHERE clauses with an AND condition. 111 | 112 | - ```sDateColumnName``` - If this property and ```dateFrom``` and/or ```dateTo``` is set, a date range WHERE clause 113 | will be added to the SQL query. This should be set to the name of the datetime column that is to be used in the clause. 114 | 115 | - ```dateFrom``` - If set, the query will return records greater than or equal to this date. 116 | 117 | - ```dateTo``` - If set, the query will return records less than or equal to this date. 118 | 119 | #### Returns ##### 120 | 121 | The query builder object. 122 | 123 | Example: 124 | 125 | ```javascript 126 | var queryBuilder = new QueryBuilder({ 127 | sTableName: 'user' 128 | }); 129 | ``` 130 | 131 | ### buildQuery ### 132 | 133 | Builds an object containing the following properties: 134 | 135 | ```changeDatabaseOrSchema```: _(Optional, if ```sDatabaseOrSchema``` is set)_ A USE sDatabaseOrSchema_ statement for 136 | MySQL / Postgres or an _ALTER SESSION SET CURRENT_SCHEMA = sDatabaseOrSchema_ statement for Oracle. 137 | 138 | ```recordsFiltered```: _(Optional, if ```requestQuery.search.value``` is set)_ A SELECT statement that counts 139 | the number of filtered entries in the database. This is used to calculate the ```recordsFiltered``` return value. 140 | 141 | ```recordsTotal```: A SELECT statement that counts the total number of unfiltered 142 | entries in the database. This is used to calculate the ```recordsTotal``` return value. 143 | 144 | ```select```: A SELECT statement that returns a page of filtered records from the database. 145 | This will use a LIMIT statement for MySQL / Postgres or a top-n query for Oracle. 146 | 147 | Note that #2, #3, and #4 will include date filtering as well as any other filtering specified in ```sWhereAndSql```. 148 | 149 | #### Parameters #### 150 | 151 | - ```requestQuery```: An object containing the properties set by the client-side DataTables 152 | library as defined in [sent parameters](https://www.datatables.net/manual/server-side#Sent-parameters). 153 | Note that you may build your own ```requestQuery```, omitting certain properties, and achieve a different outcome. 154 | For example, passing an empty ```requestQuery``` object will build a select statement that gets all rows from the 155 | table. Such a scenario could be useful for building a custom file export function. 156 | 157 | #### Returns ##### 158 | 159 | The resultant object of query strings. The ```changeDatabaseOrSchema``` query should be executed first, and the others 160 | can be executed in sequence or (ideally) in parallel. Each database response should be collected into an object property 161 | having a key that matches the query object. The response object can later be passed to the ```parseReponse``` function. 162 | 163 | Example: 164 | 165 | ```javascript 166 | var queries = queryBuilder.buildQuery(oRequestQuery); 167 | ``` 168 | 169 | ### parseResponse ### 170 | 171 | Parses an object of responses that were received in response to each of the queries generated by ```buildQuery```. 172 | 173 | #### Parameters #### 174 | 175 | - ```queryResult```: The object of query responses. 176 | 177 | #### Returns ##### 178 | 179 | An object containing the properties defined in [returned data](https://www.datatables.net/manual/server-side#Returned-data). 180 | 181 | Example: 182 | 183 | ```javascript 184 | var result = queryBuilder.parseResponse(queryResponseObject); 185 | res.json(result); 186 | ``` 187 | 188 | ### extractResponseVal ### 189 | 190 | Extract a value from a database response. This is useful in situations where your 191 | database query returns a primitive value nested inside of an object inside of an array: 192 | 193 | #### Parameters #### 194 | 195 | - ```res```: A database response. 196 | 197 | #### Returns ##### 198 | 199 | The first enumerable object property of the first element in an array, or undefined 200 | 201 | Example: 202 | 203 | ```javascript 204 | var val = queryBuilder.extractResponseVal([{COUNT(ID): 13}]); 205 | console.log(val) //13 206 | ``` 207 | 208 | ## Database queries involving JOIN ## 209 | 210 | Example using ```sSelectSql``` and ```sFromSql``` to create a JOIN query: 211 | 212 | ```javascript 213 | { 214 | sSelectSql: "table3.username,table1.timestamp,urlType,mimeType,table1.table3Id,url,table2.code,table2.description", 215 | sFromSql: "table1 LEFT JOIN table2 ON table1.errorId=table2.errorId LEFT JOIN table3 ON table1.sessionId=table3.sessionId", 216 | } 217 | ``` 218 | 219 | ### Contributors ### 220 | 221 | * [Matthew Hasbach](https://github.com/mjhasbach) 222 | * [Benjamin Flesch](https://github.com/bf) 223 | 224 | ## TODO ## 225 | 226 | 1. Add an additional parameter to allow more then the requested number of records 227 | to be returned. This can be used to reduce the number of client-server calls (I think). 228 | 2. A more thorough SQL injection security review (volunteers?). 229 | 3. Unit tests (the original author is no longer working on the project that uses this module, so need volunteer) 230 | 231 | ## References ## 232 | 233 | 1. [Datatables Manual](http://www.datatables.net/manual/server-side) 234 | 2. [How to Handle large datasets](http://datatables.net/forums/discussion/4214/solved-how-to-handle-large-datasets/p1) 235 | -------------------------------------------------------------------------------- /examples/generate.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * generate 3 | * https://github.com/jpravetz/node-datatable 4 | * Copyright(c) 2012 Jim Pravetz 5 | * node-datatable may be freely distributed under the MIT license. 6 | */ 7 | 8 | var QueryBuilder = require('../index.js'); 9 | 10 | var oTableDef = { 11 | sTableName: "Orgs", 12 | aoColumnDefs: [ 13 | { mData: "o", bSearchable: true }, 14 | { mData: "cn", bSearchable: true }, 15 | { mData: "support" }, 16 | { mData: "email" } 17 | ] 18 | }; 19 | 20 | var oDatatableParams = { 21 | iDisplayStart: 0, 22 | iDisplayLength: 4, 23 | "sSearch": "", 24 | "bRegex": "false", 25 | "sSearch_0": "", 26 | "bRegex_0": "false", 27 | "bSearchable_0": "true", 28 | "sSearch_1": "", 29 | "bRegex_1": "false", 30 | "bSearchable_1": "true", 31 | "sSearch_2": "", 32 | "bRegex_2": "false", 33 | "bSearchable_2": "true", 34 | "sSearch_3": "", 35 | "bRegex_3": "false", 36 | "bSearchable_3": "true", 37 | "iSortCol_0": "0", 38 | "sSortDir_0": "asc", 39 | "iSortingCols": "1", 40 | "bSortable_0": "true", 41 | "bSortable_1": "true", 42 | "bSortable_2": "true", 43 | "bSortable_3": "true" 44 | }; 45 | 46 | generate( oTableDef, oDatatableParams ); 47 | oDatatableParams.sSearch = 'hello'; 48 | generate( oTableDef, oDatatableParams ); 49 | oDatatableParams.iSortCol_0 = 2; 50 | oDatatableParams.sSortDir_0 = 'desc'; 51 | generate( oTableDef, oDatatableParams ); 52 | oDatatableParams.iDisplayStart = 30; 53 | oDatatableParams.iDisplayLength = 15; 54 | generate( oTableDef, oDatatableParams ); 55 | 56 | function generate( oTableDef, oDatatableParams ) { 57 | var queryBuilder = new QueryBuilder( oTableDef ); 58 | var queries = queryBuilder.buildQuery( oDatatableParams ); 59 | console.log( "Queries:\n %s", queries.join("\n ") ); 60 | } 61 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/builder'); -------------------------------------------------------------------------------- /lib/builder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-datatable 3 | * https://github.com/jpravetz/node-datatable 4 | * Copyright(c) 2012-2013 Jim Pravetz 5 | * node-datatable may be freely distributed under the MIT license. 6 | */ 7 | 8 | var _u = require('underscore'); 9 | 10 | var DEFAULT_LIMIT = 100; 11 | 12 | /** 13 | * Constructor 14 | * @param options Refer to README.md for a list of properties 15 | * @return {Object} 16 | */ 17 | module.exports = function (options) { 18 | 19 | var self = { 20 | sTableName: options.sTableName, 21 | sCountColumnName: options.sCountColumnName, // Name of column to use when counting total number of rows. Defaults to "id" 22 | sDatabaseOrSchema: options.sDatabaseOrSchema, // Add a "USE" statement for MySQL / Postgres or "ALTER SESSION SET CURRENT_SCHEMA" statement for Oracle. 23 | aSearchColumns: options.aSearchColumns || [], // Used to determine names of columns to search 24 | sSelectSql: options.sSelectSql, // alternate select statement 25 | sFromSql: options.sFromSql, // alternate select statement 26 | sWhereAndSql: options.sWhereAndSql, // Custom caller SQL, added as AND where to add date range or other checks (caller must write the SQL) 27 | sGroupBySql: options.sGroupBySql || [], // Custom caller SQL, added as GROUP BY statement 28 | sDateColumnName: options.sDateColumnName, // If set then only get entries within the range (can use sWhereSql instead) 29 | dateFrom: options.dateFrom, // Only retrieve content from before this date. sDateColumnName must be set. 30 | dateTo: options.dateTo, // Only retrieve content from after this date. sDateColumnName must be set. 31 | oRequestQuery: options.oRequestQuery, // Usually passed in with buildQuery 32 | sAjaxDataProp: 'data', // The name of the data prop to set on the return value 33 | 34 | dbType: options.dbType, // "postgres" or "oracle", defaults to MySQL syntax 35 | 36 | buildQuery: buildQuery, 37 | parseResponse: parseResponse, 38 | extractResponseVal: extractResponseVal, 39 | filteredResult: filteredResult 40 | }; 41 | 42 | /** 43 | * (private) Build an optional "USE sDatabaseOrSchema" for MySQL / Postgres or 44 | * "ALTER SESSION SET CURRENT_SCHEMA = sDatabaseOrSchema" statement for Oracle if sDatabaseOrSchema is set. 45 | * @return {string|undefined} The SQL statement or undefined 46 | */ 47 | function buildSetDatabaseOrSchemaStatement() { 48 | if (self.sDatabaseOrSchema){ 49 | if (self.dbType === 'oracle'){ 50 | return 'ALTER SESSION SET CURRENT_SCHEMA = ' + self.sDatabaseOrSchema; 51 | } 52 | else{ 53 | return "USE " + self.sDatabaseOrSchema; 54 | } 55 | } 56 | } 57 | 58 | /** 59 | * (private) Build the date partial that is used in a WHERE clause 60 | * @return {*} 61 | */ 62 | function buildDatePartial() { 63 | if (self.sDateColumnName && self.dateFrom || self.dateTo) { 64 | // console.log( "DateFrom %s to %s", self.dateFrom, self.dateTo ); 65 | if (self.dateFrom && self.dateTo) { 66 | return self.sDateColumnName + " BETWEEN '" + self.dateFrom.toISOString() + "' AND '" + self.dateTo.toISOString() + "'"; 67 | } else if (self.dateFrom) { 68 | return self.sDateColumnName + " >= '" + self.dateFrom.toISOString() + "'"; 69 | } else if (self.dateTo) { 70 | return self.sDateColumnName + " <= '" + self.dateTo.toISOString() + "'"; 71 | } 72 | } 73 | return undefined; 74 | } 75 | 76 | /** 77 | * (private) Build a complete SELECT statement that counts the number of entries. 78 | * @param searchString If specified then produces a statement to count the filtered list of records. 79 | * Otherwise the statement counts the unfiltered list of records. 80 | * @return {String} A complete SELECT statement 81 | */ 82 | function buildCountStatement(requestQuery) { 83 | var dateSql = buildDatePartial(); 84 | var result; 85 | if(self.sGroupBySql.length) { 86 | result = "SELECT COUNT(*) FROM ("; 87 | result += "SELECT COUNT(" + self.sGroupBySql[0] + ") FROM "; 88 | result += self.sFromSql ? self.sFromSql : self.sTableName; 89 | result += buildWherePartial(requestQuery); 90 | result += buildGroupByPartial(); 91 | result += ") temp"; 92 | } else { 93 | result = "SELECT COUNT("; 94 | result += self.sSelectSql ? "*" : (self.sCountColumnName ? self.sCountColumnName : "id"); 95 | result += ") FROM "; 96 | result += self.sFromSql ? self.sFromSql : self.sTableName; 97 | result += buildWherePartial(requestQuery); 98 | } 99 | // var sSearchQuery = buildSearchPartial( sSearchString ); 100 | // var sWheres = sSearchQuery ? [ sSearchQuery ] : []; 101 | // if( self.sWhereAndSql ) 102 | // sWheres.push( self.sWhereAndSql ) 103 | // if( dateSql ) 104 | // sWheres.push( dateSql ); 105 | // if( sWheres.length ) 106 | // result += " WHERE (" + sWheres.join( ") AND (" ) + ")"; 107 | return result; 108 | } 109 | 110 | /** 111 | * (private) Build the WHERE clause 112 | * otherwise uses aoColumnDef mData property. 113 | * @param searchString 114 | * @return {String} 115 | */ 116 | function buildWherePartial(requestQuery) { 117 | var sWheres = []; 118 | var searchQuery = buildSearchPartial(requestQuery); 119 | if (searchQuery) 120 | sWheres.push(searchQuery); 121 | if (self.sWhereAndSql) 122 | sWheres.push(self.sWhereAndSql); 123 | var dateSql = buildDatePartial(); 124 | if (dateSql) 125 | sWheres.push(dateSql); 126 | if (sWheres.length) 127 | return " WHERE (" + sWheres.join(") AND (") + ")"; 128 | return ""; 129 | } 130 | 131 | /** 132 | * (private) Build the GROUP BY clause 133 | * @return {String} 134 | */ 135 | function buildGroupByPartial() { 136 | if (self.sGroupBySql.length) 137 | return " GROUP BY " + self.sGroupBySql.join(',') + " "; 138 | return ""; 139 | } 140 | 141 | /** 142 | * (private) Builds the search portion of the WHERE clause using LIKE (or ILIKE for PostgreSQL). 143 | * @param {Object} requestQuery The datatable parameters that are generated by the client 144 | * @return {String} A portion of a WHERE clause that does a search on all searchable row entries. 145 | */ 146 | function buildSearchPartial(requestQuery) { 147 | var searches = [], 148 | colSearches = buildSearchArray(requestQuery), 149 | globalSearches = buildSearchArray(requestQuery, true); 150 | 151 | if (colSearches.length){ 152 | searches.push('(' + colSearches.join(" AND ") + ')'); 153 | } 154 | 155 | if (globalSearches.length){ 156 | searches.push('(' + globalSearches.join(" OR ") + ')'); 157 | } 158 | 159 | return searches.join(" AND "); 160 | } 161 | 162 | /** 163 | * (private) Builds an array of LIKE / ILIKE statements to be added to the WHERE clause 164 | * @param {Object} requestQuery The datatable parameters that are generated by the client 165 | * @param {*} [global] If truthy, build a global search array. If falsy, build a column search array 166 | * @returns {Array} An array of LIKE / ILIKE statements 167 | */ 168 | function buildSearchArray(requestQuery, global) { 169 | var searchArray = [], 170 | customColumns = _u.isArray(self.aSearchColumns) && !_u.isEmpty(self.aSearchColumns) && global; 171 | 172 | _u.each(customColumns ? self.aSearchColumns : requestQuery.columns, function(column){ 173 | if (customColumns || column.searchable === 'true'){ 174 | var colName = sanitize(customColumns ? column : column.name), 175 | searchVal = sanitize(global ? requestQuery.search.value : column.search.value); 176 | 177 | if (colName && searchVal){ 178 | searchArray.push(self.dbType === 'postgres' ? 179 | buildILIKESearch(colName, searchVal) : 180 | buildLIKESearch(colName, searchVal)); 181 | } 182 | } 183 | }); 184 | 185 | return searchArray; 186 | } 187 | 188 | /** 189 | * (private) Builds the search portion of the WHERE clause using ILIKE 190 | * @param {string} colName The column to search 191 | * @param {string} searchVal The value to search for 192 | * @returns {string} An ILIKE statement to be added to the where clause 193 | */ 194 | function buildILIKESearch(colName, searchVal) { 195 | return "CAST(" + colName + " as text)" + " ILIKE '%" + searchVal + "%'"; 196 | } 197 | 198 | /** 199 | * (private) Builds the search portion of the WHERE clause using LIKE 200 | * @param {string} colName The column to search 201 | * @param {string} searchVal The value to search for 202 | * @returns {string} A LIKE statement to be added to the where clause 203 | */ 204 | function buildLIKESearch(colName, searchVal) { 205 | return colName + " LIKE '%" + searchVal + "%'"; 206 | } 207 | 208 | /** 209 | * (private) Adds an ORDER clause 210 | * @param requestQuery The Datatable query string (we look at sort direction and sort columns) 211 | * @return {String} The ORDER clause 212 | */ 213 | function buildOrderingPartial(requestQuery) { 214 | var query = []; 215 | var l = _u.isArray(requestQuery.order) ? requestQuery.order.length : 0; 216 | for (var fdx = 0; fdx < l; ++fdx) { 217 | var order = requestQuery.order[fdx], 218 | column = requestQuery.columns[order.column]; 219 | 220 | if (column.orderable === 'true' && column.name) { 221 | query.push(column.name + " " + order.dir); 222 | } 223 | } 224 | if (query.length) 225 | return " ORDER BY " + query.join(", "); 226 | return ""; 227 | } 228 | 229 | /** 230 | * Build a LIMIT clause 231 | * @param requestQuery The Datatable query string (we look at length and start) 232 | * @return {String} The LIMIT clause 233 | */ 234 | function buildLimitPartial(requestQuery) { 235 | var sLimit = ""; 236 | if (requestQuery && requestQuery.start !== undefined && self.dbType !== 'oracle') { 237 | var start = parseInt(requestQuery.start, 10); 238 | if (start >= 0) { 239 | var len = parseInt(requestQuery.length, 10); 240 | sLimit = (self.dbType === 'postgres') ? " OFFSET " + String(start) + " LIMIT " : " LIMIT " + String(start) + ", "; 241 | sLimit += ( len > 0 ) ? String(len) : String(DEFAULT_LIMIT); 242 | } 243 | } 244 | return sLimit; 245 | } 246 | 247 | /** 248 | * Build the base SELECT statement. 249 | * @return {String} The SELECT partial 250 | */ 251 | function buildSelectPartial() { 252 | var query = "SELECT "; 253 | query += self.sSelectSql ? self.sSelectSql : "*"; 254 | query += " FROM "; 255 | query += self.sFromSql ? self.sFromSql : self.sTableName; 256 | return query; 257 | } 258 | 259 | /** 260 | * Build an array of query strings based on the Datatable parameters 261 | * @param requestQuery The datatable parameters that are generated by the client 262 | * @return {Object} An array of query strings, each including a terminating semicolon. 263 | */ 264 | function buildQuery(requestQuery) { 265 | var queries = {}; 266 | if (typeof requestQuery !== 'object') 267 | return queries; 268 | var searchString = sanitize(_u.isObject(requestQuery.search) ? requestQuery.search.value : ''); 269 | self.oRequestQuery = requestQuery; 270 | var useStmt = buildSetDatabaseOrSchemaStatement(); 271 | if (useStmt) { 272 | queries.changeDatabaseOrSchema = useStmt; 273 | } 274 | queries.recordsTotal = buildCountStatement(requestQuery); 275 | if (searchString) { 276 | queries.recordsFiltered = buildCountStatement(requestQuery); 277 | } 278 | var query = buildSelectPartial(); 279 | query += buildWherePartial(requestQuery); 280 | query += buildGroupByPartial(); 281 | query += buildOrderingPartial(requestQuery); 282 | query += buildLimitPartial(requestQuery); 283 | if (self.dbType === 'oracle'){ 284 | var start = parseInt(requestQuery.start, 10); 285 | var len = parseInt(requestQuery.length, 10); 286 | if (len >= 0 && start >= 0) { 287 | query = 'SELECT * FROM (SELECT a.*, ROWNUM rnum FROM (' + query + ') '; 288 | query += 'a)' + ' WHERE rnum BETWEEN ' + (start + 1) + ' AND ' + (start + len); 289 | } 290 | } 291 | queries.select = query; 292 | return queries; 293 | } 294 | 295 | /** 296 | * Parse the responses from the database and build a Datatable response object. 297 | * @param queryResult An array of SQL response objects, each of which must, in order, correspond with a query string 298 | * returned by buildQuery. 299 | * @return {Object} A Datatable reply that is suitable for sending in a response to the client. 300 | */ 301 | function parseResponse(queryResult) { 302 | var oQuery = self.oRequestQuery; 303 | var result = { recordsFiltered: 0, recordsTotal: 0 }; 304 | if (oQuery && typeof oQuery.draw === 'string') { 305 | // Cast for security reasons, as per http://datatables.net/usage/server-side 306 | result.draw = parseInt(oQuery.draw,10); 307 | } else { 308 | result.draw = 0; 309 | } 310 | if (_u.isObject(queryResult) && _u.keys(queryResult).length > 1) { 311 | result.recordsFiltered = result.recordsTotal = extractResponseVal(queryResult.recordsTotal) || 0; 312 | if (queryResult.recordsFiltered) { 313 | result.recordsFiltered = extractResponseVal(queryResult.recordsFiltered) || 0; 314 | } 315 | result.data = queryResult.select; 316 | } 317 | return result; 318 | } 319 | 320 | /** 321 | * (private) Extract the value from a database response 322 | * @param {Array} res A database response array 323 | * @return {*} 324 | */ 325 | function extractResponseVal(res) { 326 | if (_u.isArray(res) && res.length && _u.isObject(res[0])) { 327 | var resObj = _u.values(res[0]); 328 | 329 | if (resObj.length) { 330 | return resObj[0]; 331 | } 332 | } 333 | } 334 | 335 | /** 336 | * Debug, reduced size object for display 337 | * @param obj 338 | * @return {*} 339 | */ 340 | function filteredResult(obj, count) { 341 | if (obj) { 342 | var result = _u.omit(obj, self.sAjaxDataProp ); 343 | result.aaLength = obj[self.sAjaxDataProp] ? obj[self.sAjaxDataProp].length : 0; 344 | result[self.sAjaxDataProp] = []; 345 | var count = count ? Math.min(count, result.aaLength) : result.aaLength; 346 | for (var idx = 0; idx < count; ++idx) { 347 | result[self.sAjaxDataProp].push(obj[self.sAjaxDataProp][idx]); 348 | } 349 | return result; 350 | } 351 | return null; 352 | } 353 | 354 | return self; 355 | } 356 | 357 | /** 358 | * Sanitize to prevent SQL injections. 359 | * @param str 360 | * @return {*} 361 | */ 362 | function sanitize(str, len) { 363 | len = len || 256; 364 | if (!str || typeof str === 'string' && str.length < 1) 365 | return str; 366 | if (typeof str !== 'string' || str.length > len) 367 | return null; 368 | return str.replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, function (char) { 369 | switch (char) { 370 | case "\0": 371 | return "\\0"; 372 | case "\x08": 373 | return "\\b"; 374 | case "\x09": 375 | return "\\t"; 376 | case "\x1a": 377 | return "\\z"; 378 | case "\n": 379 | return "\\n"; 380 | case "\r": 381 | return "\\r"; 382 | case "\"": 383 | case "'": 384 | case "\\": 385 | case "%": 386 | return "\\" + char; // prepends a backslash to backslash, percent, 387 | // and double/single quotes 388 | } 389 | }); 390 | } 391 | 392 | /* Example datatable querystring = { 393 | "draw": "1", 394 | "iColumns": "4", 395 | "sColumns": "", 396 | "iDisplayStart": "0", 397 | "iDisplayLength": "10", 398 | "mDataProp_0": "0", 399 | "mDataProp_1": "1", 400 | "mDataProp_2": "2", 401 | "mDataProp_3": "3", 402 | "sSearch": "", 403 | "bRegex": "false", 404 | "sSearch_0": "", 405 | "bRegex_0": "false", 406 | "bSearchable_0": "true", 407 | "sSearch_1": "", 408 | "bRegex_1": "false", 409 | "bSearchable_1": "true", 410 | "sSearch_2": "", 411 | "bRegex_2": "false", 412 | "bSearchable_2": "true", 413 | "sSearch_3": "", 414 | "bRegex_3": "false", 415 | "bSearchable_3": "true", 416 | "iSortCol_0": "0", 417 | "sSortDir_0": "asc", 418 | "iSortingCols": "1", 419 | "bSortable_0": "true", 420 | "bSortable_1": "true", 421 | "bSortable_2": "true", 422 | "bSortable_3": "true", 423 | "_": "1349495139702" 424 | } 425 | */ 426 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datatable", 3 | "version": "2.0.2", 4 | "description": "Server-side processing for JQuery Datatable plug-in", 5 | "author": { 6 | "name": "Jim Pravetz", 7 | "email": "jpravetz@epdoc.com" 8 | }, 9 | "contributors": [ 10 | { 11 | "name": "Matthew Hasbach", 12 | "email": "hasbach.git@gmail.com" 13 | } 14 | ], 15 | "maintainers": [ 16 | { 17 | "name": "Jim Pravetz", 18 | "email": "jpravetz@epdoc.com" 19 | } 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/jpravetz/node-datatable.git" 24 | }, 25 | "keywords": [ 26 | "jquery", 27 | "datatable", 28 | "sever-side processor" 29 | ], 30 | "dependencies": { 31 | "underscore": "1.4.x" 32 | }, 33 | "engines": { 34 | "node": ">= 0.6.x" 35 | } 36 | } 37 | --------------------------------------------------------------------------------