Browse Source

Feature : xjoin API 🔥 🔥

npm v0.4.0
pull/13/head
oof1lab 7 years ago
parent
commit
d048221a89
  1. 71
      README.md
  2. 2
      lib/util/cmd.helper.js
  3. 179
      lib/util/whereClause.helper.js
  4. 227
      lib/xapi.js
  5. 49
      lib/xsql.js
  6. 2
      package.json
  7. 103
      tests/tests.js

71
README.md

@ -74,7 +74,8 @@ Powered by popular node packages : ([express](https://github.com/expressjs/expre
* Group By, Having (as a separate API) :fire::fire:
* Multiple group by in one API :fire::fire::fire::fire:
* Chart API for numeric column :fire::fire::fire::fire::fire::fire:
* Auto Chart API - (Must see : a gift to lazy while prototyping) :fire::fire::fire::fire::fire::fire:
* Auto Chart API - (a gift for lazy while prototyping) :fire::fire::fire::fire::fire::fire:
* #### [XJOIN - (Supports any number of JOINS)](#xjoin) :fire::fire::fire::fire::fire::fire::fire::fire::fire::fire::fire::fire:
* Supports views
* Prototyping (features available when using local MySql server only)
* Run dynamic queries :fire::fire::fire:
@ -118,6 +119,7 @@ if you haven't on your system.
| GET :fire:| [/api/tableName/ugroupby](#union-of-multiple-group-by-statements) | Multiple group by results using one call |
| GET :fire:| [/api/tableName/chart](#chart) | Numeric column distribution based on (min,max,step) or(step array) or (automagic)|
| GET :fire:| [/api/tableName/autochart](#autochart) | Same as Chart but identifies which are numeric column automatically - gift for lazy while prototyping|
| GET :fire:| [/api/xjoin](#xjoin) | handles join |
| GET :fire:| [/dynamic](#run-dynamic-queries) | execute dynamic mysql statements with params |
| GET :fire:| [/upload](#upload-single-file) | upload single file |
| GET :fire:| [/uploads](#upload-multiple-files) | upload multiple files |
@ -684,6 +686,71 @@ http://localhost:3000/api/payments/autochart
]
```
## XJOIN
### Xjoin Syntax:
```
_join : List of tableNames alternated by type of join to be made (_j, _ij,_ lj, _rj, _fj)
alias.tableName : TableName as alias
_j : Join [ _j => join, _ij => ij, _lj => left join , _rj => right join , _fj => full join)
_onNumber : Number 'n' indicates condition to be applied for 'n'th join between (n-1) and 'n'th table in list
```
#### Simple example of two table join:
Sql join query:
```sql
SELECT *
FROM productlines as pl
JOIN products as pr
ON pl.productline = pr.productline
```
Equivalent xjoin query API:
```
/api/xjoin?_join=pl.productlines,j,pr.products&_on1=(pl.productline,eq,pr.productline)
```
#### Multiple tables join
Sql join query:
```sql
SELECT *
FROM productlines as pl
JOIN products as pr
ON pl.productline = pr.productline
JOIN orderdetails as ord
ON pr.productcode = ord.productcode
```
Equivalent xjoin query API:
```
/api/xjoin?_join=pl.productlines,j,pr.products,j,ord.orderDetails&_on1=(pl.productline,eq,pr.productline)&_on2=(pr.productcode,eq,ord.productcode)
```
**Explanation:**
> pl.productlines => productlines as pl
> _j => join
> pr.products => products as pl
> _on1 => join condition between productlines and products => (pl.productline,eq,pr.productline)
> _on2 => join condition between products and orderdetails => (pr.productcode,eq,ord.productcode)
Example to use : _fields, _where, _p, _size in query params
```
/api/xjoin?_join=pl.productlines,_j,pr.products&_on1=(pl.productline,eq,pr.productline)&_fields=pl.productline,pr.productName&_size=2&_where=(productName,like,1972~)
```
## Run dynamic queries
[:arrow_heading_up:](#api-overview)
@ -758,7 +825,7 @@ http://localhost:3000/download?name=fileName
## When to use ?
[:arrow_heading_up:](#api-overview)
* You need just REST APIs without much hassle for (ANY) MySql database.
* You need just REST APIs for (ANY) MySql database at blink of an eye (literally).
* You are learning new frontend frameworks and need REST APIs for your MySql database.
* You are working on a demo, hacks etc

2
lib/util/cmd.helper.js

@ -11,7 +11,7 @@ program.on('--help', () => {
})
program
.version('0.3.4')
.version('0.4.0')
.option('-h, --host <n>', 'hostname / localhost by default')
.option('-u, --user <n>', 'username of database / root by default')
.option('-p, --password <n>', 'password of database / empty by default')

179
lib/util/whereClause.helper.js

@ -78,7 +78,7 @@ function prepareBetweenValue(value) {
}
function replaceWhereParamsToQMarks(openParentheses, str, comparisonOperator) {
function replaceWhereParamsToQMarks(openParentheses, str, comparisonOperator, condType) {
let converted = ''
@ -93,8 +93,11 @@ function replaceWhereParamsToQMarks(openParentheses, str, comparisonOperator) {
}
} else {
if (comparisonOperator !== ' in ' && comparisonOperator !== ' between ')
if (comparisonOperator !== ' in ' && comparisonOperator !== ' between ' && condType !== ' on ') {
converted = '?'
} else if (condType === ' on ') {
converted = '??'
}
for (var i = str.length - 1; i >= 0; --i) {
if (str[i] === ')') {
@ -105,6 +108,7 @@ function replaceWhereParamsToQMarks(openParentheses, str, comparisonOperator) {
}
}
return converted;
}
@ -215,116 +219,113 @@ exports.getConditionClause = function (whereInQueryParams, condType = 'where') {
if (numOfConditions && logicalOperatorsInClause && numOfConditions.length !== logicalOperatorsInClause.length + 1) {
console.log('conditions and logical operators mismatch', numOfConditions.length, logicalOperatorsInClause.length);
return
}
// console.log('numOfConditions',numOfConditions,whereInQueryParams);
// console.log('logicalOperatorsInClause',logicalOperatorsInClause);
for (var i = 0; i < numOfConditions.length; ++i) {
let variable = ''
let comparisonOperator = ''
let logicalOperator = ''
let variableValue = ''
let temp = ''
} else {
//console.log('numOfConditions',numOfConditions,whereInQueryParams);
//console.log('logicalOperatorsInClause',logicalOperatorsInClause);
// split on commas
var arr = numOfConditions[i].split(',');
for (var i = 0; i < numOfConditions.length; ++i) {
// consider first two splits only
var result = arr.splice(0, 2);
let variable = ''
let comparisonOperator = ''
let logicalOperator = ''
let variableValue = ''
let temp = ''
// join to make only 3 array elements
result.push(arr.join(','));
// split on commas
var arr = numOfConditions[i].split(',');
// variable, operator, variablevalue
if (result.length !== 3) {
grammarErr = 1;
break;
}
// consider first two splits only
var result = arr.splice(0, 2);
/**************** START : variable ****************/
//console.log(result);
variable = result[0].match(/\(+(.*)/);
// join to make only 3 array elements
result.push(arr.join(','));
// console.log('variable',variable);
if (!variable || variable.length !== 2) {
grammarErr = 1;
break;
}
// variable, operator, variablevalue
if (result.length !== 3) {
grammarErr = 1;
break;
}
// get the variableName and push
whereParams.push(variable[1])
/**************** START : variable ****************/
//console.log('variable, operator, variablevalue',result);
variable = result[0].match(/\(+(.*)/);
// then replace the variable name with ??
temp = replaceWhereParamsToQMarks(true, result[0])
if (!temp) {
grammarErr = 1;
break;
}
whereQuery += temp
//console.log('variable',variable);
/**************** END : variable ****************/
if (!variable || variable.length !== 2) {
grammarErr = 1;
break;
}
// get the variableName and push
whereParams.push(variable[1])
/**************** START : operator and value ****************/
comparisonOperator = getComparisonOperator(result[1])
if (!comparisonOperator) {
grammarErr = 1;
break;
}
whereQuery += comparisonOperator
// then replace the variable name with ??
temp = replaceWhereParamsToQMarks(true, result[0], ' ignore ', condType)
if (!temp) {
grammarErr = 1;
break;
}
whereQuery += temp
// get the variableValue and push to params
variableValue = result[2].match(/(.*?)\)/)
if (!variableValue || variableValue.length !== 2) {
grammarErr = 1;
break;
}
/**************** END : variable ****************/
if (comparisonOperator === ' in ') {
let obj = prepareInClauseParams(variableValue[1])
whereQuery += obj.whereQuery
whereParams = whereParams.concat(obj.whereParams)
} else if (comparisonOperator === ' like ' || comparisonOperator === ' not like ') {
whereParams.push(prepareLikeValue(variableValue[1]))
} else if (comparisonOperator === ' is ') {
whereParams.push(prepareIsValue(variableValue[1]))
} else if (comparisonOperator === ' between ') {
let obj = prepareBetweenValue(variableValue[1])
whereQuery += obj.whereQuery
whereParams = whereParams.concat(obj.whereParams)
//console.log(whereQuery, whereParams);
} else {
whereParams.push(variableValue[1])
}
// then replace the variableValue with ?
temp = replaceWhereParamsToQMarks(false, result[2], comparisonOperator)
if (!temp) {
grammarErr = 1;
break;
}
whereQuery += temp
/**************** START : operator and value ****************/
comparisonOperator = getComparisonOperator(result[1])
if (!comparisonOperator) {
grammarErr = 1;
break;
}
whereQuery += comparisonOperator
// get the variableValue and push to params
variableValue = result[2].match(/(.*?)\)/)
if (!variableValue || variableValue.length !== 2) {
grammarErr = 1;
break;
}
// only
if (numOfConditions.length !== -1 && i !== numOfConditions.length - 1) {
//console.log('finding logical operator for',logicalOperatorsInClause[i]);
logicalOperator = getLogicalOperator(logicalOperatorsInClause[i])
if (comparisonOperator === ' in ') {
let obj = prepareInClauseParams(variableValue[1])
whereQuery += obj.whereQuery
whereParams = whereParams.concat(obj.whereParams)
} else if (comparisonOperator === ' like ' || comparisonOperator === ' not like ') {
whereParams.push(prepareLikeValue(variableValue[1]))
} else if (comparisonOperator === ' is ') {
whereParams.push(prepareIsValue(variableValue[1]))
} else if (comparisonOperator === ' between ') {
let obj = prepareBetweenValue(variableValue[1])
whereQuery += obj.whereQuery
whereParams = whereParams.concat(obj.whereParams)
//console.log(whereQuery, whereParams);
} else {
whereParams.push(variableValue[1])
}
if (!logicalOperator) {
// then replace the variableValue with ?
temp = replaceWhereParamsToQMarks(false, result[2], comparisonOperator, condType)
if (!temp) {
grammarErr = 1;
break;
}
whereQuery += temp
whereQuery += getLogicalOperator(logicalOperatorsInClause[i])
}
/**************** END : operator ****************/
if (numOfConditions.length !== -1 && i !== numOfConditions.length - 1) {
//console.log('finding logical operator for',logicalOperatorsInClause[i]);
logicalOperator = getLogicalOperator(logicalOperatorsInClause[i])
if (!logicalOperator) {
grammarErr = 1;
break;
}
whereQuery += getLogicalOperator(logicalOperatorsInClause[i])
}
/**************** END : operator ****************/
}
}
let obj = {}

227
lib/xapi.js

@ -1,10 +1,12 @@
'use strict';
var Xsql = require('./xsql.js');
var whrHelp = require('./util/whereClause.helper.js');
var multer = require('multer');
var path = require('path');
const colors = require('colors');
//define class
class Xapi {
@ -110,6 +112,11 @@ class Xapi {
this.app.route('/api/tables')
.get(this.asyncMiddleware(this.tables.bind(this)));
this.app.route('/api/xjoin')
.get(this.asyncMiddleware(this.xjoin.bind(this)));
stat.api += 3;
/**************** START : setup routes for each table ****************/
let resources = [];
@ -276,12 +283,34 @@ class Xapi {
}
_getGrpByHavingOrderBy(req, tableName, queryParamsObj, listType) {
/**************** add group by ****************/
this.mysql.getGroupByClause(req.query._groupby, req.app.locals._tableName, queryParamsObj);
/**************** add having ****************/
this.mysql.getHavingClause(req.query._having, req.app.locals._tableName, queryParamsObj);
/**************** add order clause ****************/
this.mysql.getOrderByClause(req.query, req.app.locals._tableName, queryParamsObj);
/**************** add limit clause ****************/
if (listType === 2) { //nested
queryParamsObj.query += ' limit 1 '
} else {
queryParamsObj.query += ' limit ?,? '
queryParamsObj.params = queryParamsObj.params.concat(this.mysql.getLimitClause(req.query));
}
}
/**
*
* @param req
* @param res
* @param queryParamsObj : {query, params}
* @param listType : 0:list, 1:nested, 2:findOne, 3:bulkRead, 4:distinct
* @param listType : 0:list, 1:nested, 2:findOne, 3:bulkRead, 4:distinct, 5:xjoin
*
* Updates query, params for query of type listType
*/
@ -304,7 +333,7 @@ class Xapi {
/**************** add tableName ****************/
queryParamsObj.query += ' from ?? ';
if (listType === 1) {
if (listType === 1) { //nested list
req.app.locals._tableName = req.app.locals._childTable;
@ -357,27 +386,165 @@ class Xapi {
}
/**************** add group by ****************/
this.mysql.getGroupByClause(req.query._groupby, req.app.locals._tableName, queryParamsObj);
this._getGrpByHavingOrderBy(req, req.app.locals._tableName, queryParamsObj)
/**************** add having ****************/
this.mysql.getHavingClause(req.query._having, req.app.locals._tableName, queryParamsObj);
/**************** add order clause ****************/
this.mysql.getOrderByClause(req.query, req.app.locals._tableName, queryParamsObj);
//console.log(queryParamsObj.query, queryParamsObj.params);
}
_joinTableNames(isSecondJoin, joinTables, index, queryParamsObj) {
if (isSecondJoin) {
/**
* in second join - there will be ONE table and an ON condition
* this if clause deals with this
*
*/
// add : join / left join / right join / full join / inner join
queryParamsObj.query += this.mysql.getJoinType(joinTables[index])
queryParamsObj.query += ' ?? as ?? '
// eg: tbl.tableName
let tableNameAndAs = joinTables[index + 1].split('.')
if (tableNameAndAs.length === 2) {
queryParamsObj.params.push(tableNameAndAs[1])
queryParamsObj.params.push(tableNameAndAs[0])
} else {
queryParamsObj.grammarErr = 1
console.log('there was no dot for tableName ', joinTables[index + 1]);
}
/**************** add limit clause ****************/
if (listType === 2) {
queryParamsObj.query += ' limit 1 '
} else {
queryParamsObj.query += ' limit ?,? '
queryParamsObj.params = queryParamsObj.params.concat(this.mysql.getLimitClause(req.query));
/**
* in first join - there will be TWO tables and an ON condition
* this else clause deals with this
*/
// first table
queryParamsObj.query += ' ?? as ?? '
// add : join / left join / right join / full join / inner join
queryParamsObj.query += this.mysql.getJoinType(joinTables[index + 1])
// second table
queryParamsObj.query += ' ?? as ?? '
let tableNameAndAs = joinTables[index].split('.')
if (tableNameAndAs.length === 2) {
queryParamsObj.params.push(tableNameAndAs[1])
queryParamsObj.params.push(tableNameAndAs[0])
} else {
queryParamsObj.grammarErr = 1
console.log('there was no dot for tableName ', joinTables[index]);
}
tableNameAndAs = []
tableNameAndAs = joinTables[index + 2].split('.')
if (tableNameAndAs.length === 2) {
queryParamsObj.params.push(tableNameAndAs[1])
queryParamsObj.params.push(tableNameAndAs[0])
} else {
queryParamsObj.grammarErr = 1
console.log('there was no dot for tableName ', joinTables[index]);
}
}
//console.log(queryParamsObj.query, queryParamsObj.params);
}
prepareJoinQuery(req, res, queryParamsObj) {
queryParamsObj.query = 'SELECT '
queryParamsObj.grammarErr = 0;
/**************** START : get fields ****************/
if (req.query._fields) {
let fields = req.query._fields.split(',')
// from _fields to - ??, ??, ?? [col1,col2,col3]
for (var i = 0; i < fields.length; ++i) {
if (i) {
queryParamsObj.query += ','
}
queryParamsObj.query += ' ?? '
queryParamsObj.params.push(fields[i])
}
} else {
queryParamsObj.query += ' * '
}
queryParamsObj.query += ' from '
/**************** END : get fields ****************/
/**************** START : get join + on ****************/
let joinTables = req.query._join.split(',')
if (joinTables.length < 3) {
//console.log('grammar error ', joinTables.length);
queryParamsObj.grammarErr = 1;
}
//console.log('jointables.length', joinTables);
let onCondnCount = 0;
for (let i = 0; i < joinTables.length - 1 && queryParamsObj.grammarErr === 0; i = i + 2) {
onCondnCount++;
this._joinTableNames(i, joinTables, i, queryParamsObj)
if (queryParamsObj.grammarErr) {
console.log('failed at _joinTableNames', queryParamsObj);
break;
}
//console.log('after join tables', queryParamsObj);
let onCondn = '_on' + (onCondnCount)
let onCondnObj = {}
if (onCondn in req.query) {
//console.log(onCondn, req.query[onCondn]);
onCondnObj = whrHelp.getConditionClause(req.query[onCondn], ' on ')
//console.log('onCondnObj', onCondnObj);
queryParamsObj.query += ' on ' + onCondnObj.query
queryParamsObj.params = queryParamsObj.params.concat(onCondnObj.params)
} else {
queryParamsObj.grammarErr = 1;
//console.log('No on condition: ', onCondn);
break;
}
//console.log('- - - - - - -');
if (i === 0) {
i = i + 1
}
//console.log('index after loop', i);
}
/**************** END : get join + on ****************/
if (queryParamsObj.grammarErr) {
queryParamsObj.query = ''
queryParamsObj.params = []
}
this.mysql.getWhereClause(req.query._where, ' ignore ', queryParamsObj, ' where ');
//console.log('after where',queryParamsObj);
this._getGrpByHavingOrderBy(req, 'ignore', queryParamsObj, 5)
return queryParamsObj;
}
async list(req, res) {
let queryParamsObj = {}
@ -391,6 +558,25 @@ class Xapi {
}
async xjoin(req, res) {
let obj = {}
obj.query = '';
obj.params = [];
this.prepareJoinQuery(req, res, obj)
//console.log(obj);
let results = await this.mysql.exec(obj.query, obj.params)
res.status(200).json(results)
//http://localhost:3000/api/xjoin?_join=pl.productlines,j,pr.products,j,ord.orderdetails&on1=(pl.productline,eq,pr.products)&on2=(pr.productcode,eq,ord.productcode)
}
async distinct(req, res) {
let queryParamsObj = {}
@ -628,19 +814,24 @@ class Xapi {
let results = await this.mysql.exec(query, params);
res.status(200).json(results);
}
async tables(req, res) {
let query = 'show tables';
let params = [];
let query = 'SELECT table_name AS resource FROM information_schema.tables WHERE table_schema = ? ';
let params = [this.config.database];
if (Object.keys(this.config.ignoreTables).length > 0) {
query += 'and table_name not in (?)'
params.push(Object.keys(this.config.ignoreTables))
}
let results = await this.mysql.exec(query, params)
res.status(200).json(results)
res.status(200).json(results)
}
async runQuery(req, res) {
let query = req.body.query;

49
lib/xsql.js

@ -245,7 +245,7 @@ class Xsql {
/**************** START : prepare value object in prepared statement ****************/
// iterate over sent object array
// iterate over sent object array
let arrOfArr = []
for (var i = 0; i < objectArray.length; ++i) {
let arrValues = []
@ -297,7 +297,6 @@ class Xsql {
queryParamsObj.params = queryParamsObj.params.concat(whereClauseObj.params)
}
//console.log('> > > after where clause filling up:', queryParamsObj.query, queryParamsObj.params);
}
}
@ -604,11 +603,57 @@ class Xsql {
}
getJoinType(joinInQueryParams) {
//console.log('joinInQueryParams',joinInQueryParams);
switch (joinInQueryParams) {
case '_lj':
return ' left join '
break;
case '_rj':
return ' right join '
break;
case '_fj':
return ' full join '
break;
case '_ij':
return ' inner join '
break;
case '_j':
return ' join '
break;
}
return ' join '
}
getJoinTables(req) {
}
getJoinOnConditions() {
}
getJoinQuery() {
}
globalRoutesPrint(apiPrefix) {
let r = []
r.push(apiPrefix + "tables")
r.push(apiPrefix + "xjoin")
if (this.sqlConfig.dynamic) {
r.push(apiPrefix + "dynamic")

2
package.json

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

103
tests/tests.js

@ -1593,6 +1593,108 @@ describe('xmysql : tests', function () {
});
});
it('GET /api/xjoin?_join=pl.productlines,_j,pr.products&_on1=(pl.productline,eq,pr.productline) should PASS', function (done) {
//post to an url with data
agent.get('/api/xjoin?_join=pl.productlines,_j,pr.products&_on1=(pl.productline,eq,pr.productline)') //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[0]).length.should.be.equals(12)
return done();
});
});
it('GET /api/xjoin?_join=pl.productlines,_j,pr.products&_on1=(pl.productline,eq,pr.productline)&_fields=pl.productline,pr.productName should PASS', function (done) {
//post to an url with data
agent.get('/api/xjoin?_join=pl.productlines,_j,pr.products&_on1=(pl.productline,eq,pr.productline)&_fields=pl.productline,pr.productName') //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[0]).length.should.be.equals(2)
res.body.length.should.be.equals(20)
return done();
});
});
it('GET /api/xjoin?_join=pl.productlines,_j,pr.products&_on1=(pl.productline,eq,pr.productline)&_fields=pl.productline,pr.productName&_size=2 should PASS', function (done) {
//post to an url with data
agent.get('/api/xjoin?_join=pl.productlines,_j,pr.products&_on1=(pl.productline,eq,pr.productline)&_fields=pl.productline,pr.productName&_size=2') //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[0]).length.should.be.equals(2)
res.body.length.should.be.equals(2)
return done();
});
});
it('GET /api/xjoin?_join=pl.productlines,_j,pr.products,_j,ord.orderDetails&_on1=(pl.productline,eq,pr.productline)&_on2=(pr.productcode,eq,ord.productcode) should PASS', function (done) {
//post to an url with data
agent.get('/api/xjoin?_join=pl.productlines,_j,pr.products,_j,ord.orderDetails&_on1=(pl.productline,eq,pr.productline)&_on2=(pr.productcode,eq,ord.productcode)') //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[0]).length.should.be.equals(16)
res.body.length.should.be.equals(20)
return done();
});
});
it('GET /api/xjoin?_join=pl.productlines,_j,pr.products&_on1=(pl.productline,eq,pr.productline)&_fields=pl.productline,pr.productName&_size=2&_where=(productName,like,1972~) should PASS', function (done) {
//post to an url with data
agent.get('/api/xjoin?_join=pl.productlines,_j,pr.products&_on1=(pl.productline,eq,pr.productline)&_fields=pl.productline,pr.productName&_size=2&_where=(productName,like,1972~)') //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(1)
return done();
});
});
it('where clause unit ?_where=(abc,eq,1234) should PASS', function (done) {
@ -1609,7 +1711,6 @@ describe('xmysql : tests', function () {
});
it('where clause unit ?_where=(abc,ne,1234) should PASS', function (done) {
var err = whereClause.getConditionClause('(abc,ne,1234)')

Loading…
Cancel
Save