diff --git a/README.md b/README.md index 5811355276..b4410a6399 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,6 @@ # Xmysql : One command to generate REST APIs for any MySql database -
- - # Why this ?

xmysql gif @@ -18,8 +15,6 @@ frameworks such as rails, django, laravel etc is a small adventure that one like Hence this. -

- # Setup and Usage ``` @@ -72,10 +67,11 @@ Powered by popular node packages : ([express](https://github.com/expressjs/expre * Sorting * 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 :fire::fire: **[ HOTNESS ALERT ]** +* Aggregate functions +* :fire::fire: Group By, Having (as query params) +* :fire::fire: Group By, Having (as a separate route) **[ HOTNESS ALERT ]** +* :fire::fire: Multiple group by API :fire::fire: **[ HOTNESS ALERT ]** +* :fire::fire: Chart API :fire::fire: **[ HOTNESS ALERT ]** * Prototyping (features available with ONLY local MySql server) * Run dynamic queries * Upload single file @@ -109,9 +105,11 @@ Root URL (localhost:3000/) returns all REST API urls for each table in schema. * DELETE  /api/tableName/:id ## HOT features -* GET       /api/tableName/groupby * GET       /api/tableName/aggregate * GET       /api/tableName/groups :fire::fire: **[ HOTNESS ALERT ]** +* GET       /api/tableName/groupby :fire::fire: +* GET       /api/tableName/ugroupby :fire::fire: **[ HOTNESS ALERT ]** +* GET       /api/tableName/chart :fire::fire: **[ HOTNESS ALERT ]** ## Only in Prototyping :snowboarder: :tophat: * POST     /dynamic @@ -385,9 +383,6 @@ response body } ], "reportsTo":[ - { - "":1 - }, { "1002":2 }, @@ -406,11 +401,129 @@ response body { "1621":1 } + { + "":1 + }, ] } ``` +## Chart :fire::fire: **[ HOTNESS ALERT ]** + +Chart API returns distribution of a numeric column in a table + +It comes in three flavours + +1. Chart : With min, max, step in query params :fire::fire: + +This API returns the number of rows where amount is between (0,25000), (25001,50000) ... + +``` +/api/payments/chart?_fields=amount&min=0&max=131000&step=25000 + +Response + +[ + { + "amount": "0 to 25000", + "_count": 107 + }, + { + "amount": "25001 to 50000", + "_count": 124 + }, + { + "amount": "50001 to 75000", + "_count": 30 + }, + { + "amount": "75001 to 100000", + "_count": 7 + }, + { + "amount": "100001 to 125000", + "_count": 5 + }, + { + "amount": "125001 to 150000", + "_count": 0 + } +] + +``` + +2. Chart : With step array in params :fire::fire: + +This API returns distribution between the step array specified + +``` +/api/payments/chart?_fields=amount&steparray=0,10000,20000,70000,140000 + +Response + +[ + { + "amount": "0 to 10000", + "_count": 42 + }, + { + "amount": "10001 to 20000", + "_count": 36 + }, + { + "amount": "20001 to 70000", + "_count": 183 + }, + { + "amount": "70001 to 140000", + "_count": 12 + } +] + + +``` + +3. Chart : with no params :fire::fire: + +This API figures out even distribution of a numeric column in table and returns the data + +``` +/api/payments/chart?_fields=amount + +Response +[ + { + "amount": "-9860 to 11100", + "_count": 45 + }, + { + "amount": "11101 to 32060", + "_count": 91 + }, + { + "amount": "32061 to 53020", + "_count": 109 + }, + { + "amount": "53021 to 73980", + "_count": 16 + }, + { + "amount": "73981 to 94940", + "_count": 7 + }, + { + "amount": "94941 to 115900", + "_count": 3 + }, + { + "amount": "115901 to 130650", + "_count": 2 + } +] + +``` ## Run dynamic queries diff --git a/lib/util/cmd.helper.js b/lib/util/cmd.helper.js index c2cdaef9de..cee0b42a16 100644 --- a/lib/util/cmd.helper.js +++ b/lib/util/cmd.helper.js @@ -11,7 +11,7 @@ program.on('--help', () => { }) program - .version('0.2.2') + .version('0.2.3') .option('-h, --host ', 'hostname') .option('-d, --database ', 'database schema name') .option('-u, --user ', 'username of database / root by default') diff --git a/lib/util/data.helper.js b/lib/util/data.helper.js index 0f67979d36..de21f6a5c4 100644 --- a/lib/util/data.helper.js +++ b/lib/util/data.helper.js @@ -44,6 +44,92 @@ exports.round = function (number, precision) { return roundedTempNumber / factor; }; + +exports.numberRound = (number, precision) => { + var factor = Math.pow(10, precision); + var tempNumber = number * factor; + var roundedTempNumber = Math.round(tempNumber); + return roundedTempNumber / factor; +} + +exports.numberGetLength = (number) => { + + var n = number; + + if (number < 0) { + n = n * -1; + } + + return n.toString().length; +} + +exports.numberGetFixed = (number) => { + //console.log(number, typeof number); + return parseInt(number.toFixed()) +} + +exports.getRangeSimple = function (min, max, step) { + + var arr = [] + for (var i = min; i <= max; i = i + step) { + arr.push(i) + } + + return arr; + +}; + +exports.getRange = (min, max, stddev) => { + + // console.log(' = = = = = = = '); + //console.log('original numbers', min, max, stddev); + + min = this.numberGetFixed(min) + max = this.numberGetFixed(max) + stddev = this.numberGetFixed(stddev) + + // console.log('fixed numbers', min, max, stddev); + + let minMinusHalf = min - stddev / 2 + let maxMinusHalf = max + stddev / 2 + + minMinusHalf = this.numberGetFixed(minMinusHalf) + maxMinusHalf = this.numberGetFixed(maxMinusHalf) + + // console.log('fixed numbers + (min,max)', min, max, stddev, '(', minMinusHalf, ',', maxMinusHalf, ')'); + // console.log('numbers length', 'min', numberGetLength(min), 'max', numberGetLength(max), 'stddev', numberGetLength(stddev)); + + let minLen = this.numberGetLength(minMinusHalf) + let maxLen = this.numberGetLength(maxMinusHalf) + let stddevLen = this.numberGetLength(stddev) + // + // console.log('- - - -'); + // console.log('Range', 'min', numberRound(minMinusHalf, -1)); + // console.log('Range', 'max', numberRound(maxMinusHalf, -1)); + // console.log('Range', 'stddev', numberRound(stddev, -1)); + + if (minLen > 1) + minMinusHalf = this.numberRound(minMinusHalf, -1) + + if (maxLen > 2) + maxMinusHalf = this.numberRound(maxMinusHalf, -1) + + if (stddevLen !== 1) + stddev = this.numberRound(stddev, -1) + + + var arr = [] + for (var step = minMinusHalf; step < maxMinusHalf; step = step + stddev) { + arr.push(step) + } + arr.push(maxMinusHalf) + + // console.log(arr); + + return arr; +} + + exports.getSchemaQuery = function () { return 'select c.table_name, c.column_name, c.ordinal_position,c.column_key,c.is_nullable, c.data_type, c.column_type,c.extra,c.privileges, ' + 'c.column_comment,c.column_default,c.data_type,c.character_maximum_length, ' + @@ -68,3 +154,8 @@ exports.getSchemaQuery = function () { 'order by ' + 'c.table_name, c.ordinal_position'; }; + +exports.getChartQuery = function () { + return 'select ? as ??, count(*) as _count from ?? where ?? between ? and ? ' +} + diff --git a/lib/xapi.js b/lib/xapi.js index 0d3acd6f10..9e22448136 100644 --- a/lib/xapi.js +++ b/lib/xapi.js @@ -190,6 +190,11 @@ class Xapi { .get(this.asyncMiddleware(this.ugroupby.bind(this))); break; + case 'chart': + this.app.route(routes[i]['routeUrl']) + .get(this.asyncMiddleware(this.chart.bind(this))); + break; + case 'aggregate': this.app.route(routes[i]['routeUrl']) .get(this.asyncMiddleware(this.aggregate.bind(this))); @@ -597,7 +602,7 @@ class Xapi { res.status(200).json(uGrpByResults); } else { - res.status(400).json({message: 'Missing _fields query params eg: /api/tableName/groupby?_fields=column1'}) + res.status(400).json({message: 'Missing _fields query params eg: /api/tableName/ugroupby?_fields=column1,column2'}) } } @@ -637,12 +642,78 @@ class Xapi { res.status(200).json(results); } else { - res.status(400).json({message: 'Missing _fields in query params eg: /api/tableName/groupby?_fields=numericColumn1'}); + res.status(400).json({message: 'Missing _fields in query params eg: /api/tableName/aggregate?_fields=numericColumn1'}); } } + async chart(req, res) { + + let query = '' + let params = [] + let obj = {} + + if (req.query && req.query._fields) { + + if (req.query && req.query.min && req.query.max && req.query.step) { + + //console.log(req.params.min, req.params.max, req.params.step); + + obj = this.mysql.getChartQueryAndParamsFromMinMaxStep(req.app.locals._tableName, + req.query._fields, + parseInt(req.query.min), + parseInt(req.query.max), + parseInt(req.query.step)) + + + } else if (req.query && req.query.steparray && req.query.steparray.length > 1) { + + obj = this.mysql.getChartQueryAndParamsFromStepArray(req.app.locals._tableName, + req.query._fields, + (req.query.steparray.split(',')).map(Number)) + + + } else { + + query = 'select min(??) as min,max(??) as max,stddev(??) as stddev,avg(??) as avg from ??'; + params = []; + + params.push(req.query._fields); + params.push(req.query._fields); + params.push(req.query._fields); + params.push(req.query._fields); + params.push(req.app.locals._tableName); + + let _this = this; + + let results = await + _this.mysql.exec(query, params); + + //console.log(results, results['max'], req.params); + + obj = _this.mysql.getChartQueryAndParamsFromMinMaxStddev(req.app.locals._tableName, + req.query._fields, + results[0]['min'], + results[0]['max'], + results[0]['stddev'] + ) + + } + + let results = await + this.mysql.exec(obj.query, obj.params); + + res.status(200).json(results); + + } else { + res.status(400).json({message: 'Missing _fields in query params eg: /api/tableName/chart?_fields=numericColumn1'}); + } + + + } + + /**************** START : files related ****************/ downloadFile(req, res) { let file = path.join(process.cwd(), req.query.name); diff --git a/lib/xsql.js b/lib/xsql.js index 17eceb20fd..f19e37f973 100644 --- a/lib/xsql.js +++ b/lib/xsql.js @@ -230,7 +230,7 @@ class Xsql { if (orderByCols[i][0] === '-') { let len = orderByCols[i].length; queryParamsObj.query += ' ?? DESC' - queryParamsObj.params.push(orderByCols[i].substring(1, len) ) + queryParamsObj.params.push(orderByCols[i].substring(1, len)) } else { queryParamsObj.query += ' ?? ASC' queryParamsObj.params.push(orderByCols[i]) @@ -457,6 +457,7 @@ class Xsql { 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 + '/chart', 'chart')) 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')) @@ -499,6 +500,76 @@ class Xsql { } + getChartQueryAndParamsFromStepArray(tableName, columnName, stepArray) { + + let obj = {} + + obj.query = '' + obj.params = [] + + if (stepArray.length && stepArray.length >= 2) { + + let params = [tableName, columnName, stepArray[0], stepArray[1]] + + for (let i = 0; i < stepArray.length - 1; i = i + 1) { + + obj.query = obj.query + dataHelp.getChartQuery(); + if (i + 2 < stepArray.length) { + obj.query = obj.query + ' union ' + } + + if (i) { + stepArray[i] = stepArray[i] + 1 + } + + obj.params.push((stepArray[i]) + ' to ' + stepArray[i + 1]) + obj.params.push(columnName) + obj.params.push(tableName) + obj.params.push(columnName) + obj.params.push(stepArray[i]) + obj.params.push(stepArray[i + 1]) + + } + + } + + //console.log('step spread query', obj); + + return obj; + + } + + + getChartQueryAndParamsFromMinMaxStddev(tableName, columnName, min, max, stddev) { + + let stepArray = dataHelp.getRange(min, max, stddev) + + //console.log('steparray', stepArray); + + let obj = this.getChartQueryAndParamsFromStepArray(tableName, columnName, stepArray) + + //console.log('steparray', obj); + + return obj + + } + + getChartQueryAndParamsFromMinMaxStep(tableName, columnName, min, max, step) { + + let stepArray = dataHelp.getRangeSimple(min, max, step) + + //console.log('steparray', stepArray); + + let obj = this.getChartQueryAndParamsFromStepArray(tableName, columnName, stepArray) + + //console.log('steparray', obj); + + return obj + + } + + + } diff --git a/package.json b/package.json index eab80330b4..223681a601 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmysql", - "version": "0.2.2", + "version": "0.2.3", "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 4260b66b19..f5bcc78126 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -947,6 +947,67 @@ describe('xmysql : tests', function () { }); }); + it('GET /api/payments/chart?_fields=amount should PASS', function (done) { + + //post to an url with data + agent.get('/api/payments/chart?_fields=amount') //enter url + .expect(200)//200 for success 4xx for failure + .end(function (err, res) { + // Handle /api/v error + if (err) { + return done(err); + } + + res.body.length.should.be.equals(7) + res.body[0]['_count'].should.be.equals(45) + res.body[2]['_count'].should.be.equals(109) + res.body[6]['_count'].should.be.equals(2) + + return done(); + + }); + }) + + it('GET /api/payments/chart?_fields=amount&min=0&max=131000&step=25000 should PASS', function (done) { + + //post to an url with data + agent.get('/api/payments/chart?_fields=amount&min=0&max=131000&step=25000') //enter url + .expect(200)//200 for success 4xx for failure + .end(function (err, res) { + // Handle /api/v error + if (err) { + return done(err); + } + + res.body.length.should.be.equals(5) + res.body[0]['_count'].should.be.equals(107) + res.body[1]['_count'].should.be.equals(124) + + return done(); + + }); + }) + + it('GET /api/payments/chart?_fields=amount&steparray=0,50000,100000,140000 should PASS', function (done) { + + //post to an url with data + agent.get('/api/payments/chart?_fields=amount&steparray=0,50000,100000,140000') //enter url + .expect(200)//200 for success 4xx for failure + .end(function (err, res) { + // Handle /api/v error + if (err) { + return done(err); + } + + res.body.length.should.be.equals(3) + res.body[0]['_count'].should.be.equals(231) + res.body[1]['_count'].should.be.equals(37) + res.body[2]['_count'].should.be.equals(5) + + return done(); + + }); + }) it('GET /api/offices/1/employees?_groupby=jobTitle&_having=(_count,gt,1) should PASS', function (done) {