From 5034c7fcb7f57d5537f4c30ad32b2a945355c31a Mon Sep 17 00:00:00 2001 From: Arsisakarn Srilatanart Date: Tue, 21 Mar 2017 17:12:29 +0700 Subject: [PATCH] mysql date timezone offset from timezone setting (default local timezone get by moments) --- .eslintrc | 13 +++++++++++++ .github/ISSUE_TEMPLATE.md | 36 ++++++++++++++++++++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 24 ++++++++++++++++++++++++ .gitignore | 7 +++++++ .travis.yml | 7 +++++++ CHANGES.md | 436 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 25 +++++++++++++++++++++++++ NOTICE.md | 24 ++++++++++++++++++++++++ README.md | 362 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ example/app.js | 41 +++++++++++++++++++++++++++++++++++++++++ example/table.sql | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 10 ++++++++++ intl/de/messages.json | 11 +++++++++++ intl/en/messages.json | 10 ++++++++++ intl/es/messages.json | 11 +++++++++++ intl/fr/messages.json | 11 +++++++++++ intl/it/messages.json | 11 +++++++++++ intl/ja/messages.json | 11 +++++++++++ intl/ko/messages.json | 11 +++++++++++ intl/nl/messages.json | 11 +++++++++++ intl/pt/messages.json | 11 +++++++++++ intl/tr/messages.json | 11 +++++++++++ intl/zh-Hans/messages.json | 11 +++++++++++ intl/zh-Hant/messages.json | 11 +++++++++++ lib/discovery.js | 395 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/enumFactory.js | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/migration.js | 900 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/mysql.js | 563 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/transaction.js | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 37 +++++++++++++++++++++++++++++++++++++ pretest.js | 43 +++++++++++++++++++++++++++++++++++++++++++ test/connection.test.js | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/datatypes.test.js | 155 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/helpers/platform.js | 8 ++++++++ test/imported.test.js | 14 ++++++++++++++ test/init.js | 42 ++++++++++++++++++++++++++++++++++++++++++ test/migration.test.js | 559 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/mocha.opts | 2 ++ test/mysql.autoupdate.test.js | 542 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/mysql.discover.test.js | 433 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/mysql.test.js | 760 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/persistence-hooks.test.js | 12 ++++++++++++ test/schema.sql | 239 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/transaction.promise.test.js | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/transaction.test.js | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 46 files changed, 6812 insertions(+), 0 deletions(-) create mode 100644 .eslintrc create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CHANGES.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 NOTICE.md create mode 100644 README.md create mode 100644 example/app.js create mode 100644 example/table.sql create mode 100644 index.js create mode 100644 intl/de/messages.json create mode 100644 intl/en/messages.json create mode 100644 intl/es/messages.json create mode 100644 intl/fr/messages.json create mode 100644 intl/it/messages.json create mode 100644 intl/ja/messages.json create mode 100644 intl/ko/messages.json create mode 100644 intl/nl/messages.json create mode 100644 intl/pt/messages.json create mode 100644 intl/tr/messages.json create mode 100644 intl/zh-Hans/messages.json create mode 100644 intl/zh-Hant/messages.json create mode 100644 lib/discovery.js create mode 100644 lib/enumFactory.js create mode 100644 lib/migration.js create mode 100644 lib/mysql.js create mode 100644 lib/transaction.js create mode 100644 package.json create mode 100644 pretest.js create mode 100644 test/connection.test.js create mode 100644 test/datatypes.test.js create mode 100644 test/helpers/platform.js create mode 100644 test/imported.test.js create mode 100644 test/init.js create mode 100644 test/migration.test.js create mode 100644 test/mocha.opts create mode 100644 test/mysql.autoupdate.test.js create mode 100644 test/mysql.discover.test.js create mode 100644 test/mysql.test.js create mode 100644 test/persistence-hooks.test.js create mode 100644 test/schema.sql create mode 100644 test/transaction.promise.test.js create mode 100644 test/transaction.test.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e702b2b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,13 @@ +{ + "extends": "loopback", + "rules": { + "max-len": ["error", 120, 4, { + "ignoreComments": true, + "ignoreUrls": true, + "ignorePattern": "^\\s*var\\s.=\\s*(require\\s*\\()|(/)" + }], + "camelcase": 0, + "one-var": "off", + "no-unused-expressions": "off" + } + } diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..ccc915a --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,36 @@ + + +### Bug or feature request + + + +- [ ] Bug +- [ ] Feature request + +### Description of feature (or steps to reproduce if bug) + + + +### Link to sample repo to reproduce issue (if bug) + + + +### Expected result + + + +### Actual result (if bug) + + + +### Additional information (Node.js version, LoopBack version, etc) + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d2b240f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,24 @@ +### Description + + +#### Related issues + + + +- None + +### Checklist + + + +- [ ] New tests added or existing tests modified to cover all changes +- [ ] Code conforms with the [style + guide](http://loopback.io/doc/en/contrib/style-guide.html) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8011db9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +coverage +*.tgz +*.xml +.loopbackrc +.idea + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..169b61c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - 0.6 + - 0.8 + - 0.10 +before_script: + - "mysql -e 'create database myapp_test;'" diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..e7e35dc --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,436 @@ +2017-01-13, Version 3.0.0 +========================= + + * Follow mysql recommendations for handling booleans (Carl Fürstenberg) + + * Fix readme glitch (#231) (Rand McKinney) + + * Update readme w info from docs (#229) (Rand McKinney) + + * Fix expected column name when autoupdate (muhammad hasan) + + * Update paid support URL (Siddhi Pai) + + * Fix CI Failures (Loay Gewily) + + * Drop support for Node v0.10 and v0.12 (Siddhi Pai) + + * Start the development of the next major version (Siddhi Pai) + + * Update README with correct doc links, etc (Amir Jafarian) + + +2016-10-17, Version 2.4.0 +========================= + + * Add connectorCapabilities global object (#201) (Nicholas Duffy) + + * Remove unused prefix for test env vars (#203) (Simon Ho) + + * Update translation files - round#2 (#199) (Candy) + + * Add CI fixes (#197) (Loay) + + * Add translated files (gunjpan) + + * Update deps to loopback 3.0.0 RC (Miroslav Bajtoš) + + * Remove Makefile in favour of NPM test scripts (Simon Ho) + + * Fixing lint errors (Ron Lloyd) + + * Autoupdate mysql.columnName bug fix (Ron Lloyd) + + * Tests for autoupdate mysql.columnName bug fix (Ron Lloyd) + + * Use juggler@3 for running the tests (Miroslav Bajtoš) + + * Explictly set forceId:false in test model (Miroslav Bajtoš) + + * Fix pretest and init test configs (Simon Ho) + + * Fix to configure model index in keys field (deepakrkris) + + * Update eslint infrastructure (Loay) + + * test: use dump of original test DB as seed (Ryan Graham) + + * test: skip cardinality, update sub_part (Ryan Graham) + + * test: accept alternate test db credentials (Ryan Graham) + + * test: use should for easier debugging (Ryan Graham) + + * test: account for mysql version differences (Ryan Graham) + + * test: match case with example/table.sql (Ryan Graham) + + * test: separate assertions from test flow control (Ryan Graham) + + * test: update tests to use example DB (Ryan Graham) + + * test: seed test DB with example (Ryan Graham) + + * test: fix undefined password (Ryan Graham) + + * Add special handling of zero date/time entries (Carl Fürstenberg) + + * Add globalization (Candy) + + * Update URLs in CONTRIBUTING.md (#176) (Ryan Graham) + + +2016-06-21, Version 2.3.0 +========================= + + * Add function connect (juehou) + + * insert/update copyright notices (Ryan Graham) + + * relicense as MIT only (Ryan Graham) + + * Override other settings if url provided (juehou) + + * Add `connectorCapabilities ` (Amir Jafarian) + + * Implement ReplaceOrCreate (Amir Jafarian) + + +2016-02-19, Version 2.2.1 +========================= + + * Remove sl-blip from dependencies (Miroslav Bajtoš) + + * Upgrade `should` module (Amir Jafarian) + + * removed console.log (cgole) + + * seperate env variable for test db (cgole) + + * Changed username to user (cgole) + + * Added db username password (cgole) + + * Add mysql CI host (cgole) + + * Refer to licenses with a link (Sam Roberts) + + * Pass options to the execute command. (Diogo Correia) + + * Use strongloop conventions for licensing (Sam Roberts) + + +2015-07-30, Version 2.2.0 +========================= + + * Clean up regexop tests (Simon Ho) + + * Add regexp operator tests (Simon Ho) + + * Fix RegExp unit test setup/teardown (Simon Ho) + + * Add support for RegExp operator (Simon Ho) + + +2015-05-29, Version 2.1.1 +========================= + + * Fix the failing tests (Raymond Feng) + + +2015-05-18, Version 2.1.0 +========================= + + * Update deps (Raymond Feng) + + * Start to add transaction support (Raymond Feng) + + +2015-05-14, Version 2.0.1 +========================= + + * Fix the typo (Raymond Feng) + + +2015-05-13, Version 2.0.0 +========================= + + * Update deps (Raymond Feng) + + * Refactor the code to use base SqlConnector (Raymond Feng) + + +2015-04-02, Version 1.7.0 +========================= + + * Return isNewInstance from upsert (Raymond Feng) + + * Update rc dep (Raymond Feng) + + * Return count when updating or deleting models (Simon Ho) + + * Update README.md (Simon Ho) + + * Add test running instructions to readme (Simon Ho) + + * Fix mysql neq for NULL value. (ulion) + + * replace dataLength instead of adding length property (Partap Davis) + + * Allow models backed by MySQL to reference mongodb ObjectID (Raymond Feng) + + * Query string length for schema in characters in addition to bytes (Partap Davis) + + +2015-02-20, Version 1.6.0 +========================= + + * Update deps (Raymond Feng) + + * Include tests of persistence hooks from juggler. (Miroslav Bajtoš) + + +2015-01-15, Version 1.5.1 +========================= + + * Fix the loop of models (Raymond Feng) + + * Set ok default to false (Geoffroy Lesage) + + * Fixed missing 'ok' (Geoffroy Lesage) + + * Changed default type mapping (Geoffroy Lesage) + + * Fixed isActual syntax to accept optional model arg (Geoffroy Lesage) + + * Fixed isActual implemenation (Geoffroy Lesage) + + * Inherit Schema From DataSource if not defined (Serkan Serttop) + + +2015-01-09, Version 1.5.0 +========================= + + * Use mysql.escape/escapeId() (Raymond Feng) + + * Fix bad CLA URL in CONTRIBUTING.md (Ryan Graham) + + * (cherry picked from commit a6d31e8) (yogesh) + + +2014-12-05, Version 1.4.9 +========================= + + * Add a test case for autoupdate (Raymond Feng) + + * Create 'NOT NULL' constraint for required or id properties (Raymond Feng) + + * Better handle discovery of nullable columns (Raymond Feng) + + +2014-11-27, Version 1.4.8 +========================= + + * fix(initialization): bug fix for setting limit on number of connections in connection pool (cpentra1) + + * Add contribution guidelines (Ryan Graham) + + +2014-09-11, Version 1.4.7 +========================= + + * Enhance error reporting for automigrate/autoupdate (Raymond Feng) + + +2014-09-10, Version 1.4.6 +========================= + + * Bump version (Raymond Feng) + + * Use table name instead of model name (Raymond Feng) + + * Use async and make sure errors are passed to callback (Raymond Feng) + + +2014-08-25, Version 1.4.5 +========================= + + * Bump version (Raymond Feng) + + * Make sure the deferred query will be invoked only once (Raymond Feng) + + +2014-08-20, Version 1.4.4 +========================= + + * Bump version (Raymond Feng) + + * Add ping() (Raymond Feng) + + +2014-08-20, Version 1.4.3 +========================= + + * Bump version (Raymond Feng) + + * Fix MySQL conversion for embedded model instance (Raymond Feng) + + * Fix the createDatabase option (Raymond Feng) + + +2014-08-15, Version 1.4.2 +========================= + + * Bump version (Raymond Feng) + + * Allow properties to pass through mysql driver (Raymond Feng) + + * Fix the default length for strings to avoid row size overflow (Raymond Feng) + + +2014-06-27, Version 1.4.1 +========================= + + * Bump version (Raymond Feng) + + * Fix the test cases as now inq/nin is checked for array values (Raymond Feng) + + * Update link to doc (Rand McKinney) + + +2014-06-23, Version 1.4.0 +========================= + + * Bump version (Raymond Feng) + + * cannot read property of undefined fixed (Johnny Bill) + + * Fix comparison for null and boolean values (Raymond Feng) + + * Map object/json to TEXT (Raymond Feng) + + +2014-06-04, Version 1.3.0 +========================= + + * Remove peer dependency on datasource-juggler (Miroslav Bajtoš) + + +2014-06-02, Version 1.2.3 +========================= + + * Bump version (Raymond Feng) + + * Fix sql injection and add test cases (Raymond Feng) + + +2014-05-29, Version 1.2.2 +========================= + + * Bump version (Raymond Feng) + + * Fix the varchar length (Raymond Feng) + + * Add like/nlike support (Raymond Feng) + + * Fix object/json type mapping (Raymond Feng) + + +2014-05-16, Version 1.2.1 +========================= + + * Bump versions (Raymond Feng) + + * Fix buildWhere (Raymond Feng) + + * Add support for logical operators (AND/OR) (Raymond Feng) + + * updateOrCreate assumes numeric primary key(s) (Scott Anderson) + + +2014-04-08, Version 1.2.0 +========================= + + * Bump version (Raymond Feng) + + * Remove the commented out code (Raymond Feng) + + * Fix the query for discovery with current user (Raymond Feng) + + * Fix the table generation for string ids (Raymond Feng) + + * Update deps (Raymond Feng) + + * Use NULL for undefined (Raymond Feng) + + * Prevent inserting undefined values (Marat Dyatko) + + * Update to dual MIT/StrongLoop license (Raymond Feng) + + * Fix merge issue (Raymond Feng) + + * Reformat code (Raymond Feng) + + * Update discovery.js (Samer Aldefai) + + * Fix link to docs. (Rand McKinney) + + * Replaced most content with link to docs. (Rand McKinney) + + * Move mocha args to test/mocha.opts (Ryan Graham) + + * Make 'npm test' more useful to CI (Ryan Graham) + + * Prevent extra files from going into npm (Ryan Graham) + + +2013-12-06, Version 1.1.1 +========================= + + * Bump version (Raymond Feng) + + * Update deps (Raymond Feng) + + * Add the test for loopback-datasource-juggler PR-48 (Raymond Feng) + + * Fix the orderBy (Raymond Feng) + + +2013-11-27, Version 1.1.0 +========================= + + * Bump version (Raymond Feng) + + * Refactor the runQuery logic into a function (Raymond Feng) + + * Improve the connector based on review feedbacks (Raymond Feng) + + * Allow connectionLmit to be set (Raymond Feng) + + * Use connection pool for MySQL (Raymond Feng) + + * Update docs.json (Rand McKinney) + + * Fix the regression caused by juggler (Raymond Feng) + + +2013-11-20, Version 1.0.2 +========================= + + * Remove blanket (Raymond Feng) + + * Bump version and update deps (Raymond Feng) + + * Append error to the message (Raymond Feng) + + * Add NOTICE and update READE (Raymond Feng) + + * Update README.md (Rand McKinney) + + * Update the internal github dependency (Raymond Feng) + + +2013-10-28, Version 1.0.0 +========================= + + * First release! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5687a63 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,151 @@ +### Contributing ### + +Thank you for your interest in `loopback-connector-mysql`, an open source project +administered by StrongLoop. + +Contributing to `loopback-connector-mysql` is easy. In a few simple steps: + + * Ensure that your effort is aligned with the project's roadmap by + talking to the maintainers, especially if you are going to spend a + lot of time on it. + + * Make something better or fix a bug. + + * Adhere to code style outlined in the [Google C++ Style Guide][] and + [Google Javascript Style Guide][]. + + * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback-connector-mysql) + + * Submit a pull request through Github. + + +### Contributor License Agreement ### + +``` + Individual Contributor License Agreement + + By signing this Individual Contributor License Agreement + ("Agreement"), and making a Contribution (as defined below) to + StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and + agree to the following terms and conditions for Your present and + future Contributions submitted to StrongLoop. Except for the license + granted in this Agreement to StrongLoop and recipients of software + distributed by StrongLoop, You reserve all right, title, and interest + in and to Your Contributions. + + 1. Definitions + + "You" or "Your" shall mean the copyright owner or the individual + authorized by the copyright owner that is entering into this + Agreement with StrongLoop. + + "Contribution" shall mean any original work of authorship, + including any modifications or additions to an existing work, that + is intentionally submitted by You to StrongLoop for inclusion in, + or documentation of, any of the products owned or managed by + StrongLoop ("Work"). For purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication + sent to StrongLoop or its representatives, including but not + limited to communication or electronic mailing lists, source code + control systems, and issue tracking systems that are managed by, + or on behalf of, StrongLoop for the purpose of discussing and + improving the Work, but excluding communication that is + conspicuously marked or otherwise designated in writing by You as + "Not a Contribution." + + 2. You Grant a Copyright License to StrongLoop + + Subject to the terms and conditions of this Agreement, You hereby + grant to StrongLoop and recipients of software distributed by + StrongLoop, a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable copyright license to reproduce, prepare + derivative works of, publicly display, publicly perform, + sublicense, and distribute Your Contributions and such derivative + works under any license and without any restrictions. + + 3. You Grant a Patent License to StrongLoop + + Subject to the terms and conditions of this Agreement, You hereby + grant to StrongLoop and to recipients of software distributed by + StrongLoop a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable (except as stated in this Section) + patent license to make, have made, use, offer to sell, sell, + import, and otherwise transfer the Work under any license and + without any restrictions. The patent license You grant to + StrongLoop under this Section applies only to those patent claims + licensable by You that are necessarily infringed by Your + Contributions(s) alone or by combination of Your Contributions(s) + with the Work to which such Contribution(s) was submitted. If any + entity institutes a patent litigation against You or any other + entity (including a cross-claim or counterclaim in a lawsuit) + alleging that Your Contribution, or the Work to which You have + contributed, constitutes direct or contributory patent + infringement, any patent licenses granted to that entity under + this Agreement for that Contribution or Work shall terminate as + of the date such litigation is filed. + + 4. You Have the Right to Grant Licenses to StrongLoop + + You represent that You are legally entitled to grant the licenses + in this Agreement. + + If Your employer(s) has rights to intellectual property that You + create, You represent that You have received permission to make + the Contributions on behalf of that employer, that Your employer + has waived such rights for Your Contributions, or that Your + employer has executed a separate Corporate Contributor License + Agreement with StrongLoop. + + 5. The Contributions Are Your Original Work + + You represent that each of Your Contributions are Your original + works of authorship (see Section 8 (Submissions on Behalf of + Others) for submission on behalf of others). You represent that to + Your knowledge, no other person claims, or has the right to claim, + any right in any intellectual property right related to Your + Contributions. + + You also represent that You are not legally obligated, whether by + entering into an agreement or otherwise, in any way that conflicts + with the terms of this Agreement. + + You represent that Your Contribution submissions include complete + details of any third-party license or other restriction (including, + but not limited to, related patents and trademarks) of which You + are personally aware and which are associated with any part of + Your Contributions. + + 6. You Don't Have an Obligation to Provide Support for Your Contributions + + You are not expected to provide support for Your Contributions, + except to the extent You desire to provide support. You may provide + support for free, for a fee, or not at all. + + 6. No Warranties or Conditions + + StrongLoop acknowledges that unless required by applicable law or + agreed to in writing, You provide Your Contributions on an "AS IS" + BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER + EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES + OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR + FITNESS FOR A PARTICULAR PURPOSE. + + 7. Submission on Behalf of Others + + If You wish to submit work that is not Your original creation, You + may submit it to StrongLoop separately from any Contribution, + identifying the complete details of its source and of any license + or other restriction (including, but not limited to, related + patents, trademarks, and license agreements) of which You are + personally aware, and conspicuously marking the work as + "Submitted on Behalf of a Third-Party: [named here]". + + 8. Agree to Notify of Change of Circumstances + + You agree to notify StrongLoop of any facts or circumstances of + which You become aware that would make these representations + inaccurate in any respect. Email us at callback@strongloop.com. +``` + +[Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html +[Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4be3520 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2012,2016. All Rights Reserved. +Node module: loopback-connector-mysql +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..c6f2244 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,24 @@ +The project was initially forked from [mysql-adapter](https://github.com/jugglingdb/mysql-adapter) +which carries the following copyright and permission notices: + + + Copyright (C) 2012 by Anatoliy Chakkaev + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..174da5f --- /dev/null +++ b/README.md @@ -0,0 +1,362 @@ +# loopback-connector-mysql + +[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. + +
See also LoopBack MySQL Connector in LoopBack documentation. +

