diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..7cd53fd --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/node_modules/ diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..bf8f245 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,11 @@ +{ + "extends": "loopback", + "rules": { + "max-len": ["error", 140, 4, { + "ignoreComments": true, + "ignoreUrls": true, + "ignorePattern": "^\\s*var\\s.+=\\s*(require\\s*\\()|(/)" + }], + "no-unused-expressions": "off" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d9d09b --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +lib-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz + +pids +logs +results + +*.sublime* +node_modules/ +db/ + +npm-debug.log + diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..f72c785 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,447 @@ +2020-03-10, Version 3.8.0 +========================= + + * Exclude `deps` and `.github` from npm publish (Raymond Feng) + + * Remove obsolete npm script to delete regenerator (Raymond Feng) + + * chore: update CODEOWNERS (Diana Lau) + + * chore: update copyrights year (Diana Lau) + + * fix problem setting passwords with chars like '(' (Francisco Buceta) + + +2020-01-22, Version 3.7.0 +========================= + + * add tests for limit&limit with supportsOffsetFetch (Francisco Buceta) + + * add missing 'ORDER BY' before 'OFFSET' (Rifa Achrinza) + + * chore: improve issue and PR templates (Nora) + + +2019-11-14, Version 3.6.0 +========================= + + * update mssql to 6.x (Joost de Bruijn) + + +2019-11-06, Version 3.5.0 +========================= + + * fix linter errors (Joost de Bruijn) + + * update mssql 5.x and mark sqlcmdjs devDependency (Joost de Bruijn) + + +2019-09-19, Version 3.4.0 +========================= + + * add juggler-v3 and v4 to dev dependencies (Nora) + + * run shared tests from both v3 and v4 of juggler (Nora) + + * fix eslint violations (Nora) + + * update dependencies (Nora) + + * update CODEOWNERS (Nora) + + * drop support for node.js 6 (Nora) + + +2018-08-07, Version 3.3.0 +========================= + + * update to MIT license (Diana Lau) + + * chore: update node + dependencies (virkt25) + + * Increase test limit from 9.9 to 30 seconds (Miroslav Bajtoš) + + * Move Mocha config to `test/mocha.opts` (Miroslav Bajtoš) + + * [WebFM] cs/pl/ru translation (candytangnb) + + +2018-05-04, Version 3.2.1 +========================= + + * chore: update CODEOWNERS (Diana Lau) + + * Update Queries to Uppercase (Rafael E. Ajuria) + + +2017-11-29, Version 3.2.0 +========================= + + * Implement test for single column indices (Daan Middendorp) + + * Fix single column index altering (Daan Middendorp) + + * Fix deletion of indices and test (Daan Middendorp) + + * Solve index naming issue (Daan Middendorp) + + * Test update indexes (Daan Middendorp) + + * Fix altering of indexes (Daan Middendorp) + + * Solved imported unittests for previous commit (Daan Middendorp) + + * Fix showIndexes function (Daan Middendorp) + + * Fix bigint (jannyHou) + + +2017-10-05, Version 3.1.0 +========================= + + * update globalize string (Diana Lau) + + * Add stalebot configuration (Kevin Delisle) + + * Create Issue and PR Templates (#167) (Sakib Hasan) + + * Add CODEOWNER file (Diana Lau) + + * Require init on mocha args (ssh24) + + * Add docker setup (#148) (Sakib Hasan) + + * Return if column is generated or not (#100) (Christiaan Westerbeek) + + * Fix discovery of primary keys (#99) (Christiaan Westerbeek) + + * Remove appveyor service (#145) (Sakib Hasan) + + * Fix params for isActual function (#143) (Nguyễn Kim Kha) + + * Enable clean DB seed as pre-test (#142) (Sakib Hasan) + + * Fix eslint issues & buildPropertyType() signature (Raymond Feng) + + * Upgrade deps to mssql@4.x (Raymond Feng) + + * add appveyor for CI (#137) (Ryan Graham) + + * fix discovery turkish collation (#123) (emrahcetiner) + + +2017-03-31, Version 3.0.0 +========================= + + * Replicate issue_template from loopback repo (#120) (siddhipai) + + * Refactor alter table (#134) (Diana Lau) + + * Upgrade to loopback-connector@4.x (Loay) + + * Refactor migration methods (ssh24) + + * Refactor discovery methods (Loay Gewily) + + * Update mocha timeout (Loay Gewily) + + * Update LB-connector version (Loay) + + * Add buildreplace method (Loay Gewily) + + * Update README.md (#117) (Rand McKinney) + + * Update w info from docs (#115) (Rand McKinney) + + * Update paid support URL (Siddhi Pai) + + * increase the timeout for autoupdate test (Eddie Monge) + + * Start 3.x + drop support for Node v0.10/v0.12 (siddhipai) + + * Drop support for Node v0.10 and v0.12 (Siddhi Pai) + + * Start the development of the next major version (Siddhi Pai) + + * Update README doc links (Candy) + + +2016-10-14, Version 2.9.0 +========================= + + * Add connectorCapabilities global object (#102) (Nicholas Duffy) + + * Update translation files - round#2 (Candy) + + * Add translated files (gunjpan) + + * Update deps to loopback 3.0.0 RC (Miroslav Bajtoš) + + * Update eslint infrastructure (Loay) + + * Use juggler@3 for running tests (Simon Ho) + + * Add globalization (Candy) + + * Revert "Update Fix" (Loay) + + * Update Fix (Loay) + + * Update URLs in CONTRIBUTING.md (#88) (Ryan Graham) + + +2016-06-21, Version 2.8.0 +========================= + + * update copyright notices and license (Ryan Graham) + + * Lazy connect when booting app (juehou) + + * Add feature/eslint (Amir-61) + + * Fix linting errors (Amir Jafarian) + + * Auto-update by eslint --fix (Amir Jafarian) + + * Add eslint infrastructure (Amir Jafarian) + + +2016-04-07, Version 2.7.1 +========================= + + * Keep float numbers (Raymond Feng) + + * override other settings if url provided (juehou) + + +2016-04-05, Version 2.7.0 +========================= + + * Use request.input to avoid SQL injection (Raymond Feng) + + +2016-03-15, Version 2.6.0 +========================= + + * Remove regenerator from babel-runtime and bundle mssql (Raymond Feng) + + +2016-03-10, Version 2.5.1 +========================= + + * Remove the license check (Raymond Feng) + + +2016-03-04, Version 2.5.0 +========================= + + + +2016-02-19, Version 2.4.1 +========================= + + * Remove sl-blip from dependencies (Miroslav Bajtoš) + + +2016-02-09, Version 2.4.0 +========================= + + * Refactor Fix for Insert into Table with Active Trigger by getting the column data type instead of varchar. https://github.com/strongloop/loopback-connector-mssql/issues/21 (FoysalOsmany) + + * Fix for Insert into Table with Active Trigger https://github.com/strongloop/loopback-connector-mssql/issues/21 (FoysalOsmany) + + * Upgrade should to 8.0.2 (Simon Ho) + + * Add help for Azure SQL users (Oleksandr Sochka) + + +2015-11-27, Version 2.3.3 +========================= + + * Remove buildPartitionBy() that became redundant (eugene-frb) + + * Updated option that triggers PARTITION BY injection, fixed buildPartitionByFirst's 'where' argument. (eugene-frb) + + +2015-11-18, Version 2.3.2 +========================= + + * Inject Partition By clause into buildColumnNames of SQL query for include filter (eugene-frb) + + * Refer to licenses with a link (Sam Roberts) + + * Use strongloop conventions for licensing (Sam Roberts) + + +2015-09-11, Version 2.3.1 +========================= + + * Allow models without PK (Raymond Feng) + + +2015-08-14, Version 2.3.0 +========================= + + * Added support to unicode (Ahmed Abdul Moniem) + + +2015-08-13, Version 2.2.1 +========================= + + * Allow the `multipleResultSets` flag for execute (Raymond Feng) + + +2015-07-29, Version 2.2.0 +========================= + + * Add support for regex operator (Simon Ho) + + +2015-05-18, Version 2.1.0 +========================= + + * Update deps (Raymond Feng) + + * Add transaction support (Raymond Feng) + + +2015-05-13, Version 2.0.0 +========================= + + * Update deps (Raymond Feng) + + * Refactor the mssql connector to use base SqlConnector (Raymond Feng) + + * Use SET IDENTITY_INSERT option to allow explicit id (Raymond Feng) + + * Return count when updating or deleting models (Simon Ho) + + * Add strongloop license check (Raymond Feng) + + * Add "Running tests" section to readme (Simon Ho) + + +2015-03-02, Version 1.5.1 +========================= + + * Test if the id is generated (Raymond Feng) + + * add test case for id manipulation (Ido Shamun) + + * Add support for custom column mapping in primary key column Add support for idInjection (Ido Shamun) + + +2015-02-20, Version 1.5.0 +========================= + + * Add support for custom column mapping (Raymond Feng) + + +2015-01-27, Version 1.4.0 +========================= + + * Fix the empty column list (Raymond Feng) + + * Increase the limit to make sure other owners are selected (Raymond Feng) + + * Enhance id to pk mapping (Raymond Feng) + + * Fix: empty inq/nin function correctly (bitmage) + + +2015-01-09, Version 1.3.0 +========================= + + * Fix SQL injection (Raymond Feng) + + * Fix bad CLA URL in CONTRIBUTING.md (Ryan Graham) + + +2014-12-08, Version 1.2.0 +========================= + + * Update test dep (Raymond Feng) + + * Fix the missing var (Raymond Feng) + + * fixed race condition causing incorrect IDs to be reported on INSERT (bitmage) + + * handle precision and scale (bitmage) + + +2014-12-05, Version 1.1.6 +========================= + + * Update deps (Raymond Feng) + + * Map required/id properties to NOT NULL (Raymond Feng) + + +2014-11-27, Version 1.1.5 +========================= + + * Update README.md (Rand McKinney) + + * Add contribution guidelines (Ryan Graham) + + +2014-09-11, Version 1.1.4 +========================= + + * Bump version (Raymond Feng) + + * Bump versions (Raymond Feng) + + * Make sure errors are reported for automigrate/autoupdate (Raymond Feng) + + +2014-08-25, Version 1.1.3 +========================= + + * Bump version (Raymond Feng) + + * Remove ON[PRIMARY] option (Raymond Feng) + + +2014-08-20, Version 1.1.2 +========================= + + * Bump version (Raymond Feng) + + * Add ping() (Raymond Feng) + + +2014-06-27, Version 1.1.1 +========================= + + * Bump versions (Raymond Feng) + + * Tidy up filter.order parsing (Raymond Feng) + + * Update link to doc (Rand McKinney) + + * Bump version (Raymond Feng) + + +2014-06-23, Version 1.1.0 +========================= + + * Use base connector and add update support (Raymond Feng) + + * Fix comparison for null/boolean values (Raymond Feng) + + * Updated to allow global replacement (Jason Douglas) + + * Update mssql.js to properly escape ' chars (Jason Douglas) + + * Remove 'module deps' from JSDocs (Rand McKinney) + + * Replace old README with link to docs and basic info. (Rand McKinney) + + * Create docs.json (Rand McKinney) + + +2014-05-16, Version 1.0.1 +========================= + + * First release! diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..2e0d143 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,9 @@ +# Lines starting with '#' are comments. +# Each line is a file pattern followed by one or more owners, +# the last matching pattern has the most precendence. + +# Alumni members +# @kjdelisle @loay @ssh24 @virkt25 @b-admike + +# Core team members from IBM +* @jannyHou @dhmlau @emonddr diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0f79fa5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,178 @@ +# Code of Conduct + +LoopBack, as member project of the OpenJS Foundation, use +[Contributor Covenant v2.0](https://contributor-covenant.org/version/2/0/code_of_conduct) +as their code of conduct. The full text is included +[below](#contributor-covenant-code-of-conduct-v2.0) in English, and translations +are available from the Contributor Covenant organisation: + +- [contributor-covenant.org/translations](https://www.contributor-covenant.org/translations) +- [github.com/ContributorCovenant](https://github.com/ContributorCovenant/contributor_covenant/tree/release/content/version/2/0) + +Refer to the sections on reporting and escalation in this document for the +specific emails that can be used to report and escalate issues. + +## Reporting + +### Project Spaces + +For reporting issues in spaces related to LoopBack, please use the email +`tsc@loopback.io`. The LoopBack Technical Steering Committee (TSC) handles CoC issues related to the spaces that it +maintains. The project TSC commits to: + +- maintain the confidentiality with regard to the reporter of an incident +- to participate in the path for escalation as outlined in the section on + Escalation when required. + +### Foundation Spaces + +For reporting issues in spaces managed by the OpenJS Foundation, for example, +repositories within the OpenJS organization, use the email +`report@lists.openjsf.org`. The Cross Project Council (CPC) is responsible for +managing these reports and commits to: + +- maintain the confidentiality with regard to the reporter of an incident +- to participate in the path for escalation as outlined in the section on + Escalation when required. + +## Escalation + +The OpenJS Foundation maintains a Code of Conduct Panel (CoCP). This is a +foundation-wide team established to manage escalation when a reporter believes +that a report to a member project or the CPC has not been properly handled. In +order to escalate to the CoCP send an email to +`coc-escalation@lists.openjsf.org`. + +For more information, refer to the full +[Code of Conduct governance document](https://github.com/openjs-foundation/cross-project-council/blob/HEAD/CODE_OF_CONDUCT.md). + +--- + +## Contributor Covenant Code of Conduct v2.0 + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[tsc@loopback.io](mailto:tsc@loopback.io). All complaints will be reviewed and +investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8cc81cb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +### Contributing + +Thank you for your interest in `loopback-connector-mssql`, an open source project +administered by IBM. + +Contributing to `loopback-connector-mssql` is easy. In a few simple steps: + + * Ensure that your effort is aligned with the project's roadmap by + talking to the maintainers, especially if you are going to spend a + lot of time on it. + + * Make something better or fix a bug. + + * Adhere to code style outlined in the [Google C++ Style Guide][] and + [Google Javascript Style Guide][]. + + * [Sign](https://loopback.io/doc/en/contrib/code-contrib.html) all commits with DCO. + + * Submit a pull request through Github. + +### Developer Certificate of Origin + +This project uses [DCO](https://developercertificate.org/). Be sure to sign off +your commits using the `-s` flag or adding `Signed-off-By: Name` in the +commit message. + +**Example** + +``` +git commit -s -m "feat: my commit message" +``` + +Also see the [Contributing to LoopBack](https://loopback.io/doc/en/contrib/code-contrib.html) to get you started. + + +[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.md b/LICENSE.md new file mode 100644 index 0000000..55807f9 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2012,2018. All Rights Reserved. +Node module: loopback-connector-mssql +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. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..3a5202d --- /dev/null +++ b/NOTICE @@ -0,0 +1,22 @@ +The project contains code from [https://github.com/code-vicar/jugglingdb-mssql](https://github.com/code-vicar/jugglingdb-mssql) +under the MIT license: + +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/README.md b/README.md new file mode 100644 index 0000000..cf5a89f --- /dev/null +++ b/README.md @@ -0,0 +1,384 @@ +# loopback-connector-mssql + +[Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server/default.aspx) is a relational database management system developed by Microsoft. +The `loopback-connector-mssql` module is the Microsoft SQL Server connector for the LoopBack framework. + +
+For more information, see LoopBack documentation. +
+ +## Installation + +In your application root directory, enter: + +```shell +$ npm install loopback-connector-mssql --save +``` + +This will install the module from npm and add it as a dependency to the application's `package.json` file. + +If you create a SQL Server 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 SQL Server data source + +Use the [Data source generator](http://loopback.io/doc/en/lb3/Data-source-generator.html) to add a SQL Server data source to your application. +The generator will prompt for the database server hostname, port, and other settings +required to connect to a SQL Server 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 (for example): + +{% include code-caption.html content="/server/datasources.json" %} +```javascript +"sqlserverdb": { + "name": "sqlserverdb", + "connector": "mssql", + "host": "myhost", + "port": 1234, + "url": "mssql://username:password@dbhost/dbname", + "database": "mydb", + "password": "admin", + "user": "admin", + } +``` + +Edit `datasources.json` to add other properties that enable you to connect the data source to a SQL Server database. + +To connect to a SQL Server instance running in Azure, you must specify a qualified user name with hostname, and add the following to the data source declaration: + +```js +"options": { + "encrypt": true + ... +} +``` + +### Connector settings + +To configure the data source to use your MS SQL Server database, edit `datasources.json` and add the following settings as appropriate. +The MSSQL connector uses [node-mssql](https://github.com/patriksimek/node-mssql) as the driver. For more information about configuration parameters, +see [node-mssql documentation](https://github.com/patriksimek/node-mssql#configuration-1). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyTypeDefaultDescription
connectorString + Either "loopback-connector-mssql" or "mssql" +
databaseString Database name
debugBoolean If true, turn on verbose mode to debug database queries and lifecycle.
hostStringlocalhostDatabase host name
passwordString Password to connect to database
portNumber1433Database TCP port
schemaStringdboDatabase schema
urlString Use instead of the host, port, user, password, + and database properties. For example: 'mssql://test:mypassword@localhost:1433/dev'. +
userString Qualified username with host name, for example "user@your.sqlserver.dns.host".
+ +Instead of specifying individual connection properties, you can use a single `url` property that combines them into a single string, for example: + +```javascript +"accountDB": { + "url": "mssql://test:mypassword@localhost:1433/demo?schema=dbo" +} +``` + +The application will automatically load the data source when it starts. You can then refer to it in code, for example: + +{% include code-caption.html content="/server/boot/script.js" %} +```javascript +var app = require('./app'); +var dataSource = app.dataSources.accountDB; +``` + +Alternatively, you can create the data source in application code; for example: + +{% include code-caption.html content="/server/script.js" %} +```javascript +var DataSource = require('loopback-datasource-juggler').DataSource; +var dataSource = new DataSource('mssql', config); +config = { ... }; // JSON object as specified above in "Connector settings" +``` + + +### Model discovery + +The SQL Server 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 SQL Server 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 each model, the LoopBack SQL Server connector creates a table in the 'dbo' schema in the database. + +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 by calling delete on models with relationships. + +## Defining models + +The model definition consists of the following properties: + +* `name`: Name of the model, by default, the table name in camel-case. +* `options`: Model-level operations and mapping to Microsoft SQL Server schema/table. Use the `mssql` model property to specify additional SQL Server-specific properties for a LoopBack model. +* `properties`: Property definitions, including mapping to Microsoft SQL Server columns. + - For each property, use the `mssql` key to specify additional settings for that property/field. + +For example: + +{% include code-caption.html content="/common/models/inventory.json" %} +```javascript +{"name": "Inventory",  + "options": { + "idInjection": false, + "mssql": { + "schema": "strongloop", + "table": "inventory" + } + }, "properties": { + "id": { + "type": "String", + "required": false, + "length": 64, + "precision": null, + "scale": null, + "mssql": { + "columnName": "id", + "dataType": "varchar", + "dataLength": 64, + "dataPrecision": null, + "dataScale": null, + "nullable": "NO" + } + }, + "productId": { + "type": "String", + "required": false, + "length": 64, + "precision": null, + "scale": null, + "id": 1, + "mssql": { + "columnName": "product_id", + "dataType": "varchar", + "dataLength": 64, + "dataPrecision": null, + "dataScale": null, + "nullable": "YES" + } + }, + "locationId": { + "type": "String", + "required": false, + "length": 64, + "precision": null, + "scale": null, + "id": 1, + "mssql": { + "columnName": "location_id", + "dataType": "varchar", + "dataLength": 64, + "dataPrecision": null, + "dataScale": null, + "nullable": "YES" + } + }, + "available": { + "type": "Number", + "required": false, + "length": null, + "precision": 10, + "scale": 0, + "mssql": { + "columnName": "available", + "dataType": "int", + "dataLength": null, + "dataPrecision": 10, + "dataScale": 0, + "nullable": "YES" + } + }, + "total": { + "type": "Number", + "required": false, + "length": null, + "precision": 10, + "scale": 0, + "mssql": { + "columnName": "total", + "dataType": "int", + "dataLength": null, + "dataPrecision": 10, + "dataScale": 0, + "nullable": "YES" + } + } + }} +``` + +## Type mapping + +See [LoopBack types](http://loopback.io/doc/en/lb3/LoopBack-types.html) for details on LoopBack's data types. + +### LoopBack to SQL Server types + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LoopBack TypeSQL Server Type
BooleanBIT
DateDATETIME
GeoPointFLOAT
NumberINT
+ String + JSON + + NVARCHAR +
+ +### SQL Server to LoopBack types + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SQL Server TypeLoopBack Type
BITBoolean
+ BINARY
VARBINARY
IMAGE +
Node.js Buffer object
+ DATE
DATETIMEOFFSET
DATETIME2
SMALLDATETIME
DATETIME
TIME +
Date
POINTGeoPoint
+ BIGINT
NUMERIC
SMALLINT
DECIMAL
SMALLMONEY
INT
TINYINT
MONEY
FLOAT
REAL +
Number
+ CHAR
VARCHAR
TEXT
NCHAR
NVARCHAR
NTEXT
CHARACTER VARYING
CHARACTER +
String
+ +## Running tests + +### Own instance +If you have a local or remote MSSQL instance and would like to use that to run the test suite, use the following command: +- Linux +```bash +MSSQL_HOST= MSSQL_PORT= MSSQL_USER= MSSQL_PASSWORD= MSSQL_DATABASE= CI=true npm test +``` +- Windows +```bash +SET MSSQL_HOST= SET MSSQL_PORT= SET MSSQL_USER= SET MSSQL_PASSWORD= SET MSSQL_DATABASE= SET CI=true npm test +``` + +### Docker +If you do not have a local MSSQL instance, you can also run the test suite with very minimal requirements. +- Assuming you have [Docker](https://docs.docker.com/engine/installation/) installed, run the following script which would spawn a MSSQL instance on your local: +```bash +source setup.sh +``` +where ``, ``, ``, `` and `` are optional parameters. The default values are `localhost`, `1433`, `sa`, `M55sqlT35t` and `master` respectively. +- Run the test: +```bash +npm test +``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0961858 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Security advisories + +Security advisories can be found on the +[LoopBack website](https://loopback.io/doc/en/sec/index.html). + +## Reporting a vulnerability + +If you think you have discovered a new security issue with any LoopBack package, +**please do not report it on GitHub**. Instead, send an email to +[security@loopback.io](mailto:security@loopback.io) with the following details: + +- Full description of the vulnerability. +- Steps to reproduce the issue. +- Possible solutions. + +If you are sending us any logs as part of the report, then make sure to redact +any sensitive data from them. \ No newline at end of file diff --git a/deps/juggler-v3/package.json b/deps/juggler-v3/package.json new file mode 100644 index 0000000..a662daa --- /dev/null +++ b/deps/juggler-v3/package.json @@ -0,0 +1,8 @@ +{ + "name": "juggler-v3", + "version": "3.0.0", + "dependencies": { + "loopback-datasource-juggler":"3.x", + "should": "^13.2.3" + } +} diff --git a/deps/juggler-v3/test.js b/deps/juggler-v3/test.js new file mode 100644 index 0000000..778e621 --- /dev/null +++ b/deps/juggler-v3/test.js @@ -0,0 +1,24 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +const juggler = require('loopback-datasource-juggler'); +const name = require('./package.json').name; + +require('../../test/init'); + +describe(name, function() { + before(function() { + return global.resetDataSourceClass(juggler.DataSource); + }); + + after(function() { + return global.resetDataSourceClass(); + }); + + require('loopback-datasource-juggler/test/common.batch.js'); + require('loopback-datasource-juggler/test/include.test.js'); +}); diff --git a/deps/juggler-v4/package.json b/deps/juggler-v4/package.json new file mode 100644 index 0000000..e1d258f --- /dev/null +++ b/deps/juggler-v4/package.json @@ -0,0 +1,8 @@ +{ + "name": "juggler-v4", + "version": "4.0.0", + "dependencies": { + "loopback-datasource-juggler":"4.x", + "should": "^13.2.3" + } +} diff --git a/deps/juggler-v4/test.js b/deps/juggler-v4/test.js new file mode 100644 index 0000000..778e621 --- /dev/null +++ b/deps/juggler-v4/test.js @@ -0,0 +1,24 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +const juggler = require('loopback-datasource-juggler'); +const name = require('./package.json').name; + +require('../../test/init'); + +describe(name, function() { + before(function() { + return global.resetDataSourceClass(juggler.DataSource); + }); + + after(function() { + return global.resetDataSourceClass(); + }); + + require('loopback-datasource-juggler/test/common.batch.js'); + require('loopback-datasource-juggler/test/include.test.js'); +}); diff --git a/docs.json b/docs.json new file mode 100644 index 0000000..c0d1d67 --- /dev/null +++ b/docs.json @@ -0,0 +1,15 @@ +{ + "content": [ + { + "title": "LoopBack SQL Server Connector API", + "depth": 2 + }, + "lib/mssql.js", + { + "title": "SQL Server Discovery API", + "depth": 2 + }, + "lib/discovery.js" + ], + "codeSectionDepth": 3 +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..5e006a9 --- /dev/null +++ b/index.js @@ -0,0 +1,10 @@ +// Copyright IBM Corp. 2013,2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +const SG = require('strong-globalize'); +SG.SetRootDir(__dirname); + +module.exports = require('./lib/mssql.js'); diff --git a/intl/cs/messages.json b/intl/cs/messages.json new file mode 100644 index 0000000..31191b8 --- /dev/null +++ b/intl/cs/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} musí být {{object}}: {0}", + "80a32e80cbed65eba2103201a7c94710": "Model nebyl nalezen: {0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}} nepodporuje operátor regulárního výrazu", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} je povinný řetězcový argument: {0}" +} + diff --git a/intl/de/messages.json b/intl/de/messages.json new file mode 100644 index 0000000..79feca9 --- /dev/null +++ b/intl/de/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} muss ein {{object}} sein: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} ist ein erforderliches Zeichenfolgeargument: {0}", + "80a32e80cbed65eba2103201a7c94710": "Modell nicht gefunden: {0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}} unterstützt nicht den Operator für reguläre Ausdrücke" +} + diff --git a/intl/en/messages.json b/intl/en/messages.json new file mode 100644 index 0000000..5184e10 --- /dev/null +++ b/intl/en/messages.json @@ -0,0 +1,6 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} must be an {{object}}: {0}", + "80a32e80cbed65eba2103201a7c94710": "Model not found: {0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}} does not support the regular expression operator", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} is a required string argument: {0}" +} diff --git a/intl/es/messages.json b/intl/es/messages.json new file mode 100644 index 0000000..59a5950 --- /dev/null +++ b/intl/es/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} debe ser un {{object}}: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} es un argumento de serie necesario: {0}", + "80a32e80cbed65eba2103201a7c94710": "No se ha encontrado el modelo: {0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}} no admite el operador de expresión regular" +} + diff --git a/intl/fr/messages.json b/intl/fr/messages.json new file mode 100644 index 0000000..4cee4eb --- /dev/null +++ b/intl/fr/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} doit être un {{object}} : {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} est un argument de chaîne obligatoire : {0}", + "80a32e80cbed65eba2103201a7c94710": "Modèle introuvable : {0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}} ne prend pas en charge l'opérateur d'expression régulière" +} + diff --git a/intl/it/messages.json b/intl/it/messages.json new file mode 100644 index 0000000..4167b1f --- /dev/null +++ b/intl/it/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} deve essere un {{object}}: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} è un argomento stringa obbligatorio: {0}", + "80a32e80cbed65eba2103201a7c94710": "Modello non trovato: {0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}} non supporta l'operatore dell'espressione regolare" +} + diff --git a/intl/ja/messages.json b/intl/ja/messages.json new file mode 100644 index 0000000..f4a617c --- /dev/null +++ b/intl/ja/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} は {{object}} でなければなりません: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} は必須のストリング引数です: {0}", + "80a32e80cbed65eba2103201a7c94710": "モデルが見つかりません: {0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}} では正規表現演算子はサポートされません。" +} + diff --git a/intl/ko/messages.json b/intl/ko/messages.json new file mode 100644 index 0000000..767d737 --- /dev/null +++ b/intl/ko/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}}이(가) {{object}}이어야 함: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}}은 필수 문자열 인수임: {0}", + "80a32e80cbed65eba2103201a7c94710": "모델을 찾을 수 없음: {0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}}에서 정규식 연산자를 지원하지 않음" +} + diff --git a/intl/nl/messages.json b/intl/nl/messages.json new file mode 100644 index 0000000..90e4392 --- /dev/null +++ b/intl/nl/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} moet een {{object}} zijn: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} is een verplicht tekenreeksargument: {0}", + "80a32e80cbed65eba2103201a7c94710": "Model is niet gevonden: {0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}} biedt geen ondersteuning voor de expressieoperator" +} + diff --git a/intl/pl/messages.json b/intl/pl/messages.json new file mode 100644 index 0000000..f89e2e0 --- /dev/null +++ b/intl/pl/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "Właściwość {{options}} musi być obiektem {{object}}: {0}", + "80a32e80cbed65eba2103201a7c94710": "Nie znaleziono modelu: {0}", + "9dca910dae94ac66098b3992911e1b23": "Program {{Microsoft SQL Server}} nie obsługuje operatora wyrażenia regularnego", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} jest wymaganym argumentem łańcuchowym: {0}" +} + diff --git a/intl/pt/messages.json b/intl/pt/messages.json new file mode 100644 index 0000000..1915943 --- /dev/null +++ b/intl/pt/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} deve ser um {{object}}: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} é um argumento de sequência necessário: {0}", + "80a32e80cbed65eba2103201a7c94710": "Modelo não localizado: {0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}} não suporta o operador de expressão regular" +} + diff --git a/intl/ru/messages.json b/intl/ru/messages.json new file mode 100644 index 0000000..d418aac --- /dev/null +++ b/intl/ru/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} должны иметь тип {{object}}: {0}", + "80a32e80cbed65eba2103201a7c94710": "Модель не найдена: {0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}} не поддерживает оператор регулярного выражения", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} - это обязательный строковый аргумент: {0}" +} + diff --git a/intl/tr/messages.json b/intl/tr/messages.json new file mode 100644 index 0000000..e8d8b25 --- /dev/null +++ b/intl/tr/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} bir {{object}} olmalıdır: {0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} zorunlu bir dizgi bağımsız değişkeni: {0}", + "80a32e80cbed65eba2103201a7c94710": "Model bulunamadı: {0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}}, düzenli ifade işlecini desteklemiyor" +} + diff --git a/intl/zh-Hans/messages.json b/intl/zh-Hans/messages.json new file mode 100644 index 0000000..e15c7b5 --- /dev/null +++ b/intl/zh-Hans/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} 必须为 {{object}}:{0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} 是必需的字符串自变量:{0}", + "80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}} 不支持正则表达式运算符" +} + diff --git a/intl/zh-Hant/messages.json b/intl/zh-Hant/messages.json new file mode 100644 index 0000000..ecb9970 --- /dev/null +++ b/intl/zh-Hant/messages.json @@ -0,0 +1,7 @@ +{ + "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} 必須是 {{object}}:{0}", + "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} 是必要的字串引數:{0}", + "80a32e80cbed65eba2103201a7c94710": "找不到模型:{0}", + "9dca910dae94ac66098b3992911e1b23": "{{Microsoft SQL Server}} 不支援正規表示式運算子" +} + diff --git a/lib/discovery.js b/lib/discovery.js new file mode 100644 index 0000000..de42afc --- /dev/null +++ b/lib/discovery.js @@ -0,0 +1,405 @@ +// Copyright IBM Corp. 2014,2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +const g = require('strong-globalize')(); + +module.exports = mixinDiscovery; + +function mixinDiscovery(MsSQL) { + const async = require('async'); + + function paginateSQL2012(sql, orderBy, options) { + options = options || {}; + let offset = options.offset || options.skip; + if (isNaN(offset)) { + offset = 0; + } + let limit = options.limit; + if (isNaN(limit)) { + limit = -1; + } + if (offset === 0 && limit === -1) { + return sql; + } + + let fetch = ''; + if (options.offset || options.skip || options.limit) { + fetch = ' OFFSET ' + offset + ' ROWS'; // Offset starts from 0 + if (options.limit) { + fetch = fetch + ' FETCH NEXT ' + options.limit + 'ROWS ONLY'; + } + if (!orderBy) { + sql += ' ORDER BY 1'; + } + } + if (orderBy) { + sql += ' ORDER BY ' + orderBy; + } + return sql + fetch; + } + + MsSQL.prototype.paginateSQL = function(sql, orderBy, options) { + options = options || {}; + let offset = options.offset || options.skip; + if (isNaN(offset)) { + offset = 0; + } + let limit = options.limit; + if (isNaN(limit)) { + limit = -1; + } + if (offset === 0 && limit === -1) { + return sql; + } + const index = sql.indexOf(' FROM'); + const select = sql.substring(0, index); + const from = sql.substring(index); + if (orderBy) { + orderBy = 'ORDER BY ' + orderBy; + } else { + orderBy = 'ORDER BY 1'; + } + let paginatedSQL = 'SELECT *' + MsSQL.newline + + 'FROM (' + MsSQL.newline + + select + ', ROW_NUMBER() OVER' + + ' (' + orderBy + ') AS rowNum' + MsSQL.newline + + from + MsSQL.newline; + paginatedSQL += ') AS S' + MsSQL.newline + + 'WHERE S.rowNum > ' + offset; + + if (limit !== -1) { + paginatedSQL += ' AND S.rowNum <= ' + (offset + limit); + } + + return paginatedSQL + MsSQL.newline; + }; + + /*! + * Build sql for listing tables + * @param options {all: for all owners, owner: for a given owner} + * @returns {string} The sql statement + */ + MsSQL.prototype.buildQueryTables = function(options) { + let sqlTables = null; + const owner = options.owner || options.schema; + + if (options.all && !owner) { + sqlTables = this.paginateSQL('SELECT \'table\' AS "type", TABLE_NAME AS "name", TABLE_SCHEMA AS "owner"' + + ' FROM INFORMATION_SCHEMA.TABLES', 'TABLE_SCHEMA, TABLE_NAME', options); + } else if (owner) { + sqlTables = this.paginateSQL('SELECT \'table\' AS "type", TABLE_NAME AS "name", TABLE_SCHEMA AS "owner"' + + ' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=\'' + owner + '\'', 'TABLE_SCHEMA, TABLE_NAME', options); + } else { + sqlTables = this.paginateSQL('SELECT \'table\' AS "type", TABLE_NAME AS "name",' + + ' TABLE_SCHEMA AS "owner" FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=schema_name()', + 'TABLE_NAME', options); + } + return sqlTables; + }; + + /*! + * Build sql for listing views + * @param options {all: for all owners, owner: for a given owner} + * @returns {string} The sql statement + */ + MsSQL.prototype.buildQueryViews = function(options) { + let sqlViews = null; + if (options.views) { + const owner = options.owner || options.schema; + + if (options.all && !owner) { + sqlViews = this.paginateSQL('SELECT \'view\' AS "type", TABLE_NAME AS "name",' + + ' TABLE_SCHEMA AS "owner" FROM INFORMATION_SCHEMA.VIEWS', + 'TABLE_SCHEMA, TABLE_NAME', options); + } else if (owner) { + sqlViews = this.paginateSQL('SELECT \'view\' AS "type", TABLE_NAME AS "name",' + + ' TABLE_SCHEMA AS "owner" FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_SCHEMA=\'' + owner + '\'', + 'TABLE_SCHEMA, TABLE_NAME', options); + } else { + sqlViews = this.paginateSQL('SELECT \'view\' AS "type", TABLE_NAME AS "name",' + + ' schema_name() AS "owner" FROM INFORMATION_SCHEMA.VIEWS', + 'TABLE_NAME', options); + } + } + return sqlViews; + }; + + /** + * Discover model definitions + * + * @param {Object} options Options for discovery + * @param {Function} [cb] The callback function + */ + + /*! + * Normalize the arguments + * @param table string, required + * @param options object, optional + * @param cb function, optional + */ + MsSQL.prototype.getArgs = function(table, options, cb) { + if ('string' !== typeof table || !table) { + throw new Error(g.f('{{table}} is a required string argument: %s', table)); + } + options = options || {}; + if (!cb && 'function' === typeof options) { + cb = options; + options = {}; + } + if (typeof options !== 'object') { + throw new Error(g.f('{{options}} must be an {{object}}: %s', options)); + } + return { + schema: options.owner || options.schema, + owner: options.owner || options.schema, + table: table, + options: options, + cb: cb, + }; + }; + + /*! + * Build the sql statement to query columns for a given table + * @param owner + * @param table + * @returns {String} The sql statement + */ + MsSQL.prototype.buildQueryColumns = function(owner, table) { + let sql = null; + if (owner) { + sql = this.paginateSQL('SELECT TABLE_SCHEMA AS "owner", TABLE_NAME AS "tableName", COLUMN_NAME' + + ' AS "columnName", DATA_TYPE AS "dataType",' + + ' CHARACTER_MAXIMUM_LENGTH AS "dataLength", NUMERIC_PRECISION AS' + + ' "dataPrecision", NUMERIC_SCALE AS "dataScale", IS_NULLABLE AS "nullable"' + + ' ,COLUMNPROPERTY(object_id(TABLE_SCHEMA+\'.\'+TABLE_NAME), COLUMN_NAME, \'IsIdentity\') AS "generated"' + + ' FROM INFORMATION_SCHEMA.COLUMNS' + + ' WHERE TABLE_SCHEMA=\'' + owner + '\'' + + (table ? ' AND TABLE_NAME=\'' + table + '\'' : ''), + 'TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION', {}); + } else { + sql = this.paginateSQL('SELECT schema_name() AS "owner", TABLE_NAME' + + ' AS "tableName", COLUMN_NAME AS "columnName", DATA_TYPE AS "dataType",' + + ' CHARACTER_MAXIMUM_LENGTH AS "dataLength", NUMERIC_PRECISION AS "dataPrecision", NUMERIC_SCALE AS' + + ' "dataScale", IS_NULLABLE AS "nullable"' + + ' ,COLUMNPROPERTY(object_id(schema_name()+\'.\'+TABLE_NAME), COLUMN_NAME, \'IsIdentity\') AS "generated"' + + ' FROM INFORMATION_SCHEMA.COLUMNS' + + (table ? ' WHERE TABLE_NAME=\'' + table + '\'' : ''), + 'TABLE_NAME, ORDINAL_POSITION', {}); + } + return sql; + }; + + /** + * Discover model properties from a table + * @param {String} table The table name + * @param {Object} options The options for discovery + * @param {Function} [cb] The callback function + * + */ + + /*! + * Build the sql statement for querying primary keys of a given table + * @param owner + * @param table + * @returns {string} + */ + // http://docs.oracle.com/javase/6/docs/api/java/sql/DatabaseMetaData.html#getPrimaryKeys(java.lang.String, java.lang.String, java.lang.String) + + /* + select tc.TABLE_SCHEMA, tc.TABLE_NAME, kc.COLUMN_NAME + from + INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + join INFORMATION_SCHEMA.KEY_COLUMN_USAGE kc + on kc.TABLE_NAME = tc.TABLE_NAME and kc.TABLE_SCHEMA = tc.TABLE_SCHEMA + where + tc.CONSTRAINT_TYPE = 'PRIMARY KEY' + and kc.ORDINAL_POSITION is not null + order by tc.TABLE_SCHEMA, + tc.TABLE_NAME, + kc.ORDINAL_POSITION; + */ + + MsSQL.prototype.buildQueryPrimaryKeys = function(owner, table) { + let sql = 'SELECT kc.TABLE_SCHEMA AS "owner", ' + + 'kc.TABLE_NAME AS "tableName", kc.COLUMN_NAME AS "columnName", kc.ORDINAL_POSITION' + + ' AS "keySeq", kc.CONSTRAINT_NAME AS "pkName" FROM' + + ' INFORMATION_SCHEMA.KEY_COLUMN_USAGE kc' + + ' JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc' + + ' ON kc.TABLE_NAME = tc.TABLE_NAME AND kc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME AND kc.TABLE_SCHEMA = tc.TABLE_SCHEMA' + + ' WHERE tc.CONSTRAINT_TYPE=\'PRIMARY KEY\' AND kc.ORDINAL_POSITION IS NOT NULL'; + + if (owner) { + sql += ' AND kc.TABLE_SCHEMA=\'' + owner + '\''; + } + if (table) { + sql += ' AND kc.TABLE_NAME=\'' + table + '\''; + } + sql += ' ORDER BY kc.TABLE_SCHEMA, kc.TABLE_NAME, kc.ORDINAL_POSITION'; + return sql; + }; + + /** + * Discover primary keys for a given table + * @param {String} table The table name + * @param {Object} options The options for discovery + * @param {Function} [cb] The callback function + */ + // MsSQL.prototype.discoverPrimaryKeys = function(table, options, cb) { + // var args = this.getArgs(table, options, cb); + // var owner = args.owner; + // table = args.table; + // options = args.options; + // cb = args.cb; + // + // var sql = this.queryPrimaryKeys(owner, table); + // this.execute(sql, cb); + // }; + + /*! + * Build the sql statement for querying foreign keys of a given table + * @param owner + * @param table + * @returns {string} + */ + /* + SELECT + tc.CONSTRAINT_NAME, tc.TABLE_NAME, kcu.COLUMN_NAME, + ccu.TABLE_NAME AS foreign_table_name, + ccu.COLUMN_NAME AS foreign_column_name + FROM + INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu + ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME + JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS ccu + ON ccu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME + WHERE CONSTRAINT_TYPE = 'FOREIGN KEY' AND tc.TABLE_NAME='mytable'; + + */ + MsSQL.prototype.buildQueryForeignKeys = function(owner, table) { + let sql = + 'SELECT tc.TABLE_SCHEMA AS "fkOwner", tc.CONSTRAINT_NAME AS "fkName", tc.TABLE_NAME AS "fkTableName",' + + ' kcu.COLUMN_NAME AS "fkColumnName", kcu.ORDINAL_POSITION AS "keySeq",' + + ' ccu.TABLE_SCHEMA AS "pkOwner", \'PK\' AS "pkName", ' + + ' ccu.TABLE_NAME AS "pkTableName", ccu.COLUMN_NAME AS "pkColumnName"' + + ' FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc' + + ' JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu' + + ' ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME' + + ' JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE ccu' + + ' ON ccu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA AND ccu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME' + + ' WHERE tc.CONSTRAINT_TYPE = \'FOREIGN KEY\''; + if (owner) { + sql += ' AND tc.TABLE_SCHEMA=\'' + owner + '\''; + } + if (table) { + sql += ' AND tc.TABLE_NAME=\'' + table + '\''; + } + return sql; + }; + + /** + * Discover foreign keys for a given table + * @param {String} table The table name + * @param {Object} options The options for discovery + * @param {Function} [cb] The callback function + */ + + /*! + * Retrieves a description of the foreign key columns that reference the given table's primary key columns (the foreign keys exported by a table). + * They are ordered by fkTableOwner, fkTableName, and keySeq. + * @param owner + * @param table + * @returns {string} + */ + MsSQL.prototype.buildQueryExportedForeignKeys = function(owner, table) { + let sql = 'SELECT kcu.CONSTRAINT_NAME AS "fkName", kcu.TABLE_SCHEMA AS "fkOwner", kcu.TABLE_NAME AS "fkTableName",' + + ' kcu.COLUMN_NAME AS "fkColumnName", kcu.ORDINAL_POSITION AS "keySeq",' + + ' \'PK\' AS "pkName", ccu.TABLE_SCHEMA AS "pkOwner",' + + ' ccu.TABLE_NAME AS "pkTableName", ccu.COLUMN_NAME AS "pkColumnName"' + + ' FROM' + + ' INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE ccu' + + ' JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu' + + ' ON ccu.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND ccu.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME' + + ' WHERE kcu.ORDINAL_POSITION IS NOT NULL'; + if (owner) { + sql += ' and ccu.TABLE_SCHEMA=\'' + owner + '\''; + } + if (table) { + sql += ' and ccu.TABLE_NAME=\'' + table + '\''; + } + sql += ' order by kcu.TABLE_SCHEMA, kcu.TABLE_NAME, kcu.ORDINAL_POSITION'; + + return sql; + }; + + /** + * Discover foreign keys that reference to the primary key of this table + * @param {String} table The table name + * @param {Object} options The options for discovery + * @param {Function} [cb] The callback function + // */ + + MsSQL.prototype.buildPropertyType = function(columnDefinition, options) { + const mysqlType = columnDefinition.dataType; + const dataLength = options.dataLength || columnDefinition.dataLength; + const type = mysqlType.toUpperCase(); + switch (type) { + case 'BIT': + return 'Boolean'; + + case 'CHAR': + case 'VARCHAR': + case 'TEXT': + + case 'NCHAR': + case 'NVARCHAR': + case 'NTEXT': + + case 'CHARACTER VARYING': + case 'CHARACTER': + return 'String'; + + case 'BINARY': + case 'VARBINARY': + case 'IMAGE': + return 'Binary'; + + case 'BIGINT': + case 'NUMERIC': + case 'SMALLINT': + case 'DECIMAL': + case 'SMALLMONEY': + case 'INT': + case 'TINYINT': + case 'MONEY': + case 'FLOAT': + case 'REAL': + return 'Number'; + + case 'DATE': + case 'DATETIMEOFFSET': + case 'DATETIME2': + case 'SMALLDATETIME': + case 'DATETIME': + case 'TIME': + return 'Date'; + + case 'POINT': + return 'GeoPoint'; + default: + return 'String'; + } + }; + + MsSQL.prototype.setDefaultOptions = function(options) { + }; + + MsSQL.prototype.setNullableProperty = function(property) { + }; + + MsSQL.prototype.getDefaultSchema = function() { + return ''; + }; +} diff --git a/lib/migration.js b/lib/migration.js new file mode 100644 index 0000000..5f68d96 --- /dev/null +++ b/lib/migration.js @@ -0,0 +1,609 @@ +// Copyright IBM Corp. 2015,2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +const g = require('strong-globalize')(); + +const async = require('async'); + +module.exports = mixinMigration; + +function mixinMigration(MsSQL) { + MsSQL.prototype.showFields = function(model, cb) { + const sql = 'select [COLUMN_NAME] as [Field], ' + + ' [IS_NULLABLE] as [Null], [DATA_TYPE] as [Type],' + + ' [CHARACTER_MAXIMUM_LENGTH] as [Length],' + + ' [NUMERIC_PRECISION] as [Precision], NUMERIC_SCALE as [Scale]' + + ' from INFORMATION_SCHEMA.COLUMNS' + + ' where [TABLE_SCHEMA] = \'' + this.schema(model) + '\'' + + ' and [TABLE_NAME] = \'' + this.table(model) + '\'' + + ' order by [ORDINAL_POSITION]'; + this.execute(sql, function(err, fields) { + if (err) { + return cb && cb(err); + } else { + if (Array.isArray(fields)) { + fields.forEach(function(f) { + if (f.Length) { + f.Type = f.Type + '(' + f.Length + ')'; + } else if (f.Precision) { + f.Type = f.Type + '(' + f.Precision, +',' + f.Scale + ')'; + } + }); + } + cb && cb(err, fields); + } + }); + }; + + MsSQL.prototype.showIndexes = function(model, cb) { + const schema = "'" + this.schema(model) + "'"; + const table = "'" + this.table(model) + "'"; + const sql = 'SELECT OBJECT_SCHEMA_NAME(T.[object_id],DB_ID()) AS [table_schema],' + + ' T.[name] AS [Table], I.[name] AS [Key_name], AC.[name] AS [Column_name],' + + ' I.[type_desc], I.[is_unique], I.[data_space_id], I.[ignore_dup_key], I.[is_primary_key],' + + ' I.[is_unique_constraint], I.[fill_factor], I.[is_padded], I.[is_disabled], I.[is_hypothetical],' + + ' I.[allow_row_locks], I.[allow_page_locks], IC.[is_descending_key], IC.[is_included_column]' + + ' FROM sys.[tables] AS T' + + ' INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]' + + ' INNER JOIN sys.[index_columns] IC ON I.[object_id] = IC.[object_id]' + + ' INNER JOIN sys.[all_columns] AC ON T.[object_id] = AC.[object_id] AND IC.[column_id] = AC.[column_id]' + + ' WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> \'HEAP\'' + + ' AND OBJECT_SCHEMA_NAME(T.[object_id],DB_ID()) = ' + schema + ' AND T.[name] = ' + table + + ' ORDER BY T.[name], I.[index_id], IC.[key_ordinal]'; + + this.execute(sql, function(err, fields) { + cb && cb(err, fields); + }); + }; + + MsSQL.prototype.isActual = function(models, cb) { + let ok = false; + const self = this; + + if ((!cb) && ('function' === typeof models)) { + cb = models; + models = undefined; + } + // First argument is a model name + if ('string' === typeof models) { + models = [models]; + } + + models = models || Object.keys(this._models); + + async.each(models, function(model, done) { + self.getTableStatus(model, function(err, fields, indexes) { + self.alterTable(model, fields, indexes, function(err, needAlter) { + if (err) { + return done(err); + } else { + ok = ok || needAlter; + done(err); + } + }, true); + }); + }, function(err) { + if (err) { + return err; + } + cb(null, !ok); + }); + }; + + MsSQL.prototype.getColumnsToAdd = function(model, actualFields) { + const self = this; + const m = self._models[model]; + const propNames = Object.keys(m.properties).filter(function(name) { + return !!m.properties[name]; + }); + const idName = this.idName(model); + + const statements = []; + const columnsToAdd = []; + const columnsToAlter = []; + + // change/add new fields + propNames.forEach(function(propName) { + if (propName === idName) return; + let found; + if (actualFields) { + actualFields.forEach(function(f) { + if (f.Field === propName) { + found = f; + } + }); + } + + if (found) { + actualize(propName, found); + } else { + columnsToAdd.push(self.columnEscaped(model, propName) + + ' ' + self.propertySettingsSQL(model, propName)); + } + }); + + if (columnsToAdd.length) { + statements.push('ADD ' + columnsToAdd.join(',' + MsSQL.newline)); + } + + if (columnsToAlter.length) { + // SQL Server doesn't allow multiple columns to be altered in one statement + columnsToAlter.forEach(function(c) { + statements.push('ALTER COLUMN ' + c); + }); + } + + function actualize(propName, oldSettings) { + const newSettings = m.properties[propName]; + if (newSettings && changed(newSettings, oldSettings)) { + columnsToAlter.push(self.columnEscaped(model, propName) + ' ' + + self.propertySettingsSQL(model, propName)); + } + } + + function changed(newSettings, oldSettings) { + if (oldSettings.Null === 'YES' && + (newSettings.allowNull === false || newSettings.null === false)) { + return true; + } + if (oldSettings.Null === 'NO' && !(newSettings.allowNull === false || + newSettings.null === false)) { + return true; + } + if (oldSettings.Type.toUpperCase() !== datatype(newSettings)) { + return true; + } + return false; + } + return statements; + }; + + MsSQL.prototype.getColumnsToDrop = function(model, actualFields) { + const self = this; + const m = this._models[model]; + const propNames = Object.keys(m.properties).filter(function(name) { + return !!m.properties[name]; + }); + const idName = this.idName(model); + + const statements = []; + const columnsToDrop = []; + + if (actualFields) { + // drop columns + actualFields.forEach(function(f) { + const notFound = !~propNames.indexOf(f.Field); + if (f.Field === idName) return; + if (notFound || !m.properties[f.Field]) { + columnsToDrop.push(self.columnEscaped(model, f.Field)); + } + }); + + if (columnsToDrop.length) { + statements.push('DROP COLUMN' + columnsToDrop.join(',' + MsSQL.newline)); + } + } + return statements; + }; + + MsSQL.prototype.addIndexes = function(model, actualIndexes) { + const self = this; + const m = this._models[model]; + const idName = this.idName(model); + + const indexNames = m.settings.indexes ? Object.keys(m.settings.indexes).filter(function(name) { + return !!m.settings.indexes[name]; + }) : []; + const propNames = Object.keys(m.properties).filter(function(name) { + return !!m.properties[name]; + }); + + const ai = {}; + const sql = []; + + if (actualIndexes) { + actualIndexes.forEach(function(i) { + const name = i.Key_name; + if (!ai[name]) { + ai[name] = { + info: i, + columns: [], + }; + } + ai[name].columns[i.Seq_in_index - 1] = i.Column_name; + }); + } + + const aiNames = Object.keys(ai); + + // remove indexes + aiNames.forEach(function(indexName) { + if (indexName.substr(0, 3) === 'PK_') { + return; + } + if (indexNames.indexOf(indexName) === -1 && !m.properties[indexName] || + m.properties[indexName] && !m.properties[indexName].index) { + sql.push('DROP INDEX ' + indexName + ' ON ' + self.tableEscaped(model)); + } else { + // first: check single (only type and kind) + if (m.properties[indexName] && !m.properties[indexName].index) { + // TODO + return; + } + // second: check multiple indexes + let 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; + } + }); + } + if (!orderMatched) { + sql.push('DROP INDEX ' + self.columnEscaped(model, indexName) + ' ON ' + self.tableEscaped(model)); + delete ai[indexName]; + } + } + }); + + // add single-column indexes + propNames.forEach(function(propName) { + const found = ai[propName] && ai[propName].info; + if (!found) { + const tblName = self.tableEscaped(model); + const i = m.properties[propName].index; + if (!i) { + return; + } + let type = 'ASC'; + let kind = 'NONCLUSTERED'; + let unique = false; + if (i.type) { + type = i.type; + } + if (i.kind) { + kind = i.kind; + } + if (i.unique) { + unique = true; + } + let name = propName + '_' + kind + '_' + type + '_idx'; + if (i.name) { + name = i.name; + } + self._idxNames[model].push(name); + let cmd = 'CREATE ' + (unique ? 'UNIQUE ' : '') + kind + ' INDEX [' + name + '] ON ' + + tblName + MsSQL.newline; + cmd += '(' + MsSQL.newline; + cmd += ' [' + propName + '] ' + type; + cmd += MsSQL.newline + ') WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,' + + ' SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ' + + 'ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON);' + + MsSQL.newline; + sql.push(cmd); + } + }); + + // add multi-column indexes + indexNames.forEach(function(indexName) { + const found = ai[indexName] && ai[indexName].info; + if (!found) { + const tblName = self.tableEscaped(model); + const i = m.settings.indexes[indexName]; + let type = 'ASC'; + let kind = 'NONCLUSTERED'; + let unique = false; + if (i.type) { + type = i.type; + } + if (i.kind) { + kind = i.kind; + } + if (i.unique) { + unique = true; + } + const splitcolumns = i.columns.split(','); + const columns = []; + let name = ''; + + splitcolumns.forEach(function(elem, ind) { + let trimmed = elem.trim(); + name += trimmed + '_'; + trimmed = '[' + trimmed + '] ' + type; + columns.push(trimmed); + }); + + name += kind + '_' + type + '_idx'; + self._idxNames[model].push(name); + + let cmd = 'CREATE ' + (unique ? 'UNIQUE ' : '') + kind + ' INDEX [' + name + '] ON ' + + tblName + MsSQL.newline; + cmd += '(' + MsSQL.newline; + cmd += columns.join(',' + MsSQL.newline); + cmd += MsSQL.newline + ') WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ' + + 'SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ' + + 'ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON);' + + MsSQL.newline; + sql.push(cmd); + } + }); + return sql; + }; + + MsSQL.prototype.alterTable = function(model, actualFields, actualIndexes, done, checkOnly) { + const self = this; + + let statements = self.getAddModifyColumns(model, actualFields); + statements = statements.concat(self.getDropColumns(model, actualFields)); + statements = statements.concat(self.addIndexes(model, actualIndexes)); + + async.eachSeries(statements, function(query, fn) { + if (checkOnly) { + fn(null, true, {statements: statements, query: query}); + } else { + self.applySqlChanges(model, [query], fn); + } + }, function(err, results) { + done && done(err, results); + }); + }; + + MsSQL.prototype.propertiesSQL = function(model) { + // debugger; + const self = this; + const objModel = this._models[model]; + const modelPKIDs = this.idNames(model); + let j = 0; + + const sql = []; + const props = Object.keys(objModel.properties); + for (let i = 0, n = props.length; i < n; i++) { + const prop = props[i]; + if (modelPKIDs && modelPKIDs.indexOf(prop) !== -1) { + const modelPKID = modelPKIDs[j++]; + const idProp = objModel.properties[modelPKID]; + if (idProp.type === Number) { + if (idProp.generated !== false) { + sql.push(self.columnEscaped(model, modelPKID) + + ' ' + self.columnDataType(model, modelPKID) + ' IDENTITY(1,1) NOT NULL'); + } else { + sql.push(self.columnEscaped(model, modelPKID) + + ' ' + self.columnDataType(model, modelPKID) + ' NOT NULL'); + } + continue; + } else if (idProp.type === String) { + if (idProp.generated !== false) { + sql.push(self.columnEscaped(model, modelPKID) + + ' [uniqueidentifier] DEFAULT newid() NOT NULL'); + } else { + sql.push(self.columnEscaped(model, modelPKID) + ' ' + + self.propertySettingsSQL(model, prop) + ' DEFAULT newid()'); + } + continue; + } + } + sql.push(self.columnEscaped(model, prop) + ' ' + self.propertySettingsSQL(model, prop)); + } + let joinedSql = sql.join(',' + MsSQL.newline + ' '); + let cmd = ''; + if (modelPKIDs && modelPKIDs.length) { + cmd = 'PRIMARY KEY CLUSTERED' + MsSQL.newline + '(' + MsSQL.newline; + cmd += ' ' + modelPKIDs.map(function(modelPKID) { return self.columnEscaped(model, modelPKID) + ' ASC'; }).join(', ') + MsSQL.newline; + cmd += ') WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ' + + 'IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)'; + } + + joinedSql += ',' + MsSQL.newline + cmd; + + return joinedSql; + }; + + MsSQL.prototype.singleIndexSettingsSQL = function(model, prop, add) { + // Recycled from alterTable single indexes above, more or less. + const tblName = this.tableEscaped(model); + const i = this._models[model].properties[prop].index; + let type = 'ASC'; + let kind = 'NONCLUSTERED'; + let unique = false; + if (i.type) { + type = i.type; + } + if (i.kind) { + kind = i.kind; + } + if (i.unique) { + unique = true; + } + let name = prop + '_' + kind + '_' + type + '_idx'; + if (i.name) { + name = i.name; + } + this._idxNames[model].push(name); + let cmd = 'CREATE ' + (unique ? 'UNIQUE ' : '') + kind + ' INDEX [' + name + '] ON ' + + tblName + MsSQL.newline; + cmd += '(' + MsSQL.newline; + cmd += ' [' + prop + '] ' + type; + cmd += MsSQL.newline + ') WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,' + + ' SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ' + + 'ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON);' + + MsSQL.newline; + return cmd; + }; + + MsSQL.prototype.indexSettingsSQL = function(model, prop) { + // Recycled from alterTable multi-column indexes above, more or less. + const tblName = this.tableEscaped(model); + const i = this._models[model].settings.indexes[prop]; + let type = 'ASC'; + let kind = 'NONCLUSTERED'; + let unique = false; + if (i.type) { + type = i.type; + } + if (i.kind) { + kind = i.kind; + } + if (i.unique) { + unique = true; + } + const splitcolumns = i.columns.split(','); + const columns = []; + let name = ''; + splitcolumns.forEach(function(elem, ind) { + let trimmed = elem.trim(); + name += trimmed + '_'; + trimmed = '[' + trimmed + '] ' + type; + columns.push(trimmed); + }); + + name += kind + '_' + type + '_idx'; + this._idxNames[model].push(name); + + let cmd = 'CREATE ' + (unique ? 'UNIQUE ' : '') + kind + ' INDEX [' + name + '] ON ' + + tblName + MsSQL.newline; + cmd += '(' + MsSQL.newline; + cmd += columns.join(',' + MsSQL.newline); + cmd += MsSQL.newline + ') WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ' + + 'SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ' + + 'ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON);' + + MsSQL.newline; + return cmd; + }; + + function isNullable(p) { + return !(p.required || p.id || p.nullable === false || + p.allowNull === false || p['null'] === false); + } + + MsSQL.prototype.propertySettingsSQL = function(model, prop) { + const p = this._models[model].properties[prop]; + return this.columnDataType(model, prop) + ' ' + + (isNullable(p) ? 'NULL' : 'NOT NULL'); + }; + + MsSQL.prototype.automigrate = function(models, cb) { + const self = this; + if ((!cb) && ('function' === typeof models)) { + cb = models; + models = undefined; + } + // First argument is a model name + if ('string' === typeof models) { + models = [models]; + } + + models = models || Object.keys(this._models); + async.each(models, function(model, done) { + if (!(model in self._models)) { + return process.nextTick(function() { + done(new Error(g.f('Model not found: %s', model))); + }); + } + self.dropTable(model, function(err) { + if (err) { + return done(err); + } + self.createTable(model, done); + }); + }, function(err) { + cb && cb(err); + }); + }; + + MsSQL.prototype.dropTable = function(model, cb) { + const tblName = this.tableEscaped(model); + let cmd = "IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'" + + tblName + "') AND type in (N'U'))"; + cmd += MsSQL.newline + 'BEGIN' + MsSQL.newline; + cmd += ' DROP TABLE ' + tblName; + cmd += MsSQL.newline + 'END'; + this.execute(cmd, cb); + }; + + MsSQL.prototype.createTable = function(model, cb) { + const tblName = this.tableEscaped(model); + let cmd = 'SET ANSI_NULLS ON;' + MsSQL.newline + 'SET QUOTED_IDENTIFIER ON;' + + MsSQL.newline + 'SET ANSI_PADDING ON;' + MsSQL.newline; + cmd += "IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'" + + tblName + "') AND type in (N'U'))" + MsSQL.newline + 'BEGIN' + MsSQL.newline; + cmd += 'CREATE TABLE ' + this.tableEscaped(model) + ' ('; + cmd += MsSQL.newline + ' ' + this.propertiesSQL(model) + MsSQL.newline; + cmd += ')' + MsSQL.newline + 'END;' + MsSQL.newline; + cmd += this.createIndexes(model); + this.execute(cmd, cb); + }; + + MsSQL.prototype.createIndexes = function(model) { + const self = this; + const sql = []; + // Declared in model index property indexes. + Object.keys(this._models[model].properties).forEach(function(prop) { + const i = self._models[model].properties[prop].index; + if (i) { + sql.push(self.singleIndexSettingsSQL(model, prop)); + } + }); + + // Settings might not have an indexes property. + const dxs = this._models[model].settings.indexes; + if (dxs) { + Object.keys(this._models[model].settings.indexes).forEach(function(prop) { + sql.push(self.indexSettingsSQL(model, prop)); + }); + } + + return sql.join(MsSQL.newline); + }; + + MsSQL.prototype.columnDataType = function(model, property) { + const columnMetadata = this.columnMetadata(model, property); + let colType = columnMetadata && columnMetadata.dataType; + if (colType) { + colType = colType.toUpperCase(); + } + const prop = this._models[model].properties[property]; + if (!prop) { + return null; + } + const colLength = columnMetadata && columnMetadata.dataLength || prop.length; + if (colType) { + const dataPrecision = columnMetadata.dataPrecision; + const dataScale = columnMetadata.dataScale; + if (dataPrecision && dataScale) { + return colType + '(' + dataPrecision + ', ' + dataScale + ')'; + } + return colType + (colLength ? '(' + colLength + ')' : ''); + } + return datatype(prop); + }; + + function datatype(p) { + let dt = ''; + switch (p.type.name) { + default: + case 'String': + case 'JSON': + dt = '[nvarchar](' + (p.length || p.limit || 255) + ')'; + break; + case 'Text': + dt = '[text]'; + break; + case 'Number': + dt = '[int]'; + break; + case 'Date': + dt = '[datetime]'; + break; + case 'Boolean': + dt = '[bit]'; + break; + case 'Point': + dt = '[float]'; + break; + } + return dt; + } +} diff --git a/lib/mssql.js b/lib/mssql.js new file mode 100644 index 0000000..1beb5eb --- /dev/null +++ b/lib/mssql.js @@ -0,0 +1,535 @@ +// Copyright IBM Corp. 2013,2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +const g = require('strong-globalize')(); + +/*! Module dependencies */ +const mssql = require('mssql'); +const SqlConnector = require('loopback-connector').SqlConnector; +const ParameterizedSQL = SqlConnector.ParameterizedSQL; +const util = require('util'); +const debug = require('debug')('loopback:connector:mssql'); + +mssql.map.register(Number, mssql.BigInt); + +const name = 'mssql'; + +exports.name = name; +exports.initialize = function initializeSchema(dataSource, callback) { + const settings = dataSource.settings || {}; + debug('Settings: %j', settings); + const driver = new MsSQL(settings); + dataSource.connector = driver; + dataSource.connector.dataSource = dataSource; + dataSource.connector.tableNameID = dataSource.settings.tableNameID; + + if (settings.lazyConnect) { + process.nextTick(function() { + callback(); + }); + } else { + driver.connect(function(err, connection) { + dataSource.client = connection; + callback && callback(err, connection); + }); + } +}; + +function MsSQL(settings) { + MsSQL.super_.call(this, name, settings); + // this.name = name; + // this.settings = settings || {}; + this.settings.server = this.settings.host || this.settings.hostname; + this.settings.user = this.settings.user || this.settings.username; + // use url to override settings if url provided + this.connConfig = this.settings.url || this.settings; + this._models = {}; + this._idxNames = {}; +} + +util.inherits(MsSQL, SqlConnector); + +MsSQL.newline = '\r\n'; + +/*! + * This is a workaround to the limitation that 'msssql' driver doesn't support + * parameterized SQL execution + * @param {String} sql The SQL string with parameters as (?) + * @param {*[]) params An array of parameter values + * @returns {*} The fulfilled SQL string + */ +function format(sql, params) { + if (Array.isArray(params)) { + let count = 0; + let index = -1; + while (count < params.length) { + index = sql.indexOf('(?)'); + if (index === -1) { + return sql; + } + sql = sql.substring(0, index) + escape(params[count]) + + sql.substring(index + 3); + count++; + } + } + return sql; +} + +MsSQL.prototype.connect = function(callback) { + const self = this; + if (self.client) { + return process.nextTick(callback); + } + const connection = new mssql.ConnectionPool(this.connConfig, function(err) { + if (err) { + debug('Connection error: ', err); + return callback(err); + } + debug('Connection established: ', self.settings.server); + self.client = connection; + callback(err, connection); + }); +}; + +function parameterizedSQL(sql) { + let count = 0; + let index = -1; + while (true) { + index = sql.indexOf('(?)'); + if (index === -1) { + break; + } + count++; + sql = sql.substring(0, index) + ('@param' + count) + + sql.substring(index + 3); + } + return sql; +} + +MsSQL.prototype.executeSQL = function(sql, params, options, callback) { + debug('SQL: %s Parameters: %j', sql, params); + + // Convert (?) to @paramX + sql = parameterizedSQL(sql); + + let connection = this.client; + + const transaction = options.transaction; + if (transaction && transaction.connector === this && transaction.connection) { + debug('Execute SQL in a transaction'); + connection = transaction.connection; + } + const innerCB = function(err, data) { + debug('Result: %j %j', err, data); + if (data) { + data = data.recordset; + } + callback && callback(err, data); + }; + + const request = new mssql.Request(connection); + // Allow multiple result sets + if (options.multipleResultSets) { + request.multiple = true; + } + + if (Array.isArray(params) && params.length > 0) { + for (let i = 0, n = params.length; i < n; i++) { + if (typeof params[i] === 'number' && + params[i] % 1 !== 0) { + // TODO: prefer decimal or numeric data + // until https://github.com/loopbackio/loopback-connector-mssql/issues/183 is resolved + request.input('param' + (i + 1), mssql.Decimal(38, 10), params[i]); + } else if (typeof params[i] === 'number' && isBigInt(params[i])) { + request.input('param' + (i + 1), mssql.BigInt, params[i]); + } else { + request.input('param' + (i + 1), params[i]); + } + } + } + + // request.verbose = true; + request.query(sql, innerCB); +}; + +function isBigInt(num) { + if (num > 2147483647 && num <= 9223372036854775807) return true; + if (num < -2147483648 && num >= -9223372036854775808) return true; + return false; +} + +MsSQL.prototype.disconnect = function disconnect(cb) { + this.client.close(cb); +}; + +// MsSQL.prototype.command = function (sql, callback) { +// return this.execute(sql, callback); +// }; + +// params +// descr = { +// model: ... +// properties: ... +// settings: ... +// } +MsSQL.prototype.define = function(modelDefinition) { + if (!modelDefinition.settings) { + modelDefinition.settings = {}; + } + + this._models[modelDefinition.model.modelName] = modelDefinition; + + // track database index names for this model + this._idxNames[modelDefinition.model.modelName] = []; +}; + +MsSQL.prototype.getPlaceholderForValue = function(key) { + return '@param' + key; +}; + +MsSQL.prototype.buildInsertDefaultValues = function(model, data, options) { + return 'DEFAULT VALUES'; +}; + +MsSQL.prototype.buildInsertInto = function(model, fields, options) { + const stmt = this.invokeSuper('buildInsertInto', model, fields, options); + const idName = this.idName(model); + + stmt.sql = idName ? (MsSQL.newline + + 'DECLARE @insertedIds TABLE (id ' + this.columnDataType(model, idName) + ')' + + MsSQL.newline) + stmt.sql : stmt.sql; + + if (idName) { + stmt.merge( + 'OUTPUT INSERTED.' + + this.columnEscaped(model, idName) + + ' into @insertedIds', + ); + } + return stmt; +}; + +MsSQL.prototype.buildInsert = function(model, data, options) { + const idName = this.idName(model); + const prop = this.getPropertyDefinition(model, idName); + const isIdentity = (prop && prop.type === Number && prop.generated !== false); + if (isIdentity && data[idName] == null) { + // remove the pkid column if it's in the data, since we're going to insert a + // new record, not update an existing one. + delete data[idName]; + // delete the hardcoded id property that jugglindb automatically creates + // delete data.id; + } + + const stmt = this.invokeSuper('buildInsert', model, data, options); + const tblName = this.tableEscaped(model); + + if (isIdentity && data[idName] != null) { + stmt.sql = 'SET IDENTITY_INSERT ' + tblName + ' ON;' + MsSQL.newline + + stmt.sql; + } + if (isIdentity && data[idName] != null) { + stmt.sql += MsSQL.newline + 'SET IDENTITY_INSERT ' + tblName + ' OFF;' + + MsSQL.newline; + } + if (idName) { + stmt.sql += MsSQL.newline + 'SELECT id AS insertId from @insertedIds' + MsSQL.newline; + } + + return stmt; +}; + +MsSQL.prototype.getInsertedId = function(model, info) { + return info && info.length > 0 && info[0].insertId; +}; + +MsSQL.prototype.buildDelete = function(model, where, options) { + const stmt = this.invokeSuper('buildDelete', model, where, options); + stmt.merge(';SELECT @@ROWCOUNT as count', ''); + return stmt; +}; + +MsSQL.prototype.buildReplace = function(model, where, data, options) { + const stmt = this.invokeSuper('buildReplace', model, where, data, options); + stmt.merge(';SELECT @@ROWCOUNT as count', ''); + return stmt; +}; + +MsSQL.prototype.getCountForAffectedRows = function(model, info) { + const affectedCountQueryResult = info && info[0]; + if (!affectedCountQueryResult) { + return undefined; + } + const affectedCount = typeof affectedCountQueryResult.count === 'number' ? + affectedCountQueryResult.count : undefined; + return affectedCount; +}; + +MsSQL.prototype.buildUpdate = function(model, where, data, options) { + const stmt = this.invokeSuper('buildUpdate', model, where, data, options); + stmt.merge(';SELECT @@ROWCOUNT as count', ''); + return stmt; +}; + +// Convert to ISO8601 format YYYY-MM-DDThh:mm:ss[.mmm] +function dateToMsSql(val) { + const dateStr = val.getUTCFullYear() + '-' + + fillZeros(val.getUTCMonth() + 1) + '-' + + fillZeros(val.getUTCDate()) + + 'T' + fillZeros(val.getUTCHours()) + ':' + + fillZeros(val.getUTCMinutes()) + ':' + + fillZeros(val.getUTCSeconds()) + '.'; + + let ms = val.getUTCMilliseconds(); + if (ms < 10) { + ms = '00' + ms; + } else if (ms < 100) { + ms = '0' + ms; + } else { + ms = '' + ms; + } + return dateStr + ms; + + function fillZeros(v) { + return v < 10 ? '0' + v : v; + } +} + +function escape(val) { + if (val === undefined || val === null) { + return 'NULL'; + } + + switch (typeof val) { + case 'boolean': + return (val) ? 1 : 0; + case 'number': + return val + ''; + } + + if (typeof val === 'object') { + val = (typeof val.toISOString === 'function') ? + val.toISOString() : + val.toString(); + } + + val = val.replace(/[\0\n\r\b\t\\\'\"\x1a]/g, function(s) { + switch (s) { + case '\0': + return '\\0'; + case '\n': + return '\\n'; + case '\r': + return '\\r'; + case '\b': + return '\\b'; + case '\t': + return '\\t'; + case '\x1a': + return '\\Z'; + case "\'": + return "''"; // For sql server, double quote + case '"': + return s; // For oracle + default: + return '\\' + s; + } + }); + // return "q'#"+val+"#'"; + return "N'" + val + "'"; +} + +MsSQL.prototype.toColumnValue = function(prop, val) { + if (val == null) { + return null; + } + if (prop.type === String) { + return String(val); + } + if (prop.type === Number) { + if (isNaN(val)) { + // Map NaN to NULL + return val; + } + return val; + } + + if (prop.type === Date || prop.type.name === 'Timestamp') { + if (!val.toUTCString) { + val = new Date(val); + } + val = dateToMsSql(val); + return val; + } + + if (prop.type === Boolean) { + if (val) { + return true; + } else { + return false; + } + } + + return this.serializeObject(val); +}; + +MsSQL.prototype.fromColumnValue = function(prop, val) { + if (val == null) { + return val; + } + const type = prop && prop.type; + if (type === Boolean) { + val = !!val; // convert to a boolean type from number + } + if (type === Date) { + if (!(val instanceof Date)) { + val = new Date(val.toString()); + } + } + return val; +}; + +MsSQL.prototype.escapeName = function(name) { + return '[' + name.replace(/\./g, '_') + ']'; +}; + +MsSQL.prototype.getDefaultSchemaName = function() { + return 'dbo'; +}; + +MsSQL.prototype.tableEscaped = function(model) { + return this.escapeName(this.schema(model)) + '.' + + this.escapeName(this.table(model)); +}; + +MsSQL.prototype.applySqlChanges = function(model, pendingChanges, cb) { + const self = this; + if (pendingChanges.length) { + const alterTable = (pendingChanges[0].substring(0, 10) !== + 'DROP INDEX' && pendingChanges[0].substring(0, 6) !== 'CREATE'); + let thisQuery = alterTable ? 'ALTER TABLE ' + self.tableEscaped(model) : ''; + let ranOnce = false; + pendingChanges.forEach(function(change) { + if (ranOnce) { + thisQuery = thisQuery + ' '; + } + thisQuery = thisQuery + ' ' + change; + ranOnce = true; + }); + self.execute(thisQuery, cb); + } +}; + +function buildLimit(limit, offset) { + if (isNaN(offset)) { + offset = 0; + } + let sql = 'ORDER BY RowNum OFFSET ' + offset + ' ROWS'; + if (limit >= 0) { + sql += ' FETCH NEXT ' + limit + ' ROWS ONLY'; + } + return sql; +} + +MsSQL.prototype.buildColumnNames = function(model, filter, options) { + let columnNames = this.invokeSuper('buildColumnNames', model, filter); + if (filter.limit || filter.offset || filter.skip) { + const orderBy = this.buildOrderBy(model, filter.order); + let orderClause = ''; + let partitionByClause = ''; + if (options && options.partitionBy) { + partitionByClause = 'PARTITION BY ' + this.columnEscaped(model, options.partitionBy); + } + if (orderBy) { + orderClause = 'OVER (' + partitionByClause + ' ' + orderBy + ') '; + } else { + orderClause = 'OVER (' + partitionByClause + ' ' + 'ORDER BY (SELECT 1)) '; + } + columnNames += ',ROW_NUMBER() ' + orderClause + 'AS RowNum'; + } + return columnNames; +}; + +MsSQL.prototype.buildSelect = function(model, filter, options) { + if (!filter.order) { + const idNames = this.idNames(model); + if (idNames && idNames.length) { + filter.order = idNames; + } + } + + let selectStmt = new ParameterizedSQL('SELECT ' + + this.buildColumnNames(model, filter, options) + + ' FROM ' + this.tableEscaped(model)); + + if (filter) { + if (filter.where) { + const whereStmt = this.buildWhere(model, filter.where); + selectStmt.merge(whereStmt); + } + + if (filter.limit || filter.skip || filter.offset) { + selectStmt = this.applyPagination( + model, selectStmt, filter, + ); + } else { + if (filter.order) { + selectStmt.merge(this.buildOrderBy(model, filter.order)); + } + } + } + return this.parameterize(selectStmt); +}; + +MsSQL.prototype.applyPagination = + function(model, stmt, filter) { + const offset = filter.offset || filter.skip || 0; + if (this.settings.supportsOffsetFetch) { + // SQL 2012 or later + // http://technet.microsoft.com/en-us/library/gg699618.aspx + const limitClause = buildLimit(filter.limit, filter.offset || filter.skip); + return stmt.merge(limitClause); + } else { + // SQL 2005/2008 + // http://blog.sqlauthority.com/2013/04/14/sql-server-tricks-for-row-offset-and-paging-in-various-versions-of-sql-server/ + let paginatedSQL = 'SELECT * FROM (' + stmt.sql + MsSQL.newline + + ') AS S' + MsSQL.newline + ' WHERE S.RowNum > ' + offset; + + if (filter.limit !== -1) { + paginatedSQL += ' AND S.RowNum <= ' + (offset + filter.limit); + } + + stmt.sql = paginatedSQL + MsSQL.newline; + return stmt; + } + }; + +MsSQL.prototype.buildExpression = function(columnName, operator, operatorValue, + propertyDefinition) { + switch (operator) { + case 'like': + return new ParameterizedSQL(columnName + " LIKE ? ESCAPE '\\'", + [operatorValue]); + case 'nlike': + return new ParameterizedSQL(columnName + " NOT LIKE ? ESCAPE '\\'", + [operatorValue]); + case 'regexp': + g.warn('{{Microsoft SQL Server}} does not support the regular ' + + 'expression operator'); + default: + // invoke the base implementation of `buildExpression` + return this.invokeSuper('buildExpression', columnName, operator, + operatorValue, propertyDefinition); + } +}; + +MsSQL.prototype.ping = function(cb) { + this.execute('SELECT 1 AS result', cb); +}; + +require('./discovery')(MsSQL, mssql); +require('./migration')(MsSQL, mssql); +require('./transaction')(MsSQL, mssql); diff --git a/lib/transaction.js b/lib/transaction.js new file mode 100644 index 0000000..a0662c2 --- /dev/null +++ b/lib/transaction.js @@ -0,0 +1,48 @@ +// Copyright IBM Corp. 2015,2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +const debug = require('debug')('loopback:connector:mssql:transaction'); + +module.exports = mixinTransaction; + +/*! + * @param {MsSQL} MsSQL connector class + */ +function mixinTransaction(MsSQL, mssql) { + /** + * Begin a new transaction + * @param isolationLevel + * @param cb + */ + MsSQL.prototype.beginTransaction = function(isolationLevel, cb) { + debug('Begin a transaction with isolation level: %s', isolationLevel); + isolationLevel = mssql.ISOLATION_LEVEL[isolationLevel.replace(' ', '_')]; + const transaction = new mssql.Transaction(this.client); + transaction.begin(isolationLevel, function(err) { + cb(err, transaction); + }); + }; + + /** + * + * @param connection + * @param cb + */ + MsSQL.prototype.commit = function(connection, cb) { + debug('Commit a transaction'); + connection.commit(cb); + }; + + /** + * + * @param connection + * @param cb + */ + MsSQL.prototype.rollback = function(connection, cb) { + debug('Rollback a transaction'); + connection.rollback(cb); + }; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b019eec --- /dev/null +++ b/package.json @@ -0,0 +1,57 @@ +{ + "name": "loopback-connector-mssql", + "version": "3.8.0", + "description": "Microsoft SQL Server connector for LoopBack", + "engines": { + "node": ">=10" + }, + "keywords": [ + "StrongLoop", + "LoopBack", + "Microsoft SQL Server", + "MSSQL", + "Database", + "DataSource", + "Connector" + ], + "main": "index.js", + "dependencies": { + "async": "^3.1.0", + "debug": "^4.3.1", + "loopback-connector": "^5.0.1", + "mssql": "^6.4.0", + "strong-globalize": "^5.0.0" + }, + "devDependencies": { + "bluebird": "^3.5.5", + "eslint": "^7.0.0", + "eslint-config-loopback": "^13.1.0", + "juggler-v3": "file:./deps/juggler-v3", + "juggler-v4": "file:./deps/juggler-v4", + "loopback-datasource-juggler": "^3.0.0 || ^4.0.0", + "mocha": "^6.1.4", + "rc": "^1.2.8", + "should": "^13.2.3", + "sinon": "^6.1.3", + "sqlcmdjs": "^1.5.0" + }, + "scripts": { + "lint": "eslint .", + "pretest": "node pretest.js", + "test": "mocha test/*.test.js node_modules/juggler-v3/test.js node_modules/juggler-v4/test.js", + "posttest": "npm run lint" + }, + "files": [ + "intl", + "lib", + "docs.json", + "index.js", + "setup.sh" + ], + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-connector-mssql.git" + }, + "copyright.owner": "IBM Corp.", + "license": "MIT" +} diff --git a/pretest.js b/pretest.js new file mode 100644 index 0000000..7112056 --- /dev/null +++ b/pretest.js @@ -0,0 +1,34 @@ +// Copyright IBM Corp. 2017,2020. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +require('./test/init'); +const exec = require('child_process').exec; +const path = require('path'); + +const isWin = (process.platform === 'win32'); +const sqlFileDir = path.resolve(__dirname, 'test', 'tables.sql'); +const sqlDependencyDir = path.resolve(__dirname, 'node_modules', '.bin', 'sqlcmd'); + +if (!process.env.CI) { + return console.log('not seeding DB with test db'); +} + +if (!process.env.MSSQL_HOST) process.env.MSSQL_HOST = global.getConfig().host; +if (!process.env.MSSQL_PORT) process.env.MSSQL_PORT = global.getConfig().port; +if (!process.env.MSSQL_USER) process.env.MSSQL_USER = global.getConfig().user; +if (!process.env.MSSQL_PASSWORD) process.env.MSSQL_PASSWORD = global.getConfig().password; +if (!process.env.MSSQL_DATABASE) process.env.MSSQL_DATABASE = global.getConfig().database; + +const catFileCmd = (isWin ? 'type ' : 'cat ') + sqlFileDir; + +const sqlcmd = `${catFileCmd} | ${sqlDependencyDir} -s ${process.env.MSSQL_HOST} -o ${process.env.MSSQL_PORT} ` + + `-u "${process.env.MSSQL_USER}" -p "${process.env.MSSQL_PASSWORD}" -d "${process.env.MSSQL_DATABASE}"`; + +exec(sqlcmd, function(err, result) { + if (err) return console.log('Database seeding failed.\n%s', err); + return console.log('Database successfully seeded.'); +}); diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..ed0de4d --- /dev/null +++ b/setup.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +### Shell script to spin up a docker container for mssql. + +## color codes +RED='\033[1;31m' +GREEN='\033[1;32m' +YELLOW='\033[1;33m' +CYAN='\033[1;36m' +PLAIN='\033[0m' + +## variables +MSSQL_CONTAINER="mssql_c" +HOST="localhost" +PORT=1433 +USER="sa" +PASSWORD="M55sqlT35t" +DATABASE="master" +if [ "$1" ]; then + HOST=$1 +fi +if [ "$2" ]; then + PORT=$2 +fi +if [ "$3" ]; then + USER=$3 +fi +if [ "$4" ]; then + PASSWORD=$4 +fi +if [ "$5" ]; then + DATABASE=$5 +fi + +## check if docker exists +printf "\n${RED}>> Checking for docker${PLAIN} ${GREEN}...${PLAIN}" +docker -v > /dev/null 2>&1 +DOCKER_EXISTS=$? +if [ "$DOCKER_EXISTS" -ne 0 ]; then + printf "\n\n${CYAN}Status: ${PLAIN}${RED}Docker not found. Terminating setup.${PLAIN}\n\n" + exit 1 +fi +printf "\n${CYAN}Found docker. Moving on with the setup.${PLAIN}\n" + + +## cleaning up previous builds +printf "\n${RED}>> Finding old builds and cleaning up${PLAIN} ${GREEN}...${PLAIN}" +docker rm -f $MSSQL_CONTAINER > /dev/null 2>&1 +printf "\n${CYAN}Clean up complete.${PLAIN}\n" + +## pull latest mssql image +printf "\n${RED}>> Pulling latest mssql image${PLAIN} ${GREEN}...${PLAIN}" +docker pull mcr.microsoft.com/mssql/server:2019-latest > /dev/null 2>&1 +printf "\n${CYAN}Image successfully built.${PLAIN}\n" + +## run the mssql container +printf "\n${RED}>> Starting the mssql container${PLAIN} ${GREEN}...${PLAIN}" +CONTAINER_STATUS=$(docker run --name $MSSQL_CONTAINER -e ACCEPT_EULA=Y -e SA_PASSWORD=$PASSWORD -p $PORT:1433 -d mcr.microsoft.com/mssql/server:2019-latest 2>&1) +if [[ "$CONTAINER_STATUS" == *"Error"* ]]; then + printf "\n\n${CYAN}Status: ${PLAIN}${RED}Error starting container. Terminating setup.${PLAIN}\n\n" + exit 1 +fi +docker cp ./test/tables.sql $MSSQL_CONTAINER:/home/ > /dev/null 2>&1 +printf "\n${CYAN}Container is up and running.${PLAIN}\n" + +## export the schema to the mssql database +printf "\n${RED}>> Exporting schema to database${PLAIN} ${GREEN}...${PLAIN}\n" + +## command to export schema +docker exec $MSSQL_CONTAINER /bin/sh -c "/opt/mssql-tools/bin/sqlcmd -S $HOST -U $USER -P $PASSWORD -i /home/tables.sql" > /dev/null 2>&1 + +## variables needed to health check export schema +OUTPUT=$? +TIMEOUT=120 +TIME_PASSED=0 +WAIT_STRING="." + +printf "\n${GREEN}Waiting for database to respond with updated schema $WAIT_STRING${PLAIN}" +while [ "$OUTPUT" -ne 0 ] && [ "$TIMEOUT" -gt 0 ] + do + docker exec $MSSQL_CONTAINER /bin/sh -c "/opt/mssql-tools/bin/sqlcmd -S $HOST -U $USER -P $PASSWORD -i /home/tables.sql" > /dev/null 2>&1 + OUTPUT=$? + sleep 1s + TIMEOUT=$((TIMEOUT - 1)) + TIME_PASSED=$((TIME_PASSED + 1)) + + if [ "$TIME_PASSED" -eq 5 ]; then + printf "${GREEN}.${PLAIN}" + TIME_PASSED=0 + fi + done + +if [ "$TIMEOUT" -le 0 ]; then + printf "\n\n${CYAN}Status: ${PLAIN}${RED}Failed to export schema. Terminating setup.${PLAIN}\n\n" + exit 1 +fi +printf "\n${CYAN}Successfully exported schema to database.${PLAIN}\n" + +## create the database +printf "\n${RED}>> Create the database${PLAIN} ${GREEN}...${PLAIN}" +docker exec -it $MSSQL_CONTAINER /bin/sh -c "/opt/mssql-tools/bin/sqlcmd -S $HOST -U $USER -P $PASSWORD -Q 'CREATE DATABASE $DATABASE'" +CREATE_DATABASE=$? +if [ "$CREATE_DATABASE" -ne 0 ]; then + printf "\n\n${CYAN}Status: ${PLAIN}${RED}Error creating database: $DATABASE. Terminating setup.${PLAIN}\n\n" + exit 1 +fi +printf "\n${CYAN}Database created.${PLAIN}\n" + +## set env variables for running test +printf "\n${RED}>> Setting env variables to run test${PLAIN} ${GREEN}...${PLAIN}" +export MSSQL_HOST=$HOST +export MSSQL_PORT=$PORT +export MSSQL_USER=$USER +export MSSQL_PASSWORD=$PASSWORD +export MSSQL_DATABASE=$DATABASE +printf "\n${CYAN}Env variables set.${PLAIN}\n" + +printf "\n${CYAN}Status: ${PLAIN}${GREEN}Set up completed successfully.${PLAIN}\n" +printf "\n${CYAN}Instance url: ${YELLOW}mssql://$USER:$PASSWORD@$HOST/$DATABASE${PLAIN}\n" +printf "\n${CYAN}To run the test suite:${PLAIN} ${YELLOW}npm test${PLAIN}\n\n" diff --git a/test/autoupdate.test.js b/test/autoupdate.test.js new file mode 100644 index 0000000..5955963 --- /dev/null +++ b/test/autoupdate.test.js @@ -0,0 +1,224 @@ +// Copyright IBM Corp. 2014,2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +require('./init.js'); +const assert = require('assert'); +let ds; + +before(function() { + /* global getDataSource */ + ds = getDataSource(); +}); + +describe('MS SQL server connector', function() { + it('should auto migrate/update tables', function(done) { + this.timeout(30000); + + /* eslint-disable camelcase */ + const schema_v1 = + { + name: 'CustomerTest', + options: { + idInjection: false, + mssql: { + schema: 'dbo', + table: 'CUSTOMER_TEST', + }, + indexes: { + idEmailIndex: { + keys: { + id: 1, + email: 1, + }, + options: { + unique: true, + }, + columns: 'id,email', + kind: 'unique', + }, + }, + }, + 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, + }, + firstName: { + type: 'String', + required: false, + }, + }, + }; + + const schema_v2 = + { + name: 'CustomerTest', + options: { + idInjection: false, + mssql: { + schema: 'dbo', + table: 'CUSTOMER_TEST', + }, + indexes: { + idCityIndex: { + keys: { + id: 1, + city: 1, + }, + options: { + unique: true, + }, + columns: 'id,city', + kind: 'unique', + }, + }, + }, + properties: { + id: { + type: 'String', + length: 20, + id: 1, + }, + email: { + type: 'String', + required: false, + length: 60, + mssql: { + columnName: 'EMAIL', + dataType: 'nvarchar', + dataLength: 60, + nullable: 'YES', + }, + }, + firstName: { + type: 'String', + required: false, + length: 40, + }, + lastName: { + type: 'String', + required: false, + length: 40, + }, + city: { + type: 'String', + required: false, + length: 40, + index: { + unique: true, + }, + }, + }, + }; + + ds.createModel(schema_v1.name, schema_v1.properties, schema_v1.options); + /* eslint-enable camelcase */ + + ds.automigrate(function(err) { + assert(!err); + ds.discoverModelProperties('CUSTOMER_TEST', function(err, props) { + assert(!err); + assert.equal(props.length, 5); + const names = props.map(function(p) { + return p.columnName; + }); + assert.equal(props[0].nullable, 'NO'); + assert.equal(props[1].nullable, 'YES'); + assert.equal(props[2].nullable, 'NO'); + assert.equal(props[3].nullable, 'YES'); + + assert.equal(names[0], 'id'); + assert.equal(names[1], 'name'); + assert.equal(names[2], 'email'); + assert.equal(names[3], 'age'); + /* eslint-disable camelcase */ + ds.createModel(schema_v2.name, schema_v2.properties, schema_v2.options); + /* eslint-enable camelcase */ + ds.autoupdate(function(err, result) { + ds.discoverModelProperties('CUSTOMER_TEST', function(err, props) { + assert.equal(props.length, 5); + const names = props.map(function(p) { + return p.columnName; + }); + assert.equal(names[0], 'id'); + assert.equal(names[1], 'email'); + assert.equal(names[2], 'firstName'); + assert.equal(names[3], 'lastName'); + assert.equal(names[4], 'city'); + + const schema = "'dbo'"; + const table = "'CUSTOMER_TEST'"; + const sql = 'SELECT OBJECT_SCHEMA_NAME(T.[object_id],DB_ID()) AS [table_schema],' + + ' T.[name] AS [Table], I.[name] AS [Key_name], AC.[name] AS [Column_name],' + + ' I.[type_desc], I.[is_unique], I.[data_space_id], I.[ignore_dup_key], I.[is_primary_key],' + + ' I.[is_unique_constraint], I.[fill_factor], I.[is_padded], I.[is_disabled], I.[is_hypothetical],' + + ' I.[allow_row_locks], I.[allow_page_locks], IC.[is_descending_key], IC.[is_included_column]' + + ' FROM sys.[tables] AS T' + + ' INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]' + + ' INNER JOIN sys.[index_columns] IC ON I.[object_id] = IC.[object_id]' + + ' INNER JOIN sys.[all_columns] AC ON T.[object_id] = AC.[object_id] AND IC.[column_id] = AC.[column_id]' + + ' WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> \'HEAP\'' + + ' AND OBJECT_SCHEMA_NAME(T.[object_id],DB_ID()) = ' + schema + ' AND T.[name] = ' + table + + ' ORDER BY T.[name], I.[index_id], IC.[key_ordinal]'; + + ds.connector.execute(sql, function(err, indexes) { + let countIdEmailIndex = 0; + let countIdCityIndex = 0; + let countCityIndex = 0; + let countAgeIndex = 0; + for (let i = 0; i < indexes.length; i++) { + if (indexes[i].Key_name == 'id_email_unique_ASC_idx') { + countIdEmailIndex++; + } + if (indexes[i].Key_name == 'id_city_unique_ASC_idx') { + countIdCityIndex++; + } + if (indexes[i].Key_name == 'city_NONCLUSTERED_ASC_idx') { + countCityIndex++; + } + if (indexes[i].Key_name == 'age_NONCLUSTERED_ASC_idx') { + countAgeIndex++; + } + } + assert.equal(countIdEmailIndex, 0); + assert.equal(countAgeIndex, 0); + assert.equal(countIdCityIndex > 0, true); + assert.equal(countCityIndex > 0, true); + done(err, result); + }); + }); + }); + }); + }); + }); + + it('should report errors for automigrate', function() { + ds.automigrate('XYZ', function(err) { + assert(err); + }); + }); + + it('should report errors for autoupdate', function() { + ds.autoupdate('XYZ', function(err) { + assert(err); + }); + }); +}); diff --git a/test/commontests.js b/test/commontests.js new file mode 100644 index 0000000..16853b2 --- /dev/null +++ b/test/commontests.js @@ -0,0 +1,237 @@ +// Copyright IBM Corp. 2013,2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +const jdb = require('loopback-datasource-juggler'); +const commonTest = jdb.test; + +require('./init'); + +/* global getDataSource */ +const schema = getDataSource(); + +// run the tests exposed by jugglingdb +commonTest(module.exports, schema); + +// skip the order test from jugglingdb, it wasn't working right +commonTest.skip('should handle ORDER clause'); + +// re-implement the order test as pretty much the same thing, but run an automigration beforehand +commonTest.it('should automigrate', function(test) { + schema.automigrate(function(err) { + test.ifError(err); + test.done(); + }); +}); + +commonTest.it('should be able to ORDER results', function(test) { + const titles = [ + {title: 'Title A', subject: 'B'}, + {title: 'Title Z', subject: 'A'}, + {title: 'Title M', subject: 'C'}, + {title: 'Title A', subject: 'A'}, + {title: 'Title B', subject: 'A'}, + {title: 'Title C', subject: 'D'}, + ]; + + const dates = [ + new Date(1000 * 5), + new Date(1000 * 9), + new Date(1000 * 0), + new Date(1000 * 17), + new Date(1000 * 10), + new Date(1000 * 9), + ]; + + titles.forEach(function(t, i) { + schema.models.Post.create({title: t.title, subject: t.subject, date: dates[i]}, done); + }); + let i = 0; + let tests = 0; + function done(err, obj) { + if (++i === titles.length) { + doFilterAndSortTest(); + doFilterAndSortReverseTest(); + doStringTest(); + doNumberTest(); + doMultipleSortTest(); + doMultipleReverseSortTest(); + } + } + + function compare(a, b) { + if (a.title < b.title) return -1; + if (a.title > b.title) return 1; + return 0; + } + + function doStringTest() { + tests += 1; + schema.models.Post.all({order: 'title'}, function(err, posts) { + if (err) console.log(err); + test.equal(posts.length, 6); + titles.sort(compare).forEach(function(t, i) { + if (posts[i]) test.equal(posts[i].title, t.title); + }); + finished(); + }); + } + + function doNumberTest() { + tests += 1; + schema.models.Post.all({order: 'date'}, function(err, posts) { + if (err) console.log(err); + test.equal(posts.length, 6); + dates.sort(numerically).forEach(function(d, i) { + if (posts[i]) + test.equal(posts[i].date.toString(), d.toString(), 'doNumberTest'); + }); + finished(); + }); + } + + function doFilterAndSortTest() { + tests += 1; + schema.models.Post.all({where: {date: new Date(1000 * 9)}, order: 'title', limit: 3}, function(err, posts) { + if (err) console.log(err); + console.log(posts.length); + test.equal(posts.length, 2, 'Exactly 2 posts returned by query'); + ['Title C', 'Title Z'].forEach(function(t, i) { + if (posts[i]) { + test.equal(posts[i].title, t, 'doFilterAndSortTest'); + } + }); + finished(); + }); + } + + function doFilterAndSortReverseTest() { + tests += 1; + schema.models.Post.all({where: {date: new Date(1000 * 9)}, order: 'title DESC', limit: 3}, + function(err, posts) { + if (err) console.log(err); + test.equal(posts.length, 2, 'Exactly 2 posts returned by query'); + ['Title Z', 'Title C'].forEach(function(t, i) { + if (posts[i]) { + test.equal(posts[i].title, t, 'doFilterAndSortReverseTest'); + } + }); + finished(); + }); + } + + function doMultipleSortTest() { + tests += 1; + schema.models.Post.all({order: 'title ASC, subject ASC'}, function(err, posts) { + if (err) console.log(err); + test.equal(posts.length, 6); + test.equal(posts[0].title, 'Title A'); + test.equal(posts[0].subject, 'A'); + test.equal(posts[1].title, 'Title A'); + test.equal(posts[1].subject, 'B'); + test.equal(posts[5].title, 'Title Z'); + finished(); + }); + } + + function doMultipleReverseSortTest() { + tests += 1; + schema.models.Post.all({order: 'title ASC, subject DESC'}, function(err, posts) { + if (err) console.log(err); + test.equal(posts.length, 6); + test.equal(posts[0].title, 'Title A'); + test.equal(posts[0].subject, 'B'); + test.equal(posts[1].title, 'Title A'); + test.equal(posts[1].subject, 'A'); + test.equal(posts[5].title, 'Title Z'); + finished(); + }); + } + + let fin = 0; + function finished() { + if (++fin === tests) { + test.done(); + } + } + + // TODO: do mixed test, do real dates tests, ensure that dates stored in UNIX timestamp format + + function numerically(a, b) { + return a - b; + } +}); + +commonTest.it('should count posts', function(test) { + test.expect(2); + schema.models.Post.count({title: 'Title A'}, function(err, cnt) { + test.ifError(err); + test.equal(cnt, 2); + test.done(); + }); +}); + +commonTest.it('should delete a post', function(test) { + schema.models.Post.all({ + where: { + title: 'Title Z', + }, + }, function(err, posts) { + test.ifError(err); + test.equal(posts.length, 1); + const id = posts[0].id; + posts[0].destroy(function(err) { + test.ifError(err); + schema.models.Post.find(id, function(err, post) { + test.ifError(err); + test.equal(post, null); + test.done(); + }); + }); + }); +}); + +commonTest.it('should delete all posts', function(test) { + test.expect(3); + schema.models.Post.destroyAll(function(err) { + test.ifError(err); + schema.models.Post.count(function(err, cnt) { + test.ifError(err); + test.equal(cnt, 0); + test.done(); + }); + }); +}); + +// custom primary keys not quite working :(, hopefully 1602 will implement that functionality in jugglingdb soon. +commonTest.it('should support custom primary key', function(test) { + test.expect(3); + const AppliesTo = schema.define('AppliesTo', { + AppliesToID: { + type: Number, + primaryKey: true, + }, + Title: { + type: String, + limit: 100, + }, + Identifier: { + type: String, + limit: 100, + }, + Editable: { + type: Number, + }, + }); + + schema.automigrate(function(err) { + test.ifError(err); + AppliesTo.create({Title: 'custom key', Identifier: 'ck', Editable: false}, function(err, data) { + test.ifError(err); + test.notStrictEqual(typeof data.AppliesToID, 'undefined'); + test.done(); + }); + }); +}); diff --git a/test/connection.js b/test/connection.js new file mode 100644 index 0000000..a162010 --- /dev/null +++ b/test/connection.js @@ -0,0 +1,92 @@ +// Copyright IBM Corp. 2016,2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +/* eslint-env node, mocha */ +'use strict'; +require('./init.js'); +const assert = require('assert'); +const DataSource = require('loopback-datasource-juggler').DataSource; +const url = require('url'); +const mssqlConnector = require('../'); + +let config; + +before(function() { + config = global.getConfig(); +}); + +describe('testConnection', function() { + it('should pass with valid settings', function(done) { + const db = new DataSource(mssqlConnector, config); + db.ping(done); + }); + + it('ignores all other settings when url is present', function(done) { + const formatedUrl = generateURL(config); + const dbConfig = { + url: formatedUrl, + host: 'invalid-hostname', + port: 80, + database: 'invalid-database', + username: 'invalid-username', + password: 'invalid-password', + }; + + const db = new DataSource(mssqlConnector, dbConfig); + db.ping(done); + }); +}); + +function generateURL(config) { + const urlObj = { + protocol: 'mssql', + auth: config.user + ':' + config.password, + hostname: config.host, + port: config.port, + pathname: config.database, + query: {encrypt: true}, + slashes: true, + }; + const formatedUrl = url.format(urlObj); + return formatedUrl; +} + +describe('lazyConnect', function() { + const getDS = function(myconfig) { + const db = new DataSource(mssqlConnector, myconfig); + return db; + }; + + it('should skip connect phase (lazyConnect = true)', function(done) { + const dsConfig = { + host: 'invalid-hostname', + port: 80, + lazyConnect: true, + }; + const ds = getDS(dsConfig); + + const errTimeout = setTimeout(function() { + done(); + }, 2000); + ds.on('error', function(err) { + clearTimeout(errTimeout); + done(err); + }); + }); + + it('should report connection error (lazyConnect = false)', function(done) { + const dsConfig = { + host: 'invalid-hostname', + port: 80, + lazyConnect: false, + }; + const ds = getDS(dsConfig); + + ds.on('error', function(err) { + err.message.should.containEql('ENOTFOUND'); + done(); + }); + }); +}); diff --git a/test/discover.test.js b/test/discover.test.js new file mode 100644 index 0000000..026a248 --- /dev/null +++ b/test/discover.test.js @@ -0,0 +1,269 @@ +// Copyright IBM Corp. 2014,2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// 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('./init.js'); +require('should'); + +const assert = require('assert'); + +/* global getDataSource */ +const db = getDataSource(); + +describe('discoverModels', function() { + describe('Discover database schemas', function() { + it('should return an array of db schemas', function(done) { + db.connector.discoverDatabaseSchemas(function(err, schemas) { + if (err) return done(err); + schemas.should.be.an.array; + schemas.length.should.be.above(0); + done(); + }); + }); + }); + + describe('Discover models including views', function() { + it('should return an array of tables and views', function(done) { + db.discoverModelDefinitions({ + views: true, + limit: 3, + }, function(err, models) { + if (err) return done(err); + let views = false; + models.forEach(function(m) { + // console.dir(m); + if (m.type === 'view') { + views = true; + } + }); + assert(views, 'Should have views'); + done(); + }); + }); + }); + + describe('Discover models excluding views', function() { + it('should return an array of only tables', function(done) { + db.discoverModelDefinitions({ + views: false, + limit: 3, + }, function(err, models) { + if (err) return done(err); + let views = false; + models.forEach(function(m) { + // console.dir(m); + if (m.type === 'view') { + views = true; + } + }); + models.should.have.length(3); + assert(!views, 'Should not have views'); + done(); + }); + }); + }); +}); + +describe('Discover models including other users', function() { + it('should return an array of all tables and views', function(done) { + db.discoverModelDefinitions({ + all: true, + limit: 100, + }, function(err, models) { + if (err) return done(err); + let others = false; + models.forEach(function(m) { + // console.dir(m); + if (m.owner !== 'dbo') { + others = true; + } + }); + assert(others, 'Should have tables/views owned by others'); + done(err, models); + }); + }); +}); + +describe('Discover model properties', function() { + describe('Discover a named model', function() { + it('should return an array of columns for product', function(done) { + db.discoverModelProperties('product', function(err, models) { + if (err) return done(err); + models.forEach(function(m) { + // console.dir(m); + assert.equal(m.tableName, 'product'); + }); + done(); + }); + }); + }); +}); + +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) return done(err); + models.forEach(function(m) { + // console.dir(m); + assert.equal(m.tableName, 'product'); + }); + done(); + }); + }); + + it('should return an array of primary keys for dbo.product', function(done) { + db.discoverPrimaryKeys('product', {owner: 'dbo'}, function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + models.forEach(function(m) { + // console.dir(m); + assert(m.tableName === 'product'); + }); + done(null, models); + } + }); + }); + + it('should return just the primary keys and not the foreign keys for inventory', function(done) { + db.discoverPrimaryKeys('inventory', function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + models.forEach(function(m) { + // console.dir(m); + assert(m.tableName === 'inventory'); + assert(m.columnName === 'id'); + assert(m.pkName.match(/_pk$/)); + }); + done(null, models); + } + }); + }); + + it('should return just the primary keys and not the foreign keys for dbo.inventory', function(done) { + db.discoverPrimaryKeys('inventory', {owner: 'dbo'}, function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + models.forEach(function(m) { + // console.dir(m); + assert(m.tableName === 'inventory'); + assert(m.columnName === 'id'); + assert(m.pkName === 'inventory_pk'); + }); + done(null, models); + } + }); + }); + it('should return the primary key for version which consists of two columns', function(done) { + db.discoverPrimaryKeys('version', function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + assert(models.length === 2); + models.forEach(function(m) { + // console.dir(m); + assert(m.tableName === 'version'); + assert(m.pkName.match(/_pk$/)); + }); + done(null, models); + } + }); + }); + it('should return the primary key for dbo.version which consists of two columns', function(done) { + db.discoverPrimaryKeys('version', {owner: 'dbo'}, function(err, models) { + if (err) { + console.error(err); + done(err); + } else { + assert(models.length === 2); + models.forEach(function(m) { + // console.dir(m); + assert(m.tableName === 'version'); + assert(m.pkName.match(/_pk$/)); + }); + done(null, models); + } + }); + }); +}); + +describe('Discover model foreign keys', function() { + it('should return an array of foreign keys for inventory', function(done) { + db.discoverForeignKeys('inventory', function(err, models) { + if (err) return done(err); + models.forEach(function(m) { + // console.dir(m); + assert.equal(m.fkTableName, 'inventory'); + }); + done(); + }); + }); + it('should return an array of foreign keys for dbo.inventory', function(done) { + db.discoverForeignKeys('inventory', {owner: 'dbo'}, function(err, models) { + if (err) return done(err); + models.forEach(function(m) { + // console.dir(m); + assert.equal(m.fkTableName, 'inventory'); + }); + done(); + }); + }); +}); + +describe('Discover model generated columns', function() { + it('should return an array of columns for product and none of them is generated', function(done) { + db.discoverModelProperties('product', function(err, models) { + if (err) return done(err); + models.forEach(function(m) { + // console.dir(m); + assert.equal(m.tableName, 'product'); + assert(!m.generated, 'product table should not have generated (identity) columns'); + }); + done(); + }); + }); + it('should return an array of columns for testgen and the first is generated', function(done) { + db.discoverModelProperties('testgen', function(err, models) { + if (err) return done(err); + models.forEach(function(m) { + // console.dir(m); + assert.equal(m.tableName, 'testgen'); + if (m.columnName === 'id') { + assert(m.generated, 'testgen.id should be a generated (identity) column'); + } + }); + done(); + }); + }); +}); + +describe('Discover adl schema from a table', function() { + it('should return an adl schema for inventory', function(done) { + db.discoverSchema('inventory', {owner: 'dbo'}, function(err, schema) { + // console.log('%j', schema); + assert(schema.name === 'Inventory'); + assert(schema.options.mssql.schema === 'dbo'); + assert(schema.options.mssql.table === 'inventory'); + assert(schema.properties.productId); + assert(schema.properties.productId.type === 'String'); + assert(schema.properties.productId.mssql.columnName === 'product_id'); + assert(schema.properties.locationId); + assert(schema.properties.locationId.type === 'String'); + assert(schema.properties.locationId.mssql.columnName === 'location_id'); + assert(schema.properties.available); + assert(schema.properties.available.type === 'Number'); + assert(schema.properties.total); + assert(schema.properties.total.type === 'Number'); + done(null, schema); + }); + }); +}); diff --git a/test/id.test.js b/test/id.test.js new file mode 100644 index 0000000..af0dc40 --- /dev/null +++ b/test/id.test.js @@ -0,0 +1,242 @@ +// Copyright IBM Corp. 2015,2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +require('./init.js'); +const should = require('should'); +const assert = require('assert'); +const async = require('async'); +let ds; + +before(function() { + /* global getDataSource */ + ds = getDataSource(); +}); + +describe('Manipulating id column', function() { + it('should auto generate id', function(done) { + const schema = + { + name: 'WarehouseTest', + options: { + mssql: { + schema: 'dbo', + table: 'WAREHOUSE_TEST', + }, + }, + properties: { + id: { + type: 'Number', + id: true, + }, + name: { + type: 'String', + required: false, + length: 40, + }, + }, + }; + + const models = ds.modelBuilder.buildModels(schema); + const Model = models.WarehouseTest; + Model.attachTo(ds); + + ds.automigrate(function(err) { + assert(!err); + async.series([ + function(callback) { + Model.destroyAll(callback); + }, + function(callback) { + Model.create({name: 'w1'}, + callback); + }, + function(callback) { + Model.create({name: 'w2'}, + callback); + }, + function(callback) { + Model.create({name: 'w3'}, + callback); + }, + function(callback) { + Model.find({order: 'id asc'}, + function(err, results) { + assert(!err); + results.should.have.lengthOf(3); + for (let i = 0; i < results.length; i++) { + should.equal(results[i].id, i + 1); + } + callback(); + }); + }, + ], done); + }); + }); + + it('should use manual id', function(done) { + const schema = + { + name: 'WarehouseTest', + options: { + idInjection: false, + mssql: { + schema: 'dbo', + table: 'WAREHOUSE_TEST', + }, + }, + properties: { + id: { + type: 'Number', + id: true, + generated: false, + }, + name: { + type: 'String', + required: false, + length: 40, + }, + }, + }; + + const models = ds.modelBuilder.buildModels(schema); + const Model = models.WarehouseTest; + Model.attachTo(ds); + + ds.automigrate(function(err) { + assert(!err); + async.series([ + function(callback) { + Model.destroyAll(callback); + }, + function(callback) { + Model.create({id: 501, name: 'w1'}, + callback); + }, + function(callback) { + Model.find({order: 'id asc'}, + function(err, results) { + assert(!err); + results.should.have.lengthOf(1); + should.equal(results[0].id, 501); + callback(); + }); + }, + ], done); + }); + }); + + it('should create composite key', function(done) { + const schema = + { + name: 'CompositeKeyTest', + options: { + idInjection: false, + mssql: { + schema: 'dbo', + table: 'COMPOSITE_KEY_TEST', + }, + }, + properties: { + idOne: { + type: 'Number', + id: true, + generated: false, + }, + idTwo: { + type: 'Number', + id: true, + generated: false, + }, + name: { + type: 'String', + required: false, + length: 40, + }, + }, + }; + + const models = ds.modelBuilder.buildModels(schema); + const Model = models.CompositeKeyTest; + Model.attachTo(ds); + + ds.automigrate(function(err) { + assert(!err); + async.series([ + function(callback) { + Model.destroyAll(callback); + }, + function(callback) { + Model.create({idOne: 1, idTwo: 2, name: 'w1'}, + callback); + }, + function(callback) { + ds.discoverPrimaryKeys('COMPOSITE_KEY_TEST', function(err, models) { + if (err) return done(err); + assert(models.length === 2); + callback(); + }); + }, + ], done); + }); + }); + + it('should use bigint id', function(done) { + const schema = + { + name: 'WarehouseTest', + options: { + idInjection: false, + mssql: { + schema: 'dbo', + table: 'WAREHOUSE_TEST', + }, + }, + properties: { + id: { + type: 'Number', + id: true, + generated: false, + mssql: { + dataType: 'bigint', + dataPrecision: 20, + dataScale: 0, + }, + }, + name: { + type: 'String', + required: false, + length: 40, + }, + }, + }; + + const models = ds.modelBuilder.buildModels(schema); + const Model = models.WarehouseTest; + Model.attachTo(ds); + + ds.automigrate(function(err) { + assert(!err); + async.series([ + function(callback) { + Model.destroyAll(callback); + }, + function(callback) { + Model.create({id: 962744456683738, name: 'w1'}, + callback); + }, + function(callback) { + Model.find({order: 'id asc'}, + function(err, results) { + assert(!err); + results.should.have.lengthOf(1); + should.equal(results[0].id, 962744456683738); + callback(); + }); + }, + ], done); + }); + }); +}); diff --git a/test/init.js b/test/init.js new file mode 100644 index 0000000..33e717f --- /dev/null +++ b/test/init.js @@ -0,0 +1,72 @@ +// Copyright IBM Corp. 2014,2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +module.exports = require('should'); + +const juggler = require('loopback-datasource-juggler'); +let DataSource = juggler.DataSource; + +let config = {}; +try { + config = require('rc')('loopback', {test: {mssql: {}}}).test.mssql; +} catch (err) { + config = { + user: 'demo', + password: 'L00pBack', + host: 'localhost', + database: 'demo', + supportsOffSetFetch: Math.random() > 0.5, + }; +} + +global.getConfig = function(options) { + const dbConf = { + host: process.env.MSSQL_HOST || config.host || config.hostname || config.server || 'localhost', + port: process.env.MSSQL_PORT || config.port || 1433, + database: process.env.MSSQL_DATABASE || config.database || 'test', + user: process.env.MSSQL_USER || config.user || config.username, + password: process.env.MSSQL_PASSWORD || config.password, + pool: { + max: 10, + min: 0, + idleTimeoutMillis: 30000, + }, + }; + + if (options) { + for (const el in options) { + dbConf[el] = options[el]; + } + } + + if (typeof dbConf.port === 'string') { + dbConf.port = parseInt(dbConf.port); + } + + return dbConf; +}; + +let db; + +global.getDataSource = global.getSchema = function(options) { + /* global getConfig */ + db = new DataSource(require('../'), getConfig(options)); + return db; +}; + +global.resetDataSourceClass = function(ctor) { + DataSource = ctor || juggler.DataSource; + const promise = db ? db.disconnect() : Promise.resolve(); + db = undefined; + return promise; +}; + +global.connectorCapabilities = { + ilike: false, + nilike: false, +}; + +global.sinon = require('sinon'); diff --git a/test/mapping.test.js b/test/mapping.test.js new file mode 100644 index 0000000..1f56ef7 --- /dev/null +++ b/test/mapping.test.js @@ -0,0 +1,120 @@ +// Copyright IBM Corp. 2015,2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +const should = require('should'); +require('./init'); + +const async = require('async'); + +let db; + +before(function() { + /* global getDataSource */ + db = getDataSource(); +}); + +describe('Mapping models', function() { + it('should honor the mssql settings for table/column', function(done) { + const schema = { + name: 'TestInventory', + options: { + idInjection: false, + mssql: { + schema: 'dbo', table: 'INVENTORYTEST', + }, + }, + properties: { + productId: { + type: 'Number', + id: true, + generated: true, + mssql: { + columnName: 'PRODUCT_ID', + nullable: 'N', + }, + }, + locationId: { + type: 'String', + required: true, + length: 20, + mssql: { + columnName: 'LOCATION_ID', + dataType: 'nvarchar', + nullable: 'N', + }, + }, + available: { + type: 'Number', + required: false, + mssql: { + columnName: 'AVAILABLE', + dataType: 'int', + nullable: 'Y', + }, + }, + total: { + type: 'Number', + required: false, + mssql: { + columnName: 'TOTAL', + dataType: 'int', + nullable: 'Y', + }, + }, + }, + }; + const models = db.modelBuilder.buildModels(schema); + const Model = models.TestInventory; + Model.attachTo(db); + + db.automigrate(function(err, data) { + async.series([ + function(callback) { + Model.destroyAll(callback); + }, + function(callback) { + Model.create({locationId: 'l001', available: 10, total: 50}, + callback); + }, + function(callback) { + Model.create({locationId: 'l002', available: 30, total: 40}, + callback); + }, + function(callback) { + Model.create({locationId: 'l001', available: 15, total: 30}, + callback); + }, + function(callback) { + Model.find({fields: ['productId', 'locationId', 'available']}, + function(err, results) { + // console.log(results); + results.should.have.lengthOf(3); + results.forEach(function(r) { + r.should.have.property('productId'); + r.should.have.property('locationId'); + r.should.have.property('available'); + should.equal(r.total, undefined); + }); + callback(null, results); + }); + }, + function(callback) { + Model.find({fields: {'total': false}}, function(err, results) { + // console.log(results); + results.should.have.lengthOf(3); + results.forEach(function(r) { + r.should.have.property('productId'); + r.should.have.property('locationId'); + r.should.have.property('available'); + should.equal(r.total, undefined); + }); + callback(null, results); + }); + }, + ], done); + }); + }); +}); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..3735350 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,4 @@ +--timeout 30000 +--require test/init.js +--reporter spec +--exit diff --git a/test/mssql.test.js b/test/mssql.test.js new file mode 100644 index 0000000..4868852 --- /dev/null +++ b/test/mssql.test.js @@ -0,0 +1,330 @@ +// Copyright IBM Corp. 2015,2020. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +require('./init'); + +const should = require('should'); +let User, Post, PostWithUUID, PostWithStringId, db, dbOSF; + +describe('mssql connector', function() { + before(function() { + /* global getDataSource */ + db = getDataSource(); + dbOSF = getDataSource({ + supportsOffSetFetch: true, + }); + + User = dbOSF.define('User', { + id: {type: Number, generated: true, id: true}, + name: String, + age: Number, + }); + + Post = db.define('PostWithBoolean', { + title: {type: String, length: 255, index: true}, + content: {type: String}, + approved: Boolean, + }); + + PostWithUUID = db.define('PostWithUUID', { + id: {type: String, generated: true, id: true}, + title: {type: String, length: 255, index: true}, + content: {type: String}, + approved: Boolean, + }); + + PostWithStringId = db.define('PostWithStringId', { + id: {type: String, id: true, generated: false}, + title: {type: String, length: 255, index: true}, + content: {type: String}, + rating: {type: Number, mssql: {dataType: 'FLOAT'}}, + approved: Boolean, + }); + }); + + it('should run migration', function(done) { + db.automigrate(['PostWithBoolean', 'PostWithUUID', 'PostWithStringId'], + function(err) { + done(err); + }); + }); + + let post; + it('should support boolean types with true value', function(done) { + Post.create({title: 'T1', content: 'C1', approved: true}, function(err, p) { + should.not.exists(err); + post = p; + Post.findById(p.id, function(err, p) { + should.not.exists(err); + p.should.have.property('approved', true); + done(); + }); + }); + }); + + it('should support updating boolean types with false value', function(done) { + Post.update({id: post.id}, {approved: false}, function(err) { + should.not.exists(err); + Post.findById(post.id, function(err, p) { + should.not.exists(err); + p.should.have.property('approved', false); + done(); + }); + }); + }); + + it('should support boolean types with false value', function(done) { + Post.create({title: 'T2', content: 'C2', approved: false}, function(err, p) { + should.not.exists(err); + post = p; + Post.findById(p.id, function(err, p) { + should.not.exists(err); + p.should.have.property('approved', false); + done(); + }); + }); + }); + + it('should single quote escape', function(done) { + Post.create({title: 'T2', content: 'C,D', approved: false}, function(err, p) { + should.not.exists(err); + post = p; + Post.findById(p.id, function(err, p) { + should.not.exists(err); + p.should.have.property('content', 'C,D'); + done(); + }); + }); + }); + + it('should return the model instance for upsert', function(done) { + Post.upsert({id: post.id, title: 'T2_new', content: 'C2_new', + approved: true}, function(err, p) { + p.should.have.property('id', post.id); + p.should.have.property('title', 'T2_new'); + p.should.have.property('content', 'C2_new'); + p.should.have.property('approved', true); + done(); + }); + }); + + it('should return the model instance for upsert when id is not present', + function(done) { + Post.upsert({title: 'T2_new', content: 'C2_new', approved: true}, + function(err, p) { + p.should.have.property('id'); + p.should.have.property('title', 'T2_new'); + p.should.have.property('content', 'C2_new'); + p.should.have.property('approved', true); + done(); + }); + }); + + it('should escape number values to defect SQL injection in findById', + function(done) { + Post.findById('(SELECT 1+1)', function(err, p) { + should.exists(err); + done(); + }); + }); + + it('should escape number values to defect SQL injection in find', + function(done) { + Post.find({where: {id: '(SELECT 1+1)'}}, function(err, p) { + should.exists(err); + done(); + }); + }); + + it('should escape number values to defect SQL injection in find with gt', + function(done) { + Post.find({where: {id: {gt: '(SELECT 1+1)'}}}, function(err, p) { + should.exists(err); + done(); + }); + }); + + it('should escape number values to defect SQL injection in find - test 2', + function(done) { + Post.find({limit: '(SELECT 1+1)'}, function(err, p) { + should.exists(err); + done(); + }); + }); + + it('should escape number values to defect SQL injection in find with inq', + function(done) { + Post.find({where: {id: {inq: ['(SELECT 1+1)']}}}, function(err, p) { + should.exists(err); + done(); + }); + }); + + it('should avoid SQL injection for parameters containing (?)', + function(done) { + const connector = db.connector; + const value1 = '(?)'; + const value2 = ', 1 ); INSERT INTO SQLI_TEST VALUES (1, 2); --'; + + connector.execute('DROP TABLE SQLI_TEST;', function(err) { + connector.execute('CREATE TABLE SQLI_TEST' + + '(V1 VARCHAR(100), V2 VARCHAR(100) )', + function(err) { + if (err) return done(err); + connector.execute('INSERT INTO SQLI_TEST VALUES ( (?), (?) )', + [value1, value2], function(err) { + if (err) return done(err); + connector.execute('SELECT * FROM SQLI_TEST', function(err, data) { + if (err) return done(err); + data.should.be.eql( + [{V1: '(?)', + V2: ', 1 ); INSERT INTO SQLI_TEST VALUES (1, 2); --'}], + ); + done(); + }); + }); + }); + }); + }); + + it('should allow string array for inq', + function(done) { + Post.find({where: {content: {inq: ['C1', 'C2']}}}, function(err, p) { + should.not.exist(err); + should.exist(p); + p.should.have.length(2); + done(); + }); + }); + + it('should perform an empty inq', + function(done) { + Post.find({where: {id: {inq: []}}}, function(err, p) { + should.not.exist(err); + should.exist(p); + p.should.have.length(0); + done(); + }); + }); + + it('should perform an empty nin', + function(done) { + Post.find({where: {id: {nin: []}}}, function(err, p) { + should.not.exist(err); + should.exist(p); + p.should.have.length(4); + done(); + }); + }); + + it('should support uuid', function(done) { + PostWithUUID.create({title: 'T1', content: 'C1', approved: true}, + function(err, p) { + should.not.exists(err); + p.should.have.property('id'); + // p.id.should.be.a.string(); + PostWithUUID.findById(p.id, function(err, p) { + should.not.exists(err); + p.should.have.property('title', 'T1'); + done(); + }); + }); + }); + + it('should support string id', function(done) { + PostWithStringId.create( + {title: 'T1', content: 'C1', approved: true, rating: 3.5}, + function(err, p) { + should.not.exists(err); + p.should.have.property('id'); + p.id.should.be.a.string; + PostWithStringId.findById(p.id, function(err, p) { + should.not.exists(err); + p.should.have.property('title', 'T1'); + p.should.have.property('rating', 3.5); + done(); + }); + }, + ); + }); + + it('should support limit', function(done) { + Post.find({ + limit: 3, + }, (err, result) => { + should.not.exist(err); + should.exist(result); + result.should.have.length(3); + done(); + }); + }); + + it('should support limit with \'supportsOffsetFetch\'', function(done) { + User.find({ + limit: 3, + }, (err, result) => { + should.not.exist(err); + should.exist(result); + result.should.have.length(3); + done(); + }); + }); + + context('regexp operator', function() { + beforeEach(function deleteExistingTestFixtures(done) { + Post.destroyAll(done); + }); + beforeEach(function createTestFixtures(done) { + Post.create([ + {title: 'a', content: 'AAA'}, + {title: 'b', content: 'BBB'}, + ], done); + }); + beforeEach(function addSpy() { + /* global sinon */ + sinon.stub(console, 'warn'); + }); + afterEach(function removeSpy() { + console.warn.restore(); + }); + after(function deleteTestFixtures(done) { + Post.destroyAll(done); + }); + + context('with regex strings', function() { + it('should print a warning and return an error', function(done) { + Post.find({where: {content: {regexp: '^A'}}}, function(err, posts) { + console.warn.calledOnce.should.be.ok; + should.exist(err); + done(); + }); + }); + }); + + context('with regex literals', function() { + it('should print a warning and return an error', function(done) { + Post.find({where: {content: {regexp: /^A/}}}, function(err, posts) { + console.warn.calledOnce.should.be.ok; + should.exist(err); + done(); + }); + }); + }); + + context('with regex objects', function() { + it('should print a warning and return an error', function(done) { + Post.find( + {where: {content: {regexp: new RegExp(/^A/)}}}, + function(err, posts) { + console.warn.calledOnce.should.be.ok; + should.exist(err); + done(); + }, + ); + }); + }); + }); +}); diff --git a/test/tables.sql b/test/tables.sql new file mode 100644 index 0000000..569cc92 --- /dev/null +++ b/test/tables.sql @@ -0,0 +1,759 @@ +IF OBJECT_ID('dbo.inventory', 'U') IS NOT NULL +DROP TABLE inventory; + +IF OBJECT_ID('dbo.reservation', 'U') IS NOT NULL +DROP TABLE reservation; + +IF OBJECT_ID('dbo.version', 'U') IS NOT NULL +DROP TABLE version; + +IF OBJECT_ID('dbo.customer', 'U') IS NOT NULL +DROP TABLE customer; + +IF OBJECT_ID('dbo.location', 'U') IS NOT NULL +DROP TABLE location; + +IF OBJECT_ID('dbo.product', 'U') IS NOT NULL +DROP TABLE product; + +IF OBJECT_ID('dbo.session', 'U') IS NOT NULL +DROP TABLE session; + +IF OBJECT_ID('sa.movies', 'U') IS NOT NULL +DROP TABLE sa.movies; + +IF OBJECT_ID('inventory_view','v') IS NOT NULL +DROP VIEW inventory_view; + +GO + + IF NOT EXISTS ( + SELECT schema_name + FROM information_schema.schemata + WHERE schema_name = 'sa' ) + + BEGIN + EXEC sp_executesql N'CREATE SCHEMA sa' + END + +GO + + create table customer + ( id varchar(64) not null, + username varchar(1024), + email varchar(1024), + password varchar(1024), + name varchar(40), + military_agency varchar(20), + realm varchar(1024), + emailverified char(1), + verificationtoken varchar(1024), + credentials varchar(1024), + challenges varchar(1024), + status varchar(1024), + created date, + lastupdated date + ) ; + + create table inventory + ( id varchar(64) not null, + product_id varchar(64), + location_id varchar(64), + available integer, + total integer + ) ; + + create table reservation + ( id varchar(64) not null, + product_id varchar(64), + location_id varchar(64), + customer_id varchar(64), + qty integer, + status varchar(20), + reserve_date date, + pickup_date date, + return_date date + ) ; + + create table location + ( id varchar(64) not null, + street varchar(64), + city varchar(64), + zipcode varchar(16), + name varchar(32), + geo varchar(32) + ) ; + + create table product + ( id varchar(64) not null, + name varchar(64), + audible_range integer, + effective_range integer, + rounds integer, + extras varchar(64), + fire_modes varchar(64) + ) ; + + create table session + ( id varchar(64) not null, + uid varchar(1024), + ttl integer + ) ; + + create table version + ( product_id varchar(64) not null, + version integer not null + ) ; + + create table movies + ( id varchar(64) not null, + name varchar(1024), + year integer + ) ; + + ALTER SCHEMA sa TRANSFER dbo.movies + +insert into inventory (id,product_id,location_id,available,total) values ('441','6','91',8,19); +insert into inventory (id,product_id,location_id,available,total) values ('442','7','91',21,23); +insert into inventory (id,product_id,location_id,available,total) values ('443','8','91',35,63); +insert into inventory (id,product_id,location_id,available,total) values ('444','9','91',0,7); +insert into inventory (id,product_id,location_id,available,total) values ('445','10','91',0,2); +insert into inventory (id,product_id,location_id,available,total) values ('446','11','91',1,6); +insert into inventory (id,product_id,location_id,available,total) values ('447','12','91',67,77); +insert into inventory (id,product_id,location_id,available,total) values ('448','13','91',7,51); +insert into inventory (id,product_id,location_id,available,total) values ('449','14','91',39,96); +insert into inventory (id,product_id,location_id,available,total) values ('450','15','91',36,74); +insert into inventory (id,product_id,location_id,available,total) values ('451','16','91',15,73); +insert into inventory (id,product_id,location_id,available,total) values ('453','18','91',0,19); +insert into inventory (id,product_id,location_id,available,total) values ('452','17','91',36,63); +insert into inventory (id,product_id,location_id,available,total) values ('454','19','91',24,94); +insert into inventory (id,product_id,location_id,available,total) values ('455','20','91',8,38); +insert into inventory (id,product_id,location_id,available,total) values ('456','21','91',41,58); +insert into inventory (id,product_id,location_id,available,total) values ('457','22','91',18,22); +insert into inventory (id,product_id,location_id,available,total) values ('458','23','91',25,37); +insert into inventory (id,product_id,location_id,available,total) values ('459','24','91',39,60); +insert into inventory (id,product_id,location_id,available,total) values ('460','25','91',30,55); +insert into inventory (id,product_id,location_id,available,total) values ('461','26','91',4,4); +insert into inventory (id,product_id,location_id,available,total) values ('462','27','91',6,17); +insert into inventory (id,product_id,location_id,available,total) values ('463','28','91',63,82); +insert into inventory (id,product_id,location_id,available,total) values ('464','29','91',30,76); +insert into inventory (id,product_id,location_id,available,total) values ('465','30','91',13,31); +insert into inventory (id,product_id,location_id,available,total) values ('466','31','91',10,59); +insert into inventory (id,product_id,location_id,available,total) values ('467','32','91',39,80); +insert into inventory (id,product_id,location_id,available,total) values ('468','33','91',69,89); +insert into inventory (id,product_id,location_id,available,total) values ('469','34','91',62,93); +insert into inventory (id,product_id,location_id,available,total) values ('470','35','91',13,27); +insert into inventory (id,product_id,location_id,available,total) values ('471','36','91',8,22); +insert into inventory (id,product_id,location_id,available,total) values ('472','37','91',0,31); +insert into inventory (id,product_id,location_id,available,total) values ('473','38','91',9,79); +insert into inventory (id,product_id,location_id,available,total) values ('474','39','91',6,49); +insert into inventory (id,product_id,location_id,available,total) values ('475','40','91',39,40); +insert into inventory (id,product_id,location_id,available,total) values ('476','41','91',1,22); +insert into inventory (id,product_id,location_id,available,total) values ('477','42','91',12,82); +insert into inventory (id,product_id,location_id,available,total) values ('478','43','91',1,7); +insert into inventory (id,product_id,location_id,available,total) values ('479','44','91',15,26); +insert into inventory (id,product_id,location_id,available,total) values ('480','45','91',22,31); +insert into inventory (id,product_id,location_id,available,total) values ('481','46','91',64,65); +insert into inventory (id,product_id,location_id,available,total) values ('482','47','91',10,99); +insert into inventory (id,product_id,location_id,available,total) values ('483','48','91',26,56); +insert into inventory (id,product_id,location_id,available,total) values ('484','49','91',14,19); +insert into inventory (id,product_id,location_id,available,total) values ('485','50','91',51,55); +insert into inventory (id,product_id,location_id,available,total) values ('486','51','91',25,29); +insert into inventory (id,product_id,location_id,available,total) values ('487','52','91',31,37); +insert into inventory (id,product_id,location_id,available,total) values ('488','53','91',35,71); +insert into inventory (id,product_id,location_id,available,total) values ('489','54','91',5,61); +insert into inventory (id,product_id,location_id,available,total) values ('490','55','91',4,26); +insert into inventory (id,product_id,location_id,available,total) values ('491','56','91',29,50); +insert into inventory (id,product_id,location_id,available,total) values ('492','57','91',15,34); +insert into inventory (id,product_id,location_id,available,total) values ('493','58','91',30,38); +insert into inventory (id,product_id,location_id,available,total) values ('494','59','91',54,71); +insert into inventory (id,product_id,location_id,available,total) values ('495','60','91',6,43); +insert into inventory (id,product_id,location_id,available,total) values ('496','61','91',40,80); +insert into inventory (id,product_id,location_id,available,total) values ('497','62','91',32,33); +insert into inventory (id,product_id,location_id,available,total) values ('498','63','91',44,53); +insert into inventory (id,product_id,location_id,available,total) values ('499','64','91',10,68); +insert into inventory (id,product_id,location_id,available,total) values ('500','65','91',11,13); +insert into inventory (id,product_id,location_id,available,total) values ('501','66','91',7,40); +insert into inventory (id,product_id,location_id,available,total) values ('502','67','91',5,20); +insert into inventory (id,product_id,location_id,available,total) values ('503','68','91',30,40); +insert into inventory (id,product_id,location_id,available,total) values ('504','69','91',6,48); +insert into inventory (id,product_id,location_id,available,total) values ('505','70','91',7,53); +insert into inventory (id,product_id,location_id,available,total) values ('506','71','91',2,21); +insert into inventory (id,product_id,location_id,available,total) values ('507','72','91',25,56); +insert into inventory (id,product_id,location_id,available,total) values ('508','73','91',13,85); +insert into inventory (id,product_id,location_id,available,total) values ('509','74','91',63,67); +insert into inventory (id,product_id,location_id,available,total) values ('510','75','91',9,11); +insert into inventory (id,product_id,location_id,available,total) values ('511','76','91',18,46); +insert into inventory (id,product_id,location_id,available,total) values ('512','77','91',7,88); +insert into inventory (id,product_id,location_id,available,total) values ('513','78','91',36,55); +insert into inventory (id,product_id,location_id,available,total) values ('514','79','91',8,33); +insert into inventory (id,product_id,location_id,available,total) values ('515','80','91',63,73); +insert into inventory (id,product_id,location_id,available,total) values ('517','82','91',2,5); +insert into inventory (id,product_id,location_id,available,total) values ('516','81','91',36,71); +insert into inventory (id,product_id,location_id,available,total) values ('518','83','91',11,11); +insert into inventory (id,product_id,location_id,available,total) values ('519','84','91',21,39); +insert into inventory (id,product_id,location_id,available,total) values ('520','85','91',90,91); +insert into inventory (id,product_id,location_id,available,total) values ('521','86','91',1,2); +insert into inventory (id,product_id,location_id,available,total) values ('522','87','91',36,47); +insert into inventory (id,product_id,location_id,available,total) values ('523','2','92',6,7); +insert into inventory (id,product_id,location_id,available,total) values ('524','3','92',15,23); +insert into inventory (id,product_id,location_id,available,total) values ('525','4','92',1,1); +insert into inventory (id,product_id,location_id,available,total) values ('527','6','92',22,24); +insert into inventory (id,product_id,location_id,available,total) values ('526','5','92',37,42); +insert into inventory (id,product_id,location_id,available,total) values ('528','7','92',12,13); +insert into inventory (id,product_id,location_id,available,total) values ('529','8','92',4,25); +insert into inventory (id,product_id,location_id,available,total) values ('531','10','92',9,31); +insert into inventory (id,product_id,location_id,available,total) values ('530','9','92',32,87); +insert into inventory (id,product_id,location_id,available,total) values ('532','11','92',2,38); +insert into inventory (id,product_id,location_id,available,total) values ('533','12','92',66,88); +insert into inventory (id,product_id,location_id,available,total) values ('534','13','92',4,15); +insert into inventory (id,product_id,location_id,available,total) values ('535','14','92',9,88); +insert into inventory (id,product_id,location_id,available,total) values ('536','15','92',18,72); +insert into inventory (id,product_id,location_id,available,total) values ('537','16','92',13,26); +insert into inventory (id,product_id,location_id,available,total) values ('538','17','92',20,55); +insert into inventory (id,product_id,location_id,available,total) values ('539','18','92',17,76); +insert into inventory (id,product_id,location_id,available,total) values ('540','19','92',28,58); +insert into inventory (id,product_id,location_id,available,total) values ('542','21','92',7,12); +insert into inventory (id,product_id,location_id,available,total) values ('541','20','92',78,99); +insert into inventory (id,product_id,location_id,available,total) values ('543','22','92',4,13); +insert into inventory (id,product_id,location_id,available,total) values ('544','23','92',12,96); +insert into inventory (id,product_id,location_id,available,total) values ('545','24','92',82,84); +insert into inventory (id,product_id,location_id,available,total) values ('546','25','92',29,64); +insert into inventory (id,product_id,location_id,available,total) values ('547','26','92',5,7); +insert into inventory (id,product_id,location_id,available,total) values ('548','27','92',3,35); +insert into inventory (id,product_id,location_id,available,total) values ('549','28','92',23,46); +insert into inventory (id,product_id,location_id,available,total) values ('550','29','92',21,39); +insert into inventory (id,product_id,location_id,available,total) values ('551','30','92',19,21); +insert into inventory (id,product_id,location_id,available,total) values ('552','31','92',24,73); +insert into inventory (id,product_id,location_id,available,total) values ('553','32','92',51,89); +insert into inventory (id,product_id,location_id,available,total) values ('554','33','92',22,32); +insert into inventory (id,product_id,location_id,available,total) values ('555','34','92',56,95); +insert into inventory (id,product_id,location_id,available,total) values ('556','35','92',47,95); +insert into inventory (id,product_id,location_id,available,total) values ('557','36','92',17,24); +insert into inventory (id,product_id,location_id,available,total) values ('558','37','92',0,0); +insert into inventory (id,product_id,location_id,available,total) values ('559','38','92',14,53); +insert into inventory (id,product_id,location_id,available,total) values ('560','39','92',65,67); +insert into inventory (id,product_id,location_id,available,total) values ('561','40','92',64,95); +insert into inventory (id,product_id,location_id,available,total) values ('562','41','92',5,5); +insert into inventory (id,product_id,location_id,available,total) values ('563','42','92',7,10); +insert into inventory (id,product_id,location_id,available,total) values ('564','43','92',34,45); +insert into inventory (id,product_id,location_id,available,total) values ('565','44','92',0,3); +insert into inventory (id,product_id,location_id,available,total) values ('566','45','92',20,67); +insert into inventory (id,product_id,location_id,available,total) values ('567','46','92',58,92); +insert into inventory (id,product_id,location_id,available,total) values ('568','47','92',21,70); +insert into inventory (id,product_id,location_id,available,total) values ('569','48','92',56,62); +insert into inventory (id,product_id,location_id,available,total) values ('570','49','92',0,5); +insert into inventory (id,product_id,location_id,available,total) values ('571','50','92',16,97); +insert into inventory (id,product_id,location_id,available,total) values ('572','51','92',6,46); +insert into inventory (id,product_id,location_id,available,total) values ('573','52','92',58,84); +insert into inventory (id,product_id,location_id,available,total) values ('574','53','92',25,42); +insert into inventory (id,product_id,location_id,available,total) values ('575','54','92',13,40); +insert into inventory (id,product_id,location_id,available,total) values ('576','55','92',18,34); +insert into inventory (id,product_id,location_id,available,total) values ('577','56','92',44,92); +insert into inventory (id,product_id,location_id,available,total) values ('578','57','92',0,19); +insert into inventory (id,product_id,location_id,available,total) values ('579','58','92',13,67); +insert into inventory (id,product_id,location_id,available,total) values ('580','59','92',18,38); +insert into inventory (id,product_id,location_id,available,total) values ('581','60','92',7,7); +insert into inventory (id,product_id,location_id,available,total) values ('582','61','92',6,53); +insert into inventory (id,product_id,location_id,available,total) values ('583','62','92',4,25); +insert into inventory (id,product_id,location_id,available,total) values ('584','63','92',31,59); +insert into inventory (id,product_id,location_id,available,total) values ('585','64','92',25,40); +insert into inventory (id,product_id,location_id,available,total) values ('586','65','92',2,81); +insert into inventory (id,product_id,location_id,available,total) values ('587','66','92',23,81); +insert into inventory (id,product_id,location_id,available,total) values ('588','67','92',9,33); +insert into inventory (id,product_id,location_id,available,total) values ('589','68','92',2,37); +insert into inventory (id,product_id,location_id,available,total) values ('590','69','92',53,64); +insert into inventory (id,product_id,location_id,available,total) values ('591','70','92',21,22); +insert into inventory (id,product_id,location_id,available,total) values ('592','71','92',7,45); +insert into inventory (id,product_id,location_id,available,total) values ('593','72','92',9,25); +insert into inventory (id,product_id,location_id,available,total) values ('594','73','92',0,40); +insert into inventory (id,product_id,location_id,available,total) values ('595','74','92',21,34); +insert into inventory (id,product_id,location_id,available,total) values ('596','75','92',33,87); +insert into inventory (id,product_id,location_id,available,total) values ('597','76','92',44,48); +insert into inventory (id,product_id,location_id,available,total) values ('598','77','92',64,69); +insert into inventory (id,product_id,location_id,available,total) values ('599','78','92',31,56); +insert into inventory (id,product_id,location_id,available,total) values ('600','79','92',11,12); +insert into inventory (id,product_id,location_id,available,total) values ('601','80','92',3,7); +insert into inventory (id,product_id,location_id,available,total) values ('602','81','92',26,74); +insert into inventory (id,product_id,location_id,available,total) values ('603','82','92',29,46); +insert into inventory (id,product_id,location_id,available,total) values ('604','83','92',1,5); +insert into inventory (id,product_id,location_id,available,total) values ('605','84','92',35,37); +insert into inventory (id,product_id,location_id,available,total) values ('606','85','92',12,100); +insert into inventory (id,product_id,location_id,available,total) values ('607','86','92',9,18); +insert into inventory (id,product_id,location_id,available,total) values ('608','87','92',49,64); +insert into inventory (id,product_id,location_id,available,total) values ('95','4','87',18,30); +insert into inventory (id,product_id,location_id,available,total) values ('97','6','87',10,21); +insert into inventory (id,product_id,location_id,available,total) values ('96','5','87',3,38); +insert into inventory (id,product_id,location_id,available,total) values ('98','7','87',43,58); +insert into inventory (id,product_id,location_id,available,total) values ('99','8','87',6,12); +insert into inventory (id,product_id,location_id,available,total) values ('100','9','87',0,3); +insert into inventory (id,product_id,location_id,available,total) values ('101','10','87',0,31); +insert into inventory (id,product_id,location_id,available,total) values ('102','11','87',73,93); +insert into inventory (id,product_id,location_id,available,total) values ('103','12','87',22,25); +insert into inventory (id,product_id,location_id,available,total) values ('104','13','87',44,70); +insert into inventory (id,product_id,location_id,available,total) values ('105','14','87',26,50); +insert into inventory (id,product_id,location_id,available,total) values ('106','15','87',36,83); +insert into inventory (id,product_id,location_id,available,total) values ('107','16','87',20,59); +insert into inventory (id,product_id,location_id,available,total) values ('108','17','87',28,44); +insert into inventory (id,product_id,location_id,available,total) values ('109','18','87',5,50); +insert into inventory (id,product_id,location_id,available,total) values ('110','19','87',2,29); +insert into inventory (id,product_id,location_id,available,total) values ('111','20','87',38,54); +insert into inventory (id,product_id,location_id,available,total) values ('112','21','87',4,29); +insert into inventory (id,product_id,location_id,available,total) values ('113','22','87',1,59); +insert into inventory (id,product_id,location_id,available,total) values ('114','23','87',20,36); +insert into inventory (id,product_id,location_id,available,total) values ('115','24','87',10,10); +insert into inventory (id,product_id,location_id,available,total) values ('116','25','87',58,60); +insert into inventory (id,product_id,location_id,available,total) values ('117','26','87',0,18); +insert into inventory (id,product_id,location_id,available,total) values ('118','27','87',29,50); +insert into inventory (id,product_id,location_id,available,total) values ('119','28','87',24,34); +insert into inventory (id,product_id,location_id,available,total) values ('120','29','87',36,43); +insert into inventory (id,product_id,location_id,available,total) values ('121','30','87',43,64); +insert into inventory (id,product_id,location_id,available,total) values ('122','31','87',79,90); +insert into inventory (id,product_id,location_id,available,total) values ('123','32','87',13,13); +insert into inventory (id,product_id,location_id,available,total) values ('124','33','87',9,60); +insert into inventory (id,product_id,location_id,available,total) values ('125','34','87',7,13); +insert into inventory (id,product_id,location_id,available,total) values ('126','35','87',43,54); +insert into inventory (id,product_id,location_id,available,total) values ('127','36','87',67,69); +insert into inventory (id,product_id,location_id,available,total) values ('128','37','87',1,15); +insert into inventory (id,product_id,location_id,available,total) values ('129','38','87',36,44); +insert into inventory (id,product_id,location_id,available,total) values ('130','39','87',1,17); +insert into inventory (id,product_id,location_id,available,total) values ('131','40','87',13,16); +insert into inventory (id,product_id,location_id,available,total) values ('132','41','87',24,64); +insert into inventory (id,product_id,location_id,available,total) values ('133','42','87',87,99); +insert into inventory (id,product_id,location_id,available,total) values ('134','43','87',27,99); +insert into inventory (id,product_id,location_id,available,total) values ('135','44','87',71,71); +insert into inventory (id,product_id,location_id,available,total) values ('136','45','87',9,20); +insert into inventory (id,product_id,location_id,available,total) values ('137','46','87',9,67); +insert into inventory (id,product_id,location_id,available,total) values ('138','47','87',19,21); +insert into inventory (id,product_id,location_id,available,total) values ('139','48','87',5,5); +insert into inventory (id,product_id,location_id,available,total) values ('140','49','87',82,91); +insert into inventory (id,product_id,location_id,available,total) values ('141','50','87',27,42); +insert into inventory (id,product_id,location_id,available,total) values ('142','51','87',51,60); +insert into inventory (id,product_id,location_id,available,total) values ('143','52','87',8,72); +insert into inventory (id,product_id,location_id,available,total) values ('145','54','87',3,71); +insert into inventory (id,product_id,location_id,available,total) values ('144','53','87',5,13); +insert into inventory (id,product_id,location_id,available,total) values ('146','55','87',55,56); +insert into inventory (id,product_id,location_id,available,total) values ('147','56','87',9,90); +insert into inventory (id,product_id,location_id,available,total) values ('148','57','87',3,18); +insert into inventory (id,product_id,location_id,available,total) values ('149','58','87',2,14); +insert into inventory (id,product_id,location_id,available,total) values ('150','59','87',54,95); +insert into inventory (id,product_id,location_id,available,total) values ('151','60','87',62,70); +insert into inventory (id,product_id,location_id,available,total) values ('152','61','87',18,50); +insert into inventory (id,product_id,location_id,available,total) values ('153','62','87',60,78); +insert into inventory (id,product_id,location_id,available,total) values ('154','63','87',23,59); +insert into inventory (id,product_id,location_id,available,total) values ('155','64','87',14,23); +insert into inventory (id,product_id,location_id,available,total) values ('156','65','87',2,97); +insert into inventory (id,product_id,location_id,available,total) values ('157','66','87',49,50); +insert into inventory (id,product_id,location_id,available,total) values ('158','67','87',47,93); +insert into inventory (id,product_id,location_id,available,total) values ('159','68','87',34,42); +insert into inventory (id,product_id,location_id,available,total) values ('160','69','87',3,18); +insert into inventory (id,product_id,location_id,available,total) values ('161','70','87',37,84); +insert into inventory (id,product_id,location_id,available,total) values ('162','71','87',22,40); +insert into inventory (id,product_id,location_id,available,total) values ('163','72','87',8,61); +insert into inventory (id,product_id,location_id,available,total) values ('164','73','87',2,3); +insert into inventory (id,product_id,location_id,available,total) values ('165','74','87',10,16); +insert into inventory (id,product_id,location_id,available,total) values ('166','75','87',53,89); +insert into inventory (id,product_id,location_id,available,total) values ('167','76','87',35,60); +insert into inventory (id,product_id,location_id,available,total) values ('168','77','87',57,80); +insert into inventory (id,product_id,location_id,available,total) values ('169','78','87',53,81); +insert into inventory (id,product_id,location_id,available,total) values ('170','79','87',32,54); +insert into inventory (id,product_id,location_id,available,total) values ('171','80','87',1,4); +insert into inventory (id,product_id,location_id,available,total) values ('172','81','87',78,86); +insert into inventory (id,product_id,location_id,available,total) values ('173','82','87',11,21); +insert into inventory (id,product_id,location_id,available,total) values ('174','83','87',28,81); +insert into inventory (id,product_id,location_id,available,total) values ('175','84','87',2,57); +insert into inventory (id,product_id,location_id,available,total) values ('176','85','87',30,37); +insert into inventory (id,product_id,location_id,available,total) values ('177','86','87',17,80); +insert into inventory (id,product_id,location_id,available,total) values ('179','2','88',10,10); +insert into inventory (id,product_id,location_id,available,total) values ('178','87','87',1,9); +insert into inventory (id,product_id,location_id,available,total) values ('180','3','88',1,1); +insert into inventory (id,product_id,location_id,available,total) values ('181','4','88',8,27); +insert into inventory (id,product_id,location_id,available,total) values ('182','5','88',3,38); +insert into inventory (id,product_id,location_id,available,total) values ('183','6','88',28,76); +insert into inventory (id,product_id,location_id,available,total) values ('184','7','88',40,83); +insert into inventory (id,product_id,location_id,available,total) values ('185','8','88',1,4); +insert into inventory (id,product_id,location_id,available,total) values ('186','9','88',87,95); +insert into inventory (id,product_id,location_id,available,total) values ('187','10','88',29,35); +insert into inventory (id,product_id,location_id,available,total) values ('188','11','88',10,69); +insert into inventory (id,product_id,location_id,available,total) values ('189','12','88',32,86); +insert into inventory (id,product_id,location_id,available,total) values ('190','13','88',27,28); +insert into inventory (id,product_id,location_id,available,total) values ('191','14','88',59,66); +insert into inventory (id,product_id,location_id,available,total) values ('192','15','88',59,70); +insert into inventory (id,product_id,location_id,available,total) values ('193','16','88',43,70); +insert into inventory (id,product_id,location_id,available,total) values ('194','17','88',50,63); +insert into inventory (id,product_id,location_id,available,total) values ('195','18','88',8,20); +insert into inventory (id,product_id,location_id,available,total) values ('196','19','88',20,29); +insert into inventory (id,product_id,location_id,available,total) values ('197','20','88',36,50); +insert into inventory (id,product_id,location_id,available,total) values ('198','21','88',40,63); +insert into inventory (id,product_id,location_id,available,total) values ('199','22','88',4,96); +insert into inventory (id,product_id,location_id,available,total) values ('200','23','88',70,98); +insert into inventory (id,product_id,location_id,available,total) values ('201','24','88',1,1); +insert into inventory (id,product_id,location_id,available,total) values ('202','25','88',17,45); +insert into inventory (id,product_id,location_id,available,total) values ('203','26','88',52,97); +insert into inventory (id,product_id,location_id,available,total) values ('204','27','88',0,0); +insert into inventory (id,product_id,location_id,available,total) values ('205','28','88',97,98); +insert into inventory (id,product_id,location_id,available,total) values ('206','29','88',26,80); +insert into inventory (id,product_id,location_id,available,total) values ('207','30','88',11,33); +insert into inventory (id,product_id,location_id,available,total) values ('208','31','88',10,21); +insert into inventory (id,product_id,location_id,available,total) values ('209','32','88',14,36); +insert into inventory (id,product_id,location_id,available,total) values ('210','33','88',71,86); +insert into inventory (id,product_id,location_id,available,total) values ('211','34','88',85,100); +insert into inventory (id,product_id,location_id,available,total) values ('212','35','88',3,45); +insert into inventory (id,product_id,location_id,available,total) values ('213','36','88',0,3); +insert into inventory (id,product_id,location_id,available,total) values ('214','37','88',17,71); +insert into inventory (id,product_id,location_id,available,total) values ('215','38','88',41,75); +insert into inventory (id,product_id,location_id,available,total) values ('216','39','88',37,41); +insert into inventory (id,product_id,location_id,available,total) values ('217','40','88',37,49); +insert into inventory (id,product_id,location_id,available,total) values ('218','41','88',1,2); +insert into inventory (id,product_id,location_id,available,total) values ('219','42','88',49,72); +insert into inventory (id,product_id,location_id,available,total) values ('220','43','88',24,38); +insert into inventory (id,product_id,location_id,available,total) values ('221','44','88',6,66); +insert into inventory (id,product_id,location_id,available,total) values ('222','45','88',31,49); +insert into inventory (id,product_id,location_id,available,total) values ('223','46','88',9,10); +insert into inventory (id,product_id,location_id,available,total) values ('224','47','88',57,72); +insert into inventory (id,product_id,location_id,available,total) values ('225','48','88',17,24); +insert into inventory (id,product_id,location_id,available,total) values ('226','49','88',41,61); +insert into inventory (id,product_id,location_id,available,total) values ('227','50','88',33,87); +insert into inventory (id,product_id,location_id,available,total) values ('228','51','88',11,25); +insert into inventory (id,product_id,location_id,available,total) values ('229','52','88',1,8); +insert into inventory (id,product_id,location_id,available,total) values ('230','53','88',14,64); +insert into inventory (id,product_id,location_id,available,total) values ('231','54','88',50,89); +insert into inventory (id,product_id,location_id,available,total) values ('232','55','88',16,66); +insert into inventory (id,product_id,location_id,available,total) values ('233','56','88',0,6); +insert into inventory (id,product_id,location_id,available,total) values ('234','57','88',18,32); +insert into inventory (id,product_id,location_id,available,total) values ('235','58','88',6,6); +insert into inventory (id,product_id,location_id,available,total) values ('236','59','88',68,84); +insert into inventory (id,product_id,location_id,available,total) values ('237','60','88',50,74); +insert into inventory (id,product_id,location_id,available,total) values ('238','61','88',7,18); +insert into inventory (id,product_id,location_id,available,total) values ('239','62','88',14,49); +insert into inventory (id,product_id,location_id,available,total) values ('240','63','88',3,3); +insert into inventory (id,product_id,location_id,available,total) values ('241','64','88',21,83); +insert into inventory (id,product_id,location_id,available,total) values ('242','65','88',48,90); +insert into inventory (id,product_id,location_id,available,total) values ('243','66','88',11,65); +insert into inventory (id,product_id,location_id,available,total) values ('244','67','88',29,90); +insert into inventory (id,product_id,location_id,available,total) values ('245','68','88',44,45); +insert into inventory (id,product_id,location_id,available,total) values ('246','69','88',23,30); +insert into inventory (id,product_id,location_id,available,total) values ('247','70','88',53,71); +insert into inventory (id,product_id,location_id,available,total) values ('248','71','88',50,76); +insert into inventory (id,product_id,location_id,available,total) values ('249','72','88',13,20); +insert into inventory (id,product_id,location_id,available,total) values ('250','73','88',6,8); +insert into inventory (id,product_id,location_id,available,total) values ('251','74','88',7,11); +insert into inventory (id,product_id,location_id,available,total) values ('252','75','88',0,3); +insert into inventory (id,product_id,location_id,available,total) values ('253','76','88',49,51); +insert into inventory (id,product_id,location_id,available,total) values ('254','77','88',37,61); +insert into inventory (id,product_id,location_id,available,total) values ('255','78','88',4,78); +insert into inventory (id,product_id,location_id,available,total) values ('257','80','88',23,29); +insert into inventory (id,product_id,location_id,available,total) values ('256','79','88',1,5); +insert into inventory (id,product_id,location_id,available,total) values ('259','82','88',1,2); +insert into inventory (id,product_id,location_id,available,total) values ('258','81','88',3,52); +insert into inventory (id,product_id,location_id,available,total) values ('260','83','88',65,67); +insert into inventory (id,product_id,location_id,available,total) values ('261','84','88',41,87); +insert into inventory (id,product_id,location_id,available,total) values ('262','85','88',20,21); +insert into inventory (id,product_id,location_id,available,total) values ('93','2','87',43,56); +insert into inventory (id,product_id,location_id,available,total) values ('94','3','87',27,85); +insert into inventory (id,product_id,location_id,available,total) values ('263','86','88',46,94); +insert into inventory (id,product_id,location_id,available,total) values ('264','87','88',64,68); +insert into inventory (id,product_id,location_id,available,total) values ('265','2','89',5,78); +insert into inventory (id,product_id,location_id,available,total) values ('266','3','89',29,41); +insert into inventory (id,product_id,location_id,available,total) values ('267','4','89',2,39); +insert into inventory (id,product_id,location_id,available,total) values ('268','5','89',57,67); +insert into inventory (id,product_id,location_id,available,total) values ('269','6','89',1,2); +insert into inventory (id,product_id,location_id,available,total) values ('270','7','89',68,80); +insert into inventory (id,product_id,location_id,available,total) values ('271','8','89',22,81); +insert into inventory (id,product_id,location_id,available,total) values ('272','9','89',9,52); +insert into inventory (id,product_id,location_id,available,total) values ('273','10','89',26,42); +insert into inventory (id,product_id,location_id,available,total) values ('274','11','89',42,91); +insert into inventory (id,product_id,location_id,available,total) values ('275','12','89',23,35); +insert into inventory (id,product_id,location_id,available,total) values ('276','13','89',38,59); +insert into inventory (id,product_id,location_id,available,total) values ('277','14','89',43,51); +insert into inventory (id,product_id,location_id,available,total) values ('278','15','89',19,29); +insert into inventory (id,product_id,location_id,available,total) values ('279','16','89',21,29); +insert into inventory (id,product_id,location_id,available,total) values ('280','17','89',18,47); +insert into inventory (id,product_id,location_id,available,total) values ('281','18','89',26,52); +insert into inventory (id,product_id,location_id,available,total) values ('282','19','89',18,61); +insert into inventory (id,product_id,location_id,available,total) values ('283','20','89',33,97); +insert into inventory (id,product_id,location_id,available,total) values ('284','21','89',1,35); +insert into inventory (id,product_id,location_id,available,total) values ('285','22','89',41,65); +insert into inventory (id,product_id,location_id,available,total) values ('286','23','89',16,41); +insert into inventory (id,product_id,location_id,available,total) values ('287','24','89',26,32); +insert into inventory (id,product_id,location_id,available,total) values ('288','25','89',0,11); +insert into inventory (id,product_id,location_id,available,total) values ('289','26','89',30,52); +insert into inventory (id,product_id,location_id,available,total) values ('290','27','89',29,73); +insert into inventory (id,product_id,location_id,available,total) values ('291','28','89',26,86); +insert into inventory (id,product_id,location_id,available,total) values ('292','29','89',48,48); +insert into inventory (id,product_id,location_id,available,total) values ('293','30','89',0,68); +insert into inventory (id,product_id,location_id,available,total) values ('294','31','89',25,32); +insert into inventory (id,product_id,location_id,available,total) values ('295','32','89',37,80); +insert into inventory (id,product_id,location_id,available,total) values ('296','33','89',12,43); +insert into inventory (id,product_id,location_id,available,total) values ('297','34','89',34,89); +insert into inventory (id,product_id,location_id,available,total) values ('298','35','89',54,97); +insert into inventory (id,product_id,location_id,available,total) values ('299','36','89',2,18); +insert into inventory (id,product_id,location_id,available,total) values ('300','37','89',13,16); +insert into inventory (id,product_id,location_id,available,total) values ('301','38','89',19,54); +insert into inventory (id,product_id,location_id,available,total) values ('302','39','89',16,40); +insert into inventory (id,product_id,location_id,available,total) values ('303','40','89',10,93); +insert into inventory (id,product_id,location_id,available,total) values ('304','41','89',35,39); +insert into inventory (id,product_id,location_id,available,total) values ('305','42','89',24,25); +insert into inventory (id,product_id,location_id,available,total) values ('306','43','89',5,55); +insert into inventory (id,product_id,location_id,available,total) values ('307','44','89',9,43); +insert into inventory (id,product_id,location_id,available,total) values ('308','45','89',36,82); +insert into inventory (id,product_id,location_id,available,total) values ('309','46','89',5,8); +insert into inventory (id,product_id,location_id,available,total) values ('310','47','89',18,20); +insert into inventory (id,product_id,location_id,available,total) values ('311','48','89',2,9); +insert into inventory (id,product_id,location_id,available,total) values ('312','49','89',34,91); +insert into inventory (id,product_id,location_id,available,total) values ('313','50','89',27,55); +insert into inventory (id,product_id,location_id,available,total) values ('314','51','89',11,72); +insert into inventory (id,product_id,location_id,available,total) values ('315','52','89',8,13); +insert into inventory (id,product_id,location_id,available,total) values ('316','53','89',4,31); +insert into inventory (id,product_id,location_id,available,total) values ('317','54','89',1,1); +insert into inventory (id,product_id,location_id,available,total) values ('318','55','89',7,19); +insert into inventory (id,product_id,location_id,available,total) values ('319','56','89',3,35); +insert into inventory (id,product_id,location_id,available,total) values ('320','57','89',58,73); +insert into inventory (id,product_id,location_id,available,total) values ('321','58','89',2,32); +insert into inventory (id,product_id,location_id,available,total) values ('322','59','89',51,64); +insert into inventory (id,product_id,location_id,available,total) values ('323','60','89',34,79); +insert into inventory (id,product_id,location_id,available,total) values ('324','61','89',44,66); +insert into inventory (id,product_id,location_id,available,total) values ('325','62','89',37,46); +insert into inventory (id,product_id,location_id,available,total) values ('326','63','89',10,11); +insert into inventory (id,product_id,location_id,available,total) values ('327','64','89',15,74); +insert into inventory (id,product_id,location_id,available,total) values ('328','65','89',8,19); +insert into inventory (id,product_id,location_id,available,total) values ('329','66','89',13,26); +insert into inventory (id,product_id,location_id,available,total) values ('330','67','89',0,1); +insert into inventory (id,product_id,location_id,available,total) values ('331','68','89',5,17); +insert into inventory (id,product_id,location_id,available,total) values ('332','69','89',0,0); +insert into inventory (id,product_id,location_id,available,total) values ('333','70','89',1,48); +insert into inventory (id,product_id,location_id,available,total) values ('334','71','89',13,70); +insert into inventory (id,product_id,location_id,available,total) values ('335','72','89',24,68); +insert into inventory (id,product_id,location_id,available,total) values ('336','73','89',21,48); +insert into inventory (id,product_id,location_id,available,total) values ('337','74','89',17,68); +insert into inventory (id,product_id,location_id,available,total) values ('338','75','89',72,72); +insert into inventory (id,product_id,location_id,available,total) values ('339','76','89',6,24); +insert into inventory (id,product_id,location_id,available,total) values ('340','77','89',18,22); +insert into inventory (id,product_id,location_id,available,total) values ('341','78','89',8,24); +insert into inventory (id,product_id,location_id,available,total) values ('342','79','89',26,31); +insert into inventory (id,product_id,location_id,available,total) values ('343','80','89',14,19); +insert into inventory (id,product_id,location_id,available,total) values ('344','81','89',10,31); +insert into inventory (id,product_id,location_id,available,total) values ('345','82','89',88,92); +insert into inventory (id,product_id,location_id,available,total) values ('346','83','89',5,11); +insert into inventory (id,product_id,location_id,available,total) values ('347','84','89',13,72); +insert into inventory (id,product_id,location_id,available,total) values ('348','85','89',18,37); +insert into inventory (id,product_id,location_id,available,total) values ('349','86','89',6,12); +insert into inventory (id,product_id,location_id,available,total) values ('350','87','89',79,99); +insert into inventory (id,product_id,location_id,available,total) values ('351','2','90',10,19); +insert into inventory (id,product_id,location_id,available,total) values ('353','4','90',8,38); +insert into inventory (id,product_id,location_id,available,total) values ('352','3','90',3,6); +insert into inventory (id,product_id,location_id,available,total) values ('354','5','90',26,54); +insert into inventory (id,product_id,location_id,available,total) values ('355','6','90',20,73); +insert into inventory (id,product_id,location_id,available,total) values ('356','7','90',30,95); +insert into inventory (id,product_id,location_id,available,total) values ('357','8','90',32,93); +insert into inventory (id,product_id,location_id,available,total) values ('358','9','90',4,18); +insert into inventory (id,product_id,location_id,available,total) values ('359','10','90',32,94); +insert into inventory (id,product_id,location_id,available,total) values ('360','11','90',57,80); +insert into inventory (id,product_id,location_id,available,total) values ('361','12','90',3,6); +insert into inventory (id,product_id,location_id,available,total) values ('362','13','90',40,58); +insert into inventory (id,product_id,location_id,available,total) values ('363','14','90',54,91); +insert into inventory (id,product_id,location_id,available,total) values ('364','15','90',10,11); +insert into inventory (id,product_id,location_id,available,total) values ('365','16','90',34,58); +insert into inventory (id,product_id,location_id,available,total) values ('366','17','90',34,99); +insert into inventory (id,product_id,location_id,available,total) values ('367','18','90',72,90); +insert into inventory (id,product_id,location_id,available,total) values ('368','19','90',13,76); +insert into inventory (id,product_id,location_id,available,total) values ('369','20','90',37,71); +insert into inventory (id,product_id,location_id,available,total) values ('370','21','90',21,39); +insert into inventory (id,product_id,location_id,available,total) values ('371','22','90',4,20); +insert into inventory (id,product_id,location_id,available,total) values ('372','23','90',11,73); +insert into inventory (id,product_id,location_id,available,total) values ('373','24','90',18,100); +insert into inventory (id,product_id,location_id,available,total) values ('375','26','90',0,1); +insert into inventory (id,product_id,location_id,available,total) values ('374','25','90',26,62); +insert into inventory (id,product_id,location_id,available,total) values ('376','27','90',10,28); +insert into inventory (id,product_id,location_id,available,total) values ('377','28','90',68,78); +insert into inventory (id,product_id,location_id,available,total) values ('378','29','90',10,73); +insert into inventory (id,product_id,location_id,available,total) values ('379','30','90',73,96); +insert into inventory (id,product_id,location_id,available,total) values ('380','31','90',35,75); +insert into inventory (id,product_id,location_id,available,total) values ('381','32','90',58,88); +insert into inventory (id,product_id,location_id,available,total) values ('382','33','90',14,26); +insert into inventory (id,product_id,location_id,available,total) values ('383','34','90',22,24); +insert into inventory (id,product_id,location_id,available,total) values ('384','35','90',23,72); +insert into inventory (id,product_id,location_id,available,total) values ('385','36','90',23,59); +insert into inventory (id,product_id,location_id,available,total) values ('387','38','90',51,71); +insert into inventory (id,product_id,location_id,available,total) values ('386','37','90',3,6); +insert into inventory (id,product_id,location_id,available,total) values ('388','39','90',48,60); +insert into inventory (id,product_id,location_id,available,total) values ('389','40','90',44,56); +insert into inventory (id,product_id,location_id,available,total) values ('390','41','90',25,36); +insert into inventory (id,product_id,location_id,available,total) values ('391','42','90',32,83); +insert into inventory (id,product_id,location_id,available,total) values ('392','43','90',77,92); +insert into inventory (id,product_id,location_id,available,total) values ('393','44','90',30,38); +insert into inventory (id,product_id,location_id,available,total) values ('394','45','90',43,49); +insert into inventory (id,product_id,location_id,available,total) values ('395','46','90',23,27); +insert into inventory (id,product_id,location_id,available,total) values ('396','47','90',78,84); +insert into inventory (id,product_id,location_id,available,total) values ('397','48','90',26,48); +insert into inventory (id,product_id,location_id,available,total) values ('398','49','90',15,52); +insert into inventory (id,product_id,location_id,available,total) values ('399','50','90',4,45); +insert into inventory (id,product_id,location_id,available,total) values ('400','51','90',53,77); +insert into inventory (id,product_id,location_id,available,total) values ('401','52','90',5,6); +insert into inventory (id,product_id,location_id,available,total) values ('402','53','90',17,30); +insert into inventory (id,product_id,location_id,available,total) values ('403','54','90',4,44); +insert into inventory (id,product_id,location_id,available,total) values ('404','55','90',12,20); +insert into inventory (id,product_id,location_id,available,total) values ('405','56','90',15,25); +insert into inventory (id,product_id,location_id,available,total) values ('406','57','90',1,33); +insert into inventory (id,product_id,location_id,available,total) values ('407','58','90',22,34); +insert into inventory (id,product_id,location_id,available,total) values ('408','59','90',6,12); +insert into inventory (id,product_id,location_id,available,total) values ('409','60','90',3,9); +insert into inventory (id,product_id,location_id,available,total) values ('410','61','90',41,59); +insert into inventory (id,product_id,location_id,available,total) values ('411','62','90',16,32); +insert into inventory (id,product_id,location_id,available,total) values ('412','63','90',7,15); +insert into inventory (id,product_id,location_id,available,total) values ('413','64','90',49,95); +insert into inventory (id,product_id,location_id,available,total) values ('414','65','90',41,45); +insert into inventory (id,product_id,location_id,available,total) values ('416','67','90',11,39); +insert into inventory (id,product_id,location_id,available,total) values ('415','66','90',18,45); +insert into inventory (id,product_id,location_id,available,total) values ('417','68','90',26,84); +insert into inventory (id,product_id,location_id,available,total) values ('418','69','90',3,4); +insert into inventory (id,product_id,location_id,available,total) values ('419','70','90',72,98); +insert into inventory (id,product_id,location_id,available,total) values ('420','71','90',26,28); +insert into inventory (id,product_id,location_id,available,total) values ('421','72','90',2,2); +insert into inventory (id,product_id,location_id,available,total) values ('422','73','90',57,90); +insert into inventory (id,product_id,location_id,available,total) values ('423','74','90',12,75); +insert into inventory (id,product_id,location_id,available,total) values ('424','75','90',23,37); +insert into inventory (id,product_id,location_id,available,total) values ('425','76','90',22,22); +insert into inventory (id,product_id,location_id,available,total) values ('426','77','90',30,86); +insert into inventory (id,product_id,location_id,available,total) values ('427','78','90',44,82); +insert into inventory (id,product_id,location_id,available,total) values ('428','79','90',13,17); +insert into inventory (id,product_id,location_id,available,total) values ('429','80','90',38,45); +insert into inventory (id,product_id,location_id,available,total) values ('430','81','90',26,91); +insert into inventory (id,product_id,location_id,available,total) values ('431','82','90',34,41); +insert into inventory (id,product_id,location_id,available,total) values ('432','83','90',19,43); +insert into inventory (id,product_id,location_id,available,total) values ('433','84','90',43,43); +insert into inventory (id,product_id,location_id,available,total) values ('434','85','90',34,69); +insert into inventory (id,product_id,location_id,available,total) values ('435','86','90',10,25); +insert into inventory (id,product_id,location_id,available,total) values ('436','87','90',18,34); +insert into inventory (id,product_id,location_id,available,total) values ('437','2','91',25,98); +insert into inventory (id,product_id,location_id,available,total) values ('438','3','91',15,28); +insert into inventory (id,product_id,location_id,available,total) values ('439','4','91',56,97); +insert into inventory (id,product_id,location_id,available,total) values ('440','5','91',20,30); + +insert into customer (id,[username],email,password,name,military_agency,realm,emailverified,verificationtoken,credentials,challenges,status,created,lastupdated) values ('612','bat','bat@bar.com','$2a$10$beg18wcyqn7trkfic59eb.vmnsewqjwmlym4dng73izb.mka1rjac',null,null,null,null,null,']',']',null,null,null); +insert into customer (id,username,email,password,name,military_agency,realm,emailverified,verificationtoken,credentials,challenges,status,created,lastupdated) values ('613','baz','baz@bar.com','$2a$10$jksyf2glmdi4cwvqh8astos0b24ldu9p8jccnmri/0rvhtwsicm9c',null,null,null,null,null,']',']',null,null,null); +insert into customer (id,username,email,password,name,military_agency,realm,emailverified,verificationtoken,credentials,challenges,status,created,lastupdated) values ('610','foo','foo@bar.com','$2a$10$tn1hn7xv6x74ccb7tvfwkeaajtd4/6q4rbcmzgmajewe40xqrrsui',null,null,null,null,null,']',']',null,null,null); +insert into customer (id,username,email,password,name,military_agency,realm,emailverified,verificationtoken,credentials,challenges,status,created,lastupdated) values ('611','bar','bar@bar.com','$2a$10$a8mcol6d5vqxm6vubqxl8e5v66steg6e8vzjqqppoyk95vm3smpik',null,null,null,null,null,']',']',null,null,null); + +insert into location (id,street,city,zipcode,name,geo) values ('87','7153 east thomas road','scottsdale','85251','phoenix equipment rentals','-111.9271738,33.48034450000001'); +insert into location (id,street,city,zipcode,name,geo) values ('91','2799 broadway','new york','10025','cascabel armory','-73.9676965,40.8029807'); +insert into location (id,street,city,zipcode,name,geo) values ('89','1850 el camino real','menlo park','94027','military weaponry','-122.194253,37.459525'); +insert into location (id,street,city,zipcode,name,geo) values ('92','32/66-70 marine parade','coolangatta','4225','marine parade','153.536972,-28.167598'); +insert into location (id,street,city,zipcode,name,geo) values ('90','tolstraat 200','amsterdam','1074','munitions shopmore','4.907475499999999,52.3530638'); +insert into location (id,street,city,zipcode,name,geo) values ('88','390 lang road','burlingame','94010','bay area firearms','-122.3381437,37.5874391'); + +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('4','m9',53,75,15,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('3','m1911',53,50,7,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('6','makarov sd',0,50,8,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('7','pdw',53,75,30,null,'single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('8','makarov pm',53,50,8,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('9','double-barreled shotgun',90,null,2,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('10','saiga 12k',90,250,8,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('11','remington 870 (flashlight)',90,null,8,'flashlight','single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('12','revolver',53,100,6,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('13','winchester 1866',125,150,15,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('14','bizon pp-19 sd',0,100,64,'silenced','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('15','mp5sd6',0,100,30,'silenced','single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('16','mp5a5',53,100,30,null,'single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('17','ak-107',80,400,30,'kobra sight','single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('18','ak-107 gl',80,null,30,'kobra sight','single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('19','ak-107 gl pso',80,400,30,'scope,gp-25 launcher]','single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('20','ak-107 pso',80,600,30,'scope','single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('21','ak-74',80,300,30,null,'single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('22','akm',149,400,30,null,'single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('23','aks',149,200,30,null,'single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('24','aks (gold)',149,200,30,null,'single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('25','m1014',90,null,8,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('26','aks-74 kobra',80,300,30,'kobra sight','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('27','aks-74 pso',80,400,30,'scope','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('28','aks-74u',80,200,30,null,'single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('29','aks-74un kobra',0,300,30,'kobra sight,silenced]','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('30','ak-74 gp-25',80,300,30,'gp-25 launcher','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('31','fn fal an/pvs-4',180,400,20,'nv scope','single,burst]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('32','g36',80,400,30,'scope,aimpoint sight]','single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('33','fn fal',180,400,20,null,'single,burst]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('34','g36 c',80,300,30,null,'single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('35','g36-c sd (camo)',0,300,30,'aimpoint sight,silenced]','single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('36','g36a (camo)',80,400,30,'scope,aimpoint sight]','single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('37','g36c (camo)',80,300,30,null,'single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('38','g36 k',80,400,30,'scope,aimpoint sight]','single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('39','g36c-sd',0,300,30,'aimpoint sight','single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('40','g36k (camo)',80,400,30,'scope,aimpoint sight]','single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('41','l85a2 acog gl',80,600,30,'acog scope,m203 launcher]','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('42','l85a2 susat',80,300,30,'susat optical scope','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('43','m16a2',80,400,30,null,'single,burst]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('44','l85a2 aws',80,300,30,'thermal scope,nv scope,laser sight,variable zoom]','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('45','l85a2 holo',80,300,30,'holographic sight','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('46','lee enfield',162,400,10,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('47','m16a4 acog',80,600,30,'acog scope','single,burst]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('49','m16a2 m203',80,400,30,'m203 launcher','single,burst]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('48','m4a1',80,300,30,null,'single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('50','m4a1 holo',80,300,30,'holographic sight,m203 launcher]','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('51','m4a1 cco',80,300,30,'aimpoint sight','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('52','m4a1 cco sd',0,200,30,'aimpoint sight,silenced]','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('53','m4a1 m203 rco',80,600,30,'acog sight,m203 launcher]','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('54','m4a3 cco',80,300,30,'aimpoint sight,flashlight]','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('55','rpk',80,400,30,null,'single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('56','sa-58 cco',149,300,30,'aimpoint sight','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('57','sa-58p',149,400,30,null,'single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('58','sa-58v',149,200,30,null,'single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('59','sa-58v acog',149,400,30,'acog sight','single,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('60','er7 rfw',180,2000,25,'scope,aimpoint sight]','single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('61','as50',455,1600,5,'scope','single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('62','ksvk',455,800,5,'scope','single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('63','cz550',180,800,5,'scope','single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('64','dmr',180,800,20,'scope','single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('65','m107',455,1200,10,'scope','single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('66','m24',180,800,5,'scope','single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('67','m40a3',180,800,5,'scope,camo]','single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('68','m14 aim',180,500,20,'aimpoint sight','single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('69','m240',180,400,100,null,'full auto'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('70','mg36',80,400,100,'aimpoint sight','single,burst,full auto]'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('72','pkm',180,400,100,null,'full auto'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('71','svd camo',180,1200,10,'scope,camo]','single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('73','mk 48 mod 0',180,400,100,'aimpoint sight','full auto'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('74','m249 saw',80,300,200,null,'full auto'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('75','crowbar',2,1,null,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('76','hatchet',2,1,null,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('77','pkp',180,600,100,'scope','full auto'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('78','machete',2,1,null,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('79','m67 frag grenade',null,null,null,null,null); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('80','compound crossbow',3,30,1,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('81','smoke grenade',null,null,null,null,null); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('82','m136 launcher',160,1000,1,null,'single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('83','30rnd. ak sd',0,null,30,null,null); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('84','30rnd g36 sd',0,null,30,null,null); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('85','g36 mag.',80,null,30,null,null); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('86','flashlight',null,null,null,null,null); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('87','nv goggles',null,null,null,null,null); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('2','g17',53,75,15,'flashlight','single'); +insert into product (id,name,audible_range,effective_range,rounds,extras,fire_modes) values ('5','m9 sd',0,75,15,'silenced','single'); + + alter table inventory add constraint inventory_pk primary key (id); + alter table location add constraint location_pk primary key (id); + + alter table customer add constraint customer_pk primary key (id); + alter table product add constraint product_pk primary key (id); + alter table session add constraint session_pk primary key (id); + alter table version add constraint version_pk primary key (product_id, version); + + alter table inventory add constraint location_fk foreign key (location_id) references location (id); + alter table inventory add constraint product_fk foreign key (product_id) references product (id); + + alter table reservation add constraint reservation_customer_fk foreign key (customer_id) references customer (id); + alter table reservation add constraint reservation_location_fk foreign key (location_id) references location (id); + alter table reservation add constraint reservation_product_fk foreign key (product_id) references product (id); + alter table version add constraint version_product_fk foreign key (product_id) references product (id); + +GO + + create view inventory_view + as + select p.name as product, + l.name as location, + i.available + from inventory i, + product p, + location l + where p.id = i.product_id + and l.id = i.location_id; diff --git a/test/transaction.test.js b/test/transaction.test.js new file mode 100644 index 0000000..e891504 --- /dev/null +++ b/test/transaction.test.js @@ -0,0 +1,99 @@ +// Copyright IBM Corp. 2015,2019. All Rights Reserved. +// Node module: loopback-connector-mssql +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; +require('./init.js'); +require('should'); + +const Transaction = require('loopback-connector').Transaction; + +let db, Post; + +describe('transactions', function() { + before(function(done) { + /* global getDataSource */ + db = getDataSource(); + Post = db.define('PostTX', { + title: {type: String, length: 255, index: true}, + content: {type: String}, + }); + db.automigrate('PostTX', done); + }); + + let currentTx; + // Return an async function to start a transaction and create a post + function createPostInTx(post) { + return function(done) { + Transaction.begin(db.connector, Transaction.READ_COMMITTED, + function(err, tx) { + if (err) return done(err); + currentTx = tx; + Post.create(post, {transaction: tx}, + function(err, p) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + }; + } + + // Return an async function to find matching posts and assert number of + // records to equal to the count + function expectToFindPosts(where, count, inTx) { + return function(done) { + const options = {}; + if (inTx) { + options.transaction = currentTx; + } + Post.find({where: where}, options, + function(err, posts) { + if (err) return done(err); + posts.length.should.be.eql(count); + done(); + }); + }; + } + + describe('commit', function() { + const post = {title: 't1', content: 'c1'}; + before(createPostInTx(post)); + + // FIXME: [rfeng] SQL server creates LCK_M_S (Shared Lock on the table + // and it prevents the following test to run as the SELECT will be suspended + // until the transaction releases the lock + it.skip('should not see the uncommitted insert', expectToFindPosts(post, 0)); + + it('should see the uncommitted insert from the same transaction', + expectToFindPosts(post, 1, true)); + + it('should commit a transaction', function(done) { + currentTx.commit(done); + }); + + it('should see the committed insert', expectToFindPosts(post, 1)); + }); + + describe('rollback', function() { + const post = {title: 't2', content: 'c2'}; + before(createPostInTx(post)); + + // FIXME: [rfeng] SQL server creates LCK_M_S (Shared Lock on the table + // and it prevents the following test to run as the SELECT will be suspended + // until the transaction releases the lock + it.skip('should not see the uncommitted insert', expectToFindPosts(post, 0)); + + it('should see the uncommitted insert from the same transaction', + expectToFindPosts(post, 1, true)); + + it('should rollback a transaction', function(done) { + currentTx.rollback(done); + }); + + it('should not see the rolledback insert', expectToFindPosts(post, 0)); + }); +});