├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── assets ├── doubon copy.png ├── doubon.png ├── facebook.png ├── google.png ├── hn.jpg ├── hn1.png ├── linkedin.png ├── log.gif ├── out.gif ├── pinterest.png ├── reddit.png ├── renren.png ├── rick-and-morty.gif ├── stumbleupon.png ├── twitter.png ├── vk.png ├── weibo.png └── wykop.png ├── bin └── index.js ├── dev.Dockerfile ├── docker-compose.yml ├── docker-entrypoint.sh ├── dockerfile ├── examples └── aws-lambda │ └── index.js ├── index.js ├── lib ├── util │ ├── cmd.helper.js │ ├── data.helper.js │ └── whereClause.helper.js ├── xapi.js ├── xctrl.js └── xsql.js ├── package-lock.json ├── package.json └── tests ├── docker-sample.sql ├── sample.sql └── tests.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["eslint-config-airbnb-base", "eslint-config-prettier"], 3 | plugins: ["eslint-plugin-import", "eslint-plugin-prettier"], 4 | parserOptions: { 5 | ecmaFeatures: { 6 | ecmaVersion: 6 7 | } 8 | }, 9 | env: { 10 | es6: true, 11 | node: true 12 | }, 13 | rules: { 14 | "prettier/prettier": ["error", {}], 15 | "max-len": ["error", { code: 2000, ignoreUrls: true }], 16 | "linebreak-style": 0, 17 | "no-use-before-define": ["error", { functions: false, classes: false }], 18 | "no-plusplus": ["error", { allowForLoopAfterthoughts: true }], 19 | "no-underscore-dangle": 0, 20 | "import/no-amd": 0, 21 | "import/no-dynamic-require": 0, 22 | "no-console": 0, 23 | "no-param-reassign": 0, 24 | "no-unused-vars": ["error", { argsIgnorePattern: "next" }], 25 | "comma-dangle": 0 26 | } 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | # =========== 3 | .DS_Store 4 | ehthumbs.db 5 | Icon? 6 | Thumbs.db 7 | 8 | # Node and related ecosystem 9 | # ========================== 10 | .nodemonignore 11 | .sass-cache/ 12 | node_modules/ 13 | help/ 14 | a 15 | public/lib/ 16 | app/tests/coverage/ 17 | .bower-*/ 18 | .idea/ 19 | coverage/ 20 | 21 | 22 | # MEAN.js app and assets 23 | # ====================== 24 | public/dist/ 25 | uploads 26 | modules/users/client/img/profile/uploads 27 | config/env/local.js 28 | *.pem 29 | 30 | # Ignoring MEAN.JS's gh-pages branch for documenation 31 | _site/ 32 | 33 | # General 34 | # ======= 35 | *.log 36 | *.csv 37 | *.dat 38 | *.out 39 | *.pid 40 | *.gz 41 | *.tmp 42 | *.bak 43 | *.swp 44 | logs/ 45 | build/ 46 | uploads/ 47 | 48 | # Sublime editor 49 | # ============== 50 | .sublime-project 51 | *.sublime-project 52 | *.sublime-workspace 53 | 54 | # Eclipse project files 55 | # ===================== 56 | .project 57 | .settings/ 58 | .*.md.html 59 | .metadata 60 | *~.nib 61 | local.properties 62 | 63 | # IntelliJ 64 | # ======== 65 | *.iml 66 | 67 | # Cloud9 IDE 68 | # ========= 69 | .c9/ 70 | data/ 71 | mongod 72 | 73 | # Visual Studio 74 | # ========= 75 | *.suo 76 | *.ntvs* 77 | *.njsproj 78 | *.sln 79 | 80 | .history 81 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '7.6.0' 4 | - '8' 5 | - '10' 6 | - '12' 7 | - 'node' 8 | services: 9 | - mysql 10 | install: 11 | - npm install 12 | before_install: 13 | - mysql -e 'CREATE DATABASE IF NOT EXISTS classicmodels;' 14 | - mysql -u root --default-character-set=utf8 classicmodels < tests/sample.sql 15 | script: 16 | - npm test 17 | 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contribution guidelines 2 | - Try to keep it close to original design. Key idea here being zero configuration. So automate by going out of the way. 3 | - Tests to cover new functionality. 4 | - If the issue is a major or large change. Please create a PR issue before working on it. 5 | 6 | 7 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Please provide dev environment versions of your system 4 | 5 | node -v 6 | ( xmysql requires node >= 7.6.0 ) 7 | 8 | npm -v 9 | mysql --version 10 | xmysql --version 11 | 12 | - - - 13 | 14 | Provide steps to reproduce this issue 15 | or 16 | Provide minimum code to reproduce this issue 17 | or 18 | HTTP request,response,error snapshot of this issue 19 | 20 | - - - 21 | 22 | 23 | Mark issue with suitable label if it is clear what this issue is. 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 o1lab 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 | ![npm version](https://img.shields.io/node/v/xmysql.svg) 2 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/o1lab/xmysql/master/LICENSE) 3 | 4 |

5 | 6 | Xmysql is now NocoDB
7 |
8 | ✨ The Open Source Airtable Alternative ✨
9 | 10 |

11 | 12 | https://github.com/nocodb/nocodb 13 | 14 | ## Xmysql : One command to generate REST APIs for any MySql database 15 | 16 | ## Why this ? 17 |

18 | xmysql gif 19 |

20 | 21 | Generating REST APIs for a MySql database which does not follow conventions of 22 | frameworks such as rails, django, laravel etc is a small adventure that one like to avoid .. 23 | 24 | Hence this. 25 | 26 | ## Setup and Usage 27 | 28 | xmysql requires node >= 7.6.0 29 | 30 | ``` 31 | npm install -g xmysql 32 | ``` 33 | ``` 34 | xmysql -h localhost -u mysqlUsername -p mysqlPassword -d databaseName 35 | ``` 36 | ``` 37 | http://localhost:3000 38 | ``` 39 |
40 | 41 | That is it! Simple and minimalistic! 42 | 43 | Happy hackery! 44 | 45 | 46 | 47 | 48 | ## Example : Generate REST APIs for [Magento](http://www.magereverse.com/index/magento-sql-structure/version/1-7-0-2) 49 | 50 | Powered by popular node packages : ([express](https://github.com/expressjs/express), [mysql](https://github.com/mysqljs/mysql)) => { [xmysql](https://github.com/o1lab/xmysql) } 51 |

52 | xmysql gif 53 |

54 | 55 | 56 | 57 |
58 |

Boost Your Hacker Karma By Sharing :

59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 | 71 | 72 | 73 | 74 | ## Features 75 | * Generates API for **ANY** MySql database :fire::fire: 76 | * Serves APIs irrespective of naming conventions of primary keys, foreign keys, tables etc :fire::fire: 77 | * Support for composite primary keys :fire::fire: 78 | * REST API Usual suspects : CRUD, List, FindOne, Count, Exists, Distinct 79 | * Bulk insert, Bulk delete, Bulk read :fire: 80 | * Relations 81 | * Pagination 82 | * Sorting 83 | * Column filtering - Fields :fire: 84 | * Row filtering - Where :fire: 85 | * Aggregate functions 86 | * Group By, Having (as query params) :fire::fire: 87 | * Group By, Having (as a separate API) :fire::fire: 88 | * Multiple group by in one API :fire::fire::fire::fire: 89 | * Chart API for numeric column :fire::fire::fire::fire::fire::fire: 90 | * Auto Chart API - (a gift for lazy while prototyping) :fire::fire::fire::fire::fire::fire: 91 | * [XJOIN - (Supports any number of JOINS)](#xjoin) :fire::fire::fire::fire::fire::fire::fire::fire::fire: 92 | * Supports views 93 | * Prototyping (features available when using local MySql server only) 94 | * Run dynamic queries :fire::fire::fire: 95 | * Upload single file 96 | * Upload multiple files 97 | * Download file 98 | * Health and version apis 99 | * Use more than one CPU Cores 100 | * [Docker support](#docker) and [Nginx reverse proxy config](#nginx-reverse-proxy-config-with-docker) :fire::fire::fire: - Thanks to [@markuman](https://github.com/markuman) 101 | * AWS Lambda Example - Thanks to [@bertyhell](https://github.com/bertyhell) :fire::fire::fire: 102 | 103 | 104 | Use HTTP clients like [Postman](https://www.getpostman.com/) or [similar tools](https://chrome.google.com/webstore/search/http%20client?_category=apps) to invoke REST API calls 105 | 106 | ____ 107 | 108 | Download [node](https://nodejs.org/en/download/current/), 109 | [mysql](https://dev.mysql.com/downloads/mysql/) 110 | [(setup mysql)](https://dev.mysql.com/doc/mysql-getting-started/en/#mysql-getting-started-installing), 111 | [sample database](https://dev.mysql.com/doc/employee/en/employees-installation.html) - 112 | if you haven't on your system. 113 | 114 | 115 | ## API Overview 116 | 117 | | HTTP Type | API URL | Comments | 118 | |-----------|----------------------------------|--------------------------------------------------------- 119 | | GET | / | Gets all REST APIs | 120 | | GET | /api/tableName | Lists rows of table | 121 | | POST | /api/tableName | Create a new row | 122 | | PUT | /api/tableName | Replaces existing row with new row | 123 | | POST :fire:| /api/tableName/bulk | Create multiple rows - send object array in request body| 124 | | GET :fire:| /api/tableName/bulk | Lists multiple rows - /api/tableName/bulk?_ids=1,2,3 | 125 | | DELETE :fire:| /api/tableName/bulk | Deletes multiple rows - /api/tableName/bulk?_ids=1,2,3 | 126 | | GET | /api/tableName/:id | Retrieves a row by primary key | 127 | | PATCH | /api/tableName/:id | Updates row element by primary key | 128 | | DELETE | /api/tableName/:id | Delete a row by primary key | 129 | | GET | /api/tableName/findOne | Works as list but gets single record matching criteria | 130 | | GET | /api/tableName/count | Count number of rows in a table | 131 | | GET | /api/tableName/distinct | Distinct row(s) in table - /api/tableName/distinct?_fields=col1| 132 | | GET | /api/tableName/:id/exists | True or false whether a row exists or not | 133 | | GET | [/api/parentTable/:id/childTable](#relational-tables) | Get list of child table rows with parent table foreign key | 134 | | GET :fire:| [/api/tableName/aggregate](#aggregate-functions) | Aggregate results of numeric column(s) | 135 | | GET :fire:| [/api/tableName/groupby](#group-by-having-as-api) | Group by results of column(s) | 136 | | GET :fire:| [/api/tableName/ugroupby](#union-of-multiple-group-by-statements) | Multiple group by results using one call | 137 | | GET :fire:| [/api/tableName/chart](#chart) | Numeric column distribution based on (min,max,step) or(step array) or (automagic)| 138 | | GET :fire:| [/api/tableName/autochart](#autochart) | Same as Chart but identifies which are numeric column automatically - gift for lazy while prototyping| 139 | | GET :fire:| [/api/xjoin](#xjoin) | handles join | 140 | | GET :fire:| [/dynamic](#run-dynamic-queries) | execute dynamic mysql statements with params | 141 | | GET :fire:| [/upload](#upload-single-file) | upload single file | 142 | | GET :fire:| [/uploads](#upload-multiple-files) | upload multiple files | 143 | | GET :fire:| [/download](#download-file) | download a file | 144 | | GET | /api/tableName/describe | describe each table for its columns | 145 | | GET | /api/tables | get all tables in database | 146 | | GET | [/_health](#health) | gets health of process and mysql -- details query params for more details | 147 | | GET | [/_version](#version) | gets version of Xmysql, mysql, node| 148 | 149 | 150 | ## Relational Tables 151 | xmysql identifies foreign key relations automatically and provides GET api. 152 | ``` 153 | /api/blogs/103/comments 154 | ``` 155 | eg: blogs is parent table and comments is child table. API invocation will result in all comments for blog primary key 103. 156 | [:arrow_heading_up:](#api-overview) 157 | 158 | 159 | ## Support for composite primary keys 160 | 161 | #### ___ (three underscores) 162 | 163 | ``` 164 | /api/payments/103___JM555205 165 | ``` 166 | *___* : If there are multiple primary keys - separate them by three underscores as shown 167 | 168 | ## Pagination 169 | 170 | #### _p & _size 171 | 172 | _p indicates page and _size indicates size of response rows 173 | 174 | By default 20 records and max of 100 are returned per GET request on a table. 175 | 176 | ``` 177 | /api/payments?_size=50 178 | ``` 179 | ``` 180 | /api/payments?_p=2 181 | ``` 182 | ``` 183 | /api/payments?_p=2&_size=50 184 | ``` 185 | 186 | When _size is greater than 100 - number of records defaults to 100 (i.e maximum) 187 | 188 | When _size is less than or equal to 0 - number of records defaults to 20 (i.e minimum) 189 | 190 | ## Order by / Sorting 191 | 192 | #### ASC 193 | 194 | ``` 195 | /api/payments?_sort=column1 196 | ``` 197 | eg: sorts ascending by column1 198 | 199 | #### DESC 200 | 201 | ``` 202 | /api/payments?_sort=-column1 203 | ``` 204 | eg: sorts descending by column1 205 | 206 | #### Multiple fields in sort 207 | 208 | ``` 209 | /api/payments?_sort=column1,-column2 210 | ``` 211 | eg: sorts ascending by column1 and descending by column2 212 | 213 | 214 | ## Column filtering / Fields 215 | ``` 216 | /api/payments?_fields=customerNumber,checkNumber 217 | ``` 218 | eg: gets only customerNumber and checkNumber in response of each record 219 | ``` 220 | /api/payments?_fields=-checkNumber 221 | ``` 222 | eg: gets all fields in table row but not checkNumber 223 | 224 | ## Row filtering / Where 225 | 226 | #### Comparison operators 227 | 228 | ``` 229 | eq - '=' - (colName,eq,colValue) 230 | ne - '!=' - (colName,ne,colValue) 231 | gt - '>' - (colName,gt,colValue) 232 | gte - '>=' - (colName,gte,colValue) 233 | lt - '<' - (colName,lt,colValue) 234 | lte - '<=' - (colName,lte,colValue) 235 | is - 'is' - (colName,is,true/false/null) 236 | in - 'in' - (colName,in,val1,val2,val3,val4) 237 | bw - 'between' - (colName,bw,val1,val2) 238 | like - 'like' - (colName,like,~name) note: use ~ in place of % 239 | nlike - 'not like' - (colName,nlike,~name) note: use ~ in place of % 240 | ``` 241 | 242 | #### Use of comparison operators 243 | ``` 244 | /api/payments?_where=(checkNumber,eq,JM555205)~or((amount,gt,200)~and(amount,lt,2000)) 245 | ``` 246 | 247 | #### Logical operators 248 | ``` 249 | ~or - 'or' 250 | ~and - 'and' 251 | ~xor - 'xor' 252 | ``` 253 | 254 | #### Use of logical operators 255 | 256 | eg: simple logical expression 257 | ``` 258 | /api/payments?_where=(checkNumber,eq,JM555205)~or(checkNumber,eq,OM314933) 259 | ``` 260 | 261 | eg: complex logical expression 262 | ``` 263 | /api/payments?_where=((checkNumber,eq,JM555205)~or(checkNumber,eq,OM314933))~and(amount,gt,100) 264 | ``` 265 | 266 | eg: logical expression with sorting(_sort), pagination(_p), column filtering (_fields) 267 | ``` 268 | /api/payments?_where=(amount,gte,1000)&_sort=-amount&p=2&_fields=customerNumber 269 | ``` 270 | 271 | eg: filter of rows using _where is available for relational route URLs too. 272 | ``` 273 | /api/offices/1/employees?_where=(jobTitle,eq,Sales%20Rep) 274 | ``` 275 | 276 | ## FindOne 277 | ``` 278 | /api/tableName/findOne?_where=(id,eq,1) 279 | ``` 280 | Works similar to list but only returns top/one result. Used in conjunction with _where 281 | [:arrow_heading_up:](#api-overview) 282 | 283 | ## Count 284 | ``` 285 | /api/tableName/count 286 | ``` 287 | 288 | Returns number of rows in table 289 | [:arrow_heading_up:](#api-overview) 290 | 291 | ## Exists 292 | ``` 293 | /api/tableName/1/exists 294 | ``` 295 | 296 | Returns true or false depending on whether record exists 297 | [:arrow_heading_up:](#api-overview) 298 | 299 | ## Group By Having as query params 300 | [:arrow_heading_up:](#api-overview) 301 | 302 | ``` 303 | /api/offices?_groupby=country 304 | ``` 305 | eg: SELECT country,count(*) FROM offices GROUP BY country 306 | 307 | ``` 308 | /api/offices?_groupby=country&_having=(_count,gt,1) 309 | ``` 310 | eg: SELECT country,count(1) as _count FROM offices GROUP BY country having _count > 1 311 | 312 | 313 | ## Group By Having as API 314 | [:arrow_heading_up:](#api-overview) 315 | 316 | ``` 317 | /api/offices/groupby?_fields=country 318 | ``` 319 | eg: SELECT country,count(*) FROM offices GROUP BY country 320 | 321 | ``` 322 | /api/offices/groupby?_fields=country,city 323 | ``` 324 | eg: SELECT country,city,count(*) FROM offices GROUP BY country,city 325 | 326 | ``` 327 | /api/offices/groupby?_fields=country,city&_having=(_count,gt,1) 328 | ``` 329 | eg: SELECT country,city,count(*) as _count FROM offices GROUP BY country,city having _count > 1 330 | 331 | 332 | ### Group By, Order By 333 | [:arrow_heading_up:](#api-overview) 334 | 335 | ``` 336 | /api/offices/groupby?_fields=country,city&_sort=city 337 | ``` 338 | eg: SELECT country,city,count(*) FROM offices GROUP BY country,city ORDER BY city ASC 339 | 340 | ``` 341 | /api/offices/groupby?_fields=country,city&_sort=city,country 342 | ``` 343 | eg: SELECT country,city,count(*) FROM offices GROUP BY country,city ORDER BY city ASC, country ASC 344 | 345 | ``` 346 | /api/offices/groupby?_fields=country,city&_sort=city,-country 347 | ``` 348 | eg: SELECT country,city,count(*) FROM offices GROUP BY country,city ORDER BY city ASC, country DESC 349 | 350 | 351 | ## Aggregate functions 352 | [:arrow_heading_up:](#api-overview) 353 | 354 | ``` 355 | http://localhost:3000/api/payments/aggregate?_fields=amount 356 | 357 | response body 358 | [ 359 | { 360 | "min_of_amount": 615.45, 361 | "max_of_amount": 120166.58, 362 | "avg_of_amount": 32431.645531, 363 | "sum_of_amount": 8853839.23, 364 | "stddev_of_amount": 20958.625377426568, 365 | "variance_of_amount": 439263977.71130896 366 | } 367 | ] 368 | ``` 369 | 370 | eg: retrieves all numeric aggregate of a column in a table 371 | 372 | ``` 373 | http://localhost:3000/api/orderDetails/aggregate?_fields=priceEach,quantityOrdered 374 | 375 | response body 376 | [ 377 | { 378 | "min_of_priceEach": 26.55, 379 | "max_of_priceEach": 214.3, 380 | "avg_of_priceEach": 90.769499, 381 | "sum_of_priceEach": 271945.42, 382 | "stddev_of_priceEach": 36.576811252187795, 383 | "variance_of_priceEach": 1337.8631213781719, 384 | "min_of_quantityOrdered": 6, 385 | "max_of_quantityOrdered": 97, 386 | "avg_of_quantityOrdered": 35.219, 387 | "sum_of_quantityOrdered": 105516, 388 | "stddev_of_quantityOrdered": 9.832243813502942, 389 | "variance_of_quantityOrdered": 96.67301840816688 390 | } 391 | ] 392 | ``` 393 | 394 | eg: retrieves numeric aggregate can be done for multiple columns too 395 | 396 | ## Union of multiple group by statements 397 | [:arrow_heading_up:](#api-overview) 398 | 399 | :fire::fire:**[ HOTNESS ALERT ]** 400 | 401 | Group by multiple columns in one API call using _fields query params - comes really handy 402 | 403 | ``` 404 | http://localhost:3000/api/employees/ugroupby?_fields=jobTitle,reportsTo 405 | 406 | response body 407 | { 408 | "jobTitle":[ 409 | { 410 | "Sales Rep":17 411 | }, 412 | { 413 | "President":1 414 | }, 415 | { 416 | "Sale Manager (EMEA)":1 417 | }, 418 | { 419 | "Sales Manager (APAC)":1 420 | }, 421 | { 422 | "Sales Manager (NA)":1 423 | }, 424 | { 425 | "VP Marketing":1 426 | }, 427 | { 428 | "VP Sales":1 429 | } 430 | ], 431 | "reportsTo":[ 432 | { 433 | "1002":2 434 | }, 435 | { 436 | "1056":4 437 | }, 438 | { 439 | "1088":3 440 | }, 441 | { 442 | "1102":6 443 | }, 444 | { 445 | "1143":6 446 | }, 447 | { 448 | "1621":1 449 | } 450 | { 451 | "":1 452 | }, 453 | ] 454 | } 455 | ``` 456 | 457 | 458 | ## Chart 459 | [:arrow_heading_up:](#api-overview) 460 | 461 | :fire::fire::fire::fire::fire::fire: **[ HOTNESS ALERT ]** 462 | 463 | Chart API returns distribution of a numeric column in a table 464 | 465 | It comes in **SEVEN** powerful flavours 466 | 467 | 1. Chart : With min, max, step in query params :fire::fire: 468 | [:arrow_heading_up:](#api-overview) 469 | 470 | This API returns the number of rows where amount is between (0,25000), (25001,50000) ... 471 | 472 | ``` 473 | /api/payments/chart?_fields=amount&min=0&max=131000&step=25000 474 | 475 | Response 476 | 477 | [ 478 | { 479 | "amount": "0 to 25000", 480 | "_count": 107 481 | }, 482 | { 483 | "amount": "25001 to 50000", 484 | "_count": 124 485 | }, 486 | { 487 | "amount": "50001 to 75000", 488 | "_count": 30 489 | }, 490 | { 491 | "amount": "75001 to 100000", 492 | "_count": 7 493 | }, 494 | { 495 | "amount": "100001 to 125000", 496 | "_count": 5 497 | }, 498 | { 499 | "amount": "125001 to 150000", 500 | "_count": 0 501 | } 502 | ] 503 | 504 | ``` 505 | 506 | 2. Chart : With step array in params :fire::fire: 507 | [:arrow_heading_up:](#api-overview) 508 | 509 | This API returns distribution between the step array specified 510 | 511 | ``` 512 | /api/payments/chart?_fields=amount&steparray=0,10000,20000,70000,140000 513 | 514 | Response 515 | 516 | [ 517 | { 518 | "amount": "0 to 10000", 519 | "_count": 42 520 | }, 521 | { 522 | "amount": "10001 to 20000", 523 | "_count": 36 524 | }, 525 | { 526 | "amount": "20001 to 70000", 527 | "_count": 183 528 | }, 529 | { 530 | "amount": "70001 to 140000", 531 | "_count": 12 532 | } 533 | ] 534 | 535 | 536 | ``` 537 | 538 | 3. Chart : With step pairs in params :fire::fire: 539 | [:arrow_heading_up:](#api-overview) 540 | 541 | This API returns distribution between each step pair 542 | 543 | ``` 544 | /api/payments/chart?_fields=amount&steppair=0,50000,40000,100000 545 | 546 | Response 547 | 548 | [ 549 | {"amount":"0 to 50000","_count":231}, 550 | {"amount":"40000 to 100000","_count":80} 551 | ] 552 | 553 | 554 | ``` 555 | 556 | 4. Chart : with no params :fire::fire: 557 | [:arrow_heading_up:](#api-overview) 558 | 559 | This API figures out even distribution of a numeric column in table and returns the data 560 | 561 | ``` 562 | /api/payments/chart?_fields=amount 563 | 564 | Response 565 | [ 566 | { 567 | "amount": "-9860 to 11100", 568 | "_count": 45 569 | }, 570 | { 571 | "amount": "11101 to 32060", 572 | "_count": 91 573 | }, 574 | { 575 | "amount": "32061 to 53020", 576 | "_count": 109 577 | }, 578 | { 579 | "amount": "53021 to 73980", 580 | "_count": 16 581 | }, 582 | { 583 | "amount": "73981 to 94940", 584 | "_count": 7 585 | }, 586 | { 587 | "amount": "94941 to 115900", 588 | "_count": 3 589 | }, 590 | { 591 | "amount": "115901 to 130650", 592 | "_count": 2 593 | } 594 | ] 595 | 596 | ``` 597 | 598 | 5. Chart : range, min, max, step in query params :fire::fire: 599 | [:arrow_heading_up:](#api-overview) 600 | 601 | This API returns the number of rows where amount is between (0,25000), (0,50000) ... (0,maxValue) 602 | 603 | Number of records for amount is counted from min value to extended *Range* instead of incremental steps 604 | 605 | ``` 606 | /api/payments/chart?_fields=amount&min=0&max=131000&step=25000&range=1 607 | 608 | Response 609 | 610 | [ 611 | { 612 | "amount": "0 to 25000", 613 | "_count": 107 614 | }, 615 | { 616 | "amount": "0 to 50000", 617 | "_count": 231 618 | }, 619 | { 620 | "amount": "0 to 75000", 621 | "_count": 261 622 | }, 623 | { 624 | "amount": "0 to 100000", 625 | "_count": 268 626 | }, 627 | { 628 | "amount": "0 to 125000", 629 | "_count": 273 630 | } 631 | ] 632 | 633 | ``` 634 | 635 | 6. Range can be specified with step array like below 636 | 637 | ``` 638 | /api/payments/chart?_fields=amount&steparray=0,10000,20000,70000,140000&range=1 639 | 640 | [ 641 | { 642 | "amount": "0 to 10000", 643 | "_count": 42 644 | }, 645 | { 646 | "amount": "0 to 20000", 647 | "_count": 78 648 | }, 649 | { 650 | "amount": "0 to 70000", 651 | "_count": 261 652 | }, 653 | { 654 | "amount": "0 to 140000", 655 | "_count": 273 656 | } 657 | ] 658 | ``` 659 | 660 | 7. Range can be specified without any step params like below 661 | 662 | ``` 663 | /api/payments/chart?_fields=amount&range=1 664 | 665 | [ 666 | { 667 | "amount": "-9860 to 11100", 668 | "_count": 45 669 | }, 670 | { 671 | "amount": "-9860 to 32060", 672 | "_count": 136 673 | }, 674 | ... 675 | 676 | ] 677 | 678 | ``` 679 | 680 | Please Note: 681 | _fields in Chart API can only take numeric column as its argument. 682 | 683 | ## Autochart 684 | 685 | Identifies numeric columns in a table which are not any sort of key and applies chart API as before - 686 | feels like magic when there are multiple numeric columns in table while hacking/prototyping and you invoke this API. 687 | 688 | ``` 689 | http://localhost:3000/api/payments/autochart 690 | 691 | [ 692 | { 693 | "column": "amount", 694 | "chart": [ 695 | { 696 | "amount": "-9860 to 11100", 697 | "_count": 45 698 | }, 699 | { 700 | "amount": "11101 to 32060", 701 | "_count": 91 702 | }, 703 | { 704 | "amount": "32061 to 53020", 705 | "_count": 109 706 | }, 707 | { 708 | "amount": "53021 to 73980", 709 | "_count": 16 710 | }, 711 | { 712 | "amount": "73981 to 94940", 713 | "_count": 7 714 | }, 715 | { 716 | "amount": "94941 to 115900", 717 | "_count": 3 718 | }, 719 | { 720 | "amount": "115901 to 130650", 721 | "_count": 2 722 | } 723 | ] 724 | } 725 | ] 726 | ``` 727 | 728 | ## XJOIN 729 | 730 | ### Xjoin query params and values: 731 | 732 | ``` 733 | _join : List of tableNames alternated by type of join to be made (_j, _ij,_ lj, _rj) 734 | alias.tableName : TableName as alias 735 | _j : Join [ _j => join, _ij => ij, _lj => left join , _rj => right join) 736 | _onNumber : Number 'n' indicates condition to be applied for 'n'th join between (n-1) and 'n'th table in list 737 | ``` 738 | 739 | #### Simple example of two table join: 740 | 741 | Sql join query: 742 | 743 | ```sql 744 | 745 | SELECT pl.field1, pr.field2 746 | FROM productlines as pl 747 | JOIN products as pr 748 | ON pl.productline = pr.productline 749 | 750 | ``` 751 | 752 | Equivalent xjoin query API: 753 | ``` 754 | /api/xjoin?_join=pl.productlines,_j,pr.products&_on1=(pl.productline,eq,pr.productline)&_fields=pl.field1,pr.field2 755 | ``` 756 | 757 | #### Multiple tables join 758 | 759 | Sql join query: 760 | ```sql 761 | SELECT pl.field1, pr.field2, ord.field3 762 | FROM productlines as pl 763 | JOIN products as pr 764 | ON pl.productline = pr.productline 765 | JOIN orderdetails as ord 766 | ON pr.productcode = ord.productcode 767 | ``` 768 | 769 | Equivalent xjoin query API: 770 | 771 | ``` 772 | /api/xjoin?_join=pl.productlines,_j,pr.products,_j,ord.orderDetails&_on1=(pl.productline,eq,pr.productline)&_on2=(pr.productcode,eq,ord.productcode)&_fields=pl.field1,pr.field2,ord.field3 773 | 774 | ``` 775 | 776 | **Explanation:** 777 | > pl.productlines => productlines as pl 778 | 779 | > _j => join 780 | 781 | > pr.products => products as pl 782 | 783 | > _on1 => join condition between productlines and products => (pl.productline,eq,pr.productline) 784 | 785 | > _on2 => join condition between products and orderdetails => (pr.productcode,eq,ord.productcode) 786 | 787 | Example to use : _fields, _where, _p, _size in query params 788 | 789 | ``` 790 | /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~) 791 | ``` 792 | 793 | Response: 794 | 795 | ``` 796 | [{"pl_productline":"Classic Cars","pr_productName":"1972 Alfa Romeo GTA"}] 797 | ``` 798 | 799 | Please note : 800 | Xjoin response has aliases for fields like below aliasTableName + '_' + columnName. 801 | eg: pl.productline in _fields query params - returns as pl_productline in response. 802 | 803 | ## Run dynamic queries 804 | [:arrow_heading_up:](#api-overview) 805 | 806 | Dynamic queries on a database can be run by POST method to URL localhost:3000/dynamic 807 | 808 | This is enabled **ONLY when using local mysql server** i.e -h localhost or -h 127.0.0.1 option. 809 | 810 | Post body takes two fields : query and params. 811 | 812 | >query: SQL query or SQL prepared query (ones with ?? and ?) 813 | 814 | >params : parameters for SQL prepared query 815 | ``` 816 | POST /dynamic 817 | 818 | { 819 | "query": "select * from ?? limit 1,20", 820 | "params": ["customers"] 821 | } 822 | ``` 823 | 824 | POST /dynamic URL can have any suffix to it - which can be helpful in prototyping 825 | 826 | eg: 827 | 828 | ``` 829 | POST /dynamic/weeklyReport 830 | ``` 831 | 832 | ``` 833 | POST /dynamic/user/update 834 | ``` 835 | 836 | 837 | ## Upload single file 838 | [:arrow_heading_up:](#api-overview) 839 | 840 | ``` 841 | POST /upload 842 | ``` 843 | Do POST operation on /upload url with multiform 'field' assigned to local file to be uploaded 844 | 845 | eg: curl --form file=@/Users/me/Desktop/a.png http://localhost:3000/upload 846 | 847 | returns uploaded file name else 'upload failed' 848 | 849 | (Note: POSTMAN has issues with file uploading hence examples with curl) 850 | 851 | 852 | ## Upload multiple files 853 | [:arrow_heading_up:](#api-overview) 854 | 855 | ``` 856 | POST /uploads 857 | ``` 858 | Do POST operation on /uploads url with multiform 'fields' assigned to local files to be uploaded 859 | 860 | > Notice 's' near /api/upload**s** and file**s** in below example 861 | 862 | eg: curl --form files=@/Users/me/Desktop/a.png --form files=@/Users/me/Desktop/b.png http://localhost:3000/uploads 863 | 864 | returns uploaded file names as string 865 | 866 | ## Download file 867 | [:arrow_heading_up:](#api-overview) 868 | 869 | http://localhost:3000/download?name=fileName 870 | 871 | > For upload and download of files -> you can specify storage folder using -s option 872 | > Upload and download apis are available only with local mysql server 873 | 874 | ## Health 875 | [:arrow_heading_up:](#api-overview) 876 | 877 | http://localhost:3000/_health 878 | 879 | ``` 880 | {"process_uptime":3.858,"mysql_uptime":"2595"} 881 | ``` 882 | 883 | Shows up time of Xmysql process and mysql server 884 | 885 | 886 | http://localhost:3000/_health?details=1 887 | 888 | ``` 889 | {"process_uptime":1.151,"mysql_uptime":"2798", 890 | "os_total_memory":17179869184, 891 | "os_free_memory":2516357120, 892 | "os_load_average":[2.29931640625,2.1845703125,2.13818359375], 893 | "v8_heap_statistics":{"total_heap_size":24735744, 894 | "total_heap_size_executable":5242880, 895 | "total_physical_size":23521048, 896 | "total_available_size":1475503064, 897 | "used_heap_size":18149064, 898 | "heap_size_limit":1501560832, 899 | "malloced_memory":8192, 900 | "peak_malloced_memory":11065664, 901 | "does_zap_garbage":0}} 902 | ``` 903 | 904 | Provides more details on process. 905 | 906 | Infact passing any query param gives detailed health output: example below 907 | 908 | http://localhost:3000/_health?voila 909 | ``` 910 | {"process_uptime":107.793,"mysql_uptime":"2905","os_total_memory":17179869184,"os_free_memory":2573848576,"os_load_average":[2.052734375,2.12890625,2.11767578125],"v8_heap_statistics":{"total_heap_size":24735744,"total_heap_size_executable":5242880,"total_physical_size":23735016,"total_available_size":1475411128,"used_heap_size":18454968,"heap_size_limit":1501560832,"malloced_memory":8192,"peak_malloced_memory":11065664,"does_zap_garbage":0}} 911 | ``` 912 | 913 | ## Version 914 | [:arrow_heading_up:](#api-overview) 915 | 916 | http://localhost:3000/_version 917 | 918 | ``` 919 | {"Xmysql":"0.4.1","mysql":"5.7.15","node":"8.2.1"} 920 | ``` 921 | 922 | ## When to use ? 923 | [:arrow_heading_up:](#api-overview) 924 | 925 | * You need just REST APIs for (ANY) MySql database at blink of an eye (literally). 926 | * You are learning new frontend frameworks and need REST APIs for your MySql database. 927 | * You are working on a demo, hacks etc 928 | 929 | ## When NOT to use ? 930 | [:arrow_heading_up:](#api-overview) 931 | 932 | * If you are in need of a full blown MVC framework, ACL, Validations, Authorisation etc - its early days please watch/star this repo to keep a tab on progress. 933 | 934 | 935 | ### Command line options 936 | [:arrow_heading_up:](#api-overview) 937 | 938 | ``` 939 | Options: 940 | 941 | -V, --version Output the version number 942 | -h, --host Hostname of database -> localhost by default 943 | -u, --user Username of database -> root by default 944 | -p, --password Password of database -> empty by default 945 | -d, --database database schema name 946 | -r, --ipAddress IP interface of your server / localhost by default 947 | -n, --portNumber Port number for app -> 3000 by default 948 | -o, --port Port number of mysql -> 3306 by default 949 | -a, --apiPrefix Api url prefix -> /api/ by default 950 | -s, --storageFolder Storage folder -> current working dir by default (available only with local) 951 | -i, --ignoreTables Comma separated table names to ignore 952 | -c, --useCpuCores Specify number of cpu cores to use / 1 by default / 0 to use max 953 | -y, --readOnly readonly apis -> false by default 954 | -h, --help Output usage information 955 | 956 | 957 | Examples: 958 | 959 | $ xmysql -u username -p password -d databaseSchema 960 | ``` 961 | 962 |
963 |

