├── .babelrc ├── circle.yml ├── docker-compose.yml ├── .gitignore ├── .github ├── stale.yml └── workflows │ └── main.yml ├── .npmignore ├── test ├── helper.js ├── integration │ ├── transaction.test.js │ ├── hasOne.test.js │ ├── belongsTo.test.js │ ├── findByPk.test.js │ ├── belongsToMany.test.js │ └── hasMany.test.js └── unit │ └── getCacheKey.test.js ├── src ├── helper.js └── index.js ├── LICENSE ├── README.md ├── resources └── mocha-bootload.js ├── package.json └── .eslintrc /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-node4"], 3 | "plugins": [ 4 | "transform-object-rest-spread", 5 | ["transform-async-to-module-method", { 6 | "module": "bluebird", 7 | "method": "coroutine" 8 | }] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4.5 4 | 5 | test: 6 | override: 7 | - DB_HOST=localhost DB_DATABASE=circle_test DB_USER=ubuntu DB_PASSWORD= npm run lint && npm run cover 8 | - bash <(curl -s https://codecov.io/bash) -f coverage/lcov.info -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | dev: 2 | image: mhart/alpine-node:8.10 3 | links: 4 | - db 5 | working_dir: /src 6 | volumes: 7 | - .:/src 8 | environment: 9 | DB_HOST: db 10 | DB_DATABASE: dataloader_test 11 | DB_USER: dataloader_test 12 | DB_PASSWORD: dataloader_test 13 | 14 | db: 15 | image: postgres:9.4 16 | environment: 17 | POSTGRES_USER: dataloader_test 18 | POSTGRES_PASSWORD: dataloader_test 19 | -------------------------------------------------------------------------------- /.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 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | .idea 30 | test/mocha.opts 31 | lib 32 | -------------------------------------------------------------------------------- /.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: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | #staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 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 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | .idea 30 | test/mocha.opts 31 | 32 | src 33 | yarn.lock 34 | test 35 | resources 36 | .github 37 | .yml 38 | .eslintrc -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | 3 | let lastInt = 1000; 4 | 5 | export const connection = new Sequelize( 6 | process.env.DB_DATABASE, 7 | process.env.DB_USER, 8 | process.env.DB_PASSWORD, { 9 | dialect: 'postgres', 10 | host: process.env.DB_HOST, 11 | logging: false 12 | } 13 | ); 14 | 15 | export function createConnection() { 16 | const connection = new Sequelize( 17 | process.env.DB_DATABASE, 18 | process.env.DB_USER, 19 | process.env.DB_PASSWORD, { 20 | dialect: 'postgres', 21 | host: process.env.DB_HOST, 22 | logging: false 23 | } 24 | ); 25 | 26 | this.connection = connection; 27 | return connection; 28 | } 29 | 30 | // Having a sequential id helps with the queries with limit 31 | export function randint() { 32 | lastInt += 1; 33 | return lastInt; 34 | } 35 | -------------------------------------------------------------------------------- /src/helper.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | 3 | export const MODEL = 'MODEL'; 4 | export const ASSOCIATION = 'ASSOCIATION'; 5 | export const SEQUELIZE = 'SEQUELIZE'; 6 | 7 | export function methods(version) { 8 | return { 9 | findByPk: /^[56]/.test(version) ? ['findByPk'] : 10 | /^[4]/.test(version) ? ['findByPk', 'findById'] : 11 | ['findById', 'findByPrimary'] 12 | }; 13 | } 14 | 15 | export function method(target, alias) { 16 | if (type(target) === MODEL) { 17 | return methods(target.sequelize.constructor.version)[alias][0]; 18 | } 19 | throw new Error('Unknown target'); 20 | } 21 | 22 | export function type(target) { 23 | if (target.associationType) { 24 | return ASSOCIATION; 25 | } else if (/(SequelizeModel|class extends Model)/.test(target.toString()) || Sequelize.Model.isPrototypeOf(target)) { 26 | return MODEL; 27 | } else { 28 | return SEQUELIZE; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mick Hansen 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 | # dataloader-sequelize 2 | 3 | Batching, caching and simplification of Sequelize with facebook/dataloader 4 | 5 | # How it works 6 | 7 | dataloader-sequelize is designed to provide per-request caching/batching for sequelize lookups, most likely in a graphql environment 8 | 9 | # API 10 | 11 | ## `createContext(sequelize, object options)` 12 | * Should be called after all models and associations are defined 13 | * `sequelize` a sequelize instance 14 | * `options.max=500` the maximum number of simultaneous dataloaders to store in memory. The loaders are stored in an LRU cache 15 | 16 | # Usage 17 | ```js 18 | import {createContext, EXPECTED_OPTIONS_KEY} from 'dataloader-sequelize'; 19 | 20 | /* Per request */ 21 | const context = createContext(sequelize); // must not be called before all models and associations are defined 22 | await User.findById(2, {[EXPECTED_OPTIONS_KEY]: context}); 23 | await User.findById(2, {[EXPECTED_OPTIONS_KEY]: context}); // Cached or batched, depending on timing 24 | ``` 25 | 26 | ## Priming 27 | 28 | Commonly you might have some sort of custom findAll requests that isn't going through the dataloader. To reuse the results from a call such as this in later findById calls you need to prime the cache: 29 | 30 | ```js 31 | import {createContext, EXPECTED_OPTIONS_KEY} from 'dataloader-sequelize'; 32 | const context = createContext(sequelize); 33 | 34 | const results = await User.findAll({where: {/* super complicated */}}); 35 | context.prime(results); 36 | 37 | await User.findById(2, {[EXPECTED_OPTIONS_KEY]: context}); // Cached, if was in results 38 | ``` 39 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | name: CI 7 | services: 8 | postgres: 9 | image: postgres:11-alpine 10 | env: 11 | POSTGRES_USER: dataloader_test 12 | POSTGRES_PASSWORD: dataloader_test 13 | ports: 14 | - 5432:5432 15 | options: >- 16 | --health-cmd pg_isready 17 | --health-interval 10s 18 | --health-timeout 5s 19 | --health-retries 10 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: '12.x' 26 | - run: npm ci 27 | - name: Test v3 28 | run: npm run test:v3:raw 29 | env: 30 | DB_HOST: localhost 31 | DB_DATABASE: dataloader_test 32 | DB_USER: dataloader_test 33 | DB_PASSWORD: dataloader_test 34 | DB_PORT: 5432 35 | - name: Test v4 36 | run: npm run test:v4:raw 37 | env: 38 | DB_HOST: localhost 39 | DB_DATABASE: dataloader_test 40 | DB_USER: dataloader_test 41 | DB_PASSWORD: dataloader_test 42 | DB_PORT: 5432 43 | - name: Test v5 44 | run: npm run test:v5:raw 45 | env: 46 | DB_HOST: localhost 47 | DB_DATABASE: dataloader_test 48 | DB_USER: dataloader_test 49 | DB_PASSWORD: dataloader_test 50 | - name: Test Latest Version 51 | run: npm run test:latest:raw 52 | env: 53 | DB_HOST: localhost 54 | DB_DATABASE: dataloader_test 55 | DB_USER: dataloader_test 56 | DB_PASSWORD: dataloader_test -------------------------------------------------------------------------------- /resources/mocha-bootload.js: -------------------------------------------------------------------------------- 1 | require("babel-register"); 2 | 3 | var unexpected = require('unexpected'); 4 | unexpected.use(require('unexpected-sinon')); 5 | unexpected.use(require('unexpected-set')); 6 | 7 | var Bluebird = require('bluebird'); 8 | require('sinon-as-promised')(Bluebird); 9 | 10 | var Sequelize = require('sequelize'); 11 | unexpected.addType({ 12 | name: 'Sequelize.Instance', 13 | identify: /^[456]/.test(Sequelize.version) ? 14 | function (value) { 15 | return value && value instanceof Sequelize.Model && 'isNewRecord' in value; 16 | } : 17 | function (value) { 18 | return value && value instanceof Sequelize.Instance; 19 | }, 20 | inspect: function (value, depth, output, inspect) { 21 | const name = value.name 22 | || value.$modelOptions && value.$modelOptions.name // v3 23 | || value._modelOptions && value._modelOptions.name; // v4+ 24 | output 25 | .text(name.singular).text('(') 26 | .append(inspect(value.get(), depth)) 27 | .text(')'); 28 | }, 29 | equal: function (a, b) { 30 | const aModel = a.Model || a.constructor; // v3 vs v4 31 | const bModel = b.Model || b.constructor; 32 | const pk = aModel.primaryKeyAttribute; 33 | return aModel.name === bModel.name && a.get(pk) === b.get(pk); 34 | } 35 | }); 36 | 37 | unexpected.addType({ 38 | name: 'Sequelize.Association', 39 | identify: function (value) { 40 | return value && value instanceof Sequelize.Association; 41 | }, 42 | inspect: function (value, depth, output) { 43 | output 44 | .text(value.associationType).text(': ') 45 | .text(value.source.name).text(' -> ').text(value.target.name) 46 | .text('(').text(value.as).text(')'); 47 | }, 48 | equal: function (a, b, equal) { 49 | return a.associationType === b.associationType && equal(a.source, b.source) && equal(a.target, b.target) && a.as === b.as; 50 | } 51 | }); 52 | 53 | unexpected.addAssertion(' [not] to be shimmed', function (expect, subject) { 54 | return expect(subject, '[not] to have property', '__wrapped'); 55 | }); 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dataloader-sequelize", 3 | "version": "2.3.3", 4 | "description": "Batching and simplification of Sequelize with facebook/dataloader", 5 | "main": "lib/index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "prepublish": "npm run build", 9 | "lint": "eslint src", 10 | "build": "babel src -d lib", 11 | "test": "npm run lint && npm run test:v3 && npm run test:v4 && npm run test:v5 && npm run test:latest", 12 | "test:current": "npm run test:unit && npm run test:integration", 13 | "test:current:raw": "npm run test:unit && npm run test:integration:raw", 14 | "test:unit": "cross-env NODE_ENV=test mocha --require resources/mocha-bootload --check-leaks --timeout 10000 --colors --reporter spec --recursive 'test/unit/**.test.js'", 15 | "test:integration": "docker-compose run --rm -e NODE_ENV=test dev npm run test:integration:raw", 16 | "test:integration:raw": "mocha --require resources/mocha-bootload --check-leaks --timeout 10000 --colors --reporter spec --recursive 'test/integration/**.test.js'", 17 | "test:v3": "npm install sequelize@3 && npm run test:current", 18 | "test:v3:raw": "npm install sequelize@3 && npm run test:current:raw", 19 | "test:latest": "npm install sequelize@latest && npm run test:current", 20 | "test:latest:raw": "npm install sequelize@latest && npm run test:current:raw", 21 | "test:v5": "npm install sequelize@5 && npm run test:current", 22 | "test:v5:raw": "npm install sequelize@5 && npm run test:current:raw", 23 | "test:v4": "npm install sequelize@4 && npm run test:current", 24 | "test:v4:raw": "npm install sequelize@4 && npm run test:current:raw", 25 | "cover": "babel-node node_modules/.bin/isparta cover --excludes **/resources/** _mocha -- --require resources/mocha-bootload --check-leaks --timeout 10000 --colors --reporter spec --recursive 'test/**/*.test.js'" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/mickhansen/dataloader-sequelize.git" 30 | }, 31 | "devDependencies": { 32 | "babel-cli": "^6.10.1", 33 | "babel-eslint": "^6.1.0", 34 | "babel-plugin-transform-async-to-module-method": "^6.8.0", 35 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 36 | "babel-preset-es2015-node4": "^2.1.0", 37 | "babel-register": "^6.9.0", 38 | "bluebird": "^3.4.6", 39 | "cls-hooked": "^4.2.2", 40 | "continuation-local-storage": "^3.2.1", 41 | "cross-env": "^7.0.3", 42 | "eslint": "^3.0.0", 43 | "isparta": "^4.0.0", 44 | "mocha": "^3.0.0", 45 | "pg": "^6.1.0", 46 | "sequelize": "^3.35.1", 47 | "sinon": "^1.17.4", 48 | "sinon-as-promised": "^4.0.0", 49 | "unexpected": "^10.14.2", 50 | "unexpected-set": "^1.1.0", 51 | "unexpected-sinon": "^10.2.1" 52 | }, 53 | "peerDependencies": { 54 | "sequelize": "^3.24.6 || ^4.0.0 || ^5.0.0 || ^6.0.0" 55 | }, 56 | "dependencies": { 57 | "dataloader": "^1.2.0", 58 | "lodash": "^4.15.0", 59 | "lru-cache": "^4.0.1", 60 | "shimmer": "^1.2.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/integration/transaction.test.js: -------------------------------------------------------------------------------- 1 | import {createConnection, randint} from '../helper'; 2 | import Sequelize from 'sequelize'; 3 | import sinon from 'sinon'; 4 | import {createContext, EXPECTED_OPTIONS_KEY} from '../../src'; 5 | import expect from 'unexpected'; 6 | import Promise from 'bluebird'; 7 | import cls from 'continuation-local-storage'; 8 | import clsh from 'cls-hooked'; 9 | import {method} from '../../src/helper'; 10 | 11 | describe('Transactions', function () { 12 | beforeEach(createConnection); 13 | beforeEach(async function () { 14 | this.sandbox = sinon.sandbox.create(); 15 | }); 16 | afterEach(function () { 17 | this.sandbox.restore(); 18 | }); 19 | 20 | describe('Managed', function () { 21 | beforeEach(async function () { 22 | this.sandbox = sinon.sandbox.create(); 23 | 24 | this.User = this.connection.define('user'); 25 | 26 | await this.connection.sync({ 27 | force: true 28 | }); 29 | 30 | this.users = await this.User.bulkCreate([ 31 | { id: randint() }, 32 | { id: randint() } 33 | ], { returning: true }); 34 | 35 | this.sandbox.spy(this.User, 'findAll'); 36 | 37 | this.context = createContext(this.connection); 38 | }); 39 | 40 | it('does not batch during managed transactions', async function () { 41 | let user1, user2; 42 | console.log(method(this.User, 'findByPk')); 43 | await this.connection.transaction(async (t) => { 44 | [user1, user2] = await Promise.all([ 45 | this.User[method(this.User, 'findByPk')](this.users[1].get('id'), {transaction: t, [EXPECTED_OPTIONS_KEY]: this.context}), 46 | this.User[method(this.User, 'findByPk')](this.users[0].get('id'), {transaction: t, [EXPECTED_OPTIONS_KEY]: this.context}) 47 | ]); 48 | }); 49 | expect(user1, 'to equal', this.users[1]); 50 | expect(user2, 'to equal', this.users[0]); 51 | 52 | expect(this.User.findAll, 'not to have calls satisfying', [{ 53 | where: { 54 | id: [this.users[1].get('id'), this.users[0].get('id')] 55 | } 56 | }]); 57 | }); 58 | }); 59 | 60 | describe('CLS', function () { 61 | beforeEach(async function () { 62 | 63 | this.namespace = (/^[6]/.test(Sequelize.version) ? clsh : cls).createNamespace('sequelize'); 64 | if (/^[456]/.test(Sequelize.version)) { 65 | Sequelize.useCLS(this.namespace); 66 | } else { 67 | Sequelize.cls = this.namespace; 68 | } 69 | this.sandbox = sinon.sandbox.create(); 70 | 71 | this.User = this.connection.define('user'); 72 | 73 | this.context = createContext(this.connection); 74 | 75 | await this.connection.sync({ 76 | force: true 77 | }); 78 | 79 | this.users = await this.User.bulkCreate([ 80 | { id: randint() }, 81 | { id: randint() } 82 | ], { returning: true }); 83 | 84 | this.sandbox.spy(this.User, 'findAll'); 85 | }) 86 | 87 | after(function () { 88 | if (/^[456]/.test(Sequelize.version)) { 89 | delete Sequelize._cls; 90 | } else { 91 | delete Sequelize.cls; 92 | } 93 | }) 94 | 95 | it('does not batch during CLS transactions', async function () { 96 | let user1, user2; 97 | await this.connection.transaction(async (t) => { 98 | [user1, user2] = await Promise.all([ 99 | this.User[method(this.User, 'findByPk')](this.users[1].get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}), 100 | this.User[method(this.User, 'findByPk')](this.users[0].get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}) 101 | ]); 102 | }); 103 | expect(user1, 'to equal', this.users[1]); 104 | expect(user2, 'to equal', this.users[0]); 105 | 106 | expect(this.User.findAll, 'not to have calls satisfying', [{ 107 | where: { 108 | id: [this.users[1].get('id'), this.users[0].get('id')] 109 | } 110 | }]); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "arrowFunctions": true, 4 | "blockBindings": true, 5 | "classes": true, 6 | "defaultParams": true, 7 | "destructuring": true, 8 | "forOf": true, 9 | "generators": true, 10 | "modules": true, 11 | "objectLiteralComputedProperties": true, 12 | "objectLiteralShorthandMethods": true, 13 | "objectLiteralShorthandProperties": true, 14 | "spread": true, 15 | "templateStrings": true, 16 | "env": { 17 | "node": true, 18 | "es6": true, 19 | "mocha": true 20 | }, 21 | "rules": { 22 | "comma-dangle": 0, 23 | "no-cond-assign": 2, 24 | "no-console": 0, 25 | "no-constant-condition": 2, 26 | "no-control-regex": 0, 27 | "no-debugger": 0, 28 | "no-dupe-args": 2, 29 | "no-dupe-keys": 2, 30 | "no-duplicate-case": 2, 31 | "no-empty": 2, 32 | "no-empty-character-class": 2, 33 | "no-ex-assign": 2, 34 | "no-extra-boolean-cast": 2, 35 | "no-extra-parens": 2, 36 | "no-extra-semi": 2, 37 | "no-func-assign": 2, 38 | "no-inner-declarations": [ 39 | 2, 40 | "functions" 41 | ], 42 | "no-invalid-regexp": 2, 43 | "no-irregular-whitespace": 2, 44 | "no-negated-in-lhs": 2, 45 | "no-obj-calls": 2, 46 | "no-regex-spaces": 2, 47 | "no-reserved-keys": 0, 48 | "no-sparse-arrays": 2, 49 | "no-unreachable": 2, 50 | "use-isnan": 2, 51 | "valid-jsdoc": 0, 52 | "valid-typeof": 2, 53 | "block-scoped-var": 0, 54 | "complexity": 0, 55 | "consistent-return": 0, 56 | "default-case": 0, 57 | "dot-notation": 0, 58 | "eqeqeq": 2, 59 | "guard-for-in": 2, 60 | "no-alert": 2, 61 | "no-caller": 2, 62 | "no-div-regex": 2, 63 | "no-eq-null": 0, 64 | "no-eval": 2, 65 | "no-extend-native": 2, 66 | "no-extra-bind": 2, 67 | "no-fallthrough": 2, 68 | "no-floating-decimal": 2, 69 | "no-implied-eval": 2, 70 | "no-iterator": 2, 71 | "no-labels": 0, 72 | "no-lone-blocks": 0, 73 | "no-loop-func": 0, 74 | "no-multi-spaces": 2, 75 | "no-multi-str": 2, 76 | "no-native-reassign": 0, 77 | "no-new": 2, 78 | "no-new-func": 0, 79 | "no-new-wrappers": 2, 80 | "no-octal": 2, 81 | "no-octal-escape": 2, 82 | "no-param-reassign": 0, 83 | "no-process-env": 0, 84 | "no-proto": 2, 85 | "no-redeclare": 2, 86 | "no-return-assign": 2, 87 | "no-script-url": 2, 88 | "no-self-compare": 0, 89 | "no-sequences": 2, 90 | "no-throw-literal": 2, 91 | "no-unused-expressions": 2, 92 | "no-void": 2, 93 | "no-warning-comments": 0, 94 | "no-with": 2, 95 | "radix": 2, 96 | "vars-on-top": 0, 97 | "wrap-iife": 2, 98 | "yoda": [ 99 | 2, 100 | "never", 101 | { 102 | "exceptRange": true 103 | } 104 | ], 105 | "strict": 0, 106 | "no-catch-shadow": 0, 107 | "no-delete-var": 2, 108 | "no-label-var": 2, 109 | "no-shadow": 0, 110 | "no-shadow-restricted-names": 2, 111 | "no-undef": 2, 112 | "no-undef-init": 2, 113 | "no-undefined": 0, 114 | "no-unused-vars": [ 115 | 2, 116 | { 117 | "vars": "all", 118 | "args": "after-used" 119 | } 120 | ], 121 | "no-use-before-define": 0, 122 | "handle-callback-err": [ 123 | 2, 124 | "error" 125 | ], 126 | "no-mixed-requires": [ 127 | 0, 128 | true 129 | ], 130 | "no-new-require": 2, 131 | "no-path-concat": 0, 132 | "no-process-exit": 0, 133 | "no-restricted-modules": 0, 134 | "curly": 0, 135 | "no-sync": 2, 136 | "indent": [ 137 | 2, 138 | 2, 139 | { 140 | "SwitchCase": 1 141 | } 142 | ], 143 | "brace-style": [ 144 | 2, 145 | "1tbs", 146 | { 147 | "allowSingleLine": true 148 | } 149 | ], 150 | "camelcase": [ 151 | 0, 152 | { 153 | "properties": "always" 154 | } 155 | ], 156 | "comma-spacing": 0, 157 | "comma-style": 0, 158 | "consistent-this": 0, 159 | "eol-last": 2, 160 | "func-names": 0, 161 | "func-style": 0, 162 | "key-spacing": [ 163 | 2, 164 | { 165 | "beforeColon": false, 166 | "afterColon": true 167 | } 168 | ], 169 | "max-nested-callbacks": 0, 170 | "new-cap": 0, 171 | "new-parens": 2, 172 | "newline-after-var": 0, 173 | "no-array-constructor": 2, 174 | "no-inline-comments": 0, 175 | "no-lonely-if": 2, 176 | "no-mixed-spaces-and-tabs": 2, 177 | "no-multiple-empty-lines": 0, 178 | "no-nested-ternary": 0, 179 | "no-new-object": 2, 180 | "no-spaced-func": 2, 181 | "no-ternary": 0, 182 | "no-trailing-spaces": 2, 183 | "no-underscore-dangle": 0, 184 | "one-var": 0, 185 | "operator-assignment": [ 186 | 2, 187 | "always" 188 | ], 189 | "padded-blocks": 0, 190 | "quote-props": [ 191 | 2, 192 | "as-needed" 193 | ], 194 | "semi": [ 195 | 2, 196 | "always" 197 | ], 198 | "semi-spacing": [ 199 | 2, 200 | { 201 | "before": false, 202 | "after": true 203 | } 204 | ], 205 | "sort-vars": 0, 206 | "keyword-spacing": 2, 207 | "space-before-blocks": [ 208 | 2, 209 | "always" 210 | ], 211 | "space-before-function-paren": [ 212 | 2, 213 | { 214 | "anonymous": "always", 215 | "named": "never" 216 | } 217 | ], 218 | "space-in-brackets": 0, 219 | "space-in-parens": 0, 220 | "space-infix-ops": [ 221 | 2, 222 | { 223 | "int32Hint": false 224 | } 225 | ], 226 | "space-unary-ops": [ 227 | 2, 228 | { 229 | "words": true, 230 | "nonwords": false 231 | } 232 | ], 233 | "spaced-comment": [ 234 | 2, 235 | "always" 236 | ], 237 | "wrap-regex": 0, 238 | "no-var": 0, 239 | "max-len": [2, 200, 4] 240 | } 241 | } -------------------------------------------------------------------------------- /test/unit/getCacheKey.test.js: -------------------------------------------------------------------------------- 1 | import {getCacheKey} from '../../src'; 2 | import expect from 'unexpected'; 3 | import {connection} from '../helper'; 4 | 5 | describe('getCacheKey', function () { 6 | const User = connection.define('user') 7 | , Task = connection.define('task') 8 | , association = User.hasMany(Task); 9 | 10 | it('handles circular structures', function () { 11 | let foo = {} 12 | , bar = {} 13 | , options = { 14 | foo, 15 | bar 16 | }; 17 | 18 | foo.bar = bar; 19 | bar.foo = foo; 20 | 21 | expect(getCacheKey({ 22 | name: 'user' 23 | }, 'id', options), 'to equal', 24 | 'user|id|association:undefined|attributes:undefined|groupedLimit:undefined|limit:undefined|offset:undefined|order:undefined|paranoid:undefined|raw:undefined|searchPath:undefined|through:undefined|where:undefined'); 25 | }); 26 | 27 | it('handles nulls', function () { 28 | expect(getCacheKey(User, 'id', { 29 | order: null 30 | }), 'to equal', 'user|id|association:undefined|attributes:undefined|groupedLimit:undefined|limit:undefined|offset:undefined|order:null|paranoid:undefined|raw:undefined|searchPath:undefined|through:undefined|where:undefined'); 31 | }); 32 | 33 | it('does not modify arrays', function () { 34 | let options = { 35 | order: ['foo', 'bar'] 36 | }; 37 | 38 | expect(getCacheKey(User, 'id', options), 'to equal', 39 | 'user|id|association:undefined|attributes:undefined|groupedLimit:undefined|limit:undefined|offset:undefined|order:foo,bar|paranoid:undefined|raw:undefined|searchPath:undefined|through:undefined|where:undefined'); 40 | expect(options.order, 'to equal', ['foo', 'bar']); 41 | }); 42 | 43 | it('handles associations', function () { 44 | expect(getCacheKey(User, 'id', { 45 | association, 46 | limit: 42 47 | }), 'to equal', 'user|id|association:HasMany,task,tasks|attributes:undefined|groupedLimit:undefined|limit:42|offset:undefined|order:undefined|paranoid:undefined|raw:undefined|searchPath:undefined|through:undefined|where:undefined'); 48 | }); 49 | 50 | it('handles attributes', function () { 51 | expect(getCacheKey(User, 'id', { 52 | attributes: ['foo', 'bar', 'baz'] 53 | }), 'to equal', 'user|id|association:undefined|attributes:bar,baz,foo|groupedLimit:undefined|limit:undefined|offset:undefined|order:undefined|paranoid:undefined|raw:undefined|searchPath:undefined|through:undefined|where:undefined'); 54 | }); 55 | 56 | it('handles schemas', function () { 57 | expect(getCacheKey({ 58 | name: 'user', 59 | options: { 60 | schema: 'app' 61 | } 62 | }, 'id', { 63 | attributes: ['foo', 'bar', 'baz'] 64 | }), 'to equal', 'app|user|id|association:undefined|attributes:bar,baz,foo|groupedLimit:undefined|limit:undefined|offset:undefined|order:undefined|paranoid:undefined|raw:undefined|searchPath:undefined|through:undefined|where:undefined'); 65 | }); 66 | 67 | describe('where statements', function () { 68 | it('POJO', function () { 69 | expect(getCacheKey(User, 'id', { 70 | where: { 71 | completed: true 72 | } 73 | }), 'to equal', 74 | 'user|id|association:undefined|attributes:undefined|groupedLimit:undefined|limit:undefined|offset:undefined|order:undefined|paranoid:undefined|raw:undefined|searchPath:undefined|through:undefined|where:completed:true'); 75 | }); 76 | 77 | it('symbols', function () { 78 | expect(getCacheKey(User, 'id', { 79 | where: { 80 | [Symbol('or')]: { 81 | name: { [Symbol('iLike')]: '%test%' } 82 | }, 83 | [Symbol('or')]: { 84 | name: { [Symbol('iLike')]: '%test%' } 85 | } 86 | } 87 | }), 'to equal', 88 | 'user|id|association:undefined|attributes:undefined|groupedLimit:undefined|limit:undefined|offset:undefined|order:undefined|paranoid:undefined|raw:undefined|searchPath:undefined|through:undefined|where:Symbol(or):name:Symbol(iLike):%test%|Symbol(or):name:Symbol(iLike):%test%'); 89 | }); 90 | 91 | it('date', function () { 92 | const from = new Date(Date.UTC(2016, 1, 1)); 93 | const to = new Date(Date.UTC(2016, 2, 1)); 94 | 95 | expect(getCacheKey(User, 'id', { 96 | where: { 97 | completed: { 98 | $between: [ 99 | from, 100 | to 101 | ] 102 | } 103 | } 104 | }), 'to equal', 105 | 'user|id|association:undefined|attributes:undefined|groupedLimit:undefined|limit:undefined|offset:undefined|order:undefined|paranoid:undefined|raw:undefined|searchPath:undefined|through:undefined|where:completed:$between:2016-02-01T00:00:00.000Z,2016-03-01T00:00:00.000Z'); 106 | }); 107 | 108 | it('literal', function () { 109 | expect(getCacheKey(User, 'id', { 110 | where: { 111 | foo: connection.literal('SELECT foo FROM bar') 112 | } 113 | }), 'to equal', 114 | 'user|id|association:undefined|attributes:undefined|groupedLimit:undefined|limit:undefined|offset:undefined|order:undefined|paranoid:undefined|raw:undefined|searchPath:undefined|through:undefined|where:foo:val:SELECT foo FROM bar'); 115 | }); 116 | 117 | it('fn + col', function () { 118 | expect(getCacheKey(User, 'id', { 119 | where: { 120 | foo: { 121 | $gt: connection.fn('FOO', connection.col('bar')) 122 | } 123 | } 124 | }), 'to equal', 125 | 'user|id|association:undefined|attributes:undefined|groupedLimit:undefined|limit:undefined|offset:undefined|order:undefined|paranoid:undefined|raw:undefined|searchPath:undefined|through:undefined|where:foo:$gt:args:col:bar|fn:FOO'); 126 | }); 127 | }); 128 | 129 | it('searchPath', function () { 130 | expect(getCacheKey(User, 'id', { 131 | searchPath: 'test' 132 | }), 'to equal', 133 | 'user|id|association:undefined|attributes:undefined|groupedLimit:undefined|limit:undefined|offset:undefined|order:undefined|paranoid:undefined|raw:undefined|searchPath:test|through:undefined|where:undefined'); 134 | }); 135 | 136 | }); 137 | -------------------------------------------------------------------------------- /test/integration/hasOne.test.js: -------------------------------------------------------------------------------- 1 | import {createConnection, randint} from '../helper'; 2 | import sinon from 'sinon'; 3 | import {createContext, EXPECTED_OPTIONS_KEY} from '../../src'; 4 | import expect from 'unexpected'; 5 | import Promise from 'bluebird'; 6 | 7 | describe('hasOne', function () { 8 | describe('simple association', function () { 9 | beforeEach(createConnection); 10 | beforeEach(async function () { 11 | this.sandbox = sinon.sandbox.create(); 12 | 13 | this.User = this.connection.define('user'); 14 | this.Project = this.connection.define('project'); 15 | 16 | this.User.hasOne(this.Project, { 17 | as: 'mainProject', 18 | foreignKey: { 19 | name: 'ownerId', 20 | field: 'owner_id' 21 | } 22 | }); 23 | 24 | await this.connection.sync({ 25 | force: true 26 | }); 27 | 28 | [this.user0, this.user1, this.user2, this.user3] = await this.User.bulkCreate([ 29 | { id: '0' }, 30 | { id: randint() }, 31 | { id: randint() }, 32 | { id: randint() } 33 | ], { returning: true }); 34 | [this.project0, this.project1, this.project2] = await this.Project.bulkCreate([ 35 | { id: '0' }, 36 | { id: randint() }, 37 | { id: randint() } 38 | ], { returning: true }); 39 | await Promise.join( 40 | this.user0.setMainProject(this.project0), 41 | this.user1.setMainProject(this.project1), 42 | this.user2.setMainProject(this.project2) 43 | ); 44 | 45 | this.sandbox.spy(this.Project, 'findAll'); 46 | 47 | this.context = createContext(this.connection); 48 | }); 49 | afterEach(function () { 50 | this.sandbox.restore(); 51 | }); 52 | 53 | it('batches and caches to a single findAll call', async function () { 54 | let project1 = this.user1.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context}) 55 | , project2 = this.user2.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context}); 56 | 57 | await expect(project1, 'to be fulfilled with', this.project1); 58 | await expect(project2, 'to be fulfilled with', this.project2); 59 | 60 | project1 = this.user1.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context}); 61 | project2 = this.user2.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context}); 62 | 63 | await expect(project1, 'to be fulfilled with', this.project1); 64 | await expect(project2, 'to be fulfilled with', this.project2); 65 | 66 | expect(this.Project.findAll, 'was called once'); 67 | expect(this.Project.findAll, 'to have a call satisfying', [{ 68 | where: { 69 | ownerId: [this.user1.get('id'), this.user2.get('id')] 70 | } 71 | }]); 72 | }); 73 | 74 | it('caches based on priming', async function () { 75 | this.context.prime(await this.Project.findAll()); 76 | 77 | const project1 = this.user1.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context}) 78 | , project2 = this.user2.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context}); 79 | 80 | await expect(project1, 'to be fulfilled with', this.project1); 81 | await expect(project2, 'to be fulfilled with', this.project2); 82 | 83 | expect(this.Project.findAll, 'was called once'); 84 | }); 85 | 86 | it('works for user without projects', async function () { 87 | const project3 = this.user3.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context}); 88 | 89 | await expect(project3, 'to be fulfilled with', null); 90 | }); 91 | 92 | it('works with id of 0', async function () { 93 | const project0 = await this.user0.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context}); 94 | 95 | expect(project0.get('id'), 'to equal', 0); 96 | expect(this.Project.findAll, 'was called once'); 97 | }); 98 | 99 | it('supports rejectOnEmpty', async function () { 100 | const project1 = this.user1.getMainProject({ rejectOnEmpty: true, [EXPECTED_OPTIONS_KEY]: this.context }) 101 | , project2 = this.user3.getMainProject({ rejectOnEmpty: true, [EXPECTED_OPTIONS_KEY]: this.context }) 102 | , project3 = this.user3.getMainProject({ [EXPECTED_OPTIONS_KEY]: this.context }); 103 | 104 | await expect(project1, 'to be fulfilled with', this.project1); 105 | await expect(project2, 'to be rejected with', new this.connection.constructor.EmptyResultError()); 106 | await expect(project3, 'to be fulfilled with', null); 107 | }); 108 | }); 109 | 110 | describe('simple association', function () { 111 | beforeEach(createConnection); 112 | beforeEach(async function () { 113 | this.sandbox = sinon.sandbox.create(); 114 | 115 | this.User = this.connection.define('user'); 116 | this.Project = this.connection.define('project', {}, { paranoid: true }); 117 | 118 | this.User.hasOne(this.Project, { 119 | as: 'mainProject', 120 | foreignKey: { 121 | name: 'ownerId', 122 | field: 'owner_id' 123 | } 124 | }); 125 | 126 | await this.connection.sync({ 127 | force: true 128 | }); 129 | 130 | [this.user1, this.user2] = await this.User.bulkCreate([ 131 | { id: randint() }, 132 | { id: randint() }, 133 | ], { returning: true }); 134 | [this.project1, this.project2] = await this.Project.bulkCreate([ 135 | { id: randint(), deletedAt: new Date() }, 136 | { id: randint() } 137 | ], { returning: true }); 138 | await Promise.join( 139 | this.user1.setMainProject(this.project1), 140 | this.user2.setMainProject(this.project2) 141 | ); 142 | 143 | this.sandbox.spy(this.Project, 'findAll'); 144 | 145 | this.context = createContext(this.connection); 146 | }); 147 | afterEach(function () { 148 | this.sandbox.restore(); 149 | }); 150 | 151 | it('batches and caches to a single findAll call (paranoid)', async function () { 152 | let project1 = this.user1.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context}) 153 | , project2 = this.user2.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context}); 154 | 155 | await expect(project1, 'to be fulfilled with', null); 156 | await expect(project2, 'to be fulfilled with', this.project2); 157 | 158 | project1 = this.user1.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context}); 159 | project2 = this.user2.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context}); 160 | 161 | await expect(project1, 'to be fulfilled with', null); 162 | await expect(project2, 'to be fulfilled with', this.project2); 163 | 164 | expect(this.Project.findAll, 'was called once'); 165 | expect(this.Project.findAll, 'to have a call satisfying', [{ 166 | where: { 167 | ownerId: [this.user1.get('id'), this.user2.get('id')] 168 | } 169 | }]); 170 | }); 171 | 172 | it('batches and caches to a single findAll call (not paranoid)', async function () { 173 | let project1 = this.user1.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}) 174 | , project2 = this.user2.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 175 | 176 | await expect(project1, 'to be fulfilled with', this.project1); 177 | await expect(project2, 'to be fulfilled with', this.project2); 178 | 179 | project1 = this.user1.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 180 | project2 = this.user2.getMainProject({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 181 | 182 | await expect(project1, 'to be fulfilled with', this.project1); 183 | await expect(project2, 'to be fulfilled with', this.project2); 184 | 185 | expect(this.Project.findAll, 'was called once'); 186 | expect(this.Project.findAll, 'to have a call satisfying', [{ 187 | where: { 188 | ownerId: [this.user1.get('id'), this.user2.get('id')] 189 | } 190 | }]); 191 | }); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /test/integration/belongsTo.test.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import {createConnection, randint} from '../helper'; 3 | import sinon from 'sinon'; 4 | import {createContext, EXPECTED_OPTIONS_KEY} from '../../src'; 5 | import Promise from 'bluebird'; 6 | import expect from 'unexpected'; 7 | 8 | describe('belongsTo', function () { 9 | describe('simple association', function () { 10 | beforeEach(createConnection); 11 | beforeEach(async function () { 12 | this.sandbox = sinon.sandbox.create(); 13 | 14 | this.User = this.connection.define('user'); 15 | this.Project = this.connection.define('project'); 16 | 17 | this.Project.belongsTo(this.User, { 18 | as: 'owner', 19 | foreignKey: { 20 | name: 'ownerId', 21 | field: 'owner_id' 22 | } 23 | }); 24 | 25 | await this.connection.sync({ 26 | force: true 27 | }); 28 | 29 | [this.user0, this.user1, this.user2] = await this.User.bulkCreate([ 30 | { id: '0' }, 31 | { id: randint() }, 32 | { id: randint() } 33 | ], { returning: true }); 34 | [this.project0, this.project1, this.project2, this.project3] = await this.Project.bulkCreate([ 35 | { id: '0' }, 36 | { id: randint() }, 37 | { id: randint() }, 38 | { id: randint() } 39 | ], { returning: true }); 40 | await Promise.join( 41 | this.project0.setOwner(this.user0), 42 | this.project1.setOwner(this.user1), 43 | this.project2.setOwner(this.user2) 44 | ); 45 | 46 | this.sandbox.spy(this.User, 'findAll'); 47 | 48 | this.context = createContext(this.connection); 49 | }); 50 | afterEach(function () { 51 | this.sandbox.restore(); 52 | }); 53 | 54 | it('batches and caches to a single findAll call', async function () { 55 | let user1 = this.project1.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}) 56 | , user2 = this.project2.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}); 57 | 58 | await expect(user1, 'to be fulfilled with', this.user1); 59 | await expect(user2, 'to be fulfilled with', this.user2); 60 | 61 | user1 = this.project1.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}); 62 | user2 = this.project2.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}); 63 | 64 | await expect(user1, 'to be fulfilled with', this.user1); 65 | await expect(user2, 'to be fulfilled with', this.user2); 66 | 67 | expect(this.User.findAll, 'was called once'); 68 | expect(this.User.findAll, 'to have a call satisfying', [{ 69 | where: { 70 | id: [this.user1.get('id'), this.user2.get('id')] 71 | } 72 | }]); 73 | }); 74 | 75 | it('caches based on priming', async function () { 76 | this.context.prime(await this.User.findAll()); 77 | 78 | const user1 = this.project1.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}) 79 | , user2 = this.project2.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}); 80 | 81 | await expect(user1, 'to be fulfilled with', this.user1); 82 | await expect(user2, 'to be fulfilled with', this.user2); 83 | 84 | expect(this.User.findAll, 'was called once'); 85 | }); 86 | 87 | it('works for project without owner', async function () { 88 | const user3 = this.project3.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}); 89 | 90 | await expect(user3, 'to be fulfilled with', null); 91 | await expect(this.User.findAll, 'was not called'); 92 | }); 93 | 94 | it('works with id of 0', async function () { 95 | const user0 = await this.project0.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}); 96 | 97 | expect(user0.get('id'), 'to equal', 0); 98 | expect(this.User.findAll, 'was called once'); 99 | }); 100 | 101 | it('supports rejectOnEmpty', async function () { 102 | const user1 = this.project1.getOwner({ [EXPECTED_OPTIONS_KEY]: this.context, rejectOnEmpty: Error }) 103 | , user2 = this.project3.getOwner({ [EXPECTED_OPTIONS_KEY]: this.context, rejectOnEmpty: Error }) 104 | , user3 = this.project3.getOwner({ [EXPECTED_OPTIONS_KEY]: this.context }); 105 | 106 | await expect(user1, 'to be fulfilled with', this.user1); 107 | await expect(user2, 'to be rejected with', Error); 108 | await expect(user3, 'to be fulfilled with', null); 109 | }); 110 | }); 111 | 112 | describe('with targetKey', function () { 113 | beforeEach(createConnection); 114 | beforeEach(async function () { 115 | this.sandbox = sinon.sandbox.create(); 116 | 117 | this.User = this.connection.define('user', { 118 | someId: { 119 | type: Sequelize.INTEGER, 120 | field: 'some_id' 121 | } 122 | }); 123 | this.Project = this.connection.define('project', { 124 | ownerId: { 125 | type: Sequelize.INTEGER, 126 | field: 'owner_id' 127 | } 128 | }); 129 | 130 | this.Project.belongsTo(this.User, { 131 | foreignKey: 'ownerId', 132 | targetKey: 'someId', 133 | as: 'owner', 134 | constraints: false 135 | }); 136 | 137 | await this.connection.sync({ 138 | force: true 139 | }); 140 | 141 | [this.user1, this.user2] = await Promise.join( 142 | this.User.create({ id: randint(), someId: randint() }), 143 | this.User.create({ id: randint(), someId: randint() }) 144 | ); 145 | [this.project1, this.project2] = await this.Project.bulkCreate([ 146 | { id: randint() }, 147 | { id: randint() } 148 | ], { returning: true }); 149 | await Promise.join( 150 | this.project1.setOwner(this.user1), 151 | this.project2.setOwner(this.user2) 152 | ); 153 | 154 | this.sandbox.spy(this.User, 'findAll'); 155 | 156 | this.context = createContext(this.connection); 157 | }); 158 | afterEach(function () { 159 | this.sandbox.restore(); 160 | }); 161 | 162 | it('batches and caches to a single findAll call (createContext)', async function () { 163 | let user1 = this.project1.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}) 164 | , user2 = this.project2.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}); 165 | 166 | await expect(user1, 'to be fulfilled with', this.user1); 167 | await expect(user2, 'to be fulfilled with', this.user2); 168 | 169 | user1 = this.project1.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}); 170 | user2 = this.project2.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}); 171 | 172 | await expect(user1, 'to be fulfilled with', this.user1); 173 | await expect(user2, 'to be fulfilled with', this.user2); 174 | 175 | expect(this.User.findAll, 'was called once'); 176 | expect(this.User.findAll, 'to have a call satisfying', [{ 177 | where: { 178 | someId: [this.project1.get('ownerId'), this.project2.get('ownerId')] 179 | } 180 | }]); 181 | }); 182 | 183 | it('caches based on priming', async function () { 184 | this.context.prime(await this.User.findAll()); 185 | 186 | const user1 = this.project1.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}) 187 | , user2 = this.project2.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}); 188 | 189 | await expect(user1, 'to be fulfilled with', this.user1); 190 | await expect(user2, 'to be fulfilled with', this.user2); 191 | 192 | expect(this.User.findAll, 'was called once'); 193 | }); 194 | }); 195 | 196 | describe('paranoid', function () { 197 | beforeEach(createConnection); 198 | beforeEach(async function () { 199 | this.sandbox = sinon.sandbox.create(); 200 | 201 | this.User = this.connection.define('user', {}, { paranoid: true }); 202 | this.Project = this.connection.define('project'); 203 | 204 | this.Project.belongsTo(this.User, { 205 | as: 'owner', 206 | foreignKey: { 207 | name: 'ownerId', 208 | field: 'owner_id' 209 | } 210 | }); 211 | 212 | await this.connection.sync({ 213 | force: true 214 | }); 215 | 216 | [this.user1, this.user2] = await this.User.bulkCreate([ 217 | { id: randint(), deletedAt: new Date() }, 218 | { id: randint() } 219 | ], { returning: true }); 220 | [this.project1, this.project2] = await this.Project.bulkCreate([ 221 | { id: randint() }, 222 | { id: randint() }, 223 | ], { returning: true }); 224 | await Promise.join( 225 | this.project1.setOwner(this.user1), 226 | this.project2.setOwner(this.user2) 227 | ); 228 | 229 | this.sandbox.spy(this.User, 'findAll'); 230 | 231 | this.context = createContext(this.connection); 232 | }); 233 | afterEach(function () { 234 | this.sandbox.restore(); 235 | }); 236 | 237 | it('batches and caches to a single findAll call (paranoid)', async function () { 238 | let user1 = this.project1.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}) 239 | , user2 = this.project2.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}); 240 | 241 | await expect(user1, 'to be fulfilled with', null); 242 | await expect(user2, 'to be fulfilled with', this.user2); 243 | 244 | user1 = this.project1.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}); 245 | user2 = this.project2.getOwner({[EXPECTED_OPTIONS_KEY]: this.context}); 246 | 247 | await expect(user1, 'to be fulfilled with', null); 248 | await expect(user2, 'to be fulfilled with', this.user2); 249 | 250 | expect(this.User.findAll, 'was called once'); 251 | expect(this.User.findAll, 'to have a call satisfying', [{ 252 | where: { 253 | id: [this.user1.get('id'), this.user2.get('id')] 254 | } 255 | }]); 256 | }); 257 | 258 | it('batches and caches to a single findAll call (not paranoid)', async function () { 259 | let user1 = this.project1.getOwner({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}) 260 | , user2 = this.project2.getOwner({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 261 | 262 | await expect(user1, 'to be fulfilled with', this.user1); 263 | await expect(user2, 'to be fulfilled with', this.user2); 264 | 265 | user1 = this.project1.getOwner({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 266 | user2 = this.project2.getOwner({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 267 | 268 | await expect(user1, 'to be fulfilled with', this.user1); 269 | await expect(user2, 'to be fulfilled with', this.user2); 270 | 271 | expect(this.User.findAll, 'was called once'); 272 | expect(this.User.findAll, 'to have a call satisfying', [{ 273 | paranoid: false, 274 | where: { 275 | id: [this.user1.get('id'), this.user2.get('id')] 276 | } 277 | }]); 278 | }); 279 | }); 280 | }); 281 | -------------------------------------------------------------------------------- /test/integration/findByPk.test.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import {createConnection, randint} from '../helper'; 3 | import sinon from 'sinon'; 4 | import {createContext, removeContext, EXPECTED_OPTIONS_KEY} from '../../src'; 5 | import Promise from 'bluebird'; 6 | import expect from 'unexpected'; 7 | import {method} from '../../src/helper'; 8 | 9 | describe('findByPk', function () { 10 | describe('id primary key', function () { 11 | beforeEach(createConnection); 12 | beforeEach(async function () { 13 | this.sandbox = sinon.sandbox.create(); 14 | 15 | this.User = this.connection.define('user'); 16 | 17 | this.sandbox.spy(this.User, 'findAll'); 18 | 19 | await this.User.sync({ 20 | force: true 21 | }); 22 | 23 | [this.user0, this.user1, this.user2, this.user3] = await this.User.bulkCreate([ 24 | { id: '0' }, 25 | { id: randint() }, 26 | { id: randint() }, 27 | { id: randint() } 28 | ], { returning: true }); 29 | 30 | this.context = createContext(this.connection); 31 | this.method = method(this.User, 'findByPk'); 32 | }); 33 | afterEach(function () { 34 | this.sandbox.restore(); 35 | }); 36 | 37 | it('works with null', async function () { 38 | const userNull = this.User[this.method](null, {[EXPECTED_OPTIONS_KEY]: this.context}); 39 | 40 | await expect(userNull, 'to be fulfilled with', null); 41 | expect(this.User.findAll, 'was not called'); 42 | }); 43 | 44 | it('works with id of 0', async function () { 45 | const user0 = await this.User[this.method](0, {[EXPECTED_OPTIONS_KEY]: this.context}); 46 | 47 | expect(user0.get('id'), 'to equal', 0); 48 | expect(this.User.findAll, 'was called once'); 49 | }); 50 | 51 | it('batches and caches to a single findAll call (createContext)', async function () { 52 | let user1 = this.User[this.method](this.user1.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}) 53 | , user2 = this.User[this.method](this.user2.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}); 54 | 55 | await expect(user1, 'to be fulfilled with', this.user1); 56 | await expect(user2, 'to be fulfilled with', this.user2); 57 | 58 | user1 = this.User[this.method](this.user1.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}); 59 | user2 = this.User[this.method](this.user2.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}); 60 | 61 | await expect(user1, 'to be fulfilled with', this.user1); 62 | await expect(user2, 'to be fulfilled with', this.user2); 63 | 64 | expect(this.User.findAll, 'was called once'); 65 | expect(this.User.findAll, 'to have a call satisfying', [{ 66 | where: { 67 | id: [this.user1.get('id'), this.user2.get('id')] 68 | } 69 | }]); 70 | }); 71 | 72 | it('supports rejectOnEmpty', async function () { 73 | const user1 = this.User[this.method](this.user1.get('id'), { rejectOnEmpty: true, [EXPECTED_OPTIONS_KEY]: this.context }) 74 | , user2 = this.User[this.method](42, { rejectOnEmpty: true, [EXPECTED_OPTIONS_KEY]: this.context }) 75 | , user3 = this.User[this.method](42, { [EXPECTED_OPTIONS_KEY]: this.context }); 76 | 77 | await expect(user1, 'to be fulfilled with', this.user1); 78 | await expect(user2, 'to be rejected'); 79 | await expect(user3, 'to be fulfilled with', null); 80 | }); 81 | 82 | it('supports raw/attributes', async function () { 83 | await Promise.all([ 84 | this.User[this.method](this.user1.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}), 85 | this.User[this.method](this.user2.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context, raw: true}), 86 | this.User[this.method](this.user3.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context, raw: true}) 87 | ]); 88 | 89 | expect(this.User.findAll, 'was called twice'); 90 | expect(this.User.findAll, 'to have a call satisfying', [{ 91 | where: { 92 | id: [this.user1.get('id')] 93 | } 94 | }]); 95 | expect(this.User.findAll, 'to have a call satisfying', [{ 96 | raw: true, 97 | where: { 98 | id: [this.user2.get('id'), this.user3.get('id')] 99 | } 100 | }]); 101 | }); 102 | 103 | it('works if model method is shimmed', async function () { 104 | removeContext(this.connection); 105 | 106 | const original = this.User[this.method]; 107 | this.User[this.method] = function (...args) { 108 | return original.call(this, ...args); 109 | }; 110 | 111 | this.context = createContext(this.connection); 112 | 113 | let user1 = this.User[this.method](this.user1.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}) 114 | , user2 = this.User[this.method](this.user2.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}); 115 | 116 | await expect(user1, 'to be fulfilled with', this.user1); 117 | await expect(user2, 'to be fulfilled with', this.user2); 118 | 119 | user1 = this.User[this.method](this.user1.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}); 120 | user2 = this.User[this.method](this.user2.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}); 121 | 122 | await expect(user1, 'to be fulfilled with', this.user1); 123 | await expect(user2, 'to be fulfilled with', this.user2); 124 | 125 | expect(this.User.findAll, 'was called once'); 126 | expect(this.User.findAll, 'to have a call satisfying', [{ 127 | where: { 128 | id: [this.user1.get('id'), this.user2.get('id')] 129 | } 130 | }]); 131 | }); 132 | }); 133 | 134 | describe('other primary key', function () { 135 | beforeEach(createConnection); 136 | beforeEach(async function () { 137 | this.sandbox = sinon.sandbox.create(); 138 | 139 | this.User = this.connection.define('user', { 140 | identifier: { 141 | primaryKey: true, 142 | type: Sequelize.INTEGER 143 | } 144 | }); 145 | 146 | this.sandbox.spy(this.User, 'findAll'); 147 | 148 | await this.User.sync({ 149 | force: true 150 | }); 151 | 152 | [this.user1, this.user2, this.user3] = await this.User.bulkCreate([ 153 | { identifier: randint() }, 154 | { identifier: randint() }, 155 | { identifier: randint() } 156 | ], { returning: true }); 157 | 158 | this.context = createContext(this.connection); 159 | this.method = method(this.User, 'findByPk'); 160 | }); 161 | afterEach(function () { 162 | this.sandbox.restore(); 163 | }); 164 | 165 | it('batches to a single findAll call', async function () { 166 | const user1 = this.User[this.method](this.user1.get('identifier'), {[EXPECTED_OPTIONS_KEY]: this.context}) 167 | , user2 = this.User[this.method](this.user2.get('identifier'), {[EXPECTED_OPTIONS_KEY]: this.context}); 168 | 169 | await expect(user1, 'to be fulfilled with', this.user1); 170 | await expect(user2, 'to be fulfilled with', this.user2); 171 | 172 | expect(this.User.findAll, 'was called once'); 173 | expect(this.User.findAll, 'to have a call satisfying', [{ 174 | where: { 175 | identifier: [this.user1.get('identifier'), this.user2.get('identifier')] 176 | } 177 | }]); 178 | }); 179 | 180 | it('batches and caches to a single findAll call (createContext)', async function () { 181 | let user1 = this.User[this.method](this.user1.get('identifier'), {[EXPECTED_OPTIONS_KEY]: this.context}) 182 | , user2 = this.User[this.method](this.user2.get('identifier'), {[EXPECTED_OPTIONS_KEY]: this.context}); 183 | 184 | await expect(user1, 'to be fulfilled with', this.user1); 185 | await expect(user2, 'to be fulfilled with', this.user2); 186 | 187 | user1 = this.User[this.method](this.user1.get('identifier'), {[EXPECTED_OPTIONS_KEY]: this.context}); 188 | user2 = this.User[this.method](this.user2.get('identifier'), {[EXPECTED_OPTIONS_KEY]: this.context}); 189 | 190 | await expect(user1, 'to be fulfilled with', this.user1); 191 | await expect(user2, 'to be fulfilled with', this.user2); 192 | 193 | expect(this.User.findAll, 'was called once'); 194 | expect(this.User.findAll, 'to have a call satisfying', [{ 195 | where: { 196 | identifier: [this.user1.get('identifier'), this.user2.get('identifier')] 197 | } 198 | }]); 199 | }); 200 | }); 201 | 202 | describe('primary key with field', function () { 203 | beforeEach(createConnection); 204 | beforeEach(async function () { 205 | this.sandbox = sinon.sandbox.create(); 206 | 207 | this.User = this.connection.define('user', { 208 | id: { 209 | primaryKey: true, 210 | type: Sequelize.INTEGER, 211 | field: 'identifier' 212 | } 213 | }); 214 | 215 | this.sandbox.spy(this.User, 'findAll'); 216 | 217 | await this.User.sync({ 218 | force: true 219 | }); 220 | 221 | [this.user1, this.user2, this.user3] = await this.User.bulkCreate([ 222 | { id: randint() }, 223 | { id: randint() }, 224 | { id: randint() } 225 | ], { returning: true }); 226 | 227 | this.context = createContext(this.connection); 228 | this.method = method(this.User, 'findByPk'); 229 | }); 230 | afterEach(function () { 231 | this.sandbox.restore(); 232 | }); 233 | 234 | it('batches to a single findAll call', async function () { 235 | const user1 = this.User[this.method](this.user1.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}) 236 | , user2 = this.User[this.method](this.user2.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}); 237 | 238 | await expect(user1, 'to be fulfilled with', this.user1); 239 | await expect(user2, 'to be fulfilled with', this.user2); 240 | 241 | expect(this.User.findAll, 'was called once'); 242 | expect(this.User.findAll, 'to have a call satisfying', [{ 243 | where: { 244 | id: [this.user1.get('id'), this.user2.get('id')] 245 | } 246 | }]); 247 | }); 248 | }); 249 | 250 | describe('paranoid', function () { 251 | beforeEach(createConnection); 252 | beforeEach(async function () { 253 | this.sandbox = sinon.sandbox.create(); 254 | 255 | this.User = this.connection.define('user', {}, { paranoid: true }); 256 | 257 | this.sandbox.spy(this.User, 'findAll'); 258 | 259 | await this.User.sync({ 260 | force: true 261 | }); 262 | 263 | [this.user1, this.user2] = await this.User.bulkCreate([ 264 | { id: randint(), deletedAt: new Date() }, 265 | { id: randint() } 266 | ], { returning: true }); 267 | 268 | this.context = createContext(this.connection); 269 | this.method = method(this.User, 'findByPk'); 270 | }); 271 | afterEach(function () { 272 | this.sandbox.restore(); 273 | }); 274 | 275 | it('batches and caches to a single findAll call (paranoid)', async function () { 276 | let user1 = this.User[this.method](this.user1.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}) 277 | , user2 = this.User[this.method](this.user2.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}); 278 | 279 | await expect(user1, 'to be fulfilled with', null); 280 | await expect(user2, 'to be fulfilled with', this.user2); 281 | 282 | user1 = this.User[this.method](this.user1.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}); 283 | user2 = this.User[this.method](this.user2.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context}); 284 | 285 | await expect(user1, 'to be fulfilled with', null); 286 | await expect(user2, 'to be fulfilled with', this.user2); 287 | 288 | expect(this.User.findAll, 'was called once'); 289 | expect(this.User.findAll, 'to have a call satisfying', [{ 290 | where: { 291 | id: [this.user1.get('id'), this.user2.get('id')] 292 | } 293 | }]); 294 | }); 295 | 296 | it('batches and caches to a single findAll call (not paranoid)', async function () { 297 | let user1 = this.User[this.method](this.user1.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}) 298 | , user2 = this.User[this.method](this.user2.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 299 | 300 | await expect(user1, 'to be fulfilled with', this.user1); 301 | await expect(user2, 'to be fulfilled with', this.user2); 302 | 303 | user1 = this.User[this.method](this.user1.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 304 | user2 = this.User[this.method](this.user2.get('id'), {[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 305 | 306 | await expect(user1, 'to be fulfilled with', this.user1); 307 | await expect(user2, 'to be fulfilled with', this.user2); 308 | 309 | expect(this.User.findAll, 'was called once'); 310 | expect(this.User.findAll, 'to have a call satisfying', [{ 311 | paranoid: false, 312 | where: { 313 | id: [this.user1.get('id'), this.user2.get('id')] 314 | } 315 | }]); 316 | }); 317 | }); 318 | }); 319 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import shimmer from 'shimmer'; 3 | import DataLoader from 'dataloader'; 4 | import {groupBy, property, values, clone, isEmpty, uniq} from 'lodash'; 5 | import LRU from 'lru-cache'; 6 | import assert from 'assert'; 7 | import {methods} from './helper'; 8 | 9 | const versionTestRegEx = /^[456]/; 10 | 11 | function mapResult(attribute, keys, options, result) { 12 | // Convert an array of results to an object of attribute (primary / foreign / target key) -> array of matching rows 13 | if (Array.isArray(attribute) && options && options.multiple && !options.raw) { 14 | // Regular belongs to many 15 | let [throughAttribute, foreignKey] = attribute; 16 | result = result.reduce((carry, row) => { 17 | for (const throughRow of row.get(throughAttribute)) { 18 | let key = throughRow[foreignKey]; 19 | if (!(key in carry)) { 20 | carry[key] = []; 21 | } 22 | 23 | carry[key].push(row); 24 | } 25 | 26 | return carry; 27 | }, {}); 28 | } else { 29 | if (Array.isArray(attribute)) { 30 | // Belongs to many count is a raw query, so we have to get the attribute directly 31 | attribute = attribute.join('.'); 32 | } 33 | result = groupBy(result, property(attribute)); 34 | } 35 | 36 | return keys.map(key => { 37 | if (key in result) { 38 | let value = result[key]; 39 | 40 | return options && options.multiple ? value : value[0]; 41 | } 42 | return options && options.multiple ? [] : null; 43 | }); 44 | } 45 | 46 | function stringifyValue(value, key) { 47 | if (value && value.associationType) { 48 | return `${value.associationType},${value.target.name},${value.as}`; 49 | } else if (Array.isArray(value)) { 50 | // the list of attributes' order doesn't matter, 51 | // but we assumer that order does matter for any other array-like option 52 | if (key === 'attributes') { 53 | value = clone(value).sort(); 54 | } 55 | return value.map(stringifyValue).join(','); 56 | } else if (typeof value === 'object' && value !== null) { 57 | if (value instanceof Date) { 58 | return value.toJSON(); 59 | } 60 | return stringifyObject(value); 61 | } 62 | return value; 63 | } 64 | 65 | // This is basically a home-grown JSON.stringifier. However, JSON.stringify on objects 66 | // depends on the order in which the properties were defined - which we don't like! 67 | // Additionally, JSON.stringify escapes strings, which we don't need here 68 | function stringifyObject(object, keys = [...Object.keys(object), ...Object.getOwnPropertySymbols(object)]) { 69 | return keys.sort((lhs, rhs) => { 70 | const l = lhs.toString(); 71 | const r = rhs.toString(); 72 | if (l > r) return 1; 73 | if (l < r) return -1; 74 | return 0; 75 | }).map(key => `${key.toString()}:${stringifyValue(object[key], key)}`).join('|'); 76 | } 77 | 78 | export function getCacheKey(model, attribute, options) { 79 | options = stringifyObject(options, ['association', 'attributes', 'groupedLimit', 'limit', 'offset', 'order', 'where', 'through', 'raw', 'searchPath', 'paranoid']); 80 | 81 | let name = `${model.name}|${attribute}|${options}`; 82 | const schema = model.options && model.options.schema; 83 | if (schema) { 84 | name = `${schema}|${name}`; 85 | } 86 | return name; 87 | } 88 | 89 | function mergeWhere(where, optionsWhere) { 90 | if (optionsWhere) { 91 | return { 92 | [Sequelize.Op ? Sequelize.Op.and : '$and']: [where, optionsWhere] 93 | }; 94 | } 95 | return where; 96 | } 97 | 98 | function rejectOnEmpty(options, result) { 99 | if (isEmpty(result) && options.rejectOnEmpty) { 100 | if (typeof options.rejectOnEmpty === 'function') { 101 | throw new options.rejectOnEmpty(); 102 | } else if (typeof options.rejectOnEmpty === 'object') { 103 | throw options.rejectOnEmpty; 104 | } else { 105 | throw new Sequelize.EmptyResultError(); 106 | } 107 | } 108 | 109 | return result; 110 | } 111 | 112 | function loaderForBTM(model, joinTableName, foreignKey, foreignKeyField, options = {}) { 113 | assert(options.include === undefined, 'options.include is not supported by model loader'); 114 | assert(options.association !== undefined, 'options.association should be set for BTM loader'); 115 | 116 | let attributes = [joinTableName, foreignKey]; 117 | const association = options.association; 118 | delete options.association; 119 | 120 | return new DataLoader(keys => { 121 | let findOptions = Object.assign({}, options); 122 | delete findOptions.rejectOnEmpty; 123 | if (findOptions.limit) { 124 | const limit = findOptions.offset && findOptions.offset > 0 ? [findOptions.limit, findOptions.offset] : findOptions.limit; 125 | findOptions.groupedLimit = { 126 | through: options.through, 127 | on: association, 128 | limit, 129 | values: uniq(keys) 130 | }; 131 | } else { 132 | 133 | const attributes = options.through && options.through.attributes ? [...options.through.attributes, foreignKey] : [foreignKey]; 134 | 135 | findOptions.include = [{ 136 | attributes, 137 | association: association.manyFromSource, 138 | where: { 139 | [foreignKeyField]: keys, 140 | ...options.through.where 141 | } 142 | }]; 143 | } 144 | 145 | return model.findAll(findOptions).then(mapResult.bind(null, attributes, keys, findOptions)); 146 | }, { 147 | cache: options.cache 148 | }); 149 | } 150 | 151 | function loaderForModel(model, attribute, attributeField, options = {}) { 152 | assert(options.include === undefined, 'options.include is not supported by model loader'); 153 | 154 | return new DataLoader(keys => { 155 | const findOptions = Object.assign({}, options); 156 | delete findOptions.rejectOnEmpty; 157 | 158 | if (findOptions.limit && keys.length > 1) { 159 | const limit = findOptions.offset && findOptions.offset > 0 ? [findOptions.limit, findOptions.offset] : findOptions.limit; 160 | findOptions.groupedLimit = { 161 | limit, 162 | on: attributeField, 163 | values: uniq(keys) 164 | }; 165 | delete findOptions.limit; 166 | delete findOptions.offset; 167 | } else { 168 | findOptions.where = mergeWhere({ 169 | [attributeField]: keys 170 | }, findOptions.where); 171 | } 172 | 173 | return model.findAll(findOptions).then(mapResult.bind(null, attribute, keys, findOptions)); 174 | }, { 175 | cache: options.cache 176 | }); 177 | } 178 | 179 | function shimModel(target) { 180 | if (target.findByPk ? target.findByPk.__wrapped : target.findById.__wrapped) return; 181 | 182 | shimmer.massWrap(target, methods(Sequelize.version).findByPk, original => { 183 | return function batchedFindById(id, options = {}) { 184 | if (options.transaction || options.include || activeClsTransaction() || !options[EXPECTED_OPTIONS_KEY]) { 185 | return original.apply(this, arguments); 186 | } 187 | 188 | return Promise.resolve().then(() => { 189 | if ([null, undefined].indexOf(id) !== -1) { 190 | return Promise.resolve(null); 191 | } 192 | 193 | const loaders = options[EXPECTED_OPTIONS_KEY].loaders; 194 | let loader = loaders[this.name].byPrimaryKey; 195 | if (options.raw || options.paranoid === false) { 196 | const cacheKey = getCacheKey(this, this.primaryKeyAttribute, { raw: options.raw, paranoid: options.paranoid }); 197 | loader = loaders.autogenerated.get(cacheKey); 198 | if (!loader) { 199 | loader = createModelAttributeLoader(this, this.primaryKeyAttribute, { raw: options.raw, paranoid: options.paranoid, logging: options.logging }); 200 | loaders.autogenerated.set(cacheKey, loader); 201 | } 202 | } 203 | return loader.load(id); 204 | }).then(rejectOnEmpty.bind(null, options)); 205 | }; 206 | }); 207 | } 208 | 209 | function shimBelongsTo(target) { 210 | if (target.get.__wrapped) return; 211 | 212 | shimmer.wrap(target, 'get', original => { 213 | return function batchedGetBelongsTo(instance, options = {}) { 214 | if (Array.isArray(instance) || options.include || options.transaction || activeClsTransaction() || !options[EXPECTED_OPTIONS_KEY] || options.where) { 215 | return original.apply(this, arguments); 216 | } 217 | 218 | return Promise.resolve().then(() => { 219 | const foreignKeyValue = instance.get(this.foreignKey); 220 | if (foreignKeyValue === undefined || foreignKeyValue === null) { 221 | return Promise.resolve(null); 222 | } 223 | 224 | const loaders = options[EXPECTED_OPTIONS_KEY].loaders; 225 | let loader = loaders[this.target.name].bySingleAttribute[this.targetKey]; 226 | if (options.raw || options.paranoid === false) { 227 | const cacheKey = getCacheKey(this.target, this.targetKey, { raw: options.raw, paranoid: options.paranoid }); 228 | loader = loaders.autogenerated.get(cacheKey); 229 | if (!loader) { 230 | loader = createModelAttributeLoader(this.target, this.targetKey, { raw: options.raw, paranoid: options.paranoid, logging: options.logging }); 231 | loaders.autogenerated.set(cacheKey, loader); 232 | } 233 | } 234 | return Promise.resolve(loader.load(foreignKeyValue)); 235 | }).then(rejectOnEmpty.bind(null, options)); 236 | }; 237 | }); 238 | } 239 | 240 | function shimHasOne(target) { 241 | if (target.get.__wrapped) return; 242 | 243 | shimmer.wrap(target, 'get', original => { 244 | return function batchedGetHasOne(instance, options = {}) { 245 | if (Array.isArray(instance) || options.include || options.transaction || activeClsTransaction() || !options[EXPECTED_OPTIONS_KEY]) { 246 | return original.apply(this, arguments); 247 | } 248 | 249 | return Promise.resolve().then(() => { 250 | const sourceKey = instance.get(this.sourceKey); 251 | 252 | const loaders = options[EXPECTED_OPTIONS_KEY].loaders; 253 | let loader = loaders[this.target.name].bySingleAttribute[this.foreignKey]; 254 | if (options.raw || options.paranoid === false) { 255 | const cacheKey = getCacheKey(this.target, this.foreignKey, { raw: options.raw, paranoid: options.paranoid }); 256 | loader = loaders.autogenerated.get(cacheKey); 257 | if (!loader) { 258 | loader = createModelAttributeLoader(this.target, this.foreignKey, { raw: options.raw, paranoid: options.paranoid, logging: options.logging }); 259 | loaders.autogenerated.set(cacheKey, loader); 260 | } 261 | } 262 | return loader.load(sourceKey); 263 | }).then(rejectOnEmpty.bind(null, options)); 264 | }; 265 | }); 266 | } 267 | 268 | function shimHasMany(target) { 269 | if (target.get.__wrapped) return; 270 | 271 | shimmer.wrap(target, 'get', original => { 272 | return function batchedGetHasMany(instances, options = {}) { 273 | let isCount = false; 274 | if (options.include || options.transaction || options.separate || activeClsTransaction() || !options[EXPECTED_OPTIONS_KEY]) { 275 | return original.apply(this, arguments); 276 | } 277 | 278 | const attributes = options.attributes; 279 | if (attributes && attributes.length === 1 && attributes[0][0].fn && attributes[0][0].fn === 'COUNT' && !options.group) { 280 | // Phew, what an if statement - It avoids duplicating the count code from sequelize, 281 | // at the expense of slightly tighter coupling to the sequelize implementation 282 | options.attributes.push(this.foreignKey); 283 | options.multiple = false; 284 | options.group = [this.foreignKey]; 285 | delete options.plain; 286 | isCount = true; 287 | } 288 | 289 | if (this.scope) { 290 | options.where = { 291 | [Sequelize.Op ? Sequelize.Op.and : '$and']: [ 292 | options.where, 293 | this.scope 294 | ] 295 | }; 296 | } 297 | 298 | let loader 299 | , loaderOptions = { 300 | multiple: true, 301 | ...options 302 | }; 303 | 304 | const cacheKey = getCacheKey(this.target, this.foreignKey, loaderOptions); 305 | loader = options[EXPECTED_OPTIONS_KEY].loaders.autogenerated.get(cacheKey); 306 | if (!loader) { 307 | loader = loaderForModel(this.target, this.foreignKey, this.foreignKeyField, { 308 | ...loaderOptions, 309 | cache: true 310 | }); 311 | options[EXPECTED_OPTIONS_KEY].loaders.autogenerated.set(cacheKey, loader); 312 | } 313 | 314 | let key = this.sourceKey || this.source.primaryKeyAttribute; 315 | 316 | if (Array.isArray(instances)) { 317 | return Promise.map(instances, instance => { 318 | let sourceKeyValue = instance.get(key); 319 | 320 | if (sourceKeyValue === undefined || sourceKeyValue === null) { 321 | return Promise.resolve(null); 322 | } 323 | 324 | return loader.load(sourceKeyValue); 325 | }); 326 | } else { 327 | let sourceKeyValue = instances.get(key); 328 | 329 | if (sourceKeyValue === undefined || sourceKeyValue === null) { 330 | return Promise.resolve(null); 331 | } 332 | 333 | return Promise.resolve(loader.load(sourceKeyValue)).then(result => { 334 | if (isCount && !result) { 335 | result = { count: 0 }; 336 | } 337 | return result; 338 | }).then(rejectOnEmpty.bind(null, options)); 339 | } 340 | }; 341 | }); 342 | } 343 | 344 | function shimBelongsToMany(target) { 345 | if (target.get.__wrapped) return; 346 | 347 | shimmer.wrap(target, 'get', original => { 348 | return function batchedGetBelongsToMany(instances, options = {}) { 349 | let isCount = false; 350 | assert(this.paired, '.paired missing on belongsToMany association. You need to set up both sides of the association'); 351 | 352 | if (options.include || options.transaction || activeClsTransaction() || !options[EXPECTED_OPTIONS_KEY]) { 353 | return original.apply(this, arguments); 354 | } 355 | 356 | const attributes = options.attributes; 357 | if (attributes && attributes.length === 1 && attributes[0][0].fn && attributes[0][0].fn === 'COUNT' && !options.group) { 358 | // Phew, what an if statement - It avoids duplicating the count code from sequelize, 359 | // at the expense of slightly tighter coupling to the sequelize implementation 360 | options.multiple = false; 361 | options.group = [`${this.paired.manyFromSource.as}.${this.identifierField}`]; 362 | delete options.plain; 363 | isCount = true; 364 | } 365 | 366 | if (this.scope) { 367 | options.where = { 368 | [Sequelize.Op ? Sequelize.Op.and : '$and']: [ 369 | options.where, 370 | this.scope 371 | ] 372 | }; 373 | } 374 | 375 | options.through = options.through || {}; 376 | if (this.through.scope) { 377 | options.through.where = { 378 | [Sequelize.Op ? Sequelize.Op.and : '$and']: [ 379 | options.through.where, 380 | this.through.scope 381 | ] 382 | }; 383 | } 384 | 385 | let loader 386 | , loaderOptions = { 387 | association: this.paired, 388 | multiple: true, 389 | ...options 390 | }; 391 | 392 | const cacheKey = getCacheKey(this.target, [this.paired.manyFromSource.as, this.foreignKey], loaderOptions); 393 | loader = options[EXPECTED_OPTIONS_KEY].loaders.autogenerated.get(cacheKey); 394 | if (!loader) { 395 | loader = loaderForBTM(this.target, this.paired.manyFromSource.as, this.foreignKey, this.identifierField, { 396 | ...loaderOptions, 397 | cache: true 398 | }); 399 | options[EXPECTED_OPTIONS_KEY].loaders.autogenerated.set(cacheKey, loader); 400 | } 401 | 402 | if (Array.isArray(instances)) { 403 | return Promise.map(instances, instance => loader.load(instance.get(this.source.primaryKeyAttribute))); 404 | } else { 405 | return Promise.resolve(loader.load(instances.get(this.source.primaryKeyAttribute))).then(result => { 406 | if (isCount && !result) { 407 | result = { count: 0 }; 408 | } 409 | return result; 410 | }).then(rejectOnEmpty.bind(null, options)); 411 | } 412 | }; 413 | }); 414 | } 415 | 416 | function activeClsTransaction() { 417 | if (versionTestRegEx.test(Sequelize.version)) { 418 | if (Sequelize._cls && Sequelize._cls.get('transaction')) { 419 | return true; 420 | } 421 | } else if (Sequelize.cls && Sequelize.cls.get('transaction')) { 422 | return true; 423 | } 424 | return false; 425 | } 426 | 427 | export const EXPECTED_OPTIONS_KEY = 'dataloader_sequelize_context'; 428 | export function createContext(sequelize, options = {}) { 429 | const loaders = {}; 430 | 431 | shimModel(versionTestRegEx.test(sequelize.constructor.version) ? // v3 vs v4 432 | sequelize.constructor.Model : sequelize.constructor.Model.prototype); 433 | shimBelongsTo(sequelize.constructor.Association.BelongsTo.prototype); 434 | shimHasOne(sequelize.constructor.Association.HasOne.prototype); 435 | shimHasMany(sequelize.constructor.Association.HasMany.prototype); 436 | shimBelongsToMany(sequelize.constructor.Association.BelongsToMany.prototype); 437 | 438 | loaders.autogenerated = LRU({max: options.max || 500}); 439 | 440 | for (const Model of Object.values(sequelize.models)) { 441 | shimModel(Model); 442 | loaders[Model.name] = { 443 | bySingleAttribute: {} 444 | }; 445 | loaders[Model.name].bySingleAttribute[Model.primaryKeyAttribute] = createModelAttributeLoader(Model, Model.primaryKeyAttribute, options); 446 | loaders[Model.name].byId = loaders[Model.name].byPrimaryKey = loaders[Model.name].bySingleAttribute[Model.primaryKeyAttribute]; 447 | } 448 | 449 | for (const Model of Object.values(sequelize.models)) { 450 | values(Model.associations).forEach(association => { 451 | if (association.associationType === 'BelongsTo') { 452 | const Target = association.target; 453 | if (association.targetKey !== Target.primaryKeyAttribute) { 454 | loaders[Target.name].bySingleAttribute[association.targetKey] = createModelAttributeLoader(Target, association.targetKey, options); 455 | } 456 | } else if (association.associationType === 'HasOne') { 457 | const Target = association.target; 458 | loaders[Target.name].bySingleAttribute[association.foreignKey] = createModelAttributeLoader(Target, association.foreignKey, options); 459 | } 460 | }); 461 | } 462 | 463 | function prime(results) { 464 | if (!Array.isArray(results)) { 465 | results = [results]; 466 | } 467 | 468 | results.forEach(result => { 469 | const modelName = result.Model ? result.Model.name : result.constructor.name; 470 | Object.keys(loaders[modelName].bySingleAttribute).forEach(attribute => { 471 | loaders[modelName].bySingleAttribute[attribute].prime(result.get(attribute), result); 472 | }); 473 | }); 474 | } 475 | 476 | return {loaders, prime}; 477 | } 478 | 479 | export function removeContext(sequelize) { 480 | const Model = versionTestRegEx.test(sequelize.constructor.version) ? // v3 vs v4 481 | sequelize.constructor.Model : sequelize.constructor.Model.prototype; 482 | 483 | shimmer.massUnwrap(Model, methods(Sequelize.version).findByPk); 484 | shimmer.unwrap(sequelize.constructor.Association.BelongsTo.prototype, 'get'); 485 | shimmer.unwrap(sequelize.constructor.Association.HasOne.prototype, 'get'); 486 | shimmer.unwrap(sequelize.constructor.Association.HasMany.prototype, 'get'); 487 | shimmer.unwrap(sequelize.constructor.Association.BelongsToMany.prototype, 'get'); 488 | } 489 | 490 | function createModelAttributeLoader(Model, attribute, options = {}) { 491 | return new DataLoader(keys => { 492 | return Model.findAll({ 493 | ...options, 494 | where: { 495 | [attribute]: keys 496 | } 497 | }).then(mapResult.bind(null, attribute, keys, {})); 498 | }, { 499 | cache: true 500 | }); 501 | } 502 | -------------------------------------------------------------------------------- /test/integration/belongsToMany.test.js: -------------------------------------------------------------------------------- 1 | import {createConnection, randint} from '../helper'; 2 | import {intersection} from 'lodash'; 3 | import sinon from 'sinon'; 4 | import {createContext, EXPECTED_OPTIONS_KEY} from '../../src'; 5 | import Promise from 'bluebird'; 6 | import expect from 'unexpected'; 7 | import Sequelize from 'sequelize'; 8 | 9 | const setups = [ 10 | ['string through', context => { 11 | context.Project.Users = context.Project.belongsToMany(context.User, { 12 | as: 'members', 13 | through: 'project_members', 14 | sourceKey: 'id', 15 | targetKey: 'id', 16 | foreignKey: { 17 | name: 'projectId', 18 | field: 'project_id' 19 | }, 20 | }); 21 | context.User.belongsToMany(context.Project, { 22 | through: 'project_members', 23 | sourceKey: 'id', 24 | targetKey: 'id', 25 | foreignKey: { 26 | name: 'userId', 27 | field: 'user_id' 28 | }, 29 | }); 30 | }], 31 | ['model through', context => { 32 | context.ProjectMembers = context.connection.define('project_members', { 33 | projectId: { 34 | type: Sequelize.INTEGER, 35 | field: 'project_id' 36 | }, 37 | userId: { 38 | type: Sequelize.INTEGER, 39 | field: 'user_id' 40 | } 41 | }); 42 | context.Project.Users = context.Project.belongsToMany(context.User, { 43 | as: 'members', 44 | through: context.ProjectMembers, 45 | foreignKey: 'projectId', 46 | targetKey: 'id' 47 | }); 48 | context.User.belongsToMany(context.Project, { 49 | through: context.ProjectMembers, 50 | foreignKey: 'userId', 51 | targetKey: 'id' 52 | }); 53 | }] 54 | ]; 55 | 56 | async function createData() { 57 | [this.project1, this.project2, this.project3, this.project4, this.project5] = await this.Project.bulkCreate([ 58 | { id: randint() }, 59 | { id: randint() }, 60 | { id: randint() }, 61 | { id: randint() }, 62 | { id: randint() } 63 | ], {returning: true}); 64 | this.users = await this.User.bulkCreate([ 65 | { id: randint(), awesome: false }, 66 | { id: randint(), awesome: true }, 67 | { id: randint(), awesome: true }, 68 | { id: randint(), awesome: false }, 69 | { id: randint(), awesome: true }, 70 | { id: randint(), awesome: false }, 71 | { id: randint(), awesome: true }, 72 | { id: randint(), awesome: true }, 73 | { id: randint(), awesome: true }, 74 | { id: randint(), awesome: false }, 75 | { id: randint(), awesome: true } 76 | ], {returning: true}); 77 | 78 | await this.project1.setMembers(this.users.slice(0, 4)); 79 | await this.project2.setMembers(this.users.slice(3, 7)); 80 | await this.project3.setMembers(this.users.slice(6, 9)); 81 | await this.project5.setMembers(this.users.slice(9, 11)); 82 | 83 | await this.User.update({ deletedAt: new Date() }, { 84 | where: { 85 | id: [this.users[9].get('id')] 86 | } 87 | }); 88 | } 89 | 90 | describe('belongsToMany', function () { 91 | beforeEach(async function () { 92 | this.sandbox = sinon.sandbox.create(); 93 | }); 94 | 95 | before(createConnection); 96 | 97 | setups.forEach(([description, setup]) => { 98 | describe(description, function () { 99 | describe('simple association', function () { 100 | before(async function () { 101 | this.User = this.connection.define('user', { 102 | name: Sequelize.STRING, 103 | awesome: Sequelize.BOOLEAN, 104 | deletedAt: Sequelize.DATE, 105 | }, { 106 | paranoid: true, 107 | }); 108 | this.Project = this.connection.define('project'); 109 | 110 | setup(this); 111 | 112 | await this.connection.sync({ force: true }); 113 | await createData.call(this); 114 | 115 | this.context = createContext(this.connection); 116 | }); 117 | 118 | beforeEach(function () { 119 | this.sandbox = sinon.sandbox.create(); 120 | 121 | this.sandbox.spy(this.User, 'findAll'); 122 | }); 123 | 124 | afterEach(function () { 125 | this.sandbox.restore(); 126 | }); 127 | 128 | it('batches/caches to a single findAll call when getting (createContext)', async function () { 129 | let members1 = this.project1.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 130 | , members2 = this.project2.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 131 | 132 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 133 | this.users[0], 134 | this.users[1], 135 | this.users[2], 136 | this.users[3], 137 | ]); 138 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 139 | this.users[3], 140 | this.users[4], 141 | this.users[5], 142 | this.users[6] 143 | ]); 144 | 145 | members1 = this.project1.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 146 | members2 = this.project2.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 147 | 148 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 149 | this.users[0], 150 | this.users[1], 151 | this.users[2], 152 | this.users[3], 153 | ]); 154 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 155 | this.users[3], 156 | this.users[4], 157 | this.users[5], 158 | this.users[6] 159 | ]); 160 | 161 | expect(this.User.findAll, 'was called once'); 162 | expect(this.User.findAll, 'to have a call satisfying', [{ 163 | include: [{ 164 | association: this.Project.Users.manyFromTarget, 165 | where: { project_id: [ this.project1.get('id'), this.project2.get('id') ] } 166 | }] 167 | }]); 168 | }); 169 | 170 | it('supports rejectOnEmpty', async function () { 171 | let members1 = this.project1.getMembers({ [EXPECTED_OPTIONS_KEY]: this.context, rejectOnEmpty: true }) 172 | , members2 = this.project4.getMembers({ [EXPECTED_OPTIONS_KEY]: this.context, rejectOnEmpty: true }) 173 | , members3 = this.project4.getMembers({ [EXPECTED_OPTIONS_KEY]: this.context }); 174 | 175 | await expect(members1, 'to be fulfilled with', Array); 176 | await expect(members2, 'to be rejected'); 177 | await expect(members3, 'to be fulfilled with', []); 178 | }); 179 | 180 | it('batches to a single findAll call when counting', async function () { 181 | let project4 = await this.Project.create(); 182 | 183 | let members1 = this.project1.countMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 184 | , members2 = this.project2.countMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 185 | , members3 = project4.countMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 186 | 187 | await expect(members1, 'to be fulfilled with', 4); 188 | await expect(members2, 'to be fulfilled with', 4); 189 | await expect(members3, 'to be fulfilled with', 0); 190 | 191 | expect(this.User.findAll, 'was called once'); 192 | expect(this.User.findAll, 'to have a call satisfying', [{ 193 | attributes: [ 194 | [this.connection.fn('COUNT', this.connection.col(['user', 'id'].join('.'))), 'count'] 195 | ], 196 | include: [{ 197 | attributes: [ 198 | 'projectId' 199 | ], 200 | association: this.Project.Users.manyFromTarget, 201 | where: { project_id: [ this.project1.get('id'), this.project2.get('id'), project4.get('id') ] } 202 | }], 203 | raw: true, 204 | group: [`${this.ProjectMembers ? 'project_members' : 'members'}.project_id`], 205 | multiple: false 206 | }]); 207 | }); 208 | 209 | it('batches to multiple findAll call when different limits are applied', async function () { 210 | const [members1, members2, members3] = await Promise.all([ 211 | this.project1.getMembers({ limit: 4, [EXPECTED_OPTIONS_KEY]: this.context }), 212 | this.project2.getMembers({ limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 213 | this.project3.getMembers({ limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 214 | ]); 215 | 216 | // there is no guaranteed order and this test returns different 217 | // values depending on the sequelize version. 218 | const allMembers1 = this.users.slice(0, 4).map(user => user.get('id')); 219 | const allMembers2 = this.users.slice(3, 7).map(user => user.get('id')); 220 | const allMembers3 = this.users.slice(6, 9).map(user => user.get('id')); 221 | 222 | const intersection1 = intersection(members1.map(user => user.get('id')), allMembers1); 223 | expect(intersection1.length, 'to equal', 4); 224 | 225 | const intersection2 = intersection(members2.map(user => user.get('id')), allMembers2); 226 | expect(intersection2.length, 'to equal', 2); 227 | 228 | const intersection3 = intersection(members3.map(user => user.get('id')), allMembers3); 229 | expect(intersection3.length, 'to equal', 2); 230 | 231 | expect(this.User.findAll, 'was called twice'); 232 | }); 233 | 234 | it('find call with through model has all attributes', async function () { 235 | 236 | await this.project2.getMembers({ through: {attributes: ['projectId', 'userId']}, [EXPECTED_OPTIONS_KEY]: this.context }); 237 | 238 | expect(this.User.findAll, 'to have a call satisfying', [{ 239 | through: {attributes: ['projectId', 'userId']} 240 | }]); 241 | }); 242 | 243 | it('batches to multiple findAll call with where', async function () { 244 | let members1 = this.project1.getMembers({ where: { awesome: true }, [EXPECTED_OPTIONS_KEY]: this.context }) 245 | , members2 = this.project2.getMembers({ where: { awesome: false }, [EXPECTED_OPTIONS_KEY]: this.context }); 246 | 247 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 248 | this.users[1], 249 | this.users[2], 250 | ]); 251 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 252 | this.users[3], 253 | this.users[5] 254 | ]); 255 | 256 | expect(this.User.findAll, 'was called twice'); 257 | expect(this.User.findAll, 'to have a call satisfying', [{ 258 | where: { 259 | awesome: true 260 | }, 261 | }]); 262 | expect(this.User.findAll, 'to have a call satisfying', [{ 263 | where: { 264 | awesome: true 265 | }, 266 | }]); 267 | }); 268 | 269 | it('batches to multiple findAll call with where + limit', async function () { 270 | const [members1, members2, members3, members4, members5] = await Promise.all([ 271 | this.project1.getMembers({ where: { awesome: true }, limit: 1, [EXPECTED_OPTIONS_KEY]: this.context }), 272 | this.project2.getMembers({ where: { awesome: true }, limit: 1, [EXPECTED_OPTIONS_KEY]: this.context }), 273 | this.project2.getMembers({ where: { awesome: false }, limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 274 | this.project3.getMembers({ where: { awesome: true }, limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 275 | this.project3.getMembers({ where: { awesome: true }, limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 276 | ]); 277 | 278 | // there is no guaranteed order and this test returns different 279 | // values depending on the sequelize version. 280 | const allMembers1 = this.users.slice(0, 4).map(user => user.get('id')); 281 | const allMembers2 = this.users.slice(3, 7).map(user => user.get('id')); 282 | const allMembers3 = this.users.slice(3, 7).map(user => user.get('id')); 283 | const allMembers4 = this.users.slice(6, 9).map(user => user.get('id')); 284 | const allMembers5 = this.users.slice(6, 9).map(user => user.get('id')); 285 | 286 | const intersection1 = intersection(members1.map(user => user.get('id')), allMembers1); 287 | expect(intersection1.length, 'to equal', 1); 288 | 289 | const intersection2 = intersection(members2.map(user => user.get('id')), allMembers2); 290 | expect(intersection2.length, 'to equal', 1); 291 | 292 | const intersection3 = intersection(members3.map(user => user.get('id')), allMembers3); 293 | expect(intersection3.length, 'to equal', 2); 294 | 295 | const intersection4 = intersection(members4.map(user => user.get('id')), allMembers4); 296 | expect(intersection4.length, 'to equal', 2); 297 | 298 | const intersection5 = intersection(members5.map(user => user.get('id')), allMembers5); 299 | expect(intersection5.length, 'to equal', 2); 300 | 301 | expect(this.User.findAll, 'was called thrice'); 302 | expect(this.User.findAll, 'to have a call satisfying', [{ 303 | where: { 304 | awesome: true 305 | }, 306 | groupedLimit: { 307 | on: this.Project.Users.paired, 308 | limit: 1, 309 | values: [this.project1.get('id'), this.project2.get('id')] 310 | } 311 | }]); 312 | expect(this.User.findAll, 'to have a call satisfying', [{ 313 | where: { 314 | awesome: true 315 | }, 316 | groupedLimit: { 317 | on: this.Project.Users.paired, 318 | limit: 2, 319 | values: [this.project3.get('id')] 320 | } 321 | }]); 322 | expect(this.User.findAll, 'to have a call satisfying', [{ 323 | where: { 324 | awesome: false 325 | }, 326 | groupedLimit: { 327 | on: this.Project.Users.paired, 328 | limit: 2, 329 | values: [this.project2.get('id')] 330 | } 331 | }]); 332 | }); 333 | }); 334 | }); 335 | }); 336 | 337 | setups.forEach(([description, setup]) => { 338 | describe(description, function () { 339 | describe('paranoid', function () { 340 | before(async function () { 341 | this.User = this.connection.define('user', { 342 | name: Sequelize.STRING, 343 | awesome: Sequelize.BOOLEAN, 344 | deletedAt: Sequelize.DATE, 345 | }, { 346 | paranoid: true, 347 | }); 348 | this.Project = this.connection.define('project'); 349 | 350 | setup(this); 351 | 352 | await this.connection.sync({ force: true }); 353 | await createData.call(this); 354 | 355 | this.context = createContext(this.connection); 356 | }); 357 | 358 | beforeEach(function () { 359 | this.sandbox = sinon.sandbox.create(); 360 | 361 | this.sandbox.spy(this.User, 'findAll'); 362 | }); 363 | 364 | afterEach(function () { 365 | this.sandbox.restore(); 366 | }); 367 | 368 | it('batches and caches to a single findAll call (paranoid)', async function () { 369 | let members1 = this.project1.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 370 | , members5 = this.project5.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 371 | 372 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 373 | this.users[0], 374 | this.users[1], 375 | this.users[2], 376 | this.users[3], 377 | ]); 378 | await expect(members5, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 379 | this.users[10], 380 | ]); 381 | 382 | members1 = this.project1.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 383 | members5 = this.project5.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 384 | 385 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 386 | this.users[0], 387 | this.users[1], 388 | this.users[2], 389 | this.users[3], 390 | ]); 391 | await expect(members5, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 392 | this.users[10], 393 | ]); 394 | 395 | expect(this.User.findAll, 'was called once'); 396 | expect(this.User.findAll, 'to have a call satisfying', [{ 397 | include: [{ 398 | association: this.Project.Users.manyFromTarget, 399 | where: { project_id: [this.project1.get('id'), this.project5.get('id')] } 400 | }] 401 | }]); 402 | }); 403 | 404 | it('batches and caches to a single findAll call (not paranoid)', async function () { 405 | let members1 = this.project1.getMembers({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}) 406 | , members5 = this.project5.getMembers({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 407 | 408 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 409 | this.users[0], 410 | this.users[1], 411 | this.users[2], 412 | this.users[3], 413 | ]); 414 | await expect(members5, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 415 | this.users[9], 416 | this.users[10], 417 | ]); 418 | 419 | members1 = this.project1.getMembers({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 420 | members5 = this.project5.getMembers({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 421 | 422 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 423 | this.users[0], 424 | this.users[1], 425 | this.users[2], 426 | this.users[3], 427 | ]); 428 | await expect(members5, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 429 | this.users[9], 430 | this.users[10], 431 | ]); 432 | 433 | expect(this.User.findAll, 'was called once'); 434 | expect(this.User.findAll, 'to have a call satisfying', [{ 435 | paranoid: false, 436 | include: [{ 437 | association: this.Project.Users.manyFromTarget, 438 | where: { project_id: [this.project1.get('id'), this.project5.get('id')] }, 439 | }] 440 | }]); 441 | }); 442 | }); 443 | }); 444 | }); 445 | 446 | describe('scopes', function () { 447 | describe('scope on target', function () { 448 | before(async function () { 449 | this.User = this.connection.define('user', { 450 | name: Sequelize.STRING, 451 | awesome: Sequelize.BOOLEAN 452 | }); 453 | this.Project = this.connection.define('project'); 454 | 455 | this.Project.AwesomeMembers = this.Project.belongsToMany(this.User, { 456 | as: 'awesomeMembers', 457 | through: 'project_members', 458 | foreignKey: 'projectId', 459 | targetKey: 'id', 460 | scope: { 461 | awesome: true 462 | } 463 | }); 464 | 465 | this.Project.Members = this.Project.belongsToMany(this.User, { 466 | as: 'members', 467 | through: 'project_members', 468 | foreignKey: 'projectId', 469 | targetKey: 'id' 470 | }); 471 | 472 | this.User.belongsToMany(this.Project, { 473 | through: 'project_members', 474 | foreignKey: 'userId', 475 | targetKey: 'id' 476 | }); 477 | 478 | await this.connection.sync({ force: true }); 479 | await createData.call(this); 480 | }); 481 | 482 | beforeEach(function () { 483 | this.context = createContext(this.connection); 484 | this.sandbox.spy(this.User, 'findAll'); 485 | }); 486 | 487 | afterEach(function () { 488 | this.sandbox.restore(); 489 | }); 490 | 491 | it('batches to multiple findAll call when different limits are applied', async function () { 492 | let members1 = this.project1.getAwesomeMembers({ limit: 10, [EXPECTED_OPTIONS_KEY]: this.context }) 493 | , members2 = this.project2.getAwesomeMembers({ limit: 10, [EXPECTED_OPTIONS_KEY]: this.context }) 494 | , members3 = this.project3.getAwesomeMembers({ limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }); 495 | 496 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 497 | this.users[1], 498 | this.users[2] 499 | ]); 500 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 501 | this.users[4], 502 | this.users[6] 503 | ]); 504 | await expect(members3, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 505 | this.users[6], 506 | this.users[7] 507 | ]); 508 | 509 | expect(this.User.findAll, 'was called twice'); 510 | }); 511 | 512 | it('batches to a single findAll call', async function () { 513 | let members1 = this.project1.getAwesomeMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 514 | , members2 = this.project2.getAwesomeMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 515 | , members3 = this.project3.getAwesomeMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 516 | 517 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 518 | this.users[1], 519 | this.users[2] 520 | ]); 521 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 522 | this.users[4], 523 | this.users[6] 524 | ]); 525 | await expect(members3, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 526 | this.users[6], 527 | this.users[7], 528 | this.users[8] 529 | ]); 530 | 531 | expect(this.User.findAll, 'was called once'); 532 | }); 533 | 534 | it('works for raw queries', async function () { 535 | let members1 = this.project1.getAwesomeMembers({ [EXPECTED_OPTIONS_KEY]: this.context }) 536 | , members2 = this.project2.getAwesomeMembers({ raw: true, [EXPECTED_OPTIONS_KEY]: this.context }) 537 | , members3 = this.project3.getAwesomeMembers({ raw: true, [EXPECTED_OPTIONS_KEY]: this.context }); 538 | 539 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 540 | this.users[1], 541 | this.users[2] 542 | ]); 543 | expect((await members2).map(m => m.id), 'with set semantics to exhaustively satisfy', [ 544 | this.users[4].id, 545 | this.users[6].id 546 | ]); 547 | expect((await members3).map(m => m.id), 'with set semantics to exhaustively satisfy', [ 548 | this.users[6].id, 549 | this.users[7].id, 550 | this.users[8].id 551 | ]); 552 | 553 | expect(this.User.findAll, 'was called twice'); 554 | }); 555 | 556 | it('batches to multiple findAll call when different scopes are applied', async function () { 557 | let members1 = this.project1.getAwesomeMembers({ limit: 10, [EXPECTED_OPTIONS_KEY]: this.context }) 558 | , members2 = this.project1.getMembers({ limit: 10, [EXPECTED_OPTIONS_KEY]: this.context }); 559 | 560 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 561 | this.users[1], 562 | this.users[2] 563 | ]); 564 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 565 | this.users[0], 566 | this.users[1], 567 | this.users[2], 568 | this.users[3] 569 | ]); 570 | 571 | expect(this.User.findAll, 'was called twice'); 572 | }); 573 | }); 574 | 575 | describe('scope on through', function () { 576 | before(async function () { 577 | this.User = this.connection.define('user', { 578 | name: Sequelize.STRING, 579 | awesome: Sequelize.BOOLEAN 580 | }); 581 | this.Project = this.connection.define('project'); 582 | this.ProjectMembers = this.connection.define('project_members', { 583 | secret: Sequelize.BOOLEAN 584 | }); 585 | 586 | this.Project.SecretMembers = this.Project.belongsToMany(this.User, { 587 | as: 'secretMembers', 588 | through: { 589 | model: this.ProjectMembers, 590 | scope: { 591 | secret: true 592 | } 593 | }, 594 | foreignKey: 'projectId', 595 | targetKey: 'id' 596 | }); 597 | 598 | this.Project.Members = this.Project.belongsToMany(this.User, { 599 | as: 'members', 600 | through: this.ProjectMembers, 601 | foreignKey: 'projectId', 602 | targetKey: 'id' 603 | }); 604 | 605 | this.User.belongsToMany(this.Project, { 606 | through: this.ProjectMembers, 607 | foreignKey: 'userId', 608 | targetKey: 'id' 609 | }); 610 | 611 | await this.connection.sync({ force: true }); 612 | await createData.call(this); 613 | 614 | await this.ProjectMembers.update({ 615 | secret: true 616 | }, { 617 | where: { 618 | [Sequelize.Op ? Sequelize.Op.or : '$or']: [ 619 | { projectId: this.project1.get('id'), userId: [this.users[0].get('id'), this.users[1].get('id')]}, 620 | { projectId: this.project2.get('id'), userId: [this.users[4].get('id')]}, 621 | { projectId: this.project3.get('id'), userId: [this.users[6].get('id'), this.users[7].get('id'), this.users[8].get('id')]} 622 | ] 623 | } 624 | }); 625 | }); 626 | 627 | beforeEach(function () { 628 | this.context = createContext(this.connection); 629 | this.sandbox.spy(this.User, 'findAll'); 630 | }); 631 | 632 | afterEach(function () { 633 | this.sandbox.restore(); 634 | }); 635 | 636 | it('batches to multiple findAll call when different limits are applied', async function () { 637 | let members1 = this.project1.getSecretMembers({ limit: 10, [EXPECTED_OPTIONS_KEY]: this.context }) 638 | , members2 = this.project2.getSecretMembers({ limit: 10, [EXPECTED_OPTIONS_KEY]: this.context }) 639 | , members3 = this.project3.getSecretMembers({ limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }); 640 | 641 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 642 | this.users[0], 643 | this.users[1] 644 | ]); 645 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 646 | this.users[4] 647 | ]); 648 | await expect(members3, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 649 | this.users[6], 650 | this.users[7] 651 | ]); 652 | 653 | expect(this.User.findAll, 'was called twice'); 654 | }); 655 | 656 | it('batches to one findAll call without limits', async function () { 657 | let members1 = this.project1.getSecretMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 658 | , members2 = this.project2.getSecretMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 659 | , members3 = this.project3.getSecretMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 660 | 661 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 662 | this.users[0], 663 | this.users[1] 664 | ]); 665 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 666 | this.users[4] 667 | ]); 668 | await expect(members3, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 669 | this.users[6], 670 | this.users[7], 671 | this.users[8] 672 | ]); 673 | 674 | expect(this.User.findAll, 'was called once'); 675 | }); 676 | 677 | it('batches to multiple findAll call when different scopes are applied', async function () { 678 | let members1 = this.project1.getSecretMembers({ limit: 10, [EXPECTED_OPTIONS_KEY]: this.context }) 679 | , members2 = this.project1.getMembers({ limit: 10, [EXPECTED_OPTIONS_KEY]: this.context }); 680 | 681 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 682 | this.users[0], 683 | this.users[1] 684 | ]); 685 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 686 | this.users[0], 687 | this.users[1], 688 | this.users[2], 689 | this.users[3] 690 | ]); 691 | 692 | expect(this.User.findAll, 'was called twice'); 693 | }); 694 | }); 695 | }); 696 | }); 697 | -------------------------------------------------------------------------------- /test/integration/hasMany.test.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import {intersection} from 'lodash'; 3 | import {createConnection, randint} from '../helper'; 4 | import sinon from 'sinon'; 5 | import DataLoader from 'dataloader'; 6 | import {createContext, EXPECTED_OPTIONS_KEY} from '../../src'; 7 | import Promise from 'bluebird'; 8 | import expect from 'unexpected'; 9 | import {method} from '../../src/helper'; 10 | 11 | async function createData() { 12 | [this.project1, this.project2, this.project3, this.project4] = await this.Project.bulkCreate([ 13 | { id: randint() }, 14 | { id: randint() }, 15 | { id: randint() }, 16 | { id: randint() } 17 | ], {returning: true}); 18 | this.users = await this.User.bulkCreate([ 19 | { id: randint(), awesome: false }, 20 | { id: randint(), awesome: true }, 21 | { id: randint(), awesome: true }, 22 | { id: randint(), awesome: false }, 23 | { id: randint(), awesome: true }, 24 | { id: randint(), awesome: false }, 25 | { id: randint(), awesome: true }, 26 | { id: randint(), awesome: true }, 27 | { id: randint(), awesome: true } 28 | ], {returning: true}); 29 | 30 | await this.project1.setMembers(this.users.slice(0, 3)); 31 | await this.project2.setMembers(this.users.slice(3, 7)); 32 | await this.project3.setMembers(this.users.slice(7)); 33 | 34 | await this.User.update({ deletedAt: new Date() }, { 35 | where: { 36 | id: [this.users[0].get('id'), this.users[8].get('id')] 37 | } 38 | }); 39 | } 40 | 41 | describe('hasMany', function () { 42 | beforeEach(async function () { 43 | this.sandbox = sinon.sandbox.create(); 44 | }); 45 | 46 | before(createConnection); 47 | 48 | describe('simple association', function () { 49 | before(async function () { 50 | this.User = this.connection.define('user', { 51 | awesome: Sequelize.BOOLEAN 52 | }); 53 | this.Project = this.connection.define('project'); 54 | 55 | this.Project.hasMany(this.User, { 56 | as: 'members', 57 | foreignKey: { 58 | name: 'projectId', 59 | field: 'project_id' 60 | } 61 | }); 62 | await this.connection.sync({ force: true }); 63 | await createData.call(this); 64 | }); 65 | 66 | beforeEach(function () { 67 | this.context = createContext(this.connection); 68 | this.sandbox.spy(this.User, 'findAll'); 69 | }); 70 | 71 | afterEach(function () { 72 | this.sandbox.restore(); 73 | }); 74 | 75 | it('batches/caches to a single findAll call when getting', async function () { 76 | let members1 = this.project1.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 77 | , members2 = this.project2.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 78 | 79 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 80 | this.users[0], 81 | this.users[1], 82 | this.users[2], 83 | ]); 84 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 85 | this.users[3], 86 | this.users[4], 87 | this.users[5], 88 | this.users[6] 89 | ]); 90 | 91 | members1 = this.project1.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 92 | members2 = this.project2.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 93 | 94 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 95 | this.users[0], 96 | this.users[1], 97 | this.users[2], 98 | ]); 99 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 100 | this.users[3], 101 | this.users[4], 102 | this.users[5], 103 | this.users[6] 104 | ]); 105 | 106 | expect(this.User.findAll, 'was called once'); 107 | expect(this.User.findAll, 'to have a call satisfying', [{ 108 | where: { 109 | project_id: [this.project1.get('id'), this.project2.get('id')] 110 | } 111 | }]); 112 | }); 113 | 114 | it('supports rejectOnEmpty', async function () { 115 | let error = new Error('FooBar!'); 116 | let members1 = this.project1.getMembers({ rejectOnEmpty: error, [EXPECTED_OPTIONS_KEY]: this.context }) 117 | , members2 = this.project4.getMembers({ rejectOnEmpty: error, [EXPECTED_OPTIONS_KEY]: this.context }) 118 | , members3 = this.project4.getMembers({ [EXPECTED_OPTIONS_KEY]: this.context }); 119 | 120 | await expect(members1, 'to be fulfilled with', Array); 121 | await expect(members2, 'to be rejected with', 'FooBar!'); 122 | await expect(members3, 'to be fulfilled with', []); 123 | }); 124 | 125 | it('batches to a single findAll call when counting', async function () { 126 | let project4 = await this.Project.create(); 127 | 128 | let members1 = this.project1.countMembers({[EXPECTED_OPTIONS_KEY]: this.context} ) 129 | , members2 = this.project2.countMembers({[EXPECTED_OPTIONS_KEY]: this.context} ) 130 | , members3 = project4.countMembers({[EXPECTED_OPTIONS_KEY]: this.context} ); 131 | 132 | await expect(members1, 'to be fulfilled with', 3); 133 | await expect(members2, 'to be fulfilled with', 4); 134 | await expect(members3, 'to be fulfilled with', 0); 135 | 136 | expect(this.User.findAll, 'was called once'); 137 | expect(this.User.findAll, 'to have a call satisfying', [{ 138 | where: { 139 | project_id: [this.project1.get('id'), this.project2.get('id'), project4.get('id')] 140 | }, 141 | attributes: [ 142 | [this.connection.fn('COUNT', expect.it('to be defined')), 'count'], 143 | 'projectId' 144 | ], 145 | raw: true, 146 | group: ['projectId'], 147 | multiple: false 148 | }]); 149 | }); 150 | 151 | it('batches/caches to a single findAll call when counting', async function () { 152 | let project4 = await this.Project.create(); 153 | 154 | let members1 = this.project1.countMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 155 | , members2 = this.project2.countMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 156 | , members3 = project4.countMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 157 | 158 | await expect(members1, 'to be fulfilled with', 3); 159 | await expect(members2, 'to be fulfilled with', 4); 160 | await expect(members3, 'to be fulfilled with', 0); 161 | 162 | members1 = this.project1.countMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 163 | members2 = this.project2.countMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 164 | members3 = project4.countMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 165 | 166 | await expect(members1, 'to be fulfilled with', 3); 167 | await expect(members2, 'to be fulfilled with', 4); 168 | await expect(members3, 'to be fulfilled with', 0); 169 | 170 | expect(this.User.findAll, 'was called once'); 171 | expect(this.User.findAll, 'to have a call satisfying', [{ 172 | where: { 173 | project_id: [this.project1.get('id'), this.project2.get('id'), project4.get('id')] 174 | }, 175 | attributes: [ 176 | [this.connection.fn('COUNT', expect.it('to be defined')), 'count'], 177 | 'projectId' 178 | ], 179 | raw: true, 180 | group: ['projectId'], 181 | multiple: false 182 | }]); 183 | }); 184 | 185 | it('batches to a single findAll call when limits are the same', async function () { 186 | const [members1, members2] = await Promise.all([ 187 | this.project1.getMembers({ limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 188 | this.project2.getMembers({ limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 189 | ]); 190 | 191 | // there is no guaranteed order and this test returns different 192 | // values depending on the sequelize version. 193 | const allMembers1 = this.users.slice(0, 3).map(user => user.get('id')); 194 | const allMembers2 = this.users.slice(3, 7).map(user => user.get('id')); 195 | 196 | const intersection1 = intersection(members1.map(user => user.get('id')), allMembers1); 197 | expect(intersection1.length, 'to equal', 2); 198 | 199 | const intersection2 = intersection(members2.map(user => user.get('id')), allMembers2); 200 | expect(intersection2.length, 'to equal', 2); 201 | 202 | expect(this.User.findAll, 'was called once'); 203 | expect(this.User.findAll, 'to have a call satisfying', [{ 204 | groupedLimit: { 205 | limit: 2, 206 | on: 'project_id', 207 | values: [ this.project1.get('id'), this.project2.get('id') ] 208 | } 209 | }]); 210 | }); 211 | 212 | it('batches to multiple findAll call when limits are different', async function () { 213 | const [members1, members2, members3] = await Promise.all([ 214 | this.project1.getMembers({ limit: 4, [EXPECTED_OPTIONS_KEY]: this.context }), 215 | this.project2.getMembers({ limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 216 | this.project3.getMembers({ limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 217 | ]); 218 | 219 | // there is no guaranteed order and this test returns different 220 | // values depending on the sequelize version. 221 | const allMembers1 = this.users.slice(0, 3).map(user => user.get('id')); 222 | const allMembers2 = this.users.slice(3, 7).map(user => user.get('id')); 223 | const allMembers3 = this.users.slice(7).map(user => user.get('id')); 224 | 225 | const intersection1 = intersection(members1.map(user => user.get('id')), allMembers1); 226 | expect(intersection1.length, 'to equal', 3); 227 | 228 | const intersection2 = intersection(members2.map(user => user.get('id')), allMembers2); 229 | expect(intersection2.length, 'to equal', 2); 230 | 231 | const intersection3 = intersection(members3.map(user => user.get('id')), allMembers3); 232 | expect(intersection3.length, 'to equal', 2); 233 | 234 | expect(this.User.findAll, 'was called twice'); 235 | expect(this.User.findAll, 'to have a call satisfying', [{ 236 | where: { 237 | project_id: [this.project1.get('id')], 238 | }, 239 | limit: 4 240 | }]); 241 | expect(this.User.findAll, 'to have a call satisfying', [{ 242 | groupedLimit: { 243 | limit: 2, 244 | on: 'project_id', 245 | values: [ this.project2.get('id'), this.project3.get('id') ] 246 | } 247 | }]); 248 | }); 249 | 250 | it('batches to multiple findAll call when where clauses are different', async function () { 251 | let members1 = this.project1.getMembers({ where: { awesome: true }, [EXPECTED_OPTIONS_KEY]: this.context}) 252 | , members2 = this.project2.getMembers({ where: { awesome: false }, [EXPECTED_OPTIONS_KEY]: this.context}) 253 | , members3 = this.project3.getMembers({ where: { awesome: true }, [EXPECTED_OPTIONS_KEY]: this.context}); 254 | 255 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 256 | this.users[1], 257 | this.users[2] 258 | ]); 259 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 260 | this.users[3], 261 | this.users[5] 262 | ]); 263 | await expect(members3, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 264 | this.users[7], 265 | this.users[8] 266 | ]); 267 | 268 | expect(this.User.findAll, 'was called twice'); 269 | expect(this.User.findAll, 'to have a call satisfying', [{ 270 | where: { 271 | [Sequelize.Op ? Sequelize.Op.and : '$and']: [ 272 | { project_id: [this.project1.get('id'), this.project3.get('id')]}, 273 | { awesome: true } 274 | ] 275 | } 276 | }]); 277 | expect(this.User.findAll, 'to have a call satisfying', [{ 278 | where: { 279 | [Sequelize.Op ? Sequelize.Op.and : '$and']: [ 280 | { project_id: [this.project2.get('id')]}, 281 | { awesome: false } 282 | ] 283 | } 284 | }]); 285 | }); 286 | 287 | it('batches and caches to multiple findAll call when where clauses are different (createContext)', async function () { 288 | let members1 = this.project1.getMembers({ where: { awesome: true }, [EXPECTED_OPTIONS_KEY]: this.context}) 289 | , members2 = this.project2.getMembers({ where: { awesome: false }, [EXPECTED_OPTIONS_KEY]: this.context}) 290 | , members3 = this.project3.getMembers({ where: { awesome: true }, [EXPECTED_OPTIONS_KEY]: this.context}); 291 | 292 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 293 | this.users[1], 294 | this.users[2] 295 | ]); 296 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 297 | this.users[3], 298 | this.users[5] 299 | ]); 300 | await expect(members3, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 301 | this.users[7], 302 | this.users[8] 303 | ]); 304 | 305 | members1 = this.project1.getMembers({ where: { awesome: true }, [EXPECTED_OPTIONS_KEY]: this.context}); 306 | members2 = this.project2.getMembers({ where: { awesome: false }, [EXPECTED_OPTIONS_KEY]: this.context}); 307 | members3 = this.project3.getMembers({ where: { awesome: true }, [EXPECTED_OPTIONS_KEY]: this.context}); 308 | 309 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 310 | this.users[1], 311 | this.users[2] 312 | ]); 313 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 314 | this.users[3], 315 | this.users[5] 316 | ]); 317 | await expect(members3, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 318 | this.users[7], 319 | this.users[8] 320 | ]); 321 | 322 | expect(this.User.findAll, 'was called twice'); 323 | expect(this.User.findAll, 'to have a call satisfying', [{ 324 | where: { 325 | [Sequelize.Op ? Sequelize.Op.and : '$and']: [ 326 | { project_id: [this.project1.get('id'), this.project3.get('id')]}, 327 | { awesome: true } 328 | ] 329 | } 330 | }]); 331 | expect(this.User.findAll, 'to have a call satisfying', [{ 332 | where: { 333 | [Sequelize.Op ? Sequelize.Op.and : '$and']: [ 334 | { project_id: [this.project2.get('id')]}, 335 | { awesome: false } 336 | ] 337 | } 338 | }]); 339 | }); 340 | 341 | it('batches to multiple findAll call with where + limit', async function () { 342 | let [members1, members2, members3, members4] = await Promise.all([ 343 | this.project1.getMembers({ where: { awesome: true }, limit: 1, [EXPECTED_OPTIONS_KEY]: this.context }), 344 | this.project2.getMembers({ where: { awesome: true }, limit: 1, [EXPECTED_OPTIONS_KEY]: this.context }), 345 | this.project2.getMembers({ where: { awesome: false }, limit: 1, [EXPECTED_OPTIONS_KEY]: this.context }), 346 | this.project3.getMembers({ where: { awesome: true }, limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 347 | ]); 348 | 349 | // there is no guaranteed order and this test returns different 350 | // values depending on the sequelize version. 351 | const allMembers1 = this.users.slice(0, 3).map(user => user.get('id')); 352 | const allMembers2 = this.users.slice(3, 7).map(user => user.get('id')); 353 | const allMembers3 = this.users.slice(3, 7).map(user => user.get('id')); 354 | const allMembers4 = this.users.slice(7).map(user => user.get('id')); 355 | 356 | const intersection1 = intersection(members1.map(user => user.get('id')), allMembers1); 357 | expect(intersection1.length, 'to equal', 1); 358 | 359 | const intersection2 = intersection(members2.map(user => user.get('id')), allMembers2); 360 | expect(intersection2.length, 'to equal', 1); 361 | 362 | const intersection3 = intersection(members3.map(user => user.get('id')), allMembers3); 363 | expect(intersection3.length, 'to equal', 1); 364 | 365 | const intersection4 = intersection(members4.map(user => user.get('id')), allMembers4); 366 | expect(intersection4.length, 'to equal', 2); 367 | 368 | expect(this.User.findAll, 'was called thrice'); 369 | expect(this.User.findAll, 'to have a call satisfying', [{ 370 | where: { 371 | awesome: true 372 | }, 373 | groupedLimit: { 374 | limit: 1, 375 | values: [this.project1.get('id'), this.project2.get('id')] 376 | } 377 | }]); 378 | expect(this.User.findAll, 'to have a call satisfying', [{ 379 | where: { 380 | [Sequelize.Op ? Sequelize.Op.and : '$and']: [ 381 | { project_id: [this.project3.get('id')] }, 382 | { awesome: true } 383 | ] 384 | }, 385 | limit: 2 386 | }]); 387 | expect(this.User.findAll, 'to have a call satisfying', [{ 388 | where: { 389 | [Sequelize.Op ? Sequelize.Op.and : '$and']: [ 390 | { project_id: [this.project2.get('id')] }, 391 | { awesome: false } 392 | ] 393 | }, 394 | limit: 1 395 | }]); 396 | }); 397 | 398 | it('should skip batching if include is set', async function() { 399 | this.sandbox.spy(DataLoader.prototype, 'load'); 400 | let project1 = await this.Project[method(this.Project, 'findByPk')](this.project1.id, { [EXPECTED_OPTIONS_KEY]: this.context, include: [ this.Project.associations.members ]}); 401 | let project2 = await this.Project[method(this.Project, 'findByPk')](this.project2.id, { [EXPECTED_OPTIONS_KEY]: this.context, include: [ this.Project.associations.members ]}); 402 | 403 | expect(project1.members, 'not to be undefined'); 404 | expect(project2.members, 'not to be undefined'); 405 | expect(project1.members, 'to have length', 3); 406 | expect(project2.members, 'to have length', 4); 407 | expect(DataLoader.prototype.load, 'was not called'); 408 | }); 409 | }); 410 | 411 | describe('deep association with include.separate', function () { 412 | before(async function () { 413 | this.UserDeep = this.connection.define('userDeep', { 414 | userId: { 415 | type: Sequelize.INTEGER, 416 | primaryKey: true, 417 | autoIncrement: true 418 | }, 419 | name: Sequelize.STRING, 420 | }); 421 | 422 | this.RoleDeep = this.connection.define('roleDeep', { 423 | roleId: { 424 | type: Sequelize.INTEGER, 425 | primaryKey: true, 426 | autoIncrement: true 427 | }, 428 | title: Sequelize.STRING, 429 | }); 430 | 431 | this.PermissionDeep = this.connection.define('permissionDeep', { 432 | permissionId: { 433 | type: Sequelize.INTEGER, 434 | primaryKey: true, 435 | autoIncrement: true 436 | }, 437 | title: Sequelize.STRING, 438 | }); 439 | 440 | this.RoleDeep.hasMany(this.PermissionDeep, { 441 | as: 'permissions', 442 | foreignKey: 'roleId' 443 | }); 444 | 445 | this.UserDeep.hasOne(this.RoleDeep, { 446 | as: 'role', 447 | foreignKey: 'roleId' 448 | }); 449 | 450 | await this.connection.sync({ force: true }); 451 | 452 | this.user1 = await this.UserDeep.create({ 453 | name: 'John Doe', 454 | role: { 455 | title: 'admin', 456 | permissions: [ 457 | { title: 'permission #1' }, 458 | { title: 'permission #2' }, 459 | ] 460 | } 461 | }, { 462 | include: [{ 463 | model: this.RoleDeep, 464 | as: 'role', 465 | include: [{ model: this.PermissionDeep, as: 'permissions' }] 466 | }] 467 | }); 468 | }); 469 | 470 | beforeEach(function () { 471 | this.context = createContext(this.connection); 472 | }); 473 | 474 | it('correctly finds twice with separated query', async function() { 475 | const userFirstFetch = await this.UserDeep[method(this.UserDeep, 'findByPk')](this.user1.userId, { 476 | include: [{ model: this.RoleDeep, as: 'role' }], 477 | [EXPECTED_OPTIONS_KEY]: this.context 478 | }); 479 | 480 | expect(userFirstFetch, 'not to be null'); 481 | expect(userFirstFetch.name, 'to be', 'John Doe'); 482 | expect(userFirstFetch.role, 'not to be null'); 483 | expect(userFirstFetch.role.title, 'to be', 'admin'); 484 | 485 | const userSecondFetch = await this.UserDeep[method(this.UserDeep, 'findByPk')](this.user1.userId, { 486 | [EXPECTED_OPTIONS_KEY]: this.context, 487 | include: [{ 488 | model: this.RoleDeep, 489 | as: 'role', 490 | include: [{ model: this.PermissionDeep, as: 'permissions', separate: true }] 491 | }] 492 | }); 493 | 494 | expect(userSecondFetch, 'not to be null'); 495 | expect(userSecondFetch.name, 'to be', 'John Doe'); 496 | expect(userSecondFetch.role, 'not to be null'); 497 | expect(userSecondFetch.role.title, 'to be', 'admin'); 498 | expect(userSecondFetch.role.permissions, 'to have length', 2); 499 | expect( 500 | userSecondFetch.role.permissions[0].title, 501 | 'to be one of', 502 | ['permission #1', 'permission #2'] 503 | ); 504 | }); 505 | }); 506 | 507 | describe('paranoid', function () { 508 | before(async function () { 509 | this.User = this.connection.define('user', {}, { 510 | paranoid: true 511 | }); 512 | this.Project = this.connection.define('project'); 513 | this.Project.hasMany(this.User, { as: 'members' }); 514 | await this.connection.sync({ force: true }); 515 | await createData.call(this); 516 | }); 517 | 518 | beforeEach(function () { 519 | this.context = createContext(this.connection); 520 | this.sandbox.spy(this.User, 'findAll'); 521 | }); 522 | afterEach(function () { 523 | this.sandbox.restore(); 524 | }); 525 | 526 | it('batches and caches to a single findAll call (paranoid)', async function () { 527 | let members1 = this.project1.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 528 | , members2 = this.project2.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 529 | 530 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 531 | this.users[1], 532 | this.users[2], 533 | ]); 534 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 535 | this.users[3], 536 | this.users[4], 537 | this.users[5], 538 | this.users[6] 539 | ]); 540 | 541 | members1 = this.project1.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 542 | members2 = this.project2.getMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 543 | 544 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 545 | this.users[1], 546 | this.users[2], 547 | ]); 548 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 549 | this.users[3], 550 | this.users[4], 551 | this.users[5], 552 | this.users[6] 553 | ]); 554 | 555 | expect(this.User.findAll, 'was called once'); 556 | expect(this.User.findAll, 'to have a call satisfying', [{ 557 | where: { 558 | projectId: [this.project1.get('id'), this.project2.get('id')] 559 | } 560 | }]); 561 | }); 562 | 563 | it('batches and caches to a single findAll call (not paranoid)', async function () { 564 | let members1 = this.project1.getMembers({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}) 565 | , members2 = this.project2.getMembers({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 566 | 567 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 568 | this.users[0], 569 | this.users[1], 570 | this.users[2], 571 | ]); 572 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 573 | this.users[3], 574 | this.users[4], 575 | this.users[5], 576 | this.users[6] 577 | ]); 578 | 579 | members1 = this.project1.getMembers({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 580 | members2 = this.project2.getMembers({[EXPECTED_OPTIONS_KEY]: this.context, paranoid: false}); 581 | 582 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 583 | this.users[0], 584 | this.users[1], 585 | this.users[2], 586 | ]); 587 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 588 | this.users[3], 589 | this.users[4], 590 | this.users[5], 591 | this.users[6] 592 | ]); 593 | 594 | expect(this.User.findAll, 'was called once'); 595 | expect(this.User.findAll, 'to have a call satisfying', [{ 596 | paranoid: false, 597 | where: { 598 | projectId: [this.project1.get('id'), this.project2.get('id')] 599 | } 600 | }]); 601 | }); 602 | 603 | it('batches to a single findAll call when limits are the same', async function () { 604 | let [members1, members2] = await Promise.all([ 605 | this.project1.getMembers({ limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 606 | this.project2.getMembers({ limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 607 | ]); 608 | 609 | // there is no guaranteed order and this test returns different 610 | // values depending on the sequelize version. 611 | const allMembers1 = this.users.slice(1, 3).map(user => user.get('id')); // 0 is deleted 612 | const allMembers2 = this.users.slice(3, 7).map(user => user.get('id')); 613 | 614 | const intersection1 = intersection(members1.map(user => user.get('id')), allMembers1); 615 | expect(intersection1.length, 'to equal', 2); 616 | 617 | const intersection2 = intersection(members2.map(user => user.get('id')), allMembers2); 618 | expect(intersection2.length, 'to equal', 2); 619 | 620 | expect(this.User.findAll, 'was called once'); 621 | expect(this.User.findAll, 'to have a call satisfying', [{ 622 | groupedLimit: { 623 | limit: 2, 624 | on: 'projectId', 625 | values: [ this.project1.get('id'), this.project2.get('id') ] 626 | } 627 | }]); 628 | }); 629 | 630 | it('batches to multiple findAll calls when limits are different', async function () { 631 | let [members1, members2, members3] = await Promise.all([ 632 | this.project1.getMembers({ limit: 4, [EXPECTED_OPTIONS_KEY]: this.context }), 633 | this.project2.getMembers({ limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 634 | this.project3.getMembers({ limit: 2, [EXPECTED_OPTIONS_KEY]: this.context }), 635 | ]); 636 | 637 | // there is no guaranteed order and this test returns different 638 | // values depending on the sequelize version. 639 | const allMembers1 = this.users.slice(1, 3).map(user => user.get('id')); // 0 is deleted 640 | const allMembers2 = this.users.slice(3, 7).map(user => user.get('id')); 641 | const allMembers3 = this.users.slice(7, 8).map(user => user.get('id')); // 8 is deleted 642 | 643 | const intersection1 = intersection(members1.map(user => user.get('id')), allMembers1); 644 | expect(intersection1.length, 'to equal', 2); 645 | 646 | const intersection2 = intersection(members2.map(user => user.get('id')), allMembers2); 647 | expect(intersection2.length, 'to equal', 2); 648 | 649 | const intersection3 = intersection(members3.map(user => user.get('id')), allMembers3); 650 | expect(intersection3.length, 'to equal', 1); 651 | 652 | expect(this.User.findAll, 'was called twice'); 653 | expect(this.User.findAll, 'to have a call satisfying', [{ 654 | where: { 655 | projectId: [this.project1.get('id')], 656 | }, 657 | limit: 4 658 | }]); 659 | expect(this.User.findAll, 'to have a call satisfying', [{ 660 | groupedLimit: { 661 | limit: 2, 662 | on: 'projectId', 663 | values: [ this.project2.get('id'), this.project3.get('id') ] 664 | } 665 | }]); 666 | }); 667 | }); 668 | 669 | describe('scope on target', function () { 670 | before(async function () { 671 | this.User = this.connection.define('user', { 672 | awesome: Sequelize.BOOLEAN 673 | }); 674 | this.Project = this.connection.define('project'); 675 | this.Project.hasMany(this.User, { as: 'members' }); 676 | this.Project.hasMany(this.User, { 677 | as: 'awesomeMembers', 678 | scope: { 679 | awesome: true 680 | }, 681 | foreignKey: { 682 | name: 'projectId', 683 | field: 'project_id' 684 | } 685 | }); 686 | 687 | await this.connection.sync({ force: true }); 688 | await createData.call(this); 689 | }); 690 | 691 | beforeEach(function () { 692 | this.context = createContext(this.connection); 693 | this.sandbox.spy(this.User, 'findAll'); 694 | }); 695 | 696 | afterEach(function () { 697 | this.sandbox.restore(); 698 | }); 699 | 700 | it('batches to multiple findAll call when different limits are applied', async function () { 701 | let members1 = this.project1.getAwesomeMembers({ limit: 10, [EXPECTED_OPTIONS_KEY]: this.context }) 702 | , members2 = this.project2.getAwesomeMembers({ limit: 10, [EXPECTED_OPTIONS_KEY]: this.context }) 703 | , members3 = this.project3.getAwesomeMembers({ limit: 1, [EXPECTED_OPTIONS_KEY]: this.context }); 704 | 705 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 706 | this.users[1], 707 | this.users[2] 708 | ]); 709 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 710 | this.users[4], 711 | this.users[6] 712 | ]); 713 | await expect(members3, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 714 | this.users[7] 715 | ]); 716 | 717 | expect(this.User.findAll, 'was called twice'); 718 | }); 719 | 720 | it('batches to a single findAll call', async function () { 721 | let members1 = this.project1.getAwesomeMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 722 | , members2 = this.project2.getAwesomeMembers({[EXPECTED_OPTIONS_KEY]: this.context}) 723 | , members3 = this.project3.getAwesomeMembers({[EXPECTED_OPTIONS_KEY]: this.context}); 724 | 725 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 726 | this.users[1], 727 | this.users[2] 728 | ]); 729 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 730 | this.users[4], 731 | this.users[6] 732 | ]); 733 | await expect(members3, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 734 | this.users[7], 735 | this.users[8] 736 | ]); 737 | 738 | expect(this.User.findAll, 'was called once'); 739 | }); 740 | 741 | it('batches to multiple findAll call when different scopes are applied', async function () { 742 | let members1 = this.project1.getAwesomeMembers({ limit: 10, [EXPECTED_OPTIONS_KEY]: this.context }) 743 | , members2 = this.project1.getMembers({ limit: 10, [EXPECTED_OPTIONS_KEY]: this.context }); 744 | 745 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 746 | this.users[1], 747 | this.users[2] 748 | ]); 749 | await expect(members2, 'when fulfilled', 'with set semantics to exhaustively satisfy', [ 750 | this.users[0], 751 | this.users[1], 752 | this.users[2] 753 | ]); 754 | 755 | expect(this.User.findAll, 'was called twice'); 756 | }); 757 | }); 758 | 759 | describe('support sourceKey in hasMany associations', function () { 760 | before(async function () { 761 | this.User = this.connection.define('user', { 762 | project__c: Sequelize.STRING, 763 | }); 764 | this.Project = this.connection.define('project', { 765 | sfid: { 766 | type: Sequelize.STRING, 767 | unique: true, 768 | }, 769 | }); 770 | this.User.belongsTo(this.Project, { 771 | foreignKey: 'project__c', 772 | targetKey: 'sfid', 773 | }); 774 | this.Project.hasMany(this.User, { 775 | foreignKey: 'project__c', 776 | sourceKey: 'sfid', 777 | }); 778 | 779 | await this.connection.sync({ force: true }); 780 | 781 | this.project1 = await this.Project.create({ 782 | id: randint(), 783 | sfid: '001abc', 784 | }, {returning: true}); 785 | 786 | this.userlessProject = await this.Project.create({ 787 | id: randint(), 788 | }, {returning: true}); 789 | 790 | this.users = await this.User.bulkCreate([ 791 | { id: randint() }, 792 | { id: randint() }, 793 | { id: randint() }, 794 | { id: randint() }, 795 | ], {returning: true}); 796 | 797 | await this.project1.setUsers(this.users); 798 | }); 799 | 800 | beforeEach(function () { 801 | this.context = createContext(this.connection); 802 | this.sandbox.spy(this.User, 'findAll'); 803 | }); 804 | 805 | afterEach(function () { 806 | this.sandbox.restore(); 807 | }); 808 | 809 | it('correctly links sourceKey and foreignKey', async function () { 810 | let members1 = this.project1.getUsers({[EXPECTED_OPTIONS_KEY]: this.context}); 811 | 812 | await expect(members1, 'when fulfilled', 'with set semantics to exhaustively satisfy', this.users); 813 | expect(this.User.findAll, 'was called once'); 814 | }); 815 | 816 | it('does not try to load if sourceKey is null', async function () { 817 | let users = this.userlessProject.getUsers({[EXPECTED_OPTIONS_KEY]: this.context}); 818 | 819 | await expect(users, 'when fulfilled', 'to exhaustively satisfy', null); 820 | expect(this.User.findAll, 'was not called'); 821 | }); 822 | }); 823 | }); 824 | --------------------------------------------------------------------------------