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 ?
@@ -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) {