Browse Source

API: Chart added v0.2.3

pull/13/head
oof1lab 7 years ago
parent
commit
16b4b7295f
  1. 137
      README.md
  2. 2
      lib/util/cmd.helper.js
  3. 91
      lib/util/data.helper.js
  4. 75
      lib/xapi.js
  5. 73
      lib/xsql.js
  6. 2
      package.json
  7. 61
      tests/tests.js

137
README.md

@ -5,9 +5,6 @@
# Xmysql : One command to generate REST APIs for any MySql database # Xmysql : One command to generate REST APIs for any MySql database
<div style="background-color:rgb(128,128,128)">
# Why this ? # Why this ?
<p align="center"> <p align="center">
<img src="./assets/rick-and-morty.gif" alt="xmysql gif"/> <img src="./assets/rick-and-morty.gif" alt="xmysql gif"/>
@ -18,8 +15,6 @@ frameworks such as rails, django, laravel etc is a small adventure that one like
Hence this. Hence this.
</div>
# Setup and Usage # Setup and Usage
``` ```
@ -72,10 +67,11 @@ Powered by popular node packages : ([express](https://github.com/expressjs/expre
* Sorting * Sorting
* Column filtering - Fields * Column filtering - Fields
* Row filtering - Where * Row filtering - Where
* Group By, Having (as query params)
* Group By, Having (as a separate route)
* Aggregate functions * Aggregate functions
* Union of many group by statements :fire::fire: **[ HOTNESS ALERT ]** * :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) * Prototyping (features available with ONLY local MySql server)
* Run dynamic queries * Run dynamic queries
* Upload single file * Upload single file
@ -109,9 +105,11 @@ Root URL (localhost:3000/) returns all REST API urls for each table in schema.
* DELETE&nbsp; /api/tableName/:id * DELETE&nbsp; /api/tableName/:id
## HOT features ## HOT features
* GET&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /api/tableName/groupby
* GET&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /api/tableName/aggregate * GET&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /api/tableName/aggregate
* GET&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /api/tableName/groups :fire::fire: **[ HOTNESS ALERT ]** * GET&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /api/tableName/groups :fire::fire: **[ HOTNESS ALERT ]**
* GET&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /api/tableName/groupby :fire::fire:
* GET&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /api/tableName/ugroupby :fire::fire: **[ HOTNESS ALERT ]**
* GET&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /api/tableName/chart :fire::fire: **[ HOTNESS ALERT ]**
## Only in Prototyping :snowboarder: :tophat: ## Only in Prototyping :snowboarder: :tophat:
* POST&nbsp;&nbsp;&nbsp;&nbsp; /dynamic * POST&nbsp;&nbsp;&nbsp;&nbsp; /dynamic
@ -385,9 +383,6 @@ response body
} }
], ],
"reportsTo":[ "reportsTo":[
{
"":1
},
{ {
"1002":2 "1002":2
}, },
@ -406,11 +401,129 @@ response body
{ {
"1621":1 "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 ## Run dynamic queries

2
lib/util/cmd.helper.js

@ -11,7 +11,7 @@ program.on('--help', () => {
}) })
program program
.version('0.2.2') .version('0.2.3')
.option('-h, --host <n>', 'hostname') .option('-h, --host <n>', 'hostname')
.option('-d, --database <n>', 'database schema name') .option('-d, --database <n>', 'database schema name')
.option('-u, --user <n>', 'username of database / root by default') .option('-u, --user <n>', 'username of database / root by default')

91
lib/util/data.helper.js

@ -44,6 +44,92 @@ exports.round = function (number, precision) {
return roundedTempNumber / factor; 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 () { 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, ' + 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, ' + 'c.column_comment,c.column_default,c.data_type,c.character_maximum_length, ' +
@ -68,3 +154,8 @@ exports.getSchemaQuery = function () {
'order by ' + 'order by ' +
'c.table_name, c.ordinal_position'; 'c.table_name, c.ordinal_position';
}; };
exports.getChartQuery = function () {
return 'select ? as ??, count(*) as _count from ?? where ?? between ? and ? '
}

75
lib/xapi.js

@ -190,6 +190,11 @@ class Xapi {
.get(this.asyncMiddleware(this.ugroupby.bind(this))); .get(this.asyncMiddleware(this.ugroupby.bind(this)));
break; break;
case 'chart':
this.app.route(routes[i]['routeUrl'])
.get(this.asyncMiddleware(this.chart.bind(this)));
break;
case 'aggregate': case 'aggregate':
this.app.route(routes[i]['routeUrl']) this.app.route(routes[i]['routeUrl'])
.get(this.asyncMiddleware(this.aggregate.bind(this))); .get(this.asyncMiddleware(this.aggregate.bind(this)));
@ -597,7 +602,7 @@ class Xapi {
res.status(200).json(uGrpByResults); res.status(200).json(uGrpByResults);
} else { } 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,9 +642,75 @@ class Xapi {
res.status(200).json(results); res.status(200).json(results);
} else { } 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'});
} }
} }

73
lib/xsql.js

@ -230,7 +230,7 @@ class Xsql {
if (orderByCols[i][0] === '-') { if (orderByCols[i][0] === '-') {
let len = orderByCols[i].length; let len = orderByCols[i].length;
queryParamsObj.query += ' ?? DESC' queryParamsObj.query += ' ?? DESC'
queryParamsObj.params.push(orderByCols[i].substring(1, len) ) queryParamsObj.params.push(orderByCols[i].substring(1, len))
} else { } else {
queryParamsObj.query += ' ?? ASC' queryParamsObj.query += ' ?? ASC'
queryParamsObj.params.push(orderByCols[i]) 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 + '/count', 'count'))
routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/groupby', 'groupby')) 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 + '/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 + '/aggregate', 'aggregate'))
routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/findOne', 'findOne')) routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/findOne', 'findOne'))
routes.push(this.prepareRoute(internal, 'post', apiPrefix, tableName, 'create')) 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
}
} }

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "xmysql", "name": "xmysql",
"version": "0.2.2", "version": "0.2.3",
"description": "One command to generate REST APIs for any MySql database", "description": "One command to generate REST APIs for any MySql database",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

61
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) { it('GET /api/offices/1/employees?_groupby=jobTitle&_having=(_count,gt,1) should PASS', function (done) {

Loading…
Cancel
Save