├── .npmignore ├── .gitignore ├── index.js ├── test ├── views │ ├── error.html │ ├── subs.html │ ├── index.html │ └── base.html ├── public │ ├── stylesheets │ │ └── style.css │ └── javascripts │ │ ├── init-subs-datatable.js │ │ └── init-zipcodes-datatable.js ├── config │ ├── local.js │ └── dycode.js ├── package.json ├── bower.json ├── routes │ └── index.js ├── README.md └── app.js ├── lib ├── string.js ├── validator.js ├── MongoDataTable.js └── columns.js ├── package.json ├── LICENSE ├── HISTORY.md └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | bower_components/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | *.log 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/MongoDataTable'); 2 | -------------------------------------------------------------------------------- /test/views/error.html: -------------------------------------------------------------------------------- 1 |

{{ message }}

2 |

{{ error.status }}

3 |
{{ error.stack }}
4 | -------------------------------------------------------------------------------- /test/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } -------------------------------------------------------------------------------- /test/config/local.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongodb: { 3 | host: 'localhost', 4 | port: 27017, 5 | dbname: 'samples', 6 | username: '', 7 | password: '', 8 | get connectionUri() { 9 | return 'mongodb://' + this.host + ':' + this.port + '/' + this.dbname; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/config/dycode.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongodb: { 3 | host: '192.168.1.157', 4 | port: 27018, 5 | dbname: 'cloud', 6 | username: '', 7 | password: '', 8 | get connectionUri() { 9 | return 'mongodb://' + this.host + ':' + this.port + '/' + this.dbname; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/views/subs.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block initscripts %} 4 | 5 | {% endblock %} 6 | 7 | {% block table %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
IdNameScreen NameServicesState
19 | {% endblock %} -------------------------------------------------------------------------------- /lib/string.js: -------------------------------------------------------------------------------- 1 | function escapeNonAlphanumeric(string) { 2 | return string.replace(/[\W\s]/g, '\\$&'); 3 | } 4 | 5 | function stringToBoolean(string) { 6 | if (typeof string === 'boolean') { 7 | return string; 8 | } 9 | 10 | if (string === 'true') { 11 | return true; 12 | } else if (string === 'false') { 13 | return false; 14 | } 15 | 16 | throw new Error('Argument passed is not boolean type'); 17 | } 18 | 19 | exports.escapeNonAlphanumeric = escapeNonAlphanumeric; 20 | exports.stringToBoolean = stringToBoolean; -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongo-datatable-test", 3 | "version": "0.1.0", 4 | "description": "Express-based app for handling jquery datatables server-side processing using mongo-datatable module.", 5 | "private": true, 6 | "scripts": { 7 | "start": "node app.js" 8 | }, 9 | "dependencies": { 10 | "body-parser": "~1.12.4", 11 | "cookie-parser": "~1.3.5", 12 | "debug": "~2.2.0", 13 | "express": "~4.12.4", 14 | "mongodb": "^2.0.36", 15 | "morgan": "~1.5.3", 16 | "serve-favicon": "~2.2.1", 17 | "swig": "^1.4.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongo-datatable", 3 | "version": "1.1.1", 4 | "description": "NodeJS module for server-side processing using jquery datatables and mongodb native driver.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/dycodedev/mongo-datatable.git" 8 | }, 9 | "main": "index.js", 10 | "dependencies": { 11 | "async": "^1.3.0", 12 | "lodash": "^4.17.10" 13 | }, 14 | "keywords": [ 15 | "mongo", 16 | "mongodb", 17 | "datatable" 18 | ], 19 | "author": "Alwin Arrasyid ", 20 | "license": "MIT" 21 | } 22 | -------------------------------------------------------------------------------- /test/views/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block initscripts %} 4 | 5 | {% endblock %} 6 | 7 | {% block table %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
IdCityStatePopulationLocation
IdCityStatePopulationLocation
29 | {% endblock %} -------------------------------------------------------------------------------- /test/public/javascripts/init-subs-datatable.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $('#subs-table').DataTable({ 3 | serverSide: true, 4 | processing: true, 5 | autoWidth: false, 6 | ajax: 'subs/subs.json', 7 | columns: [ 8 | { data: '_id' }, 9 | { data: 'subs_name' }, 10 | { data: 'subs_screen_name' }, 11 | { 12 | data: 'services', 13 | render: function(data) { 14 | return data.join(', '); 15 | } 16 | }, 17 | { data: 'state' } 18 | ] 19 | }); 20 | 21 | $('#subs-table') 22 | .removeClass('display') 23 | .addClass('table table-stripped table-bordered'); 24 | }); -------------------------------------------------------------------------------- /test/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongo-datatable-test", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/dycodedev/mongo-datatable", 5 | "private": true, 6 | "authors": [ 7 | "Alwin Arrasyid " 8 | ], 9 | "description": "Express-based app for handling jquery datatables server-side processing using mongo-datatable module.", 10 | "license": "MIT", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "datatables": "^1.10.16", 20 | "bootstrap": "~3.3.5", 21 | "Plugins": "git://github.com/DataTables/Plugins.git#~1.10.7" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/views/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% block initscripts %} 16 | 17 | {% endblock %} 18 | 19 |
20 |
21 | {% block table %} 22 | 23 | {% endblock %} 24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /test/routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var MongoClient = require('mongodb').MongoClient; 4 | 5 | // does not require mongo-datatable from npm. 6 | // Uses locally developed version instead 7 | var MongoDataTable = require('../../index'); 8 | var config = require('../config/local'); 9 | 10 | router.get('/', function(req, res, next) { 11 | res.render('index', { title: 'MongoDB Datatable' }); 12 | }); 13 | 14 | router.get('/zipcodes.json', function(req, res) { 15 | var options = req.query; 16 | options.caseInsensitiveSearch = true; 17 | options.showAlertOnError = true; 18 | // Select data with state MA 19 | options.customQuery = { 20 | state: 'MA', 21 | }; 22 | 23 | MongoClient.connect(config.mongodb.connectionUri, function(err, db) { 24 | if (err) { 25 | console.error(err); 26 | } 27 | 28 | new MongoDataTable(db).get('zipcodes', options, function(err, result) { 29 | if (err) { 30 | console.error(err); 31 | } 32 | 33 | res.json(result); 34 | }); 35 | }); 36 | }); 37 | 38 | module.exports = router; -------------------------------------------------------------------------------- /lib/validator.js: -------------------------------------------------------------------------------- 1 | function isOptionsValid(options, immediateCallback) { 2 | if (typeof options === 'undefined') 3 | return immediateCallback(new Error('Options must be defined!')); 4 | 5 | if (typeof options.columns === 'undefined') 6 | return immediateCallback(new Error('Columns must be defined!')); 7 | 8 | if (typeof options.order === 'undefined') 9 | return immediateCallback(new Error('Columns order field must be defined!')); 10 | 11 | if (typeof options.search === 'undefined') 12 | return immediateCallback(new Error('Search field must be defined!')); 13 | 14 | var isStartValid = (typeof options.start !== 'undefined' 15 | || parseInt(options.start, 10) >= 0); 16 | 17 | var isLengthValid = (typeof options.length !== 'undefined' 18 | || parseInt(options.length, 10) > 0); 19 | 20 | if (!isStartValid) 21 | return immediateCallback(new Error('Start field must be defined!')); 22 | 23 | if (!isLengthValid) 24 | return immediateCallback(new Error('Length field must be defined!')); 25 | 26 | return immediateCallback(null); 27 | } 28 | 29 | exports.isOptionsValid = isOptionsValid; 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alwin Arrasyid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /test/public/javascripts/init-zipcodes-datatable.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $('#zipcodes-table tfoot th').each(function() { 3 | var title = $('#zipcodes-table tfoot th').eq($(this).index()).text(); 4 | $(this).html(''); 5 | }); 6 | 7 | var table = $('#zipcodes-table').DataTable({ 8 | serverSide: true, 9 | processing: true, 10 | autoWidth: false, 11 | lengthMenu: [[10, 25, 50, -1], [10, 25, 50, 'All']], 12 | ajax: 'zipcodes.json', 13 | columns: [ 14 | { data: '_id', searchable: false, name: 'id' }, 15 | { data: 'city', name: 'city' }, 16 | { data: 'state', name: 'state' }, 17 | { data: 'pop', name: 'population'}, 18 | { 19 | data: 'loc', 20 | name: 'location', 21 | render: function(data) { 22 | return data.join(', '); 23 | } 24 | } 25 | ] 26 | }); 27 | 28 | $('#zipcodes-table') 29 | .removeClass('display') 30 | .addClass('table table-stripped table-bordered'); 31 | 32 | table.columns().every(function() { 33 | var that = this; 34 | 35 | $('input', this.footer()).on('keyup change', function() { 36 | that 37 | .search(this.value) 38 | .draw(); 39 | }); 40 | }); 41 | }); -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Mongo Datatable Test 2 | 3 | Express-based app for handling jquery datatables server-side processing using mongo-datatable module. 4 | 5 | ## Data source 6 | 7 | In this example, I am using database `samples` and collection `zipcodes`. And documents for `zipcodes` can be downloaded by clicking [this link](http://media.mongodb.org/zips.json?_ga=1.69149396.496420190.1431955345). You can import it to your collection by using `mongoimport` command which is explained below. 8 | 9 | The data model is described [here](http://docs.mongodb.org/manual/tutorial/aggregation-zip-code-data-set/). 10 | 11 | ## Importing Data 12 | 13 | You can import your data using following command 14 | 15 | ```bash 16 | mongoimport --host localhost --port 27017 --username youruser --password yourpassword --collection zipcodes --db samples --file zips.json 17 | ``` 18 | 19 | You can omit `--username` and `--password` if you don't have any user, and you can omit `--port` as well if your mongodb server is using default port. 20 | 21 | Note that you must be in the same directory as `zips.json` file to run the above command, otherwise you should move to directory where `zips.json` file lies or specify either absolute or relative path to that file. 22 | 23 | You can find more detail about importing data to mongodb in [here](http://docs.mongodb.org/manual/reference/program/mongoimport/) 24 | 25 | ## Running the app 26 | 27 | Using port number 8000 or any available port numbers. 28 | 29 | ```bash 30 | PORT=8000 node app.js 31 | ``` -------------------------------------------------------------------------------- /test/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | var swig = require('swig'); 8 | var http = require('http'); 9 | 10 | var indexRoutes = require('./routes/index'); 11 | 12 | var app = express(); 13 | 14 | // view engine setup 15 | app.engine('html', swig.renderFile); 16 | app.set('views', path.join(__dirname, 'views')); 17 | app.set('view engine', 'html'); 18 | app.set('view cache', false); 19 | 20 | // no view cache in development environment 21 | if (app.get('env') === 'development') { 22 | swig.setDefaults({ cache: false }); 23 | } 24 | 25 | // uncomment after placing your favicon in /public 26 | //app.use(favicon(__dirname + '/public/favicon.ico')); 27 | app.use(logger('dev')); 28 | app.use(bodyParser.json()); 29 | app.use(bodyParser.urlencoded({ extended: false })); 30 | app.use(cookieParser()); 31 | app.use(express.static(path.join(__dirname, 'public'))); 32 | app.use(express.static(path.join(__dirname, 'bower_components'))); 33 | 34 | app.use('/', indexRoutes); 35 | 36 | // catch 404 and forward to error handler 37 | app.use(function(req, res, next) { 38 | var err = new Error('Not Found'); 39 | err.status = 404; 40 | next(err); 41 | }); 42 | 43 | // error handlers 44 | 45 | // development error handler 46 | // will print stacktrace 47 | if (app.get('env') === 'development') { 48 | app.use(function(err, req, res, next) { 49 | res.status(err.status || 500); 50 | res.render('error', { 51 | message: err.message, 52 | error: err 53 | }); 54 | }); 55 | } 56 | 57 | // production error handler 58 | // no stacktraces leaked to user 59 | app.use(function(err, req, res, next) { 60 | res.status(err.status || 500); 61 | res.render('error', { 62 | message: err.message, 63 | error: {} 64 | }); 65 | }); 66 | 67 | 68 | var server = http.createServer(app); 69 | var port = process.env.PORT || 3000; 70 | 71 | server.listen(port); 72 | 73 | server.on('listening', function() { 74 | console.log('MongoDataTable Test Server is running on port', port); 75 | }) 76 | 77 | process.on('uncaughtException', function(exception) { 78 | console.error('uncaughtException', exception.stack); 79 | }); 80 | -------------------------------------------------------------------------------- /lib/MongoDataTable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var asyncjs = require('async'); 4 | var forEach = require('lodash/forEach'); 5 | var cols = require('./columns'); 6 | var validator = require('./validator'); 7 | 8 | function MongoDataTable(dbObject) { 9 | this.db = dbObject; 10 | } 11 | 12 | MongoDataTable.prototype.get = function(collectionName, options, onDataReady) { 13 | var self = this; 14 | var columns = cols.extractColumns(options); 15 | 16 | var response = { 17 | draw: 0, 18 | recordsTotal: 0, 19 | recordsFiltered: 0, 20 | data: [], 21 | error: null 22 | }; 23 | 24 | var searchCriteria = cols.buildSearchCriteria(options); 25 | 26 | function getCollectionLength(callback) { 27 | if (self.db === null || typeof self.db === 'undefined') { 28 | return callback(new Error('You are not connected to any database server')); 29 | } 30 | 31 | var earlyCollection = self.db.collection(collectionName); 32 | response.draw = parseInt(options.draw, 10); 33 | 34 | earlyCollection 35 | .find(searchCriteria, columns) 36 | .count(function(error, result) { 37 | 38 | if (error) { 39 | return callback(error, null); 40 | } 41 | 42 | response.recordsTotal = result; 43 | response.recordsFiltered = result; 44 | 45 | return callback(null); 46 | }); 47 | } 48 | 49 | function validateOptions(callback) { 50 | validator.isOptionsValid(options, callback); 51 | } 52 | 53 | function buildDefaultValue(callback) { 54 | var showAlertOnError = options.showAlertOnError; 55 | 56 | if (!showAlertOnError) { 57 | options.showAlertOnError = false; 58 | } 59 | 60 | return callback(null); 61 | } 62 | 63 | function getAndSortData(callback) { 64 | var sortOrder = cols.buildColumnSortOrder(options); 65 | var collection = self.db.collection(collectionName); 66 | 67 | collection = collection.find(searchCriteria, columns); 68 | 69 | if (parseInt(options.length) > 0) { 70 | collection = collection 71 | .skip(parseInt(options.start)) 72 | .limit(parseInt(options.length)); 73 | } 74 | 75 | forEach(sortOrder, function(order) { 76 | collection = collection.sort(order); 77 | }); 78 | 79 | collection.toArray(callback); 80 | } 81 | 82 | function returnData(error, result) { 83 | if (error) { 84 | if (options.showAlertOnError) { 85 | response.error = error.message; 86 | } 87 | 88 | return onDataReady(error, response); 89 | } 90 | 91 | // Everything's ok! 92 | response.data = result; 93 | return onDataReady(null, response); 94 | } 95 | 96 | var tasks = [ 97 | validateOptions, 98 | buildDefaultValue, 99 | getCollectionLength, 100 | getAndSortData 101 | ]; 102 | 103 | asyncjs.waterfall(tasks, returnData); 104 | }; 105 | 106 | module.exports = MongoDataTable; 107 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # Project History 2 | 3 | All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ### [1.1.1] - 2018-05-20 6 | 7 | #### Added 8 | * Example for mongodb native driver v3 9 | 10 | #### Modified 11 | * Internal naming of reference to `async` module so that it would not conflict with `async` keyword. 12 | * Using strict mode in MongoDataTable.js 13 | * Updated lodash version 14 | 15 | ### [1.1.0] - 2017-09-15 16 | 17 | #### Added 18 | * Support for case insensitive search through `caseInsensitiveSearch` option. 19 | 20 | ### [1.0.1] - 2016-09-20 21 | 22 | #### Modified 23 | * Fixed left curly braces 24 | 25 | ### [1.0.0] - 2016-09-20 26 | 27 | #### Modified 28 | * Refactored validator and entry point. 29 | * Only generate search query once. 30 | 31 | ### [0.4.1] - 2016-06-25 32 | 33 | #### Modified 34 | * Upgrade lodash to version 4.13.1. (See [#2](https://github.com/dycodedev/mongo-datatable/pull/2)) 35 | 36 | ### [0.4.0] - 2015-10-20 37 | 38 | #### Added 39 | * Support for showing all rows using -1 page length configuration. See [length menu](https://datatables.net/examples/advanced_init/length_menu.html). 40 | 41 | ### [0.3.1] - 2015-10-20 42 | 43 | #### Fixed 44 | * jquery datatables now display error when showAlertOnError is true 45 | 46 | ### [0.3.0] - 2015-10-19 47 | 48 | #### Added 49 | * `customQuery` option. 50 | * Specific column search in global search input element. 51 | 52 | 53 | #### Changed 54 | * `showAlert` option changed to `showAlertOnError`. 55 | * Using lodash to process request data. 56 | * Express-based example app is no longer using `bin/www` to start app. 57 | * Express-based example app uses newly added feature. 58 | 59 | #### Removed 60 | * `emptyOnError` option as this module will always return empty data when error occurs. 61 | 62 | ### [0.2.1] - 2015-09-05 63 | 64 | #### Changed 65 | * Revised README.md 66 | 67 | #### Fixed 68 | * Treat numeric search value differently. 69 | 70 | ### [0.2.0] - 2015-07-13 71 | 72 | #### Added 73 | * Added `emptyOnError` and `showAlert` to `options`. 74 | 75 | ### [0.1.2] - 2015-07-11 76 | 77 | #### Added 78 | * Express-based app to test this module 79 | * `.npmignore` to ignore that test app 80 | 81 | #### Changed 82 | * `.gitignore` ignores log files 83 | * Revised README.md 84 | 85 | #### Fixed 86 | * Throws error if `db` object is undefined or null 87 | 88 | ### [0.1.1] - 2015-07-09 89 | 90 | #### Changed 91 | * Revised README.md 92 | * Removed `console.log` 93 | 94 | ### 0.1.0 - 2015-07-09 95 | 96 | #### Added 97 | * README.md 98 | * Multiple columns search 99 | * Columns sorting 100 | * Options validation 101 | * Non alphanumeric string replacement 102 | 103 | [1.1.1]: https://github.com/dycodedev/mongo-datatable/compare/1.1.0...1.1.1 104 | [1.1.0]: https://github.com/dycodedev/mongo-datatable/compare/1.0.1...1.1.0 105 | [1.0.1]: https://github.com/dycodedev/mongo-datatable/compare/1.0.0...1.0.1 106 | [1.0.0]: https://github.com/dycodedev/mongo-datatable/compare/0.4.1...1.0.0 107 | [0.4.1]: https://github.com/dycodedev/mongo-datatable/compare/0.4.0...0.4.1 108 | [0.4.0]: https://github.com/dycodedev/mongo-datatable/compare/0.3.1...0.4.0 109 | [0.3.1]: https://github.com/dycodedev/mongo-datatable/compare/0.3.0...0.3.1 110 | [0.3.0]: https://github.com/dycodedev/mongo-datatable/compare/0.2.1...0.3.0 111 | [0.2.1]: https://github.com/dycodedev/mongo-datatable/compare/0.2.0...0.2.1 112 | [0.2.0]: https://github.com/dycodedev/mongo-datatable/compare/0.1.2...0.2.0 113 | [0.1.2]: https://github.com/dycodedev/mongo-datatable/compare/0.1.1...0.1.2 114 | [0.1.1]: https://github.com/dycodedev/mongo-datatable/compare/0.1.0...0.1.1 -------------------------------------------------------------------------------- /lib/columns.js: -------------------------------------------------------------------------------- 1 | var filter = require('lodash/filter'); 2 | var forEach = require('lodash/forEach'); 3 | var merge = require('lodash/merge'); 4 | var string = require('./string'); 5 | 6 | function buildSearchCriteria(options) { 7 | var globalSearchValue = options.search.value; 8 | var searchCriteria = {}; 9 | var searchAbleColumns = getSearchableColumns(options); 10 | var globallySearchedColumns = []; 11 | var currentColumn; 12 | var currentSearch; 13 | var currentSearchValue; 14 | var escapedSearchValue; 15 | 16 | var shouldCaseInsensitive = typeof options.caseInsensitiveSearch !== 'undefined' 17 | ? string.stringToBoolean(options.caseInsensitiveSearch) 18 | : false; 19 | 20 | forEach(searchAbleColumns, function(column) { 21 | if (column.search.value.length > 0) { 22 | currentSearchValue = column.search.value; 23 | 24 | escapedSearchValue = string.escapeNonAlphanumeric(currentSearchValue); 25 | currentSearch = parseSearchValue(escapedSearchValue, shouldCaseInsensitive); 26 | 27 | searchCriteria[column.data] = currentSearch; 28 | } 29 | else { 30 | globallySearchedColumns.push(column); 31 | } 32 | }); 33 | 34 | // If global search value is provided 35 | 36 | if (globalSearchValue.length > 0 && globallySearchedColumns.length > 0) { 37 | searchCriteria['$or'] = []; 38 | 39 | if (globalSearchValue.indexOf(':') > 0) { 40 | var splitted = globalSearchValue.split(':'); 41 | var matchingColumn = filter(searchAbleColumns, function(column) { 42 | return column.name.toLowerCase() === splitted[0].toLowerCase(); 43 | })[0]; 44 | 45 | // Column name and search data match. 46 | if (matchingColumn) { 47 | currentSearch = {}; 48 | currentSearch[matchingColumn.data] = parseSearchValue(splitted[1], shouldCaseInsensitive); 49 | searchCriteria['$or'].push(currentSearch); 50 | } 51 | } 52 | else { 53 | globalSearchValue = string.escapeNonAlphanumeric(globalSearchValue); 54 | 55 | forEach(globallySearchedColumns, function(column) { 56 | currentSearch = {}; 57 | currentSearch[column.data] = parseSearchValue(globalSearchValue, shouldCaseInsensitive); 58 | 59 | searchCriteria['$or'].push(currentSearch); 60 | }); 61 | } 62 | } 63 | 64 | if (Object.keys(searchCriteria).length < 1) 65 | searchCriteria = {}; 66 | 67 | if (options.customQuery) 68 | searchCriteria = merge(searchCriteria, options.customQuery); 69 | 70 | return searchCriteria; 71 | } 72 | 73 | function buildColumnSortOrder(options) { 74 | var sortOrder = []; 75 | var columns = options.columns; 76 | var currentOrder; 77 | var currentColumn; 78 | 79 | forEach(options.order, function(order) { 80 | currentColumn = columns[order.column]; 81 | 82 | if (currentColumn.orderable === 'true' || currentColumn.orderable === true) { 83 | currentOrder = {}; 84 | currentOrder[currentColumn.data] = (order.dir === 'asc') ? 1 : -1; 85 | 86 | sortOrder.push(currentOrder); 87 | } 88 | }); 89 | 90 | return sortOrder; 91 | } 92 | 93 | function extractColumns(options) { 94 | var columns = {}; 95 | 96 | forEach(options.columns, function(column){ 97 | columns[column.data] = 1; 98 | }); 99 | 100 | return columns; 101 | } 102 | 103 | function getSearchableColumns(options) { 104 | return filter(options.columns, function(column) { 105 | return column.searchable === 'true' || column.searchable === true; 106 | }); 107 | } 108 | 109 | function parseSearchValue(value, caseInsensitive) { 110 | if (!isNaN(Number(value))) 111 | return Number(value); 112 | 113 | // default to case sensitive 114 | var shouldCaseInsensitive = typeof caseInsensitive === 'undefined' 115 | ? false 116 | : caseInsensitive; 117 | 118 | return shouldCaseInsensitive === true 119 | ? new RegExp(value, 'ig') 120 | : new RegExp(value, 'g'); 121 | } 122 | 123 | exports.buildSearchCriteria = buildSearchCriteria; 124 | exports.buildColumnSortOrder = buildColumnSortOrder; 125 | exports.extractColumns = extractColumns; 126 | exports.getSearchableColumns = getSearchableColumns; 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mongo DataTable 2 | 3 | Node.js module for server-side processing using jQuery datatables and MongoDB native driver. 4 | 5 | Supports: 6 | 7 | * jQuery Datatables v1.10 8 | * mongodb native driver v2.0 and later 9 | * MongoDB database server v2.4 and later 10 | 11 | ## Install 12 | 13 | ```bash 14 | npm install mongo-datatable 15 | ``` 16 | 17 | ## Documentation 18 | This module returns `MongoDataTable` constructor when loaded using `require`. 19 | 20 | ### MongoDataTable(db) 21 | 22 | This constructor takes one argument and must be instantiated using `new` keyword. 23 | 24 | __Argument:__ 25 | 26 | * `db` - An instance of `Db` from `mongodb` module. 27 | 28 | ### MongoDataTable.prototype.get(collection, options, callback) 29 | 30 | This method validates the `options` argument and checks the connection to database. If the `options` is invalid or there is no connection made to database, the callback will be called immediately with error. If everything is ok, the callback will be called with `result`. 31 | 32 | __Arguments:__ 33 | 34 | * `collection` (*String*) - A string represents name of a collection in your database. 35 | * `options` (*Object*) - An object identic to [sent parameter](https://www.datatables.net/manual/server-side#Sent-parameters) by jquery datatables. 36 | * `callback(error, result)` (*Function*) - The `result` parameter is an object identic to [returned data](https://www.datatables.net/manual/server-side#Returned-data) to jquery datatables. 37 | 38 | __Extra Options:__ 39 | 40 | * `showAlertOnError` (*Boolean*) - If this field is set to `true` and `callback` is called with `error`, the error message will be displayed to the user by the datatables. The default value is `false`. 41 | * `customQuery` (*Object*) - Add custom query. Suppose you have a user collection with each user has either admin or user role and you want to display only users with admin role. You can add something like `{ role: 'admin' }` to this field. This query has higher precedence over constructed query. 42 | * `caseInsensitiveSearch` (*Boolean*) - To enable case insensitive search, set this option value to `true`. It is case sensitive by default. 43 | 44 | #### Search Operation 45 | 46 | * If both individual column and global search value are not given, then the search query will be an empty object. Therefore this method will fetch all documents inside the collection. 47 | 48 | * If there is no individual column search value is given and global search value is given, then the global search value will be used as each column's search value. Then, the search query will be like `{ $or: [{ column_0: value }, ... , { column_n: value }] }`. 49 | 50 | * If there is one or more individual column search value is given and the global search value is not given, then the search query will be like `{ column_0: value_0, ... , column_n: value_n }`. 51 | 52 | * If both individual column and global search value are given, then the search query will be like `{ column_0: value_0, column_1: value_1, $or: [{ column_2 : value }, ... , { column_n: value }] }`. 53 | 54 | __There's More:__ 55 | You can search data in a specific column using global search input element with `column_name:value` format. This will be useful if you want to search data in a specific column or field but you don't want to display search input element for that column. 56 | 57 | Note that this will work only if you specify `name` in `columns` configuration. See [columns.name configuration](https://datatables.net/reference/option/columns.name). 58 | 59 | 60 | ## Usage 61 | 62 | These examples assume that you are using Express v4 63 | 64 | * Using `MongoClient` 65 | 66 | ```js 67 | var express = require('express'); 68 | var mongodb = require('mongodb'); 69 | var MongoDataTable = require('mongo-datatable'); 70 | var MongoClient = mongodb.MongoClient; 71 | var router = express.Router(); 72 | 73 | router.get('/data.json', function(req, res, next) { 74 | var options = req.query; 75 | options.showAlertOnError = true; 76 | 77 | /** 78 | * Using customQuery for specific needs such as 79 | * filtering data which has `role` property set to user 80 | */ 81 | options.customQuery = { 82 | role: 'user' 83 | }; 84 | 85 | /* uncomment the line below to enable case insensitive search */ 86 | // options.caseInsensitiveSearch = true; 87 | 88 | MongoClient.connect('mongodb://localhost/database', function(err, db) { 89 | new MongoDataTable(db).get('collection', options, function(err, result) { 90 | if (err) { 91 | // handle the error 92 | } 93 | 94 | res.json(result); 95 | }); 96 | }); 97 | }); 98 | ... 99 | ``` 100 | 101 | * With MongoDB native driver v3 102 | 103 | ```js 104 | var express = require('express'); 105 | var mongodb = require('mongodb'); 106 | var MongoDataTable = require('mongo-datatable'); 107 | var MongoClient = mongodb.MongoClient; 108 | var router = express.Router(); 109 | 110 | router.get('/data.json', function(req, res, next) { 111 | var options = req.query; 112 | options.showAlertOnError = true; 113 | 114 | /** 115 | * Using customQuery for specific needs such as 116 | * filtering data which has `role` property set to user 117 | */ 118 | options.customQuery = { 119 | role: 'user' 120 | }; 121 | 122 | // uncomment the line below to enable case insensitive search 123 | // options.caseInsensitiveSearch = true; 124 | 125 | 126 | /** 127 | * MongoDB native driver v3, MongoClient.connect no longer yields instance of Db. 128 | * It yields the instance of MongoClient instead. 129 | * To get Db instance, you can call `db` method of client with the database name as the argument 130 | */ 131 | 132 | MongoClient.connect('mongodb://localhost/database', function(err, client) { 133 | var db = client.db('database'); 134 | new MongoDataTable(db).get('collection', options, function(err, result) { 135 | if (err) { 136 | // handle the error 137 | } 138 | 139 | res.json(result); 140 | }); 141 | }); 142 | }); 143 | ... 144 | ``` 145 | 146 | * Using `Db` and `Server` 147 | 148 | ```js 149 | var express = require('express'); 150 | var mongodb = require('mongodb'); 151 | var MongoDataTable = require('mongo-datatable'); 152 | var Db = mongodb.Db; 153 | var Server = mongodb.Server; 154 | var router = express.Router(); 155 | 156 | router.get('/data.json', function(req, res, next) { 157 | var options = req.query; 158 | var db = new Db('database', new Server('localhost', 27017)); 159 | 160 | options.showAlertOnError = true; 161 | 162 | /** 163 | * Using customQuery for specific needs such as 164 | * filtering data which has `role` property set to user 165 | */ 166 | options.customQuery = { 167 | role: 'user' 168 | }; 169 | 170 | // uncomment the line below to enable case insensitive search 171 | // options.caseInsensitiveSearch = true; 172 | 173 | db.open(function(error, db) { 174 | new MongoDataTable(db).get('collection', options, function(err, result) { 175 | if (err) { 176 | // handle the error 177 | } 178 | 179 | res.json(result); 180 | }); 181 | }); 182 | }); 183 | ... 184 | ``` --------------------------------------------------------------------------------