├── .babelrc ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── circle.yml ├── conf.json ├── dist └── js-data-sql.d.ts ├── docker-compose.yml ├── mocha.start.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── index.js ├── test ├── create_trx.spec.js ├── destroy_trx.spec.js ├── filterQuery.spec.js ├── findAll.spec.js ├── knexfile.js ├── migrations │ └── 20160124161805_initial_schema.js └── update_trx.spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [ 6 | "syntax-async-functions", 7 | "transform-regenerator" 8 | ] 9 | } -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to js-data-sql 2 | 3 | [Read the general Contributing Guide](http://js-data.io/docs/contributing). 4 | 5 | ## Project structure 6 | 7 | * `dist/` - Contains final build files for distribution 8 | * `doc/` - Output folder for JSDocs 9 | * `src/` - Project source code 10 | * `test/` - Project tests 11 | 12 | ## Clone, build & test 13 | 14 | 1. `clone git@github.com:js-data/js-data-sql.git` 15 | 1. `cd js-data-sql` 16 | 1. `npm install` 17 | 1. `npm run build` - Lint and build distribution files 18 | 1. `npm run mocha` - Run tests (A compatible sql server must be running) 19 | 20 | #### Submitting Pull Requests 21 | 22 | 1. Contribute to the issue/discussion that is the reason you'll be developing in 23 | the first place 24 | 1. Fork js-data-sql 25 | 1. `git clone git@github.com:/js-data-sql.git` 26 | 1. `cd js-data-sql; npm install;` 27 | 1. Write your code, including relevant documentation and tests 28 | 1. Run `npm test` (build and test) 29 | - You need Node 4.x that includes generator support without a flag 30 | - The tests expect a database to be running as follows, but can be overridden by passing the applicable environment variable as indicated (ex. `DB_HOST=192.168.99.100 npm test`). 31 | - `DB_HOST`: `localhost` 32 | - `DB_NAME`: `circle_test` 33 | - `DB_USER`: `ubuntu` 34 | - You may use `docker`/`docker-compose` to create MySql and Postgres containers to test against 35 | - `docker-compose up -d` 36 | - Start containers named `js-data-sql-mysql` and `js-data-sql-pg` 37 | - MySQL 38 | - Environment variables 39 | - `DB_CLIENT` = `mysql` 40 | - `DB_USER` = `root` 41 | - `DB_HOST` = `IP of docker-machine if not localhost` 42 | - Populate schema 43 | - `DB_CLIENT=mysql DB_USER=root npm run migrate-db` 44 | - Also set `DB_HOST` if different from `localhost` 45 | - Run tests 46 | - `npm run mocha-mysql` 47 | - Set `DB_HOST` if different from `localhost` 48 | - Run cli 49 | - `docker exec -it js-data-sql-mysql mysql circle_test` 50 | - Postgres 51 | - Environment variables 52 | - `DB_CLIENT` = `pg` 53 | - `DB_USER` = `ubuntu` 54 | - `DB_HOST` = `IP of docker-machine if not localhost` 55 | - Populate schema 56 | - `DB_CLIENT=pg npm run migrate-db` 57 | - Also set `DB_HOST` if different from `localhost` 58 | - Run tests 59 | - `npm run mocha-pg` 60 | - Also set `DB_HOST` if different from `localhost` 61 | - `docker exec -it js-data-sql-pg psql -U ubuntu -d circle_test` 62 | - Run cli 63 | - All databases 64 | - Run all tests against MySQL and Postgres 65 | - `npm run mocha-all` 66 | - Also set `DB_HOST` if different from `localhost` 67 | 68 | 1. Your code will be linted and checked for formatting, the tests will be run 69 | 1. The `dist/` folder & files will be generated, do NOT commit `dist/*`! They 70 | will be committed when a release is cut. 71 | 1. Submit your PR and we'll review! 72 | 1. Thanks! 73 | 74 | ## To cut a release 75 | 76 | 1. Checkout master 77 | 1. Bump version in `package.json` appropriately 78 | 1. Update `CHANGELOG.md` appropriately 79 | 1. Run `npm run release` 80 | 1. Commit and push changes 81 | 1. Checkout `release`, merge `master` into `release` 82 | 1. Run `npm run release` again 83 | 1. Commit and push changes 84 | 1. Make a GitHub release 85 | - tag from `release` branch 86 | - set tag name to version 87 | - set release name to version 88 | - set release body to changelog entry for the version 89 | 1. `npm publish .` 90 | 91 | See also [Community & Support](http://js-data.io/docs/community). 92 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | (delete this line) Find out how to get help here: http://js-data.io/docs/community. 2 | 3 | 4 | 5 | Thanks! 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # (it's a good idea to open an issue first for discussion) 2 | 3 | - [ ] - `npm test` succeeds 4 | - [ ] - Code coverage does not decrease (if any source code was changed) 5 | - [ ] - Appropriate JSDoc comments were updated in source code (if applicable) 6 | - [ ] - Approprate changes to js-data.io docs have been suggested ("Suggest Edits" button) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | .idea/ 31 | *.iml 32 | coverage/ 33 | .open 34 | doc/ 35 | *.db 36 | 37 | .nyc_output/ 38 | dist/*.js 39 | dist/*.map 40 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of js-data-sql project authors. 2 | # 3 | # Names are formatted as: 4 | # # commits Name or Organization 5 | # The email address is not required for organizations. 6 | Andy Vanbutsele 7 | Jason Dobry 8 | Jason Dobry 9 | Mike Eldridge 10 | Mike Eldridge 11 | Nathan Vecchiarelli 12 | Robert P 13 | Sean Lynch 14 | Simon Williams 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ##### 1.0.1 - 18 August 2017 2 | 3 | ###### Bug fixes 4 | - Remove `mysql` from dependencies 5 | 6 | ##### 1.0.0 - 17 August 2017 7 | 8 | Stable 1.0.0 release 9 | 10 | ##### 1.0.0-beta.3 - 16 May 2016 11 | 12 | ###### Bug fixes 13 | - Small fix for filterQuery 14 | 15 | ##### 1.0.0-beta.2 - 16 May 2016 16 | 17 | ###### Breaking changes 18 | - Renamed `knexOptions` to `knexOpts` to be consistent with other adapters. 19 | 20 | ###### Backwards compatible changes 21 | - Added support for grouped where clauses 22 | 23 | ##### 1.0.0-beta.1 - 14 May 2016 24 | 25 | Official v1 beta release 26 | 27 | ###### Breaking changes 28 | 29 | - `SqlAdapter#query` has been renamed to `SqlAdapter#knex` 30 | - Options passed to `new SqlAdapter` are no longer passed directly to `knex(options)`, instead `knex` options must be nested under a `knexOpts` field in the `options` object passed to `new SqlAdapter`, so as to separate `knex` options from adapter options. 31 | - Now depends on js-data v3, no longer works with js-data v2 32 | - The signature for the `filterQuery` method has been changed. `filterQuery` no longer performs any select, but only as `where` modifier to an existing sqlBuilder instance that must be passed as the first argument to `filterQuery`. 33 | - Now you must import like this: 34 | 35 | ```js 36 | // CommonJS 37 | var JSDataSql = require('js-data-sql') 38 | var SqlAdapter = JSDataSql.SqlAdapter 39 | var adapter = new SqlAdapter({...}) 40 | ``` 41 | 42 | ```js 43 | // ES2015 modules 44 | import {SqlAdapter} from 'js-data-sql' 45 | const adapter = new SqlAdapter({...}) 46 | ``` 47 | 48 | - `SqlAdapter` now extends the base `Adapter` class, which does the heavy lifting for 49 | eager-loading relations and adds a multitude of lifecycle hook methods. 50 | 51 | ##### 0.11.2 - 19 October 2015 52 | 53 | - Fixed build 54 | 55 | ##### 0.11.1 - 19 October 2015 56 | 57 | - #23 - Allow sending 'offset' and 'limit' params as strings. 58 | - #24 - Do not return relation columns on parent when performing filter 59 | - Simplified build and devDependencies 60 | 61 | ##### 0.11.0 - 08 October 2015 62 | 63 | - #18, #21 - Support filtering across relations (apply joins / fix column name) by @techniq 64 | - #19, #20 - Use co-mocha for tests to simplify async flow. by @techniq 65 | - Project now enforces JS Standard Style 66 | 67 | ##### 0.10.0 - 19 September 2015 68 | 69 | - #15, #16 - Removed the various database libs from peerDependencies. 70 | 71 | ##### 0.9.2 - 16 July 2015 72 | 73 | ###### Backwards compatible bug fixes 74 | - create and update weren't accepting options 75 | 76 | ##### 0.9.1 - 10 July 2015 77 | 78 | ###### Backwards compatible bug fixes 79 | - Fix for loading relations in find() and findAll() 80 | 81 | ##### 0.9.0 - 10 July 2015 82 | 83 | ###### Backwards compatible API changes 84 | - Support for loading deeply nested relations in `find` 85 | 86 | ##### 0.8.0 - 09 July 2015 87 | 88 | ###### Backwards compatible API changes 89 | - #5 - Support for loading relations in `findAll` 90 | 91 | ##### 0.7.0 - 02 July 2015 92 | 93 | Stable Version 0.7.0 94 | 95 | ##### 0.6.1 - 24 June 2015 96 | 97 | ###### Backwards compatible bug fixes 98 | - #13 - global leak (deepMixIn) 99 | 100 | ##### 0.6.0 - 15 June 2015 101 | 102 | ###### Backwards compatible bug fixes 103 | - #12 - Create and Update don't work with non-numeric and/or primary keys 104 | 105 | ##### 0.5.0 - 08 June 2015 106 | 107 | ###### Backwards compatible API changes 108 | - #4 - Add support for loading relations in find() 109 | - #8 - LIKE operator support 110 | 111 | ###### Backwards compatible bug fixes 112 | - #9 - Throw error when using bad WHERE operator 113 | 114 | ##### 0.4.0 - 26 March 2015 115 | 116 | ###### Backwards compatible bug fixes 117 | - #2 - Should not be saving relations (duplicating data) 118 | - #3 - Need to use removeCircular 119 | 120 | ##### 0.3.0 - 11 March 2015 121 | 122 | ###### Other 123 | - #1 - Converted to ES6. 124 | 125 | ##### 0.2.0 - 25 February 2015 126 | 127 | - Upgraded dependencies 128 | 129 | ##### 0.1.0 - 05 February 2015 130 | 131 | - Initial Release 132 | 133 | ##### 0.0.1 - 05 February 2015 134 | 135 | - Initial Commit 136 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # People who have contributed to the js-data-sql project. 2 | # 3 | # Names should be added to this file as: 4 | # [commit count] Name 5 | 2 Andy Vanbutsele 6 | 44 Jason Dobry 7 | 19 Jason Dobry 8 | 3 Mike Eldridge 9 | 2 Mike Eldridge 10 | 1 Nathan Vecchiarelli 11 | 1 Robert P 12 | 69 Sean Lynch 13 | 1 Simon Williams -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2017 js-data-sql project authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | js-data logo 2 | 3 | # js-data-sql 4 | 5 | [![Slack][1]][2] 6 | [![NPM][3]][4] 7 | [![Tests][5]][6] 8 | [![Downloads][7]][8] 9 | [![Coverage][9]][10] 10 | 11 | A Postgres/MySQL/MariaDB/SQLite3 adapter for the [JSData Node.js ORM][11]. 12 | 13 | ### Installation 14 | 15 | npm install --save js-data js-data-sql 16 | 17 | And then you also need to install one of the following: 18 | 19 | * `pg` 20 | * `sqlite3` 21 | * `mysql` 22 | * `mysql2` 23 | * `mariasql` 24 | * `strong-oracle` 25 | * `oracle` 26 | * `mssql` 27 | 28 | ### Usage 29 | 30 | ```js 31 | import { SqlAdapter } from 'js-data-sql'; 32 | 33 | // Create an instance of SqlAdapter 34 | const adapter = new SqlAdapter({ 35 | knexOpts: { 36 | client: 'mysql' 37 | } 38 | }); 39 | 40 | // Other JSData setup hidden 41 | 42 | // Register the adapter instance 43 | store.registerAdapter('sql', adapter, { default: true }); 44 | ``` 45 | 46 | ### JSData + SQL Tutorial 47 | 48 | Start with the [JSData + SQL tutorial][12] or checkout the [API Reference Documentation][13]. 49 | 50 | ### Need help? 51 | 52 | Please [post a question][14] on Stack Overflow. **This is the preferred method.** 53 | 54 | You can also chat with folks on the [Slack Channel][15]. If you end up getting 55 | your question answered, please still consider consider posting your question to 56 | Stack Overflow (then possibly answering it yourself). Thanks! 57 | 58 | ### Want to contribute? 59 | 60 | Awesome! You can get started over at the [Contributing guide][16]. 61 | 62 | Thank you! 63 | 64 | ### License 65 | 66 | [The MIT License (MIT)][17] 67 | 68 | Copyright (c) 2014-2017 [js-data-sql project authors][18] 69 | 70 | [1]: http://slack.js-data.io/badge.svg 71 | [2]: http://slack.js-data.io 72 | [3]: https://img.shields.io/npm/v/js-data-sql.svg?style=flat 73 | [4]: https://www.npmjs.org/package/js-data-sql 74 | [5]: https://img.shields.io/circleci/project/js-data/js-data-sql.svg?style=flat 75 | [6]: https://circleci.com/gh/js-data/js-data-sql 76 | [7]: https://img.shields.io/npm/dm/js-data-sql.svg?style=flat 77 | [8]: https://www.npmjs.org/package/js-data-sql 78 | [9]: https://img.shields.io/codecov/c/github/js-data/js-data-sql.svg?style=flat 79 | [10]: https://codecov.io/github/js-data/js-data-sql 80 | [11]: http://www.js-data.io/ 81 | [12]: http://www.js-data.io/docs/js-data-sql 82 | [13]: http://api.js-data.io/js-data-sql 83 | [14]: http://stackoverflow.com/questions/tagged/jsdata 84 | [15]: http://slack.js-data.io/ 85 | [16]: https://github.com/js-data/js-data-sql/blob/master/.github/CONTRIBUTING.md 86 | [17]: https://github.com/js-data/js-data-sql/blob/master/LICENSE 87 | [18]: https://github.com/js-data/js-data-sql/blob/master/AUTHORS 88 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | # Adjust the behavior of the virtual machine (VM) 2 | machine: 3 | node: 4 | version: 6.11.2 5 | database: 6 | override: 7 | - DB_CLIENT=mysql npm run migrate-db 8 | - DB_CLIENT=pg npm run migrate-db 9 | 10 | # Use for broader build-related configuration 11 | general: 12 | branches: 13 | ignore: 14 | - gh-pages 15 | 16 | # Install your project's language-specific dependencies 17 | dependencies: 18 | pre: 19 | - npm install -g nyc codecov 20 | - npm install pg mysql 21 | 22 | # Run your tests 23 | test: 24 | post: 25 | - nyc report --reporter=lcov > coverage.lcov && codecov 26 | -------------------------------------------------------------------------------- /conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "includePattern": ".*js$" 4 | }, 5 | "plugins": ["plugins/markdown"], 6 | "opts": { 7 | "template": "./node_modules/ink-docstrap/template", 8 | "destination": "./doc/", 9 | "recurse": true, 10 | "verbose": true, 11 | "readme": "./README.md", 12 | "package": "./package.json" 13 | }, 14 | "templates": { 15 | "theme": "jsdata", 16 | "systemName": "js-data-sql", 17 | "copyright": "js-data-sql Copyright © 2014-2017 js-data-sql project authors", 18 | "outputSourceFiles": true, 19 | "linenums": true, 20 | "footer": "", 21 | "analytics": { 22 | "ua": "UA-55528236-2", 23 | "domain": "api.js-data.io" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /dist/js-data-sql.d.ts: -------------------------------------------------------------------------------- 1 | import {Adapter} from 'js-data-adapter' 2 | 3 | interface IDict { 4 | [key: string]: any; 5 | } 6 | interface IBaseAdapter extends IDict { 7 | debug?: boolean, 8 | raw?: boolean 9 | } 10 | interface IBaseSqlAdapter extends IBaseAdapter { 11 | knexOpt?: IDict 12 | } 13 | export class SqlAdapter extends Adapter { 14 | static extend(instanceProps?: IDict, classProps?: IDict): typeof SqlAdapter 15 | constructor(opts?: IBaseSqlAdapter) 16 | } 17 | export interface OPERATORS { 18 | '=': Function 19 | '==': Function 20 | '===': Function 21 | '!=': Function 22 | '!==': Function 23 | '>': Function 24 | '>=': Function 25 | '<': Function 26 | '<=': Function 27 | 'isectEmpty': Function 28 | 'isectNotEmpty': Function 29 | 'in': Function 30 | 'notIn': Function 31 | 'contains': Function 32 | 'notContains': Function 33 | } 34 | export interface version { 35 | full: string 36 | minor: string 37 | major: string 38 | patch: string 39 | alpha: string | boolean 40 | beta: string | boolean 41 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | mysql: 2 | image: mysql:5.6.28 3 | container_name: 'js-data-sql-mysql' 4 | environment: 5 | - MYSQL_DATABASE=circle_test 6 | # - MYSQL_USER=ubuntu 7 | # - MYSQL_PASSWORD= 8 | - MYSQL_ALLOW_EMPTY_PASSWORD=yes 9 | ports: 10 | - "3306:3306" 11 | 12 | postgres: 13 | image: postgres:9.4.5 14 | container_name: 'js-data-sql-pg' 15 | ports: 16 | - "5432:5432" 17 | environment: 18 | - POSTGRES_DB=circle_test 19 | - POSTGRES_USER=ubuntu 20 | 21 | -------------------------------------------------------------------------------- /mocha.start.js: -------------------------------------------------------------------------------- 1 | /* global assert:true */ 2 | 'use strict' 3 | 4 | // prepare environment for js-data-adapter-tests 5 | 'babel-polyfill' 6 | 7 | import * as JSData from 'js-data' 8 | import JSDataAdapterTests from './node_modules/js-data-adapter/dist/js-data-adapter-tests' 9 | import * as JSDataSql from './src/index' 10 | 11 | const assert = global.assert = JSDataAdapterTests.assert 12 | global.sinon = JSDataAdapterTests.sinon 13 | 14 | const DB_CLIENT = process.env.DB_CLIENT || 'mysql' 15 | 16 | let connection 17 | 18 | if (DB_CLIENT === 'sqlite3') { 19 | connection = { 20 | filename: process.env.DB_FILE 21 | } 22 | } else { 23 | connection = { 24 | host: process.env.DB_HOST || '127.0.0.1', 25 | user: process.env.DB_USER || 'root', 26 | database: process.env.DB_NAME || 'test' 27 | } 28 | } 29 | 30 | JSDataAdapterTests.init({ 31 | debug: false, 32 | JSData: JSData, 33 | Adapter: JSDataSql.SqlAdapter, 34 | adapterConfig: { 35 | knexOpts: { 36 | client: DB_CLIENT, 37 | connection: connection, 38 | pool: { 39 | min: 1, 40 | max: 10 41 | }, 42 | debug: !!process.env.DEBUG 43 | }, 44 | debug: !!process.env.DEBUG 45 | }, 46 | // js-data-sql does NOT support these features 47 | xmethods: [ 48 | // The adapter extends aren't flexible enough yet, the SQL adapter has 49 | // required parameters, which aren't passed in the extend tests. 50 | 'extend' 51 | ], 52 | xfeatures: [ 53 | 'findHasManyLocalKeys', 54 | 'findHasManyForeignKeys', 55 | 'filterOnRelations' 56 | ] 57 | }) 58 | 59 | describe('exports', function () { 60 | it('should have correct exports', function () { 61 | assert(JSDataSql.SqlAdapter) 62 | assert(JSDataSql.OPERATORS) 63 | assert(JSDataSql.OPERATORS['==']) 64 | assert(JSDataSql.version) 65 | }) 66 | }) 67 | 68 | require('./test/create_trx.spec') 69 | require('./test/destroy_trx.spec') 70 | require('./test/filterQuery.spec') 71 | require('./test/update_trx.spec') 72 | 73 | afterEach(function () { 74 | return this.$$adapter.knex.destroy() 75 | }) 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-data-sql", 3 | "description": "Postgres/MySQL/MariaDB/SQLite3 adapter for js-data.", 4 | "version": "1.0.1", 5 | "homepage": "https://github.com/js-data/js-data-sql", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/js-data/js-data-sql.git" 9 | }, 10 | "author": "js-data-sql project authors", 11 | "license": "MIT", 12 | "main": "./dist/js-data-sql.js", 13 | "typings": "./dist/js-data-sql.d.ts", 14 | "files": [ 15 | "dist/", 16 | "src/", 17 | "AUTHORS", 18 | "CONTRIBUTORS" 19 | ], 20 | "keywords": [ 21 | "data", 22 | "datastore", 23 | "store", 24 | "database", 25 | "adapter", 26 | "sql", 27 | "mysql", 28 | "postgres", 29 | "mariadb", 30 | "sqlite" 31 | ], 32 | "standard": { 33 | "parser": "babel-eslint", 34 | "globals": [ 35 | "describe", 36 | "it", 37 | "sinon", 38 | "assert", 39 | "before", 40 | "after", 41 | "beforeEach", 42 | "afterEach" 43 | ], 44 | "ignore": [ 45 | "dist/" 46 | ] 47 | }, 48 | "scripts": { 49 | "lint": "standard '**/*.js'", 50 | "bundle": "rollup src/index.js -c -o dist/js-data-sql.js -m dist/js-data-sql.js.map -f cjs && repo-tools write-version dist/js-data-sql.js", 51 | "doc": "jsdoc -c conf.json src node_modules/js-data-adapter/src", 52 | "build": "npm run lint && npm run bundle", 53 | "mocha-sqlite3": "DB_CLIENT=sqlite3 DB_FILE=\"./test.db\" mocha -t 30000 -R dot -r source-map-support/register mocha.start.js", 54 | "mocha-mysql": "DB_CLIENT=mysql mocha -t 30000 -R dot -r babel-core/register -r babel-polyfill mocha.start.js", 55 | "mocha-pg": "DB_CLIENT=pg mocha -t 30000 -R dot -r babel-core/register -r babel-polyfill mocha.start.js", 56 | "mocha-all": "npm run mocha-mysql && npm run mocha-pg", 57 | "cover": "nyc --require babel-core/register --require babel-polyfill --cache mocha -t 20000 -R dot mocha.start.js && nyc report --reporter=html", 58 | "test": "npm run build && npm run cover", 59 | "release": "npm test && npm run doc && repo-tools changelog && repo-tools authors", 60 | "create-migration": "knex --cwd=test migrate:make", 61 | "migrate-db": "knex --cwd=test migrate:latest", 62 | "rollback-db": "knex --cwd=test migrate:rollback" 63 | }, 64 | "dependencies": { 65 | "js-data": ">=3.0.0", 66 | "js-data-adapter": "1.0.0", 67 | "knex": ">=0.13.0", 68 | "lodash.snakecase": "4.1.1", 69 | "lodash.tostring": "4.1.4" 70 | }, 71 | "peerDependencies": { 72 | "js-data": ">=3.0.0", 73 | "knex": ">=0.13.0" 74 | }, 75 | "devDependencies": { 76 | "babel-core": "6.26.0", 77 | "babel-eslint": "7.2.3", 78 | "babel-plugin-external-helpers": "6.22.0", 79 | "babel-plugin-syntax-async-functions": "6.13.0", 80 | "babel-plugin-transform-regenerator": "6.26.0", 81 | "babel-polyfill": "6.26.0", 82 | "babel-preset-es2015": "6.24.1", 83 | "chai": "4.1.1", 84 | "ink-docstrap": "git+https://github.com/js-data/docstrap.git#cfbe45fa313e1628c493076d5e15d2b855dfbf2c", 85 | "js-data-repo-tools": "1.0.0", 86 | "jsdoc": "3.5.4", 87 | "mocha": "3.5.0", 88 | "nyc": "11.1.0", 89 | "rollup": "0.47.6", 90 | "rollup-plugin-babel": "3.0.2", 91 | "sinon": "3.2.1", 92 | "standard": "10.0.3" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | 3 | export default { 4 | external: [ 5 | 'knex', 6 | 'js-data', 7 | 'js-data-adapter', 8 | 'lodash.tostring', 9 | 'lodash.snakecase' 10 | ], 11 | plugins: [ 12 | babel({ 13 | babelrc: false, 14 | plugins: [ 15 | 'external-helpers' 16 | ], 17 | presets: [ 18 | [ 19 | 'es2015', 20 | { 21 | modules: false 22 | } 23 | ] 24 | ], 25 | exclude: 'node_modules/**' 26 | }) 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import knex from 'knex' 2 | import {utils} from 'js-data' 3 | 4 | import { 5 | Adapter, 6 | reserved 7 | } from 'js-data-adapter' 8 | import toString from 'lodash.tostring' 9 | import snakeCase from 'lodash.snakecase' 10 | 11 | const DEFAULTS = {} 12 | 13 | const equal = function (query, field, value, isOr) { 14 | if (value === null) { 15 | return query[isOr ? 'orWhereNull' : 'whereNull'](field) 16 | } 17 | return query[getWhereType(isOr)](field, value) 18 | } 19 | 20 | const notEqual = function (query, field, value, isOr) { 21 | if (value === null) { 22 | return query[isOr ? 'orWhereNotNull' : 'whereNotNull'](field) 23 | } 24 | return query[getWhereType(isOr)](field, '!=', value) 25 | } 26 | 27 | const getWhereType = function (isOr) { 28 | return isOr ? 'orWhere' : 'where' 29 | } 30 | 31 | const MILES_REGEXP = /(\d+(\.\d+)?)\s*(m|M)iles$/ 32 | const KILOMETERS_REGEXP = /(\d+(\.\d+)?)\s*(k|K)$/ 33 | 34 | /** 35 | * Default predicate functions for the filtering operators. 36 | * 37 | * @name module:js-data-sql.OPERATORS 38 | * @property {Function} == Equality operator. 39 | * @property {Function} != Inequality operator. 40 | * @property {Function} > "Greater than" operator. 41 | * @property {Function} >= "Greater than or equal to" operator. 42 | * @property {Function} < "Less than" operator. 43 | * @property {Function} <= "Less than or equal to" operator. 44 | * @property {Function} isectEmpty Operator to test that the intersection 45 | * between two arrays is empty. Not supported. 46 | * @property {Function} isectNotEmpty Operator to test that the intersection 47 | * between two arrays is NOT empty. Not supported. 48 | * @property {Function} in Operator to test whether a value is found in the 49 | * provided array. 50 | * @property {Function} notIn Operator to test whether a value is NOT found in 51 | * the provided array. 52 | * @property {Function} contains Operator to test whether an array contains the 53 | * provided value. Not supported. 54 | * @property {Function} notContains Operator to test whether an array does NOT 55 | * contain the provided value. Not supported. 56 | */ 57 | export const OPERATORS = { 58 | '=': equal, 59 | '==': equal, 60 | '===': equal, 61 | '!=': notEqual, 62 | '!==': notEqual, 63 | '>': function (query, field, value, isOr) { 64 | return query[getWhereType(isOr)](field, '>', value) 65 | }, 66 | '>=': function (query, field, value, isOr) { 67 | return query[getWhereType(isOr)](field, '>=', value) 68 | }, 69 | '<': function (query, field, value, isOr) { 70 | return query[getWhereType(isOr)](field, '<', value) 71 | }, 72 | '<=': function (query, field, value, isOr) { 73 | return query[getWhereType(isOr)](field, '<=', value) 74 | }, 75 | 'isectEmpty': function (query, field, value, isOr) { 76 | throw new Error('isectEmpty not supported!') 77 | }, 78 | 'isectNotEmpty': function (query, field, value, isOr) { 79 | throw new Error('isectNotEmpty not supported!') 80 | }, 81 | 'in': function (query, field, value, isOr) { 82 | return query[getWhereType(isOr)](field, 'in', value) 83 | }, 84 | 'notIn': function (query, field, value, isOr) { 85 | return query[isOr ? 'orNotIn' : 'notIn'](field, value) 86 | }, 87 | 'contains': function (query, field, value, isOr) { 88 | throw new Error('contains not supported!') 89 | }, 90 | 'notContains': function (query, field, value, isOr) { 91 | throw new Error('notContains not supported!') 92 | }, 93 | 'like': function (query, field, value, isOr) { 94 | return query[getWhereType(isOr)](field, 'like', value) 95 | }, 96 | 'near': function (query, field, value, isOr) { 97 | let radius 98 | let unitsPerDegree 99 | if (typeof value.radius === 'number' || MILES_REGEXP.test(value.radius)) { 100 | radius = typeof value.radius === 'number' ? value.radius : value.radius.match(MILES_REGEXP)[1] 101 | unitsPerDegree = 69.0 // miles per degree 102 | } else if (KILOMETERS_REGEXP.test(value.radius)) { 103 | radius = value.radius.match(KILOMETERS_REGEXP)[1] 104 | unitsPerDegree = 111.045 // kilometers per degree; 105 | } else { 106 | throw new Error('Unknown radius distance units') 107 | } 108 | 109 | let [latitudeColumn, longitudeColumn] = field.split(',').map((c) => c.trim()) 110 | let [latitude, longitude] = value.center 111 | 112 | // Uses indexes on `latitudeColumn` / `longitudeColumn` if available 113 | query = query 114 | .whereBetween(latitudeColumn, [ 115 | latitude - (radius / unitsPerDegree), 116 | latitude + (radius / unitsPerDegree) 117 | ]) 118 | .whereBetween(longitudeColumn, [ 119 | longitude - (radius / (unitsPerDegree * Math.cos(latitude * (Math.PI / 180)))), 120 | longitude + (radius / (unitsPerDegree * Math.cos(latitude * (Math.PI / 180)))) 121 | ]) 122 | 123 | if (value.calculateDistance) { 124 | let distanceColumn = (typeof value.calculateDistance === 'string') ? value.calculateDistance : 'distance' 125 | query = query.select(knex.raw(` 126 | ${unitsPerDegree} * DEGREES(ACOS( 127 | COS(RADIANS(?)) * COS(RADIANS(${latitudeColumn})) * 128 | COS(RADIANS(${longitudeColumn}) - RADIANS(?)) + 129 | SIN(RADIANS(?)) * SIN(RADIANS(${latitudeColumn})) 130 | )) AS ${distanceColumn}`, [latitude, longitude, latitude])) 131 | } 132 | return query 133 | } 134 | } 135 | 136 | Object.freeze(OPERATORS) 137 | 138 | /** 139 | * SqlAdapter class. 140 | * 141 | * @example 142 | * // Use Container instead of DataStore on the server 143 | * import { Container } from 'js-data'; 144 | * import SqlAdapter from 'js-data-sql'; 145 | * 146 | * // Create a store to hold your Mappers 147 | * const store = new Container(); 148 | * 149 | * // Create an instance of SqlAdapter with default settings 150 | * const adapter = new SqlAdapter(); 151 | * 152 | * // Mappers in "store" will use the Sql adapter by default 153 | * store.registerAdapter('sql', adapter, { default: true }); 154 | * 155 | * // Create a Mapper that maps to a "user" table 156 | * store.defineMapper('user'); 157 | * 158 | * @class SqlAdapter 159 | * @extends Adapter 160 | * @param {Object} [opts] Configuration options. 161 | * @param {boolean} [opts.debug=false] See {@link Adapter#debug}. 162 | * @param {Object} [opts.knexOpts] See {@link SqlAdapter#knexOpts}. 163 | * @param {Object} [opts.operators] See {@link SqlAdapter#operators}. 164 | * @param {boolean} [opts.raw=false] See {@link Adapter#raw}. 165 | */ 166 | export function SqlAdapter (opts) { 167 | utils.classCallCheck(this, SqlAdapter) 168 | opts || (opts = {}) 169 | opts.knexOpts || (opts.knexOpts = {}) 170 | utils.fillIn(opts, DEFAULTS) 171 | 172 | Object.defineProperty(this, 'knex', { 173 | writable: true, 174 | value: undefined 175 | }) 176 | 177 | Adapter.call(this, opts) 178 | 179 | /** 180 | * Override the default predicate functions for specified operators. 181 | * 182 | * @name SqlAdapter#operators 183 | * @type {Object} 184 | * @default {} 185 | */ 186 | this.knex || (this.knex = knex(this.knexOpts)) 187 | 188 | /** 189 | * Override the default predicate functions for specified operators. 190 | * 191 | * @name SqlAdapter#operators 192 | * @type {Object} 193 | * @default {} 194 | */ 195 | this.operators || (this.operators = {}) 196 | utils.fillIn(this.operators, OPERATORS) 197 | } 198 | 199 | function getTable (mapper) { 200 | return mapper.table || snakeCase(mapper.name) 201 | } 202 | 203 | /** 204 | * Alternative to ES2015 class syntax for extending `SqlAdapter`. 205 | * 206 | * @example Using the ES2015 class syntax. 207 | * class MySqlAdapter extends SqlAdapter {...}; 208 | * const adapter = new MySqlAdapter(); 209 | * 210 | * @example Using {@link SqlAdapter.extend}. 211 | * const instanceProps = {...}; 212 | * const classProps = {...}; 213 | * 214 | * const MySqlAdapter = SqlAdapter.extend(instanceProps, classProps); 215 | * const adapter = new MySqlAdapter(); 216 | * 217 | * @method SqlAdapter.extend 218 | * @static 219 | * @param {Object} [instanceProps] Properties that will be added to the 220 | * prototype of the subclass. 221 | * @param {Object} [classProps] Properties that will be added as static 222 | * properties to the subclass itself. 223 | * @return {Constructor} Subclass of `SqlAdapter`. 224 | */ 225 | SqlAdapter.extend = utils.extend 226 | 227 | /* 228 | function processRelationField (resourceConfig, query, field, criteria, options, joinedTables) { 229 | let fieldParts = field.split('.') 230 | let localResourceConfig = resourceConfig 231 | let relationPath = [] 232 | let relationName = null; 233 | 234 | while (fieldParts.length >= 2) { 235 | relationName = fieldParts.shift() 236 | let [relation] = localResourceConfig.relationList.filter(r => r.relation === relationName || r.localField === relationName) 237 | 238 | if (relation) { 239 | let relationResourceConfig = resourceConfig.getResource(relation.relation) 240 | relationPath.push(relation.relation) 241 | 242 | if (relation.type === 'belongsTo' || relation.type === 'hasOne') { 243 | // Apply table join for belongsTo/hasOne property (if not done already) 244 | if (!joinedTables.some(t => t === relationPath.join('.'))) { 245 | let table = getTable(localResourceConfig) 246 | let localId = `${table}.${relation.localKey}` 247 | 248 | let relationTable = getTable(relationResourceConfig) 249 | let foreignId = `${relationTable}.${relationResourceConfig.idAttribute}` 250 | 251 | query.leftJoin(relationTable, localId, foreignId) 252 | joinedTables.push(relationPath.join('.')) 253 | } 254 | } else if (relation.type === 'hasMany') { 255 | // Perform `WHERE EXISTS` subquery for hasMany property 256 | let existsParams = { 257 | [`${relationName}.${fieldParts.splice(0).join('.')}`]: criteria // remaining field(s) handled by EXISTS subquery 258 | }; 259 | let subQueryTable = getTable(relationResourceConfig); 260 | let subQueryOptions = deepMixIn({}, options, { query: knex(this.defaults).select(`${subQueryTable}.*`).from(subQueryTable) }) 261 | let subQuery = this.filterQuery(relationResourceConfig, existsParams, subQueryOptions) 262 | .whereRaw('??.??=??.??', [ 263 | getTable(relationResourceConfig), 264 | relation.foreignKey, 265 | getTable(localResourceConfig), 266 | localResourceConfig.idAttribute 267 | ]) 268 | if (Object.keys(criteria).some(k => k.indexOf('|') > -1)) { 269 | query.orWhereExists(subQuery); 270 | } else { 271 | query.whereExists(subQuery); 272 | } 273 | } 274 | 275 | localResourceConfig = relationResourceConfig 276 | } else { 277 | // hopefully a qualified local column 278 | } 279 | } 280 | relationName = fieldParts.shift(); 281 | 282 | return relationName ? `${getTable(localResourceConfig)}.${relationName}` : null; 283 | } 284 | 285 | function loadWithRelations (items, resourceConfig, options) { 286 | let tasks = [] 287 | let instance = Array.isArray(items) ? null : items 288 | 289 | if (resourceConfig.relationList) { 290 | resourceConfig.relationList.forEach(def => { 291 | let relationName = def.relation 292 | let relationDef = resourceConfig.getResource(relationName) 293 | 294 | let containedName = null 295 | if (contains(options.with, relationName)) { 296 | containedName = relationName 297 | } else if (contains(options.with, def.localField)) { 298 | containedName = def.localField 299 | } else { 300 | return 301 | } 302 | 303 | let __options = deepMixIn({}, options.orig ? options.orig() : options) 304 | 305 | // Filter to only properties under current relation 306 | __options.with = options.with.filter(relation => { 307 | return relation !== containedName && 308 | relation.indexOf(containedName) === 0 && 309 | relation.length >= containedName.length && 310 | relation[containedName.length] === '.' 311 | }).map(relation => relation.substr(containedName.length + 1)) 312 | 313 | let task 314 | 315 | if ((def.type === 'hasOne' || def.type === 'hasMany') && def.foreignKey) { 316 | task = this.findAll(resourceConfig.getResource(relationName), { 317 | where: { 318 | [def.foreignKey]: instance ? 319 | { '==': instance[def.localKey || resourceConfig.idAttribute] } : 320 | { 'in': items.map(item => item[def.localKey || resourceConfig.idAttribute]) } 321 | } 322 | }, __options).then(relatedItems => { 323 | if (instance) { 324 | if (def.type === 'hasOne' && relatedItems.length) { 325 | instance[def.localField] = relatedItems[0] 326 | } else { 327 | instance[def.localField] = relatedItems 328 | } 329 | } else { 330 | items.forEach(item => { 331 | let attached = relatedItems.filter(ri => ri[def.foreignKey] === item[def.localKey || resourceConfig.idAttribute]) 332 | if (def.type === 'hasOne' && attached.length) { 333 | item[def.localField] = attached[0] 334 | } else { 335 | item[def.localField] = attached 336 | } 337 | }) 338 | } 339 | 340 | return relatedItems 341 | }) 342 | } else if (def.type === 'hasMany' && def.localKeys) { 343 | // TODO: Write test for with: hasMany property with localKeys 344 | let localKeys = [] 345 | 346 | if (instance) { 347 | let itemKeys = instance[def.localKeys] || [] 348 | itemKeys = Array.isArray(itemKeys) ? itemKeys : Object.keys(itemKeys) 349 | localKeys = localKeys.concat(itemKeys || []) 350 | } else { 351 | items.forEach(item => { 352 | let itemKeys = item[def.localKeys] || [] 353 | itemKeys = Array.isArray(itemKeys) ? itemKeys : Object.keys(itemKeys) 354 | localKeys = localKeys.concat(itemKeys || []) 355 | }) 356 | } 357 | 358 | task = this.findAll(resourceConfig.getResource(relationName), { 359 | where: { 360 | [relationDef.idAttribute]: { 361 | 'in': filter(unique(localKeys), x => x) 362 | } 363 | } 364 | }, __options).then(relatedItems => { 365 | if (instance) { 366 | instance[def.localField] = relatedItems 367 | } else { 368 | items.forEach(item => { 369 | let itemKeys = item[def.localKeys] || [] 370 | let attached = relatedItems.filter(ri => itemKeys && contains(itemKeys, ri[relationDef.idAttribute])) 371 | item[def.localField] = attached 372 | }) 373 | } 374 | 375 | return relatedItems 376 | }) 377 | } else if (def.type === 'belongsTo' || (def.type === 'hasOne' && def.localKey)) { 378 | if (instance) { 379 | let id = get(instance, def.localKey) 380 | if (id) { 381 | task = this.findAll(resourceConfig.getResource(relationName), { 382 | where: { 383 | [def.foreignKey || relationDef.idAttribute]: { '==': id } 384 | } 385 | }, __options).then(relatedItems => { 386 | let relatedItem = relatedItems && relatedItems[0]; 387 | instance[def.localField] = relatedItem 388 | return relatedItem 389 | }) 390 | } 391 | } else { 392 | let ids = items.map(item => get(item, def.localKey)).filter(x => x) 393 | if (ids.length) { 394 | task = this.findAll(resourceConfig.getResource(relationName), { 395 | where: { 396 | [def.foreignKey || relationDef.idAttribute]: { 'in': ids } 397 | } 398 | }, __options).then(relatedItems => { 399 | items.forEach(item => { 400 | relatedItems.forEach(relatedItem => { 401 | if (relatedItem[def.foreignKey || relationDef.idAttribute] === item[def.localKey]) { 402 | item[def.localField] = relatedItem 403 | } 404 | }) 405 | }) 406 | return relatedItems 407 | }) 408 | } 409 | } 410 | } 411 | 412 | if (task) { 413 | tasks.push(task) 414 | } 415 | }) 416 | } 417 | return Promise.all(tasks) 418 | } 419 | */ 420 | 421 | Adapter.extend({ 422 | constructor: SqlAdapter, 423 | 424 | _count (mapper, query, opts) { 425 | opts || (opts = {}) 426 | query || (query = {}) 427 | 428 | const sqlBuilder = utils.isUndefined(opts.transaction) ? this.knex : opts.transaction 429 | return this.filterQuery(sqlBuilder(getTable(mapper)), query, opts) 430 | .count('* as count') 431 | .then((rows) => [rows[0].count, {}]) 432 | }, 433 | 434 | _create (mapper, props, opts) { 435 | const idAttribute = mapper.idAttribute 436 | props || (props = {}) 437 | opts || (opts = {}) 438 | 439 | const sqlBuilder = utils.isUndefined(opts.transaction) ? this.knex : opts.transaction 440 | return sqlBuilder(getTable(mapper)) 441 | .insert(props, idAttribute) 442 | .then((ids) => { 443 | const id = utils.isUndefined(props[idAttribute]) ? (ids.length ? ids[0] : undefined) : props[idAttribute] 444 | if (utils.isUndefined(id)) { 445 | throw new Error('Failed to create!') 446 | } 447 | return this._find(mapper, id, opts).then((result) => [result[0], { ids }]) 448 | }) 449 | }, 450 | 451 | _createMany (mapper, props, opts) { 452 | props || (props = {}) 453 | opts || (opts = {}) 454 | 455 | const tasks = props.map((record) => this._create(mapper, record, opts)) 456 | return Promise.all(tasks).then((results) => [results.map((result) => result[0]), {}]) 457 | }, 458 | 459 | _destroy (mapper, id, opts) { 460 | opts || (opts = {}) 461 | 462 | const sqlBuilder = utils.isUndefined(opts.transaction) ? this.knex : opts.transaction 463 | return sqlBuilder(getTable(mapper)) 464 | .where(mapper.idAttribute, toString(id)) 465 | .del() 466 | .then(() => [undefined, {}]) 467 | }, 468 | 469 | _destroyAll (mapper, query, opts) { 470 | query || (query = {}) 471 | opts || (opts = {}) 472 | 473 | const sqlBuilder = utils.isUndefined(opts.transaction) ? this.knex : opts.transaction 474 | return this.filterQuery(sqlBuilder(getTable(mapper)), query, opts) 475 | .del() 476 | .then(() => [undefined, {}]) 477 | }, 478 | 479 | _find (mapper, id, opts) { 480 | opts || (opts = {}) 481 | 482 | const sqlBuilder = utils.isUndefined(opts.transaction) ? this.knex : opts.transaction 483 | const table = getTable(mapper) 484 | return sqlBuilder 485 | .select(`${table}.*`) 486 | .from(table) 487 | .where(`${table}.${mapper.idAttribute}`, toString(id)) 488 | .then((rows) => { 489 | if (!rows || !rows.length) { 490 | return [undefined, {}] 491 | } 492 | return [rows[0], {}] 493 | }) 494 | }, 495 | 496 | _findAll (mapper, query, opts) { 497 | query || (query = {}) 498 | opts || (opts = {}) 499 | 500 | return this.filterQuery(this.selectTable(mapper, opts), query, opts).then((rows) => [rows || [], {}]) 501 | }, 502 | 503 | _sum (mapper, field, query, opts) { 504 | if (!utils.isString(field)) { 505 | throw new Error('field must be a string!') 506 | } 507 | opts || (opts = {}) 508 | query || (query = {}) 509 | 510 | const sqlBuilder = utils.isUndefined(opts.transaction) ? this.knex : opts.transaction 511 | return this.filterQuery(sqlBuilder(getTable(mapper)), query, opts) 512 | .sum(`${field} as sum`) 513 | .then((rows) => [rows[0].sum || 0, {}]) 514 | }, 515 | 516 | _update (mapper, id, props, opts) { 517 | props || (props = {}) 518 | opts || (opts = {}) 519 | 520 | const sqlBuilder = utils.isUndefined(opts.transaction) ? this.knex : opts.transaction 521 | return sqlBuilder(getTable(mapper)) 522 | .where(mapper.idAttribute, toString(id)) 523 | .update(props) 524 | .then(() => this._find(mapper, id, opts)) 525 | .then((result) => { 526 | if (!result[0]) { 527 | throw new Error('Not Found') 528 | } 529 | return result 530 | }) 531 | }, 532 | 533 | _updateAll (mapper, props, query, opts) { 534 | const idAttribute = mapper.idAttribute 535 | props || (props = {}) 536 | query || (query = {}) 537 | opts || (opts = {}) 538 | 539 | let ids 540 | 541 | return this._findAll(mapper, query, opts).then((result) => { 542 | const records = result[0] 543 | ids = records.map((record) => record[idAttribute]) 544 | const sqlBuilder = utils.isUndefined(opts.transaction) ? this.knex : opts.transaction 545 | return this.filterQuery(sqlBuilder(getTable(mapper)), query, opts).update(props) 546 | }).then(() => { 547 | const _query = { where: {} } 548 | _query.where[idAttribute] = { 'in': ids } 549 | return this._findAll(mapper, _query, opts) 550 | }) 551 | }, 552 | 553 | _updateMany (mapper, records, opts) { 554 | const idAttribute = mapper.idAttribute 555 | records || (records = []) 556 | opts || (opts = {}) 557 | 558 | const tasks = records.map((record) => this._update(mapper, record[idAttribute], record, opts)) 559 | return Promise.all(tasks).then((results) => [results.map((result) => result[0]), {}]) 560 | }, 561 | 562 | applyWhereFromObject (sqlBuilder, where, opts) { 563 | utils.forOwn(where, (criteria, field) => { 564 | if (!utils.isObject(criteria)) { 565 | criteria = { '==': criteria } 566 | } 567 | // Apply filter for each operator 568 | utils.forOwn(criteria, (value, operator) => { 569 | let isOr = false 570 | if (operator && operator[0] === '|') { 571 | operator = operator.substr(1) 572 | isOr = true 573 | } 574 | let predicateFn = this.getOperator(operator, opts) 575 | if (predicateFn) { 576 | sqlBuilder = predicateFn(sqlBuilder, field, value, isOr) 577 | } else { 578 | throw new Error(`Operator ${operator} not supported!`) 579 | } 580 | }) 581 | }) 582 | return sqlBuilder 583 | }, 584 | 585 | applyWhereFromArray (sqlBuilder, where, opts) { 586 | where.forEach((_where, i) => { 587 | if (_where === 'and' || _where === 'or') { 588 | return 589 | } 590 | const self = this 591 | const prev = where[i - 1] 592 | const parser = utils.isArray(_where) ? this.applyWhereFromArray : this.applyWhereFromObject 593 | if (prev) { 594 | if (prev === 'or') { 595 | sqlBuilder = sqlBuilder.orWhere(function () { 596 | parser.call(self, this, _where, opts) 597 | }) 598 | } else { 599 | sqlBuilder = sqlBuilder.andWhere(function () { 600 | parser.call(self, this, _where, opts) 601 | }) 602 | } 603 | } else { 604 | sqlBuilder = sqlBuilder.where(function () { 605 | parser.call(self, this, _where, opts) 606 | }) 607 | } 608 | }) 609 | return sqlBuilder 610 | }, 611 | 612 | filterQuery (sqlBuilder, query, opts) { 613 | query = utils.plainCopy(query || {}) 614 | opts || (opts = {}) 615 | opts.operators || (opts.operators = {}) 616 | query.where || (query.where = {}) 617 | query.orderBy || (query.orderBy = query.sort) 618 | query.orderBy || (query.orderBy = []) 619 | query.skip || (query.skip = query.offset) 620 | 621 | // Transform non-keyword properties to "where" clause configuration 622 | utils.forOwn(query, (config, keyword) => { 623 | if (reserved.indexOf(keyword) === -1) { 624 | if (utils.isObject(config)) { 625 | query.where[keyword] = config 626 | } else { 627 | query.where[keyword] = { 628 | '==': config 629 | } 630 | } 631 | delete query[keyword] 632 | } 633 | }) 634 | 635 | // Filter 636 | if (utils.isObject(query.where) && Object.keys(query.where).length !== 0) { 637 | // Apply filter for each field 638 | sqlBuilder = this.applyWhereFromObject(sqlBuilder, query.where, opts) 639 | } else if (utils.isArray(query.where)) { 640 | sqlBuilder = this.applyWhereFromArray(sqlBuilder, query.where, opts) 641 | } 642 | 643 | // Sort 644 | if (query.orderBy) { 645 | if (utils.isString(query.orderBy)) { 646 | query.orderBy = [ 647 | [query.orderBy, 'asc'] 648 | ] 649 | } 650 | for (var i = 0; i < query.orderBy.length; i++) { 651 | if (utils.isString(query.orderBy[i])) { 652 | query.orderBy[i] = [query.orderBy[i], 'asc'] 653 | } 654 | sqlBuilder = sqlBuilder.orderBy(query.orderBy[i][0], (query.orderBy[i][1] || '').toUpperCase() === 'DESC' ? 'desc' : 'asc') 655 | } 656 | } 657 | 658 | // Offset 659 | if (query.skip) { 660 | sqlBuilder = sqlBuilder.offset(+query.skip) 661 | } 662 | 663 | // Limit 664 | if (query.limit) { 665 | sqlBuilder = sqlBuilder.limit(+query.limit) 666 | } 667 | 668 | return sqlBuilder 669 | // if (!isEmpty(params.where)) { 670 | // forOwn(params.where, (criteria, field) => { 671 | // if (contains(field, '.')) { 672 | // if (contains(field, ',')) { 673 | // let splitFields = field.split(',').map(c => c.trim()) 674 | // field = splitFields.map(splitField => processRelationField.call(this, resourceConfig, query, splitField, criteria, options, joinedTables)).join(',') 675 | // } else { 676 | // field = processRelationField.call(this, resourceConfig, query, field, criteria, options, joinedTables) 677 | // } 678 | // } 679 | // }) 680 | // } 681 | }, 682 | 683 | /** 684 | * Resolve the predicate function for the specified operator based on the 685 | * given options and this adapter's settings. 686 | * 687 | * @name SqlAdapter#getOperator 688 | * @method 689 | * @param {string} operator The name of the operator. 690 | * @param {Object} [opts] Configuration options. 691 | * @param {Object} [opts.operators] Override the default predicate functions 692 | * for specified operators. 693 | * @return {*} The predicate function for the specified operator. 694 | */ 695 | getOperator (operator, opts) { 696 | opts || (opts = {}) 697 | opts.operators || (opts.operators = {}) 698 | let ownOps = this.operators || {} 699 | return utils.isUndefined(opts.operators[operator]) ? ownOps[operator] : opts.operators[operator] 700 | }, 701 | 702 | getTable (mapper) { 703 | return mapper.table || snakeCase(mapper.name) 704 | }, 705 | 706 | selectTable (mapper, opts) { 707 | opts || (opts = {}) 708 | const query = utils.isUndefined(opts.query) ? this.knex : opts.query 709 | const table = this.getTable(mapper) 710 | return query.select(`${table}.*`).from(table) 711 | } 712 | }) 713 | 714 | /** 715 | * Details of the current version of the `js-data-sql` module. 716 | * 717 | * @example 718 | * import { version } from 'js-data-sql'; 719 | * console.log(version.full); 720 | * 721 | * @name module:js-data-sql.version 722 | * @type {object} 723 | * @property {string} version.full The full semver value. 724 | * @property {number} version.major The major version number. 725 | * @property {number} version.minor The minor version number. 726 | * @property {number} version.patch The patch version number. 727 | * @property {(string|boolean)} version.alpha The alpha version value, 728 | * otherwise `false` if the current version is not alpha. 729 | * @property {(string|boolean)} version.beta The beta version value, 730 | * otherwise `false` if the current version is not beta. 731 | */ 732 | export const version = '<%= version %>' 733 | 734 | /** 735 | * {@link SqlAdapter} class. 736 | * 737 | * @example CommonJS 738 | * const SqlAdapter = require('js-data-sql').SqlAdapter; 739 | * const adapter = new SqlAdapter(); 740 | * 741 | * @example ES2015 Modules 742 | * import { SqlAdapter } from 'js-data-sql'; 743 | * const adapter = new SqlAdapter(); 744 | * 745 | * @name module:js-data-sql.SqlAdapter 746 | * @see SqlAdapter 747 | * @type {Constructor} 748 | */ 749 | 750 | /** 751 | * Registered as `js-data-sql` in NPM. 752 | * 753 | * @example Install from NPM (for use with MySQL) 754 | * npm i --save js-data-sql js-data mysql 755 | * 756 | * @example Load via CommonJS 757 | * const SqlAdapter = require('js-data-sql').SqlAdapter; 758 | * const adapter = new SqlAdapter(); 759 | * 760 | * @example Load via ES2015 Modules 761 | * import { SqlAdapter } from 'js-data-sql'; 762 | * const adapter = new SqlAdapter(); 763 | * 764 | * @module js-data-sql 765 | */ 766 | 767 | /** 768 | * Create a subclass of this SqlAdapter: 769 | * @example SqlAdapter.extend 770 | * // Normally you would do: import { SqlAdapter } from 'js-data-sql'; 771 | * const JSDataSql = require('js-data-sql'); 772 | * const { SqlAdapter } = JSDataSql; 773 | * console.log('Using JSDataSql v' + JSDataSql.version.full); 774 | * 775 | * // Extend the class using ES2015 class syntax. 776 | * class CustomSqlAdapterClass extends SqlAdapter { 777 | * foo () { return 'bar'; } 778 | * static beep () { return 'boop'; } 779 | * } 780 | * const customSqlAdapter = new CustomSqlAdapterClass(); 781 | * console.log(customSqlAdapter.foo()); 782 | * console.log(CustomSqlAdapterClass.beep()); 783 | * 784 | * // Extend the class using alternate method. 785 | * const OtherSqlAdapterClass = SqlAdapter.extend({ 786 | * foo () { return 'bar'; } 787 | * }, { 788 | * beep () { return 'boop'; } 789 | * }); 790 | * const otherSqlAdapter = new OtherSqlAdapterClass(); 791 | * console.log(otherSqlAdapter.foo()); 792 | * console.log(OtherSqlAdapterClass.beep()); 793 | * 794 | * // Extend the class, providing a custom constructor. 795 | * function AnotherSqlAdapterClass () { 796 | * SqlAdapter.call(this); 797 | * this.created_at = new Date().getTime(); 798 | * } 799 | * SqlAdapter.extend({ 800 | * constructor: AnotherSqlAdapterClass, 801 | * foo () { return 'bar'; } 802 | * }, { 803 | * beep () { return 'boop'; } 804 | * }); 805 | * const anotherSqlAdapter = new AnotherSqlAdapterClass(); 806 | * console.log(anotherSqlAdapter.created_at); 807 | * console.log(anotherSqlAdapter.foo()); 808 | * console.log(AnotherSqlAdapterClass.beep()); 809 | * 810 | * @method SqlAdapter.extend 811 | * @param {object} [props={}] Properties to add to the prototype of the 812 | * subclass. 813 | * @param {object} [props.constructor] Provide a custom constructor function 814 | * to be used as the subclass itself. 815 | * @param {object} [classProps={}] Static properties to add to the subclass. 816 | * @returns {Constructor} Subclass of this SqlAdapter class. 817 | * @since 3.0.0 818 | */ 819 | -------------------------------------------------------------------------------- /test/create_trx.spec.js: -------------------------------------------------------------------------------- 1 | describe('SqlAdapter#create + transaction', function () { 2 | var adapter, User 3 | 4 | beforeEach(function () { 5 | adapter = this.$$adapter 6 | User = this.$$User 7 | }) 8 | 9 | it('commit should persist created user in a sql db', function () { 10 | var id 11 | 12 | return adapter.knex.transaction((trx) => { 13 | return adapter.create(User, { name: 'Jane' }, { transaction: trx }) 14 | }) 15 | .then((user) => { 16 | id = user.id 17 | assert.equal(user.name, 'Jane') 18 | assert.isDefined(user.id) 19 | }) 20 | .then(() => { 21 | return adapter.find(User, id) 22 | }) 23 | .then((user) => { 24 | assert.isObject(user, 'user committed to database') 25 | assert.equal(user.name, 'Jane') 26 | assert.isDefined(user.id) 27 | assert.equalObjects(user, { id: id, name: 'Jane', age: null, addressId: null }) 28 | }) 29 | }) 30 | 31 | it('rollback should not persist created user in a sql db', function () { 32 | var id 33 | 34 | return adapter.knex.transaction((trx) => { 35 | return adapter.create(User, { name: 'John' }, { transaction: trx }) 36 | .then((user) => { 37 | id = user.id 38 | assert.equal(user.name, 'John') 39 | assert.isDefined(user.id) 40 | 41 | throw new Error('rollback') 42 | }) 43 | }) 44 | .then(() => { 45 | throw new Error('should not have reached this!') 46 | }, (err) => { 47 | assert.equal(err.message, 'rollback') 48 | return adapter.find(User, id) 49 | }) 50 | .then((user) => { 51 | assert.equal(user, undefined, 'user should not have been commited to the database') 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/destroy_trx.spec.js: -------------------------------------------------------------------------------- 1 | describe('SqlAdapter#destroy + transaction', function () { 2 | var adapter, User 3 | 4 | beforeEach(function () { 5 | adapter = this.$$adapter 6 | User = this.$$User 7 | }) 8 | 9 | it('commit should destroy a user from a Sql db', function () { 10 | var id 11 | 12 | return adapter.create(User, { name: 'John' }) 13 | .then((user) => { 14 | assert.isObject(user) 15 | id = user.id 16 | return adapter.knex.transaction((trx) => { 17 | return adapter.destroy(User, id, { transaction: trx }) 18 | }) 19 | }) 20 | .then(() => { 21 | return adapter.find(User, id) 22 | }) 23 | .then((user) => { 24 | assert.equal(user, undefined, 'user should have been destroyed') 25 | }) 26 | }) 27 | 28 | it('rollback should not destroy a user from a Sql db', function () { 29 | var id 30 | 31 | return adapter.create(User, { name: 'John' }) 32 | .then((user) => { 33 | assert.isObject(user) 34 | id = user.id 35 | return adapter.knex.transaction((trx) => { 36 | return adapter.destroy(User, id, { transaction: trx }) 37 | .then(() => { 38 | throw new Error('rollback') 39 | }) 40 | }) 41 | }) 42 | .then(() => { 43 | throw new Error('should not have reached this') 44 | }, (err) => { 45 | assert.equal(err.message, 'rollback') 46 | return adapter.find(User, id) 47 | }) 48 | .then((user) => { 49 | assert.isObject(user, 'user still exists') 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /test/filterQuery.spec.js: -------------------------------------------------------------------------------- 1 | describe('DSSqlAdapter#filterQuery', function () { 2 | var adapter 3 | beforeEach(function () { 4 | adapter = this.$$adapter 5 | }) 6 | it('should use custom query', function () { 7 | var query = adapter.knex.from('test') 8 | var filterQuery = adapter.filterQuery(query) 9 | var expectedQuery = adapter.knex 10 | .from('test') 11 | 12 | assert.equal(filterQuery.toString(), expectedQuery.toString()) 13 | }) 14 | it('should apply where from params to query', function () { 15 | var query = adapter.knex.from('test') 16 | var filterQuery = adapter.filterQuery(query, { name: 'Sean' }) 17 | var expectedQuery = adapter.knex 18 | .from('test') 19 | .where({name: 'Sean'}) 20 | 21 | assert.equal(filterQuery.toString(), expectedQuery.toString()) 22 | }) 23 | it('should apply limit from params to custom query', function () { 24 | var query = adapter.knex.from('test') 25 | var filterQuery = adapter.filterQuery(query, { limit: 2 }) 26 | var expectedQuery = adapter.knex 27 | .from('test') 28 | .limit(2) 29 | 30 | assert.equal(filterQuery.toString(), expectedQuery.toString()) 31 | }) 32 | it('should apply order from params to custom query', function () { 33 | var query = adapter.knex.from('test') 34 | var filterQuery = adapter.filterQuery(query, { orderBy: 'name' }) 35 | var expectedQuery = adapter.knex 36 | .from('test') 37 | .orderBy('name', 'asc') 38 | 39 | assert.equal(filterQuery.toString(), expectedQuery.toString()) 40 | }) 41 | it('should convert == null to IS NULL', function () { 42 | var query = adapter.knex.from('test') 43 | var filterQuery = adapter.filterQuery(query, { name: { '==': null } }) 44 | var expectedQuery = adapter.knex 45 | .from('test') 46 | .whereNull('name') 47 | 48 | assert.equal(filterQuery.toString(), expectedQuery.toString()) 49 | }) 50 | it('should convert != null to IS NOT NULL', function () { 51 | var query = adapter.knex.from('test') 52 | var filterQuery = adapter.filterQuery(query, { name: { '!=': null } }) 53 | var expectedQuery = adapter.knex 54 | .from('test') 55 | .whereNotNull('name') 56 | 57 | assert.equal(filterQuery.toString(), expectedQuery.toString()) 58 | }) 59 | it('should convert |== null to OR field IS NULL', function () { 60 | var query = adapter.knex.from('test') 61 | var filterQuery = adapter.filterQuery(query, { name: 'Sean', age: { '|==': null } }) 62 | var expectedQuery = adapter.knex 63 | .from('test') 64 | .where('name', 'Sean') 65 | .orWhereNull('age') 66 | 67 | assert.equal(filterQuery.toString(), expectedQuery.toString()) 68 | }) 69 | it('should convert |!= null to OR field IS NOT NULL', function () { 70 | var query = adapter.knex.from('test') 71 | var filterQuery = adapter.filterQuery(query, { name: 'Sean', age: { '|!=': null } }) 72 | var expectedQuery = adapter.knex 73 | .from('test') 74 | .where('name', 'Sean') 75 | .orWhereNotNull('age') 76 | 77 | assert.equal(filterQuery.toString(), expectedQuery.toString()) 78 | }) 79 | it('should apply query from array', function () { 80 | var query = {} 81 | var sql = adapter.filterQuery(adapter.knex('user'), query).toString() 82 | assert.deepEqual(sql, 'select * from `user`') 83 | 84 | query = { 85 | age: 30 86 | } 87 | sql = adapter.filterQuery(adapter.knex('user'), query).toString() 88 | assert.deepEqual(sql, 'select * from `user` where `age` = 30') 89 | 90 | query = { 91 | age: 30, 92 | role: 'admin' 93 | } 94 | sql = adapter.filterQuery(adapter.knex('user'), query).toString() 95 | assert.deepEqual(sql, 'select * from `user` where `age` = 30 and `role` = \'admin\'') 96 | 97 | query = { 98 | role: 'admin', 99 | age: 30 100 | } 101 | sql = adapter.filterQuery(adapter.knex('user'), query).toString() 102 | assert.deepEqual(sql, 'select * from `user` where `role` = \'admin\' and `age` = 30') 103 | 104 | query = { 105 | role: 'admin', 106 | age: 30, 107 | skip: 10, 108 | limit: 5, 109 | orderBy: [ 110 | ['role', 'desc'], 111 | ['age'] 112 | ] 113 | } 114 | sql = adapter.filterQuery(adapter.knex('user'), query).toString() 115 | assert.deepEqual(sql, 'select * from `user` where `role` = \'admin\' and `age` = 30 order by `role` desc, `age` asc limit 5 offset 10') 116 | 117 | query = { 118 | where: { 119 | role: { 120 | '=': 'admin' 121 | }, 122 | age: { 123 | '=': 30 124 | } 125 | } 126 | } 127 | sql = adapter.filterQuery(adapter.knex('user'), query).toString() 128 | assert.deepEqual(sql, 'select * from `user` where `role` = \'admin\' and `age` = 30') 129 | 130 | query = { 131 | where: [ 132 | { 133 | role: { 134 | '=': 'admin' 135 | } 136 | }, 137 | { 138 | age: { 139 | '=': 30 140 | } 141 | } 142 | ] 143 | } 144 | sql = adapter.filterQuery(adapter.knex('user'), query).toString() 145 | assert.deepEqual(sql, 'select * from `user` where (`role` = \'admin\') and (`age` = 30)') 146 | 147 | query = { 148 | where: [ 149 | [ 150 | { 151 | role: { 152 | '=': 'admin' 153 | }, 154 | age: { 155 | '=': 30 156 | } 157 | }, 158 | 'or', 159 | { 160 | name: { 161 | '=': 'John' 162 | } 163 | } 164 | ], 165 | 'or', 166 | { 167 | role: { 168 | '=': 'dev' 169 | }, 170 | age: { 171 | '=': 22 172 | } 173 | } 174 | ] 175 | } 176 | sql = adapter.filterQuery(adapter.knex('user'), query).toString() 177 | assert.deepEqual(sql, 'select * from `user` where ((`role` = \'admin\' and `age` = 30) or (`name` = \'John\')) or (`role` = \'dev\' and `age` = 22)') 178 | }) 179 | describe('Custom/override query operators', function () { 180 | it('should use custom query operator if provided', function () { 181 | var query = adapter.knex 182 | .from('user') 183 | .select('user.*') 184 | adapter.operators.equals = (sql, field, value) => sql.where(field, value) 185 | var filterQuery = adapter.filterQuery(query, { name: { equals: 'Sean' } }) 186 | var expectedQuery = adapter.knex 187 | .from('user') 188 | .select('user.*') 189 | .where('name', 'Sean') 190 | 191 | assert.equal(filterQuery.toString(), expectedQuery.toString()) 192 | }) 193 | it('should override built-in operator with custom query operator', function () { 194 | var query = adapter.knex 195 | .from('user') 196 | .select('user.*') 197 | adapter.operators['=='] = (query, field, value) => query.where(field, '!=', value) 198 | var filterQuery = adapter.filterQuery(query, { name: { '==': 'Sean' } }) 199 | var expectedQuery = adapter.knex 200 | .from('user') 201 | .select('user.*') 202 | .where('name', '!=', 'Sean') 203 | 204 | assert.equal(filterQuery.toString(), expectedQuery.toString()) 205 | }) 206 | }) 207 | }) 208 | -------------------------------------------------------------------------------- /test/findAll.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | describe('DSSqlAdapter#findAll', function () { 4 | var adapter, User, Address, Profile, Comment, Post 5 | 6 | beforeEach(function () { 7 | adapter = this.$$adapter 8 | User = this.$$User 9 | Address = this.$$Address 10 | Profile = this.$$Profile 11 | Comment = this.$$Comment 12 | Post = this.$$Post 13 | }) 14 | it('should not return relation columns on parent', function * () { 15 | let profile1 = yield adapter.create(Profile, { email: 'foo@test.com' }) 16 | yield adapter.create(User, { name: 'John', profileId: profile1.id }) 17 | 18 | let users = yield adapter.findAll(User, { 'profile.email': 'foo@test.com' }) 19 | assert.equal(users.length, 1) 20 | assert.equal(users[0].profileId, profile1.id) 21 | assert.isUndefined(users[0].email) 22 | }) 23 | 24 | it('should filter when relations have same column if column is qualified', function * () { 25 | let profile1 = yield adapter.create(Profile, { email: 'foo@test.com' }) 26 | let user1 = yield adapter.create(User, { 27 | name: 'John', 28 | profileId: profile1.id 29 | }) 30 | 31 | // `id` column must be qualified with `user.` 32 | let users = yield adapter.findAll(User, { 33 | 'user.id': user1.id, 34 | 'profile.email': 'foo@test.com' 35 | }) 36 | assert.equal(users.length, 1) 37 | assert.equal(users[0].profileId, profile1.id) 38 | }) 39 | 40 | it('should filter using the "like" operator', function * () { 41 | let user1 = yield adapter.create(User, { name: 'Sean' }) 42 | yield adapter.create(Post, { userId: user1.id, content: 'foo' }) 43 | yield adapter.create(Post, { userId: user1.id, content: 'bar' }) 44 | yield adapter.create(Post, { userId: user1.id, content: 'baz' }) 45 | 46 | let posts = yield adapter.findAll(Post, { 47 | where: { content: { like: 'ba%' } } 48 | }) 49 | assert.equal(posts.length, 2) 50 | assert.equal(posts[0].content, 'bar') 51 | assert.equal(posts[1].content, 'baz') 52 | }) 53 | 54 | it('should filter using the "or like" operator', function * () { 55 | let user1 = yield adapter.create(User, { name: 'Sean' }) 56 | yield adapter.create(Post, { userId: user1.id, content: 'foo' }) 57 | yield adapter.create(Post, { userId: user1.id, content: 'bar' }) 58 | yield adapter.create(Post, { userId: user1.id, content: 'baz' }) 59 | 60 | let posts = yield adapter.findAll(Post, { 61 | where: { content: { like: 'ba%', '|like': 'f%' } } 62 | }) 63 | assert.equal(posts.length, 3) 64 | assert.equal(posts[0].content, 'foo') 65 | assert.equal(posts[1].content, 'bar') 66 | assert.equal(posts[2].content, 'baz') 67 | }) 68 | 69 | it("should filter using a hasMany relation's name", function * () { 70 | let user1 = yield adapter.create(User, { name: 'Sean' }) 71 | yield adapter.create(Post, { userId: user1.id, content: 'foo' }) 72 | yield adapter.create(Post, { userId: user1.id, content: 'bar' }) 73 | yield adapter.create(Post, { userId: user1.id, content: 'baz' }) 74 | 75 | let user2 = yield adapter.create(User, { name: 'Jason' }) 76 | yield adapter.create(Post, { userId: user2.id, content: 'foo' }) 77 | yield adapter.create(Post, { userId: user2.id, content: 'bar' }) 78 | 79 | let user3 = yield adapter.create(User, { name: 'Ed' }) 80 | yield adapter.create(Post, { userId: user3.id, content: 'bar' }) 81 | yield adapter.create(Post, { userId: user3.id, content: 'baz' }) 82 | 83 | let users = yield adapter.findAll(User, { 84 | where: { 'post.content': { '==': 'foo' } }, 85 | orderBy: 'name' 86 | }) 87 | assert.equal(users.length, 2) 88 | assert.equal(users[0].name, 'Jason') 89 | assert.equal(users[1].name, 'Sean') 90 | }) 91 | 92 | it("should filter using a hasMany relation's localField", function * () { 93 | let user1 = yield adapter.create(User, { name: 'Sean' }) 94 | yield adapter.create(Post, { userId: user1.id, content: 'foo' }) 95 | yield adapter.create(Post, { userId: user1.id, content: 'bar' }) 96 | yield adapter.create(Post, { userId: user1.id, content: 'baz' }) 97 | 98 | let user2 = yield adapter.create(User, { name: 'Jason' }) 99 | yield adapter.create(Post, { userId: user2.id, content: 'foo' }) 100 | yield adapter.create(Post, { userId: user2.id, content: 'bar' }) 101 | 102 | let user3 = yield adapter.create(User, { name: 'Ed' }) 103 | yield adapter.create(Post, { userId: user3.id, content: 'bar' }) 104 | yield adapter.create(Post, { userId: user3.id, content: 'baz' }) 105 | 106 | let users = yield adapter.findAll(User, { 107 | where: { 'posts.content': { '==': 'foo' } }, 108 | orderBy: 'name' 109 | }) 110 | assert.equal(users.length, 2) 111 | assert.equal(users[0].name, 'Jason') 112 | assert.equal(users[1].name, 'Sean') 113 | }) 114 | 115 | it('should filter through a hasMany relation', function * () { 116 | let user1 = yield adapter.create(User, { name: 'Sean' }) 117 | let user2 = yield adapter.create(User, { name: 'Jason' }) 118 | let user3 = yield adapter.create(User, { name: 'Ed' }) 119 | 120 | let post1 = yield adapter.create(Post, { 121 | userId: user1.id, 122 | content: 'post1' 123 | }) 124 | yield adapter.create(Comment, { 125 | userId: user1.id, 126 | postId: post1.id, 127 | content: 'comment1_1' 128 | }) 129 | yield adapter.create(Comment, { 130 | userId: user2.id, 131 | postId: post1.id, 132 | content: 'comment1_2' 133 | }) 134 | yield adapter.create(Comment, { 135 | userId: user3.id, 136 | postId: post1.id, 137 | content: 'comment1_3' 138 | }) 139 | 140 | let post2 = yield adapter.create(Post, { 141 | userId: user1.id, 142 | content: 'post2' 143 | }) 144 | yield adapter.create(Comment, { 145 | userId: user2.id, 146 | postId: post2.id, 147 | content: 'comment1_2' 148 | }) 149 | yield adapter.create(Comment, { 150 | userId: user3.id, 151 | postId: post2.id, 152 | content: 'comment1_3' 153 | }) 154 | 155 | let post3 = yield adapter.create(Post, { 156 | userId: user1.id, 157 | content: 'post3' 158 | }) 159 | yield adapter.create(Comment, { 160 | userId: user1.id, 161 | postId: post3.id, 162 | content: 'comment1_1' 163 | }) 164 | yield adapter.create(Comment, { 165 | userId: user3.id, 166 | postId: post3.id, 167 | content: 'comment1_3' 168 | }) 169 | 170 | let posts = yield adapter.findAll(Post, { 171 | where: { 'comments.user.name': { '==': 'Sean' } }, 172 | orderBy: 'content' 173 | }) 174 | assert.equal(posts.length, 2) 175 | assert.equal(posts[0].content, 'post1') 176 | assert.equal(posts[1].content, 'post3') 177 | }) 178 | 179 | describe('near', function () { 180 | beforeEach(function * () { 181 | this.googleAddress = yield adapter.create(Address, { 182 | name: 'Google', 183 | latitude: 37.4219999, 184 | longitude: -122.0862515 185 | }) 186 | this.appleAddress = yield adapter.create(Address, { 187 | name: 'Apple', 188 | latitude: 37.331852, 189 | longitude: -122.029599 190 | }) 191 | this.microsoftAddress = yield adapter.create(Address, { 192 | name: 'Microsoft', 193 | latitude: 47.639649, 194 | longitude: -122.128255 195 | }) 196 | this.amazonAddress = yield adapter.create(Address, { 197 | name: 'Amazon', 198 | latitude: 47.622915, 199 | longitude: -122.336384 200 | }) 201 | }) 202 | 203 | it('should filter using "near"', function * () { 204 | let addresses = yield adapter.findAll(Address, { 205 | where: { 206 | 'latitude,longitude': { 207 | near: { 208 | center: [37.41, -122.06], 209 | radius: 10 210 | } 211 | } 212 | } 213 | }) 214 | assert.equal(addresses.length, 2) 215 | assert.equal(addresses[0].name, 'Google') 216 | assert.equal(addresses[1].name, 'Apple') 217 | }) 218 | 219 | it('should not contain distance column by default', function * () { 220 | let addresses = yield adapter.findAll(Address, { 221 | where: { 222 | 'latitude,longitude': { 223 | near: { 224 | center: [37.41, -122.06], 225 | radius: 5 226 | } 227 | } 228 | } 229 | }) 230 | assert.equal(addresses.length, 1) 231 | assert.equal(addresses[0].name, 'Google') 232 | assert.equal(addresses[0].distance, undefined) 233 | }) 234 | 235 | it('should contain distance column if "calculateDistance" is truthy', function * () { 236 | let addresses = yield adapter.findAll(Address, { 237 | where: { 238 | 'latitude,longitude': { 239 | near: { 240 | center: [37.41, -122.06], 241 | radius: 10, 242 | calculateDistance: true 243 | } 244 | } 245 | } 246 | }) 247 | assert.equal(addresses.length, 2) 248 | 249 | assert.equal(addresses[0].name, 'Google') 250 | assert.isNotNull(addresses[0].distance) 251 | assert.equal(Math.round(addresses[0].distance), 2) 252 | 253 | assert.equal(addresses[1].name, 'Apple') 254 | assert.isNotNull(addresses[1].distance) 255 | assert.equal(Math.round(addresses[1].distance), 6) 256 | }) 257 | 258 | it('should contain custom distance column if "calculateDistance" is string', function * () { 259 | let addresses = yield adapter.findAll(Address, { 260 | where: { 261 | 'latitude,longitude': { 262 | near: { 263 | center: [37.41, -122.06], 264 | radius: 10, 265 | calculateDistance: 'howfar' 266 | } 267 | } 268 | } 269 | }) 270 | assert.equal(addresses.length, 2) 271 | 272 | assert.equal(addresses[0].name, 'Google') 273 | assert.equal(addresses[0].distance, undefined) 274 | assert.isNotNull(addresses[0].howfar) 275 | assert.equal(Math.round(addresses[0].howfar), 2) 276 | 277 | assert.equal(addresses[1].name, 'Apple') 278 | assert.equal(addresses[1].distance, undefined) 279 | assert.isNotNull(addresses[1].howfar) 280 | assert.equal(Math.round(addresses[1].howfar), 6) 281 | }) 282 | 283 | it('should use kilometers instead of miles if radius ends with "k"', function * () { 284 | let addresses = yield adapter.findAll(Address, { 285 | where: { 286 | 'latitude,longitude': { 287 | near: { 288 | center: [37.41, -122.06], 289 | radius: '10k', 290 | calculateDistance: true 291 | } 292 | } 293 | } 294 | }) 295 | assert.equal(addresses.length, 2) 296 | 297 | assert.equal(addresses[0].name, 'Google') 298 | assert.isNotNull(addresses[0].distance) 299 | assert.equal(Math.round(addresses[0].distance), 3) // in kilometers 300 | 301 | assert.equal(addresses[1].name, 'Apple') 302 | assert.isNotNull(addresses[1].distance) 303 | assert.equal(Math.round(addresses[1].distance), 9) // in kilometers 304 | }) 305 | 306 | it('should filter through relationships', function * () { 307 | yield adapter.create(User, { 308 | name: 'Larry Page', 309 | addressId: this.googleAddress.id 310 | }) 311 | yield adapter.create(User, { 312 | name: 'Tim Cook', 313 | addressId: this.appleAddress.id 314 | }) 315 | 316 | let users = yield adapter.findAll(User, { 317 | where: { 318 | 'address.latitude, address.longitude': { 319 | near: { 320 | center: [37.41, -122.06], 321 | radius: 10, 322 | calculateDistance: 'howfar' 323 | } 324 | } 325 | } 326 | }) 327 | assert.equal(users.length, 2) 328 | assert.equal(users[0].name, 'Larry Page') 329 | assert.equal(users[1].name, 'Tim Cook') 330 | }) 331 | 332 | it('should filter through multiple hasOne/belongsTo relations', function * () { 333 | let user1 = yield adapter.create(User, { 334 | name: 'Larry Page', 335 | addressId: this.googleAddress.id 336 | }) 337 | var post1 = yield adapter.create(Post, { 338 | content: 'foo', 339 | userId: user1.id 340 | }) 341 | yield adapter.create(Comment, { 342 | content: 'test1', 343 | postId: post1.id, 344 | userId: post1.userId 345 | }) 346 | 347 | var user2 = yield adapter.create(User, { 348 | name: 'Tim Cook', 349 | addressId: this.appleAddress.id 350 | }) 351 | var post2 = yield adapter.create(Post, { 352 | content: 'bar', 353 | userId: user2.id 354 | }) 355 | yield adapter.create(Comment, { 356 | content: 'test2', 357 | postId: post2.id, 358 | userId: post2.userId 359 | }) 360 | 361 | let comments = yield adapter.findAll(Comment, { 362 | where: { 363 | 'user.address.latitude, user.address.longitude': { 364 | near: { 365 | center: [37.41, -122.06], 366 | radius: 5 367 | } 368 | } 369 | } 370 | }) 371 | 372 | assert.equal(comments.length, 1) 373 | assert.equal(comments[0].userId, user1.id) 374 | assert.equal(comments[0].content, 'test1') 375 | }) 376 | }) 377 | }) 378 | -------------------------------------------------------------------------------- /test/knexfile.js: -------------------------------------------------------------------------------- 1 | const DB_CLIENT = process.env.DB_CLIENT || 'mysql' 2 | 3 | let connection 4 | 5 | if (DB_CLIENT === 'sqlite3') { 6 | connection = { 7 | filename: process.env.DB_FILE 8 | } 9 | } else { 10 | connection = { 11 | host: process.env.DB_HOST || '127.0.0.1', 12 | user: process.env.DB_USER, 13 | database: process.env.DB_NAME 14 | } 15 | } 16 | 17 | const config = { 18 | client: DB_CLIENT, 19 | connection: connection, 20 | pool: { 21 | min: 1, 22 | max: 10 23 | }, 24 | migrations: { 25 | tableName: 'migrations' 26 | }, 27 | debug: !!process.env.DEBUG 28 | } 29 | 30 | module.exports = config 31 | -------------------------------------------------------------------------------- /test/migrations/20160124161805_initial_schema.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function (knex, Promise) { 3 | return knex.schema 4 | .createTable('address', function (table) { 5 | table.increments('id').primary() 6 | table.string('name') 7 | table.decimal('latitude', 10, 7) 8 | table.decimal('longitude', 10, 7) 9 | }) 10 | .createTable('user', function (table) { 11 | table.increments('id').primary() 12 | table.string('name') 13 | table.integer('age').unsigned() 14 | 15 | table.integer('addressId') 16 | .unsigned() 17 | .references('address.id') 18 | }) 19 | .createTable('profile', function (table) { 20 | table.increments('id').primary() 21 | table.string('email') 22 | 23 | table.integer('userId') 24 | .unsigned() 25 | .references('user.id') 26 | }) 27 | .createTable('post', function (table) { 28 | table.increments('id').primary() 29 | table.text('status') 30 | table.text('content') 31 | 32 | table.integer('userId') 33 | .unsigned() 34 | .references('user.id') 35 | }) 36 | .createTable('comment', function (table) { 37 | table.increments('id').primary() 38 | table.text('content') 39 | 40 | table.integer('userId') 41 | .unsigned() 42 | .references('user.id') 43 | 44 | table.integer('postId') 45 | .unsigned() 46 | .references('post.id') 47 | }) 48 | .createTable('tag', function (table) { 49 | table.increments('id').primary() 50 | table.text('value') 51 | }) 52 | } 53 | 54 | exports.down = function (knex, Promise) { 55 | return knex.schema 56 | .dropTableIfExists('comment') 57 | .dropTableIfExists('post') 58 | .dropTableIfExists('user') 59 | .dropTableIfExists('address') 60 | .dropTableIfExists('profile') 61 | .dropTableIfExists('tag') 62 | } 63 | -------------------------------------------------------------------------------- /test/update_trx.spec.js: -------------------------------------------------------------------------------- 1 | describe('DSSqlAdapter#update + transaction', function () { 2 | var adapter, User 3 | 4 | beforeEach(function () { 5 | adapter = this.$$adapter 6 | User = this.$$User 7 | }) 8 | 9 | it('commit should update a user in a Sql db', function () { 10 | var id 11 | return adapter.create(User, { name: 'John' }) 12 | .then((user) => { 13 | id = user.id 14 | assert.equal(user.name, 'John') 15 | assert.isDefined(user.id) 16 | 17 | return adapter.knex.transaction((trx) => { 18 | return adapter.update(User, id, { name: 'Johnny' }, { transaction: trx }) 19 | }) 20 | }) 21 | .then((user) => { 22 | assert.equal(user.name, 'Johnny') 23 | assert.isDefined(user.id) 24 | assert.equalObjects(user, { id: id, name: 'Johnny', age: null, addressId: null }) 25 | return adapter.find(User, id) 26 | }) 27 | .then((user) => { 28 | assert.equal(user.name, 'Johnny') 29 | assert.isDefined(user.id) 30 | assert.equalObjects(user, { id: id, name: 'Johnny', age: null, addressId: null }) 31 | }) 32 | }) 33 | 34 | it('rollback should not update a user in a Sql db', function () { 35 | var id 36 | return adapter.create(User, { name: 'John' }) 37 | .then((user) => { 38 | id = user.id 39 | assert.equal(user.name, 'John') 40 | assert.isDefined(user.id) 41 | 42 | return adapter.knex.transaction((trx) => { 43 | return adapter.update(User, id, { name: 'Johnny' }, { transaction: trx }) 44 | .then((user) => { 45 | assert.equal(user.name, 'Johnny') 46 | assert.isDefined(user.id) 47 | assert.equalObjects(user, {id: id, name: 'Johnny', age: null, addressId: null}) 48 | 49 | throw new Error('rollback') 50 | }) 51 | }) 52 | }) 53 | .then(() => { 54 | throw new Error('should not have reached this!') 55 | }, (err) => { 56 | assert.equal(err.message, 'rollback') 57 | return adapter.find(User, id) 58 | }) 59 | .then((user) => { 60 | assert.equal(user.name, 'John') 61 | assert.isDefined(user.id) 62 | assert.equalObjects(user, { id: id, name: 'John', age: null, addressId: null }) 63 | }) 64 | }) 65 | }) 66 | --------------------------------------------------------------------------------