├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ ├── Feature_request.md │ ├── Question.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── stale.yml ├── .gitignore ├── CHANGES.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── NOTICE ├── README.md ├── SECURITY.md ├── api.md ├── deps ├── juggler-v3 │ ├── package.json │ └── test.js └── juggler-v4 │ ├── package.json │ └── test.js ├── docs.json ├── example └── app.js ├── index.js ├── intl ├── cs │ └── messages.json ├── de │ └── messages.json ├── en │ └── messages.json ├── es │ └── messages.json ├── fr │ └── messages.json ├── it │ └── messages.json ├── ja │ └── messages.json ├── ko │ └── messages.json ├── nl │ └── messages.json ├── pl │ └── messages.json ├── pt │ └── messages.json ├── ru │ └── messages.json ├── tr │ └── messages.json ├── zh-Hans │ └── messages.json └── zh-Hant │ └── messages.json ├── lib ├── discovery.js ├── migration.js ├── oracle.js └── transaction.js ├── package.json ├── setup.sh └── test ├── init └── init.js ├── mocha.opts ├── oracle.autoupdate.test.js ├── oracle.clob.test.js ├── oracle.connectionpool.test.js ├── oracle.discover.test.js ├── oracle.mapping.js ├── oracle.regexp.test.js ├── oracle.test.js ├── oracle.transaction.test.js └── tables.sql /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "loopback", 3 | "rules": { 4 | "max-len": ["error", 80, 4, { 5 | "ignoreComments": true, 6 | "ignoreUrls": true, 7 | "ignorePattern": "^\\s*var\\s.+=\\s*(require\\s*\\()|(/)" 8 | }] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | labels: bug 5 | 6 | --- 7 | 8 | 18 | 19 | ## Steps to reproduce 20 | 21 | 22 | 23 | ## Current Behavior 24 | 25 | 26 | 27 | ## Expected Behavior 28 | 29 | 30 | 31 | ## Link to reproduction sandbox 32 | 33 | 37 | 38 | ## Additional information 39 | 40 | 45 | 46 | ## Related Issues 47 | 48 | 49 | 50 | _See [Reporting Issues](http://loopback.io/doc/en/contrib/Reporting-issues.html) for more tips on writing good issues_ 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | labels: feature 5 | 6 | --- 7 | 8 | ## Suggestion 9 | 10 | 11 | 12 | ## Use Cases 13 | 14 | 18 | 19 | ## Examples 20 | 21 | 22 | 23 | ## Acceptance criteria 24 | 25 | TBD - will be filled by the team. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: The issue tracker is not for questions. Please use Stack Overflow or other resources for help. 4 | labels: question 5 | 6 | --- 7 | 8 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Report a security vulnerability 4 | url: https://loopback.io/doc/en/contrib/Reporting-issues.html#security-issues 5 | about: Do not report security vulnerabilities using GitHub issues. Please send an email to `security@loopback.io` instead. 6 | - name: Get help on StackOverflow 7 | url: https://stackoverflow.com/tags/loopbackjs 8 | about: Please ask and answer questions on StackOverflow. 9 | - name: Join our mailing list 10 | url: https://groups.google.com/forum/#!forum/loopbackjs 11 | about: You can also post your question to our mailing list. 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ## Checklist 12 | 13 | - [ ] DCO (Developer Certificate of Origin) [signed in all commits](https://loopback.io/doc/en/contrib/code-contrib.html) 14 | - [ ] `npm test` passes on your machine 15 | - [ ] New tests added or existing tests modified to cover all changes 16 | - [ ] Code conforms with the [style guide](https://loopback.io/doc/en/contrib/style-guide-es6.html) 17 | - [ ] Commit messages are following our [guidelines](https://loopback.io/doc/en/contrib/git-commit-messages.html) 18 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - critical 10 | - p1 11 | - major 12 | # Label to use when marking an issue as stale 13 | staleLabel: stale 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | # Comment to post when closing a stale issue. Set to `false` to disable 20 | closeComment: > 21 | This issue has been closed due to continued inactivity. Thank you for your understanding. 22 | If you believe this to be in error, please contact one of the code owners, 23 | listed in the `CODEOWNERS` file at the top-level of this repository. 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .project 3 | .idea 4 | coverage 5 | lib-cov 6 | *.seed 7 | *.log 8 | *.csv 9 | *.dat 10 | *.out 11 | *.pid 12 | *.gz 13 | 14 | pids 15 | logs 16 | results 17 | 18 | npm-debug.log 19 | docs 20 | *.xml 21 | *.tgz 22 | .loopbackrc 23 | node_modules 24 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2020-03-19, Version 4.5.2 2 | ========================= 3 | 4 | * Exclude 'deps' and '.github' from npm publish (Dominique Emond) 5 | 6 | * tests: id column should be convered and mapped; (Agnes Lin) 7 | 8 | * chore: update copyrights year (Diana Lau) 9 | 10 | 11 | 2019-12-19, Version 4.5.1 12 | ========================= 13 | 14 | * buildQueryColumns should return columns in order (ataft) 15 | 16 | * chore: update CODEOWNERS file (Diana Lau) 17 | 18 | * chore: improve issue and PR templates (Nora) 19 | 20 | 21 | 2019-11-01, Version 4.5.0 22 | ========================= 23 | 24 | * Allow CLOB/BLOB data types for migration (ataft) 25 | 26 | * Only callback after the connection is released (Raymond Feng) 27 | 28 | * Fix eslint violations (Raymond Feng) 29 | 30 | 31 | 2019-09-19, Version 4.4.0 32 | ========================= 33 | 34 | * temp (Hage Yaapa) 35 | 36 | * update oracledb to v4 (Nora) 37 | 38 | 39 | 2019-07-25, Version 4.3.0 40 | ========================= 41 | 42 | * fix eslint violations (Nora) 43 | 44 | * update dependencies (Nora) 45 | 46 | * run shared tests from both v3 and v4 of juggler (Nora) 47 | 48 | * drop support for node.js 6 (Nora) 49 | 50 | 51 | 2019-05-22, Version 4.2.0 52 | ========================= 53 | 54 | * Added the connection property "_enableStats" (Eric Alves) 55 | 56 | 57 | 2018-12-07, Version 4.1.2 58 | ========================= 59 | 60 | * chore: update CODEOWNERS (Diana Lau) 61 | 62 | * Upgrade to oracledb 3.x (Joost de Bruijn) 63 | 64 | 65 | 2018-09-19, Version 4.1.1 66 | ========================= 67 | 68 | * update strong-globalize to 4.x (Diana Lau) 69 | 70 | 71 | 2018-08-07, Version 4.1.0 72 | ========================= 73 | 74 | * remove unnecessary text (Diana Lau) 75 | 76 | * Update to MIT license (Diana Lau) 77 | 78 | 79 | 2018-07-16, Version 4.0.2 80 | ========================= 81 | 82 | * fix schema/owner for discovery (Raymond Feng) 83 | 84 | 85 | 2018-07-10, Version 4.0.1 86 | ========================= 87 | 88 | * [WebFM] cs/pl/ru translation (candytangnb) 89 | 90 | 91 | 2018-05-23, Version 4.0.0 92 | ========================= 93 | 94 | * Upgrade to oracledb 2.x (Raymond Feng) 95 | 96 | * Fix datatypeChanged for fields with length (Joost de Bruijn) 97 | 98 | * fix typo in readme (#150) (Biniam Admikew) 99 | 100 | * Add stalebot configuration (Kevin Delisle) 101 | 102 | * Create Issue and PR Templates (#143) (Sakib Hasan) 103 | 104 | * Update translated strings Q3 2017 (Allen Boone) 105 | 106 | * update messages.json (Diana Lau) 107 | 108 | * Add CODEOWNER file (Diana Lau) 109 | 110 | * Require init on mocha args (ssh24) 111 | 112 | * Add docker setup (#130) (Biniam Admikew) 113 | 114 | 115 | 2017-04-18, Version 3.2.0 116 | ========================= 117 | 118 | * Use fetchAsString for CLOB (Raymond Feng) 119 | 120 | 121 | 2017-03-31, Version 3.1.1 122 | ========================= 123 | 124 | * Change node vers. (#122) (Rand McKinney) 125 | 126 | 127 | 2017-03-06, Version 3.1.0 128 | ========================= 129 | 130 | * Upgrade to loopback-connector@4.x (Raymond Feng) 131 | 132 | * Upgrade deps and fix styles (Raymond Feng) 133 | 134 | * Add regexp support (Raymond Feng) 135 | 136 | * Refactor migration methods (ssh24) 137 | 138 | * Refactor discovery methods (Loay Gewily) 139 | 140 | * Update test script (Loay Gewily) 141 | 142 | * Replicate new issue_template from loopback (Siddhi Pai) 143 | 144 | * Replicate issue_template from loopback repo (Siddhi Pai) 145 | 146 | * Update LB connector version (Loay) 147 | 148 | * Update readme (#102) (Rand McKinney) 149 | 150 | * Allow oracledb settings to be honored at pool/execution levels (Raymond Feng) 151 | 152 | * Update paid support URL (Siddhi Pai) 153 | 154 | 155 | 2016-11-29, Version 3.0.0 156 | ========================= 157 | 158 | * Update installer dep (Raymond Feng) 159 | 160 | * Move config to installer (#93) (Rashmi Hunt) 161 | 162 | * Drop support for Node v0.10 and v0.12 (Miroslav Bajtoš) 163 | 164 | * Remove the obselete example (Raymond Feng) 165 | 166 | * Update README doc link (Candy) 167 | 168 | * fixed a space (rashmihunt) 169 | 170 | * fixed link to node-oracledb (rashmihunt) 171 | 172 | * Add connectorCapabilities global object (#83) (Nicholas Duffy) 173 | 174 | * Update translation files - round#2 (Candy) 175 | 176 | * Add translated files (gunjpan) 177 | 178 | * Add eslint infrastructure (Candy) 179 | 180 | * Update deps to loopback 3.0.0 RC (Miroslav Bajtoš) 181 | 182 | * Run CI with juggler3 (Loay) 183 | 184 | * Update strong-globalize to 2.6.2 (Simon Ho) 185 | 186 | * Add globalization (Simon Ho) 187 | 188 | * Update URLs in CONTRIBUTING.md (#67) (Ryan Graham) 189 | 190 | * Fix package.json to use oracle installer (Raymond Feng) 191 | 192 | * Update dependencies (Raymond Feng) 193 | 194 | * Check lob type (Raymond Feng) 195 | 196 | * Fix the merge issue (Raymond Feng) 197 | 198 | * Upgrade to oracledb 1.0 (Raymond Feng) 199 | 200 | * Update to oracledb driver 0.5.0 (Raymond Feng) 201 | 202 | * Support more config properties (Raymond Feng) 203 | 204 | * Port to the oracledb driver (Raymond Feng) 205 | 206 | * Use try-catch to test error message (jannyHou) 207 | 208 | * update copyright notices and license (Ryan Graham) 209 | 210 | * Lazy connect when booting from swagger generator (juehou) 211 | 212 | 213 | 2016-03-04, Version 2.4.1 214 | ========================= 215 | 216 | * Remove license check (Raymond Feng) 217 | 218 | 219 | 2016-03-04, Version 2.4.0 220 | ========================= 221 | 222 | 223 | 224 | 2016-02-19, Version 2.3.1 225 | ========================= 226 | 227 | * Remove sl-blip from dependencies (Miroslav Bajtoš) 228 | 229 | * Add NOTICE (Raymond Feng) 230 | 231 | * Upgrade should to 8.0.2 (Simon Ho) 232 | 233 | 234 | 2015-12-03, Version 2.3.0 235 | ========================= 236 | 237 | * Upgrade oracle driver version (Raymond Feng) 238 | 239 | * Refer to licenses with a link (Sam Roberts) 240 | 241 | * Use strongloop conventions for licensing (Sam Roberts) 242 | 243 | * Fix the test to make it agnositic to the order of columns (Raymond Feng) 244 | 245 | * Tidy up tests (Raymond Feng) 246 | 247 | * Increase the timeout (Raymond Feng) 248 | 249 | 250 | 2015-08-01, Version 2.2.0 251 | ========================= 252 | 253 | * Update link (Raymond Feng) 254 | 255 | * Add ORA-22408 troubleshooting notes (Simon Ho) 256 | 257 | 258 | 2015-05-18, Version 2.1.0 259 | ========================= 260 | 261 | * Update deps (Raymond Feng) 262 | 263 | * Add transaction support (Raymond Feng) 264 | 265 | 266 | 2015-05-13, Version 2.0.0 267 | ========================= 268 | 269 | * Update deps (Raymond Feng) 270 | 271 | * Refactor the oracle connector to use base SqlConnector (Raymond Feng) 272 | 273 | * Fix the trigger so that it is only invoked when the id is not set (Raymond Feng) 274 | 275 | * Update the README (Raymond Feng) 276 | 277 | 278 | 2015-03-26, Version 1.7.0 279 | ========================= 280 | 281 | * Upgrade oracle driver to pass all tests (Raymond Feng) 282 | 283 | * Return count when updating or deleting models (Simon Ho) 284 | 285 | * Improve concurrency for testing (Raymond Feng) 286 | 287 | * Add strongloop license check (Raymond Feng) 288 | 289 | * Add instructions to running tests section (Simon Ho) 290 | 291 | 292 | 2015-02-20, Version 1.6.0 293 | ========================= 294 | 295 | * Fix test failures with juggler@2.18.1 (Raymond Feng) 296 | 297 | * Tweaked Node version caveat. (Rand McKinney) 298 | 299 | * Caveat re Node v 0.11 per Al (Rand McKinney) 300 | 301 | 302 | 2015-01-09, Version 1.5.0 303 | ========================= 304 | 305 | * Update deps (Raymond Feng) 306 | 307 | * Fix SQL injection (Raymond Feng) 308 | 309 | * Fix bad CLA URL in CONTRIBUTING.md (Ryan Graham) 310 | 311 | 312 | 2014-12-05, Version 1.4.5 313 | ========================= 314 | 315 | * Map required/id properties to NOT NULL (Raymond Feng) 316 | 317 | 318 | 2014-11-27, Version 1.4.4 319 | ========================= 320 | 321 | * Add contribution guidelines (Ryan Graham) 322 | 323 | 324 | 2014-09-11, Version 1.4.3 325 | ========================= 326 | 327 | * Bump versions (Raymond Feng) 328 | 329 | * Make sure errors are reported during automigrate/autoupdate (Raymond Feng) 330 | 331 | * Change "Unbuntu" to "Ubuntu" in readme (superkhau) 332 | 333 | 334 | 2014-08-21, Version 1.4.2 335 | ========================= 336 | 337 | * Bump version (Raymond Feng) 338 | 339 | * Add ping() (Raymond Feng) 340 | 341 | 342 | 2014-06-27, Version 1.4.1 343 | ========================= 344 | 345 | * Bump versions (Raymond Feng) 346 | 347 | * Tidy up filter.order parsing (Raymond Feng) 348 | 349 | * Update link to doc (Rand McKinney) 350 | 351 | 352 | 2014-06-23, Version 1.4.0 353 | ========================= 354 | 355 | * Bump version (Raymond Feng) 356 | 357 | * Use base connector and add update support (Raymond Feng) 358 | 359 | * Fix comparison for null and boolean values (Raymond Feng) 360 | 361 | 362 | 2014-05-22, Version 1.3.0 363 | ========================= 364 | 365 | * Update to strong-oracle 1.2.0 (Raymond Feng) 366 | 367 | 368 | 2014-05-16, Version 1.2.1 369 | ========================= 370 | 371 | * Bump versions (Raymond Feng) 372 | 373 | * Add and/or (Raymond Feng) 374 | 375 | * Add support for logical operators (AND/OR) (Raymond Feng) 376 | 377 | * Allow schema settings (Raymond Feng) 378 | 379 | * ADL -> LDL (Raymond Feng) 380 | 381 | * Reformat code (Raymond Feng) 382 | 383 | * Update the license file (Raymond Feng) 384 | 385 | 386 | 2014-03-26, Version 1.2.0 387 | ========================= 388 | 389 | * Catch exception for disconnect (Raymond Feng) 390 | 391 | * Remove the disconnect() to avoid racing condition (Raymond Feng) 392 | 393 | * Remove dead comments (Raymond Feng) 394 | 395 | * Add clob test case (Raymond Feng) 396 | 397 | * Throw the 1st error (Raymond Feng) 398 | 399 | * Bump version and update deps (Raymond Feng) 400 | 401 | * Use parameterized query to handle CLOB (Raymond Feng) 402 | 403 | * Align the lines around 80 chars (Raymond Feng) 404 | 405 | * Reformat code (Raymond Feng) 406 | 407 | * Allow use TNS compiled string for connection (Sergey Nosenko) 408 | 409 | * Removed doc for discovery functions exposed by juggler. (crandmck) 410 | 411 | * Update license url (Raymond Feng) 412 | 413 | * Add the test case for connection pooling (Raymond Feng) 414 | 415 | * Update dep to node-oracle (Raymond Feng) 416 | 417 | * Make sure pooled connections are released (Raymond Feng) 418 | 419 | * Add debug info (Raymond Feng) 420 | 421 | * Enable connection pool (Raymond Feng) 422 | 423 | 424 | 2013-12-17, Version 1.1.5 425 | ========================= 426 | 427 | * Exposed the `Oracle` class/constructor (Salehen Shovon Rahman) 428 | 429 | * Ignore CI related files (Ryan Graham) 430 | 431 | * Use istanbul for optional code coverage (Ryan Graham) 432 | 433 | * Run tests more similar to how CI runs them (Ryan Graham) 434 | 435 | 436 | 2013-12-06, Version 1.1.4 437 | ========================= 438 | 439 | * Bump version (Raymond Feng) 440 | 441 | * Update juggler dep to be peer (Raymond Feng) 442 | 443 | * fix link to api.md (Rand McKinney) 444 | 445 | * Reword final heading (Rand McKinney) 446 | 447 | * add one space. (Rand McKinney) 448 | 449 | * Unfortunately, wiki macro doesn't like raw HTML in md. Another try. (Rand McKinney) 450 | 451 | * Gave up and used HTML instead of md for table. (Rand McKinney) 452 | 453 | * Try to fix bullet list in 1st table (again) (Rand McKinney) 454 | 455 | * Try to fix bullet list in 1st table (Rand McKinney) 456 | 457 | * Fixed heading level. (Rand McKinney) 458 | 459 | * Update api.md (Rand McKinney) 460 | 461 | * Formatting fixes to make tables layout properly. (Rand McKinney) 462 | 463 | * Pulled API doc into separate .md file. Deleted non-essential content moved to wiki. (Rand McKinney) 464 | 465 | * Moved discovery API documentation here. (Rand McKinney) 466 | 467 | * Update docs.json (Rand McKinney) 468 | 469 | 470 | 2013-11-20, Version 1.1.3 471 | ========================= 472 | 473 | * First release! 474 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners, 3 | # the last matching pattern has the most precedence. 4 | 5 | # Alumni maintainers 6 | # @kjdelisle @loay @ssh24 @virkt25 @b-admike 7 | 8 | # Core team members from IBM 9 | * @jannyHou @dhmlau @raymondfeng @hacksparrow @emonddr 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | LoopBack, as member project of the OpenJS Foundation, use 4 | [Contributor Covenant v2.0](https://contributor-covenant.org/version/2/0/code_of_conduct) 5 | as their code of conduct. The full text is included 6 | [below](#contributor-covenant-code-of-conduct-v2.0) in English, and translations 7 | are available from the Contributor Covenant organisation: 8 | 9 | - [contributor-covenant.org/translations](https://www.contributor-covenant.org/translations) 10 | - [github.com/ContributorCovenant](https://github.com/ContributorCovenant/contributor_covenant/tree/release/content/version/2/0) 11 | 12 | Refer to the sections on reporting and escalation in this document for the 13 | specific emails that can be used to report and escalate issues. 14 | 15 | ## Reporting 16 | 17 | ### Project Spaces 18 | 19 | For reporting issues in spaces related to LoopBack, please use the email 20 | `tsc@loopback.io`. The LoopBack Technical Steering Committee (TSC) handles CoC issues related to the spaces that it 21 | maintains. The project TSC commits to: 22 | 23 | - maintain the confidentiality with regard to the reporter of an incident 24 | - to participate in the path for escalation as outlined in the section on 25 | Escalation when required. 26 | 27 | ### Foundation Spaces 28 | 29 | For reporting issues in spaces managed by the OpenJS Foundation, for example, 30 | repositories within the OpenJS organization, use the email 31 | `report@lists.openjsf.org`. The Cross Project Council (CPC) is responsible for 32 | managing these reports and commits to: 33 | 34 | - maintain the confidentiality with regard to the reporter of an incident 35 | - to participate in the path for escalation as outlined in the section on 36 | Escalation when required. 37 | 38 | ## Escalation 39 | 40 | The OpenJS Foundation maintains a Code of Conduct Panel (CoCP). This is a 41 | foundation-wide team established to manage escalation when a reporter believes 42 | that a report to a member project or the CPC has not been properly handled. In 43 | order to escalate to the CoCP send an email to 44 | `coc-escalation@lists.openjsf.org`. 45 | 46 | For more information, refer to the full 47 | [Code of Conduct governance document](https://github.com/openjs-foundation/cross-project-council/blob/HEAD/CODE_OF_CONDUCT.md). 48 | 49 | --- 50 | 51 | ## Contributor Covenant Code of Conduct v2.0 52 | 53 | ## Our Pledge 54 | 55 | We as members, contributors, and leaders pledge to make participation in our 56 | community a harassment-free experience for everyone, regardless of age, body 57 | size, visible or invisible disability, ethnicity, sex characteristics, gender 58 | identity and expression, level of experience, education, socio-economic status, 59 | nationality, personal appearance, race, religion, or sexual identity and 60 | orientation. 61 | 62 | We pledge to act and interact in ways that contribute to an open, welcoming, 63 | diverse, inclusive, and healthy community. 64 | 65 | ## Our Standards 66 | 67 | Examples of behavior that contributes to a positive environment for our 68 | community include: 69 | 70 | - Demonstrating empathy and kindness toward other people 71 | - Being respectful of differing opinions, viewpoints, and experiences 72 | - Giving and gracefully accepting constructive feedback 73 | - Accepting responsibility and apologizing to those affected by our mistakes, 74 | and learning from the experience 75 | - Focusing on what is best not just for us as individuals, but for the overall 76 | community 77 | 78 | Examples of unacceptable behavior include: 79 | 80 | - The use of sexualized language or imagery, and sexual attention or advances of 81 | any kind 82 | - Trolling, insulting or derogatory comments, and personal or political attacks 83 | - Public or private harassment 84 | - Publishing others' private information, such as a physical or email address, 85 | without their explicit permission 86 | - Other conduct which could reasonably be considered inappropriate in a 87 | professional setting 88 | 89 | ## Enforcement Responsibilities 90 | 91 | Community leaders are responsible for clarifying and enforcing our standards of 92 | acceptable behavior and will take appropriate and fair corrective action in 93 | response to any behavior that they deem inappropriate, threatening, offensive, 94 | or harmful. 95 | 96 | Community leaders have the right and responsibility to remove, edit, or reject 97 | comments, commits, code, wiki edits, issues, and other contributions that are 98 | not aligned to this Code of Conduct, and will communicate reasons for moderation 99 | decisions when appropriate. 100 | 101 | ## Scope 102 | 103 | This Code of Conduct applies within all community spaces, and also applies when 104 | an individual is officially representing the community in public spaces. 105 | Examples of representing our community include using an official e-mail address, 106 | posting via an official social media account, or acting as an appointed 107 | representative at an online or offline event. 108 | 109 | ## Enforcement 110 | 111 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 112 | reported to the community leaders responsible for enforcement at 113 | [tsc@loopback.io](mailto:tsc@loopback.io). All complaints will be reviewed and 114 | investigated promptly and fairly. 115 | 116 | All community leaders are obligated to respect the privacy and security of the 117 | reporter of any incident. 118 | 119 | ## Enforcement Guidelines 120 | 121 | Community leaders will follow these Community Impact Guidelines in determining 122 | the consequences for any action they deem in violation of this Code of Conduct: 123 | 124 | ### 1. Correction 125 | 126 | **Community Impact**: Use of inappropriate language or other behavior deemed 127 | unprofessional or unwelcome in the community. 128 | 129 | **Consequence**: A private, written warning from community leaders, providing 130 | clarity around the nature of the violation and an explanation of why the 131 | behavior was inappropriate. A public apology may be requested. 132 | 133 | ### 2. Warning 134 | 135 | **Community Impact**: A violation through a single incident or series of 136 | actions. 137 | 138 | **Consequence**: A warning with consequences for continued behavior. No 139 | interaction with the people involved, including unsolicited interaction with 140 | those enforcing the Code of Conduct, for a specified period of time. This 141 | includes avoiding interactions in community spaces as well as external channels 142 | like social media. Violating these terms may lead to a temporary or permanent 143 | ban. 144 | 145 | ### 3. Temporary Ban 146 | 147 | **Community Impact**: A serious violation of community standards, including 148 | sustained inappropriate behavior. 149 | 150 | **Consequence**: A temporary ban from any sort of interaction or public 151 | communication with the community for a specified period of time. No public or 152 | private interaction with the people involved, including unsolicited interaction 153 | with those enforcing the Code of Conduct, is allowed during this period. 154 | Violating these terms may lead to a permanent ban. 155 | 156 | ### 4. Permanent Ban 157 | 158 | **Community Impact**: Demonstrating a pattern of violation of community 159 | standards, including sustained inappropriate behavior, harassment of an 160 | individual, or aggression toward or disparagement of classes of individuals. 161 | 162 | **Consequence**: A permanent ban from any sort of public interaction within the 163 | community. 164 | 165 | ## Attribution 166 | 167 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 168 | version 2.0, available at 169 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 170 | 171 | Community Impact Guidelines were inspired by 172 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 173 | 174 | [homepage]: https://www.contributor-covenant.org 175 | 176 | For answers to common questions about this code of conduct, see the FAQ at 177 | https://www.contributor-covenant.org/faq. Translations are available at 178 | https://www.contributor-covenant.org/translations. 179 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing ### 2 | 3 | Thank you for your interest in `loopback-connector-oracle`, an open source project 4 | administered by StrongLoop. 5 | 6 | Contributing to `loopback-connector-oracle` is easy. In a few simple steps: 7 | 8 | * Ensure that your effort is aligned with the project's roadmap by 9 | talking to the maintainers, especially if you are going to spend a 10 | lot of time on it. 11 | 12 | * Make something better or fix a bug. 13 | 14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and 15 | [Google Javascript Style Guide][]. 16 | 17 | * [Sign](https://loopback.io/doc/en/contrib/code-contrib.html) all commits with DCO. 18 | 19 | * Submit a pull request through Github. 20 | 21 | 22 | ### Developer Certificate of Origin 23 | 24 | This project uses [DCO](https://developercertificate.org/). Be sure to sign off 25 | your commits using the `-s` flag or adding `Signed-off-By: Name` in the 26 | commit message. 27 | 28 | **Example** 29 | 30 | ``` 31 | git commit -s -m "feat: my commit message" 32 | ``` 33 | 34 | Also see the [Contributing to LoopBack](https://loopback.io/doc/en/contrib/code-contrib.html) to get you started. 35 | 36 | 37 | [Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html 38 | [Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) IBM Corp. 2012,2018. All Rights Reserved. 2 | Node module: loopback-connector-oracle 3 | This project is licensed under the MIT License, full text below. 4 | 5 | -------- 6 | 7 | MIT license 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## TESTS 2 | 3 | TESTER = ./node_modules/.bin/mocha 4 | OPTS = --timeout 10000 --require ./test/init.js 5 | TESTS = test/*.test.js 6 | 7 | test: 8 | $(TESTER) $(OPTS) $(TESTS) 9 | test-verbose: 10 | $(TESTER) $(OPTS) --reporter spec $(TESTS) 11 | testing: 12 | $(TESTER) $(OPTS) --watch $(TESTS) 13 | .PHONY: test docs 14 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | This module automatically installs a copy of Oracle Instant Client under 2 | node_modules/instantclient. Oracle Instant client is required by a dependent 3 | strong-oracle module (https://github.com/strongloop/strong-oracle) to provide 4 | access to Oracle databases. 5 | 6 | For more information about Oracle Instant Client, please visit 7 | http://www.oracle.com/technetwork/database/features/instant-client/index-097480.html. 8 | 9 | The Oracle Instant Client is redistributed by IBM, a partner in the Oracle 10 | Partner Network (https://solutions.oracle.com/scwar/scr/Partner/SC2PP-IBM.html) 11 | with the following terms: 12 | 13 | 1. Any use of an Oracle object be under terms that make no guarantee as to 14 | continuing maintenance in the form of upgrades and updates/bug fixes/etc. and 15 | in such form as to allow its easy removal from the value added package if such 16 | value added package affords functionality beyond that provided by the embedded 17 | object that we would want the end user to continue to be able to use. 18 | 19 | 2. Further distribution or redistribution of Oracle Instant Client from the end 20 | user is prohibited. 21 | 22 | 3. Oracle retains all ownership and intellectual property rights in Oracle 23 | Development Code. 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback-connector-oracle 2 | 3 | [Oracle](https://www.oracle.com/database/index.html) is an object-relational database management system produced by Oracle Corporation. The `loopback-connector-oracle` module is the Oracle connector for the LoopBack framework based on the [node-oracledb](https://github.com/oracle/node-oracledb) module. 4 | 5 | ## Prerequisites 6 | 7 | **Node.js**: The Oracle connector requires Node.js version 6.x and up. 8 | 9 | **Windows**: On 32-bit Windows systems, you must use the 32-bit version of Node.js. On 64-bit Windows systems, you must use the 64-bit version of Node.js. For more information, see [Node-oracledb Installation on Windows](https://github.com/oracle/node-oracledb/blob/master/INSTALL.md#-7-node-oracledb-installation-on-windows). 10 | 11 | **Oracle**: The Oracle connector requires Oracle client libraries 11.2+ and can connect to Oracle Database Server 9.2+. 12 | 13 | ## Installation 14 | 15 | Before installing this module, please follow instructions at [https://oracle.github.io/node-oracledb/INSTALL.html](https://oracle.github.io/node-oracledb/INSTALL) 16 | to make sure all the prerequisites are satisfied. 17 | 18 | In your application root directory, enter this command to install the connector: 19 | 20 | ```shell 21 | $ npm install loopback-connector-oracle --save 22 | ``` 23 | 24 | If you create a Oracle 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. 25 | 26 | The `libaio` library is required on Linux systems: 27 | 28 | On Ubuntu/Debian, get it with this command: 29 | 30 | ``` 31 | sudo apt-get install libaio1 32 | ``` 33 | 34 | On Fedora/CentOS/RHEL, get it with this command: 35 | 36 | ``` 37 | sudo yum install libaio 38 | ``` 39 | 40 | ## Creating an Oracle data source 41 | 42 | For LoopBack 4 users, use the LoopBack 4 43 | [Command-line interface](https://loopback.io/doc/en/lb4/Command-line-interface.html) 44 | to generate a DataSource with Oracle connector to your LB4 application. Run 45 | [`lb4 datasource`](https://loopback.io/doc/en/lb4/DataSource-generator.html), it 46 | will prompt for configurations such as host, post, etc. that are required to 47 | connect to an Oracle database. 48 | 49 | After setting it up, the configuration can be found under 50 | `src/datasources/.datasource.ts`, which would look like this: 51 | 52 | ```ts 53 | const config = { 54 | name: "db", 55 | connector: "oracle", 56 | tns: "", 57 | host: "localhost", 58 | port: 1521, 59 | user: "admin", 60 | password: "pass", 61 | database: "XE", 62 | }; 63 | ``` 64 | 65 |
For LoopBack 3 users 66 | 67 | Use the [Data source generator](http://loopback.io/doc/en/lb3/Data-source-generator.html) to add a Oracle data source to your application. 68 | The generator will prompt for the database server hostname, port, and other settings 69 | required to connect to a Oracle database. It will also run the `npm install` command above for you. 70 | 71 | The entry in the application's `/server/datasources.json` will look like this: 72 | 73 | {% include code-caption.html content="/server/datasources.json" %} 74 | 75 | ```javascript 76 | "mydb": { 77 | "name": "mydb", 78 | "connector": "oracle", 79 | "tns": "demo", 80 | "host": "myserver", 81 | "port": 1521, 82 | "database": "mydb", 83 | "password": "mypassword", 84 | "user": "admin" 85 | } 86 | ``` 87 | 88 |
89 | 90 | Edit `.datasources.ts` to add any other additional properties 91 | that you require. 92 | 93 | ## Connector properties 94 | 95 | The connector properties depend on [naming methods](http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm#NETAG008) you use for the Oracle database. 96 | LoopBack supports three naming methods: 97 | 98 | - Easy connect: host/port/database. 99 | - Local naming (TNS): alias to a full connection string that can specify all the attributes that Oracle supports. 100 | - Directory naming (LDAP): directory for looking up the full connection string that can specify all the attributes that Oracle supports. 101 | 102 | ### Easy Connect 103 | 104 | Easy Connect is the simplest form that provides out-of-the-box TCP/IP connectivity to databases. 105 | The data source then has the following settings. 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 |
PropertyTypeDefaultDescription
host or hostnameStringlocalhostHost name or IP address of the Oracle database server
portNumber1521Port number of the Oracle database server
username or userString User name to connect to the Oracle database server
passwordString Password to connect to the Oracle database server
databaseStringXEOracle database listener name
149 | 150 | For example (LB4 form): 151 | 152 | {% include code-caption.html content="src/datasources/db.datasource.ts" %} 153 | 154 | ```ts 155 | const config = { 156 | name: "db", 157 | connector: "oracle", 158 | host: "oracle-demo.strongloop.com", 159 | port: 1521, 160 | user: "admin", 161 | password: "pass", 162 | database: "XE", 163 | }; 164 | ``` 165 | 166 | ### Local and directory naming 167 | 168 | Both local and directory naming require that you place configuration files in a TNS admin directory, such as `/oracle/admin`. 169 | 170 | **sqlnet.ora** 171 | 172 | This specifies the supported naming methods; for example: 173 | 174 | ``` 175 | NAMES.DIRECTORY_PATH=(LDAP,TNSNAMES,EZCONNECT) 176 | ``` 177 | 178 | **nsnames.ora** 179 | 180 | This maps aliases to connection stringsl for example: 181 | 182 | ``` 183 | demo1=(DESCRIPTION=(CONNECT_DATA=(SERVICE_NAME=))(ADDRESS=(PROTOCOL=TCP)(HOST=demo.strongloop.com)(PORT=1521))) 184 | ``` 185 | 186 | **ldap.ora** 187 | 188 | This configures the LDAP server. 189 | 190 | ``` 191 | DIRECTORY_SERVERS=(localhost:1389) 192 | DEFAULT_ADMIN_CONTEXT="dc=strongloop,dc=com" 193 | DIRECTORY_SERVER_TYPE=OID 194 | ``` 195 | 196 | #### Set up TNS_ADMIN environment variable 197 | 198 | For the Oracle connector to pick up the configurations, you must set the environment variable 'TNS_ADMIN' to the directory containing the `.ora` files. 199 | 200 | ``` 201 | export TNS_ADMIN= 202 | ``` 203 | 204 | Now you can use either the TNS alias or LDAP service name to configure a data source: 205 | 206 | ```ts 207 | const config = { 208 | name: "db", 209 | connector: "oracle", 210 | tns: "demo", // The tns property can be a tns name or LDAP service name 211 | username: "demo", 212 | password: "L00pBack", 213 | }); 214 | ``` 215 | 216 | ### Connection pooling options 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 242 | 243 | 244 | 245 | 246 | 252 | 253 | 254 | 255 |
Property nameDescriptionDefault value
minConnMinimum number of connections in the connection pool1
maxConnMaximum number of connections in the connection pool10
incrConn 240 |

Incremental number of connections for the connection pool.

241 |
1
timeout 247 | Time-out period in seconds for a connection in the connection pool. 248 | The Oracle connector 249 | will terminate connections in this 250 | connection pool that are idle longer than the time-out period. 251 | 10
256 | 257 | For example, 258 | 259 | {% include code-caption.html content="src/datasources/db.datasource.ts" %} 260 | 261 | ```ts 262 | const config = { 263 | name: "db", 264 | connector: "oracle", 265 | minConn:1, 266 | maxConn:5, 267 | incrConn:1, 268 | timeout: 10, 269 | ... 270 | }; 271 | ``` 272 | 273 | ### Connection troubleshooting 274 | 275 | If you encounter this error: 276 | 277 | ``` 278 | Error: ORA-24408: could not generate unique server group name 279 | ``` 280 | 281 | Then the Oracle 11g client requires an entry with your hostname pointing to 282 | `127.0.0.1`. 283 | 284 | To resolve: 285 | 286 | Get your hostname. Check your hostname by running this command (for example, if your machine's name is "earth"): 287 | 288 | ``` 289 | $ hostname 290 | earth 291 | ``` 292 | 293 | Update `/etc/hosts` and map `127.0.0.1` to your hostname "earth": 294 | 295 | ``` 296 | ... 297 | 127.0.0.1 localhost earth 298 | ... 299 | ``` 300 | 301 | Verify the fix. Run the app: 302 | 303 | ``` 304 | $ npm start 305 | ``` 306 | 307 | For more information, see [StackOverflow question](http://stackoverflow.com/questions/10484231/ora-24408-could-not-generate-unique-server-group-name). 308 | 309 | ## How LoopBack models map to Oracle tables 310 | 311 | There are several properties you can specify to map the LoopBack models to the existing tables in the Oracle database: 312 | 313 | **Model definition** maps to Oracle schema/table 314 | 315 | - `oracle.schema`: the schema name of the table 316 | - `oracle.table`: the table name of the model 317 | 318 | **Property definition** maps to Oracle column 319 | 320 | - `oracle.columnName`: the column name of the property 321 | - `oracle.dataType`: the type of the column 322 | 323 | (Check out more available database settings in the section [Data mapping properties](https://loopback.io/doc/en/lb4/Model.html#data-mapping-properties).) 324 | 325 | The following example model `User` maps to the table `USER` under schema `XE` in the database with its columns: 326 | 327 | {% include code-caption.html content="/models/user.model.ts" %} 328 | 329 | ```ts 330 | @model({ 331 | settings: { 332 | oracle: { 333 | schema: 'XE', 334 | table: 'USER' 335 | } 336 | } 337 | }) 338 | export class User extends Entity { 339 | @property({ 340 | type: 'number', 341 | required: true, 342 | id: true, 343 | oracle: { 344 | columnName: 'ID', 345 | dataType: 'NUMBER', 346 | nullable: 'N' 347 | }, 348 | }) 349 | id: number; 350 | 351 | @property({ 352 | type: 'string', 353 | required: true, 354 | oracle:{ 355 | columnName: 'LOCALTIONID', 356 | dataType: 'VARCHAR2', 357 | nullable: 'N' 358 | } 359 | }) 360 | locationId: string; 361 | ``` 362 | 363 |
For LoopBack 3 users 364 | 365 | {% include code-caption.html content="/common/models/model.json" %} 366 | 367 | ```javascript 368 | { 369 | "name":"User", 370 | "options":{ 371 | "idInjection":false, 372 | "oracle":{ 373 | "schema":"XE", 374 | "table":"USER" 375 | } 376 | }, 377 | "properties":{ 378 | "myId":{ 379 | "type":"number", 380 | "required":true, 381 | "id":1, 382 | "oracle":{ 383 | "columnName":"MYID", 384 | "dataType":"NUMBER", 385 | } 386 | }, 387 | "locationId":{ 388 | "type":"String", 389 | "required":true, 390 | "length":20, 391 | "id":2, 392 | "oracle":{ 393 | "columnName":"LOCATION_ID", 394 | "dataType":"VARCHAR2", 395 | "dataLength":20, 396 | "nullable":"N" 397 | } 398 | }, 399 | } 400 | } 401 | ``` 402 | 403 |
404 | 405 | **Notice**: the Oracle database default type is UPPERCASE. If the oracle settings are not specified in the model, for example: 406 | 407 | ```ts 408 | export class Demo extends Entity { 409 | @property({ 410 | type: 'number', 411 | required: true, 412 | id: true, 413 | }) 414 | id: number; 415 | ``` 416 | 417 | the connector would look for a table named `DEMO` under the default schema in the database and also map the id property to a column named `ID` in that table. This might cause errors if the default table/colum name doesn't exist. Please do specify the settings if needed. 418 | 419 | ### Configure a custom table/column name 420 | 421 | On the other hand, such settings would also allow you to have different names for models and tables. Take the `User` model as an example, we can map the `User` model to the table `MYUSER` and map the `id` property to column `MY_ID`as long as they are specified correctly: 422 | 423 | ```ts 424 | @model({ 425 | settings: { 426 | oracle: { 427 | schema: 'XE', 428 | table: 'MYUSER' // customized name 429 | } 430 | } 431 | }) 432 | export class User extends Entity { 433 | @property({ 434 | type: 'number', 435 | required: true, 436 | id: true, 437 | oracle: { 438 | columnName: 'MYID' // customized name 439 | }, 440 | }) 441 | id: number; 442 | //... 443 | ``` 444 | 445 |
For LoopBack 3 users 446 | 447 | ```javascript 448 | { 449 | "name":"User", 450 | "options":{ 451 | "idInjection":false, 452 | "oracle":{ 453 | "schema":"XE", 454 | "table":"MYUSER" // customized name 455 | } 456 | }, 457 | "properties":{ 458 | "id":{ 459 | "type":"number", 460 | "required":true, 461 | "id":1, 462 | "oracle":{ 463 | "columnName":"MYID", // customized name 464 | "dataType":"NUMBER", 465 | } 466 | }, 467 | //... 468 | } 469 | } 470 | ``` 471 | 472 |
473 | 474 | ### Type mapping 475 | 476 | See [LoopBack 4 types](http://loopback.io/doc/en/lb4/LoopBack-types.html) (or [LoopBack 3 types](http://loopback.io/doc/en/lb3/LoopBack-types.html)) for 477 | details on LoopBack's data types. 478 | 479 | #### JSON to Oracle Types 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 |
LoopBack TypeOracle Type
String
JSON
Text
default
VARCHAR2 492 |
Default length is 1024 493 |
NumberNUMBER
DateDATE
TimestampTIMESTAMP(3)
BooleanCHAR(1)
513 | 514 | #### Oracle Types to JSON 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 |
Oracle TypeLoopBack Type
CHAR(1)Boolean
CHAR(n)
VARCHAR
VARCHAR2,
LONG VARCHAR
NCHAR
NVARCHAR2
String
LONG, BLOB, CLOB, NCLOBNode.js Buffer object
NUMBER
INTEGER
DECIMAL
DOUBLE
FLOAT
BIGINT
SMALLINT
REAL
NUMERIC
BINARY_FLOAT
BINARY_DOUBLE
UROWID
ROWID
Number
DATE
TIMESTAMP
Date
546 | 547 | ## Discovery and auto-migration 548 | 549 | ### Model discovery 550 | 551 | The Oracle connector supports _model discovery_ that enables you to create LoopBack models based on an existing database schema. Once you defined your datasource: 552 | 553 | - LoopBack 4 users could use the commend 554 | [`lb4 discover`](https://loopback.io/doc/en/lb4/Discovering-models.html) to 555 | discover models. 556 | - For LB3 users, please check 557 | [Discovering models from relational databases](https://loopback.io/doc/en/lb3/Discovering-models-from-relational-databases.html). 558 | (See 559 | [database discovery API](http://apidocs.strongloop.com/loopback-datasource-juggler/#datasource-prototype-discoverandbuildmodels) 560 | for related APIs information) 561 | 562 | ### Auto-migration 563 | 564 | The Oracle connector also supports _auto-migration_ that enables you to create a database schema 565 | from LoopBack models. 566 | 567 | For example, based on the following model, the auto-migration method would create/alter existing `CUSTOMER` table under `XE` schema in the database. Table `CUSTOMER` would have two columns: `NAME` and `ID`, where `ID` is also the primary key, and its value would be generated by the database as it has `type: 'Number'` and `generated: true` set: 568 | 569 | ```ts 570 | @model() 571 | export class Customer extends Entity { 572 | @property({ 573 | id: true, 574 | type: 'Number', 575 | generated: true 576 | }) 577 | id: number; 578 | 579 | @property({ 580 | type: 'string' 581 | }) 582 | name: string; 583 | } 584 | ``` 585 | 586 |
For LoopBack 3 users 587 | 588 | ```javascript 589 | { 590 | "name":"Customer", 591 | "options":{ 592 | "idInjection":false, 593 | }, 594 | "properties":{ 595 | "id":{ 596 | "type":"number", 597 | "required":true, 598 | "id":1, 599 | }, 600 | "name":{ 601 | "type":"string", 602 | "required":false, 603 | }, 604 | } 605 | } 606 | ``` 607 | 608 |
609 | 610 | LoopBack Oracle connector creates the following schema objects for a given model: 611 | 612 | - A table, for example, PRODUCT 613 | - A sequence for the primary key, for example, PRODUCT_ID_SEQUENCE 614 | - A trigger to generate the primary key from the sequence, for example, PRODUCT_ID_TRIGGER 615 | 616 | #### Specifying database schema definition via model 617 | 618 | By default, table and column names are generated in uppercase. 619 | 620 | Besides the basic model metadata, you can also to specify part of the database schema definition via the 621 | property definition then run the migration script. They will be mapped to the database. The setting is the same as what we introduced in the section [Configure a custom table/column name](#configure-a-custom-table/column-name). One just needs to create models first, then run the migration script. 622 | 623 | For how to run the script and more details: 624 | 625 | - For LB4 users, please check [Database Migration](https://loopback.io/doc/en/lb4/Database-migrations.html) 626 | - For LB3 users, please check [Creating a database schema from models](https://loopback.io/doc/en/lb3/Creating-a-database-schema-from-models.html) 627 | - Check discovery/migration section the Oracle tutorial 628 | 629 | (See [LoopBack auto-migrate method](http://apidocs.strongloop.com/loopback-datasource-juggler/#datasource-prototype-automigrate) for related APIs information) 630 | 631 | Here are some limitations and tips: 632 | 633 | - If you defined `generated: true` in the id property, it generates **integers** by default. The Oracle connector does not support other auto-generated id types yet. Please check the [Auto-generated ids](#auto-generated-ids) section below if you would like use auto-generated id in different types such as uuid. 634 | - Only the id property supports the auto-generation setting `generated: true` for now 635 | - Destroying models may result in errors due to foreign key integrity. First delete any related models by calling delete on models with relationships. 636 | 637 | #### Auto-generated ids 638 | 639 | Auto-migrate supports the automatic generation of property values for the id property. For Oracle, the default id type is **integer**. Thus if you have `generated: true` set in the id property definition, it generates integers by default: 640 | 641 | ```ts 642 | { 643 | id: true, 644 | type: 'Number', 645 | required: false, 646 | generated: true // enables auto-generation 647 | } 648 | ``` 649 | 650 | It might be a case to use UUIDs as the primary key in Oracle instead of integers. You can enable it with either the following ways: 651 | 652 | - use uuid that is **generated by your LB application** by setting [`defaultFn: uuid`](https://loopback.io/doc/en/lb4/Model.html#property-decorator): 653 | 654 | ```ts 655 | @property({ 656 | id: true, 657 | type: 'string' 658 | defaultFn: 'uuid', 659 | // generated: true, -> not needed 660 | }) 661 | id: string; 662 | ``` 663 | 664 | - alter the table to use Oracle built-in uuid functions (`SYS_GUID()` for example): 665 | 666 | ```ts 667 | @property({ 668 | id: true, 669 | type: 'String', 670 | required: false, 671 | // settings below are needed 672 | generated: true, 673 | useDefaultIdType: false, 674 | }) 675 | id: string; 676 | ``` 677 | 678 | Then you will need to alter the table manually. 679 | 680 | ## Running tests 681 | 682 | ### Own instance 683 | 684 | If you have a local or remote Oracle instance and would like to use that to run the test suite, use the following command: 685 | 686 | - Linux 687 | 688 | ```bash 689 | ORACLE_HOST= ORACLE_PORT= ORACLE_USER= ORACLE_PASSWORD= ORACLE_DATABASE= npm test 690 | ``` 691 | 692 | - Windows 693 | 694 | ```bash 695 | SET ORACLE_HOST= 696 | SET ORACLE_PORT= 697 | SET ORACLE_USER= 698 | SET ORACLE_PASSWORD= 699 | SET ORACLE_DATABASE= 700 | npm test 701 | ``` 702 | 703 | ### Docker 704 | 705 | If you do not have a local Oracle instance, you can also run the test suite with very minimal requirements. 706 | 707 | - Assuming you have [Docker](https://docs.docker.com/engine/installation/) installed, run the following script which would spawn an Oracle instance on your local machine: 708 | 709 | ```bash 710 | source setup.sh 711 | ``` 712 | 713 | where ``, ``, ``, and `PASSWORD` are optional parameters. The default values are `localhost`, `1521`, `admin`, and `0raclep4ss` respectively. The `DATABASE` setting is always `XE`. 714 | 715 | - Run the test: 716 | 717 | ```bash 718 | npm test 719 | ``` 720 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security advisories 4 | 5 | Security advisories can be found on the 6 | [LoopBack website](https://loopback.io/doc/en/sec/index.html). 7 | 8 | ## Reporting a vulnerability 9 | 10 | If you think you have discovered a new security issue with any LoopBack package, 11 | **please do not report it on GitHub**. Instead, send an email to 12 | [security@loopback.io](mailto:security@loopback.io) with the following details: 13 | 14 | - Full description of the vulnerability. 15 | - Steps to reproduce the issue. 16 | - Possible solutions. 17 | 18 | If you are sending us any logs as part of the report, then make sure to redact 19 | any sensitive data from them. -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | ## Methods for discovery 2 | 3 | ***NOTE***: All the following methods are asynchronous. 4 | 5 | ### discoverModelDefinitions(options, cb) 6 | 7 | | Parameter | Description | 8 | | ----- | ----- | 9 | | options| Object with properties described below.| 10 | | cb | Get a list of table/view names; see example below.| 11 | 12 | Properties of options parameter: 13 | 14 | * all: {Boolean} To include tables/views from all schemas/owners 15 | * owner/schema: {String} The schema/owner name 16 | * views: {Boolean} Whether to include views 17 | 18 | Example of callback function return value: 19 | 20 | {type: 'table', name: 'INVENTORY', owner: 'STRONGLOOP' } 21 | {type: 'table', name: 'LOCATION', owner: 'STRONGLOOP' } 22 | {type: 'view', name: 'INVENTORY_VIEW', owner: 'STRONGLOOP' } 23 | 24 | ### discoverModelProperties(table, options, cb) 25 | 26 | | Parameter | Description | 27 | | ----- | ----- | 28 | | table: {String} | The name of a table or view | 29 | | options | owner/schema: {String} The schema/owner name | 30 | | cb | Get a list of model property definitions; see example below. | 31 | 32 | { owner: 'STRONGLOOP', 33 | tableName: 'PRODUCT', 34 | columnName: 'ID', 35 | dataType: 'VARCHAR2', 36 | dataLength: 20, 37 | nullable: 'N', 38 | type: 'String' } 39 | { owner: 'STRONGLOOP', 40 | tableName: 'PRODUCT', 41 | columnName: 'NAME', 42 | dataType: 'VARCHAR2', 43 | dataLength: 64, 44 | nullable: 'Y', 45 | type: 'String' } 46 | 47 | ### discoverPrimaryKeys(table, options, cb) 48 | 49 | | Parameter | Description | 50 | | ----- | ----- | 51 | | table: {String} | Name of a table or view | 52 | | options | owner/schema: {String} The schema/owner name | 53 | | cb | Get a list of primary key definitions; see example below. | 54 | 55 | { owner: 'STRONGLOOP', 56 | tableName: 'INVENTORY', 57 | columnName: 'PRODUCT_ID', 58 | keySeq: 1, 59 | pkName: 'ID_PK' } 60 | { owner: 'STRONGLOOP', 61 | tableName: 'INVENTORY', 62 | columnName: 'LOCATION_ID', 63 | keySeq: 2, 64 | pkName: 'ID_PK' } 65 | 66 | ### discoverForeignKeys(table, options, cb) 67 | 68 | | Parameter | Description | 69 | | ----- | ----- | 70 | | table: {String} | Name of a table or view | 71 | | options | owner/schema: {String} The schema/owner name | 72 | | cb | Get a list of foreign key definitions; see example below. | 73 | 74 | { fkOwner: 'STRONGLOOP', 75 | fkName: 'PRODUCT_FK', 76 | fkTableName: 'INVENTORY', 77 | fkColumnName: 'PRODUCT_ID', 78 | keySeq: 1, 79 | pkOwner: 'STRONGLOOP', 80 | pkName: 'PRODUCT_PK', 81 | pkTableName: 'PRODUCT', 82 | pkColumnName: 'ID' } 83 | 84 | ### discoverExportedForeignKeys(table, options, cb) 85 | 86 | | Parameter | Description | 87 | | ----- | ----- | 88 | | table: {String} | The name of a table or view | 89 | | options | owner/schema: {String} The schema/owner name 90 | | cb | Get a list of foreign key definitions that reference the primary key of the given table; see example below. | 91 | 92 | { fkName: 'PRODUCT_FK', 93 | fkOwner: 'STRONGLOOP', 94 | fkTableName: 'INVENTORY', 95 | fkColumnName: 'PRODUCT_ID', 96 | keySeq: 1, 97 | pkName: 'PRODUCT_PK', 98 | pkOwner: 'STRONGLOOP', 99 | pkTableName: 'PRODUCT', 100 | pkColumnName: 'ID' } 101 | 102 | ## Synchronous discovery methods 103 | 104 | * Oracle.prototype.discoverModelDefinitionsSync = function (options) 105 | * Oracle.prototype.discoverModelPropertiesSync = function (table, options) 106 | * Oracle.prototype.discoverPrimaryKeysSync= function(table, options) 107 | * Oracle.prototype.discoverForeignKeysSync= function(table, options) 108 | * Oracle.prototype.discoverExportedForeignKeysSync= function(table, options) 109 | -------------------------------------------------------------------------------- /deps/juggler-v3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juggler-v3", 3 | "version": "3.0.0", 4 | "dependencies": { 5 | "loopback-datasource-juggler": "3.x", 6 | "should": "^13.2.3" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /deps/juggler-v3/test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const juggler = require('loopback-datasource-juggler'); 9 | const name = require('./package.json').name; 10 | 11 | require('../../test/init/init'); 12 | 13 | describe(name, function() { 14 | before(function() { 15 | return global.resetDataSourceClass(juggler.DataSource); 16 | }); 17 | 18 | after(function() { 19 | return global.resetDataSourceClass(); 20 | }); 21 | 22 | require('loopback-datasource-juggler/test/common.batch.js'); 23 | require('loopback-datasource-juggler/test/include.test.js'); 24 | }); 25 | -------------------------------------------------------------------------------- /deps/juggler-v4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juggler-v4", 3 | "version": "4.0.0", 4 | "dependencies": { 5 | "loopback-datasource-juggler": "4.x", 6 | "should": "^13.2.3" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /deps/juggler-v4/test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const juggler = require('loopback-datasource-juggler'); 9 | const name = require('./package.json').name; 10 | 11 | require('../../test/init/init'); 12 | 13 | describe(name, function() { 14 | before(function() { 15 | return global.resetDataSourceClass(juggler.DataSource); 16 | }); 17 | 18 | after(function() { 19 | return global.resetDataSourceClass(); 20 | }); 21 | 22 | require('loopback-datasource-juggler/test/common.batch.js'); 23 | require('loopback-datasource-juggler/test/include.test.js'); 24 | }); 25 | -------------------------------------------------------------------------------- /docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": [ 3 | {"title": "LoopBack Oracle Connector API", "depth": 2}, 4 | "lib/oracle.js" 5 | ], 6 | "codeSectionDepth": 3 7 | } 8 | 9 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const g = require('strong-globalize')(); 9 | const DataSource = require('loopback-datasource-juggler').DataSource; 10 | const config = require('rc')('loopback', {dev: {oracle: {}}}).dev.oracle; 11 | 12 | const ds = new DataSource(require('../'), config); 13 | 14 | function show(err, models) { 15 | if (err) { 16 | console.error(err); 17 | } else { 18 | models.forEach(function(m) { 19 | console.log(m); 20 | }); 21 | } 22 | } 23 | 24 | /* 25 | ds.discoverModelDefinitions({views: true, limit: 20}, show); 26 | 27 | ds.discoverModelProperties('PRODUCT', show); 28 | 29 | // ds.discoverModelProperties('INVENTORY_VIEW', {owner: 'STRONGLOOP'}, show); 30 | 31 | ds.discoverPrimaryKeys('INVENTORY', show); 32 | ds.discoverForeignKeys('INVENTORY', show); 33 | 34 | ds.discoverExportedForeignKeys('PRODUCT', show); 35 | */ 36 | 37 | const table = (process.argv.length > 2) ? process.argv[2] : 'INVENTORY_VIEW'; 38 | 39 | ds.discoverSchema(table, {owner: 'STRONGLOOP'}, function(err, schema) { 40 | console.log(JSON.stringify(schema)); 41 | const model = ds.define(schema.name, schema.properties, schema.options); 42 | // console.log(model); 43 | model.all(show); 44 | }); 45 | 46 | ds.discoverAndBuildModels( 47 | 'INVENTORY', 48 | {owner: 'STRONGLOOP', 49 | visited: {}, 50 | associations: true}, 51 | function(err, models) { 52 | for (const m in models) { 53 | models[m].all(show); 54 | } 55 | 56 | models.Inventory.findOne({}, function(err, inv) { 57 | console.log(g.f('\nInventory: %s', inv)); 58 | inv.product(function(err, prod) { 59 | console.log(g.f('\nProduct: %s', prod)); 60 | console.log('\n ------------- '); 61 | // ds.disconnect(); // This will crash node-oracle as the connection is disconnected while other statements are still running 62 | }); 63 | }); 64 | }, 65 | ); 66 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const SG = require('strong-globalize'); 8 | SG.SetRootDir(__dirname); 9 | 10 | module.exports = require('./lib/oracle.js'); 11 | -------------------------------------------------------------------------------- /intl/cs/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}. Bylo zjištěno duplicitní ID.", 3 | "56b8408263e115ae231c85f10d988fdc": "\nProdukt: {0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} musí být {{object}}: {0}", 5 | "781db463c682d5eb7259c74773ab1860": "\nInventář: {0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "Syntaxe regulárního výrazu {{Oracle}} nerespektuje příznak {{`g`}}", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} je povinný řetězcový argument: {0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "Neplatná {{isolationLevel}}: {0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /intl/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}. Duplizierte ID erkannt.", 3 | "56b8408263e115ae231c85f10d988fdc": "\nProdukt: {0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} muss ein {{object}} sein: {0}", 5 | "781db463c682d5eb7259c74773ab1860": "\nBestand: {0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "{{Oracle}}-regex-Syntax berücksichtigt nicht das {{`g`}}-Flag", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} ist ein erforderliches Zeichenfolgeargument: {0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "Ungültiges {{isolationLevel}}: {0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /intl/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}. Duplicate id detected.", 3 | "56b8408263e115ae231c85f10d988fdc": "\nProduct: {0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} must be an {{object}}: {0}", 5 | "781db463c682d5eb7259c74773ab1860": "\nInventory: {0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "{{Oracle}} regex syntax does not respect the {{`g`}} flag", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} is a required string argument: {0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "Invalid {{isolationLevel}}: {0}" 9 | } 10 | -------------------------------------------------------------------------------- /intl/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}. Se ha detectado un id duplicado.", 3 | "56b8408263e115ae231c85f10d988fdc": "\nProducto: {0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} debe ser un {{object}}: {0}", 5 | "781db463c682d5eb7259c74773ab1860": "\nInventario: {0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "la sintaxis de expresión regular {{Oracle}} no respeta el distintivo {{`g`}}", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} es un argumento de serie obligatorio: {0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "{{isolationLevel}} no válido: {0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /intl/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}. ID en double détecté.", 3 | "56b8408263e115ae231c85f10d988fdc": "\nProduit : {0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} doit être un {{object}} : {0}", 5 | "781db463c682d5eb7259c74773ab1860": "\nInventaire : {0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "La syntaxe {{Oracle}} regex ne respecte pas l'indicateur {{`g`}}", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} est un argument de chaîne obligatoire : {0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "{{isolationLevel}} non valide : {0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /intl/it/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}. È stato rilevato un ID duplicato.", 3 | "56b8408263e115ae231c85f10d988fdc": "\nProdotto: {0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} deve essere un {{object}}: {0}", 5 | "781db463c682d5eb7259c74773ab1860": "\nInventario: {0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "La sintassi regex {{Oracle}} non rispetta l'indicatore {{`g`}}", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} è un argomento stringa obbligatorio: {0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "{{isolationLevel}} non valido: {0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /intl/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}。重複する ID が検出されました。", 3 | "56b8408263e115ae231c85f10d988fdc": "\n製品: {0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} は {{object}} でなければなりません: {0}", 5 | "781db463c682d5eb7259c74773ab1860": "\nインベントリー: {0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "{{Oracle}} 正規表現の構文では {{`g`}} フラグは考慮されません", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} は必須のストリング引数です: {0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "無効な {{isolationLevel}}: {0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /intl/ko/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}. 중복 ID가 발견되었습니다.", 3 | "56b8408263e115ae231c85f10d988fdc": "\n제품 {0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}}이(가) {{object}}이어야 함: {0}", 5 | "781db463c682d5eb7259c74773ab1860": "\n인벤토리: {0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "{{Oracle}} regex 구문에서 {{`g`}} 플래그를 준수하지 않음", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}}은 필수 문자열 인수임: {0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "올바르지 않은 {{isolationLevel}}: {0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /intl/nl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}. Dubbel ID gevonden.", 3 | "56b8408263e115ae231c85f10d988fdc": "\nProduct: {0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} moet een {{object}} zijn: {0}", 5 | "781db463c682d5eb7259c74773ab1860": "\nInventaris: {0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "Syntaxis van {{Oracle}}-expressie respecteert de vlag {{`g`}} niet", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} is een verplicht tekenreeksargument: {0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "Ongeldig {{isolationLevel}}: {0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /intl/pl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}. Wykryto zduplikowany identyfikator.", 3 | "56b8408263e115ae231c85f10d988fdc": "\nProdukt: {0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "Właściwość {{options}} musi być obiektem {{object}}: {0}", 5 | "781db463c682d5eb7259c74773ab1860": "\nMagazyn: {0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "Składnia wyrażenia regularnego bazy danych {{Oracle}} nie respektuje flagi {{`g`}}", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} jest wymaganym argumentem łańcuchowym: {0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "Niepoprawny element {{isolationLevel}}: {0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /intl/pt/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}. ID duplicado detectado.", 3 | "56b8408263e115ae231c85f10d988fdc": "\nProduto: {0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} deve ser um {{object}}: {0}", 5 | "781db463c682d5eb7259c74773ab1860": "\nInventário: {0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "Sintaxe regex de {{Oracle}} não respeita a sinalização {{`g`}}", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} é um argumento de sequência necessário: {0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "{{isolationLevel}} inválido: {0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /intl/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}. Обнаружена копия ИД.", 3 | "56b8408263e115ae231c85f10d988fdc": "\nПродукт:{0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} должны иметь тип {{object}}: {0}", 5 | "781db463c682d5eb7259c74773ab1860": "\nРеестр: {0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "Синтаксис regex {{Oracle}} не учитывает флаг {{`g`}}", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} - это обязательный строковый аргумент: {0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "Недопустимый {{isolationLevel}}: {0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /intl/tr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}. Yinelenen tanıtıcısı saptandı.", 3 | "56b8408263e115ae231c85f10d988fdc": "\nÜrün: {0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} bir {{object}} olmalıdır: {0}", 5 | "781db463c682d5eb7259c74773ab1860": "\nStok: {0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "{{Oracle}} düzenli ifade sözdizimi {{`g`}} işareti kuralına uymuyor", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} zorunlu bir dizgi bağımsız değişkeni: {0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "Geçersiz {{isolationLevel}}: {0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /intl/zh-Hans/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}。检测到重复标识。", 3 | "56b8408263e115ae231c85f10d988fdc": "\n产品:{0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} 必须为 {{object}}:{0}", 5 | "781db463c682d5eb7259c74773ab1860": "\n库存:{0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "{{Oracle}} 正则表达式语法不考虑 {{`g`}} 标志", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} 是必需的字符串自变量:{0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "无效的 {{isolationLevel}}:{0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /intl/zh-Hant/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "3d26dd09004339e3c04f61799780e5a4": "{0}。偵測到重複的 ID。", 3 | "56b8408263e115ae231c85f10d988fdc": "\n產品:{0}", 4 | "6ce5c3a3d305e965ff06e2b3e16e1252": "{{options}} 必須是 {{object}}:{0}", 5 | "781db463c682d5eb7259c74773ab1860": "\n庫存:{0}", 6 | "9926ce70a3a4fb85b8e19605c448c58a": "{{Oracle}} regex 語法未遵循 {{`g`}} 旗標", 7 | "a0078d732b2dbabf98ed2efcdb55b402": "{{table}} 是必要的字串引數:{0}", 8 | "cac93b1aaea2a0f4601f295837eef4a7": "無效 {{isolationLevel}}:{0}" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /lib/discovery.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const g = require('strong-globalize')(); 8 | const async = require('async'); 9 | 10 | module.exports = mixinDiscovery; 11 | 12 | function mixinDiscovery(Oracle) { 13 | function getPagination(filter) { 14 | const pagination = []; 15 | if (filter && (filter.limit || filter.offset || filter.skip)) { 16 | let offset = Number(filter.offset); 17 | if (!offset) { 18 | offset = Number(filter.skip); 19 | } 20 | if (offset) { 21 | pagination.push('R >= ' + (offset + 1)); 22 | } else { 23 | offset = 0; 24 | } 25 | const limit = Number(filter.limit); 26 | if (limit) { 27 | pagination.push('R <= ' + (offset + limit)); 28 | } 29 | } 30 | return pagination; 31 | } 32 | 33 | Oracle.prototype.buildQuerySchemas = function(options) { 34 | const sql = 'select username from SYS.all_users'; 35 | return this.paginateSQL(sql, 'username', options); 36 | }; 37 | 38 | /*! 39 | * Create a SQL statement that supports pagination 40 | * @param {String} sql The SELECT statement that supports pagination 41 | * @param {String} orderBy The 'order by' columns 42 | * @param {Object} options options 43 | * @returns {String} The SQL statement 44 | */ 45 | Oracle.prototype.paginateSQL = function(sql, orderBy, options) { 46 | const pagination = getPagination(options); 47 | orderBy = orderBy || '1'; 48 | if (pagination.length) { 49 | return 'SELECT * FROM (SELECT ROW_NUMBER() OVER (ORDER BY ' + orderBy + 50 | ') R, ' + sql.substring(7) + ') WHERE ' + pagination.join(' AND '); 51 | } else { 52 | return sql; 53 | } 54 | }; 55 | 56 | /*! 57 | * Build sql for listing tables 58 | * @param {Object} options {all: for all owners, owner: for a given owner} 59 | * @returns {String} The sql statement 60 | */ 61 | Oracle.prototype.buildQueryTables = function(options) { 62 | let sqlTables = null; 63 | const owner = options.owner || options.schema; 64 | 65 | if (options.all && !owner) { 66 | sqlTables = this.paginateSQL('SELECT \'table\' AS "type", ' + 67 | 'table_name AS "name", ' + 68 | 'owner AS "owner" FROM all_tables', 'owner, table_name', options); 69 | } else if (owner) { 70 | sqlTables = this.paginateSQL('SELECT \'table\' AS "type", ' + 71 | 'table_name AS "name", ' + 72 | 'owner AS "owner" FROM all_tables WHERE owner=\'' + owner + '\'', 73 | 'owner, table_name', options); 74 | } else { 75 | sqlTables = this.paginateSQL('SELECT \'table\' AS "type", ' + 76 | 'table_name AS "name", SYS_CONTEXT(\'USERENV\',' + 77 | ' \'SESSION_USER\') AS "owner" FROM user_tables', 78 | 'table_name', options); 79 | } 80 | return sqlTables; 81 | }; 82 | 83 | /*! 84 | * Build sql for listing views 85 | * @param {Object} options {all: for all owners, owner: for a given owner} 86 | * @returns {String} The sql statement 87 | */ 88 | Oracle.prototype.buildQueryViews = function(options) { 89 | let sqlViews = null; 90 | if (options.views) { 91 | const owner = options.owner || options.schema; 92 | 93 | if (options.all && !owner) { 94 | sqlViews = this.paginateSQL('SELECT \'view\' AS "type", ' + 95 | 'view_name AS "name", owner AS "owner" FROM all_views', 96 | 'owner, view_name', options); 97 | } else if (owner) { 98 | sqlViews = this.paginateSQL('SELECT \'view\' AS "type", ' + 99 | 'view_name AS "name", owner AS "owner" FROM all_views ' + 100 | 'WHERE owner=\'' + owner + '\'', 101 | 'owner, view_name', options); 102 | } else { 103 | sqlViews = this.paginateSQL('SELECT \'view\' AS "type", ' + 104 | 'view_name AS "name", SYS_CONTEXT(\'USERENV\', \'SESSION_USER\')' + 105 | ' AS "owner" FROM user_views', 'view_name', options); 106 | } 107 | } 108 | return sqlViews; 109 | }; 110 | 111 | /** 112 | * Discover model definitions. 113 | * Example of callback function return value: 114 | * ```js 115 | * {type: 'table', name: 'INVENTORY', owner: 'STRONGLOOP' } 116 | * {type: 'table', name: 'LOCATION', owner: 'STRONGLOOP' } 117 | * {type: 'view', name: 'INVENTORY_VIEW', owner: 'STRONGLOOP' } 118 | *``` 119 | * @options {Object} options Options for discovery; see below. 120 | * @prop {Boolean} all If true, include tables/views from all schemas/owners. 121 | * @prop {String} owner/schema The schema/owner name. QUESTION: What is the actual name of this property: 'owner' or 'schema'? Or can it be either? 122 | * @prop {Boolean} views If true, include views. 123 | * @prop {Number} limit Maximimum number of results to return. NOTE: This was not in .md doc, but is included in tests. 124 | * @param {Function} [cb] The callback function. 125 | * @private 126 | */ 127 | 128 | /** 129 | * Discover the model definitions synchronously. 130 | * 131 | * @options {Object} options Options for discovery; see below. 132 | * @prop {Boolean} all If true, include tables/views from all schemas/owners. 133 | * @prop {String} owner/schema The schema/owner name. QUESTION: What is the actual name of this property: 'owner' or 'schema'? Or can it be either? 134 | * @prop {Boolean} views If true, include views. 135 | * @prop {Number} limit Maximimum number of results to return. NOTE: This was not in .md doc, but is included in tests. 136 | * @private 137 | */ 138 | Oracle.prototype.discoverModelDefinitionsSync = function(options) { 139 | options = options || {}; 140 | const sqlTables = this.buildQueryTables(options); 141 | let tables = this.querySync(sqlTables); 142 | const sqlViews = this.buildQueryViews(options); 143 | if (sqlViews) { 144 | const views = this.querySync(sqlViews); 145 | tables = tables.concat(views); 146 | } 147 | return tables; 148 | }; 149 | 150 | /*! 151 | * Normalize the arguments 152 | * @param {String} table The table name 153 | * @param {Object} [options] The options object 154 | * @param {Function} [cb] The callback function 155 | */ 156 | Oracle.prototype.getArgs = function(table, options, cb) { 157 | if ('string' !== typeof table || !table) { 158 | throw new Error(g.f('{{table}} is a required string argument: %s', 159 | table)); 160 | } 161 | options = options || {}; 162 | if (!cb && 'function' === typeof options) { 163 | cb = options; 164 | options = {}; 165 | } 166 | if (typeof options !== 'object') { 167 | throw new Error(g.f('{{options}} must be an {{object}}: %s', options)); 168 | } 169 | return { 170 | owner: options.owner || options.schema, 171 | // Base connector expects `schema` 172 | schema: options.owner || options.schema, 173 | table: table, 174 | options: options, 175 | cb: cb, 176 | }; 177 | }; 178 | 179 | /*! 180 | * Build the sql statement to query columns for a given table 181 | * @param {String} owner The DB owner/schema name 182 | * @param {String} table The table name 183 | * @returns {String} The sql statement 184 | */ 185 | Oracle.prototype.buildQueryColumns = function(owner, table) { 186 | let sql = null; 187 | if (owner) { 188 | sql = this.paginateSQL('SELECT owner AS "owner", table_name AS ' + 189 | ' "tableName", column_name AS "columnName", data_type AS "dataType",' + 190 | ' data_length AS "dataLength", data_precision AS "dataPrecision",' + 191 | ' data_scale AS "dataScale", nullable AS "nullable"' + 192 | ' FROM all_tab_columns' + 193 | ' WHERE owner=\'' + owner + '\'' + 194 | (table ? ' AND table_name=\'' + table + '\'' : ''), 195 | 'table_name, column_id', {}); 196 | } else { 197 | sql = this.paginateSQL('SELECT' + 198 | ' SYS_CONTEXT(\'USERENV\', \'SESSION_USER\') ' + 199 | ' AS "owner", table_name AS "tableName", column_name AS "columnName",' + 200 | ' data_type AS "dataType",' + 201 | ' data_length AS "dataLength", data_precision AS "dataPrecision",' + 202 | ' data_scale AS "dataScale", nullable AS "nullable"' + 203 | ' FROM user_tab_columns' + 204 | (table ? ' WHERE table_name=\'' + table + '\'' : ''), + 205 | 'table_name, column_id', {}); 206 | } 207 | sql += ' ORDER BY column_id'; 208 | return sql; 209 | }; 210 | 211 | /** 212 | * Discover model properties from a table. Returns an array of columns for the specified table. 213 | * 214 | * @param {String} table The table name 215 | * @options {Object} options Options for discovery; see below. 216 | * @prop {Boolean} all If true, include tables/views from all schemas/owners. 217 | * @prop {String} owner/schema The schema/owner name. QUESTION: What is the actual name of this property: 'owner' or 'schema'? Or can it be either? 218 | * @prop {Boolean} views If true, include views. 219 | * @prop {Number} limit Maximimum number of results to return. NOTE: This was not in .md doc, but is included in tests. 220 | * @param {Function} [cb] The callback function 221 | * 222 | * ```js 223 | * { owner: 'STRONGLOOP', 224 | * tableName: 'PRODUCT', 225 | * columnName: 'ID', 226 | * dataType: 'VARCHAR2', 227 | * dataLength: 20, 228 | * nullable: 'N', 229 | * type: 'String' 230 | * } 231 | * { owner: 'STRONGLOOP', 232 | * tableName: 'PRODUCT', 233 | * columnName: 'NAME', 234 | * dataType: 'VARCHAR2', 235 | * dataLength: 64, 236 | * nullable: 'Y', 237 | * type: 'String' 238 | * } 239 | *``` 240 | * @private 241 | */ 242 | 243 | /** 244 | * Discover model properties from a table synchronously. See example return value for discoverModelProperties(). 245 | * 246 | * @param {String} table The table name 247 | * @options {Object} options Options for discovery; see below. 248 | * @prop {Boolean} all If true, include tables/views from all schemas/owners. 249 | * @prop {String} owner/schema The schema/owner name. QUESTION: What is the actual name of this property: 'owner' or 'schema'? Or can it be either? 250 | * @prop {Boolean} views If true, include views. 251 | * @prop {Number} limit Maximimum number of results to return. NOTE: This was not in .md doc, but is included in tests. 252 | * @private 253 | * 254 | */ 255 | Oracle.prototype.discoverModelPropertiesSync = function(table, options) { 256 | const self = this; 257 | const args = this.getArgs(table, options); 258 | const owner = args.owner; 259 | table = args.table; 260 | options = args.options; 261 | 262 | const sql = this.buildQueryColumns(owner, table); 263 | const results = this.querySync(sql); 264 | results.map(function(r) { 265 | r.type = self.buildPropertyType(r.dataType, r.dataLength); 266 | }); 267 | return results; 268 | }; 269 | 270 | /*! 271 | * Build the sql statement for querying primary keys of a given table 272 | * @param owner 273 | * @param table 274 | * @returns {String} 275 | */ 276 | // http://docs.oracle.com/javase/6/docs/api/java/sql/DatabaseMetaData.html# 277 | // getPrimaryKeys(java.lang.String, java.lang.String, java.lang.String) 278 | Oracle.prototype.buildQueryPrimaryKeys = function(owner, table) { 279 | let sql = 'SELECT uc.owner AS "owner", ' + 280 | 'uc.table_name AS "tableName", col.column_name AS "columnName",' + 281 | ' col.position AS "keySeq", uc.constraint_name AS "pkName" FROM' + 282 | (owner ? 283 | ' ALL_CONSTRAINTS uc, ALL_CONS_COLUMNS col' : 284 | ' USER_CONSTRAINTS uc, USER_CONS_COLUMNS col') + 285 | ' WHERE uc.constraint_type=\'P\' AND ' + 286 | 'uc.constraint_name=col.constraint_name'; 287 | 288 | if (owner) { 289 | sql += ' AND uc.owner=\'' + owner + '\''; 290 | } 291 | if (table) { 292 | sql += ' AND uc.table_name=\'' + table + '\''; 293 | } 294 | sql += ' ORDER BY uc.owner, col.constraint_name, uc.table_name, ' + 295 | 'col.position'; 296 | return sql; 297 | }; 298 | 299 | /** 300 | * Discover primary keys for specified table. Returns an array of primary keys for the specified table. 301 | * Example return value: 302 | * ```js 303 | * { owner: 'STRONGLOOP', 304 | * tableName: 'INVENTORY', 305 | * columnName: 'PRODUCT_ID', 306 | * keySeq: 1, 307 | * pkName: 'ID_PK' } 308 | * { owner: 'STRONGLOOP', 309 | * tableName: 'INVENTORY', 310 | * columnName: 'LOCATION_ID', 311 | * keySeq: 2, 312 | * pkName: 'ID_PK' } 313 | *``` 314 | * 315 | * @param {String} table The table name 316 | * @options {Object} options Options for discovery; see below. 317 | * @prop {Boolean} all If true, include tables/views from all schemas/owners. 318 | * @prop {String} owner/schema The schema/owner name. QUESTION: What is the actual name of this property: 'owner' or 'schema'? Or can it be either? 319 | * @prop {Boolean} views If true, include views. 320 | * @prop {Number} limit Maximimum number of results to return. NOTE: This was not in .md doc, but is included in tests. 321 | * @param {Function} [cb] The callback function 322 | * @private 323 | */ 324 | 325 | /** 326 | * Discover primary keys synchronously for specified table. See example return value for discoverPrimaryKeys(). 327 | * 328 | * @param {String} table 329 | * @options {Object} options Options for discovery; see below. 330 | * @prop {Boolean} all If true, include tables/views from all schemas/owners. 331 | * @prop {String} owner/schema The schema/owner name. QUESTION: What is the actual name of this property: 'owner' or 'schema'? Or can it be either? 332 | * @prop {Boolean} views If true, include views. 333 | * @prop {Number} limit Maximimum number of results to return. NOTE: This was not in .md doc, but is included in tests. 334 | * @private 335 | */ 336 | Oracle.prototype.discoverPrimaryKeysSync = function(table, options) { 337 | const args = this.getArgs(table, options); 338 | const owner = args.owner; 339 | table = args.table; 340 | options = args.options; 341 | 342 | const sql = this.buildQueryPrimaryKeys(owner, table); 343 | return this.querySync(sql); 344 | }; 345 | 346 | /*! 347 | * Build the sql statement for querying foreign keys of a given table 348 | * @param {String} owner The DB owner/schema name 349 | * @param {String} table The table name 350 | * @returns {String} The SQL statement to find foreign keys 351 | */ 352 | Oracle.prototype.buildQueryForeignKeys = function(owner, table) { 353 | let sql = 354 | 'SELECT a.owner AS "fkOwner", a.constraint_name AS "fkName", ' + 355 | 'a.table_name AS "fkTableName", a.column_name AS "fkColumnName", ' + 356 | 'a.position AS "keySeq", jcol.owner AS "pkOwner", ' + 357 | 'jcol.constraint_name AS "pkName", jcol.table_name AS "pkTableName", ' + 358 | 'jcol.column_name AS "pkColumnName"' + 359 | ' FROM' + 360 | ' (SELECT' + 361 | ' uc.owner, uc.table_name, uc.constraint_name, uc.r_constraint_name, ' + 362 | 'col.column_name, col.position' + 363 | ' FROM' + 364 | (owner ? 365 | ' ALL_CONSTRAINTS uc, ALL_CONS_COLUMNS col' : 366 | ' USER_CONSTRAINTS uc, USER_CONS_COLUMNS col') + 367 | ' WHERE' + 368 | ' uc.constraint_type=\'R\' and uc.constraint_name=col.constraint_name'; 369 | if (owner) { 370 | sql += ' AND uc.owner=\'' + owner + '\''; 371 | } 372 | if (table) { 373 | sql += ' AND uc.table_name=\'' + table + '\''; 374 | } 375 | sql += ' ) a' + 376 | ' INNER JOIN' + 377 | ' USER_CONS_COLUMNS jcol' + 378 | ' ON' + 379 | ' a.r_constraint_name=jcol.constraint_name'; 380 | return sql; 381 | }; 382 | 383 | /** 384 | * Discover foreign keys for specified table. Example return value: 385 | * ```js 386 | * { fkOwner: 'STRONGLOOP', 387 | * fkName: 'PRODUCT_FK', 388 | * fkTableName: 'INVENTORY', 389 | * fkColumnName: 'PRODUCT_ID', 390 | * keySeq: 1, 391 | * pkOwner: 'STRONGLOOP', 392 | * pkName: 'PRODUCT_PK', 393 | * pkTableName: 'PRODUCT', 394 | * pkColumnName: 'ID' } 395 | * ``` 396 | * 397 | * @param {String} table The table name 398 | * @options {Object} options Options for discovery; see below. 399 | * @prop {Boolean} all If true, include tables/views from all schemas/owners. 400 | * @prop {String} owner/schema The schema/owner name. QUESTION: What is the actual name of this property: 'owner' or 'schema'? Or can it be either? 401 | * @prop {Boolean} views If true, include views. 402 | * @prop {Number} limit Maximimum number of results to return. NOTE: This was not in .md doc, but is included in tests. 403 | * @param {Function} [cb] The callback function 404 | * @private 405 | */ 406 | 407 | /** 408 | * Discover foreign keys synchronously for a given table. See example return value for discoverForeignKeys(). 409 | * 410 | * @param {String} table The table name 411 | * @options {Object} options Options for discovery; see below. 412 | * @prop {Boolean} all If true, include tables/views from all schemas/owners. 413 | * @prop {String} owner/schema The schema/owner name. QUESTION: What is the actual name of this property: 'owner' or 'schema'? Or can it be either? 414 | * @prop {Boolean} views If true, include views. 415 | * @prop {Number} limit Maximimum number of results to return. NOTE: This was not in .md doc, but is included in tests. 416 | * @private 417 | */ 418 | Oracle.prototype.discoverForeignKeysSync = function(table, options) { 419 | const args = this.getArgs(table, options); 420 | const owner = args.owner; 421 | table = args.table; 422 | options = args.options; 423 | 424 | const sql = this.buildQueryForeignKeys(owner, table); 425 | return this.querySync(sql); 426 | }; 427 | 428 | /*! 429 | * Retrieves a description of the foreign key columns that reference the given 430 | * table's primary key columns (the foreign keys exported by a table). 431 | * They are ordered by fkTableOwner, fkTableName, and keySeq. 432 | * @param {String} owner The DB owner/schema name 433 | * @param {String} table The table name 434 | * @returns {String} The SQL statement 435 | */ 436 | Oracle.prototype.buildQueryExportedForeignKeys = function(owner, table) { 437 | let sql = 'SELECT a.constraint_name AS "fkName", a.owner AS "fkOwner", ' + 438 | 'a.table_name AS "fkTableName",' + 439 | ' a.column_name AS "fkColumnName", a.position AS "keySeq",' + 440 | ' jcol.constraint_name AS "pkName", jcol.owner AS "pkOwner",' + 441 | ' jcol.table_name AS "pkTableName", jcol.column_name AS "pkColumnName"' + 442 | ' FROM' + 443 | ' (SELECT' + 444 | ' uc1.table_name, uc1.constraint_name, uc1.r_constraint_name, ' + 445 | 'col.column_name, col.position, col.owner' + 446 | ' FROM' + 447 | (owner ? 448 | ' ALL_CONSTRAINTS uc, ALL_CONSTRAINTS uc1, ALL_CONS_COLUMNS col' : 449 | ' USER_CONSTRAINTS uc, USER_CONSTRAINTS uc1, USER_CONS_COLUMNS col') + 450 | ' WHERE' + 451 | ' uc.constraint_type=\'P\' and' + 452 | ' uc1.r_constraint_name = uc.constraint_name and' + 453 | ' uc1.constraint_type = \'R\' and' + 454 | ' uc1.constraint_name=col.constraint_name'; 455 | if (owner) { 456 | sql += ' and col.owner=\'' + owner + '\''; 457 | } 458 | if (table) { 459 | sql += ' and uc.table_Name=\'' + table + '\''; 460 | } 461 | sql += ' ) a' + 462 | ' INNER JOIN' + 463 | ' USER_CONS_COLUMNS jcol' + 464 | ' ON' + 465 | ' a.r_constraint_name=jcol.constraint_name' + 466 | ' order by a.owner, a.table_name, a.position'; 467 | 468 | return sql; 469 | }; 470 | 471 | /** 472 | * Discover foreign keys that reference to the primary key of this table. 473 | * Example return value: 474 | * ```js 475 | * { fkName: 'PRODUCT_FK', 476 | * fkOwner: 'STRONGLOOP', 477 | * fkTableName: 'INVENTORY', 478 | * fkColumnName: 'PRODUCT_ID', 479 | * keySeq: 1, 480 | * pkName: 'PRODUCT_PK', 481 | * pkOwner: 'STRONGLOOP', 482 | * pkTableName: 'PRODUCT', 483 | * pkColumnName: 'ID' } 484 | * ```` 485 | * @param {String} table The table name 486 | * @options {Object} options The options for discovery 487 | * @prop {Boolean} all If true, include tables/views from all schemas/owners. 488 | * @prop {String} owner/schema The schema/owner name. QUESTION: What is the actual name of this property: 'owner' or 'schema'? Or can it be either? 489 | * @prop {Boolean} views If true, include views. 490 | * @param {Function} [cb] The callback function 491 | * @private 492 | */ 493 | 494 | /** 495 | * Discover foreign keys synchronously for a given table; see example return value for discoverExportedForeignKeys(). 496 | * @param {String} owner The DB owner/schema name 497 | * @options {Object} options The options for discovery; see below. 498 | * @prop {Boolean} all If true, include tables/views from all schemas/owners. 499 | * @prop {String} owner/schema The schema/owner name. QUESTION: What is the actual name of this property: 'owner' or 'schema'? Or can it be either? 500 | * @prop {Boolean} views If true, include views. 501 | * @returns {*} 502 | * @private 503 | */ 504 | Oracle.prototype.discoverExportedForeignKeysSync = function(table, options) { 505 | const args = this.getArgs(table, options); 506 | const owner = args.owner; 507 | table = args.table; 508 | options = args.options; 509 | 510 | const sql = this.buildQueryExportedForeignKeys(owner, table); 511 | return this.querySync(sql); 512 | }; 513 | 514 | /*! 515 | * Map oracle data types to json types 516 | * @param {String} oracleType 517 | * @param {Number} dataLength 518 | * @returns {String} 519 | */ 520 | Oracle.prototype.buildPropertyType = function(columnDefinition, options) { 521 | const oracleType = columnDefinition.dataType; 522 | const dataLength = columnDefinition.dataLength; 523 | const type = oracleType.toUpperCase(); 524 | switch (type) { 525 | case 'CHAR': 526 | if (dataLength === 1) { 527 | // Treat char(1) as boolean 528 | return 'Boolean'; 529 | } else { 530 | return 'String'; 531 | } 532 | break; 533 | case 'VARCHAR': 534 | case 'VARCHAR2': 535 | case 'LONG VARCHAR': 536 | case 'NCHAR': 537 | case 'NVARCHAR2': 538 | return 'String'; 539 | case 'LONG': 540 | case 'BLOB': 541 | case 'CLOB': 542 | case 'NCLOB': 543 | return 'Binary'; 544 | case 'NUMBER': 545 | case 'INTEGER': 546 | case 'DECIMAL': 547 | case 'DOUBLE': 548 | case 'FLOAT': 549 | case 'BIGINT': 550 | case 'SMALLINT': 551 | case 'REAL': 552 | case 'NUMERIC': 553 | case 'BINARY_FLOAT': 554 | case 'BINARY_DOUBLE': 555 | case 'UROWID': 556 | case 'ROWID': 557 | return 'Number'; 558 | case 'DATE': 559 | case 'TIMESTAMP': 560 | return 'Date'; 561 | default: 562 | return 'String'; 563 | } 564 | }; 565 | 566 | Oracle.prototype.setDefaultOptions = function(options) { 567 | }; 568 | 569 | Oracle.prototype.setNullableProperty = function(property) { 570 | }; 571 | 572 | Oracle.prototype.getDefaultSchema = function() { 573 | return ''; 574 | }; 575 | } 576 | -------------------------------------------------------------------------------- /lib/migration.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const async = require('async'); 9 | 10 | module.exports = mixinMigration; 11 | 12 | function mixinMigration(Oracle) { 13 | Oracle.prototype.showFields = function(model, cb) { 14 | const sql = 'SELECT column_name AS "column", data_type AS "type",' + 15 | ' data_length AS "length", nullable AS "nullable"' + // , data_default AS "Default"' 16 | ' FROM "SYS"."USER_TAB_COLUMNS" WHERE table_name=\'' + 17 | this.table(model) + '\''; 18 | this.execute(sql, function(err, fields) { 19 | if (err) 20 | return cb(err); 21 | else { 22 | fields.forEach(function(field) { 23 | field.type = mapOracleDatatypes(field.type); 24 | }); 25 | cb(err, fields); 26 | } 27 | }); 28 | }; 29 | 30 | /*! 31 | * Discover the properties from a table 32 | * @param {String} model The model name 33 | * @param {Function} cb The callback function 34 | */ 35 | Oracle.prototype.getTableStatus = function(model, cb) { 36 | let fields; 37 | const self = this; 38 | 39 | this.showFields(model, function(err, data) { 40 | if (err) return cb(err); 41 | fields = data; 42 | 43 | if (fields) 44 | return cb(null, fields); 45 | }); 46 | }; 47 | 48 | /** 49 | * Perform autoupdate for the given models 50 | * @param {String[]} [models] A model name or an array of model names. If not 51 | * present, apply to all models 52 | * @param {Function} [cb] The callback function 53 | */ 54 | Oracle.prototype.autoupdate = function(models, cb) { 55 | const self = this; 56 | if ((!cb) && ('function' === typeof models)) { 57 | cb = models; 58 | models = undefined; 59 | } 60 | // First argument is a model name 61 | if ('string' === typeof models) { 62 | models = [models]; 63 | } 64 | 65 | models = models || Object.keys(this._models); 66 | 67 | async.eachLimit(models, this.parallelLimit, function(model, done) { 68 | if (!(model in self._models)) { 69 | return process.nextTick(function() { 70 | done(new Error('Model not found: ' + model)); 71 | }); 72 | } 73 | self.getTableStatus(model, function(err, fields) { 74 | if (!err && fields.length) { 75 | self.alterTable(model, fields, done); 76 | } else { 77 | self.createTable(model, done); 78 | } 79 | }); 80 | }, cb); 81 | }; 82 | 83 | /** 84 | * Alter the table for the given model 85 | * @param {String} model The model name 86 | * @param {Object[]} actualFields Actual columns in the table 87 | * @param {Function} [cb] The callback function 88 | */ 89 | Oracle.prototype.alterTable = function(model, actualFields, cb) { 90 | const self = this; 91 | const pendingChanges = self.getAddModifyColumns(model, actualFields); 92 | if (pendingChanges.length > 0) { 93 | self.applySqlChanges(model, pendingChanges, function(err, results) { 94 | const dropColumns = self.getDropColumns(model, actualFields); 95 | if (dropColumns.length > 0) { 96 | self.applySqlChanges(model, dropColumns, cb); 97 | } else { 98 | if (cb) cb(err, results); 99 | } 100 | }); 101 | } else { 102 | const dropColumns = self.getDropColumns(model, actualFields); 103 | if (dropColumns.length > 0) { 104 | self.applySqlChanges(model, dropColumns, cb); 105 | } else { 106 | if (cb) process.nextTick(cb.bind(null, null, [])); 107 | } 108 | } 109 | }; 110 | 111 | Oracle.prototype.getAddModifyColumns = function(model, actualFields) { 112 | let sql = []; 113 | const self = this; 114 | sql = sql.concat(self.getColumnsToAdd(model, actualFields)); 115 | sql = sql.concat(self.getPropertiesToModify(model, actualFields)); 116 | return sql; 117 | }; 118 | 119 | Oracle.prototype.getColumnsToAdd = function(model, actualFields) { 120 | const self = this; 121 | const m = self._models[model]; 122 | const propNames = Object.keys(m.properties); 123 | let sql = []; 124 | propNames.forEach(function(propName) { 125 | if (self.id(model, propName)) { 126 | return; 127 | } 128 | const found = self.searchForPropertyInActual(model, 129 | self.column(model, propName), actualFields); 130 | if (!found && self.propertyHasNotBeenDeleted(model, propName)) { 131 | sql.push(self.addPropertyToActual(model, propName)); 132 | } 133 | }); 134 | if (sql.length > 0) { 135 | sql = ['ADD', '(' + sql.join(',') + ')']; 136 | } 137 | return sql; 138 | }; 139 | 140 | Oracle.prototype.getPropertiesToModify = function(model, actualFields) { 141 | const self = this; 142 | let sql = []; 143 | const m = self._models[model]; 144 | const propNames = Object.keys(m.properties); 145 | let found; 146 | propNames.forEach(function(propName) { 147 | if (self.id(model, propName)) { 148 | return; 149 | } 150 | found = self.searchForPropertyInActual(model, propName, actualFields); 151 | if (found && self.propertyHasNotBeenDeleted(model, propName)) { 152 | const column = self.columnEscaped(model, propName); 153 | let clause = ''; 154 | if (datatypeChanged(propName, found)) { 155 | clause = column + ' ' + 156 | self.modifyDatatypeInActual(model, propName); 157 | } 158 | if (nullabilityChanged(propName, found)) { 159 | if (!clause) { 160 | clause = column; 161 | } 162 | clause = clause + ' ' + 163 | self.modifyNullabilityInActual(model, propName); 164 | } 165 | if (clause) { 166 | sql.push(clause); 167 | } 168 | } 169 | }); 170 | 171 | if (sql.length > 0) { 172 | sql = ['MODIFY', '(' + sql.join(',') + ')']; 173 | } 174 | return sql; 175 | 176 | function datatypeChanged(propName, oldSettings) { 177 | const newSettings = m.properties[propName]; 178 | if (!newSettings) { 179 | return false; 180 | } 181 | 182 | let oldType; 183 | if (hasLength(self.columnDataType(model, propName))) { 184 | oldType = oldSettings.type.toUpperCase() + 185 | '(' + oldSettings.length + ')'; 186 | } else { 187 | oldType = oldSettings.type.toUpperCase(); 188 | } 189 | 190 | return oldType !== self.columnDataType(model, propName); 191 | 192 | function hasLength(type) { 193 | const hasLengthRegex = new RegExp(/^[A-Z0-9]*\([0-9]*\)$/); 194 | return hasLengthRegex.test(type); 195 | } 196 | } 197 | 198 | function nullabilityChanged(propName, oldSettings) { 199 | const newSettings = m.properties[propName]; 200 | if (!newSettings) { 201 | return false; 202 | } 203 | let changed = false; 204 | if (oldSettings.nullable === 'Y' && !self.isNullable(newSettings)) { 205 | changed = true; 206 | } 207 | if (oldSettings.nullable === 'N' && self.isNullable(newSettings)) { 208 | changed = true; 209 | } 210 | return changed; 211 | } 212 | }; 213 | 214 | Oracle.prototype.modifyDatatypeInActual = function(model, propName) { 215 | const self = this; 216 | const sqlCommand = self.columnDataType(model, propName); 217 | return sqlCommand; 218 | }; 219 | 220 | Oracle.prototype.modifyNullabilityInActual = function(model, propName) { 221 | const self = this; 222 | let sqlCommand = ''; 223 | if (self.isNullable(self.getPropertyDefinition(model, propName))) { 224 | sqlCommand = sqlCommand + 'NULL'; 225 | } else { 226 | sqlCommand = sqlCommand + 'NOT NULL'; 227 | } 228 | return sqlCommand; 229 | }; 230 | 231 | Oracle.prototype.getColumnsToDrop = function(model, actualFields) { 232 | const self = this; 233 | let sql = []; 234 | actualFields.forEach(function(actualField) { 235 | if (self.idColumn(model) === actualField.column) { 236 | return; 237 | } 238 | if (actualFieldNotPresentInModel(actualField, model)) { 239 | sql.push(self.escapeName(actualField.column)); 240 | } 241 | }); 242 | if (sql.length > 0) { 243 | sql = ['DROP', '(' + sql.join(',') + ')']; 244 | } 245 | return sql; 246 | 247 | function actualFieldNotPresentInModel(actualField, model) { 248 | return !(self.propertyName(model, actualField.column)); 249 | } 250 | }; 251 | 252 | /*! 253 | * Build a list of columns for the given model 254 | * @param {String} model The model name 255 | * @returns {String} 256 | */ 257 | Oracle.prototype.buildColumnDefinitions = function(model) { 258 | const self = this; 259 | const sql = []; 260 | const pks = this.idNames(model).map(function(i) { 261 | return self.columnEscaped(model, i); 262 | }); 263 | Object.keys(this.getModelDefinition(model).properties). 264 | forEach(function(prop) { 265 | const colName = self.columnEscaped(model, prop); 266 | sql.push(colName + ' ' + self.buildColumnDefinition(model, prop)); 267 | }); 268 | if (pks.length > 0) { 269 | sql.push('PRIMARY KEY(' + pks.join(',') + ')'); 270 | } 271 | return sql.join(',\n '); 272 | }; 273 | 274 | /*! 275 | * Build settings for the model property 276 | * @param {String} model The model name 277 | * @param {String} propName The property name 278 | * @returns {*|string} 279 | */ 280 | Oracle.prototype.buildColumnDefinition = function(model, propName) { 281 | const self = this; 282 | let result = self.columnDataType(model, propName); 283 | if (!self.isNullable(self.getPropertyDefinition(model, propName))) { 284 | result = result + ' NOT NULL'; 285 | } 286 | return result; 287 | }; 288 | 289 | Oracle.prototype._isIdGenerated = function(model) { 290 | const idNames = this.idNames(model); 291 | if (!idNames) { 292 | return false; 293 | } 294 | const idName = idNames[0]; 295 | const id = this.getModelDefinition(model).properties[idName]; 296 | const idGenerated = idNames.length > 1 ? false : id && id.generated; 297 | return idGenerated; 298 | }; 299 | 300 | /** 301 | * Drop a table for the given model 302 | * @param {String} model The model name 303 | * @param {Function} [cb] The callback function 304 | */ 305 | Oracle.prototype.dropTable = function(model, cb) { 306 | const self = this; 307 | const name = self.tableEscaped(model); 308 | const seqName = self.escapeName(model + '_ID_SEQUENCE'); 309 | 310 | let count = 0; 311 | const dropTableFun = function(callback) { 312 | self.execute('DROP TABLE ' + name, function(err, data) { 313 | if (err && err.toString().indexOf('ORA-00054') >= 0) { 314 | count++; 315 | if (count <= 5) { 316 | self.debug('Retrying ' + count + ': ' + err); 317 | // Resource busy, try again 318 | setTimeout(dropTableFun, 200 * Math.pow(count, 2)); 319 | return; 320 | } 321 | } 322 | if (err && err.toString().indexOf('ORA-00942') >= 0) { 323 | err = null; // Ignore it 324 | } 325 | callback(err, data); 326 | }); 327 | }; 328 | 329 | const tasks = [dropTableFun]; 330 | if (this._isIdGenerated(model)) { 331 | tasks.push( 332 | function(callback) { 333 | self.execute('DROP SEQUENCE ' + seqName, function(err) { 334 | if (err && err.toString().indexOf('ORA-02289') >= 0) { 335 | err = null; // Ignore it 336 | } 337 | callback(err); 338 | }); 339 | }, 340 | ); 341 | // Triggers will be dropped as part the drop table 342 | } 343 | async.series(tasks, cb); 344 | }; 345 | 346 | /** 347 | * Create a table for the given model 348 | * @param {String} model The model name 349 | * @param {Function} [cb] The callback function 350 | */ 351 | Oracle.prototype.createTable = function(model, cb) { 352 | const self = this; 353 | const name = self.tableEscaped(model); 354 | const seqName = self.escapeName(model + '_ID_SEQUENCE'); 355 | const triggerName = self.escapeName(model + '_ID_TRIGGER'); 356 | const idName = self.idColumnEscaped(model); 357 | 358 | const tasks = [ 359 | function(callback) { 360 | self.execute('CREATE TABLE ' + name + ' (\n ' + 361 | self.buildColumnDefinitions(model) + '\n)', callback); 362 | }]; 363 | 364 | if (this._isIdGenerated(model)) { 365 | tasks.push( 366 | function(callback) { 367 | self.execute('CREATE SEQUENCE ' + seqName + 368 | ' START WITH 1 INCREMENT BY 1 CACHE 100', callback); 369 | }, 370 | ); 371 | 372 | tasks.push( 373 | function(callback) { 374 | self.execute('CREATE OR REPLACE TRIGGER ' + triggerName + 375 | ' BEFORE INSERT ON ' + name + ' FOR EACH ROW\n' + 376 | 'WHEN (new.' + idName + ' IS NULL)\n' + 377 | 'BEGIN\n' + 378 | ' SELECT ' + seqName + '.NEXTVAL INTO :new.' + 379 | idName + ' FROM dual;\n' + 380 | 'END;', callback); 381 | }, 382 | ); 383 | } 384 | 385 | async.series(tasks, cb); 386 | }; 387 | 388 | /*! 389 | * Find the column type for a given model property 390 | * 391 | * @param {String} model The model name 392 | * @param {String} property The property name 393 | * @returns {String} The column type 394 | */ 395 | Oracle.prototype.columnDataType = function(model, property) { 396 | const columnMetadata = this.columnMetadata(model, property); 397 | let colType = columnMetadata && columnMetadata.dataType; 398 | if (colType) { 399 | colType = colType.toUpperCase(); 400 | } 401 | const prop = this.getModelDefinition(model).properties[property]; 402 | if (!prop) { 403 | return null; 404 | } 405 | const colLength = columnMetadata && columnMetadata.dataLength || 406 | prop.length; 407 | if (colType) { 408 | if (colType === 'CLOB' || colType === 'BLOB') { 409 | return colType; 410 | } 411 | return colType + (colLength ? '(' + colLength + ')' : ''); 412 | } 413 | 414 | switch (prop.type.name) { 415 | default: 416 | case 'String': 417 | case 'JSON': 418 | return 'VARCHAR2' + (colLength ? '(' + colLength + ')' : '(1024)'); 419 | case 'Text': 420 | return 'VARCHAR2' + (colLength ? '(' + colLength + ')' : '(1024)'); 421 | case 'Number': 422 | return 'NUMBER'; 423 | case 'Date': 424 | return 'DATE'; 425 | case 'Timestamp': 426 | return 'TIMESTAMP(3)'; 427 | case 'Boolean': 428 | return 'CHAR(1)'; // Oracle doesn't have built-in boolean 429 | } 430 | }; 431 | 432 | function mapOracleDatatypes(typeName) { 433 | // TODO there are a lot of synonymous type names that should go here-- 434 | // this is just what i've run into so far 435 | switch (typeName) { 436 | case 'int4': 437 | return 'NUMBER'; 438 | case 'bool': 439 | return 'CHAR(1)'; 440 | default: 441 | return typeName; 442 | } 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /lib/oracle.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const g = require('strong-globalize')(); 9 | 10 | /*! 11 | * Oracle connector for LoopBack 12 | */ 13 | const oracle = require('oracledb'); 14 | const SqlConnector = require('loopback-connector').SqlConnector; 15 | const ParameterizedSQL = SqlConnector.ParameterizedSQL; 16 | const debug = require('debug')('loopback:connector:oracle'); 17 | const stream = require('stream'); 18 | const async = require('async'); 19 | 20 | /*! 21 | * @module loopback-connector-oracle 22 | * 23 | * Initialize the Oracle connector against the given data source 24 | * 25 | * @param {DataSource} dataSource The loopback-datasource-juggler dataSource 26 | * @param {Function} [callback] The callback function 27 | */ 28 | exports.initialize = function initializeDataSource(dataSource, callback) { 29 | if (!oracle) { 30 | return; 31 | } 32 | 33 | const s = dataSource.settings || {}; 34 | 35 | const configProperties = { 36 | autoCommit: true, 37 | connectionClass: 'loopback-connector-oracle', 38 | extendedMetaData: undefined, 39 | externalAuth: undefined, 40 | fetchAsString: undefined, 41 | lobPrefetchSize: undefined, 42 | maxRows: undefined, 43 | outFormat: oracle.OBJECT, 44 | poolIncrement: undefined, 45 | poolMax: undefined, 46 | poolMin: undefined, 47 | poolPingInterval: undefined, 48 | poolTimeout: undefined, 49 | prefetchRows: undefined, 50 | fetchArraySize: undefined, 51 | Promise: undefined, 52 | queueRequests: undefined, 53 | queueTimeout: undefined, 54 | stmtCacheSize: undefined, 55 | _enableStats: undefined, 56 | }; 57 | 58 | const oracleSettings = { 59 | connectString: s.connectString || s.url || s.tns, 60 | user: s.username || s.user, 61 | password: s.password, 62 | debug: s.debug || debug.enabled, 63 | poolMin: s.poolMin || s.minConn || 1, 64 | poolMax: s.poolMax || s.maxConn || 10, 65 | poolIncrement: s.poolIncrement || s.incrConn || 1, 66 | poolTimeout: s.poolTimeout || s.timeout || 60, 67 | autoCommit: s.autoCommit || s.isAutoCommit, 68 | outFormat: oracle.OBJECT, 69 | maxRows: s.maxRows || 0, 70 | stmtCacheSize: s.stmtCacheSize || 30, 71 | _enableStats: s._enableStats || false, 72 | connectionClass: 'loopback-connector-oracle', 73 | }; 74 | 75 | if (oracleSettings.autoCommit === undefined) { 76 | oracleSettings.autoCommit = true; // Default to true 77 | } 78 | 79 | if (!oracleSettings.connectString) { 80 | const hostname = s.host || s.hostname || 'localhost'; 81 | const port = s.port || 1521; 82 | const database = s.database || 'XE'; 83 | oracleSettings.connectString = '//' + hostname + ':' + port + 84 | '/' + database; 85 | } 86 | 87 | for (const p in s) { 88 | if (!(p in oracleSettings) && p in configProperties) { 89 | oracleSettings[p] = s[p]; 90 | } 91 | } 92 | 93 | dataSource.connector = new Oracle(oracle, oracleSettings); 94 | dataSource.connector.dataSource = dataSource; 95 | 96 | if (callback) { 97 | if (s.lazyConnect) { 98 | process.nextTick(function() { 99 | callback(); 100 | }); 101 | } else { 102 | dataSource.connector.connect(callback); 103 | } 104 | } 105 | }; 106 | 107 | exports.Oracle = Oracle; 108 | 109 | /** 110 | * Oracle connector constructor 111 | * 112 | * 113 | * @param {object} driver Oracle node.js binding 114 | * @options {Object} settings Options specifying data source settings; see below. 115 | * @prop {String} hostname The host name or ip address of the Oracle DB server 116 | * @prop {Number} port The port number of the Oracle DB Server 117 | * @prop {String} user The user name 118 | * @prop {String} password The password 119 | * @prop {String} database The database name (TNS listener name) 120 | * @prop {Boolean|Number} debug If true, print debug messages. If Number, ? 121 | * @class 122 | */ 123 | function Oracle(oracle, settings) { 124 | this.constructor.super_.call(this, 'oracle', settings); 125 | this.driver = oracle; 126 | this.pool = null; 127 | this.parallelLimit = settings.maxConn || settings.poolMax || 16; 128 | if (settings.debug || debug.enabled) { 129 | debug('Settings: %j', settings); 130 | } 131 | oracle.fetchAsString = settings.fetchAsString || [oracle.CLOB]; 132 | oracle.fetchAsBuffer = settings.fetchAsBuffer || [oracle.BLOB]; 133 | this.executeOptions = { 134 | autoCommit: settings.autoCommit, 135 | fetchAsString: settings.fetchAsString || [oracle.CLOB], 136 | fetchAsBuffer: settings.fetchAsBuffer || [oracle.BLOB], 137 | lobPrefetchSize: settings.lobPrefetchSize, 138 | maxRows: settings.maxRows | 0, 139 | outFormat: oracle.OBJECT, 140 | fetchArraySize: settings.fetchArraySize || settings.prefetchRows, 141 | }; 142 | } 143 | 144 | // Inherit from loopback-datasource-juggler BaseSQL 145 | require('util').inherits(Oracle, SqlConnector); 146 | 147 | Oracle.prototype.debug = function() { 148 | if (this.settings.debug || debug.enabled) { 149 | debug.apply(null, arguments); 150 | } 151 | }; 152 | 153 | /** 154 | * Connect to Oracle 155 | * @param {Function} [callback] The callback after the connection is established 156 | */ 157 | Oracle.prototype.connect = function(callback) { 158 | const self = this; 159 | if (this.pool) { 160 | if (callback) { 161 | process.nextTick(function() { 162 | if (callback) callback(null, self.pool); 163 | }); 164 | } 165 | return; 166 | } 167 | if (this.settings.debug) { 168 | this.debug('Connecting to ' + 169 | (this.settings.hostname || this.settings.connectString)); 170 | } 171 | this.driver.createPool(this.settings, function(err, pool) { 172 | if (!err) { 173 | self.pool = pool; 174 | if (self.settings.debug) { 175 | self.debug('Connected to ' + 176 | (self.settings.hostname || self.settings.connectString)); 177 | self.debug('Connection pool ', pool); 178 | } 179 | } 180 | if (callback) callback(err, pool); 181 | }); 182 | }; 183 | 184 | /** 185 | * Execute the SQL statement. 186 | * 187 | * @param {String} sql The SQL statement. 188 | * @param {String[]} params The parameter values for the SQL statement. 189 | * @param {Function} [callback] The callback after the SQL statement is executed. 190 | */ 191 | Oracle.prototype.executeSQL = function(sql, params, options, callback) { 192 | const self = this; 193 | 194 | if (self.settings.debug) { 195 | if (params && params.length > 0) { 196 | self.debug('SQL: %s \nParameters: %j', sql, params); 197 | } else { 198 | self.debug('SQL: %s', sql); 199 | } 200 | } 201 | 202 | const executeOptions = {}; 203 | for (const i in this.executeOptions) { 204 | executeOptions[i] = this.executeOptions[i]; 205 | } 206 | const transaction = options.transaction; 207 | if (transaction && transaction.connection && 208 | transaction.connector === this) { 209 | debug('Execute SQL within a transaction'); 210 | executeOptions.autoCommit = false; 211 | transaction.connection.execute(sql, params, executeOptions, 212 | function(err, data) { 213 | if (err && self.settings.debug) { 214 | self.debug(err); 215 | } 216 | if (self.settings.debug && data) { 217 | self.debug('Result: %j', data); 218 | } 219 | if (data && data.rows) { 220 | data = data.rows; 221 | } 222 | callback(err, data); 223 | }); 224 | return; 225 | } 226 | 227 | self.pool.getConnection(function(err, connection) { 228 | if (err) { 229 | if (callback) callback(err); 230 | return; 231 | } 232 | if (self.settings.debug) { 233 | self.debug('Connection acquired: ', self.pool); 234 | } 235 | connection.clientId = self.settings.clientId || 'LoopBack'; 236 | connection.module = self.settings.module || 'loopback-connector-oracle'; 237 | connection.action = self.settings.action || ''; 238 | 239 | executeOptions.autoCommit = true; 240 | connection.execute(sql, params, executeOptions, 241 | function(err, data) { 242 | if (err && self.settings.debug) { 243 | self.debug(err); 244 | } 245 | 246 | if (!err && data) { 247 | if (data.rows) { 248 | data = data.rows; 249 | if (self.settings.debug && data) { 250 | self.debug('Result: %j', data); 251 | } 252 | } 253 | } 254 | releaseConnectionAndCallback(); 255 | 256 | function releaseConnectionAndCallback() { 257 | connection.release(function(err2) { 258 | if (err2) { 259 | self.debug(err2); 260 | } 261 | if (self.settings.debug) { 262 | self.debug('Connection released: ', self.pool); 263 | } 264 | callback(err || err2, data); 265 | }); 266 | } 267 | }); 268 | }); 269 | }; 270 | 271 | /** 272 | * Get the place holder in SQL for values, such as :1 or ? 273 | * @param {String} key Optional key, such as 1 or id 274 | * @returns {String} The place holder 275 | */ 276 | Oracle.prototype.getPlaceholderForValue = function(key) { 277 | return ':' + key; 278 | }; 279 | 280 | Oracle.prototype.getCountForAffectedRows = function(model, info) { 281 | return info && info.rowsAffected; 282 | }; 283 | 284 | Oracle.prototype.getInsertedId = function(model, info) { 285 | return info && info.outBinds && info.outBinds[0][0]; 286 | }; 287 | 288 | Oracle.prototype.buildInsertDefaultValues = function(model, data, options) { 289 | // Oracle doesn't like empty column/value list 290 | const idCol = this.idColumnEscaped(model); 291 | return '(' + idCol + ') VALUES(DEFAULT)'; 292 | }; 293 | 294 | Oracle.prototype.buildInsertReturning = function(model, data, options) { 295 | const modelDef = this.getModelDefinition(model); 296 | const type = modelDef.properties[this.idName(model)].type; 297 | let outParam = null; 298 | if (type === Number) { 299 | outParam = {type: oracle.NUMBER, dir: oracle.BIND_OUT}; 300 | } else if (type === Date) { 301 | outParam = {type: oracle.DATE, dir: oracle.BIND_OUT}; 302 | } else { 303 | outParam = {type: oracle.STRING, dir: oracle.BIND_OUT}; 304 | } 305 | const params = [outParam]; 306 | const returningStmt = new ParameterizedSQL('RETURNING ' + 307 | this.idColumnEscaped(model) + ' into ?', params); 308 | return returningStmt; 309 | }; 310 | 311 | /** 312 | * Create the data model in Oracle 313 | * 314 | * @param {String} model The model name 315 | * @param {Object} data The model instance data 316 | * @param {Function} [callback] The callback function 317 | */ 318 | Oracle.prototype.create = function(model, data, options, callback) { 319 | const self = this; 320 | const stmt = this.buildInsert(model, data, options); 321 | this.execute(stmt.sql, stmt.params, options, function(err, info) { 322 | if (err) { 323 | if (err.toString().indexOf('ORA-00001: unique constraint') >= 0) { 324 | // Transform the error so that duplicate can be checked using regex 325 | err = new Error(g.f('%s. Duplicate id detected.', err.toString())); 326 | } 327 | callback(err); 328 | } else { 329 | const insertedId = self.getInsertedId(model, info); 330 | callback(err, insertedId); 331 | } 332 | }); 333 | }; 334 | 335 | function dateToOracle(val, dateOnly) { 336 | function fz(v) { 337 | return v < 10 ? '0' + v : v; 338 | } 339 | 340 | function ms(v) { 341 | if (v < 10) { 342 | return '00' + v; 343 | } else if (v < 100) { 344 | return '0' + v; 345 | } else { 346 | return '' + v; 347 | } 348 | } 349 | 350 | let dateStr = [ 351 | val.getUTCFullYear(), 352 | fz(val.getUTCMonth() + 1), 353 | fz(val.getUTCDate()), 354 | ].join('-') + ' ' + [ 355 | fz(val.getUTCHours()), 356 | fz(val.getUTCMinutes()), 357 | fz(val.getUTCSeconds()), 358 | ].join(':'); 359 | 360 | if (!dateOnly) { 361 | dateStr += '.' + ms(val.getMilliseconds()); 362 | } 363 | 364 | if (dateOnly) { 365 | return new ParameterizedSQL( 366 | "to_date(?,'yyyy-mm-dd hh24:mi:ss')", [dateStr], 367 | ); 368 | } else { 369 | return new ParameterizedSQL( 370 | "to_timestamp(?,'yyyy-mm-dd hh24:mi:ss.ff3')", [dateStr], 371 | ); 372 | } 373 | } 374 | 375 | Oracle.prototype.toColumnValue = function(prop, val) { 376 | if (val == null) { 377 | // PostgreSQL complains with NULLs in not null columns 378 | // If we have an autoincrement value, return DEFAULT instead 379 | if (prop.autoIncrement || prop.id) { 380 | return new ParameterizedSQL('DEFAULT'); 381 | } else { 382 | return null; 383 | } 384 | } 385 | if (prop.type === String) { 386 | return String(val); 387 | } 388 | if (prop.type === Number) { 389 | if (isNaN(val)) { 390 | // Map NaN to NULL 391 | return val; 392 | } 393 | return val; 394 | } 395 | 396 | if (prop.type === Date || prop.type.name === 'Timestamp') { 397 | return dateToOracle(val, prop.type === Date); 398 | } 399 | 400 | // Oracle support char(1) Y/N 401 | if (prop.type === Boolean) { 402 | if (val) { 403 | return 'Y'; 404 | } else { 405 | return 'N'; 406 | } 407 | } 408 | 409 | return this.serializeObject(val); 410 | }; 411 | 412 | Oracle.prototype.fromColumnValue = function(prop, val) { 413 | if (val == null) { 414 | return val; 415 | } 416 | const type = prop && prop.type; 417 | if (type === Boolean) { 418 | if (typeof val === 'boolean') { 419 | return val; 420 | } else { 421 | return (val === 'Y' || val === 'y' || val === 'T' || 422 | val === 't' || val === '1'); 423 | } 424 | } 425 | return val; 426 | }; 427 | 428 | /*! 429 | * Convert to the Database name 430 | * @param {String} name The name 431 | * @returns {String} The converted name 432 | */ 433 | Oracle.prototype.dbName = function(name) { 434 | if (!name) { 435 | return name; 436 | } 437 | return name.toUpperCase(); 438 | }; 439 | 440 | /*! 441 | * Escape the name for Oracle DB 442 | * @param {String} name The name 443 | * @returns {String} The escaped name 444 | */ 445 | Oracle.prototype.escapeName = function(name) { 446 | if (!name) { 447 | return name; 448 | } 449 | return '"' + name.replace(/\./g, '"."') + '"'; 450 | }; 451 | 452 | Oracle.prototype.tableEscaped = function(model) { 453 | const schemaName = this.schema(model); 454 | if (schemaName && schemaName !== this.settings.user) { 455 | return this.escapeName(schemaName) + '.' + 456 | this.escapeName(this.table(model)); 457 | } else { 458 | return this.escapeName(this.table(model)); 459 | } 460 | }; 461 | 462 | Oracle.prototype.buildExpression = 463 | function(columnName, operator, columnValue, propertyDescriptor) { 464 | let val = columnValue; 465 | if (columnValue instanceof RegExp) { 466 | val = columnValue.source; 467 | operator = 'regexp'; 468 | } 469 | switch (operator) { 470 | case 'like': 471 | return new ParameterizedSQL({ 472 | sql: columnName + " LIKE ? ESCAPE '\\'", 473 | params: [val], 474 | }); 475 | case 'nlike': 476 | return new ParameterizedSQL({ 477 | sql: columnName + " NOT LIKE ? ESCAPE '\\'", 478 | params: [val], 479 | }); 480 | case 'regexp': 481 | /** 482 | * match_parameter is a text literal that lets you change the default 483 | * matching behavior of the function. You can specify one or more of 484 | * the following values for match_parameter: 485 | * - 'i' specifies case-insensitive matching. 486 | * - 'c' specifies case-sensitive matching. 487 | * - 'n' allows the period (.), which is the match-any-character 488 | * wildcard character, to match the newline character. If you omit this 489 | * parameter, the period does not match the newline character. 490 | * - 'm' treats the source string as multiple lines. Oracle interprets 491 | * ^ and $ as the start and end, respectively, of any line anywhere in 492 | * the source string, rather than only at the start or end of the entire 493 | * source string. If you omit this parameter, Oracle treats the source 494 | * string as a single line. 495 | * 496 | * If you specify multiple contradictory values, Oracle uses the last 497 | * value. For example, if you specify 'ic', then Oracle uses 498 | * case-sensitive matching. If you specify a character other than those 499 | * shown above, then Oracle returns an error. 500 | * 501 | * If you omit match_parameter, then: 502 | * - The default case sensitivity is determined by the value of the NLS_SORT parameter. 503 | * - A period (.) does not match the newline character. 504 | * - The source string is treated as a single line. 505 | */ 506 | let flag = ''; 507 | if (columnValue.ignoreCase) { 508 | flag += 'i'; 509 | } 510 | if (columnValue.multiline) { 511 | flag += 'm'; 512 | } 513 | if (columnValue.global) { 514 | g.warn('{{Oracle}} regex syntax does not respect the {{`g`}} flag'); 515 | } 516 | 517 | if (flag) { 518 | return new ParameterizedSQL({ 519 | sql: 'REGEXP_LIKE(' + columnName + ', ?, ?)', 520 | params: [val, flag], 521 | }); 522 | } else { 523 | return new ParameterizedSQL({ 524 | sql: 'REGEXP_LIKE(' + columnName + ', ?)', 525 | params: [val], 526 | }); 527 | } 528 | default: 529 | // Invoke the base implementation of `buildExpression` 530 | const exp = this.invokeSuper('buildExpression', 531 | columnName, operator, columnValue, propertyDescriptor); 532 | return exp; 533 | } 534 | }; 535 | 536 | function buildLimit(limit, offset) { 537 | if (isNaN(offset)) { 538 | offset = 0; 539 | } 540 | let sql = 'OFFSET ' + offset + ' ROWS'; 541 | if (limit >= 0) { 542 | sql += ' FETCH NEXT ' + limit + ' ROWS ONLY'; 543 | } 544 | return sql; 545 | } 546 | 547 | Oracle.prototype.applyPagination = 548 | function(model, stmt, filter) { 549 | const offset = filter.offset || filter.skip || 0; 550 | if (this.settings.supportsOffsetFetch) { 551 | // Oracle 12.c or later 552 | const limitClause = 553 | buildLimit(filter.limit, filter.offset || filter.skip); 554 | return stmt.merge(limitClause); 555 | } else { 556 | let paginatedSQL = 'SELECT * FROM (' + stmt.sql + ' ' + 557 | ')' + ' ' + ' WHERE R > ' + offset; 558 | 559 | if (filter.limit !== -1) { 560 | paginatedSQL += ' AND R <= ' + (offset + filter.limit); 561 | } 562 | 563 | stmt.sql = paginatedSQL + ' '; 564 | return stmt; 565 | } 566 | }; 567 | 568 | Oracle.prototype.buildColumnNames = function(model, filter) { 569 | let columnNames = this.invokeSuper('buildColumnNames', model, filter); 570 | if (filter.limit || filter.offset || filter.skip) { 571 | const orderBy = this.buildOrderBy(model, filter.order); 572 | columnNames += ',ROW_NUMBER() OVER' + ' (' + orderBy + ') R'; 573 | } 574 | return columnNames; 575 | }; 576 | 577 | Oracle.prototype.buildSelect = function(model, filter, options) { 578 | if (!filter.order) { 579 | const idNames = this.idNames(model); 580 | if (idNames && idNames.length) { 581 | filter.order = idNames; 582 | } 583 | } 584 | 585 | let selectStmt = new ParameterizedSQL('SELECT ' + 586 | this.buildColumnNames(model, filter) + 587 | ' FROM ' + this.tableEscaped(model)); 588 | 589 | if (filter) { 590 | if (filter.where) { 591 | const whereStmt = this.buildWhere(model, filter.where); 592 | selectStmt.merge(whereStmt); 593 | } 594 | 595 | if (filter.limit || filter.skip || filter.offset) { 596 | selectStmt = this.applyPagination( 597 | model, selectStmt, filter, 598 | ); 599 | } else { 600 | if (filter.order) { 601 | selectStmt.merge(this.buildOrderBy(model, filter.order)); 602 | } 603 | } 604 | } 605 | return this.parameterize(selectStmt); 606 | }; 607 | 608 | /** 609 | * Disconnect from Oracle 610 | * @param {Function} [cb] The callback function 611 | */ 612 | Oracle.prototype.disconnect = function disconnect(cb) { 613 | const err = null; 614 | if (this.pool) { 615 | if (this.settings.debug) { 616 | this.debug('Disconnecting from ' + 617 | (this.settings.hostname || this.settings.connectString)); 618 | } 619 | const pool = this.pool; 620 | this.pool = null; 621 | return pool.terminate(cb); 622 | } 623 | 624 | if (cb) { 625 | process.nextTick(function() { 626 | cb(err); 627 | }); 628 | } 629 | }; 630 | 631 | Oracle.prototype.ping = function(cb) { 632 | this.execute('select count(*) as result from user_tables', [], cb); 633 | }; 634 | 635 | require('./migration')(Oracle, oracle); 636 | require('./discovery')(Oracle, oracle); 637 | require('./transaction')(Oracle, oracle); 638 | -------------------------------------------------------------------------------- /lib/transaction.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const g = require('strong-globalize')(); 9 | const debug = require('debug')('loopback:connector:oracle:transaction'); 10 | const Transaction = require('loopback-connector').Transaction; 11 | 12 | module.exports = mixinTransaction; 13 | 14 | /*! 15 | * @param {Oracle} Oracle connector class 16 | */ 17 | function mixinTransaction(Oracle, oracle) { 18 | /** 19 | * Begin a new transaction 20 | * @param isolationLevel 21 | * @param cb 22 | */ 23 | Oracle.prototype.beginTransaction = function(isolationLevel, cb) { 24 | debug('Begin a transaction with isolation level: %s', isolationLevel); 25 | if (isolationLevel !== Transaction.READ_COMMITTED && 26 | isolationLevel !== Transaction.SERIALIZABLE) { 27 | const err = new Error(g.f('Invalid {{isolationLevel}}: %s', 28 | isolationLevel)); 29 | err.statusCode = 400; 30 | return process.nextTick(function() { 31 | cb(err); 32 | }); 33 | } 34 | this.pool.getConnection(function(err, connection) { 35 | if (err) return cb(err); 36 | if (isolationLevel) { 37 | const sql = 'SET TRANSACTION ISOLATION LEVEL ' + isolationLevel; 38 | connection.execute(sql, [], 39 | {outFormat: oracle.OBJECT, autoCommit: false}, function(err) { 40 | cb(err, connection); 41 | }); 42 | } else { 43 | cb(err, connection); 44 | } 45 | }); 46 | }; 47 | 48 | /** 49 | * 50 | * @param connection 51 | * @param cb 52 | */ 53 | Oracle.prototype.commit = function(connection, cb) { 54 | debug('Commit a transaction'); 55 | connection.commit(function(err) { 56 | if (err) return cb(err); 57 | connection.release(cb); 58 | }); 59 | }; 60 | 61 | /** 62 | * 63 | * @param connection 64 | * @param cb 65 | */ 66 | Oracle.prototype.rollback = function(connection, cb) { 67 | debug('Rollback a transaction'); 68 | connection.rollback(function(err) { 69 | if (err) return cb(err); 70 | connection.release(cb); 71 | }); 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-connector-oracle", 3 | "version": "4.5.2", 4 | "description": "Loopback Oracle Connector", 5 | "keywords": [ 6 | "StrongLoop", 7 | "LoopBack", 8 | "Oracle", 9 | "DataSource", 10 | "Connector" 11 | ], 12 | "engines": { 13 | "node": ">=8" 14 | }, 15 | "main": "index.js", 16 | "scripts": { 17 | "lint": "eslint .", 18 | "lint:fix": "eslint . --fix", 19 | "test": "mocha --UV_THREADPOOL_SIZE=100 test/*.test.js node_modules/juggler-v3/test.js node_modules/juggler-v4/test.js", 20 | "posttest": "npm run lint" 21 | }, 22 | "files": [ 23 | "intl", 24 | "lib", 25 | "docs.json", 26 | "index.js", 27 | "setup.sh" 28 | ], 29 | "dependencies": { 30 | "async": "^3.1.0", 31 | "debug": "^4.1.1", 32 | "loopback-connector": "^4.10.2", 33 | "oracledb": "^4.0.0", 34 | "strong-globalize": "^5.0.0" 35 | }, 36 | "devDependencies": { 37 | "bluebird": "^3.5.5", 38 | "eslint": "^6.0.1", 39 | "eslint-config-loopback": "^13.1.0", 40 | "juggler-v3": "file:./deps/juggler-v3", 41 | "juggler-v4": "file:./deps/juggler-v4", 42 | "loopback-datasource-juggler": "^3.0.0 || ^4.0.0", 43 | "mocha": "^6.1.4", 44 | "rc": "^1.2.8", 45 | "should": "^13.2.3" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/strongloop/loopback-connector-oracle.git" 50 | }, 51 | "copyright.owner": "IBM Corp.", 52 | "license": "MIT" 53 | } 54 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### Shell script to spin up a docker container for oracle. 4 | 5 | ## color codes 6 | RED='\033[1;31m' 7 | GREEN='\033[1;32m' 8 | YELLOW='\033[1;33m' 9 | CYAN='\033[1;36m' 10 | PLAIN='\033[0m' 11 | 12 | ## variables 13 | ORACLE_CONTAINER="oracle_c" 14 | HOST="localhost" 15 | PORT=1521 16 | DATABASE="XE" 17 | USER="admin" 18 | PASSWORD="0raclep4ss" 19 | if [ "$1" ]; then 20 | HOST=$1 21 | fi 22 | if [ "$2" ]; then 23 | PORT=$2 24 | fi 25 | if [ "$3" ]; then 26 | USER=$3 27 | fi 28 | if [ "$4" ]; then 29 | PASSWORD=$4 30 | fi 31 | 32 | ## check if docker exists 33 | printf "\n${RED}>> Checking for docker${PLAIN} ${GREEN}...${PLAIN}" 34 | docker -v > /dev/null 2>&1 35 | DOCKER_EXISTS=$? 36 | if [ "$DOCKER_EXISTS" -ne 0 ]; then 37 | printf "\n\n${CYAN}Status: ${PLAIN}${RED}Docker not found. Terminating setup.${PLAIN}\n\n" 38 | exit 1 39 | fi 40 | printf "\n${CYAN}Found docker. Moving on with the setup.${PLAIN}\n" 41 | 42 | ## cleaning up previous builds 43 | printf "\n${RED}>> Finding old builds and cleaning up${PLAIN} ${GREEN}...${PLAIN}" 44 | docker rm -f $ORACLE_CONTAINER > /dev/null 2>&1 45 | printf "\n${CYAN}Clean up complete.${PLAIN}\n" 46 | 47 | ## pull latest oracle image 48 | printf "\n${RED}>> Pulling latest oracle image${PLAIN} ${GREEN}...${PLAIN}" 49 | docker pull sath89/oracle-xe-11g:latest > /dev/null 2>&1 50 | printf "\n${CYAN}Image successfully built.${PLAIN}\n" 51 | 52 | ## run the oracle container 53 | printf "\n${RED}>> Starting the oracle container${PLAIN} ${GREEN}...${PLAIN}\n" 54 | docker run --name $ORACLE_CONTAINER -p $PORT:1521 -d sath89/oracle-xe-11g:latest > /dev/null 2>&1 55 | 56 | ##wait for orale database container to be ready 57 | OUTPUT=1 58 | TIMEOUT=300 59 | TIME_PASSED=0 60 | WAIT_STRING="." 61 | START_MESSAGE="Database ready to use." 62 | printf "${RED}Waiting for database to be ready${PLAIN} ${GREEN}...${PLAIN}" 63 | while [ "$OUTPUT" -ne 0 ] && [ "$TIMEOUT" -gt 0 ] 64 | do 65 | docker logs ${ORACLE_CONTAINER} 2>&1 | grep "${START_MESSAGE}" > /dev/null 66 | OUTPUT=$? 67 | sleep 1s 68 | let "TIME_PASSED = $TIME_PASSED + 1" 69 | 70 | if [ "${TIME_PASSED}" -eq 5 ]; then 71 | printf "${GREEN}${WAIT_STRING}${PLAIN}" 72 | TIME_PASSED=0 73 | fi 74 | done 75 | 76 | if [ "$TIMEOUT" -lt 0 ]; then 77 | printf "\n${RED}Failed to start container successfully. Terminating setup ...${PLAIN}\n" 78 | exit 1 79 | else 80 | printf "\n${CYAN}Container is up and running.${PLAIN}\n" 81 | fi 82 | 83 | 84 | ## export the schema to the oracle database 85 | printf "\n${RED}>> Exporting schema to database${PLAIN} ${GREEN}...${PLAIN}\n" 86 | ## copy over our db seed file 87 | docker cp ./test/tables.sql $ORACLE_CONTAINER:/home/ > /dev/null 2>&1 88 | 89 | ##make user, give it privileges, and copy sql file to container 90 | CREATEUSER="CREATE USER ${USER} IDENTIFIED by \"${PASSWORD}\";\n \ 91 | GRANT CONNECT, RESOURCE, DBA TO ${USER};\n \ 92 | GRANT CREATE SESSION TO ${USER};\n \ 93 | GRANT UNLIMITED TABLESPACE TO ${USER};\r" 94 | 95 | touch dockerusercreate.sql && echo ${CREATEUSER} > dockerusercreate.sql 96 | docker cp dockerusercreate.sql $ORACLE_CONTAINER:/home/ > /dev/null 2>&1 97 | rm dockerusercreate.sql 98 | ## run create user script 99 | docker exec -it $ORACLE_CONTAINER /bin/sh -c "echo exit | sqlplus sys/oracle@//${HOST}:${PORT}/${DATABASE} as sysdba @/home/dockerusercreate.sql" > /dev/null 2>&1 100 | 101 | 102 | ## variables needed to health check export schema 103 | OUTPUT=$? 104 | TIMEOUT=120 105 | TIME_PASSED=0 106 | WAIT_STRING="." 107 | 108 | printf "\n${GREEN}Waiting for database to respond with updated schema $WAIT_STRING${PLAIN}" 109 | while [ "$OUTPUT" -ne 0 ] && [ "$TIMEOUT" -gt 0 ] 110 | do 111 | docker exec -it $ORACLE_CONTAINER /bin/sh -c "echo exit | sqlplus ${USER}/{$PASSWORD}@//${HOST}:${PORT}/${DATABASE} @/home/tables.sql" > /dev/null 2>&1 112 | OUTPUT=$? 113 | sleep 1s 114 | TIMEOUT=$((TIMEOUT - 1)) 115 | TIME_PASSED=$((TIME_PASSED + 1)) 116 | if [ "$TIME_PASSED" -eq 5 ]; then 117 | printf "${GREEN}.${PLAIN}" 118 | TIME_PASSED=0 119 | fi 120 | done 121 | 122 | if [ "$TIMEOUT" -le 0 ]; then 123 | printf "\n\n${CYAN}Status: ${PLAIN}${RED}Failed to export schema. Terminating setup.${PLAIN}\n\n" 124 | exit 1 125 | fi 126 | printf "\n${CYAN}Successfully exported schema to database.${PLAIN}\n" 127 | 128 | 129 | ## set env variables for running test 130 | printf "\n${RED}>> Setting env variables to run test${PLAIN} ${GREEN}...${PLAIN}" 131 | export ORACLE_HOST=$HOST 132 | export ORACLE_PORT=$PORT 133 | export ORACLE_USER=$USER 134 | export ORACLE_PASSWORD=$PASSWORD 135 | export ORACLE_DATABASE=$DATABASE 136 | printf "\n${CYAN}Env variables set.${PLAIN}\n" 137 | 138 | printf "\n${CYAN}Status: ${PLAIN}${GREEN}Set up completed successfully.${PLAIN}\n" 139 | printf "\n${CYAN}Instance url: ${YELLOW}oracle://$USER:$PASSWORD@$HOST/$DATABASE${PLAIN}\n" 140 | printf "\n${CYAN}To run the test suite:${PLAIN} ${YELLOW}npm test${PLAIN}\n\n" 141 | -------------------------------------------------------------------------------- /test/init/init.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const juggler = require('loopback-datasource-juggler'); 9 | let DataSource = juggler.DataSource; 10 | 11 | const config = require('rc')('loopback', {test: {oracle: {}}}).test.oracle; 12 | config.maxConn = 64; 13 | 14 | let db; 15 | 16 | global.getDataSource = global.getSchema = function() { 17 | if (db) { 18 | return db; 19 | } 20 | db = new DataSource(require('../../'), config); 21 | db.log = function(a) { 22 | // console.log(a); 23 | }; 24 | return db; 25 | }; 26 | 27 | global.resetDataSourceClass = function(ctor) { 28 | DataSource = ctor || juggler.DataSource; 29 | const promise = db ? db.disconnect() : Promise.resolve(); 30 | db = undefined; 31 | return promise; 32 | }; 33 | 34 | global.connectorCapabilities = { 35 | ilike: false, 36 | nilike: false, 37 | }; 38 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 120000 2 | -------------------------------------------------------------------------------- /test/oracle.autoupdate.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /* global getDataSource */ 9 | const assert = require('assert'); 10 | const should = require('should'); 11 | require('./init/init'); 12 | let ds; 13 | 14 | before(function() { 15 | ds = getDataSource(); 16 | }); 17 | 18 | describe('Oracle connector', function() { 19 | const schema_v1 = // eslint-disable-line camelcase 20 | { 21 | name: 'CustomerTest', 22 | options: { 23 | idInjection: false, 24 | oracle: { 25 | schema: 'TEST', 26 | table: 'CUSTOMER_TEST', 27 | }, 28 | }, 29 | properties: { 30 | id: { 31 | type: 'String', 32 | length: 20, 33 | id: 1, 34 | }, 35 | name: { 36 | type: 'String', 37 | required: false, 38 | length: 40, 39 | }, 40 | email: { 41 | type: 'String', 42 | required: true, 43 | length: 40, 44 | }, 45 | age: { 46 | type: 'Number', 47 | required: false, 48 | }, 49 | }, 50 | }; 51 | 52 | const schema_v2 = // eslint-disable-line camelcase 53 | { 54 | name: 'CustomerTest', 55 | options: { 56 | idInjection: false, 57 | oracle: { 58 | schema: 'TEST', 59 | table: 'CUSTOMER_TEST', 60 | }, 61 | }, 62 | properties: { 63 | id: { 64 | type: 'String', 65 | length: 20, 66 | id: 1, 67 | }, 68 | email: { 69 | type: 'String', 70 | required: false, 71 | length: 60, 72 | oracle: { 73 | columnName: 'EMAIL', 74 | dataType: 'VARCHAR', 75 | dataLength: 60, 76 | nullable: 'Y', 77 | }, 78 | }, 79 | firstName: { 80 | type: 'String', 81 | required: false, 82 | length: 40, 83 | }, 84 | lastName: { 85 | type: 'String', 86 | required: false, 87 | length: 40, 88 | }, 89 | }, 90 | }; 91 | 92 | it('should auto migrate/update tables', function(done) { 93 | ds.createModel(schema_v1.name, schema_v1.properties, schema_v1.options); // eslint-disable-line camelcase 94 | 95 | ds.automigrate(function(err) { 96 | if (err) return done(err); 97 | 98 | ds.discoverModelProperties('CUSTOMER_TEST', function(err, props) { 99 | if (err) return done(err); 100 | assert.equal(props.length, 4); 101 | const columns = {}; 102 | props.forEach(function(p) { 103 | columns[p.columnName] = p.nullable; 104 | }); 105 | columns.should.be.eql({ 106 | AGE: 'Y', 107 | EMAIL: 'N', 108 | NAME: 'Y', 109 | ID: 'N', 110 | }); 111 | 112 | const columnsLength = {}; 113 | props.forEach(function(p) { 114 | columnsLength[p.columnName] = p.dataLength; 115 | }); 116 | columnsLength.should.be.eql({ 117 | AGE: 22, 118 | EMAIL: 40, 119 | NAME: 40, 120 | ID: 20, 121 | }); 122 | 123 | ds.createModel(schema_v2.name, schema_v2.properties, schema_v2.options); // eslint-disable-line camelcase 124 | 125 | ds.autoupdate(function(err, result) { 126 | ds.discoverModelProperties('CUSTOMER_TEST', function(err, props) { 127 | assert.equal(props.length, 4); 128 | const columns = {}; 129 | props.forEach(function(p) { 130 | columns[p.columnName] = p.nullable; 131 | }); 132 | columns.should.be.eql({ 133 | LASTNAME: 'Y', 134 | FIRSTNAME: 'Y', 135 | EMAIL: 'Y', 136 | ID: 'N', 137 | }); 138 | 139 | const columnsLength = {}; 140 | props.forEach(function(p) { 141 | columnsLength[p.columnName] = p.dataLength; 142 | }); 143 | columnsLength.should.be.eql({ 144 | LASTNAME: 40, 145 | FIRSTNAME: 40, 146 | EMAIL: 60, 147 | ID: 20, 148 | }); 149 | 150 | done(err, result); 151 | }); 152 | }); 153 | }); 154 | }); 155 | }); 156 | 157 | it('should report errors for automigrate', function() { 158 | ds.automigrate('XYZ', function(err) { 159 | assert(err); 160 | }); 161 | }); 162 | 163 | it('should report errors for autoupdate', function() { 164 | ds.autoupdate('XYZ', function(err) { 165 | assert(err); 166 | }); 167 | }); 168 | 169 | it('should check if a model is actual properly', function(done) { 170 | ds.createModel(schema_v1.name, schema_v1.properties, schema_v1.options); // eslint-disable-line camelcase 171 | 172 | ds.automigrate(function(err) { 173 | if (err) return done(err); 174 | 175 | ds.isActual('CustomerTest', function(err, isActual) { 176 | if (err) return done(err); 177 | 178 | assert.equal(isActual, true); 179 | 180 | ds.createModel(schema_v2.name, schema_v2.properties, schema_v2.options); // eslint-disable-line camelcase 181 | 182 | ds.isActual('CustomerTest', function(err, isActual) { 183 | if (err) return done(err); 184 | 185 | assert.equal(isActual, false); 186 | done(); 187 | }); 188 | }); 189 | }); 190 | }); 191 | }); 192 | -------------------------------------------------------------------------------- /test/oracle.clob.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /* global getDataSource */ 9 | const assert = require('assert'); 10 | let ds, Note; 11 | require('./init/init'); 12 | 13 | before(function(done) { 14 | ds = getDataSource(); 15 | const schema = 16 | { 17 | name: 'ClobTest', 18 | options: { 19 | oracle: { 20 | schema: 'TEST', 21 | table: 'CLOB_TEST', 22 | }, 23 | }, 24 | properties: { 25 | note: { 26 | type: 'String', 27 | oracle: { 28 | dataType: 'CLOB', 29 | }, 30 | }, 31 | }, 32 | }; 33 | 34 | Note = ds.createModel(schema.name, schema.properties, schema.options); 35 | ds.automigrate(done); 36 | }); 37 | 38 | function generateString(size, char) { 39 | let str = ''; 40 | for (let i = 0; i < size; i++) { 41 | str += (char || 'A'); 42 | } 43 | return str; 44 | } 45 | 46 | describe('Oracle connector', function() { 47 | it('should support clob size < 4000 chars', function(done) { 48 | const clob = generateString(1000, 'A'); 49 | Note.create({note: clob}, function(err, note) { 50 | assert(!err); 51 | Note.findById(note.id, function(err, note) { 52 | assert(!err); 53 | assert.equal(note.note, clob); 54 | done(err); 55 | }); 56 | }); 57 | }); 58 | 59 | it('should support clob size < 32k chars', function(done) { 60 | const clob = generateString(32000, 'B'); 61 | Note.create({note: clob}, function(err, note) { 62 | assert(!err); 63 | Note.findById(note.id, function(err, note) { 64 | assert(!err); 65 | assert.equal(note.note, clob); 66 | done(err); 67 | }); 68 | }); 69 | }); 70 | 71 | it('should support clob size > 32k chars', function(done) { 72 | const clob = generateString(50000, 'C'); 73 | Note.create({note: clob}, function(err, note) { 74 | assert(!err); 75 | Note.findById(note.id, function(err, note) { 76 | assert(!err); 77 | assert.equal(note.note, clob); 78 | done(err); 79 | }); 80 | }); 81 | }); 82 | }); 83 | 84 | -------------------------------------------------------------------------------- /test/oracle.connectionpool.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /* global getDataSource */ 9 | process.env.NODE_ENV = 'test'; 10 | require('should'); 11 | 12 | const async = require('async'); 13 | 14 | const DataSource = require('loopback-datasource-juggler').DataSource; 15 | let db, config; 16 | 17 | before(function() { 18 | config = require('rc')('loopback', {dev: {oracle: {}}}).dev.oracle; 19 | }); 20 | 21 | after(function() { 22 | db = null; 23 | }); 24 | 25 | describe('Oracle connector', function() { 26 | it('should create connection pool', function(done) { 27 | db = new DataSource(require('../'), config); 28 | db.connect(function() { 29 | const info = db.connector.pool; 30 | info.should.have.property('connectionsOpen', 1); 31 | info.should.have.property('connectionsInUse', 0); 32 | info.should.have.property('poolMax', 10); 33 | info.should.have.property('poolMin', 1); 34 | info.should.have.property('poolIncrement', 1); 35 | info.should.have.property('poolTimeout', 60); 36 | db.disconnect(done); 37 | }); 38 | }); 39 | 40 | it('should create connection pool with config', function(done) { 41 | config.minConn = 2; 42 | config.maxConn = 4; 43 | config.incrConn = 2; 44 | config.timeout = 5; 45 | db = new DataSource(require('../'), config); 46 | db.connect(function() { 47 | const info = db.connector.pool; 48 | info.should.have.property('connectionsOpen', 2); 49 | info.should.have.property('connectionsInUse', 0); 50 | info.should.have.property('poolMax', 4); 51 | info.should.have.property('poolMin', 2); 52 | info.should.have.property('poolIncrement', 2); 53 | info.should.have.property('poolTimeout', 5); 54 | 55 | const tasks = []; 56 | for (let i = 0; i < 3; i++) { 57 | tasks.push(db.connector.pool.getConnection.bind(db.connector.pool)); 58 | } 59 | async.parallel(tasks, function(err, connections) { 60 | connections.should.have.property('length', 3); 61 | async.each(connections, function(c, done) { 62 | c.release(done); 63 | }, function(err) { 64 | // var info = db.connector.pool; 65 | // console.log(info); 66 | db.disconnect(done); 67 | }); 68 | }); 69 | }); 70 | }); 71 | 72 | it('should close connection pool gracefully', function(done) { 73 | db = new DataSource(require('../'), config); 74 | db.connect(function() { 75 | // Call ping to acquire/release a connection from the pool 76 | db.ping(function(err) { 77 | if (err) return done(err); 78 | // It should disconnect gracefully 79 | db.disconnect(done); 80 | }); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/oracle.discover.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /* global getDataSource */ 9 | process.env.NODE_ENV = 'test'; 10 | const should = require('should'); 11 | 12 | const assert = require('assert'); 13 | 14 | const DataSource = require('loopback-datasource-juggler').DataSource; 15 | let db; 16 | 17 | describe('discoverModels', function() { 18 | before(function() { 19 | const config = require('rc')('loopback', {dev: {oracle: {}}}).dev.oracle; 20 | db = new DataSource(require('../'), config); 21 | }); 22 | 23 | after(function() { 24 | db.disconnect(); 25 | }); 26 | 27 | describe('Discover database schemas', function() { 28 | it('should return an array of db schemas', function(done) { 29 | db.connector.discoverDatabaseSchemas(function(err, schemas) { 30 | if (err) return done(err); 31 | schemas.should.be.instanceof(Array); 32 | schemas.length.should.be.above(0); 33 | done(); 34 | }); 35 | }); 36 | }); 37 | 38 | describe('Discover models including views', function() { 39 | it('should return an array of tables and views', function(done) { 40 | db.discoverModelDefinitions({ 41 | views: true, 42 | limit: 3, 43 | }, function(err, models) { 44 | if (err) { 45 | console.error(err); 46 | done(err); 47 | } else { 48 | let views = false; 49 | models.forEach(function(m) { 50 | // console.dir(m); 51 | if (m.type === 'view') { 52 | views = true; 53 | } 54 | }); 55 | assert(views, 'Should have views'); 56 | done(null, models); 57 | } 58 | }); 59 | }); 60 | }); 61 | 62 | describe('Discover models excluding views', function() { 63 | it('should return an array of only tables', function(done) { 64 | db.discoverModelDefinitions({ 65 | views: false, 66 | limit: 3, 67 | }, function(err, models) { 68 | if (err) { 69 | console.error(err); 70 | done(err); 71 | } else { 72 | let views = false; 73 | models.forEach(function(m) { 74 | // console.dir(m); 75 | if (m.type === 'view') { 76 | views = true; 77 | } 78 | }); 79 | models.should.have.length(3); 80 | assert(!views, 'Should not have views'); 81 | done(null, models); 82 | } 83 | }); 84 | }); 85 | }); 86 | 87 | describe('Discover models including other users', function() { 88 | it('should return an array of all tables and views', function(done) { 89 | db.discoverModelDefinitions({ 90 | all: true, 91 | limit: 3, 92 | }, function(err, models) { 93 | if (err) { 94 | console.error(err); 95 | done(err); 96 | } else { 97 | let others = false; 98 | models.forEach(function(m) { 99 | // console.dir(m); 100 | if (m.owner !== 'STRONGLOOP') { 101 | others = true; 102 | } 103 | }); 104 | assert(others, 'Should have tables/views owned by others'); 105 | done(err, models); 106 | } 107 | }); 108 | }); 109 | }); 110 | 111 | describe('Discover model properties', function() { 112 | describe('Discover a named model', function() { 113 | it('should return an array of columns for PRODUCT', function(done) { 114 | db.discoverModelProperties('PRODUCT', function(err, models) { 115 | if (err) { 116 | console.error(err); 117 | done(err); 118 | } else { 119 | models.forEach(function(m) { 120 | // console.dir(m); 121 | assert(m.tableName === 'PRODUCT'); 122 | }); 123 | done(null, models); 124 | } 125 | }); 126 | }); 127 | 128 | it('should return an array of columns for PRODUCT ', function(done) { 129 | db.discoverModelProperties('PRODUCT', {schema: 'STRONGLOOP'}, 130 | function(err, models) { 131 | if (err) { 132 | console.error(err); 133 | done(err); 134 | } else { 135 | models.forEach(function(m) { 136 | // console.dir(m); 137 | assert(m.tableName === 'PRODUCT'); 138 | }); 139 | done(null, models); 140 | } 141 | }); 142 | }); 143 | }); 144 | }); 145 | 146 | describe('Discover model primary keys', function() { 147 | let Case; 148 | before(function(done) { 149 | const caseSchema = { 150 | name: 'CHECKCASE', 151 | options: { 152 | idInjection: false, 153 | oracle: { 154 | schema: 'STRONGLOOP', 155 | table: 'CHECKCASE', 156 | }, 157 | }, 158 | properties: { 159 | id: { 160 | type: 'Number', 161 | required: true, 162 | id: 1, 163 | oracle: { 164 | columnName: 'camelCase', 165 | }, 166 | }, 167 | USERNAME: { 168 | type: 'String', 169 | required: true, 170 | }, 171 | }, 172 | }; 173 | Case = db.createModel( 174 | caseSchema.name, caseSchema.properties, caseSchema.options, 175 | ); 176 | db.automigrate(done); 177 | Case.destroyAll(); 178 | }); 179 | 180 | it('should return an array of primary keys for PRODUCT', function(done) { 181 | db.discoverPrimaryKeys('PRODUCT', function(err, models) { 182 | if (err) { 183 | console.error(err); 184 | done(err); 185 | } else { 186 | models.forEach(function(m) { 187 | // console.dir(m); 188 | assert(m.tableName === 'PRODUCT'); 189 | }); 190 | done(null, models); 191 | } 192 | }); 193 | }); 194 | 195 | it('should return an array of primary keys for STRONGLOOP.PRODUCT', 196 | function(done) { 197 | db.discoverPrimaryKeys('PRODUCT', {owner: 'STRONGLOOP'}, 198 | function(err, models) { 199 | if (err) { 200 | console.error(err); 201 | done(err); 202 | } else { 203 | models.forEach(function(m) { 204 | // console.dir(m); 205 | assert(m.tableName === 'PRODUCT'); 206 | }); 207 | done(null, models); 208 | } 209 | }); 210 | }); 211 | 212 | it('primary key should be discovered, and db generates instances properly', 213 | function(done) { 214 | db.discoverPrimaryKeys('CHECKCASE', {owner: 'STRONGLOOP'}, 215 | function(err, models) { 216 | if (err) { 217 | console.error(err); 218 | done(err); 219 | } else { 220 | assert.equal(models[0].owner, 'STRONGLOOP'); 221 | assert.equal(models[0].tableName, 'CHECKCASE'); 222 | assert.equal(models[0].columnName, 'camelCase'); 223 | } 224 | Case.create({id: 1, USERNAME: 'checkCase'}, function(err) { 225 | should.not.exists(err); 226 | Case.findOne({}, function(err, c) { 227 | should.not.exist(err); 228 | should.exist(c); 229 | assert.equal(c.id, 1); 230 | assert.equal(c.USERNAME, 'checkCase'); 231 | done(); 232 | }); 233 | }); 234 | }); 235 | }); 236 | }); 237 | 238 | describe('Discover model foreign keys', function() { 239 | it('should return an array of foreign keys for INVENTORY', function(done) { 240 | db.discoverForeignKeys('INVENTORY', function(err, models) { 241 | if (err) { 242 | console.error(err); 243 | done(err); 244 | } else { 245 | models.forEach(function(m) { 246 | // console.dir(m); 247 | assert(m.fkTableName === 'INVENTORY'); 248 | }); 249 | done(null, models); 250 | } 251 | }); 252 | }); 253 | it('should return an array of foreign keys for STRONGLOOP.INVENTORY', 254 | function(done) { 255 | db.discoverForeignKeys('INVENTORY', {owner: 'STRONGLOOP'}, 256 | function(err, models) { 257 | if (err) { 258 | console.error(err); 259 | done(err); 260 | } else { 261 | models.forEach(function(m) { 262 | // console.dir(m); 263 | assert(m.fkTableName === 'INVENTORY'); 264 | }); 265 | done(null, models); 266 | } 267 | }); 268 | }); 269 | }); 270 | 271 | describe('Discover LDL schema from a table', function() { 272 | it('should return an LDL schema for INVENTORY', function(done) { 273 | db.discoverSchema('INVENTORY', {owner: 'STRONGLOOP'}, 274 | function(err, schema) { 275 | // console.log('%j', schema); 276 | assert(schema.name === 'Inventory'); 277 | assert(schema.options.oracle.schema === 'STRONGLOOP'); 278 | assert(schema.options.oracle.table === 'INVENTORY'); 279 | assert(schema.properties.productId); 280 | assert(schema.properties.productId.type === 'String'); 281 | assert(schema.properties.productId.oracle.columnName === 282 | 'PRODUCT_ID'); 283 | assert(schema.properties.locationId); 284 | assert(schema.properties.locationId.type === 'String'); 285 | assert(schema.properties.locationId.oracle.columnName === 286 | 'LOCATION_ID'); 287 | assert(schema.properties.available); 288 | assert(schema.properties.available.type === 'Number'); 289 | assert(schema.properties.total); 290 | assert(schema.properties.total.type === 'Number'); 291 | done(null, schema); 292 | }); 293 | }); 294 | }); 295 | }); 296 | -------------------------------------------------------------------------------- /test/oracle.mapping.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /* global getDataSource */ 9 | process.env.NODE_ENV = 'test'; 10 | require('should'); 11 | require('./init/init'); 12 | 13 | const async = require('async'); 14 | 15 | let db; 16 | 17 | before(function() { 18 | db = getDataSource(); 19 | }); 20 | 21 | describe('Mapping models', function() { 22 | it('should honor the oracle settings for table/column', function(done) { 23 | const schema = 24 | { 25 | name: 'TestInventory', 26 | options: { 27 | idInjection: false, 28 | oracle: { 29 | schema: 'TEST', 'table': 'INVENTORY_TEST', 30 | }, 31 | }, 32 | properties: { 33 | /* 34 | "id": { 35 | "type": "String", "required": true, "length": 20, "id": 1, "oracle": { 36 | "columnName": "INVENTORY_ID", "dataType": "VARCHAR2", "nullable": "N" 37 | } 38 | }, 39 | */ 40 | productId: { 41 | type: 'String', 42 | required: true, 43 | length: 20, 44 | id: 1, 45 | oracle: { 46 | columnName: 'PRODUCT_ID', 47 | dataType: 'VARCHAR2', 48 | nullable: 'N', 49 | }, 50 | }, 51 | locationId: { 52 | type: 'String', 53 | required: true, 54 | length: 20, 55 | id: 2, 56 | oracle: { 57 | columnName: 'LOCATION_ID', 58 | dataType: 'VARCHAR2', 59 | nullable: 'N', 60 | }, 61 | }, 62 | available: { 63 | type: 'Number', 64 | required: false, 65 | length: 22, 66 | oracle: { 67 | columnName: 'AVAILABLE', 68 | dataType: 'NUMBER', 69 | nullable: 'Y', 70 | }, 71 | }, 72 | total: { 73 | type: 'Number', 74 | required: false, 75 | length: 22, 76 | oracle: { 77 | columnName: 'TOTAL', 78 | dataType: 'NUMBER', 79 | nullable: 'Y', 80 | }, 81 | }, 82 | }, 83 | }; 84 | const models = db.modelBuilder.buildModels(schema); 85 | // console.log(models); 86 | const Model = models['TestInventory']; 87 | Model.attachTo(db); 88 | 89 | db.automigrate(function(err, data) { 90 | async.series([ 91 | function(callback) { 92 | Model.destroyAll(callback); 93 | }, 94 | function(callback) { 95 | Model.create( 96 | {productId: 'p001', locationId: 'l001', available: 10, total: 50}, 97 | callback, 98 | ); 99 | }, 100 | function(callback) { 101 | Model.create( 102 | {productId: 'p001', locationId: 'l002', available: 30, total: 40}, 103 | callback, 104 | ); 105 | }, 106 | function(callback) { 107 | Model.create( 108 | {productId: 'p002', locationId: 'l001', available: 15, total: 30}, 109 | callback, 110 | ); 111 | }, 112 | function(callback) { 113 | Model.find({fields: ['productId', 'locationId', 'available']}, 114 | function(err, results) { 115 | // console.log(results); 116 | results.should.have.lengthOf(3); 117 | results.forEach(function(r) { 118 | r.should.have.property('productId'); 119 | r.should.have.property('locationId'); 120 | r.should.have.property('available'); 121 | r.should.have.property('total', undefined); 122 | }); 123 | callback(null, results); 124 | }); 125 | }, 126 | function(callback) { 127 | Model.find({fields: {'total': false}}, function(err, results) { 128 | // console.log(results); 129 | results.should.have.lengthOf(3); 130 | results.forEach(function(r) { 131 | r.should.have.property('productId'); 132 | r.should.have.property('locationId'); 133 | r.should.have.property('available'); 134 | r.should.have.property('total', undefined); 135 | }); 136 | callback(null, results); 137 | }); 138 | }, 139 | ], done); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /test/oracle.regexp.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /* global getDataSource */ 9 | const async = require('async'); 10 | 11 | require('./init/init'); 12 | const should = require('should'); 13 | let db, User; 14 | 15 | describe('regexp', function() { 16 | db = getDataSource(); 17 | 18 | const User = db.define('UserRegExp', { 19 | seq: {type: Number, index: true}, 20 | name: {type: String, index: true, sort: true}, 21 | email: {type: String, index: true}, 22 | }); 23 | 24 | before(seed); 25 | 26 | it('should work when a regex is provided without the regexp operator', 27 | function(done) { 28 | User.find({where: {name: /John.*/i}}, function(err, users) { 29 | should.not.exist(err); 30 | users.length.should.equal(1); 31 | users[0].name.should.equal('John Lennon'); 32 | done(); 33 | }); 34 | }); 35 | 36 | it('should work when a regex is case insensitive', 37 | function(done) { 38 | User.find({where: {name: /JOHN.*/i}}, function(err, users) { 39 | should.not.exist(err); 40 | users.length.should.equal(1); 41 | users[0].name.should.equal('John Lennon'); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should work when a regex is case sensitive', 47 | function(done) { 48 | User.find({where: {name: /JOHN.*/}}, function(err, users) { 49 | should.not.exist(err); 50 | users.length.should.equal(0); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should support the regexp operator with regex strings', function(done) { 56 | User.find({where: {name: {regexp: '^J'}}}, function(err, users) { 57 | should.not.exist(err); 58 | users.length.should.equal(1); 59 | users[0].name.should.equal('John Lennon'); 60 | done(); 61 | }); 62 | }); 63 | 64 | it('should support the regexp operator with regex literals', function(done) { 65 | User.find({where: {name: {regexp: /^J/}}}, function(err, users) { 66 | should.not.exist(err); 67 | users.length.should.equal(1); 68 | users[0].name.should.equal('John Lennon'); 69 | done(); 70 | }); 71 | }); 72 | 73 | it('should support the regexp operator with regex objects', function(done) { 74 | User.find({where: {name: {regexp: new RegExp(/^J/)}}}, function(err, 75 | users) { 76 | should.not.exist(err); 77 | users.length.should.equal(1); 78 | users[0].name.should.equal('John Lennon'); 79 | done(); 80 | }); 81 | }); 82 | 83 | function seed(done) { 84 | const beatles = [ 85 | { 86 | seq: 0, 87 | name: 'John Lennon', 88 | email: 'john@b3atl3s.co.uk', 89 | }, 90 | { 91 | seq: 1, 92 | name: 'Paul McCartney', 93 | email: 'paul@b3atl3s.co.uk', 94 | }, 95 | {seq: 2, name: 'George Harrison'}, 96 | {seq: 3, name: 'Ringo Starr'}, 97 | {seq: 4, name: 'Pete Best'}, 98 | {seq: 5, name: 'Stuart Sutcliffe'}, 99 | ]; 100 | 101 | async.series([ 102 | function(cb) { 103 | db.automigrate('UserRegExp', cb); 104 | }, 105 | function(cb) { 106 | async.each(beatles, User.create.bind(User), cb); 107 | }, 108 | ], done); 109 | } 110 | }); 111 | 112 | -------------------------------------------------------------------------------- /test/oracle.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /* global getDataSource */ 9 | const juggler = require('loopback-datasource-juggler'); 10 | const CreateDS = juggler.DataSource; 11 | 12 | require('./init/init'); 13 | const should = require('should'); 14 | let Post, db; 15 | 16 | describe('oracle connector', function() { 17 | before(function() { 18 | db = getDataSource(); 19 | 20 | Post = db.define('PostWithBoolean', { 21 | title: {type: String, length: 255, index: true}, 22 | content: {type: String}, 23 | approved: Boolean, 24 | }); 25 | }); 26 | 27 | it('should run migration', function(done) { 28 | db.automigrate('PostWithBoolean', function() { 29 | done(); 30 | }); 31 | }); 32 | 33 | let post; 34 | it('should support boolean types with true value', function(done) { 35 | Post.create({title: 'T1', content: 'C1', approved: true}, function(err, p) { 36 | should.not.exists(err); 37 | post = p; 38 | Post.findById(p.id, function(err, p) { 39 | should.not.exists(err); 40 | p.should.have.property('approved', true); 41 | done(); 42 | }); 43 | }); 44 | }); 45 | 46 | it('should support updating boolean types with false value', function(done) { 47 | Post.update({id: post.id}, {approved: false}, function(err) { 48 | should.not.exists(err); 49 | Post.findById(post.id, function(err, p) { 50 | should.not.exists(err); 51 | p.should.have.property('approved', false); 52 | done(); 53 | }); 54 | }); 55 | }); 56 | 57 | it('should support boolean types with false value', function(done) { 58 | Post.create({title: 'T2', content: 'C2', approved: false}, 59 | function(err, p) { 60 | should.not.exists(err); 61 | post = p; 62 | Post.findById(p.id, function(err, p) { 63 | should.not.exists(err); 64 | p.should.have.property('approved', false); 65 | done(); 66 | }); 67 | }); 68 | }); 69 | 70 | it('should return the model instance for upsert', function(done) { 71 | Post.upsert({id: post.id, title: 'T2_new', content: 'C2_new', 72 | approved: true}, function(err, p) { 73 | p.should.have.property('id', post.id); 74 | p.should.have.property('title', 'T2_new'); 75 | p.should.have.property('content', 'C2_new'); 76 | p.should.have.property('approved', true); 77 | done(); 78 | }); 79 | }); 80 | 81 | it('should return the model instance for upsert when id is not present', 82 | function(done) { 83 | Post.upsert({title: 'T2_new', content: 'C2_new', approved: true}, 84 | function(err, p) { 85 | p.should.have.property('id'); 86 | p.should.have.property('title', 'T2_new'); 87 | p.should.have.property('content', 'C2_new'); 88 | p.should.have.property('approved', true); 89 | done(); 90 | }); 91 | }); 92 | 93 | it('should escape number values to defect SQL injection in findById', 94 | function(done) { 95 | Post.findById('(SELECT 1+1)', function(err, p) { 96 | should.exists(err); 97 | done(); 98 | }); 99 | }); 100 | 101 | it('should escape number values to defect SQL injection in find', 102 | function(done) { 103 | Post.find({where: {id: '(SELECT 1+1)'}}, function(err, p) { 104 | should.exists(err); 105 | done(); 106 | }); 107 | }); 108 | 109 | it('should escape number values to defect SQL injection in find with gt', 110 | function(done) { 111 | Post.find({where: {id: {gt: '(SELECT 1+1)'}}}, function(err, p) { 112 | should.exists(err); 113 | done(); 114 | }); 115 | }); 116 | 117 | it('should escape number values to defect SQL injection in find ', 118 | function(done) { 119 | Post.find({limit: '(SELECT 1+1)'}, function(err, p) { 120 | should.exists(err); 121 | done(); 122 | }); 123 | }); 124 | 125 | it('should escape number values to defect SQL injection in find with inq', 126 | function(done) { 127 | Post.find({where: {id: {inq: ['(SELECT 1+1)']}}}, function(err, p) { 128 | should.exists(err); 129 | done(); 130 | }); 131 | }); 132 | }); 133 | 134 | describe('lazyConnect', function() { 135 | it('should skip connect phase (lazyConnect = true)', function(done) { 136 | const dsConfig = { 137 | host: '127.0.0.1', 138 | port: 4, 139 | lazyConnect: true, 140 | debug: false, 141 | }; 142 | const ds = getDS(dsConfig); 143 | 144 | const errTimeout = setTimeout(function() { 145 | done(); 146 | }, 2000); 147 | ds.on('error', function(err) { 148 | clearTimeout(errTimeout); 149 | done(err); 150 | }); 151 | }); 152 | 153 | it('should report connection error (lazyConnect = false)', function(done) { 154 | const dsConfig = { 155 | host: '127.0.0.1', 156 | port: 4, 157 | lazyConnect: false, 158 | debug: false, 159 | username: 'user', 160 | }; 161 | const ds = getDS(dsConfig); 162 | 163 | ds.on('error', function(err) { 164 | err.message.should.containEql('TNS'); 165 | done(); 166 | }); 167 | }); 168 | 169 | const getDS = function(config) { 170 | const db = new CreateDS(require('../'), config); 171 | return db; 172 | }; 173 | }); 174 | 175 | -------------------------------------------------------------------------------- /test/oracle.transaction.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-connector-oracle 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /* global getDataSource */ 9 | require('./init/init.js'); 10 | require('should'); 11 | 12 | const Transaction = require('loopback-connector').Transaction; 13 | 14 | let db, Post; 15 | 16 | describe('transactions', function() { 17 | before(function(done) { 18 | db = getDataSource(); 19 | Post = db.define('PostTX', { 20 | title: {type: String, length: 255, index: true}, 21 | content: {type: String}, 22 | }); 23 | db.automigrate('PostTX', done); 24 | }); 25 | 26 | let currentTx; 27 | // Return an async function to start a transaction and create a post 28 | function createPostInTx(post) { 29 | return function(done) { 30 | Transaction.begin(db.connector, Transaction.READ_COMMITTED, 31 | function(err, tx) { 32 | if (err) return done(err); 33 | currentTx = tx; 34 | Post.create(post, {transaction: tx}, 35 | function(err, p) { 36 | if (err) { 37 | done(err); 38 | } else { 39 | done(); 40 | } 41 | }); 42 | }); 43 | }; 44 | } 45 | 46 | // Return an async function to find matching posts and assert number of 47 | // records to equal to the count 48 | function expectToFindPosts(where, count, inTx) { 49 | return function(done) { 50 | const options = {}; 51 | if (inTx) { 52 | options.transaction = currentTx; 53 | } 54 | Post.find({where: where}, options, 55 | function(err, posts) { 56 | if (err) return done(err); 57 | posts.length.should.be.eql(count); 58 | done(); 59 | }); 60 | }; 61 | } 62 | 63 | describe('commit', function() { 64 | const post = {title: 't1', content: 'c1'}; 65 | before(createPostInTx(post)); 66 | 67 | it('should not see the uncommitted insert', expectToFindPosts(post, 0)); 68 | 69 | it('should see the uncommitted insert from the same transaction', 70 | expectToFindPosts(post, 1, true)); 71 | 72 | it('should commit a transaction', function(done) { 73 | currentTx.commit(done); 74 | }); 75 | 76 | it('should see the committed insert', expectToFindPosts(post, 1)); 77 | }); 78 | 79 | describe('rollback', function() { 80 | const post = {title: 't2', content: 'c2'}; 81 | before(createPostInTx(post)); 82 | 83 | it('should not see the uncommitted insert', expectToFindPosts(post, 0)); 84 | 85 | it('should see the uncommitted insert from the same transaction', 86 | expectToFindPosts(post, 1, true)); 87 | 88 | it('should rollback a transaction', function(done) { 89 | currentTx.rollback(done); 90 | }); 91 | 92 | it('should not see the rolledback insert', expectToFindPosts(post, 0)); 93 | }); 94 | }); 95 | 96 | --------------------------------------------------------------------------------