From e72dee4b9304e2691a9cfaca2a5a4c9671480aab Mon Sep 17 00:00:00 2001 From: oof1lab Date: Sat, 11 Nov 2017 23:14:16 +0530 Subject: [PATCH] New API : Groups v0.2.2 --- README.md | 67 ++++++++++++++++++++++----------- lib/util/cmd.helper.js | 2 +- lib/xapi.js | 57 +++++++++++++++++++++++++++- lib/xsql.js | 1 + package.json | 2 +- tests/tests.js | 85 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 189 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 72c9e0fca7..a7dcffd599 100644 --- a/README.md +++ b/README.md @@ -58,22 +58,26 @@ Powered by popular node packages : ([express](https://github.com/expressjs/expre # Features -* Generates API for **ANY** MySql database -* Serves APIs irrespective of naming conventions of primary keys, foreign keys, tables etc -* CRUD : Usual suspects -* Relations -* Support for composite primary keys -* Pagination -* Sorting -* Column filtering - Fields -* Row filtering - Where -* Group By, Having (as query params) -* Group By, Having (as a separate route) -* Aggregate functions -* Run dynamic queries -* Upload single file -* Upload multiple files -* Download file +* Rest API Usual Suspects + * CRUD, List, Count, Exists + * Relations + * Pagination + * Sorting +* Xmysql Rest API - Start of cool things + * Generates API for **ANY** MySql database + * Serves APIs irrespective of naming conventions of primary keys, foreign keys, tables etc + * Support for composite primary keys + * Column filtering - Fields + * Row filtering - Where + * Group By, Having (as query params) + * Group By, Having (as a separate route) + * Aggregate functions + * Union of many group by statements [ AWESOME ALERT ] +* Prototyping (features available with only local MySql server) + * Run dynamic queries + * Upload single file + * Upload multiple files + * Download file Use HTTP clients like [Postman](https://www.getpostman.com/) or [similar tools](https://chrome.google.com/webstore/search/http%20client?_category=apps) to invoke REST API calls @@ -98,13 +102,19 @@ Root URL (localhost:3000/) returns all REST API urls for each table in schema. * GET       /api/tableName/findOne * GET       /api/tableName/count * GET       /api/tableName/:id/exists -* GET       /api/tableName/groupby -* GET       /api/tableName/aggregate * GET       /api/parentTable/:id/childTable * DELETE  /api/tableName/:id -* POST     /dynamic -## Other APIS +## Cool features +* GET       /api/tableName/groupby +* GET       /api/tableName/aggregate +* GET       /api/tableName/groups + +## Only in Prototyping +* POST     /dynamic +* GET       /upload +* GET       /uploads +* GET       /download * GET      /api/tableName/describe * GET      /api/tables @@ -295,7 +305,7 @@ eg: SELECT country,city,count(*) FROM offices GROUP BY country,city ORDER BY cit eg: SELECT country,city,count(*) FROM offices GROUP BY country,city ORDER BY city ASC, country DESC -## Aggregate functions :jack_o_lantern: :sunglasses: +## Aggregate functions ``` http://localhost:3000/api/payments/aggregate?_fields=amount @@ -339,6 +349,21 @@ response body eg: retrieves numeric aggregate can be done for multiple columns too +## Union of many group by statements + +Group by multiple columns in one API call using _fields query params + +``` +http://localhost:3000/api/offices/ugroupby?_fields=country,city,state + +response body +{ + "country":[{"Australia":1},{"France":1},{"Japan":1},{"UK":1},{"USA":3}], + "state":[{"":3},{"CA":1},{"Chiyoda-Ku":1},{"MA":1},{"NY":1}] +} +``` + + ## Run dynamic queries diff --git a/lib/util/cmd.helper.js b/lib/util/cmd.helper.js index 9689e04856..c2cdaef9de 100644 --- a/lib/util/cmd.helper.js +++ b/lib/util/cmd.helper.js @@ -11,7 +11,7 @@ program.on('--help', () => { }) program - .version('0.2.1') + .version('0.2.2') .option('-h, --host ', 'hostname') .option('-d, --database ', 'database schema name') .option('-u, --user ', 'username of database / root by default') diff --git a/lib/xapi.js b/lib/xapi.js index ab23b8a385..0d3acd6f10 100644 --- a/lib/xapi.js +++ b/lib/xapi.js @@ -185,12 +185,16 @@ class Xapi { .get(this.asyncMiddleware(this.groupBy.bind(this))); break; + case 'ugroupby': + this.app.route(routes[i]['routeUrl']) + .get(this.asyncMiddleware(this.ugroupby.bind(this))); + break; + case 'aggregate': this.app.route(routes[i]['routeUrl']) .get(this.asyncMiddleware(this.aggregate.bind(this))); break; - } } } @@ -506,7 +510,6 @@ class Xapi { let results = await this.mysql.exec(query, params); res.status(200).json(results); - } async groupBy(req, res) { @@ -546,9 +549,59 @@ class Xapi { res.status(400).json({message: 'Missing _fields query params eg: /api/tableName/groupby?_fields=column1'}) } + } + + async ugroupby(req, res) { + + if (req.query && req.query._fields) { + + let queryParamsObj = {} + queryParamsObj.query = ''; + queryParamsObj.params = []; + let uGrpByResults = {} + + /**************** add fields with count(*) *****************/ + let fields = req.query._fields.split(',') + + for (var i = 0; i < fields.length; ++i) { + + uGrpByResults[fields[i]] = [] + + if (i) { + queryParamsObj.query += ' UNION ' + } + queryParamsObj.query += ' SELECT IFNULL(CONCAT(?,?,??),?) as ugroupby, count(*) as _count from ?? GROUP BY ?? ' + queryParamsObj.params.push(fields[i]) + queryParamsObj.params.push('~') + queryParamsObj.params.push(fields[i]) + queryParamsObj.params.push(fields[i] + '~') + queryParamsObj.params.push(req.app.locals._tableName) + queryParamsObj.params.push(fields[i]) + } + + //console.log(queryParamsObj.query, queryParamsObj.params); + var results = await this.mysql.exec(queryParamsObj.query, queryParamsObj.params); + + for (var i = 0; i < results.length; ++i) { + + let grpByColName = results[i]['ugroupby'].split('~')[0] + let grpByColValue = results[i]['ugroupby'].split('~')[1] + + let obj = {} + obj[grpByColValue] = results[i]['_count']; + uGrpByResults[grpByColName].push(obj) + + } + + res.status(200).json(uGrpByResults); + + } else { + res.status(400).json({message: 'Missing _fields query params eg: /api/tableName/groupby?_fields=column1'}) + } } + async aggregate(req, res) { diff --git a/lib/xsql.js b/lib/xsql.js index 249c9e21b2..17eceb20fd 100644 --- a/lib/xsql.js +++ b/lib/xsql.js @@ -456,6 +456,7 @@ class Xsql { routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/describe', 'describe')) routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/count', 'count')) routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/groupby', 'groupby')) + routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/ugroupby', 'ugroupby')) routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/aggregate', 'aggregate')) routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/findOne', 'findOne')) routes.push(this.prepareRoute(internal, 'post', apiPrefix, tableName, 'create')) diff --git a/package.json b/package.json index 1dba93f81b..eab80330b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmysql", - "version": "0.2.1", + "version": "0.2.2", "description": "One command to generate REST APIs for any MySql database", "main": "index.js", "scripts": { diff --git a/tests/tests.js b/tests/tests.js index 428f9e2810..4260b66b19 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -863,6 +863,91 @@ describe('xmysql : tests', function () { }); }); + + it('GET /api/offices/ugroupby?_fields=country should PASS', function (done) { + + //post to an url with data + agent.get('/api/offices/ugroupby?_fields=country') //enter url + .expect(200)//200 for success 4xx for failure + .end(function (err, res) { + // Handle /api/v error + if (err) { + return done(err); + } + + //validate response + Object.keys(res.body).length.should.be.equals(1) + res.body['country'].length.should.be.equals(5) + + return done(); + + }); + }); + + + it('GET /api/offices/ugroupby?_fields=country,city,state should PASS', function (done) { + + //post to an url with data + agent.get('/api/offices/ugroupby?_fields=country,city,state') //enter url + .expect(200)//200 for success 4xx for failure + .end(function (err, res) { + // Handle /api/v error + if (err) { + return done(err); + } + + //validate response + //res.body.length.should.be.equals(3) + Object.keys(res.body).length.should.be.equals(3) + res.body['country'].length.should.be.equals(5) + res.body['city'].length.should.be.equals(7) + res.body['state'].length.should.be.equals(5) + + return done(); + + }); + }); + + it('GET /api/offices/ugroupby?_fields=country,city should PASS', function (done) { + + //post to an url with data + agent.get('/api/offices/ugroupby?_fields=country,city') //enter url + .expect(200)//200 for success 4xx for failure + .end(function (err, res) { + // Handle /api/v error + if (err) { + return done(err); + } + + //validate response + Object.keys(res.body).length.should.be.equals(2) + res.body['country'].length.should.be.equals(5) + res.body['city'].length.should.be.equals(7) + + return done(); + + }); + }); + + it('GET /api/offices/ugroupby?_fields= should PASS', function (done) { + + //post to an url with data + agent.get('/api/offices/ugroupby?_fields=') //enter url + .expect(400)//200 for success 4xx for failure + .end(function (err, res) { + // Handle /api/v error + if (err) { + return done(err); + } + + Object.keys(res.body).length.should.be.equals(1) + + return done(); + + }); + }); + + it('GET /api/offices/1/employees?_groupby=jobTitle&_having=(_count,gt,1) should PASS', function (done) { //post to an url with data