Commit 5034c7fcb7f57d5537f4c30ad32b2a945355c31a

Authored by Arsisakarn Srilatanart
0 parents
Exists in master

mysql date timezone offset from timezone setting (default local timezone get by moments)

.eslintrc 0 โ†’ 100644
  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 + }
... ...
.github/ISSUE_TEMPLATE.md 0 โ†’ 100644
  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 +
... ...
.github/PULL_REQUEST_TEMPLATE.md 0 โ†’ 100644
  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)
... ...
.gitignore 0 โ†’ 100644
  1 +++ a/.gitignore
... ... @@ -0,0 +1,7 @@
  1 +node_modules
  2 +coverage
  3 +*.tgz
  4 +*.xml
  5 +.loopbackrc
  6 +.idea
  7 +
... ...
.travis.yml 0 โ†’ 100644
  1 +++ a/.travis.yml
... ... @@ -0,0 +1,7 @@
  1 +language: node_js
  2 +node_js:
  3 + - 0.6
  4 + - 0.8
  5 + - 0.10
  6 +before_script:
  7 + - "mysql -e 'create database myapp_test;'"
... ...
CHANGES.md 0 โ†’ 100644
  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!
... ...
CONTRIBUTING.md 0 โ†’ 100644
  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
... ...
LICENSE 0 โ†’ 100644
  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.
... ...
NOTICE.md 0 โ†’ 100644
  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 +
... ...
README.md 0 โ†’ 100644
  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`
... ...
example/app.js 0 โ†’ 100644
  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 +});
... ...
example/table.sql 0 โ†’ 100644
No preview for this file type
index.js 0 โ†’ 100644
  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');
... ...
intl/de/messages.json 0 โ†’ 100644
  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 +
... ...
intl/en/messages.json 0 โ†’ 100644
  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 +}
... ...
intl/es/messages.json 0 โ†’ 100644
  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 +
... ...
intl/fr/messages.json 0 โ†’ 100644
  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 +
... ...
intl/it/messages.json 0 โ†’ 100644
  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 +
... ...
intl/ja/messages.json 0 โ†’ 100644
  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 +
... ...
intl/ko/messages.json 0 โ†’ 100644
  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 +
... ...
intl/nl/messages.json 0 โ†’ 100644
  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 +
... ...
intl/pt/messages.json 0 โ†’ 100644
  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 +
... ...
intl/tr/messages.json 0 โ†’ 100644
  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 +
... ...
intl/zh-Hans/messages.json 0 โ†’ 100644
  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 +
... ...
intl/zh-Hant/messages.json 0 โ†’ 100644
  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 +
... ...
lib/discovery.js 0 โ†’ 100644
  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 +}
... ...
lib/enumFactory.js 0 โ†’ 100644
  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;
... ...
lib/migration.js 0 โ†’ 100644
  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 +}
... ...
lib/mysql.js 0 โ†’ 100644
  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);
... ...
lib/transaction.js 0 โ†’ 100644
  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 +}
... ...
package.json 0 โ†’ 100644
  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 +}
... ...
pretest.js 0 โ†’ 100644
  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 +});
... ...
test/connection.test.js 0 โ†’ 100644
  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 +}
... ...
test/datatypes.test.js 0 โ†’ 100644
  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 +};
... ...
test/helpers/platform.js 0 โ†’ 100644
  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);
... ...
test/imported.test.js 0 โ†’ 100644
  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 +});
... ...
test/init.js 0 โ†’ 100644
  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');
... ...
test/migration.test.js 0 โ†’ 100644
  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 +};
... ...
test/mocha.opts 0 โ†’ 100644
  1 +++ a/test/mocha.opts
... ... @@ -0,0 +1,2 @@
  1 +--globals getSchema
  2 +--timeout 15000
... ...
test/mysql.autoupdate.test.js 0 โ†’ 100644
  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 +});
... ...
test/mysql.discover.test.js 0 โ†’ 100644
  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 +});
... ...
test/mysql.test.js 0 โ†’ 100644
  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 +});
... ...
test/persistence-hooks.test.js 0 โ†’ 100644
  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 +});
... ...
test/schema.sql 0 โ†’ 100644
  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
... ...
test/transaction.promise.test.js 0 โ†’ 100644
  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 +});
... ...
test/transaction.test.js 0 โ†’ 100644
  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 +});
... ...