|
|
|
"use strict";
|
|
|
|
|
|
|
|
var Xsql = require("./xsql.js");
|
|
|
|
var Xctrl = require("./xctrl.js");
|
|
|
|
var multer = require("multer");
|
|
|
|
const path = require("path");
|
|
|
|
|
|
|
|
const v8 = require("v8"),
|
|
|
|
os = require("os");
|
|
|
|
|
|
|
|
//define class
|
|
|
|
class Xapi {
|
|
|
|
constructor(args, mysqlPool, app) {
|
|
|
|
this.config = args;
|
|
|
|
this.mysql = new Xsql(args, mysqlPool);
|
|
|
|
this.app = app;
|
|
|
|
this.ctrls = [];
|
|
|
|
|
|
|
|
/**************** START : multer ****************/
|
|
|
|
this.storage = multer.diskStorage({
|
|
|
|
destination: function(req, file, cb) {
|
|
|
|
cb(null, process.cwd());
|
|
|
|
},
|
|
|
|
filename: function(req, file, cb) {
|
|
|
|
console.log(file);
|
|
|
|
cb(null, Date.now() + "-" + file.originalname);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.upload = multer({ storage: this.storage });
|
|
|
|
/**************** END : multer ****************/
|
|
|
|
}
|
|
|
|
|
|
|
|
init(cbk) {
|
|
|
|
this.mysql.init((err, results) => {
|
|
|
|
this.app.use(this.urlMiddleware.bind(this));
|
|
|
|
let stat = this.setupRoutes();
|
|
|
|
this.app.use(this.errorMiddleware.bind(this));
|
|
|
|
cbk(err, stat);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
urlMiddleware(req, res, next) {
|
|
|
|
// get only request url from originalUrl
|
|
|
|
let justUrl = req.originalUrl.split("?")[0];
|
|
|
|
let pathSplit = [];
|
|
|
|
|
|
|
|
// split by apiPrefix
|
|
|
|
let apiSuffix = justUrl.split(this.config.apiPrefix);
|
|
|
|
|
|
|
|
if (apiSuffix.length === 2) {
|
|
|
|
// split by /
|
|
|
|
pathSplit = apiSuffix[1].split("/");
|
|
|
|
if (pathSplit.length) {
|
|
|
|
if (pathSplit.length >= 3) {
|
|
|
|
// handle for relational routes
|
|
|
|
req.app.locals._parentTable = pathSplit[0];
|
|
|
|
req.app.locals._childTable = pathSplit[2];
|
|
|
|
} else {
|
|
|
|
// handles rest of routes
|
|
|
|
req.app.locals._tableName = pathSplit[0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
|
|
|
|
errorMiddleware(err, req, res, next) {
|
|
|
|
if (err && err.code) res.status(400).json({ error: err });
|
|
|
|
else if (err && err.message)
|
|
|
|
res.status(500).json({ error: "Internal server error : " + err.message });
|
|
|
|
else res.status(500).json({ error: "Internal server error : " + err });
|
|
|
|
|
|
|
|
next(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
asyncMiddleware(fn) {
|
|
|
|
return (req, res, next) => {
|
|
|
|
Promise.resolve(fn(req, res, next)).catch(err => {
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
root(req, res) {
|
|
|
|
let routes = [];
|
|
|
|
routes = this.mysql.getSchemaRoutes(
|
|
|
|
false,
|
|
|
|
req.protocol + "://" + req.get("host") + this.config.apiPrefix
|
|
|
|
);
|
|
|
|
routes = routes.concat(
|
|
|
|
this.mysql.globalRoutesPrint(
|
|
|
|
req.protocol + "://" + req.get("host") + this.config.apiPrefix
|
|
|
|
)
|
|
|
|
);
|
|
|
|
res.json(routes);
|
|
|
|
}
|
|
|
|
|
|
|
|
setupRoutes() {
|
|
|
|
let stat = {}
|
|
|
|
stat.tables = 0
|
|
|
|
stat.apis = 0
|
|
|
|
stat.routines = 0
|
|
|
|
|
|
|
|
// console.log('this.config while setting up routes', this.config);
|
|
|
|
|
|
|
|
// show routes for database schema
|
|
|
|
this.app.get("/", this.asyncMiddleware(this.root.bind(this)));
|
|
|
|
|
|
|
|
// show all resouces
|
|
|
|
this.app
|
|
|
|
.route(this.config.apiPrefix + "tables")
|
|
|
|
.get(this.asyncMiddleware(this.tables.bind(this)));
|
|
|
|
|
|
|
|
this.app
|
|
|
|
.route(this.config.apiPrefix + "xjoin")
|
|
|
|
.get(this.asyncMiddleware(this.xjoin.bind(this)));
|
|
|
|
|
|
|
|
stat.apis += 3;
|
|
|
|
|
|
|
|
/**************** START : setup routes for each table ****************/
|
|
|
|
let resources = [];
|
|
|
|
resources = this.mysql.getSchemaRoutes(true, this.config.apiPrefix);
|
|
|
|
|
|
|
|
stat.tables += resources.length;
|
|
|
|
|
|
|
|
// iterate over each resource
|
|
|
|
for (var j = 0; j < resources.length; ++j) {
|
|
|
|
let resourceCtrl = new Xctrl(this.app, this.mysql);
|
|
|
|
this.ctrls.push(resourceCtrl);
|
|
|
|
|
|
|
|
let routes = resources[j]["routes"];
|
|
|
|
|
|
|
|
stat.apis += resources[j]["routes"].length;
|
|
|
|
|
|
|
|
// iterate over each routes in resource and map function
|
|
|
|
for (var i = 0; i < routes.length; ++i) {
|
|
|
|
switch (routes[i]["routeType"]) {
|
|
|
|
case "list":
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(this.asyncMiddleware(resourceCtrl.list.bind(resourceCtrl)));
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "findOne":
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(
|
|
|
|
this.asyncMiddleware(resourceCtrl.findOne.bind(resourceCtrl))
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "create":
|
|
|
|
if (!this.config.readOnly)
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.post(
|
|
|
|
this.asyncMiddleware(resourceCtrl.create.bind(resourceCtrl))
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "read":
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(this.asyncMiddleware(resourceCtrl.read.bind(resourceCtrl)));
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "bulkInsert":
|
|
|
|
if (!this.config.readOnly) {
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.post(
|
|
|
|
this.asyncMiddleware(
|
|
|
|
resourceCtrl.bulkInsert.bind(resourceCtrl)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "bulkRead":
|
|
|
|
if (!this.config.readOnly) {
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(
|
|
|
|
this.asyncMiddleware(resourceCtrl.bulkRead.bind(resourceCtrl))
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
stat.apis--;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "bulkDelete":
|
|
|
|
if (!this.config.readOnly) {
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.delete(
|
|
|
|
this.asyncMiddleware(
|
|
|
|
resourceCtrl.bulkDelete.bind(resourceCtrl)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
stat.apis--;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "patch":
|
|
|
|
if (!this.config.readOnly) {
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.patch(
|
|
|
|
this.asyncMiddleware(resourceCtrl.patch.bind(resourceCtrl))
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
stat.apis--;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "update":
|
|
|
|
if (!this.config.readOnly) {
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.put(
|
|
|
|
this.asyncMiddleware(resourceCtrl.update.bind(resourceCtrl))
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
stat.apis--;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "delete":
|
|
|
|
if (!this.config.readOnly) {
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.delete(
|
|
|
|
this.asyncMiddleware(resourceCtrl.delete.bind(resourceCtrl))
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
stat.apis--;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "exists":
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(
|
|
|
|
this.asyncMiddleware(resourceCtrl.exists.bind(resourceCtrl))
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "count":
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(this.asyncMiddleware(resourceCtrl.count.bind(resourceCtrl)));
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "distinct":
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(
|
|
|
|
this.asyncMiddleware(resourceCtrl.distinct.bind(resourceCtrl))
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "describe":
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(this.asyncMiddleware(this.tableDescribe.bind(this)));
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "relational":
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(
|
|
|
|
this.asyncMiddleware(resourceCtrl.nestedList.bind(resourceCtrl))
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "groupby":
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(
|
|
|
|
this.asyncMiddleware(resourceCtrl.groupBy.bind(resourceCtrl))
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "ugroupby":
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(
|
|
|
|
this.asyncMiddleware(resourceCtrl.ugroupby.bind(resourceCtrl))
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "chart":
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(this.asyncMiddleware(resourceCtrl.chart.bind(resourceCtrl)));
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "autoChart":
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(
|
|
|
|
this.asyncMiddleware(resourceCtrl.autoChart.bind(resourceCtrl))
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "aggregate":
|
|
|
|
this.app
|
|
|
|
.route(routes[i]["routeUrl"])
|
|
|
|
.get(
|
|
|
|
this.asyncMiddleware(resourceCtrl.aggregate.bind(resourceCtrl))
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**************** END : setup routes for each table ****************/
|
|
|
|
|
|
|
|
if (this.config.dynamic === 1 && !this.config.readOnly) {
|
|
|
|
this.app
|
|
|
|
.route("/dynamic*")
|
|
|
|
.post(this.asyncMiddleware(this.runQuery.bind(this)));
|
|
|
|
|
|
|
|
/**************** START : multer routes ****************/
|
|
|
|
this.app.post(
|
|
|
|
"/upload",
|
|
|
|
this.upload.single("file"),
|
|
|
|
this.uploadFile.bind(this)
|
|
|
|
);
|
|
|
|
this.app.post(
|
|
|
|
"/uploads",
|
|
|
|
this.upload.array("files", 10),
|
|
|
|
this.uploadFiles.bind(this)
|
|
|
|
);
|
|
|
|
this.app.get("/download", this.downloadFile.bind(this));
|
|
|
|
/**************** END : multer routes ****************/
|
|
|
|
|
|
|
|
stat.apis += 4;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**************** START : health and version ****************/
|
|
|
|
this.app.get("/_health", this.asyncMiddleware(this.health.bind(this)));
|
|
|
|
this.app.get("/_version", this.asyncMiddleware(this.version.bind(this)));
|
|
|
|
stat.apis += 2;
|
|
|
|
/**************** END : health and version ****************/
|
|
|
|
|
|
|
|
/**************** START : call stored procedures ****************/
|
|
|
|
this.app.get('/_proc', this.asyncMiddleware(this.proc.bind(this)))
|
|
|
|
stat.apis += 1
|
|
|
|
const procResources = this.mysql.getProcList(true, this.config.apiPrefix)
|
|
|
|
this.app.post('/_proc/:proc', this.asyncMiddleware(this.callProc.bind(this)))
|
|
|
|
stat.routines += procResources.length
|
|
|
|
stat.apis += procResources.length
|
|
|
|
/**************** END : call stored procedures ****************/
|
|
|
|
|
|
|
|
|
|
|
|
let statStr = ' Generated: ' + stat.apis + ' REST APIs for ' + stat.tables + ' tables '
|
|
|
|
|
|
|
|
console.log(' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ');
|
|
|
|
console.log(' ');
|
|
|
|
console.log(' Database : %s', this.config.database);
|
|
|
|
console.log(' Number of Tables : %s', stat.tables);
|
|
|
|
console.log(' Number of Routines : %s', stat.routines);
|
|
|
|
console.log(' ');
|
|
|
|
console.log(' REST APIs Generated : %s'.green.bold, stat.apis);
|
|
|
|
console.log(' ');
|
|
|
|
|
|
|
|
return stat
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
async xjoin(req, res) {
|
|
|
|
let obj = {};
|
|
|
|
|
|
|
|
obj.query = "";
|
|
|
|
obj.params = [];
|
|
|
|
|
|
|
|
this.mysql.prepareJoinQuery(req, res, obj);
|
|
|
|
|
|
|
|
//console.log(obj);
|
|
|
|
if (obj.query.length) {
|
|
|
|
let results = await this.mysql.exec(obj.query, obj.params);
|
|
|
|
res.status(200).json(results);
|
|
|
|
} else {
|
|
|
|
res.status(400).json({ err: "Invalid Xjoin request" });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async tableDescribe(req, res) {
|
|
|
|
let query = "describe ??";
|
|
|
|
let params = [req.app.locals._tableName];
|
|
|
|
|
|
|
|
let results = await this.mysql.exec(query, params);
|
|
|
|
res.status(200).json(results);
|
|
|
|
}
|
|
|
|
|
|
|
|
async tables(req, res) {
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
async runQuery(req, res) {
|
|
|
|
let query = req.body.query;
|
|
|
|
let params = req.body.params;
|
|
|
|
|
|
|
|
let results = await this.mysql.exec(query, params);
|
|
|
|
res.status(200).json(results);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**************** START : files related ****************/
|
|
|
|
downloadFile(req, res) {
|
|
|
|
let file = path.join(process.cwd(), req.query.name);
|
|
|
|
res.download(file);
|
|
|
|
}
|
|
|
|
|
|
|
|
uploadFile(req, res) {
|
|
|
|
if (req.file) {
|
|
|
|
console.log(req.file.path);
|
|
|
|
res.end(req.file.path);
|
|
|
|
} else {
|
|
|
|
res.end("upload failed");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
uploadFiles(req, res) {
|
|
|
|
if (!req.files || req.files.length === 0) {
|
|
|
|
res.end("upload failed");
|
|
|
|
} else {
|
|
|
|
let files = [];
|
|
|
|
for (let i = 0; i < req.files.length; ++i) {
|
|
|
|
files.push(req.files[i].path);
|
|
|
|
}
|
|
|
|
|
|
|
|
res.end(files.toString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**************** END : files related ****************/
|
|
|
|
|
|
|
|
/**************** START : health and version ****************/
|
|
|
|
|
|
|
|
async getMysqlUptime() {
|
|
|
|
let v = await this.mysql.exec("SHOW GLOBAL STATUS LIKE 'Uptime';", []);
|
|
|
|
return v[0]["Value"];
|
|
|
|
}
|
|
|
|
|
|
|
|
async getMysqlHealth() {
|
|
|
|
let v = await this.mysql.exec("select version() as version", []);
|
|
|
|
return v[0]["version"];
|
|
|
|
}
|
|
|
|
|
|
|
|
async health(req, res) {
|
|
|
|
let status = {};
|
|
|
|
status["process_uptime"] = process.uptime();
|
|
|
|
status["mysql_uptime"] = await this.getMysqlUptime();
|
|
|
|
|
|
|
|
if (Object.keys(req.query).length) {
|
|
|
|
status["process_memory_usage"] = process.memoryUsage();
|
|
|
|
status["os_total_memory"] = os.totalmem();
|
|
|
|
status["os_free_memory"] = os.freemem();
|
|
|
|
status["os_load_average"] = os.loadavg();
|
|
|
|
status["v8_heap_statistics"] = v8.getHeapStatistics();
|
|
|
|
}
|
|
|
|
|
|
|
|
res.json(status);
|
|
|
|
}
|
|
|
|
|
|
|
|
async version(req, res) {
|
|
|
|
let version = {};
|
|
|
|
|
|
|
|
version["Xmysql"] = this.app.get("version");
|
|
|
|
version["mysql"] = await this.getMysqlHealth();
|
|
|
|
version["node"] = process.versions.node;
|
|
|
|
res.json(version);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**************** END : health and version ****************/
|
|
|
|
|
|
|
|
async proc(req, res) {
|
|
|
|
let query = 'SELECT routine_name AS resource FROM information_schema.routines WHERE routine_schema = ? ';
|
|
|
|
let params = [this.config.database];
|
|
|
|
let results = await this.mysql.exec(query, params)
|
|
|
|
res.status(200).json(results)
|
|
|
|
}
|
|
|
|
|
|
|
|
async callProc(req, res) {
|
|
|
|
let query = 'CALL ??(?)'
|
|
|
|
let params = [req.params.proc, Object.values(req.body)]
|
|
|
|
let results = await this.mysql.exec(query, params)
|
|
|
|
res.status(200).json(results)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
//expose class
|
|
|
|
module.exports = Xapi;
|