├── .gitignore ├── LICENSE ├── README.md ├── controllers └── api.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # Costum 30 | uploads 31 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Keno Dressel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mysql-to-rest 2 | 3 | mysql-to-rest is a module to map a MySQL-DB to an REST API. 4 | 5 | The foundation for this module is the express framework, as well as the mysql package for the DB connection. 6 | 7 | ## API 8 | 9 | ### Installation 10 | 11 | `$ npm install mysql-to-rest` 12 | 13 | First load express and mysql 14 | 15 | ```js 16 | var express = require('express'); 17 | var mysql = require('mysql'); 18 | var mysqltorest = require('mysql-to-rest'); 19 | var app = express(); 20 | ``` 21 | 22 | Then open your DB connection. Find more [here](https://github.com/felixge/node-mysql/#introduction). (You can also use `mysql.createPool()` to create a pool of connections and pass that to `mysql-to-rest`.) 23 | 24 | When all dependencies are up and ready, init the API like this: 25 | 26 | ```js 27 | var api = mysqltorest(app,connection); 28 | //Dont forget to start the server 29 | app.listen(8000); 30 | ``` 31 | 32 | ### Usage 33 | 34 | Once the app is up and running you have the following options: 35 | 36 | #### GET Table 37 | 38 | ##### Request 39 | 40 | `GET /api/:table` 41 | 42 | ##### Result 43 | 44 | ```json 45 | { 46 | "result": "success", 47 | "json": [ 48 | { 49 | "id": 1, 50 | "col1": 15, 51 | "col2": null, 52 | "col3": "String" 53 | } 54 | ], 55 | "table": "test", 56 | "length": 1 57 | } 58 | ``` 59 | 60 | ##### Params 61 | 62 | You can further specify your Requests with GET params. As an example: 63 | 64 | `GET /api/:table?_limit=0,10&_order[id]=DESC&id[GREAT]=4` 65 | 66 | ##### General params 67 | 68 | **Important: The general params are noted with the prefix you can define in the options. Default is underscore. Eg: \_limit** 69 | 70 | All general params are as close to the MYSQL feeling as it would make sense in a web API. So it really helps if you understand MYSQL Syntax. 71 | 72 | * `limit=` Takes either one or two comma separated integers. Acts like specified [here](https://dev.mysql.com/doc/refman/5.0/en/select.html) 73 | * `order[column]=` Takes either ASC or DESC. Orders the result ASC|DESC according to the column. Acts like specified [here](https://dev.mysql.com/doc/refman/5.0/en/select.html) 74 | * `fields=` Takes one or more comma separated columns as an argument. Filters the results to only show the specified columns. Acts like specified [here](https://dev.mysql.com/doc/refman/5.0/en/select.html) 75 | 76 | ##### Field specific params 77 | 78 | Here you can apply further conditions to your selection. 79 | 80 | Syntax: `column=value` or `column[operator]=value` 81 | 82 | The first option is simple and can be used to select entries where the column equals (=) the provided value. 83 | 84 | In the second option one can specify exactly the operator which should be used. Full list: 85 | 86 | * `GREAT` results in \> 87 | * `SMALL` results in \< 88 | * `EQGREAT`results in \>= 89 | * `EQSMALL`results in \<= 90 | * `LIKE`results in LIKE 91 | * `EQ`results in = 92 | 93 | --- 94 | 95 | #### GET Row 96 | 97 | ##### Request 98 | 99 | `GET /api/:table/:id` 100 | 101 | ##### Result 102 | 103 | For results and params see at [`GET /api/:table`](#get-table) 104 | 105 | --- 106 | 107 | #### POST 108 | 109 | ##### Request 110 | 111 | `POST /api/:table` 112 | 113 | ##### Result 114 | 115 | This will return the created row like at [`GET /api/:table`](#get-table) 116 | 117 | --- 118 | 119 | #### PUT 120 | 121 | ##### Request 122 | 123 | `PUT /api/:table/:id` 124 | 125 | ##### Result 126 | 127 | This will return the updated row like at [`GET /api/:table`](#get-table) 128 | 129 | --- 130 | 131 | #### DELETE 132 | 133 | ##### Request 134 | 135 | `DELETE /api/:table/:id` 136 | 137 | ##### Result 138 | 139 | This will return the deleted id. Whereby the id is the first primary key of the table. Example: 140 | 141 | ```json 142 | { 143 | "result": "success", 144 | "json": { 145 | "deletedID": "1" 146 | }, 147 | "table": "test" 148 | } 149 | ``` 150 | 151 | ## Config 152 | 153 | This line inits the api. You can provide a config object to adjust the settings to your need by adding an options object: 154 | 155 | `mysqltorest(app,connection,options);` 156 | 157 | If not specified, the following options will be used: 158 | 159 | ```js 160 | var default_options = { 161 | uploadDestination:__dirname + '/uploads', 162 | allowOrigin:'*', 163 | maxFileSize:-1, 164 | apiURL:'/api', 165 | paramPrefix:'_' 166 | }; 167 | ``` 168 | 169 | 170 | The options consist of the following: 171 | 172 | ### Options 173 | 174 | #### uploadDestination 175 | 176 | This specifies the multer upload destination. The default is ` __dirname + '/uploads'`. For more read the [multer documentation](https://github.com/expressjs/multer). 177 | 178 | #### allowOrigin 179 | 180 | As the API sets some default headers this sets the Access-Control-Allow-Origin header. Provide the domain or url the API should be accessed by. Default is `*` so be careful! 181 | 182 | #### maxFileSize 183 | 184 | This checks the filesize of the uploaded files. The value is in bytes. Default (and off) is `-1`. 185 | 186 | #### apiURL 187 | 188 | Here the url to the api is specified. Default is `/api`. 189 | 190 | #### paramPrefix 191 | 192 | This is the query prefix for not select querys like order or limit. 193 | 194 | ### Functions 195 | 196 | Currently there is only one API call: 197 | 198 | `api.setAuth(function)` 199 | 200 | #### function 201 | 202 | Provide an express middleware to authenticate the requests to the api specifically. The following example shows the basic idea: 203 | 204 | ```js 205 | api.setAuth(function(req,res,next) { 206 | if(req.isAuthenticated && req.method === 'GET'){ 207 | next(); 208 | } else { 209 | //Handle unauthorized access 210 | } 211 | }); 212 | ``` 213 | 214 | 215 | ## MySQL Config 216 | 217 | To make the setup as easy as possible mysql-to-rest reads almost all config directly form the database. This has two "pitfalls": 218 | 219 | * `NOT NULL` Columns are seen as required. Even if they have a default value. 220 | * If you want to upload a file. You have to do the following steps: 221 | * Create a varchar or text column. 222 | * Set the default value to `FILE`. 223 | 224 | ## Docker 225 | 226 | A full version can be deployed using docker. (Thanks to @reduardo7) 227 | https://hub.docker.com/r/reduardo7/db-to-api/ 228 | -------------------------------------------------------------------------------- /controllers/api.js: -------------------------------------------------------------------------------- 1 | var connection, 2 | settings, 3 | lastQry, 4 | mysql = require('mysql'); 5 | ; 6 | 7 | module.exports = function(connection_,settings_) { 8 | 9 | //Parse params 10 | if (!connection_) throw new Error('No connection specified!'); 11 | else connection = connection_; 12 | 13 | if (settings_) settings = settings_; 14 | 15 | 16 | //Export API 17 | return { 18 | /** 19 | * Find all rows given to special params 20 | * @param req 21 | * @param res 22 | */ 23 | findAll: function (req, res) { 24 | 25 | //To avoid errors, escape table 26 | req.params.table = escape(req.params.table); 27 | 28 | //Get all fields from table 29 | lastQry = connection.query('SHOW COLUMNS FROM ??', req.params.table, function (err, rows) { 30 | if (err) return sendError(res, err.code); 31 | 32 | 33 | var fields, limit, order, where; 34 | 35 | 36 | //All general Settings 37 | 38 | 39 | //Select only certain columns? 40 | if (typeof req.query[settings.paramPrefix + "fields"] !== "undefined" && typeof req.query[settings.paramPrefix + "fields"] == "string" && req.query[settings.paramPrefix + "fields"] !== "") { 41 | var req_fields = escape(req.query[settings.paramPrefix + "fields"]); 42 | if (checkIfFieldsExist(req_fields, rows)) { 43 | fields = req_fields; 44 | } else { 45 | return sendError(res, "Failed to fetch all fields specified in _fields"); 46 | } 47 | } else { 48 | fields = '*'; 49 | } 50 | 51 | //Parse order by and order direction 52 | if (typeof req.query[settings.paramPrefix + "order"] !== "undefined" && typeof req.query[settings.paramPrefix + "order"] === "object") { 53 | var orderObj = req.query[settings.paramPrefix + "order"]; 54 | var orderArr = []; 55 | for (var order_field in orderObj) { 56 | if (orderObj.hasOwnProperty(order_field)) { 57 | if (checkIfFieldsExist(order_field, rows)) { 58 | if (orderObj[order_field].toUpperCase() == "ASC" || orderObj[order_field].toUpperCase() == "DESC") { 59 | orderArr.push(req.params.table + ".`" + order_field + "` " + orderObj[order_field].toUpperCase()); 60 | } else { 61 | return sendError(res, "Order option for " + order_field + " in _order is invalid. Please use either ASC or DESC"); 62 | } 63 | } else { 64 | return sendError(res, "Failed to fetch " + order_field + " specified in _order"); 65 | } 66 | } 67 | } 68 | order = orderArr.join(','); 69 | } 70 | 71 | //Parse limit 72 | if (typeof req.query[settings.paramPrefix + "limit"] !== "undefined" && typeof req.query[settings.paramPrefix + "limit"] === "string" && req.query[settings.paramPrefix + "limit"] !== "") { 73 | 74 | var limitStr = req.query[settings.paramPrefix + "limit"]; 75 | 76 | 77 | var limitArr = limitStr.split(','); 78 | if (limitArr.length > 2) { 79 | return sendError(res, "Limit specificed in _limit is invalid. Limit does not allow more than 2 comma separated values"); 80 | } 81 | for (var i = 0; i < limitArr.length; i++) { 82 | var lim = parseInt(limitArr[i]); 83 | if (isNaN(lim)) { 84 | return sendError(res, "Limit specificed in _limit is invalid. Please use a comma to separate start and amount"); 85 | } 86 | limitArr[i] = lim; 87 | } 88 | 89 | limit = limitArr.join(','); 90 | 91 | } 92 | 93 | 94 | //Parse all other get params as selector 95 | var whereArr = []; 96 | 97 | for (var q in req.query) { 98 | if (req.query.hasOwnProperty(q)) { 99 | if (q.substr(0, 1) !== settings.paramPrefix) { 100 | if (checkIfFieldsExist(q, rows)) { 101 | 102 | if (typeof req.query[q] === "string") { 103 | //If it is a simple string, the api assumes = as the operator 104 | whereArr.push(mysql.format('?? = ?', [q, escape(req.query[q])])); 105 | 106 | } else if (typeof req.query[q] === "object") { 107 | 108 | for (var selector in req.query[q]) { 109 | if (req.query[q].hasOwnProperty(selector)) { 110 | var op, val = connection.escape(escape(req.query[q][selector])); 111 | //MYSQLify the operator 112 | switch (selector.toUpperCase()) { 113 | case 'GREAT': 114 | op = "> "; 115 | break; 116 | case 'SMALL': 117 | op = "< "; 118 | break; 119 | case 'EQGREAT': 120 | op = '>= '; 121 | break; 122 | case 'EQSMALL': 123 | op = '<= '; 124 | break; 125 | case 'IN': 126 | op = 'IN ('; 127 | val = escape(req.query[q][selector]).split(',').map(function (item) { 128 | return connection.escape(item); 129 | }).join(',') + ')'; 130 | break; 131 | case 'LIKE': 132 | op = 'LIKE '; 133 | break; 134 | case 'EQ': 135 | op = '= '; 136 | break; 137 | default: 138 | op = '= '; 139 | break; 140 | } 141 | whereArr.push(mysql.format('?? ' + op + val, [q])); 142 | } 143 | } 144 | 145 | } else { 146 | return sendError(res, "Can't read parameter specified for the field " + q + " in the table " + req.params.table); 147 | } 148 | 149 | 150 | } else { 151 | return sendError(res, "Can't find the field" + q + " in the table " + req.params.table); 152 | } 153 | } 154 | } 155 | } 156 | 157 | if (whereArr.length > 0) { 158 | where = whereArr.join(' AND ') 159 | } 160 | 161 | //Bring in MYSQL syntax 162 | fields = fields ? fields : '*'; 163 | order = order ? 'ORDER BY ' + order : ''; 164 | limit = limit ? 'LIMIT ' + limit : ''; 165 | where = where ? 'WHERE ' + where : ''; 166 | 167 | //Fire query 168 | lastQry = connection.query("SELECT " + fields + " FROM ?? " + where + " " + order + " " + limit, [req.params.table], function (err, rows) { 169 | if (err) return sendError(res, err.code); 170 | res.send({ 171 | result: 'success', 172 | json: rows, 173 | table: req.params.table, 174 | length: rows.length 175 | }); 176 | }); 177 | }); 178 | }, 179 | /** 180 | * find single entry by selector (default is primary key) 181 | * @param req 182 | * @param res 183 | */ 184 | findById: function (req, res) { 185 | 186 | req.params.table = escape(req.params.table); 187 | 188 | //Request DB structure 189 | lastQry = connection.query('SHOW COLUMNS FROM ??', req.params.table, function (err, columns) { 190 | 191 | //Get primary key of table if not specified via query 192 | var field = findPrim(columns, req.query[settings.paramPrefix + 'field']); 193 | 194 | lastQry = connection.query('SELECT * FROM ?? WHERE ??.?? IN (' + req.params.id + ')', [req.params.table, req.params.table, field], function (err, rows) { 195 | if (err) return sendError(res, err.code); 196 | res.send({ 197 | result: 'success', 198 | json: rows, 199 | table: req.params.table, 200 | length: rows.length 201 | }); 202 | }); 203 | }); 204 | 205 | }, 206 | /** 207 | * Adds an element to the database. 208 | * @param req 209 | * @param res 210 | */ 211 | addElement: function(req, res){ 212 | 213 | var insertJson = {}; 214 | 215 | //Request DB structure 216 | lastQry = connection.query('SHOW COLUMNS FROM ??', req.params.table , function (err, columns) { 217 | if (err) return sendError(res,err.code); 218 | 219 | var value; 220 | 221 | //Forced sync iterator 222 | var iterator = function (i) { 223 | if (i >= columns.length) { 224 | insertIntoDB(); 225 | return; 226 | } 227 | 228 | var dbField = columns[i]; 229 | var field = dbField.Field; 230 | 231 | //Check required fields 232 | if (dbField.Null === 'NO' && 233 | dbField.Default === '' && 234 | dbField.Extra !== 'auto_increment' && 235 | dbField.Extra.search('on update')===-1) { 236 | 237 | //Check if field not set 238 | if (undefOrEmpty(req.body[field])) { 239 | return sendError(res,"Field " + field + " is NOT NULL but not specified in this request"); 240 | } else { 241 | //Check if the set values are roughly okay 242 | value = checkIfSentvaluesAreSufficient(req,dbField); 243 | console.log(value); 244 | if(value !== false) { 245 | //Value seems okay, go to the next field 246 | insertJson[field] = value; 247 | iterator(i + 1); 248 | } else { 249 | return sendError(res,'Value for field ' + field + ' is not sufficient. Expecting ' + dbField.Type + ' but got ' + typeof req.body[field] ); 250 | } 251 | } 252 | } else { 253 | //Check for not required fields 254 | //Skip auto_incremented fields 255 | if(dbField.Extra === 'auto_increment') { 256 | iterator(i + 1); 257 | } else { 258 | //Check if the field was provided by the client 259 | var defined = false; 260 | if(dbField.Default == "FILE") { 261 | if(req.files.hasOwnProperty(dbField.Field)) { 262 | defined = true; 263 | } 264 | } else { 265 | if(typeof req.body[field] !== "undefined") { 266 | defined = true; 267 | } 268 | } 269 | 270 | //If it was provided, check if the values are okay 271 | if(defined) { 272 | value = checkIfSentvaluesAreSufficient(req,dbField); 273 | if(value !== false) { 274 | insertJson[field] = value; 275 | iterator(i + 1); 276 | } else { 277 | if(dbField.Default == "FILE") { 278 | return sendError(res, 'Value for field ' + field + ' is not sufficient. Either the file is to large or an other error occured'); 279 | } else { 280 | return sendError(res, 'Value for field ' + field + ' is not sufficient. Expecting ' + dbField.Type + ' but got ' + typeof req.body[field]); 281 | } 282 | } 283 | } else { 284 | //If not, don't mind 285 | iterator(i + 1); 286 | } 287 | 288 | } 289 | } 290 | 291 | }; 292 | 293 | iterator(0); //start the async "for" loop 294 | 295 | /** 296 | * When the loop is finished write everything in the database 297 | */ 298 | function insertIntoDB() { 299 | lastQry = connection.query('INSERT INTO ?? SET ?', [req.params.table , insertJson] , function (err, rows) { 300 | if (err) { 301 | console.error(err); 302 | res.statusCode = 500; 303 | res.send({ 304 | result: 'error', 305 | err: err.code 306 | }); 307 | } else { 308 | sendSuccessAnswer(req.params.table , res, rows.insertId); 309 | } 310 | 311 | }); 312 | } 313 | }); 314 | }, 315 | /** 316 | * Update an existing row 317 | * @param req 318 | * @param res 319 | */ 320 | updateElement: function(req, res) { 321 | var updateJson = {}; 322 | var updateSelector = {}; 323 | 324 | //Request database structure 325 | lastQry = connection.query('SHOW COLUMNS FROM ??', req.params.table , function (err, columns) { 326 | if (err) return sendError(res,err.code); 327 | 328 | //Check if the request is provided an select value 329 | if (typeof req.params.id === "undefined") { 330 | return sendError(res,'Please specify an selector value for the fields '); 331 | } else { 332 | updateSelector.value = req.params.id; 333 | updateSelector.field = findPrim(columns,req.query[settings.paramPrefix + 'field']); 334 | } 335 | 336 | //If the user only wants to update files 337 | if( Object.keys(req.body).length == 0) { 338 | req.body = req.files; 339 | } 340 | var value; 341 | var iterator = function (i) { 342 | if (i >= columns.length || (typeof req.body.length === "undefined" && typeof req.files.length !== "undefined")) { 343 | if(JSON.stringify(updateJson) !== '{}') { 344 | updateIntoDB(); 345 | } else { 346 | return sendError(res,'No Data received!'); 347 | } 348 | return; 349 | } 350 | var dbField = columns[i]; 351 | var field = dbField.Field; 352 | //Check if the current checked field is set 353 | if(typeof req.body[field] !== "undefined" || (dbField.Default == 'FILE' && typeof req.files[field] !== "undefined") ) { 354 | //First check if the field is required 355 | if(dbField.Null == 'NO' && dbField.Extra != 'auto_increment') { 356 | value = checkIfSentvaluesAreSufficient(req,dbField); 357 | if(value !== false) { 358 | updateJson[field] = value; 359 | iterator(i + 1); 360 | } else { 361 | return sendError(res,"Not all 'NOT NULL' fields are filled ("+ field +" is missing)"); 362 | } 363 | } else { 364 | //Check for not required fields 365 | //Skip auto_incremented fields 366 | if(dbField.Extra === 'auto_increment') { 367 | iterator(i + 1); 368 | } else { 369 | //Check if the field was provided by the client 370 | var defined = false; 371 | if(dbField.Default == "FILE") { 372 | if(req.files.hasOwnProperty(dbField.Field)) { 373 | defined = true; 374 | } 375 | } else { 376 | if(typeof req.body[field] !== "undefined") { 377 | defined = true; 378 | } 379 | } 380 | 381 | //If it was provided, check if the values are okay 382 | if(defined) { 383 | value = checkIfSentvaluesAreSufficient(req,dbField); 384 | if(value !== false) { 385 | updateJson[field] = value; 386 | iterator(i + 1); 387 | } else { 388 | if(dbField.Default == "FILE") { 389 | return sendError(res, 'Value for field ' + field + ' is not sufficient. Either the file is to large or an other error occured'); 390 | } else { 391 | return sendError(res, 'Value for field ' + field + ' is not sufficient. Expecting ' + dbField.Type + ' but got ' + typeof req.body[field]); 392 | } 393 | } 394 | } else { 395 | //If not, don't mind 396 | iterator(i + 1); 397 | } 398 | } 399 | } 400 | } else { 401 | iterator(i + 1); 402 | } 403 | }; 404 | 405 | iterator(0); //start the async "for" loop 406 | 407 | function updateIntoDB() { 408 | //Yaaay, alle Tests bestanden gogo, insert! 409 | lastQry = connection.query('UPDATE ?? SET ? WHERE ?? = ?', [req.params.table , updateJson, updateSelector.field, updateSelector.value] , function (err) { 410 | if (err) return sendError(res,err.code); 411 | sendSuccessAnswer(req.params.table , res, req.params.id, updateSelector.field); 412 | 413 | }); 414 | } 415 | }); 416 | }, 417 | deleteElement: function(req, res){ 418 | 419 | var deleteSelector = {}; 420 | 421 | lastQry = connection.query('SHOW COLUMNS FROM ??', req.params.table , function (err, columns) { 422 | if (err) return sendError(res,err.code); 423 | 424 | //Check if selector is sent 425 | if (typeof req.params.id === "undefined") { 426 | return sendError(res,'You have to specify an ID to update an entry at /api/table/ID'); 427 | } else { 428 | deleteSelector.field = findPrim(columns,req.query[settings.paramPrefix + 'field']); 429 | deleteSelector.value = req.params.id; 430 | } 431 | 432 | lastQry = connection.query('DELETE FROM ?? WHERE ?? = ?', [req.params.table, deleteSelector.field, deleteSelector.value] , function (err, rows) { 433 | if (err) return sendError(res,err.code); 434 | 435 | if(rows.affectedRows > 0) { 436 | res.send({ 437 | result: 'success', 438 | json: {deletedID:req.params.id}, 439 | table: req.params.table 440 | }); 441 | } else return sendError(res,'No rows deleted'); 442 | 443 | }); 444 | }); 445 | } 446 | } 447 | }; 448 | 449 | /** 450 | * Send the edited element to the requester 451 | * @param table 452 | * @param res 453 | * @param id 454 | * @param field 455 | */ 456 | 457 | function sendSuccessAnswer(table, res, id, field) { 458 | if(typeof field === "undefined") { 459 | if(id === 0) { 460 | //Just assume that everything went okay. It looks like a non numeric primary key. 461 | res.send({ 462 | result: 'success', 463 | table: table 464 | }); 465 | return; 466 | } else { 467 | field = "id"; 468 | } 469 | } 470 | lastQry = connection.query('SELECT * FROM ?? WHERE ?? = ?', [table, field, id] , function (err, rows) { 471 | if (err) { 472 | sendError(res, err.code) 473 | } else { 474 | res.send({ 475 | result: 'success', 476 | json: rows, 477 | table: table 478 | }); 479 | } 480 | }); 481 | } 482 | 483 | /** 484 | * check if object is undefined or empty 485 | * @param obj 486 | * @returns {boolean} 487 | */ 488 | 489 | function undefOrEmpty(obj) { 490 | return !!(typeof obj === 'undefined' || obj === null || obj === undefined || obj === ''); 491 | } 492 | 493 | /** 494 | * Check roughly if the provided value is sufficient for the database field 495 | * @param req 496 | * @param dbField 497 | * @returns {*} 498 | */ 499 | 500 | function checkIfSentvaluesAreSufficient(req,dbField) { 501 | if(dbField.Default == 'FILE') { 502 | //For 'File' fields just return the link ot the file 503 | if(req.files.hasOwnProperty(dbField.Field)) { 504 | 505 | var file = req.files[dbField.Field].hasOwnProperty('name') ? req.files[dbField.Field] : req.files[dbField.Field][0]; 506 | 507 | if(settings.maxFileSize !== -1 && file.size > settings.maxFileSize) { 508 | return false; 509 | } 510 | 511 | return file.name; 512 | 513 | } else { 514 | return false; 515 | } 516 | } else { 517 | if (req.body[dbField.Field] === null || typeof req.body[dbField.Field] == "undefined") { 518 | return dbField.Null == "YES" ? null : false; 519 | } 520 | //Normle Werte 521 | if((dbField.Type.indexOf("int") != -1 || dbField.Type.indexOf("float") != -1 || dbField.Type.indexOf("double") != -1 )) { 522 | return !isNaN(req.body[dbField.Field]) ? req.body[dbField.Field] : false; 523 | } else if(typeof req.body[dbField.Field] === 'string') { 524 | return escape(req.body[dbField.Field]); 525 | } 526 | return false; 527 | } 528 | } 529 | 530 | /** 531 | * Credits to Paul d'Aoust @ http://stackoverflow.com/questions/7744912/making-a-javascript-string-sql-friendly 532 | * @param str 533 | * @returns {string} 534 | */ 535 | 536 | function escape (str) { 537 | return str.replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, function (char) { 538 | switch (char) { 539 | case "\0": 540 | return "\\0"; 541 | case "\x08": 542 | return "\\b"; 543 | case "\x09": 544 | return "\\t"; 545 | case "\x1a": 546 | return "\\z"; 547 | case "\n": 548 | return "\\n"; 549 | case "\r": 550 | return "\\r"; 551 | case "\"": 552 | case "'": 553 | case "\\": 554 | return "\\"+char; // prepends a backslash to backslash, percent, 555 | // and double/single quotes 556 | case "%": 557 | return "%"; 558 | } 559 | }); 560 | } 561 | 562 | /** 563 | * Search in DB if the field(s) exist 564 | * @param fieldStr 565 | * @param rows 566 | * @returns {boolean} 567 | */ 568 | 569 | function checkIfFieldsExist(fieldStr,rows) { 570 | 571 | var ret = true; 572 | 573 | if(fieldStr.search(',') > -1 ) { 574 | var fieldArr = fieldStr.split(','); 575 | fieldArr.forEach(function (field) { 576 | if(ret) { 577 | if(rows.filter(function (r) {return r.Field === field;}).length == 0) { 578 | ret = false; 579 | } 580 | } 581 | }); 582 | } else { 583 | if(rows.filter(function (r) {return r.Field === fieldStr;}).length == 0) { 584 | ret = false; 585 | } 586 | } 587 | 588 | return ret; 589 | } 590 | 591 | /** 592 | * Send error messsage to the user 593 | * @param res 594 | * @param err 595 | */ 596 | 597 | function sendError(res,err) { 598 | console.error(err); 599 | // also log last executed query, for easier debugging 600 | console.error(lastQry.sql); 601 | res.statusCode = 500; 602 | res.send({ 603 | result: 'error', 604 | err: err 605 | }); 606 | } 607 | 608 | /** 609 | * Get primary key, or if specified 610 | * @param columns 611 | * @param field 612 | * @returns {*} 613 | */ 614 | 615 | function findPrim(columns,field) { 616 | 617 | var primary_keys = columns.filter(function (r) {return r.Key === 'PRI';}); 618 | 619 | //for multiple primary keys, just take the first 620 | if(primary_keys.length > 0) { 621 | return primary_keys[0].Field; 622 | } 623 | 624 | //If the provided field is a string, we might have a chance 625 | if(typeof field === "string") { 626 | if(checkIfFieldsExist(field,columns)) { 627 | return escape(field); 628 | } 629 | } 630 | 631 | //FALLBACK 632 | return "id"; 633 | } 634 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // BASE SETUP 2 | // ============================================================================= 3 | 4 | // call the packages we need 5 | var bodyParser = require('body-parser'); 6 | var multer = require('multer'); 7 | 8 | module.exports = function(app,connection,options) { 9 | 10 | if(!app) throw new Error('Please provide express to this module'); 11 | 12 | //Parse Options 13 | options = options || {}; 14 | options.uploadDestination = options.uploadDestination || __dirname + '/uploads/'; //Where to save the uploaded files 15 | options.allowOrigin = options.allowOrigin || '*'; //Sets the Access-Control-Allow-Origin header 16 | options.maxFileSize = options.maxFileSize || -1; //Max filesize for uploads in bytes 17 | options.apiURL = options.apiURL || '/api'; //Url Prefix for API 18 | options.paramPrefix = options.paramPrefix || '_'; //Prefix for special params (eg. order or fields). 19 | 20 | app.use(bodyParser.urlencoded({ extended: false }) ); 21 | app.use(multer({dest: options.uploadDestination })); 22 | 23 | //============================================================================== 24 | //Routing 25 | 26 | // Add headers 27 | app.use(function (req, res, next) { 28 | 29 | // Website you wish to allow to connect 30 | res.setHeader('Access-Control-Allow-Origin', options.allowOrigin); 31 | 32 | // Request methods you wish to allow 33 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); 34 | 35 | // Request headers you wish to allow 36 | res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); 37 | 38 | // Set to true if you need the website to include cookies in the requests sent 39 | // to the API (e.g. in case you use sessions) 40 | res.setHeader('Access-Control-Allow-Credentials', true); 41 | 42 | // Pass to next layer of middleware 43 | next(); 44 | }); 45 | 46 | var ensureAuthenticated = function(req,res,next) { 47 | next(); 48 | }; 49 | 50 | var api = require('./controllers/api.js')(connection,{ 51 | maxFileSize:options.maxFileSize, 52 | paramPrefix:options.paramPrefix 53 | }); 54 | 55 | //Set actual routes 56 | app.get(options.apiURL + '/:table', ensureAuthenticated, api.findAll); 57 | app.get(options.apiURL + '/:table/:id', ensureAuthenticated, api.findById); 58 | app.post(options.apiURL + '/:table', ensureAuthenticated, api.addElement); 59 | app.put(options.apiURL + '/:table/:id', ensureAuthenticated, api.updateElement); 60 | app.delete(options.apiURL + '/:table/:id', ensureAuthenticated, api.deleteElement); 61 | 62 | 63 | //Export API 64 | return { 65 | setAuth:function(fnc) { 66 | ensureAuthenticated = fnc; 67 | } 68 | } 69 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql-to-rest", 3 | "version": "1.0.8", 4 | "description": "This module maps an MYSQL DB to an REST API interface.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "body-parser": "^1.12.4", 8 | "multer": "^0.1.8" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "start": "node index.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Ardobras/mysql-to-rest.git" 18 | }, 19 | "keywords": [ 20 | "mysql", 21 | "rest", 22 | "api" 23 | ], 24 | "author": "Keno Dressel", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/Ardobras/mysql-to-rest/issues" 28 | }, 29 | "homepage": "https://github.com/Ardobras/mysql-to-rest" 30 | } 31 | --------------------------------------------------------------------------------