├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .nvmrc ├── Gruntfile.js ├── README.md ├── index.js ├── package.json └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 4 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = true 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Ignore this files 3 | ############################################################################### 4 | 5 | *~ 6 | .* 7 | *.log 8 | 9 | ############################################################################### 10 | # Ignore this directories 11 | ############################################################################### 12 | 13 | node_modules 14 | 15 | ############################################################################### 16 | # Accept this files 17 | ############################################################################### 18 | 19 | !.editorconfig 20 | !.gitignore 21 | !.jscsrc 22 | !.jshintrc 23 | !.nvmrc 24 | !.gitattributes 25 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowEmptyBlocks": true, 3 | "disallowFunctionDeclarations": true, 4 | "disallowImplicitTypeConversion": [ 5 | "numeric", 6 | "boolean", 7 | "binary", 8 | "string" 9 | ], 10 | "disallowKeywords": ["with"], 11 | "disallowMultipleLineStrings": true, 12 | "disallowNewlineBeforeBlockStatements": true, 13 | "disallowSpaceAfterObjectKeys": true, 14 | "disallowOperatorBeforeLineBreak": ["."], 15 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 16 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 17 | "disallowSpacesInCallExpression": true, 18 | "disallowSpacesInsideArrayBrackets": { 19 | "allExcept": [ "[", "]", "{", "}" ] 20 | }, 21 | "disallowSpacesInsideParentheses": true, 22 | "disallowTrailingComma": true, 23 | "disallowTrailingWhitespace": true, 24 | "disallowYodaConditions": true, 25 | "maximumLineLength": 120, 26 | "requireBlocksOnNewline": true, 27 | "requireCamelCaseOrUpperCaseIdentifiers": true, 28 | "requireCapitalizedConstructors": true, 29 | "requireCommaBeforeLineBreak": true, 30 | "requireCurlyBraces": [ 31 | "if", 32 | "else", 33 | "for", 34 | "while", 35 | "do", 36 | "try", 37 | "catch", 38 | "case", 39 | "default" 40 | ], 41 | "requireDotNotation": true, 42 | "requireLineBreakAfterVariableAssignment": true, 43 | "requireLineFeedAtFileEnd": true, 44 | "requireMultipleVarDecl": true, 45 | "requirePaddingNewlinesBeforeKeywords": [ 46 | "do", 47 | "for", 48 | "if", 49 | "switch", 50 | "case", 51 | "try", 52 | "while" 53 | ], 54 | "requireParenthesesAroundIIFE": true, 55 | "requireSpaceAfterBinaryOperators": [ 56 | "=", 57 | ",", 58 | "+", 59 | "-", 60 | "/", 61 | "*", 62 | "==", 63 | "===", 64 | "!=", 65 | "!==", 66 | "%", 67 | ">", 68 | "<", 69 | ">=", 70 | "<=" 71 | ], 72 | "requireSpaceAfterKeywords": [ 73 | "do", 74 | "for", 75 | "if", 76 | "else", 77 | "switch", 78 | "case", 79 | "try", 80 | "catch", 81 | "void", 82 | "while", 83 | "with", 84 | "return", 85 | "typeof", 86 | "function" 87 | ], 88 | "requireSpaceBeforeBinaryOperators": [ 89 | "=", 90 | "+", 91 | "-", 92 | "/", 93 | "*", 94 | "==", 95 | "===", 96 | "!=", 97 | "!==", 98 | "%", 99 | ">", 100 | "<", 101 | ">=", 102 | "<=" 103 | ], 104 | "requireSpaceAfterLineComment": { "allExcept": ["#", "="] }, 105 | "requireSpaceBeforeBlockStatements": true, 106 | "requireSpaceBeforeKeywords": [ 107 | "else", 108 | "while", 109 | "catch" 110 | ], 111 | "requireSpaceBeforeObjectValues": true, 112 | "requireSpaceBetweenArguments": true, 113 | "requireSpacesInAnonymousFunctionExpression": { 114 | "beforeOpeningRoundBrace": true, 115 | "beforeOpeningCurlyBrace": true 116 | }, 117 | "requireSpacesInConditionalExpression": { 118 | "afterTest": true, 119 | "beforeConsequent": true, 120 | "afterConsequent": true, 121 | "beforeAlternate": true 122 | }, 123 | "requireSpacesInForStatement": true, 124 | "requireSpacesInFunctionDeclaration": { 125 | "beforeOpeningRoundBrace": true, 126 | "beforeOpeningCurlyBrace": true 127 | }, 128 | "requireSpacesInFunctionExpression": { 129 | "beforeOpeningRoundBrace": true, 130 | "beforeOpeningCurlyBrace": true 131 | }, 132 | "requireSpacesInFunctionExpression": { 133 | "beforeOpeningRoundBrace": true, 134 | "beforeOpeningCurlyBrace": true 135 | }, 136 | "requireSpacesInNamedFunctionExpression": { 137 | "beforeOpeningRoundBrace": true, 138 | "beforeOpeningCurlyBrace": true 139 | }, 140 | "validateIndentation": 4, 141 | "validateParameterSeparator": ", ", 142 | "validateQuoteMarks": "'" 143 | } 144 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "expr" : true, 3 | "bitwise" : true, 4 | "browser" : true, 5 | "camelcase" : true, 6 | "curly" : true, 7 | "eqeqeq" : true, 8 | "esnext" : true, 9 | "immed" : true, 10 | "indent" : 4, 11 | "latedef" : true, 12 | "newcap" : true, 13 | "noarg" : true, 14 | "node" : true, 15 | "noempty" : true, 16 | "nonbsp" : true, 17 | "phantom" : true, 18 | "quotmark" : "single", 19 | "regexp" : true, 20 | "smarttabs" : true, 21 | "strict" : true, 22 | "trailing" : true, 23 | "undef" : true, 24 | "unused" : true 25 | } 26 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | grunt.loadNpmTasks('grunt-contrib-jshint'); 5 | grunt.loadNpmTasks('grunt-jscs'); 6 | grunt.loadNpmTasks('grunt-release'); 7 | 8 | grunt.initConfig({ 9 | jshint: { 10 | options: { 11 | jshintrc: true, 12 | ignores: ['node_modules/**/*'] 13 | }, 14 | all: ['./**/*.js'] 15 | }, 16 | 17 | jscs: { 18 | src: './**/*.js', 19 | options: { 20 | config: '.jscsrc', 21 | excludeFiles: ['node_modules/**/*'] 22 | } 23 | }, 24 | 25 | release: { 26 | options: { 27 | npm: true, 28 | indentation: ' ', 29 | github: { 30 | repo: 'fiddus/datatablesQuery', 31 | usernameVar: 'GITHUB_USERNAME', 32 | passwordVar: 'GITHUB_ACCESS_TOKEN' 33 | } 34 | } 35 | } 36 | }); 37 | 38 | grunt.registerTask('lint', ['jshint', 'jscs']); 39 | }; 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # datatablesQuery 2 | 3 | datatablesQuery is a module for making the integration between front-end tables using 4 | [datatables](https://www.datatables.net/) and a REST API, node.js, express, MongoDB and Mongoose backed Servers, easier. 5 | 6 | The main purpose is dealing with server side processing available in datatables, making it easy to integrate client and 7 | server. 8 | 9 | ## Getting Started 10 | 11 | Install the module. 12 | 13 | ``` 14 | npm install datatables-query 15 | ``` 16 | 17 | In your front-end, configure your DataTable to use serverSide processing and Ajax. The request type MUST be 'POST'. 18 | 19 | ```javascript 20 | // jQuery way 21 | $('#example').DataTable( { 22 | serverSide: true, 23 | ajax: { 24 | url: '/path/to/api/endpoint', 25 | type: 'POST' 26 | } 27 | } ); 28 | ``` 29 | 30 | 31 | ```javascript 32 | // Angular way - @see https://l-lin.github.io/angular-datatables/#/serverSideProcessing for full example 33 | 34 | vm.dtOptions = DTOptionsBuilder.newOptions() 35 | .withOptions('serverSide', true) 36 | .withOptions('ajax', { 37 | url: '/path/to/api/endpoint', 38 | type: 'POST' 39 | }) 40 | .// all other options 41 | 42 | ``` 43 | 44 | In your server, you MUST use `body-parser` with `urlencoded extended` 45 | 46 | ```javascript 47 | var app = express(); 48 | 49 | app.use(bodyParser.urlencoded({extended: true}); 50 | ``` 51 | 52 | 53 | In the route handler, import the module and pass a reference to the mongoose model you wish to use as data source. 54 | 55 | The DataTables params will get caught in the request body. It should be passed to the run method, which will return a 56 | promise. 57 | 58 | ```javascript 59 | 60 | app.post('/path/to/api/endpoint', function (req, res) { 61 | var Model = require('./path/to/model'), 62 | datatablesQuery = require('datatables-query'), 63 | params = req.body, 64 | query = datatablesQuery(Model); 65 | 66 | query.run(params).then(function (data) { 67 | res.json(data); 68 | }, function (err) { 69 | res.status(500).json(err); 70 | }); 71 | }; 72 | ``` 73 | 74 | That's all folks. Your table should be working just fine. 75 | 76 | ## Assumptions 77 | 78 | As noted above, it is assumed that the server parses the request using extended urlencoded and that the data object is 79 | a Mongoose object. 80 | 81 | Datatables with serverSide processing enabled makes POST requests with content-type `application/x-www-form-urlencoded`, 82 | and express' module body parser makes it easiear to work with this data, transforming it to JSON and parsing arrays and 83 | objects. 84 | 85 | ## Using Without Datatables 86 | 87 | One could use this module without datatables in the front-end making requests. For this to work, the POST body must 88 | be a configuration object equivalent to the one shown below: 89 | 90 | ```javascript 91 | // req.body should be equivalent to: 92 | { 93 | "draw": "3", // datatable stuff, but is mandatory nonetheless 94 | "start": "0", 95 | "length": "10", 96 | "search": { 97 | "value": "" 98 | }, 99 | "columns": [ 100 | { 101 | "data": "name", // field name in the MongoDB Schema 102 | "searchable": "true", // mandatory 103 | "orderable": "true" // mandatory 104 | }, 105 | { 106 | // .. same structure as above for each field 107 | } 108 | ], 109 | "order": [ 110 | { 111 | "column": "1", // index of the column used for sorting 112 | "dir": "asc" // direction of sorting ('asc' | 'desc') 113 | } 114 | ] 115 | } 116 | ``` 117 | 118 | ## TODO 119 | 120 | - Add examples to this repo 121 | - Implement filter by column 122 | - Add tests to the 'run' method 123 | 124 | ## Contributing 125 | 126 | Feel free to fork and mess with this code. But, before opening PRs, be sure that you adhere to the Code Style and Conventions 127 | (run `grunt lint`) and add/correct as many tests as needed to ensure your code is working as expected. 128 | 129 | ## License 130 | 131 | The MIT License (MIT) 132 | 133 | [![Fiddus Tecnologia](http://fiddus.com.br/assets/img/logo-site.png)](http://fiddus.com.br) 134 | 135 | Copyright (c) 2015 Vinicius Teixeira vinicius0026@gmail.com 136 | 137 | Permission is hereby granted, free of charge, to any person obtaining a copy 138 | of this software and associated documentation files (the "Software"), to deal 139 | in the Software without restriction, including without limitation the rights 140 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 141 | copies of the Software, and to permit persons to whom the Software is 142 | furnished to do so, subject to the following conditions: 143 | 144 | The above copyright notice and this permission notice shall be included in 145 | all copies or substantial portions of the Software. 146 | 147 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 148 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 149 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 150 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 151 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 152 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 153 | THE SOFTWARE. 154 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'), 4 | 5 | /** 6 | * Method getSearchableFields 7 | * Returns an array of fieldNames based on DataTable params object 8 | * All columns in params.columns that have .searchable == true field will have the .data param returned in an String 9 | * array. The .data property is used because in angular frontend DTColumnBuilder.newColumn('str') puts 'str' in the 10 | * data field, instead of the name field. 11 | * @param params 12 | * @returns {Array} 13 | */ 14 | getSearchableFields = function (params) { 15 | return params.columns.filter(function (column) { 16 | return JSON.parse(column.searchable); 17 | }).map(function (column) { 18 | return column.data; 19 | }); 20 | }, 21 | 22 | /** 23 | * Method isNaNorUndefined 24 | * Checks if any of the passed params is NaN or undefined. 25 | * Used to check DataTable's properties draw, start and length 26 | * @returns {boolean} 27 | */ 28 | isNaNorUndefined = function () { 29 | var args = Array.prototype.slice.call(arguments); 30 | return args.some(function (arg) { 31 | return isNaN(arg) || (!arg && arg !== 0); 32 | }); 33 | }, 34 | 35 | /** 36 | * Methdd buildFindParameters 37 | * Builds a MongoDB find expression based on DataTables param object 38 | * - If no search text if provided (in params.search.value) an empty object is returned, meaning all data in DB will 39 | * be returned. 40 | * - If only one column is searchable (that means, only one params.columns[i].searchable equals true) a normal one 41 | * field regex MongoDB query is returned, that is {`fieldName`: new Regex(params.search.value, 'i'} 42 | * - If multiple columns are searchable, an $or MongoDB is returned, that is: 43 | * ``` 44 | * { 45 | * $or: [ 46 | * {`searchableField1`: new Regex(params.search.value, 'i')}, 47 | * {`searchableField2`: new Regex(params.search.value, 'i')} 48 | * ] 49 | * } 50 | * ``` 51 | * and so on.
52 | * All search are by regex so the field param.search.regex is ignored. 53 | * @param params DataTable params object 54 | * @returns {*} 55 | */ 56 | buildFindParameters = function (params) { 57 | 58 | if (!params || !params.columns || !params.search || (!params.search.value && params.search.value !== '')) { 59 | return null; 60 | } 61 | 62 | var searchText = params.search.value, 63 | findParameters = {}, 64 | searchRegex, 65 | searchOrArray = []; 66 | 67 | if (searchText === '') { 68 | return findParameters; 69 | } 70 | 71 | searchRegex = new RegExp(searchText, 'i'); 72 | 73 | var searchableFields = getSearchableFields(params); 74 | 75 | if (searchableFields.length === 1) { 76 | findParameters[searchableFields[0]] = searchRegex; 77 | return findParameters; 78 | } 79 | 80 | searchableFields.forEach(function (field) { 81 | var orCondition = {}; 82 | orCondition[field] = searchRegex; 83 | searchOrArray.push(orCondition); 84 | }); 85 | 86 | findParameters.$or = searchOrArray; 87 | 88 | return findParameters; 89 | }, 90 | 91 | /** 92 | * Method buildSortParameters 93 | * Based on DataTable parameters, this method returns a MongoDB ordering parameter for the appropriate field 94 | * The params object must contain the following properties: 95 | * order: Array containing a single object 96 | * order[0].column: A string parseable to an Integer, that references the column index of the reference field 97 | * order[0].dir: A string that can be either 'asc' for ascending order or 'desc' for descending order 98 | * columns: Array of column's description object 99 | * columns[i].data: The name of the field in MongoDB. If the index i is equal to order[0].column, and 100 | * the column is orderable, then this will be the returned search param 101 | * columns[i].orderable: A string (either 'true' or 'false') that denotes if the given column is orderable 102 | * @param params 103 | * @returns {*} 104 | */ 105 | buildSortParameters = function (params) { 106 | if (!params || !Array.isArray(params.order) || params.order.length === 0) { 107 | return null; 108 | } 109 | 110 | var sortColumn = Number(params.order[0].column), 111 | sortOrder = params.order[0].dir, 112 | sortField; 113 | 114 | if (isNaNorUndefined(sortColumn) || !Array.isArray(params.columns) || sortColumn >= params.columns.length) { 115 | return null; 116 | } 117 | 118 | if (params.columns[sortColumn].orderable === 'false') { 119 | return null; 120 | } 121 | 122 | sortField = params.columns[sortColumn].data; 123 | 124 | if (!sortField) { 125 | return null; 126 | } 127 | 128 | if (sortOrder === 'asc') { 129 | return sortField; 130 | } 131 | 132 | return '-' + sortField; 133 | }, 134 | 135 | buildSelectParameters = function (params) { 136 | 137 | if (!params || !params.columns || !Array.isArray(params.columns)) { 138 | return null; 139 | } 140 | 141 | return params 142 | .columns 143 | .map(col => col.data) 144 | .reduce((selectParams, field) => { 145 | selectParams[field] = 1; 146 | return selectParams; 147 | }, {}); 148 | }, 149 | 150 | /** 151 | * Run wrapper function 152 | * Serves only to the Model parameter in the wrapped run function's scope 153 | * @param {Object} Model Mongoose Model Object, target of the search 154 | * @returns {Function} the actual run function with Model in its scope 155 | */ 156 | run = function (Model) { 157 | 158 | /** 159 | * Method Run 160 | * The actual run function 161 | * Performs the query on the passed Model object, using the DataTable params argument 162 | * @param {Object} params DataTable params object 163 | */ 164 | return function (params) { 165 | 166 | var draw = Number(params.draw), 167 | start = Number(params.start), 168 | length = Number(params.length), 169 | findParameters = buildFindParameters(params), 170 | sortParameters = buildSortParameters(params), 171 | selectParameters = buildSelectParameters(params), 172 | recordsTotal, 173 | recordsFiltered; 174 | 175 | return new Promise(function (fullfill, reject) { 176 | 177 | async.series([ 178 | function checkParams (cb) { 179 | if (isNaNorUndefined(draw, start, length)) { 180 | return cb(new Error('Some parameters are missing or in a wrong state. ' + 181 | 'Could be any of draw, start or length')); 182 | } 183 | 184 | if (!findParameters || !sortParameters || !selectParameters) { 185 | return cb(new Error('Invalid findParameters or sortParameters or selectParameters')); 186 | } 187 | cb(); 188 | }, 189 | function fetchRecordsTotal (cb) { 190 | Model.count({}, function (err, count) { 191 | if (err) { 192 | return cb(err); 193 | } 194 | recordsTotal = count; 195 | cb(); 196 | }); 197 | }, 198 | function fetchRecordsFiltered (cb) { 199 | Model.count(findParameters, function (err, count) { 200 | if (err) { 201 | return cb(err); 202 | } 203 | recordsFiltered = count; 204 | cb(); 205 | }); 206 | }, 207 | function runQuery (cb) { 208 | Model 209 | .find(findParameters) 210 | .select(selectParameters) 211 | .limit(length) 212 | .skip(start) 213 | .sort(sortParameters) 214 | .exec(function (err, results) { 215 | if (err) { 216 | return cb(err); 217 | } 218 | cb(null, { 219 | draw: draw, 220 | recordsTotal: recordsTotal, 221 | recordsFiltered: recordsFiltered, 222 | data: results 223 | }); 224 | }); 225 | 226 | } 227 | ], function resolve (err, results) { 228 | if (err) { 229 | reject({ 230 | error: err 231 | }); 232 | } else { 233 | var answer = results[results.length - 1]; 234 | fullfill(answer); 235 | } 236 | }); 237 | }); 238 | }; 239 | }, 240 | 241 | /** 242 | * Module datatablesQuery 243 | * Performs queries in the given Mongoose Model object, following DataTables conventions for search and 244 | * pagination. 245 | * The only interesting exported function is `run`. The others are exported only to allow unit testing. 246 | * @param Model 247 | * @returns {{run: Function, isNaNorUndefined: Function, buildFindParameters: Function, buildSortParameters: 248 | * Function}} 249 | */ 250 | datatablesQuery = function (Model) { 251 | return { 252 | run: run(Model), 253 | isNaNorUndefined: isNaNorUndefined, 254 | buildFindParameters: buildFindParameters, 255 | buildSortParameters: buildSortParameters, 256 | buildSelectParameters: buildSelectParameters 257 | }; 258 | }; 259 | 260 | module.exports = datatablesQuery; 261 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datatables-query", 3 | "version": "0.2.0", 4 | "description": "A module for doing MongoDB pagination and search, based on DataTables query model. Relies on Express and Mongoose.", 5 | "repository": "https://github.com/fiddus/datatables-query.git", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "mocha" 9 | }, 10 | "keywords": [ 11 | "datatables", 12 | "pagination", 13 | "mongoose", 14 | "MongoDB", 15 | "Express", 16 | "node", 17 | "nodejs", 18 | "expressjs" 19 | ], 20 | "author": "Vinicius Teixeira", 21 | "license": "MIT", 22 | "dependencies": { 23 | "async": "^1.4.2" 24 | }, 25 | "devDependencies": { 26 | "chai": "^3.2.0", 27 | "grunt": "^0.4.5", 28 | "grunt-contrib-jshint": "^0.11.3", 29 | "grunt-jscs": "^2.1.0", 30 | "grunt-release": "^0.13.0", 31 | "sinon": "^1.16.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | 'use strict'; 4 | 5 | var async = require('async'), 6 | sinon = require('sinon'), 7 | datatablesQuery = require('./index'), 8 | expect = require('chai').expect; 9 | 10 | describe('datatablesQuery tests', function () { 11 | 12 | describe('isNaNorUndefined tests', function () { 13 | it('should return true when isNaN or is undefined', function (done) { 14 | var query = datatablesQuery({}), 15 | a = {}.a; // accessing property that does not exists should return undefined; 16 | 17 | expect(query.isNaNorUndefined(NaN)).to.equal(true); 18 | expect(query.isNaNorUndefined(undefined)).to.equal(true); 19 | expect(query.isNaNorUndefined(Number(undefined))).to.equal(true); 20 | expect(query.isNaNorUndefined(a)).to.equal(true); 21 | done(); 22 | }); 23 | 24 | it('should should return false when param is a number', function (done) { 25 | var query = datatablesQuery({}); 26 | 27 | expect(query.isNaNorUndefined(1)).to.equal(false); 28 | done(); 29 | }); 30 | 31 | it('should return false when param is number zero', function (done) { 32 | var query = datatablesQuery({}); 33 | 34 | expect(query.isNaNorUndefined(0)).to.equal(false); 35 | done(); 36 | }); 37 | }); 38 | 39 | describe('buildFindParameters tests', function () { 40 | var query = datatablesQuery({}); 41 | 42 | it('should return an empty object if no search term was defined', function (done) { 43 | var emptySearchText = { 44 | search: { 45 | value: '', 46 | regex: false 47 | }, 48 | columns: [] 49 | }; 50 | 51 | expect(query.buildFindParameters(emptySearchText)).to.deep.equal({}); 52 | done(); 53 | }); 54 | 55 | it('should return a simple query if only one column is searchable', function (done) { 56 | var oneSearchableColumn = { 57 | search: { 58 | value: 'searchText', 59 | regex: false 60 | }, 61 | columns: [ 62 | { 63 | data: '', 64 | name: '', 65 | searchable: 'false' 66 | 67 | }, 68 | { 69 | data: 'name', 70 | name: '', 71 | searchable: 'true' 72 | } 73 | ] 74 | }; 75 | 76 | expect(query.buildFindParameters(oneSearchableColumn)) 77 | .to.deep.equal({name: new RegExp(oneSearchableColumn.search.value, 'i')}); 78 | done(); 79 | }); 80 | 81 | it('should return a compound $or query when multiple columns are searchable and a search value is provided', 82 | function (done) { 83 | var multipleSearchableColumns = { 84 | search: { 85 | value: 'searchText', 86 | regex: false 87 | }, 88 | columns: [ 89 | { 90 | data: 'name', 91 | name: '', 92 | searchable: 'true' 93 | }, 94 | { 95 | data: 'email', 96 | name: '', 97 | searchable: 'true' 98 | } 99 | ] 100 | }; 101 | 102 | expect(query.buildFindParameters(multipleSearchableColumns)) 103 | .to.deep.equal({ 104 | $or: [{ 105 | name: new RegExp(multipleSearchableColumns.search.value, 'i') 106 | }, { 107 | email: new RegExp(multipleSearchableColumns.search.value, 'i') 108 | }] 109 | }); 110 | done(); 111 | }); 112 | }); 113 | 114 | describe('buildSortParameters tests', function () { 115 | var query = datatablesQuery({}); 116 | 117 | it('should build ascending sort parameters correctly', function (done) { 118 | var params = { 119 | columns: [ 120 | { 121 | data: 'name', 122 | name: '', 123 | searchable: 'true', 124 | orderable: 'true' 125 | }, 126 | { 127 | data: 'email', 128 | name: '', 129 | searchable: 'true', 130 | orderable: 'true' 131 | } 132 | ], 133 | order: [{ 134 | column: '0', 135 | dir: 'asc' 136 | }] 137 | }; 138 | 139 | expect(query.buildSortParameters(params)).to.equal('name'); 140 | done(); 141 | }); 142 | 143 | it('should build descending sort parameters correctly', function (done) { 144 | var params = { 145 | columns: [ 146 | { 147 | data: 'name', 148 | name: '', 149 | searchable: 'true', 150 | orderable: 'true' 151 | }, 152 | { 153 | data: 'email', 154 | name: '', 155 | searchable: 'true', 156 | orderable: 'true' 157 | } 158 | ], 159 | order: [{ 160 | column: '1', 161 | dir: 'desc' 162 | }] 163 | }; 164 | 165 | expect(query.buildSortParameters(params)).to.equal('-email'); 166 | done(); 167 | }); 168 | 169 | it('should return null if a non orderable column is set as sort param', function (done) { 170 | var params = { 171 | columns: [ 172 | { 173 | data: 'name', 174 | name: '', 175 | searchable: 'true', 176 | orderable: 'false' 177 | }, 178 | { 179 | data: 'email', 180 | name: '', 181 | searchable: 'true', 182 | orderable: 'true' 183 | } 184 | ], 185 | order: [{ 186 | column: '0', 187 | dir: 'desc' 188 | }] 189 | }; 190 | 191 | expect(query.buildSortParameters(params)).to.equal(null); 192 | done(); 193 | }); 194 | 195 | it('should return a correct search param if non searchable but orderable column is set as sort param', 196 | function (done) { 197 | var params = { 198 | columns: [ 199 | { 200 | data: 'name', 201 | name: '', 202 | searchable: 'true', 203 | orderable: 'true' 204 | }, 205 | { 206 | data: 'email', 207 | name: '', 208 | searchable: 'false', 209 | orderable: 'true' 210 | } 211 | ], 212 | order: [{ 213 | column: '1', 214 | dir: 'desc' 215 | }] 216 | }; 217 | 218 | expect(query.buildSortParameters(params)).to.equal('-email'); 219 | done(); 220 | }); 221 | 222 | it('should return null if an out of bound column is set as sort param', function (done) { 223 | var params = { 224 | columns: [ 225 | { 226 | data: 'name', 227 | name: '', 228 | searchable: 'true', 229 | orderable: 'true' 230 | }, 231 | { 232 | data: 'email', 233 | name: '', 234 | searchable: 'true', 235 | orderable: 'true' 236 | } 237 | ], 238 | order: [{ 239 | column: '2', 240 | dir: 'desc' 241 | }] 242 | }; 243 | 244 | expect(query.buildSortParameters(params)).to.equal(null); 245 | done(); 246 | }); 247 | 248 | it('should return null if params doesnot contain an order field', function (done) { 249 | var params = { 250 | columns: [ 251 | { 252 | data: 'name', 253 | name: '', 254 | searchable: 'true', 255 | orderable: 'true' 256 | }, 257 | { 258 | data: 'email', 259 | name: '', 260 | searchable: 'true', 261 | orderable: 'true' 262 | } 263 | ] 264 | }; 265 | 266 | expect(query.buildSortParameters(params)).to.equal(null); 267 | done(); 268 | }); 269 | 270 | it('should return null if params.order.column is not provided or is not a number', function (done) { 271 | var paramsSet = [{ 272 | columns: [ 273 | { 274 | data: 'name', 275 | name: '', 276 | searchable: 'true', 277 | orderable: 'true' 278 | }, 279 | { 280 | data: 'email', 281 | name: '', 282 | searchable: 'true', 283 | orderable: 'true' 284 | } 285 | ], 286 | order: [{ 287 | dir: 'desc' 288 | }] 289 | }, { 290 | columns: [ 291 | { 292 | data: 'name', 293 | name: '', 294 | searchable: 'true', 295 | orderable: 'true' 296 | }, 297 | { 298 | data: 'email', 299 | name: '', 300 | searchable: 'true', 301 | orderable: 'true' 302 | } 303 | ], 304 | order: [{ 305 | column: 'abc', 306 | dir: 'desc' 307 | }] 308 | }]; 309 | 310 | paramsSet.forEach(function (params) { 311 | expect(query.buildSortParameters(params)).to.equal(null); 312 | }); 313 | 314 | done(); 315 | }); 316 | 317 | it('should return null if params.columns is not an array', function (done) { 318 | var params = { 319 | order: [{ 320 | column: '1', 321 | dir: 'desc' 322 | }] 323 | }; 324 | 325 | expect(query.buildSortParameters(params)).to.equal(null); 326 | done(); 327 | }); 328 | }); 329 | 330 | describe('buildSelectParameters tests', function () { 331 | var query = datatablesQuery({}); 332 | 333 | it('should include only and all columns passed in parameters in the select params', function (done) { 334 | var params = { 335 | columns: [ 336 | { 337 | data: 'name', 338 | name: '', 339 | searchable: 'true', 340 | orderable: 'true' 341 | }, 342 | { 343 | data: 'email', 344 | name: '', 345 | searchable: 'false', 346 | orderable: 'true' 347 | } 348 | ], 349 | order: [{ 350 | column: '1', 351 | dir: 'desc' 352 | }] 353 | }; 354 | 355 | expect(query.buildSelectParameters(params)) 356 | .to.deep.equal({ 357 | email: 1, 358 | name: 1 359 | }); 360 | done(); 361 | }); 362 | }); 363 | 364 | describe('run tests', function () { 365 | it('should reject promise if params argument is lacking draw start or length', function (done) { 366 | var query = datatablesQuery({}); 367 | 368 | async.each([{start: 0, length: 10}, {draw: 1, length: 10}, {draw: 1, start: 0}], function (params, cb) { 369 | var success = sinon.spy(), 370 | error = sinon.spy(); 371 | 372 | async.series([ 373 | function resolvePromise (cb) { 374 | query.run(params).then(function () { 375 | success(); 376 | cb(); 377 | }, function () { 378 | error(); 379 | cb(); 380 | }); 381 | }, 382 | function test () { 383 | expect(success.callCount).to.equal(0); 384 | expect(error.calledOnce).to.equal(true); 385 | cb(); 386 | } 387 | ]); 388 | }, function () { 389 | done(); 390 | }); 391 | }); 392 | 393 | it('should reject promise if findParams is null', function (done) { 394 | var query = datatablesQuery({}), 395 | success = sinon.spy(), 396 | error = sinon.spy(), 397 | 398 | params = { 399 | draw: '1', 400 | start: '10', 401 | length: '10', 402 | order: [{ 403 | column: '0', 404 | dir: 'desc' 405 | }] 406 | }; 407 | 408 | expect(query.buildFindParameters(params)).to.equal(null); 409 | 410 | async.series([ 411 | function resolvePromise (cb) { 412 | query.run(params).then(function () { 413 | success(); 414 | cb(); 415 | }, function () { 416 | error(); 417 | cb(); 418 | }); 419 | }, 420 | function test (cb) { 421 | expect(success.callCount).to.equal(0); 422 | expect(error.calledOnce).to.equal(true); 423 | cb(); 424 | }, 425 | function end () { 426 | done(); 427 | } 428 | ]); 429 | }); 430 | 431 | it('should reject promise if sortParams is null', function (done) { 432 | var query = datatablesQuery({}), 433 | success = sinon.spy(), 434 | error = sinon.spy(), 435 | params = { 436 | draw: '1', 437 | start: '10', 438 | length: '10', 439 | columns: [ 440 | { 441 | data: 'name', 442 | name: '', 443 | searchable: 'true', 444 | orderable: 'false' 445 | }, 446 | { 447 | data: 'email', 448 | name: '', 449 | searchable: 'true', 450 | orderable: 'true' 451 | } 452 | ], 453 | order: [{ 454 | column: '0', 455 | dir: 'desc' 456 | }] 457 | }; 458 | 459 | expect(query.buildSortParameters(params)).to.equal(null); 460 | 461 | async.series([ 462 | function resolvePromise (cb) { 463 | query.run(params).then(function () { 464 | success(); 465 | cb(); 466 | }, function () { 467 | error(); 468 | cb(); 469 | }); 470 | }, 471 | function test (cb) { 472 | expect(success.callCount).to.equal(0); 473 | expect(error.calledOnce).to.equal(true); 474 | cb(); 475 | }, 476 | function end () { 477 | done(); 478 | } 479 | ]); 480 | }); 481 | }); 482 | }); 483 | --------------------------------------------------------------------------------