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. 27
      lib/util/whereClause.helper.js
  4. 227
      lib/xapi.js
  5. 47
      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: * Group By, Having (as a separate API) :fire::fire:
* Multiple group by in one API :fire::fire::fire::fire: * Multiple group by in one API :fire::fire::fire::fire:
* Chart API for numeric column :fire::fire::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 * Supports views
* Prototyping (features available when using local MySql server only) * Prototyping (features available when using local MySql server only)
* Run dynamic queries :fire::fire::fire: * 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/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/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/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:| [/dynamic](#run-dynamic-queries) | execute dynamic mysql statements with params |
| GET :fire:| [/upload](#upload-single-file) | upload single file | | GET :fire:| [/upload](#upload-single-file) | upload single file |
| GET :fire:| [/uploads](#upload-multiple-files) | upload multiple files | | 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 ## Run dynamic queries
[:arrow_heading_up:](#api-overview) [:arrow_heading_up:](#api-overview)
@ -758,7 +825,7 @@ http://localhost:3000/download?name=fileName
## When to use ? ## When to use ?
[:arrow_heading_up:](#api-overview) [: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 learning new frontend frameworks and need REST APIs for your MySql database.
* You are working on a demo, hacks etc * You are working on a demo, hacks etc

2
lib/util/cmd.helper.js

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

27
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 = '' let converted = ''
@ -93,8 +93,11 @@ function replaceWhereParamsToQMarks(openParentheses, str, comparisonOperator) {
} }
} else { } else {
if (comparisonOperator !== ' in ' && comparisonOperator !== ' between ') if (comparisonOperator !== ' in ' && comparisonOperator !== ' between ' && condType !== ' on ') {
converted = '?' converted = '?'
} else if (condType === ' on ') {
converted = '??'
}
for (var i = str.length - 1; i >= 0; --i) { for (var i = str.length - 1; i >= 0; --i) {
if (str[i] === ')') { if (str[i] === ')') {
@ -105,6 +108,7 @@ function replaceWhereParamsToQMarks(openParentheses, str, comparisonOperator) {
} }
} }
return converted; return converted;
} }
@ -215,11 +219,9 @@ exports.getConditionClause = function (whereInQueryParams, condType = 'where') {
if (numOfConditions && logicalOperatorsInClause && numOfConditions.length !== logicalOperatorsInClause.length + 1) { if (numOfConditions && logicalOperatorsInClause && numOfConditions.length !== logicalOperatorsInClause.length + 1) {
console.log('conditions and logical operators mismatch', numOfConditions.length, logicalOperatorsInClause.length); console.log('conditions and logical operators mismatch', numOfConditions.length, logicalOperatorsInClause.length);
return } else {
} //console.log('numOfConditions',numOfConditions,whereInQueryParams);
//console.log('logicalOperatorsInClause',logicalOperatorsInClause);
// console.log('numOfConditions',numOfConditions,whereInQueryParams);
// console.log('logicalOperatorsInClause',logicalOperatorsInClause);
for (var i = 0; i < numOfConditions.length; ++i) { for (var i = 0; i < numOfConditions.length; ++i) {
@ -245,10 +247,10 @@ exports.getConditionClause = function (whereInQueryParams, condType = 'where') {
} }
/**************** START : variable ****************/ /**************** START : variable ****************/
//console.log(result); //console.log('variable, operator, variablevalue',result);
variable = result[0].match(/\(+(.*)/); variable = result[0].match(/\(+(.*)/);
// console.log('variable',variable); //console.log('variable',variable);
if (!variable || variable.length !== 2) { if (!variable || variable.length !== 2) {
grammarErr = 1; grammarErr = 1;
@ -259,7 +261,7 @@ exports.getConditionClause = function (whereInQueryParams, condType = 'where') {
whereParams.push(variable[1]) whereParams.push(variable[1])
// then replace the variable name with ?? // then replace the variable name with ??
temp = replaceWhereParamsToQMarks(true, result[0]) temp = replaceWhereParamsToQMarks(true, result[0], ' ignore ', condType)
if (!temp) { if (!temp) {
grammarErr = 1; grammarErr = 1;
break; break;
@ -302,15 +304,13 @@ exports.getConditionClause = function (whereInQueryParams, condType = 'where') {
} }
// then replace the variableValue with ? // then replace the variableValue with ?
temp = replaceWhereParamsToQMarks(false, result[2], comparisonOperator) temp = replaceWhereParamsToQMarks(false, result[2], comparisonOperator, condType)
if (!temp) { if (!temp) {
grammarErr = 1; grammarErr = 1;
break; break;
} }
whereQuery += temp whereQuery += temp
// only
if (numOfConditions.length !== -1 && i !== numOfConditions.length - 1) { if (numOfConditions.length !== -1 && i !== numOfConditions.length - 1) {
//console.log('finding logical operator for',logicalOperatorsInClause[i]); //console.log('finding logical operator for',logicalOperatorsInClause[i]);
logicalOperator = getLogicalOperator(logicalOperatorsInClause[i]) logicalOperator = getLogicalOperator(logicalOperatorsInClause[i])
@ -326,6 +326,7 @@ exports.getConditionClause = function (whereInQueryParams, condType = 'where') {
} }
}
let obj = {} let obj = {}

227
lib/xapi.js

@ -1,10 +1,12 @@
'use strict'; 'use strict';
var Xsql = require('./xsql.js'); var Xsql = require('./xsql.js');
var whrHelp = require('./util/whereClause.helper.js');
var multer = require('multer'); var multer = require('multer');
var path = require('path'); var path = require('path');
const colors = require('colors'); const colors = require('colors');
//define class //define class
class Xapi { class Xapi {
@ -110,6 +112,11 @@ class Xapi {
this.app.route('/api/tables') this.app.route('/api/tables')
.get(this.asyncMiddleware(this.tables.bind(this))); .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 ****************/ /**************** START : setup routes for each table ****************/
let resources = []; 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 req
* @param res * @param res
* @param queryParamsObj : {query, params} * @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 * Updates query, params for query of type listType
*/ */
@ -304,7 +333,7 @@ class Xapi {
/**************** add tableName ****************/ /**************** add tableName ****************/
queryParamsObj.query += ' from ?? '; queryParamsObj.query += ' from ?? ';
if (listType === 1) { if (listType === 1) { //nested list
req.app.locals._tableName = req.app.locals._childTable; req.app.locals._tableName = req.app.locals._childTable;
@ -357,27 +386,165 @@ class Xapi {
} }
/**************** add group by ****************/ this._getGrpByHavingOrderBy(req, req.app.locals._tableName, queryParamsObj)
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 ****************/ //console.log(queryParamsObj.query, queryParamsObj.params);
this.mysql.getOrderByClause(req.query, req.app.locals._tableName, queryParamsObj); }
/**************** add limit clause ****************/
if (listType === 2) { _joinTableNames(isSecondJoin, joinTables, index, queryParamsObj) {
queryParamsObj.query += ' limit 1 '
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 { } else {
queryParamsObj.query += ' limit ?,? ' queryParamsObj.grammarErr = 1
queryParamsObj.params = queryParamsObj.params.concat(this.mysql.getLimitClause(req.query)); console.log('there was no dot for tableName ', joinTables[index + 1]);
} }
//console.log(queryParamsObj.query, queryParamsObj.params); } else {
/**
* 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]);
}
}
}
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) { async list(req, res) {
let queryParamsObj = {} 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) { async distinct(req, res) {
let queryParamsObj = {} let queryParamsObj = {}
@ -628,19 +814,24 @@ class Xapi {
let results = await this.mysql.exec(query, params); let results = await this.mysql.exec(query, params);
res.status(200).json(results); res.status(200).json(results);
} }
async tables(req, res) { async tables(req, res) {
let query = 'show tables'; let query = 'SELECT table_name AS resource FROM information_schema.tables WHERE table_schema = ? ';
let params = []; 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) let results = await this.mysql.exec(query, params)
res.status(200).json(results)
res.status(200).json(results)
} }
async runQuery(req, res) { async runQuery(req, res) {
let query = req.body.query; let query = req.body.query;

47
lib/xsql.js

@ -297,7 +297,6 @@ class Xsql {
queryParamsObj.params = queryParamsObj.params.concat(whereClauseObj.params) 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) { globalRoutesPrint(apiPrefix) {
let r = [] let r = []
r.push(apiPrefix + "tables") r.push(apiPrefix + "tables")
r.push(apiPrefix + "xjoin")
if (this.sqlConfig.dynamic) { if (this.sqlConfig.dynamic) {
r.push(apiPrefix + "dynamic") r.push(apiPrefix + "dynamic")

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "xmysql", "name": "xmysql",
"version": "0.3.4", "version": "0.4.0",
"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": {

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) { 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) { it('where clause unit ?_where=(abc,ne,1234) should PASS', function (done) {
var err = whereClause.getConditionClause('(abc,ne,1234)') var err = whereClause.getConditionClause('(abc,ne,1234)')

Loading…
Cancel
Save