Commit 5034c7fcb7f57d5537f4c30ad32b2a945355c31a
0 parents
Exists in
master
mysql date timezone offset from timezone setting (default local timezone get by moments)
Showing
46 changed files
with
6812 additions
and
0 deletions
Show diff stats
| 1 | +++ a/.eslintrc | |
| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | +{ | |
| 2 | + "extends": "loopback", | |
| 3 | + "rules": { | |
| 4 | + "max-len": ["error", 120, 4, { | |
| 5 | + "ignoreComments": true, | |
| 6 | + "ignoreUrls": true, | |
| 7 | + "ignorePattern": "^\\s*var\\s.=\\s*(require\\s*\\()|(/)" | |
| 8 | + }], | |
| 9 | + "camelcase": 0, | |
| 10 | + "one-var": "off", | |
| 11 | + "no-unused-expressions": "off" | |
| 12 | + } | |
| 13 | + } | ... | ... |
| 1 | +++ a/.github/ISSUE_TEMPLATE.md | |
| ... | ... | @@ -0,0 +1,36 @@ |
| 1 | +<!-- | |
| 2 | +- Please ask questions at https://groups.google.com/forum/#!forum/loopbackjs or | |
| 3 | + https://gitter.im/strongloop/loopback | |
| 4 | + | |
| 5 | +- Immediate support is available through our subscription plans, see | |
| 6 | + https://strongloop.com/api-connect-faqs/ | |
| 7 | +--> | |
| 8 | + | |
| 9 | +### Bug or feature request | |
| 10 | + | |
| 11 | +<!-- | |
| 12 | +Mark your choice with an "x" (eg. [x], NOT [*]). | |
| 13 | +--> | |
| 14 | + | |
| 15 | +- [ ] Bug | |
| 16 | +- [ ] Feature request | |
| 17 | + | |
| 18 | +### Description of feature (or steps to reproduce if bug) | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | +### Link to sample repo to reproduce issue (if bug) | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | +### Expected result | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | +### Actual result (if bug) | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | +### Additional information (Node.js version, LoopBack version, etc) | |
| 35 | + | |
| 36 | + | ... | ... |
| 1 | +++ a/.github/PULL_REQUEST_TEMPLATE.md | |
| ... | ... | @@ -0,0 +1,24 @@ |
| 1 | +### Description | |
| 2 | + | |
| 3 | + | |
| 4 | +#### Related issues | |
| 5 | + | |
| 6 | +<!-- | |
| 7 | +Please use the following link syntaxes: | |
| 8 | + | |
| 9 | +- #49 (to reference issues in the current repository) | |
| 10 | +- strongloop/loopback#49 (to reference issues in another repository) | |
| 11 | +--> | |
| 12 | + | |
| 13 | +- None | |
| 14 | + | |
| 15 | +### Checklist | |
| 16 | + | |
| 17 | +<!-- | |
| 18 | +Please mark your choice with an "x" (i.e. [x], see | |
| 19 | +https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments) | |
| 20 | +--> | |
| 21 | + | |
| 22 | +- [ ] New tests added or existing tests modified to cover all changes | |
| 23 | +- [ ] Code conforms with the [style | |
| 24 | + guide](http://loopback.io/doc/en/contrib/style-guide.html) | ... | ... |
| 1 | +++ a/CHANGES.md | |
| ... | ... | @@ -0,0 +1,436 @@ |
| 1 | +2017-01-13, Version 3.0.0 | |
| 2 | +========================= | |
| 3 | + | |
| 4 | + * Follow mysql recommendations for handling booleans (Carl Fรผrstenberg) | |
| 5 | + | |
| 6 | + * Fix readme glitch (#231) (Rand McKinney) | |
| 7 | + | |
| 8 | + * Update readme w info from docs (#229) (Rand McKinney) | |
| 9 | + | |
| 10 | + * Fix expected column name when autoupdate (muhammad hasan) | |
| 11 | + | |
| 12 | + * Update paid support URL (Siddhi Pai) | |
| 13 | + | |
| 14 | + * Fix CI Failures (Loay Gewily) | |
| 15 | + | |
| 16 | + * Drop support for Node v0.10 and v0.12 (Siddhi Pai) | |
| 17 | + | |
| 18 | + * Start the development of the next major version (Siddhi Pai) | |
| 19 | + | |
| 20 | + * Update README with correct doc links, etc (Amir Jafarian) | |
| 21 | + | |
| 22 | + | |
| 23 | +2016-10-17, Version 2.4.0 | |
| 24 | +========================= | |
| 25 | + | |
| 26 | + * Add connectorCapabilities global object (#201) (Nicholas Duffy) | |
| 27 | + | |
| 28 | + * Remove unused prefix for test env vars (#203) (Simon Ho) | |
| 29 | + | |
| 30 | + * Update translation files - round#2 (#199) (Candy) | |
| 31 | + | |
| 32 | + * Add CI fixes (#197) (Loay) | |
| 33 | + | |
| 34 | + * Add translated files (gunjpan) | |
| 35 | + | |
| 36 | + * Update deps to loopback 3.0.0 RC (Miroslav Bajtoลก) | |
| 37 | + | |
| 38 | + * Remove Makefile in favour of NPM test scripts (Simon Ho) | |
| 39 | + | |
| 40 | + * Fixing lint errors (Ron Lloyd) | |
| 41 | + | |
| 42 | + * Autoupdate mysql.columnName bug fix (Ron Lloyd) | |
| 43 | + | |
| 44 | + * Tests for autoupdate mysql.columnName bug fix (Ron Lloyd) | |
| 45 | + | |
| 46 | + * Use juggler@3 for running the tests (Miroslav Bajtoลก) | |
| 47 | + | |
| 48 | + * Explictly set forceId:false in test model (Miroslav Bajtoลก) | |
| 49 | + | |
| 50 | + * Fix pretest and init test configs (Simon Ho) | |
| 51 | + | |
| 52 | + * Fix to configure model index in keys field (deepakrkris) | |
| 53 | + | |
| 54 | + * Update eslint infrastructure (Loay) | |
| 55 | + | |
| 56 | + * test: use dump of original test DB as seed (Ryan Graham) | |
| 57 | + | |
| 58 | + * test: skip cardinality, update sub_part (Ryan Graham) | |
| 59 | + | |
| 60 | + * test: accept alternate test db credentials (Ryan Graham) | |
| 61 | + | |
| 62 | + * test: use should for easier debugging (Ryan Graham) | |
| 63 | + | |
| 64 | + * test: account for mysql version differences (Ryan Graham) | |
| 65 | + | |
| 66 | + * test: match case with example/table.sql (Ryan Graham) | |
| 67 | + | |
| 68 | + * test: separate assertions from test flow control (Ryan Graham) | |
| 69 | + | |
| 70 | + * test: update tests to use example DB (Ryan Graham) | |
| 71 | + | |
| 72 | + * test: seed test DB with example (Ryan Graham) | |
| 73 | + | |
| 74 | + * test: fix undefined password (Ryan Graham) | |
| 75 | + | |
| 76 | + * Add special handling of zero date/time entries (Carl Fรผrstenberg) | |
| 77 | + | |
| 78 | + * Add globalization (Candy) | |
| 79 | + | |
| 80 | + * Update URLs in CONTRIBUTING.md (#176) (Ryan Graham) | |
| 81 | + | |
| 82 | + | |
| 83 | +2016-06-21, Version 2.3.0 | |
| 84 | +========================= | |
| 85 | + | |
| 86 | + * Add function connect (juehou) | |
| 87 | + | |
| 88 | + * insert/update copyright notices (Ryan Graham) | |
| 89 | + | |
| 90 | + * relicense as MIT only (Ryan Graham) | |
| 91 | + | |
| 92 | + * Override other settings if url provided (juehou) | |
| 93 | + | |
| 94 | + * Add `connectorCapabilities ` (Amir Jafarian) | |
| 95 | + | |
| 96 | + * Implement ReplaceOrCreate (Amir Jafarian) | |
| 97 | + | |
| 98 | + | |
| 99 | +2016-02-19, Version 2.2.1 | |
| 100 | +========================= | |
| 101 | + | |
| 102 | + * Remove sl-blip from dependencies (Miroslav Bajtoลก) | |
| 103 | + | |
| 104 | + * Upgrade `should` module (Amir Jafarian) | |
| 105 | + | |
| 106 | + * removed console.log (cgole) | |
| 107 | + | |
| 108 | + * seperate env variable for test db (cgole) | |
| 109 | + | |
| 110 | + * Changed username to user (cgole) | |
| 111 | + | |
| 112 | + * Added db username password (cgole) | |
| 113 | + | |
| 114 | + * Add mysql CI host (cgole) | |
| 115 | + | |
| 116 | + * Refer to licenses with a link (Sam Roberts) | |
| 117 | + | |
| 118 | + * Pass options to the execute command. (Diogo Correia) | |
| 119 | + | |
| 120 | + * Use strongloop conventions for licensing (Sam Roberts) | |
| 121 | + | |
| 122 | + | |
| 123 | +2015-07-30, Version 2.2.0 | |
| 124 | +========================= | |
| 125 | + | |
| 126 | + * Clean up regexop tests (Simon Ho) | |
| 127 | + | |
| 128 | + * Add regexp operator tests (Simon Ho) | |
| 129 | + | |
| 130 | + * Fix RegExp unit test setup/teardown (Simon Ho) | |
| 131 | + | |
| 132 | + * Add support for RegExp operator (Simon Ho) | |
| 133 | + | |
| 134 | + | |
| 135 | +2015-05-29, Version 2.1.1 | |
| 136 | +========================= | |
| 137 | + | |
| 138 | + * Fix the failing tests (Raymond Feng) | |
| 139 | + | |
| 140 | + | |
| 141 | +2015-05-18, Version 2.1.0 | |
| 142 | +========================= | |
| 143 | + | |
| 144 | + * Update deps (Raymond Feng) | |
| 145 | + | |
| 146 | + * Start to add transaction support (Raymond Feng) | |
| 147 | + | |
| 148 | + | |
| 149 | +2015-05-14, Version 2.0.1 | |
| 150 | +========================= | |
| 151 | + | |
| 152 | + * Fix the typo (Raymond Feng) | |
| 153 | + | |
| 154 | + | |
| 155 | +2015-05-13, Version 2.0.0 | |
| 156 | +========================= | |
| 157 | + | |
| 158 | + * Update deps (Raymond Feng) | |
| 159 | + | |
| 160 | + * Refactor the code to use base SqlConnector (Raymond Feng) | |
| 161 | + | |
| 162 | + | |
| 163 | +2015-04-02, Version 1.7.0 | |
| 164 | +========================= | |
| 165 | + | |
| 166 | + * Return isNewInstance from upsert (Raymond Feng) | |
| 167 | + | |
| 168 | + * Update rc dep (Raymond Feng) | |
| 169 | + | |
| 170 | + * Return count when updating or deleting models (Simon Ho) | |
| 171 | + | |
| 172 | + * Update README.md (Simon Ho) | |
| 173 | + | |
| 174 | + * Add test running instructions to readme (Simon Ho) | |
| 175 | + | |
| 176 | + * Fix mysql neq for NULL value. (ulion) | |
| 177 | + | |
| 178 | + * replace dataLength instead of adding length property (Partap Davis) | |
| 179 | + | |
| 180 | + * Allow models backed by MySQL to reference mongodb ObjectID (Raymond Feng) | |
| 181 | + | |
| 182 | + * Query string length for schema in characters in addition to bytes (Partap Davis) | |
| 183 | + | |
| 184 | + | |
| 185 | +2015-02-20, Version 1.6.0 | |
| 186 | +========================= | |
| 187 | + | |
| 188 | + * Update deps (Raymond Feng) | |
| 189 | + | |
| 190 | + * Include tests of persistence hooks from juggler. (Miroslav Bajtoลก) | |
| 191 | + | |
| 192 | + | |
| 193 | +2015-01-15, Version 1.5.1 | |
| 194 | +========================= | |
| 195 | + | |
| 196 | + * Fix the loop of models (Raymond Feng) | |
| 197 | + | |
| 198 | + * Set ok default to false (Geoffroy Lesage) | |
| 199 | + | |
| 200 | + * Fixed missing 'ok' (Geoffroy Lesage) | |
| 201 | + | |
| 202 | + * Changed default type mapping (Geoffroy Lesage) | |
| 203 | + | |
| 204 | + * Fixed isActual syntax to accept optional model arg (Geoffroy Lesage) | |
| 205 | + | |
| 206 | + * Fixed isActual implemenation (Geoffroy Lesage) | |
| 207 | + | |
| 208 | + * Inherit Schema From DataSource if not defined (Serkan Serttop) | |
| 209 | + | |
| 210 | + | |
| 211 | +2015-01-09, Version 1.5.0 | |
| 212 | +========================= | |
| 213 | + | |
| 214 | + * Use mysql.escape/escapeId() (Raymond Feng) | |
| 215 | + | |
| 216 | + * Fix bad CLA URL in CONTRIBUTING.md (Ryan Graham) | |
| 217 | + | |
| 218 | + * (cherry picked from commit a6d31e8) (yogesh) | |
| 219 | + | |
| 220 | + | |
| 221 | +2014-12-05, Version 1.4.9 | |
| 222 | +========================= | |
| 223 | + | |
| 224 | + * Add a test case for autoupdate (Raymond Feng) | |
| 225 | + | |
| 226 | + * Create 'NOT NULL' constraint for required or id properties (Raymond Feng) | |
| 227 | + | |
| 228 | + * Better handle discovery of nullable columns (Raymond Feng) | |
| 229 | + | |
| 230 | + | |
| 231 | +2014-11-27, Version 1.4.8 | |
| 232 | +========================= | |
| 233 | + | |
| 234 | + * fix(initialization): bug fix for setting limit on number of connections in connection pool (cpentra1) | |
| 235 | + | |
| 236 | + * Add contribution guidelines (Ryan Graham) | |
| 237 | + | |
| 238 | + | |
| 239 | +2014-09-11, Version 1.4.7 | |
| 240 | +========================= | |
| 241 | + | |
| 242 | + * Enhance error reporting for automigrate/autoupdate (Raymond Feng) | |
| 243 | + | |
| 244 | + | |
| 245 | +2014-09-10, Version 1.4.6 | |
| 246 | +========================= | |
| 247 | + | |
| 248 | + * Bump version (Raymond Feng) | |
| 249 | + | |
| 250 | + * Use table name instead of model name (Raymond Feng) | |
| 251 | + | |
| 252 | + * Use async and make sure errors are passed to callback (Raymond Feng) | |
| 253 | + | |
| 254 | + | |
| 255 | +2014-08-25, Version 1.4.5 | |
| 256 | +========================= | |
| 257 | + | |
| 258 | + * Bump version (Raymond Feng) | |
| 259 | + | |
| 260 | + * Make sure the deferred query will be invoked only once (Raymond Feng) | |
| 261 | + | |
| 262 | + | |
| 263 | +2014-08-20, Version 1.4.4 | |
| 264 | +========================= | |
| 265 | + | |
| 266 | + * Bump version (Raymond Feng) | |
| 267 | + | |
| 268 | + * Add ping() (Raymond Feng) | |
| 269 | + | |
| 270 | + | |
| 271 | +2014-08-20, Version 1.4.3 | |
| 272 | +========================= | |
| 273 | + | |
| 274 | + * Bump version (Raymond Feng) | |
| 275 | + | |
| 276 | + * Fix MySQL conversion for embedded model instance (Raymond Feng) | |
| 277 | + | |
| 278 | + * Fix the createDatabase option (Raymond Feng) | |
| 279 | + | |
| 280 | + | |
| 281 | +2014-08-15, Version 1.4.2 | |
| 282 | +========================= | |
| 283 | + | |
| 284 | + * Bump version (Raymond Feng) | |
| 285 | + | |
| 286 | + * Allow properties to pass through mysql driver (Raymond Feng) | |
| 287 | + | |
| 288 | + * Fix the default length for strings to avoid row size overflow (Raymond Feng) | |
| 289 | + | |
| 290 | + | |
| 291 | +2014-06-27, Version 1.4.1 | |
| 292 | +========================= | |
| 293 | + | |
| 294 | + * Bump version (Raymond Feng) | |
| 295 | + | |
| 296 | + * Fix the test cases as now inq/nin is checked for array values (Raymond Feng) | |
| 297 | + | |
| 298 | + * Update link to doc (Rand McKinney) | |
| 299 | + | |
| 300 | + | |
| 301 | +2014-06-23, Version 1.4.0 | |
| 302 | +========================= | |
| 303 | + | |
| 304 | + * Bump version (Raymond Feng) | |
| 305 | + | |
| 306 | + * cannot read property of undefined fixed (Johnny Bill) | |
| 307 | + | |
| 308 | + * Fix comparison for null and boolean values (Raymond Feng) | |
| 309 | + | |
| 310 | + * Map object/json to TEXT (Raymond Feng) | |
| 311 | + | |
| 312 | + | |
| 313 | +2014-06-04, Version 1.3.0 | |
| 314 | +========================= | |
| 315 | + | |
| 316 | + * Remove peer dependency on datasource-juggler (Miroslav Bajtoลก) | |
| 317 | + | |
| 318 | + | |
| 319 | +2014-06-02, Version 1.2.3 | |
| 320 | +========================= | |
| 321 | + | |
| 322 | + * Bump version (Raymond Feng) | |
| 323 | + | |
| 324 | + * Fix sql injection and add test cases (Raymond Feng) | |
| 325 | + | |
| 326 | + | |
| 327 | +2014-05-29, Version 1.2.2 | |
| 328 | +========================= | |
| 329 | + | |
| 330 | + * Bump version (Raymond Feng) | |
| 331 | + | |
| 332 | + * Fix the varchar length (Raymond Feng) | |
| 333 | + | |
| 334 | + * Add like/nlike support (Raymond Feng) | |
| 335 | + | |
| 336 | + * Fix object/json type mapping (Raymond Feng) | |
| 337 | + | |
| 338 | + | |
| 339 | +2014-05-16, Version 1.2.1 | |
| 340 | +========================= | |
| 341 | + | |
| 342 | + * Bump versions (Raymond Feng) | |
| 343 | + | |
| 344 | + * Fix buildWhere (Raymond Feng) | |
| 345 | + | |
| 346 | + * Add support for logical operators (AND/OR) (Raymond Feng) | |
| 347 | + | |
| 348 | + * updateOrCreate assumes numeric primary key(s) (Scott Anderson) | |
| 349 | + | |
| 350 | + | |
| 351 | +2014-04-08, Version 1.2.0 | |
| 352 | +========================= | |
| 353 | + | |
| 354 | + * Bump version (Raymond Feng) | |
| 355 | + | |
| 356 | + * Remove the commented out code (Raymond Feng) | |
| 357 | + | |
| 358 | + * Fix the query for discovery with current user (Raymond Feng) | |
| 359 | + | |
| 360 | + * Fix the table generation for string ids (Raymond Feng) | |
| 361 | + | |
| 362 | + * Update deps (Raymond Feng) | |
| 363 | + | |
| 364 | + * Use NULL for undefined (Raymond Feng) | |
| 365 | + | |
| 366 | + * Prevent inserting undefined values (Marat Dyatko) | |
| 367 | + | |
| 368 | + * Update to dual MIT/StrongLoop license (Raymond Feng) | |
| 369 | + | |
| 370 | + * Fix merge issue (Raymond Feng) | |
| 371 | + | |
| 372 | + * Reformat code (Raymond Feng) | |
| 373 | + | |
| 374 | + * Update discovery.js (Samer Aldefai) | |
| 375 | + | |
| 376 | + * Fix link to docs. (Rand McKinney) | |
| 377 | + | |
| 378 | + * Replaced most content with link to docs. (Rand McKinney) | |
| 379 | + | |
| 380 | + * Move mocha args to test/mocha.opts (Ryan Graham) | |
| 381 | + | |
| 382 | + * Make 'npm test' more useful to CI (Ryan Graham) | |
| 383 | + | |
| 384 | + * Prevent extra files from going into npm (Ryan Graham) | |
| 385 | + | |
| 386 | + | |
| 387 | +2013-12-06, Version 1.1.1 | |
| 388 | +========================= | |
| 389 | + | |
| 390 | + * Bump version (Raymond Feng) | |
| 391 | + | |
| 392 | + * Update deps (Raymond Feng) | |
| 393 | + | |
| 394 | + * Add the test for loopback-datasource-juggler PR-48 (Raymond Feng) | |
| 395 | + | |
| 396 | + * Fix the orderBy (Raymond Feng) | |
| 397 | + | |
| 398 | + | |
| 399 | +2013-11-27, Version 1.1.0 | |
| 400 | +========================= | |
| 401 | + | |
| 402 | + * Bump version (Raymond Feng) | |
| 403 | + | |
| 404 | + * Refactor the runQuery logic into a function (Raymond Feng) | |
| 405 | + | |
| 406 | + * Improve the connector based on review feedbacks (Raymond Feng) | |
| 407 | + | |
| 408 | + * Allow connectionLmit to be set (Raymond Feng) | |
| 409 | + | |
| 410 | + * Use connection pool for MySQL (Raymond Feng) | |
| 411 | + | |
| 412 | + * Update docs.json (Rand McKinney) | |
| 413 | + | |
| 414 | + * Fix the regression caused by juggler (Raymond Feng) | |
| 415 | + | |
| 416 | + | |
| 417 | +2013-11-20, Version 1.0.2 | |
| 418 | +========================= | |
| 419 | + | |
| 420 | + * Remove blanket (Raymond Feng) | |
| 421 | + | |
| 422 | + * Bump version and update deps (Raymond Feng) | |
| 423 | + | |
| 424 | + * Append error to the message (Raymond Feng) | |
| 425 | + | |
| 426 | + * Add NOTICE and update READE (Raymond Feng) | |
| 427 | + | |
| 428 | + * Update README.md (Rand McKinney) | |
| 429 | + | |
| 430 | + * Update the internal github dependency (Raymond Feng) | |
| 431 | + | |
| 432 | + | |
| 433 | +2013-10-28, Version 1.0.0 | |
| 434 | +========================= | |
| 435 | + | |
| 436 | + * First release! | ... | ... |
| 1 | +++ a/CONTRIBUTING.md | |
| ... | ... | @@ -0,0 +1,151 @@ |
| 1 | +### Contributing ### | |
| 2 | + | |
| 3 | +Thank you for your interest in `loopback-connector-mysql`, an open source project | |
| 4 | +administered by StrongLoop. | |
| 5 | + | |
| 6 | +Contributing to `loopback-connector-mysql` is easy. In a few simple steps: | |
| 7 | + | |
| 8 | + * Ensure that your effort is aligned with the project's roadmap by | |
| 9 | + talking to the maintainers, especially if you are going to spend a | |
| 10 | + lot of time on it. | |
| 11 | + | |
| 12 | + * Make something better or fix a bug. | |
| 13 | + | |
| 14 | + * Adhere to code style outlined in the [Google C++ Style Guide][] and | |
| 15 | + [Google Javascript Style Guide][]. | |
| 16 | + | |
| 17 | + * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback-connector-mysql) | |
| 18 | + | |
| 19 | + * Submit a pull request through Github. | |
| 20 | + | |
| 21 | + | |
| 22 | +### Contributor License Agreement ### | |
| 23 | + | |
| 24 | +``` | |
| 25 | + Individual Contributor License Agreement | |
| 26 | + | |
| 27 | + By signing this Individual Contributor License Agreement | |
| 28 | + ("Agreement"), and making a Contribution (as defined below) to | |
| 29 | + StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and | |
| 30 | + agree to the following terms and conditions for Your present and | |
| 31 | + future Contributions submitted to StrongLoop. Except for the license | |
| 32 | + granted in this Agreement to StrongLoop and recipients of software | |
| 33 | + distributed by StrongLoop, You reserve all right, title, and interest | |
| 34 | + in and to Your Contributions. | |
| 35 | + | |
| 36 | + 1. Definitions | |
| 37 | + | |
| 38 | + "You" or "Your" shall mean the copyright owner or the individual | |
| 39 | + authorized by the copyright owner that is entering into this | |
| 40 | + Agreement with StrongLoop. | |
| 41 | + | |
| 42 | + "Contribution" shall mean any original work of authorship, | |
| 43 | + including any modifications or additions to an existing work, that | |
| 44 | + is intentionally submitted by You to StrongLoop for inclusion in, | |
| 45 | + or documentation of, any of the products owned or managed by | |
| 46 | + StrongLoop ("Work"). For purposes of this definition, "submitted" | |
| 47 | + means any form of electronic, verbal, or written communication | |
| 48 | + sent to StrongLoop or its representatives, including but not | |
| 49 | + limited to communication or electronic mailing lists, source code | |
| 50 | + control systems, and issue tracking systems that are managed by, | |
| 51 | + or on behalf of, StrongLoop for the purpose of discussing and | |
| 52 | + improving the Work, but excluding communication that is | |
| 53 | + conspicuously marked or otherwise designated in writing by You as | |
| 54 | + "Not a Contribution." | |
| 55 | + | |
| 56 | + 2. You Grant a Copyright License to StrongLoop | |
| 57 | + | |
| 58 | + Subject to the terms and conditions of this Agreement, You hereby | |
| 59 | + grant to StrongLoop and recipients of software distributed by | |
| 60 | + StrongLoop, a perpetual, worldwide, non-exclusive, no-charge, | |
| 61 | + royalty-free, irrevocable copyright license to reproduce, prepare | |
| 62 | + derivative works of, publicly display, publicly perform, | |
| 63 | + sublicense, and distribute Your Contributions and such derivative | |
| 64 | + works under any license and without any restrictions. | |
| 65 | + | |
| 66 | + 3. You Grant a Patent License to StrongLoop | |
| 67 | + | |
| 68 | + Subject to the terms and conditions of this Agreement, You hereby | |
| 69 | + grant to StrongLoop and to recipients of software distributed by | |
| 70 | + StrongLoop a perpetual, worldwide, non-exclusive, no-charge, | |
| 71 | + royalty-free, irrevocable (except as stated in this Section) | |
| 72 | + patent license to make, have made, use, offer to sell, sell, | |
| 73 | + import, and otherwise transfer the Work under any license and | |
| 74 | + without any restrictions. The patent license You grant to | |
| 75 | + StrongLoop under this Section applies only to those patent claims | |
| 76 | + licensable by You that are necessarily infringed by Your | |
| 77 | + Contributions(s) alone or by combination of Your Contributions(s) | |
| 78 | + with the Work to which such Contribution(s) was submitted. If any | |
| 79 | + entity institutes a patent litigation against You or any other | |
| 80 | + entity (including a cross-claim or counterclaim in a lawsuit) | |
| 81 | + alleging that Your Contribution, or the Work to which You have | |
| 82 | + contributed, constitutes direct or contributory patent | |
| 83 | + infringement, any patent licenses granted to that entity under | |
| 84 | + this Agreement for that Contribution or Work shall terminate as | |
| 85 | + of the date such litigation is filed. | |
| 86 | + | |
| 87 | + 4. You Have the Right to Grant Licenses to StrongLoop | |
| 88 | + | |
| 89 | + You represent that You are legally entitled to grant the licenses | |
| 90 | + in this Agreement. | |
| 91 | + | |
| 92 | + If Your employer(s) has rights to intellectual property that You | |
| 93 | + create, You represent that You have received permission to make | |
| 94 | + the Contributions on behalf of that employer, that Your employer | |
| 95 | + has waived such rights for Your Contributions, or that Your | |
| 96 | + employer has executed a separate Corporate Contributor License | |
| 97 | + Agreement with StrongLoop. | |
| 98 | + | |
| 99 | + 5. The Contributions Are Your Original Work | |
| 100 | + | |
| 101 | + You represent that each of Your Contributions are Your original | |
| 102 | + works of authorship (see Section 8 (Submissions on Behalf of | |
| 103 | + Others) for submission on behalf of others). You represent that to | |
| 104 | + Your knowledge, no other person claims, or has the right to claim, | |
| 105 | + any right in any intellectual property right related to Your | |
| 106 | + Contributions. | |
| 107 | + | |
| 108 | + You also represent that You are not legally obligated, whether by | |
| 109 | + entering into an agreement or otherwise, in any way that conflicts | |
| 110 | + with the terms of this Agreement. | |
| 111 | + | |
| 112 | + You represent that Your Contribution submissions include complete | |
| 113 | + details of any third-party license or other restriction (including, | |
| 114 | + but not limited to, related patents and trademarks) of which You | |
| 115 | + are personally aware and which are associated with any part of | |
| 116 | + Your Contributions. | |
| 117 | + | |
| 118 | + 6. You Don't Have an Obligation to Provide Support for Your Contributions | |
| 119 | + | |
| 120 | + You are not expected to provide support for Your Contributions, | |
| 121 | + except to the extent You desire to provide support. You may provide | |
| 122 | + support for free, for a fee, or not at all. | |
| 123 | + | |
| 124 | + 6. No Warranties or Conditions | |
| 125 | + | |
| 126 | + StrongLoop acknowledges that unless required by applicable law or | |
| 127 | + agreed to in writing, You provide Your Contributions on an "AS IS" | |
| 128 | + BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER | |
| 129 | + EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES | |
| 130 | + OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR | |
| 131 | + FITNESS FOR A PARTICULAR PURPOSE. | |
| 132 | + | |
| 133 | + 7. Submission on Behalf of Others | |
| 134 | + | |
| 135 | + If You wish to submit work that is not Your original creation, You | |
| 136 | + may submit it to StrongLoop separately from any Contribution, | |
| 137 | + identifying the complete details of its source and of any license | |
| 138 | + or other restriction (including, but not limited to, related | |
| 139 | + patents, trademarks, and license agreements) of which You are | |
| 140 | + personally aware, and conspicuously marking the work as | |
| 141 | + "Submitted on Behalf of a Third-Party: [named here]". | |
| 142 | + | |
| 143 | + 8. Agree to Notify of Change of Circumstances | |
| 144 | + | |
| 145 | + You agree to notify StrongLoop of any facts or circumstances of | |
| 146 | + which You become aware that would make these representations | |
| 147 | + inaccurate in any respect. Email us at callback@strongloop.com. | |
| 148 | +``` | |
| 149 | + | |
| 150 | +[Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html | |
| 151 | +[Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml | ... | ... |
| 1 | +++ a/LICENSE | |
| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | +Copyright (c) IBM Corp. 2012,2016. All Rights Reserved. | |
| 2 | +Node module: loopback-connector-mysql | |
| 3 | +This project is licensed under the MIT License, full text below. | |
| 4 | + | |
| 5 | +-------- | |
| 6 | + | |
| 7 | +MIT license | |
| 8 | + | |
| 9 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 10 | +of this software and associated documentation files (the "Software"), to deal | |
| 11 | +in the Software without restriction, including without limitation the rights | |
| 12 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 13 | +copies of the Software, and to permit persons to whom the Software is | |
| 14 | +furnished to do so, subject to the following conditions: | |
| 15 | + | |
| 16 | +The above copyright notice and this permission notice shall be included in | |
| 17 | +all copies or substantial portions of the Software. | |
| 18 | + | |
| 19 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 20 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 21 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 22 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 23 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 24 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 25 | +THE SOFTWARE. | ... | ... |
| 1 | +++ a/NOTICE.md | |
| ... | ... | @@ -0,0 +1,24 @@ |
| 1 | +The project was initially forked from [mysql-adapter](https://github.com/jugglingdb/mysql-adapter) | |
| 2 | +which carries the following copyright and permission notices: | |
| 3 | + | |
| 4 | + | |
| 5 | + Copyright (C) 2012 by Anatoliy Chakkaev | |
| 6 | + | |
| 7 | + Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 8 | + of this software and associated documentation files (the "Software"), to deal | |
| 9 | + in the Software without restriction, including without limitation the rights | |
| 10 | + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 11 | + copies of the Software, and to permit persons to whom the Software is | |
| 12 | + furnished to do so, subject to the following conditions: | |
| 13 | + | |
| 14 | + The above copyright notice and this permission notice shall be included in | |
| 15 | + all copies or substantial portions of the Software. | |
| 16 | + | |
| 17 | + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 18 | + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 19 | + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 20 | + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 21 | + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 22 | + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 23 | + THE SOFTWARE. | |
| 24 | + | ... | ... |
| 1 | +++ a/README.md | |
| ... | ... | @@ -0,0 +1,362 @@ |
| 1 | +# loopback-connector-mysql | |
| 2 | + | |
| 3 | +[MySQL](https://www.mysql.com/) is a popular open-source relational database management system (RDBMS). The `loopback-connector-mysql` module provides the MySQL connector module for the LoopBack framework. | |
| 4 | + | |
| 5 | +<div class="gh-only">See also <a href="http://loopback.io/doc/en/lb3/MySQL-connector.html">LoopBack MySQL Connector</a> in LoopBack documentation. | |
| 6 | +<br/><br/> | |
| 7 | +<b>NOTE</b>: The MySQL connector requires MySQL 5.0+. | |
| 8 | +</div> | |
| 9 | + | |
| 10 | +## Installation | |
| 11 | + | |
| 12 | +In your application root directory, enter this command to install the connector: | |
| 13 | + | |
| 14 | +```sh | |
| 15 | +npm install loopback-connector-mysql --save | |
| 16 | +``` | |
| 17 | + | |
| 18 | +This installs the module from npm and adds it as a dependency to the application'sย `package.json`ย file. | |
| 19 | + | |
| 20 | +If you create a MySQL data source using the data source generator as described below, you don't have to do this, since the generator will run `npm install` for you. | |
| 21 | + | |
| 22 | +## Creating a MySQL data source | |
| 23 | + | |
| 24 | +Use theย [Data source generator](http://loopback.io/doc/en/lb3/Data-source-generator.html)ย to add a MySQLย data source to your application. | |
| 25 | +The generator will prompt for the database server hostname, port, and other settings | |
| 26 | +required to connect to a MySQL database. It will also run the `npm install` command above for you. | |
| 27 | + | |
| 28 | +The entry in the application's `/server/datasources.json` will look like this: | |
| 29 | + | |
| 30 | +```javascript | |
| 31 | +"mydb": { | |
| 32 | + "name": "mydb", | |
| 33 | + "connector": "mysql", | |
| 34 | + "host": "myserver", | |
| 35 | + "port": 3306, | |
| 36 | + "database": "mydb", | |
| 37 | + "password": "mypassword", | |
| 38 | + "user": "admin" | |
| 39 | + } | |
| 40 | +``` | |
| 41 | + | |
| 42 | +Edit `datasources.json` to add any other additional properties that you require. | |
| 43 | + | |
| 44 | +### Properties | |
| 45 | + | |
| 46 | +<table> | |
| 47 | + <thead> | |
| 48 | + <tr> | |
| 49 | + <th width="150">Property</th> | |
| 50 | + <th width="80">Type</th> | |
| 51 | + <th>Description</th> | |
| 52 | + </tr> | |
| 53 | + </thead> | |
| 54 | + <tbody> | |
| 55 | + <tr> | |
| 56 | + <td>collation</td> | |
| 57 | + <td>String</td> | |
| 58 | + <td>Determines the charset for the connection. Default is utf8_general_ci.</td> | |
| 59 | + </tr> | |
| 60 | + <tr> | |
| 61 | + <td>connector</td> | |
| 62 | + <td>String</td> | |
| 63 | + <td>Connector name, either โloopback-connector-mysqlโ or โmysqlโ.</td> | |
| 64 | + </tr> | |
| 65 | + <tr> | |
| 66 | + <td>connectionLimit</td> | |
| 67 | + <td>Number</td> | |
| 68 | + <td>The maximum number of connections to create at once. Default is 10.</td> | |
| 69 | + </tr> | |
| 70 | + <tr> | |
| 71 | + <td>database</td> | |
| 72 | + <td>String</td> | |
| 73 | + <td>Database name</td> | |
| 74 | + </tr> | |
| 75 | + <tr> | |
| 76 | + <td>debug</td> | |
| 77 | + <td>Boolean</td> | |
| 78 | + <td>If true, turn on verbose mode to debug database queries and lifecycle.</td> | |
| 79 | + </tr> | |
| 80 | + <tr> | |
| 81 | + <td>host</td> | |
| 82 | + <td>String</td> | |
| 83 | + <td>Database host name</td> | |
| 84 | + </tr> | |
| 85 | + <tr> | |
| 86 | + <td>password</td> | |
| 87 | + <td>String</td> | |
| 88 | + <td>Password to connect to database</td> | |
| 89 | + </tr> | |
| 90 | + <tr> | |
| 91 | + <td>port</td> | |
| 92 | + <td>Number</td> | |
| 93 | + <td>Database TCP port</td> | |
| 94 | + </tr> | |
| 95 | + <tr> | |
| 96 | + <td>socketPath</td> | |
| 97 | + <td>String</td> | |
| 98 | + <td>The path to a unix domain socket to connect to. When used host and port are ignored.</td> | |
| 99 | + </tr> | |
| 100 | + <tr> | |
| 101 | + <td>supportBigNumbers</td> | |
| 102 | + <td>Boolean</td> | |
| 103 | + <td>Enable this option to deal with big numbers (BIGINT and DECIMAL columns) in the database. Default is false.</td> | |
| 104 | + </tr> | |
| 105 | + <tr> | |
| 106 | + <td>timeZone</td> | |
| 107 | + <td>String</td> | |
| 108 | + <td>The timezone used to store local dates. Default is โlocalโ.</td> | |
| 109 | + </tr> | |
| 110 | + <tr> | |
| 111 | + <td>url</td> | |
| 112 | + <td>String</td> | |
| 113 | + <td>Connection URL of form <code>mysql://user:password@host/db</code>. Overrides other connection settings.</td> | |
| 114 | + </tr> | |
| 115 | + <tr> | |
| 116 | + <td>username</td> | |
| 117 | + <td>String</td> | |
| 118 | + <td>Username to connect to database</td> | |
| 119 | + </tr> | |
| 120 | + </tbody> | |
| 121 | +</table> | |
| 122 | + | |
| 123 | +**NOTE**: In addition to these properties, you can use additional parameters supported byย [`node-mysql`](https://github.com/felixge/node-mysql). | |
| 124 | + | |
| 125 | +## Type mappings | |
| 126 | + | |
| 127 | +Seeย [LoopBack types](http://loopback.io/doc/en/lb3/LoopBack-types.html)ย for details on LoopBack's data types. | |
| 128 | + | |
| 129 | +### LoopBack to MySQL types | |
| 130 | + | |
| 131 | +<table> | |
| 132 | + <thead> | |
| 133 | + <tr> | |
| 134 | + <th>LoopBack Type</th> | |
| 135 | + <th>MySQL Type</th> | |
| 136 | + </tr> | |
| 137 | + </thead> | |
| 138 | + <tbody> | |
| 139 | + <tr> | |
| 140 | + <td>String/JSON</td> | |
| 141 | + <td>VARCHAR</td> | |
| 142 | + </tr> | |
| 143 | + <tr> | |
| 144 | + <td>Text</td> | |
| 145 | + <td>TEXT</td> | |
| 146 | + </tr> | |
| 147 | + <tr> | |
| 148 | + <td>Number</td> | |
| 149 | + <td>INT</td> | |
| 150 | + </tr> | |
| 151 | + <tr> | |
| 152 | + <td>Date</td> | |
| 153 | + <td>DATETIME</td> | |
| 154 | + </tr> | |
| 155 | + <tr> | |
| 156 | + <td>Boolean</td> | |
| 157 | + <td>TINYINT(1)</td> | |
| 158 | + </tr> | |
| 159 | + <tr> | |
| 160 | + <td><a href="http://apidocs.strongloop.com/loopback-datasource-juggler/#geopoint" class="external-link">GeoPoint</a> object</td> | |
| 161 | + <td>POINT</td> | |
| 162 | + </tr> | |
| 163 | + <tr> | |
| 164 | + <td>Custom Enum type<br>(See <a href="#enum">Enum</a> below)</td> | |
| 165 | + <td>ENUM</td> | |
| 166 | + </tr> | |
| 167 | + </tbody> | |
| 168 | +</table> | |
| 169 | + | |
| 170 | +### MySQL to LoopBack types | |
| 171 | + | |
| 172 | +<table> | |
| 173 | + <tbody> | |
| 174 | + <tr> | |
| 175 | + <th>MySQL Type</th> | |
| 176 | + <th>LoopBack Type</th> | |
| 177 | + </tr> | |
| 178 | + <tr> | |
| 179 | + <td>CHAR</td> | |
| 180 | + <td>String</td> | |
| 181 | + </tr> | |
| 182 | + <tr> | |
| 183 | + <td>CHAR(1)</td> | |
| 184 | + <td>Boolean</td> | |
| 185 | + </tr> | |
| 186 | + <tr> | |
| 187 | + <td>VARCHAR<br>TINYTEXT<br>MEDIUMTEXT<br>LONGTEXT<br>TEXT<br>ENUM<br>SET</td> | |
| 188 | + <td>String</td> | |
| 189 | + </tr> | |
| 190 | + <tr> | |
| 191 | + <td>TINYBLOB<br>MEDIUMBLOB<br>LONGBLOB<br>BLOB<br>BINARY<br>VARBINARY<br>BIT</td> | |
| 192 | + <td>Node.js <a href="http://nodejs.org/api/buffer.html">Buffer object</a></td> | |
| 193 | + </tr> | |
| 194 | + <tr> | |
| 195 | + <td>TINYINT<br>SMALLINT<br>INT<br>MEDIUMINT<br>YEAR<br>FLOAT<br>DOUBLE<br>NUMERIC<br>DECIMAL</td> | |
| 196 | + <td> | |
| 197 | + <p>Number<br>For FLOAT and DOUBLE, see <a href="#floating-point-types">Floating-point types</a>. </p> | |
| 198 | + <p>For NUMERIC and DECIMAL, see <a href="MySQL-connector.html">Fixed-point exact value types</a></p> | |
| 199 | + </td> | |
| 200 | + </tr> | |
| 201 | + <tr> | |
| 202 | + <td>DATE<br>TIMESTAMP<br>DATETIME</td> | |
| 203 | + <td>Date</td> | |
| 204 | + </tr> | |
| 205 | + </tbody> | |
| 206 | +</table> | |
| 207 | + | |
| 208 | +## Using theย datatypeย field/column option with MySQL | |
| 209 | + | |
| 210 | +Use the `mysql` model property to specify additional MySQL-specific properties for a LoopBack model. | |
| 211 | + | |
| 212 | +For example: | |
| 213 | + | |
| 214 | +{% include code-caption.html content="/common/models/model.json" %} | |
| 215 | +```javascript | |
| 216 | +"locationId":{ | |
| 217 | + "type":"String", | |
| 218 | + "required":true, | |
| 219 | + "length":20, | |
| 220 | + "mysql": | |
| 221 | + { | |
| 222 | + "columnName":"LOCATION_ID", | |
| 223 | + "dataType":"VARCHAR", | |
| 224 | + "dataLength":20, | |
| 225 | + "nullable":"N" | |
| 226 | + } | |
| 227 | +} | |
| 228 | +``` | |
| 229 | + | |
| 230 | +You can also use theย dataTypeย column/property attribute to specify what MySQL column type to use for many loopback-datasource-juggler types.ย | |
| 231 | +The following type-dataType combinations are supported: | |
| 232 | + | |
| 233 | +* Number | |
| 234 | +* integer | |
| 235 | +* tinyint | |
| 236 | +* smallint | |
| 237 | +* mediumint | |
| 238 | +* int | |
| 239 | +* bigint | |
| 240 | + | |
| 241 | +Use the `limit` option to alter the display width. Example: | |
| 242 | + | |
| 243 | +```javascript | |
| 244 | +{ userName : { | |
| 245 | + type: String, | |
| 246 | + dataType: 'char', | |
| 247 | + limit: 24 | |
| 248 | + } | |
| 249 | +} | |
| 250 | +``` | |
| 251 | + | |
| 252 | +### Floating-point types | |
| 253 | + | |
| 254 | +For Float and Double data types, use theย `precision`ย andย `scale`ย options to specify custom precision. Default is (16,8).ย For example: | |
| 255 | + | |
| 256 | +```javascript | |
| 257 | +{ average : | |
| 258 | + { type: Number, | |
| 259 | + dataType: 'float', | |
| 260 | + precision: 20, | |
| 261 | + scale: 4 | |
| 262 | + } | |
| 263 | +} | |
| 264 | +``` | |
| 265 | + | |
| 266 | +### Fixed-point exact value types | |
| 267 | + | |
| 268 | +For Decimal and Numeric types, use theย `precision`ย andย `scale`ย options to specify custom precision. Default is (9,2). | |
| 269 | +These aren't likely to function as true fixed-point. | |
| 270 | + | |
| 271 | +Example: | |
| 272 | + | |
| 273 | +```javascript | |
| 274 | +{ stdDev : | |
| 275 | + { type: Number, | |
| 276 | + dataType: 'decimal', | |
| 277 | + precision: 12, | |
| 278 | + scale: 8 | |
| 279 | + } | |
| 280 | +} | |
| 281 | +``` | |
| 282 | + | |
| 283 | +### Other types | |
| 284 | + | |
| 285 | +Convert String / DataSource.Text / DataSource.JSON to the following MySQL types: | |
| 286 | + | |
| 287 | +* varchar | |
| 288 | +* char | |
| 289 | +* text | |
| 290 | +* mediumtext | |
| 291 | +* tinytext | |
| 292 | +* longtext | |
| 293 | + | |
| 294 | +Example:ย | |
| 295 | + | |
| 296 | +```javascript | |
| 297 | +{ userName : | |
| 298 | + { type: String, | |
| 299 | + dataType: 'char', | |
| 300 | + limit: 24 | |
| 301 | + } | |
| 302 | +} | |
| 303 | +``` | |
| 304 | + | |
| 305 | +Example:ย | |
| 306 | + | |
| 307 | +```javascript | |
| 308 | +{ biography : | |
| 309 | + { type: String, | |
| 310 | + dataType: 'longtext' | |
| 311 | + } | |
| 312 | +} | |
| 313 | +``` | |
| 314 | + | |
| 315 | +Convert JSON Date types to ย datetime orย timestamp | |
| 316 | + | |
| 317 | +Example:ย | |
| 318 | + | |
| 319 | +```javascript | |
| 320 | +{ startTime : | |
| 321 | + { type: Date, | |
| 322 | + dataType: 'timestamp' | |
| 323 | + } | |
| 324 | +} | |
| 325 | +``` | |
| 326 | + | |
| 327 | +### Enum | |
| 328 | + | |
| 329 | +Enums are special. Create an Enum using Enum factory: | |
| 330 | + | |
| 331 | +```javascript | |
| 332 | +var MOOD = dataSource.EnumFactory('glad', 'sad', 'mad');ย | |
| 333 | +MOOD.SAD; // 'sad'ย | |
| 334 | +MOOD(2); // 'sad'ย | |
| 335 | +MOOD('SAD'); // 'sad'ย | |
| 336 | +MOOD('sad'); // 'sad' | |
| 337 | +{ mood: { type: MOOD }} | |
| 338 | +{ choice: { type: dataSource.EnumFactory('yes', 'no', 'maybe'), null: false }} | |
| 339 | +``` | |
| 340 | + | |
| 341 | +## Discovery and auto-migration | |
| 342 | + | |
| 343 | +### Model discovery | |
| 344 | + | |
| 345 | +The MySQL connector supports _model discovery_ that enables you to create LoopBack models | |
| 346 | +based on an existing database schema using the unifiedย [database discovery API](http://apidocs.strongloop.com/loopback-datasource-juggler/#datasource-prototype-discoverandbuildmodels). For more information on discovery, see [Discovering models from relational databases](https://loopback.io/doc/en/lb3/Discovering-models-from-relational-databases.html). | |
| 347 | + | |
| 348 | +### Auto-migratiion | |
| 349 | + | |
| 350 | +The MySQL connector also supports _auto-migration_ that enables you to create a database schema | |
| 351 | +from LoopBack models using the [LoopBack automigrate method](http://apidocs.strongloop.com/loopback-datasource-juggler/#datasource-prototype-automigrate). | |
| 352 | + | |
| 353 | +For more information on auto-migration, seeย [Creating a database schema from models](https://loopback.io/doc/en/lb3/Creating-a-database-schema-from-models.html) for more information. | |
| 354 | + | |
| 355 | +Destroying models may result in errors due to foreign key integrity. First delete any related models first calling delete on models with relationships. | |
| 356 | + | |
| 357 | +## Running tests | |
| 358 | + | |
| 359 | +The tests in this repository are mainly integration tests, meaning you will need to run them using our preconfigured test server. | |
| 360 | + | |
| 361 | +1. Ask a core developer for instructions on how to set up test server credentials on your machine | |
| 362 | +2. `npm test` | ... | ... |
| 1 | +++ a/example/app.js | |
| ... | ... | @@ -0,0 +1,41 @@ |
| 1 | +// Copyright IBM Corp. 2013,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +var DataSource = require('loopback-datasource-juggler').DataSource; | |
| 8 | + | |
| 9 | +var config = require('rc')('loopback', {dev: {mysql: {}}}).dev.mysql; | |
| 10 | + | |
| 11 | +var ds = new DataSource(require('../'), config); | |
| 12 | + | |
| 13 | +function show(err, models) { | |
| 14 | + if (err) { | |
| 15 | + console.error(err); | |
| 16 | + } else { | |
| 17 | + console.log(models); | |
| 18 | + if (models) { | |
| 19 | + models.forEach(function(m) { | |
| 20 | + console.dir(m); | |
| 21 | + }); | |
| 22 | + } | |
| 23 | + } | |
| 24 | +} | |
| 25 | + | |
| 26 | +ds.discoverModelDefinitions({views: true, limit: 20}, show); | |
| 27 | + | |
| 28 | +ds.discoverModelProperties('customer', show); | |
| 29 | + | |
| 30 | +ds.discoverModelProperties('location', {owner: 'strongloop'}, show); | |
| 31 | + | |
| 32 | +ds.discoverPrimaryKeys('customer', show); | |
| 33 | +ds.discoverForeignKeys('inventory', show); | |
| 34 | + | |
| 35 | +ds.discoverExportedForeignKeys('location', show); | |
| 36 | + | |
| 37 | +ds.discoverAndBuildModels('weapon', {owner: 'strongloop', visited: {}, associations: true}, function(err, models) { | |
| 38 | + for (var m in models) { | |
| 39 | + models[m].all(show); | |
| 40 | + } | |
| 41 | +}); | ... | ... |
No preview for this file type
| 1 | +++ a/index.js | |
| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | +// Copyright IBM Corp. 2012. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +var SG = require('strong-globalize'); | |
| 8 | +SG.SetRootDir(__dirname); | |
| 9 | + | |
| 10 | +module.exports = require('./lib/mysql.js'); | ... | ... |
| 1 | +++ a/intl/de/messages.json | |
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +{ | |
| 2 | + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} muss ein {{object}} sein: {0}", | |
| 3 | + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} ist ein erforderliches Zeichenfolgeargument: {0}", | |
| 4 | + "b7c60421de706ca1e050f2a86953745e": "Keine Argumente - {{Enum}} konnte nicht erstellt werden.", | |
| 5 | + "80a32e80cbed65eba2103201a7c94710": "Modell nicht gefunden: {0}", | |
| 6 | + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}}-Syntax berรผcksichtigt nicht das {{`g`}}-Flag", | |
| 7 | + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}}-Syntax berรผcksichtigt nicht das {{`i`}}-Flag", | |
| 8 | + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}}-Syntax berรผcksichtigt nicht das {{`m`}}-Flag", | |
| 9 | + "57512a471969647e8eaa2509cc292018": "{{callback}} sollte eine Funktion sein" | |
| 10 | +} | |
| 11 | + | ... | ... |
| 1 | +++ a/intl/en/messages.json | |
| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | +{ | |
| 2 | + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} must be an {{object}}: {0}", | |
| 3 | + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} is a required string argument: {0}", | |
| 4 | + "b7c60421de706ca1e050f2a86953745e": "No arguments - could not create {{Enum}}.", | |
| 5 | + "80a32e80cbed65eba2103201a7c94710": "Model not found: {0}", | |
| 6 | + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}} syntax does not respect the {{`g`}} flag", | |
| 7 | + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}} syntax does not respect the {{`i`}} flag", | |
| 8 | + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}} syntax does not respect the {{`m`}} flag", | |
| 9 | + "57512a471969647e8eaa2509cc292018": "{{callback}} should be a function" | |
| 10 | +} | ... | ... |
| 1 | +++ a/intl/es/messages.json | |
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +{ | |
| 2 | + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} debe ser un {{object}}: {0}", | |
| 3 | + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} es un argumento de serie necesario: {0}", | |
| 4 | + "b7c60421de706ca1e050f2a86953745e": "No hay argumentos - no se ha podido crear {{Enum}}.", | |
| 5 | + "80a32e80cbed65eba2103201a7c94710": "No se ha encontrado el modelo: {0}", | |
| 6 | + "026ed55518f3812a9ef4b86e8a195e76": "la sintaxis de {{MySQL}} {{regex}} no respeta el distintivo {{`g`}}", | |
| 7 | + "0ac9f848b934332210bb27747d12a033": "la sintaxis de {{MySQL}} {{regex}} no respeta el distintivo {{`i`}}", | |
| 8 | + "4e9e35876bfb1511205456b52c6659d0": "la sintaxis de {{MySQL}} {{regex}} no respeta el distintivo {{`m`}}", | |
| 9 | + "57512a471969647e8eaa2509cc292018": "{{callback}} debe ser una funciรณn" | |
| 10 | +} | |
| 11 | + | ... | ... |
| 1 | +++ a/intl/fr/messages.json | |
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +{ | |
| 2 | + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} doit รชtre un {{object}} : {0}", | |
| 3 | + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} est un argument de chaรฎne obligatoire : {0}", | |
| 4 | + "b7c60421de706ca1e050f2a86953745e": "Aucun argument - impossible de crรฉer {{Enum}}.", | |
| 5 | + "80a32e80cbed65eba2103201a7c94710": "Modรจle introuvable : {0}", | |
| 6 | + "026ed55518f3812a9ef4b86e8a195e76": "La syntaxe {{MySQL}} {{regex}} ne respecte pas l'indicateur {{`g`}}", | |
| 7 | + "0ac9f848b934332210bb27747d12a033": "La syntaxe {{MySQL}} {{regex}} ne respecte pas l'indicateur {{`i`}}", | |
| 8 | + "4e9e35876bfb1511205456b52c6659d0": "La syntaxe {{MySQL}} {{regex}} ne respecte pas l'indicateur {{`m`}}", | |
| 9 | + "57512a471969647e8eaa2509cc292018": "{{callback}} doit รชtre une fonction" | |
| 10 | +} | |
| 11 | + | ... | ... |
| 1 | +++ a/intl/it/messages.json | |
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +{ | |
| 2 | + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} deve essere un {{object}}: {0}", | |
| 3 | + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} รจ un argomento stringa obbligatorio: {0}", | |
| 4 | + "b7c60421de706ca1e050f2a86953745e": "Nessun argomento - impossibile creare {{Enum}}.", | |
| 5 | + "80a32e80cbed65eba2103201a7c94710": "Modello non trovato: {0}", | |
| 6 | + "026ed55518f3812a9ef4b86e8a195e76": "La sintassi {{MySQL}} {{regex}} non rispetta l'indicatore {{`g`}}", | |
| 7 | + "0ac9f848b934332210bb27747d12a033": "La sintassi {{MySQL}} {{regex}} non rispetta l'indicatore {{`i`}}", | |
| 8 | + "4e9e35876bfb1511205456b52c6659d0": "La sintassi {{MySQL}} {{regex}} non rispetta l'indicatore {{`m`}}", | |
| 9 | + "57512a471969647e8eaa2509cc292018": "{{callback}} deve essere una funzione" | |
| 10 | +} | |
| 11 | + | ... | ... |
| 1 | +++ a/intl/ja/messages.json | |
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +{ | |
| 2 | + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} ใฏ {{object}} ใงใชใใใฐใชใใพใใ: {0}", | |
| 3 | + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} ใฏๅฟ ้ ใฎในใใชใณใฐๅผๆฐใงใ: {0}", | |
| 4 | + "b7c60421de706ca1e050f2a86953745e": "ๅผๆฐใใใใพใใ - {{Enum}} ใไฝๆใงใใพใใใงใใใ", | |
| 5 | + "80a32e80cbed65eba2103201a7c94710": "ใขใใซใ่ฆใคใใใพใใ: {0}", | |
| 6 | + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}} ๆงๆใงใฏ {{`g`}} ใใฉใฐใฏ่ๆ ฎใใใพใใ", | |
| 7 | + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}} ๆงๆใงใฏ {{`i`}} ใใฉใฐใฏ่ๆ ฎใใใพใใ", | |
| 8 | + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}} ๆงๆใงใฏ {{`m`}} ใใฉใฐใฏ่ๆ ฎใใใพใใ", | |
| 9 | + "57512a471969647e8eaa2509cc292018": "{{callback}} ใฏ้ขๆฐใงใชใใใฐใชใใพใใ" | |
| 10 | +} | |
| 11 | + | ... | ... |
| 1 | +++ a/intl/ko/messages.json | |
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +{ | |
| 2 | + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}}์ด(๊ฐ) {{object}}์ด์ด์ผ ํจ: {0}", | |
| 3 | + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}}์ ํ์ ๋ฌธ์์ด ์ธ์์: {0}", | |
| 4 | + "b7c60421de706ca1e050f2a86953745e": "์ธ์ ์์ - {{Enum}}์(๋ฅผ) ์์ฑํ ์ ์์ต๋๋ค. ", | |
| 5 | + "80a32e80cbed65eba2103201a7c94710": "๋ชจ๋ธ์ ์ฐพ์ ์ ์์: {0}", | |
| 6 | + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}} ๊ตฌ๋ฌธ์์ {{`g`}} ํ๋๊ทธ๋ฅผ ์ค์ํ์ง ์์", | |
| 7 | + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}} ๊ตฌ๋ฌธ์์ {{`i`}} ํ๋๊ทธ๋ฅผ ์ค์ํ์ง ์์", | |
| 8 | + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}} ๊ตฌ๋ฌธ์์ {{`m`}} ํ๋๊ทธ๋ฅผ ์ค์ํ์ง ์์", | |
| 9 | + "57512a471969647e8eaa2509cc292018": "{{callback}}์ด(๊ฐ) ํจ์์ฌ์ผ ํจ" | |
| 10 | +} | |
| 11 | + | ... | ... |
| 1 | +++ a/intl/nl/messages.json | |
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +{ | |
| 2 | + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} moet een {{object}} zijn: {0}", | |
| 3 | + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} is een verplicht tekenreeksargument: {0}", | |
| 4 | + "b7c60421de706ca1e050f2a86953745e": "Geen argumenten - {{Enum}} kan niet worden gemaakt.", | |
| 5 | + "80a32e80cbed65eba2103201a7c94710": "Model is niet gevonden: {0}", | |
| 6 | + "026ed55518f3812a9ef4b86e8a195e76": "Syntaxis van {{MySQL}} {{regex}} voldoet niet aan vlag {{`g`}}", | |
| 7 | + "0ac9f848b934332210bb27747d12a033": "Syntaxis van {{MySQL}} {{regex}} voldoet niet aan vlag {{`i`}}", | |
| 8 | + "4e9e35876bfb1511205456b52c6659d0": "Syntaxis van {{MySQL}} {{regex}} voldoet niet aan vlag {{`m`}}", | |
| 9 | + "57512a471969647e8eaa2509cc292018": "{{callback}} moet een functie zijn" | |
| 10 | +} | |
| 11 | + | ... | ... |
| 1 | +++ a/intl/pt/messages.json | |
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +{ | |
| 2 | + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} deve ser um {{object}}: {0}", | |
| 3 | + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} รฉ um argumento de sequรชncia necessรกrio: {0}", | |
| 4 | + "b7c60421de706ca1e050f2a86953745e": "Sem argumentos - nรฃo foi possรญvel criar {{Enum}}.", | |
| 5 | + "80a32e80cbed65eba2103201a7c94710": "Modelo nรฃo localizado: {0}", | |
| 6 | + "026ed55518f3812a9ef4b86e8a195e76": "Sintaxe {{regex}} de {{MySQL}} nรฃo respeita a sinalizaรงรฃo {{`g`}}", | |
| 7 | + "0ac9f848b934332210bb27747d12a033": "Sintaxe {{regex}} de {{MySQL}} nรฃo respeita a sinalizaรงรฃo {{`i`}}", | |
| 8 | + "4e9e35876bfb1511205456b52c6659d0": "Sintaxe {{regex}} de {{MySQL}} nรฃo respeita a sinalizaรงรฃo {{`m`}}", | |
| 9 | + "57512a471969647e8eaa2509cc292018": "{{callback}} deve ser uma funรงรฃo" | |
| 10 | +} | |
| 11 | + | ... | ... |
| 1 | +++ a/intl/tr/messages.json | |
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +{ | |
| 2 | + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} bir {{object}} olmalฤฑdฤฑr: {0}", | |
| 3 | + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} zorunlu bir dizgi baฤฤฑmsฤฑz deฤiลkeni: {0}", | |
| 4 | + "b7c60421de706ca1e050f2a86953745e": "Baฤฤฑmsฤฑz deฤiลken yok - {{Enum}} yaratฤฑlamadฤฑ.", | |
| 5 | + "80a32e80cbed65eba2103201a7c94710": "Model bulunamadฤฑ: {0}", | |
| 6 | + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}} dรผzenli ifade sรถzdizimi {{`g`}} iลareti kuralฤฑna uymuyor", | |
| 7 | + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}} dรผzenli ifade sรถzdizimi {{`i`}} iลareti kuralฤฑna uymuyor", | |
| 8 | + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}} dรผzenli ifade sรถzdizimi {{`m`}} iลareti kuralฤฑna uymuyor", | |
| 9 | + "57512a471969647e8eaa2509cc292018": "{{callback}} bir iลlev olmalฤฑdฤฑr" | |
| 10 | +} | |
| 11 | + | ... | ... |
| 1 | +++ a/intl/zh-Hans/messages.json | |
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +{ | |
| 2 | + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} ๅฟ ้กปไธบ {{object}}๏ผ{0}", | |
| 3 | + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} ๆฏๅฟ ้็ๅญ็ฌฆไธฒ่ชๅ้๏ผ{0}", | |
| 4 | + "b7c60421de706ca1e050f2a86953745e": "ๆ ่ชๅ้ - ๆ ๆณๅๅปบ {{Enum}}ใ", | |
| 5 | + "80a32e80cbed65eba2103201a7c94710": "ๆพไธๅฐๆจกๅ๏ผ{0}", | |
| 6 | + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}} ่ฏญๆณไธ่่ {{`g`}} ๆ ๅฟ", | |
| 7 | + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}} ่ฏญๆณไธ่่ {{`i`}} ๆ ๅฟ", | |
| 8 | + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}} ่ฏญๆณไธ่่ {{`m`}} ๆ ๅฟ", | |
| 9 | + "57512a471969647e8eaa2509cc292018": "{{callback}} ๅบ่ฏฅๆฏๅฝๆฐ" | |
| 10 | +} | |
| 11 | + | ... | ... |
| 1 | +++ a/intl/zh-Hant/messages.json | |
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +{ | |
| 2 | + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} ๅฟ ้ ๆฏ {{object}}๏ผ{0}", | |
| 3 | + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} ๆฏๅฟ ่ฆ็ๅญไธฒๅผๆธ๏ผ{0}", | |
| 4 | + "b7c60421de706ca1e050f2a86953745e": "ๆฒๆๅผๆธ - ็กๆณๅปบ็ซ {{Enum}}ใ", | |
| 5 | + "80a32e80cbed65eba2103201a7c94710": "ๆพไธๅฐๆจกๅ๏ผ{0}", | |
| 6 | + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}} ่ชๆณๆช้ตๅพช {{`g`}} ๆๆจ", | |
| 7 | + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}} ่ชๆณๆช้ตๅพช {{`i`}} ๆๆจ", | |
| 8 | + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}} ่ชๆณๆช้ตๅพช {{`m`}} ๆๆจ", | |
| 9 | + "57512a471969647e8eaa2509cc292018": "{{callback}} ๆ่ฉฒๆฏๅฝๆธ" | |
| 10 | +} | |
| 11 | + | ... | ... |
| 1 | +++ a/lib/discovery.js | |
| ... | ... | @@ -0,0 +1,395 @@ |
| 1 | +// Copyright IBM Corp. 2013,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +var g = require('strong-globalize')(); | |
| 8 | + | |
| 9 | +module.exports = mixinDiscovery; | |
| 10 | + | |
| 11 | +/*! | |
| 12 | + * @param {MySQL} MySQL connector class | |
| 13 | + * @param {Object} mysql mysql driver | |
| 14 | + */ | |
| 15 | +function mixinDiscovery(MySQL, mysql) { | |
| 16 | + var async = require('async'); | |
| 17 | + | |
| 18 | + function paginateSQL(sql, orderBy, options) { | |
| 19 | + options = options || {}; | |
| 20 | + var limitClause = ''; | |
| 21 | + if (options.offset || options.skip || options.limit) { | |
| 22 | + // Offset starts from 0 | |
| 23 | + var offset = Number(options.offset || options.skip || 0); | |
| 24 | + if (isNaN(offset)) { | |
| 25 | + offset = 0; | |
| 26 | + } | |
| 27 | + limitClause = ' LIMIT ' + offset; | |
| 28 | + if (options.limit) { | |
| 29 | + var limit = Number(options.limit); | |
| 30 | + if (isNaN(limit)) { | |
| 31 | + limit = 0; | |
| 32 | + } | |
| 33 | + limitClause = limitClause + ',' + limit; | |
| 34 | + } | |
| 35 | + } | |
| 36 | + if (!orderBy) { | |
| 37 | + sql += ' ORDER BY ' + orderBy; | |
| 38 | + } | |
| 39 | + return sql + limitClause; | |
| 40 | + } | |
| 41 | + | |
| 42 | + /*! | |
| 43 | + * Build sql for listing schemas (databases in MySQL) | |
| 44 | + * @params {Object} [options] Options object | |
| 45 | + * @returns {String} The SQL statement | |
| 46 | + */ | |
| 47 | + MySQL.prototype.buildQuerySchemas = function(options) { | |
| 48 | + var sql = 'SELECT catalog_name as "catalog",' + | |
| 49 | + ' schema_name as "schema"' + | |
| 50 | + ' FROM information_schema.schemata'; | |
| 51 | + return paginateSQL(sql, 'schema_name', options); | |
| 52 | + }; | |
| 53 | + | |
| 54 | + /*! | |
| 55 | + * Build sql for listing tables | |
| 56 | + * @param options {all: for all owners, owner: for a given owner} | |
| 57 | + * @returns {string} The sql statement | |
| 58 | + */ | |
| 59 | + MySQL.prototype.buildQueryTables = function(options) { | |
| 60 | + var sqlTables = null; | |
| 61 | + var schema = options.owner || options.schema; | |
| 62 | + | |
| 63 | + if (options.all && !schema) { | |
| 64 | + sqlTables = paginateSQL('SELECT \'table\' AS "type",' + | |
| 65 | + ' table_name AS "name", table_schema AS "owner"' + | |
| 66 | + ' FROM information_schema.tables', | |
| 67 | + 'table_schema, table_name', options); | |
| 68 | + } else if (schema) { | |
| 69 | + sqlTables = paginateSQL('SELECT \'table\' AS "type",' + | |
| 70 | + ' table_name AS "name", table_schema AS "schema"' + | |
| 71 | + ' FROM information_schema.tables' + | |
| 72 | + ' WHERE table_schema=' + mysql.escape(schema), | |
| 73 | + 'table_schema, table_name', options); | |
| 74 | + } else { | |
| 75 | + sqlTables = paginateSQL('SELECT \'table\' AS "type",' + | |
| 76 | + ' table_name AS "name", ' + | |
| 77 | + ' table_schema AS "owner" FROM information_schema.tables' + | |
| 78 | + ' WHERE table_schema=SUBSTRING_INDEX(USER(),\'@\',1)', | |
| 79 | + 'table_name', options); | |
| 80 | + } | |
| 81 | + return sqlTables; | |
| 82 | + }; | |
| 83 | + | |
| 84 | + /*! | |
| 85 | + * Build sql for listing views | |
| 86 | + * @param options {all: for all owners, owner: for a given owner} | |
| 87 | + * @returns {string} The sql statement | |
| 88 | + */ | |
| 89 | + MySQL.prototype.buildQueryViews = function(options) { | |
| 90 | + var sqlViews = null; | |
| 91 | + if (options.views) { | |
| 92 | + var schema = options.owner || options.schema; | |
| 93 | + | |
| 94 | + if (options.all && !schema) { | |
| 95 | + sqlViews = paginateSQL('SELECT \'view\' AS "type",' + | |
| 96 | + ' table_name AS "name",' + | |
| 97 | + ' table_schema AS "owner"' + | |
| 98 | + ' FROM information_schema.views', | |
| 99 | + 'table_schema, table_name', options); | |
| 100 | + } else if (schema) { | |
| 101 | + sqlViews = paginateSQL('SELECT \'view\' AS "type",' + | |
| 102 | + ' table_name AS "name",' + | |
| 103 | + ' table_schema AS "owner"' + | |
| 104 | + ' FROM information_schema.views' + | |
| 105 | + ' WHERE table_schema=' + mysql.escape(schema), | |
| 106 | + 'table_schema, table_name', options); | |
| 107 | + } else { | |
| 108 | + sqlViews = paginateSQL('SELECT \'view\' AS "type",' + | |
| 109 | + ' table_name AS "name",' + | |
| 110 | + ' table_schema AS "owner"' + | |
| 111 | + ' FROM information_schema.views', | |
| 112 | + 'table_name', options); | |
| 113 | + } | |
| 114 | + } | |
| 115 | + return sqlViews; | |
| 116 | + }; | |
| 117 | + | |
| 118 | + /** | |
| 119 | + * Discover model definitions | |
| 120 | + * | |
| 121 | + * @param {Object} options Options for discovery | |
| 122 | + * @param {Function} [cb] The callback function | |
| 123 | + */ | |
| 124 | + | |
| 125 | + /*! | |
| 126 | + * Normalize the arguments | |
| 127 | + * @param table string, required | |
| 128 | + * @param options object, optional | |
| 129 | + * @param cb function, optional | |
| 130 | + */ | |
| 131 | + MySQL.prototype.getArgs = function(table, options, cb) { | |
| 132 | + if ('string' !== typeof table || !table) { | |
| 133 | + throw new Error(g.f('{{table}} is a required string argument: %s', table)); | |
| 134 | + } | |
| 135 | + options = options || {}; | |
| 136 | + if (!cb && 'function' === typeof options) { | |
| 137 | + cb = options; | |
| 138 | + options = {}; | |
| 139 | + } | |
| 140 | + if (typeof options !== 'object') { | |
| 141 | + throw new Error(g.f('{{options}} must be an {{object}}: %s', options)); | |
| 142 | + } | |
| 143 | + return { | |
| 144 | + schema: options.owner || options.schema, | |
| 145 | + table: table, | |
| 146 | + options: options, | |
| 147 | + cb: cb, | |
| 148 | + }; | |
| 149 | + }; | |
| 150 | + | |
| 151 | + /*! | |
| 152 | + * Build the sql statement to query columns for a given table | |
| 153 | + * @param schema | |
| 154 | + * @param table | |
| 155 | + * @returns {String} The sql statement | |
| 156 | + */ | |
| 157 | + MySQL.prototype.buildQueryColumns = function(schema, table) { | |
| 158 | + var sql = null; | |
| 159 | + if (schema) { | |
| 160 | + sql = paginateSQL('SELECT table_schema AS "owner",' + | |
| 161 | + ' table_name AS "tableName",' + | |
| 162 | + ' column_name AS "columnName",' + | |
| 163 | + ' data_type AS "dataType",' + | |
| 164 | + ' character_maximum_length AS "dataLength",' + | |
| 165 | + ' numeric_precision AS "dataPrecision",' + | |
| 166 | + ' numeric_scale AS "dataScale",' + | |
| 167 | + ' column_type AS "columnType",' + | |
| 168 | + ' is_nullable = \'YES\' AS "nullable",' + | |
| 169 | + ' CASE WHEN extra LIKE \'%auto_increment%\' THEN 1 ELSE 0 END AS "generated"' + | |
| 170 | + ' FROM information_schema.columns' + | |
| 171 | + ' WHERE table_schema=' + mysql.escape(schema) + | |
| 172 | + (table ? ' AND table_name=' + mysql.escape(table) : ''), | |
| 173 | + 'table_name, ordinal_position', {}); | |
| 174 | + } else { | |
| 175 | + sql = paginateSQL('SELECT table_schema AS "owner",' + | |
| 176 | + ' table_name AS "tableName",' + | |
| 177 | + ' column_name AS "columnName",' + | |
| 178 | + ' data_type AS "dataType",' + | |
| 179 | + ' character_maximum_length AS "dataLength",' + | |
| 180 | + ' numeric_precision AS "dataPrecision",' + | |
| 181 | + ' numeric_scale AS "dataScale",' + | |
| 182 | + ' column_type AS "columnType",' + | |
| 183 | + ' is_nullable = \'YES\' AS "nullable",' + | |
| 184 | + ' CASE WHEN extra LIKE \'%auto_increment%\' THEN 1 ELSE 0 END AS "generated"' + | |
| 185 | + ' FROM information_schema.columns' + | |
| 186 | + (table ? ' WHERE table_name=' + mysql.escape(table) : ''), | |
| 187 | + 'table_name, ordinal_position', {}); | |
| 188 | + } | |
| 189 | + return sql; | |
| 190 | + }; | |
| 191 | + | |
| 192 | + /** | |
| 193 | + * Discover model properties from a table | |
| 194 | + * @param {String} table The table name | |
| 195 | + * @param {Object} options The options for discovery | |
| 196 | + * @param {Function} [cb] The callback function | |
| 197 | + * | |
| 198 | + */ | |
| 199 | + | |
| 200 | + /*! | |
| 201 | + * Build the sql statement for querying primary keys of a given table | |
| 202 | + * @param schema | |
| 203 | + * @param table | |
| 204 | + * @returns {string} | |
| 205 | + */ | |
| 206 | +// http://docs.oracle.com/javase/6/docs/api/java/sql/DatabaseMetaData.html | |
| 207 | +// #getPrimaryKeys(java.lang.String, java.lang.String, java.lang.String) | |
| 208 | + MySQL.prototype.buildQueryPrimaryKeys = function(schema, table) { | |
| 209 | + var sql = 'SELECT table_schema AS "owner",' + | |
| 210 | + ' table_name AS "tableName",' + | |
| 211 | + ' column_name AS "columnName",' + | |
| 212 | + ' ordinal_position AS "keySeq",' + | |
| 213 | + ' constraint_name AS "pkName"' + | |
| 214 | + ' FROM information_schema.key_column_usage' + | |
| 215 | + ' WHERE constraint_name=\'PRIMARY\''; | |
| 216 | + | |
| 217 | + if (schema) { | |
| 218 | + sql += ' AND table_schema=' + mysql.escape(schema); | |
| 219 | + } | |
| 220 | + if (table) { | |
| 221 | + sql += ' AND table_name=' + mysql.escape(table); | |
| 222 | + } | |
| 223 | + sql += ' ORDER BY' + | |
| 224 | + ' table_schema, constraint_name, table_name, ordinal_position'; | |
| 225 | + return sql; | |
| 226 | + }; | |
| 227 | + | |
| 228 | + /** | |
| 229 | + * Discover primary keys for a given table | |
| 230 | + * @param {String} table The table name | |
| 231 | + * @param {Object} options The options for discovery | |
| 232 | + * @param {Function} [cb] The callback function | |
| 233 | + */ | |
| 234 | + | |
| 235 | + /*! | |
| 236 | + * Build the sql statement for querying foreign keys of a given table | |
| 237 | + * @param schema | |
| 238 | + * @param table | |
| 239 | + * @returns {string} | |
| 240 | + */ | |
| 241 | + MySQL.prototype.buildQueryForeignKeys = function(schema, table) { | |
| 242 | + var sql = | |
| 243 | + 'SELECT table_schema AS "fkOwner",' + | |
| 244 | + ' constraint_name AS "fkName",' + | |
| 245 | + ' table_name AS "fkTableName",' + | |
| 246 | + ' column_name AS "fkColumnName",' + | |
| 247 | + ' ordinal_position AS "keySeq",' + | |
| 248 | + ' referenced_table_schema AS "pkOwner", \'PRIMARY\' AS "pkName",' + | |
| 249 | + ' referenced_table_name AS "pkTableName",' + | |
| 250 | + ' referenced_column_name AS "pkColumnName"' + | |
| 251 | + ' FROM information_schema.key_column_usage' + | |
| 252 | + ' WHERE constraint_name!=\'PRIMARY\'' + | |
| 253 | + ' AND POSITION_IN_UNIQUE_CONSTRAINT IS NOT NULL'; | |
| 254 | + if (schema) { | |
| 255 | + sql += ' AND table_schema=' + mysql.escape(schema); | |
| 256 | + } | |
| 257 | + if (table) { | |
| 258 | + sql += ' AND table_name=' + mysql.escape(table); | |
| 259 | + } | |
| 260 | + return sql; | |
| 261 | + }; | |
| 262 | + | |
| 263 | + /** | |
| 264 | + * Discover foreign keys for a given table | |
| 265 | + * @param {String} table The table name | |
| 266 | + * @param {Object} options The options for discovery | |
| 267 | + * @param {Function} [cb] The callback function | |
| 268 | + */ | |
| 269 | + | |
| 270 | + /*! | |
| 271 | + * Retrieves a description of the foreign key columns that reference the | |
| 272 | + * given table's primary key columns (the foreign keys exported by a table). | |
| 273 | + * They are ordered by fkTableOwner, fkTableName, and keySeq. | |
| 274 | + * @param schema | |
| 275 | + * @param table | |
| 276 | + * @returns {string} | |
| 277 | + */ | |
| 278 | + MySQL.prototype.buildQueryExportedForeignKeys = function(schema, table) { | |
| 279 | + var sql = 'SELECT a.constraint_name AS "fkName",' + | |
| 280 | + ' a.table_schema AS "fkOwner",' + | |
| 281 | + ' a.table_name AS "fkTableName",' + | |
| 282 | + ' a.column_name AS "fkColumnName",' + | |
| 283 | + ' a.ordinal_position AS "keySeq",' + | |
| 284 | + ' NULL AS "pkName",' + | |
| 285 | + ' a.referenced_table_schema AS "pkOwner",' + | |
| 286 | + ' a.referenced_table_name AS "pkTableName",' + | |
| 287 | + ' a.referenced_column_name AS "pkColumnName"' + | |
| 288 | + ' FROM information_schema.key_column_usage a' + | |
| 289 | + ' WHERE a.position_in_unique_constraint IS NOT NULL'; | |
| 290 | + if (schema) { | |
| 291 | + sql += ' AND a.referenced_table_schema=' + mysql.escape(schema); | |
| 292 | + } | |
| 293 | + if (table) { | |
| 294 | + sql += ' AND a.referenced_table_name=' + mysql.escape(table); | |
| 295 | + } | |
| 296 | + sql += ' ORDER BY a.table_schema, a.table_name, a.ordinal_position'; | |
| 297 | + | |
| 298 | + return sql; | |
| 299 | + }; | |
| 300 | + | |
| 301 | + /** | |
| 302 | + * Discover foreign keys that reference to the primary key of this table | |
| 303 | + * @param {String} table The table name | |
| 304 | + * @param {Object} options The options for discovery | |
| 305 | + * @param {Function} [cb] The callback function | |
| 306 | + */ | |
| 307 | + | |
| 308 | + MySQL.prototype.buildPropertyType = function(columnDefinition, options) { | |
| 309 | + var mysqlType = columnDefinition.dataType; | |
| 310 | + var columnType = columnDefinition.columnType; | |
| 311 | + var dataLength = columnDefinition.dataLength; | |
| 312 | + | |
| 313 | + var type = mysqlType.toUpperCase(); | |
| 314 | + switch (type) { | |
| 315 | + case 'CHAR': | |
| 316 | + if (!options.treatCHAR1AsString && columnType === 'char(1)') { | |
| 317 | + // Treat char(1) as boolean ('Y', 'N', 'T', 'F', '0', '1') | |
| 318 | + return 'Boolean'; | |
| 319 | + } | |
| 320 | + case 'VARCHAR': | |
| 321 | + case 'TINYTEXT': | |
| 322 | + case 'MEDIUMTEXT': | |
| 323 | + case 'LONGTEXT': | |
| 324 | + case 'TEXT': | |
| 325 | + case 'ENUM': | |
| 326 | + case 'SET': | |
| 327 | + return 'String'; | |
| 328 | + case 'TINYBLOB': | |
| 329 | + case 'MEDIUMBLOB': | |
| 330 | + case 'LONGBLOB': | |
| 331 | + case 'BLOB': | |
| 332 | + case 'BINARY': | |
| 333 | + case 'VARBINARY': | |
| 334 | + case 'BIT': | |
| 335 | + // treat BIT(1) as boolean as it's 1 or 0 | |
| 336 | + if (!options.treatBIT1AsBit && columnType === 'bit(1)') { | |
| 337 | + return 'Boolean'; | |
| 338 | + } | |
| 339 | + return 'Binary'; | |
| 340 | + case 'TINYINT': | |
| 341 | + // treat TINYINT(1) as boolean as it is aliased as BOOL and BOOLEAN in mysql | |
| 342 | + if (!options.treatTINYINT1AsTinyInt && columnType === 'tinyint(1)') { | |
| 343 | + return 'Boolean'; | |
| 344 | + } | |
| 345 | + case 'SMALLINT': | |
| 346 | + case 'INT': | |
| 347 | + case 'MEDIUMINT': | |
| 348 | + case 'YEAR': | |
| 349 | + case 'FLOAT': | |
| 350 | + case 'DOUBLE': | |
| 351 | + case 'BIGINT': | |
| 352 | + return 'Number'; | |
| 353 | + case 'DATE': | |
| 354 | + case 'TIMESTAMP': | |
| 355 | + case 'DATETIME': | |
| 356 | + return 'Date'; | |
| 357 | + case 'POINT': | |
| 358 | + return 'GeoPoint'; | |
| 359 | + case 'BOOL': | |
| 360 | + case 'BOOLEAN': | |
| 361 | + return 'Boolean'; | |
| 362 | + default: | |
| 363 | + return 'String'; | |
| 364 | + } | |
| 365 | + }; | |
| 366 | + | |
| 367 | + MySQL.prototype.getDefaultSchema = function() { | |
| 368 | + if (this.dataSource && this.dataSource.settings && | |
| 369 | + this.dataSource.settings.database) { | |
| 370 | + return this.dataSource.settings.database; | |
| 371 | + } | |
| 372 | + return undefined; | |
| 373 | + }; | |
| 374 | + | |
| 375 | + // Recommended MySQL 5.7 Boolean scheme. See | |
| 376 | + // http://dev.mysql.com/doc/refman/5.7/en/numeric-type-overview.html | |
| 377 | + // Currently default is the inverse of the recommendation for backward compatibility. | |
| 378 | + MySQL.prototype.setDefaultOptions = function(options) { | |
| 379 | + var defaultOptions = { | |
| 380 | + treatCHAR1AsString: false, | |
| 381 | + treatBIT1AsBit: true, | |
| 382 | + treatTINYINT1AsTinyInt: true, | |
| 383 | + }; | |
| 384 | + | |
| 385 | + for (var opt in defaultOptions) { | |
| 386 | + if (defaultOptions.hasOwnProperty(opt) && !options.hasOwnProperty(opt)) { | |
| 387 | + options[opt] = defaultOptions[opt]; | |
| 388 | + } | |
| 389 | + } | |
| 390 | + }; | |
| 391 | + | |
| 392 | + MySQL.prototype.setNullableProperty = function(r) { | |
| 393 | + r.nullable = r.nullable ? 'Y' : 'N'; | |
| 394 | + }; | |
| 395 | +} | ... | ... |
| 1 | +++ a/lib/enumFactory.js | |
| ... | ... | @@ -0,0 +1,67 @@ |
| 1 | +// Copyright IBM Corp. 2013,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +var g = require('strong-globalize')(); | |
| 8 | + | |
| 9 | +var EnumFactory = function() { | |
| 10 | + if (arguments.length > 0) { | |
| 11 | + var Enum = function Enum(arg) { | |
| 12 | + if (typeof arg === 'number' && arg % 1 == 0) { | |
| 13 | + return Enum._values[arg]; | |
| 14 | + } else if (Enum[arg]) { | |
| 15 | + return Enum[arg]; | |
| 16 | + } else if (Enum._values.indexOf(arg) !== -1) { | |
| 17 | + return arg; | |
| 18 | + } else if (arg === null) { | |
| 19 | + return null; | |
| 20 | + } else { | |
| 21 | + return ''; | |
| 22 | + } | |
| 23 | + }; | |
| 24 | + var dxList = []; | |
| 25 | + // Want empty value to be at index 0 to match MySQL Enum values and | |
| 26 | + // MySQL non-strict behavior. | |
| 27 | + dxList.push(''); | |
| 28 | + for (var arg in arguments) { | |
| 29 | + arg = String(arguments[arg]); | |
| 30 | + Object.defineProperty(Enum, arg.toUpperCase(), { | |
| 31 | + configurable: false, | |
| 32 | + enumerable: true, | |
| 33 | + value: arg, | |
| 34 | + writable: false, | |
| 35 | + }); | |
| 36 | + dxList.push(arg); | |
| 37 | + } | |
| 38 | + Object.defineProperty(Enum, '_values', { | |
| 39 | + configurable: false, | |
| 40 | + enumerable: false, | |
| 41 | + value: dxList, | |
| 42 | + writable: false, | |
| 43 | + }); | |
| 44 | + Object.defineProperty(Enum, '_string', { | |
| 45 | + configurable: false, | |
| 46 | + enumerable: false, | |
| 47 | + value: stringified(Enum), | |
| 48 | + writable: false, | |
| 49 | + }); | |
| 50 | + Object.freeze(Enum); | |
| 51 | + return Enum; | |
| 52 | + } else { | |
| 53 | + throw g.f('No arguments - could not create {{Enum}}.'); | |
| 54 | + } | |
| 55 | +}; | |
| 56 | + | |
| 57 | +function stringified(anEnum) { | |
| 58 | + var s = []; | |
| 59 | + for (var i in anEnum._values) { | |
| 60 | + if (anEnum._values[i] != '') { | |
| 61 | + s.push("'" + anEnum._values[i] + "'"); | |
| 62 | + } | |
| 63 | + } | |
| 64 | + return s.join(','); | |
| 65 | +} | |
| 66 | + | |
| 67 | +exports.EnumFactory = EnumFactory; | ... | ... |
| 1 | +++ a/lib/migration.js | |
| ... | ... | @@ -0,0 +1,900 @@ |
| 1 | +// Copyright IBM Corp. 2015,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +var g = require('strong-globalize')(); | |
| 8 | +var async = require('async'); | |
| 9 | +module.exports = mixinMigration; | |
| 10 | + | |
| 11 | +/*! | |
| 12 | + * @param {MySQL} MySQL connector class | |
| 13 | + * @param {Object} mysql mysql driver | |
| 14 | + */ | |
| 15 | +function mixinMigration(MySQL, mysql) { | |
| 16 | + MySQL.prototype.showFields = function(model, cb) { | |
| 17 | + var table = this.tableEscaped(model); | |
| 18 | + var sql = 'SHOW FIELDS FROM ' + table; | |
| 19 | + this.execute(sql, function(err, fields) { | |
| 20 | + if (err) { | |
| 21 | + return cb(err); | |
| 22 | + } else { | |
| 23 | + cb(err, fields); | |
| 24 | + } | |
| 25 | + }); | |
| 26 | + }; | |
| 27 | + | |
| 28 | + MySQL.prototype.showIndexes = function(model, cb) { | |
| 29 | + var table = this.tableEscaped(model); | |
| 30 | + var sql = 'SHOW INDEXES FROM ' + table; | |
| 31 | + this.execute(sql, function(err, indexes) { | |
| 32 | + if (err) { | |
| 33 | + return cb(err); | |
| 34 | + } else { | |
| 35 | + cb(err, indexes); | |
| 36 | + } | |
| 37 | + }); | |
| 38 | + }; | |
| 39 | + | |
| 40 | + /** | |
| 41 | + * Perform autoupdate for the given models | |
| 42 | + * @param {String[]} [models] A model name or an array of model names. | |
| 43 | + * If not present, apply to all models | |
| 44 | + * @param {Function} [cb] The callback function | |
| 45 | + */ | |
| 46 | + MySQL.prototype.autoupdate = function(models, cb) { | |
| 47 | + var self = this; | |
| 48 | + var foreignKeyStatements = []; | |
| 49 | + | |
| 50 | + if ((!cb) && ('function' === typeof models)) { | |
| 51 | + cb = models; | |
| 52 | + models = undefined; | |
| 53 | + } | |
| 54 | + // First argument is a model name | |
| 55 | + if ('string' === typeof models) { | |
| 56 | + models = [models]; | |
| 57 | + } | |
| 58 | + | |
| 59 | + models = models || Object.keys(this._models); | |
| 60 | + | |
| 61 | + async.each(models, function(model, done) { | |
| 62 | + if (!(model in self._models)) { | |
| 63 | + return process.nextTick(function() { | |
| 64 | + done(new Error(g.f('Model not found: %s', model))); | |
| 65 | + }); | |
| 66 | + } | |
| 67 | + | |
| 68 | + self.getTableStatus(model, function(err, fields, indexes) { | |
| 69 | + self.discoverForeignKeys(self.table(model), {}, function(err, foreignKeys) { | |
| 70 | + if (err) console.log('Failed to discover "' + table + '" foreign keys', err); | |
| 71 | + | |
| 72 | + if (!err && fields && fields.length) { | |
| 73 | + //if we already have a definition, update this table | |
| 74 | + self.alterTable(model, fields, indexes, foreignKeys, function(err, result) { | |
| 75 | + if (!err) { | |
| 76 | + self.addForeignKeys(model, function(err, result) { | |
| 77 | + done(err); | |
| 78 | + }); | |
| 79 | + } else { | |
| 80 | + done(err); | |
| 81 | + } | |
| 82 | + }); | |
| 83 | + } else { | |
| 84 | + //if there is not yet a definition, create this table | |
| 85 | + self.createTable(model, function(err) { | |
| 86 | + if (!err) { | |
| 87 | + self.addForeignKeys(model, function(err, result) { | |
| 88 | + done(err); | |
| 89 | + }); | |
| 90 | + } else { | |
| 91 | + done(err); | |
| 92 | + } | |
| 93 | + }); | |
| 94 | + } | |
| 95 | + }); | |
| 96 | + }); | |
| 97 | + }, function(err) { | |
| 98 | + return cb(err); | |
| 99 | + }); | |
| 100 | + }; | |
| 101 | + | |
| 102 | + /*! | |
| 103 | + * Create a DB table for the given model | |
| 104 | + * @param {string} model Model name | |
| 105 | + * @param cb | |
| 106 | + */ | |
| 107 | + MySQL.prototype.createTable = function(model, cb) { | |
| 108 | + var metadata = this.getModelDefinition(model).settings[this.name]; | |
| 109 | + var engine = metadata && metadata.engine; | |
| 110 | + var sql = 'CREATE TABLE ' + this.tableEscaped(model) + | |
| 111 | + ' (\n ' + this.buildColumnDefinitions(model) + '\n)'; | |
| 112 | + if (engine) { | |
| 113 | + sql += 'ENGINE=' + engine + '\n'; | |
| 114 | + } | |
| 115 | + this.execute(sql, cb); | |
| 116 | + }; | |
| 117 | + | |
| 118 | + /** | |
| 119 | + * Check if the models exist | |
| 120 | + * @param {String[]} [models] A model name or an array of model names. If not | |
| 121 | + * present, apply to all models | |
| 122 | + * @param {Function} [cb] The callback function | |
| 123 | + */ | |
| 124 | + MySQL.prototype.isActual = function(models, cb) { | |
| 125 | + var self = this; | |
| 126 | + var ok = false; | |
| 127 | + | |
| 128 | + if ((!cb) && ('function' === typeof models)) { | |
| 129 | + cb = models; | |
| 130 | + models = undefined; | |
| 131 | + } | |
| 132 | + // First argument is a model name | |
| 133 | + if ('string' === typeof models) { | |
| 134 | + models = [models]; | |
| 135 | + } | |
| 136 | + | |
| 137 | + models = models || Object.keys(this._models); | |
| 138 | + | |
| 139 | + async.each(models, function(model, done) { | |
| 140 | + self.getTableStatus(model, function(err, fields, indexes) { | |
| 141 | + self.discoverForeignKeys(self.table(model), {}, function(err, foreignKeys) { | |
| 142 | + if (err) console.log('Failed to discover "' + table + '" foreign keys', err); | |
| 143 | + | |
| 144 | + self.alterTable(model, fields, indexes, foreignKeys, function(err, needAlter) { | |
| 145 | + if (err) { | |
| 146 | + return done(err); | |
| 147 | + } else { | |
| 148 | + ok = ok || needAlter; | |
| 149 | + done(err); | |
| 150 | + } | |
| 151 | + }, true); | |
| 152 | + }); | |
| 153 | + }); | |
| 154 | + }, function(err) { | |
| 155 | + cb(err, !ok); | |
| 156 | + }); | |
| 157 | + }; | |
| 158 | + | |
| 159 | + MySQL.prototype.getColumnsToAdd = function(model, actualFields) { | |
| 160 | + var self = this; | |
| 161 | + var m = this.getModelDefinition(model); | |
| 162 | + var propNames = Object.keys(m.properties).filter(function(name) { | |
| 163 | + return !!m.properties[name]; | |
| 164 | + }); | |
| 165 | + var sql = []; | |
| 166 | + | |
| 167 | + propNames.forEach(function(propName) { | |
| 168 | + if (m.properties[propName] && self.id(model, propName)) return; | |
| 169 | + var found; | |
| 170 | + var colName = expectedColNameForModel(propName, m); | |
| 171 | + if (actualFields) { | |
| 172 | + actualFields.forEach(function(f) { | |
| 173 | + if (f.Field === colName) { | |
| 174 | + found = f; | |
| 175 | + } | |
| 176 | + }); | |
| 177 | + } | |
| 178 | + if (found) { | |
| 179 | + actualize(colName, found); | |
| 180 | + } else { | |
| 181 | + sql.push('ADD COLUMN ' + self.client.escapeId(colName) + ' ' + | |
| 182 | + self.buildColumnDefinition(model, propName)); | |
| 183 | + } | |
| 184 | + }); | |
| 185 | + | |
| 186 | + function actualize(propName, oldSettings) { | |
| 187 | + var newSettings = m.properties[propName]; | |
| 188 | + if (newSettings && changed(newSettings, oldSettings)) { | |
| 189 | + var pName = self.client.escapeId(propName); | |
| 190 | + sql.push('CHANGE COLUMN ' + pName + ' ' + pName + ' ' + | |
| 191 | + self.buildColumnDefinition(model, propName)); | |
| 192 | + } | |
| 193 | + } | |
| 194 | + | |
| 195 | + function changed(newSettings, oldSettings) { | |
| 196 | + if (oldSettings.Null === 'YES') { | |
| 197 | + // Used to allow null and does not now. | |
| 198 | + if (!self.isNullable(newSettings)) { | |
| 199 | + return true; | |
| 200 | + } | |
| 201 | + } | |
| 202 | + if (oldSettings.Null === 'NO') { | |
| 203 | + // Did not allow null and now does. | |
| 204 | + if (self.isNullable(newSettings)) { | |
| 205 | + return true; | |
| 206 | + } | |
| 207 | + } | |
| 208 | + | |
| 209 | + if (oldSettings.Type.toUpperCase() !== | |
| 210 | + self.buildColumnType(newSettings).toUpperCase()) { | |
| 211 | + return true; | |
| 212 | + } | |
| 213 | + return false; | |
| 214 | + } | |
| 215 | + return sql; | |
| 216 | + }; | |
| 217 | + | |
| 218 | + MySQL.prototype.getColumnsToDrop = function(model, actualFields) { | |
| 219 | + var self = this; | |
| 220 | + var fields = actualFields; | |
| 221 | + var sql = []; | |
| 222 | + var m = this.getModelDefinition(model); | |
| 223 | + var propNames = Object.keys(m.properties).filter(function(name) { | |
| 224 | + return !!m.properties[name]; | |
| 225 | + }); | |
| 226 | + // drop columns | |
| 227 | + if (fields) { | |
| 228 | + fields.forEach(function(f) { | |
| 229 | + var colNames = propNames.map(function expectedColName(propName) { | |
| 230 | + return expectedColNameForModel(propName, m); | |
| 231 | + }); | |
| 232 | + var index = colNames.indexOf(f.Field); | |
| 233 | + var propName = index >= 0 ? propNames[index] : f.Field; | |
| 234 | + var notFound = !~index; | |
| 235 | + if (m.properties[propName] && self.id(model, propName)) return; | |
| 236 | + if (notFound || !m.properties[propName]) { | |
| 237 | + sql.push('DROP COLUMN ' + self.client.escapeId(f.Field)); | |
| 238 | + } | |
| 239 | + }); | |
| 240 | + } | |
| 241 | + return sql; | |
| 242 | + }; | |
| 243 | + | |
| 244 | + MySQL.prototype.addIndexes = function(model, actualIndexes) { | |
| 245 | + var self = this; | |
| 246 | + var m = this.getModelDefinition(model); | |
| 247 | + var propNames = Object.keys(m.properties).filter(function(name) { | |
| 248 | + return !!m.properties[name]; | |
| 249 | + }); | |
| 250 | + var indexNames = m.settings.indexes && Object.keys(m.settings.indexes).filter(function(name) { | |
| 251 | + return !!m.settings.indexes[name]; | |
| 252 | + }) || []; | |
| 253 | + var sql = []; | |
| 254 | + var ai = {}; | |
| 255 | + | |
| 256 | + if (actualIndexes) { | |
| 257 | + actualIndexes.forEach(function(i) { | |
| 258 | + var name = i.Key_name; | |
| 259 | + if (!ai[name]) { | |
| 260 | + ai[name] = { | |
| 261 | + info: i, | |
| 262 | + columns: [], | |
| 263 | + }; | |
| 264 | + } | |
| 265 | + ai[name].columns[i.Seq_in_index - 1] = i.Column_name; | |
| 266 | + }); | |
| 267 | + } | |
| 268 | + var aiNames = Object.keys(ai); | |
| 269 | + | |
| 270 | + // remove indexes | |
| 271 | + aiNames.forEach(function(indexName) { | |
| 272 | + if (indexName === 'PRIMARY' || | |
| 273 | + (m.properties[indexName] && self.id(model, indexName))) return; | |
| 274 | + | |
| 275 | + if (indexNames.indexOf(indexName) === -1 && !m.properties[indexName] || | |
| 276 | + m.properties[indexName] && !m.properties[indexName].index) { | |
| 277 | + sql.push('DROP INDEX ' + self.client.escapeId(indexName)); | |
| 278 | + } else { | |
| 279 | + // first: check single (only type and kind) | |
| 280 | + if (m.properties[indexName] && !m.properties[indexName].index) { | |
| 281 | + // TODO | |
| 282 | + return; | |
| 283 | + } | |
| 284 | + // second: check multiple indexes | |
| 285 | + var orderMatched = true; | |
| 286 | + if (indexNames.indexOf(indexName) !== -1) { | |
| 287 | + //check if indexes are configured as "columns" | |
| 288 | + if (m.settings.indexes[indexName].columns) { | |
| 289 | + m.settings.indexes[indexName].columns.split(/,\s*/).forEach( | |
| 290 | + function(columnName, i) { | |
| 291 | + if (ai[indexName].columns[i] !== columnName) orderMatched = false; | |
| 292 | + }); | |
| 293 | + } else if (m.settings.indexes[indexName].keys) { | |
| 294 | + //if indexes are configured as "keys" | |
| 295 | + var index = 0; | |
| 296 | + for (var key in m.settings.indexes[indexName].keys) { | |
| 297 | + var sortOrder = m.settings.indexes[indexName].keys[key]; | |
| 298 | + if (ai[indexName].columns[index] !== key) { | |
| 299 | + orderMatched = false; | |
| 300 | + break; | |
| 301 | + } | |
| 302 | + index++; | |
| 303 | + } | |
| 304 | + //if number of columns differ between new and old index | |
| 305 | + if (index !== ai[indexName].columns.length) { | |
| 306 | + orderMatched = false; | |
| 307 | + } | |
| 308 | + } | |
| 309 | + } | |
| 310 | + if (!orderMatched) { | |
| 311 | + sql.push('DROP INDEX ' + self.client.escapeId(indexName)); | |
| 312 | + delete ai[indexName]; | |
| 313 | + } | |
| 314 | + } | |
| 315 | + }); | |
| 316 | + | |
| 317 | + // add single-column indexes | |
| 318 | + propNames.forEach(function(propName) { | |
| 319 | + var i = m.properties[propName].index; | |
| 320 | + if (!i) { | |
| 321 | + return; | |
| 322 | + } | |
| 323 | + var found = ai[propName] && ai[propName].info; | |
| 324 | + if (!found) { | |
| 325 | + var colName = expectedColNameForModel(propName, m); | |
| 326 | + var pName = self.client.escapeId(colName); | |
| 327 | + var type = ''; | |
| 328 | + var kind = ''; | |
| 329 | + if (i.type) { | |
| 330 | + type = 'USING ' + i.type; | |
| 331 | + } | |
| 332 | + if (kind && type) { | |
| 333 | + sql.push('ADD ' + kind + ' INDEX ' + pName + | |
| 334 | + ' (' + pName + ') ' + type); | |
| 335 | + } else { | |
| 336 | + if (typeof i === 'object' && i.unique && i.unique === true) { | |
| 337 | + kind = 'UNIQUE'; | |
| 338 | + } | |
| 339 | + sql.push('ADD ' + kind + ' INDEX ' + pName + ' ' + type + | |
| 340 | + ' (' + pName + ') '); | |
| 341 | + } | |
| 342 | + } | |
| 343 | + }); | |
| 344 | + | |
| 345 | + // add multi-column indexes | |
| 346 | + indexNames.forEach(function(indexName) { | |
| 347 | + var i = m.settings.indexes[indexName]; | |
| 348 | + var found = ai[indexName] && ai[indexName].info; | |
| 349 | + if (!found) { | |
| 350 | + var iName = self.client.escapeId(indexName); | |
| 351 | + var type = ''; | |
| 352 | + var kind = ''; | |
| 353 | + if (i.type) { | |
| 354 | + type = 'USING ' + i.type; | |
| 355 | + } | |
| 356 | + if (i.kind) { | |
| 357 | + kind = i.kind; | |
| 358 | + } else if (i.options && i.options.unique && i.options.unique == true) { | |
| 359 | + //if index unique indicator is configured | |
| 360 | + kind = 'UNIQUE'; | |
| 361 | + } | |
| 362 | + | |
| 363 | + var indexedColumns = []; | |
| 364 | + var columns = ''; | |
| 365 | + //if indexes are configured as "keys" | |
| 366 | + if (i.keys) { | |
| 367 | + for (var key in i.keys) { | |
| 368 | + if (i.keys[key] !== -1) { | |
| 369 | + indexedColumns.push(key); | |
| 370 | + } else { | |
| 371 | + indexedColumns.push(key + ' DESC '); | |
| 372 | + } | |
| 373 | + } | |
| 374 | + } | |
| 375 | + if (indexedColumns.length > 0) { | |
| 376 | + columns = indexedColumns.join(','); | |
| 377 | + } else if (i.columns) { | |
| 378 | + //if indexes are configured as "columns" | |
| 379 | + columns = i.columns; | |
| 380 | + } | |
| 381 | + if (kind && type) { | |
| 382 | + sql.push('ADD ' + kind + ' INDEX ' + iName + | |
| 383 | + ' (' + columns + ') ' + type); | |
| 384 | + } else { | |
| 385 | + sql.push('ADD ' + kind + ' INDEX ' + type + ' ' + iName + | |
| 386 | + ' (' + columns + ')'); | |
| 387 | + } | |
| 388 | + } | |
| 389 | + }); | |
| 390 | + return sql; | |
| 391 | + }; | |
| 392 | + | |
| 393 | + MySQL.prototype.getForeignKeySQL = function(model, actualFks) { | |
| 394 | + var self = this; | |
| 395 | + var m = this.getModelDefinition(model); | |
| 396 | + var addFksSql = []; | |
| 397 | + | |
| 398 | + if (actualFks) { | |
| 399 | + var keys = Object.keys(actualFks); | |
| 400 | + for (var i = 0; i < keys.length; i++) { | |
| 401 | + var constraint = self.buildForeignKeyDefinition(model, keys[i]); | |
| 402 | + | |
| 403 | + if (constraint) { | |
| 404 | + addFksSql.push('ADD ' + constraint); | |
| 405 | + } | |
| 406 | + } | |
| 407 | + } | |
| 408 | + return addFksSql; | |
| 409 | + }; | |
| 410 | + | |
| 411 | + MySQL.prototype.addForeignKeys = function(model, fkSQL, cb) { | |
| 412 | + var self = this; | |
| 413 | + var m = this.getModelDefinition(model); | |
| 414 | + | |
| 415 | + if ((!cb) && ('function' === typeof fkSQL)) { | |
| 416 | + cb = fkSQL; | |
| 417 | + fkSQL = undefined; | |
| 418 | + } | |
| 419 | + | |
| 420 | + if (!fkSQL) { | |
| 421 | + var newFks = m.settings.foreignKeys; | |
| 422 | + if (newFks) | |
| 423 | + fkSQL = self.getForeignKeySQL(model, newFks); | |
| 424 | + } | |
| 425 | + if (fkSQL && fkSQL.length) { | |
| 426 | + self.applySqlChanges(model, fkSQL, function(err, result) { | |
| 427 | + if (err) cb(err); | |
| 428 | + else | |
| 429 | + cb(null, result); | |
| 430 | + }); | |
| 431 | + } else cb(null, {}); | |
| 432 | + }; | |
| 433 | + | |
| 434 | + MySQL.prototype.dropForeignKeys = function(model, actualFks) { | |
| 435 | + var self = this; | |
| 436 | + var m = this.getModelDefinition(model); | |
| 437 | + | |
| 438 | + var fks = actualFks; | |
| 439 | + var sql = []; | |
| 440 | + var correctFks = m.settings.foreignKeys || {}; | |
| 441 | + | |
| 442 | + //drop foreign keys for removed fields | |
| 443 | + if (fks && fks.length) { | |
| 444 | + var removedFks = []; | |
| 445 | + fks.forEach(function(fk) { | |
| 446 | + var needsToDrop = false; | |
| 447 | + var newFk = correctFks[fk.fkName]; | |
| 448 | + if (newFk) { | |
| 449 | + var fkCol = expectedColNameForModel(newFk.foreignKey, m); | |
| 450 | + var fkEntity = self.getModelDefinition(newFk.entity); | |
| 451 | + var fkRefKey = expectedColNameForModel(newFk.entityKey, fkEntity); | |
| 452 | + var fkRefTable = newFk.entity.name; //TODO check for mysql name | |
| 453 | + needsToDrop = fkCol != fk.fkColumnName || | |
| 454 | + fkRefKey != fk.pkColumnName || | |
| 455 | + fkRefTable != fk.pkTableName; | |
| 456 | + } else { | |
| 457 | + needsToDrop = true; | |
| 458 | + } | |
| 459 | + | |
| 460 | + if (needsToDrop) { | |
| 461 | + sql.push('DROP FOREIGN KEY ' + fk.fkName); | |
| 462 | + removedFks.push(fk); //keep track that we removed these | |
| 463 | + } | |
| 464 | + }); | |
| 465 | + | |
| 466 | + //update out list of existing keys by removing dropped keys | |
| 467 | + fks = actualFks.filter(function(k) { | |
| 468 | + return removedFks.indexOf(k) == -1; | |
| 469 | + }); | |
| 470 | + } | |
| 471 | + return sql; | |
| 472 | + }; | |
| 473 | + | |
| 474 | + MySQL.prototype.getAlterStatement = function(model, statements) { | |
| 475 | + return statements.length ? | |
| 476 | + 'ALTER TABLE ' + this.tableEscaped(model) + ' ' + statements.join(',\n') : | |
| 477 | + ''; | |
| 478 | + }; | |
| 479 | + | |
| 480 | + MySQL.prototype.alterTable = function(model, actualFields, actualIndexes, actualFks, done, checkOnly) { | |
| 481 | + //if this is using an old signature, then grab the correct callback and check boolean | |
| 482 | + if ('function' == typeof actualFks && typeof done !== 'function') { | |
| 483 | + checkOnly = done || false; | |
| 484 | + done = actualFks; | |
| 485 | + } | |
| 486 | + var self = this; | |
| 487 | + | |
| 488 | + var statements = []; | |
| 489 | + | |
| 490 | + async.series([ | |
| 491 | + function(cb) { | |
| 492 | + statements = self.getAddModifyColumns(model, actualFields); | |
| 493 | + cb(); | |
| 494 | + }, | |
| 495 | + function(cb) { | |
| 496 | + statements = statements.concat(self.getDropColumns(model, actualFields)); | |
| 497 | + cb(); | |
| 498 | + }, | |
| 499 | + function(cb) { | |
| 500 | + statements = statements.concat(self.addIndexes(model, actualIndexes)); | |
| 501 | + cb(); | |
| 502 | + }, | |
| 503 | + function(cb) { | |
| 504 | + statements = statements.concat(self.dropForeignKeys(model, actualFks)); | |
| 505 | + cb(); | |
| 506 | + }, | |
| 507 | + ], function(err, result) { | |
| 508 | + if (err) done(err); | |
| 509 | + | |
| 510 | + //determine if there are column, index, or foreign keys changes (all require update) | |
| 511 | + if (statements.length) { | |
| 512 | + //get the required alter statements | |
| 513 | + var alterStmt = self.getAlterStatement(model, statements); | |
| 514 | + var stmtList = [alterStmt]; | |
| 515 | + | |
| 516 | + //set up an object to pass back all changes, changes that have been run, | |
| 517 | + //and foreign key statements that haven't been run | |
| 518 | + var retValues = { | |
| 519 | + statements: stmtList, | |
| 520 | + query: stmtList.join(';'), | |
| 521 | + }; | |
| 522 | + | |
| 523 | + //if we're running in read only mode OR if the only changes are foreign keys additions, | |
| 524 | + //then just return the object directly | |
| 525 | + if (checkOnly) { | |
| 526 | + done(null, true, retValues); | |
| 527 | + } else { | |
| 528 | + //if there are changes in the alter statement, then execute them and return the object | |
| 529 | + self.execute(alterStmt, function(err) { | |
| 530 | + done(err, true, retValues); | |
| 531 | + }); | |
| 532 | + } | |
| 533 | + } else { | |
| 534 | + done(); | |
| 535 | + } | |
| 536 | + }); | |
| 537 | + }; | |
| 538 | + | |
| 539 | + MySQL.prototype.buildForeignKeyDefinition = function(model, keyName) { | |
| 540 | + var definition = this.getModelDefinition(model); | |
| 541 | + | |
| 542 | + var fk = definition.settings.foreignKeys[keyName]; | |
| 543 | + if (fk) { | |
| 544 | + //get the definition of the referenced object | |
| 545 | + var fkEntityName = (typeof fk.entity === 'object') ? fk.entity.name : fk.entity; | |
| 546 | + | |
| 547 | + //verify that the other model in the same DB | |
| 548 | + if (this._models[fkEntityName]) { | |
| 549 | + return ' CONSTRAINT ' + this.client.escapeId(fk.name) + | |
| 550 | + ' FOREIGN KEY (' + fk.foreignKey + ')' + | |
| 551 | + ' REFERENCES ' + this.tableEscaped(fkEntityName) + | |
| 552 | + '(' + this.client.escapeId(fk.entityKey) + ')'; | |
| 553 | + } | |
| 554 | + } | |
| 555 | + return ''; | |
| 556 | + }; | |
| 557 | + | |
| 558 | + MySQL.prototype.buildColumnDefinitions = | |
| 559 | + MySQL.prototype.propertiesSQL = function(model) { | |
| 560 | + var self = this; | |
| 561 | + | |
| 562 | + var pks = this.idNames(model).map(function(i) { | |
| 563 | + return self.columnEscaped(model, i); | |
| 564 | + }); | |
| 565 | + | |
| 566 | + var definition = this.getModelDefinition(model); | |
| 567 | + var sql = []; | |
| 568 | + if (pks.length === 1) { | |
| 569 | + var idName = this.idName(model); | |
| 570 | + var idProp = this.getModelDefinition(model).properties[idName]; | |
| 571 | + if (idProp.generated) { | |
| 572 | + sql.push(self.columnEscaped(model, idName) + | |
| 573 | + ' INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY'); | |
| 574 | + } else { | |
| 575 | + idProp.nullable = false; | |
| 576 | + sql.push(self.columnEscaped(model, idName) + ' ' + | |
| 577 | + self.buildColumnDefinition(model, idName) + ' PRIMARY KEY'); | |
| 578 | + } | |
| 579 | + } | |
| 580 | + Object.keys(definition.properties).forEach(function(prop) { | |
| 581 | + if (self.id(model, prop) && pks.length === 1) { | |
| 582 | + return; | |
| 583 | + } | |
| 584 | + var colName = self.columnEscaped(model, prop); | |
| 585 | + sql.push(colName + ' ' + self.buildColumnDefinition(model, prop)); | |
| 586 | + }); | |
| 587 | + if (pks.length > 1) { | |
| 588 | + sql.push('PRIMARY KEY(' + pks.join(',') + ')'); | |
| 589 | + } | |
| 590 | + | |
| 591 | + var indexes = self.buildIndexes(model); | |
| 592 | + indexes.forEach(function(i) { | |
| 593 | + sql.push(i); | |
| 594 | + }); | |
| 595 | + | |
| 596 | + return sql.join(',\n '); | |
| 597 | + }; | |
| 598 | + | |
| 599 | + MySQL.prototype.buildIndex = function(model, property) { | |
| 600 | + var prop = this.getModelDefinition(model).properties[property]; | |
| 601 | + var i = prop && prop.index; | |
| 602 | + if (!i) { | |
| 603 | + return ''; | |
| 604 | + } | |
| 605 | + var type = ''; | |
| 606 | + var kind = ''; | |
| 607 | + if (i.type) { | |
| 608 | + type = 'USING ' + i.type; | |
| 609 | + } | |
| 610 | + if (i.kind) { | |
| 611 | + kind = i.kind; | |
| 612 | + } | |
| 613 | + var columnName = this.columnEscaped(model, property); | |
| 614 | + if (kind && type) { | |
| 615 | + return (kind + ' INDEX ' + columnName + ' (' + columnName + ') ' + type); | |
| 616 | + } else { | |
| 617 | + if (typeof i === 'object' && i.unique && i.unique === true) { | |
| 618 | + kind = 'UNIQUE'; | |
| 619 | + } | |
| 620 | + return (kind + ' INDEX ' + columnName + ' ' + type + ' (' + columnName + ') '); | |
| 621 | + } | |
| 622 | + }; | |
| 623 | + | |
| 624 | + MySQL.prototype.buildIndexes = function(model) { | |
| 625 | + var self = this; | |
| 626 | + var indexClauses = []; | |
| 627 | + var definition = this.getModelDefinition(model); | |
| 628 | + var indexes = definition.settings.indexes || {}; | |
| 629 | + // Build model level indexes | |
| 630 | + for (var index in indexes) { | |
| 631 | + var i = indexes[index]; | |
| 632 | + var type = ''; | |
| 633 | + var kind = ''; | |
| 634 | + if (i.type) { | |
| 635 | + type = 'USING ' + i.type; | |
| 636 | + } | |
| 637 | + if (i.kind) { | |
| 638 | + //if index uniqueness is configured as "kind" | |
| 639 | + kind = i.kind; | |
| 640 | + } else if (i.options && i.options.unique && i.options.unique == true) { | |
| 641 | + //if index unique indicator is configured | |
| 642 | + kind = 'UNIQUE'; | |
| 643 | + } | |
| 644 | + var indexedColumns = []; | |
| 645 | + var indexName = this.escapeName(index); | |
| 646 | + var columns = ''; | |
| 647 | + //if indexes are configured as "keys" | |
| 648 | + if (i.keys) { | |
| 649 | + //for each field in "keys" object | |
| 650 | + for (var key in i.keys) { | |
| 651 | + if (i.keys[key] !== -1) { | |
| 652 | + indexedColumns.push(key); | |
| 653 | + } else { | |
| 654 | + //mysql does not support index sorting Currently | |
| 655 | + //but mysql has added DESC keyword for future support | |
| 656 | + indexedColumns.push(key + ' DESC '); | |
| 657 | + } | |
| 658 | + } | |
| 659 | + } | |
| 660 | + if (indexedColumns.length) { | |
| 661 | + columns = indexedColumns.join(','); | |
| 662 | + } else if (i.columns) { | |
| 663 | + columns = i.columns; | |
| 664 | + } | |
| 665 | + if (columns.length) { | |
| 666 | + if (kind && type) { | |
| 667 | + indexClauses.push(kind + ' INDEX ' + | |
| 668 | + indexName + ' (' + columns + ') ' + type); | |
| 669 | + } else { | |
| 670 | + indexClauses.push(kind + ' INDEX ' + type + | |
| 671 | + ' ' + indexName + ' (' + columns + ')'); | |
| 672 | + } | |
| 673 | + } | |
| 674 | + } | |
| 675 | + // Define index for each of the properties | |
| 676 | + for (var p in definition.properties) { | |
| 677 | + var propIndex = self.buildIndex(model, p); | |
| 678 | + if (propIndex) { | |
| 679 | + indexClauses.push(propIndex); | |
| 680 | + } | |
| 681 | + } | |
| 682 | + return indexClauses; | |
| 683 | + }; | |
| 684 | + | |
| 685 | + MySQL.prototype.buildColumnDefinition = function(model, prop) { | |
| 686 | + var p = this.getModelDefinition(model).properties[prop]; | |
| 687 | + var line = this.columnDataType(model, prop) + ' ' + | |
| 688 | + (this.isNullable(p) ? 'NULL' : 'NOT NULL'); | |
| 689 | + return line; | |
| 690 | + }; | |
| 691 | + | |
| 692 | + MySQL.prototype.buildColumnType = function buildColumnType(propertyDefinition) { | |
| 693 | + var dt = ''; | |
| 694 | + var p = propertyDefinition; | |
| 695 | + switch (p.type.name) { | |
| 696 | + default: | |
| 697 | + case 'JSON': | |
| 698 | + case 'Object': | |
| 699 | + case 'Any': | |
| 700 | + case 'Text': | |
| 701 | + dt = columnType(p, 'TEXT'); | |
| 702 | + dt = stringOptionsByType(p, dt); | |
| 703 | + break; | |
| 704 | + case 'String': | |
| 705 | + dt = columnType(p, 'VARCHAR'); | |
| 706 | + dt = stringOptionsByType(p, dt); | |
| 707 | + break; | |
| 708 | + case 'Number': | |
| 709 | + dt = columnType(p, 'INT'); | |
| 710 | + dt = numericOptionsByType(p, dt); | |
| 711 | + break; | |
| 712 | + case 'Date': | |
| 713 | + dt = columnType(p, 'DATETIME'); // Currently doesn't need options. | |
| 714 | + break; | |
| 715 | + case 'Boolean': | |
| 716 | + dt = 'TINYINT(1)'; | |
| 717 | + break; | |
| 718 | + case 'Point': | |
| 719 | + case 'GeoPoint': | |
| 720 | + dt = 'POINT'; | |
| 721 | + break; | |
| 722 | + case 'Enum': | |
| 723 | + dt = 'ENUM(' + p.type._string + ')'; | |
| 724 | + dt = stringOptions(p, dt); // Enum columns can have charset/collation. | |
| 725 | + break; | |
| 726 | + } | |
| 727 | + return dt; | |
| 728 | + }; | |
| 729 | + | |
| 730 | + function columnType(p, defaultType) { | |
| 731 | + var dt = defaultType; | |
| 732 | + if (p.dataType) { | |
| 733 | + dt = String(p.dataType); | |
| 734 | + } | |
| 735 | + return dt; | |
| 736 | + } | |
| 737 | + | |
| 738 | + function stringOptionsByType(p, columnType) { | |
| 739 | + switch (columnType.toLowerCase()) { | |
| 740 | + default: | |
| 741 | + case 'varchar': | |
| 742 | + // The maximum length for an ID column is 1000 bytes | |
| 743 | + // The maximum row size is 64K | |
| 744 | + var len = p.length || p.limit || | |
| 745 | + ((p.type !== String) ? 4096 : p.id || p.index ? 255 : 512); | |
| 746 | + columnType += '(' + len + ')'; | |
| 747 | + break; | |
| 748 | + case 'char': | |
| 749 | + len = p.length || p.limit || 255; | |
| 750 | + columnType += '(' + len + ')'; | |
| 751 | + break; | |
| 752 | + | |
| 753 | + case 'text': | |
| 754 | + case 'tinytext': | |
| 755 | + case 'mediumtext': | |
| 756 | + case 'longtext': | |
| 757 | + | |
| 758 | + break; | |
| 759 | + } | |
| 760 | + columnType = stringOptions(p, columnType); | |
| 761 | + return columnType; | |
| 762 | + } | |
| 763 | + | |
| 764 | + function stringOptions(p, columnType) { | |
| 765 | + if (p.charset) { | |
| 766 | + columnType += ' CHARACTER SET ' + p.charset; | |
| 767 | + } | |
| 768 | + if (p.collation) { | |
| 769 | + columnType += ' COLLATE ' + p.collation; | |
| 770 | + } | |
| 771 | + return columnType; | |
| 772 | + } | |
| 773 | + | |
| 774 | + function numericOptionsByType(p, columnType) { | |
| 775 | + switch (columnType.toLowerCase()) { | |
| 776 | + default: | |
| 777 | + case 'tinyint': | |
| 778 | + case 'smallint': | |
| 779 | + case 'mediumint': | |
| 780 | + case 'int': | |
| 781 | + case 'integer': | |
| 782 | + case 'bigint': | |
| 783 | + columnType = integerOptions(p, columnType); | |
| 784 | + break; | |
| 785 | + | |
| 786 | + case 'decimal': | |
| 787 | + case 'numeric': | |
| 788 | + columnType = fixedPointOptions(p, columnType); | |
| 789 | + break; | |
| 790 | + | |
| 791 | + case 'float': | |
| 792 | + case 'double': | |
| 793 | + columnType = floatingPointOptions(p, columnType); | |
| 794 | + break; | |
| 795 | + } | |
| 796 | + columnType = unsigned(p, columnType); | |
| 797 | + return columnType; | |
| 798 | + } | |
| 799 | + | |
| 800 | + function floatingPointOptions(p, columnType) { | |
| 801 | + var precision = 16; | |
| 802 | + var scale = 8; | |
| 803 | + if (p.precision) { | |
| 804 | + precision = Number(p.precision); | |
| 805 | + } | |
| 806 | + if (p.scale) { | |
| 807 | + scale = Number(p.scale); | |
| 808 | + } | |
| 809 | + if (p.precision && p.scale) { | |
| 810 | + columnType += '(' + precision + ',' + scale + ')'; | |
| 811 | + } else if (p.precision) { | |
| 812 | + columnType += '(' + precision + ')'; | |
| 813 | + } | |
| 814 | + return columnType; | |
| 815 | + } | |
| 816 | + | |
| 817 | + /* @TODO: Change fixed point to use an arbitrary precision arithmetic library. */ | |
| 818 | + /* Currently fixed point will lose precision because it's turned to non-fixed in */ | |
| 819 | + /* JS. Also, defaulting column to (9,2) and not allowing non-specified 'DECIMAL' */ | |
| 820 | + /* declaration which would default to DECIMAL(10,0). Instead defaulting to (9,2). */ | |
| 821 | + function fixedPointOptions(p, columnType) { | |
| 822 | + var precision = 9; | |
| 823 | + var scale = 2; | |
| 824 | + if (p.precision) { | |
| 825 | + precision = Number(p.precision); | |
| 826 | + } | |
| 827 | + if (p.scale) { | |
| 828 | + scale = Number(p.scale); | |
| 829 | + } | |
| 830 | + columnType += '(' + precision + ',' + scale + ')'; | |
| 831 | + return columnType; | |
| 832 | + } | |
| 833 | + | |
| 834 | + function integerOptions(p, columnType) { | |
| 835 | + var tmp = 0; | |
| 836 | + if (p.display || p.limit) { | |
| 837 | + tmp = Number(p.display || p.limit); | |
| 838 | + } | |
| 839 | + if (tmp > 0) { | |
| 840 | + columnType += '(' + tmp + ')'; | |
| 841 | + } else if (p.unsigned) { | |
| 842 | + switch (columnType.toLowerCase()) { | |
| 843 | + default: | |
| 844 | + case 'int': | |
| 845 | + columnType += '(10)'; | |
| 846 | + break; | |
| 847 | + case 'mediumint': | |
| 848 | + columnType += '(8)'; | |
| 849 | + break; | |
| 850 | + case 'smallint': | |
| 851 | + columnType += '(5)'; | |
| 852 | + break; | |
| 853 | + case 'tinyint': | |
| 854 | + columnType += '(3)'; | |
| 855 | + break; | |
| 856 | + case 'bigint': | |
| 857 | + columnType += '(20)'; | |
| 858 | + break; | |
| 859 | + } | |
| 860 | + } else { | |
| 861 | + switch (columnType.toLowerCase()) { | |
| 862 | + default: | |
| 863 | + case 'int': | |
| 864 | + columnType += '(11)'; | |
| 865 | + break; | |
| 866 | + case 'mediumint': | |
| 867 | + columnType += '(9)'; | |
| 868 | + break; | |
| 869 | + case 'smallint': | |
| 870 | + columnType += '(6)'; | |
| 871 | + break; | |
| 872 | + case 'tinyint': | |
| 873 | + columnType += '(4)'; | |
| 874 | + break; | |
| 875 | + case 'bigint': | |
| 876 | + columnType += '(20)'; | |
| 877 | + break; | |
| 878 | + } | |
| 879 | + } | |
| 880 | + return columnType; | |
| 881 | + } | |
| 882 | + | |
| 883 | + function unsigned(p, columnType) { | |
| 884 | + if (p.unsigned) { | |
| 885 | + columnType += ' UNSIGNED'; | |
| 886 | + } | |
| 887 | + return columnType; | |
| 888 | + } | |
| 889 | + function expectedColNameForModel(propName, modelToCheck) { | |
| 890 | + var mysql = modelToCheck.properties[propName].mysql; | |
| 891 | + if (typeof mysql === 'undefined') { | |
| 892 | + return propName; | |
| 893 | + } | |
| 894 | + var colName = mysql.columnName; | |
| 895 | + if (typeof colName === 'undefined') { | |
| 896 | + return propName; | |
| 897 | + } | |
| 898 | + return colName; | |
| 899 | + } | |
| 900 | +} | ... | ... |
| 1 | +++ a/lib/mysql.js | |
| ... | ... | @@ -0,0 +1,563 @@ |
| 1 | +// Copyright IBM Corp. 2012,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +var g = require('strong-globalize')(); | |
| 8 | +var moment = require('moment'); | |
| 9 | + | |
| 10 | +/*! | |
| 11 | + * Module dependencies | |
| 12 | + */ | |
| 13 | +var mysql = require('mysql'); | |
| 14 | + | |
| 15 | +var SqlConnector = require('loopback-connector').SqlConnector; | |
| 16 | +var ParameterizedSQL = SqlConnector.ParameterizedSQL; | |
| 17 | +var EnumFactory = require('./enumFactory').EnumFactory; | |
| 18 | + | |
| 19 | +var debug = require('debug')('loopback:connector:mysql'); | |
| 20 | + | |
| 21 | +/** | |
| 22 | + * @module loopback-connector-mysql | |
| 23 | + * | |
| 24 | + * Initialize the MySQL connector against the given data source | |
| 25 | + * | |
| 26 | + * @param {DataSource} dataSource The loopback-datasource-juggler dataSource | |
| 27 | + * @param {Function} [callback] The callback function | |
| 28 | + */ | |
| 29 | +exports.initialize = function initializeDataSource(dataSource, callback) { | |
| 30 | + dataSource.driver = mysql; // Provide access to the native driver | |
| 31 | + dataSource.connector = new MySQL(dataSource.settings); | |
| 32 | + dataSource.connector.dataSource = dataSource; | |
| 33 | + | |
| 34 | + defineMySQLTypes(dataSource); | |
| 35 | + | |
| 36 | + dataSource.EnumFactory = EnumFactory; // factory for Enums. Note that currently Enums can not be registered. | |
| 37 | + | |
| 38 | + if (callback) { | |
| 39 | + if (dataSource.settings.lazyConnect) { | |
| 40 | + process.nextTick(function() { | |
| 41 | + callback(); | |
| 42 | + }); | |
| 43 | + } else { | |
| 44 | + dataSource.connector.connect(callback); | |
| 45 | + } | |
| 46 | + } | |
| 47 | +}; | |
| 48 | + | |
| 49 | +exports.MySQL = MySQL; | |
| 50 | + | |
| 51 | +function defineMySQLTypes(dataSource) { | |
| 52 | + var modelBuilder = dataSource.modelBuilder; | |
| 53 | + var defineType = modelBuilder.defineValueType ? | |
| 54 | + // loopback-datasource-juggler 2.x | |
| 55 | + modelBuilder.defineValueType.bind(modelBuilder) : | |
| 56 | + // loopback-datasource-juggler 1.x | |
| 57 | + modelBuilder.constructor.registerType.bind(modelBuilder.constructor); | |
| 58 | + | |
| 59 | + // The Point type is inherited from jugglingdb mysql adapter. | |
| 60 | + // LoopBack uses GeoPoint instead. | |
| 61 | + // The Point type can be removed at some point in the future. | |
| 62 | + defineType(function Point() { | |
| 63 | + }); | |
| 64 | +} | |
| 65 | + | |
| 66 | +/** | |
| 67 | + * @constructor | |
| 68 | + * Constructor for MySQL connector | |
| 69 | + * @param {Object} client The node-mysql client object | |
| 70 | + */ | |
| 71 | +function MySQL(settings) { | |
| 72 | + SqlConnector.call(this, 'mysql', settings); | |
| 73 | +} | |
| 74 | + | |
| 75 | +require('util').inherits(MySQL, SqlConnector); | |
| 76 | + | |
| 77 | +MySQL.prototype.connect = function(callback) { | |
| 78 | + var self = this; | |
| 79 | + var options = generateOptions(this.settings); | |
| 80 | + var s = self.settings || {}; | |
| 81 | + | |
| 82 | + // arsis 2017-03-21 | |
| 83 | + var tz = this.settings.timezone; | |
| 84 | + if (!isNaN(tz)) { | |
| 85 | + this.tzOffset = parseInt(tz.substring(0,3)); | |
| 86 | + } else { | |
| 87 | + var momentTz = moment().format('Z').replace(':', ''); | |
| 88 | + this.tzOffset = parseInt(momentTz.substring(0,3)); | |
| 89 | + } | |
| 90 | + // end arsis | |
| 91 | + | |
| 92 | + if (this.client) { | |
| 93 | + if (callback) { | |
| 94 | + process.nextTick(function() { | |
| 95 | + callback(null, self.client); | |
| 96 | + }); | |
| 97 | + } | |
| 98 | + } else { | |
| 99 | + this.client = mysql.createPool(options); | |
| 100 | + this.client.getConnection(function(err, connection) { | |
| 101 | + var conn = connection; | |
| 102 | + if (!err) { | |
| 103 | + if (self.debug) { | |
| 104 | + debug('MySQL connection is established: ' + self.settings || {}); | |
| 105 | + } | |
| 106 | + connection.release(); | |
| 107 | + } else { | |
| 108 | + if (self.debug || !callback) { | |
| 109 | + console.error('MySQL connection is failed: ' + self.settings || {}, err); | |
| 110 | + } | |
| 111 | + } | |
| 112 | + callback && callback(err, conn); | |
| 113 | + }); | |
| 114 | + } | |
| 115 | +}; | |
| 116 | + | |
| 117 | +function generateOptions(settings) { | |
| 118 | + var s = settings || {}; | |
| 119 | + if (s.collation) { | |
| 120 | + // Charset should be first 'chunk' of collation. | |
| 121 | + s.charset = s.collation.substr(0, s.collation.indexOf('_')); | |
| 122 | + } else { | |
| 123 | + s.collation = 'utf8_general_ci'; | |
| 124 | + s.charset = 'utf8'; | |
| 125 | + } | |
| 126 | + | |
| 127 | + s.supportBigNumbers = (s.supportBigNumbers || false); | |
| 128 | + s.timezone = (s.timezone || 'local'); | |
| 129 | + | |
| 130 | + if (isNaN(s.connectionLimit)) { | |
| 131 | + s.connectionLimit = 10; | |
| 132 | + } | |
| 133 | + | |
| 134 | + var options; | |
| 135 | + if (s.url) { | |
| 136 | + // use url to override other settings if url provided | |
| 137 | + options = s.url; | |
| 138 | + } else { | |
| 139 | + options = { | |
| 140 | + host: s.host || s.hostname || 'localhost', | |
| 141 | + port: s.port || 3306, | |
| 142 | + user: s.username || s.user, | |
| 143 | + password: s.password, | |
| 144 | + timezone: s.timezone, | |
| 145 | + socketPath: s.socketPath, | |
| 146 | + charset: s.collation.toUpperCase(), // Correct by docs despite seeming odd. | |
| 147 | + supportBigNumbers: s.supportBigNumbers, | |
| 148 | + connectionLimit: s.connectionLimit, | |
| 149 | + }; | |
| 150 | + | |
| 151 | + // Don't configure the DB if the pool can be used for multiple DBs | |
| 152 | + if (!s.createDatabase) { | |
| 153 | + options.database = s.database; | |
| 154 | + } | |
| 155 | + | |
| 156 | + // Take other options for mysql driver | |
| 157 | + // See https://github.com/strongloop/loopback-connector-mysql/issues/46 | |
| 158 | + for (var p in s) { | |
| 159 | + if (p === 'database' && s.createDatabase) { | |
| 160 | + continue; | |
| 161 | + } | |
| 162 | + if (options[p] === undefined) { | |
| 163 | + options[p] = s[p]; | |
| 164 | + } | |
| 165 | + } | |
| 166 | + } | |
| 167 | + return options; | |
| 168 | +} | |
| 169 | +/** | |
| 170 | + * Execute the sql statement | |
| 171 | + * | |
| 172 | + * @param {String} sql The SQL statement | |
| 173 | + * @param {Function} [callback] The callback after the SQL statement is executed | |
| 174 | + */ | |
| 175 | +MySQL.prototype.executeSQL = function(sql, params, options, callback) { | |
| 176 | + var self = this; | |
| 177 | + var client = this.client; | |
| 178 | + var debugEnabled = debug.enabled; | |
| 179 | + var db = this.settings.database; | |
| 180 | + if (typeof callback !== 'function') { | |
| 181 | + throw new Error(g.f('{{callback}} should be a function')); | |
| 182 | + } | |
| 183 | + if (debugEnabled) { | |
| 184 | + debug('SQL: %s, params: %j', sql, params); | |
| 185 | + } | |
| 186 | + | |
| 187 | + var transaction = options.transaction; | |
| 188 | + | |
| 189 | + function handleResponse(connection, err, result) { | |
| 190 | + if (!transaction) { | |
| 191 | + connection.release(); | |
| 192 | + } | |
| 193 | + callback && callback(err, result); | |
| 194 | + } | |
| 195 | + | |
| 196 | + function runQuery(connection, release) { | |
| 197 | + connection.query(sql, params, function(err, data) { | |
| 198 | + if (debugEnabled) { | |
| 199 | + if (err) { | |
| 200 | + debug('Error: %j', err); | |
| 201 | + } | |
| 202 | + debug('Data: ', data); | |
| 203 | + } | |
| 204 | + handleResponse(connection, err, data); | |
| 205 | + }); | |
| 206 | + } | |
| 207 | + | |
| 208 | + function executeWithConnection(err, connection) { | |
| 209 | + if (err) { | |
| 210 | + return callback && callback(err); | |
| 211 | + } | |
| 212 | + if (self.settings.createDatabase) { | |
| 213 | + // Call USE db ... | |
| 214 | + connection.query('USE ??', [db], function(err) { | |
| 215 | + if (err) { | |
| 216 | + if (err && err.message.match(/(^|: )unknown database/i)) { | |
| 217 | + var charset = self.settings.charset; | |
| 218 | + var collation = self.settings.collation; | |
| 219 | + var q = 'CREATE DATABASE ?? CHARACTER SET ?? COLLATE ??'; | |
| 220 | + connection.query(q, [db, charset, collation], function(err) { | |
| 221 | + if (!err) { | |
| 222 | + connection.query('USE ??', [db], function(err) { | |
| 223 | + runQuery(connection); | |
| 224 | + }); | |
| 225 | + } else { | |
| 226 | + handleResponse(connection, err); | |
| 227 | + } | |
| 228 | + }); | |
| 229 | + return; | |
| 230 | + } else { | |
| 231 | + handleResponse(connection, err); | |
| 232 | + return; | |
| 233 | + } | |
| 234 | + } | |
| 235 | + runQuery(connection); | |
| 236 | + }); | |
| 237 | + } else { | |
| 238 | + // Bypass USE db | |
| 239 | + runQuery(connection); | |
| 240 | + } | |
| 241 | + } | |
| 242 | + | |
| 243 | + if (transaction && transaction.connection && | |
| 244 | + transaction.connector === this) { | |
| 245 | + if (debugEnabled) { | |
| 246 | + debug('Execute SQL within a transaction'); | |
| 247 | + } | |
| 248 | + executeWithConnection(null, transaction.connection); | |
| 249 | + } else { | |
| 250 | + client.getConnection(executeWithConnection); | |
| 251 | + } | |
| 252 | +}; | |
| 253 | + | |
| 254 | +MySQL.prototype._modifyOrCreate = function(model, data, options, fields, cb) { | |
| 255 | + var sql = new ParameterizedSQL('INSERT INTO ' + this.tableEscaped(model)); | |
| 256 | + var columnValues = fields.columnValues; | |
| 257 | + var fieldNames = fields.names; | |
| 258 | + if (fieldNames.length) { | |
| 259 | + sql.merge('(' + fieldNames.join(',') + ')', ''); | |
| 260 | + var values = ParameterizedSQL.join(columnValues, ','); | |
| 261 | + values.sql = 'VALUES(' + values.sql + ')'; | |
| 262 | + sql.merge(values); | |
| 263 | + } else { | |
| 264 | + sql.merge(this.buildInsertDefaultValues(model, data, options)); | |
| 265 | + } | |
| 266 | + | |
| 267 | + sql.merge('ON DUPLICATE KEY UPDATE'); | |
| 268 | + var setValues = []; | |
| 269 | + for (var i = 0, n = fields.names.length; i < n; i++) { | |
| 270 | + if (!fields.properties[i].id) { | |
| 271 | + setValues.push(new ParameterizedSQL(fields.names[i] + '=' + | |
| 272 | + columnValues[i].sql, columnValues[i].params)); | |
| 273 | + } | |
| 274 | + } | |
| 275 | + | |
| 276 | + sql.merge(ParameterizedSQL.join(setValues, ',')); | |
| 277 | + | |
| 278 | + this.execute(sql.sql, sql.params, options, function(err, info) { | |
| 279 | + if (!err && info && info.insertId) { | |
| 280 | + data.id = info.insertId; | |
| 281 | + } | |
| 282 | + var meta = {}; | |
| 283 | + if (info) { | |
| 284 | + // When using the INSERT ... ON DUPLICATE KEY UPDATE statement, | |
| 285 | + // the returned value is as follows: | |
| 286 | + // 1 for each successful INSERT. | |
| 287 | + // 2 for each successful UPDATE. | |
| 288 | + meta.isNewInstance = (info.affectedRows === 1); | |
| 289 | + } | |
| 290 | + cb(err, data, meta); | |
| 291 | + }); | |
| 292 | +}; | |
| 293 | + | |
| 294 | +/** | |
| 295 | + * Replace if the model instance exists with the same id or create a new instance | |
| 296 | + * | |
| 297 | + * @param {String} model The model name | |
| 298 | + * @param {Object} data The model instance data | |
| 299 | + * @param {Object} options The options | |
| 300 | + * @param {Function} [cb] The callback function | |
| 301 | + */ | |
| 302 | +MySQL.prototype.replaceOrCreate = function(model, data, options, cb) { | |
| 303 | + var fields = this.buildReplaceFields(model, data); | |
| 304 | + this._modifyOrCreate(model, data, options, fields, cb); | |
| 305 | +}; | |
| 306 | + | |
| 307 | +/** | |
| 308 | + * Update if the model instance exists with the same id or create a new instance | |
| 309 | + * | |
| 310 | + * @param {String} model The model name | |
| 311 | + * @param {Object} data The model instance data | |
| 312 | + * @param {Object} options The options | |
| 313 | + * @param {Function} [cb] The callback function | |
| 314 | + */ | |
| 315 | +MySQL.prototype.save = | |
| 316 | +MySQL.prototype.updateOrCreate = function(model, data, options, cb) { | |
| 317 | + var fields = this.buildFields(model, data); | |
| 318 | + this._modifyOrCreate(model, data, options, fields, cb); | |
| 319 | +}; | |
| 320 | + | |
| 321 | +// arsis 2017-03-21 add tzOffset | |
| 322 | +function dateToMysql(val, tzOffset) { | |
| 323 | + var tz = tzOffset || 0; | |
| 324 | + return val.getUTCFullYear() + '-' + | |
| 325 | + fillZeros(val.getUTCMonth() + 1) + '-' + | |
| 326 | + fillZeros(val.getUTCDate()) + ' ' + | |
| 327 | + fillZeros(val.getUTCHours() + tz) + ':' + | |
| 328 | + fillZeros(val.getUTCMinutes()) + ':' + | |
| 329 | + fillZeros(val.getUTCSeconds()); | |
| 330 | + | |
| 331 | + function fillZeros(v) { | |
| 332 | + return v < 10 ? '0' + v : v; | |
| 333 | + } | |
| 334 | +} | |
| 335 | + | |
| 336 | +MySQL.prototype.getInsertedId = function(model, info) { | |
| 337 | + var insertedId = info && typeof info.insertId === 'number' ? | |
| 338 | + info.insertId : undefined; | |
| 339 | + return insertedId; | |
| 340 | +}; | |
| 341 | + | |
| 342 | +/*! | |
| 343 | + * Convert property name/value to an escaped DB column value | |
| 344 | + * @param {Object} prop Property descriptor | |
| 345 | + * @param {*} val Property value | |
| 346 | + * @returns {*} The escaped value of DB column | |
| 347 | + */ | |
| 348 | +MySQL.prototype.toColumnValue = function(prop, val) { | |
| 349 | + if (val == null) { | |
| 350 | + if (prop.autoIncrement || prop.id) { | |
| 351 | + return new ParameterizedSQL('DEFAULT'); | |
| 352 | + } | |
| 353 | + return null; | |
| 354 | + } | |
| 355 | + if (!prop) { | |
| 356 | + return val; | |
| 357 | + } | |
| 358 | + if (prop.type === String) { | |
| 359 | + return String(val); | |
| 360 | + } | |
| 361 | + if (prop.type === Number) { | |
| 362 | + if (isNaN(val)) { | |
| 363 | + // FIXME: [rfeng] Should fail fast? | |
| 364 | + return val; | |
| 365 | + } | |
| 366 | + return val; | |
| 367 | + } | |
| 368 | + if (prop.type === Date) { | |
| 369 | + if (!val.toUTCString) { | |
| 370 | + val = new Date(val); | |
| 371 | + } | |
| 372 | + return dateToMysql(val, this.tzOffset); // arsis 2017-03-21 add tzOffset | |
| 373 | + } | |
| 374 | + if (prop.type === Boolean) { | |
| 375 | + return !!val; | |
| 376 | + } | |
| 377 | + if (prop.type.name === 'GeoPoint') { | |
| 378 | + return new ParameterizedSQL({ | |
| 379 | + sql: 'Point(?,?)', | |
| 380 | + params: [val.lat, val.lng], | |
| 381 | + }); | |
| 382 | + } | |
| 383 | + if (prop.type === Object) { | |
| 384 | + return this._serializeObject(val); | |
| 385 | + } | |
| 386 | + if (typeof prop.type === 'function') { | |
| 387 | + return this._serializeObject(val); | |
| 388 | + } | |
| 389 | + return this._serializeObject(val); | |
| 390 | +}; | |
| 391 | + | |
| 392 | +MySQL.prototype._serializeObject = function(obj) { | |
| 393 | + var val; | |
| 394 | + if (obj && typeof obj.toJSON === 'function') { | |
| 395 | + obj = obj.toJSON(); | |
| 396 | + } | |
| 397 | + if (typeof obj !== 'string') { | |
| 398 | + val = JSON.stringify(obj); | |
| 399 | + } else { | |
| 400 | + val = obj; | |
| 401 | + } | |
| 402 | + return val; | |
| 403 | +}; | |
| 404 | + | |
| 405 | +/*! | |
| 406 | + * Convert the data from database column to model property | |
| 407 | + * @param {object} Model property descriptor | |
| 408 | + * @param {*) val Column value | |
| 409 | + * @returns {*} Model property value | |
| 410 | + */ | |
| 411 | +MySQL.prototype.fromColumnValue = function(prop, val) { | |
| 412 | + if (val == null) { | |
| 413 | + return val; | |
| 414 | + } | |
| 415 | + if (prop) { | |
| 416 | + switch (prop.type.name) { | |
| 417 | + case 'Number': | |
| 418 | + val = Number(val); | |
| 419 | + break; | |
| 420 | + case 'String': | |
| 421 | + val = String(val); | |
| 422 | + break; | |
| 423 | + case 'Date': | |
| 424 | + | |
| 425 | + // MySQL allows, unless NO_ZERO_DATE is set, dummy date/time entries | |
| 426 | + // new Date() will return Invalid Date for those, so we need to handle | |
| 427 | + // those separate. | |
| 428 | + if (val == '0000-00-00 00:00:00') { | |
| 429 | + val = null; | |
| 430 | + } else { | |
| 431 | + val = new Date(val.toString().replace(/GMT.*$/, 'GMT')); | |
| 432 | + } | |
| 433 | + break; | |
| 434 | + case 'Boolean': | |
| 435 | + val = Boolean(val); | |
| 436 | + break; | |
| 437 | + case 'GeoPoint': | |
| 438 | + case 'Point': | |
| 439 | + val = { | |
| 440 | + lat: val.x, | |
| 441 | + lng: val.y, | |
| 442 | + }; | |
| 443 | + break; | |
| 444 | + case 'List': | |
| 445 | + case 'Array': | |
| 446 | + case 'Object': | |
| 447 | + case 'JSON': | |
| 448 | + if (typeof val === 'string') { | |
| 449 | + val = JSON.parse(val); | |
| 450 | + } | |
| 451 | + break; | |
| 452 | + default: | |
| 453 | + if (!Array.isArray(prop.type) && !prop.type.modelName) { | |
| 454 | + // Do not convert array and model types | |
| 455 | + val = prop.type(val); | |
| 456 | + } | |
| 457 | + break; | |
| 458 | + } | |
| 459 | + } | |
| 460 | + return val; | |
| 461 | +}; | |
| 462 | + | |
| 463 | +/** | |
| 464 | + * Escape an identifier such as the column name | |
| 465 | + * @param {string} name A database identifier | |
| 466 | + * @returns {string} The escaped database identifier | |
| 467 | + */ | |
| 468 | +MySQL.prototype.escapeName = function(name) { | |
| 469 | + return this.client.escapeId(name); | |
| 470 | +}; | |
| 471 | + | |
| 472 | +/** | |
| 473 | + * Build the LIMIT clause | |
| 474 | + * @param {string} model Model name | |
| 475 | + * @param {number} limit The limit | |
| 476 | + * @param {number} offset The offset | |
| 477 | + * @returns {string} The LIMIT clause | |
| 478 | + */ | |
| 479 | +MySQL.prototype._buildLimit = function(model, limit, offset) { | |
| 480 | + if (isNaN(limit)) { | |
| 481 | + limit = 0; | |
| 482 | + } | |
| 483 | + if (isNaN(offset)) { | |
| 484 | + offset = 0; | |
| 485 | + } | |
| 486 | + if (!limit && !offset) { | |
| 487 | + return ''; | |
| 488 | + } | |
| 489 | + return 'LIMIT ' + (offset ? (offset + ',' + limit) : limit); | |
| 490 | +}; | |
| 491 | + | |
| 492 | +MySQL.prototype.applyPagination = function(model, stmt, filter) { | |
| 493 | + var limitClause = this._buildLimit(model, filter.limit, | |
| 494 | + filter.offset || filter.skip); | |
| 495 | + return stmt.merge(limitClause); | |
| 496 | +}; | |
| 497 | + | |
| 498 | +/** | |
| 499 | + * Get the place holder in SQL for identifiers, such as ?? | |
| 500 | + * @param {String} key Optional key, such as 1 or id | |
| 501 | + * @returns {String} The place holder | |
| 502 | + */ | |
| 503 | +MySQL.prototype.getPlaceholderForIdentifier = function(key) { | |
| 504 | + return '??'; | |
| 505 | +}; | |
| 506 | + | |
| 507 | +/** | |
| 508 | + * Get the place holder in SQL for values, such as :1 or ? | |
| 509 | + * @param {String} key Optional key, such as 1 or id | |
| 510 | + * @returns {String} The place holder | |
| 511 | + */ | |
| 512 | +MySQL.prototype.getPlaceholderForValue = function(key) { | |
| 513 | + return '?'; | |
| 514 | +}; | |
| 515 | + | |
| 516 | +MySQL.prototype.getCountForAffectedRows = function(model, info) { | |
| 517 | + var affectedRows = info && typeof info.affectedRows === 'number' ? | |
| 518 | + info.affectedRows : undefined; | |
| 519 | + return affectedRows; | |
| 520 | +}; | |
| 521 | + | |
| 522 | +/** | |
| 523 | + * Disconnect from MySQL | |
| 524 | + */ | |
| 525 | +MySQL.prototype.disconnect = function(cb) { | |
| 526 | + if (this.debug) { | |
| 527 | + debug('disconnect'); | |
| 528 | + } | |
| 529 | + if (this.client) { | |
| 530 | + this.client.end(cb); | |
| 531 | + } else { | |
| 532 | + process.nextTick(cb); | |
| 533 | + } | |
| 534 | +}; | |
| 535 | + | |
| 536 | +MySQL.prototype.ping = function(cb) { | |
| 537 | + this.execute('SELECT 1 AS result', cb); | |
| 538 | +}; | |
| 539 | + | |
| 540 | +MySQL.prototype.buildExpression = function(columnName, operator, operatorValue, | |
| 541 | + propertyDefinition) { | |
| 542 | + if (operator === 'regexp') { | |
| 543 | + if (operatorValue.ignoreCase) | |
| 544 | + g.warn('{{MySQL}} {{regex}} syntax does not respect the {{`i`}} flag'); | |
| 545 | + | |
| 546 | + if (operatorValue.global) | |
| 547 | + g.warn('{{MySQL}} {{regex}} syntax does not respect the {{`g`}} flag'); | |
| 548 | + | |
| 549 | + if (operatorValue.multiline) | |
| 550 | + g.warn('{{MySQL}} {{regex}} syntax does not respect the {{`m`}} flag'); | |
| 551 | + | |
| 552 | + return new ParameterizedSQL(columnName + ' REGEXP ?', | |
| 553 | + [operatorValue.source]); | |
| 554 | + } | |
| 555 | + | |
| 556 | + // invoke the base implementation of `buildExpression` | |
| 557 | + return this.invokeSuper('buildExpression', columnName, operator, | |
| 558 | + operatorValue, propertyDefinition); | |
| 559 | +}; | |
| 560 | + | |
| 561 | +require('./migration')(MySQL, mysql); | |
| 562 | +require('./discovery')(MySQL, mysql); | |
| 563 | +require('./transaction')(MySQL, mysql); | ... | ... |
| 1 | +++ a/lib/transaction.js | |
| ... | ... | @@ -0,0 +1,68 @@ |
| 1 | +// Copyright IBM Corp. 2015,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +var debug = require('debug')('loopback:connector:mysql:transaction'); | |
| 8 | +module.exports = mixinTransaction; | |
| 9 | + | |
| 10 | +/*! | |
| 11 | + * @param {MySQL} MySQL connector class | |
| 12 | + * @param {Object} mysql mysql driver | |
| 13 | + */ | |
| 14 | +function mixinTransaction(MySQL, mysql) { | |
| 15 | + /** | |
| 16 | + * Begin a new transaction | |
| 17 | + * @param isolationLevel | |
| 18 | + * @param cb | |
| 19 | + */ | |
| 20 | + MySQL.prototype.beginTransaction = function(isolationLevel, cb) { | |
| 21 | + debug('Begin a transaction with isolation level: %s', isolationLevel); | |
| 22 | + this.client.getConnection(function(err, connection) { | |
| 23 | + if (err) return cb(err); | |
| 24 | + if (isolationLevel) { | |
| 25 | + connection.query( | |
| 26 | + 'SET SESSION TRANSACTION ISOLATION LEVEL ' + isolationLevel, | |
| 27 | + function(err) { | |
| 28 | + if (err) return cb(err); | |
| 29 | + connection.beginTransaction(function(err) { | |
| 30 | + if (err) return cb(err); | |
| 31 | + return cb(null, connection); | |
| 32 | + }); | |
| 33 | + }); | |
| 34 | + } else { | |
| 35 | + connection.beginTransaction(function(err) { | |
| 36 | + if (err) return cb(err); | |
| 37 | + return cb(null, connection); | |
| 38 | + }); | |
| 39 | + } | |
| 40 | + }); | |
| 41 | + }; | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * | |
| 45 | + * @param connection | |
| 46 | + * @param cb | |
| 47 | + */ | |
| 48 | + MySQL.prototype.commit = function(connection, cb) { | |
| 49 | + debug('Commit a transaction'); | |
| 50 | + connection.commit(function(err) { | |
| 51 | + connection.release(); | |
| 52 | + cb(err); | |
| 53 | + }); | |
| 54 | + }; | |
| 55 | + | |
| 56 | + /** | |
| 57 | + * | |
| 58 | + * @param connection | |
| 59 | + * @param cb | |
| 60 | + */ | |
| 61 | + MySQL.prototype.rollback = function(connection, cb) { | |
| 62 | + debug('Rollback a transaction'); | |
| 63 | + connection.rollback(function(err) { | |
| 64 | + connection.release(); | |
| 65 | + cb(err); | |
| 66 | + }); | |
| 67 | + }; | |
| 68 | +} | ... | ... |
| 1 | +++ a/package.json | |
| ... | ... | @@ -0,0 +1,37 @@ |
| 1 | +{ | |
| 2 | + "name": "loopback-connector-mysql", | |
| 3 | + "version": "3.0.0", | |
| 4 | + "description": "MySQL connector for loopback-datasource-juggler", | |
| 5 | + "engines": { | |
| 6 | + "node": ">=4" | |
| 7 | + }, | |
| 8 | + "main": "index.js", | |
| 9 | + "scripts": { | |
| 10 | + "pretest": "node pretest.js", | |
| 11 | + "lint": "eslint .", | |
| 12 | + "test": "mocha --timeout 10000 test/*.js", | |
| 13 | + "posttest": "npm run lint" | |
| 14 | + }, | |
| 15 | + "dependencies": { | |
| 16 | + "async": "^0.9.0", | |
| 17 | + "debug": "^2.1.1", | |
| 18 | + "loopback-connector": "^4.0.0", | |
| 19 | + "mysql": "^2.11.1", | |
| 20 | + "strong-globalize": "^2.5.8" | |
| 21 | + }, | |
| 22 | + "devDependencies": { | |
| 23 | + "bluebird": "~2.9.10", | |
| 24 | + "eslint": "^2.13.1", | |
| 25 | + "eslint-config-loopback": "^4.0.0", | |
| 26 | + "loopback-datasource-juggler": "^3.0.0", | |
| 27 | + "mocha": "^2.1.0", | |
| 28 | + "rc": "^1.0.0", | |
| 29 | + "should": "^8.0.2", | |
| 30 | + "sinon": "^1.15.4" | |
| 31 | + }, | |
| 32 | + "repository": { | |
| 33 | + "type": "git", | |
| 34 | + "url": "https://github.com/strongloop/loopback-connector-mysql.git" | |
| 35 | + }, | |
| 36 | + "license": "MIT" | |
| 37 | +} | ... | ... |
| 1 | +++ a/pretest.js | |
| ... | ... | @@ -0,0 +1,43 @@ |
| 1 | +'use strict'; | |
| 2 | + | |
| 3 | +if (!process.env.TEST_MYSQL_USER && | |
| 4 | + !process.env.MYSQL_USER && | |
| 5 | + !process.env.CI) { | |
| 6 | + return console.log('Not seeding DB with test db'); | |
| 7 | +} | |
| 8 | + | |
| 9 | +process.env.TEST_MYSQL_HOST = | |
| 10 | + process.env.TEST_MYSQL_HOST || process.env.MYSQL_HOST || 'localhost'; | |
| 11 | +process.env.TEST_MYSQL_PORT = | |
| 12 | + process.env.TEST_MYSQL_PORT || process.env.MYSQL_PORT || 3306; | |
| 13 | +process.env.TEST_MYSQL_USER = | |
| 14 | + process.env.TEST_MYSQL_USER || process.env.MYSQL_USER || 'test'; | |
| 15 | +process.env.TEST_MYSQL_PASSWORD = | |
| 16 | + process.env.TEST_MYSQL_PASSWORD || process.env.MYSQL_PASSWORD || 'test'; | |
| 17 | + | |
| 18 | +var fs = require('fs'); | |
| 19 | +var cp = require('child_process'); | |
| 20 | + | |
| 21 | +var sql = fs.createReadStream(require.resolve('./test/schema.sql')); | |
| 22 | +var stdio = ['pipe', process.stdout, process.stderr]; | |
| 23 | +var args = ['--user=' + process.env.TEST_MYSQL_USER]; | |
| 24 | + | |
| 25 | +if (process.env.TEST_MYSQL_HOST) { | |
| 26 | + args.push('--host=' + process.env.TEST_MYSQL_HOST); | |
| 27 | +} | |
| 28 | +if (process.env.TEST_MYSQL_PORT) { | |
| 29 | + args.push('--port=' + process.env.TEST_MYSQL_PORT); | |
| 30 | +} | |
| 31 | +if (process.env.TEST_MYSQL_PASSWORD) { | |
| 32 | + args.push('--password=' + process.env.TEST_MYSQL_PASSWORD); | |
| 33 | +} | |
| 34 | + | |
| 35 | +console.log('seeding DB with example db...'); | |
| 36 | +var mysql = cp.spawn('mysql', args, {stdio: stdio}); | |
| 37 | +sql.pipe(mysql.stdin); | |
| 38 | +mysql.on('exit', function(code) { | |
| 39 | + console.log('done seeding DB'); | |
| 40 | + setTimeout(function() { | |
| 41 | + process.exit(code); | |
| 42 | + }, 200); | |
| 43 | +}); | ... | ... |
| 1 | +++ a/test/connection.test.js | |
| ... | ... | @@ -0,0 +1,176 @@ |
| 1 | +// Copyright IBM Corp. 2013,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +require('./init.js'); | |
| 8 | +var assert = require('assert'); | |
| 9 | +var should = require('should'); | |
| 10 | +var DataSource = require('loopback-datasource-juggler').DataSource; | |
| 11 | +var mysqlConnector = require('../'); | |
| 12 | +var url = require('url'); | |
| 13 | + | |
| 14 | +var db, DummyModel, odb, config; | |
| 15 | + | |
| 16 | +describe('connections', function() { | |
| 17 | + before(function() { | |
| 18 | + require('./init.js'); | |
| 19 | + | |
| 20 | + config = global.getConfig(); | |
| 21 | + | |
| 22 | + odb = getDataSource({collation: 'utf8_general_ci', createDatabase: true}); | |
| 23 | + db = odb; | |
| 24 | + }); | |
| 25 | + | |
| 26 | + it('should pass with valid settings', function(done) { | |
| 27 | + var db = new DataSource(mysqlConnector, config); | |
| 28 | + db.ping(done); | |
| 29 | + }); | |
| 30 | + | |
| 31 | + it('ignores all other settings when url is present', function(done) { | |
| 32 | + var formatedUrl = generateURL(config); | |
| 33 | + var dbConfig = { | |
| 34 | + url: formatedUrl, | |
| 35 | + host: 'invalid-hostname', | |
| 36 | + port: 80, | |
| 37 | + database: 'invalid-database', | |
| 38 | + username: 'invalid-username', | |
| 39 | + password: 'invalid-password', | |
| 40 | + }; | |
| 41 | + | |
| 42 | + var db = new DataSource(mysqlConnector, dbConfig); | |
| 43 | + db.ping(done); | |
| 44 | + }); | |
| 45 | + | |
| 46 | + it('should use utf8 charset', function(done) { | |
| 47 | + var test_set = /utf8/; | |
| 48 | + var test_collo = /utf8_general_ci/; | |
| 49 | + var test_set_str = 'utf8'; | |
| 50 | + var test_set_collo = 'utf8_general_ci'; | |
| 51 | + charsetTest(test_set, test_collo, test_set_str, test_set_collo, done); | |
| 52 | + }); | |
| 53 | + | |
| 54 | + it('should disconnect first db', function(done) { | |
| 55 | + db.disconnect(function() { | |
| 56 | + odb = getDataSource(); | |
| 57 | + done(); | |
| 58 | + }); | |
| 59 | + }); | |
| 60 | + | |
| 61 | + it('should use latin1 charset', function(done) { | |
| 62 | + var test_set = /latin1/; | |
| 63 | + var test_collo = /latin1_general_ci/; | |
| 64 | + var test_set_str = 'latin1'; | |
| 65 | + var test_set_collo = 'latin1_general_ci'; | |
| 66 | + charsetTest(test_set, test_collo, test_set_str, test_set_collo, done); | |
| 67 | + }); | |
| 68 | + | |
| 69 | + it('should drop db and disconnect all', function(done) { | |
| 70 | + db.connector.execute('DROP DATABASE IF EXISTS ' + db.settings.database, function(err) { | |
| 71 | + db.disconnect(function() { | |
| 72 | + done(); | |
| 73 | + }); | |
| 74 | + }); | |
| 75 | + }); | |
| 76 | + | |
| 77 | + describe('lazyConnect', function() { | |
| 78 | + it('should skip connect phase (lazyConnect = true)', function(done) { | |
| 79 | + var dbConfig = { | |
| 80 | + host: '127.0.0.1', | |
| 81 | + port: 4, | |
| 82 | + lazyConnect: true, | |
| 83 | + }; | |
| 84 | + var ds = new DataSource(mysqlConnector, dbConfig); | |
| 85 | + | |
| 86 | + var errTimeout = setTimeout(function() { | |
| 87 | + done(); | |
| 88 | + }, 2000); | |
| 89 | + ds.on('error', function(err) { | |
| 90 | + clearTimeout(errTimeout); | |
| 91 | + done(err); | |
| 92 | + }); | |
| 93 | + }); | |
| 94 | + | |
| 95 | + it('should report connection error (lazyConnect = false)', function(done) { | |
| 96 | + var dbConfig = { | |
| 97 | + host: '127.0.0.1', | |
| 98 | + port: 4, | |
| 99 | + lazyConnect: false, | |
| 100 | + }; | |
| 101 | + var ds = new DataSource(mysqlConnector, dbConfig); | |
| 102 | + | |
| 103 | + ds.on('error', function(err) { | |
| 104 | + err.message.should.containEql('ECONNREFUSED'); | |
| 105 | + done(); | |
| 106 | + }); | |
| 107 | + }); | |
| 108 | + }); | |
| 109 | +}); | |
| 110 | + | |
| 111 | +function charsetTest(test_set, test_collo, test_set_str, test_set_collo, done) { | |
| 112 | + query('DROP DATABASE IF EXISTS ' + odb.settings.database, function(err) { | |
| 113 | + assert.ok(!err); | |
| 114 | + odb.disconnect(function() { | |
| 115 | + db = getDataSource({collation: test_set_collo, createDatabase: true}); | |
| 116 | + DummyModel = db.define('DummyModel', {string: String}); | |
| 117 | + db.automigrate(function() { | |
| 118 | + var q = 'SELECT DEFAULT_COLLATION_NAME' + | |
| 119 | + ' FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ' + | |
| 120 | + db.driver.escape(db.settings.database) + ' LIMIT 1'; | |
| 121 | + db.connector.execute(q, function(err, r) { | |
| 122 | + assert.ok(!err); | |
| 123 | + should(r[0].DEFAULT_COLLATION_NAME).match(test_collo); | |
| 124 | + db.connector.execute('SHOW VARIABLES LIKE "character_set%"', function(err, r) { | |
| 125 | + assert.ok(!err); | |
| 126 | + var hit_all = 0; | |
| 127 | + for (var result in r) { | |
| 128 | + hit_all += matchResult(r[result], 'character_set_connection', test_set); | |
| 129 | + hit_all += matchResult(r[result], 'character_set_database', test_set); | |
| 130 | + hit_all += matchResult(r[result], 'character_set_results', test_set); | |
| 131 | + hit_all += matchResult(r[result], 'character_set_client', test_set); | |
| 132 | + } | |
| 133 | + assert.equal(hit_all, 4); | |
| 134 | + }); | |
| 135 | + db.connector.execute('SHOW VARIABLES LIKE "collation%"', function(err, r) { | |
| 136 | + assert.ok(!err); | |
| 137 | + var hit_all = 0; | |
| 138 | + for (var result in r) { | |
| 139 | + hit_all += matchResult(r[result], 'collation_connection', test_set); | |
| 140 | + hit_all += matchResult(r[result], 'collation_database', test_set); | |
| 141 | + } | |
| 142 | + assert.equal(hit_all, 2); | |
| 143 | + done(); | |
| 144 | + }); | |
| 145 | + }); | |
| 146 | + }); | |
| 147 | + }); | |
| 148 | + }); | |
| 149 | +} | |
| 150 | + | |
| 151 | +function matchResult(result, variable_name, match) { | |
| 152 | + if (result.Variable_name === variable_name) { | |
| 153 | + assert.ok(result.Value.match(match)); | |
| 154 | + return 1; | |
| 155 | + } | |
| 156 | + return 0; | |
| 157 | +} | |
| 158 | + | |
| 159 | +var query = function(sql, cb) { | |
| 160 | + odb.connector.execute(sql, cb); | |
| 161 | +}; | |
| 162 | + | |
| 163 | +function generateURL(config) { | |
| 164 | + var urlObj = { | |
| 165 | + protocol: 'mysql', | |
| 166 | + auth: config.username || '', | |
| 167 | + hostname: config.host, | |
| 168 | + pathname: config.database, | |
| 169 | + slashes: true, | |
| 170 | + }; | |
| 171 | + if (config.password) { | |
| 172 | + urlObj.auth += ':' + config.password; | |
| 173 | + } | |
| 174 | + var formatedUrl = url.format(urlObj); | |
| 175 | + return formatedUrl; | |
| 176 | +} | ... | ... |
| 1 | +++ a/test/datatypes.test.js | |
| ... | ... | @@ -0,0 +1,155 @@ |
| 1 | +// Copyright IBM Corp. 2013,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +require('./init.js'); | |
| 8 | +var assert = require('assert'); | |
| 9 | + | |
| 10 | +var db, EnumModel, ANIMAL_ENUM; | |
| 11 | +var mysqlVersion; | |
| 12 | + | |
| 13 | +describe('MySQL specific datatypes', function() { | |
| 14 | + before(setup); | |
| 15 | + | |
| 16 | + it('should run migration', function(done) { | |
| 17 | + db.automigrate(function() { | |
| 18 | + done(); | |
| 19 | + }); | |
| 20 | + }); | |
| 21 | + | |
| 22 | + it('An enum should parse itself', function(done) { | |
| 23 | + assert.equal(ANIMAL_ENUM.CAT, ANIMAL_ENUM('cat')); | |
| 24 | + assert.equal(ANIMAL_ENUM.CAT, ANIMAL_ENUM('CAT')); | |
| 25 | + assert.equal(ANIMAL_ENUM.CAT, ANIMAL_ENUM(2)); | |
| 26 | + assert.equal(ANIMAL_ENUM.CAT, 'cat'); | |
| 27 | + assert.equal(ANIMAL_ENUM(null), null); | |
| 28 | + assert.equal(ANIMAL_ENUM(''), ''); | |
| 29 | + assert.equal(ANIMAL_ENUM(0), ''); | |
| 30 | + done(); | |
| 31 | + }); | |
| 32 | + | |
| 33 | + it('should create a model instance with Enums', function(done) { | |
| 34 | + var em = EnumModel.create({animal: ANIMAL_ENUM.CAT, condition: 'sleepy', mood: 'happy'}, function(err, obj) { | |
| 35 | + assert.ok(!err); | |
| 36 | + assert.equal(obj.condition, 'sleepy'); | |
| 37 | + EnumModel.findOne({where: {animal: ANIMAL_ENUM.CAT}}, function(err, found) { | |
| 38 | + assert.ok(!err); | |
| 39 | + assert.equal(found.mood, 'happy'); | |
| 40 | + assert.equal(found.animal, ANIMAL_ENUM.CAT); | |
| 41 | + done(); | |
| 42 | + }); | |
| 43 | + }); | |
| 44 | + }); | |
| 45 | + | |
| 46 | + it('should fail spectacularly with invalid enum values', function(done) { | |
| 47 | + // In MySQL 5.6/5.7, An ENUM value must be one of those listed in the column definition, | |
| 48 | + // or the internal numeric equivalent thereof. Invalid values are rejected. | |
| 49 | + // Reference: http://dev.mysql.com/doc/refman/5.7/en/constraint-enum.html | |
| 50 | + EnumModel.create({animal: 'horse', condition: 'sleepy', mood: 'happy'}, function(err, obj) { | |
| 51 | + assert.ok(err); | |
| 52 | + assert.equal(err.code, 'WARN_DATA_TRUNCATED'); | |
| 53 | + assert.equal(err.errno, 1265); | |
| 54 | + done(); | |
| 55 | + }); | |
| 56 | + }); | |
| 57 | + | |
| 58 | + it('should create a model instance with object/json types', function(done) { | |
| 59 | + var note = {a: 1, b: '2'}; | |
| 60 | + var extras = {c: 3, d: '4'}; | |
| 61 | + var em = EnumModel.create({animal: ANIMAL_ENUM.DOG, condition: 'sleepy', | |
| 62 | + mood: 'happy', note: note, extras: extras}, function(err, obj) { | |
| 63 | + assert.ok(!err); | |
| 64 | + assert.equal(obj.condition, 'sleepy'); | |
| 65 | + EnumModel.findOne({where: {animal: ANIMAL_ENUM.DOG}}, function(err, found) { | |
| 66 | + assert.ok(!err); | |
| 67 | + assert.equal(found.mood, 'happy'); | |
| 68 | + assert.equal(found.animal, ANIMAL_ENUM.DOG); | |
| 69 | + assert.deepEqual(found.note, note); | |
| 70 | + assert.deepEqual(found.extras, extras); | |
| 71 | + done(); | |
| 72 | + }); | |
| 73 | + }); | |
| 74 | + }); | |
| 75 | + | |
| 76 | + it('should disconnect when done', function(done) { | |
| 77 | + db.disconnect(); | |
| 78 | + done(); | |
| 79 | + }); | |
| 80 | +}); | |
| 81 | + | |
| 82 | +function setup(done) { | |
| 83 | + require('./init.js'); | |
| 84 | + | |
| 85 | + db = getSchema(); | |
| 86 | + | |
| 87 | + ANIMAL_ENUM = db.EnumFactory('dog', 'cat', 'mouse'); | |
| 88 | + | |
| 89 | + EnumModel = db.define('EnumModel', { | |
| 90 | + animal: {type: ANIMAL_ENUM, null: false}, | |
| 91 | + condition: {type: db.EnumFactory('hungry', 'sleepy', 'thirsty')}, | |
| 92 | + mood: {type: db.EnumFactory('angry', 'happy', 'sad')}, | |
| 93 | + note: Object, | |
| 94 | + extras: 'JSON', | |
| 95 | + }); | |
| 96 | + | |
| 97 | + query('SELECT VERSION()', function(err, res) { | |
| 98 | + mysqlVersion = res && res[0] && res[0]['VERSION()']; | |
| 99 | + blankDatabase(db, done); | |
| 100 | + }); | |
| 101 | +} | |
| 102 | + | |
| 103 | +var query = function(sql, cb) { | |
| 104 | + db.adapter.execute(sql, cb); | |
| 105 | +}; | |
| 106 | + | |
| 107 | +var blankDatabase = function(db, cb) { | |
| 108 | + var dbn = db.settings.database; | |
| 109 | + var cs = db.settings.charset; | |
| 110 | + var co = db.settings.collation; | |
| 111 | + query('DROP DATABASE IF EXISTS ' + dbn, function(err) { | |
| 112 | + var q = 'CREATE DATABASE ' + dbn; | |
| 113 | + if (cs) { | |
| 114 | + q += ' CHARACTER SET ' + cs; | |
| 115 | + } | |
| 116 | + if (co) { | |
| 117 | + q += ' COLLATE ' + co; | |
| 118 | + } | |
| 119 | + query(q, function(err) { | |
| 120 | + query('USE ' + dbn, cb); | |
| 121 | + }); | |
| 122 | + }); | |
| 123 | +}; | |
| 124 | + | |
| 125 | +var getFields = function(model, cb) { | |
| 126 | + query('SHOW FIELDS FROM ' + model, function(err, res) { | |
| 127 | + if (err) { | |
| 128 | + cb(err); | |
| 129 | + } else { | |
| 130 | + var fields = {}; | |
| 131 | + res.forEach(function(field) { | |
| 132 | + fields[field.Field] = field; | |
| 133 | + }); | |
| 134 | + cb(err, fields); | |
| 135 | + } | |
| 136 | + }); | |
| 137 | +}; | |
| 138 | + | |
| 139 | +var getIndexes = function(model, cb) { | |
| 140 | + query('SHOW INDEXES FROM ' + model, function(err, res) { | |
| 141 | + if (err) { | |
| 142 | + console.log(err); | |
| 143 | + cb(err); | |
| 144 | + } else { | |
| 145 | + var indexes = {}; | |
| 146 | + // Note: this will only show the first key of compound keys | |
| 147 | + res.forEach(function(index) { | |
| 148 | + if (parseInt(index.Seq_in_index, 10) == 1) { | |
| 149 | + indexes[index.Key_name] = index; | |
| 150 | + } | |
| 151 | + }); | |
| 152 | + cb(err, indexes); | |
| 153 | + } | |
| 154 | + }); | |
| 155 | +}; | ... | ... |
| 1 | +++ a/test/helpers/platform.js | |
| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | +// Copyright IBM Corp. 2013,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | + | |
| 8 | +exports.isWindows = /^win/.test(process.platform); | ... | ... |
| 1 | +++ a/test/imported.test.js | |
| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | +// Copyright IBM Corp. 2013,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +describe('mysql imported features', function() { | |
| 8 | + before(function() { | |
| 9 | + require('./init.js'); | |
| 10 | + }); | |
| 11 | + | |
| 12 | + require('loopback-datasource-juggler/test/common.batch.js'); | |
| 13 | + require('loopback-datasource-juggler/test/include.test.js'); | |
| 14 | +}); | ... | ... |
| 1 | +++ a/test/init.js | |
| ... | ... | @@ -0,0 +1,42 @@ |
| 1 | +// Copyright IBM Corp. 2013,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | + | |
| 8 | +module.exports = require('should'); | |
| 9 | + | |
| 10 | +var DataSource = require('loopback-datasource-juggler').DataSource; | |
| 11 | + | |
| 12 | +var config = require('rc')('loopback', {test: {mysql: {}}}).test.mysql; | |
| 13 | +console.log(config); | |
| 14 | +global.getConfig = function(options) { | |
| 15 | + var dbConf = { | |
| 16 | + host: process.env.MYSQL_HOST || config.host || 'localhost', | |
| 17 | + port: process.env.MYSQL_PORT || config.port || 3306, | |
| 18 | + database: 'myapp_test', | |
| 19 | + username: process.env.MYSQL_USER || config.username, | |
| 20 | + password: process.env.MYSQL_PASSWORD || config.password, | |
| 21 | + createDatabase: true, | |
| 22 | + }; | |
| 23 | + | |
| 24 | + if (options) { | |
| 25 | + for (var el in options) { | |
| 26 | + dbConf[el] = options[el]; | |
| 27 | + } | |
| 28 | + } | |
| 29 | + return dbConf; | |
| 30 | +}; | |
| 31 | + | |
| 32 | +global.getDataSource = global.getSchema = function(options) { | |
| 33 | + var db = new DataSource(require('../'), getConfig(options)); | |
| 34 | + return db; | |
| 35 | +}; | |
| 36 | + | |
| 37 | +global.connectorCapabilities = { | |
| 38 | + ilike: false, | |
| 39 | + nilike: false, | |
| 40 | +}; | |
| 41 | + | |
| 42 | +global.sinon = require('sinon'); | ... | ... |
| 1 | +++ a/test/migration.test.js | |
| ... | ... | @@ -0,0 +1,559 @@ |
| 1 | +// Copyright IBM Corp. 2013,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +var assert = require('assert'); | |
| 8 | +var async = require('async'); | |
| 9 | +var platform = require('./helpers/platform'); | |
| 10 | +var should = require('./init'); | |
| 11 | +var Schema = require('loopback-datasource-juggler').Schema; | |
| 12 | + | |
| 13 | +var db, UserData, StringData, NumberData, DateData; | |
| 14 | +var mysqlVersion; | |
| 15 | + | |
| 16 | +describe('migrations', function() { | |
| 17 | + before(setup); | |
| 18 | + | |
| 19 | + it('should run migration', function(done) { | |
| 20 | + db.automigrate(function() { | |
| 21 | + done(); | |
| 22 | + }); | |
| 23 | + }); | |
| 24 | + | |
| 25 | + it('UserData should have correct columns', function(done) { | |
| 26 | + getFields('UserData', function(err, fields) { | |
| 27 | + if (!fields) return done(); | |
| 28 | + fields.should.be.eql({ | |
| 29 | + id: { | |
| 30 | + Field: 'id', | |
| 31 | + Type: 'int(11)', | |
| 32 | + Null: 'NO', | |
| 33 | + Key: 'PRI', | |
| 34 | + Default: null, | |
| 35 | + Extra: 'auto_increment'}, | |
| 36 | + email: { | |
| 37 | + Field: 'email', | |
| 38 | + Type: 'varchar(255)', | |
| 39 | + Null: 'NO', | |
| 40 | + Key: 'MUL', | |
| 41 | + Default: null, | |
| 42 | + Extra: ''}, | |
| 43 | + name: { | |
| 44 | + Field: 'name', | |
| 45 | + Type: 'varchar(512)', | |
| 46 | + Null: 'YES', | |
| 47 | + Key: '', | |
| 48 | + Default: null, | |
| 49 | + Extra: ''}, | |
| 50 | + bio: { | |
| 51 | + Field: 'bio', | |
| 52 | + Type: 'text', | |
| 53 | + Null: 'YES', | |
| 54 | + Key: '', | |
| 55 | + Default: null, | |
| 56 | + Extra: ''}, | |
| 57 | + birthDate: { | |
| 58 | + Field: 'birthDate', | |
| 59 | + Type: 'datetime', | |
| 60 | + Null: 'YES', | |
| 61 | + Key: '', | |
| 62 | + Default: null, | |
| 63 | + Extra: ''}, | |
| 64 | + pendingPeriod: { | |
| 65 | + Field: 'pendingPeriod', | |
| 66 | + Type: 'int(11)', | |
| 67 | + Null: 'YES', | |
| 68 | + Key: '', | |
| 69 | + Default: null, | |
| 70 | + Extra: ''}, | |
| 71 | + createdByAdmin: { | |
| 72 | + Field: 'createdByAdmin', | |
| 73 | + Type: 'tinyint(1)', | |
| 74 | + Null: 'YES', | |
| 75 | + Key: '', | |
| 76 | + Default: null, | |
| 77 | + Extra: ''}, | |
| 78 | + }); | |
| 79 | + done(); | |
| 80 | + }); | |
| 81 | + }); | |
| 82 | + | |
| 83 | + it('UserData should have correct indexes', function(done) { | |
| 84 | + // Note: getIndexes truncates multi-key indexes to the first member. | |
| 85 | + // Hence index1 is correct. | |
| 86 | + getIndexes('UserData', function(err, fields) { | |
| 87 | + if (!fields) return done(); | |
| 88 | + fields.should.match({ | |
| 89 | + PRIMARY: { | |
| 90 | + Table: /UserData/i, | |
| 91 | + Non_unique: 0, | |
| 92 | + Key_name: 'PRIMARY', | |
| 93 | + Seq_in_index: 1, | |
| 94 | + Column_name: 'id', | |
| 95 | + Collation: 'A', | |
| 96 | + // XXX: this actually has more to do with whether the table existed or not and | |
| 97 | + // what kind of data is in it that MySQL has analyzed: | |
| 98 | + // https://dev.mysql.com/doc/refman/5.5/en/show-index.html | |
| 99 | + // Cardinality: /^5\.[567]/.test(mysqlVersion) ? 0 : null, | |
| 100 | + Sub_part: null, | |
| 101 | + Packed: null, | |
| 102 | + Null: '', | |
| 103 | + Index_type: 'BTREE', | |
| 104 | + Comment: ''}, | |
| 105 | + email: { | |
| 106 | + Table: /UserData/i, | |
| 107 | + Non_unique: 1, | |
| 108 | + Key_name: 'email', | |
| 109 | + Seq_in_index: 1, | |
| 110 | + Column_name: 'email', | |
| 111 | + Collation: 'A', | |
| 112 | + // XXX: this actually has more to do with whether the table existed or not and | |
| 113 | + // what kind of data is in it that MySQL has analyzed: | |
| 114 | + // https://dev.mysql.com/doc/refman/5.5/en/show-index.html | |
| 115 | + // Cardinality: /^5\.[567]/.test(mysqlVersion) ? 0 : null, | |
| 116 | + Sub_part: null, | |
| 117 | + Packed: null, | |
| 118 | + Null: '', | |
| 119 | + Index_type: 'BTREE', | |
| 120 | + Comment: ''}, | |
| 121 | + index0: { | |
| 122 | + Table: /UserData/i, | |
| 123 | + Non_unique: 1, | |
| 124 | + Key_name: 'index0', | |
| 125 | + Seq_in_index: 1, | |
| 126 | + Column_name: 'email', | |
| 127 | + Collation: 'A', | |
| 128 | + // XXX: this actually has more to do with whether the table existed or not and | |
| 129 | + // what kind of data is in it that MySQL has analyzed: | |
| 130 | + // https://dev.mysql.com/doc/refman/5.5/en/show-index.html | |
| 131 | + // Cardinality: /^5\.[567]/.test(mysqlVersion) ? 0 : null, | |
| 132 | + Sub_part: null, | |
| 133 | + Packed: null, | |
| 134 | + Null: '', | |
| 135 | + Index_type: 'BTREE', | |
| 136 | + Comment: ''}, | |
| 137 | + }); | |
| 138 | + done(); | |
| 139 | + }); | |
| 140 | + }); | |
| 141 | + | |
| 142 | + it('StringData should have correct columns', function(done) { | |
| 143 | + getFields('StringData', function(err, fields) { | |
| 144 | + fields.should.be.eql({ | |
| 145 | + idString: {Field: 'idString', | |
| 146 | + Type: 'varchar(255)', | |
| 147 | + Null: 'NO', | |
| 148 | + Key: 'PRI', | |
| 149 | + Default: null, | |
| 150 | + Extra: ''}, | |
| 151 | + smallString: {Field: 'smallString', | |
| 152 | + Type: 'char(127)', | |
| 153 | + Null: 'NO', | |
| 154 | + Key: 'MUL', | |
| 155 | + Default: null, | |
| 156 | + Extra: ''}, | |
| 157 | + mediumString: {Field: 'mediumString', | |
| 158 | + Type: 'varchar(255)', | |
| 159 | + Null: 'NO', | |
| 160 | + Key: '', | |
| 161 | + Default: null, | |
| 162 | + Extra: ''}, | |
| 163 | + tinyText: {Field: 'tinyText', | |
| 164 | + Type: 'tinytext', | |
| 165 | + Null: 'YES', | |
| 166 | + Key: '', | |
| 167 | + Default: null, | |
| 168 | + Extra: ''}, | |
| 169 | + giantJSON: {Field: 'giantJSON', | |
| 170 | + Type: 'longtext', | |
| 171 | + Null: 'YES', | |
| 172 | + Key: '', | |
| 173 | + Default: null, | |
| 174 | + Extra: ''}, | |
| 175 | + text: {Field: 'text', | |
| 176 | + Type: 'varchar(1024)', | |
| 177 | + Null: 'YES', | |
| 178 | + Key: '', | |
| 179 | + Default: null, | |
| 180 | + Extra: ''}, | |
| 181 | + }); | |
| 182 | + done(); | |
| 183 | + }); | |
| 184 | + }); | |
| 185 | + | |
| 186 | + it('NumberData should have correct columns', function(done) { | |
| 187 | + getFields('NumberData', function(err, fields) { | |
| 188 | + fields.should.be.eql({ | |
| 189 | + id: {Field: 'id', | |
| 190 | + Type: 'int(11)', | |
| 191 | + Null: 'NO', | |
| 192 | + Key: 'PRI', | |
| 193 | + Default: null, | |
| 194 | + Extra: 'auto_increment'}, | |
| 195 | + number: {Field: 'number', | |
| 196 | + Type: 'decimal(10,3) unsigned', | |
| 197 | + Null: 'NO', | |
| 198 | + Key: 'MUL', | |
| 199 | + Default: null, | |
| 200 | + Extra: ''}, | |
| 201 | + tinyInt: {Field: 'tinyInt', | |
| 202 | + Type: 'tinyint(2)', | |
| 203 | + Null: 'YES', | |
| 204 | + Key: '', | |
| 205 | + Default: null, | |
| 206 | + Extra: ''}, | |
| 207 | + mediumInt: {Field: 'mediumInt', | |
| 208 | + Type: 'mediumint(8) unsigned', | |
| 209 | + Null: 'NO', | |
| 210 | + Key: '', | |
| 211 | + Default: null, | |
| 212 | + Extra: ''}, | |
| 213 | + floater: {Field: 'floater', | |
| 214 | + Type: 'double(14,6)', | |
| 215 | + Null: 'YES', | |
| 216 | + Key: '', | |
| 217 | + Default: null, | |
| 218 | + Extra: ''}, | |
| 219 | + }); | |
| 220 | + done(); | |
| 221 | + }); | |
| 222 | + }); | |
| 223 | + | |
| 224 | + it('DateData should have correct columns', function(done) { | |
| 225 | + getFields('DateData', function(err, fields) { | |
| 226 | + fields.should.be.eql({ | |
| 227 | + id: {Field: 'id', | |
| 228 | + Type: 'int(11)', | |
| 229 | + Null: 'NO', | |
| 230 | + Key: 'PRI', | |
| 231 | + Default: null, | |
| 232 | + Extra: 'auto_increment'}, | |
| 233 | + dateTime: {Field: 'dateTime', | |
| 234 | + Type: 'datetime', | |
| 235 | + Null: 'YES', | |
| 236 | + Key: '', | |
| 237 | + Default: null, | |
| 238 | + Extra: ''}, | |
| 239 | + timestamp: {Field: 'timestamp', | |
| 240 | + Type: 'timestamp', | |
| 241 | + Null: 'YES', | |
| 242 | + Key: '', | |
| 243 | + Default: null, | |
| 244 | + Extra: ''}, | |
| 245 | + }); | |
| 246 | + done(); | |
| 247 | + }); | |
| 248 | + }); | |
| 249 | + | |
| 250 | + it('should autoupdate', function(done) { | |
| 251 | + // With an install of MYSQL5.7 on windows, these queries `randomly` fail and raise errors | |
| 252 | + // especially with decimals, number and Date format. | |
| 253 | + if (platform.isWindows) { | |
| 254 | + return done(); | |
| 255 | + } | |
| 256 | + var userExists = function(cb) { | |
| 257 | + query('SELECT * FROM UserData', function(err, res) { | |
| 258 | + cb(!err && res[0].email == 'test@example.com'); | |
| 259 | + }); | |
| 260 | + }; | |
| 261 | + | |
| 262 | + UserData.create({email: 'test@example.com'}, function(err, user) { | |
| 263 | + assert.ok(!err, 'Could not create user: ' + err); | |
| 264 | + userExists(function(yep) { | |
| 265 | + assert.ok(yep, 'User does not exist'); | |
| 266 | + }); | |
| 267 | + UserData.defineProperty('email', {type: String}); | |
| 268 | + UserData.defineProperty('name', {type: String, | |
| 269 | + dataType: 'char', limit: 50}); | |
| 270 | + UserData.defineProperty('newProperty', {type: Number, unsigned: true, | |
| 271 | + dataType: 'bigInt'}); | |
| 272 | + // UserData.defineProperty('pendingPeriod', false); | |
| 273 | + // This will not work as expected. | |
| 274 | + db.autoupdate(function(err) { | |
| 275 | + getFields('UserData', function(err, fields) { | |
| 276 | + // change nullable for email | |
| 277 | + assert.equal(fields.email.Null, 'YES', 'Email does not allow null'); | |
| 278 | + // change type of name | |
| 279 | + assert.equal(fields.name.Type, 'char(50)', 'Name is not char(50)'); | |
| 280 | + // add new column | |
| 281 | + assert.ok(fields.newProperty, 'New column was not added'); | |
| 282 | + if (fields.newProperty) { | |
| 283 | + assert.equal(fields.newProperty.Type, 'bigint(20) unsigned', | |
| 284 | + 'New column type is not bigint(20) unsigned'); | |
| 285 | + } | |
| 286 | + // drop column - will not happen. | |
| 287 | + // assert.ok(!fields.pendingPeriod, | |
| 288 | + // 'Did not drop column pendingPeriod'); | |
| 289 | + // user still exists | |
| 290 | + userExists(function(yep) { | |
| 291 | + assert.ok(yep, 'User does not exist'); | |
| 292 | + done(); | |
| 293 | + }); | |
| 294 | + }); | |
| 295 | + }); | |
| 296 | + }); | |
| 297 | + }); | |
| 298 | + | |
| 299 | + it('should check actuality of dataSource', function(done) { | |
| 300 | + // With an install of MYSQL5.7 on windows, these queries `randomly` fail and raise errors | |
| 301 | + // with date, number and decimal format | |
| 302 | + if (platform.isWindows) { | |
| 303 | + return done(); | |
| 304 | + } | |
| 305 | + // 'drop column' | |
| 306 | + UserData.dataSource.isActual(function(err, ok) { | |
| 307 | + assert.ok(ok, 'dataSource is not actual (should be)'); | |
| 308 | + UserData.defineProperty('essay', {type: Schema.Text}); | |
| 309 | + // UserData.defineProperty('email', false); Can't undefine currently. | |
| 310 | + UserData.dataSource.isActual(function(err, ok) { | |
| 311 | + assert.ok(!ok, 'dataSource is actual (shouldn\t be)'); | |
| 312 | + done(); | |
| 313 | + }); | |
| 314 | + }); | |
| 315 | + }); | |
| 316 | + | |
| 317 | + // In MySQL 5.6/5.7 Out of range values are rejected. | |
| 318 | + // Reference: http://dev.mysql.com/doc/refman/5.7/en/integer-types.html | |
| 319 | + it('allows numbers with decimals', function(done) { | |
| 320 | + NumberData.create( | |
| 321 | + {number: 1.1234567, tinyInt: 127, mediumInt: 16777215, floater: 12345678.123456}, | |
| 322 | + function(err, obj) { | |
| 323 | + if (err) return (err); | |
| 324 | + NumberData.findById(obj.id, function(err, found) { | |
| 325 | + assert.equal(found.number, 1.123); | |
| 326 | + assert.equal(found.tinyInt, 127); | |
| 327 | + assert.equal(found.mediumInt, 16777215); | |
| 328 | + assert.equal(found.floater, 12345678.123456); | |
| 329 | + done(); | |
| 330 | + }); | |
| 331 | + }); | |
| 332 | + }); | |
| 333 | + | |
| 334 | + // Reference: http://dev.mysql.com/doc/refman/5.7/en/out-of-range-and-overflow.html | |
| 335 | + it('rejects out-of-range and overflow values', function(done) { | |
| 336 | + async.series([ | |
| 337 | + function(next) { | |
| 338 | + NumberData.create({number: 1.1234567, tinyInt: 128, mediumInt: 16777215}, function(err, obj) { | |
| 339 | + assert(err); | |
| 340 | + assert.equal(err.code, 'ER_WARN_DATA_OUT_OF_RANGE'); | |
| 341 | + next(); | |
| 342 | + }); | |
| 343 | + }, function(next) { | |
| 344 | + NumberData.create({number: 1.1234567, mediumInt: 16777215 + 1}, function(err, obj) { | |
| 345 | + assert(err); | |
| 346 | + assert.equal(err.code, 'ER_WARN_DATA_OUT_OF_RANGE'); | |
| 347 | + next(); | |
| 348 | + }); | |
| 349 | + }, function(next) { | |
| 350 | + //Minimum value for unsigned mediumInt is 0 | |
| 351 | + NumberData.create({number: 1.1234567, mediumInt: -8388608}, function(err, obj) { | |
| 352 | + assert(err); | |
| 353 | + assert.equal(err.code, 'ER_WARN_DATA_OUT_OF_RANGE'); | |
| 354 | + next(); | |
| 355 | + }); | |
| 356 | + }, function(next) { | |
| 357 | + NumberData.create({number: 1.1234567, tinyInt: -129, mediumInt: 0}, function(err, obj) { | |
| 358 | + assert(err); | |
| 359 | + assert.equal(err.code, 'ER_WARN_DATA_OUT_OF_RANGE'); | |
| 360 | + next(); | |
| 361 | + }); | |
| 362 | + }, | |
| 363 | + ], done); | |
| 364 | + }); | |
| 365 | + | |
| 366 | + it('should allow both kinds of date columns', function(done) { | |
| 367 | + DateData.create({ | |
| 368 | + dateTime: new Date('Aug 9 1996 07:47:33 GMT'), | |
| 369 | + timestamp: new Date('Sep 22 2007 17:12:22 GMT'), | |
| 370 | + }, function(err, obj) { | |
| 371 | + assert.ok(!err); | |
| 372 | + assert.ok(obj); | |
| 373 | + DateData.findById(obj.id, function(err, found) { | |
| 374 | + assert.equal(found.dateTime.toGMTString(), | |
| 375 | + 'Fri, 09 Aug 1996 07:47:33 GMT'); | |
| 376 | + assert.equal(found.timestamp.toGMTString(), | |
| 377 | + 'Sat, 22 Sep 2007 17:12:22 GMT'); | |
| 378 | + done(); | |
| 379 | + }); | |
| 380 | + }); | |
| 381 | + }); | |
| 382 | + | |
| 383 | + // InMySQL5.7, DATETIME supported range is '1000-01-01 00:00:00' to '9999-12-31 23:59:59'. | |
| 384 | + // TIMESTAMP has a range of '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC | |
| 385 | + // Reference: http://dev.mysql.com/doc/refman/5.7/en/datetime.html | |
| 386 | + // Out of range values are set to null in windows but rejected elsewhere | |
| 387 | + // the next example is designed for windows while the following 2 are for other platforms | |
| 388 | + it('should map zero dateTime into null', function(done) { | |
| 389 | + if (!platform.isWindows) { | |
| 390 | + return done(); | |
| 391 | + } | |
| 392 | + | |
| 393 | + query('INSERT INTO `DateData` ' + | |
| 394 | + '(`dateTime`, `timestamp`) ' + | |
| 395 | + 'VALUES("0000-00-00 00:00:00", "0000-00-00 00:00:00") ', | |
| 396 | + function(err, ret) { | |
| 397 | + should.not.exists(err); | |
| 398 | + DateData.findById(ret.insertId, function(err, dateData) { | |
| 399 | + should(dateData.dateTime) | |
| 400 | + .be.null(); | |
| 401 | + should(dateData.timestamp) | |
| 402 | + .be.null(); | |
| 403 | + done(); | |
| 404 | + }); | |
| 405 | + }); | |
| 406 | + }); | |
| 407 | + | |
| 408 | + it('rejects out of range datetime', function(done) { | |
| 409 | + if (platform.isWindows) { | |
| 410 | + return done(); | |
| 411 | + } | |
| 412 | + | |
| 413 | + query('INSERT INTO `DateData` ' + | |
| 414 | + '(`dateTime`, `timestamp`) ' + | |
| 415 | + 'VALUES("0000-00-00 00:00:00", "0000-00-00 00:00:00") ', function(err) { | |
| 416 | + var errMsg = 'ER_TRUNCATED_WRONG_VALUE: Incorrect datetime value: ' + | |
| 417 | + '\'0000-00-00 00:00:00\' for column \'dateTime\' at row 1'; | |
| 418 | + assert(err); | |
| 419 | + assert.equal(err.message, errMsg); | |
| 420 | + done(); | |
| 421 | + }); | |
| 422 | + }); | |
| 423 | + | |
| 424 | + it('rejects out of range timestamp', function(done) { | |
| 425 | + if (platform.isWindows) { | |
| 426 | + return done(); | |
| 427 | + } | |
| 428 | + | |
| 429 | + query('INSERT INTO `DateData` ' + | |
| 430 | + '(`dateTime`, `timestamp`) ' + | |
| 431 | + 'VALUES("1000-01-01 00:00:00", "0000-00-00 00:00:00") ', function(err) { | |
| 432 | + var errMsg = 'ER_TRUNCATED_WRONG_VALUE: Incorrect datetime value: ' + | |
| 433 | + '\'0000-00-00 00:00:00\' for column \'timestamp\' at row 1'; | |
| 434 | + assert(err); | |
| 435 | + assert.equal(err.message, errMsg); | |
| 436 | + done(); | |
| 437 | + }); | |
| 438 | + }); | |
| 439 | + | |
| 440 | + it('should report errors for automigrate', function() { | |
| 441 | + db.automigrate('XYZ', function(err) { | |
| 442 | + assert(err); | |
| 443 | + }); | |
| 444 | + }); | |
| 445 | + | |
| 446 | + it('should report errors for autoupdate', function() { | |
| 447 | + db.autoupdate('XYZ', function(err) { | |
| 448 | + assert(err); | |
| 449 | + }); | |
| 450 | + }); | |
| 451 | + | |
| 452 | + it('should disconnect when done', function(done) { | |
| 453 | + db.disconnect(); | |
| 454 | + done(); | |
| 455 | + }); | |
| 456 | +}); | |
| 457 | + | |
| 458 | +function setup(done) { | |
| 459 | + require('./init.js'); | |
| 460 | + | |
| 461 | + db = getSchema(); | |
| 462 | + | |
| 463 | + UserData = db.define('UserData', { | |
| 464 | + email: {type: String, null: false, index: true}, | |
| 465 | + name: String, | |
| 466 | + bio: Schema.Text, | |
| 467 | + birthDate: Date, | |
| 468 | + pendingPeriod: Number, | |
| 469 | + createdByAdmin: Boolean, | |
| 470 | + }, {indexes: { | |
| 471 | + index0: { | |
| 472 | + columns: 'email, createdByAdmin', | |
| 473 | + }, | |
| 474 | + }, | |
| 475 | + }); | |
| 476 | + | |
| 477 | + StringData = db.define('StringData', { | |
| 478 | + idString: {type: String, id: true}, | |
| 479 | + smallString: {type: String, null: false, index: true, | |
| 480 | + dataType: 'char', limit: 127}, | |
| 481 | + mediumString: {type: String, null: false, dataType: 'varchar', limit: 255}, | |
| 482 | + tinyText: {type: String, dataType: 'tinyText'}, | |
| 483 | + giantJSON: {type: Schema.JSON, dataType: 'longText'}, | |
| 484 | + text: {type: Schema.Text, dataType: 'varchar', limit: 1024}, | |
| 485 | + }); | |
| 486 | + | |
| 487 | + NumberData = db.define('NumberData', { | |
| 488 | + number: {type: Number, null: false, index: true, unsigned: true, | |
| 489 | + dataType: 'decimal', precision: 10, scale: 3}, | |
| 490 | + tinyInt: {type: Number, dataType: 'tinyInt', display: 2}, | |
| 491 | + mediumInt: {type: Number, dataType: 'mediumInt', unsigned: true, | |
| 492 | + required: true}, | |
| 493 | + floater: {type: Number, dataType: 'double', precision: 14, scale: 6}, | |
| 494 | + }); | |
| 495 | + | |
| 496 | + DateData = db.define('DateData', { | |
| 497 | + dateTime: {type: Date, dataType: 'datetime'}, | |
| 498 | + timestamp: {type: Date, dataType: 'timestamp'}, | |
| 499 | + }); | |
| 500 | + | |
| 501 | + query('SELECT VERSION()', function(err, res) { | |
| 502 | + mysqlVersion = res && res[0] && res[0]['VERSION()']; | |
| 503 | + blankDatabase(db, done); | |
| 504 | + }); | |
| 505 | +} | |
| 506 | + | |
| 507 | +var query = function(sql, cb) { | |
| 508 | + db.adapter.execute(sql, cb); | |
| 509 | +}; | |
| 510 | + | |
| 511 | +var blankDatabase = function(db, cb) { | |
| 512 | + var dbn = db.settings.database; | |
| 513 | + var cs = db.settings.charset; | |
| 514 | + var co = db.settings.collation; | |
| 515 | + query('DROP DATABASE IF EXISTS ' + dbn, function(err) { | |
| 516 | + var q = 'CREATE DATABASE ' + dbn; | |
| 517 | + if (cs) { | |
| 518 | + q += ' CHARACTER SET ' + cs; | |
| 519 | + } | |
| 520 | + if (co) { | |
| 521 | + q += ' COLLATE ' + co; | |
| 522 | + } | |
| 523 | + query(q, function(err) { | |
| 524 | + query('USE ' + dbn, cb); | |
| 525 | + }); | |
| 526 | + }); | |
| 527 | +}; | |
| 528 | + | |
| 529 | +var getFields = function(model, cb) { | |
| 530 | + query('SHOW FIELDS FROM ' + model, function(err, res) { | |
| 531 | + if (err) { | |
| 532 | + cb(err); | |
| 533 | + } else { | |
| 534 | + var fields = {}; | |
| 535 | + res.forEach(function(field) { | |
| 536 | + fields[field.Field] = field; | |
| 537 | + }); | |
| 538 | + cb(err, fields); | |
| 539 | + } | |
| 540 | + }); | |
| 541 | +}; | |
| 542 | + | |
| 543 | +var getIndexes = function(model, cb) { | |
| 544 | + query('SHOW INDEXES FROM ' + model, function(err, res) { | |
| 545 | + if (err) { | |
| 546 | + console.log(err); | |
| 547 | + cb(err); | |
| 548 | + } else { | |
| 549 | + var indexes = {}; | |
| 550 | + // Note: this will only show the first key of compound keys | |
| 551 | + res.forEach(function(index) { | |
| 552 | + if (parseInt(index.Seq_in_index, 10) == 1) { | |
| 553 | + indexes[index.Key_name] = index; | |
| 554 | + } | |
| 555 | + }); | |
| 556 | + cb(err, indexes); | |
| 557 | + } | |
| 558 | + }); | |
| 559 | +}; | ... | ... |
| 1 | +++ a/test/mysql.autoupdate.test.js | |
| ... | ... | @@ -0,0 +1,542 @@ |
| 1 | +// Copyright IBM Corp. 2014,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +var assert = require('assert'); | |
| 8 | +require('./init'); | |
| 9 | +var ds; | |
| 10 | + | |
| 11 | +before(function() { | |
| 12 | + ds = getDataSource(); | |
| 13 | +}); | |
| 14 | + | |
| 15 | +describe('MySQL connector', function() { | |
| 16 | + before(function() { | |
| 17 | + setupAltColNameData(); | |
| 18 | + }); | |
| 19 | + | |
| 20 | + it('should auto migrate/update tables', function(done) { | |
| 21 | + var schema_v1 = | |
| 22 | + { | |
| 23 | + 'name': 'CustomerTest', | |
| 24 | + 'options': { | |
| 25 | + 'idInjection': false, | |
| 26 | + 'mysql': { | |
| 27 | + 'schema': 'myapp_test', | |
| 28 | + 'table': 'customer_test', | |
| 29 | + }, | |
| 30 | + 'indexes': { | |
| 31 | + 'name_index': { | |
| 32 | + 'keys': { | |
| 33 | + 'name': 1, | |
| 34 | + }, | |
| 35 | + 'options': { | |
| 36 | + 'unique': true, | |
| 37 | + }, | |
| 38 | + }, | |
| 39 | + }, | |
| 40 | + }, | |
| 41 | + 'properties': { | |
| 42 | + 'id': { | |
| 43 | + 'type': 'String', | |
| 44 | + 'length': 20, | |
| 45 | + 'id': 1, | |
| 46 | + }, | |
| 47 | + 'name': { | |
| 48 | + 'type': 'String', | |
| 49 | + 'required': false, | |
| 50 | + 'length': 40, | |
| 51 | + }, | |
| 52 | + 'email': { | |
| 53 | + 'type': 'String', | |
| 54 | + 'required': true, | |
| 55 | + 'length': 40, | |
| 56 | + }, | |
| 57 | + 'age': { | |
| 58 | + 'type': 'Number', | |
| 59 | + 'required': false, | |
| 60 | + }, | |
| 61 | + 'discount': { | |
| 62 | + 'type': 'Number', | |
| 63 | + 'required': false, | |
| 64 | + 'dataType': 'decimal', | |
| 65 | + 'precision': 10, | |
| 66 | + 'scale': 2, | |
| 67 | + 'mysql': { | |
| 68 | + 'columnName': 'customer_discount', | |
| 69 | + 'dataType': 'decimal', | |
| 70 | + 'dataPrecision': 10, | |
| 71 | + 'dataScale': 2, | |
| 72 | + }, | |
| 73 | + }, | |
| 74 | + }, | |
| 75 | + }; | |
| 76 | + | |
| 77 | + var schema_v2 = | |
| 78 | + { | |
| 79 | + 'name': 'CustomerTest', | |
| 80 | + 'options': { | |
| 81 | + 'idInjection': false, | |
| 82 | + 'mysql': { | |
| 83 | + 'schema': 'myapp_test', | |
| 84 | + 'table': 'customer_test', | |
| 85 | + }, | |
| 86 | + 'indexes': { | |
| 87 | + 'updated_name_index': { | |
| 88 | + 'keys': { | |
| 89 | + 'firstName': 1, | |
| 90 | + 'lastName': -1, | |
| 91 | + }, | |
| 92 | + 'options': { | |
| 93 | + 'unique': true, | |
| 94 | + }, | |
| 95 | + }, | |
| 96 | + }, | |
| 97 | + }, | |
| 98 | + 'properties': { | |
| 99 | + 'id': { | |
| 100 | + 'type': 'String', | |
| 101 | + 'length': 20, | |
| 102 | + 'id': 1, | |
| 103 | + }, | |
| 104 | + 'email': { | |
| 105 | + 'type': 'String', | |
| 106 | + 'required': false, | |
| 107 | + 'length': 60, | |
| 108 | + 'mysql': { | |
| 109 | + 'columnName': 'email', | |
| 110 | + 'dataType': 'varchar', | |
| 111 | + 'dataLength': 60, | |
| 112 | + 'nullable': 'YES', | |
| 113 | + }, | |
| 114 | + }, | |
| 115 | + 'firstName': { | |
| 116 | + 'type': 'String', | |
| 117 | + 'required': false, | |
| 118 | + 'length': 40, | |
| 119 | + }, | |
| 120 | + 'lastName': { | |
| 121 | + 'type': 'String', | |
| 122 | + 'required': false, | |
| 123 | + 'length': 40, | |
| 124 | + }, | |
| 125 | + // remove age | |
| 126 | + // change data type details with column name | |
| 127 | + 'discount': { | |
| 128 | + 'type': 'Number', | |
| 129 | + 'required': false, | |
| 130 | + 'dataType': 'decimal', | |
| 131 | + 'precision': 12, | |
| 132 | + 'scale': 5, | |
| 133 | + 'mysql': { | |
| 134 | + 'columnName': 'customer_discount', | |
| 135 | + 'dataType': 'decimal', | |
| 136 | + 'dataPrecision': 12, | |
| 137 | + 'dataScale': 5, | |
| 138 | + }, | |
| 139 | + }, | |
| 140 | + // add new column with column name | |
| 141 | + 'address': { | |
| 142 | + 'type': 'String', | |
| 143 | + 'required': false, | |
| 144 | + 'length': 10, | |
| 145 | + 'mysql': { | |
| 146 | + 'columnName': 'customer_address', | |
| 147 | + 'dataType': 'varchar', | |
| 148 | + 'length': 10, | |
| 149 | + }, | |
| 150 | + }, | |
| 151 | + // add new column with index & column name | |
| 152 | + 'code': { | |
| 153 | + 'type': 'String', | |
| 154 | + 'required': true, | |
| 155 | + 'length': 12, | |
| 156 | + 'index': { | |
| 157 | + unique: true, | |
| 158 | + }, | |
| 159 | + 'mysql': { | |
| 160 | + 'columnName': 'customer_code', | |
| 161 | + 'dataType': 'varchar', | |
| 162 | + 'length': 12, | |
| 163 | + }, | |
| 164 | + }, | |
| 165 | + }, | |
| 166 | + }; | |
| 167 | + | |
| 168 | + ds.createModel(schema_v1.name, schema_v1.properties, schema_v1.options); | |
| 169 | + | |
| 170 | + ds.automigrate(function() { | |
| 171 | + ds.discoverModelProperties('customer_test', function(err, props) { | |
| 172 | + assert.equal(props.length, 5); | |
| 173 | + var names = props.map(function(p) { | |
| 174 | + return p.columnName; | |
| 175 | + }); | |
| 176 | + assert.equal(props[0].nullable, 'N'); | |
| 177 | + assert.equal(props[1].nullable, 'Y'); | |
| 178 | + assert.equal(props[2].nullable, 'N'); | |
| 179 | + assert.equal(props[3].nullable, 'Y'); | |
| 180 | + assert.equal(names[0], 'id'); | |
| 181 | + assert.equal(names[1], 'name'); | |
| 182 | + assert.equal(names[2], 'email'); | |
| 183 | + assert.equal(names[3], 'age'); | |
| 184 | + assert.equal(names[4], 'customer_discount'); | |
| 185 | + | |
| 186 | + ds.connector.execute('SHOW INDEXES FROM customer_test', function(err, indexes) { | |
| 187 | + if (err) return done (err); | |
| 188 | + assert(indexes); | |
| 189 | + assert(indexes.length.should.be.above(1)); | |
| 190 | + assert.equal(indexes[1].Key_name, 'name_index'); | |
| 191 | + assert.equal(indexes[1].Non_unique, 0); | |
| 192 | + ds.createModel(schema_v2.name, schema_v2.properties, schema_v2.options); | |
| 193 | + ds.autoupdate(function(err, result) { | |
| 194 | + if (err) return done (err); | |
| 195 | + ds.discoverModelProperties('customer_test', function(err, props) { | |
| 196 | + if (err) return done (err); | |
| 197 | + assert.equal(props.length, 7); | |
| 198 | + var names = props.map(function(p) { | |
| 199 | + return p.columnName; | |
| 200 | + }); | |
| 201 | + assert.equal(names[0], 'id'); | |
| 202 | + assert.equal(names[1], 'email'); | |
| 203 | + assert.equal(names[2], 'customer_discount'); | |
| 204 | + assert.equal(names[3], 'firstName'); | |
| 205 | + assert.equal(names[4], 'lastName'); | |
| 206 | + assert.equal(names[5], 'customer_address'); | |
| 207 | + assert.equal(names[6], 'customer_code'); | |
| 208 | + ds.connector.execute('SHOW INDEXES FROM customer_test', function(err, updatedindexes) { | |
| 209 | + if (err) return done (err); | |
| 210 | + assert(updatedindexes); | |
| 211 | + assert(updatedindexes.length.should.be.above(3)); | |
| 212 | + assert.equal(updatedindexes[1].Key_name, 'customer_code'); | |
| 213 | + assert.equal(updatedindexes[2].Key_name, 'updated_name_index'); | |
| 214 | + assert.equal(updatedindexes[3].Key_name, 'updated_name_index'); | |
| 215 | + //Mysql supports only index sorting in ascending; DESC is ignored | |
| 216 | + assert.equal(updatedindexes[1].Collation, 'A'); | |
| 217 | + assert.equal(updatedindexes[2].Collation, 'A'); | |
| 218 | + assert.equal(updatedindexes[3].Collation, 'A'); | |
| 219 | + assert.equal(updatedindexes[1].Non_unique, 0); | |
| 220 | + assert.equal(updatedindexes[2].Non_unique, 0); | |
| 221 | + assert.equal(updatedindexes[3].Non_unique, 0); | |
| 222 | + done(err, result); | |
| 223 | + }); | |
| 224 | + }); | |
| 225 | + }); | |
| 226 | + }); | |
| 227 | + }); | |
| 228 | + }); | |
| 229 | + }); | |
| 230 | + | |
| 231 | + it('should auto migrate/update foreign keys in tables', function(done) { | |
| 232 | + var customer2_schema = | |
| 233 | + { | |
| 234 | + 'name': 'CustomerTest2', | |
| 235 | + 'options': { | |
| 236 | + 'idInjection': false, | |
| 237 | + 'mysql': { | |
| 238 | + 'schema': 'myapp_test', | |
| 239 | + 'table': 'customer_test2', | |
| 240 | + }, | |
| 241 | + }, | |
| 242 | + 'properties': { | |
| 243 | + 'id': { | |
| 244 | + 'type': 'String', | |
| 245 | + 'length': 20, | |
| 246 | + 'id': 1, | |
| 247 | + }, | |
| 248 | + 'name': { | |
| 249 | + 'type': 'String', | |
| 250 | + 'required': false, | |
| 251 | + 'length': 40, | |
| 252 | + }, | |
| 253 | + 'email': { | |
| 254 | + 'type': 'String', | |
| 255 | + 'required': true, | |
| 256 | + 'length': 40, | |
| 257 | + }, | |
| 258 | + 'age': { | |
| 259 | + 'type': 'Number', | |
| 260 | + 'required': false, | |
| 261 | + }, | |
| 262 | + }, | |
| 263 | + }; | |
| 264 | + var customer3_schema = | |
| 265 | + { | |
| 266 | + 'name': 'CustomerTest3', | |
| 267 | + 'options': { | |
| 268 | + 'idInjection': false, | |
| 269 | + 'mysql': { | |
| 270 | + 'schema': 'myapp_test', | |
| 271 | + 'table': 'customer_test3', | |
| 272 | + }, | |
| 273 | + }, | |
| 274 | + 'properties': { | |
| 275 | + 'id': { | |
| 276 | + 'type': 'String', | |
| 277 | + 'length': 20, | |
| 278 | + 'id': 1, | |
| 279 | + }, | |
| 280 | + 'name': { | |
| 281 | + 'type': 'String', | |
| 282 | + 'required': false, | |
| 283 | + 'length': 40, | |
| 284 | + }, | |
| 285 | + 'email': { | |
| 286 | + 'type': 'String', | |
| 287 | + 'required': true, | |
| 288 | + 'length': 40, | |
| 289 | + }, | |
| 290 | + 'age': { | |
| 291 | + 'type': 'Number', | |
| 292 | + 'required': false, | |
| 293 | + }, | |
| 294 | + }, | |
| 295 | + }; | |
| 296 | + | |
| 297 | + var schema_v1 = | |
| 298 | + { | |
| 299 | + 'name': 'OrderTest', | |
| 300 | + 'options': { | |
| 301 | + 'idInjection': false, | |
| 302 | + 'mysql': { | |
| 303 | + 'schema': 'myapp_test', | |
| 304 | + 'table': 'order_test', | |
| 305 | + }, | |
| 306 | + 'foreignKeys': { | |
| 307 | + 'fk_ordertest_customerId': { | |
| 308 | + 'name': 'fk_ordertest_customerId', | |
| 309 | + 'entity': 'CustomerTest3', | |
| 310 | + 'entityKey': 'id', | |
| 311 | + 'foreignKey': 'customerId', | |
| 312 | + }, | |
| 313 | + }, | |
| 314 | + }, | |
| 315 | + 'properties': { | |
| 316 | + 'id': { | |
| 317 | + 'type': 'String', | |
| 318 | + 'length': 20, | |
| 319 | + 'id': 1, | |
| 320 | + }, | |
| 321 | + 'customerId': { | |
| 322 | + 'type': 'String', | |
| 323 | + 'length': 20, | |
| 324 | + 'id': 1, | |
| 325 | + }, | |
| 326 | + 'description': { | |
| 327 | + 'type': 'String', | |
| 328 | + 'required': false, | |
| 329 | + 'length': 40, | |
| 330 | + }, | |
| 331 | + }, | |
| 332 | + }; | |
| 333 | + | |
| 334 | + var schema_v2 = | |
| 335 | + { | |
| 336 | + 'name': 'OrderTest', | |
| 337 | + 'options': { | |
| 338 | + 'idInjection': false, | |
| 339 | + 'mysql': { | |
| 340 | + 'schema': 'myapp_test', | |
| 341 | + 'table': 'order_test', | |
| 342 | + }, | |
| 343 | + 'foreignKeys': { | |
| 344 | + 'fk_ordertest_customerId': { | |
| 345 | + 'name': 'fk_ordertest_customerId', | |
| 346 | + 'entity': 'CustomerTest2', | |
| 347 | + 'entityKey': 'id', | |
| 348 | + 'foreignKey': 'customerId', | |
| 349 | + }, | |
| 350 | + }, | |
| 351 | + }, | |
| 352 | + 'properties': { | |
| 353 | + 'id': { | |
| 354 | + 'type': 'String', | |
| 355 | + 'length': 20, | |
| 356 | + 'id': 1, | |
| 357 | + }, | |
| 358 | + 'customerId': { | |
| 359 | + 'type': 'String', | |
| 360 | + 'length': 20, | |
| 361 | + 'id': 1, | |
| 362 | + }, | |
| 363 | + 'description': { | |
| 364 | + 'type': 'String', | |
| 365 | + 'required': false, | |
| 366 | + 'length': 40, | |
| 367 | + }, | |
| 368 | + }, | |
| 369 | + }; | |
| 370 | + | |
| 371 | + var schema_v3 = | |
| 372 | + { | |
| 373 | + 'name': 'OrderTest', | |
| 374 | + 'options': { | |
| 375 | + 'idInjection': false, | |
| 376 | + 'mysql': { | |
| 377 | + 'schema': 'myapp_test', | |
| 378 | + 'table': 'order_test', | |
| 379 | + }, | |
| 380 | + }, | |
| 381 | + 'properties': { | |
| 382 | + 'id': { | |
| 383 | + 'type': 'String', | |
| 384 | + 'length': 20, | |
| 385 | + 'id': 1, | |
| 386 | + }, | |
| 387 | + 'customerId': { | |
| 388 | + 'type': 'String', | |
| 389 | + 'length': 20, | |
| 390 | + 'id': 1, | |
| 391 | + }, | |
| 392 | + 'description': { | |
| 393 | + 'type': 'String', | |
| 394 | + 'required': false, | |
| 395 | + 'length': 40, | |
| 396 | + }, | |
| 397 | + }, | |
| 398 | + }; | |
| 399 | + | |
| 400 | + var foreignKeySelect = | |
| 401 | + 'SELECT COLUMN_NAME,CONSTRAINT_NAME,REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME ' + | |
| 402 | + 'FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE ' + | |
| 403 | + 'WHERE REFERENCED_TABLE_SCHEMA = "myapp_test" ' + | |
| 404 | + 'AND TABLE_NAME = "order_test"'; | |
| 405 | + | |
| 406 | + ds.createModel(customer2_schema.name, customer2_schema.properties, customer2_schema.options); | |
| 407 | + ds.createModel(customer3_schema.name, customer3_schema.properties, customer3_schema.options); | |
| 408 | + ds.createModel(schema_v1.name, schema_v1.properties, schema_v1.options); | |
| 409 | + | |
| 410 | + //do initial update/creation of table | |
| 411 | + ds.autoupdate(function() { | |
| 412 | + ds.discoverModelProperties('order_test', function(err, props) { | |
| 413 | + //validate that we have the correct number of properties | |
| 414 | + assert.equal(props.length, 3); | |
| 415 | + | |
| 416 | + //get the foreign keys for this table | |
| 417 | + ds.connector.execute(foreignKeySelect, function(err, foreignKeys) { | |
| 418 | + if (err) return done (err); | |
| 419 | + //validate that the foreign key exists and points to the right column | |
| 420 | + assert(foreignKeys); | |
| 421 | + assert(foreignKeys.length.should.be.equal(1)); | |
| 422 | + assert.equal(foreignKeys[0].REFERENCED_TABLE_NAME, 'customer_test3'); | |
| 423 | + assert.equal(foreignKeys[0].COLUMN_NAME, 'customerId'); | |
| 424 | + assert.equal(foreignKeys[0].CONSTRAINT_NAME, 'fk_ordertest_customerId'); | |
| 425 | + assert.equal(foreignKeys[0].REFERENCED_COLUMN_NAME, 'id'); | |
| 426 | + | |
| 427 | + //update our model (move foreign key) and run autoupdate to migrate | |
| 428 | + ds.createModel(schema_v2.name, schema_v2.properties, schema_v2.options); | |
| 429 | + ds.autoupdate(function(err, result) { | |
| 430 | + if (err) return done (err); | |
| 431 | + | |
| 432 | + //get and validate the properties on this model | |
| 433 | + ds.discoverModelProperties('order_test', function(err, props) { | |
| 434 | + if (err) return done (err); | |
| 435 | + | |
| 436 | + assert.equal(props.length, 3); | |
| 437 | + | |
| 438 | + //get the foreign keys that exist after the migration | |
| 439 | + ds.connector.execute(foreignKeySelect, function(err, updatedForeignKeys) { | |
| 440 | + if (err) return done (err); | |
| 441 | + //validate that the foreign keys was moved to the new column | |
| 442 | + assert(updatedForeignKeys); | |
| 443 | + assert(updatedForeignKeys.length.should.be.equal(1)); | |
| 444 | + assert.equal(updatedForeignKeys[0].REFERENCED_TABLE_NAME, 'customer_test2'); | |
| 445 | + assert.equal(updatedForeignKeys[0].COLUMN_NAME, 'customerId'); | |
| 446 | + assert.equal(updatedForeignKeys[0].CONSTRAINT_NAME, 'fk_ordertest_customerId'); | |
| 447 | + assert.equal(updatedForeignKeys[0].REFERENCED_COLUMN_NAME, 'id'); | |
| 448 | + | |
| 449 | + //update model (to drop foreign key) and autoupdate | |
| 450 | + ds.createModel(schema_v3.name, schema_v3.properties, schema_v3.options); | |
| 451 | + ds.autoupdate(function(err, result) { | |
| 452 | + if (err) return done (err); | |
| 453 | + //validate the properties | |
| 454 | + ds.discoverModelProperties('order_test', function(err, props) { | |
| 455 | + if (err) return done (err); | |
| 456 | + | |
| 457 | + assert.equal(props.length, 3); | |
| 458 | + | |
| 459 | + //get the foreign keys and validate the foreign key has been dropped | |
| 460 | + ds.connector.execute(foreignKeySelect, function(err, thirdForeignKeys) { | |
| 461 | + if (err) return done (err); | |
| 462 | + assert(thirdForeignKeys); | |
| 463 | + assert(thirdForeignKeys.length.should.be.equal(0)); | |
| 464 | + | |
| 465 | + done(err, result); | |
| 466 | + }); | |
| 467 | + }); | |
| 468 | + }); | |
| 469 | + }); | |
| 470 | + }); | |
| 471 | + }); | |
| 472 | + }); | |
| 473 | + }); | |
| 474 | + }); | |
| 475 | + }); | |
| 476 | + | |
| 477 | + function setupAltColNameData() { | |
| 478 | + var schema = { | |
| 479 | + name: 'ColRenameTest', | |
| 480 | + options: { | |
| 481 | + idInjection: false, | |
| 482 | + mysql: { | |
| 483 | + schema: 'myapp_test', | |
| 484 | + table: 'col_rename_test', | |
| 485 | + }, | |
| 486 | + }, | |
| 487 | + properties: { | |
| 488 | + firstName: { | |
| 489 | + type: 'String', | |
| 490 | + required: false, | |
| 491 | + length: 40, | |
| 492 | + mysql: { | |
| 493 | + columnName: 'first_name', | |
| 494 | + dataType: 'varchar', | |
| 495 | + dataLength: 40, | |
| 496 | + }, | |
| 497 | + }, | |
| 498 | + lastName: { | |
| 499 | + type: 'String', | |
| 500 | + required: false, | |
| 501 | + length: 40, | |
| 502 | + }, | |
| 503 | + }, | |
| 504 | + }; | |
| 505 | + ds.createModel(schema.name, schema.properties, schema.options); | |
| 506 | + } | |
| 507 | + | |
| 508 | + it('should report errors for automigrate', function(done) { | |
| 509 | + ds.automigrate('XYZ', function(err) { | |
| 510 | + assert(err); | |
| 511 | + done(); | |
| 512 | + }); | |
| 513 | + }); | |
| 514 | + | |
| 515 | + it('should report errors for autoupdate', function(done) { | |
| 516 | + ds.autoupdate('XYZ', function(err) { | |
| 517 | + assert(err); | |
| 518 | + done(); | |
| 519 | + }); | |
| 520 | + }); | |
| 521 | + | |
| 522 | + it('"mysql.columnName" is updated with correct name on create table', function(done) { | |
| 523 | + // first autoupdate call uses create table | |
| 524 | + verifyMysqlColumnNameAutoupdate(done); | |
| 525 | + }); | |
| 526 | + | |
| 527 | + it('"mysql.columnName" is updated without changing column name on alter table', function(done) { | |
| 528 | + // second autoupdate call uses alter table | |
| 529 | + verifyMysqlColumnNameAutoupdate(done); | |
| 530 | + }); | |
| 531 | + | |
| 532 | + function verifyMysqlColumnNameAutoupdate(done) { | |
| 533 | + ds.autoupdate('ColRenameTest', function(err) { | |
| 534 | + ds.discoverModelProperties('col_rename_test', function(err, props) { | |
| 535 | + assert.equal(props[0].columnName, 'first_name'); | |
| 536 | + assert.equal(props[1].columnName, 'lastName'); | |
| 537 | + assert.equal(props.length, 2); | |
| 538 | + done(); | |
| 539 | + }); | |
| 540 | + }); | |
| 541 | + } | |
| 542 | +}); | ... | ... |
| 1 | +++ a/test/mysql.discover.test.js | |
| ... | ... | @@ -0,0 +1,433 @@ |
| 1 | +// Copyright IBM Corp. 2013,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +process.env.NODE_ENV = 'test'; | |
| 8 | +require('should'); | |
| 9 | + | |
| 10 | +var assert = require('assert'); | |
| 11 | +var DataSource = require('loopback-datasource-juggler').DataSource; | |
| 12 | +var db, config; | |
| 13 | + | |
| 14 | +before(function() { | |
| 15 | + require('./init'); | |
| 16 | + config = getConfig(); | |
| 17 | + config.database = 'STRONGLOOP'; | |
| 18 | + db = new DataSource(require('../'), config); | |
| 19 | +}); | |
| 20 | + | |
| 21 | +describe('discoverModels', function() { | |
| 22 | + describe('Discover database schemas', function() { | |
| 23 | + it('should return an array of db schemas', function(done) { | |
| 24 | + db.connector.discoverDatabaseSchemas(function(err, schemas) { | |
| 25 | + if (err) return done(err); | |
| 26 | + schemas.should.be.an.array; | |
| 27 | + schemas.length.should.be.above(0); | |
| 28 | + done(); | |
| 29 | + }); | |
| 30 | + }); | |
| 31 | + }); | |
| 32 | + | |
| 33 | + describe('Discover models including views', function() { | |
| 34 | + it('should return an array of tables and views', function(done) { | |
| 35 | + db.discoverModelDefinitions({ | |
| 36 | + views: true, | |
| 37 | + limit: 3, | |
| 38 | + }, function(err, models) { | |
| 39 | + if (err) { | |
| 40 | + console.error(err); | |
| 41 | + done(err); | |
| 42 | + } else { | |
| 43 | + var views = false; | |
| 44 | + models.forEach(function(m) { | |
| 45 | + // console.dir(m); | |
| 46 | + if (m.type === 'view') { | |
| 47 | + views = true; | |
| 48 | + } | |
| 49 | + }); | |
| 50 | + assert(views, 'Should have views'); | |
| 51 | + done(null, models); | |
| 52 | + } | |
| 53 | + }); | |
| 54 | + }); | |
| 55 | + }); | |
| 56 | + | |
| 57 | + describe('Discover current user\'s tables', function() { | |
| 58 | + it('should return an array of tables for the current user', function(done) { | |
| 59 | + db.discoverModelDefinitions({ | |
| 60 | + limit: 3, | |
| 61 | + }, function(err, models) { | |
| 62 | + if (err) { | |
| 63 | + console.error(err); | |
| 64 | + done(err); | |
| 65 | + } else { | |
| 66 | + var views = false; | |
| 67 | + models.forEach(function(m) { | |
| 68 | + assert.equal(m.owner, config.username); | |
| 69 | + }); | |
| 70 | + done(null, models); | |
| 71 | + } | |
| 72 | + }); | |
| 73 | + }); | |
| 74 | + }); | |
| 75 | + | |
| 76 | + describe('Discover models excluding views', function() { | |
| 77 | + // TODO: this test assumes the current user owns the tables | |
| 78 | + it.skip('should return an array of only tables', function(done) { | |
| 79 | + db.discoverModelDefinitions({ | |
| 80 | + views: false, | |
| 81 | + limit: 3, | |
| 82 | + }, function(err, models) { | |
| 83 | + if (err) { | |
| 84 | + console.error(err); | |
| 85 | + done(err); | |
| 86 | + } else { | |
| 87 | + var views = false; | |
| 88 | + models.forEach(function(m) { | |
| 89 | + // console.dir(m); | |
| 90 | + if (m.type === 'view') { | |
| 91 | + views = true; | |
| 92 | + } | |
| 93 | + }); | |
| 94 | + models.should.have.length(3); | |
| 95 | + assert(!views, 'Should not have views'); | |
| 96 | + done(null, models); | |
| 97 | + } | |
| 98 | + }); | |
| 99 | + }); | |
| 100 | + }); | |
| 101 | +}); | |
| 102 | + | |
| 103 | +describe('Discover models including other users', function() { | |
| 104 | + it('should return an array of all tables and views', function(done) { | |
| 105 | + db.discoverModelDefinitions({ | |
| 106 | + all: true, | |
| 107 | + limit: 3, | |
| 108 | + }, function(err, models) { | |
| 109 | + if (err) { | |
| 110 | + console.error(err); | |
| 111 | + done(err); | |
| 112 | + } else { | |
| 113 | + var others = false; | |
| 114 | + models.forEach(function(m) { | |
| 115 | + // console.dir(m); | |
| 116 | + if (m.owner !== 'STRONGLOOP') { | |
| 117 | + others = true; | |
| 118 | + } | |
| 119 | + }); | |
| 120 | + assert(others, 'Should have tables/views owned by others'); | |
| 121 | + done(err, models); | |
| 122 | + } | |
| 123 | + }); | |
| 124 | + }); | |
| 125 | +}); | |
| 126 | + | |
| 127 | +describe('Discover model properties', function() { | |
| 128 | + describe('Discover a named model', function() { | |
| 129 | + it('should return an array of columns for product', function(done) { | |
| 130 | + db.discoverModelProperties('product', function(err, models) { | |
| 131 | + if (err) { | |
| 132 | + console.error(err); | |
| 133 | + done(err); | |
| 134 | + } else { | |
| 135 | + models.forEach(function(m) { | |
| 136 | + // console.dir(m); | |
| 137 | + assert(m.tableName === 'product'); | |
| 138 | + }); | |
| 139 | + done(null, models); | |
| 140 | + } | |
| 141 | + }); | |
| 142 | + }); | |
| 143 | + }); | |
| 144 | +}); | |
| 145 | + | |
| 146 | +describe('Discover model primary keys', function() { | |
| 147 | + it('should return an array of primary keys for product', function(done) { | |
| 148 | + db.discoverPrimaryKeys('product', function(err, models) { | |
| 149 | + if (err) { | |
| 150 | + console.error(err); | |
| 151 | + done(err); | |
| 152 | + } else { | |
| 153 | + models.forEach(function(m) { | |
| 154 | + // console.dir(m); | |
| 155 | + assert(m.tableName === 'product'); | |
| 156 | + }); | |
| 157 | + done(null, models); | |
| 158 | + } | |
| 159 | + }); | |
| 160 | + }); | |
| 161 | + | |
| 162 | + it('should return an array of primary keys for STRONGLOOP.PRODUCT', function(done) { | |
| 163 | + db.discoverPrimaryKeys('product', {owner: 'STRONGLOOP'}, function(err, models) { | |
| 164 | + if (err) { | |
| 165 | + console.error(err); | |
| 166 | + done(err); | |
| 167 | + } else { | |
| 168 | + models.forEach(function(m) { | |
| 169 | + // console.dir(m); | |
| 170 | + assert(m.tableName === 'product'); | |
| 171 | + }); | |
| 172 | + done(null, models); | |
| 173 | + } | |
| 174 | + }); | |
| 175 | + }); | |
| 176 | +}); | |
| 177 | + | |
| 178 | +describe('Discover model foreign keys', function() { | |
| 179 | + it('should return an array of foreign keys for INVENTORY', function(done) { | |
| 180 | + db.discoverForeignKeys('INVENTORY', function(err, models) { | |
| 181 | + if (err) { | |
| 182 | + console.error(err); | |
| 183 | + done(err); | |
| 184 | + } else { | |
| 185 | + models.forEach(function(m) { | |
| 186 | + // console.dir(m); | |
| 187 | + assert(m.fkTableName === 'INVENTORY'); | |
| 188 | + }); | |
| 189 | + done(null, models); | |
| 190 | + } | |
| 191 | + }); | |
| 192 | + }); | |
| 193 | + it('should return an array of foreign keys for STRONGLOOP.INVENTORY', function(done) { | |
| 194 | + db.discoverForeignKeys('INVENTORY', {owner: 'STRONGLOOP'}, function(err, models) { | |
| 195 | + if (err) { | |
| 196 | + console.error(err); | |
| 197 | + done(err); | |
| 198 | + } else { | |
| 199 | + models.forEach(function(m) { | |
| 200 | + // console.dir(m); | |
| 201 | + assert(m.fkTableName === 'INVENTORY'); | |
| 202 | + }); | |
| 203 | + done(null, models); | |
| 204 | + } | |
| 205 | + }); | |
| 206 | + }); | |
| 207 | +}); | |
| 208 | + | |
| 209 | +describe('Discover model generated columns', function() { | |
| 210 | + it('should return an array of columns for STRONGLOOP.PRODUCT and none of them is generated', function(done) { | |
| 211 | + db.discoverModelProperties('product', function(err, models) { | |
| 212 | + if (err) return done(err); | |
| 213 | + models.forEach(function(model) { | |
| 214 | + assert(model.tableName === 'product'); | |
| 215 | + assert(!model.generated, 'STRONGLOOP.PRODUCT table should not have generated (identity) columns'); | |
| 216 | + }); | |
| 217 | + done(); | |
| 218 | + }); | |
| 219 | + }); | |
| 220 | + it('should return an array of columns for STRONGLOOP.TESTGEN and the first is generated', function(done) { | |
| 221 | + db.discoverModelProperties('testgen', function(err, models) { | |
| 222 | + if (err) return done(err); | |
| 223 | + models.forEach(function(model) { | |
| 224 | + assert(model.tableName === 'testgen'); | |
| 225 | + if (model.columnName === 'ID') { | |
| 226 | + assert(model.generated, 'STRONGLOOP.TESTGEN.ID should be a generated (identity) column'); | |
| 227 | + } | |
| 228 | + }); | |
| 229 | + done(); | |
| 230 | + }); | |
| 231 | + }); | |
| 232 | +}); | |
| 233 | + | |
| 234 | +describe('Discover LDL schema from a table', function() { | |
| 235 | + var schema; | |
| 236 | + before(function(done) { | |
| 237 | + db.discoverSchema('INVENTORY', {owner: 'STRONGLOOP'}, function(err, schema_) { | |
| 238 | + schema = schema_; | |
| 239 | + done(err); | |
| 240 | + }); | |
| 241 | + }); | |
| 242 | + it('should return an LDL schema for INVENTORY', function() { | |
| 243 | + var productId = 'productId' in schema.properties ? 'productId' : 'productid'; | |
| 244 | + var locationId = 'locationId' in schema.properties ? 'locationId' : 'locationid'; | |
| 245 | + console.error('schema:', schema); | |
| 246 | + assert.strictEqual(schema.name, 'Inventory'); | |
| 247 | + assert.ok(/STRONGLOOP/i.test(schema.options.mysql.schema)); | |
| 248 | + assert.strictEqual(schema.options.mysql.table, 'INVENTORY'); | |
| 249 | + assert(schema.properties[productId]); | |
| 250 | + // TODO: schema shows this field is default NULL, which means it isn't required | |
| 251 | + // assert(schema.properties[productId].required); | |
| 252 | + assert.strictEqual(schema.properties[productId].type, 'String'); | |
| 253 | + assert.strictEqual(schema.properties[productId].mysql.columnName, 'PRODUCT_ID'); | |
| 254 | + assert(schema.properties[locationId]); | |
| 255 | + assert.strictEqual(schema.properties[locationId].type, 'String'); | |
| 256 | + assert.strictEqual(schema.properties[locationId].mysql.columnName, 'LOCATION_ID'); | |
| 257 | + assert(schema.properties.available); | |
| 258 | + assert.strictEqual(schema.properties.available.required, false); | |
| 259 | + assert.strictEqual(schema.properties.available.type, 'Number'); | |
| 260 | + assert(schema.properties.total); | |
| 261 | + assert.strictEqual(schema.properties.total.type, 'Number'); | |
| 262 | + }); | |
| 263 | +}); | |
| 264 | + | |
| 265 | +describe('Discover and build models', function() { | |
| 266 | + var models; | |
| 267 | + before(function(done) { | |
| 268 | + db.discoverAndBuildModels('INVENTORY', {owner: 'STRONGLOOP', visited: {}, associations: true}, | |
| 269 | + function(err, models_) { | |
| 270 | + models = models_; | |
| 271 | + done(err); | |
| 272 | + }); | |
| 273 | + }); | |
| 274 | + it('should discover and build models', function() { | |
| 275 | + assert(models.Inventory, 'Inventory model should be discovered and built'); | |
| 276 | + var schema = models.Inventory.definition; | |
| 277 | + var productId = 'productId' in schema.properties ? 'productId' : 'productid'; | |
| 278 | + var locationId = 'locationId' in schema.properties ? 'locationId' : 'locationid'; | |
| 279 | + assert(/STRONGLOOP/i.test(schema.settings.mysql.schema)); | |
| 280 | + assert.strictEqual(schema.settings.mysql.table, 'INVENTORY'); | |
| 281 | + assert(schema.properties[productId]); | |
| 282 | + assert.strictEqual(schema.properties[productId].type, String); | |
| 283 | + assert.strictEqual(schema.properties[productId].mysql.columnName, 'PRODUCT_ID'); | |
| 284 | + assert(schema.properties[locationId]); | |
| 285 | + assert.strictEqual(schema.properties[locationId].type, String); | |
| 286 | + assert.strictEqual(schema.properties[locationId].mysql.columnName, 'LOCATION_ID'); | |
| 287 | + assert(schema.properties.available); | |
| 288 | + assert.strictEqual(schema.properties.available.type, Number); | |
| 289 | + assert(schema.properties.total); | |
| 290 | + assert.strictEqual(schema.properties.total.type, Number); | |
| 291 | + }); | |
| 292 | + it('should be able to find an instance', function(done) { | |
| 293 | + assert(models.Inventory, 'Inventory model must exist'); | |
| 294 | + models.Inventory.findOne(function(err, inv) { | |
| 295 | + assert(!err, 'error should not be reported'); | |
| 296 | + done(); | |
| 297 | + }); | |
| 298 | + }); | |
| 299 | + | |
| 300 | + describe('discoverModelProperties() flags', function() { | |
| 301 | + context('with default flags', function() { | |
| 302 | + var models, schema; | |
| 303 | + before(discoverAndBuildModels); | |
| 304 | + | |
| 305 | + it('handles CHAR(1) as Boolean', function() { | |
| 306 | + assert(schema.properties.enabled); | |
| 307 | + assert.strictEqual(schema.properties.enabled.type, Boolean); | |
| 308 | + }); | |
| 309 | + | |
| 310 | + it('handles BIT(1) as Bit', function() { | |
| 311 | + assert(schema.properties.disabled); | |
| 312 | + assert.strictEqual(schema.properties.disabled.type, Buffer); | |
| 313 | + }); | |
| 314 | + | |
| 315 | + it('handles TINYINT(1) as Number', function() { | |
| 316 | + assert(schema.properties.active); | |
| 317 | + assert.strictEqual(schema.properties.active.type, Number); | |
| 318 | + }); | |
| 319 | + | |
| 320 | + function discoverAndBuildModels(done) { | |
| 321 | + db.discoverAndBuildModels('INVENTORY', { | |
| 322 | + owner: 'STRONGLOOP', | |
| 323 | + visited: {}, | |
| 324 | + associations: true, | |
| 325 | + }, function(err, models_) { | |
| 326 | + models = models_; | |
| 327 | + schema = models.Inventory.definition; | |
| 328 | + done(err); | |
| 329 | + }); | |
| 330 | + } | |
| 331 | + }); | |
| 332 | + | |
| 333 | + context('with flag treatCHAR1AsString = true', function() { | |
| 334 | + var models, schema; | |
| 335 | + before(discoverAndBuildModels); | |
| 336 | + | |
| 337 | + it('handles CHAR(1) as String', function() { | |
| 338 | + assert(schema.properties.enabled); | |
| 339 | + assert.strictEqual(schema.properties.enabled.type, String); | |
| 340 | + }); | |
| 341 | + | |
| 342 | + it('handles BIT(1) as Binary', function() { | |
| 343 | + assert(schema.properties.disabled); | |
| 344 | + assert.strictEqual(schema.properties.disabled.type, Buffer); | |
| 345 | + }); | |
| 346 | + | |
| 347 | + it('handles TINYINT(1) as Number', function() { | |
| 348 | + assert(schema.properties.active); | |
| 349 | + assert.strictEqual(schema.properties.active.type, Number); | |
| 350 | + }); | |
| 351 | + | |
| 352 | + function discoverAndBuildModels(done) { | |
| 353 | + db.discoverAndBuildModels('INVENTORY', { | |
| 354 | + owner: 'STRONGLOOP', | |
| 355 | + visited: {}, | |
| 356 | + associations: true, | |
| 357 | + treatCHAR1AsString: true, | |
| 358 | + }, function(err, models_) { | |
| 359 | + models = models_; | |
| 360 | + schema = models.Inventory.definition; | |
| 361 | + done(err); | |
| 362 | + }); | |
| 363 | + } | |
| 364 | + }); | |
| 365 | + | |
| 366 | + context('with flag treatBIT1AsBit = false', function() { | |
| 367 | + var models, schema; | |
| 368 | + before(discoverAndBuildModels); | |
| 369 | + | |
| 370 | + it('handles CHAR(1) as Boolean', function() { | |
| 371 | + assert(schema.properties.enabled); | |
| 372 | + assert.strictEqual(schema.properties.enabled.type, Boolean); | |
| 373 | + }); | |
| 374 | + | |
| 375 | + it('handles BIT(1) as Boolean', function() { | |
| 376 | + assert(schema.properties.disabled); | |
| 377 | + assert.strictEqual(schema.properties.disabled.type, Boolean); | |
| 378 | + }); | |
| 379 | + | |
| 380 | + it('handles TINYINT(1) as Number', function() { | |
| 381 | + assert(schema.properties.active); | |
| 382 | + assert.strictEqual(schema.properties.active.type, Number); | |
| 383 | + }); | |
| 384 | + | |
| 385 | + function discoverAndBuildModels(done) { | |
| 386 | + db.discoverAndBuildModels('INVENTORY', { | |
| 387 | + owner: 'STRONGLOOP', | |
| 388 | + visited: {}, | |
| 389 | + associations: true, | |
| 390 | + treatBIT1AsBit: false, | |
| 391 | + }, function(err, models_) { | |
| 392 | + models = models_; | |
| 393 | + schema = models.Inventory.definition; | |
| 394 | + done(err); | |
| 395 | + }); | |
| 396 | + } | |
| 397 | + }); | |
| 398 | + | |
| 399 | + context('with flag treatTINYINT1AsTinyInt = false', function() { | |
| 400 | + var models, schema; | |
| 401 | + before(discoverAndBuildModels); | |
| 402 | + | |
| 403 | + it('handles CHAR(1) as Boolean', function() { | |
| 404 | + assert(schema.properties.enabled); | |
| 405 | + assert.strictEqual(schema.properties.enabled.type, Boolean); | |
| 406 | + }); | |
| 407 | + | |
| 408 | + it('handles BIT(1) as Binary', function() { | |
| 409 | + assert(schema.properties.disabled); | |
| 410 | + assert.strictEqual(schema.properties.disabled.type, Buffer); | |
| 411 | + }); | |
| 412 | + | |
| 413 | + it('handles TINYINT(1) as Boolean', function() { | |
| 414 | + assert(schema.properties.active); | |
| 415 | + assert.strictEqual(schema.properties.active.type, Boolean); | |
| 416 | + }); | |
| 417 | + | |
| 418 | + function discoverAndBuildModels(done) { | |
| 419 | + db.discoverAndBuildModels('INVENTORY', { | |
| 420 | + owner: 'STRONGLOOP', | |
| 421 | + visited: {}, | |
| 422 | + associations: true, | |
| 423 | + treatTINYINT1AsTinyInt: false, | |
| 424 | + }, function(err, models_) { | |
| 425 | + if (err) return done(err); | |
| 426 | + models = models_; | |
| 427 | + schema = models.Inventory.definition; | |
| 428 | + done(); | |
| 429 | + }); | |
| 430 | + } | |
| 431 | + }); | |
| 432 | + }); | |
| 433 | +}); | ... | ... |
| 1 | +++ a/test/mysql.test.js | |
| ... | ... | @@ -0,0 +1,760 @@ |
| 1 | +// Copyright IBM Corp. 2013,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +var should = require('./init.js'); | |
| 8 | + | |
| 9 | +var Post, PostWithStringId, PostWithUniqueTitle, db; | |
| 10 | + | |
| 11 | +// Mock up mongodb ObjectID | |
| 12 | +function ObjectID(id) { | |
| 13 | + if (!(this instanceof ObjectID)) { | |
| 14 | + return new ObjectID(id); | |
| 15 | + } | |
| 16 | + this.id1 = id.substring(0, 2); | |
| 17 | + this.id2 = id.substring(2); | |
| 18 | +} | |
| 19 | + | |
| 20 | +ObjectID.prototype.toJSON = function() { | |
| 21 | + return this.id1 + this.id2; | |
| 22 | +}; | |
| 23 | + | |
| 24 | +describe('mysql', function() { | |
| 25 | + before(function(done) { | |
| 26 | + db = getDataSource(); | |
| 27 | + | |
| 28 | + Post = db.define('PostWithDefaultId', { | |
| 29 | + title: {type: String, length: 255, index: true}, | |
| 30 | + content: {type: String}, | |
| 31 | + comments: [String], | |
| 32 | + history: Object, | |
| 33 | + stars: Number, | |
| 34 | + userId: ObjectID, | |
| 35 | + }, { | |
| 36 | + forceId: false, | |
| 37 | + }); | |
| 38 | + | |
| 39 | + PostWithStringId = db.define('PostWithStringId', { | |
| 40 | + id: {type: String, id: true}, | |
| 41 | + title: {type: String, length: 255, index: true}, | |
| 42 | + content: {type: String}, | |
| 43 | + }); | |
| 44 | + | |
| 45 | + PostWithUniqueTitle = db.define('PostWithUniqueTitle', { | |
| 46 | + title: {type: String, length: 255, index: {unique: true}}, | |
| 47 | + content: {type: String}, | |
| 48 | + }); | |
| 49 | + | |
| 50 | + db.automigrate(['PostWithDefaultId', 'PostWithStringId', 'PostWithUniqueTitle'], function(err) { | |
| 51 | + should.not.exist(err); | |
| 52 | + done(err); | |
| 53 | + }); | |
| 54 | + }); | |
| 55 | + | |
| 56 | + beforeEach(function(done) { | |
| 57 | + Post.destroyAll(function() { | |
| 58 | + PostWithStringId.destroyAll(function() { | |
| 59 | + PostWithUniqueTitle.destroyAll(function() { | |
| 60 | + done(); | |
| 61 | + }); | |
| 62 | + }); | |
| 63 | + }); | |
| 64 | + }); | |
| 65 | + | |
| 66 | + it('should allow array or object', function(done) { | |
| 67 | + Post.create({title: 'a', content: 'AAA', comments: ['1', '2'], | |
| 68 | + history: {a: 1, b: 'b'}}, function(err, post) { | |
| 69 | + should.not.exist(err); | |
| 70 | + | |
| 71 | + Post.findById(post.id, function(err, p) { | |
| 72 | + p.id.should.be.equal(post.id); | |
| 73 | + | |
| 74 | + p.content.should.be.equal(post.content); | |
| 75 | + p.title.should.be.equal('a'); | |
| 76 | + p.comments.should.eql(['1', '2']); | |
| 77 | + p.history.should.eql({a: 1, b: 'b'}); | |
| 78 | + | |
| 79 | + done(); | |
| 80 | + }); | |
| 81 | + }); | |
| 82 | + }); | |
| 83 | + | |
| 84 | + it('should allow ObjectID', function(done) { | |
| 85 | + var uid = new ObjectID('123'); | |
| 86 | + Post.create({title: 'a', content: 'AAA', userId: uid}, | |
| 87 | + function(err, post) { | |
| 88 | + should.not.exist(err); | |
| 89 | + | |
| 90 | + Post.findById(post.id, function(err, p) { | |
| 91 | + p.id.should.be.equal(post.id); | |
| 92 | + | |
| 93 | + p.content.should.be.equal(post.content); | |
| 94 | + p.title.should.be.equal('a'); | |
| 95 | + p.userId.should.eql(uid); | |
| 96 | + done(); | |
| 97 | + }); | |
| 98 | + }); | |
| 99 | + }); | |
| 100 | + | |
| 101 | + it('updateOrCreate should update the instance', function(done) { | |
| 102 | + Post.create({title: 'a', content: 'AAA'}, function(err, post) { | |
| 103 | + post.title = 'b'; | |
| 104 | + Post.updateOrCreate(post, function(err, p) { | |
| 105 | + should.not.exist(err); | |
| 106 | + p.id.should.be.equal(post.id); | |
| 107 | + p.content.should.be.equal(post.content); | |
| 108 | + | |
| 109 | + Post.findById(post.id, function(err, p) { | |
| 110 | + p.id.should.be.equal(post.id); | |
| 111 | + | |
| 112 | + p.content.should.be.equal(post.content); | |
| 113 | + p.title.should.be.equal('b'); | |
| 114 | + | |
| 115 | + done(); | |
| 116 | + }); | |
| 117 | + }); | |
| 118 | + }); | |
| 119 | + }); | |
| 120 | + | |
| 121 | + it('updateOrCreate should update the instance without removing existing properties', function(done) { | |
| 122 | + Post.create({title: 'a', content: 'AAA'}, function(err, post) { | |
| 123 | + post = post.toObject(); | |
| 124 | + delete post.title; | |
| 125 | + Post.updateOrCreate(post, function(err, p) { | |
| 126 | + should.not.exist(err); | |
| 127 | + p.id.should.be.equal(post.id); | |
| 128 | + p.content.should.be.equal(post.content); | |
| 129 | + Post.findById(post.id, function(err, p) { | |
| 130 | + p.id.should.be.equal(post.id); | |
| 131 | + | |
| 132 | + p.content.should.be.equal(post.content); | |
| 133 | + p.title.should.be.equal('a'); | |
| 134 | + | |
| 135 | + done(); | |
| 136 | + }); | |
| 137 | + }); | |
| 138 | + }); | |
| 139 | + }); | |
| 140 | + | |
| 141 | + it('updateOrCreate should create a new instance if it does not exist', function(done) { | |
| 142 | + var post = {id: 123, title: 'a', content: 'AAA'}; | |
| 143 | + Post.updateOrCreate(post, function(err, p) { | |
| 144 | + should.not.exist(err); | |
| 145 | + p.title.should.be.equal(post.title); | |
| 146 | + p.content.should.be.equal(post.content); | |
| 147 | + p.id.should.be.equal(post.id); | |
| 148 | + | |
| 149 | + Post.findById(p.id, function(err, p) { | |
| 150 | + p.id.should.be.equal(post.id); | |
| 151 | + | |
| 152 | + p.content.should.be.equal(post.content); | |
| 153 | + p.title.should.be.equal(post.title); | |
| 154 | + p.id.should.be.equal(post.id); | |
| 155 | + | |
| 156 | + done(); | |
| 157 | + }); | |
| 158 | + }); | |
| 159 | + }); | |
| 160 | + | |
| 161 | + context('replaceOrCreate', function() { | |
| 162 | + it('should replace the instance', function(done) { | |
| 163 | + Post.create({title: 'a', content: 'AAA'}, function(err, post) { | |
| 164 | + if (err) return done(err); | |
| 165 | + post = post.toObject(); | |
| 166 | + delete post.content; | |
| 167 | + Post.replaceOrCreate(post, function(err, p) { | |
| 168 | + if (err) return done(err); | |
| 169 | + p.id.should.equal(post.id); | |
| 170 | + p.title.should.equal('a'); | |
| 171 | + should.not.exist(p.content); | |
| 172 | + should.not.exist(p._id); | |
| 173 | + Post.findById(post.id, function(err, p) { | |
| 174 | + if (err) return done(err); | |
| 175 | + p.id.should.equal(post.id); | |
| 176 | + p.title.should.equal('a'); | |
| 177 | + should.not.exist(post.content); | |
| 178 | + should.not.exist(p._id); | |
| 179 | + done(); | |
| 180 | + }); | |
| 181 | + }); | |
| 182 | + }); | |
| 183 | + }); | |
| 184 | + | |
| 185 | + it('should replace with new data', function(done) { | |
| 186 | + Post.create({title: 'a', content: 'AAA', comments: ['Comment1']}, | |
| 187 | + function(err, post) { | |
| 188 | + if (err) return done(err); | |
| 189 | + post = post.toObject(); | |
| 190 | + delete post.comments; | |
| 191 | + delete post.content; | |
| 192 | + post.title = 'b'; | |
| 193 | + Post.replaceOrCreate(post, function(err, p) { | |
| 194 | + if (err) return done(err); | |
| 195 | + p.id.should.equal(post.id); | |
| 196 | + should.not.exist(p._id); | |
| 197 | + p.title.should.equal('b'); | |
| 198 | + should.not.exist(p.content); | |
| 199 | + should.not.exist(p.comments); | |
| 200 | + Post.findById(post.id, function(err, p) { | |
| 201 | + if (err) return done(err); | |
| 202 | + p.id.should.equal(post.id); | |
| 203 | + should.not.exist(p._id); | |
| 204 | + p.title.should.equal('b'); | |
| 205 | + should.not.exist(p.content); | |
| 206 | + should.not.exist(p.comments); | |
| 207 | + done(); | |
| 208 | + }); | |
| 209 | + }); | |
| 210 | + }); | |
| 211 | + }); | |
| 212 | + | |
| 213 | + it('should create a new instance if it does not exist', function(done) { | |
| 214 | + var post = {id: 123, title: 'a', content: 'AAA'}; | |
| 215 | + Post.replaceOrCreate(post, function(err, p) { | |
| 216 | + if (err) return done(err); | |
| 217 | + p.id.should.equal(post.id); | |
| 218 | + should.not.exist(p._id); | |
| 219 | + p.title.should.equal(post.title); | |
| 220 | + p.content.should.equal(post.content); | |
| 221 | + Post.findById(p.id, function(err, p) { | |
| 222 | + if (err) return done(err); | |
| 223 | + p.id.should.equal(post.id); | |
| 224 | + should.not.exist(p._id); | |
| 225 | + p.title.should.equal(post.title); | |
| 226 | + p.content.should.equal(post.content); | |
| 227 | + done(); | |
| 228 | + }); | |
| 229 | + }); | |
| 230 | + }); | |
| 231 | + }); | |
| 232 | + | |
| 233 | + it('save should update the instance with the same id', function(done) { | |
| 234 | + Post.create({title: 'a', content: 'AAA'}, function(err, post) { | |
| 235 | + post.title = 'b'; | |
| 236 | + post.save(function(err, p) { | |
| 237 | + should.not.exist(err); | |
| 238 | + p.id.should.be.equal(post.id); | |
| 239 | + p.content.should.be.equal(post.content); | |
| 240 | + | |
| 241 | + Post.findById(post.id, function(err, p) { | |
| 242 | + p.id.should.be.equal(post.id); | |
| 243 | + | |
| 244 | + p.content.should.be.equal(post.content); | |
| 245 | + p.title.should.be.equal('b'); | |
| 246 | + | |
| 247 | + done(); | |
| 248 | + }); | |
| 249 | + }); | |
| 250 | + }); | |
| 251 | + }); | |
| 252 | + | |
| 253 | + it('save should update the instance without removing existing properties', function(done) { | |
| 254 | + Post.create({title: 'a', content: 'AAA'}, function(err, post) { | |
| 255 | + delete post.title; | |
| 256 | + post.save(function(err, p) { | |
| 257 | + should.not.exist(err); | |
| 258 | + p.id.should.be.equal(post.id); | |
| 259 | + p.content.should.be.equal(post.content); | |
| 260 | + | |
| 261 | + Post.findById(post.id, function(err, p) { | |
| 262 | + p.id.should.be.equal(post.id); | |
| 263 | + | |
| 264 | + p.content.should.be.equal(post.content); | |
| 265 | + p.title.should.be.equal('a'); | |
| 266 | + | |
| 267 | + done(); | |
| 268 | + }); | |
| 269 | + }); | |
| 270 | + }); | |
| 271 | + }); | |
| 272 | + | |
| 273 | + it('save should create a new instance if it does not exist', function(done) { | |
| 274 | + var post = new Post({id: 123, title: 'a', content: 'AAA'}); | |
| 275 | + post.save(post, function(err, p) { | |
| 276 | + should.not.exist(err); | |
| 277 | + p.title.should.be.equal(post.title); | |
| 278 | + p.content.should.be.equal(post.content); | |
| 279 | + p.id.should.be.equal(post.id); | |
| 280 | + | |
| 281 | + Post.findById(p.id, function(err, p) { | |
| 282 | + should.not.exist(err); | |
| 283 | + p.id.should.be.equal(post.id); | |
| 284 | + | |
| 285 | + p.content.should.be.equal(post.content); | |
| 286 | + p.title.should.be.equal(post.title); | |
| 287 | + p.id.should.be.equal(post.id); | |
| 288 | + | |
| 289 | + done(); | |
| 290 | + }); | |
| 291 | + }); | |
| 292 | + }); | |
| 293 | + | |
| 294 | + it('all return should honor filter.fields', function(done) { | |
| 295 | + var post = new Post({title: 'b', content: 'BBB'}); | |
| 296 | + post.save(function(err, post) { | |
| 297 | + Post.all({fields: ['title'], where: {title: 'b'}}, function(err, posts) { | |
| 298 | + should.not.exist(err); | |
| 299 | + posts.should.have.lengthOf(1); | |
| 300 | + post = posts[0]; | |
| 301 | + post.should.have.property('title', 'b'); | |
| 302 | + post.should.have.property('content', undefined); | |
| 303 | + should.not.exist(post.id); | |
| 304 | + | |
| 305 | + done(); | |
| 306 | + }); | |
| 307 | + }); | |
| 308 | + }); | |
| 309 | + | |
| 310 | + it('find should order by id if the order is not set for the query filter', | |
| 311 | + function(done) { | |
| 312 | + PostWithStringId.create({id: '2', title: 'c', content: 'CCC'}, function(err, post) { | |
| 313 | + PostWithStringId.create({id: '1', title: 'd', content: 'DDD'}, function(err, post) { | |
| 314 | + PostWithStringId.find(function(err, posts) { | |
| 315 | + should.not.exist(err); | |
| 316 | + posts.length.should.be.equal(2); | |
| 317 | + posts[0].id.should.be.equal('1'); | |
| 318 | + | |
| 319 | + PostWithStringId.find({limit: 1, offset: 0}, function(err, posts) { | |
| 320 | + should.not.exist(err); | |
| 321 | + posts.length.should.be.equal(1); | |
| 322 | + posts[0].id.should.be.equal('1'); | |
| 323 | + | |
| 324 | + PostWithStringId.find({limit: 1, offset: 1}, function(err, posts) { | |
| 325 | + should.not.exist(err); | |
| 326 | + posts.length.should.be.equal(1); | |
| 327 | + posts[0].id.should.be.equal('2'); | |
| 328 | + done(); | |
| 329 | + }); | |
| 330 | + }); | |
| 331 | + }); | |
| 332 | + }); | |
| 333 | + }); | |
| 334 | + }); | |
| 335 | + | |
| 336 | + it('should allow to find using like', function(done) { | |
| 337 | + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { | |
| 338 | + Post.find({where: {title: {like: 'M%st'}}}, function(err, posts) { | |
| 339 | + should.not.exist(err); | |
| 340 | + posts.should.have.property('length', 1); | |
| 341 | + done(); | |
| 342 | + }); | |
| 343 | + }); | |
| 344 | + }); | |
| 345 | + | |
| 346 | + it('should support like for no match', function(done) { | |
| 347 | + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { | |
| 348 | + Post.find({where: {title: {like: 'M%XY'}}}, function(err, posts) { | |
| 349 | + should.not.exist(err); | |
| 350 | + posts.should.have.property('length', 0); | |
| 351 | + done(); | |
| 352 | + }); | |
| 353 | + }); | |
| 354 | + }); | |
| 355 | + | |
| 356 | + it('should allow to find using nlike', function(done) { | |
| 357 | + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { | |
| 358 | + Post.find({where: {title: {nlike: 'M%st'}}}, function(err, posts) { | |
| 359 | + should.not.exist(err); | |
| 360 | + posts.should.have.property('length', 0); | |
| 361 | + done(); | |
| 362 | + }); | |
| 363 | + }); | |
| 364 | + }); | |
| 365 | + | |
| 366 | + it('should support nlike for no match', function(done) { | |
| 367 | + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { | |
| 368 | + Post.find({where: {title: {nlike: 'M%XY'}}}, function(err, posts) { | |
| 369 | + should.not.exist(err); | |
| 370 | + posts.should.have.property('length', 1); | |
| 371 | + done(); | |
| 372 | + }); | |
| 373 | + }); | |
| 374 | + }); | |
| 375 | + | |
| 376 | + it('should support "and" operator that is satisfied', function(done) { | |
| 377 | + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { | |
| 378 | + Post.find({where: {and: [ | |
| 379 | + {title: 'My Post'}, | |
| 380 | + {content: 'Hello'}, | |
| 381 | + ]}}, function(err, posts) { | |
| 382 | + should.not.exist(err); | |
| 383 | + posts.should.have.property('length', 1); | |
| 384 | + done(); | |
| 385 | + }); | |
| 386 | + }); | |
| 387 | + }); | |
| 388 | + | |
| 389 | + it('should support "and" operator that is not satisfied', function(done) { | |
| 390 | + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { | |
| 391 | + Post.find({where: {and: [ | |
| 392 | + {title: 'My Post'}, | |
| 393 | + {content: 'Hello1'}, | |
| 394 | + ]}}, function(err, posts) { | |
| 395 | + should.not.exist(err); | |
| 396 | + posts.should.have.property('length', 0); | |
| 397 | + done(); | |
| 398 | + }); | |
| 399 | + }); | |
| 400 | + }); | |
| 401 | + | |
| 402 | + it('should support "or" that is satisfied', function(done) { | |
| 403 | + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { | |
| 404 | + Post.find({where: {or: [ | |
| 405 | + {title: 'My Post'}, | |
| 406 | + {content: 'Hello1'}, | |
| 407 | + ]}}, function(err, posts) { | |
| 408 | + should.not.exist(err); | |
| 409 | + posts.should.have.property('length', 1); | |
| 410 | + done(); | |
| 411 | + }); | |
| 412 | + }); | |
| 413 | + }); | |
| 414 | + | |
| 415 | + it('should support "or" operator that is not satisfied', function(done) { | |
| 416 | + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { | |
| 417 | + Post.find({where: {or: [ | |
| 418 | + {title: 'My Post1'}, | |
| 419 | + {content: 'Hello1'}, | |
| 420 | + ]}}, function(err, posts) { | |
| 421 | + should.not.exist(err); | |
| 422 | + posts.should.have.property('length', 0); | |
| 423 | + done(); | |
| 424 | + }); | |
| 425 | + }); | |
| 426 | + }); | |
| 427 | + | |
| 428 | + // The where object should be parsed by the connector | |
| 429 | + it('should support where for count', function(done) { | |
| 430 | + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { | |
| 431 | + Post.count({and: [ | |
| 432 | + {title: 'My Post'}, | |
| 433 | + {content: 'Hello'}, | |
| 434 | + ]}, function(err, count) { | |
| 435 | + should.not.exist(err); | |
| 436 | + count.should.be.equal(1); | |
| 437 | + Post.count({and: [ | |
| 438 | + {title: 'My Post1'}, | |
| 439 | + {content: 'Hello'}, | |
| 440 | + ]}, function(err, count) { | |
| 441 | + should.not.exist(err); | |
| 442 | + count.should.be.equal(0); | |
| 443 | + done(); | |
| 444 | + }); | |
| 445 | + }); | |
| 446 | + }); | |
| 447 | + }); | |
| 448 | + | |
| 449 | + // The where object should be parsed by the connector | |
| 450 | + it('should support where for destroyAll', function(done) { | |
| 451 | + Post.create({title: 'My Post1', content: 'Hello'}, function(err, post) { | |
| 452 | + Post.create({title: 'My Post2', content: 'Hello'}, function(err, post) { | |
| 453 | + Post.destroyAll({and: [ | |
| 454 | + {title: 'My Post1'}, | |
| 455 | + {content: 'Hello'}, | |
| 456 | + ]}, function(err) { | |
| 457 | + should.not.exist(err); | |
| 458 | + Post.count(function(err, count) { | |
| 459 | + should.not.exist(err); | |
| 460 | + count.should.be.equal(1); | |
| 461 | + done(); | |
| 462 | + }); | |
| 463 | + }); | |
| 464 | + }); | |
| 465 | + }); | |
| 466 | + }); | |
| 467 | + | |
| 468 | + it('should not allow SQL injection for inq operator', function(done) { | |
| 469 | + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, | |
| 470 | + function(err, post) { | |
| 471 | + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, | |
| 472 | + function(err, post) { | |
| 473 | + Post.find({where: {title: {inq: ['SELECT title from PostWithDefaultId']}}}, | |
| 474 | + function(err, posts) { | |
| 475 | + should.not.exist(err); | |
| 476 | + posts.should.have.property('length', 0); | |
| 477 | + done(); | |
| 478 | + }); | |
| 479 | + }); | |
| 480 | + }); | |
| 481 | + }); | |
| 482 | + | |
| 483 | + it('should not allow SQL injection for lt operator', function(done) { | |
| 484 | + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, | |
| 485 | + function(err, post) { | |
| 486 | + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, | |
| 487 | + function(err, post) { | |
| 488 | + Post.find({where: {stars: {lt: 'SELECT title from PostWithDefaultId'}}}, | |
| 489 | + function(err, posts) { | |
| 490 | + should.not.exist(err); | |
| 491 | + posts.should.have.property('length', 0); | |
| 492 | + done(); | |
| 493 | + }); | |
| 494 | + }); | |
| 495 | + }); | |
| 496 | + }); | |
| 497 | + | |
| 498 | + it('should not allow SQL injection for nin operator', function(done) { | |
| 499 | + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, | |
| 500 | + function(err, post) { | |
| 501 | + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, | |
| 502 | + function(err, post) { | |
| 503 | + Post.find({where: {title: {nin: ['SELECT title from PostWithDefaultId']}}}, | |
| 504 | + function(err, posts) { | |
| 505 | + should.not.exist(err); | |
| 506 | + posts.should.have.property('length', 2); | |
| 507 | + done(); | |
| 508 | + }); | |
| 509 | + }); | |
| 510 | + }); | |
| 511 | + }); | |
| 512 | + | |
| 513 | + it('should not allow SQL injection for inq operator with number column', function(done) { | |
| 514 | + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, | |
| 515 | + function(err, post) { | |
| 516 | + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, | |
| 517 | + function(err, post) { | |
| 518 | + Post.find({where: {stars: {inq: ['SELECT title from PostWithDefaultId']}}}, | |
| 519 | + function(err, posts) { | |
| 520 | + should.not.exist(err); | |
| 521 | + posts.should.have.property('length', 0); | |
| 522 | + done(); | |
| 523 | + }); | |
| 524 | + }); | |
| 525 | + }); | |
| 526 | + }); | |
| 527 | + | |
| 528 | + it('should not allow SQL injection for inq operator with array value', function(done) { | |
| 529 | + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, | |
| 530 | + function(err, post) { | |
| 531 | + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, | |
| 532 | + function(err, post) { | |
| 533 | + Post.find({where: {stars: {inq: [5, 'SELECT title from PostWithDefaultId']}}}, | |
| 534 | + function(err, posts) { | |
| 535 | + should.not.exist(err); | |
| 536 | + posts.should.have.property('length', 1); | |
| 537 | + done(); | |
| 538 | + }); | |
| 539 | + }); | |
| 540 | + }); | |
| 541 | + }); | |
| 542 | + | |
| 543 | + it('should not allow SQL injection for between operator', function(done) { | |
| 544 | + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, | |
| 545 | + function(err, post) { | |
| 546 | + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, | |
| 547 | + function(err, post) { | |
| 548 | + Post.find({where: {stars: {between: [5, 'SELECT title from PostWithDefaultId']}}}, | |
| 549 | + function(err, posts) { | |
| 550 | + should.not.exist(err); | |
| 551 | + posts.should.have.property('length', 0); | |
| 552 | + done(); | |
| 553 | + }); | |
| 554 | + }); | |
| 555 | + }); | |
| 556 | + }); | |
| 557 | + | |
| 558 | + it('should not allow duplicate titles', function(done) { | |
| 559 | + var data = {title: 'a', content: 'AAA'}; | |
| 560 | + PostWithUniqueTitle.create(data, function(err, post) { | |
| 561 | + should.not.exist(err); | |
| 562 | + PostWithUniqueTitle.create(data, function(err, post) { | |
| 563 | + should.exist(err); | |
| 564 | + done(); | |
| 565 | + }); | |
| 566 | + }); | |
| 567 | + }); | |
| 568 | + | |
| 569 | + context('regexp operator', function() { | |
| 570 | + beforeEach(function deleteExistingTestFixtures(done) { | |
| 571 | + Post.destroyAll(done); | |
| 572 | + }); | |
| 573 | + beforeEach(function createTestFixtures(done) { | |
| 574 | + Post.create([ | |
| 575 | + {title: 'a', content: 'AAA'}, | |
| 576 | + {title: 'b', content: 'BBB'}, | |
| 577 | + ], done); | |
| 578 | + }); | |
| 579 | + after(function deleteTestFixtures(done) { | |
| 580 | + Post.destroyAll(done); | |
| 581 | + }); | |
| 582 | + | |
| 583 | + context('with regex strings', function() { | |
| 584 | + context('using no flags', function() { | |
| 585 | + it('should work', function(done) { | |
| 586 | + Post.find({where: {content: {regexp: '^A'}}}, function(err, posts) { | |
| 587 | + should.not.exist(err); | |
| 588 | + posts.length.should.equal(1); | |
| 589 | + posts[0].content.should.equal('AAA'); | |
| 590 | + done(); | |
| 591 | + }); | |
| 592 | + }); | |
| 593 | + }); | |
| 594 | + | |
| 595 | + context('using flags', function() { | |
| 596 | + beforeEach(function addSpy() { | |
| 597 | + sinon.stub(console, 'warn'); | |
| 598 | + }); | |
| 599 | + afterEach(function removeSpy() { | |
| 600 | + console.warn.restore(); | |
| 601 | + }); | |
| 602 | + | |
| 603 | + it('should work', function(done) { | |
| 604 | + Post.find({where: {content: {regexp: '^a/i'}}}, function(err, posts) { | |
| 605 | + should.not.exist(err); | |
| 606 | + posts.length.should.equal(1); | |
| 607 | + posts[0].content.should.equal('AAA'); | |
| 608 | + done(); | |
| 609 | + }); | |
| 610 | + }); | |
| 611 | + | |
| 612 | + it('should print a warning when the ignore flag is set', | |
| 613 | + function(done) { | |
| 614 | + Post.find({where: {content: {regexp: '^a/i'}}}, function(err, posts) { | |
| 615 | + console.warn.calledOnce.should.be.ok; | |
| 616 | + done(); | |
| 617 | + }); | |
| 618 | + }); | |
| 619 | + | |
| 620 | + it('should print a warning when the global flag is set', | |
| 621 | + function(done) { | |
| 622 | + Post.find({where: {content: {regexp: '^a/g'}}}, function(err, posts) { | |
| 623 | + console.warn.calledOnce.should.be.ok; | |
| 624 | + done(); | |
| 625 | + }); | |
| 626 | + }); | |
| 627 | + | |
| 628 | + it('should print a warning when the multiline flag is set', | |
| 629 | + function(done) { | |
| 630 | + Post.find({where: {content: {regexp: '^a/m'}}}, function(err, posts) { | |
| 631 | + console.warn.calledOnce.should.be.ok; | |
| 632 | + done(); | |
| 633 | + }); | |
| 634 | + }); | |
| 635 | + }); | |
| 636 | + }); | |
| 637 | + | |
| 638 | + context('with regex literals', function() { | |
| 639 | + context('using no flags', function() { | |
| 640 | + it('should work', function(done) { | |
| 641 | + Post.find({where: {content: {regexp: /^A/}}}, function(err, posts) { | |
| 642 | + should.not.exist(err); | |
| 643 | + posts.length.should.equal(1); | |
| 644 | + posts[0].content.should.equal('AAA'); | |
| 645 | + done(); | |
| 646 | + }); | |
| 647 | + }); | |
| 648 | + }); | |
| 649 | + | |
| 650 | + context('using flags', function() { | |
| 651 | + beforeEach(function addSpy() { | |
| 652 | + sinon.stub(console, 'warn'); | |
| 653 | + }); | |
| 654 | + afterEach(function removeSpy() { | |
| 655 | + console.warn.restore(); | |
| 656 | + }); | |
| 657 | + | |
| 658 | + it('should work', function(done) { | |
| 659 | + Post.find({where: {content: {regexp: /^a/i}}}, function(err, posts) { | |
| 660 | + should.not.exist(err); | |
| 661 | + posts.length.should.equal(1); | |
| 662 | + posts[0].content.should.equal('AAA'); | |
| 663 | + done(); | |
| 664 | + }); | |
| 665 | + }); | |
| 666 | + | |
| 667 | + it('should print a warning when the ignore flag is set', | |
| 668 | + function(done) { | |
| 669 | + Post.find({where: {content: {regexp: /^a/i}}}, function(err, posts) { | |
| 670 | + console.warn.calledOnce.should.be.ok; | |
| 671 | + done(); | |
| 672 | + }); | |
| 673 | + }); | |
| 674 | + | |
| 675 | + it('should print a warning when the global flag is set', | |
| 676 | + function(done) { | |
| 677 | + Post.find({where: {content: {regexp: /^a/g}}}, function(err, posts) { | |
| 678 | + console.warn.calledOnce.should.be.ok; | |
| 679 | + done(); | |
| 680 | + }); | |
| 681 | + }); | |
| 682 | + | |
| 683 | + it('should print a warning when the multiline flag is set', | |
| 684 | + function(done) { | |
| 685 | + Post.find({where: {content: {regexp: /^a/m}}}, function(err, posts) { | |
| 686 | + console.warn.calledOnce.should.be.ok; | |
| 687 | + done(); | |
| 688 | + }); | |
| 689 | + }); | |
| 690 | + }); | |
| 691 | + }); | |
| 692 | + | |
| 693 | + context('with regex objects', function() { | |
| 694 | + beforeEach(function addSpy() { | |
| 695 | + sinon.stub(console, 'warn'); | |
| 696 | + }); | |
| 697 | + afterEach(function removeSpy() { | |
| 698 | + console.warn.restore(); | |
| 699 | + }); | |
| 700 | + | |
| 701 | + context('using no flags', function() { | |
| 702 | + it('should work', function(done) { | |
| 703 | + Post.find({where: {content: {regexp: new RegExp(/^A/)}}}, | |
| 704 | + function(err, posts) { | |
| 705 | + should.not.exist(err); | |
| 706 | + posts.length.should.equal(1); | |
| 707 | + posts[0].content.should.equal('AAA'); | |
| 708 | + done(); | |
| 709 | + }); | |
| 710 | + }); | |
| 711 | + }); | |
| 712 | + | |
| 713 | + context('using flags', function() { | |
| 714 | + it('should work', function(done) { | |
| 715 | + Post.find({where: {content: {regexp: new RegExp(/^a/i)}}}, | |
| 716 | + function(err, posts) { | |
| 717 | + should.not.exist(err); | |
| 718 | + posts.length.should.equal(1); | |
| 719 | + posts[0].content.should.equal('AAA'); | |
| 720 | + done(); | |
| 721 | + }); | |
| 722 | + }); | |
| 723 | + it('should print a warning when the ignore flag is set', | |
| 724 | + function(done) { | |
| 725 | + Post.find({where: {content: {regexp: new RegExp(/^a/i)}}}, | |
| 726 | + function(err, posts) { | |
| 727 | + console.warn.calledOnce.should.be.ok; | |
| 728 | + done(); | |
| 729 | + }); | |
| 730 | + }); | |
| 731 | + | |
| 732 | + it('should print a warning when the global flag is set', | |
| 733 | + function(done) { | |
| 734 | + Post.find({where: {content: {regexp: new RegExp(/^a/g)}}}, | |
| 735 | + function(err, posts) { | |
| 736 | + console.warn.calledOnce.should.be.ok; | |
| 737 | + done(); | |
| 738 | + }); | |
| 739 | + }); | |
| 740 | + | |
| 741 | + it('should print a warning when the multiline flag is set', | |
| 742 | + function(done) { | |
| 743 | + Post.find({where: {content: {regexp: new RegExp(/^a/m)}}}, | |
| 744 | + function(err, posts) { | |
| 745 | + console.warn.calledOnce.should.be.ok; | |
| 746 | + done(); | |
| 747 | + }); | |
| 748 | + }); | |
| 749 | + }); | |
| 750 | + }); | |
| 751 | + }); | |
| 752 | + | |
| 753 | + after(function(done) { | |
| 754 | + Post.destroyAll(function() { | |
| 755 | + PostWithStringId.destroyAll(function() { | |
| 756 | + PostWithUniqueTitle.destroyAll(done); | |
| 757 | + }); | |
| 758 | + }); | |
| 759 | + }); | |
| 760 | +}); | ... | ... |
| 1 | +++ a/test/persistence-hooks.test.js | |
| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | +// Copyright IBM Corp. 2015,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +var should = require('./init'); | |
| 8 | +var suite = require('loopback-datasource-juggler/test/persistence-hooks.suite.js'); | |
| 9 | + | |
| 10 | +suite(global.getDataSource(), should, { | |
| 11 | + replaceOrCreateReportsNewInstance: true, | |
| 12 | +}); | ... | ... |
| 1 | +++ a/test/schema.sql | |
| ... | ... | @@ -0,0 +1,239 @@ |
| 1 | +-- MySQL dump 10.13 Distrib 5.7.14, for osx10.10 (x86_64) | |
| 2 | +-- | |
| 3 | +-- Host: 166.78.158.45 Database: STRONGLOOP | |
| 4 | +-- ------------------------------------------------------ | |
| 5 | +-- Server version 5.1.69 | |
| 6 | + | |
| 7 | +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; | |
| 8 | +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; | |
| 9 | +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; | |
| 10 | +/*!40101 SET NAMES utf8 */; | |
| 11 | +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; | |
| 12 | +/*!40103 SET TIME_ZONE='+00:00' */; | |
| 13 | +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; | |
| 14 | +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; | |
| 15 | +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; | |
| 16 | +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; | |
| 17 | + | |
| 18 | +-- | |
| 19 | +-- Current Database: `STRONGLOOP` | |
| 20 | +-- | |
| 21 | + | |
| 22 | +/*!40000 DROP DATABASE IF EXISTS `STRONGLOOP`*/; | |
| 23 | + | |
| 24 | +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `STRONGLOOP` /*!40100 DEFAULT CHARACTER SET utf8 */; | |
| 25 | + | |
| 26 | +USE `STRONGLOOP`; | |
| 27 | + | |
| 28 | +-- | |
| 29 | +-- Table structure for table `CUSTOMER` | |
| 30 | +-- | |
| 31 | + | |
| 32 | +DROP TABLE IF EXISTS `CUSTOMER`; | |
| 33 | +/*!40101 SET @saved_cs_client = @@character_set_client */; | |
| 34 | +/*!40101 SET character_set_client = utf8 */; | |
| 35 | +CREATE TABLE `CUSTOMER` ( | |
| 36 | + `ID` varchar(20) NOT NULL, | |
| 37 | + `NAME` varchar(40) DEFAULT NULL, | |
| 38 | + `MILITARY_AGENCY` varchar(20) DEFAULT NULL, | |
| 39 | + PRIMARY KEY (`ID`) | |
| 40 | +) ENGINE=MyISAM DEFAULT CHARSET=utf8; | |
| 41 | +/*!40101 SET character_set_client = @saved_cs_client */; | |
| 42 | + | |
| 43 | +-- | |
| 44 | +-- Dumping data for table `CUSTOMER` | |
| 45 | +-- | |
| 46 | + | |
| 47 | +LOCK TABLES `CUSTOMER` WRITE; | |
| 48 | +/*!40000 ALTER TABLE `CUSTOMER` DISABLE KEYS */; | |
| 49 | +/*!40000 ALTER TABLE `CUSTOMER` ENABLE KEYS */; | |
| 50 | +UNLOCK TABLES; | |
| 51 | + | |
| 52 | +-- | |
| 53 | +-- Table structure for table `INVENTORY` | |
| 54 | +-- | |
| 55 | + | |
| 56 | +DROP TABLE IF EXISTS `INVENTORY`; | |
| 57 | +/*!40101 SET @saved_cs_client = @@character_set_client */; | |
| 58 | +/*!40101 SET character_set_client = utf8 */; | |
| 59 | +CREATE TABLE `INVENTORY` ( | |
| 60 | + `PRODUCT_ID` varchar(20) NOT NULL, | |
| 61 | + `LOCATION_ID` varchar(20) NOT NULL, | |
| 62 | + `AVAILABLE` int(11) DEFAULT NULL, | |
| 63 | + `TOTAL` int(11) DEFAULT NULL, | |
| 64 | + `ACTIVE` BOOLEAN DEFAULT TRUE, | |
| 65 | + `DISABLED` BIT(1) DEFAULT 0, | |
| 66 | + `ENABLED` CHAR(1) DEFAULT 'Y', | |
| 67 | + PRIMARY KEY (`PRODUCT_ID`,`LOCATION_ID`), | |
| 68 | + KEY `LOCATION_FK` (`LOCATION_ID`) | |
| 69 | +) ENGINE=MyISAM DEFAULT CHARSET=utf8; | |
| 70 | +/*!40101 SET character_set_client = @saved_cs_client */; | |
| 71 | + | |
| 72 | +-- | |
| 73 | +-- Dumping data for table `INVENTORY` | |
| 74 | +-- | |
| 75 | + | |
| 76 | +LOCK TABLES `INVENTORY` WRITE; | |
| 77 | +/*!40000 ALTER TABLE `INVENTORY` DISABLE KEYS */; | |
| 78 | +/*!40000 ALTER TABLE `INVENTORY` ENABLE KEYS */; | |
| 79 | +UNLOCK TABLES; | |
| 80 | + | |
| 81 | +-- | |
| 82 | +-- Temporary view structure for view `INVENTORY_VIEW` | |
| 83 | +-- | |
| 84 | + | |
| 85 | +DROP TABLE IF EXISTS `INVENTORY_VIEW`; | |
| 86 | +/*!50001 DROP VIEW IF EXISTS `INVENTORY_VIEW`*/; | |
| 87 | +SET @saved_cs_client = @@character_set_client; | |
| 88 | +SET character_set_client = utf8; | |
| 89 | +/*!50001 CREATE VIEW `INVENTORY_VIEW` AS SELECT | |
| 90 | + 1 AS `ID`, | |
| 91 | + 1 AS `PRODUCT_ID`, | |
| 92 | + 1 AS `PRODUCT_NAME`, | |
| 93 | + 1 AS `AUDIBLE_RANGE`, | |
| 94 | + 1 AS `EFFECTIVE_RANGE`, | |
| 95 | + 1 AS `ROUNDS`, | |
| 96 | + 1 AS `EXTRAS`, | |
| 97 | + 1 AS `FIRE_MODES`, | |
| 98 | + 1 AS `LOCATION_ID`, | |
| 99 | + 1 AS `LOCATION`, | |
| 100 | + 1 AS `CITY`, | |
| 101 | + 1 AS `ZIPCODE`, | |
| 102 | + 1 AS `AVAILABLE`*/; | |
| 103 | +SET character_set_client = @saved_cs_client; | |
| 104 | + | |
| 105 | +-- | |
| 106 | +-- Table structure for table `LOCATION` | |
| 107 | +-- | |
| 108 | + | |
| 109 | +DROP TABLE IF EXISTS `LOCATION`; | |
| 110 | +/*!40101 SET @saved_cs_client = @@character_set_client */; | |
| 111 | +/*!40101 SET character_set_client = utf8 */; | |
| 112 | +CREATE TABLE `LOCATION` ( | |
| 113 | + `ID` varchar(20) NOT NULL, | |
| 114 | + `STREET` varchar(20) DEFAULT NULL, | |
| 115 | + `CITY` varchar(20) DEFAULT NULL, | |
| 116 | + `ZIPCODE` varchar(20) DEFAULT NULL, | |
| 117 | + `NAME` varchar(20) DEFAULT NULL, | |
| 118 | + PRIMARY KEY (`ID`) | |
| 119 | +) ENGINE=MyISAM DEFAULT CHARSET=utf8; | |
| 120 | +/*!40101 SET character_set_client = @saved_cs_client */; | |
| 121 | + | |
| 122 | +-- | |
| 123 | +-- Dumping data for table `LOCATION` | |
| 124 | +-- | |
| 125 | + | |
| 126 | +LOCK TABLES `LOCATION` WRITE; | |
| 127 | +/*!40000 ALTER TABLE `LOCATION` DISABLE KEYS */; | |
| 128 | +/*!40000 ALTER TABLE `LOCATION` ENABLE KEYS */; | |
| 129 | +UNLOCK TABLES; | |
| 130 | + | |
| 131 | +-- | |
| 132 | +-- Table structure for table `PRODUCT` | |
| 133 | +-- | |
| 134 | + | |
| 135 | +DROP TABLE IF EXISTS `PRODUCT`; | |
| 136 | +/*!40101 SET @saved_cs_client = @@character_set_client */; | |
| 137 | +/*!40101 SET character_set_client = utf8 */; | |
| 138 | +CREATE TABLE `PRODUCT` ( | |
| 139 | + `ID` varchar(20) NOT NULL, | |
| 140 | + `NAME` varchar(64) DEFAULT NULL, | |
| 141 | + `AUDIBLE_RANGE` decimal(12,2) DEFAULT NULL, | |
| 142 | + `EFFECTIVE_RANGE` decimal(12,2) DEFAULT NULL, | |
| 143 | + `ROUNDS` decimal(10,0) DEFAULT NULL, | |
| 144 | + `EXTRAS` varchar(64) DEFAULT NULL, | |
| 145 | + `FIRE_MODES` varchar(64) DEFAULT NULL, | |
| 146 | + PRIMARY KEY (`ID`) | |
| 147 | +) ENGINE=MyISAM DEFAULT CHARSET=utf8; | |
| 148 | +/*!40101 SET character_set_client = @saved_cs_client */; | |
| 149 | + | |
| 150 | +-- | |
| 151 | +-- Dumping data for table `PRODUCT` | |
| 152 | +-- | |
| 153 | + | |
| 154 | +LOCK TABLES `PRODUCT` WRITE; | |
| 155 | +/*!40000 ALTER TABLE `PRODUCT` DISABLE KEYS */; | |
| 156 | +/*!40000 ALTER TABLE `PRODUCT` ENABLE KEYS */; | |
| 157 | +UNLOCK TABLES; | |
| 158 | + | |
| 159 | +-- | |
| 160 | +-- Table structure for table `RESERVATION` | |
| 161 | +-- | |
| 162 | + | |
| 163 | +DROP TABLE IF EXISTS `RESERVATION`; | |
| 164 | +/*!40101 SET @saved_cs_client = @@character_set_client */; | |
| 165 | +/*!40101 SET character_set_client = utf8 */; | |
| 166 | +CREATE TABLE `RESERVATION` ( | |
| 167 | + `ID` varchar(20) NOT NULL, | |
| 168 | + `PRODUCT_ID` varchar(20) NOT NULL, | |
| 169 | + `LOCATION_ID` varchar(20) NOT NULL, | |
| 170 | + `CUSTOMER_ID` varchar(20) NOT NULL, | |
| 171 | + `QTY` int(11) DEFAULT NULL, | |
| 172 | + `STATUS` varchar(20) DEFAULT NULL, | |
| 173 | + `RESERVE_DATE` date DEFAULT NULL, | |
| 174 | + `PICKUP_DATE` date DEFAULT NULL, | |
| 175 | + `RETURN_DATE` date DEFAULT NULL, | |
| 176 | + PRIMARY KEY (`ID`), | |
| 177 | + KEY `RESERVATION_PRODUCT_FK` (`PRODUCT_ID`), | |
| 178 | + KEY `RESERVATION_LOCATION_FK` (`LOCATION_ID`), | |
| 179 | + KEY `RESERVATION_CUSTOMER_FK` (`CUSTOMER_ID`) | |
| 180 | +) ENGINE=MyISAM DEFAULT CHARSET=utf8; | |
| 181 | +/*!40101 SET character_set_client = @saved_cs_client */; | |
| 182 | + | |
| 183 | +-- | |
| 184 | +-- Dumping data for table `RESERVATION` | |
| 185 | +-- | |
| 186 | + | |
| 187 | +LOCK TABLES `RESERVATION` WRITE; | |
| 188 | +/*!40000 ALTER TABLE `RESERVATION` DISABLE KEYS */; | |
| 189 | +/*!40000 ALTER TABLE `RESERVATION` ENABLE KEYS */; | |
| 190 | +UNLOCK TABLES; | |
| 191 | + | |
| 192 | +-- | |
| 193 | +-- Table structure for table `TESTGEN` | |
| 194 | +-- | |
| 195 | + | |
| 196 | +DROP TABLE IF EXISTS `TESTGEN`; | |
| 197 | +/*!40101 SET @saved_cs_client = @@character_set_client */; | |
| 198 | +/*!40101 SET character_set_client = utf8 */; | |
| 199 | +CREATE TABLE `TESTGEN` ( | |
| 200 | + `ID` int(11) NOT NULL AUTO_INCREMENT, | |
| 201 | + `NAME` varchar(64) DEFAULT NULL, | |
| 202 | + PRIMARY KEY (`ID`) | |
| 203 | +) ENGINE=MyISAM DEFAULT CHARSET=utf8; | |
| 204 | +/*!40101 SET character_set_client = @saved_cs_client */; | |
| 205 | + | |
| 206 | +-- | |
| 207 | +-- Current Database: `STRONGLOOP` | |
| 208 | +-- | |
| 209 | + | |
| 210 | +USE `STRONGLOOP`; | |
| 211 | + | |
| 212 | +-- | |
| 213 | +-- Final view structure for view `INVENTORY_VIEW` | |
| 214 | +-- | |
| 215 | + | |
| 216 | +/*!50001 DROP VIEW IF EXISTS `INVENTORY_VIEW`*/; | |
| 217 | +/*!50001 SET @saved_cs_client = @@character_set_client */; | |
| 218 | +/*!50001 SET @saved_cs_results = @@character_set_results */; | |
| 219 | +/*!50001 SET @saved_col_connection = @@collation_connection */; | |
| 220 | +/*!50001 SET character_set_client = utf8 */; | |
| 221 | +/*!50001 SET character_set_results = utf8 */; | |
| 222 | +/*!50001 SET collation_connection = utf8_general_ci */; | |
| 223 | +/*!50001 CREATE ALGORITHM=UNDEFINED */ | |
| 224 | +/*!50013 DEFINER=`strongloop`@`%` SQL SECURITY DEFINER */ | |
| 225 | +/*!50001 VIEW `INVENTORY_VIEW` AS select concat(concat(`P`.`ID`,':'),`L`.`ID`) AS `ID`,`P`.`ID` AS `PRODUCT_ID`,`P`.`NAME` AS `PRODUCT_NAME`,`P`.`AUDIBLE_RANGE` AS `AUDIBLE_RANGE`,`P`.`EFFECTIVE_RANGE` AS `EFFECTIVE_RANGE`,`P`.`ROUNDS` AS `ROUNDS`,`P`.`EXTRAS` AS `EXTRAS`,`P`.`FIRE_MODES` AS `FIRE_MODES`,`L`.`ID` AS `LOCATION_ID`,`L`.`NAME` AS `LOCATION`,`L`.`CITY` AS `CITY`,`L`.`ZIPCODE` AS `ZIPCODE`,`I`.`AVAILABLE` AS `AVAILABLE` from ((`INVENTORY` `I` join `PRODUCT` `P`) join `LOCATION` `L`) where ((`P`.`ID` = `I`.`PRODUCT_ID`) and (`L`.`ID` = `I`.`LOCATION_ID`)) */; | |
| 226 | +/*!50001 SET character_set_client = @saved_cs_client */; | |
| 227 | +/*!50001 SET character_set_results = @saved_cs_results */; | |
| 228 | +/*!50001 SET collation_connection = @saved_col_connection */; | |
| 229 | +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; | |
| 230 | + | |
| 231 | +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; | |
| 232 | +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; | |
| 233 | +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; | |
| 234 | +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; | |
| 235 | +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; | |
| 236 | +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; | |
| 237 | +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; | |
| 238 | + | |
| 239 | +-- Dump completed on 2016-08-09 19:14:01 | ... | ... |
| 1 | +++ a/test/transaction.promise.test.js | |
| ... | ... | @@ -0,0 +1,177 @@ |
| 1 | +// Copyright IBM Corp. 2015,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +if (typeof Promise === 'undefined') { | |
| 8 | + global.Promise = require('bluebird'); | |
| 9 | +} | |
| 10 | +var Transaction = require('loopback-datasource-juggler').Transaction; | |
| 11 | +require('./init.js'); | |
| 12 | +require('should'); | |
| 13 | + | |
| 14 | +var db, Post, Review; | |
| 15 | + | |
| 16 | +describe('transactions with promise', function() { | |
| 17 | + before(function(done) { | |
| 18 | + db = getDataSource({collation: 'utf8_general_ci', createDatabase: true}); | |
| 19 | + db.once('connected', function() { | |
| 20 | + Post = db.define('PostTX', { | |
| 21 | + title: {type: String, length: 255, index: true}, | |
| 22 | + content: {type: String}, | |
| 23 | + }, {mysql: {engine: 'INNODB'}}); | |
| 24 | + Review = db.define('ReviewTX', { | |
| 25 | + author: String, | |
| 26 | + content: {type: String}, | |
| 27 | + }, {mysql: {engine: 'INNODB'}}); | |
| 28 | + Post.hasMany(Review, {as: 'reviews', foreignKey: 'postId'}); | |
| 29 | + db.automigrate(['PostTX', 'ReviewTX'], done); | |
| 30 | + }); | |
| 31 | + }); | |
| 32 | + | |
| 33 | + var currentTx; | |
| 34 | + var hooks = []; | |
| 35 | + // Return an async function to start a transaction and create a post | |
| 36 | + function createPostInTx(post, timeout) { | |
| 37 | + return function(done) { | |
| 38 | + // Transaction.begin(db.connector, Transaction.READ_COMMITTED, | |
| 39 | + var promise = Post.beginTransaction({ | |
| 40 | + isolationLevel: Transaction.READ_COMMITTED, | |
| 41 | + timeout: timeout, | |
| 42 | + }); | |
| 43 | + promise.then(function(tx) { | |
| 44 | + (typeof tx.id).should.be.eql('string'); | |
| 45 | + currentTx = tx; | |
| 46 | + hooks = []; | |
| 47 | + tx.observe('before commit', function(context, next) { | |
| 48 | + hooks.push('before commit'); | |
| 49 | + next(); | |
| 50 | + }); | |
| 51 | + tx.observe('after commit', function(context, next) { | |
| 52 | + hooks.push('after commit'); | |
| 53 | + next(); | |
| 54 | + }); | |
| 55 | + tx.observe('before rollback', function(context, next) { | |
| 56 | + hooks.push('before rollback'); | |
| 57 | + next(); | |
| 58 | + }); | |
| 59 | + tx.observe('after rollback', function(context, next) { | |
| 60 | + hooks.push('after rollback'); | |
| 61 | + next(); | |
| 62 | + }); | |
| 63 | + }).then(function() { | |
| 64 | + Post.create(post, {transaction: currentTx}).then( | |
| 65 | + function(p) { | |
| 66 | + p.reviews.create({ | |
| 67 | + author: 'John', | |
| 68 | + content: 'Review for ' + p.title, | |
| 69 | + }, {transaction: currentTx}).then( | |
| 70 | + function(c) { | |
| 71 | + done(null, c); | |
| 72 | + }); | |
| 73 | + }); | |
| 74 | + }).catch(done); | |
| 75 | + }; | |
| 76 | + } | |
| 77 | + | |
| 78 | +// Return an async function to find matching posts and assert number of | |
| 79 | +// records to equal to the count | |
| 80 | + function expectToFindPosts(where, count, inTx) { | |
| 81 | + return function(done) { | |
| 82 | + var options = {}; | |
| 83 | + if (inTx) { | |
| 84 | + options.transaction = currentTx; | |
| 85 | + } | |
| 86 | + Post.find({where: where}, options).then( | |
| 87 | + function(posts) { | |
| 88 | + posts.length.should.be.eql(count); | |
| 89 | + if (count) { | |
| 90 | + // Find related reviews | |
| 91 | + // Please note the empty {} is required, otherwise, the options | |
| 92 | + // will be treated as a filter | |
| 93 | + posts[0].reviews({}, options).then(function(reviews) { | |
| 94 | + reviews.length.should.be.eql(count); | |
| 95 | + done(); | |
| 96 | + }); | |
| 97 | + } else { | |
| 98 | + done(); | |
| 99 | + } | |
| 100 | + }).catch(done); | |
| 101 | + }; | |
| 102 | + } | |
| 103 | + | |
| 104 | + describe('commit', function() { | |
| 105 | + var post = {title: 't1', content: 'c1'}; | |
| 106 | + before(createPostInTx(post)); | |
| 107 | + | |
| 108 | + it('should not see the uncommitted insert', expectToFindPosts(post, 0)); | |
| 109 | + | |
| 110 | + it('should see the uncommitted insert from the same transaction', | |
| 111 | + expectToFindPosts(post, 1, true)); | |
| 112 | + | |
| 113 | + it('should commit a transaction', function(done) { | |
| 114 | + currentTx.commit().then(function() { | |
| 115 | + hooks.should.be.eql(['before commit', 'after commit']); | |
| 116 | + done(); | |
| 117 | + }).catch(done); | |
| 118 | + }); | |
| 119 | + | |
| 120 | + it('should see the committed insert', expectToFindPosts(post, 1)); | |
| 121 | + | |
| 122 | + it('should report error if the transaction is not active', function(done) { | |
| 123 | + currentTx.commit().catch(function(err) { | |
| 124 | + (err).should.be.instanceof(Error); | |
| 125 | + done(); | |
| 126 | + }); | |
| 127 | + }); | |
| 128 | + }); | |
| 129 | + | |
| 130 | + describe('rollback', function() { | |
| 131 | + var post = {title: 't2', content: 'c2'}; | |
| 132 | + before(createPostInTx(post)); | |
| 133 | + | |
| 134 | + it('should not see the uncommitted insert', expectToFindPosts(post, 0)); | |
| 135 | + | |
| 136 | + it('should see the uncommitted insert from the same transaction', | |
| 137 | + expectToFindPosts(post, 1, true)); | |
| 138 | + | |
| 139 | + it('should rollback a transaction', function(done) { | |
| 140 | + currentTx.rollback().then(function() { | |
| 141 | + hooks.should.be.eql(['before rollback', 'after rollback']); | |
| 142 | + done(); | |
| 143 | + }).catch(done); | |
| 144 | + }); | |
| 145 | + | |
| 146 | + it('should not see the rolledback insert', expectToFindPosts(post, 0)); | |
| 147 | + | |
| 148 | + it('should report error if the transaction is not active', function(done) { | |
| 149 | + currentTx.rollback().catch(function(err) { | |
| 150 | + (err).should.be.instanceof(Error); | |
| 151 | + done(); | |
| 152 | + }); | |
| 153 | + }); | |
| 154 | + }); | |
| 155 | + | |
| 156 | + describe('timeout', function() { | |
| 157 | + var post = {title: 't3', content: 'c3'}; | |
| 158 | + before(createPostInTx(post, 500)); | |
| 159 | + | |
| 160 | + it('should invoke the timeout hook', function(done) { | |
| 161 | + currentTx.observe('timeout', function(context, next) { | |
| 162 | + next(); | |
| 163 | + // It will only proceed upon timeout | |
| 164 | + done(); | |
| 165 | + }); | |
| 166 | + }); | |
| 167 | + | |
| 168 | + it('should rollback the transaction if timeout', function(done) { | |
| 169 | + Post.find({where: {title: 't3'}}, {transaction: currentTx}, | |
| 170 | + function(err, posts) { | |
| 171 | + if (err) return done(err); | |
| 172 | + posts.length.should.be.eql(0); | |
| 173 | + done(); | |
| 174 | + }); | |
| 175 | + }); | |
| 176 | + }); | |
| 177 | +}); | ... | ... |
| 1 | +++ a/test/transaction.test.js | |
| ... | ... | @@ -0,0 +1,180 @@ |
| 1 | +// Copyright IBM Corp. 2015,2016. All Rights Reserved. | |
| 2 | +// Node module: loopback-connector-mysql | |
| 3 | +// This file is licensed under the MIT License. | |
| 4 | +// License text available at https://opensource.org/licenses/MIT | |
| 5 | + | |
| 6 | +'use strict'; | |
| 7 | +var Transaction = require('loopback-datasource-juggler').Transaction; | |
| 8 | +require('./init.js'); | |
| 9 | +require('should'); | |
| 10 | + | |
| 11 | +var db, Post, Review; | |
| 12 | + | |
| 13 | +describe('transactions', function() { | |
| 14 | + before(function(done) { | |
| 15 | + db = getDataSource({collation: 'utf8_general_ci', createDatabase: true}); | |
| 16 | + db.once('connected', function() { | |
| 17 | + Post = db.define('PostTX', { | |
| 18 | + title: {type: String, length: 255, index: true}, | |
| 19 | + content: {type: String}, | |
| 20 | + }, {mysql: {engine: 'INNODB'}}); | |
| 21 | + Review = db.define('ReviewTX', { | |
| 22 | + author: String, | |
| 23 | + content: {type: String}, | |
| 24 | + }, {mysql: {engine: 'INNODB'}}); | |
| 25 | + Post.hasMany(Review, {as: 'reviews', foreignKey: 'postId'}); | |
| 26 | + db.automigrate(['PostTX', 'ReviewTX'], done); | |
| 27 | + }); | |
| 28 | + }); | |
| 29 | + | |
| 30 | + var currentTx; | |
| 31 | + var hooks = []; | |
| 32 | + // Return an async function to start a transaction and create a post | |
| 33 | + function createPostInTx(post, timeout) { | |
| 34 | + return function(done) { | |
| 35 | + // Transaction.begin(db.connector, Transaction.READ_COMMITTED, | |
| 36 | + Post.beginTransaction({ | |
| 37 | + isolationLevel: Transaction.READ_COMMITTED, | |
| 38 | + timeout: timeout, | |
| 39 | + }, | |
| 40 | + function(err, tx) { | |
| 41 | + if (err) return done(err); | |
| 42 | + (typeof tx.id).should.be.eql('string'); | |
| 43 | + hooks = []; | |
| 44 | + tx.observe('before commit', function(context, next) { | |
| 45 | + hooks.push('before commit'); | |
| 46 | + next(); | |
| 47 | + }); | |
| 48 | + tx.observe('after commit', function(context, next) { | |
| 49 | + hooks.push('after commit'); | |
| 50 | + next(); | |
| 51 | + }); | |
| 52 | + tx.observe('before rollback', function(context, next) { | |
| 53 | + hooks.push('before rollback'); | |
| 54 | + next(); | |
| 55 | + }); | |
| 56 | + tx.observe('after rollback', function(context, next) { | |
| 57 | + hooks.push('after rollback'); | |
| 58 | + next(); | |
| 59 | + }); | |
| 60 | + currentTx = tx; | |
| 61 | + Post.create(post, {transaction: tx}, | |
| 62 | + function(err, p) { | |
| 63 | + if (err) { | |
| 64 | + done(err); | |
| 65 | + } else { | |
| 66 | + p.reviews.create({ | |
| 67 | + author: 'John', | |
| 68 | + content: 'Review for ' + p.title, | |
| 69 | + }, {transaction: tx}, | |
| 70 | + function(err, c) { | |
| 71 | + done(err); | |
| 72 | + }); | |
| 73 | + } | |
| 74 | + }); | |
| 75 | + }); | |
| 76 | + }; | |
| 77 | + } | |
| 78 | + | |
| 79 | + // Return an async function to find matching posts and assert number of | |
| 80 | + // records to equal to the count | |
| 81 | + function expectToFindPosts(where, count, inTx) { | |
| 82 | + return function(done) { | |
| 83 | + var options = {}; | |
| 84 | + if (inTx) { | |
| 85 | + options.transaction = currentTx; | |
| 86 | + } | |
| 87 | + Post.find({where: where}, options, | |
| 88 | + function(err, posts) { | |
| 89 | + if (err) return done(err); | |
| 90 | + posts.length.should.be.eql(count); | |
| 91 | + if (count) { | |
| 92 | + // Find related reviews | |
| 93 | + // Please note the empty {} is required, otherwise, the options | |
| 94 | + // will be treated as a filter | |
| 95 | + posts[0].reviews({}, options, function(err, reviews) { | |
| 96 | + if (err) return done(err); | |
| 97 | + reviews.length.should.be.eql(count); | |
| 98 | + done(); | |
| 99 | + }); | |
| 100 | + } else { | |
| 101 | + done(); | |
| 102 | + } | |
| 103 | + }); | |
| 104 | + }; | |
| 105 | + } | |
| 106 | + | |
| 107 | + describe('commit', function() { | |
| 108 | + var post = {title: 't1', content: 'c1'}; | |
| 109 | + before(createPostInTx(post)); | |
| 110 | + | |
| 111 | + it('should not see the uncommitted insert', expectToFindPosts(post, 0)); | |
| 112 | + | |
| 113 | + it('should see the uncommitted insert from the same transaction', | |
| 114 | + expectToFindPosts(post, 1, true)); | |
| 115 | + | |
| 116 | + it('should commit a transaction', function(done) { | |
| 117 | + currentTx.commit(function(err) { | |
| 118 | + hooks.should.be.eql(['before commit', 'after commit']); | |
| 119 | + done(err); | |
| 120 | + }); | |
| 121 | + }); | |
| 122 | + | |
| 123 | + it('should see the committed insert', expectToFindPosts(post, 1)); | |
| 124 | + | |
| 125 | + it('should report error if the transaction is not active', function(done) { | |
| 126 | + currentTx.commit(function(err) { | |
| 127 | + (err).should.be.instanceof(Error); | |
| 128 | + done(); | |
| 129 | + }); | |
| 130 | + }); | |
| 131 | + }); | |
| 132 | + | |
| 133 | + describe('rollback', function() { | |
| 134 | + var post = {title: 't2', content: 'c2'}; | |
| 135 | + before(createPostInTx(post)); | |
| 136 | + | |
| 137 | + it('should not see the uncommitted insert', expectToFindPosts(post, 0)); | |
| 138 | + | |
| 139 | + it('should see the uncommitted insert from the same transaction', | |
| 140 | + expectToFindPosts(post, 1, true)); | |
| 141 | + | |
| 142 | + it('should rollback a transaction', function(done) { | |
| 143 | + currentTx.rollback(function(err) { | |
| 144 | + hooks.should.be.eql(['before rollback', 'after rollback']); | |
| 145 | + done(err); | |
| 146 | + }); | |
| 147 | + }); | |
| 148 | + | |
| 149 | + it('should not see the rolledback insert', expectToFindPosts(post, 0)); | |
| 150 | + | |
| 151 | + it('should report error if the transaction is not active', function(done) { | |
| 152 | + currentTx.rollback(function(err) { | |
| 153 | + (err).should.be.instanceof(Error); | |
| 154 | + done(); | |
| 155 | + }); | |
| 156 | + }); | |
| 157 | + }); | |
| 158 | + | |
| 159 | + describe('timeout', function() { | |
| 160 | + var post = {title: 't3', content: 'c3'}; | |
| 161 | + before(createPostInTx(post, 500)); | |
| 162 | + | |
| 163 | + it('should invoke the timeout hook', function(done) { | |
| 164 | + currentTx.observe('timeout', function(context, next) { | |
| 165 | + next(); | |
| 166 | + // It will only proceed upon timeout | |
| 167 | + done(); | |
| 168 | + }); | |
| 169 | + }); | |
| 170 | + | |
| 171 | + it('should rollback the transaction if timeout', function(done) { | |
| 172 | + Post.find({where: {title: 't3'}}, {transaction: currentTx}, | |
| 173 | + function(err, posts) { | |
| 174 | + if (err) return done(err); | |
| 175 | + posts.length.should.be.eql(0); | |
| 176 | + done(); | |
| 177 | + }); | |
| 178 | + }); | |
| 179 | + }); | |
| 180 | +}); | ... | ... |