+NOTE: The MySQL connector requires MySQL 5.0+. +
+ +## Installation + +In your application root directory, enter this command to install the connector: + +```sh +npm install loopback-connector-mysql --save +``` + +This installs the module from npm and adds it as a dependency to the application's `package.json` file. + +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. + +## Creating a MySQL data source + +Use the [Data source generator](http://loopback.io/doc/en/lb3/Data-source-generator.html) to add a MySQL data source to your application. +The generator will prompt for the database server hostname, port, and other settings +required to connect to a MySQL database. It will also run the `npm install` command above for you. + +The entry in the application's `/server/datasources.json` will look like this: + +```javascript +"mydb": { + "name": "mydb", + "connector": "mysql", + "host": "myserver", + "port": 3306, + "database": "mydb", + "password": "mypassword", + "user": "admin" + } +``` + +Edit `datasources.json` to add any other additional properties that you require. + +### Properties + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyTypeDescription
collationStringDetermines the charset for the connection. Default is utf8_general_ci.
connectorStringConnector name, either “loopback-connector-mysql” or “mysql”.
connectionLimitNumberThe maximum number of connections to create at once. Default is 10.
databaseStringDatabase name
debugBooleanIf true, turn on verbose mode to debug database queries and lifecycle.
hostStringDatabase host name
passwordStringPassword to connect to database
portNumberDatabase TCP port
socketPathStringThe path to a unix domain socket to connect to. When used host and port are ignored.
supportBigNumbersBooleanEnable this option to deal with big numbers (BIGINT and DECIMAL columns) in the database. Default is false.
timeZoneStringThe timezone used to store local dates. Default is ‘local’.
urlStringConnection URL of form mysql://user:password@host/db. Overrides other connection settings.
usernameStringUsername to connect to database
+ +**NOTE**: In addition to these properties, you can use additional parameters supported by [`node-mysql`](https://github.com/felixge/node-mysql). + +## Type mappings + +See [LoopBack types](http://loopback.io/doc/en/lb3/LoopBack-types.html) for details on LoopBack's data types. + +### LoopBack to MySQL types + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LoopBack TypeMySQL Type
String/JSONVARCHAR
TextTEXT
NumberINT
DateDATETIME
BooleanTINYINT(1)
GeoPoint objectPOINT
Custom Enum type
(See Enum below)
ENUM
+ +### MySQL to LoopBack types + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MySQL TypeLoopBack Type
CHARString
CHAR(1)Boolean
VARCHAR
TINYTEXT
MEDIUMTEXT
LONGTEXT
TEXT
ENUM
SET
String
TINYBLOB
MEDIUMBLOB
LONGBLOB
BLOB
BINARY
VARBINARY
BIT
Node.js Buffer object
TINYINT
SMALLINT
INT
MEDIUMINT
YEAR
FLOAT
DOUBLE
NUMERIC
DECIMAL
+

Number
For FLOAT and DOUBLE, see Floating-point types.

+

For NUMERIC and DECIMAL, see Fixed-point exact value types

+
DATE
TIMESTAMP
DATETIME
Date
+ +## Using the datatype field/column option with MySQL + +Use the `mysql` model property to specify additional MySQL-specific properties for a LoopBack model. + +For example: + +{% include code-caption.html content="/common/models/model.json" %} +```javascript +"locationId":{ + "type":"String", + "required":true, + "length":20, + "mysql": + { + "columnName":"LOCATION_ID", + "dataType":"VARCHAR", + "dataLength":20, + "nullable":"N" + } +} +``` + +You can also use the dataType column/property attribute to specify what MySQL column type to use for many loopback-datasource-juggler types.  +The following type-dataType combinations are supported: + +* Number +* integer +* tinyint +* smallint +* mediumint +* int +* bigint + +Use the `limit` option to alter the display width. Example: + +```javascript +{ userName : { + type: String, + dataType: 'char', + limit: 24 + } +} +``` + +### Floating-point types + +For Float and Double data types, use the `precision` and `scale` options to specify custom precision. Default is (16,8). For example: + +```javascript +{ average : + { type: Number, + dataType: 'float', + precision: 20, + scale: 4 + } +} +``` + +### Fixed-point exact value types + +For Decimal and Numeric types, use the `precision` and `scale` options to specify custom precision. Default is (9,2). +These aren't likely to function as true fixed-point. + +Example: + +```javascript +{ stdDev : + { type: Number, + dataType: 'decimal', + precision: 12, + scale: 8 + } +} +``` + +### Other types + +Convert String / DataSource.Text / DataSource.JSON to the following MySQL types: + +* varchar +* char +* text +* mediumtext +* tinytext +* longtext + +Example:  + +```javascript +{ userName : + { type: String, + dataType: 'char', + limit: 24 + } +} +``` + +Example:  + +```javascript +{ biography : + { type: String, + dataType: 'longtext' + } +} +``` + +Convert JSON Date types to  datetime or timestamp + +Example:  + +```javascript +{ startTime : + { type: Date, + dataType: 'timestamp' + } +} +``` + +### Enum + +Enums are special. Create an Enum using Enum factory: + +```javascript +var MOOD = dataSource.EnumFactory('glad', 'sad', 'mad');  +MOOD.SAD; // 'sad'  +MOOD(2); // 'sad'  +MOOD('SAD'); // 'sad'  +MOOD('sad'); // 'sad' +{ mood: { type: MOOD }} +{ choice: { type: dataSource.EnumFactory('yes', 'no', 'maybe'), null: false }} +``` + +## Discovery and auto-migration + +### Model discovery + +The MySQL connector supports _model discovery_ that enables you to create LoopBack models +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). + +### Auto-migratiion + +The MySQL connector also supports _auto-migration_ that enables you to create a database schema +from LoopBack models using the [LoopBack automigrate method](http://apidocs.strongloop.com/loopback-datasource-juggler/#datasource-prototype-automigrate). + +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. + +Destroying models may result in errors due to foreign key integrity. First delete any related models first calling delete on models with relationships. + +## Running tests + +The tests in this repository are mainly integration tests, meaning you will need to run them using our preconfigured test server. + +1. Ask a core developer for instructions on how to set up test server credentials on your machine +2. `npm test` diff --git a/example/app.js b/example/app.js new file mode 100644 index 0000000..f4e68b2 --- /dev/null +++ b/example/app.js @@ -0,0 +1,41 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +var DataSource = require('loopback-datasource-juggler').DataSource; + +var config = require('rc')('loopback', {dev: {mysql: {}}}).dev.mysql; + +var ds = new DataSource(require('../'), config); + +function show(err, models) { + if (err) { + console.error(err); + } else { + console.log(models); + if (models) { + models.forEach(function(m) { + console.dir(m); + }); + } + } +} + +ds.discoverModelDefinitions({views: true, limit: 20}, show); + +ds.discoverModelProperties('customer', show); + +ds.discoverModelProperties('location', {owner: 'strongloop'}, show); + +ds.discoverPrimaryKeys('customer', show); +ds.discoverForeignKeys('inventory', show); + +ds.discoverExportedForeignKeys('location', show); + +ds.discoverAndBuildModels('weapon', {owner: 'strongloop', visited: {}, associations: true}, function(err, models) { + for (var m in models) { + models[m].all(show); + } +}); diff --git a/example/table.sql b/example/table.sql new file mode 100644 index 0000000..96cefec --- /dev/null +++ b/example/table.sql @@ -0,0 +1,173 @@ +CREATE DATABASE IF NOT EXISTS `strongloop` /*!40100 DEFAULT CHARACTER SET utf8 */; +USE `strongloop`; +-- MySQL dump 10.13 Distrib 5.5.24, for osx10.5 (i386) +-- +-- Host: 192.237.213.245 Database: strongloop +-- ------------------------------------------------------ +-- Server version 5.1.67 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `customer` +-- + +DROP TABLE IF EXISTS `customer`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `customer` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `militaryAgency` varchar(255) DEFAULT NULL, + `realm` varchar(255) DEFAULT NULL, + `username` varchar(255) DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + `emailVerified` tinyint(1) DEFAULT NULL, + `verificationToken` varchar(255) DEFAULT NULL, + `credentials` varchar(255) DEFAULT NULL, + `challenges` varchar(255) DEFAULT NULL, + `status` varchar(255) DEFAULT NULL, + `created` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `customer` +-- + +LOCK TABLES `customer` WRITE; +/*!40000 ALTER TABLE `customer` DISABLE KEYS */; +INSERT INTO `customer` VALUES (1,NULL,NULL,NULL,'foo','$2a$10$sQ6/CEKSQpEc3hVrY8kb8ug/Spy80rlUYJ3fQ/33w3.kpq5SD4zqG','foo@bar.com',NULL,NULL,'[]','[]',NULL,NULL,NULL),(2,NULL,NULL,NULL,'bar','$2a$10$IdlCSes8/BsNbZAaV2xhwOtRp5722Uexbdo1VpITidk8VFBaaw2sW','bar@bar.com',NULL,NULL,'[]','[]',NULL,NULL,NULL),(3,NULL,NULL,NULL,'bat','$2a$10$PEOrqotDnWRhYYU9ungKM.gTuAvvRTT70y0Be8/Wm4VdCCecm38tW','bat@bar.com',NULL,NULL,'[]','[]',NULL,NULL,NULL),(4,NULL,NULL,NULL,'baz','$2a$10$MWchndR.QMPEIvyBpIJT3.2CRKNpSmPkhWfTIDiSpBnAn2iN/lH42','baz@bar.com',NULL,NULL,'[]','[]',NULL,NULL,NULL); +/*!40000 ALTER TABLE `customer` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `inventory` +-- + +DROP TABLE IF EXISTS `inventory`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `inventory` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `productId` varchar(255) DEFAULT NULL, + `locationId` varchar(255) DEFAULT NULL, + `available` int(11) DEFAULT NULL, + `total` int(11) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=518 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `inventory` +-- + +LOCK TABLES `inventory` WRITE; +/*!40000 ALTER TABLE `inventory` DISABLE KEYS */; +INSERT INTO `inventory` VALUES (1,'1','5',43,56),(2,'2','5',43,56),(3,'3','5',27,85),(4,'4','5',18,30),(5,'5','5',3,38),(6,'6','5',10,21),(7,'7','5',43,58),(8,'8','5',6,12),(9,'9','5',0,3),(10,'10','5',0,31),(11,'11','5',73,93),(12,'12','5',22,25),(13,'13','5',44,70),(14,'14','5',26,50),(15,'15','5',36,83),(16,'16','5',20,59),(17,'17','5',28,44),(18,'18','5',5,50),(19,'19','5',2,29),(20,'20','5',38,54),(21,'21','5',4,29),(22,'22','5',1,59),(23,'23','5',20,36),(24,'24','5',10,10),(25,'25','5',58,60),(26,'26','5',0,18),(27,'27','5',29,50),(28,'28','5',24,34),(29,'29','5',36,43),(30,'30','5',43,64),(31,'31','5',79,90),(32,'32','5',13,13),(33,'33','5',9,60),(34,'34','5',7,13),(35,'35','5',43,54),(36,'36','5',67,69),(37,'37','5',1,15),(38,'38','5',36,44),(39,'39','5',1,17),(40,'40','5',13,16),(41,'41','5',24,64),(42,'42','5',87,99),(43,'43','5',27,99),(44,'44','5',71,71),(45,'45','5',9,20),(46,'46','5',9,67),(47,'47','5',19,21),(48,'48','5',5,5),(49,'49','5',82,91),(50,'50','5',27,42),(51,'51','5',51,60),(52,'52','5',8,72),(53,'53','5',5,13),(54,'54','5',3,71),(55,'55','5',55,56),(56,'56','5',9,90),(57,'57','5',3,18),(58,'58','5',2,14),(59,'59','5',54,95),(60,'60','5',62,70),(61,'61','5',18,50),(62,'62','5',60,78),(63,'63','5',23,59),(64,'64','5',14,23),(65,'65','5',2,97),(66,'66','5',49,50),(67,'67','5',47,93),(68,'68','5',34,42),(69,'69','5',3,18),(70,'70','5',37,84),(71,'71','5',22,40),(72,'72','5',8,61),(73,'73','5',2,3),(74,'74','5',10,16),(75,'75','5',53,89),(76,'76','5',35,60),(77,'77','5',57,80),(78,'78','5',53,81),(79,'79','5',32,54),(80,'80','5',1,4),(81,'81','5',78,86),(82,'82','5',11,21),(83,'83','5',28,81),(84,'84','5',2,57),(85,'85','5',30,37),(86,'86','5',17,80),(87,'5','5',1,9),(88,'2','88',10,10),(89,'3','88',1,1),(90,'4','88',8,27),(91,'5','88',3,38),(92,'6','88',28,76),(93,'7','88',40,83),(94,'8','88',1,4),(95,'9','88',87,95),(96,'10','88',29,35),(97,'11','88',10,69),(98,'12','88',32,86),(99,'13','88',27,28),(100,'14','88',59,66),(101,'15','88',59,70),(102,'16','88',43,70),(103,'17','88',50,63),(104,'18','88',8,20),(105,'19','88',20,29),(106,'20','88',36,50),(107,'21','88',40,63),(108,'22','88',4,96),(109,'23','88',70,98),(110,'24','88',1,1),(111,'25','88',17,45),(112,'26','88',52,97),(113,'27','88',0,0),(114,'28','88',97,98),(115,'29','88',26,80),(116,'30','88',11,33),(117,'31','88',10,21),(118,'32','88',14,36),(119,'33','88',71,86),(120,'34','88',85,100),(121,'35','88',3,45),(122,'36','88',0,3),(123,'37','88',17,71),(124,'38','88',41,75),(125,'39','88',37,41),(126,'40','88',37,49),(127,'41','88',1,2),(128,'42','88',49,72),(129,'43','88',24,38),(130,'44','88',6,66),(131,'45','88',31,49),(132,'46','88',9,10),(133,'47','88',57,72),(134,'48','88',17,24),(135,'49','88',41,61),(136,'50','88',33,87),(137,'51','88',11,25),(138,'52','88',1,8),(139,'53','88',14,64),(140,'54','88',50,89),(141,'55','88',16,66),(142,'56','88',0,6),(143,'57','88',18,32),(144,'58','88',6,6),(145,'59','88',68,84),(146,'60','88',50,74),(147,'61','88',7,18),(148,'62','88',14,49),(149,'63','88',3,3),(150,'64','88',21,83),(151,'65','88',48,90),(152,'66','88',11,65),(153,'67','88',29,90),(154,'68','88',44,45),(155,'69','88',23,30),(156,'70','88',53,71),(157,'71','88',50,76),(158,'72','88',13,20),(159,'73','88',6,8),(160,'74','88',7,11),(161,'75','88',0,3),(162,'76','88',49,51),(163,'77','88',37,61),(164,'78','88',4,78),(165,'79','88',1,5),(166,'80','88',23,29),(167,'81','88',3,52),(168,'82','88',1,2),(169,'83','88',65,67),(170,'84','88',41,87),(171,'85','88',20,21),(172,'86','88',46,94),(173,'5','88',64,68),(174,'2','89',5,78),(175,'3','89',29,41),(176,'4','89',2,39),(177,'5','89',57,67),(178,'6','89',1,2),(179,'7','89',68,80),(180,'8','89',22,81),(181,'9','89',9,52),(182,'10','89',26,42),(183,'11','89',42,91),(184,'12','89',23,35),(185,'13','89',38,59),(186,'14','89',43,51),(187,'15','89',19,29),(188,'16','89',21,29),(189,'17','89',18,47),(190,'18','89',26,52),(191,'19','89',18,61),(192,'20','89',33,97),(193,'21','89',1,35),(194,'22','89',41,65),(195,'23','89',16,41),(196,'24','89',26,32),(197,'25','89',0,11),(198,'26','89',30,52),(199,'27','89',29,73),(200,'28','89',26,86),(201,'29','89',48,48),(202,'30','89',0,68),(203,'31','89',25,32),(204,'32','89',37,80),(205,'33','89',12,43),(206,'34','89',34,89),(207,'35','89',54,97),(208,'36','89',2,18),(209,'37','89',13,16),(210,'38','89',19,54),(211,'39','89',16,40),(212,'40','89',10,93),(213,'41','89',35,39),(214,'42','89',24,25),(215,'43','89',5,55),(216,'44','89',9,43),(217,'45','89',36,82),(218,'46','89',5,8),(219,'47','89',18,20),(220,'48','89',2,9),(221,'49','89',34,91),(222,'50','89',27,55),(223,'51','89',11,72),(224,'52','89',8,13),(225,'53','89',4,31),(226,'54','89',1,1),(227,'55','89',7,19),(228,'56','89',3,35),(229,'57','89',58,73),(230,'58','89',2,32),(231,'59','89',51,64),(232,'60','89',34,79),(233,'61','89',44,66),(234,'62','89',37,46),(235,'63','89',10,11),(236,'64','89',15,74),(237,'65','89',8,19),(238,'66','89',13,26),(239,'67','89',0,1),(240,'68','89',5,17),(241,'69','89',0,0),(242,'70','89',1,48),(243,'71','89',13,70),(244,'72','89',24,68),(245,'73','89',21,48),(246,'74','89',17,68),(247,'75','89',72,72),(248,'76','89',6,24),(249,'77','89',18,22),(250,'78','89',8,24),(251,'79','89',26,31),(252,'80','89',14,19),(253,'81','89',10,31),(254,'82','89',88,92),(255,'83','89',5,11),(256,'84','89',13,72),(257,'85','89',18,37),(258,'86','89',6,12),(259,'5','89',79,99),(260,'2','90',10,19),(261,'3','90',3,6),(262,'4','90',8,38),(263,'5','90',26,54),(264,'6','90',20,73),(265,'7','90',30,95),(266,'8','90',32,93),(267,'9','90',4,18),(268,'10','90',32,94),(269,'11','90',57,80),(270,'12','90',3,6),(271,'13','90',40,58),(272,'14','90',54,91),(273,'15','90',10,11),(274,'16','90',34,58),(275,'17','90',34,99),(276,'18','90',72,90),(277,'19','90',13,76),(278,'20','90',37,71),(279,'21','90',21,39),(280,'22','90',4,20),(281,'23','90',11,73),(282,'24','90',18,100),(283,'25','90',26,62),(284,'26','90',0,1),(285,'27','90',10,28),(286,'28','90',68,78),(287,'29','90',10,73),(288,'30','90',73,96),(289,'31','90',35,75),(290,'32','90',58,88),(291,'33','90',14,26),(292,'34','90',22,24),(293,'35','90',23,72),(294,'36','90',23,59),(295,'37','90',3,6),(296,'38','90',51,71),(297,'39','90',48,60),(298,'40','90',44,56),(299,'41','90',25,36),(300,'42','90',32,83),(301,'43','90',77,92),(302,'44','90',30,38),(303,'45','90',43,49),(304,'46','90',23,27),(305,'47','90',78,84),(306,'48','90',26,48),(307,'49','90',15,52),(308,'50','90',4,45),(309,'51','90',53,77),(310,'52','90',5,6),(311,'53','90',17,30),(312,'54','90',4,44),(313,'55','90',12,20),(314,'56','90',15,25),(315,'57','90',1,33),(316,'58','90',22,34),(317,'59','90',6,12),(318,'60','90',3,9),(319,'61','90',41,59),(320,'62','90',16,32),(321,'63','90',7,15),(322,'64','90',49,95),(323,'65','90',41,45),(324,'66','90',18,45),(325,'67','90',11,39),(326,'68','90',26,84),(327,'69','90',3,4),(328,'70','90',72,98),(329,'71','90',26,28),(330,'72','90',2,2),(331,'73','90',57,90),(332,'74','90',12,75),(333,'75','90',23,37),(334,'76','90',22,22),(335,'77','90',30,86),(336,'78','90',44,82),(337,'79','90',13,17),(338,'80','90',38,45),(339,'81','90',26,91),(340,'82','90',34,41),(341,'83','90',19,43),(342,'84','90',43,43),(343,'85','90',34,69),(344,'86','90',10,25),(345,'5','90',18,34),(346,'2','91',25,98),(347,'3','91',15,28),(348,'4','91',56,97),(349,'5','91',20,30),(350,'6','91',8,19),(351,'7','91',21,23),(352,'8','91',35,63),(353,'9','91',0,7),(354,'10','91',0,2),(355,'11','91',1,6),(356,'12','91',67,77),(357,'13','91',7,51),(358,'14','91',39,96),(359,'15','91',36,74),(360,'16','91',15,73),(361,'17','91',36,63),(362,'18','91',0,19),(363,'19','91',24,94),(364,'20','91',8,38),(365,'21','91',41,58),(366,'22','91',18,22),(367,'23','91',25,37),(368,'24','91',39,60),(369,'25','91',30,55),(370,'26','91',4,4),(371,'27','91',6,17),(372,'28','91',63,82),(373,'29','91',30,76),(374,'30','91',13,31),(375,'31','91',10,59),(376,'32','91',39,80),(377,'33','91',69,89),(378,'34','91',62,93),(379,'35','91',13,27),(380,'36','91',8,22),(381,'37','91',0,31),(382,'38','91',9,79),(383,'39','91',6,49),(384,'40','91',39,40),(385,'41','91',1,22),(386,'42','91',12,82),(387,'43','91',1,7),(388,'44','91',15,26),(389,'45','91',22,31),(390,'46','91',64,65),(391,'47','91',10,99),(392,'48','91',26,56),(393,'49','91',14,19),(394,'50','91',51,55),(395,'51','91',25,29),(396,'52','91',31,37),(397,'53','91',35,71),(398,'54','91',5,61),(399,'55','91',4,26),(400,'56','91',29,50),(401,'57','91',15,34),(402,'58','91',30,38),(403,'59','91',54,71),(404,'60','91',6,43),(405,'61','91',40,80),(406,'62','91',32,33),(407,'63','91',44,53),(408,'64','91',10,68),(409,'65','91',11,13),(410,'66','91',7,40),(411,'67','91',5,20),(412,'68','91',30,40),(413,'69','91',6,48),(414,'70','91',7,53),(415,'71','91',2,21),(416,'72','91',25,56),(417,'73','91',13,85),(418,'74','91',63,67),(419,'75','91',9,11),(420,'76','91',18,46),(421,'77','91',7,88),(422,'78','91',36,55),(423,'79','91',8,33),(424,'80','91',63,73),(425,'81','91',36,71),(426,'82','91',2,5),(427,'83','91',11,11),(428,'84','91',21,39),(429,'85','91',90,91),(430,'86','91',1,2),(431,'5','91',36,47),(432,'2','92',6,7),(433,'3','92',15,23),(434,'4','92',1,1),(435,'5','92',37,42),(436,'6','92',22,24),(437,'7','92',12,13),(438,'8','92',4,25),(439,'9','92',32,87),(440,'10','92',9,31),(441,'11','92',2,38),(442,'12','92',66,88),(443,'13','92',4,15),(444,'14','92',9,88),(445,'15','92',18,72),(446,'16','92',13,26),(447,'17','92',20,55),(448,'18','92',17,76),(449,'19','92',28,58),(450,'20','92',78,99),(451,'21','92',7,12),(452,'22','92',4,13),(453,'23','92',12,96),(454,'24','92',82,84),(455,'25','92',29,64),(456,'26','92',5,7),(457,'27','92',3,35),(458,'28','92',23,46),(459,'29','92',21,39),(460,'30','92',19,21),(461,'31','92',24,73),(462,'32','92',51,89),(463,'33','92',22,32),(464,'34','92',56,95),(465,'35','92',47,95),(466,'36','92',17,24),(467,'37','92',0,0),(468,'38','92',14,53),(469,'39','92',65,67),(470,'40','92',64,95),(471,'41','92',5,5),(472,'42','92',7,10),(473,'43','92',34,45),(474,'44','92',0,3),(475,'45','92',20,67),(476,'46','92',58,92),(477,'47','92',21,70),(478,'48','92',56,62),(479,'49','92',0,5),(480,'50','92',16,97),(481,'51','92',6,46),(482,'52','92',58,84),(483,'53','92',25,42),(484,'54','92',13,40),(485,'55','92',18,34),(486,'56','92',44,92),(487,'57','92',0,19),(488,'58','92',13,67),(489,'59','92',18,38),(490,'60','92',7,7),(491,'61','92',6,53),(492,'62','92',4,25),(493,'63','92',31,59),(494,'64','92',25,40),(495,'65','92',2,81),(496,'66','92',23,81),(497,'67','92',9,33),(498,'68','92',2,37),(499,'69','92',53,64),(500,'70','92',21,22),(501,'71','92',7,45),(502,'72','92',9,25),(503,'73','92',0,40),(504,'74','92',21,34),(505,'75','92',33,87),(506,'76','92',44,48),(507,'77','92',64,69),(508,'78','92',31,56),(509,'79','92',11,12),(510,'80','92',3,7),(511,'81','92',26,74),(512,'82','92',29,46),(513,'83','92',1,5),(514,'84','92',35,37),(515,'85','92',12,100),(516,'86','92',9,18),(517,'5','92',49,64); +/*!40000 ALTER TABLE `inventory` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `location` +-- + +DROP TABLE IF EXISTS `location`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `location` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `street` varchar(255) DEFAULT NULL, + `city` varchar(255) DEFAULT NULL, + `zipcode` int(11) DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `geo` point DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `location` +-- + +LOCK TABLES `location` WRITE; +/*!40000 ALTER TABLE `location` DISABLE KEYS */; +INSERT INTO `location` VALUES (2,'390 Lang Road','Burlingame',94010,'Bay Area Firearms','\0\0\0\0\0\0\0\"rU41B@M6y%^'),(1,'7153 East Thomas Road','Scottsdale',85251,'Phoenix Equipment Rentals','\0\0\0\0\0\0\0Իx?n@@0 X['),(4,'Tolstraat 200','Amsterdam',1074,'Munitions Shopmore','\0\0\0\0\0\0\0d311-J@\\AA@'),(5,'2799 Broadway','New York',10025,'Cascabel Armory','\0\0\0\0\0\0\0RfD@ L}R'),(3,'1850 El Camino Real','Menlo Park',94027,'Military Weaponry','\0\0\0\0\0\0\0XѺB@\"n^'),(6,'32/66-70 Marine Parade','Coolangatta',4225,'Marine Parade','\0\0\0\0\0\0\0س*<[.1c@'); +/*!40000 ALTER TABLE `location` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `session` +-- + +DROP TABLE IF EXISTS `session`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `session` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `uid` varchar(255) DEFAULT NULL, + `ttl` int(11) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `session` +-- + +LOCK TABLES `session` WRITE; +/*!40000 ALTER TABLE `session` DISABLE KEYS */; +/*!40000 ALTER TABLE `session` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `weapon` +-- + +DROP TABLE IF EXISTS `weapon`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `weapon` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `audibleRange` int(11) DEFAULT NULL, + `effectiveRange` int(11) DEFAULT NULL, + `rounds` int(11) DEFAULT NULL, + `extras` varchar(255) DEFAULT NULL, + `fireModes` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=87 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `weapon` +-- + +LOCK TABLES `weapon` WRITE; +/*!40000 ALTER TABLE `weapon` DISABLE KEYS */; +INSERT INTO `weapon` VALUES (1,'G17',53,75,15,'Flashlight','Single'),(2,'M1911',53,50,7,NULL,'Single'),(3,'M9',53,75,15,NULL,'Single'),(4,'M9 SD',0,75,15,'Silenced','Single'),(5,'Makarov SD',0,50,8,NULL,'Single'),(6,'PDW',53,75,30,NULL,'[\"Single\",\"Full auto\"]'),(7,'Makarov PM',53,50,8,NULL,'Single'),(9,'Saiga 12K',90,250,8,NULL,'Single'),(11,'Revolver',53,100,6,NULL,'Single'),(12,'Winchester 1866',125,150,15,NULL,'Single'),(13,'Bizon PP-19 SD',0,100,64,'Silenced','[\"Single\",\"Full auto\"]'),(14,'MP5SD6',0,100,30,'Silenced','[\"Single\",\"Burst\",\"Full auto\"]'),(15,'MP5A5',53,100,30,NULL,'[\"Single\",\"Burst\",\"Full auto\"]'),(16,'AK-107',80,400,30,'Kobra sight','[\"Single\",\"Burst\",\"Full auto\"]'),(18,'AK-107 GL PSO',80,400,30,'[\"Scope\",\"GP-25 launcher\"]','[\"Single\",\"Burst\",\"Full auto\"]'),(19,'AK-107 PSO',80,600,30,'Scope','[\"Single\",\"Burst\",\"Full auto\"]'),(20,'AK-74',80,300,30,NULL,'[\"Single\",\"Full auto\"]'),(21,'AKM',149,400,30,NULL,'[\"Single\",\"Full auto\"]'),(22,'AKS',149,200,30,NULL,'[\"Single\",\"Full auto\"]'),(23,'AKS (gold)',149,200,30,NULL,'[\"Single\",\"Full auto\"]'),(25,'AKS-74 Kobra',80,300,30,'Kobra sight','[\"Single\",\"Full auto\"]'),(26,'AKS-74 PSO',80,400,30,'Scope','[\"Single\",\"Full auto\"]'),(27,'AKS-74U',80,200,30,NULL,'[\"Single\",\"Full auto\"]'),(28,'AKS-74UN Kobra',0,300,30,'[\"Kobra sight\",\"Silenced\"]','[\"Single\",\"Full auto\"]'),(29,'AK-74 GP-25',80,300,30,'GP-25 launcher','[\"Single\",\"Full auto\"]'),(30,'FN FAL AN/PVS-4',180,400,20,'NV scope','[\"Single\",\"Burst\"]'),(31,'G36',80,400,30,'[\"Scope\",\"Aimpoint sight\"]','[\"Single\",\"Burst\",\"Full auto\"]'),(32,'FN FAL',180,400,20,NULL,'[\"Single\",\"Burst\"]'),(33,'G36 C',80,300,30,NULL,'[\"Single\",\"Burst\",\"Full auto\"]'),(34,'G36-C SD (camo)',0,300,30,'[\"Aimpoint sight\",\"Silenced\"]','[\"Single\",\"Burst\",\"Full auto\"]'),(35,'G36A (camo)',80,400,30,'[\"Scope\",\"Aimpoint sight\"]','[\"Single\",\"Burst\",\"Full auto\"]'),(36,'G36C (camo)',80,300,30,NULL,'[\"Single\",\"Burst\",\"Full auto\"]'),(37,'G36 K',80,400,30,'[\"Scope\",\"Aimpoint sight\"]','[\"Single\",\"Burst\",\"Full auto\"]'),(38,'G36C-SD',0,300,30,'Aimpoint sight','[\"Single\",\"Burst\",\"Full auto\"]'),(39,'G36K (camo)',80,400,30,'[\"Scope\",\"Aimpoint sight\"]','[\"Single\",\"Burst\",\"Full auto\"]'),(40,'L85A2 ACOG GL',80,600,30,'[\"ACOG scope\",\"M203 launcher\"]','[\"Single\",\"Full auto\"]'),(41,'L85A2 SUSAT',80,300,30,'SUSAT optical scope','[\"Single\",\"Full auto\"]'),(42,'M16A2',80,400,30,NULL,'[\"Single\",\"Burst\"]'),(43,'L85A2 AWS',80,300,30,'[\"Thermal scope\",\"NV scope\",\"Laser sight\",\"Variable zoom\"]','[\"Single\",\"Full auto\"]'),(44,'L85A2 Holo',80,300,30,'Holographic sight','[\"Single\",\"Full auto\"]'),(45,'Lee Enfield',162,400,10,NULL,'Single'),(46,'M16A4 ACOG',80,600,30,'ACOG scope','[\"Single\",\"Burst\"]'),(47,'M4A1',80,300,30,NULL,'[\"Single\",\"Full auto\"]'),(48,'M16A2 M203',80,400,30,'M203 launcher','[\"Single\",\"Burst\"]'),(49,'M4A1 Holo',80,300,30,'[\"Holographic sight\",\"M203 launcher\"]','[\"Single\",\"Full auto\"]'),(50,'M4A1 CCO',80,300,30,'Aimpoint sight','[\"Single\",\"Full auto\"]'),(51,'M4A1 CCO SD',0,200,30,'[\"Aimpoint sight\",\"Silenced\"]','[\"Single\",\"Full auto\"]'),(52,'M4A1 M203 RCO',80,600,30,'[\"ACOG sight\",\"M203 launcher\"]','[\"Single\",\"Full auto\"]'),(53,'M4A3 CCO',80,300,30,'[\"Aimpoint sight\",\"Flashlight\"]','[\"Single\",\"Full auto\"]'),(54,'RPK',80,400,30,NULL,'[\"Single\",\"Full auto\"]'),(55,'Sa-58 CCO',149,300,30,'Aimpoint sight','[\"Single\",\"Full auto\"]'),(56,'Sa-58P',149,400,30,NULL,'[\"Single\",\"Full auto\"]'),(57,'Sa-58V',149,200,30,NULL,'[\"Single\",\"Full auto\"]'),(58,'Sa-58V ACOG',149,400,30,'ACOG sight','[\"Single\",\"Full auto\"]'),(59,'ER7 RFW',180,2000,25,'[\"Scope\",\"Aimpoint sight\"]','Single'),(60,'AS50',455,1600,5,'Scope','Single'),(61,'KSVK',455,800,5,'Scope','Single'),(62,'CZ550',180,800,5,'Scope','Single'),(63,'DMR',180,800,20,'Scope','Single'),(64,'M107',455,1200,10,'Scope','Single'),(65,'M24',180,800,5,'Scope','Single'),(66,'M40A3',180,800,5,'[\"Scope\",\"Camo\"]','Single'),(67,'M14 AIM',180,500,20,'Aimpoint sight','Single'),(68,'M240',180,400,100,NULL,'Full auto'),(69,'MG36',80,400,100,'Aimpoint sight','[\"Single\",\"Burst\",\"Full auto\"]'),(70,'SVD Camo',180,1200,10,'[\"Scope\",\"Camo\"]','Single'),(71,'PKM',180,400,100,NULL,'Full auto'),(72,'Mk 48 Mod 0',180,400,100,'Aimpoint sight','Full auto'),(73,'M249 SAW',80,300,200,NULL,'Full auto'),(74,'Crowbar',2,1,NULL,NULL,'Single'),(75,'Hatchet',2,1,NULL,NULL,'Single'),(76,'PKP',180,600,100,'Scope','Full auto'),(77,'Machete',2,1,NULL,NULL,'Single'),(78,'M67 Frag Grenade',NULL,NULL,NULL,NULL,NULL),(79,'Compound Crossbow',3,30,1,NULL,'Single'),(80,'Smoke Grenade',NULL,NULL,NULL,NULL,NULL),(81,'M136 Launcher',160,1000,1,NULL,'Single'),(82,'30Rnd. AK SD',0,NULL,30,NULL,NULL),(83,'30rnd G36 SD',0,NULL,30,NULL,NULL),(84,'G36 Mag.',80,NULL,30,NULL,NULL),(85,'Flashlight',NULL,NULL,NULL,NULL,NULL),(86,'NV Goggles',NULL,NULL,NULL,NULL,NULL); +/*!40000 ALTER TABLE `weapon` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2013-10-08 10:51:21 diff --git a/index.js b/index.js new file mode 100644 index 0000000..03f1307 --- /dev/null +++ b/index.js @@ -0,0 +1,10 @@ +// Copyright IBM Corp. 2012. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +var SG = require('strong-globalize'); +SG.SetRootDir(__dirname); + +module.exports = require('./lib/mysql.js'); diff --git a/intl/de/messages.json b/intl/de/messages.json new file mode 100644 index 0000000..c6614df --- /dev/null +++ b/intl/de/messages.json @@ -0,0 +1,11 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} muss ein {{object}} sein: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} ist ein erforderliches Zeichenfolgeargument: {0}", + "b7c60421de706ca1e050f2a86953745e": "Keine Argumente - {{Enum}} konnte nicht erstellt werden.", + "80a32e80cbed65eba2103201a7c94710": "Modell nicht gefunden: {0}", + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}}-Syntax berücksichtigt nicht das {{`g`}}-Flag", + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}}-Syntax berücksichtigt nicht das {{`i`}}-Flag", + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}}-Syntax berücksichtigt nicht das {{`m`}}-Flag", + "57512a471969647e8eaa2509cc292018": "{{callback}} sollte eine Funktion sein" +} + diff --git a/intl/en/messages.json b/intl/en/messages.json new file mode 100644 index 0000000..7e4acdf --- /dev/null +++ b/intl/en/messages.json @@ -0,0 +1,10 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} must be an {{object}}: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} is a required string argument: {0}", + "b7c60421de706ca1e050f2a86953745e": "No arguments - could not create {{Enum}}.", + "80a32e80cbed65eba2103201a7c94710": "Model not found: {0}", + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}} syntax does not respect the {{`g`}} flag", + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}} syntax does not respect the {{`i`}} flag", + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}} syntax does not respect the {{`m`}} flag", + "57512a471969647e8eaa2509cc292018": "{{callback}} should be a function" +} diff --git a/intl/es/messages.json b/intl/es/messages.json new file mode 100644 index 0000000..fbadd85 --- /dev/null +++ b/intl/es/messages.json @@ -0,0 +1,11 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} debe ser un {{object}}: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} es un argumento de serie necesario: {0}", + "b7c60421de706ca1e050f2a86953745e": "No hay argumentos - no se ha podido crear {{Enum}}.", + "80a32e80cbed65eba2103201a7c94710": "No se ha encontrado el modelo: {0}", + "026ed55518f3812a9ef4b86e8a195e76": "la sintaxis de {{MySQL}} {{regex}} no respeta el distintivo {{`g`}}", + "0ac9f848b934332210bb27747d12a033": "la sintaxis de {{MySQL}} {{regex}} no respeta el distintivo {{`i`}}", + "4e9e35876bfb1511205456b52c6659d0": "la sintaxis de {{MySQL}} {{regex}} no respeta el distintivo {{`m`}}", + "57512a471969647e8eaa2509cc292018": "{{callback}} debe ser una función" +} + diff --git a/intl/fr/messages.json b/intl/fr/messages.json new file mode 100644 index 0000000..266ade9 --- /dev/null +++ b/intl/fr/messages.json @@ -0,0 +1,11 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} doit être un {{object}} : {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} est un argument de chaîne obligatoire : {0}", + "b7c60421de706ca1e050f2a86953745e": "Aucun argument - impossible de créer {{Enum}}.", + "80a32e80cbed65eba2103201a7c94710": "Modèle introuvable : {0}", + "026ed55518f3812a9ef4b86e8a195e76": "La syntaxe {{MySQL}} {{regex}} ne respecte pas l'indicateur {{`g`}}", + "0ac9f848b934332210bb27747d12a033": "La syntaxe {{MySQL}} {{regex}} ne respecte pas l'indicateur {{`i`}}", + "4e9e35876bfb1511205456b52c6659d0": "La syntaxe {{MySQL}} {{regex}} ne respecte pas l'indicateur {{`m`}}", + "57512a471969647e8eaa2509cc292018": "{{callback}} doit être une fonction" +} + diff --git a/intl/it/messages.json b/intl/it/messages.json new file mode 100644 index 0000000..3a23a10 --- /dev/null +++ b/intl/it/messages.json @@ -0,0 +1,11 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} deve essere un {{object}}: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} è un argomento stringa obbligatorio: {0}", + "b7c60421de706ca1e050f2a86953745e": "Nessun argomento - impossibile creare {{Enum}}.", + "80a32e80cbed65eba2103201a7c94710": "Modello non trovato: {0}", + "026ed55518f3812a9ef4b86e8a195e76": "La sintassi {{MySQL}} {{regex}} non rispetta l'indicatore {{`g`}}", + "0ac9f848b934332210bb27747d12a033": "La sintassi {{MySQL}} {{regex}} non rispetta l'indicatore {{`i`}}", + "4e9e35876bfb1511205456b52c6659d0": "La sintassi {{MySQL}} {{regex}} non rispetta l'indicatore {{`m`}}", + "57512a471969647e8eaa2509cc292018": "{{callback}} deve essere una funzione" +} + diff --git a/intl/ja/messages.json b/intl/ja/messages.json new file mode 100644 index 0000000..d45c69b --- /dev/null +++ b/intl/ja/messages.json @@ -0,0 +1,11 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} は {{object}} でなければなりません: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} は必須のストリング引数です: {0}", + "b7c60421de706ca1e050f2a86953745e": "引数がありません - {{Enum}} を作成できませんでした。", + "80a32e80cbed65eba2103201a7c94710": "モデルが見つかりません: {0}", + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}} 構文では {{`g`}} フラグは考慮されません", + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}} 構文では {{`i`}} フラグは考慮されません", + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}} 構文では {{`m`}} フラグは考慮されません", + "57512a471969647e8eaa2509cc292018": "{{callback}} は関数でなければなりません" +} + diff --git a/intl/ko/messages.json b/intl/ko/messages.json new file mode 100644 index 0000000..fdc5823 --- /dev/null +++ b/intl/ko/messages.json @@ -0,0 +1,11 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}}이(가) {{object}}이어야 함: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}}은 필수 문자열 인수임: {0}", + "b7c60421de706ca1e050f2a86953745e": "인수 없음 - {{Enum}}을(를) 작성할 수 없습니다. ", + "80a32e80cbed65eba2103201a7c94710": "모델을 찾을 수 없음: {0}", + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}} 구문에서 {{`g`}} 플래그를 준수하지 않음", + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}} 구문에서 {{`i`}} 플래그를 준수하지 않음", + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}} 구문에서 {{`m`}} 플래그를 준수하지 않음", + "57512a471969647e8eaa2509cc292018": "{{callback}}이(가) 함수여야 함" +} + diff --git a/intl/nl/messages.json b/intl/nl/messages.json new file mode 100644 index 0000000..39c0210 --- /dev/null +++ b/intl/nl/messages.json @@ -0,0 +1,11 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} moet een {{object}} zijn: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} is een verplicht tekenreeksargument: {0}", + "b7c60421de706ca1e050f2a86953745e": "Geen argumenten - {{Enum}} kan niet worden gemaakt.", + "80a32e80cbed65eba2103201a7c94710": "Model is niet gevonden: {0}", + "026ed55518f3812a9ef4b86e8a195e76": "Syntaxis van {{MySQL}} {{regex}} voldoet niet aan vlag {{`g`}}", + "0ac9f848b934332210bb27747d12a033": "Syntaxis van {{MySQL}} {{regex}} voldoet niet aan vlag {{`i`}}", + "4e9e35876bfb1511205456b52c6659d0": "Syntaxis van {{MySQL}} {{regex}} voldoet niet aan vlag {{`m`}}", + "57512a471969647e8eaa2509cc292018": "{{callback}} moet een functie zijn" +} + diff --git a/intl/pt/messages.json b/intl/pt/messages.json new file mode 100644 index 0000000..33010f1 --- /dev/null +++ b/intl/pt/messages.json @@ -0,0 +1,11 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} deve ser um {{object}}: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} é um argumento de sequência necessário: {0}", + "b7c60421de706ca1e050f2a86953745e": "Sem argumentos - não foi possível criar {{Enum}}.", + "80a32e80cbed65eba2103201a7c94710": "Modelo não localizado: {0}", + "026ed55518f3812a9ef4b86e8a195e76": "Sintaxe {{regex}} de {{MySQL}} não respeita a sinalização {{`g`}}", + "0ac9f848b934332210bb27747d12a033": "Sintaxe {{regex}} de {{MySQL}} não respeita a sinalização {{`i`}}", + "4e9e35876bfb1511205456b52c6659d0": "Sintaxe {{regex}} de {{MySQL}} não respeita a sinalização {{`m`}}", + "57512a471969647e8eaa2509cc292018": "{{callback}} deve ser uma função" +} + diff --git a/intl/tr/messages.json b/intl/tr/messages.json new file mode 100644 index 0000000..8af6c04 --- /dev/null +++ b/intl/tr/messages.json @@ -0,0 +1,11 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} bir {{object}} olmalıdır: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} zorunlu bir dizgi bağımsız değişkeni: {0}", + "b7c60421de706ca1e050f2a86953745e": "Bağımsız değişken yok - {{Enum}} yaratılamadı.", + "80a32e80cbed65eba2103201a7c94710": "Model bulunamadı: {0}", + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}} düzenli ifade sözdizimi {{`g`}} işareti kuralına uymuyor", + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}} düzenli ifade sözdizimi {{`i`}} işareti kuralına uymuyor", + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}} düzenli ifade sözdizimi {{`m`}} işareti kuralına uymuyor", + "57512a471969647e8eaa2509cc292018": "{{callback}} bir işlev olmalıdır" +} + diff --git a/intl/zh-Hans/messages.json b/intl/zh-Hans/messages.json new file mode 100644 index 0000000..9711659 --- /dev/null +++ b/intl/zh-Hans/messages.json @@ -0,0 +1,11 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} 必须为 {{object}}:{0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} 是必需的字符串自变量:{0}", + "b7c60421de706ca1e050f2a86953745e": "无自变量 - 无法创建 {{Enum}}。", + "80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}", + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}} 语法不考虑 {{`g`}} 标志", + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}} 语法不考虑 {{`i`}} 标志", + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}} 语法不考虑 {{`m`}} 标志", + "57512a471969647e8eaa2509cc292018": "{{callback}} 应该是函数" +} + diff --git a/intl/zh-Hant/messages.json b/intl/zh-Hant/messages.json new file mode 100644 index 0000000..40c0db3 --- /dev/null +++ b/intl/zh-Hant/messages.json @@ -0,0 +1,11 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} 必須是 {{object}}:{0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} 是必要的字串引數:{0}", + "b7c60421de706ca1e050f2a86953745e": "沒有引數 - 無法建立 {{Enum}}。", + "80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}", + "026ed55518f3812a9ef4b86e8a195e76": "{{MySQL}} {{regex}} 語法未遵循 {{`g`}} 旗標", + "0ac9f848b934332210bb27747d12a033": "{{MySQL}} {{regex}} 語法未遵循 {{`i`}} 旗標", + "4e9e35876bfb1511205456b52c6659d0": "{{MySQL}} {{regex}} 語法未遵循 {{`m`}} 旗標", + "57512a471969647e8eaa2509cc292018": "{{callback}} 應該是函數" +} + diff --git a/lib/discovery.js b/lib/discovery.js new file mode 100644 index 0000000..5e9e7a4 --- /dev/null +++ b/lib/discovery.js @@ -0,0 +1,395 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +var g = require('strong-globalize')(); + +module.exports = mixinDiscovery; + +/*! + * @param {MySQL} MySQL connector class + * @param {Object} mysql mysql driver + */ +function mixinDiscovery(MySQL, mysql) { + var async = require('async'); + + function paginateSQL(sql, orderBy, options) { + options = options || {}; + var limitClause = ''; + if (options.offset || options.skip || options.limit) { + // Offset starts from 0 + var offset = Number(options.offset || options.skip || 0); + if (isNaN(offset)) { + offset = 0; + } + limitClause = ' LIMIT ' + offset; + if (options.limit) { + var limit = Number(options.limit); + if (isNaN(limit)) { + limit = 0; + } + limitClause = limitClause + ',' + limit; + } + } + if (!orderBy) { + sql += ' ORDER BY ' + orderBy; + } + return sql + limitClause; + } + + /*! + * Build sql for listing schemas (databases in MySQL) + * @params {Object} [options] Options object + * @returns {String} The SQL statement + */ + MySQL.prototype.buildQuerySchemas = function(options) { + var sql = 'SELECT catalog_name as "catalog",' + + ' schema_name as "schema"' + + ' FROM information_schema.schemata'; + return paginateSQL(sql, 'schema_name', options); + }; + + /*! + * Build sql for listing tables + * @param options {all: for all owners, owner: for a given owner} + * @returns {string} The sql statement + */ + MySQL.prototype.buildQueryTables = function(options) { + var sqlTables = null; + var schema = options.owner || options.schema; + + if (options.all && !schema) { + sqlTables = paginateSQL('SELECT \'table\' AS "type",' + + ' table_name AS "name", table_schema AS "owner"' + + ' FROM information_schema.tables', + 'table_schema, table_name', options); + } else if (schema) { + sqlTables = paginateSQL('SELECT \'table\' AS "type",' + + ' table_name AS "name", table_schema AS "schema"' + + ' FROM information_schema.tables' + + ' WHERE table_schema=' + mysql.escape(schema), + 'table_schema, table_name', options); + } else { + sqlTables = paginateSQL('SELECT \'table\' AS "type",' + + ' table_name AS "name", ' + + ' table_schema AS "owner" FROM information_schema.tables' + + ' WHERE table_schema=SUBSTRING_INDEX(USER(),\'@\',1)', + 'table_name', options); + } + return sqlTables; + }; + + /*! + * Build sql for listing views + * @param options {all: for all owners, owner: for a given owner} + * @returns {string} The sql statement + */ + MySQL.prototype.buildQueryViews = function(options) { + var sqlViews = null; + if (options.views) { + var schema = options.owner || options.schema; + + if (options.all && !schema) { + sqlViews = paginateSQL('SELECT \'view\' AS "type",' + + ' table_name AS "name",' + + ' table_schema AS "owner"' + + ' FROM information_schema.views', + 'table_schema, table_name', options); + } else if (schema) { + sqlViews = paginateSQL('SELECT \'view\' AS "type",' + + ' table_name AS "name",' + + ' table_schema AS "owner"' + + ' FROM information_schema.views' + + ' WHERE table_schema=' + mysql.escape(schema), + 'table_schema, table_name', options); + } else { + sqlViews = paginateSQL('SELECT \'view\' AS "type",' + + ' table_name AS "name",' + + ' table_schema AS "owner"' + + ' FROM information_schema.views', + 'table_name', options); + } + } + return sqlViews; + }; + + /** + * Discover model definitions + * + * @param {Object} options Options for discovery + * @param {Function} [cb] The callback function + */ + + /*! + * Normalize the arguments + * @param table string, required + * @param options object, optional + * @param cb function, optional + */ + MySQL.prototype.getArgs = function(table, options, cb) { + if ('string' !== typeof table || !table) { + throw new Error(g.f('{{table}} is a required string argument: %s', table)); + } + options = options || {}; + if (!cb && 'function' === typeof options) { + cb = options; + options = {}; + } + if (typeof options !== 'object') { + throw new Error(g.f('{{options}} must be an {{object}}: %s', options)); + } + return { + schema: options.owner || options.schema, + table: table, + options: options, + cb: cb, + }; + }; + + /*! + * Build the sql statement to query columns for a given table + * @param schema + * @param table + * @returns {String} The sql statement + */ + MySQL.prototype.buildQueryColumns = function(schema, table) { + var sql = null; + if (schema) { + sql = paginateSQL('SELECT table_schema AS "owner",' + + ' table_name AS "tableName",' + + ' column_name AS "columnName",' + + ' data_type AS "dataType",' + + ' character_maximum_length AS "dataLength",' + + ' numeric_precision AS "dataPrecision",' + + ' numeric_scale AS "dataScale",' + + ' column_type AS "columnType",' + + ' is_nullable = \'YES\' AS "nullable",' + + ' CASE WHEN extra LIKE \'%auto_increment%\' THEN 1 ELSE 0 END AS "generated"' + + ' FROM information_schema.columns' + + ' WHERE table_schema=' + mysql.escape(schema) + + (table ? ' AND table_name=' + mysql.escape(table) : ''), + 'table_name, ordinal_position', {}); + } else { + sql = paginateSQL('SELECT table_schema AS "owner",' + + ' table_name AS "tableName",' + + ' column_name AS "columnName",' + + ' data_type AS "dataType",' + + ' character_maximum_length AS "dataLength",' + + ' numeric_precision AS "dataPrecision",' + + ' numeric_scale AS "dataScale",' + + ' column_type AS "columnType",' + + ' is_nullable = \'YES\' AS "nullable",' + + ' CASE WHEN extra LIKE \'%auto_increment%\' THEN 1 ELSE 0 END AS "generated"' + + ' FROM information_schema.columns' + + (table ? ' WHERE table_name=' + mysql.escape(table) : ''), + 'table_name, ordinal_position', {}); + } + return sql; + }; + + /** + * Discover model properties from a table + * @param {String} table The table name + * @param {Object} options The options for discovery + * @param {Function} [cb] The callback function + * + */ + + /*! + * Build the sql statement for querying primary keys of a given table + * @param schema + * @param table + * @returns {string} + */ +// http://docs.oracle.com/javase/6/docs/api/java/sql/DatabaseMetaData.html +// #getPrimaryKeys(java.lang.String, java.lang.String, java.lang.String) + MySQL.prototype.buildQueryPrimaryKeys = function(schema, table) { + var sql = 'SELECT table_schema AS "owner",' + + ' table_name AS "tableName",' + + ' column_name AS "columnName",' + + ' ordinal_position AS "keySeq",' + + ' constraint_name AS "pkName"' + + ' FROM information_schema.key_column_usage' + + ' WHERE constraint_name=\'PRIMARY\''; + + if (schema) { + sql += ' AND table_schema=' + mysql.escape(schema); + } + if (table) { + sql += ' AND table_name=' + mysql.escape(table); + } + sql += ' ORDER BY' + + ' table_schema, constraint_name, table_name, ordinal_position'; + return sql; + }; + + /** + * Discover primary keys for a given table + * @param {String} table The table name + * @param {Object} options The options for discovery + * @param {Function} [cb] The callback function + */ + + /*! + * Build the sql statement for querying foreign keys of a given table + * @param schema + * @param table + * @returns {string} + */ + MySQL.prototype.buildQueryForeignKeys = function(schema, table) { + var sql = + 'SELECT table_schema AS "fkOwner",' + + ' constraint_name AS "fkName",' + + ' table_name AS "fkTableName",' + + ' column_name AS "fkColumnName",' + + ' ordinal_position AS "keySeq",' + + ' referenced_table_schema AS "pkOwner", \'PRIMARY\' AS "pkName",' + + ' referenced_table_name AS "pkTableName",' + + ' referenced_column_name AS "pkColumnName"' + + ' FROM information_schema.key_column_usage' + + ' WHERE constraint_name!=\'PRIMARY\'' + + ' AND POSITION_IN_UNIQUE_CONSTRAINT IS NOT NULL'; + if (schema) { + sql += ' AND table_schema=' + mysql.escape(schema); + } + if (table) { + sql += ' AND table_name=' + mysql.escape(table); + } + return sql; + }; + + /** + * Discover foreign keys for a given table + * @param {String} table The table name + * @param {Object} options The options for discovery + * @param {Function} [cb] The callback function + */ + + /*! + * Retrieves a description of the foreign key columns that reference the + * given table's primary key columns (the foreign keys exported by a table). + * They are ordered by fkTableOwner, fkTableName, and keySeq. + * @param schema + * @param table + * @returns {string} + */ + MySQL.prototype.buildQueryExportedForeignKeys = function(schema, table) { + var sql = 'SELECT a.constraint_name AS "fkName",' + + ' a.table_schema AS "fkOwner",' + + ' a.table_name AS "fkTableName",' + + ' a.column_name AS "fkColumnName",' + + ' a.ordinal_position AS "keySeq",' + + ' NULL AS "pkName",' + + ' a.referenced_table_schema AS "pkOwner",' + + ' a.referenced_table_name AS "pkTableName",' + + ' a.referenced_column_name AS "pkColumnName"' + + ' FROM information_schema.key_column_usage a' + + ' WHERE a.position_in_unique_constraint IS NOT NULL'; + if (schema) { + sql += ' AND a.referenced_table_schema=' + mysql.escape(schema); + } + if (table) { + sql += ' AND a.referenced_table_name=' + mysql.escape(table); + } + sql += ' ORDER BY a.table_schema, a.table_name, a.ordinal_position'; + + return sql; + }; + + /** + * Discover foreign keys that reference to the primary key of this table + * @param {String} table The table name + * @param {Object} options The options for discovery + * @param {Function} [cb] The callback function + */ + + MySQL.prototype.buildPropertyType = function(columnDefinition, options) { + var mysqlType = columnDefinition.dataType; + var columnType = columnDefinition.columnType; + var dataLength = columnDefinition.dataLength; + + var type = mysqlType.toUpperCase(); + switch (type) { + case 'CHAR': + if (!options.treatCHAR1AsString && columnType === 'char(1)') { + // Treat char(1) as boolean ('Y', 'N', 'T', 'F', '0', '1') + return 'Boolean'; + } + case 'VARCHAR': + case 'TINYTEXT': + case 'MEDIUMTEXT': + case 'LONGTEXT': + case 'TEXT': + case 'ENUM': + case 'SET': + return 'String'; + case 'TINYBLOB': + case 'MEDIUMBLOB': + case 'LONGBLOB': + case 'BLOB': + case 'BINARY': + case 'VARBINARY': + case 'BIT': + // treat BIT(1) as boolean as it's 1 or 0 + if (!options.treatBIT1AsBit && columnType === 'bit(1)') { + return 'Boolean'; + } + return 'Binary'; + case 'TINYINT': + // treat TINYINT(1) as boolean as it is aliased as BOOL and BOOLEAN in mysql + if (!options.treatTINYINT1AsTinyInt && columnType === 'tinyint(1)') { + return 'Boolean'; + } + case 'SMALLINT': + case 'INT': + case 'MEDIUMINT': + case 'YEAR': + case 'FLOAT': + case 'DOUBLE': + case 'BIGINT': + return 'Number'; + case 'DATE': + case 'TIMESTAMP': + case 'DATETIME': + return 'Date'; + case 'POINT': + return 'GeoPoint'; + case 'BOOL': + case 'BOOLEAN': + return 'Boolean'; + default: + return 'String'; + } + }; + + MySQL.prototype.getDefaultSchema = function() { + if (this.dataSource && this.dataSource.settings && + this.dataSource.settings.database) { + return this.dataSource.settings.database; + } + return undefined; + }; + + // Recommended MySQL 5.7 Boolean scheme. See + // http://dev.mysql.com/doc/refman/5.7/en/numeric-type-overview.html + // Currently default is the inverse of the recommendation for backward compatibility. + MySQL.prototype.setDefaultOptions = function(options) { + var defaultOptions = { + treatCHAR1AsString: false, + treatBIT1AsBit: true, + treatTINYINT1AsTinyInt: true, + }; + + for (var opt in defaultOptions) { + if (defaultOptions.hasOwnProperty(opt) && !options.hasOwnProperty(opt)) { + options[opt] = defaultOptions[opt]; + } + } + }; + + MySQL.prototype.setNullableProperty = function(r) { + r.nullable = r.nullable ? 'Y' : 'N'; + }; +} diff --git a/lib/enumFactory.js b/lib/enumFactory.js new file mode 100644 index 0000000..4e68174 --- /dev/null +++ b/lib/enumFactory.js @@ -0,0 +1,67 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +var g = require('strong-globalize')(); + +var EnumFactory = function() { + if (arguments.length > 0) { + var Enum = function Enum(arg) { + if (typeof arg === 'number' && arg % 1 == 0) { + return Enum._values[arg]; + } else if (Enum[arg]) { + return Enum[arg]; + } else if (Enum._values.indexOf(arg) !== -1) { + return arg; + } else if (arg === null) { + return null; + } else { + return ''; + } + }; + var dxList = []; + // Want empty value to be at index 0 to match MySQL Enum values and + // MySQL non-strict behavior. + dxList.push(''); + for (var arg in arguments) { + arg = String(arguments[arg]); + Object.defineProperty(Enum, arg.toUpperCase(), { + configurable: false, + enumerable: true, + value: arg, + writable: false, + }); + dxList.push(arg); + } + Object.defineProperty(Enum, '_values', { + configurable: false, + enumerable: false, + value: dxList, + writable: false, + }); + Object.defineProperty(Enum, '_string', { + configurable: false, + enumerable: false, + value: stringified(Enum), + writable: false, + }); + Object.freeze(Enum); + return Enum; + } else { + throw g.f('No arguments - could not create {{Enum}}.'); + } +}; + +function stringified(anEnum) { + var s = []; + for (var i in anEnum._values) { + if (anEnum._values[i] != '') { + s.push("'" + anEnum._values[i] + "'"); + } + } + return s.join(','); +} + +exports.EnumFactory = EnumFactory; diff --git a/lib/migration.js b/lib/migration.js new file mode 100644 index 0000000..ce8950b --- /dev/null +++ b/lib/migration.js @@ -0,0 +1,900 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +var g = require('strong-globalize')(); +var async = require('async'); +module.exports = mixinMigration; + +/*! + * @param {MySQL} MySQL connector class + * @param {Object} mysql mysql driver + */ +function mixinMigration(MySQL, mysql) { + MySQL.prototype.showFields = function(model, cb) { + var table = this.tableEscaped(model); + var sql = 'SHOW FIELDS FROM ' + table; + this.execute(sql, function(err, fields) { + if (err) { + return cb(err); + } else { + cb(err, fields); + } + }); + }; + + MySQL.prototype.showIndexes = function(model, cb) { + var table = this.tableEscaped(model); + var sql = 'SHOW INDEXES FROM ' + table; + this.execute(sql, function(err, indexes) { + if (err) { + return cb(err); + } else { + cb(err, indexes); + } + }); + }; + + /** + * Perform autoupdate for the given models + * @param {String[]} [models] A model name or an array of model names. + * If not present, apply to all models + * @param {Function} [cb] The callback function + */ + MySQL.prototype.autoupdate = function(models, cb) { + var self = this; + var foreignKeyStatements = []; + + if ((!cb) && ('function' === typeof models)) { + cb = models; + models = undefined; + } + // First argument is a model name + if ('string' === typeof models) { + models = [models]; + } + + models = models || Object.keys(this._models); + + async.each(models, function(model, done) { + if (!(model in self._models)) { + return process.nextTick(function() { + done(new Error(g.f('Model not found: %s', model))); + }); + } + + self.getTableStatus(model, function(err, fields, indexes) { + self.discoverForeignKeys(self.table(model), {}, function(err, foreignKeys) { + if (err) console.log('Failed to discover "' + table + '" foreign keys', err); + + if (!err && fields && fields.length) { + //if we already have a definition, update this table + self.alterTable(model, fields, indexes, foreignKeys, function(err, result) { + if (!err) { + self.addForeignKeys(model, function(err, result) { + done(err); + }); + } else { + done(err); + } + }); + } else { + //if there is not yet a definition, create this table + self.createTable(model, function(err) { + if (!err) { + self.addForeignKeys(model, function(err, result) { + done(err); + }); + } else { + done(err); + } + }); + } + }); + }); + }, function(err) { + return cb(err); + }); + }; + + /*! + * Create a DB table for the given model + * @param {string} model Model name + * @param cb + */ + MySQL.prototype.createTable = function(model, cb) { + var metadata = this.getModelDefinition(model).settings[this.name]; + var engine = metadata && metadata.engine; + var sql = 'CREATE TABLE ' + this.tableEscaped(model) + + ' (\n ' + this.buildColumnDefinitions(model) + '\n)'; + if (engine) { + sql += 'ENGINE=' + engine + '\n'; + } + this.execute(sql, cb); + }; + + /** + * Check if the models exist + * @param {String[]} [models] A model name or an array of model names. If not + * present, apply to all models + * @param {Function} [cb] The callback function + */ + MySQL.prototype.isActual = function(models, cb) { + var self = this; + var ok = false; + + if ((!cb) && ('function' === typeof models)) { + cb = models; + models = undefined; + } + // First argument is a model name + if ('string' === typeof models) { + models = [models]; + } + + models = models || Object.keys(this._models); + + async.each(models, function(model, done) { + self.getTableStatus(model, function(err, fields, indexes) { + self.discoverForeignKeys(self.table(model), {}, function(err, foreignKeys) { + if (err) console.log('Failed to discover "' + table + '" foreign keys', err); + + self.alterTable(model, fields, indexes, foreignKeys, function(err, needAlter) { + if (err) { + return done(err); + } else { + ok = ok || needAlter; + done(err); + } + }, true); + }); + }); + }, function(err) { + cb(err, !ok); + }); + }; + + MySQL.prototype.getColumnsToAdd = function(model, actualFields) { + var self = this; + var m = this.getModelDefinition(model); + var propNames = Object.keys(m.properties).filter(function(name) { + return !!m.properties[name]; + }); + var sql = []; + + propNames.forEach(function(propName) { + if (m.properties[propName] && self.id(model, propName)) return; + var found; + var colName = expectedColNameForModel(propName, m); + if (actualFields) { + actualFields.forEach(function(f) { + if (f.Field === colName) { + found = f; + } + }); + } + if (found) { + actualize(colName, found); + } else { + sql.push('ADD COLUMN ' + self.client.escapeId(colName) + ' ' + + self.buildColumnDefinition(model, propName)); + } + }); + + function actualize(propName, oldSettings) { + var newSettings = m.properties[propName]; + if (newSettings && changed(newSettings, oldSettings)) { + var pName = self.client.escapeId(propName); + sql.push('CHANGE COLUMN ' + pName + ' ' + pName + ' ' + + self.buildColumnDefinition(model, propName)); + } + } + + function changed(newSettings, oldSettings) { + if (oldSettings.Null === 'YES') { + // Used to allow null and does not now. + if (!self.isNullable(newSettings)) { + return true; + } + } + if (oldSettings.Null === 'NO') { + // Did not allow null and now does. + if (self.isNullable(newSettings)) { + return true; + } + } + + if (oldSettings.Type.toUpperCase() !== + self.buildColumnType(newSettings).toUpperCase()) { + return true; + } + return false; + } + return sql; + }; + + MySQL.prototype.getColumnsToDrop = function(model, actualFields) { + var self = this; + var fields = actualFields; + var sql = []; + var m = this.getModelDefinition(model); + var propNames = Object.keys(m.properties).filter(function(name) { + return !!m.properties[name]; + }); + // drop columns + if (fields) { + fields.forEach(function(f) { + var colNames = propNames.map(function expectedColName(propName) { + return expectedColNameForModel(propName, m); + }); + var index = colNames.indexOf(f.Field); + var propName = index >= 0 ? propNames[index] : f.Field; + var notFound = !~index; + if (m.properties[propName] && self.id(model, propName)) return; + if (notFound || !m.properties[propName]) { + sql.push('DROP COLUMN ' + self.client.escapeId(f.Field)); + } + }); + } + return sql; + }; + + MySQL.prototype.addIndexes = function(model, actualIndexes) { + var self = this; + var m = this.getModelDefinition(model); + var propNames = Object.keys(m.properties).filter(function(name) { + return !!m.properties[name]; + }); + var indexNames = m.settings.indexes && Object.keys(m.settings.indexes).filter(function(name) { + return !!m.settings.indexes[name]; + }) || []; + var sql = []; + var ai = {}; + + if (actualIndexes) { + actualIndexes.forEach(function(i) { + var name = i.Key_name; + if (!ai[name]) { + ai[name] = { + info: i, + columns: [], + }; + } + ai[name].columns[i.Seq_in_index - 1] = i.Column_name; + }); + } + var aiNames = Object.keys(ai); + + // remove indexes + aiNames.forEach(function(indexName) { + if (indexName === 'PRIMARY' || + (m.properties[indexName] && self.id(model, indexName))) return; + + if (indexNames.indexOf(indexName) === -1 && !m.properties[indexName] || + m.properties[indexName] && !m.properties[indexName].index) { + sql.push('DROP INDEX ' + self.client.escapeId(indexName)); + } else { + // first: check single (only type and kind) + if (m.properties[indexName] && !m.properties[indexName].index) { + // TODO + return; + } + // second: check multiple indexes + var orderMatched = true; + if (indexNames.indexOf(indexName) !== -1) { + //check if indexes are configured as "columns" + if (m.settings.indexes[indexName].columns) { + m.settings.indexes[indexName].columns.split(/,\s*/).forEach( + function(columnName, i) { + if (ai[indexName].columns[i] !== columnName) orderMatched = false; + }); + } else if (m.settings.indexes[indexName].keys) { + //if indexes are configured as "keys" + var index = 0; + for (var key in m.settings.indexes[indexName].keys) { + var sortOrder = m.settings.indexes[indexName].keys[key]; + if (ai[indexName].columns[index] !== key) { + orderMatched = false; + break; + } + index++; + } + //if number of columns differ between new and old index + if (index !== ai[indexName].columns.length) { + orderMatched = false; + } + } + } + if (!orderMatched) { + sql.push('DROP INDEX ' + self.client.escapeId(indexName)); + delete ai[indexName]; + } + } + }); + + // add single-column indexes + propNames.forEach(function(propName) { + var i = m.properties[propName].index; + if (!i) { + return; + } + var found = ai[propName] && ai[propName].info; + if (!found) { + var colName = expectedColNameForModel(propName, m); + var pName = self.client.escapeId(colName); + var type = ''; + var kind = ''; + if (i.type) { + type = 'USING ' + i.type; + } + if (kind && type) { + sql.push('ADD ' + kind + ' INDEX ' + pName + + ' (' + pName + ') ' + type); + } else { + if (typeof i === 'object' && i.unique && i.unique === true) { + kind = 'UNIQUE'; + } + sql.push('ADD ' + kind + ' INDEX ' + pName + ' ' + type + + ' (' + pName + ') '); + } + } + }); + + // add multi-column indexes + indexNames.forEach(function(indexName) { + var i = m.settings.indexes[indexName]; + var found = ai[indexName] && ai[indexName].info; + if (!found) { + var iName = self.client.escapeId(indexName); + var type = ''; + var kind = ''; + if (i.type) { + type = 'USING ' + i.type; + } + if (i.kind) { + kind = i.kind; + } else if (i.options && i.options.unique && i.options.unique == true) { + //if index unique indicator is configured + kind = 'UNIQUE'; + } + + var indexedColumns = []; + var columns = ''; + //if indexes are configured as "keys" + if (i.keys) { + for (var key in i.keys) { + if (i.keys[key] !== -1) { + indexedColumns.push(key); + } else { + indexedColumns.push(key + ' DESC '); + } + } + } + if (indexedColumns.length > 0) { + columns = indexedColumns.join(','); + } else if (i.columns) { + //if indexes are configured as "columns" + columns = i.columns; + } + if (kind && type) { + sql.push('ADD ' + kind + ' INDEX ' + iName + + ' (' + columns + ') ' + type); + } else { + sql.push('ADD ' + kind + ' INDEX ' + type + ' ' + iName + + ' (' + columns + ')'); + } + } + }); + return sql; + }; + + MySQL.prototype.getForeignKeySQL = function(model, actualFks) { + var self = this; + var m = this.getModelDefinition(model); + var addFksSql = []; + + if (actualFks) { + var keys = Object.keys(actualFks); + for (var i = 0; i < keys.length; i++) { + var constraint = self.buildForeignKeyDefinition(model, keys[i]); + + if (constraint) { + addFksSql.push('ADD ' + constraint); + } + } + } + return addFksSql; + }; + + MySQL.prototype.addForeignKeys = function(model, fkSQL, cb) { + var self = this; + var m = this.getModelDefinition(model); + + if ((!cb) && ('function' === typeof fkSQL)) { + cb = fkSQL; + fkSQL = undefined; + } + + if (!fkSQL) { + var newFks = m.settings.foreignKeys; + if (newFks) + fkSQL = self.getForeignKeySQL(model, newFks); + } + if (fkSQL && fkSQL.length) { + self.applySqlChanges(model, fkSQL, function(err, result) { + if (err) cb(err); + else + cb(null, result); + }); + } else cb(null, {}); + }; + + MySQL.prototype.dropForeignKeys = function(model, actualFks) { + var self = this; + var m = this.getModelDefinition(model); + + var fks = actualFks; + var sql = []; + var correctFks = m.settings.foreignKeys || {}; + + //drop foreign keys for removed fields + if (fks && fks.length) { + var removedFks = []; + fks.forEach(function(fk) { + var needsToDrop = false; + var newFk = correctFks[fk.fkName]; + if (newFk) { + var fkCol = expectedColNameForModel(newFk.foreignKey, m); + var fkEntity = self.getModelDefinition(newFk.entity); + var fkRefKey = expectedColNameForModel(newFk.entityKey, fkEntity); + var fkRefTable = newFk.entity.name; //TODO check for mysql name + needsToDrop = fkCol != fk.fkColumnName || + fkRefKey != fk.pkColumnName || + fkRefTable != fk.pkTableName; + } else { + needsToDrop = true; + } + + if (needsToDrop) { + sql.push('DROP FOREIGN KEY ' + fk.fkName); + removedFks.push(fk); //keep track that we removed these + } + }); + + //update out list of existing keys by removing dropped keys + fks = actualFks.filter(function(k) { + return removedFks.indexOf(k) == -1; + }); + } + return sql; + }; + + MySQL.prototype.getAlterStatement = function(model, statements) { + return statements.length ? + 'ALTER TABLE ' + this.tableEscaped(model) + ' ' + statements.join(',\n') : + ''; + }; + + MySQL.prototype.alterTable = function(model, actualFields, actualIndexes, actualFks, done, checkOnly) { + //if this is using an old signature, then grab the correct callback and check boolean + if ('function' == typeof actualFks && typeof done !== 'function') { + checkOnly = done || false; + done = actualFks; + } + var self = this; + + var statements = []; + + async.series([ + function(cb) { + statements = self.getAddModifyColumns(model, actualFields); + cb(); + }, + function(cb) { + statements = statements.concat(self.getDropColumns(model, actualFields)); + cb(); + }, + function(cb) { + statements = statements.concat(self.addIndexes(model, actualIndexes)); + cb(); + }, + function(cb) { + statements = statements.concat(self.dropForeignKeys(model, actualFks)); + cb(); + }, + ], function(err, result) { + if (err) done(err); + + //determine if there are column, index, or foreign keys changes (all require update) + if (statements.length) { + //get the required alter statements + var alterStmt = self.getAlterStatement(model, statements); + var stmtList = [alterStmt]; + + //set up an object to pass back all changes, changes that have been run, + //and foreign key statements that haven't been run + var retValues = { + statements: stmtList, + query: stmtList.join(';'), + }; + + //if we're running in read only mode OR if the only changes are foreign keys additions, + //then just return the object directly + if (checkOnly) { + done(null, true, retValues); + } else { + //if there are changes in the alter statement, then execute them and return the object + self.execute(alterStmt, function(err) { + done(err, true, retValues); + }); + } + } else { + done(); + } + }); + }; + + MySQL.prototype.buildForeignKeyDefinition = function(model, keyName) { + var definition = this.getModelDefinition(model); + + var fk = definition.settings.foreignKeys[keyName]; + if (fk) { + //get the definition of the referenced object + var fkEntityName = (typeof fk.entity === 'object') ? fk.entity.name : fk.entity; + + //verify that the other model in the same DB + if (this._models[fkEntityName]) { + return ' CONSTRAINT ' + this.client.escapeId(fk.name) + + ' FOREIGN KEY (' + fk.foreignKey + ')' + + ' REFERENCES ' + this.tableEscaped(fkEntityName) + + '(' + this.client.escapeId(fk.entityKey) + ')'; + } + } + return ''; + }; + + MySQL.prototype.buildColumnDefinitions = + MySQL.prototype.propertiesSQL = function(model) { + var self = this; + + var pks = this.idNames(model).map(function(i) { + return self.columnEscaped(model, i); + }); + + var definition = this.getModelDefinition(model); + var sql = []; + if (pks.length === 1) { + var idName = this.idName(model); + var idProp = this.getModelDefinition(model).properties[idName]; + if (idProp.generated) { + sql.push(self.columnEscaped(model, idName) + + ' INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY'); + } else { + idProp.nullable = false; + sql.push(self.columnEscaped(model, idName) + ' ' + + self.buildColumnDefinition(model, idName) + ' PRIMARY KEY'); + } + } + Object.keys(definition.properties).forEach(function(prop) { + if (self.id(model, prop) && pks.length === 1) { + return; + } + var colName = self.columnEscaped(model, prop); + sql.push(colName + ' ' + self.buildColumnDefinition(model, prop)); + }); + if (pks.length > 1) { + sql.push('PRIMARY KEY(' + pks.join(',') + ')'); + } + + var indexes = self.buildIndexes(model); + indexes.forEach(function(i) { + sql.push(i); + }); + + return sql.join(',\n '); + }; + + MySQL.prototype.buildIndex = function(model, property) { + var prop = this.getModelDefinition(model).properties[property]; + var i = prop && prop.index; + if (!i) { + return ''; + } + var type = ''; + var kind = ''; + if (i.type) { + type = 'USING ' + i.type; + } + if (i.kind) { + kind = i.kind; + } + var columnName = this.columnEscaped(model, property); + if (kind && type) { + return (kind + ' INDEX ' + columnName + ' (' + columnName + ') ' + type); + } else { + if (typeof i === 'object' && i.unique && i.unique === true) { + kind = 'UNIQUE'; + } + return (kind + ' INDEX ' + columnName + ' ' + type + ' (' + columnName + ') '); + } + }; + + MySQL.prototype.buildIndexes = function(model) { + var self = this; + var indexClauses = []; + var definition = this.getModelDefinition(model); + var indexes = definition.settings.indexes || {}; + // Build model level indexes + for (var index in indexes) { + var i = indexes[index]; + var type = ''; + var kind = ''; + if (i.type) { + type = 'USING ' + i.type; + } + if (i.kind) { + //if index uniqueness is configured as "kind" + kind = i.kind; + } else if (i.options && i.options.unique && i.options.unique == true) { + //if index unique indicator is configured + kind = 'UNIQUE'; + } + var indexedColumns = []; + var indexName = this.escapeName(index); + var columns = ''; + //if indexes are configured as "keys" + if (i.keys) { + //for each field in "keys" object + for (var key in i.keys) { + if (i.keys[key] !== -1) { + indexedColumns.push(key); + } else { + //mysql does not support index sorting Currently + //but mysql has added DESC keyword for future support + indexedColumns.push(key + ' DESC '); + } + } + } + if (indexedColumns.length) { + columns = indexedColumns.join(','); + } else if (i.columns) { + columns = i.columns; + } + if (columns.length) { + if (kind && type) { + indexClauses.push(kind + ' INDEX ' + + indexName + ' (' + columns + ') ' + type); + } else { + indexClauses.push(kind + ' INDEX ' + type + + ' ' + indexName + ' (' + columns + ')'); + } + } + } + // Define index for each of the properties + for (var p in definition.properties) { + var propIndex = self.buildIndex(model, p); + if (propIndex) { + indexClauses.push(propIndex); + } + } + return indexClauses; + }; + + MySQL.prototype.buildColumnDefinition = function(model, prop) { + var p = this.getModelDefinition(model).properties[prop]; + var line = this.columnDataType(model, prop) + ' ' + + (this.isNullable(p) ? 'NULL' : 'NOT NULL'); + return line; + }; + + MySQL.prototype.buildColumnType = function buildColumnType(propertyDefinition) { + var dt = ''; + var p = propertyDefinition; + switch (p.type.name) { + default: + case 'JSON': + case 'Object': + case 'Any': + case 'Text': + dt = columnType(p, 'TEXT'); + dt = stringOptionsByType(p, dt); + break; + case 'String': + dt = columnType(p, 'VARCHAR'); + dt = stringOptionsByType(p, dt); + break; + case 'Number': + dt = columnType(p, 'INT'); + dt = numericOptionsByType(p, dt); + break; + case 'Date': + dt = columnType(p, 'DATETIME'); // Currently doesn't need options. + break; + case 'Boolean': + dt = 'TINYINT(1)'; + break; + case 'Point': + case 'GeoPoint': + dt = 'POINT'; + break; + case 'Enum': + dt = 'ENUM(' + p.type._string + ')'; + dt = stringOptions(p, dt); // Enum columns can have charset/collation. + break; + } + return dt; + }; + + function columnType(p, defaultType) { + var dt = defaultType; + if (p.dataType) { + dt = String(p.dataType); + } + return dt; + } + + function stringOptionsByType(p, columnType) { + switch (columnType.toLowerCase()) { + default: + case 'varchar': + // The maximum length for an ID column is 1000 bytes + // The maximum row size is 64K + var len = p.length || p.limit || + ((p.type !== String) ? 4096 : p.id || p.index ? 255 : 512); + columnType += '(' + len + ')'; + break; + case 'char': + len = p.length || p.limit || 255; + columnType += '(' + len + ')'; + break; + + case 'text': + case 'tinytext': + case 'mediumtext': + case 'longtext': + + break; + } + columnType = stringOptions(p, columnType); + return columnType; + } + + function stringOptions(p, columnType) { + if (p.charset) { + columnType += ' CHARACTER SET ' + p.charset; + } + if (p.collation) { + columnType += ' COLLATE ' + p.collation; + } + return columnType; + } + + function numericOptionsByType(p, columnType) { + switch (columnType.toLowerCase()) { + default: + case 'tinyint': + case 'smallint': + case 'mediumint': + case 'int': + case 'integer': + case 'bigint': + columnType = integerOptions(p, columnType); + break; + + case 'decimal': + case 'numeric': + columnType = fixedPointOptions(p, columnType); + break; + + case 'float': + case 'double': + columnType = floatingPointOptions(p, columnType); + break; + } + columnType = unsigned(p, columnType); + return columnType; + } + + function floatingPointOptions(p, columnType) { + var precision = 16; + var scale = 8; + if (p.precision) { + precision = Number(p.precision); + } + if (p.scale) { + scale = Number(p.scale); + } + if (p.precision && p.scale) { + columnType += '(' + precision + ',' + scale + ')'; + } else if (p.precision) { + columnType += '(' + precision + ')'; + } + return columnType; + } + + /* @TODO: Change fixed point to use an arbitrary precision arithmetic library. */ + /* Currently fixed point will lose precision because it's turned to non-fixed in */ + /* JS. Also, defaulting column to (9,2) and not allowing non-specified 'DECIMAL' */ + /* declaration which would default to DECIMAL(10,0). Instead defaulting to (9,2). */ + function fixedPointOptions(p, columnType) { + var precision = 9; + var scale = 2; + if (p.precision) { + precision = Number(p.precision); + } + if (p.scale) { + scale = Number(p.scale); + } + columnType += '(' + precision + ',' + scale + ')'; + return columnType; + } + + function integerOptions(p, columnType) { + var tmp = 0; + if (p.display || p.limit) { + tmp = Number(p.display || p.limit); + } + if (tmp > 0) { + columnType += '(' + tmp + ')'; + } else if (p.unsigned) { + switch (columnType.toLowerCase()) { + default: + case 'int': + columnType += '(10)'; + break; + case 'mediumint': + columnType += '(8)'; + break; + case 'smallint': + columnType += '(5)'; + break; + case 'tinyint': + columnType += '(3)'; + break; + case 'bigint': + columnType += '(20)'; + break; + } + } else { + switch (columnType.toLowerCase()) { + default: + case 'int': + columnType += '(11)'; + break; + case 'mediumint': + columnType += '(9)'; + break; + case 'smallint': + columnType += '(6)'; + break; + case 'tinyint': + columnType += '(4)'; + break; + case 'bigint': + columnType += '(20)'; + break; + } + } + return columnType; + } + + function unsigned(p, columnType) { + if (p.unsigned) { + columnType += ' UNSIGNED'; + } + return columnType; + } + function expectedColNameForModel(propName, modelToCheck) { + var mysql = modelToCheck.properties[propName].mysql; + if (typeof mysql === 'undefined') { + return propName; + } + var colName = mysql.columnName; + if (typeof colName === 'undefined') { + return propName; + } + return colName; + } +} diff --git a/lib/mysql.js b/lib/mysql.js new file mode 100644 index 0000000..16b6404 --- /dev/null +++ b/lib/mysql.js @@ -0,0 +1,563 @@ +// Copyright IBM Corp. 2012,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +var g = require('strong-globalize')(); +var moment = require('moment'); + +/*! + * Module dependencies + */ +var mysql = require('mysql'); + +var SqlConnector = require('loopback-connector').SqlConnector; +var ParameterizedSQL = SqlConnector.ParameterizedSQL; +var EnumFactory = require('./enumFactory').EnumFactory; + +var debug = require('debug')('loopback:connector:mysql'); + +/** + * @module loopback-connector-mysql + * + * Initialize the MySQL connector against the given data source + * + * @param {DataSource} dataSource The loopback-datasource-juggler dataSource + * @param {Function} [callback] The callback function + */ +exports.initialize = function initializeDataSource(dataSource, callback) { + dataSource.driver = mysql; // Provide access to the native driver + dataSource.connector = new MySQL(dataSource.settings); + dataSource.connector.dataSource = dataSource; + + defineMySQLTypes(dataSource); + + dataSource.EnumFactory = EnumFactory; // factory for Enums. Note that currently Enums can not be registered. + + if (callback) { + if (dataSource.settings.lazyConnect) { + process.nextTick(function() { + callback(); + }); + } else { + dataSource.connector.connect(callback); + } + } +}; + +exports.MySQL = MySQL; + +function defineMySQLTypes(dataSource) { + var modelBuilder = dataSource.modelBuilder; + var defineType = modelBuilder.defineValueType ? + // loopback-datasource-juggler 2.x + modelBuilder.defineValueType.bind(modelBuilder) : + // loopback-datasource-juggler 1.x + modelBuilder.constructor.registerType.bind(modelBuilder.constructor); + + // The Point type is inherited from jugglingdb mysql adapter. + // LoopBack uses GeoPoint instead. + // The Point type can be removed at some point in the future. + defineType(function Point() { + }); +} + +/** + * @constructor + * Constructor for MySQL connector + * @param {Object} client The node-mysql client object + */ +function MySQL(settings) { + SqlConnector.call(this, 'mysql', settings); +} + +require('util').inherits(MySQL, SqlConnector); + +MySQL.prototype.connect = function(callback) { + var self = this; + var options = generateOptions(this.settings); + var s = self.settings || {}; + + // arsis 2017-03-21 + var tz = this.settings.timezone; + if (!isNaN(tz)) { + this.tzOffset = parseInt(tz.substring(0,3)); + } else { + var momentTz = moment().format('Z').replace(':', ''); + this.tzOffset = parseInt(momentTz.substring(0,3)); + } + // end arsis + + if (this.client) { + if (callback) { + process.nextTick(function() { + callback(null, self.client); + }); + } + } else { + this.client = mysql.createPool(options); + this.client.getConnection(function(err, connection) { + var conn = connection; + if (!err) { + if (self.debug) { + debug('MySQL connection is established: ' + self.settings || {}); + } + connection.release(); + } else { + if (self.debug || !callback) { + console.error('MySQL connection is failed: ' + self.settings || {}, err); + } + } + callback && callback(err, conn); + }); + } +}; + +function generateOptions(settings) { + var s = settings || {}; + if (s.collation) { + // Charset should be first 'chunk' of collation. + s.charset = s.collation.substr(0, s.collation.indexOf('_')); + } else { + s.collation = 'utf8_general_ci'; + s.charset = 'utf8'; + } + + s.supportBigNumbers = (s.supportBigNumbers || false); + s.timezone = (s.timezone || 'local'); + + if (isNaN(s.connectionLimit)) { + s.connectionLimit = 10; + } + + var options; + if (s.url) { + // use url to override other settings if url provided + options = s.url; + } else { + options = { + host: s.host || s.hostname || 'localhost', + port: s.port || 3306, + user: s.username || s.user, + password: s.password, + timezone: s.timezone, + socketPath: s.socketPath, + charset: s.collation.toUpperCase(), // Correct by docs despite seeming odd. + supportBigNumbers: s.supportBigNumbers, + connectionLimit: s.connectionLimit, + }; + + // Don't configure the DB if the pool can be used for multiple DBs + if (!s.createDatabase) { + options.database = s.database; + } + + // Take other options for mysql driver + // See https://github.com/strongloop/loopback-connector-mysql/issues/46 + for (var p in s) { + if (p === 'database' && s.createDatabase) { + continue; + } + if (options[p] === undefined) { + options[p] = s[p]; + } + } + } + return options; +} +/** + * Execute the sql statement + * + * @param {String} sql The SQL statement + * @param {Function} [callback] The callback after the SQL statement is executed + */ +MySQL.prototype.executeSQL = function(sql, params, options, callback) { + var self = this; + var client = this.client; + var debugEnabled = debug.enabled; + var db = this.settings.database; + if (typeof callback !== 'function') { + throw new Error(g.f('{{callback}} should be a function')); + } + if (debugEnabled) { + debug('SQL: %s, params: %j', sql, params); + } + + var transaction = options.transaction; + + function handleResponse(connection, err, result) { + if (!transaction) { + connection.release(); + } + callback && callback(err, result); + } + + function runQuery(connection, release) { + connection.query(sql, params, function(err, data) { + if (debugEnabled) { + if (err) { + debug('Error: %j', err); + } + debug('Data: ', data); + } + handleResponse(connection, err, data); + }); + } + + function executeWithConnection(err, connection) { + if (err) { + return callback && callback(err); + } + if (self.settings.createDatabase) { + // Call USE db ... + connection.query('USE ??', [db], function(err) { + if (err) { + if (err && err.message.match(/(^|: )unknown database/i)) { + var charset = self.settings.charset; + var collation = self.settings.collation; + var q = 'CREATE DATABASE ?? CHARACTER SET ?? COLLATE ??'; + connection.query(q, [db, charset, collation], function(err) { + if (!err) { + connection.query('USE ??', [db], function(err) { + runQuery(connection); + }); + } else { + handleResponse(connection, err); + } + }); + return; + } else { + handleResponse(connection, err); + return; + } + } + runQuery(connection); + }); + } else { + // Bypass USE db + runQuery(connection); + } + } + + if (transaction && transaction.connection && + transaction.connector === this) { + if (debugEnabled) { + debug('Execute SQL within a transaction'); + } + executeWithConnection(null, transaction.connection); + } else { + client.getConnection(executeWithConnection); + } +}; + +MySQL.prototype._modifyOrCreate = function(model, data, options, fields, cb) { + var sql = new ParameterizedSQL('INSERT INTO ' + this.tableEscaped(model)); + var columnValues = fields.columnValues; + var fieldNames = fields.names; + if (fieldNames.length) { + sql.merge('(' + fieldNames.join(',') + ')', ''); + var values = ParameterizedSQL.join(columnValues, ','); + values.sql = 'VALUES(' + values.sql + ')'; + sql.merge(values); + } else { + sql.merge(this.buildInsertDefaultValues(model, data, options)); + } + + sql.merge('ON DUPLICATE KEY UPDATE'); + var setValues = []; + for (var i = 0, n = fields.names.length; i < n; i++) { + if (!fields.properties[i].id) { + setValues.push(new ParameterizedSQL(fields.names[i] + '=' + + columnValues[i].sql, columnValues[i].params)); + } + } + + sql.merge(ParameterizedSQL.join(setValues, ',')); + + this.execute(sql.sql, sql.params, options, function(err, info) { + if (!err && info && info.insertId) { + data.id = info.insertId; + } + var meta = {}; + if (info) { + // When using the INSERT ... ON DUPLICATE KEY UPDATE statement, + // the returned value is as follows: + // 1 for each successful INSERT. + // 2 for each successful UPDATE. + meta.isNewInstance = (info.affectedRows === 1); + } + cb(err, data, meta); + }); +}; + +/** + * Replace if the model instance exists with the same id or create a new instance + * + * @param {String} model The model name + * @param {Object} data The model instance data + * @param {Object} options The options + * @param {Function} [cb] The callback function + */ +MySQL.prototype.replaceOrCreate = function(model, data, options, cb) { + var fields = this.buildReplaceFields(model, data); + this._modifyOrCreate(model, data, options, fields, cb); +}; + +/** + * Update if the model instance exists with the same id or create a new instance + * + * @param {String} model The model name + * @param {Object} data The model instance data + * @param {Object} options The options + * @param {Function} [cb] The callback function + */ +MySQL.prototype.save = +MySQL.prototype.updateOrCreate = function(model, data, options, cb) { + var fields = this.buildFields(model, data); + this._modifyOrCreate(model, data, options, fields, cb); +}; + +// arsis 2017-03-21 add tzOffset +function dateToMysql(val, tzOffset) { + var tz = tzOffset || 0; + return val.getUTCFullYear() + '-' + + fillZeros(val.getUTCMonth() + 1) + '-' + + fillZeros(val.getUTCDate()) + ' ' + + fillZeros(val.getUTCHours() + tz) + ':' + + fillZeros(val.getUTCMinutes()) + ':' + + fillZeros(val.getUTCSeconds()); + + function fillZeros(v) { + return v < 10 ? '0' + v : v; + } +} + +MySQL.prototype.getInsertedId = function(model, info) { + var insertedId = info && typeof info.insertId === 'number' ? + info.insertId : undefined; + return insertedId; +}; + +/*! + * Convert property name/value to an escaped DB column value + * @param {Object} prop Property descriptor + * @param {*} val Property value + * @returns {*} The escaped value of DB column + */ +MySQL.prototype.toColumnValue = function(prop, val) { + if (val == null) { + if (prop.autoIncrement || prop.id) { + return new ParameterizedSQL('DEFAULT'); + } + return null; + } + if (!prop) { + return val; + } + if (prop.type === String) { + return String(val); + } + if (prop.type === Number) { + if (isNaN(val)) { + // FIXME: [rfeng] Should fail fast? + return val; + } + return val; + } + if (prop.type === Date) { + if (!val.toUTCString) { + val = new Date(val); + } + return dateToMysql(val, this.tzOffset); // arsis 2017-03-21 add tzOffset + } + if (prop.type === Boolean) { + return !!val; + } + if (prop.type.name === 'GeoPoint') { + return new ParameterizedSQL({ + sql: 'Point(?,?)', + params: [val.lat, val.lng], + }); + } + if (prop.type === Object) { + return this._serializeObject(val); + } + if (typeof prop.type === 'function') { + return this._serializeObject(val); + } + return this._serializeObject(val); +}; + +MySQL.prototype._serializeObject = function(obj) { + var val; + if (obj && typeof obj.toJSON === 'function') { + obj = obj.toJSON(); + } + if (typeof obj !== 'string') { + val = JSON.stringify(obj); + } else { + val = obj; + } + return val; +}; + +/*! + * Convert the data from database column to model property + * @param {object} Model property descriptor + * @param {*) val Column value + * @returns {*} Model property value + */ +MySQL.prototype.fromColumnValue = function(prop, val) { + if (val == null) { + return val; + } + if (prop) { + switch (prop.type.name) { + case 'Number': + val = Number(val); + break; + case 'String': + val = String(val); + break; + case 'Date': + + // MySQL allows, unless NO_ZERO_DATE is set, dummy date/time entries + // new Date() will return Invalid Date for those, so we need to handle + // those separate. + if (val == '0000-00-00 00:00:00') { + val = null; + } else { + val = new Date(val.toString().replace(/GMT.*$/, 'GMT')); + } + break; + case 'Boolean': + val = Boolean(val); + break; + case 'GeoPoint': + case 'Point': + val = { + lat: val.x, + lng: val.y, + }; + break; + case 'List': + case 'Array': + case 'Object': + case 'JSON': + if (typeof val === 'string') { + val = JSON.parse(val); + } + break; + default: + if (!Array.isArray(prop.type) && !prop.type.modelName) { + // Do not convert array and model types + val = prop.type(val); + } + break; + } + } + return val; +}; + +/** + * Escape an identifier such as the column name + * @param {string} name A database identifier + * @returns {string} The escaped database identifier + */ +MySQL.prototype.escapeName = function(name) { + return this.client.escapeId(name); +}; + +/** + * Build the LIMIT clause + * @param {string} model Model name + * @param {number} limit The limit + * @param {number} offset The offset + * @returns {string} The LIMIT clause + */ +MySQL.prototype._buildLimit = function(model, limit, offset) { + if (isNaN(limit)) { + limit = 0; + } + if (isNaN(offset)) { + offset = 0; + } + if (!limit && !offset) { + return ''; + } + return 'LIMIT ' + (offset ? (offset + ',' + limit) : limit); +}; + +MySQL.prototype.applyPagination = function(model, stmt, filter) { + var limitClause = this._buildLimit(model, filter.limit, + filter.offset || filter.skip); + return stmt.merge(limitClause); +}; + +/** + * Get the place holder in SQL for identifiers, such as ?? + * @param {String} key Optional key, such as 1 or id + * @returns {String} The place holder + */ +MySQL.prototype.getPlaceholderForIdentifier = function(key) { + return '??'; +}; + +/** + * Get the place holder in SQL for values, such as :1 or ? + * @param {String} key Optional key, such as 1 or id + * @returns {String} The place holder + */ +MySQL.prototype.getPlaceholderForValue = function(key) { + return '?'; +}; + +MySQL.prototype.getCountForAffectedRows = function(model, info) { + var affectedRows = info && typeof info.affectedRows === 'number' ? + info.affectedRows : undefined; + return affectedRows; +}; + +/** + * Disconnect from MySQL + */ +MySQL.prototype.disconnect = function(cb) { + if (this.debug) { + debug('disconnect'); + } + if (this.client) { + this.client.end(cb); + } else { + process.nextTick(cb); + } +}; + +MySQL.prototype.ping = function(cb) { + this.execute('SELECT 1 AS result', cb); +}; + +MySQL.prototype.buildExpression = function(columnName, operator, operatorValue, + propertyDefinition) { + if (operator === 'regexp') { + if (operatorValue.ignoreCase) + g.warn('{{MySQL}} {{regex}} syntax does not respect the {{`i`}} flag'); + + if (operatorValue.global) + g.warn('{{MySQL}} {{regex}} syntax does not respect the {{`g`}} flag'); + + if (operatorValue.multiline) + g.warn('{{MySQL}} {{regex}} syntax does not respect the {{`m`}} flag'); + + return new ParameterizedSQL(columnName + ' REGEXP ?', + [operatorValue.source]); + } + + // invoke the base implementation of `buildExpression` + return this.invokeSuper('buildExpression', columnName, operator, + operatorValue, propertyDefinition); +}; + +require('./migration')(MySQL, mysql); +require('./discovery')(MySQL, mysql); +require('./transaction')(MySQL, mysql); diff --git a/lib/transaction.js b/lib/transaction.js new file mode 100644 index 0000000..926aa07 --- /dev/null +++ b/lib/transaction.js @@ -0,0 +1,68 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +var debug = require('debug')('loopback:connector:mysql:transaction'); +module.exports = mixinTransaction; + +/*! + * @param {MySQL} MySQL connector class + * @param {Object} mysql mysql driver + */ +function mixinTransaction(MySQL, mysql) { + /** + * Begin a new transaction + * @param isolationLevel + * @param cb + */ + MySQL.prototype.beginTransaction = function(isolationLevel, cb) { + debug('Begin a transaction with isolation level: %s', isolationLevel); + this.client.getConnection(function(err, connection) { + if (err) return cb(err); + if (isolationLevel) { + connection.query( + 'SET SESSION TRANSACTION ISOLATION LEVEL ' + isolationLevel, + function(err) { + if (err) return cb(err); + connection.beginTransaction(function(err) { + if (err) return cb(err); + return cb(null, connection); + }); + }); + } else { + connection.beginTransaction(function(err) { + if (err) return cb(err); + return cb(null, connection); + }); + } + }); + }; + + /** + * + * @param connection + * @param cb + */ + MySQL.prototype.commit = function(connection, cb) { + debug('Commit a transaction'); + connection.commit(function(err) { + connection.release(); + cb(err); + }); + }; + + /** + * + * @param connection + * @param cb + */ + MySQL.prototype.rollback = function(connection, cb) { + debug('Rollback a transaction'); + connection.rollback(function(err) { + connection.release(); + cb(err); + }); + }; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2e5180d --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "loopback-connector-mysql", + "version": "3.0.0", + "description": "MySQL connector for loopback-datasource-juggler", + "engines": { + "node": ">=4" + }, + "main": "index.js", + "scripts": { + "pretest": "node pretest.js", + "lint": "eslint .", + "test": "mocha --timeout 10000 test/*.js", + "posttest": "npm run lint" + }, + "dependencies": { + "async": "^0.9.0", + "debug": "^2.1.1", + "loopback-connector": "^4.0.0", + "mysql": "^2.11.1", + "strong-globalize": "^2.5.8" + }, + "devDependencies": { + "bluebird": "~2.9.10", + "eslint": "^2.13.1", + "eslint-config-loopback": "^4.0.0", + "loopback-datasource-juggler": "^3.0.0", + "mocha": "^2.1.0", + "rc": "^1.0.0", + "should": "^8.0.2", + "sinon": "^1.15.4" + }, + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-connector-mysql.git" + }, + "license": "MIT" +} diff --git a/pretest.js b/pretest.js new file mode 100644 index 0000000..cbf6947 --- /dev/null +++ b/pretest.js @@ -0,0 +1,43 @@ +'use strict'; + +if (!process.env.TEST_MYSQL_USER && + !process.env.MYSQL_USER && + !process.env.CI) { + return console.log('Not seeding DB with test db'); +} + +process.env.TEST_MYSQL_HOST = + process.env.TEST_MYSQL_HOST || process.env.MYSQL_HOST || 'localhost'; +process.env.TEST_MYSQL_PORT = + process.env.TEST_MYSQL_PORT || process.env.MYSQL_PORT || 3306; +process.env.TEST_MYSQL_USER = + process.env.TEST_MYSQL_USER || process.env.MYSQL_USER || 'test'; +process.env.TEST_MYSQL_PASSWORD = + process.env.TEST_MYSQL_PASSWORD || process.env.MYSQL_PASSWORD || 'test'; + +var fs = require('fs'); +var cp = require('child_process'); + +var sql = fs.createReadStream(require.resolve('./test/schema.sql')); +var stdio = ['pipe', process.stdout, process.stderr]; +var args = ['--user=' + process.env.TEST_MYSQL_USER]; + +if (process.env.TEST_MYSQL_HOST) { + args.push('--host=' + process.env.TEST_MYSQL_HOST); +} +if (process.env.TEST_MYSQL_PORT) { + args.push('--port=' + process.env.TEST_MYSQL_PORT); +} +if (process.env.TEST_MYSQL_PASSWORD) { + args.push('--password=' + process.env.TEST_MYSQL_PASSWORD); +} + +console.log('seeding DB with example db...'); +var mysql = cp.spawn('mysql', args, {stdio: stdio}); +sql.pipe(mysql.stdin); +mysql.on('exit', function(code) { + console.log('done seeding DB'); + setTimeout(function() { + process.exit(code); + }, 200); +}); diff --git a/test/connection.test.js b/test/connection.test.js new file mode 100644 index 0000000..33d58ec --- /dev/null +++ b/test/connection.test.js @@ -0,0 +1,176 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +require('./init.js'); +var assert = require('assert'); +var should = require('should'); +var DataSource = require('loopback-datasource-juggler').DataSource; +var mysqlConnector = require('../'); +var url = require('url'); + +var db, DummyModel, odb, config; + +describe('connections', function() { + before(function() { + require('./init.js'); + + config = global.getConfig(); + + odb = getDataSource({collation: 'utf8_general_ci', createDatabase: true}); + db = odb; + }); + + it('should pass with valid settings', function(done) { + var db = new DataSource(mysqlConnector, config); + db.ping(done); + }); + + it('ignores all other settings when url is present', function(done) { + var formatedUrl = generateURL(config); + var dbConfig = { + url: formatedUrl, + host: 'invalid-hostname', + port: 80, + database: 'invalid-database', + username: 'invalid-username', + password: 'invalid-password', + }; + + var db = new DataSource(mysqlConnector, dbConfig); + db.ping(done); + }); + + it('should use utf8 charset', function(done) { + var test_set = /utf8/; + var test_collo = /utf8_general_ci/; + var test_set_str = 'utf8'; + var test_set_collo = 'utf8_general_ci'; + charsetTest(test_set, test_collo, test_set_str, test_set_collo, done); + }); + + it('should disconnect first db', function(done) { + db.disconnect(function() { + odb = getDataSource(); + done(); + }); + }); + + it('should use latin1 charset', function(done) { + var test_set = /latin1/; + var test_collo = /latin1_general_ci/; + var test_set_str = 'latin1'; + var test_set_collo = 'latin1_general_ci'; + charsetTest(test_set, test_collo, test_set_str, test_set_collo, done); + }); + + it('should drop db and disconnect all', function(done) { + db.connector.execute('DROP DATABASE IF EXISTS ' + db.settings.database, function(err) { + db.disconnect(function() { + done(); + }); + }); + }); + + describe('lazyConnect', function() { + it('should skip connect phase (lazyConnect = true)', function(done) { + var dbConfig = { + host: '127.0.0.1', + port: 4, + lazyConnect: true, + }; + var ds = new DataSource(mysqlConnector, dbConfig); + + var errTimeout = setTimeout(function() { + done(); + }, 2000); + ds.on('error', function(err) { + clearTimeout(errTimeout); + done(err); + }); + }); + + it('should report connection error (lazyConnect = false)', function(done) { + var dbConfig = { + host: '127.0.0.1', + port: 4, + lazyConnect: false, + }; + var ds = new DataSource(mysqlConnector, dbConfig); + + ds.on('error', function(err) { + err.message.should.containEql('ECONNREFUSED'); + done(); + }); + }); + }); +}); + +function charsetTest(test_set, test_collo, test_set_str, test_set_collo, done) { + query('DROP DATABASE IF EXISTS ' + odb.settings.database, function(err) { + assert.ok(!err); + odb.disconnect(function() { + db = getDataSource({collation: test_set_collo, createDatabase: true}); + DummyModel = db.define('DummyModel', {string: String}); + db.automigrate(function() { + var q = 'SELECT DEFAULT_COLLATION_NAME' + + ' FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ' + + db.driver.escape(db.settings.database) + ' LIMIT 1'; + db.connector.execute(q, function(err, r) { + assert.ok(!err); + should(r[0].DEFAULT_COLLATION_NAME).match(test_collo); + db.connector.execute('SHOW VARIABLES LIKE "character_set%"', function(err, r) { + assert.ok(!err); + var hit_all = 0; + for (var result in r) { + hit_all += matchResult(r[result], 'character_set_connection', test_set); + hit_all += matchResult(r[result], 'character_set_database', test_set); + hit_all += matchResult(r[result], 'character_set_results', test_set); + hit_all += matchResult(r[result], 'character_set_client', test_set); + } + assert.equal(hit_all, 4); + }); + db.connector.execute('SHOW VARIABLES LIKE "collation%"', function(err, r) { + assert.ok(!err); + var hit_all = 0; + for (var result in r) { + hit_all += matchResult(r[result], 'collation_connection', test_set); + hit_all += matchResult(r[result], 'collation_database', test_set); + } + assert.equal(hit_all, 2); + done(); + }); + }); + }); + }); + }); +} + +function matchResult(result, variable_name, match) { + if (result.Variable_name === variable_name) { + assert.ok(result.Value.match(match)); + return 1; + } + return 0; +} + +var query = function(sql, cb) { + odb.connector.execute(sql, cb); +}; + +function generateURL(config) { + var urlObj = { + protocol: 'mysql', + auth: config.username || '', + hostname: config.host, + pathname: config.database, + slashes: true, + }; + if (config.password) { + urlObj.auth += ':' + config.password; + } + var formatedUrl = url.format(urlObj); + return formatedUrl; +} diff --git a/test/datatypes.test.js b/test/datatypes.test.js new file mode 100644 index 0000000..3fff9ad --- /dev/null +++ b/test/datatypes.test.js @@ -0,0 +1,155 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +require('./init.js'); +var assert = require('assert'); + +var db, EnumModel, ANIMAL_ENUM; +var mysqlVersion; + +describe('MySQL specific datatypes', function() { + before(setup); + + it('should run migration', function(done) { + db.automigrate(function() { + done(); + }); + }); + + it('An enum should parse itself', function(done) { + assert.equal(ANIMAL_ENUM.CAT, ANIMAL_ENUM('cat')); + assert.equal(ANIMAL_ENUM.CAT, ANIMAL_ENUM('CAT')); + assert.equal(ANIMAL_ENUM.CAT, ANIMAL_ENUM(2)); + assert.equal(ANIMAL_ENUM.CAT, 'cat'); + assert.equal(ANIMAL_ENUM(null), null); + assert.equal(ANIMAL_ENUM(''), ''); + assert.equal(ANIMAL_ENUM(0), ''); + done(); + }); + + it('should create a model instance with Enums', function(done) { + var em = EnumModel.create({animal: ANIMAL_ENUM.CAT, condition: 'sleepy', mood: 'happy'}, function(err, obj) { + assert.ok(!err); + assert.equal(obj.condition, 'sleepy'); + EnumModel.findOne({where: {animal: ANIMAL_ENUM.CAT}}, function(err, found) { + assert.ok(!err); + assert.equal(found.mood, 'happy'); + assert.equal(found.animal, ANIMAL_ENUM.CAT); + done(); + }); + }); + }); + + it('should fail spectacularly with invalid enum values', function(done) { + // In MySQL 5.6/5.7, An ENUM value must be one of those listed in the column definition, + // or the internal numeric equivalent thereof. Invalid values are rejected. + // Reference: http://dev.mysql.com/doc/refman/5.7/en/constraint-enum.html + EnumModel.create({animal: 'horse', condition: 'sleepy', mood: 'happy'}, function(err, obj) { + assert.ok(err); + assert.equal(err.code, 'WARN_DATA_TRUNCATED'); + assert.equal(err.errno, 1265); + done(); + }); + }); + + it('should create a model instance with object/json types', function(done) { + var note = {a: 1, b: '2'}; + var extras = {c: 3, d: '4'}; + var em = EnumModel.create({animal: ANIMAL_ENUM.DOG, condition: 'sleepy', + mood: 'happy', note: note, extras: extras}, function(err, obj) { + assert.ok(!err); + assert.equal(obj.condition, 'sleepy'); + EnumModel.findOne({where: {animal: ANIMAL_ENUM.DOG}}, function(err, found) { + assert.ok(!err); + assert.equal(found.mood, 'happy'); + assert.equal(found.animal, ANIMAL_ENUM.DOG); + assert.deepEqual(found.note, note); + assert.deepEqual(found.extras, extras); + done(); + }); + }); + }); + + it('should disconnect when done', function(done) { + db.disconnect(); + done(); + }); +}); + +function setup(done) { + require('./init.js'); + + db = getSchema(); + + ANIMAL_ENUM = db.EnumFactory('dog', 'cat', 'mouse'); + + EnumModel = db.define('EnumModel', { + animal: {type: ANIMAL_ENUM, null: false}, + condition: {type: db.EnumFactory('hungry', 'sleepy', 'thirsty')}, + mood: {type: db.EnumFactory('angry', 'happy', 'sad')}, + note: Object, + extras: 'JSON', + }); + + query('SELECT VERSION()', function(err, res) { + mysqlVersion = res && res[0] && res[0]['VERSION()']; + blankDatabase(db, done); + }); +} + +var query = function(sql, cb) { + db.adapter.execute(sql, cb); +}; + +var blankDatabase = function(db, cb) { + var dbn = db.settings.database; + var cs = db.settings.charset; + var co = db.settings.collation; + query('DROP DATABASE IF EXISTS ' + dbn, function(err) { + var q = 'CREATE DATABASE ' + dbn; + if (cs) { + q += ' CHARACTER SET ' + cs; + } + if (co) { + q += ' COLLATE ' + co; + } + query(q, function(err) { + query('USE ' + dbn, cb); + }); + }); +}; + +var getFields = function(model, cb) { + query('SHOW FIELDS FROM ' + model, function(err, res) { + if (err) { + cb(err); + } else { + var fields = {}; + res.forEach(function(field) { + fields[field.Field] = field; + }); + cb(err, fields); + } + }); +}; + +var getIndexes = function(model, cb) { + query('SHOW INDEXES FROM ' + model, function(err, res) { + if (err) { + console.log(err); + cb(err); + } else { + var indexes = {}; + // Note: this will only show the first key of compound keys + res.forEach(function(index) { + if (parseInt(index.Seq_in_index, 10) == 1) { + indexes[index.Key_name] = index; + } + }); + cb(err, indexes); + } + }); +}; diff --git a/test/helpers/platform.js b/test/helpers/platform.js new file mode 100644 index 0000000..394599c --- /dev/null +++ b/test/helpers/platform.js @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +exports.isWindows = /^win/.test(process.platform); diff --git a/test/imported.test.js b/test/imported.test.js new file mode 100644 index 0000000..3437d88 --- /dev/null +++ b/test/imported.test.js @@ -0,0 +1,14 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +describe('mysql imported features', function() { + before(function() { + require('./init.js'); + }); + + require('loopback-datasource-juggler/test/common.batch.js'); + require('loopback-datasource-juggler/test/include.test.js'); +}); diff --git a/test/init.js b/test/init.js new file mode 100644 index 0000000..af9efbe --- /dev/null +++ b/test/init.js @@ -0,0 +1,42 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +module.exports = require('should'); + +var DataSource = require('loopback-datasource-juggler').DataSource; + +var config = require('rc')('loopback', {test: {mysql: {}}}).test.mysql; +console.log(config); +global.getConfig = function(options) { + var dbConf = { + host: process.env.MYSQL_HOST || config.host || 'localhost', + port: process.env.MYSQL_PORT || config.port || 3306, + database: 'myapp_test', + username: process.env.MYSQL_USER || config.username, + password: process.env.MYSQL_PASSWORD || config.password, + createDatabase: true, + }; + + if (options) { + for (var el in options) { + dbConf[el] = options[el]; + } + } + return dbConf; +}; + +global.getDataSource = global.getSchema = function(options) { + var db = new DataSource(require('../'), getConfig(options)); + return db; +}; + +global.connectorCapabilities = { + ilike: false, + nilike: false, +}; + +global.sinon = require('sinon'); diff --git a/test/migration.test.js b/test/migration.test.js new file mode 100644 index 0000000..bd32308 --- /dev/null +++ b/test/migration.test.js @@ -0,0 +1,559 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +var assert = require('assert'); +var async = require('async'); +var platform = require('./helpers/platform'); +var should = require('./init'); +var Schema = require('loopback-datasource-juggler').Schema; + +var db, UserData, StringData, NumberData, DateData; +var mysqlVersion; + +describe('migrations', function() { + before(setup); + + it('should run migration', function(done) { + db.automigrate(function() { + done(); + }); + }); + + it('UserData should have correct columns', function(done) { + getFields('UserData', function(err, fields) { + if (!fields) return done(); + fields.should.be.eql({ + id: { + Field: 'id', + Type: 'int(11)', + Null: 'NO', + Key: 'PRI', + Default: null, + Extra: 'auto_increment'}, + email: { + Field: 'email', + Type: 'varchar(255)', + Null: 'NO', + Key: 'MUL', + Default: null, + Extra: ''}, + name: { + Field: 'name', + Type: 'varchar(512)', + Null: 'YES', + Key: '', + Default: null, + Extra: ''}, + bio: { + Field: 'bio', + Type: 'text', + Null: 'YES', + Key: '', + Default: null, + Extra: ''}, + birthDate: { + Field: 'birthDate', + Type: 'datetime', + Null: 'YES', + Key: '', + Default: null, + Extra: ''}, + pendingPeriod: { + Field: 'pendingPeriod', + Type: 'int(11)', + Null: 'YES', + Key: '', + Default: null, + Extra: ''}, + createdByAdmin: { + Field: 'createdByAdmin', + Type: 'tinyint(1)', + Null: 'YES', + Key: '', + Default: null, + Extra: ''}, + }); + done(); + }); + }); + + it('UserData should have correct indexes', function(done) { + // Note: getIndexes truncates multi-key indexes to the first member. + // Hence index1 is correct. + getIndexes('UserData', function(err, fields) { + if (!fields) return done(); + fields.should.match({ + PRIMARY: { + Table: /UserData/i, + Non_unique: 0, + Key_name: 'PRIMARY', + Seq_in_index: 1, + Column_name: 'id', + Collation: 'A', + // XXX: this actually has more to do with whether the table existed or not and + // what kind of data is in it that MySQL has analyzed: + // https://dev.mysql.com/doc/refman/5.5/en/show-index.html + // Cardinality: /^5\.[567]/.test(mysqlVersion) ? 0 : null, + Sub_part: null, + Packed: null, + Null: '', + Index_type: 'BTREE', + Comment: ''}, + email: { + Table: /UserData/i, + Non_unique: 1, + Key_name: 'email', + Seq_in_index: 1, + Column_name: 'email', + Collation: 'A', + // XXX: this actually has more to do with whether the table existed or not and + // what kind of data is in it that MySQL has analyzed: + // https://dev.mysql.com/doc/refman/5.5/en/show-index.html + // Cardinality: /^5\.[567]/.test(mysqlVersion) ? 0 : null, + Sub_part: null, + Packed: null, + Null: '', + Index_type: 'BTREE', + Comment: ''}, + index0: { + Table: /UserData/i, + Non_unique: 1, + Key_name: 'index0', + Seq_in_index: 1, + Column_name: 'email', + Collation: 'A', + // XXX: this actually has more to do with whether the table existed or not and + // what kind of data is in it that MySQL has analyzed: + // https://dev.mysql.com/doc/refman/5.5/en/show-index.html + // Cardinality: /^5\.[567]/.test(mysqlVersion) ? 0 : null, + Sub_part: null, + Packed: null, + Null: '', + Index_type: 'BTREE', + Comment: ''}, + }); + done(); + }); + }); + + it('StringData should have correct columns', function(done) { + getFields('StringData', function(err, fields) { + fields.should.be.eql({ + idString: {Field: 'idString', + Type: 'varchar(255)', + Null: 'NO', + Key: 'PRI', + Default: null, + Extra: ''}, + smallString: {Field: 'smallString', + Type: 'char(127)', + Null: 'NO', + Key: 'MUL', + Default: null, + Extra: ''}, + mediumString: {Field: 'mediumString', + Type: 'varchar(255)', + Null: 'NO', + Key: '', + Default: null, + Extra: ''}, + tinyText: {Field: 'tinyText', + Type: 'tinytext', + Null: 'YES', + Key: '', + Default: null, + Extra: ''}, + giantJSON: {Field: 'giantJSON', + Type: 'longtext', + Null: 'YES', + Key: '', + Default: null, + Extra: ''}, + text: {Field: 'text', + Type: 'varchar(1024)', + Null: 'YES', + Key: '', + Default: null, + Extra: ''}, + }); + done(); + }); + }); + + it('NumberData should have correct columns', function(done) { + getFields('NumberData', function(err, fields) { + fields.should.be.eql({ + id: {Field: 'id', + Type: 'int(11)', + Null: 'NO', + Key: 'PRI', + Default: null, + Extra: 'auto_increment'}, + number: {Field: 'number', + Type: 'decimal(10,3) unsigned', + Null: 'NO', + Key: 'MUL', + Default: null, + Extra: ''}, + tinyInt: {Field: 'tinyInt', + Type: 'tinyint(2)', + Null: 'YES', + Key: '', + Default: null, + Extra: ''}, + mediumInt: {Field: 'mediumInt', + Type: 'mediumint(8) unsigned', + Null: 'NO', + Key: '', + Default: null, + Extra: ''}, + floater: {Field: 'floater', + Type: 'double(14,6)', + Null: 'YES', + Key: '', + Default: null, + Extra: ''}, + }); + done(); + }); + }); + + it('DateData should have correct columns', function(done) { + getFields('DateData', function(err, fields) { + fields.should.be.eql({ + id: {Field: 'id', + Type: 'int(11)', + Null: 'NO', + Key: 'PRI', + Default: null, + Extra: 'auto_increment'}, + dateTime: {Field: 'dateTime', + Type: 'datetime', + Null: 'YES', + Key: '', + Default: null, + Extra: ''}, + timestamp: {Field: 'timestamp', + Type: 'timestamp', + Null: 'YES', + Key: '', + Default: null, + Extra: ''}, + }); + done(); + }); + }); + + it('should autoupdate', function(done) { + // With an install of MYSQL5.7 on windows, these queries `randomly` fail and raise errors + // especially with decimals, number and Date format. + if (platform.isWindows) { + return done(); + } + var userExists = function(cb) { + query('SELECT * FROM UserData', function(err, res) { + cb(!err && res[0].email == 'test@example.com'); + }); + }; + + UserData.create({email: 'test@example.com'}, function(err, user) { + assert.ok(!err, 'Could not create user: ' + err); + userExists(function(yep) { + assert.ok(yep, 'User does not exist'); + }); + UserData.defineProperty('email', {type: String}); + UserData.defineProperty('name', {type: String, + dataType: 'char', limit: 50}); + UserData.defineProperty('newProperty', {type: Number, unsigned: true, + dataType: 'bigInt'}); + // UserData.defineProperty('pendingPeriod', false); + // This will not work as expected. + db.autoupdate(function(err) { + getFields('UserData', function(err, fields) { + // change nullable for email + assert.equal(fields.email.Null, 'YES', 'Email does not allow null'); + // change type of name + assert.equal(fields.name.Type, 'char(50)', 'Name is not char(50)'); + // add new column + assert.ok(fields.newProperty, 'New column was not added'); + if (fields.newProperty) { + assert.equal(fields.newProperty.Type, 'bigint(20) unsigned', + 'New column type is not bigint(20) unsigned'); + } + // drop column - will not happen. + // assert.ok(!fields.pendingPeriod, + // 'Did not drop column pendingPeriod'); + // user still exists + userExists(function(yep) { + assert.ok(yep, 'User does not exist'); + done(); + }); + }); + }); + }); + }); + + it('should check actuality of dataSource', function(done) { + // With an install of MYSQL5.7 on windows, these queries `randomly` fail and raise errors + // with date, number and decimal format + if (platform.isWindows) { + return done(); + } + // 'drop column' + UserData.dataSource.isActual(function(err, ok) { + assert.ok(ok, 'dataSource is not actual (should be)'); + UserData.defineProperty('essay', {type: Schema.Text}); + // UserData.defineProperty('email', false); Can't undefine currently. + UserData.dataSource.isActual(function(err, ok) { + assert.ok(!ok, 'dataSource is actual (shouldn\t be)'); + done(); + }); + }); + }); + + // In MySQL 5.6/5.7 Out of range values are rejected. + // Reference: http://dev.mysql.com/doc/refman/5.7/en/integer-types.html + it('allows numbers with decimals', function(done) { + NumberData.create( + {number: 1.1234567, tinyInt: 127, mediumInt: 16777215, floater: 12345678.123456}, + function(err, obj) { + if (err) return (err); + NumberData.findById(obj.id, function(err, found) { + assert.equal(found.number, 1.123); + assert.equal(found.tinyInt, 127); + assert.equal(found.mediumInt, 16777215); + assert.equal(found.floater, 12345678.123456); + done(); + }); + }); + }); + + // Reference: http://dev.mysql.com/doc/refman/5.7/en/out-of-range-and-overflow.html + it('rejects out-of-range and overflow values', function(done) { + async.series([ + function(next) { + NumberData.create({number: 1.1234567, tinyInt: 128, mediumInt: 16777215}, function(err, obj) { + assert(err); + assert.equal(err.code, 'ER_WARN_DATA_OUT_OF_RANGE'); + next(); + }); + }, function(next) { + NumberData.create({number: 1.1234567, mediumInt: 16777215 + 1}, function(err, obj) { + assert(err); + assert.equal(err.code, 'ER_WARN_DATA_OUT_OF_RANGE'); + next(); + }); + }, function(next) { + //Minimum value for unsigned mediumInt is 0 + NumberData.create({number: 1.1234567, mediumInt: -8388608}, function(err, obj) { + assert(err); + assert.equal(err.code, 'ER_WARN_DATA_OUT_OF_RANGE'); + next(); + }); + }, function(next) { + NumberData.create({number: 1.1234567, tinyInt: -129, mediumInt: 0}, function(err, obj) { + assert(err); + assert.equal(err.code, 'ER_WARN_DATA_OUT_OF_RANGE'); + next(); + }); + }, + ], done); + }); + + it('should allow both kinds of date columns', function(done) { + DateData.create({ + dateTime: new Date('Aug 9 1996 07:47:33 GMT'), + timestamp: new Date('Sep 22 2007 17:12:22 GMT'), + }, function(err, obj) { + assert.ok(!err); + assert.ok(obj); + DateData.findById(obj.id, function(err, found) { + assert.equal(found.dateTime.toGMTString(), + 'Fri, 09 Aug 1996 07:47:33 GMT'); + assert.equal(found.timestamp.toGMTString(), + 'Sat, 22 Sep 2007 17:12:22 GMT'); + done(); + }); + }); + }); + + // InMySQL5.7, DATETIME supported range is '1000-01-01 00:00:00' to '9999-12-31 23:59:59'. + // TIMESTAMP has a range of '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC + // Reference: http://dev.mysql.com/doc/refman/5.7/en/datetime.html + // Out of range values are set to null in windows but rejected elsewhere + // the next example is designed for windows while the following 2 are for other platforms + it('should map zero dateTime into null', function(done) { + if (!platform.isWindows) { + return done(); + } + + query('INSERT INTO `DateData` ' + + '(`dateTime`, `timestamp`) ' + + 'VALUES("0000-00-00 00:00:00", "0000-00-00 00:00:00") ', + function(err, ret) { + should.not.exists(err); + DateData.findById(ret.insertId, function(err, dateData) { + should(dateData.dateTime) + .be.null(); + should(dateData.timestamp) + .be.null(); + done(); + }); + }); + }); + + it('rejects out of range datetime', function(done) { + if (platform.isWindows) { + return done(); + } + + query('INSERT INTO `DateData` ' + + '(`dateTime`, `timestamp`) ' + + 'VALUES("0000-00-00 00:00:00", "0000-00-00 00:00:00") ', function(err) { + var errMsg = 'ER_TRUNCATED_WRONG_VALUE: Incorrect datetime value: ' + + '\'0000-00-00 00:00:00\' for column \'dateTime\' at row 1'; + assert(err); + assert.equal(err.message, errMsg); + done(); + }); + }); + + it('rejects out of range timestamp', function(done) { + if (platform.isWindows) { + return done(); + } + + query('INSERT INTO `DateData` ' + + '(`dateTime`, `timestamp`) ' + + 'VALUES("1000-01-01 00:00:00", "0000-00-00 00:00:00") ', function(err) { + var errMsg = 'ER_TRUNCATED_WRONG_VALUE: Incorrect datetime value: ' + + '\'0000-00-00 00:00:00\' for column \'timestamp\' at row 1'; + assert(err); + assert.equal(err.message, errMsg); + done(); + }); + }); + + it('should report errors for automigrate', function() { + db.automigrate('XYZ', function(err) { + assert(err); + }); + }); + + it('should report errors for autoupdate', function() { + db.autoupdate('XYZ', function(err) { + assert(err); + }); + }); + + it('should disconnect when done', function(done) { + db.disconnect(); + done(); + }); +}); + +function setup(done) { + require('./init.js'); + + db = getSchema(); + + UserData = db.define('UserData', { + email: {type: String, null: false, index: true}, + name: String, + bio: Schema.Text, + birthDate: Date, + pendingPeriod: Number, + createdByAdmin: Boolean, + }, {indexes: { + index0: { + columns: 'email, createdByAdmin', + }, + }, + }); + + StringData = db.define('StringData', { + idString: {type: String, id: true}, + smallString: {type: String, null: false, index: true, + dataType: 'char', limit: 127}, + mediumString: {type: String, null: false, dataType: 'varchar', limit: 255}, + tinyText: {type: String, dataType: 'tinyText'}, + giantJSON: {type: Schema.JSON, dataType: 'longText'}, + text: {type: Schema.Text, dataType: 'varchar', limit: 1024}, + }); + + NumberData = db.define('NumberData', { + number: {type: Number, null: false, index: true, unsigned: true, + dataType: 'decimal', precision: 10, scale: 3}, + tinyInt: {type: Number, dataType: 'tinyInt', display: 2}, + mediumInt: {type: Number, dataType: 'mediumInt', unsigned: true, + required: true}, + floater: {type: Number, dataType: 'double', precision: 14, scale: 6}, + }); + + DateData = db.define('DateData', { + dateTime: {type: Date, dataType: 'datetime'}, + timestamp: {type: Date, dataType: 'timestamp'}, + }); + + query('SELECT VERSION()', function(err, res) { + mysqlVersion = res && res[0] && res[0]['VERSION()']; + blankDatabase(db, done); + }); +} + +var query = function(sql, cb) { + db.adapter.execute(sql, cb); +}; + +var blankDatabase = function(db, cb) { + var dbn = db.settings.database; + var cs = db.settings.charset; + var co = db.settings.collation; + query('DROP DATABASE IF EXISTS ' + dbn, function(err) { + var q = 'CREATE DATABASE ' + dbn; + if (cs) { + q += ' CHARACTER SET ' + cs; + } + if (co) { + q += ' COLLATE ' + co; + } + query(q, function(err) { + query('USE ' + dbn, cb); + }); + }); +}; + +var getFields = function(model, cb) { + query('SHOW FIELDS FROM ' + model, function(err, res) { + if (err) { + cb(err); + } else { + var fields = {}; + res.forEach(function(field) { + fields[field.Field] = field; + }); + cb(err, fields); + } + }); +}; + +var getIndexes = function(model, cb) { + query('SHOW INDEXES FROM ' + model, function(err, res) { + if (err) { + console.log(err); + cb(err); + } else { + var indexes = {}; + // Note: this will only show the first key of compound keys + res.forEach(function(index) { + if (parseInt(index.Seq_in_index, 10) == 1) { + indexes[index.Key_name] = index; + } + }); + cb(err, indexes); + } + }); +}; diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..cc640c6 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,2 @@ +--globals getSchema +--timeout 15000 diff --git a/test/mysql.autoupdate.test.js b/test/mysql.autoupdate.test.js new file mode 100644 index 0000000..98077d2 --- /dev/null +++ b/test/mysql.autoupdate.test.js @@ -0,0 +1,542 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +var assert = require('assert'); +require('./init'); +var ds; + +before(function() { + ds = getDataSource(); +}); + +describe('MySQL connector', function() { + before(function() { + setupAltColNameData(); + }); + + it('should auto migrate/update tables', function(done) { + var schema_v1 = + { + 'name': 'CustomerTest', + 'options': { + 'idInjection': false, + 'mysql': { + 'schema': 'myapp_test', + 'table': 'customer_test', + }, + 'indexes': { + 'name_index': { + 'keys': { + 'name': 1, + }, + 'options': { + 'unique': true, + }, + }, + }, + }, + 'properties': { + 'id': { + 'type': 'String', + 'length': 20, + 'id': 1, + }, + 'name': { + 'type': 'String', + 'required': false, + 'length': 40, + }, + 'email': { + 'type': 'String', + 'required': true, + 'length': 40, + }, + 'age': { + 'type': 'Number', + 'required': false, + }, + 'discount': { + 'type': 'Number', + 'required': false, + 'dataType': 'decimal', + 'precision': 10, + 'scale': 2, + 'mysql': { + 'columnName': 'customer_discount', + 'dataType': 'decimal', + 'dataPrecision': 10, + 'dataScale': 2, + }, + }, + }, + }; + + var schema_v2 = + { + 'name': 'CustomerTest', + 'options': { + 'idInjection': false, + 'mysql': { + 'schema': 'myapp_test', + 'table': 'customer_test', + }, + 'indexes': { + 'updated_name_index': { + 'keys': { + 'firstName': 1, + 'lastName': -1, + }, + 'options': { + 'unique': true, + }, + }, + }, + }, + 'properties': { + 'id': { + 'type': 'String', + 'length': 20, + 'id': 1, + }, + 'email': { + 'type': 'String', + 'required': false, + 'length': 60, + 'mysql': { + 'columnName': 'email', + 'dataType': 'varchar', + 'dataLength': 60, + 'nullable': 'YES', + }, + }, + 'firstName': { + 'type': 'String', + 'required': false, + 'length': 40, + }, + 'lastName': { + 'type': 'String', + 'required': false, + 'length': 40, + }, + // remove age + // change data type details with column name + 'discount': { + 'type': 'Number', + 'required': false, + 'dataType': 'decimal', + 'precision': 12, + 'scale': 5, + 'mysql': { + 'columnName': 'customer_discount', + 'dataType': 'decimal', + 'dataPrecision': 12, + 'dataScale': 5, + }, + }, + // add new column with column name + 'address': { + 'type': 'String', + 'required': false, + 'length': 10, + 'mysql': { + 'columnName': 'customer_address', + 'dataType': 'varchar', + 'length': 10, + }, + }, + // add new column with index & column name + 'code': { + 'type': 'String', + 'required': true, + 'length': 12, + 'index': { + unique: true, + }, + 'mysql': { + 'columnName': 'customer_code', + 'dataType': 'varchar', + 'length': 12, + }, + }, + }, + }; + + ds.createModel(schema_v1.name, schema_v1.properties, schema_v1.options); + + ds.automigrate(function() { + ds.discoverModelProperties('customer_test', function(err, props) { + assert.equal(props.length, 5); + var names = props.map(function(p) { + return p.columnName; + }); + assert.equal(props[0].nullable, 'N'); + assert.equal(props[1].nullable, 'Y'); + assert.equal(props[2].nullable, 'N'); + assert.equal(props[3].nullable, 'Y'); + assert.equal(names[0], 'id'); + assert.equal(names[1], 'name'); + assert.equal(names[2], 'email'); + assert.equal(names[3], 'age'); + assert.equal(names[4], 'customer_discount'); + + ds.connector.execute('SHOW INDEXES FROM customer_test', function(err, indexes) { + if (err) return done (err); + assert(indexes); + assert(indexes.length.should.be.above(1)); + assert.equal(indexes[1].Key_name, 'name_index'); + assert.equal(indexes[1].Non_unique, 0); + ds.createModel(schema_v2.name, schema_v2.properties, schema_v2.options); + ds.autoupdate(function(err, result) { + if (err) return done (err); + ds.discoverModelProperties('customer_test', function(err, props) { + if (err) return done (err); + assert.equal(props.length, 7); + var names = props.map(function(p) { + return p.columnName; + }); + assert.equal(names[0], 'id'); + assert.equal(names[1], 'email'); + assert.equal(names[2], 'customer_discount'); + assert.equal(names[3], 'firstName'); + assert.equal(names[4], 'lastName'); + assert.equal(names[5], 'customer_address'); + assert.equal(names[6], 'customer_code'); + ds.connector.execute('SHOW INDEXES FROM customer_test', function(err, updatedindexes) { + if (err) return done (err); + assert(updatedindexes); + assert(updatedindexes.length.should.be.above(3)); + assert.equal(updatedindexes[1].Key_name, 'customer_code'); + assert.equal(updatedindexes[2].Key_name, 'updated_name_index'); + assert.equal(updatedindexes[3].Key_name, 'updated_name_index'); + //Mysql supports only index sorting in ascending; DESC is ignored + assert.equal(updatedindexes[1].Collation, 'A'); + assert.equal(updatedindexes[2].Collation, 'A'); + assert.equal(updatedindexes[3].Collation, 'A'); + assert.equal(updatedindexes[1].Non_unique, 0); + assert.equal(updatedindexes[2].Non_unique, 0); + assert.equal(updatedindexes[3].Non_unique, 0); + done(err, result); + }); + }); + }); + }); + }); + }); + }); + + it('should auto migrate/update foreign keys in tables', function(done) { + var customer2_schema = + { + 'name': 'CustomerTest2', + 'options': { + 'idInjection': false, + 'mysql': { + 'schema': 'myapp_test', + 'table': 'customer_test2', + }, + }, + 'properties': { + 'id': { + 'type': 'String', + 'length': 20, + 'id': 1, + }, + 'name': { + 'type': 'String', + 'required': false, + 'length': 40, + }, + 'email': { + 'type': 'String', + 'required': true, + 'length': 40, + }, + 'age': { + 'type': 'Number', + 'required': false, + }, + }, + }; + var customer3_schema = + { + 'name': 'CustomerTest3', + 'options': { + 'idInjection': false, + 'mysql': { + 'schema': 'myapp_test', + 'table': 'customer_test3', + }, + }, + 'properties': { + 'id': { + 'type': 'String', + 'length': 20, + 'id': 1, + }, + 'name': { + 'type': 'String', + 'required': false, + 'length': 40, + }, + 'email': { + 'type': 'String', + 'required': true, + 'length': 40, + }, + 'age': { + 'type': 'Number', + 'required': false, + }, + }, + }; + + var schema_v1 = + { + 'name': 'OrderTest', + 'options': { + 'idInjection': false, + 'mysql': { + 'schema': 'myapp_test', + 'table': 'order_test', + }, + 'foreignKeys': { + 'fk_ordertest_customerId': { + 'name': 'fk_ordertest_customerId', + 'entity': 'CustomerTest3', + 'entityKey': 'id', + 'foreignKey': 'customerId', + }, + }, + }, + 'properties': { + 'id': { + 'type': 'String', + 'length': 20, + 'id': 1, + }, + 'customerId': { + 'type': 'String', + 'length': 20, + 'id': 1, + }, + 'description': { + 'type': 'String', + 'required': false, + 'length': 40, + }, + }, + }; + + var schema_v2 = + { + 'name': 'OrderTest', + 'options': { + 'idInjection': false, + 'mysql': { + 'schema': 'myapp_test', + 'table': 'order_test', + }, + 'foreignKeys': { + 'fk_ordertest_customerId': { + 'name': 'fk_ordertest_customerId', + 'entity': 'CustomerTest2', + 'entityKey': 'id', + 'foreignKey': 'customerId', + }, + }, + }, + 'properties': { + 'id': { + 'type': 'String', + 'length': 20, + 'id': 1, + }, + 'customerId': { + 'type': 'String', + 'length': 20, + 'id': 1, + }, + 'description': { + 'type': 'String', + 'required': false, + 'length': 40, + }, + }, + }; + + var schema_v3 = + { + 'name': 'OrderTest', + 'options': { + 'idInjection': false, + 'mysql': { + 'schema': 'myapp_test', + 'table': 'order_test', + }, + }, + 'properties': { + 'id': { + 'type': 'String', + 'length': 20, + 'id': 1, + }, + 'customerId': { + 'type': 'String', + 'length': 20, + 'id': 1, + }, + 'description': { + 'type': 'String', + 'required': false, + 'length': 40, + }, + }, + }; + + var foreignKeySelect = + 'SELECT COLUMN_NAME,CONSTRAINT_NAME,REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME ' + + 'FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE ' + + 'WHERE REFERENCED_TABLE_SCHEMA = "myapp_test" ' + + 'AND TABLE_NAME = "order_test"'; + + ds.createModel(customer2_schema.name, customer2_schema.properties, customer2_schema.options); + ds.createModel(customer3_schema.name, customer3_schema.properties, customer3_schema.options); + ds.createModel(schema_v1.name, schema_v1.properties, schema_v1.options); + + //do initial update/creation of table + ds.autoupdate(function() { + ds.discoverModelProperties('order_test', function(err, props) { + //validate that we have the correct number of properties + assert.equal(props.length, 3); + + //get the foreign keys for this table + ds.connector.execute(foreignKeySelect, function(err, foreignKeys) { + if (err) return done (err); + //validate that the foreign key exists and points to the right column + assert(foreignKeys); + assert(foreignKeys.length.should.be.equal(1)); + assert.equal(foreignKeys[0].REFERENCED_TABLE_NAME, 'customer_test3'); + assert.equal(foreignKeys[0].COLUMN_NAME, 'customerId'); + assert.equal(foreignKeys[0].CONSTRAINT_NAME, 'fk_ordertest_customerId'); + assert.equal(foreignKeys[0].REFERENCED_COLUMN_NAME, 'id'); + + //update our model (move foreign key) and run autoupdate to migrate + ds.createModel(schema_v2.name, schema_v2.properties, schema_v2.options); + ds.autoupdate(function(err, result) { + if (err) return done (err); + + //get and validate the properties on this model + ds.discoverModelProperties('order_test', function(err, props) { + if (err) return done (err); + + assert.equal(props.length, 3); + + //get the foreign keys that exist after the migration + ds.connector.execute(foreignKeySelect, function(err, updatedForeignKeys) { + if (err) return done (err); + //validate that the foreign keys was moved to the new column + assert(updatedForeignKeys); + assert(updatedForeignKeys.length.should.be.equal(1)); + assert.equal(updatedForeignKeys[0].REFERENCED_TABLE_NAME, 'customer_test2'); + assert.equal(updatedForeignKeys[0].COLUMN_NAME, 'customerId'); + assert.equal(updatedForeignKeys[0].CONSTRAINT_NAME, 'fk_ordertest_customerId'); + assert.equal(updatedForeignKeys[0].REFERENCED_COLUMN_NAME, 'id'); + + //update model (to drop foreign key) and autoupdate + ds.createModel(schema_v3.name, schema_v3.properties, schema_v3.options); + ds.autoupdate(function(err, result) { + if (err) return done (err); + //validate the properties + ds.discoverModelProperties('order_test', function(err, props) { + if (err) return done (err); + + assert.equal(props.length, 3); + + //get the foreign keys and validate the foreign key has been dropped + ds.connector.execute(foreignKeySelect, function(err, thirdForeignKeys) { + if (err) return done (err); + assert(thirdForeignKeys); + assert(thirdForeignKeys.length.should.be.equal(0)); + + done(err, result); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + + function setupAltColNameData() { + var schema = { + name: 'ColRenameTest', + options: { + idInjection: false, + mysql: { + schema: 'myapp_test', + table: 'col_rename_test', + }, + }, + properties: { + firstName: { + type: 'String', + required: false, + length: 40, + mysql: { + columnName: 'first_name', + dataType: 'varchar', + dataLength: 40, + }, + }, + lastName: { + type: 'String', + required: false, + length: 40, + }, + }, + }; + ds.createModel(schema.name, schema.properties, schema.options); + } + + it('should report errors for automigrate', function(done) { + ds.automigrate('XYZ', function(err) { + assert(err); + done(); + }); + }); + + it('should report errors for autoupdate', function(done) { + ds.autoupdate('XYZ', function(err) { + assert(err); + done(); + }); + }); + + it('"mysql.columnName" is updated with correct name on create table', function(done) { + // first autoupdate call uses create table + verifyMysqlColumnNameAutoupdate(done); + }); + + it('"mysql.columnName" is updated without changing column name on alter table', function(done) { + // second autoupdate call uses alter table + verifyMysqlColumnNameAutoupdate(done); + }); + + function verifyMysqlColumnNameAutoupdate(done) { + ds.autoupdate('ColRenameTest', function(err) { + ds.discoverModelProperties('col_rename_test', function(err, props) { + assert.equal(props[0].columnName, 'first_name'); + assert.equal(props[1].columnName, 'lastName'); + assert.equal(props.length, 2); + done(); + }); + }); + } +}); diff --git a/test/mysql.discover.test.js b/test/mysql.discover.test.js new file mode 100644 index 0000000..5dd972f --- /dev/null +++ b/test/mysql.discover.test.js @@ -0,0 +1,433 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +process.env.NODE_ENV = 'test'; +require('should'); + +var assert = require('assert'); +var DataSource = require('loopback-datasource-juggler').DataSource; +var db, config; + +before(function() { + require('./init'); + config = getConfig(); + config.database = 'STRONGLOOP'; + db = new DataSource(require('../'), config); +}); + +describe('discoverModels', function() { + describe('Discover database schemas', function() { + it('should return an array of db schemas', function(done) { + db.connector.discoverDatabaseSchemas(function(err, schemas) { + if (err) return done(err); + schemas.should.be.an.array; + schemas.length.should.be.above(0); + done(); + }); + }); + }); + + describe('Discover models including views', function() { + it('should return an array of tables and views', function(done) { + db.discoverModelDefinitions({ + views: true, + limit: 3, + }, function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + var views = false; + models.forEach(function(m) { + // console.dir(m); + if (m.type === 'view') { + views = true; + } + }); + assert(views, 'Should have views'); + done(null, models); + } + }); + }); + }); + + describe('Discover current user\'s tables', function() { + it('should return an array of tables for the current user', function(done) { + db.discoverModelDefinitions({ + limit: 3, + }, function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + var views = false; + models.forEach(function(m) { + assert.equal(m.owner, config.username); + }); + done(null, models); + } + }); + }); + }); + + describe('Discover models excluding views', function() { + // TODO: this test assumes the current user owns the tables + it.skip('should return an array of only tables', function(done) { + db.discoverModelDefinitions({ + views: false, + limit: 3, + }, function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + var views = false; + models.forEach(function(m) { + // console.dir(m); + if (m.type === 'view') { + views = true; + } + }); + models.should.have.length(3); + assert(!views, 'Should not have views'); + done(null, models); + } + }); + }); + }); +}); + +describe('Discover models including other users', function() { + it('should return an array of all tables and views', function(done) { + db.discoverModelDefinitions({ + all: true, + limit: 3, + }, function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + var others = false; + models.forEach(function(m) { + // console.dir(m); + if (m.owner !== 'STRONGLOOP') { + others = true; + } + }); + assert(others, 'Should have tables/views owned by others'); + done(err, models); + } + }); + }); +}); + +describe('Discover model properties', function() { + describe('Discover a named model', function() { + it('should return an array of columns for product', function(done) { + db.discoverModelProperties('product', function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + models.forEach(function(m) { + // console.dir(m); + assert(m.tableName === 'product'); + }); + done(null, models); + } + }); + }); + }); +}); + +describe('Discover model primary keys', function() { + it('should return an array of primary keys for product', function(done) { + db.discoverPrimaryKeys('product', function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + models.forEach(function(m) { + // console.dir(m); + assert(m.tableName === 'product'); + }); + done(null, models); + } + }); + }); + + it('should return an array of primary keys for STRONGLOOP.PRODUCT', function(done) { + db.discoverPrimaryKeys('product', {owner: 'STRONGLOOP'}, function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + models.forEach(function(m) { + // console.dir(m); + assert(m.tableName === 'product'); + }); + done(null, models); + } + }); + }); +}); + +describe('Discover model foreign keys', function() { + it('should return an array of foreign keys for INVENTORY', function(done) { + db.discoverForeignKeys('INVENTORY', function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + models.forEach(function(m) { + // console.dir(m); + assert(m.fkTableName === 'INVENTORY'); + }); + done(null, models); + } + }); + }); + it('should return an array of foreign keys for STRONGLOOP.INVENTORY', function(done) { + db.discoverForeignKeys('INVENTORY', {owner: 'STRONGLOOP'}, function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + models.forEach(function(m) { + // console.dir(m); + assert(m.fkTableName === 'INVENTORY'); + }); + done(null, models); + } + }); + }); +}); + +describe('Discover model generated columns', function() { + it('should return an array of columns for STRONGLOOP.PRODUCT and none of them is generated', function(done) { + db.discoverModelProperties('product', function(err, models) { + if (err) return done(err); + models.forEach(function(model) { + assert(model.tableName === 'product'); + assert(!model.generated, 'STRONGLOOP.PRODUCT table should not have generated (identity) columns'); + }); + done(); + }); + }); + it('should return an array of columns for STRONGLOOP.TESTGEN and the first is generated', function(done) { + db.discoverModelProperties('testgen', function(err, models) { + if (err) return done(err); + models.forEach(function(model) { + assert(model.tableName === 'testgen'); + if (model.columnName === 'ID') { + assert(model.generated, 'STRONGLOOP.TESTGEN.ID should be a generated (identity) column'); + } + }); + done(); + }); + }); +}); + +describe('Discover LDL schema from a table', function() { + var schema; + before(function(done) { + db.discoverSchema('INVENTORY', {owner: 'STRONGLOOP'}, function(err, schema_) { + schema = schema_; + done(err); + }); + }); + it('should return an LDL schema for INVENTORY', function() { + var productId = 'productId' in schema.properties ? 'productId' : 'productid'; + var locationId = 'locationId' in schema.properties ? 'locationId' : 'locationid'; + console.error('schema:', schema); + assert.strictEqual(schema.name, 'Inventory'); + assert.ok(/STRONGLOOP/i.test(schema.options.mysql.schema)); + assert.strictEqual(schema.options.mysql.table, 'INVENTORY'); + assert(schema.properties[productId]); + // TODO: schema shows this field is default NULL, which means it isn't required + // assert(schema.properties[productId].required); + assert.strictEqual(schema.properties[productId].type, 'String'); + assert.strictEqual(schema.properties[productId].mysql.columnName, 'PRODUCT_ID'); + assert(schema.properties[locationId]); + assert.strictEqual(schema.properties[locationId].type, 'String'); + assert.strictEqual(schema.properties[locationId].mysql.columnName, 'LOCATION_ID'); + assert(schema.properties.available); + assert.strictEqual(schema.properties.available.required, false); + assert.strictEqual(schema.properties.available.type, 'Number'); + assert(schema.properties.total); + assert.strictEqual(schema.properties.total.type, 'Number'); + }); +}); + +describe('Discover and build models', function() { + var models; + before(function(done) { + db.discoverAndBuildModels('INVENTORY', {owner: 'STRONGLOOP', visited: {}, associations: true}, + function(err, models_) { + models = models_; + done(err); + }); + }); + it('should discover and build models', function() { + assert(models.Inventory, 'Inventory model should be discovered and built'); + var schema = models.Inventory.definition; + var productId = 'productId' in schema.properties ? 'productId' : 'productid'; + var locationId = 'locationId' in schema.properties ? 'locationId' : 'locationid'; + assert(/STRONGLOOP/i.test(schema.settings.mysql.schema)); + assert.strictEqual(schema.settings.mysql.table, 'INVENTORY'); + assert(schema.properties[productId]); + assert.strictEqual(schema.properties[productId].type, String); + assert.strictEqual(schema.properties[productId].mysql.columnName, 'PRODUCT_ID'); + assert(schema.properties[locationId]); + assert.strictEqual(schema.properties[locationId].type, String); + assert.strictEqual(schema.properties[locationId].mysql.columnName, 'LOCATION_ID'); + assert(schema.properties.available); + assert.strictEqual(schema.properties.available.type, Number); + assert(schema.properties.total); + assert.strictEqual(schema.properties.total.type, Number); + }); + it('should be able to find an instance', function(done) { + assert(models.Inventory, 'Inventory model must exist'); + models.Inventory.findOne(function(err, inv) { + assert(!err, 'error should not be reported'); + done(); + }); + }); + + describe('discoverModelProperties() flags', function() { + context('with default flags', function() { + var models, schema; + before(discoverAndBuildModels); + + it('handles CHAR(1) as Boolean', function() { + assert(schema.properties.enabled); + assert.strictEqual(schema.properties.enabled.type, Boolean); + }); + + it('handles BIT(1) as Bit', function() { + assert(schema.properties.disabled); + assert.strictEqual(schema.properties.disabled.type, Buffer); + }); + + it('handles TINYINT(1) as Number', function() { + assert(schema.properties.active); + assert.strictEqual(schema.properties.active.type, Number); + }); + + function discoverAndBuildModels(done) { + db.discoverAndBuildModels('INVENTORY', { + owner: 'STRONGLOOP', + visited: {}, + associations: true, + }, function(err, models_) { + models = models_; + schema = models.Inventory.definition; + done(err); + }); + } + }); + + context('with flag treatCHAR1AsString = true', function() { + var models, schema; + before(discoverAndBuildModels); + + it('handles CHAR(1) as String', function() { + assert(schema.properties.enabled); + assert.strictEqual(schema.properties.enabled.type, String); + }); + + it('handles BIT(1) as Binary', function() { + assert(schema.properties.disabled); + assert.strictEqual(schema.properties.disabled.type, Buffer); + }); + + it('handles TINYINT(1) as Number', function() { + assert(schema.properties.active); + assert.strictEqual(schema.properties.active.type, Number); + }); + + function discoverAndBuildModels(done) { + db.discoverAndBuildModels('INVENTORY', { + owner: 'STRONGLOOP', + visited: {}, + associations: true, + treatCHAR1AsString: true, + }, function(err, models_) { + models = models_; + schema = models.Inventory.definition; + done(err); + }); + } + }); + + context('with flag treatBIT1AsBit = false', function() { + var models, schema; + before(discoverAndBuildModels); + + it('handles CHAR(1) as Boolean', function() { + assert(schema.properties.enabled); + assert.strictEqual(schema.properties.enabled.type, Boolean); + }); + + it('handles BIT(1) as Boolean', function() { + assert(schema.properties.disabled); + assert.strictEqual(schema.properties.disabled.type, Boolean); + }); + + it('handles TINYINT(1) as Number', function() { + assert(schema.properties.active); + assert.strictEqual(schema.properties.active.type, Number); + }); + + function discoverAndBuildModels(done) { + db.discoverAndBuildModels('INVENTORY', { + owner: 'STRONGLOOP', + visited: {}, + associations: true, + treatBIT1AsBit: false, + }, function(err, models_) { + models = models_; + schema = models.Inventory.definition; + done(err); + }); + } + }); + + context('with flag treatTINYINT1AsTinyInt = false', function() { + var models, schema; + before(discoverAndBuildModels); + + it('handles CHAR(1) as Boolean', function() { + assert(schema.properties.enabled); + assert.strictEqual(schema.properties.enabled.type, Boolean); + }); + + it('handles BIT(1) as Binary', function() { + assert(schema.properties.disabled); + assert.strictEqual(schema.properties.disabled.type, Buffer); + }); + + it('handles TINYINT(1) as Boolean', function() { + assert(schema.properties.active); + assert.strictEqual(schema.properties.active.type, Boolean); + }); + + function discoverAndBuildModels(done) { + db.discoverAndBuildModels('INVENTORY', { + owner: 'STRONGLOOP', + visited: {}, + associations: true, + treatTINYINT1AsTinyInt: false, + }, function(err, models_) { + if (err) return done(err); + models = models_; + schema = models.Inventory.definition; + done(); + }); + } + }); + }); +}); diff --git a/test/mysql.test.js b/test/mysql.test.js new file mode 100644 index 0000000..1034a60 --- /dev/null +++ b/test/mysql.test.js @@ -0,0 +1,760 @@ +// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +var should = require('./init.js'); + +var Post, PostWithStringId, PostWithUniqueTitle, db; + +// Mock up mongodb ObjectID +function ObjectID(id) { + if (!(this instanceof ObjectID)) { + return new ObjectID(id); + } + this.id1 = id.substring(0, 2); + this.id2 = id.substring(2); +} + +ObjectID.prototype.toJSON = function() { + return this.id1 + this.id2; +}; + +describe('mysql', function() { + before(function(done) { + db = getDataSource(); + + Post = db.define('PostWithDefaultId', { + title: {type: String, length: 255, index: true}, + content: {type: String}, + comments: [String], + history: Object, + stars: Number, + userId: ObjectID, + }, { + forceId: false, + }); + + PostWithStringId = db.define('PostWithStringId', { + id: {type: String, id: true}, + title: {type: String, length: 255, index: true}, + content: {type: String}, + }); + + PostWithUniqueTitle = db.define('PostWithUniqueTitle', { + title: {type: String, length: 255, index: {unique: true}}, + content: {type: String}, + }); + + db.automigrate(['PostWithDefaultId', 'PostWithStringId', 'PostWithUniqueTitle'], function(err) { + should.not.exist(err); + done(err); + }); + }); + + beforeEach(function(done) { + Post.destroyAll(function() { + PostWithStringId.destroyAll(function() { + PostWithUniqueTitle.destroyAll(function() { + done(); + }); + }); + }); + }); + + it('should allow array or object', function(done) { + Post.create({title: 'a', content: 'AAA', comments: ['1', '2'], + history: {a: 1, b: 'b'}}, function(err, post) { + should.not.exist(err); + + Post.findById(post.id, function(err, p) { + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal('a'); + p.comments.should.eql(['1', '2']); + p.history.should.eql({a: 1, b: 'b'}); + + done(); + }); + }); + }); + + it('should allow ObjectID', function(done) { + var uid = new ObjectID('123'); + Post.create({title: 'a', content: 'AAA', userId: uid}, + function(err, post) { + should.not.exist(err); + + Post.findById(post.id, function(err, p) { + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal('a'); + p.userId.should.eql(uid); + done(); + }); + }); + }); + + it('updateOrCreate should update the instance', function(done) { + Post.create({title: 'a', content: 'AAA'}, function(err, post) { + post.title = 'b'; + Post.updateOrCreate(post, function(err, p) { + should.not.exist(err); + p.id.should.be.equal(post.id); + p.content.should.be.equal(post.content); + + Post.findById(post.id, function(err, p) { + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal('b'); + + done(); + }); + }); + }); + }); + + it('updateOrCreate should update the instance without removing existing properties', function(done) { + Post.create({title: 'a', content: 'AAA'}, function(err, post) { + post = post.toObject(); + delete post.title; + Post.updateOrCreate(post, function(err, p) { + should.not.exist(err); + p.id.should.be.equal(post.id); + p.content.should.be.equal(post.content); + Post.findById(post.id, function(err, p) { + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal('a'); + + done(); + }); + }); + }); + }); + + it('updateOrCreate should create a new instance if it does not exist', function(done) { + var post = {id: 123, title: 'a', content: 'AAA'}; + Post.updateOrCreate(post, function(err, p) { + should.not.exist(err); + p.title.should.be.equal(post.title); + p.content.should.be.equal(post.content); + p.id.should.be.equal(post.id); + + Post.findById(p.id, function(err, p) { + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal(post.title); + p.id.should.be.equal(post.id); + + done(); + }); + }); + }); + + context('replaceOrCreate', function() { + it('should replace the instance', function(done) { + Post.create({title: 'a', content: 'AAA'}, function(err, post) { + if (err) return done(err); + post = post.toObject(); + delete post.content; + Post.replaceOrCreate(post, function(err, p) { + if (err) return done(err); + p.id.should.equal(post.id); + p.title.should.equal('a'); + should.not.exist(p.content); + should.not.exist(p._id); + Post.findById(post.id, function(err, p) { + if (err) return done(err); + p.id.should.equal(post.id); + p.title.should.equal('a'); + should.not.exist(post.content); + should.not.exist(p._id); + done(); + }); + }); + }); + }); + + it('should replace with new data', function(done) { + Post.create({title: 'a', content: 'AAA', comments: ['Comment1']}, + function(err, post) { + if (err) return done(err); + post = post.toObject(); + delete post.comments; + delete post.content; + post.title = 'b'; + Post.replaceOrCreate(post, function(err, p) { + if (err) return done(err); + p.id.should.equal(post.id); + should.not.exist(p._id); + p.title.should.equal('b'); + should.not.exist(p.content); + should.not.exist(p.comments); + Post.findById(post.id, function(err, p) { + if (err) return done(err); + p.id.should.equal(post.id); + should.not.exist(p._id); + p.title.should.equal('b'); + should.not.exist(p.content); + should.not.exist(p.comments); + done(); + }); + }); + }); + }); + + it('should create a new instance if it does not exist', function(done) { + var post = {id: 123, title: 'a', content: 'AAA'}; + Post.replaceOrCreate(post, function(err, p) { + if (err) return done(err); + p.id.should.equal(post.id); + should.not.exist(p._id); + p.title.should.equal(post.title); + p.content.should.equal(post.content); + Post.findById(p.id, function(err, p) { + if (err) return done(err); + p.id.should.equal(post.id); + should.not.exist(p._id); + p.title.should.equal(post.title); + p.content.should.equal(post.content); + done(); + }); + }); + }); + }); + + it('save should update the instance with the same id', function(done) { + Post.create({title: 'a', content: 'AAA'}, function(err, post) { + post.title = 'b'; + post.save(function(err, p) { + should.not.exist(err); + p.id.should.be.equal(post.id); + p.content.should.be.equal(post.content); + + Post.findById(post.id, function(err, p) { + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal('b'); + + done(); + }); + }); + }); + }); + + it('save should update the instance without removing existing properties', function(done) { + Post.create({title: 'a', content: 'AAA'}, function(err, post) { + delete post.title; + post.save(function(err, p) { + should.not.exist(err); + p.id.should.be.equal(post.id); + p.content.should.be.equal(post.content); + + Post.findById(post.id, function(err, p) { + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal('a'); + + done(); + }); + }); + }); + }); + + it('save should create a new instance if it does not exist', function(done) { + var post = new Post({id: 123, title: 'a', content: 'AAA'}); + post.save(post, function(err, p) { + should.not.exist(err); + p.title.should.be.equal(post.title); + p.content.should.be.equal(post.content); + p.id.should.be.equal(post.id); + + Post.findById(p.id, function(err, p) { + should.not.exist(err); + p.id.should.be.equal(post.id); + + p.content.should.be.equal(post.content); + p.title.should.be.equal(post.title); + p.id.should.be.equal(post.id); + + done(); + }); + }); + }); + + it('all return should honor filter.fields', function(done) { + var post = new Post({title: 'b', content: 'BBB'}); + post.save(function(err, post) { + Post.all({fields: ['title'], where: {title: 'b'}}, function(err, posts) { + should.not.exist(err); + posts.should.have.lengthOf(1); + post = posts[0]; + post.should.have.property('title', 'b'); + post.should.have.property('content', undefined); + should.not.exist(post.id); + + done(); + }); + }); + }); + + it('find should order by id if the order is not set for the query filter', + function(done) { + PostWithStringId.create({id: '2', title: 'c', content: 'CCC'}, function(err, post) { + PostWithStringId.create({id: '1', title: 'd', content: 'DDD'}, function(err, post) { + PostWithStringId.find(function(err, posts) { + should.not.exist(err); + posts.length.should.be.equal(2); + posts[0].id.should.be.equal('1'); + + PostWithStringId.find({limit: 1, offset: 0}, function(err, posts) { + should.not.exist(err); + posts.length.should.be.equal(1); + posts[0].id.should.be.equal('1'); + + PostWithStringId.find({limit: 1, offset: 1}, function(err, posts) { + should.not.exist(err); + posts.length.should.be.equal(1); + posts[0].id.should.be.equal('2'); + done(); + }); + }); + }); + }); + }); + }); + + it('should allow to find using like', function(done) { + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { + Post.find({where: {title: {like: 'M%st'}}}, function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 1); + done(); + }); + }); + }); + + it('should support like for no match', function(done) { + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { + Post.find({where: {title: {like: 'M%XY'}}}, function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + + it('should allow to find using nlike', function(done) { + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { + Post.find({where: {title: {nlike: 'M%st'}}}, function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + + it('should support nlike for no match', function(done) { + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { + Post.find({where: {title: {nlike: 'M%XY'}}}, function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 1); + done(); + }); + }); + }); + + it('should support "and" operator that is satisfied', function(done) { + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { + Post.find({where: {and: [ + {title: 'My Post'}, + {content: 'Hello'}, + ]}}, function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 1); + done(); + }); + }); + }); + + it('should support "and" operator that is not satisfied', function(done) { + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { + Post.find({where: {and: [ + {title: 'My Post'}, + {content: 'Hello1'}, + ]}}, function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + + it('should support "or" that is satisfied', function(done) { + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { + Post.find({where: {or: [ + {title: 'My Post'}, + {content: 'Hello1'}, + ]}}, function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 1); + done(); + }); + }); + }); + + it('should support "or" operator that is not satisfied', function(done) { + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { + Post.find({where: {or: [ + {title: 'My Post1'}, + {content: 'Hello1'}, + ]}}, function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + + // The where object should be parsed by the connector + it('should support where for count', function(done) { + Post.create({title: 'My Post', content: 'Hello'}, function(err, post) { + Post.count({and: [ + {title: 'My Post'}, + {content: 'Hello'}, + ]}, function(err, count) { + should.not.exist(err); + count.should.be.equal(1); + Post.count({and: [ + {title: 'My Post1'}, + {content: 'Hello'}, + ]}, function(err, count) { + should.not.exist(err); + count.should.be.equal(0); + done(); + }); + }); + }); + }); + + // The where object should be parsed by the connector + it('should support where for destroyAll', function(done) { + Post.create({title: 'My Post1', content: 'Hello'}, function(err, post) { + Post.create({title: 'My Post2', content: 'Hello'}, function(err, post) { + Post.destroyAll({and: [ + {title: 'My Post1'}, + {content: 'Hello'}, + ]}, function(err) { + should.not.exist(err); + Post.count(function(err, count) { + should.not.exist(err); + count.should.be.equal(1); + done(); + }); + }); + }); + }); + }); + + it('should not allow SQL injection for inq operator', function(done) { + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, + function(err, post) { + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, + function(err, post) { + Post.find({where: {title: {inq: ['SELECT title from PostWithDefaultId']}}}, + function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + }); + + it('should not allow SQL injection for lt operator', function(done) { + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, + function(err, post) { + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, + function(err, post) { + Post.find({where: {stars: {lt: 'SELECT title from PostWithDefaultId'}}}, + function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + }); + + it('should not allow SQL injection for nin operator', function(done) { + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, + function(err, post) { + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, + function(err, post) { + Post.find({where: {title: {nin: ['SELECT title from PostWithDefaultId']}}}, + function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 2); + done(); + }); + }); + }); + }); + + it('should not allow SQL injection for inq operator with number column', function(done) { + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, + function(err, post) { + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, + function(err, post) { + Post.find({where: {stars: {inq: ['SELECT title from PostWithDefaultId']}}}, + function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + }); + + it('should not allow SQL injection for inq operator with array value', function(done) { + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, + function(err, post) { + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, + function(err, post) { + Post.find({where: {stars: {inq: [5, 'SELECT title from PostWithDefaultId']}}}, + function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 1); + done(); + }); + }); + }); + }); + + it('should not allow SQL injection for between operator', function(done) { + Post.create({title: 'My Post1', content: 'Hello', stars: 5}, + function(err, post) { + Post.create({title: 'My Post2', content: 'Hello', stars: 20}, + function(err, post) { + Post.find({where: {stars: {between: [5, 'SELECT title from PostWithDefaultId']}}}, + function(err, posts) { + should.not.exist(err); + posts.should.have.property('length', 0); + done(); + }); + }); + }); + }); + + it('should not allow duplicate titles', function(done) { + var data = {title: 'a', content: 'AAA'}; + PostWithUniqueTitle.create(data, function(err, post) { + should.not.exist(err); + PostWithUniqueTitle.create(data, function(err, post) { + should.exist(err); + done(); + }); + }); + }); + + context('regexp operator', function() { + beforeEach(function deleteExistingTestFixtures(done) { + Post.destroyAll(done); + }); + beforeEach(function createTestFixtures(done) { + Post.create([ + {title: 'a', content: 'AAA'}, + {title: 'b', content: 'BBB'}, + ], done); + }); + after(function deleteTestFixtures(done) { + Post.destroyAll(done); + }); + + context('with regex strings', function() { + context('using no flags', function() { + it('should work', function(done) { + Post.find({where: {content: {regexp: '^A'}}}, function(err, posts) { + should.not.exist(err); + posts.length.should.equal(1); + posts[0].content.should.equal('AAA'); + done(); + }); + }); + }); + + context('using flags', function() { + beforeEach(function addSpy() { + sinon.stub(console, 'warn'); + }); + afterEach(function removeSpy() { + console.warn.restore(); + }); + + it('should work', function(done) { + Post.find({where: {content: {regexp: '^a/i'}}}, function(err, posts) { + should.not.exist(err); + posts.length.should.equal(1); + posts[0].content.should.equal('AAA'); + done(); + }); + }); + + it('should print a warning when the ignore flag is set', + function(done) { + Post.find({where: {content: {regexp: '^a/i'}}}, function(err, posts) { + console.warn.calledOnce.should.be.ok; + done(); + }); + }); + + it('should print a warning when the global flag is set', + function(done) { + Post.find({where: {content: {regexp: '^a/g'}}}, function(err, posts) { + console.warn.calledOnce.should.be.ok; + done(); + }); + }); + + it('should print a warning when the multiline flag is set', + function(done) { + Post.find({where: {content: {regexp: '^a/m'}}}, function(err, posts) { + console.warn.calledOnce.should.be.ok; + done(); + }); + }); + }); + }); + + context('with regex literals', function() { + context('using no flags', function() { + it('should work', function(done) { + Post.find({where: {content: {regexp: /^A/}}}, function(err, posts) { + should.not.exist(err); + posts.length.should.equal(1); + posts[0].content.should.equal('AAA'); + done(); + }); + }); + }); + + context('using flags', function() { + beforeEach(function addSpy() { + sinon.stub(console, 'warn'); + }); + afterEach(function removeSpy() { + console.warn.restore(); + }); + + it('should work', function(done) { + Post.find({where: {content: {regexp: /^a/i}}}, function(err, posts) { + should.not.exist(err); + posts.length.should.equal(1); + posts[0].content.should.equal('AAA'); + done(); + }); + }); + + it('should print a warning when the ignore flag is set', + function(done) { + Post.find({where: {content: {regexp: /^a/i}}}, function(err, posts) { + console.warn.calledOnce.should.be.ok; + done(); + }); + }); + + it('should print a warning when the global flag is set', + function(done) { + Post.find({where: {content: {regexp: /^a/g}}}, function(err, posts) { + console.warn.calledOnce.should.be.ok; + done(); + }); + }); + + it('should print a warning when the multiline flag is set', + function(done) { + Post.find({where: {content: {regexp: /^a/m}}}, function(err, posts) { + console.warn.calledOnce.should.be.ok; + done(); + }); + }); + }); + }); + + context('with regex objects', function() { + beforeEach(function addSpy() { + sinon.stub(console, 'warn'); + }); + afterEach(function removeSpy() { + console.warn.restore(); + }); + + context('using no flags', function() { + it('should work', function(done) { + Post.find({where: {content: {regexp: new RegExp(/^A/)}}}, + function(err, posts) { + should.not.exist(err); + posts.length.should.equal(1); + posts[0].content.should.equal('AAA'); + done(); + }); + }); + }); + + context('using flags', function() { + it('should work', function(done) { + Post.find({where: {content: {regexp: new RegExp(/^a/i)}}}, + function(err, posts) { + should.not.exist(err); + posts.length.should.equal(1); + posts[0].content.should.equal('AAA'); + done(); + }); + }); + it('should print a warning when the ignore flag is set', + function(done) { + Post.find({where: {content: {regexp: new RegExp(/^a/i)}}}, + function(err, posts) { + console.warn.calledOnce.should.be.ok; + done(); + }); + }); + + it('should print a warning when the global flag is set', + function(done) { + Post.find({where: {content: {regexp: new RegExp(/^a/g)}}}, + function(err, posts) { + console.warn.calledOnce.should.be.ok; + done(); + }); + }); + + it('should print a warning when the multiline flag is set', + function(done) { + Post.find({where: {content: {regexp: new RegExp(/^a/m)}}}, + function(err, posts) { + console.warn.calledOnce.should.be.ok; + done(); + }); + }); + }); + }); + }); + + after(function(done) { + Post.destroyAll(function() { + PostWithStringId.destroyAll(function() { + PostWithUniqueTitle.destroyAll(done); + }); + }); + }); +}); diff --git a/test/persistence-hooks.test.js b/test/persistence-hooks.test.js new file mode 100644 index 0000000..ddcb28b --- /dev/null +++ b/test/persistence-hooks.test.js @@ -0,0 +1,12 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +var should = require('./init'); +var suite = require('loopback-datasource-juggler/test/persistence-hooks.suite.js'); + +suite(global.getDataSource(), should, { + replaceOrCreateReportsNewInstance: true, +}); diff --git a/test/schema.sql b/test/schema.sql new file mode 100644 index 0000000..c0bf508 --- /dev/null +++ b/test/schema.sql @@ -0,0 +1,239 @@ +-- MySQL dump 10.13 Distrib 5.7.14, for osx10.10 (x86_64) +-- +-- Host: 166.78.158.45 Database: STRONGLOOP +-- ------------------------------------------------------ +-- Server version 5.1.69 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Current Database: `STRONGLOOP` +-- + +/*!40000 DROP DATABASE IF EXISTS `STRONGLOOP`*/; + +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `STRONGLOOP` /*!40100 DEFAULT CHARACTER SET utf8 */; + +USE `STRONGLOOP`; + +-- +-- Table structure for table `CUSTOMER` +-- + +DROP TABLE IF EXISTS `CUSTOMER`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `CUSTOMER` ( + `ID` varchar(20) NOT NULL, + `NAME` varchar(40) DEFAULT NULL, + `MILITARY_AGENCY` varchar(20) DEFAULT NULL, + PRIMARY KEY (`ID`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `CUSTOMER` +-- + +LOCK TABLES `CUSTOMER` WRITE; +/*!40000 ALTER TABLE `CUSTOMER` DISABLE KEYS */; +/*!40000 ALTER TABLE `CUSTOMER` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `INVENTORY` +-- + +DROP TABLE IF EXISTS `INVENTORY`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `INVENTORY` ( + `PRODUCT_ID` varchar(20) NOT NULL, + `LOCATION_ID` varchar(20) NOT NULL, + `AVAILABLE` int(11) DEFAULT NULL, + `TOTAL` int(11) DEFAULT NULL, + `ACTIVE` BOOLEAN DEFAULT TRUE, + `DISABLED` BIT(1) DEFAULT 0, + `ENABLED` CHAR(1) DEFAULT 'Y', + PRIMARY KEY (`PRODUCT_ID`,`LOCATION_ID`), + KEY `LOCATION_FK` (`LOCATION_ID`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `INVENTORY` +-- + +LOCK TABLES `INVENTORY` WRITE; +/*!40000 ALTER TABLE `INVENTORY` DISABLE KEYS */; +/*!40000 ALTER TABLE `INVENTORY` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Temporary view structure for view `INVENTORY_VIEW` +-- + +DROP TABLE IF EXISTS `INVENTORY_VIEW`; +/*!50001 DROP VIEW IF EXISTS `INVENTORY_VIEW`*/; +SET @saved_cs_client = @@character_set_client; +SET character_set_client = utf8; +/*!50001 CREATE VIEW `INVENTORY_VIEW` AS SELECT + 1 AS `ID`, + 1 AS `PRODUCT_ID`, + 1 AS `PRODUCT_NAME`, + 1 AS `AUDIBLE_RANGE`, + 1 AS `EFFECTIVE_RANGE`, + 1 AS `ROUNDS`, + 1 AS `EXTRAS`, + 1 AS `FIRE_MODES`, + 1 AS `LOCATION_ID`, + 1 AS `LOCATION`, + 1 AS `CITY`, + 1 AS `ZIPCODE`, + 1 AS `AVAILABLE`*/; +SET character_set_client = @saved_cs_client; + +-- +-- Table structure for table `LOCATION` +-- + +DROP TABLE IF EXISTS `LOCATION`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `LOCATION` ( + `ID` varchar(20) NOT NULL, + `STREET` varchar(20) DEFAULT NULL, + `CITY` varchar(20) DEFAULT NULL, + `ZIPCODE` varchar(20) DEFAULT NULL, + `NAME` varchar(20) DEFAULT NULL, + PRIMARY KEY (`ID`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `LOCATION` +-- + +LOCK TABLES `LOCATION` WRITE; +/*!40000 ALTER TABLE `LOCATION` DISABLE KEYS */; +/*!40000 ALTER TABLE `LOCATION` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `PRODUCT` +-- + +DROP TABLE IF EXISTS `PRODUCT`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `PRODUCT` ( + `ID` varchar(20) NOT NULL, + `NAME` varchar(64) DEFAULT NULL, + `AUDIBLE_RANGE` decimal(12,2) DEFAULT NULL, + `EFFECTIVE_RANGE` decimal(12,2) DEFAULT NULL, + `ROUNDS` decimal(10,0) DEFAULT NULL, + `EXTRAS` varchar(64) DEFAULT NULL, + `FIRE_MODES` varchar(64) DEFAULT NULL, + PRIMARY KEY (`ID`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `PRODUCT` +-- + +LOCK TABLES `PRODUCT` WRITE; +/*!40000 ALTER TABLE `PRODUCT` DISABLE KEYS */; +/*!40000 ALTER TABLE `PRODUCT` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `RESERVATION` +-- + +DROP TABLE IF EXISTS `RESERVATION`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `RESERVATION` ( + `ID` varchar(20) NOT NULL, + `PRODUCT_ID` varchar(20) NOT NULL, + `LOCATION_ID` varchar(20) NOT NULL, + `CUSTOMER_ID` varchar(20) NOT NULL, + `QTY` int(11) DEFAULT NULL, + `STATUS` varchar(20) DEFAULT NULL, + `RESERVE_DATE` date DEFAULT NULL, + `PICKUP_DATE` date DEFAULT NULL, + `RETURN_DATE` date DEFAULT NULL, + PRIMARY KEY (`ID`), + KEY `RESERVATION_PRODUCT_FK` (`PRODUCT_ID`), + KEY `RESERVATION_LOCATION_FK` (`LOCATION_ID`), + KEY `RESERVATION_CUSTOMER_FK` (`CUSTOMER_ID`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `RESERVATION` +-- + +LOCK TABLES `RESERVATION` WRITE; +/*!40000 ALTER TABLE `RESERVATION` DISABLE KEYS */; +/*!40000 ALTER TABLE `RESERVATION` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `TESTGEN` +-- + +DROP TABLE IF EXISTS `TESTGEN`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `TESTGEN` ( + `ID` int(11) NOT NULL AUTO_INCREMENT, + `NAME` varchar(64) DEFAULT NULL, + PRIMARY KEY (`ID`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Current Database: `STRONGLOOP` +-- + +USE `STRONGLOOP`; + +-- +-- Final view structure for view `INVENTORY_VIEW` +-- + +/*!50001 DROP VIEW IF EXISTS `INVENTORY_VIEW`*/; +/*!50001 SET @saved_cs_client = @@character_set_client */; +/*!50001 SET @saved_cs_results = @@character_set_results */; +/*!50001 SET @saved_col_connection = @@collation_connection */; +/*!50001 SET character_set_client = utf8 */; +/*!50001 SET character_set_results = utf8 */; +/*!50001 SET collation_connection = utf8_general_ci */; +/*!50001 CREATE ALGORITHM=UNDEFINED */ +/*!50013 DEFINER=`strongloop`@`%` SQL SECURITY DEFINER */ +/*!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`)) */; +/*!50001 SET character_set_client = @saved_cs_client */; +/*!50001 SET character_set_results = @saved_cs_results */; +/*!50001 SET collation_connection = @saved_col_connection */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2016-08-09 19:14:01 diff --git a/test/transaction.promise.test.js b/test/transaction.promise.test.js new file mode 100644 index 0000000..127cbe4 --- /dev/null +++ b/test/transaction.promise.test.js @@ -0,0 +1,177 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +if (typeof Promise === 'undefined') { + global.Promise = require('bluebird'); +} +var Transaction = require('loopback-datasource-juggler').Transaction; +require('./init.js'); +require('should'); + +var db, Post, Review; + +describe('transactions with promise', function() { + before(function(done) { + db = getDataSource({collation: 'utf8_general_ci', createDatabase: true}); + db.once('connected', function() { + Post = db.define('PostTX', { + title: {type: String, length: 255, index: true}, + content: {type: String}, + }, {mysql: {engine: 'INNODB'}}); + Review = db.define('ReviewTX', { + author: String, + content: {type: String}, + }, {mysql: {engine: 'INNODB'}}); + Post.hasMany(Review, {as: 'reviews', foreignKey: 'postId'}); + db.automigrate(['PostTX', 'ReviewTX'], done); + }); + }); + + var currentTx; + var hooks = []; + // Return an async function to start a transaction and create a post + function createPostInTx(post, timeout) { + return function(done) { + // Transaction.begin(db.connector, Transaction.READ_COMMITTED, + var promise = Post.beginTransaction({ + isolationLevel: Transaction.READ_COMMITTED, + timeout: timeout, + }); + promise.then(function(tx) { + (typeof tx.id).should.be.eql('string'); + currentTx = tx; + hooks = []; + tx.observe('before commit', function(context, next) { + hooks.push('before commit'); + next(); + }); + tx.observe('after commit', function(context, next) { + hooks.push('after commit'); + next(); + }); + tx.observe('before rollback', function(context, next) { + hooks.push('before rollback'); + next(); + }); + tx.observe('after rollback', function(context, next) { + hooks.push('after rollback'); + next(); + }); + }).then(function() { + Post.create(post, {transaction: currentTx}).then( + function(p) { + p.reviews.create({ + author: 'John', + content: 'Review for ' + p.title, + }, {transaction: currentTx}).then( + function(c) { + done(null, c); + }); + }); + }).catch(done); + }; + } + +// Return an async function to find matching posts and assert number of +// records to equal to the count + function expectToFindPosts(where, count, inTx) { + return function(done) { + var options = {}; + if (inTx) { + options.transaction = currentTx; + } + Post.find({where: where}, options).then( + function(posts) { + posts.length.should.be.eql(count); + if (count) { + // Find related reviews + // Please note the empty {} is required, otherwise, the options + // will be treated as a filter + posts[0].reviews({}, options).then(function(reviews) { + reviews.length.should.be.eql(count); + done(); + }); + } else { + done(); + } + }).catch(done); + }; + } + + describe('commit', function() { + var post = {title: 't1', content: 'c1'}; + before(createPostInTx(post)); + + it('should not see the uncommitted insert', expectToFindPosts(post, 0)); + + it('should see the uncommitted insert from the same transaction', + expectToFindPosts(post, 1, true)); + + it('should commit a transaction', function(done) { + currentTx.commit().then(function() { + hooks.should.be.eql(['before commit', 'after commit']); + done(); + }).catch(done); + }); + + it('should see the committed insert', expectToFindPosts(post, 1)); + + it('should report error if the transaction is not active', function(done) { + currentTx.commit().catch(function(err) { + (err).should.be.instanceof(Error); + done(); + }); + }); + }); + + describe('rollback', function() { + var post = {title: 't2', content: 'c2'}; + before(createPostInTx(post)); + + it('should not see the uncommitted insert', expectToFindPosts(post, 0)); + + it('should see the uncommitted insert from the same transaction', + expectToFindPosts(post, 1, true)); + + it('should rollback a transaction', function(done) { + currentTx.rollback().then(function() { + hooks.should.be.eql(['before rollback', 'after rollback']); + done(); + }).catch(done); + }); + + it('should not see the rolledback insert', expectToFindPosts(post, 0)); + + it('should report error if the transaction is not active', function(done) { + currentTx.rollback().catch(function(err) { + (err).should.be.instanceof(Error); + done(); + }); + }); + }); + + describe('timeout', function() { + var post = {title: 't3', content: 'c3'}; + before(createPostInTx(post, 500)); + + it('should invoke the timeout hook', function(done) { + currentTx.observe('timeout', function(context, next) { + next(); + // It will only proceed upon timeout + done(); + }); + }); + + it('should rollback the transaction if timeout', function(done) { + Post.find({where: {title: 't3'}}, {transaction: currentTx}, + function(err, posts) { + if (err) return done(err); + posts.length.should.be.eql(0); + done(); + }); + }); + }); +}); diff --git a/test/transaction.test.js b/test/transaction.test.js new file mode 100644 index 0000000..b4f9d9b --- /dev/null +++ b/test/transaction.test.js @@ -0,0 +1,180 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback-connector-mysql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +var Transaction = require('loopback-datasource-juggler').Transaction; +require('./init.js'); +require('should'); + +var db, Post, Review; + +describe('transactions', function() { + before(function(done) { + db = getDataSource({collation: 'utf8_general_ci', createDatabase: true}); + db.once('connected', function() { + Post = db.define('PostTX', { + title: {type: String, length: 255, index: true}, + content: {type: String}, + }, {mysql: {engine: 'INNODB'}}); + Review = db.define('ReviewTX', { + author: String, + content: {type: String}, + }, {mysql: {engine: 'INNODB'}}); + Post.hasMany(Review, {as: 'reviews', foreignKey: 'postId'}); + db.automigrate(['PostTX', 'ReviewTX'], done); + }); + }); + + var currentTx; + var hooks = []; + // Return an async function to start a transaction and create a post + function createPostInTx(post, timeout) { + return function(done) { + // Transaction.begin(db.connector, Transaction.READ_COMMITTED, + Post.beginTransaction({ + isolationLevel: Transaction.READ_COMMITTED, + timeout: timeout, + }, + function(err, tx) { + if (err) return done(err); + (typeof tx.id).should.be.eql('string'); + hooks = []; + tx.observe('before commit', function(context, next) { + hooks.push('before commit'); + next(); + }); + tx.observe('after commit', function(context, next) { + hooks.push('after commit'); + next(); + }); + tx.observe('before rollback', function(context, next) { + hooks.push('before rollback'); + next(); + }); + tx.observe('after rollback', function(context, next) { + hooks.push('after rollback'); + next(); + }); + currentTx = tx; + Post.create(post, {transaction: tx}, + function(err, p) { + if (err) { + done(err); + } else { + p.reviews.create({ + author: 'John', + content: 'Review for ' + p.title, + }, {transaction: tx}, + function(err, c) { + done(err); + }); + } + }); + }); + }; + } + + // Return an async function to find matching posts and assert number of + // records to equal to the count + function expectToFindPosts(where, count, inTx) { + return function(done) { + var options = {}; + if (inTx) { + options.transaction = currentTx; + } + Post.find({where: where}, options, + function(err, posts) { + if (err) return done(err); + posts.length.should.be.eql(count); + if (count) { + // Find related reviews + // Please note the empty {} is required, otherwise, the options + // will be treated as a filter + posts[0].reviews({}, options, function(err, reviews) { + if (err) return done(err); + reviews.length.should.be.eql(count); + done(); + }); + } else { + done(); + } + }); + }; + } + + describe('commit', function() { + var post = {title: 't1', content: 'c1'}; + before(createPostInTx(post)); + + it('should not see the uncommitted insert', expectToFindPosts(post, 0)); + + it('should see the uncommitted insert from the same transaction', + expectToFindPosts(post, 1, true)); + + it('should commit a transaction', function(done) { + currentTx.commit(function(err) { + hooks.should.be.eql(['before commit', 'after commit']); + done(err); + }); + }); + + it('should see the committed insert', expectToFindPosts(post, 1)); + + it('should report error if the transaction is not active', function(done) { + currentTx.commit(function(err) { + (err).should.be.instanceof(Error); + done(); + }); + }); + }); + + describe('rollback', function() { + var post = {title: 't2', content: 'c2'}; + before(createPostInTx(post)); + + it('should not see the uncommitted insert', expectToFindPosts(post, 0)); + + it('should see the uncommitted insert from the same transaction', + expectToFindPosts(post, 1, true)); + + it('should rollback a transaction', function(done) { + currentTx.rollback(function(err) { + hooks.should.be.eql(['before rollback', 'after rollback']); + done(err); + }); + }); + + it('should not see the rolledback insert', expectToFindPosts(post, 0)); + + it('should report error if the transaction is not active', function(done) { + currentTx.rollback(function(err) { + (err).should.be.instanceof(Error); + done(); + }); + }); + }); + + describe('timeout', function() { + var post = {title: 't3', content: 'c3'}; + before(createPostInTx(post, 500)); + + it('should invoke the timeout hook', function(done) { + currentTx.observe('timeout', function(context, next) { + next(); + // It will only proceed upon timeout + done(); + }); + }); + + it('should rollback the transaction if timeout', function(done) { + Post.find({where: {title: 't3'}}, {transaction: currentTx}, + function(err, posts) { + if (err) return done(err); + posts.length.should.be.eql(0); + done(); + }); + }); + }); +}); -- libgit2 0.21.2