diff --git a/README.md b/README.md index 7cfd2ce..c31dcdf 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ __Extra Options:__ * `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`. * `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. +* `aggregateQuery` (*Array*) - Run custom aggregate query (pipeline). Be sure to manually select which data fields will be returned using projection stage and name those fields according to jquery datatable column names. Filtering (comming from front-end jquery datatable), sorting, and pagination will be automatically executed as final stages of the aggregate pipeline. * `caseInsensitiveSearch` (*Boolean*) - To enable case insensitive search, set this option value to `true`. It is case sensitive by default. #### Search Operation @@ -181,4 +182,38 @@ router.get('/data.json', function(req, res, next) { }); }); ... +``` + +* Using custom `aggregateQuery` option. Front-end jquery datatable has two columns: `role` and `count` + +```js +var express = require('express'); +var mongodb = require('mongodb'); +var MongoDataTable = require('mongo-datatable'); +var MongoClient = mongodb.MongoClient; +var router = express.Router(); + +router.get('/data.json', function(req, res, next) { + var options = req.query; //query comming from front-end jquery datatable + options.showAlertOnError = true; + + options.aggregateQuery = [ + {$group: { + _id: '$role', + role: {$last: '$role'} + count: {$sum: '1'} + }}, + {$project: { + role: 1, + count: 1 + }} + ]; + + MongoClient.connect('mongodb://localhost/database', function(err, db) { + new MongoDataTable(db).get('collection', options, function(err, result) { + res.json(result); + }); + }); +}); +... ``` \ No newline at end of file diff --git a/lib/MongoDataTable.js b/lib/MongoDataTable.js index 34c8b40..6db3dd8 100644 --- a/lib/MongoDataTable.js +++ b/lib/MongoDataTable.js @@ -22,6 +22,7 @@ MongoDataTable.prototype.get = function(collectionName, options, onDataReady) { }; var searchCriteria = cols.buildSearchCriteria(options); + var aggQuery = cols.buildAggregateQuery(searchCriteria, options); function getCollectionLength(callback) { if (self.db === null || typeof self.db === 'undefined') { @@ -31,19 +32,46 @@ MongoDataTable.prototype.get = function(collectionName, options, onDataReady) { var earlyCollection = self.db.collection(collectionName); response.draw = parseInt(options.draw, 10); - earlyCollection - .find(searchCriteria, columns) - .count(function(error, result) { + if (options.aggregateQuery) { + + var aggCountQuery = aggQuery.concat([{$group: { _id: null, count: { $sum: 1 } }}]); + + earlyCollection.aggregate(aggCountQuery).toArray(function(error, result) { if (error) { return callback(error, null); } - response.recordsTotal = result; - response.recordsFiltered = result; + if (result.length==1) { + response.recordsTotal = result[0]['count']; + response.recordsFiltered = result[0]['count']; + } else { + response.recordsTotal = 0; + response.recordsFiltered = 0; + } + return callback(null); }); + + } else { + + earlyCollection + .find(searchCriteria, columns) + .count(function(error, result) { + + if (error) { + return callback(error, null); + } + + response.recordsTotal = result; + response.recordsFiltered = result; + + return callback(null); + }); + + } + } function validateOptions(callback) { @@ -61,20 +89,45 @@ MongoDataTable.prototype.get = function(collectionName, options, onDataReady) { } function getAndSortData(callback) { - var sortOrder = cols.buildColumnSortOrder(options); + var collection = self.db.collection(collectionName); - collection = collection.find(searchCriteria, columns); + if (options.aggregateQuery) { + + var aggSortOrder = cols.buildAggregateQuerySortOrder(options); + + if (Object.keys(aggSortOrder['$sort']).length>0) { + aggQuery.push(aggSortOrder); + } + + if (parseInt(options.length) > 0) { + aggQuery.push({ + '$skip': parseInt(options.start), + }); + aggQuery.push({ + '$limit': parseInt(options.length), + }); + } + + collection = collection.aggregate(aggQuery); + + } else { + + var sortOrder = cols.buildColumnSortOrder(options); + + collection = collection.find(searchCriteria, columns); + if (parseInt(options.length) > 0) { + collection = collection + .skip(parseInt(options.start)) + .limit(parseInt(options.length)); + } + + forEach(sortOrder, function(order) { + collection = collection.sort(order); + }); - if (parseInt(options.length) > 0) { - collection = collection - .skip(parseInt(options.start)) - .limit(parseInt(options.length)); } - forEach(sortOrder, function(order) { - collection = collection.sort(order); - }); collection.toArray(callback); } diff --git a/lib/columns.js b/lib/columns.js index 8f54f4e..5b64b55 100644 --- a/lib/columns.js +++ b/lib/columns.js @@ -90,6 +90,44 @@ function buildColumnSortOrder(options) { return sortOrder; } +function buildAggregateQuery(searchCriteria, options) { + + var aggQuery = []; + + if (options.aggregateQuery) { + + options.aggregateQuery.forEach(function (query){ + aggQuery.push(query); + }); + + if (Object.keys(searchCriteria).length>0) { + aggQuery.push({ + '$match': searchCriteria + }); + } + + } + + return aggQuery; + +} + +function buildAggregateQuerySortOrder(options) { + var aggSortOrder = {'$sort':{}}; + var columns = options.columns; + var currentColumn; + + forEach(options.order, function(order) { + currentColumn = columns[order.column]; + + if (currentColumn.orderable === 'true' || currentColumn.orderable === true) { + aggSortOrder['$sort'][currentColumn.data] = (order.dir === 'asc') ? 1 : -1; + } + }); + + return aggSortOrder; +} + function extractColumns(options) { var columns = {}; @@ -122,5 +160,7 @@ function parseSearchValue(value, caseInsensitive) { exports.buildSearchCriteria = buildSearchCriteria; exports.buildColumnSortOrder = buildColumnSortOrder; +exports.buildAggregateQuerySortOrder = buildAggregateQuerySortOrder; +exports.buildAggregateQuery = buildAggregateQuery; exports.extractColumns = extractColumns; exports.getSearchableColumns = getSearchableColumns; diff --git a/lib/validator.js b/lib/validator.js index 7de41cb..7e69e7c 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -1,3 +1,5 @@ +var util = require('util'); + function isOptionsValid(options, immediateCallback) { if (typeof options === 'undefined') return immediateCallback(new Error('Options must be defined!')); @@ -11,6 +13,9 @@ function isOptionsValid(options, immediateCallback) { if (typeof options.search === 'undefined') return immediateCallback(new Error('Search field must be defined!')); + if (typeof options.aggregateQuery !== 'undefined' && (!util.isArray(options.aggregateQuery) || options.aggregateQuery.length==0) ) + return immediateCallback(new Error('Aggregate query must be non empty array object!')); + var isStartValid = (typeof options.start !== 'undefined' || parseInt(options.start, 10) >= 0);