diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..e702b2b9 --- /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 00000000..ccc915a7 --- /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 00000000..d2b240f5 --- /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 index 5b2da1dc..8011db9e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ coverage *.xml .loopbackrc .idea + diff --git a/CHANGES.md b/CHANGES.md index 137976d0..e7e35dcc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,125 @@ +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 ========================= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 01dbb547..5687a637 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -147,5 +147,5 @@ Contributing to `loopback-connector-mysql` is easy. In a few simple steps: inaccurate in any respect. Email us at callback@strongloop.com. ``` -[Google C++ Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml -[Google Javascript Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml +[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 00000000..4be35208 --- /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/LICENSE.md b/LICENSE.md deleted file mode 100644 index 4de8f1e2..00000000 --- a/LICENSE.md +++ /dev/null @@ -1,9 +0,0 @@ -Copyright (c) 2013-2015 StrongLoop, Inc. - -loopback-connector-mysql uses a dual license model. - -You may use this library under the terms of the [MIT License][], -or under the terms of the [StrongLoop Subscription Agreement][]. - -[MIT License]: http://opensource.org/licenses/MIT -[StrongLoop Subscription Agreement]: http://strongloop.com/license diff --git a/Makefile b/Makefile deleted file mode 100644 index 3d1e31a3..00000000 --- a/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -## TESTS - -TESTER = ./node_modules/.bin/mocha -OPTS = --growl --globals getSchema --timeout 15000 -TESTS = test/*.test.js - -test: - $(TESTER) $(OPTS) $(TESTS) -test-verbose: - $(TESTER) $(OPTS) --reporter spec $(TESTS) -testing: - $(TESTER) $(OPTS) --watch $(TESTS) -.PHONY: test docs coverage diff --git a/README.md b/README.md index 8c0c6cde..74c87f03 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,361 @@ -## loopback-connector-mysql +# loopback-connector-mysql -`loopback-connector-mysql` is the MySQL connector module for [loopback-datasource-juggler](https://github.com/strongloop/loopback-datasource-juggler/). +[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. -For complete documentation, see [StrongLoop Documentation | MySQL Connector](http://docs.strongloop.com/display/LB/MySQL+connector). +
See also LoopBack MySQL Connector in LoopBack documentation. +

+NOTE: The MySQL connector requires MySQL 5.0+. +
## Installation -````sh +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: + +{% include code-caption.html content="/server/datasources.json" %} +```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. -## Basic use +### LoopBack to MySQL types -To use it you need `loopback-datasource-juggler`. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LoopBack TypeMySQL Type
String/JSONVARCHAR
TextTEXT
NumberINT
DateDATETIME
BooleanTINYINT(1)
GeoPoint objectPOINT
Custom Enum type
(See Enum below)
ENUM
-1. Setup dependencies in `package.json`: +### MySQL to LoopBack types - ```json + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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": { - ... - "dependencies": { - "loopback-datasource-juggler": "latest", - "loopback-connector-mysql": "latest" - }, - ... + "columnName":"LOCATION_ID", + "dataType":"VARCHAR2", + "dataLength":20, + "nullable":"N" } - ``` - -2. Use: - - ```javascript - var DataSource = require('loopback-datasource-juggler').DataSource; - var dataSource = new DataSource('mysql', { - host: 'localhost', - port: 3306, - database: 'mydb', - username: 'myuser', - password: 'mypass' - }); - ``` - You can optionally pass a few additional parameters supported by [`node-mysql`](https://github.com/felixge/node-mysql), - most particularly `password` and `collation`. `Collation` currently defaults - to `utf8_general_ci`. The `collation` value will also be used to derive the - connection charset. - -## Running Tests +} +``` + +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. diff --git a/example/app.js b/example/app.js index cf633f69..f4e68b27 100644 --- a/example/app.js +++ b/example/app.js @@ -1,3 +1,9 @@ +// 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; @@ -10,7 +16,7 @@ function show(err, models) { } else { console.log(models); if (models) { - models.forEach(function (m) { + models.forEach(function(m) { console.dir(m); }); } @@ -28,13 +34,8 @@ ds.discoverForeignKeys('inventory', show); ds.discoverExportedForeignKeys('location', show); -ds.discoverAndBuildModels('weapon', {owner: 'strongloop', visited: {}, associations: true}, function (err, models) { - +ds.discoverAndBuildModels('weapon', {owner: 'strongloop', visited: {}, associations: true}, function(err, models) { for (var m in models) { models[m].all(show); } - }); - - - diff --git a/index.js b/index.js index 5983399f..03f1307c 100644 --- a/index.js +++ b/index.js @@ -1 +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 00000000..c6614df0 --- /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 00000000..7e4acdfb --- /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 00000000..fbadd85b --- /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 00000000..266ade9d --- /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 00000000..3a23a10d --- /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 00000000..d45c69be --- /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 00000000..fdc58235 --- /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 00000000..39c02107 --- /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 00000000..33010f1d --- /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 00000000..8af6c04b --- /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 00000000..97116597 --- /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 00000000..40c0db36 --- /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 index 3a2a786f..336a3f32 100644 --- a/lib/discovery.js +++ b/lib/discovery.js @@ -1,3 +1,11 @@ +// 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; /*! @@ -81,7 +89,6 @@ function mixinDiscovery(MySQL, mysql) { function queryViews(options) { var sqlViews = null; if (options.views) { - var schema = options.owner || options.schema; if (options.all && !schema) { @@ -162,7 +169,7 @@ function mixinDiscovery(MySQL, mysql) { */ function getArgs(table, options, cb) { if ('string' !== typeof table || !table) { - throw new Error('table is a required string argument: ' + table); + throw new Error(g.f('{{table}} is a required string argument: %s', table)); } options = options || {}; if (!cb && 'function' === typeof options) { @@ -170,13 +177,13 @@ function mixinDiscovery(MySQL, mysql) { options = {}; } if (typeof options !== 'object') { - throw new Error('options must be an object: ' + options); + throw new Error(g.f('{{options}} must be an {{object}}: %s', options)); } return { schema: options.owner || options.schema, table: table, options: options, - cb: cb + cb: cb, }; } @@ -196,6 +203,7 @@ function mixinDiscovery(MySQL, mysql) { ' character_maximum_length AS "dataLength",' + ' numeric_precision AS "dataPrecision",' + ' numeric_scale AS "dataScale",' + + ' column_type AS "columnType",' + ' is_nullable = \'YES\' AS "nullable"' + ' FROM information_schema.columns' + ' WHERE table_schema=' + mysql.escape(schema) + @@ -209,6 +217,7 @@ function mixinDiscovery(MySQL, mysql) { ' character_maximum_length AS "dataLength",' + ' numeric_precision AS "dataPrecision",' + ' numeric_scale AS "dataScale",' + + ' column_type AS "columnType",' + ' is_nullable = \'YES\' AS "nullable"' + ' FROM information_schema.columns' + (table ? ' WHERE table_name=' + mysql.escape(table) : ''), @@ -233,6 +242,22 @@ function mixinDiscovery(MySQL, mysql) { } table = args.table; options = args.options; + + // 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. + var defaultOptions = { + treatCHAR1AsString: false, + treatBIT1AsBit: true, + treatTINYINT1AsTinyInt: true, + }; + + for (var opt in defaultOptions) { + if (defaultOptions.hasOwnProperty(opt) && !options.hasOwnProperty(opt)) { + options[opt] = defaultOptions[opt]; + } + } + cb = args.cb; var sql = queryColumns(schema, table); @@ -241,7 +266,7 @@ function mixinDiscovery(MySQL, mysql) { cb(err, results); } else { results.map(function(r) { - r.type = self.buildPropertyType(r); + r.type = self.buildPropertyType(r, options); r.nullable = r.nullable ? 'Y' : 'N'; }); cb(err, results); @@ -397,20 +422,18 @@ function mixinDiscovery(MySQL, mysql) { this.execute(sql, cb); }; - MySQL.prototype.buildPropertyType = function (columnDefinition) { + 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 (dataLength === 1) { - // Treat char(1) as boolean + if (!options.treatCHAR1AsString && columnType === 'char(1)') { + // Treat char(1) as boolean ('Y', 'N', 'T', 'F', '0', '1') return 'Boolean'; - } else { - return 'String'; } - break; case 'VARCHAR': case 'TINYTEXT': case 'MEDIUMTEXT': @@ -426,8 +449,16 @@ function mixinDiscovery(MySQL, mysql) { 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': @@ -442,10 +473,13 @@ function mixinDiscovery(MySQL, mysql) { 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 && diff --git a/lib/enumFactory.js b/lib/enumFactory.js index f57f8e42..4e681748 100644 --- a/lib/enumFactory.js +++ b/lib/enumFactory.js @@ -1,10 +1,18 @@ +// 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] + return Enum[arg]; } else if (Enum._values.indexOf(arg) !== -1) { return arg; } else if (arg === null) { @@ -23,7 +31,7 @@ var EnumFactory = function() { configurable: false, enumerable: true, value: arg, - writable: false + writable: false, }); dxList.push(arg); } @@ -31,18 +39,18 @@ var EnumFactory = function() { configurable: false, enumerable: false, value: dxList, - writable: false + writable: false, }); Object.defineProperty(Enum, '_string', { configurable: false, enumerable: false, value: stringified(Enum), - writable: false + writable: false, }); Object.freeze(Enum); return Enum; } else { - throw "No arguments - could not create Enum."; + throw g.f('No arguments - could not create {{Enum}}.'); } }; @@ -57,10 +65,3 @@ function stringified(anEnum) { } exports.EnumFactory = EnumFactory; - - - - - - - diff --git a/lib/migration.js b/lib/migration.js index f1c18a5f..b8e9cdc6 100644 --- a/lib/migration.js +++ b/lib/migration.js @@ -1,3 +1,10 @@ +// 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; @@ -29,7 +36,7 @@ function mixinMigration(MySQL, mysql) { async.each(models, function(model, done) { if (!(model in self._models)) { return process.nextTick(function() { - done(new Error('Model not found: ' + model)); + done(new Error(g.f('Model not found: %s', model))); }); } var table = self.tableEscaped(model); @@ -43,7 +50,6 @@ function mixinMigration(MySQL, mysql) { }); }); }, cb); - }; /*! @@ -124,7 +130,7 @@ function mixinMigration(MySQL, mysql) { if (!ai[name]) { ai[name] = { info: i, - columns: [] + columns: [], }; } ai[name].columns[i.Seq_in_index - 1] = i.Column_name; @@ -136,18 +142,19 @@ function mixinMigration(MySQL, mysql) { propNames.forEach(function(propName) { if (m.properties[propName] && self.id(model, propName)) return; var found; + var colName = expectedColName(propName); if (actualFields) { actualFields.forEach(function(f) { - if (f.Field === propName) { + if (f.Field === colName) { found = f; } }); } if (found) { - actualize(propName, found); + actualize(colName, found); } else { - sql.push('ADD COLUMN ' + self.client.escapeId(propName) + ' ' + + sql.push('ADD COLUMN ' + self.client.escapeId(colName) + ' ' + self.buildColumnDefinition(model, propName)); } }); @@ -155,9 +162,12 @@ function mixinMigration(MySQL, mysql) { // drop columns if (actualFields) { actualFields.forEach(function(f) { - var notFound = !~propNames.indexOf(f.Field); - if (m.properties[f.Field] && self.id(model, f.Field)) return; - if (notFound || !m.properties[f.Field]) { + var colNames = propNames.map(expectedColName); + 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)); } }); @@ -179,10 +189,28 @@ function mixinMigration(MySQL, mysql) { // second: check multiple indexes var orderMatched = true; if (indexNames.indexOf(indexName) !== -1) { - m.settings.indexes[indexName].columns.split(/,\s*/).forEach( - function(columnName, i) { - if (ai[indexName].columns[i] !== columnName) orderMatched = false; - }); + //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)); @@ -199,7 +227,8 @@ function mixinMigration(MySQL, mysql) { } var found = ai[propName] && ai[propName].info; if (!found) { - var pName = self.client.escapeId(propName); + var colName = expectedColName(propName); + var pName = self.client.escapeId(colName); var type = ''; var kind = ''; if (i.type) { @@ -210,7 +239,7 @@ function mixinMigration(MySQL, mysql) { ' (' + pName + ') ' + type); } else { if (typeof i === 'object' && i.unique && i.unique === true) { - kind = "UNIQUE"; + kind = 'UNIQUE'; } sql.push('ADD ' + kind + ' INDEX ' + pName + ' ' + type + ' (' + pName + ') '); @@ -231,13 +260,35 @@ function mixinMigration(MySQL, mysql) { } 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 + - ' (' + i.columns + ') ' + type); + ' (' + columns + ') ' + type); } else { sql.push('ADD ' + kind + ' INDEX ' + type + ' ' + iName + - ' (' + i.columns + ')'); + ' (' + columns + ')'); } } }); @@ -283,6 +334,18 @@ function mixinMigration(MySQL, mysql) { } return false; } + + function expectedColName(propName) { + var mysql = m.properties[propName].mysql; + if (typeof mysql === 'undefined') { + return propName; + } + var colName = mysql.columnName; + if (typeof colName === 'undefined') { + return propName; + } + return colName; + } }; MySQL.prototype.buildColumnDefinitions = @@ -344,7 +407,7 @@ function mixinMigration(MySQL, mysql) { return (kind + ' INDEX ' + columnName + ' (' + columnName + ') ' + type); } else { if (typeof i === 'object' && i.unique && i.unique === true) { - kind = "UNIQUE"; + kind = 'UNIQUE'; } return (kind + ' INDEX ' + columnName + ' ' + type + ' (' + columnName + ') '); } @@ -364,23 +427,43 @@ function mixinMigration(MySQL, mysql) { 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); - if (Array.isArray(i.keys)) { - indexedColumns = i.keys.map(function(key) { - return self.columnEscaped(model, key); - }); + 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 '); + } + } } - var columns = indexedColumns.join(',') || i.columns; - if (kind && type) { - indexClauses.push(kind + ' INDEX ' + indexName + ' (' + columns + ') ' + type); - } else { - indexClauses.push(kind + ' INDEX ' + type + ' ' + indexName + ' (' + columns + ')'); + 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); @@ -469,7 +552,7 @@ function mixinMigration(MySQL, mysql) { // 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 ? 255 : 512); + ((p.type !== String) ? 4096 : p.id || p.index ? 255 : 512); columnType += '(' + len + ')'; break; case 'char': @@ -490,10 +573,10 @@ function mixinMigration(MySQL, mysql) { function stringOptions(p, columnType) { if (p.charset) { - columnType += " CHARACTER SET " + p.charset; + columnType += ' CHARACTER SET ' + p.charset; } if (p.collation) { - columnType += " COLLATE " + p.collation; + columnType += ' COLLATE ' + p.collation; } return columnType; } diff --git a/lib/mysql.js b/lib/mysql.js index fc545eeb..52292dc5 100644 --- a/lib/mysql.js +++ b/lib/mysql.js @@ -1,3 +1,11 @@ +// 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')(); + /*! * Module dependencies */ @@ -26,9 +34,15 @@ exports.initialize = function initializeDataSource(dataSource, callback) { dataSource.EnumFactory = EnumFactory; // factory for Enums. Note that currently Enums can not be registered. - process.nextTick(function() { - callback && callback(); - }); + if (callback) { + if (dataSource.settings.lazyConnect) { + process.nextTick(function() { + callback(); + }); + } else { + dataSource.connector.connect(callback); + } + } }; exports.MySQL = MySQL; @@ -55,7 +69,41 @@ function defineMySQLTypes(dataSource) { */ 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 || {}; + + 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. @@ -72,49 +120,41 @@ function MySQL(settings) { s.connectionLimit = 10; } - var 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]; + 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; } - } - - this.client = mysql.createPool(options); - - this.client.on('error', function(err) { - dataSource.emit('error', err); - dataSource.connected = false; - dataSource.connecting = false; - }); - if (debug.enabled) { - debug('Settings: %j', s); + // 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; } - -require('util').inherits(MySQL, SqlConnector); - /** * Execute the sql statement * @@ -127,7 +167,7 @@ MySQL.prototype.executeSQL = function(sql, params, options, callback) { var debugEnabled = debug.enabled; var db = this.settings.database; if (typeof callback !== 'function') { - throw new Error('callback should be a function'); + throw new Error(g.f('{{callback}} should be a function')); } if (debugEnabled) { debug('SQL: %s, params: %j', sql, params); @@ -200,55 +240,72 @@ MySQL.prototype.executeSQL = function(sql, params, options, callback) { } }; -/** - * 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 {Function} [callback] The callback function - */ -MySQL.prototype.updateOrCreate = MySQL.prototype.save = - function(model, data, options, callback) { - var fields = this.buildFields(model, data); - - 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)); - } +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] + '=' + + 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, ',')); + 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) { + 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); - } - callback(err, data, meta); - }); - }; + 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); +}; function dateToMysql(val) { return val.getUTCFullYear() + '-' + @@ -307,7 +364,7 @@ MySQL.prototype.toColumnValue = function(prop, val) { if (prop.type.name === 'GeoPoint') { return new ParameterizedSQL({ sql: 'Point(?,?)', - params: [val.lat, val.lng] + params: [val.lat, val.lng], }); } if (prop.type === Object) { @@ -351,7 +408,15 @@ MySQL.prototype.fromColumnValue = function(prop, val) { val = String(val); break; case 'Date': - val = new Date(val.toString().replace(/GMT.*$/, 'GMT')); + + // 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); @@ -360,7 +425,7 @@ MySQL.prototype.fromColumnValue = function(prop, val) { case 'Point': val = { lat: val.x, - lng: val.y + lng: val.y, }; break; case 'List': @@ -409,7 +474,7 @@ MySQL.prototype._buildLimit = function(model, limit, offset) { return ''; } return 'LIMIT ' + (offset ? (offset + ',' + limit) : limit); -} +}; MySQL.prototype.applyPagination = function(model, stmt, filter) { var limitClause = this._buildLimit(model, filter.limit, @@ -463,13 +528,13 @@ MySQL.prototype.buildExpression = function(columnName, operator, operatorValue, propertyDefinition) { if (operator === 'regexp') { if (operatorValue.ignoreCase) - console.warn('MySQL regex syntax does not respect the `i` flag'); + g.warn('{{MySQL}} {{regex}} syntax does not respect the {{`i`}} flag'); if (operatorValue.global) - console.warn('MySQL regex syntax does not respect the `g` flag'); + g.warn('{{MySQL}} {{regex}} syntax does not respect the {{`g`}} flag'); if (operatorValue.multiline) - console.warn('MySQL regex syntax does not respect the `m` flag'); + g.warn('{{MySQL}} {{regex}} syntax does not respect the {{`m`}} flag'); return new ParameterizedSQL(columnName + ' REGEXP ?', [operatorValue.source]); diff --git a/lib/transaction.js b/lib/transaction.js index f1fbe5ef..926aa07f 100644 --- a/lib/transaction.js +++ b/lib/transaction.js @@ -1,3 +1,9 @@ +// 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; @@ -6,7 +12,6 @@ module.exports = mixinTransaction; * @param {Object} mysql mysql driver */ function mixinTransaction(MySQL, mysql) { - /** * Begin a new transaction * @param isolationLevel @@ -15,8 +20,8 @@ function mixinTransaction(MySQL, mysql) { 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) { + if (err) return cb(err); + if (isolationLevel) { connection.query( 'SET SESSION TRANSACTION ISOLATION LEVEL ' + isolationLevel, function(err) { @@ -60,4 +65,4 @@ function mixinTransaction(MySQL, mysql) { cb(err); }); }; -} \ No newline at end of file +} diff --git a/package.json b/package.json index edfa2027..bb823fab 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,37 @@ { "name": "loopback-connector-mysql", - "version": "2.2.0", + "version": "3.0.0", "description": "MySQL connector for loopback-datasource-juggler", + "engines": { + "node": ">=4" + }, "main": "index.js", "scripts": { - "test": "mocha" + "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": "^2.1.0", - "mysql": "^2.5.4" + "loopback-connector": "^3.0.0", + "mysql": "^2.11.1", + "strong-globalize": "^2.5.8" }, "devDependencies": { "bluebird": "~2.9.10", - "loopback-datasource-juggler": "^2.28.0", + "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": "^5.0.0", + "should": "^8.0.2", "sinon": "^1.15.4" }, "repository": { "type": "git", "url": "https://github.com/strongloop/loopback-connector-mysql.git" }, - "license": "MIT", - "optionalDependencies": { - "sl-blip": "http://blip.strongloop.com/loopback-connector-mysql@2.2.0" - } + "license": "MIT" } diff --git a/pretest.js b/pretest.js new file mode 100644 index 00000000..cbf69478 --- /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 index d31360cf..33d58ecd 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1,47 +1,107 @@ +// 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; - -describe('connections', function () { +var db, DummyModel, odb, config; - before(function () { +describe('connections', function() { + before(function() { require('./init.js'); + config = global.getConfig(); + odb = getDataSource({collation: 'utf8_general_ci', createDatabase: true}); db = odb; }); - it('should use utf8 charset', function (done) { + 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 () { + it('should disconnect first db', function(done) { + db.disconnect(function() { odb = getDataSource(); done(); }); }); - it('should use latin1 charset', function (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(); + }); + }); }); - it('should drop db and disconnect all', function (done) { - db.connector.execute('DROP DATABASE IF EXISTS ' + db.settings.database, function (err) { - db.disconnect(function () { + 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(); }); }); @@ -49,21 +109,19 @@ describe('connections', function () { }); function charsetTest(test_set, test_collo, test_set_str, test_set_collo, done) { - - query('DROP DATABASE IF EXISTS ' + odb.settings.database, function (err) { + query('DROP DATABASE IF EXISTS ' + odb.settings.database, function(err) { assert.ok(!err); - odb.disconnect(function () { - + odb.disconnect(function() { db = getDataSource({collation: test_set_collo, createDatabase: true}); DummyModel = db.define('DummyModel', {string: String}); - db.automigrate(function () { + 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) { + db.connector.execute(q, function(err, r) { assert.ok(!err); - assert.ok(r[0].DEFAULT_COLLATION_NAME.match(test_collo)); - db.connector.execute('SHOW VARIABLES LIKE "character_set%"', function (err, r) { + 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) { @@ -74,7 +132,7 @@ function charsetTest(test_set, test_collo, test_set_str, test_set_collo, done) { } assert.equal(hit_all, 4); }); - db.connector.execute('SHOW VARIABLES LIKE "collation%"', function (err, r) { + db.connector.execute('SHOW VARIABLES LIKE "collation%"', function(err, r) { assert.ok(!err); var hit_all = 0; for (var result in r) { @@ -88,7 +146,6 @@ function charsetTest(test_set, test_collo, test_set_str, test_set_collo, done) { }); }); }); - } function matchResult(result, variable_name, match) { @@ -99,12 +156,21 @@ function matchResult(result, variable_name, match) { return 0; } -var query = function (sql, cb) { +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 index 998d8765..3fff9add 100644 --- a/test/datatypes.test.js +++ b/test/datatypes.test.js @@ -1,19 +1,25 @@ +// 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 () { - +describe('MySQL specific datatypes', function() { before(setup); - it('should run migration', function (done) { - db.automigrate(function () { + it('should run migration', function(done) { + db.automigrate(function() { done(); }); }); - it('An enum should parse itself', 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)); @@ -24,11 +30,11 @@ describe('MySQL specific datatypes', function () { 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) { + 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) { + 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); @@ -37,26 +43,26 @@ describe('MySQL specific datatypes', function () { }); }); - it('should fail spectacularly with invalid enum values', function (done) { - var em = EnumModel.create({animal: 'horse', condition: 'sleepy', mood: 'happy'}, function (err, obj) { - assert.ok(!err); - EnumModel.findById(obj.id, function (err, found) { - assert.ok(!err); - assert.equal(found.animal, ''); // MySQL fun. - assert.equal(found.animal, 0); - 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) { + 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) { + 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) { + 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); @@ -67,15 +73,13 @@ describe('MySQL specific datatypes', function () { }); }); - it('should disconnect when done', function (done) { + it('should disconnect when done', function(done) { db.disconnect(); - done() + done(); }); - }); function setup(done) { - require('./init.js'); db = getSchema(); @@ -83,26 +87,28 @@ function setup(done) { 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') }, + animal: {type: ANIMAL_ENUM, null: false}, + condition: {type: db.EnumFactory('hungry', 'sleepy', 'thirsty')}, + mood: {type: db.EnumFactory('angry', 'happy', 'sad')}, note: Object, - extras: 'JSON' + extras: 'JSON', }); - blankDatabase(db, done); - + query('SELECT VERSION()', function(err, res) { + mysqlVersion = res && res[0] && res[0]['VERSION()']; + blankDatabase(db, done); + }); } -var query = function (sql, cb) { +var query = function(sql, cb) { db.adapter.execute(sql, cb); }; -var blankDatabase = function (db, 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) { + query('DROP DATABASE IF EXISTS ' + dbn, function(err) { var q = 'CREATE DATABASE ' + dbn; if (cs) { q += ' CHARACTER SET ' + cs; @@ -110,46 +116,40 @@ var blankDatabase = function (db, cb) { if (co) { q += ' COLLATE ' + co; } - query(q, function (err) { + query(q, function(err) { query('USE ' + dbn, cb); }); }); }; -getFields = function (model, cb) { - query('SHOW FIELDS FROM ' + model, function (err, res) { +var getFields = function(model, cb) { + query('SHOW FIELDS FROM ' + model, function(err, res) { if (err) { cb(err); } else { var fields = {}; - res.forEach(function (field) { + res.forEach(function(field) { fields[field.Field] = field; }); cb(err, fields); } }); -} +}; -getIndexes = function (model, cb) { - query('SHOW INDEXES FROM ' + model, function (err, res) { +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) { + res.forEach(function(index) { if (parseInt(index.Seq_in_index, 10) == 1) { - indexes[index.Key_name] = index + 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 00000000..394599c4 --- /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 index 3ed5f883..3437d88a 100644 --- a/test/imported.test.js +++ b/test/imported.test.js @@ -1,10 +1,14 @@ -describe('mysql imported features', function () { +// 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 - before(function () { +'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 index e337a656..af9efbe2 100644 --- a/test/init.js +++ b/test/init.js @@ -1,18 +1,24 @@ +// 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) { - +console.log(config); +global.getConfig = function(options) { var dbConf = { - host: process.env.TEST_MYSQL_HOST || config.host || 'localhost', - port: process.env.TEST_MYSQL_PORT || config.port || 3306, + host: process.env.MYSQL_HOST || config.host || 'localhost', + port: process.env.MYSQL_PORT || config.port || 3306, database: 'myapp_test', - username: process.env.TEST_MYSQL_USER || config.username, - password: process.env.TEST_MYSQL_PASSWORD || config.password, - createDatabase: true + username: process.env.MYSQL_USER || config.username, + password: process.env.MYSQL_PASSWORD || config.password, + createDatabase: true, }; if (options) { @@ -23,9 +29,14 @@ global.getConfig = function (options) { return dbConf; }; -global.getDataSource = global.getSchema = function (options) { +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 index ce41fb22..bd32308c 100644 --- a/test/migration.test.js +++ b/test/migration.test.js @@ -1,21 +1,30 @@ -var should = require('./init.js'); +// 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 () { - +describe('migrations', function() { before(setup); - it('should run migration', function (done) { - db.automigrate(function () { + it('should run migration', function(done) { + db.automigrate(function() { done(); }); }); - it('UserData should have correct columns', function (done) { - getFields('UserData', function (err, fields) { + it('UserData should have correct columns', function(done) { + getFields('UserData', function(err, fields) { + if (!fields) return done(); fields.should.be.eql({ id: { Field: 'id', @@ -23,228 +32,247 @@ describe('migrations', function () { Null: 'NO', Key: 'PRI', Default: null, - Extra: 'auto_increment' }, + Extra: 'auto_increment'}, email: { Field: 'email', - Type: 'varchar(512)', + Type: 'varchar(255)', Null: 'NO', Key: 'MUL', Default: null, - Extra: '' }, + Extra: ''}, name: { Field: 'name', Type: 'varchar(512)', Null: 'YES', Key: '', Default: null, - Extra: '' }, + Extra: ''}, bio: { Field: 'bio', Type: 'text', Null: 'YES', Key: '', Default: null, - Extra: '' }, + Extra: ''}, birthDate: { Field: 'birthDate', Type: 'datetime', Null: 'YES', Key: '', Default: null, - Extra: '' }, + Extra: ''}, pendingPeriod: { Field: 'pendingPeriod', Type: 'int(11)', Null: 'YES', Key: '', Default: null, - Extra: '' }, + Extra: ''}, createdByAdmin: { Field: 'createdByAdmin', Type: 'tinyint(1)', Null: 'YES', Key: '', Default: null, - Extra: '' } + Extra: ''}, }); done(); }); }); - it('UserData should have correct indexes', function (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) { - fields.should.be.eql({ PRIMARY: { Table: 'UserData', - Non_unique: 0, - Key_name: 'PRIMARY', - Seq_in_index: 1, - Column_name: 'id', - Collation: 'A', - Cardinality: 0, - Sub_part: null, - Packed: null, - Null: '', - Index_type: 'BTREE', - Comment: '' }, - email: { Table: 'UserData', + 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', - Cardinality: null, - Sub_part: 333, + // 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', + Comment: ''}, + index0: { + Table: /UserData/i, Non_unique: 1, Key_name: 'index0', Seq_in_index: 1, Column_name: 'email', Collation: 'A', - Cardinality: null, - Sub_part: 333, + // 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: '' } + Comment: ''}, }); done(); }); }); - it('StringData should have correct columns', function (done) { - getFields('StringData', function (err, fields) { + it('StringData should have correct columns', function(done) { + getFields('StringData', function(err, fields) { fields.should.be.eql({ - idString: { Field: "idString", + idString: {Field: 'idString', Type: 'varchar(255)', Null: 'NO', Key: 'PRI', Default: null, Extra: ''}, - smallString: { Field: 'smallString', + smallString: {Field: 'smallString', Type: 'char(127)', Null: 'NO', Key: 'MUL', Default: null, - Extra: '' }, - mediumString: { Field: 'mediumString', + Extra: ''}, + mediumString: {Field: 'mediumString', Type: 'varchar(255)', Null: 'NO', Key: '', Default: null, - Extra: '' }, - tinyText: { Field: 'tinyText', + Extra: ''}, + tinyText: {Field: 'tinyText', Type: 'tinytext', Null: 'YES', Key: '', Default: null, - Extra: '' }, - giantJSON: { Field: 'giantJSON', + Extra: ''}, + giantJSON: {Field: 'giantJSON', Type: 'longtext', Null: 'YES', Key: '', Default: null, - Extra: '' }, - text: { Field: 'text', + Extra: ''}, + text: {Field: 'text', Type: 'varchar(1024)', Null: 'YES', Key: '', Default: null, - Extra: '' } + Extra: ''}, }); done(); }); }); - it('NumberData should have correct columns', function (done) { - getFields('NumberData', function (err, fields) { + it('NumberData should have correct columns', function(done) { + getFields('NumberData', function(err, fields) { fields.should.be.eql({ - id: { Field: 'id', + id: {Field: 'id', Type: 'int(11)', Null: 'NO', Key: 'PRI', Default: null, - Extra: 'auto_increment' }, - number: { Field: 'number', + Extra: 'auto_increment'}, + number: {Field: 'number', Type: 'decimal(10,3) unsigned', Null: 'NO', Key: 'MUL', Default: null, - Extra: '' }, - tinyInt: { Field: 'tinyInt', + Extra: ''}, + tinyInt: {Field: 'tinyInt', Type: 'tinyint(2)', Null: 'YES', Key: '', Default: null, - Extra: '' }, - mediumInt: { Field: 'mediumInt', + Extra: ''}, + mediumInt: {Field: 'mediumInt', Type: 'mediumint(8) unsigned', Null: 'NO', Key: '', Default: null, - Extra: '' }, - floater: { Field: 'floater', + Extra: ''}, + floater: {Field: 'floater', Type: 'double(14,6)', Null: 'YES', Key: '', Default: null, - Extra: '' } + Extra: ''}, }); done(); }); }); - it('DateData should have correct columns', function (done) { - getFields('DateData', function (err, fields) { + it('DateData should have correct columns', function(done) { + getFields('DateData', function(err, fields) { fields.should.be.eql({ - id: { Field: 'id', + id: {Field: 'id', Type: 'int(11)', Null: 'NO', Key: 'PRI', Default: null, - Extra: 'auto_increment' }, - dateTime: { Field: 'dateTime', + Extra: 'auto_increment'}, + dateTime: {Field: 'dateTime', Type: 'datetime', Null: 'YES', Key: '', Default: null, - Extra: '' }, - timestamp: { Field: 'timestamp', + Extra: ''}, + timestamp: {Field: 'timestamp', Type: 'timestamp', Null: 'YES', Key: '', Default: null, - Extra: '' } + Extra: ''}, }); done(); }); }); - it('should autoupdate', function (done) { - var userExists = function (cb) { - query('SELECT * FROM UserData', function (err, res) { + 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) { + UserData.create({email: 'test@example.com'}, function(err, user) { assert.ok(!err, 'Could not create user: ' + err); - userExists(function (yep) { + userExists(function(yep) { assert.ok(yep, 'User does not exist'); }); - UserData.defineProperty('email', { type: String }); + 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) { + 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 @@ -259,7 +287,7 @@ describe('migrations', function () { // assert.ok(!fields.pendingPeriod, // 'Did not drop column pendingPeriod'); // user still exists - userExists(function (yep) { + userExists(function(yep) { assert.ok(yep, 'User does not exist'); done(); }); @@ -268,42 +296,81 @@ describe('migrations', function () { }); }); - it('should check actuality of dataSource', function (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) { + 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) { + UserData.dataSource.isActual(function(err, ok) { assert.ok(!ok, 'dataSource is actual (shouldn\t be)'); - done() + done(); }); }); }); - it('should allow numbers with decimals', function (done) { - NumberData.create({number: 1.1234567, tinyInt: 123456, mediumInt: -1234567, - floater: 123456789.1234567 }, function (err, obj) { - assert.ok(!err); - assert.ok(obj); - NumberData.findById(obj.id, function (err, found) { - assert.equal(found.number, 1.123); - assert.equal(found.tinyInt, 127); - assert.equal(found.mediumInt, 0); - assert.equal(found.floater, 99999999.999999); - 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(); + }); }); - }); }); - it('should allow both kinds of date columns', function (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) { + 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) { + 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(), @@ -313,6 +380,63 @@ describe('migrations', function () { }); }); + // 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); @@ -325,31 +449,29 @@ describe('migrations', function () { }); }); - it('should disconnect when done', function (done) { + 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 }, + email: {type: String, null: false, index: true}, name: String, bio: Schema.Text, birthDate: Date, pendingPeriod: Number, createdByAdmin: Boolean, - }, { indexes: { + }, {indexes: { index0: { - columns: 'email, createdByAdmin' - } - } + columns: 'email, createdByAdmin', + }, + }, }); StringData = db.define('StringData', { @@ -359,7 +481,7 @@ function setup(done) { 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} + text: {type: Schema.Text, dataType: 'varchar', limit: 1024}, }); NumberData = db.define('NumberData', { @@ -368,27 +490,29 @@ function setup(done) { 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} + floater: {type: Number, dataType: 'double', precision: 14, scale: 6}, }); DateData = db.define('DateData', { dateTime: {type: Date, dataType: 'datetime'}, - timestamp: {type: Date, dataType: 'timestamp'} + timestamp: {type: Date, dataType: 'timestamp'}, }); - blankDatabase(db, done); - + query('SELECT VERSION()', function(err, res) { + mysqlVersion = res && res[0] && res[0]['VERSION()']; + blankDatabase(db, done); + }); } -var query = function (sql, cb) { +var query = function(sql, cb) { db.adapter.execute(sql, cb); }; -var blankDatabase = function (db, 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) { + query('DROP DATABASE IF EXISTS ' + dbn, function(err) { var q = 'CREATE DATABASE ' + dbn; if (cs) { q += ' CHARACTER SET ' + cs; @@ -396,46 +520,40 @@ var blankDatabase = function (db, cb) { if (co) { q += ' COLLATE ' + co; } - query(q, function (err) { + query(q, function(err) { query('USE ' + dbn, cb); }); }); }; -getFields = function (model, cb) { - query('SHOW FIELDS FROM ' + model, function (err, res) { +var getFields = function(model, cb) { + query('SHOW FIELDS FROM ' + model, function(err, res) { if (err) { cb(err); } else { var fields = {}; - res.forEach(function (field) { + res.forEach(function(field) { fields[field.Field] = field; }); cb(err, fields); } }); -} +}; -getIndexes = function (model, cb) { - query('SHOW INDEXES FROM ' + model, function (err, res) { +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) { + res.forEach(function(index) { if (parseInt(index.Seq_in_index, 10) == 1) { - indexes[index.Key_name] = index + indexes[index.Key_name] = index; } }); cb(err, indexes); } }); }; - - - - - - diff --git a/test/mysql.autoupdate.test.js b/test/mysql.autoupdate.test.js index eb1f89df..3975feec 100644 --- a/test/mysql.autoupdate.test.js +++ b/test/mysql.autoupdate.test.js @@ -1,94 +1,176 @@ +// 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 () { +before(function() { ds = getDataSource(); }); -describe('MySQL connector', function () { - it('should auto migrate/update tables', function (done) { +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" - } - }, - "properties": { - "id": { - "type": "String", - "length": 20, - "id": 1 + { + 'name': 'CustomerTest', + 'options': { + 'idInjection': false, + 'mysql': { + 'schema': 'myapp_test', + 'table': 'customer_test', + }, + 'indexes': { + 'name_index': { + 'keys': { + 'name': 1, + }, + 'options': { + 'unique': true, + }, + }, + }, }, - "name": { - "type": "String", - "required": false, - "length": 40 + '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, + }, + }, }, - "email": { - "type": "String", - "required": true, - "length": 40 - }, - "age": { - "type": "Number", - "required": false - } - } - } + }; var schema_v2 = - { - "name": "CustomerTest", - "options": { - "idInjection": false, - "mysql": { - "schema": "myapp_test", - "table": "customer_test" - } - }, - "properties": { - "id": { - "type": "String", - "length": 20, - "id": 1 - }, - "email": { - "type": "String", - "required": false, - "length": 60, - "mysql": { - "columnName": "email", - "dataType": "varchar", - "dataLength": 60, - "nullable": "YES" - } + { + 'name': 'CustomerTest', + 'options': { + 'idInjection': false, + 'mysql': { + 'schema': 'myapp_test', + 'table': 'customer_test', + }, + 'indexes': { + 'updated_name_index': { + 'keys': { + 'firstName': 1, + 'lastName': -1, + }, + 'options': { + 'unique': true, + }, + }, + }, }, - "firstName": { - "type": "String", - "required": false, - "length": 40 + '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, + }, + }, }, - "lastName": { - "type": "String", - "required": false, - "length": 40 - } - } - } + }; 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, 4); - var names = props.map(function (p) { + 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'); @@ -99,26 +181,84 @@ describe('MySQL connector', function () { assert.equal(names[1], 'name'); assert.equal(names[2], 'email'); assert.equal(names[3], 'age'); + assert.equal(names[4], 'customer_discount'); - ds.createModel(schema_v2.name, schema_v2.properties, schema_v2.options); - - ds.autoupdate(function (err, result) { - ds.discoverModelProperties('customer_test', function (err, props) { - assert.equal(props.length, 4); - var names = props.map(function (p) { - return p.columnName; + 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); + }); }); - assert.equal(names[0], 'id'); - assert.equal(names[1], 'email'); - assert.equal(names[2], 'firstName'); - assert.equal(names[3], 'lastName'); - 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); @@ -133,5 +273,24 @@ describe('MySQL connector', function () { }); }); -}); + 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 index 123e73db..5c176fd7 100644 --- a/test/mysql.discover.test.js +++ b/test/mysql.discover.test.js @@ -1,3 +1,9 @@ +// 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'); @@ -5,13 +11,14 @@ var assert = require('assert'); var DataSource = require('loopback-datasource-juggler').DataSource; var db, config; -before(function () { - config = require('rc')('loopback', {dev: {mysql: {}}}).dev.mysql; +before(function() { + require('./init'); + config = getConfig(); config.database = 'STRONGLOOP'; db = new DataSource(require('../'), config); }); -describe('discoverModels', function () { +describe('discoverModels', function() { describe('Discover database schemas', function() { it('should return an array of db schemas', function(done) { db.connector.discoverDatabaseSchemas(function(err, schemas) { @@ -23,19 +30,18 @@ describe('discoverModels', function () { }); }); - describe('Discover models including views', function () { - it('should return an array of tables and views', function (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) { + limit: 3, + }, function(err, models) { if (err) { console.error(err); done(err); } else { var views = false; - models.forEach(function (m) { + models.forEach(function(m) { // console.dir(m); if (m.type === 'view') { views = true; @@ -48,18 +54,17 @@ describe('discoverModels', function () { }); }); - describe('Discover current user\'s tables', function () { - it('should return an array of tables for the current user', function (done) { - + 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) { + limit: 3, + }, function(err, models) { if (err) { console.error(err); done(err); } else { var views = false; - models.forEach(function (m) { + models.forEach(function(m) { assert.equal(m.owner, config.username); }); done(null, models); @@ -68,19 +73,19 @@ describe('discoverModels', function () { }); }); - describe('Discover models excluding views', function () { - it('should return an array of only tables', function (done) { - + 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) { + limit: 3, + }, function(err, models) { if (err) { console.error(err); done(err); } else { var views = false; - models.forEach(function (m) { + models.forEach(function(m) { // console.dir(m); if (m.type === 'view') { views = true; @@ -95,19 +100,18 @@ describe('discoverModels', function () { }); }); -describe('Discover models including other users', function () { - it('should return an array of all tables and views', function (done) { - +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) { + limit: 3, + }, function(err, models) { if (err) { console.error(err); done(err); } else { var others = false; - models.forEach(function (m) { + models.forEach(function(m) { // console.dir(m); if (m.owner !== 'STRONGLOOP') { others = true; @@ -120,51 +124,50 @@ describe('Discover models including other users', function () { }); }); -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) { +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) { + models.forEach(function(m) { // console.dir(m); - assert(m.tableName === 'PRODUCT'); + 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) { +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) { + models.forEach(function(m) { // console.dir(m); - assert(m.tableName === 'PRODUCT'); + 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) { + 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) { + models.forEach(function(m) { // console.dir(m); - assert(m.tableName === 'PRODUCT'); + assert(m.tableName === 'product'); }); done(null, models); } @@ -172,14 +175,14 @@ describe('Discover model primary keys', function () { }); }); -describe('Discover model foreign keys', function () { - it('should return an array of foreign keys for INVENTORY', function (done) { - db.discoverForeignKeys('INVENTORY', function (err, 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) { + models.forEach(function(m) { // console.dir(m); assert(m.fkTableName === 'INVENTORY'); }); @@ -187,13 +190,13 @@ describe('Discover model foreign keys', function () { } }); }); - it('should return an array of foreign keys for STRONGLOOP.INVENTORY', function (done) { - db.discoverForeignKeys('INVENTORY', {owner: 'STRONGLOOP'}, function (err, 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) { + models.forEach(function(m) { // console.dir(m); assert(m.fkTableName === 'INVENTORY'); }); @@ -203,51 +206,203 @@ describe('Discover model foreign keys', function () { }); }); -describe('Discover LDL schema from a table', function () { - it('should return an LDL schema for INVENTORY', function (done) { - db.discoverSchema('INVENTORY', {owner: 'STRONGLOOP'}, function (err, schema) { - // console.log('%j', schema); - assert(schema.name === 'Inventory'); - assert(schema.options.mysql.schema === 'STRONGLOOP'); - assert(schema.options.mysql.table === 'INVENTORY'); - assert(schema.properties.productId); - assert(schema.properties.productId.required); - assert(schema.properties.productId.type === 'String'); - assert(schema.properties.productId.mysql.columnName === 'PRODUCT_ID'); - assert(schema.properties.locationId); - assert(schema.properties.locationId.type === 'String'); - assert(schema.properties.locationId.mysql.columnName === 'LOCATION_ID'); - assert(schema.properties.available); - assert(schema.properties.available.required === false); - assert(schema.properties.available.type === 'Number'); - assert(schema.properties.total); - assert(schema.properties.total.type === 'Number'); - done(null, schema); +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 () { - it('should discover and build models', function (done) { - db.discoverAndBuildModels('INVENTORY', {owner: 'STRONGLOOP', visited: {}, associations: true}, function (err, models) { - assert(models.Inventory, 'Inventory model should be discovered and built'); - var schema = models.Inventory.definition; - assert(schema.settings.mysql.schema === 'STRONGLOOP'); - assert(schema.settings.mysql.table === 'INVENTORY'); - assert(schema.properties.productId); - assert(schema.properties.productId.type === String); - assert(schema.properties.productId.mysql.columnName === 'PRODUCT_ID'); - assert(schema.properties.locationId); - assert(schema.properties.locationId.type === String); - assert(schema.properties.locationId.mysql.columnName === 'LOCATION_ID'); - assert(schema.properties.available); - assert(schema.properties.available.type === Number); - assert(schema.properties.total); - assert(schema.properties.total.type === Number); - models.Inventory.findOne(function (err, inv) { - assert(!err, 'error should not be reported'); - done(); +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 index bd521167..1034a60a 100644 --- a/test/mysql.test.js +++ b/test/mysql.test.js @@ -1,3 +1,9 @@ +// 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; @@ -15,51 +21,51 @@ ObjectID.prototype.toJSON = function() { return this.id1 + this.id2; }; -describe('mysql', function () { - - before(function (done) { +describe('mysql', function() { + before(function(done) { db = getDataSource(); Post = db.define('PostWithDefaultId', { - title: { type: String, length: 255, index: true }, - content: { type: String }, + title: {type: String, length: 255, index: true}, + content: {type: String}, comments: [String], history: Object, stars: Number, - userId: ObjectID + userId: ObjectID, + }, { + forceId: false, }); PostWithStringId = db.define('PostWithStringId', { id: {type: String, id: true}, - title: { type: String, length: 255, index: true }, - content: { type: String } + 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 } + title: {type: String, length: 255, index: {unique: true}}, + content: {type: String}, }); - db.automigrate(['PostWithDefaultId', 'PostWithStringId', 'PostWithUniqueTitle'], function (err) { + 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 () { + beforeEach(function(done) { + Post.destroyAll(function() { + PostWithStringId.destroyAll(function() { + PostWithUniqueTitle.destroyAll(function() { done(); }); }); }); }); - it('should allow array or object', 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) { @@ -79,7 +85,6 @@ describe('mysql', function () { 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) { @@ -93,15 +98,15 @@ describe('mysql', function () { }); }); - it('updateOrCreate should update the instance', function (done) { - Post.create({title: 'a', content: 'AAA'}, function (err, post) { + 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) { + 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) { + Post.findById(post.id, function(err, p) { p.id.should.be.equal(post.id); p.content.should.be.equal(post.content); @@ -110,19 +115,18 @@ describe('mysql', function () { done(); }); }); - }); }); - it('updateOrCreate should update the instance without removing existing properties', function (done) { - Post.create({title: 'a', content: 'AAA'}, function (err, post) { + 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) { + 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) { + Post.findById(post.id, function(err, p) { p.id.should.be.equal(post.id); p.content.should.be.equal(post.content); @@ -131,19 +135,18 @@ describe('mysql', function () { done(); }); }); - }); }); - it('updateOrCreate should create a new instance if it does not exist', function (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) { + 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) { + Post.findById(p.id, function(err, p) { p.id.should.be.equal(post.id); p.content.should.be.equal(post.content); @@ -153,18 +156,89 @@ describe('mysql', function () { 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) { + 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) { + 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) { + Post.findById(post.id, function(err, p) { p.id.should.be.equal(post.id); p.content.should.be.equal(post.content); @@ -173,19 +247,18 @@ describe('mysql', function () { done(); }); }); - }); }); - it('save should update the instance without removing existing properties', function (done) { - Post.create({title: 'a', content: 'AAA'}, function (err, post) { + 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) { + 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) { + Post.findById(post.id, function(err, p) { p.id.should.be.equal(post.id); p.content.should.be.equal(post.content); @@ -194,19 +267,18 @@ describe('mysql', function () { done(); }); }); - }); }); - it('save should create a new instance if it does not exist', function (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) { + 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) { + Post.findById(p.id, function(err, p) { should.not.exist(err); p.id.should.be.equal(post.id); @@ -217,13 +289,12 @@ describe('mysql', function () { 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) { + 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]; @@ -233,25 +304,24 @@ describe('mysql', function () { 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) { + 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) { + 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) { + 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'); @@ -263,9 +333,9 @@ describe('mysql', function () { }); }); - 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) { + 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(); @@ -273,9 +343,9 @@ describe('mysql', function () { }); }); - 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) { + 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(); @@ -283,9 +353,9 @@ describe('mysql', function () { }); }); - 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) { + 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(); @@ -293,9 +363,9 @@ describe('mysql', function () { }); }); - 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) { + 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(); @@ -303,12 +373,12 @@ describe('mysql', function () { }); }); - it('should support "and" operator that is satisfied', function (done) { - Post.create({title: 'My Post', content: 'Hello'}, function (err, post) { + 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) { + {content: 'Hello'}, + ]}}, function(err, posts) { should.not.exist(err); posts.should.have.property('length', 1); done(); @@ -316,12 +386,12 @@ describe('mysql', function () { }); }); - it('should support "and" operator that is not satisfied', function (done) { - Post.create({title: 'My Post', content: 'Hello'}, function (err, post) { + 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) { + {content: 'Hello1'}, + ]}}, function(err, posts) { should.not.exist(err); posts.should.have.property('length', 0); done(); @@ -329,12 +399,12 @@ describe('mysql', function () { }); }); - it('should support "or" that is satisfied', function (done) { - Post.create({title: 'My Post', content: 'Hello'}, function (err, post) { + 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) { + {content: 'Hello1'}, + ]}}, function(err, posts) { should.not.exist(err); posts.should.have.property('length', 1); done(); @@ -342,12 +412,12 @@ describe('mysql', function () { }); }); - it('should support "or" operator that is not satisfied', function (done) { - Post.create({title: 'My Post', content: 'Hello'}, function (err, post) { + 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) { + {content: 'Hello1'}, + ]}}, function(err, posts) { should.not.exist(err); posts.should.have.property('length', 0); done(); @@ -356,18 +426,18 @@ describe('mysql', function () { }); // 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) { + 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) { + {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) { + {content: 'Hello'}, + ]}, function(err, count) { should.not.exist(err); count.should.be.equal(0); done(); @@ -377,15 +447,15 @@ describe('mysql', function () { }); // 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) { + 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) { + {content: 'Hello'}, + ]}, function(err) { should.not.exist(err); - Post.count(function (err, count) { + Post.count(function(err, count) { should.not.exist(err); count.should.be.equal(1); done(); @@ -395,13 +465,13 @@ describe('mysql', function () { }); }); - it('should not allow SQL injection for inq operator', function (done) { + it('should not allow SQL injection for inq operator', function(done) { Post.create({title: 'My Post1', content: 'Hello', stars: 5}, - function (err, post) { + function(err, post) { Post.create({title: 'My Post2', content: 'Hello', stars: 20}, - function (err, post) { + function(err, post) { Post.find({where: {title: {inq: ['SELECT title from PostWithDefaultId']}}}, - function (err, posts) { + function(err, posts) { should.not.exist(err); posts.should.have.property('length', 0); done(); @@ -410,13 +480,13 @@ describe('mysql', function () { }); }); - it('should not allow SQL injection for lt operator', function (done) { + it('should not allow SQL injection for lt operator', function(done) { Post.create({title: 'My Post1', content: 'Hello', stars: 5}, - function (err, post) { + function(err, post) { Post.create({title: 'My Post2', content: 'Hello', stars: 20}, - function (err, post) { + function(err, post) { Post.find({where: {stars: {lt: 'SELECT title from PostWithDefaultId'}}}, - function (err, posts) { + function(err, posts) { should.not.exist(err); posts.should.have.property('length', 0); done(); @@ -425,13 +495,13 @@ describe('mysql', function () { }); }); - it('should not allow SQL injection for nin operator', function (done) { + it('should not allow SQL injection for nin operator', function(done) { Post.create({title: 'My Post1', content: 'Hello', stars: 5}, - function (err, post) { + function(err, post) { Post.create({title: 'My Post2', content: 'Hello', stars: 20}, - function (err, post) { + function(err, post) { Post.find({where: {title: {nin: ['SELECT title from PostWithDefaultId']}}}, - function (err, posts) { + function(err, posts) { should.not.exist(err); posts.should.have.property('length', 2); done(); @@ -440,14 +510,13 @@ describe('mysql', function () { }); }); - - it('should not allow SQL injection for inq operator with number column', function (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) { + function(err, post) { Post.create({title: 'My Post2', content: 'Hello', stars: 20}, - function (err, post) { + function(err, post) { Post.find({where: {stars: {inq: ['SELECT title from PostWithDefaultId']}}}, - function (err, posts) { + function(err, posts) { should.not.exist(err); posts.should.have.property('length', 0); done(); @@ -456,13 +525,13 @@ describe('mysql', function () { }); }); - it('should not allow SQL injection for inq operator with array value', function (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) { + function(err, post) { Post.create({title: 'My Post2', content: 'Hello', stars: 20}, - function (err, post) { + function(err, post) { Post.find({where: {stars: {inq: [5, 'SELECT title from PostWithDefaultId']}}}, - function (err, posts) { + function(err, posts) { should.not.exist(err); posts.should.have.property('length', 1); done(); @@ -471,13 +540,13 @@ describe('mysql', function () { }); }); - it('should not allow SQL injection for between operator', function (done) { + it('should not allow SQL injection for between operator', function(done) { Post.create({title: 'My Post1', content: 'Hello', stars: 5}, - function (err, post) { + function(err, post) { Post.create({title: 'My Post2', content: 'Hello', stars: 20}, - function (err, post) { + function(err, post) { Post.find({where: {stars: {between: [5, 'SELECT title from PostWithDefaultId']}}}, - function (err, posts) { + function(err, posts) { should.not.exist(err); posts.should.have.property('length', 0); done(); @@ -486,11 +555,11 @@ describe('mysql', function () { }); }); - it('should not allow duplicate titles', function (done) { + it('should not allow duplicate titles', function(done) { var data = {title: 'a', content: 'AAA'}; - PostWithUniqueTitle.create(data, function (err, post) { + PostWithUniqueTitle.create(data, function(err, post) { should.not.exist(err); - PostWithUniqueTitle.create(data, function (err, post) { + PostWithUniqueTitle.create(data, function(err, post) { should.exist(err); done(); }); @@ -504,7 +573,7 @@ describe('mysql', function () { beforeEach(function createTestFixtures(done) { Post.create([ {title: 'a', content: 'AAA'}, - {title: 'b', content: 'BBB'} + {title: 'b', content: 'BBB'}, ], done); }); after(function deleteTestFixtures(done) { @@ -542,27 +611,27 @@ describe('mysql', function () { 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(); - }); - }); + 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(); - }); - }); + 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(); - }); - }); + Post.find({where: {content: {regexp: '^a/m'}}}, function(err, posts) { + console.warn.calledOnce.should.be.ok; + done(); + }); + }); }); }); @@ -597,27 +666,27 @@ describe('mysql', function () { 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(); - }); - }); + 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(); - }); - }); + 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(); - }); - }); + Post.find({where: {content: {regexp: /^a/m}}}, function(err, posts) { + console.warn.calledOnce.should.be.ok; + done(); + }); + }); }); }); @@ -633,11 +702,11 @@ describe('mysql', 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(); - }); + should.not.exist(err); + posts.length.should.equal(1); + posts[0].content.should.equal('AAA'); + done(); + }); }); }); @@ -645,45 +714,45 @@ describe('mysql', 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(); - }); + 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)}}}, + Post.find({where: {content: {regexp: new RegExp(/^a/i)}}}, function(err, posts) { - console.warn.calledOnce.should.be.ok; - done(); - }); - }); + 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)}}}, + Post.find({where: {content: {regexp: new RegExp(/^a/g)}}}, function(err, posts) { - console.warn.calledOnce.should.be.ok; - done(); - }); - }); + 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)}}}, + Post.find({where: {content: {regexp: new RegExp(/^a/m)}}}, function(err, posts) { - console.warn.calledOnce.should.be.ok; - done(); - }); - }); + console.warn.calledOnce.should.be.ok; + done(); + }); + }); }); }); }); - after(function (done) { - Post.destroyAll(function () { - PostWithStringId.destroyAll(function () { + 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 index 26fe81ef..ddcb28be 100644 --- a/test/persistence-hooks.test.js +++ b/test/persistence-hooks.test.js @@ -1,4 +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); +suite(global.getDataSource(), should, { + replaceOrCreateReportsNewInstance: true, +}); diff --git a/test/schema.sql b/test/schema.sql new file mode 100644 index 00000000..3f10afbc --- /dev/null +++ b/test/schema.sql @@ -0,0 +1,225 @@ +-- 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; + +-- +-- 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 index 0f169277..127cbe4d 100644 --- a/test/transaction.promise.test.js +++ b/test/transaction.promise.test.js @@ -1,3 +1,9 @@ +// 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'); } @@ -8,17 +14,16 @@ 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} + content: {type: String}, }, {mysql: {engine: 'INNODB'}}); Review = db.define('ReviewTX', { author: String, - content: {type: String} + content: {type: String}, }, {mysql: {engine: 'INNODB'}}); Post.hasMany(Review, {as: 'reviews', foreignKey: 'postId'}); db.automigrate(['PostTX', 'ReviewTX'], done); @@ -33,7 +38,7 @@ describe('transactions with promise', function() { // Transaction.begin(db.connector, Transaction.READ_COMMITTED, var promise = Post.beginTransaction({ isolationLevel: Transaction.READ_COMMITTED, - timeout: timeout + timeout: timeout, }); promise.then(function(tx) { (typeof tx.id).should.be.eql('string'); @@ -60,7 +65,7 @@ describe('transactions with promise', function() { function(p) { p.reviews.create({ author: 'John', - content: 'Review for ' + p.title + content: 'Review for ' + p.title, }, {transaction: currentTx}).then( function(c) { done(null, c); @@ -97,7 +102,6 @@ describe('transactions with promise', function() { } describe('commit', function() { - var post = {title: 't1', content: 'c1'}; before(createPostInTx(post)); @@ -124,7 +128,6 @@ describe('transactions with promise', function() { }); describe('rollback', function() { - var post = {title: 't2', content: 'c2'}; before(createPostInTx(post)); @@ -151,7 +154,6 @@ describe('transactions with promise', function() { }); describe('timeout', function() { - var post = {title: 't3', content: 'c3'}; before(createPostInTx(post, 500)); @@ -171,7 +173,5 @@ describe('transactions with promise', function() { done(); }); }); - }); }); - diff --git a/test/transaction.test.js b/test/transaction.test.js index a1667a40..b4f9d9b7 100644 --- a/test/transaction.test.js +++ b/test/transaction.test.js @@ -1,3 +1,9 @@ +// 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'); @@ -5,17 +11,16 @@ 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} + content: {type: String}, }, {mysql: {engine: 'INNODB'}}); Review = db.define('ReviewTX', { author: String, - content: {type: String} + content: {type: String}, }, {mysql: {engine: 'INNODB'}}); Post.hasMany(Review, {as: 'reviews', foreignKey: 'postId'}); db.automigrate(['PostTX', 'ReviewTX'], done); @@ -29,9 +34,9 @@ describe('transactions', function() { return function(done) { // Transaction.begin(db.connector, Transaction.READ_COMMITTED, Post.beginTransaction({ - isolationLevel: Transaction.READ_COMMITTED, - timeout: timeout - }, + isolationLevel: Transaction.READ_COMMITTED, + timeout: timeout, + }, function(err, tx) { if (err) return done(err); (typeof tx.id).should.be.eql('string'); @@ -59,9 +64,9 @@ describe('transactions', function() { done(err); } else { p.reviews.create({ - author: 'John', - content: 'Review for ' + p.title - }, {transaction: tx}, + author: 'John', + content: 'Review for ' + p.title, + }, {transaction: tx}, function(err, c) { done(err); }); @@ -100,7 +105,6 @@ describe('transactions', function() { } describe('commit', function() { - var post = {title: 't1', content: 'c1'}; before(createPostInTx(post)); @@ -127,7 +131,6 @@ describe('transactions', function() { }); describe('rollback', function() { - var post = {title: 't2', content: 'c2'}; before(createPostInTx(post)); @@ -154,7 +157,6 @@ describe('transactions', function() { }); describe('timeout', function() { - var post = {title: 't3', content: 'c3'}; before(createPostInTx(post, 500)); @@ -174,7 +176,5 @@ describe('transactions', function() { done(); }); }); - }); }); -