Boost Your Hacker Karma By Sharing :

964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 |
975 | 976 | 977 | 978 | 979 | # Docker 980 | [:arrow_heading_up:](#features) 981 | 982 | Simply run with `docker run -p 3000:80 -d markuman/xmysql:0.4.2` 983 | 984 | The best way for testing is to run mysql in a docker container too and create a docker network, so that `xmysql` can access the `mysql` container with a name from docker network. 985 | 986 | 1. Create network 987 | * `docker network create mynet` 988 | 2. Start mysql with docker name `some-mysql` and bind to docker network `mynet` 989 | * `docker run --name some-mysql -p 3306:3306 --net mynet -e MYSQL_ROOT_PASSWORD=password -d markuman/mysql` 990 | 3. run xmysql and set env variable for `some-mysql` from step 2 991 | * `docker run -p 3000:80 -d -e DATABASE_HOST=some-mysql --net mynet markuman/xmysql` 992 | 993 | You can also pass the environment variables to a file and use them as an option with docker like `docker run --env-file ./env.list -p 3000:80 --net mynet -d markuman/xmysql` 994 | 995 | environment variables which can be used: 996 | 997 | ``` 998 | ENV DATABASE_HOST 127.0.0.1 999 | ENV DATABASE_USER root 1000 | ENV DATABASE_PASSWORD password 1001 | ENV DATABASE_NAME sakila 1002 | ``` 1003 | 1004 | Furthermore, the docker container of xmysql is listen on port 80. You can than request it just with `http://xmysql/api/` in other services running in the same docker network. 1005 | 1006 | ## Debugging xmysql in docker. 1007 | 1008 | Given you've deployed your xmysql docker container like 1009 | 1010 | ```shell 1011 | docker run -d \ 1012 | --network local_dev \ 1013 | --name xmysql \ 1014 | -p 3000:80 \ 1015 | -e DATABASE_HOST=mysql_host \ 1016 | -e DATABASE_USER=root \ 1017 | -e DATABASE_PASSWORD=password \ 1018 | -e DATABASE_NAME=sys \ 1019 | markuman/xmysql:0.4.2 1020 | ``` 1021 | 1022 | but the response is just 1023 | 1024 | ```json 1025 | ["http://127.0.0.1:3000/api/tables","http://127.0.0.1:3000/api/xjoin"] 1026 | ``` 1027 | 1028 | then obviously the connection to your mysql database failed. 1029 | 1030 | 1. attache to the xmysql image 1031 | * `docker exec -ti xmysql` 1032 | 2. install mysql cli client 1033 | * `apk --update --no-cache add mysql-client` 1034 | 3. try to access your mysql database 1035 | * `mysql-client -h mysql_host` 1036 | 4. profit from the `mysql-client` error output and improve the environment variables for mysql 1037 | 1038 | # Nginx Reverse Proxy Config with Docker 1039 | [:arrow_heading_up:](#features) 1040 | 1041 | This is a config example when you use nginx as reverse proxy 1042 | 1043 | ``` 1044 | events { 1045 | worker_connections 1024; 1046 | 1047 | } 1048 | http { 1049 | server { 1050 | server_name 127.0.0.1; 1051 | listen 80 ; 1052 | location / { 1053 | rewrite ^/(.*) /$1 break; 1054 | proxy_redirect off; 1055 | proxy_set_header Host $host; 1056 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 1057 | proxy_pass http://127.0.0.1:3000; 1058 | } 1059 | } 1060 | } 1061 | ``` 1062 | 1063 | e.g. 1064 | 1065 | 0. create a docker network `docker network create local_dev` 1066 | 1. start a mysql server `docker run -d --name mysql -p 3306:3306 --network local_dev -e MYSQL_ROOT_PASSWORD=password mysql` 1067 | 2. start xmysql `docker run -d --network local_dev --name xmyxql -e DATABASE_NAME=sys -e DATABASE_HOST=mysql -p 3000:80 markuman/xmysql:0.4.2` 1068 | 3. start nginx on host system with the config above `sudo nginx -g 'daemon off;' -c /tmp/nginx.conf` 1069 | 4. profit `curl http://127.0.0.1/api/host_summary_by_file_io_type/describe` 1070 | 1071 | When you start your nginx proxy in a docker container too, use as `proxy_pass` the `--name` value of xmysql. E.g. `proxy_pass http://xmysql` (remember, xmysql runs in it's docker container already on port 80). 1072 | 1073 | 1074 | # Tests : setup on local machine 1075 | [:arrow_heading_up:](#api-overview) 1076 | ``` 1077 | docker-compose run test 1078 | ``` 1079 | * Requires `docker-compose` to be installed on your machine. 1080 | -------------------------------------------------------------------------------- /assets/doubon copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/doubon copy.png -------------------------------------------------------------------------------- /assets/doubon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/doubon.png -------------------------------------------------------------------------------- /assets/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/facebook.png -------------------------------------------------------------------------------- /assets/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/google.png -------------------------------------------------------------------------------- /assets/hn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/hn.jpg -------------------------------------------------------------------------------- /assets/hn1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/hn1.png -------------------------------------------------------------------------------- /assets/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/linkedin.png -------------------------------------------------------------------------------- /assets/log.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/log.gif -------------------------------------------------------------------------------- /assets/out.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/out.gif -------------------------------------------------------------------------------- /assets/pinterest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/pinterest.png -------------------------------------------------------------------------------- /assets/reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/reddit.png -------------------------------------------------------------------------------- /assets/renren.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/renren.png -------------------------------------------------------------------------------- /assets/rick-and-morty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/rick-and-morty.gif -------------------------------------------------------------------------------- /assets/stumbleupon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/stumbleupon.png -------------------------------------------------------------------------------- /assets/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/twitter.png -------------------------------------------------------------------------------- /assets/vk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/vk.png -------------------------------------------------------------------------------- /assets/weibo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/weibo.png -------------------------------------------------------------------------------- /assets/wykop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o1lab/xmysql/8c6b00ee22860230975e43ab705d015d2235e308/assets/wykop.png -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const morgan = require("morgan"); 4 | const bodyParser = require("body-parser"); 5 | const express = require("express"); 6 | const sqlConfig = require("commander"); 7 | const mysql = require("mysql"); 8 | const cors = require("cors"); 9 | const dataHelp = require("../lib/util/data.helper.js"); 10 | const Xapi = require("../lib/xapi.js"); 11 | const cmdargs = require("../lib/util/cmd.helper.js"); 12 | const cluster = require("cluster"); 13 | const numCPUs = require("os").cpus().length; 14 | 15 | 16 | function startXmysql(sqlConfig) { 17 | /**************** START : setup express ****************/ 18 | let app = express(); 19 | app.use(morgan("tiny")); 20 | app.use(cors()); 21 | app.use(bodyParser.json()); 22 | app.use( 23 | bodyParser.urlencoded({ 24 | extended: true 25 | }) 26 | ); 27 | /**************** END : setup express ****************/ 28 | 29 | /**************** START : setup mysql ****************/ 30 | let mysqlPool = mysql.createPool(sqlConfig); 31 | /**************** END : setup mysql ****************/ 32 | 33 | /**************** START : setup Xapi ****************/ 34 | console.log(""); 35 | console.log(""); 36 | console.log(""); 37 | console.log(" Generating REST APIs at the speed of your thought.. "); 38 | console.log(""); 39 | 40 | let t = process.hrtime(); 41 | let moreApis = new Xapi(sqlConfig, mysqlPool, app); 42 | 43 | moreApis.init((err, results) => { 44 | app.listen(sqlConfig.portNumber, sqlConfig.ipAddress); 45 | var t1 = process.hrtime(t); 46 | var t2 = t1[0] + t1[1] / 1000000000; 47 | 48 | console.log( 49 | " Xmysql took : %d seconds", 50 | dataHelp.round(t2, 1) 51 | ); 52 | console.log( 53 | " API's base URL : " + 54 | "localhost:" + 55 | sqlConfig.portNumber 56 | ); 57 | console.log(" "); 58 | console.log( 59 | " - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - " 60 | ); 61 | }); 62 | /**************** END : setup Xapi ****************/ 63 | } 64 | 65 | function start(sqlConfig) { 66 | //handle cmd line arguments 67 | cmdargs.handle(sqlConfig); 68 | 69 | if (cluster.isMaster && sqlConfig.useCpuCores > 1) { 70 | console.log(`Master ${process.pid} is running`); 71 | 72 | for (let i = 0; i < numCPUs && i < sqlConfig.useCpuCores; i++) { 73 | console.log(`Forking process number ${i}...`); 74 | cluster.fork(); 75 | } 76 | 77 | cluster.on("exit", function(worker, code, signal) { 78 | console.log( 79 | "Worker " + 80 | worker.process.pid + 81 | " died with code: " + 82 | code + 83 | ", and signal: " + 84 | signal 85 | ); 86 | console.log("Starting a new worker"); 87 | cluster.fork(); 88 | }); 89 | } else { 90 | startXmysql(sqlConfig); 91 | } 92 | } 93 | 94 | start(sqlConfig); 95 | -------------------------------------------------------------------------------- /dev.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM frolvlad/alpine-python2 2 | 3 | RUN apk --update --no-cache add \ 4 | g++ \ 5 | make \ 6 | nodejs \ 7 | nodejs-npm \ 8 | paxctl \ 9 | && paxctl -cm $(which node) 10 | 11 | WORKDIR /usr/src/app 12 | 13 | COPY package.json . 14 | RUN npm install 15 | 16 | COPY . . 17 | 18 | ENTRYPOINT ["./docker-entrypoint.sh"] 19 | 20 | CMD ["sh", "-c", "node index.js -h $DATABASE_HOST -p $DATABASE_PASSWORD -d $DATABASE_NAME -u $DATABASE_USER -n 80 -r 0.0.0.0"] 21 | 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | 4 | services: 5 | db: 6 | image: mysql:5.7 7 | restart: always 8 | command: --init-file /docker-entrypoint-initdb.d/sample.sql 9 | environment: 10 | MYSQL_ROOT_PASSWORD: 'pass' 11 | MYSQL_USER: 'root' 12 | MYSQL_PASSWORD: 'pass' 13 | MYSQL_DATABASE: 'classicmodels' 14 | volumes: 15 | - ./tests/docker-sample.sql:/docker-entrypoint-initdb.d/sample.sql 16 | - db-data:/var/lib/mysql 17 | ports: 18 | - "3306:3306" 19 | 20 | db-api: &db-api 21 | build: 22 | context: ./ 23 | dockerfile: ./dev.Dockerfile 24 | environment: 25 | DATABASE_HOST: 'db' 26 | DATABASE_USER: 'root' 27 | DATABASE_PASSWORD: 'pass' 28 | DATABASE_NAME: 'classicmodels' 29 | ports: 30 | - "3002:80" 31 | depends_on: 32 | - db 33 | 34 | test: 35 | <<: *db-api 36 | command: sh -c "npm test" 37 | volumes: 38 | - ./tests:/usr/src/app/tests 39 | - ./lib:/usr/src/app/lib 40 | 41 | volumes: 42 | db-data: 43 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # https://stackoverflow.com/questions/25503412/how-do-i-know-when-my-docker-mysql-container-is-up-and-mysql-is-ready-for-taking 3 | 4 | 5 | set -e 6 | 7 | until nc -z -v -w30 $DATABASE_HOST 3306 8 | do 9 | echo "Waiting for database connection..." 10 | # wait for 5 seconds before check again 11 | sleep 5 12 | done 13 | 14 | echo "Mysql is up - executing command" 15 | 16 | exec "$@" 17 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 2 | 3 | 4 | RUN apk --update --no-cache add \ 5 | nodejs \ 6 | nodejs-npm 7 | 8 | # Bug fix for segfault ( Convert PT_GNU_STACK program header into PT_PAX_FLAGS ) 9 | RUN apk --update --no-cache add paxctl \ 10 | && paxctl -cm $(which node) 11 | 12 | RUN mkdir -p /usr/src/{app,bin,lib} 13 | WORKDIR /usr/src/app 14 | 15 | # only install production deps to keep image small 16 | COPY package.json /usr/src/app 17 | RUN npm install --production 18 | 19 | RUN apk del nodejs-npm 20 | 21 | COPY index.js /usr/src/app 22 | COPY bin/ /usr/src/app/bin 23 | COPY lib/ /usr/src/app/lib 24 | COPY docker-entrypoint.sh /docker-entrypoint.sh 25 | 26 | # env 27 | ENV DATABASE_HOST 127.0.0.1 28 | ENV DATABASE_USER root 29 | ENV DATABASE_PASSWORD password 30 | ENV DATABASE_NAME sakila 31 | 32 | EXPOSE 80 33 | ENTRYPOINT ["/docker-entrypoint.sh"] 34 | -------------------------------------------------------------------------------- /examples/aws-lambda/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * I was able to get xmysql to work on AWS lambda. This is just some docs on the process to help others. 3 | * AWS setup notes: 4 | * * The deploy uses serverless: https://serverless.com/framework/docs/getting-started/ 5 | * * The lambda function is accessed through API Gateway and access the RDS (mysql) database 6 | * ** Make sure you deploy your API Gateway setup otherwise it won't be accessible. 7 | * * The security setup: 8 | * ** You need to put your lambda in a VPC 9 | * ** The lambda role needs access to the VPC 10 | * ** The RDS server has to be inside the VPC as well 11 | * 12 | * Performance: 13 | * * Requests to the API Gateway resolve in ~ 90ms 14 | * 15 | * Extra packages needed: 16 | * * serverless-http 17 | * 18 | * More info on deploying this to lambda: 19 | * * https://serverless.com/blog/serverless-express-rest-api/ 20 | */ 21 | 22 | 23 | "use strict"; 24 | Object.defineProperty(exports, "__esModule", { value: true }); 25 | var bodyParser = require("body-parser"); 26 | var express = require("express"); 27 | var serverless = require("serverless-http"); 28 | var cors = require("cors"); 29 | var mysql = require("mysql"); 30 | var Xapi = require("./node_modules/xmysql/lib/xapi.js"); 31 | var morgan = require("morgan"); 32 | var app = express(); 33 | 34 | var onXapiInitialized = new Promise(function(resolve, reject) { 35 | try { 36 | // /**************** START : setup express ****************/ 37 | app.use(morgan("tiny")); 38 | app.use(cors()); 39 | app.use(bodyParser.json()); 40 | app.use( 41 | bodyParser.urlencoded({ 42 | extended: true 43 | }) 44 | ); 45 | // /**************** END : setup express ****************/ 46 | 47 | app.use(function(req, res, next) { 48 | // You can add authentication here 49 | console.log("Received request for: " + req.url, req); 50 | next(); 51 | }); 52 | 53 | var mysqlConfig = { 54 | host: config.mysql.host, 55 | port: 3306, 56 | database: config.mysql.database, 57 | user: config.mysql.user, 58 | password: config.mysql.password, 59 | apiPrefix: "/", 60 | ipAddress: "localhost", 61 | portNumber: 3000, 62 | ignoreTables: [], 63 | storageFolder: __dirname 64 | }; 65 | 66 | var mysqlPool = mysql.createPool(mysqlConfig); 67 | var xapi = new Xapi(mysqlConfig, mysqlPool, app); 68 | xapi.init(function(err, results) { 69 | app.listen(3000); 70 | resolve(); 71 | }); 72 | } catch (err) { 73 | reject(err); 74 | } 75 | }); 76 | 77 | function handler(event, context, callback) { 78 | onXapiInitialized.then(function() { 79 | serverless(app)(event, context, callback); 80 | }); 81 | } 82 | 83 | exports.handler = handler; 84 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.xapi = require('./lib/xapi.js') 2 | -------------------------------------------------------------------------------- /lib/util/cmd.helper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const program = require("commander"); 3 | const colors = require("colors"); 4 | const maxCpus = require("os").cpus().length; 5 | 6 | program.on("--help", () => { 7 | console.log(""); 8 | console.log(" Examples:".blue); 9 | console.log(""); 10 | console.log(" $ xmysql -u username -p password -d databaseSchema".blue); 11 | console.log(""); 12 | }); 13 | 14 | program 15 | .version(module.exports.version) 16 | .option("-h, --host ", "hostname of database / localhost by default") 17 | .option("-u, --user ", "username of database / root by default") 18 | .option("-p, --password ", "password of database / empty by default") 19 | .option("-d, --database ", "database schema name") 20 | .option( 21 | "-r, --ipAddress ", 22 | "IP interface of your server / localhost by default" 23 | ) 24 | .option("-n, --portNumber ", "port number for app / 3000 by default") 25 | .option("-o, --port ", "port number for mysql / 3306 by default") 26 | .option("-S, --socketPath ", "unix socket path / not used by default") 27 | .option( 28 | "-s, --storageFolder ", 29 | "storage folder / current working dir by default / available only with local" 30 | ) 31 | .option("-i, --ignoreTables ", "comma separated table names to ignore") 32 | .option("-a, --apiPrefix ", 'api url prefix / "/api/" by default') 33 | .option("-y, --readOnly", "readonly apis / false by default") 34 | .option( 35 | "-c, --useCpuCores ", 36 | "use number of CPU cores (using cluster) / 1 by default" 37 | ) 38 | .parse(process.argv); 39 | 40 | function paintHelp(txt) { 41 | return colors.magenta(txt); //display the help text in a color 42 | } 43 | 44 | function processInvalidArguments(program) { 45 | let err = ""; 46 | 47 | if (!program.password) { 48 | err += "Error: password for database is missing\n"; 49 | } 50 | 51 | if (!program.database) { 52 | err += "Error: database name is missing\n"; 53 | } 54 | 55 | if (err !== "") { 56 | program.outputHelp(paintHelp); 57 | console.log(err.red); 58 | } 59 | } 60 | 61 | exports.handle = program => { 62 | /**************** START : default values ****************/ 63 | program.ipAddress = program.ipAddress || "localhost"; 64 | program.portNumber = program.portNumber || 3000; 65 | program.port = program.port || 3306; 66 | program.user = program.user || "root"; 67 | program.password = program.password || ""; 68 | program.host = program.host || "localhost"; 69 | program.socketPath = program.socketPath || ""; 70 | program.storageFolder = program.storageFolder || process.cwd(); 71 | program.apiPrefix = program.apiPrefix || "/api/"; 72 | program.readOnly = program.readOnly || false; 73 | program.useCpuCores = program.useCpuCores || 1; 74 | 75 | if (program.useCpuCores === "0") { 76 | program.useCpuCores = maxCpus; 77 | } 78 | 79 | if (program.ignoreTables) { 80 | let ignoreTables = program.ignoreTables.split(","); 81 | program.ignoreTables = {}; 82 | for (var i = 0; i < ignoreTables.length; ++i) { 83 | program.ignoreTables[ignoreTables[i]] = ignoreTables[i]; 84 | } 85 | } else { 86 | program.ignoreTables = {}; 87 | } 88 | 89 | program.connectionLimit = 10; 90 | 91 | if ( 92 | program.host === "localhost" || 93 | program.host === "127.0.0.1" || 94 | program.host === "::1" 95 | ) { 96 | program.dynamic = 1; 97 | } 98 | // console.log(program); 99 | /**************** END : default values ****************/ 100 | 101 | if (program.database && program.host && program.user) { 102 | //console.log('Starting server at:', 'http://' + program.host + ':' + program.portNumber) 103 | } else { 104 | processInvalidArguments(program); 105 | process.exit(1); 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /lib/util/data.helper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.findOrInsertObjectArrayByKey = (obj, key, array) => { 4 | let found = 0; 5 | let i = 0; 6 | 7 | for (i = 0; i < array.length; ++i) { 8 | if (key in array[i]) { 9 | if (obj[key] === array[i][key]) { 10 | found = 1; 11 | break; 12 | } 13 | } 14 | } 15 | 16 | if (!found) { 17 | array.push(obj); 18 | } 19 | 20 | return array[i]; 21 | }; 22 | 23 | exports.findObjectInArrayByKey = (key, value, objArray) => { 24 | for (let i = 0; i < objArray.length; ++i) { 25 | if (objArray[i][key] === value) { 26 | return objArray[i]; 27 | } 28 | } 29 | 30 | return null; 31 | }; 32 | 33 | exports.round = function(number, precision) { 34 | var factor = Math.pow(10, precision); 35 | var tempNumber = number * factor; 36 | var roundedTempNumber = Math.round(tempNumber); 37 | return roundedTempNumber / factor; 38 | }; 39 | 40 | exports.numberRound = (number, precision) => { 41 | var factor = Math.pow(10, precision); 42 | var tempNumber = number * factor; 43 | var roundedTempNumber = Math.round(tempNumber); 44 | return roundedTempNumber / factor; 45 | }; 46 | 47 | exports.numberGetLength = number => { 48 | var n = number; 49 | 50 | if (number < 0) { 51 | n = n * -1; 52 | } 53 | 54 | return n.toString().length; 55 | }; 56 | 57 | exports.numberGetFixed = number => { 58 | //console.log(number, typeof number); 59 | return parseInt(number.toFixed()); 60 | }; 61 | 62 | exports.getStepArraySimple = function(min, max, step) { 63 | var arr = []; 64 | for (var i = min; i <= max; i = i + step) { 65 | arr.push(i); 66 | } 67 | 68 | return arr; 69 | }; 70 | 71 | exports.getStepArray = (min, max, stddev) => { 72 | // console.log(' = = = = = = = '); 73 | //console.log('original numbers', min, max, stddev); 74 | 75 | min = this.numberGetFixed(min); 76 | max = this.numberGetFixed(max); 77 | stddev = this.numberGetFixed(stddev); 78 | 79 | // console.log('fixed numbers', min, max, stddev); 80 | 81 | let minMinusHalf = min - stddev / 2; 82 | let maxMinusHalf = max + stddev / 2; 83 | 84 | minMinusHalf = this.numberGetFixed(minMinusHalf); 85 | maxMinusHalf = this.numberGetFixed(maxMinusHalf); 86 | 87 | // console.log('fixed numbers + (min,max)', min, max, stddev, '(', minMinusHalf, ',', maxMinusHalf, ')'); 88 | // console.log('numbers length', 'min', numberGetLength(min), 'max', numberGetLength(max), 'stddev', numberGetLength(stddev)); 89 | 90 | let minLen = this.numberGetLength(minMinusHalf); 91 | let maxLen = this.numberGetLength(maxMinusHalf); 92 | let stddevLen = this.numberGetLength(stddev); 93 | // 94 | // console.log('- - - -'); 95 | // console.log('Range', 'min', numberRound(minMinusHalf, -1)); 96 | // console.log('Range', 'max', numberRound(maxMinusHalf, -1)); 97 | // console.log('Range', 'stddev', numberRound(stddev, -1)); 98 | 99 | if (minLen > 1) minMinusHalf = this.numberRound(minMinusHalf, -1); 100 | 101 | if (maxLen > 2) maxMinusHalf = this.numberRound(maxMinusHalf, -1); 102 | 103 | if (stddevLen !== 1) stddev = this.numberRound(stddev, -1); 104 | 105 | var arr = []; 106 | for (var step = minMinusHalf; step < maxMinusHalf; step = step + stddev) { 107 | arr.push(step); 108 | } 109 | arr.push(maxMinusHalf); 110 | 111 | // console.log(arr); 112 | 113 | return arr; 114 | }; 115 | 116 | exports.getSchemaQuery = function() { 117 | return ( 118 | "select c.table_name, c.column_name, c.ordinal_position,c.column_key,c.is_nullable, c.data_type, c.column_type,c.extra,c.privileges, " + 119 | "c.column_comment,c.column_default,c.data_type,c.character_maximum_length, " + 120 | "k.constraint_name, k.referenced_table_name, k.referenced_column_name, " + 121 | "s.index_name,s.seq_in_index, " + 122 | "v.table_name as isView " + 123 | "from " + 124 | "information_schema.columns as c " + 125 | "left join " + 126 | "information_schema.key_column_usage as k " + 127 | "on " + 128 | "c.column_name=k.column_name and " + 129 | "c.table_schema = k.referenced_table_schema and " + 130 | "c.table_name = k.table_name " + 131 | "left join " + 132 | "information_schema.statistics as s " + 133 | "on " + 134 | "c.column_name = s.column_name and " + 135 | "c.table_schema = s.index_schema and " + 136 | "c.table_name = s.table_name " + 137 | "LEFT JOIN " + 138 | "information_schema.VIEWS as v " + 139 | "ON " + 140 | "c.table_schema = v.table_schema and " + 141 | "c.table_name = v.table_name " + 142 | "where " + 143 | "c.table_schema=? " + 144 | "order by " + 145 | "c.table_name, c.ordinal_position" 146 | ); 147 | }; 148 | 149 | exports.getRoutines = function () { 150 | return 'select routine_name from information_schema.routines' 151 | } 152 | 153 | exports.getChartQuery = function() { 154 | return "select ? as ??, count(*) as _count from ?? where ?? between ? and ? "; 155 | }; 156 | 157 | exports.getDataType = function(colType, typesArr) { 158 | // console.log(colType,typesArr); 159 | for (let i = 0; i < typesArr.length; ++i) { 160 | if (colType.indexOf(typesArr[i]) !== -1) { 161 | return 1; 162 | } 163 | } 164 | return 0; 165 | }; 166 | 167 | exports.getColumnType = function(column) { 168 | let strTypes = [ 169 | "varchar", 170 | "text", 171 | "char", 172 | "tinytext", 173 | "mediumtext", 174 | "longtext", 175 | "blob", 176 | "mediumblob", 177 | "longblob", 178 | "tinyblob", 179 | "binary", 180 | "varbinary" 181 | ]; 182 | let intTypes = ["int", "long", "smallint", "mediumint", "bigint", "tinyint"]; 183 | let flatTypes = ["float", "double", "decimal"]; 184 | let dateTypes = ["date", "datetime", "timestamp", "time", "year"]; 185 | 186 | //console.log(column); 187 | if (this.getDataType(column["data_type"], strTypes)) { 188 | return "string"; 189 | } else if (this.getDataType(column["data_type"], intTypes)) { 190 | return "int"; 191 | } else if (this.getDataType(column["data_type"], flatTypes)) { 192 | return "float"; 193 | } else if (this.getDataType(column["data_type"], dateTypes)) { 194 | return "date"; 195 | } else { 196 | return "string"; 197 | } 198 | }; 199 | 200 | exports.getType = function(colType, typesArr) { 201 | for (let i = 0; i < typesArr.length; ++i) { 202 | // if (typesArr[i].indexOf(colType) !== -1) { 203 | // return 1; 204 | // } 205 | 206 | if (colType.indexOf(typesArr[i]) !== -1) { 207 | return 1; 208 | } 209 | } 210 | return 0; 211 | }; 212 | -------------------------------------------------------------------------------- /lib/util/whereClause.helper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * 5 | * @param input : 1,2,3,4 6 | * @returns obj.query = (?,?,?,?) obj.params = [1,2,3,4] 7 | */ 8 | function prepareInClauseParams(input) { 9 | let inElems = input.split(","); 10 | let obj = {}; 11 | obj.whereQuery = ""; 12 | obj.whereParams = []; 13 | 14 | for (var j = 0; j < inElems.length; ++j) { 15 | if (j === 0) { 16 | //enclose with open parenthesis 17 | obj.whereQuery += "("; 18 | } 19 | if (j) { 20 | obj.whereQuery += ","; 21 | } 22 | // add q mark and push the variable 23 | obj.whereQuery += "?"; 24 | obj.whereParams.push(inElems[j]); 25 | 26 | if (j === inElems.length - 1) { 27 | //enclose with closing parenthesis 28 | obj.whereQuery += ")"; 29 | } 30 | } 31 | 32 | //console.log(obj); 33 | 34 | return obj; 35 | } 36 | 37 | function prepareLikeValue(value) { 38 | //return value.replace("~", "%"); 39 | return value.replace(/~/g, "%"); 40 | } 41 | 42 | function prepareIsValue(value) { 43 | //return value.replace("~", "%"); 44 | if (value === "null") { 45 | return null; 46 | } else if (value === "true") { 47 | return true; 48 | } else if (value === "false") { 49 | return false; 50 | } else { 51 | return null; 52 | } 53 | } 54 | 55 | function prepareBetweenValue(value) { 56 | let values = value.split(","); 57 | let obj = {}; 58 | obj.whereQuery = ""; 59 | obj.whereParams = []; 60 | 61 | if (values.length === 2) { 62 | obj.whereQuery = " ? and ? "; 63 | obj.whereParams.push(values[0]); 64 | obj.whereParams.push(values[1]); 65 | } 66 | 67 | //console.log('prepareBetweenValue', obj); 68 | 69 | return obj; 70 | } 71 | 72 | function replaceWhereParamsToQMarks( 73 | openParentheses, 74 | str, 75 | comparisonOperator, 76 | condType 77 | ) { 78 | let converted = ""; 79 | 80 | if (openParentheses) { 81 | for (var i = 0; i < str.length; ++i) { 82 | if (str[i] === "(") { 83 | converted += "("; 84 | } else { 85 | converted += "??"; 86 | break; 87 | } 88 | } 89 | } else { 90 | if ( 91 | comparisonOperator !== " in " && 92 | comparisonOperator !== " between " && 93 | condType !== " on " 94 | ) { 95 | converted = "?"; 96 | } else if (condType === " on ") { 97 | converted = "??"; 98 | } 99 | 100 | for (var i = str.length - 1; i >= 0; --i) { 101 | if (str[i] === ")") { 102 | converted += ")"; 103 | } else { 104 | break; 105 | } 106 | } 107 | } 108 | 109 | return converted; 110 | } 111 | 112 | function getComparisonOperator(operator) { 113 | switch (operator) { 114 | case "eq": 115 | return "="; 116 | break; 117 | 118 | case "ne": 119 | return "!="; 120 | break; 121 | 122 | case "lt": 123 | return "<"; 124 | break; 125 | 126 | case "lte": 127 | return "<="; 128 | break; 129 | 130 | case "gt": 131 | return ">"; 132 | break; 133 | 134 | case "gte": 135 | return ">="; 136 | break; 137 | 138 | case "is": 139 | return " is "; 140 | break; 141 | 142 | case "isnot": 143 | return " is not "; 144 | break; 145 | 146 | // case 'isnull': 147 | // return ' is NULL ' 148 | // break; 149 | 150 | // case 'isnnull': 151 | // return ' is not NULL ' 152 | // break; 153 | 154 | case "like": 155 | return " like "; 156 | break; 157 | 158 | case "nlike": 159 | return " not like "; 160 | break; 161 | 162 | case "in": 163 | return " in "; 164 | break; 165 | 166 | case "bw": 167 | return " between "; 168 | break; 169 | 170 | default: 171 | return null; 172 | break; 173 | } 174 | } 175 | 176 | function getLogicalOperator(operator) { 177 | switch (operator) { 178 | case "~or": 179 | return "or"; 180 | break; 181 | 182 | case "~and": 183 | return "and"; 184 | break; 185 | 186 | // case '~not': 187 | // return 'not' 188 | // break; 189 | 190 | case "~xor": 191 | return "xor"; 192 | break; 193 | 194 | default: 195 | return null; 196 | break; 197 | } 198 | } 199 | 200 | exports.getConditionClause = function(whereInQueryParams, condType = "where") { 201 | let whereQuery = ""; 202 | let whereParams = []; 203 | let grammarErr = 0; 204 | let numOfConditions = whereInQueryParams.split(/~or|~and|~not|~xor/); 205 | let logicalOperatorsInClause = whereInQueryParams.match( 206 | /(~or|~and|~not|~xor)/g 207 | ); 208 | 209 | if ( 210 | numOfConditions && 211 | logicalOperatorsInClause && 212 | numOfConditions.length !== logicalOperatorsInClause.length + 1 213 | ) { 214 | console.log( 215 | "conditions and logical operators mismatch", 216 | numOfConditions.length, 217 | logicalOperatorsInClause.length 218 | ); 219 | } else { 220 | //console.log('numOfConditions',numOfConditions,whereInQueryParams); 221 | //console.log('logicalOperatorsInClause',logicalOperatorsInClause); 222 | 223 | for (var i = 0; i < numOfConditions.length; ++i) { 224 | let variable = ""; 225 | let comparisonOperator = ""; 226 | let logicalOperator = ""; 227 | let variableValue = ""; 228 | let temp = ""; 229 | 230 | // split on commas 231 | var arr = numOfConditions[i].split(","); 232 | 233 | // consider first two splits only 234 | var result = arr.splice(0, 2); 235 | 236 | // join to make only 3 array elements 237 | result.push(arr.join(",")); 238 | 239 | // variable, operator, variablevalue 240 | if (result.length !== 3) { 241 | grammarErr = 1; 242 | break; 243 | } 244 | 245 | /**************** START : variable ****************/ 246 | //console.log('variable, operator, variablevalue',result); 247 | variable = result[0].match(/\(+(.*)/); 248 | 249 | //console.log('variable',variable); 250 | 251 | if (!variable || variable.length !== 2) { 252 | grammarErr = 1; 253 | break; 254 | } 255 | 256 | // get the variableName and push 257 | whereParams.push(variable[1]); 258 | 259 | // then replace the variable name with ?? 260 | temp = replaceWhereParamsToQMarks(true, result[0], " ignore ", condType); 261 | if (!temp) { 262 | grammarErr = 1; 263 | break; 264 | } 265 | whereQuery += temp; 266 | 267 | /**************** END : variable ****************/ 268 | 269 | /**************** START : operator and value ****************/ 270 | comparisonOperator = getComparisonOperator(result[1]); 271 | if (!comparisonOperator) { 272 | grammarErr = 1; 273 | break; 274 | } 275 | whereQuery += comparisonOperator; 276 | 277 | // get the variableValue and push to params 278 | variableValue = result[2].match(/(.*?)\)/); 279 | if (!variableValue || variableValue.length !== 2) { 280 | grammarErr = 1; 281 | break; 282 | } 283 | 284 | if (comparisonOperator === " in ") { 285 | let obj = prepareInClauseParams(variableValue[1]); 286 | whereQuery += obj.whereQuery; 287 | whereParams = whereParams.concat(obj.whereParams); 288 | } else if ( 289 | comparisonOperator === " like " || 290 | comparisonOperator === " not like " 291 | ) { 292 | whereParams.push(prepareLikeValue(variableValue[1])); 293 | } else if (comparisonOperator === " is ") { 294 | whereParams.push(prepareIsValue(variableValue[1])); 295 | } else if (comparisonOperator === " between ") { 296 | let obj = prepareBetweenValue(variableValue[1]); 297 | whereQuery += obj.whereQuery; 298 | whereParams = whereParams.concat(obj.whereParams); 299 | //console.log(whereQuery, whereParams); 300 | } else { 301 | whereParams.push(variableValue[1]); 302 | } 303 | 304 | // then replace the variableValue with ? 305 | temp = replaceWhereParamsToQMarks( 306 | false, 307 | result[2], 308 | comparisonOperator, 309 | condType 310 | ); 311 | if (!temp) { 312 | grammarErr = 1; 313 | break; 314 | } 315 | whereQuery += temp; 316 | 317 | if (numOfConditions.length !== -1 && i !== numOfConditions.length - 1) { 318 | //console.log('finding logical operator for',logicalOperatorsInClause[i]); 319 | logicalOperator = getLogicalOperator(logicalOperatorsInClause[i]); 320 | 321 | if (!logicalOperator) { 322 | grammarErr = 1; 323 | break; 324 | } 325 | 326 | whereQuery += getLogicalOperator(logicalOperatorsInClause[i]); 327 | } 328 | /**************** END : operator ****************/ 329 | } 330 | } 331 | 332 | let obj = {}; 333 | 334 | obj["query"] = ""; 335 | obj["params"] = []; 336 | obj["err"] = grammarErr; 337 | 338 | // console.log(whereInQueryParams); 339 | // console.log(whereQuery); 340 | // console.log(whereParams); 341 | // console.log(grammarErr); 342 | // console.log('= = = = = = = = ='); 343 | 344 | if (!grammarErr) { 345 | obj["query"] = whereQuery; 346 | obj["params"] = whereParams; 347 | } 348 | 349 | return obj; 350 | }; 351 | -------------------------------------------------------------------------------- /lib/xapi.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Xsql = require("./xsql.js"); 4 | var Xctrl = require("./xctrl.js"); 5 | var multer = require("multer"); 6 | const path = require("path"); 7 | 8 | const v8 = require("v8"), 9 | os = require("os"); 10 | 11 | 12 | //define class 13 | class Xapi { 14 | constructor(args, mysqlPool, app) { 15 | this.config = args; 16 | this.mysql = new Xsql(args, mysqlPool); 17 | this.app = app; 18 | this.ctrls = []; 19 | 20 | /**************** START : multer ****************/ 21 | this.storage = multer.diskStorage({ 22 | destination: function(req, file, cb) { 23 | cb(null, process.cwd()); 24 | }, 25 | filename: function(req, file, cb) { 26 | console.log(file); 27 | cb(null, Date.now() + "-" + file.originalname); 28 | } 29 | }); 30 | 31 | this.upload = multer({ storage: this.storage }); 32 | /**************** END : multer ****************/ 33 | } 34 | 35 | init(cbk) { 36 | this.mysql.init((err, results) => { 37 | this.app.use(this.urlMiddleware.bind(this)); 38 | let stat = this.setupRoutes(); 39 | this.app.use(this.errorMiddleware.bind(this)); 40 | cbk(err, stat); 41 | }); 42 | } 43 | 44 | urlMiddleware(req, res, next) { 45 | // get only request url from originalUrl 46 | let justUrl = req.originalUrl.split("?")[0]; 47 | let pathSplit = []; 48 | 49 | // split by apiPrefix 50 | let apiSuffix = justUrl.split(this.config.apiPrefix); 51 | 52 | if (apiSuffix.length === 2) { 53 | // split by / 54 | pathSplit = apiSuffix[1].split("/"); 55 | if (pathSplit.length) { 56 | if (pathSplit.length >= 3) { 57 | // handle for relational routes 58 | req.app.locals._parentTable = pathSplit[0]; 59 | req.app.locals._childTable = pathSplit[2]; 60 | } else { 61 | // handles rest of routes 62 | req.app.locals._tableName = pathSplit[0]; 63 | } 64 | } 65 | } 66 | 67 | next(); 68 | } 69 | 70 | errorMiddleware(err, req, res, next) { 71 | if (err && err.code) res.status(400).json({ error: err }); 72 | else if (err && err.message) 73 | res.status(500).json({ error: "Internal server error : " + err.message }); 74 | else res.status(500).json({ error: "Internal server error : " + err }); 75 | 76 | next(err); 77 | } 78 | 79 | asyncMiddleware(fn) { 80 | return (req, res, next) => { 81 | Promise.resolve(fn(req, res, next)).catch(err => { 82 | next(err); 83 | }); 84 | }; 85 | } 86 | 87 | root(req, res) { 88 | let routes = []; 89 | routes = this.mysql.getSchemaRoutes( 90 | false, 91 | req.protocol + "://" + req.get("host") + this.config.apiPrefix 92 | ); 93 | routes = routes.concat( 94 | this.mysql.globalRoutesPrint( 95 | req.protocol + "://" + req.get("host") + this.config.apiPrefix 96 | ) 97 | ); 98 | res.json(routes); 99 | } 100 | 101 | setupRoutes() { 102 | let stat = {} 103 | stat.tables = 0 104 | stat.apis = 0 105 | stat.routines = 0 106 | 107 | // console.log('this.config while setting up routes', this.config); 108 | 109 | // show routes for database schema 110 | this.app.get("/", this.asyncMiddleware(this.root.bind(this))); 111 | 112 | // show all resouces 113 | this.app 114 | .route(this.config.apiPrefix + "tables") 115 | .get(this.asyncMiddleware(this.tables.bind(this))); 116 | 117 | this.app 118 | .route(this.config.apiPrefix + "xjoin") 119 | .get(this.asyncMiddleware(this.xjoin.bind(this))); 120 | 121 | stat.apis += 3; 122 | 123 | /**************** START : setup routes for each table ****************/ 124 | let resources = []; 125 | resources = this.mysql.getSchemaRoutes(true, this.config.apiPrefix); 126 | 127 | stat.tables += resources.length; 128 | 129 | // iterate over each resource 130 | for (var j = 0; j < resources.length; ++j) { 131 | let resourceCtrl = new Xctrl(this.app, this.mysql); 132 | this.ctrls.push(resourceCtrl); 133 | 134 | let routes = resources[j]["routes"]; 135 | 136 | stat.apis += resources[j]["routes"].length; 137 | 138 | // iterate over each routes in resource and map function 139 | for (var i = 0; i < routes.length; ++i) { 140 | switch (routes[i]["routeType"]) { 141 | case "list": 142 | this.app 143 | .route(routes[i]["routeUrl"]) 144 | .get(this.asyncMiddleware(resourceCtrl.list.bind(resourceCtrl))); 145 | break; 146 | 147 | case "findOne": 148 | this.app 149 | .route(routes[i]["routeUrl"]) 150 | .get( 151 | this.asyncMiddleware(resourceCtrl.findOne.bind(resourceCtrl)) 152 | ); 153 | break; 154 | 155 | case "create": 156 | if (!this.config.readOnly) 157 | this.app 158 | .route(routes[i]["routeUrl"]) 159 | .post( 160 | this.asyncMiddleware(resourceCtrl.create.bind(resourceCtrl)) 161 | ); 162 | break; 163 | 164 | case "read": 165 | this.app 166 | .route(routes[i]["routeUrl"]) 167 | .get(this.asyncMiddleware(resourceCtrl.read.bind(resourceCtrl))); 168 | break; 169 | 170 | case "bulkInsert": 171 | if (!this.config.readOnly) { 172 | this.app 173 | .route(routes[i]["routeUrl"]) 174 | .post( 175 | this.asyncMiddleware( 176 | resourceCtrl.bulkInsert.bind(resourceCtrl) 177 | ) 178 | ); 179 | } 180 | break; 181 | 182 | case "bulkRead": 183 | if (!this.config.readOnly) { 184 | this.app 185 | .route(routes[i]["routeUrl"]) 186 | .get( 187 | this.asyncMiddleware(resourceCtrl.bulkRead.bind(resourceCtrl)) 188 | ); 189 | } else { 190 | stat.apis--; 191 | } 192 | break; 193 | 194 | case "bulkDelete": 195 | if (!this.config.readOnly) { 196 | this.app 197 | .route(routes[i]["routeUrl"]) 198 | .delete( 199 | this.asyncMiddleware( 200 | resourceCtrl.bulkDelete.bind(resourceCtrl) 201 | ) 202 | ); 203 | } else { 204 | stat.apis--; 205 | } 206 | break; 207 | 208 | case "patch": 209 | if (!this.config.readOnly) { 210 | this.app 211 | .route(routes[i]["routeUrl"]) 212 | .patch( 213 | this.asyncMiddleware(resourceCtrl.patch.bind(resourceCtrl)) 214 | ); 215 | } else { 216 | stat.apis--; 217 | } 218 | break; 219 | 220 | case "update": 221 | if (!this.config.readOnly) { 222 | this.app 223 | .route(routes[i]["routeUrl"]) 224 | .put( 225 | this.asyncMiddleware(resourceCtrl.update.bind(resourceCtrl)) 226 | ); 227 | } else { 228 | stat.apis--; 229 | } 230 | break; 231 | 232 | case "delete": 233 | if (!this.config.readOnly) { 234 | this.app 235 | .route(routes[i]["routeUrl"]) 236 | .delete( 237 | this.asyncMiddleware(resourceCtrl.delete.bind(resourceCtrl)) 238 | ); 239 | } else { 240 | stat.apis--; 241 | } 242 | break; 243 | 244 | case "exists": 245 | this.app 246 | .route(routes[i]["routeUrl"]) 247 | .get( 248 | this.asyncMiddleware(resourceCtrl.exists.bind(resourceCtrl)) 249 | ); 250 | break; 251 | 252 | case "count": 253 | this.app 254 | .route(routes[i]["routeUrl"]) 255 | .get(this.asyncMiddleware(resourceCtrl.count.bind(resourceCtrl))); 256 | break; 257 | 258 | case "distinct": 259 | this.app 260 | .route(routes[i]["routeUrl"]) 261 | .get( 262 | this.asyncMiddleware(resourceCtrl.distinct.bind(resourceCtrl)) 263 | ); 264 | break; 265 | 266 | case "describe": 267 | this.app 268 | .route(routes[i]["routeUrl"]) 269 | .get(this.asyncMiddleware(this.tableDescribe.bind(this))); 270 | break; 271 | 272 | case "relational": 273 | this.app 274 | .route(routes[i]["routeUrl"]) 275 | .get( 276 | this.asyncMiddleware(resourceCtrl.nestedList.bind(resourceCtrl)) 277 | ); 278 | break; 279 | 280 | case "groupby": 281 | this.app 282 | .route(routes[i]["routeUrl"]) 283 | .get( 284 | this.asyncMiddleware(resourceCtrl.groupBy.bind(resourceCtrl)) 285 | ); 286 | break; 287 | 288 | case "ugroupby": 289 | this.app 290 | .route(routes[i]["routeUrl"]) 291 | .get( 292 | this.asyncMiddleware(resourceCtrl.ugroupby.bind(resourceCtrl)) 293 | ); 294 | break; 295 | 296 | case "chart": 297 | this.app 298 | .route(routes[i]["routeUrl"]) 299 | .get(this.asyncMiddleware(resourceCtrl.chart.bind(resourceCtrl))); 300 | break; 301 | 302 | case "autoChart": 303 | this.app 304 | .route(routes[i]["routeUrl"]) 305 | .get( 306 | this.asyncMiddleware(resourceCtrl.autoChart.bind(resourceCtrl)) 307 | ); 308 | break; 309 | 310 | case "aggregate": 311 | this.app 312 | .route(routes[i]["routeUrl"]) 313 | .get( 314 | this.asyncMiddleware(resourceCtrl.aggregate.bind(resourceCtrl)) 315 | ); 316 | break; 317 | } 318 | } 319 | } 320 | /**************** END : setup routes for each table ****************/ 321 | 322 | if (this.config.dynamic === 1 && !this.config.readOnly) { 323 | this.app 324 | .route("/dynamic*") 325 | .post(this.asyncMiddleware(this.runQuery.bind(this))); 326 | 327 | /**************** START : multer routes ****************/ 328 | this.app.post( 329 | "/upload", 330 | this.upload.single("file"), 331 | this.uploadFile.bind(this) 332 | ); 333 | this.app.post( 334 | "/uploads", 335 | this.upload.array("files", 10), 336 | this.uploadFiles.bind(this) 337 | ); 338 | this.app.get("/download", this.downloadFile.bind(this)); 339 | /**************** END : multer routes ****************/ 340 | 341 | stat.apis += 4; 342 | } 343 | 344 | /**************** START : health and version ****************/ 345 | this.app.get("/_health", this.asyncMiddleware(this.health.bind(this))); 346 | this.app.get("/_version", this.asyncMiddleware(this.version.bind(this))); 347 | stat.apis += 2; 348 | /**************** END : health and version ****************/ 349 | 350 | /**************** START : call stored procedures ****************/ 351 | this.app.get('/_proc', this.asyncMiddleware(this.proc.bind(this))) 352 | stat.apis += 1 353 | const procResources = this.mysql.getProcList(true, this.config.apiPrefix) 354 | this.app.post('/_proc/:proc', this.asyncMiddleware(this.callProc.bind(this))) 355 | stat.routines += procResources.length 356 | stat.apis += procResources.length 357 | /**************** END : call stored procedures ****************/ 358 | 359 | 360 | let statStr = ' Generated: ' + stat.apis + ' REST APIs for ' + stat.tables + ' tables ' 361 | 362 | console.log(' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - '); 363 | console.log(' '); 364 | console.log(' Database : %s', this.config.database); 365 | console.log(' Number of Tables : %s', stat.tables); 366 | console.log(' Number of Routines : %s', stat.routines); 367 | console.log(' '); 368 | console.log(' REST APIs Generated : %s'.green.bold, stat.apis); 369 | console.log(' '); 370 | 371 | return stat 372 | 373 | } 374 | 375 | async xjoin(req, res) { 376 | let obj = {}; 377 | 378 | obj.query = ""; 379 | obj.params = []; 380 | 381 | this.mysql.prepareJoinQuery(req, res, obj); 382 | 383 | //console.log(obj); 384 | if (obj.query.length) { 385 | let results = await this.mysql.exec(obj.query, obj.params); 386 | res.status(200).json(results); 387 | } else { 388 | res.status(400).json({ err: "Invalid Xjoin request" }); 389 | } 390 | } 391 | 392 | async tableDescribe(req, res) { 393 | let query = "describe ??"; 394 | let params = [req.app.locals._tableName]; 395 | 396 | let results = await this.mysql.exec(query, params); 397 | res.status(200).json(results); 398 | } 399 | 400 | async tables(req, res) { 401 | let query = 402 | "SELECT table_name AS resource FROM information_schema.tables WHERE table_schema = ? "; 403 | let params = [this.config.database]; 404 | 405 | if (Object.keys(this.config.ignoreTables).length > 0) { 406 | query += "and table_name not in (?)"; 407 | params.push(Object.keys(this.config.ignoreTables)); 408 | } 409 | 410 | let results = await this.mysql.exec(query, params); 411 | 412 | res.status(200).json(results); 413 | } 414 | 415 | async runQuery(req, res) { 416 | let query = req.body.query; 417 | let params = req.body.params; 418 | 419 | let results = await this.mysql.exec(query, params); 420 | res.status(200).json(results); 421 | } 422 | 423 | /**************** START : files related ****************/ 424 | downloadFile(req, res) { 425 | let file = path.join(process.cwd(), req.query.name); 426 | res.download(file); 427 | } 428 | 429 | uploadFile(req, res) { 430 | if (req.file) { 431 | console.log(req.file.path); 432 | res.end(req.file.path); 433 | } else { 434 | res.end("upload failed"); 435 | } 436 | } 437 | 438 | uploadFiles(req, res) { 439 | if (!req.files || req.files.length === 0) { 440 | res.end("upload failed"); 441 | } else { 442 | let files = []; 443 | for (let i = 0; i < req.files.length; ++i) { 444 | files.push(req.files[i].path); 445 | } 446 | 447 | res.end(files.toString()); 448 | } 449 | } 450 | 451 | /**************** END : files related ****************/ 452 | 453 | /**************** START : health and version ****************/ 454 | 455 | async getMysqlUptime() { 456 | let v = await this.mysql.exec("SHOW GLOBAL STATUS LIKE 'Uptime';", []); 457 | return v[0]["Value"]; 458 | } 459 | 460 | async getMysqlHealth() { 461 | let v = await this.mysql.exec("select version() as version", []); 462 | return v[0]["version"]; 463 | } 464 | 465 | async health(req, res) { 466 | let status = {}; 467 | status["process_uptime"] = process.uptime(); 468 | status["mysql_uptime"] = await this.getMysqlUptime(); 469 | 470 | if (Object.keys(req.query).length) { 471 | status["process_memory_usage"] = process.memoryUsage(); 472 | status["os_total_memory"] = os.totalmem(); 473 | status["os_free_memory"] = os.freemem(); 474 | status["os_load_average"] = os.loadavg(); 475 | status["v8_heap_statistics"] = v8.getHeapStatistics(); 476 | } 477 | 478 | res.json(status); 479 | } 480 | 481 | async version(req, res) { 482 | let version = {}; 483 | 484 | version["Xmysql"] = this.app.get("version"); 485 | version["mysql"] = await this.getMysqlHealth(); 486 | version["node"] = process.versions.node; 487 | res.json(version); 488 | } 489 | 490 | /**************** END : health and version ****************/ 491 | 492 | async proc(req, res) { 493 | let query = 'SELECT routine_name AS resource FROM information_schema.routines WHERE routine_schema = ? '; 494 | let params = [this.config.database]; 495 | let results = await this.mysql.exec(query, params) 496 | res.status(200).json(results) 497 | } 498 | 499 | async callProc(req, res) { 500 | let query = 'CALL ??(?)' 501 | let params = [req.params.proc, Object.values(req.body)] 502 | let results = await this.mysql.exec(query, params) 503 | res.status(200).json(results) 504 | } 505 | 506 | } 507 | 508 | //expose class 509 | module.exports = Xapi; 510 | -------------------------------------------------------------------------------- /lib/xctrl.js: -------------------------------------------------------------------------------- 1 | //define class 2 | class xctrl { 3 | constructor(app, mysql) { 4 | this.app = app; 5 | this.mysql = mysql; 6 | } 7 | 8 | async create(req, res) { 9 | let query = "INSERT INTO ?? SET ?"; 10 | let params = []; 11 | 12 | params.push(req.app.locals._tableName); 13 | params.push(req.body); 14 | 15 | var results = await this.mysql.exec(query, params); 16 | res.status(200).json(results); 17 | } 18 | 19 | async list(req, res) { 20 | let queryParamsObj = {}; 21 | queryParamsObj.query = ""; 22 | queryParamsObj.params = []; 23 | 24 | this.mysql.prepareListQuery(req, res, queryParamsObj, 0); 25 | 26 | let results = await this.mysql.exec( 27 | queryParamsObj.query, 28 | queryParamsObj.params 29 | ); 30 | res.status(200).json(results); 31 | } 32 | 33 | async nestedList(req, res) { 34 | let queryParamsObj = {}; 35 | queryParamsObj.query = ""; 36 | queryParamsObj.params = []; 37 | 38 | this.mysql.prepareListQuery(req, res, queryParamsObj, 1); 39 | 40 | let results = await this.mysql.exec( 41 | queryParamsObj.query, 42 | queryParamsObj.params 43 | ); 44 | res.status(200).json(results); 45 | } 46 | 47 | async findOne(req, res) { 48 | let queryParamsObj = {}; 49 | queryParamsObj.query = ""; 50 | queryParamsObj.params = []; 51 | 52 | this.mysql.prepareListQuery(req, res, queryParamsObj, 2); 53 | 54 | let results = await this.mysql.exec( 55 | queryParamsObj.query, 56 | queryParamsObj.params 57 | ); 58 | res.status(200).json(results); 59 | } 60 | 61 | async read(req, res) { 62 | let query = "select * from ?? where "; 63 | let params = []; 64 | 65 | params.push(req.app.locals._tableName); 66 | 67 | let clause = this.mysql.getPrimaryKeyWhereClause( 68 | req.app.locals._tableName, 69 | req.params.id.split("___") 70 | ); 71 | 72 | if (!clause) { 73 | return res.status(400).send({ 74 | error: 75 | "Table is made of composite primary keys - all keys were not in input" 76 | }); 77 | } 78 | 79 | query += clause; 80 | query += " LIMIT 1"; 81 | 82 | let results = await this.mysql.exec(query, params); 83 | res.status(200).json(results); 84 | } 85 | 86 | async exists(req, res) { 87 | let query = "select * from ?? where "; 88 | let params = []; 89 | 90 | params.push(req.app.locals._tableName); 91 | 92 | let clause = this.mysql.getPrimaryKeyWhereClause( 93 | req.app.locals._tableName, 94 | req.params.id.split("___") 95 | ); 96 | 97 | if (!clause) { 98 | return res.status(400).send({ 99 | error: 100 | "Table is made of composite primary keys - all keys were not in input" 101 | }); 102 | } 103 | 104 | query += clause; 105 | query += " LIMIT 1"; 106 | 107 | let results = await this.mysql.exec(query, params); 108 | res.status(200).json(results); 109 | } 110 | 111 | async update(req, res) { 112 | let query = "REPLACE INTO ?? SET ?"; 113 | let params = []; 114 | 115 | params.push(req.app.locals._tableName); 116 | params.push(req.body); 117 | 118 | var results = await this.mysql.exec(query, params); 119 | res.status(200).json(results); 120 | } 121 | 122 | async patch(req, res) { 123 | let query = "UPDATE ?? SET "; 124 | let keys = Object.keys(req.body); 125 | 126 | // SET clause 127 | let updateKeys = ""; 128 | for (let i = 0; i < keys.length; ++i) { 129 | updateKeys += keys[i] + " = ? "; 130 | if (i !== keys.length - 1) updateKeys += ", "; 131 | } 132 | 133 | // where clause 134 | query += updateKeys + " where "; 135 | let clause = this.mysql.getPrimaryKeyWhereClause( 136 | req.app.locals._tableName, 137 | req.params.id.split("___") 138 | ); 139 | 140 | if (!clause) { 141 | return res.status(400).send({ 142 | error: 143 | "Table is made of composite primary keys - all keys were not in input" 144 | }); 145 | } 146 | 147 | query += clause; 148 | 149 | // params 150 | let params = []; 151 | params.push(req.app.locals._tableName); 152 | params = params.concat(Object.values(req.body)); 153 | 154 | let results = await this.mysql.exec(query, params); 155 | res.status(200).json(results); 156 | } 157 | 158 | async delete(req, res) { 159 | let query = "DELETE FROM ?? WHERE "; 160 | let params = []; 161 | 162 | params.push(req.app.locals._tableName); 163 | 164 | let clause = this.mysql.getPrimaryKeyWhereClause( 165 | req.app.locals._tableName, 166 | req.params.id.split("___") 167 | ); 168 | 169 | if (!clause) { 170 | return res.status(400).send({ 171 | error: 172 | "Table is made of composite primary keys - all keys were not in input" 173 | }); 174 | } 175 | 176 | query += clause; 177 | 178 | let results = await this.mysql.exec(query, params); 179 | res.status(200).json(results); 180 | } 181 | 182 | async bulkInsert(req, res) { 183 | let queryParamsObj = {}; 184 | queryParamsObj.query = ""; 185 | queryParamsObj.params = []; 186 | let results = []; 187 | 188 | //console.log(req.app.locals._tableName, req.body); 189 | 190 | this.mysql.prepareBulkInsert( 191 | req.app.locals._tableName, 192 | req.body, 193 | queryParamsObj 194 | ); 195 | 196 | results = await this.mysql.exec( 197 | queryParamsObj.query, 198 | queryParamsObj.params 199 | ); 200 | res.status(200).json(results); 201 | } 202 | 203 | async bulkDelete(req, res) { 204 | let query = "delete from ?? where ?? in "; 205 | let params = []; 206 | 207 | params.push(req.app.locals._tableName); 208 | params.push(this.mysql.getPrimaryKeyName(req.app.locals._tableName)); 209 | 210 | query += "("; 211 | 212 | if (req.query && req.query._ids) { 213 | let ids = req.query._ids.split(","); 214 | for (var i = 0; i < ids.length; ++i) { 215 | if (i) { 216 | query += ","; 217 | } 218 | query += "?"; 219 | params.push(ids[i]); 220 | } 221 | } 222 | 223 | query += ")"; 224 | 225 | //console.log(query, params); 226 | 227 | var results = await this.mysql.exec(query, params); 228 | res.status(200).json(results); 229 | } 230 | 231 | async bulkRead(req, res) { 232 | let queryParamsObj = {}; 233 | queryParamsObj.query = ""; 234 | queryParamsObj.params = []; 235 | 236 | this.mysql.prepareListQuery(req, res, queryParamsObj, 3); 237 | 238 | //console.log(queryParamsObj.query, queryParamsObj.params); 239 | 240 | let results = await this.mysql.exec( 241 | queryParamsObj.query, 242 | queryParamsObj.params 243 | ); 244 | res.status(200).json(results); 245 | } 246 | 247 | async count(req, res) { 248 | let queryParams = {}; 249 | 250 | queryParams.query = "select count(1) as no_of_rows from ?? "; 251 | queryParams.params = []; 252 | 253 | queryParams.params.push(req.app.locals._tableName); 254 | 255 | this.mysql.getWhereClause( 256 | req.query._where, 257 | req.app.locals._tableName, 258 | queryParams, 259 | " where " 260 | ); 261 | 262 | let results = await this.mysql.exec(queryParams.query, queryParams.params); 263 | res.status(200).json(results); 264 | } 265 | 266 | async distinct(req, res) { 267 | let queryParamsObj = {}; 268 | queryParamsObj.query = ""; 269 | queryParamsObj.params = []; 270 | 271 | this.mysql.prepareListQuery(req, res, queryParamsObj, 4); 272 | 273 | let results = await this.mysql.exec( 274 | queryParamsObj.query, 275 | queryParamsObj.params 276 | ); 277 | res.status(200).json(results); 278 | } 279 | 280 | async groupBy(req, res) { 281 | if (req.query && req.query._fields) { 282 | let queryParamsObj = {}; 283 | queryParamsObj.query = "select "; 284 | queryParamsObj.params = []; 285 | 286 | /**************** add columns and group by columns ****************/ 287 | this.mysql.getColumnsForSelectStmt( 288 | req.app.locals._tableName, 289 | req.query, 290 | queryParamsObj 291 | ); 292 | 293 | queryParamsObj.query += ",count(*) as _count from ?? group by "; 294 | let tableName = req.app.locals._tableName; 295 | queryParamsObj.params.push(tableName); 296 | 297 | this.mysql.getColumnsForGroupBy( 298 | req.app.locals._tableName, 299 | req.query, 300 | queryParamsObj 301 | ); 302 | 303 | if (!req.query._sort) { 304 | req.query._sort = {}; 305 | req.query._sort = "-_count"; 306 | } 307 | 308 | /**************** add having clause ****************/ 309 | this.mysql.getHavingClause( 310 | req.query._having, 311 | req.app.locals._tableName, 312 | queryParamsObj, 313 | " having " 314 | ); 315 | 316 | /**************** add orderby clause ****************/ 317 | this.mysql.getOrderByClause(req.query, tableName, queryParamsObj); 318 | 319 | //console.log(queryParamsObj.query, queryParamsObj.params); 320 | var results = await this.mysql.exec( 321 | queryParamsObj.query, 322 | queryParamsObj.params 323 | ); 324 | 325 | res.status(200).json(results); 326 | } else { 327 | res 328 | .status(400) 329 | .json({ 330 | message: 331 | "Missing _fields query params eg: /api/tableName/groupby?_fields=column1" 332 | }); 333 | } 334 | } 335 | 336 | async ugroupby(req, res) { 337 | if (req.query && req.query._fields) { 338 | let queryParamsObj = {}; 339 | queryParamsObj.query = ""; 340 | queryParamsObj.params = []; 341 | let uGrpByResults = {}; 342 | 343 | /**************** add fields with count(*) *****************/ 344 | let fields = req.query._fields.split(","); 345 | 346 | for (var i = 0; i < fields.length; ++i) { 347 | uGrpByResults[fields[i]] = []; 348 | 349 | if (i) { 350 | queryParamsObj.query += " UNION "; 351 | } 352 | queryParamsObj.query += 353 | " SELECT IFNULL(CONCAT(?,?,??),?) as ugroupby, count(*) as _count from ?? GROUP BY ?? "; 354 | queryParamsObj.params.push(fields[i]); 355 | queryParamsObj.params.push("~"); 356 | queryParamsObj.params.push(fields[i]); 357 | queryParamsObj.params.push(fields[i] + "~"); 358 | queryParamsObj.params.push(req.app.locals._tableName); 359 | queryParamsObj.params.push(fields[i]); 360 | } 361 | 362 | //console.log(queryParamsObj.query, queryParamsObj.params); 363 | var results = await this.mysql.exec( 364 | queryParamsObj.query, 365 | queryParamsObj.params 366 | ); 367 | 368 | for (var i = 0; i < results.length; ++i) { 369 | let grpByColName = results[i]["ugroupby"].split("~")[0]; 370 | let grpByColValue = results[i]["ugroupby"].split("~")[1]; 371 | 372 | let obj = {}; 373 | obj[grpByColValue] = results[i]["_count"]; 374 | 375 | uGrpByResults[grpByColName].push(obj); 376 | } 377 | 378 | res.status(200).json(uGrpByResults); 379 | } else { 380 | res 381 | .status(400) 382 | .json({ 383 | message: 384 | "Missing _fields query params eg: /api/tableName/ugroupby?_fields=column1,column2" 385 | }); 386 | } 387 | } 388 | 389 | async aggregate(req, res) { 390 | if (req.query && req.query._fields) { 391 | let tableName = req.app.locals._tableName; 392 | let query = "select "; 393 | let params = []; 394 | let fields = req.query._fields.split(","); 395 | 396 | for (var i = 0; i < fields.length; ++i) { 397 | if (i) { 398 | query = query + ","; 399 | } 400 | query = 401 | query + 402 | " min(??) as ?,max(??) as ?,avg(??) as ?,sum(??) as ?,stddev(??) as ?,variance(??) as ? "; 403 | params.push(fields[i]); 404 | params.push("min_of_" + fields[i]); 405 | params.push(fields[i]); 406 | params.push("max_of_" + fields[i]); 407 | params.push(fields[i]); 408 | params.push("avg_of_" + fields[i]); 409 | params.push(fields[i]); 410 | params.push("sum_of_" + fields[i]); 411 | params.push(fields[i]); 412 | params.push("stddev_of_" + fields[i]); 413 | params.push(fields[i]); 414 | params.push("variance_of_" + fields[i]); 415 | } 416 | 417 | query = query + " from ??"; 418 | params.push(tableName); 419 | 420 | var results = await this.mysql.exec(query, params); 421 | 422 | res.status(200).json(results); 423 | } else { 424 | res 425 | .status(400) 426 | .json({ 427 | message: 428 | "Missing _fields in query params eg: /api/tableName/aggregate?_fields=numericColumn1" 429 | }); 430 | } 431 | } 432 | 433 | async chart(req, res) { 434 | let query = ""; 435 | let params = []; 436 | let obj = {}; 437 | 438 | if (req.query) { 439 | let isRange = false; 440 | if (req.query.range) { 441 | isRange = true; 442 | } 443 | 444 | if (req.query && req.query.min && req.query.max && req.query.step) { 445 | //console.log(req.params.min, req.params.max, req.params.step); 446 | 447 | obj = this.mysql.getChartQueryAndParamsFromMinMaxStep( 448 | req.app.locals._tableName, 449 | req.query._fields, 450 | parseInt(req.query.min), 451 | parseInt(req.query.max), 452 | parseInt(req.query.step), 453 | isRange 454 | ); 455 | } else if ( 456 | req.query && 457 | req.query.steparray && 458 | req.query.steparray.length > 1 459 | ) { 460 | obj = this.mysql.getChartQueryAndParamsFromStepArray( 461 | req.app.locals._tableName, 462 | req.query._fields, 463 | req.query.steparray.split(",").map(Number), 464 | isRange 465 | ); 466 | } else if ( 467 | req.query && 468 | req.query.steppair && 469 | req.query.steppair.length > 1 470 | ) { 471 | obj = this.mysql.getChartQueryAndParamsFromStepPair( 472 | req.app.locals._tableName, 473 | req.query._fields, 474 | req.query.steppair.split(",").map(Number), 475 | false 476 | ); 477 | } else { 478 | query = 479 | "select min(??) as min,max(??) as max,stddev(??) as stddev,avg(??) as avg from ??"; 480 | params = []; 481 | 482 | params.push(req.query._fields); 483 | params.push(req.query._fields); 484 | params.push(req.query._fields); 485 | params.push(req.query._fields); 486 | params.push(req.app.locals._tableName); 487 | 488 | let _this = this; 489 | 490 | let results = await _this.mysql.exec(query, params); 491 | 492 | //console.log(results, results['max'], req.params); 493 | 494 | obj = _this.mysql.getChartQueryAndParamsFromMinMaxStddev( 495 | req.app.locals._tableName, 496 | req.query._fields, 497 | results[0]["min"], 498 | results[0]["max"], 499 | results[0]["stddev"], 500 | isRange 501 | ); 502 | } 503 | 504 | this.mysql.getWhereClause( 505 | req.query._where, 506 | req.app.locals._tableName, 507 | obj, 508 | " where " 509 | ); 510 | 511 | let results = await this.mysql.exec(obj.query, obj.params); 512 | 513 | res.status(200).json(results); 514 | } else { 515 | res 516 | .status(400) 517 | .json({ 518 | message: 519 | "Missing _fields in query params eg: /api/tableName/chart?_fields=numericColumn1" 520 | }); 521 | } 522 | } 523 | 524 | async autoChart(req, res) { 525 | let query = "describe ??"; 526 | let params = [req.app.locals._tableName]; 527 | let obj = {}; 528 | let results = []; 529 | 530 | let isRange = false; 531 | if (req.query.range) { 532 | isRange = true; 533 | } 534 | 535 | let describeResults = await this.mysql.exec(query, params); 536 | 537 | //console.log(describeResults); 538 | 539 | for (var i = 0; i < describeResults.length; ++i) { 540 | //console.log('is this numeric column', describeResults[i]['Type']); 541 | 542 | if ( 543 | describeResults[i]["Key"] !== "PRI" && 544 | this.mysql.isTypeOfColumnNumber(describeResults[i]["Type"]) 545 | ) { 546 | query = 547 | "select min(??) as min,max(??) as max,stddev(??) as stddev,avg(??) as avg from ??"; 548 | params = []; 549 | 550 | params.push(describeResults[i]["Field"]); 551 | params.push(describeResults[i]["Field"]); 552 | params.push(describeResults[i]["Field"]); 553 | params.push(describeResults[i]["Field"]); 554 | params.push(req.app.locals._tableName); 555 | 556 | let _this = this; 557 | 558 | let minMaxResults = await _this.mysql.exec(query, params); 559 | 560 | //console.log(minMaxResults, minMaxResults['max'], req.params); 561 | 562 | query = ""; 563 | params = []; 564 | 565 | obj = _this.mysql.getChartQueryAndParamsFromMinMaxStddev( 566 | req.app.locals._tableName, 567 | describeResults[i]["Field"], 568 | minMaxResults[0]["min"], 569 | minMaxResults[0]["max"], 570 | minMaxResults[0]["stddev"], 571 | isRange 572 | ); 573 | 574 | let r = await this.mysql.exec(obj.query, obj.params); 575 | 576 | let resultObj = {}; 577 | resultObj["column"] = describeResults[i]["Field"]; 578 | resultObj["chart"] = r; 579 | 580 | results.push(resultObj); 581 | } 582 | } 583 | 584 | res.status(200).json(results); 585 | } 586 | } 587 | 588 | //expose class 589 | module.exports = xctrl; 590 | -------------------------------------------------------------------------------- /lib/xsql.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const mysql = require("mysql"); 4 | const dataHelp = require("./util/data.helper.js"); 5 | const whereHelp = require("./util/whereClause.helper.js"); 6 | const assert = require("assert"); 7 | 8 | //define class§ 9 | class Xsql { 10 | constructor(sqlConfig, pool) { 11 | //define this variables 12 | this.sqlConfig = {}; 13 | this.pool = {}; 14 | this.metaDb = {}; 15 | this.metaDb.tables = {}; 16 | this.metaDb.routines = []; 17 | 18 | this.sqlConfig = sqlConfig; 19 | this.pool = pool; 20 | } 21 | 22 | /**************** START : Cache functions ****************/ 23 | init(cbk) { 24 | this.dbCacheInitAsync((err, results) => { 25 | cbk(err, results); 26 | }); 27 | } 28 | 29 | dbCacheInitAsync(cbk) { 30 | let self = this; 31 | 32 | self.pool.query( 33 | dataHelp.getSchemaQuery(), 34 | [this.sqlConfig.database], 35 | (err, results) => { 36 | if (err) { 37 | console.log("Cache init failed during database reading"); 38 | console.log(err, results); 39 | cbk(err, results); 40 | } else { 41 | for (var i = 0; i < results.length; ++i) { 42 | let keys = Object.keys(results[i]); 43 | 44 | for (var j = 0; j < keys.length; ++j) { 45 | let value = results[i][keys[j]]; 46 | 47 | results[i][keys[j].toLowerCase()] = value; 48 | 49 | //console.log(value); 50 | } 51 | } 52 | 53 | self.iterateToCacheTables(results); 54 | self.iterateToCacheTablePks(results); 55 | self.iterateToCacheTableColumns(results); 56 | self.iterateToCacheTableFks(results); 57 | 58 | // osx mysql server has limitations related to open_tables 59 | self.pool.query("FLUSH TABLES", [], (err, results) => { 60 | self.pool.query(dataHelp.getRoutines(), [this.sqlConfig.database], (err, results) => { 61 | if (err) { 62 | cbk(err, results) 63 | } else { 64 | self.iterateToCacheRoutines(results) 65 | cbk(null, null) 66 | } 67 | }) 68 | }); 69 | } 70 | } 71 | ); 72 | } 73 | 74 | iterateToCacheTables(schemaResults) { 75 | for (let i = 0; i < schemaResults.length; ++i) { 76 | let schemaRow = schemaResults[i]; 77 | 78 | let tableName = schemaRow["table_name"]; 79 | 80 | if (!(tableName in this.metaDb.tables)) { 81 | this.metaDb.tables[tableName] = {}; 82 | this.metaDb.tables[tableName]["primaryKeys"] = []; 83 | this.metaDb.tables[tableName]["foreignKeys"] = []; 84 | this.metaDb.tables[tableName]["columns"] = []; 85 | this.metaDb.tables[tableName]["indicies"] = []; 86 | this.metaDb.tables[tableName]["isView"] = schemaRow["isView"]; 87 | } 88 | } 89 | } 90 | 91 | iterateToCacheRoutines(routineResults) { 92 | for (let i = 0; i < routineResults.length; i++) { 93 | const routine = routineResults[i] 94 | const routineName = routine.routine_name 95 | this.metaDb.routines.push(routineName) 96 | } 97 | } 98 | 99 | iterateToCacheTableColumns(schemaResults) { 100 | for (let i = 0; i < schemaResults.length; ++i) { 101 | let schemaRow = schemaResults[i]; 102 | let tableName = schemaRow["table_name"]; 103 | let col = {}; 104 | col["column_name"] = schemaRow["column_name"]; 105 | col["ordinal_position"] = schemaRow["ordinal_position"]; 106 | col["column_key"] = schemaRow["column_key"]; 107 | col["data_type"] = schemaRow["data_type"]; 108 | col["column_type"] = schemaRow["column_type"]; 109 | 110 | dataHelp.findOrInsertObjectArrayByKey( 111 | col, 112 | "column_name", 113 | this.metaDb.tables[tableName]["columns"] 114 | ); 115 | } 116 | } 117 | 118 | iterateToCacheTablePks(schemaResults) { 119 | for (let i = 0; i < schemaResults.length; ++i) { 120 | let schemaRow = schemaResults[i]; 121 | let tableName = schemaRow["table_name"]; 122 | 123 | if (schemaRow["column_key"] === "PRI") { 124 | let pk = {}; 125 | pk["column_name"] = schemaRow["column_name"]; 126 | pk["ordinal_position"] = schemaRow["ordinal_position"]; 127 | pk["column_key"] = schemaRow["column_key"]; 128 | pk["data_type"] = schemaRow["data_type"]; 129 | pk["column_type"] = schemaRow["column_type"]; 130 | 131 | dataHelp.findOrInsertObjectArrayByKey( 132 | pk, 133 | "column_name", 134 | this.metaDb.tables[tableName]["primaryKeys"] 135 | ); 136 | } 137 | } 138 | } 139 | 140 | iterateToCacheTableFks(schemaResults) { 141 | for (let i = 0; i < schemaResults.length; ++i) { 142 | let schemaRow = schemaResults[i]; 143 | let tableName = schemaRow["table_name"]; 144 | 145 | if (schemaRow["referenced_table_name"]) { 146 | let fk = {}; 147 | 148 | fk["column_name"] = schemaRow["column_name"]; 149 | fk["table_name"] = schemaRow["table_name"]; 150 | fk["referenced_table_name"] = schemaRow["referenced_table_name"]; 151 | fk["referenced_column_name"] = schemaRow["referenced_column_name"]; 152 | fk["data_type"] = schemaRow["data_type"]; 153 | fk["column_type"] = schemaRow["column_type"]; 154 | 155 | dataHelp.findOrInsertObjectArrayByKey( 156 | fk, 157 | "column_name", 158 | this.metaDb.tables[tableName]["foreignKeys"] 159 | ); 160 | 161 | //console.log(fk['referenced_table_name'],fk['referenced_column_name'],tableName, schemaRow['column_name'], this.metaDb.tables[tableName]['foreignKeys'].length) 162 | } 163 | } 164 | } 165 | 166 | /**************** END : Cache functions ****************/ 167 | 168 | exec(query, params) { 169 | let _this = this; 170 | return new Promise(function(resolve, reject) { 171 | //console.log('mysql>', query, params); 172 | _this.pool.query(query, params, function(error, rows, _fields) { 173 | if (error) { 174 | console.log("mysql> ", error); 175 | return reject(error); 176 | } 177 | return resolve(rows); 178 | }); 179 | }); 180 | } 181 | 182 | typeOfColumn(Type) { 183 | //TODO: Im sure there are more types to handle here 184 | const strTypes = [ 185 | "varchar", 186 | "text", 187 | "char", 188 | "tinytext", 189 | "mediumtext", 190 | "longtext", 191 | "blob", 192 | "mediumblob", 193 | "longblob" 194 | ]; 195 | const intTypes = [ 196 | "int", 197 | "long", 198 | "smallint", 199 | "mediumint", 200 | "bigint", 201 | "tinyint" 202 | ]; 203 | const flatTypes = ["float", "double", "decimal"]; 204 | const dateTypes = ["date", "datetime", "timestamp", "time", "year"]; 205 | 206 | if (dataHelp.getType(Type, strTypes)) { 207 | return "string"; 208 | } else if (dataHelp.getType(Type, intTypes)) { 209 | return "int"; 210 | } else if (dataHelp.getType(Type, flatTypes)) { 211 | return "float"; 212 | } else if (dataHelp.getType(Type, dateTypes)) { 213 | return "date"; 214 | } else { 215 | return "unknown"; 216 | } 217 | } 218 | 219 | isTypeOfColumnNumber(Type) { 220 | //console.log(Type, this.typeOfColumn(Type)); 221 | return ( 222 | "int" === this.typeOfColumn(Type) || "float" === this.typeOfColumn(Type) 223 | ); 224 | } 225 | 226 | getLimitClause(reqParams) { 227 | //defaults 228 | reqParams._index = 0; 229 | reqParams._len = 20; 230 | 231 | if ("_size" in reqParams) { 232 | if (parseInt(reqParams._size) > 0 && parseInt(reqParams._size) <= 100) { 233 | reqParams._len = parseInt(reqParams._size); 234 | } else if (parseInt(reqParams._size) > 100) { 235 | reqParams._len = 100; 236 | } 237 | } 238 | 239 | if ("_p" in reqParams && parseInt(reqParams._p) > 0) { 240 | reqParams._index = parseInt(reqParams._p) * reqParams._len; 241 | } 242 | 243 | //console.log(reqParams._index, reqParams._len); 244 | 245 | return [reqParams._index, reqParams._len]; 246 | } 247 | 248 | prepareBulkInsert(tableName, objectArray, queryParamsObj) { 249 | if (tableName in this.metaDb.tables && objectArray) { 250 | let insObj = objectArray[0]; 251 | 252 | // goal => insert into ?? (?,?..?) values ? [tablName, col1,col2...coln,[[ObjValues_1],[ObjValues_2],...[ObjValues_N]] 253 | queryParamsObj.query = " INSERT INTO ?? ( "; 254 | queryParamsObj.params.push(tableName); 255 | 256 | let cols = []; 257 | let colPresent = false; 258 | 259 | /**************** START : prepare column names to be inserted ****************/ 260 | // iterate over all column in table and have only ones existing in objects to be inserted 261 | for ( 262 | var i = 0; 263 | i < this.metaDb.tables[tableName]["columns"].length; 264 | ++i 265 | ) { 266 | let colName = this.metaDb.tables[tableName]["columns"][i][ 267 | "column_name" 268 | ]; 269 | 270 | if (colName in insObj) { 271 | if (colPresent) { 272 | queryParamsObj.query += ","; 273 | } 274 | 275 | queryParamsObj.query += colName; 276 | 277 | colPresent = true; 278 | } 279 | 280 | cols.push(colName); 281 | 282 | //console.log('> > ', queryParamsObj.query); 283 | } 284 | 285 | queryParamsObj.query += " ) values ?"; 286 | /**************** END : prepare column names to be inserted ****************/ 287 | 288 | /**************** START : prepare value object in prepared statement ****************/ 289 | // iterate over sent object array 290 | let arrOfArr = []; 291 | for (var i = 0; i < objectArray.length; ++i) { 292 | let arrValues = []; 293 | for (var j = 0; j < cols.length; ++j) { 294 | if (cols[j] in objectArray[i]) 295 | arrValues.push(objectArray[i][cols[j]]); 296 | } 297 | arrOfArr.push(arrValues); 298 | } 299 | queryParamsObj.params.push(arrOfArr); 300 | /**************** END : prepare value object in prepared statement ****************/ 301 | } 302 | } 303 | 304 | getGroupByClause(_groupby, tableName, queryParamsObj) { 305 | if (_groupby) { 306 | queryParamsObj.query += " group by " + _groupby + " "; 307 | return _groupby; 308 | } 309 | } 310 | 311 | getHavingClause(_having, tableName, queryParamsObj) { 312 | if (_having) { 313 | let whereClauseObj = whereHelp.getConditionClause(_having, "having"); 314 | 315 | if (whereClauseObj.err === 0) { 316 | queryParamsObj.query = 317 | queryParamsObj.query + " having " + whereClauseObj.query; 318 | queryParamsObj.params = queryParamsObj.params.concat( 319 | whereClauseObj.params 320 | ); 321 | } 322 | 323 | //console.log('> > > after where clause filling up:', queryParamsObj.query, queryParamsObj.params); 324 | } 325 | } 326 | 327 | getWhereClause(queryparams, tableName, queryParamsObj, appendToWhere) { 328 | if (queryparams) { 329 | let whereClauseObj = whereHelp.getConditionClause(queryparams); 330 | 331 | if (whereClauseObj.err === 0) { 332 | queryParamsObj.query = 333 | queryParamsObj.query + appendToWhere + whereClauseObj.query; 334 | queryParamsObj.params = queryParamsObj.params.concat( 335 | whereClauseObj.params 336 | ); 337 | } 338 | } 339 | } 340 | 341 | getOrderByClause(queryparams, tableName, queryParamsObj) { 342 | if (queryparams._sort) { 343 | queryParamsObj.query += " ORDER BY "; 344 | 345 | let orderByCols = queryparams._sort.split(","); 346 | 347 | for (let i = 0; i < orderByCols.length; ++i) { 348 | if (i) { 349 | queryParamsObj.query += ", "; 350 | } 351 | const aggregationFunction = this.getAggregationFunction(orderByCols[i]); 352 | const columnName = this.getColumnNameWithoutAggregationFunctions(orderByCols[i]); 353 | const orderByDirection = orderByCols[i][0] === "-" ? 'DESC' : 'ASC'; 354 | 355 | if (aggregationFunction) { 356 | queryParamsObj.query += `${aggregationFunction}(??) ${orderByDirection}`; 357 | queryParamsObj.params.push(columnName); 358 | } else { 359 | queryParamsObj.query += `?? ${orderByDirection}`; 360 | queryParamsObj.params.push(columnName); 361 | } 362 | } 363 | } 364 | } 365 | 366 | getColumnsForSelectStmtWithGrpBy(reqQueryParams, tableName, queryParamsObj) { 367 | let grpByCols = reqQueryParams._groupby.split(","); 368 | 369 | for (var i = 0; i < grpByCols.length; ++i) { 370 | if (i) { 371 | queryParamsObj.query += ","; 372 | } 373 | queryParamsObj.query += " ??"; 374 | queryParamsObj.params.push(grpByCols[i]); 375 | } 376 | 377 | queryParamsObj.query += ",count(1) as _count "; 378 | } 379 | 380 | getColumnsForGroupBy(tableName, reqQueryParams, queryParamsObj) { 381 | const updatedQueryParams = Object.assign({}, reqQueryParams); 382 | if ('_groupbyfields' in updatedQueryParams) { 383 | // allows you to group by different fields than you have in the select 384 | updatedQueryParams['_fields'] = updatedQueryParams['_groupbyfields']; 385 | } 386 | 387 | return this.getColumnsForSelectStmt(tableName, updatedQueryParams, queryParamsObj) 388 | } 389 | 390 | getColumnsForSelectStmt(tableName, reqQueryParams, queryParamsObj) { 391 | let table = this.metaDb.tables[tableName]; 392 | let cols = []; 393 | let _fieldsInQuery = []; 394 | let removeFieldsObj = {}; 395 | 396 | // populate _fields array from query params 397 | if ("_fields" in reqQueryParams) { 398 | _fieldsInQuery = reqQueryParams["_fields"].split(","); 399 | } else { 400 | queryParamsObj.query += " * "; 401 | return " * "; 402 | } 403 | 404 | // get column name in _fields and mark column name which start with '-' 405 | for (let i = 0; i < _fieldsInQuery.length; ++i) { 406 | if (_fieldsInQuery[i][0] === "-") { 407 | removeFieldsObj[ 408 | _fieldsInQuery[i].substring(1, _fieldsInQuery[i].length) 409 | ] = 1; 410 | } else { 411 | cols.push(_fieldsInQuery[i]); 412 | } 413 | } 414 | 415 | if (!cols.length) { 416 | // for each column in table - add only which are not in removeFieldsObj 417 | for (let i = 0; i < table["columns"].length; ++i) { 418 | if (!(table["columns"][i]["column_name"] in removeFieldsObj)) { 419 | cols.push(table["columns"][i]["column_name"]); 420 | } 421 | } 422 | } else { 423 | cols = this.removeUnknownColumns(cols, tableName); 424 | } 425 | 426 | for (var i = 0; i < cols.length; ++i) { 427 | if (i) { 428 | queryParamsObj.query += ","; 429 | } 430 | const aggregationFunction = this.getAggregationFunction(cols[i]); 431 | 432 | if (aggregationFunction) { 433 | queryParamsObj.query += `${aggregationFunction}(??)`; 434 | const columnName = this.getColumnNameWithoutAggregationFunctions(cols[i]); 435 | queryParamsObj.params.push(columnName); 436 | } else { 437 | queryParamsObj.query += "??"; 438 | queryParamsObj.params.push(cols[i]); 439 | } 440 | } 441 | 442 | return cols.join(","); 443 | } 444 | 445 | getAggregationFunction(rawColumnName) { 446 | const AGGREGATION_FUNCTION_REGEX = /^[-]?(AVG|BIT_AND|BIT_OR|BIT_XOR|COUNT|COUNTDISTINCT|GROUP_CONCAT|JSON_ARRAYAGG|JSON_OBJECTAGG|MAX|MIN|STD|STDDEV|STDDEV_POP|STDDEV_SAMP|SUM|VAR_POP|VAR_SAMP|VARIANCE)\((.*)\)$/i; 447 | const aggFuncMatch = rawColumnName.match(AGGREGATION_FUNCTION_REGEX); 448 | if (aggFuncMatch && aggFuncMatch.length === 3) { 449 | // match will look like (3) ["AVG(timestamp)", "AVG", "timestamp", index: 0, input: "AVG(timestamp)", groups: undefined] 450 | return aggFuncMatch[1]; 451 | } 452 | return null; 453 | } 454 | 455 | getColumnNameWithoutAggregationFunctions(rawColumnName) { 456 | const AGGREGATION_FUNCTION_REGEX = /^[-]?(AVG|BIT_AND|BIT_OR|BIT_XOR|COUNT|COUNTDISTINCT|GROUP_CONCAT|JSON_ARRAYAGG|JSON_OBJECTAGG|MAX|MIN|STD|STDDEV|STDDEV_POP|STDDEV_SAMP|SUM|VAR_POP|VAR_SAMP|VARIANCE)\((.*)\)$/i; 457 | const aggFuncMatch = rawColumnName.match(AGGREGATION_FUNCTION_REGEX); 458 | if (aggFuncMatch && aggFuncMatch.length === 3) { 459 | // match will look like (3) ["AVG(timestamp)", "AVG", "timestamp", index: 0, input: "AVG(timestamp)", groups: undefined] 460 | return aggFuncMatch[2]; 461 | } 462 | return rawColumnName.replace(/-/, ''); 463 | } 464 | 465 | removeUnknownColumns(inputColumns, tableName) { 466 | let cols = inputColumns; 467 | let unknown_cols_in_input = []; 468 | let shadowCols = []; 469 | let tableColumns = this.metaDb.tables[tableName]["columns"]; 470 | 471 | // find unknown fields if any 472 | for (var j = 0; j < cols.length; ++j) { 473 | let found = 0; 474 | // Used to allow aggregation functions like AVG(timestamp) 475 | let columnNameWithoutAggregationClauses = this.getColumnNameWithoutAggregationFunctions(cols[j]); 476 | 477 | for (var i = 0; i < tableColumns.length; ++i) { 478 | if (tableColumns[i]["column_name"] === columnNameWithoutAggregationClauses) { 479 | found = 1; 480 | break; 481 | } 482 | } 483 | 484 | if (!found) { 485 | unknown_cols_in_input.push(j); 486 | } 487 | } 488 | 489 | // if there are unknown fields - remove and ignore 'em 490 | if (unknown_cols_in_input.length) { 491 | for (var i = 0; i < cols.length; ++i) { 492 | if (unknown_cols_in_input.indexOf(i) === -1) { 493 | shadowCols.push(cols[i]); 494 | } 495 | } 496 | 497 | cols = []; 498 | cols = shadowCols; 499 | } 500 | 501 | return cols; 502 | } 503 | 504 | getPrimaryKeyName(tableName) { 505 | let pk = null; 506 | if (tableName in this.metaDb.tables) { 507 | pk = this.metaDb.tables[tableName].primaryKeys[0]["column_name"]; 508 | } 509 | return pk; 510 | } 511 | 512 | getPrimaryKeyWhereClause(tableName, pksValues) { 513 | let whereClause = ""; 514 | let whereCol = ""; 515 | let whereValue = ""; 516 | let pks = []; 517 | 518 | if (tableName in this.metaDb.tables) { 519 | pks = this.metaDb.tables[tableName].primaryKeys; 520 | } else { 521 | return null; 522 | } 523 | 524 | // number of primary keys in table and one sent should be same 525 | if (pksValues.length !== pks.length) { 526 | return null; 527 | } 528 | 529 | // get a where clause out of the above columnNames and their values 530 | for (let i = 0; i < pks.length; ++i) { 531 | let type = dataHelp.getColumnType(pks[i]); 532 | 533 | whereCol = pks[i]["column_name"]; 534 | 535 | if (type === "string") { 536 | whereValue = mysql.escape(pksValues[i]); 537 | } else if (type === "int") { 538 | whereValue = parseInt(pksValues[i]); 539 | } else if (type === "float") { 540 | whereValue = parseFloat(pksValues[i]); 541 | } else if (type === "date") { 542 | whereValue = Date(pksValues[i]); 543 | } else { 544 | console.error(pks[i]); 545 | assert(false, "Unhandled type of primary key"); 546 | } 547 | 548 | if (i) { 549 | whereClause += " and "; 550 | } 551 | 552 | whereClause += whereCol + " = " + whereValue; 553 | } 554 | 555 | return whereClause; 556 | } 557 | 558 | getForeignKeyWhereClause(parentTable, parentId, childTable) { 559 | let whereValue = ""; 560 | 561 | //get all foreign keys of child table 562 | let fks = this.metaDb.tables[childTable].foreignKeys; 563 | let fk = dataHelp.findObjectInArrayByKey( 564 | "referenced_table_name", 565 | parentTable, 566 | fks 567 | ); 568 | let whereCol = fk["column_name"]; 569 | let colType = dataHelp.getColumnType(fk); 570 | 571 | if (colType === "string") { 572 | whereValue = mysql.escape(parentId); 573 | } else if (colType === "int") { 574 | whereValue = mysql.escape(parseInt(parentId)); 575 | } else if (colType === "float") { 576 | whereValue = mysql.escape(parseFloat(parentId)); 577 | } else if (colType === "date") { 578 | whereValue = mysql.escape(Date(parentId)); 579 | } else { 580 | console.error(pks[i]); 581 | assert(false, "Unhandled column type in foreign key handling"); 582 | } 583 | 584 | return whereCol + " = " + whereValue; 585 | } 586 | 587 | prepareRoute(internal, httpType, apiPrefix, urlRoute, routeType) { 588 | let route = {}; 589 | route["httpType"] = httpType; 590 | route["routeUrl"] = apiPrefix + urlRoute; 591 | if (internal) { 592 | route["routeType"] = routeType; 593 | } 594 | return route; 595 | } 596 | 597 | getSchemaRoutes(internal, apiPrefix) { 598 | let schemaRoutes = []; 599 | 600 | for (var tableName in this.metaDb.tables) { 601 | if (tableName in this.sqlConfig.ignoreTables) { 602 | //console.log('ignore table', tableName); 603 | } else { 604 | let routes = []; 605 | let tableObj = {}; 606 | let table = this.metaDb.tables[tableName]; 607 | let isView = this.metaDb.tables[tableName]["isView"]; 608 | 609 | tableObj["resource"] = tableName; 610 | 611 | // order of routes is important for express routing - DO NOT CHANGE ORDER 612 | routes.push( 613 | this.prepareRoute( 614 | internal, 615 | "get", 616 | apiPrefix, 617 | tableName + "/describe", 618 | "describe" 619 | ) 620 | ); 621 | routes.push( 622 | this.prepareRoute( 623 | internal, 624 | "get", 625 | apiPrefix, 626 | tableName + "/count", 627 | "count" 628 | ) 629 | ); 630 | routes.push( 631 | this.prepareRoute( 632 | internal, 633 | "get", 634 | apiPrefix, 635 | tableName + "/groupby", 636 | "groupby" 637 | ) 638 | ); 639 | routes.push( 640 | this.prepareRoute( 641 | internal, 642 | "get", 643 | apiPrefix, 644 | tableName + "/distinct", 645 | "distinct" 646 | ) 647 | ); 648 | routes.push( 649 | this.prepareRoute( 650 | internal, 651 | "get", 652 | apiPrefix, 653 | tableName + "/ugroupby", 654 | "ugroupby" 655 | ) 656 | ); 657 | routes.push( 658 | this.prepareRoute( 659 | internal, 660 | "get", 661 | apiPrefix, 662 | tableName + "/chart", 663 | "chart" 664 | ) 665 | ); 666 | routes.push( 667 | this.prepareRoute( 668 | internal, 669 | "get", 670 | apiPrefix, 671 | tableName + "/aggregate", 672 | "aggregate" 673 | ) 674 | ); 675 | routes.push( 676 | this.prepareRoute( 677 | internal, 678 | "get", 679 | apiPrefix, 680 | tableName + "/findOne", 681 | "findOne" 682 | ) 683 | ); 684 | routes.push( 685 | this.prepareRoute( 686 | internal, 687 | "get", 688 | apiPrefix, 689 | tableName + "/autoChart", 690 | "autoChart" 691 | ) 692 | ); 693 | 694 | if (!isView && !this.sqlConfig.readOnly) { 695 | routes.push( 696 | this.prepareRoute(internal, "post", apiPrefix, tableName, "create") 697 | ); 698 | } 699 | routes.push( 700 | this.prepareRoute(internal, "get", apiPrefix, tableName, "list") 701 | ); 702 | 703 | if (!isView && !this.sqlConfig.readOnly) { 704 | routes.push( 705 | this.prepareRoute( 706 | internal, 707 | "post", 708 | apiPrefix, 709 | tableName + "/bulk", 710 | "bulkInsert" 711 | ) 712 | ); 713 | routes.push( 714 | this.prepareRoute( 715 | internal, 716 | "delete", 717 | apiPrefix, 718 | tableName + "/bulk", 719 | "bulkDelete" 720 | ) 721 | ); 722 | } 723 | routes.push( 724 | this.prepareRoute( 725 | internal, 726 | "get", 727 | apiPrefix, 728 | tableName + "/bulk", 729 | "bulkRead" 730 | ) 731 | ); 732 | 733 | if (!isView && !this.sqlConfig.readOnly) { 734 | routes.push( 735 | this.prepareRoute(internal, "put", apiPrefix, tableName, "update") 736 | ); 737 | routes.push( 738 | this.prepareRoute( 739 | internal, 740 | "patch", 741 | apiPrefix, 742 | tableName + "/:id", 743 | "patch" 744 | ) 745 | ); 746 | routes.push( 747 | this.prepareRoute( 748 | internal, 749 | "delete", 750 | apiPrefix, 751 | tableName + "/:id", 752 | "delete" 753 | ) 754 | ); 755 | } 756 | 757 | routes.push( 758 | this.prepareRoute( 759 | internal, 760 | "get", 761 | apiPrefix, 762 | tableName + "/:id", 763 | "read" 764 | ) 765 | ); 766 | routes.push( 767 | this.prepareRoute( 768 | internal, 769 | "get", 770 | apiPrefix, 771 | tableName + "/:id/exists", 772 | "exists" 773 | ) 774 | ); 775 | 776 | for (var j = 0; j < table["foreignKeys"].length; ++j) { 777 | let fk = table["foreignKeys"][j]; 778 | 779 | if (fk["referenced_table_name"] in this.sqlConfig.ignoreTables) { 780 | //console.log('ignore table',fk['referenced_table_name']); 781 | } else { 782 | routes.push( 783 | this.prepareRoute( 784 | internal, 785 | "get", 786 | apiPrefix, 787 | fk["referenced_table_name"] + "/:id/" + fk["table_name"], 788 | "relational" 789 | ) 790 | ); 791 | } 792 | } 793 | 794 | var procList = this.getProcList() 795 | for (var j = 0; j < procList.length; j++) { 796 | routes.push(this.prepareRoute(internal, 'post', apiPrefix, '_proc/' + procList[j])) 797 | } 798 | 799 | tableObj['routes'] = routes; 800 | 801 | schemaRoutes.push(tableObj); 802 | } 803 | } 804 | 805 | return schemaRoutes; 806 | } 807 | 808 | getProcList() { 809 | let procRoutes = [] 810 | for (let procName in this.metaDb.routines) { 811 | procRoutes.push(this.metaDb.routines[procName]) 812 | } 813 | return procRoutes 814 | } 815 | 816 | getJoinType(joinInQueryParams) { 817 | //console.log('joinInQueryParams',joinInQueryParams); 818 | 819 | switch (joinInQueryParams) { 820 | case "_lj": 821 | return " left join "; 822 | break; 823 | 824 | case "_rj": 825 | return " right join "; 826 | break; 827 | 828 | // case '_fj': 829 | // return ' full join ' 830 | // break; 831 | 832 | case "_ij": 833 | return " inner join "; 834 | break; 835 | 836 | case "_j": 837 | return " join "; 838 | break; 839 | } 840 | 841 | return " join "; 842 | } 843 | 844 | globalRoutesPrint(apiPrefix) { 845 | let r = []; 846 | 847 | r.push(apiPrefix + "tables"); 848 | r.push(apiPrefix + "xjoin"); 849 | 850 | if (this.sqlConfig.dynamic) { 851 | r.push(apiPrefix + "dynamic"); 852 | r.push("/upload"); 853 | r.push("/uploads"); 854 | r.push("/download"); 855 | } 856 | 857 | return r; 858 | } 859 | 860 | getChartQueryAndParamsFromStepPair( 861 | tableName, 862 | columnName, 863 | stepArray, 864 | isRange = false 865 | ) { 866 | let obj = {}; 867 | 868 | obj.query = ""; 869 | obj.params = []; 870 | 871 | //console.log('getChartQueryAndParamsFromStepArray',isRange); 872 | 873 | //select ? as ??, count(*) as _count from ?? where ?? between ? and ? 874 | 875 | if ( 876 | stepArray.length && 877 | stepArray.length >= 2 && 878 | stepArray.length % 2 === 0 879 | ) { 880 | for ( 881 | let i = 0; 882 | i < stepArray.length && stepArray.length >= 2; 883 | i = i + 2 884 | ) { 885 | obj.query = obj.query + dataHelp.getChartQuery(); 886 | 887 | if (i + 2 < stepArray.length) { 888 | obj.query = obj.query + " union "; 889 | } 890 | 891 | obj.params.push(stepArray[i] + " to " + stepArray[i + 1]); 892 | obj.params.push(columnName); 893 | obj.params.push(tableName); 894 | obj.params.push(columnName); 895 | obj.params.push(stepArray[i]); 896 | obj.params.push(stepArray[i + 1]); 897 | } 898 | } 899 | 900 | //console.log('step spread query', obj); 901 | 902 | return obj; 903 | } 904 | 905 | getChartQueryAndParamsFromStepArray( 906 | tableName, 907 | columnName, 908 | stepArray, 909 | isRange = false 910 | ) { 911 | let obj = {}; 912 | 913 | obj.query = ""; 914 | obj.params = []; 915 | 916 | //console.log('getChartQueryAndParamsFromStepArray',isRange); 917 | 918 | if (stepArray.length && stepArray.length >= 2) { 919 | for (let i = 0; i < stepArray.length - 1; i = i + 1) { 920 | obj.query = obj.query + dataHelp.getChartQuery(); 921 | if (i + 2 < stepArray.length) { 922 | obj.query = obj.query + " union "; 923 | } 924 | 925 | if (i && isRange === false) { 926 | stepArray[i] = stepArray[i] + 1; 927 | } 928 | 929 | if (isRange === false) { 930 | obj.params.push(stepArray[i] + " to " + stepArray[i + 1]); 931 | } else { 932 | obj.params.push(stepArray[0] + " to " + stepArray[i + 1]); 933 | } 934 | 935 | obj.params.push(columnName); 936 | obj.params.push(tableName); 937 | obj.params.push(columnName); 938 | 939 | if (isRange === false) { 940 | obj.params.push(stepArray[i]); 941 | obj.params.push(stepArray[i + 1]); 942 | } else { 943 | obj.params.push(stepArray[0]); 944 | obj.params.push(stepArray[i + 1]); 945 | } 946 | } 947 | } 948 | 949 | //console.log('step spread query', obj); 950 | 951 | return obj; 952 | } 953 | 954 | getChartQueryAndParamsFromMinMaxStddev( 955 | tableName, 956 | columnName, 957 | min, 958 | max, 959 | stddev, 960 | isRange = false 961 | ) { 962 | let stepArray = dataHelp.getStepArray(min, max, stddev); 963 | 964 | //console.log('steparray', stepArray); 965 | 966 | let obj = this.getChartQueryAndParamsFromStepArray( 967 | tableName, 968 | columnName, 969 | stepArray, 970 | isRange 971 | ); 972 | 973 | //console.log('steparray', obj); 974 | 975 | return obj; 976 | } 977 | 978 | getChartQueryAndParamsFromMinMaxStep( 979 | tableName, 980 | columnName, 981 | min, 982 | max, 983 | step, 984 | isRange = false 985 | ) { 986 | let stepArray = dataHelp.getStepArraySimple(min, max, step); 987 | 988 | //console.log('steparray', stepArray); 989 | 990 | let obj = this.getChartQueryAndParamsFromStepArray( 991 | tableName, 992 | columnName, 993 | stepArray, 994 | isRange 995 | ); 996 | 997 | //console.log('steparray', obj); 998 | 999 | return obj; 1000 | } 1001 | 1002 | _getGrpByHavingOrderBy(req, tableName, queryParamsObj, listType) { 1003 | /**************** add group by ****************/ 1004 | this.getGroupByClause( 1005 | req.query._groupby, 1006 | req.app.locals._tableName, 1007 | queryParamsObj 1008 | ); 1009 | 1010 | /**************** add having ****************/ 1011 | this.getHavingClause( 1012 | req.query._having, 1013 | req.app.locals._tableName, 1014 | queryParamsObj 1015 | ); 1016 | 1017 | /**************** add order clause ****************/ 1018 | this.getOrderByClause(req.query, req.app.locals._tableName, queryParamsObj); 1019 | 1020 | /**************** add limit clause ****************/ 1021 | if (listType === 2) { 1022 | //nested 1023 | queryParamsObj.query += " limit 1 "; 1024 | } else { 1025 | queryParamsObj.query += " limit ?,? "; 1026 | queryParamsObj.params = queryParamsObj.params.concat( 1027 | this.getLimitClause(req.query) 1028 | ); 1029 | } 1030 | } 1031 | 1032 | /** 1033 | * 1034 | * @param req 1035 | * @param res 1036 | * @param queryParamsObj : {query, params} 1037 | * @param listType : 0:list, 1:nested, 2:findOne, 3:bulkRead, 4:distinct, 5:xjoin 1038 | * 1039 | * Updates query, params for query of type listType 1040 | */ 1041 | prepareListQuery(req, res, queryParamsObj, listType = 0) { 1042 | queryParamsObj.query = "select "; 1043 | queryParamsObj.params = []; 1044 | 1045 | if (listType === 4) { 1046 | //list type distinct 1047 | queryParamsObj.query += " distinct "; 1048 | } 1049 | 1050 | /**************** select columns ****************/ 1051 | if (req.query._groupby) { 1052 | this.getColumnsForSelectStmtWithGrpBy( 1053 | req.query, 1054 | req.app.locals._tableName, 1055 | queryParamsObj 1056 | ); 1057 | } else { 1058 | this.getColumnsForSelectStmt( 1059 | req.app.locals._tableName, 1060 | req.query, 1061 | queryParamsObj 1062 | ); 1063 | } 1064 | 1065 | /**************** add tableName ****************/ 1066 | queryParamsObj.query += " from ?? "; 1067 | 1068 | if (listType === 1) { 1069 | //nested list 1070 | 1071 | req.app.locals._tableName = req.app.locals._childTable; 1072 | 1073 | queryParamsObj.params.push(req.app.locals._childTable); 1074 | 1075 | queryParamsObj.query += " where "; 1076 | 1077 | /**************** add where foreign key ****************/ 1078 | let whereClause = this.getForeignKeyWhereClause( 1079 | req.app.locals._parentTable, 1080 | req.params.id, 1081 | req.app.locals._childTable 1082 | ); 1083 | 1084 | if (!whereClause) { 1085 | return res.status(400).send({ 1086 | error: 1087 | "Table is made of composite primary keys - all keys were not in input" 1088 | }); 1089 | } 1090 | queryParamsObj.query += whereClause; 1091 | 1092 | this.getWhereClause( 1093 | req.query._where, 1094 | req.app.locals._tableName, 1095 | queryParamsObj, 1096 | " and " 1097 | ); 1098 | } else if (listType === 3) { 1099 | //bulkRead 1100 | 1101 | // select * from table where pk in (ids) and whereConditions 1102 | queryParamsObj.params.push(req.app.locals._tableName); 1103 | queryParamsObj.query += " where ?? in "; 1104 | queryParamsObj.params.push( 1105 | this.getPrimaryKeyName(req.app.locals._tableName) 1106 | ); 1107 | 1108 | queryParamsObj.query += "("; 1109 | 1110 | if (req.query && req.query._ids) { 1111 | let ids = req.query._ids.split(","); 1112 | for (var i = 0; i < ids.length; ++i) { 1113 | if (i) { 1114 | queryParamsObj.query += ","; 1115 | } 1116 | queryParamsObj.query += "?"; 1117 | queryParamsObj.params.push(ids[i]); 1118 | } 1119 | } 1120 | queryParamsObj.query += ") "; 1121 | this.getWhereClause( 1122 | req.query._where, 1123 | req.app.locals._tableName, 1124 | queryParamsObj, 1125 | " and " 1126 | ); 1127 | } else { 1128 | queryParamsObj.params.push(req.app.locals._tableName); 1129 | 1130 | /**************** add where clause ****************/ 1131 | this.getWhereClause( 1132 | req.query._where, 1133 | req.app.locals._tableName, 1134 | queryParamsObj, 1135 | " where " 1136 | ); 1137 | } 1138 | 1139 | this._getGrpByHavingOrderBy(req, req.app.locals._tableName, queryParamsObj); 1140 | 1141 | //console.log(queryParamsObj.query, queryParamsObj.params); 1142 | } 1143 | 1144 | _joinTableNames(isSecondJoin, joinTables, index, queryParamsObj) { 1145 | if (isSecondJoin) { 1146 | /** 1147 | * in second join - there will be ONE table and an ON condition 1148 | * if clause deals with this 1149 | * 1150 | */ 1151 | 1152 | // add : join / left join / right join / full join / inner join 1153 | queryParamsObj.query += this.getJoinType(joinTables[index]); 1154 | queryParamsObj.query += " ?? as ?? "; 1155 | 1156 | // eg: tbl.tableName 1157 | let tableNameAndAs = joinTables[index + 1].split("."); 1158 | 1159 | if ( 1160 | tableNameAndAs.length === 2 && 1161 | !(tableNameAndAs[1] in this.sqlConfig.ignoreTables) 1162 | ) { 1163 | queryParamsObj.params.push(tableNameAndAs[1]); 1164 | queryParamsObj.params.push(tableNameAndAs[0]); 1165 | } else { 1166 | queryParamsObj.grammarErr = 1; 1167 | console.log("there was no dot for tableName ", joinTables[index + 1]); 1168 | } 1169 | } else { 1170 | /** 1171 | * in first join - there will be TWO tables and an ON condition 1172 | * else clause deals with this 1173 | */ 1174 | 1175 | // first table 1176 | queryParamsObj.query += " ?? as ?? "; 1177 | // add : join / left join / right join / full join / inner join 1178 | queryParamsObj.query += this.getJoinType(joinTables[index + 1]); 1179 | // second table 1180 | queryParamsObj.query += " ?? as ?? "; 1181 | 1182 | let tableNameAndAs = joinTables[index].split("."); 1183 | if ( 1184 | tableNameAndAs.length === 2 && 1185 | !(tableNameAndAs[1] in this.sqlConfig.ignoreTables) 1186 | ) { 1187 | queryParamsObj.params.push(tableNameAndAs[1]); 1188 | queryParamsObj.params.push(tableNameAndAs[0]); 1189 | } else { 1190 | queryParamsObj.grammarErr = 1; 1191 | console.log("there was no dot for tableName ", joinTables[index]); 1192 | } 1193 | 1194 | tableNameAndAs = []; 1195 | tableNameAndAs = joinTables[index + 2].split("."); 1196 | if ( 1197 | tableNameAndAs.length === 2 && 1198 | !(tableNameAndAs[1] in this.sqlConfig.ignoreTables) 1199 | ) { 1200 | queryParamsObj.params.push(tableNameAndAs[1]); 1201 | queryParamsObj.params.push(tableNameAndAs[0]); 1202 | } else { 1203 | queryParamsObj.grammarErr = 1; 1204 | console.log("there was no dot for tableName ", joinTables[index]); 1205 | } 1206 | } 1207 | } 1208 | 1209 | prepareJoinQuery(req, res, queryParamsObj) { 1210 | queryParamsObj.query = "SELECT "; 1211 | queryParamsObj.grammarErr = 0; 1212 | 1213 | while (1) { 1214 | /**************** START : get fields ****************/ 1215 | if (req.query._fields) { 1216 | let fields = req.query._fields.split(","); 1217 | 1218 | // from _fields to - ??, ??, ?? [col1,col2,col3] 1219 | for (var i = 0; i < fields.length && !queryParamsObj.grammarErr; ++i) { 1220 | if (i) { 1221 | queryParamsObj.query += ","; 1222 | } 1223 | queryParamsObj.query += " ?? "; 1224 | queryParamsObj.params.push(fields[i]); 1225 | let aliases = fields[i].split("."); 1226 | if (aliases.length === 2) { 1227 | queryParamsObj.query += "as " + aliases[0] + "_" + aliases[1]; 1228 | //console.log(queryParamsObj.query); 1229 | } else { 1230 | queryParamsObj.grammarErr = 1; 1231 | } 1232 | } 1233 | } else { 1234 | queryParamsObj.grammarErr = 1; 1235 | } 1236 | 1237 | queryParamsObj.query += " from "; 1238 | 1239 | if (queryParamsObj.grammarErr) { 1240 | break; 1241 | } 1242 | 1243 | /**************** END : get fields ****************/ 1244 | 1245 | /**************** START : get join + on ****************/ 1246 | let joinTables = req.query._join.split(","); 1247 | if (joinTables.length < 3) { 1248 | //console.log('grammar error ', joinTables.length); 1249 | queryParamsObj.grammarErr = 1; 1250 | break; 1251 | } 1252 | 1253 | //console.log('jointables.length', joinTables); 1254 | 1255 | let onCondnCount = 0; 1256 | 1257 | for ( 1258 | let i = 0; 1259 | i < joinTables.length - 1 && queryParamsObj.grammarErr === 0; 1260 | i = i + 2 1261 | ) { 1262 | onCondnCount++; 1263 | 1264 | this._joinTableNames(i, joinTables, i, queryParamsObj); 1265 | 1266 | if (queryParamsObj.grammarErr) { 1267 | console.log("failed at _joinTableNames", queryParamsObj); 1268 | break; 1269 | } 1270 | 1271 | //console.log('after join tables', queryParamsObj); 1272 | 1273 | let onCondn = "_on" + onCondnCount; 1274 | let onCondnObj = {}; 1275 | if (onCondn in req.query) { 1276 | //console.log(onCondn, req.query[onCondn]); 1277 | onCondnObj = whereHelp.getConditionClause(req.query[onCondn], " on "); 1278 | //console.log('onCondnObj', onCondnObj); 1279 | queryParamsObj.query += " on " + onCondnObj.query; 1280 | queryParamsObj.params = queryParamsObj.params.concat( 1281 | onCondnObj.params 1282 | ); 1283 | } else { 1284 | queryParamsObj.grammarErr = 1; 1285 | //console.log('No on condition: ', onCondn); 1286 | break; 1287 | } 1288 | 1289 | if (i === 0) { 1290 | i = i + 1; 1291 | } 1292 | } 1293 | /**************** END : get join + on ****************/ 1294 | 1295 | if (queryParamsObj.grammarErr) { 1296 | break; 1297 | } else { 1298 | this.getWhereClause( 1299 | req.query._where, 1300 | " ignore ", 1301 | queryParamsObj, 1302 | " where " 1303 | ); 1304 | this._getGrpByHavingOrderBy(req, "ignore", queryParamsObj, 5); 1305 | //console.log('after where',queryParamsObj); 1306 | } 1307 | 1308 | break; 1309 | } 1310 | 1311 | if (queryParamsObj.grammarErr) { 1312 | queryParamsObj.query = ""; 1313 | queryParamsObj.params = []; 1314 | } 1315 | 1316 | return queryParamsObj; 1317 | } 1318 | } 1319 | 1320 | //expose class 1321 | module.exports = Xsql; 1322 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmysql", 3 | "version": "0.6.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.4", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", 10 | "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", 11 | "requires": { 12 | "mime-types": "~2.1.16", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "append-field": { 17 | "version": "0.1.0", 18 | "resolved": "https://registry.npmjs.org/append-field/-/append-field-0.1.0.tgz", 19 | "integrity": "sha1-bdxY+gg8e8VF08WZWygwzCNm1Eo=" 20 | }, 21 | "array-flatten": { 22 | "version": "1.1.1", 23 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 24 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 25 | }, 26 | "assert": { 27 | "version": "1.4.1", 28 | "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", 29 | "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", 30 | "requires": { 31 | "util": "0.10.3" 32 | } 33 | }, 34 | "asynckit": { 35 | "version": "0.4.0", 36 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 37 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", 38 | "dev": true 39 | }, 40 | "balanced-match": { 41 | "version": "1.0.0", 42 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 43 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 44 | "dev": true 45 | }, 46 | "basic-auth": { 47 | "version": "2.0.1", 48 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", 49 | "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", 50 | "requires": { 51 | "safe-buffer": "5.1.2" 52 | }, 53 | "dependencies": { 54 | "safe-buffer": { 55 | "version": "5.1.2", 56 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 57 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 58 | } 59 | } 60 | }, 61 | "bignumber.js": { 62 | "version": "9.0.0", 63 | "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", 64 | "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" 65 | }, 66 | "body-parser": { 67 | "version": "1.18.2", 68 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 69 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 70 | "requires": { 71 | "bytes": "3.0.0", 72 | "content-type": "~1.0.4", 73 | "debug": "2.6.9", 74 | "depd": "~1.1.1", 75 | "http-errors": "~1.6.2", 76 | "iconv-lite": "0.4.19", 77 | "on-finished": "~2.3.0", 78 | "qs": "6.5.1", 79 | "raw-body": "2.3.2", 80 | "type-is": "~1.6.15" 81 | } 82 | }, 83 | "brace-expansion": { 84 | "version": "1.1.11", 85 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 86 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 87 | "dev": true, 88 | "requires": { 89 | "balanced-match": "^1.0.0", 90 | "concat-map": "0.0.1" 91 | } 92 | }, 93 | "browser-stdout": { 94 | "version": "1.3.1", 95 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 96 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 97 | "dev": true 98 | }, 99 | "busboy": { 100 | "version": "0.2.14", 101 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", 102 | "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", 103 | "requires": { 104 | "dicer": "0.2.5", 105 | "readable-stream": "1.1.x" 106 | }, 107 | "dependencies": { 108 | "isarray": { 109 | "version": "0.0.1", 110 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 111 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 112 | }, 113 | "readable-stream": { 114 | "version": "1.1.14", 115 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 116 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 117 | "requires": { 118 | "core-util-is": "~1.0.0", 119 | "inherits": "~2.0.1", 120 | "isarray": "0.0.1", 121 | "string_decoder": "~0.10.x" 122 | } 123 | }, 124 | "string_decoder": { 125 | "version": "0.10.31", 126 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 127 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" 128 | } 129 | } 130 | }, 131 | "bytes": { 132 | "version": "3.0.0", 133 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 134 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 135 | }, 136 | "cluster": { 137 | "version": "0.7.7", 138 | "resolved": "https://registry.npmjs.org/cluster/-/cluster-0.7.7.tgz", 139 | "integrity": "sha1-5JfiZ8yVa9CwUTrbSqOTNX0Ahe8=", 140 | "requires": { 141 | "log": ">= 1.2.0", 142 | "mkdirp": ">= 0.0.1" 143 | } 144 | }, 145 | "colors": { 146 | "version": "1.1.2", 147 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", 148 | "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" 149 | }, 150 | "combined-stream": { 151 | "version": "1.0.5", 152 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", 153 | "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", 154 | "dev": true, 155 | "requires": { 156 | "delayed-stream": "~1.0.0" 157 | } 158 | }, 159 | "commander": { 160 | "version": "2.11.0", 161 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", 162 | "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" 163 | }, 164 | "component-emitter": { 165 | "version": "1.2.1", 166 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", 167 | "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", 168 | "dev": true 169 | }, 170 | "concat-map": { 171 | "version": "0.0.1", 172 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 173 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 174 | "dev": true 175 | }, 176 | "concat-stream": { 177 | "version": "1.6.0", 178 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", 179 | "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", 180 | "requires": { 181 | "inherits": "^2.0.3", 182 | "readable-stream": "^2.2.2", 183 | "typedarray": "^0.0.6" 184 | } 185 | }, 186 | "contains-path": { 187 | "version": "0.1.0", 188 | "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", 189 | "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", 190 | "dev": true 191 | }, 192 | "content-disposition": { 193 | "version": "0.5.2", 194 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 195 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 196 | }, 197 | "content-type": { 198 | "version": "1.0.4", 199 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 200 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 201 | }, 202 | "cookie": { 203 | "version": "0.3.1", 204 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 205 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 206 | }, 207 | "cookie-signature": { 208 | "version": "1.0.6", 209 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 210 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 211 | }, 212 | "cookiejar": { 213 | "version": "2.1.1", 214 | "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz", 215 | "integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o=", 216 | "dev": true 217 | }, 218 | "core-util-is": { 219 | "version": "1.0.2", 220 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 221 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 222 | }, 223 | "cors": { 224 | "version": "2.8.4", 225 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.4.tgz", 226 | "integrity": "sha1-K9OB8usgECAQXNUOpZ2mMJBpRoY=", 227 | "requires": { 228 | "object-assign": "^4", 229 | "vary": "^1" 230 | }, 231 | "dependencies": { 232 | "object-assign": { 233 | "version": "4.1.1", 234 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 235 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 236 | } 237 | } 238 | }, 239 | "debug": { 240 | "version": "2.6.9", 241 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 242 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 243 | "requires": { 244 | "ms": "2.0.0" 245 | } 246 | }, 247 | "define-properties": { 248 | "version": "1.1.3", 249 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 250 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 251 | "dev": true, 252 | "requires": { 253 | "object-keys": "^1.0.12" 254 | } 255 | }, 256 | "delayed-stream": { 257 | "version": "1.0.0", 258 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 259 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 260 | "dev": true 261 | }, 262 | "depd": { 263 | "version": "1.1.1", 264 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 265 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 266 | }, 267 | "destroy": { 268 | "version": "1.0.4", 269 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 270 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 271 | }, 272 | "dicer": { 273 | "version": "0.2.5", 274 | "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", 275 | "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", 276 | "requires": { 277 | "readable-stream": "1.1.x", 278 | "streamsearch": "0.1.2" 279 | }, 280 | "dependencies": { 281 | "isarray": { 282 | "version": "0.0.1", 283 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 284 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 285 | }, 286 | "readable-stream": { 287 | "version": "1.1.14", 288 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 289 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 290 | "requires": { 291 | "core-util-is": "~1.0.0", 292 | "inherits": "~2.0.1", 293 | "isarray": "0.0.1", 294 | "string_decoder": "~0.10.x" 295 | } 296 | }, 297 | "string_decoder": { 298 | "version": "0.10.31", 299 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 300 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" 301 | } 302 | } 303 | }, 304 | "diff": { 305 | "version": "3.5.0", 306 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 307 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 308 | "dev": true 309 | }, 310 | "doctrine": { 311 | "version": "1.5.0", 312 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", 313 | "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", 314 | "dev": true, 315 | "requires": { 316 | "esutils": "^2.0.2", 317 | "isarray": "^1.0.0" 318 | } 319 | }, 320 | "ee-first": { 321 | "version": "1.1.1", 322 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 323 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 324 | }, 325 | "encodeurl": { 326 | "version": "1.0.1", 327 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", 328 | "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" 329 | }, 330 | "error-ex": { 331 | "version": "1.3.2", 332 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", 333 | "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", 334 | "dev": true, 335 | "requires": { 336 | "is-arrayish": "^0.2.1" 337 | } 338 | }, 339 | "es-abstract": { 340 | "version": "1.13.0", 341 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", 342 | "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", 343 | "dev": true, 344 | "requires": { 345 | "es-to-primitive": "^1.2.0", 346 | "function-bind": "^1.1.1", 347 | "has": "^1.0.3", 348 | "is-callable": "^1.1.4", 349 | "is-regex": "^1.0.4", 350 | "object-keys": "^1.0.12" 351 | } 352 | }, 353 | "es-to-primitive": { 354 | "version": "1.2.0", 355 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", 356 | "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", 357 | "dev": true, 358 | "requires": { 359 | "is-callable": "^1.1.4", 360 | "is-date-object": "^1.0.1", 361 | "is-symbol": "^1.0.2" 362 | } 363 | }, 364 | "escape-html": { 365 | "version": "1.0.3", 366 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 367 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 368 | }, 369 | "escape-string-regexp": { 370 | "version": "1.0.5", 371 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 372 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 373 | "dev": true 374 | }, 375 | "eslint-config-airbnb-base": { 376 | "version": "13.1.0", 377 | "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-13.1.0.tgz", 378 | "integrity": "sha512-XWwQtf3U3zIoKO1BbHh6aUhJZQweOwSt4c2JrPDg9FP3Ltv3+YfEv7jIDB8275tVnO/qOHbfuYg3kzw6Je7uWw==", 379 | "dev": true, 380 | "requires": { 381 | "eslint-restricted-globals": "^0.1.1", 382 | "object.assign": "^4.1.0", 383 | "object.entries": "^1.0.4" 384 | } 385 | }, 386 | "eslint-config-prettier": { 387 | "version": "4.1.0", 388 | "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-4.1.0.tgz", 389 | "integrity": "sha512-zILwX9/Ocz4SV2vX7ox85AsrAgXV3f2o2gpIicdMIOra48WYqgUnWNH/cR/iHtmD2Vb3dLSC3LiEJnS05Gkw7w==", 390 | "dev": true, 391 | "requires": { 392 | "get-stdin": "^6.0.0" 393 | } 394 | }, 395 | "eslint-import-resolver-node": { 396 | "version": "0.3.2", 397 | "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", 398 | "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", 399 | "dev": true, 400 | "requires": { 401 | "debug": "^2.6.9", 402 | "resolve": "^1.5.0" 403 | } 404 | }, 405 | "eslint-module-utils": { 406 | "version": "2.3.0", 407 | "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz", 408 | "integrity": "sha512-lmDJgeOOjk8hObTysjqH7wyMi+nsHwwvfBykwfhjR1LNdd7C2uFJBvx4OpWYpXOw4df1yE1cDEVd1yLHitk34w==", 409 | "dev": true, 410 | "requires": { 411 | "debug": "^2.6.8", 412 | "pkg-dir": "^2.0.0" 413 | } 414 | }, 415 | "eslint-plugin-import": { 416 | "version": "2.16.0", 417 | "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz", 418 | "integrity": "sha512-z6oqWlf1x5GkHIFgrSvtmudnqM6Q60KM4KvpWi5ubonMjycLjndvd5+8VAZIsTlHC03djdgJuyKG6XO577px6A==", 419 | "dev": true, 420 | "requires": { 421 | "contains-path": "^0.1.0", 422 | "debug": "^2.6.9", 423 | "doctrine": "1.5.0", 424 | "eslint-import-resolver-node": "^0.3.2", 425 | "eslint-module-utils": "^2.3.0", 426 | "has": "^1.0.3", 427 | "lodash": "^4.17.11", 428 | "minimatch": "^3.0.4", 429 | "read-pkg-up": "^2.0.0", 430 | "resolve": "^1.9.0" 431 | } 432 | }, 433 | "eslint-plugin-prettier": { 434 | "version": "3.0.1", 435 | "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.0.1.tgz", 436 | "integrity": "sha512-/PMttrarPAY78PLvV3xfWibMOdMDl57hmlQ2XqFeA37wd+CJ7WSxV7txqjVPHi/AAFKd2lX0ZqfsOc/i5yFCSQ==", 437 | "dev": true, 438 | "requires": { 439 | "prettier-linter-helpers": "^1.0.0" 440 | } 441 | }, 442 | "eslint-restricted-globals": { 443 | "version": "0.1.1", 444 | "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz", 445 | "integrity": "sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=", 446 | "dev": true 447 | }, 448 | "esutils": { 449 | "version": "2.0.2", 450 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 451 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", 452 | "dev": true 453 | }, 454 | "etag": { 455 | "version": "1.8.1", 456 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 457 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 458 | }, 459 | "express": { 460 | "version": "4.16.1", 461 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.1.tgz", 462 | "integrity": "sha512-STB7LZ4N0L+81FJHGla2oboUHTk4PaN1RsOkoRh9OSeEKylvF5hwKYVX1xCLFaCT7MD0BNG/gX2WFMLqY6EMBw==", 463 | "requires": { 464 | "accepts": "~1.3.4", 465 | "array-flatten": "1.1.1", 466 | "body-parser": "1.18.2", 467 | "content-disposition": "0.5.2", 468 | "content-type": "~1.0.4", 469 | "cookie": "0.3.1", 470 | "cookie-signature": "1.0.6", 471 | "debug": "2.6.9", 472 | "depd": "~1.1.1", 473 | "encodeurl": "~1.0.1", 474 | "escape-html": "~1.0.3", 475 | "etag": "~1.8.1", 476 | "finalhandler": "1.1.0", 477 | "fresh": "0.5.2", 478 | "merge-descriptors": "1.0.1", 479 | "methods": "~1.1.2", 480 | "on-finished": "~2.3.0", 481 | "parseurl": "~1.3.2", 482 | "path-to-regexp": "0.1.7", 483 | "proxy-addr": "~2.0.2", 484 | "qs": "6.5.1", 485 | "range-parser": "~1.2.0", 486 | "safe-buffer": "5.1.1", 487 | "send": "0.16.1", 488 | "serve-static": "1.13.1", 489 | "setprototypeof": "1.1.0", 490 | "statuses": "~1.3.1", 491 | "type-is": "~1.6.15", 492 | "utils-merge": "1.0.1", 493 | "vary": "~1.1.2" 494 | } 495 | }, 496 | "extend": { 497 | "version": "3.0.2", 498 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 499 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 500 | "dev": true 501 | }, 502 | "fast-diff": { 503 | "version": "1.2.0", 504 | "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", 505 | "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", 506 | "dev": true 507 | }, 508 | "finalhandler": { 509 | "version": "1.1.0", 510 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", 511 | "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", 512 | "requires": { 513 | "debug": "2.6.9", 514 | "encodeurl": "~1.0.1", 515 | "escape-html": "~1.0.3", 516 | "on-finished": "~2.3.0", 517 | "parseurl": "~1.3.2", 518 | "statuses": "~1.3.1", 519 | "unpipe": "~1.0.0" 520 | } 521 | }, 522 | "find-up": { 523 | "version": "2.1.0", 524 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", 525 | "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", 526 | "dev": true, 527 | "requires": { 528 | "locate-path": "^2.0.0" 529 | } 530 | }, 531 | "form-data": { 532 | "version": "2.3.1", 533 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", 534 | "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", 535 | "dev": true, 536 | "requires": { 537 | "asynckit": "^0.4.0", 538 | "combined-stream": "^1.0.5", 539 | "mime-types": "^2.1.12" 540 | } 541 | }, 542 | "forwarded": { 543 | "version": "0.1.2", 544 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 545 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 546 | }, 547 | "fresh": { 548 | "version": "0.5.2", 549 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 550 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 551 | }, 552 | "fs.realpath": { 553 | "version": "1.0.0", 554 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 555 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 556 | "dev": true 557 | }, 558 | "function-bind": { 559 | "version": "1.1.1", 560 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 561 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 562 | "dev": true 563 | }, 564 | "get-stdin": { 565 | "version": "6.0.0", 566 | "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", 567 | "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", 568 | "dev": true 569 | }, 570 | "glob": { 571 | "version": "7.1.2", 572 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 573 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 574 | "dev": true, 575 | "requires": { 576 | "fs.realpath": "^1.0.0", 577 | "inflight": "^1.0.4", 578 | "inherits": "2", 579 | "minimatch": "^3.0.4", 580 | "once": "^1.3.0", 581 | "path-is-absolute": "^1.0.0" 582 | } 583 | }, 584 | "graceful-fs": { 585 | "version": "4.1.15", 586 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", 587 | "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", 588 | "dev": true 589 | }, 590 | "growl": { 591 | "version": "1.10.5", 592 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 593 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 594 | "dev": true 595 | }, 596 | "has": { 597 | "version": "1.0.3", 598 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 599 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 600 | "dev": true, 601 | "requires": { 602 | "function-bind": "^1.1.1" 603 | } 604 | }, 605 | "has-flag": { 606 | "version": "3.0.0", 607 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 608 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 609 | "dev": true 610 | }, 611 | "has-symbols": { 612 | "version": "1.0.0", 613 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", 614 | "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", 615 | "dev": true 616 | }, 617 | "he": { 618 | "version": "1.1.1", 619 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 620 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 621 | "dev": true 622 | }, 623 | "hosted-git-info": { 624 | "version": "2.7.1", 625 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", 626 | "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", 627 | "dev": true 628 | }, 629 | "http-errors": { 630 | "version": "1.6.2", 631 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 632 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 633 | "requires": { 634 | "depd": "1.1.1", 635 | "inherits": "2.0.3", 636 | "setprototypeof": "1.0.3", 637 | "statuses": ">= 1.3.1 < 2" 638 | }, 639 | "dependencies": { 640 | "setprototypeof": { 641 | "version": "1.0.3", 642 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 643 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 644 | } 645 | } 646 | }, 647 | "iconv-lite": { 648 | "version": "0.4.19", 649 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 650 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 651 | }, 652 | "inflight": { 653 | "version": "1.0.6", 654 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 655 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 656 | "dev": true, 657 | "requires": { 658 | "once": "^1.3.0", 659 | "wrappy": "1" 660 | } 661 | }, 662 | "inherits": { 663 | "version": "2.0.3", 664 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 665 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 666 | }, 667 | "ipaddr.js": { 668 | "version": "1.5.2", 669 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", 670 | "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" 671 | }, 672 | "is-arrayish": { 673 | "version": "0.2.1", 674 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 675 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", 676 | "dev": true 677 | }, 678 | "is-callable": { 679 | "version": "1.1.4", 680 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", 681 | "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", 682 | "dev": true 683 | }, 684 | "is-date-object": { 685 | "version": "1.0.1", 686 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 687 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 688 | "dev": true 689 | }, 690 | "is-regex": { 691 | "version": "1.0.4", 692 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 693 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 694 | "dev": true, 695 | "requires": { 696 | "has": "^1.0.1" 697 | } 698 | }, 699 | "is-symbol": { 700 | "version": "1.0.2", 701 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", 702 | "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", 703 | "dev": true, 704 | "requires": { 705 | "has-symbols": "^1.0.0" 706 | } 707 | }, 708 | "isarray": { 709 | "version": "1.0.0", 710 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 711 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 712 | }, 713 | "load-json-file": { 714 | "version": "2.0.0", 715 | "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", 716 | "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", 717 | "dev": true, 718 | "requires": { 719 | "graceful-fs": "^4.1.2", 720 | "parse-json": "^2.2.0", 721 | "pify": "^2.0.0", 722 | "strip-bom": "^3.0.0" 723 | } 724 | }, 725 | "locate-path": { 726 | "version": "2.0.0", 727 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", 728 | "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", 729 | "dev": true, 730 | "requires": { 731 | "p-locate": "^2.0.0", 732 | "path-exists": "^3.0.0" 733 | } 734 | }, 735 | "lodash": { 736 | "version": "4.17.15", 737 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 738 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", 739 | "dev": true 740 | }, 741 | "log": { 742 | "version": "1.4.0", 743 | "resolved": "https://registry.npmjs.org/log/-/log-1.4.0.tgz", 744 | "integrity": "sha1-S6HYkP3iSbAx3KA7w36q8yVlbxw=" 745 | }, 746 | "media-typer": { 747 | "version": "0.3.0", 748 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 749 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 750 | }, 751 | "merge-descriptors": { 752 | "version": "1.0.1", 753 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 754 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 755 | }, 756 | "methods": { 757 | "version": "1.1.2", 758 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 759 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 760 | }, 761 | "mime": { 762 | "version": "1.4.1", 763 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 764 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 765 | }, 766 | "mime-db": { 767 | "version": "1.30.0", 768 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", 769 | "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" 770 | }, 771 | "mime-types": { 772 | "version": "2.1.17", 773 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", 774 | "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", 775 | "requires": { 776 | "mime-db": "~1.30.0" 777 | } 778 | }, 779 | "minimatch": { 780 | "version": "3.0.4", 781 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 782 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 783 | "dev": true, 784 | "requires": { 785 | "brace-expansion": "^1.1.7" 786 | } 787 | }, 788 | "minimist": { 789 | "version": "0.0.8", 790 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 791 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 792 | }, 793 | "mkdirp": { 794 | "version": "0.5.1", 795 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 796 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 797 | "requires": { 798 | "minimist": "0.0.8" 799 | } 800 | }, 801 | "mocha": { 802 | "version": "5.2.0", 803 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 804 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 805 | "dev": true, 806 | "requires": { 807 | "browser-stdout": "1.3.1", 808 | "commander": "2.15.1", 809 | "debug": "3.1.0", 810 | "diff": "3.5.0", 811 | "escape-string-regexp": "1.0.5", 812 | "glob": "7.1.2", 813 | "growl": "1.10.5", 814 | "he": "1.1.1", 815 | "minimatch": "3.0.4", 816 | "mkdirp": "0.5.1", 817 | "supports-color": "5.4.0" 818 | }, 819 | "dependencies": { 820 | "commander": { 821 | "version": "2.15.1", 822 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 823 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 824 | "dev": true 825 | }, 826 | "debug": { 827 | "version": "3.1.0", 828 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 829 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 830 | "dev": true, 831 | "requires": { 832 | "ms": "2.0.0" 833 | } 834 | } 835 | } 836 | }, 837 | "morgan": { 838 | "version": "1.9.1", 839 | "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", 840 | "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", 841 | "requires": { 842 | "basic-auth": "~2.0.0", 843 | "debug": "2.6.9", 844 | "depd": "~1.1.2", 845 | "on-finished": "~2.3.0", 846 | "on-headers": "~1.0.1" 847 | }, 848 | "dependencies": { 849 | "depd": { 850 | "version": "1.1.2", 851 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 852 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 853 | } 854 | } 855 | }, 856 | "ms": { 857 | "version": "2.0.0", 858 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 859 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 860 | }, 861 | "multer": { 862 | "version": "1.3.0", 863 | "resolved": "https://registry.npmjs.org/multer/-/multer-1.3.0.tgz", 864 | "integrity": "sha1-CSsmcPaEb6SRSWXvyM+Uwg/sbNI=", 865 | "requires": { 866 | "append-field": "^0.1.0", 867 | "busboy": "^0.2.11", 868 | "concat-stream": "^1.5.0", 869 | "mkdirp": "^0.5.1", 870 | "object-assign": "^3.0.0", 871 | "on-finished": "^2.3.0", 872 | "type-is": "^1.6.4", 873 | "xtend": "^4.0.0" 874 | } 875 | }, 876 | "mysql": { 877 | "version": "2.18.1", 878 | "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", 879 | "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", 880 | "requires": { 881 | "bignumber.js": "9.0.0", 882 | "readable-stream": "2.3.7", 883 | "safe-buffer": "5.1.2", 884 | "sqlstring": "2.3.1" 885 | }, 886 | "dependencies": { 887 | "process-nextick-args": { 888 | "version": "2.0.1", 889 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 890 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 891 | }, 892 | "readable-stream": { 893 | "version": "2.3.7", 894 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 895 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 896 | "requires": { 897 | "core-util-is": "~1.0.0", 898 | "inherits": "~2.0.3", 899 | "isarray": "~1.0.0", 900 | "process-nextick-args": "~2.0.0", 901 | "safe-buffer": "~5.1.1", 902 | "string_decoder": "~1.1.1", 903 | "util-deprecate": "~1.0.1" 904 | } 905 | }, 906 | "safe-buffer": { 907 | "version": "5.1.2", 908 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 909 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 910 | }, 911 | "string_decoder": { 912 | "version": "1.1.1", 913 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 914 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 915 | "requires": { 916 | "safe-buffer": "~5.1.0" 917 | } 918 | } 919 | } 920 | }, 921 | "nan": { 922 | "version": "2.14.0", 923 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", 924 | "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", 925 | "dev": true 926 | }, 927 | "negotiator": { 928 | "version": "0.6.1", 929 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 930 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 931 | }, 932 | "normalize-package-data": { 933 | "version": "2.5.0", 934 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", 935 | "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", 936 | "dev": true, 937 | "requires": { 938 | "hosted-git-info": "^2.1.4", 939 | "resolve": "^1.10.0", 940 | "semver": "2 || 3 || 4 || 5", 941 | "validate-npm-package-license": "^3.0.1" 942 | } 943 | }, 944 | "object-assign": { 945 | "version": "3.0.0", 946 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", 947 | "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=" 948 | }, 949 | "object-keys": { 950 | "version": "1.1.0", 951 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", 952 | "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==", 953 | "dev": true 954 | }, 955 | "object.assign": { 956 | "version": "4.1.0", 957 | "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", 958 | "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", 959 | "dev": true, 960 | "requires": { 961 | "define-properties": "^1.1.2", 962 | "function-bind": "^1.1.1", 963 | "has-symbols": "^1.0.0", 964 | "object-keys": "^1.0.11" 965 | } 966 | }, 967 | "object.entries": { 968 | "version": "1.1.0", 969 | "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", 970 | "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", 971 | "dev": true, 972 | "requires": { 973 | "define-properties": "^1.1.3", 974 | "es-abstract": "^1.12.0", 975 | "function-bind": "^1.1.1", 976 | "has": "^1.0.3" 977 | } 978 | }, 979 | "on-finished": { 980 | "version": "2.3.0", 981 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 982 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 983 | "requires": { 984 | "ee-first": "1.1.1" 985 | } 986 | }, 987 | "on-headers": { 988 | "version": "1.0.2", 989 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", 990 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" 991 | }, 992 | "once": { 993 | "version": "1.4.0", 994 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 995 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 996 | "dev": true, 997 | "requires": { 998 | "wrappy": "1" 999 | } 1000 | }, 1001 | "p-limit": { 1002 | "version": "1.3.0", 1003 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", 1004 | "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", 1005 | "dev": true, 1006 | "requires": { 1007 | "p-try": "^1.0.0" 1008 | } 1009 | }, 1010 | "p-locate": { 1011 | "version": "2.0.0", 1012 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", 1013 | "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", 1014 | "dev": true, 1015 | "requires": { 1016 | "p-limit": "^1.1.0" 1017 | } 1018 | }, 1019 | "p-try": { 1020 | "version": "1.0.0", 1021 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", 1022 | "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", 1023 | "dev": true 1024 | }, 1025 | "parse-json": { 1026 | "version": "2.2.0", 1027 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", 1028 | "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", 1029 | "dev": true, 1030 | "requires": { 1031 | "error-ex": "^1.2.0" 1032 | } 1033 | }, 1034 | "parseurl": { 1035 | "version": "1.3.2", 1036 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 1037 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 1038 | }, 1039 | "path-exists": { 1040 | "version": "3.0.0", 1041 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 1042 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", 1043 | "dev": true 1044 | }, 1045 | "path-is-absolute": { 1046 | "version": "1.0.1", 1047 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1048 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 1049 | "dev": true 1050 | }, 1051 | "path-parse": { 1052 | "version": "1.0.6", 1053 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 1054 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 1055 | "dev": true 1056 | }, 1057 | "path-to-regexp": { 1058 | "version": "0.1.7", 1059 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 1060 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 1061 | }, 1062 | "path-type": { 1063 | "version": "2.0.0", 1064 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", 1065 | "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", 1066 | "dev": true, 1067 | "requires": { 1068 | "pify": "^2.0.0" 1069 | } 1070 | }, 1071 | "pify": { 1072 | "version": "2.3.0", 1073 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 1074 | "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", 1075 | "dev": true 1076 | }, 1077 | "pkg-dir": { 1078 | "version": "2.0.0", 1079 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", 1080 | "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", 1081 | "dev": true, 1082 | "requires": { 1083 | "find-up": "^2.1.0" 1084 | } 1085 | }, 1086 | "prettier-linter-helpers": { 1087 | "version": "1.0.0", 1088 | "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", 1089 | "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", 1090 | "dev": true, 1091 | "requires": { 1092 | "fast-diff": "^1.1.2" 1093 | } 1094 | }, 1095 | "process-nextick-args": { 1096 | "version": "1.0.7", 1097 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", 1098 | "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" 1099 | }, 1100 | "proxy-addr": { 1101 | "version": "2.0.2", 1102 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", 1103 | "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", 1104 | "requires": { 1105 | "forwarded": "~0.1.2", 1106 | "ipaddr.js": "1.5.2" 1107 | } 1108 | }, 1109 | "qs": { 1110 | "version": "6.5.1", 1111 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 1112 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 1113 | }, 1114 | "range-parser": { 1115 | "version": "1.2.0", 1116 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 1117 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 1118 | }, 1119 | "raw-body": { 1120 | "version": "2.3.2", 1121 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 1122 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 1123 | "requires": { 1124 | "bytes": "3.0.0", 1125 | "http-errors": "1.6.2", 1126 | "iconv-lite": "0.4.19", 1127 | "unpipe": "1.0.0" 1128 | } 1129 | }, 1130 | "read-pkg": { 1131 | "version": "2.0.0", 1132 | "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", 1133 | "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", 1134 | "dev": true, 1135 | "requires": { 1136 | "load-json-file": "^2.0.0", 1137 | "normalize-package-data": "^2.3.2", 1138 | "path-type": "^2.0.0" 1139 | } 1140 | }, 1141 | "read-pkg-up": { 1142 | "version": "2.0.0", 1143 | "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", 1144 | "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", 1145 | "dev": true, 1146 | "requires": { 1147 | "find-up": "^2.0.0", 1148 | "read-pkg": "^2.0.0" 1149 | } 1150 | }, 1151 | "readable-stream": { 1152 | "version": "2.3.3", 1153 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", 1154 | "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", 1155 | "requires": { 1156 | "core-util-is": "~1.0.0", 1157 | "inherits": "~2.0.3", 1158 | "isarray": "~1.0.0", 1159 | "process-nextick-args": "~1.0.6", 1160 | "safe-buffer": "~5.1.1", 1161 | "string_decoder": "~1.0.3", 1162 | "util-deprecate": "~1.0.1" 1163 | } 1164 | }, 1165 | "resolve": { 1166 | "version": "1.10.0", 1167 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", 1168 | "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", 1169 | "dev": true, 1170 | "requires": { 1171 | "path-parse": "^1.0.6" 1172 | } 1173 | }, 1174 | "safe-buffer": { 1175 | "version": "5.1.1", 1176 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 1177 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 1178 | }, 1179 | "semver": { 1180 | "version": "5.7.0", 1181 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", 1182 | "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", 1183 | "dev": true 1184 | }, 1185 | "send": { 1186 | "version": "0.16.1", 1187 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", 1188 | "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", 1189 | "requires": { 1190 | "debug": "2.6.9", 1191 | "depd": "~1.1.1", 1192 | "destroy": "~1.0.4", 1193 | "encodeurl": "~1.0.1", 1194 | "escape-html": "~1.0.3", 1195 | "etag": "~1.8.1", 1196 | "fresh": "0.5.2", 1197 | "http-errors": "~1.6.2", 1198 | "mime": "1.4.1", 1199 | "ms": "2.0.0", 1200 | "on-finished": "~2.3.0", 1201 | "range-parser": "~1.2.0", 1202 | "statuses": "~1.3.1" 1203 | } 1204 | }, 1205 | "serve-static": { 1206 | "version": "1.13.1", 1207 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", 1208 | "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", 1209 | "requires": { 1210 | "encodeurl": "~1.0.1", 1211 | "escape-html": "~1.0.3", 1212 | "parseurl": "~1.3.2", 1213 | "send": "0.16.1" 1214 | } 1215 | }, 1216 | "setprototypeof": { 1217 | "version": "1.1.0", 1218 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 1219 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 1220 | }, 1221 | "should": { 1222 | "version": "13.1.2", 1223 | "resolved": "https://registry.npmjs.org/should/-/should-13.1.2.tgz", 1224 | "integrity": "sha512-oiGqKOuE4t98vdCs4ICifvzL2u9nWMaziSXVwHOYPyqqY1gBzGZS6LvzIc5uEFN0PiS69Sbvcqyw9hbYXkF4og==", 1225 | "dev": true, 1226 | "requires": { 1227 | "should-equal": "^2.0.0", 1228 | "should-format": "^3.0.3", 1229 | "should-type": "^1.4.0", 1230 | "should-type-adaptors": "^1.0.1", 1231 | "should-util": "^1.0.0" 1232 | } 1233 | }, 1234 | "should-equal": { 1235 | "version": "2.0.0", 1236 | "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", 1237 | "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", 1238 | "dev": true, 1239 | "requires": { 1240 | "should-type": "^1.4.0" 1241 | } 1242 | }, 1243 | "should-format": { 1244 | "version": "3.0.3", 1245 | "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", 1246 | "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", 1247 | "dev": true, 1248 | "requires": { 1249 | "should-type": "^1.3.0", 1250 | "should-type-adaptors": "^1.0.1" 1251 | } 1252 | }, 1253 | "should-type": { 1254 | "version": "1.4.0", 1255 | "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", 1256 | "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=", 1257 | "dev": true 1258 | }, 1259 | "should-type-adaptors": { 1260 | "version": "1.0.1", 1261 | "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.0.1.tgz", 1262 | "integrity": "sha1-7+VVPN9oz/ZuXF9RtxLcNRx3vqo=", 1263 | "dev": true, 1264 | "requires": { 1265 | "should-type": "^1.3.0", 1266 | "should-util": "^1.0.0" 1267 | } 1268 | }, 1269 | "should-util": { 1270 | "version": "1.0.0", 1271 | "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz", 1272 | "integrity": "sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=", 1273 | "dev": true 1274 | }, 1275 | "sleep": { 1276 | "version": "6.1.0", 1277 | "resolved": "https://registry.npmjs.org/sleep/-/sleep-6.1.0.tgz", 1278 | "integrity": "sha512-Z1x4JjJxsru75Tqn8F4tnOFeEu3HjtITTsumYUiuz54sGKdISgLCek9AUlXlVVrkhltRFhNUsJDJE76SFHTDIQ==", 1279 | "dev": true, 1280 | "requires": { 1281 | "nan": "^2.13.2" 1282 | } 1283 | }, 1284 | "spdx-correct": { 1285 | "version": "3.1.0", 1286 | "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", 1287 | "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", 1288 | "dev": true, 1289 | "requires": { 1290 | "spdx-expression-parse": "^3.0.0", 1291 | "spdx-license-ids": "^3.0.0" 1292 | } 1293 | }, 1294 | "spdx-exceptions": { 1295 | "version": "2.2.0", 1296 | "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", 1297 | "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", 1298 | "dev": true 1299 | }, 1300 | "spdx-expression-parse": { 1301 | "version": "3.0.0", 1302 | "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", 1303 | "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", 1304 | "dev": true, 1305 | "requires": { 1306 | "spdx-exceptions": "^2.1.0", 1307 | "spdx-license-ids": "^3.0.0" 1308 | } 1309 | }, 1310 | "spdx-license-ids": { 1311 | "version": "3.0.3", 1312 | "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz", 1313 | "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==", 1314 | "dev": true 1315 | }, 1316 | "sqlstring": { 1317 | "version": "2.3.1", 1318 | "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", 1319 | "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" 1320 | }, 1321 | "statuses": { 1322 | "version": "1.3.1", 1323 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 1324 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 1325 | }, 1326 | "streamsearch": { 1327 | "version": "0.1.2", 1328 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", 1329 | "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" 1330 | }, 1331 | "string_decoder": { 1332 | "version": "1.0.3", 1333 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", 1334 | "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", 1335 | "requires": { 1336 | "safe-buffer": "~5.1.0" 1337 | } 1338 | }, 1339 | "strip-bom": { 1340 | "version": "3.0.0", 1341 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 1342 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 1343 | "dev": true 1344 | }, 1345 | "superagent": { 1346 | "version": "3.8.3", 1347 | "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", 1348 | "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", 1349 | "dev": true, 1350 | "requires": { 1351 | "component-emitter": "^1.2.0", 1352 | "cookiejar": "^2.1.0", 1353 | "debug": "^3.1.0", 1354 | "extend": "^3.0.0", 1355 | "form-data": "^2.3.1", 1356 | "formidable": "^1.2.0", 1357 | "methods": "^1.1.1", 1358 | "mime": "^1.4.1", 1359 | "qs": "^6.5.1", 1360 | "readable-stream": "^2.3.5" 1361 | }, 1362 | "dependencies": { 1363 | "debug": { 1364 | "version": "3.2.6", 1365 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 1366 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 1367 | "dev": true, 1368 | "requires": { 1369 | "ms": "^2.1.1" 1370 | } 1371 | }, 1372 | "formidable": { 1373 | "version": "1.2.1", 1374 | "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", 1375 | "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", 1376 | "dev": true 1377 | }, 1378 | "ms": { 1379 | "version": "2.1.2", 1380 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1381 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1382 | "dev": true 1383 | }, 1384 | "process-nextick-args": { 1385 | "version": "2.0.1", 1386 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 1387 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", 1388 | "dev": true 1389 | }, 1390 | "readable-stream": { 1391 | "version": "2.3.6", 1392 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 1393 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 1394 | "dev": true, 1395 | "requires": { 1396 | "core-util-is": "~1.0.0", 1397 | "inherits": "~2.0.3", 1398 | "isarray": "~1.0.0", 1399 | "process-nextick-args": "~2.0.0", 1400 | "safe-buffer": "~5.1.1", 1401 | "string_decoder": "~1.1.1", 1402 | "util-deprecate": "~1.0.1" 1403 | } 1404 | }, 1405 | "string_decoder": { 1406 | "version": "1.1.1", 1407 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1408 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1409 | "dev": true, 1410 | "requires": { 1411 | "safe-buffer": "~5.1.0" 1412 | } 1413 | } 1414 | } 1415 | }, 1416 | "supertest": { 1417 | "version": "3.0.0", 1418 | "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.0.0.tgz", 1419 | "integrity": "sha1-jUu2j9GDDuBwM7HFpamkAhyWUpY=", 1420 | "dev": true, 1421 | "requires": { 1422 | "methods": "~1.1.2", 1423 | "superagent": "^3.0.0" 1424 | } 1425 | }, 1426 | "supports-color": { 1427 | "version": "5.4.0", 1428 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 1429 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 1430 | "dev": true, 1431 | "requires": { 1432 | "has-flag": "^3.0.0" 1433 | } 1434 | }, 1435 | "type-is": { 1436 | "version": "1.6.15", 1437 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", 1438 | "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", 1439 | "requires": { 1440 | "media-typer": "0.3.0", 1441 | "mime-types": "~2.1.15" 1442 | } 1443 | }, 1444 | "typedarray": { 1445 | "version": "0.0.6", 1446 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 1447 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" 1448 | }, 1449 | "unpipe": { 1450 | "version": "1.0.0", 1451 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1452 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1453 | }, 1454 | "util": { 1455 | "version": "0.10.3", 1456 | "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", 1457 | "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", 1458 | "requires": { 1459 | "inherits": "2.0.1" 1460 | }, 1461 | "dependencies": { 1462 | "inherits": { 1463 | "version": "2.0.1", 1464 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", 1465 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" 1466 | } 1467 | } 1468 | }, 1469 | "util-deprecate": { 1470 | "version": "1.0.2", 1471 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1472 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 1473 | }, 1474 | "utils-merge": { 1475 | "version": "1.0.1", 1476 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1477 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1478 | }, 1479 | "validate-npm-package-license": { 1480 | "version": "3.0.4", 1481 | "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", 1482 | "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", 1483 | "dev": true, 1484 | "requires": { 1485 | "spdx-correct": "^3.0.0", 1486 | "spdx-expression-parse": "^3.0.0" 1487 | } 1488 | }, 1489 | "vary": { 1490 | "version": "1.1.2", 1491 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1492 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1493 | }, 1494 | "wrappy": { 1495 | "version": "1.0.2", 1496 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1497 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1498 | "dev": true 1499 | }, 1500 | "xtend": { 1501 | "version": "4.0.1", 1502 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", 1503 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" 1504 | } 1505 | } 1506 | } 1507 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmysql", 3 | "version": "0.6.0", 4 | "description": "One command to generate REST APIs for any MySql database", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/mocha tests/*.js --exit" 8 | }, 9 | "keywords": [ 10 | "mysql-rest-api-generator", 11 | "node-mysql-rest-api", 12 | "rest", 13 | "restful", 14 | "rest-apis" 15 | ], 16 | "engines": { 17 | "node": ">= 7.6.0" 18 | }, 19 | "bin": { 20 | "xmysql": "bin/index.js" 21 | }, 22 | "repository": "o1lab/xmysql", 23 | "homepage": "https://github.com/nocodb/nocodb", 24 | "author": "o1lab", 25 | "license": "MIT", 26 | "dependencies": { 27 | "assert": "^1.4.1", 28 | "body-parser": "^1.18.2", 29 | "cluster": "^0.7.7", 30 | "colors": "^1.1.2", 31 | "commander": "^2.11.0", 32 | "cors": "^2.8.4", 33 | "express": "^4.16.1", 34 | "morgan": "^1.9.0", 35 | "multer": "^1.3.0", 36 | "mysql": "^2.18.1" 37 | }, 38 | "devDependencies": { 39 | "eslint-config-airbnb-base": "^13.1.0", 40 | "eslint-config-prettier": "^4.1.0", 41 | "eslint-plugin-import": "^2.16.0", 42 | "eslint-plugin-prettier": "^3.0.1", 43 | "mocha": "^5.2.0", 44 | "should": "^13.1.2", 45 | "sleep": "^6.1.0", 46 | "supertest": "^3.0.0" 47 | } 48 | } 49 | --------------------------------------------------------------------------------