diff --git a/README.md b/README.md index 27b8a880fe..83e80f271a 100644 --- a/README.md +++ b/README.md @@ -92,29 +92,32 @@ if you haven't on your system. ## API Overview -|# | HTTP Type | API URL | Comments | -|--|-----------|----------------------------------|--------------------------------------------------------- -|01| GET | / | Gets all REST APIs | -|02| GET | /api/tableName | Lists rows of table | -|03| POST | /api/tableName | Create a new row | -|04| PUT | /api/tableName | Replaces existing row with new row | -|05| GET | /api/tableName/:id | Retrieves a row by primary key | -|06| PATCH | /api/tableName/:id | Updates a row by primary key | -|07| GET | /api/tableName/findOne | Works as list but gets single record matching criteria | -|08| GET | /api/tableName/count | Count number of rows in a table | -|09| GET | /api/tableName/:id/exists | True or false whether a row exists or not | -|10| DELETE | /api/tableName/:id | Delete a row by primary key | -|11| GET | [/api/parentTable/:id/childTable](#relational-tables) | Get list of child table rows with parent table foreign key | -|12| GET :fire:| [/api/tableName/aggregate](#aggregate-functions) | Aggregate results of numeric column(s) | -|13| GET :fire:| [/api/tableName/groupby](#group-by-having-as-api) | Group by results of column(s) | -|14| GET :fire:| [/api/tableName/ugroupby](#union-of-multiple-group-by-statements) | Multiple group by results using one call | -|15| GET :fire:| [/api/tableName/chart](#chart) | Numeric column distribution based on (min,max,step) or(step array) or (automagic)| -|16| GET :fire:| [/dynamic](#run-dynamic-queries) | execute dynamic mysql statements with params | -|17| GET :fire:| [/upload](#upload-single-file) | upload single file | -|18| GET :fire:| [/uploads](#upload-multiple-files) | upload multiple files | -|19| GET :fire:| [/download](#download-file) | download a file | -|20| GET | /api/tableName/describe| describe each table for its columns | -|21| GET | /api/tables| get all tables in database | +| HTTP Type | API URL | Comments | +|-----------|----------------------------------|--------------------------------------------------------- +| GET | / | Gets all REST APIs | +| GET | /api/tableName | Lists rows of table | +| POST | /api/tableName | Create a new row | +| PUT | /api/tableName | Replaces existing row with new row | +| POST :fire:| /api/tableName/bulk | Create multiple rows - send object array in request body| +| GET :fire:| /api/tableName/bulk | Lists multiple rows - /api/tableName/bulk?_ids=1,2,3 | +| DELETE :fire:| /api/tableName/bulk | Deletes multiple rows - /api/tableName/bulk?_ids=1,2,3 | +| GET | /api/tableName/:id | Retrieves a row by primary key | +| PATCH | /api/tableName/:id | Updates row element by primary key | +| DELETE | /api/tableName/:id | Delete a row by primary key | +| GET | /api/tableName/findOne | Works as list but gets single record matching criteria | +| GET | /api/tableName/count | Count number of rows in a table | +| GET | /api/tableName/:id/exists | True or false whether a row exists or not | +| GET | [/api/parentTable/:id/childTable](#relational-tables) | Get list of child table rows with parent table foreign key | +| GET :fire:| [/api/tableName/aggregate](#aggregate-functions) | Aggregate results of numeric column(s) | +| GET :fire:| [/api/tableName/groupby](#group-by-having-as-api) | Group by results of column(s) | +| 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:| [/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 | +| GET :fire:| [/download](#download-file) | download a file | +| GET | /api/tableName/describe| describe each table for its columns | +| GET | /api/tables| get all tables in database | diff --git a/assets/log.gif b/assets/log.gif index e3be0cc0b5..7d011eb992 100644 Binary files a/assets/log.gif and b/assets/log.gif differ diff --git a/bin/index.js b/bin/index.js index c308cc7f10..a9e881cded 100755 --- a/bin/index.js +++ b/bin/index.js @@ -30,7 +30,9 @@ let mysqlPool = mysql.createPool(sqlConfig); /**************** START : setup Xapi ****************/ console.log(''); -console.log(' REST APIs at the speed of thought.. '); +console.log(''); +console.log(''); +console.log(' Generating REST APIs at the speed of your thought.. '); console.log(''); let t = process.hrtime(); diff --git a/lib/util/cmd.helper.js b/lib/util/cmd.helper.js index ece6868de9..7c2e54a1de 100644 --- a/lib/util/cmd.helper.js +++ b/lib/util/cmd.helper.js @@ -11,7 +11,7 @@ program.on('--help', () => { }) program - .version('0.2.4') + .version('0.2.5') .option('-h, --host ', 'hostname') .option('-d, --database ', 'database schema name') .option('-u, --user ', 'username of database / root by default') diff --git a/lib/xapi.js b/lib/xapi.js index 0e2b8bd03d..8c36e00c04 100644 --- a/lib/xapi.js +++ b/lib/xapi.js @@ -149,6 +149,21 @@ class Xapi { .get(this.asyncMiddleware(this.read.bind(this))); break; + case 'bulkInsert': + this.app.route(routes[i]['routeUrl']) + .post(this.asyncMiddleware(this.bulkInsert.bind(this))); + break; + + case 'bulkRead': + this.app.route(routes[i]['routeUrl']) + .get(this.asyncMiddleware(this.bulkRead.bind(this))); + break; + + case 'bulkDelete': + this.app.route(routes[i]['routeUrl']) + .delete(this.asyncMiddleware(this.bulkDelete.bind(this))); + break; + case 'patch': this.app.route(routes[i]['routeUrl']) .patch(this.asyncMiddleware(this.patch.bind(this))); @@ -230,7 +245,7 @@ class Xapi { console.log(' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - '); console.log(' '); console.log(' Database : %s', this.config.database); - console.log(' Number of resources : %s', stat.tables); + console.log(' Number of Tables : %s', stat.tables); console.log(' '); console.log(' REST APIs Generated : %s'.green.bold, stat.apis); console.log(' '); @@ -256,7 +271,7 @@ class Xapi { * @param req * @param res * @param queryParamsObj : {query, params} - * @param listType : 0:list, 1:nested, 2:findOne + * @param listType : 0:list, 1:nested, 2:findOne, 3:bulkRead * * Updates query, params for query of type listType */ @@ -297,6 +312,28 @@ class Xapi { this.mysql.getWhereClause(req.query._where, req.app.locals._tableName, queryParamsObj, ' and '); + } else if (listType === 3) { //bulkRead + + // select * from table where pk in (ids) and whereConditions + queryParamsObj.params.push(req.app.locals._tableName); + queryParamsObj.query += ' where ?? in '; + queryParamsObj.params.push(this.mysql.getPrimaryKeyName(req.app.locals._tableName)); + + queryParamsObj.query += '(' + + if (req.query && req.query._ids) { + let ids = req.query._ids.split(',') + for (var i = 0; i < ids.length; ++i) { + if (i) { + queryParamsObj.query += ',' + } + queryParamsObj.query += '?' + queryParamsObj.params.push(ids[i]) + } + } + queryParamsObj.query += ') ' + this.mysql.getWhereClause(req.query._where, req.app.locals._tableName, queryParamsObj, ' and '); + } else { queryParamsObj.params.push(req.app.locals._tableName); @@ -468,44 +505,6 @@ class Xapi { } - - // async update(req, res) { - // - // let query = 'UPDATE ?? SET '; - // let keys = Object.keys(req.body); - // - // // SET clause - // let updateKeys = ''; - // for (let i = 0; i < keys.length; ++i) { - // updateKeys += keys[i] + ' = ? ' - // if (i !== keys.length - 1) - // updateKeys += ', ' - // } - // - // // where clause - // query += updateKeys + ' where ' - // let clause = this.mysql.getPrimaryKeyWhereClause(req.app.locals._tableName, - // req.params.id.split('___')); - // - // if (!clause) { - // return res.status(400).send({ - // error: "Table is made of composite primary keys - all keys were not in input" - // }) - // } - // - // query += clause; - // - // // params - // let params = []; - // params.push(req.app.locals._tableName); - // params = params.concat(Object.values(req.body)); - // - // let results = await this.mysql.exec(query, params); - // res.status(200).json(results); - // - // - // } - async delete(req, res) { let query = 'DELETE FROM ?? WHERE '; @@ -530,6 +529,68 @@ class Xapi { } + async bulkInsert(req, res) { + + let queryParamsObj = {} + queryParamsObj.query = '' + queryParamsObj.params = [] + let results = [] + + //console.log(req.app.locals._tableName, req.body); + + this.mysql.getBulkInsertStatement(req.app.locals._tableName, req.body, queryParamsObj) + + results = await this.mysql.exec(queryParamsObj.query, queryParamsObj.params); + res.status(200).json(results); + + } + + async bulkDelete(req, res) { + + let query = 'delete from ?? where ?? in '; + let params = []; + + params.push(req.app.locals._tableName); + params.push(this.mysql.getPrimaryKeyName(req.app.locals._tableName)); + + query += '(' + + if (req.query && req.query._ids) { + let ids = req.query._ids.split(',') + for (var i = 0; i < ids.length; ++i) { + if (i) { + query += ',' + } + query += '?' + params.push(ids[i]) + } + } + + query += ')' + + //console.log(query, params); + + var results = await this.mysql.exec(query, params); + res.status(200).json(results); + + } + + async bulkRead(req, res) { + + let queryParamsObj = {} + queryParamsObj.query = '' + queryParamsObj.params = [] + + this.prepareListQuery(req, res, queryParamsObj, 3); + + //console.log(queryParamsObj.query, queryParamsObj.params); + + let results = await this.mysql.exec(queryParamsObj.query, queryParamsObj.params); + res.status(200).json(results); + + } + + async count(req, res) { let query = 'select count(1) as no_of_rows from ??'; diff --git a/lib/xsql.js b/lib/xsql.js index f9d3b0a5ca..426944d325 100644 --- a/lib/xsql.js +++ b/lib/xsql.js @@ -173,6 +173,65 @@ class Xsql { } + getBulkInsertStatement(tableName, objectArray, queryParamsObj) { + + if (tableName in this.metaDb.tables && objectArray) { + + let insObj = objectArray[0]; + + // goal => insert into ?? (?,?..?) values ? [tablName, col1,col2...coln,[[ObjValues_1],[ObjValues_2],...[ObjValues_N]] + queryParamsObj.query = ' INSERT INTO ?? ( ' + queryParamsObj.params.push(tableName) + + let cols = []; + let colPresent = false; + + /**************** START : prepare column names to be inserted ****************/ + // iterate over all column in table and have only ones existing in objects to be inserted + for (var i = 0; i < this.metaDb.tables[tableName]['columns'].length; ++i) { + + let colName = this.metaDb.tables[tableName]['columns'][i]['column_name'] + + + if (colName in insObj) { + + if (colPresent) { + queryParamsObj.query += ',' + } + + queryParamsObj.query += colName + + colPresent = true; + + } + + cols.push(colName) + + //console.log('> > ', queryParamsObj.query); + + } + + queryParamsObj.query += ' ) values ?' + /**************** END : prepare column names to be inserted ****************/ + + + /**************** START : prepare value object in prepared statement ****************/ + // iterate over sent object array + let arrOfArr = [] + for (var i = 0; i < objectArray.length; ++i) { + let arrValues = [] + for (var j = 0; j < cols.length; ++j) { + if (cols[j] in objectArray[i]) + arrValues.push(objectArray[i][cols[j]]) + } + arrOfArr.push(arrValues); + } + queryParamsObj.params.push(arrOfArr) + /**************** END : prepare value object in prepared statement ****************/ + } + } + + getGroupByClause(_groupby, tableName, queryParamsObj) { if (_groupby) { @@ -350,6 +409,14 @@ class Xsql { } + getPrimaryKeyName(tableName) { + let pk = null + if (tableName in this.metaDb.tables) { + pk = this.metaDb.tables[tableName].primaryKeys[0]['column_name'] + } + return pk + } + getPrimaryKeyWhereClause(tableName, pksValues) { let whereClause = ''; @@ -462,6 +529,9 @@ class Xsql { routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/findOne', 'findOne')) routes.push(this.prepareRoute(internal, 'post', apiPrefix, tableName, 'create')) routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName, 'list')) + routes.push(this.prepareRoute(internal, 'post', apiPrefix, tableName + '/bulk', 'bulkInsert')) + routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/bulk', 'bulkRead')) + routes.push(this.prepareRoute(internal, 'delete', apiPrefix, tableName + '/bulk', 'bulkDelete')) routes.push(this.prepareRoute(internal, 'put', apiPrefix, tableName, 'update')) routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/:id', 'read')) routes.push(this.prepareRoute(internal, 'patch', apiPrefix, tableName + '/:id', 'patch')) @@ -570,7 +640,6 @@ class Xsql { } - } diff --git a/package.json b/package.json index 8a4146ef28..65aa081667 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmysql", - "version": "0.2.4", + "version": "0.2.5", "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 7ba690b52f..02bcfcdf23 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -457,6 +457,115 @@ describe('xmysql : tests', function () { }); }); + it('POST /api/productlines/bulk should PASS', function (done) { + + var objArray = [] + + var obj = {}; + obj['productLine'] = 'Bulletrain' + obj['textDescription'] = 'Japan' + + var obj1 = {}; + obj1['productLine'] = 'Bulletrain_1' + obj1['textDescription'] = 'China' + + objArray.push(obj) + objArray.push(obj1) + + //post to an url with data + agent.post('/api/productlines/bulk') //enter url + .send(objArray) //postdata + .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['affectedRows'].should.be.equals(2) + + return done(); + + }); + }); + + it('POST /api/productlines/bulk should PASS', function (done) { + + var objArray = [] + + var obj = {}; + obj['productLine'] = 'Bulletrain_2' + + var obj1 = {}; + obj1['productLine'] = 'Bulletrain_3' + + + objArray.push(obj) + objArray.push(obj1) + + //post to an url with data + agent.post('/api/productlines/bulk') //enter url + .send(objArray) //postdata + .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['affectedRows'].should.be.equals(2) + + return done(); + + }); + }); + + it('GET /api/productlines/bulk should PASS', function (done) { + + //post to an url with data + agent.get('/api/productlines/bulk?_ids=Bulletrain,Bulletrain_1,Bulletrain_2,Bulletrain_3') //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(4) + + return done(); + + }); + }); + + + it('DELETE /api/productlines/bulk should PASS', function (done) { + + //post to an url with data + agent.del('/api/productlines/bulk?_ids=Bulletrain,Bulletrain_1,Bulletrain_2,Bulletrain_3') //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['affectedRows'].should.be.equals(4) + + return done(); + + }); + }); + + it('PUT /api/productlines should PASS', function (done) { var obj = {};