├── .eslintignore ├── babel.config.js ├── renovate.json ├── .gitignore ├── .flowconfig ├── .changeset ├── config.json └── README.md ├── .github ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature-request.md │ └── bug-report.md └── workflows │ └── validation.yml ├── resources ├── prepublish.sh └── watch.js ├── examples ├── Redis.md ├── GoogleDatastore.md ├── CouchDB.md ├── SQL.md ├── Knex.md └── RethinkDB.md ├── src ├── __tests__ │ ├── browser.test.js │ ├── unhandled.test.js │ ├── oldbrowser.test.js │ ├── abuse.test.js │ └── dataloader.test.js ├── index.d.ts └── index.js ├── LICENSE ├── CONTRIBUTING.md ├── package.json ├── CHANGELOG.md ├── .eslintrc ├── README.md └── flow-typed └── npm └── jest_v24.x.x.js /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | flow-typed/ 3 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = api => ({ 2 | presets: api.env('test') 3 | ? ['@babel/preset-flow'] 4 | : [['@babel/preset-env', { loose: true }], '@babel/preset-flow'], 5 | }); 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>the-guild-org/shared-config:renovate", 5 | ":preserveSemverRanges" 6 | ], 7 | "packageRules": [] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.iml 4 | .*.haste_cache.* 5 | .DS_Store 6 | .idea 7 | npm-debug.log 8 | 9 | node_modules 10 | coverage 11 | dist 12 | 13 | # Generated with release scripts 14 | index.d.ts 15 | index.js 16 | index.js.flow 17 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/lib/.* 3 | .*/dist/.* 4 | .*/coverage/.* 5 | .*/resources/.* 6 | .*/node_modules/y18n/test/.* 7 | 8 | [include] 9 | 10 | [libs] 11 | 12 | [options] 13 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectError 14 | include_warnings=true 15 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.1.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "graphql/dataloader" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "main", 12 | "updateInternalDependencies": "patch", 13 | "ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Not a bug or feature request? 4 | title: "[QUESTION]" 5 | labels: help wanted 6 | --- 7 | 8 | 18 | -------------------------------------------------------------------------------- /resources/prepublish.sh: -------------------------------------------------------------------------------- 1 | # Remove existing build artifacts 2 | rm -rf dist; 3 | 4 | # Build before publishing 5 | npm run build; 6 | 7 | # When Travis CI publishes to NPM, the published files are available in the root 8 | # directory, which produces a cleaner distribution. 9 | # 10 | cp dist/* . 11 | 12 | # Ensure a vanilla package.json before deploying so other tools do not interpret 13 | # The built output as requiring any further transformation. 14 | node -e "var package = require('./package.json'); \ 15 | delete package.scripts; \ 16 | delete package.devDependencies; \ 17 | delete package.publishConfig; \ 18 | require('fs').writeFileSync('dist/package.json', JSON.stringify(package, null, 2));" 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[REQUEST]" 5 | labels: enhancement 6 | --- 7 | 8 | ## What problem are you trying to solve? 9 | 10 | 11 | ## Describe the solution you'd like 12 | 13 | 14 | ## Describe alternatives you've considered 15 | 16 | 17 | ## Additional context 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | --- 7 | 8 | 9 | 10 | ## Expected Behavior 11 | 12 | 13 | ## Current Behavior 14 | 15 | 16 | ## Possible Solution 17 | 18 | 19 | ## Steps to Reproduce 20 | 21 | 22 | 23 | ## Context 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/validation.yml: -------------------------------------------------------------------------------- 1 | name: Flow check, Lint and Tests 2 | 3 | on: push 4 | 5 | jobs: 6 | validation: 7 | name: Testing on Node ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: [18, 20, 22] 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Use Node ${{ matrix.node-version }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | cache: 'yarn' 21 | 22 | - name: Install Dependencies using Yarn 23 | run: yarn --ignore-engines 24 | 25 | - name: Tests 26 | run: yarn test:ci 27 | 28 | - name: Upload coverage to Codecov 29 | uses: codecov/codecov-action@v4 30 | env: 31 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 32 | with: 33 | fail_ci_if_error: true 34 | -------------------------------------------------------------------------------- /examples/Redis.md: -------------------------------------------------------------------------------- 1 | # Using DataLoader with Redis 2 | 3 | Redis is a very simple key-value store which provides the batch load method 4 | [MGET](http://redis.io/commands/mget) which makes it very well suited for use 5 | with DataLoader. 6 | 7 | Here we build an example Redis DataLoader using [node_redis][]. 8 | 9 | ```js 10 | const DataLoader = require('dataloader'); 11 | const redis = require('redis'); 12 | 13 | const client = redis.createClient(); 14 | 15 | const redisLoader = new DataLoader( 16 | keys => 17 | new Promise((resolve, reject) => { 18 | client.mget(keys, (error, results) => { 19 | if (error) { 20 | return reject(error); 21 | } 22 | resolve( 23 | results.map((result, index) => 24 | result !== null ? result : new Error(`No key: ${keys[index]}`), 25 | ), 26 | ); 27 | }); 28 | }), 29 | ); 30 | ``` 31 | 32 | [node_redis]: https://github.com/NodeRedis/node_redis 33 | -------------------------------------------------------------------------------- /src/__tests__/browser.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present, GraphQL Foundation 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | // Mock out process.nextTick as not existing for this test before requiring. 11 | process.nextTick = (null: any); 12 | const DataLoader = require('..'); 13 | 14 | describe('Browser support', () => { 15 | it('batches multiple requests without process.nextTick', async () => { 16 | const loadCalls = []; 17 | const identityLoader = new DataLoader(async keys => { 18 | loadCalls.push(keys); 19 | return keys; 20 | }); 21 | 22 | const promise1 = identityLoader.load(1); 23 | const promise2 = identityLoader.load(2); 24 | 25 | const [value1, value2] = await Promise.all([promise1, promise2]); 26 | expect(value1).toBe(1); 27 | expect(value2).toBe(2); 28 | 29 | expect(loadCalls).toEqual([[1, 2]]); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/__tests__/unhandled.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present, GraphQL Foundation 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | const DataLoader = require('..'); 11 | 12 | describe('Unhandled rejections', () => { 13 | it('Not catching a primed error is an unhandled rejection', async () => { 14 | let hadUnhandledRejection = false; 15 | // Override Jest's unhandled detection 16 | global.jasmine.process.removeAllListeners('unhandledRejection'); 17 | global.jasmine.process.on('unhandledRejection', () => { 18 | hadUnhandledRejection = true; 19 | }); 20 | 21 | const identityLoader = new DataLoader(async keys => keys); 22 | 23 | identityLoader.prime(1, new Error('Error: 1')); 24 | 25 | // Ignore result. 26 | identityLoader.load(1); 27 | 28 | await new Promise(resolve => setTimeout(resolve, 10)); 29 | expect(hadUnhandledRejection).toBe(true); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/__tests__/oldbrowser.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present, GraphQL Foundation 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | // Mock out process.nextTick and setImmediate as not existing for this test 11 | // before requiring. 12 | process.nextTick = (null: any); 13 | global.setImmediate = (null: any); 14 | const DataLoader = require('..'); 15 | 16 | describe('Old browser support', () => { 17 | it('batches multiple requests without setImmediate', async () => { 18 | const loadCalls = []; 19 | const identityLoader = new DataLoader(async keys => { 20 | loadCalls.push(keys); 21 | return keys; 22 | }); 23 | 24 | const promise1 = identityLoader.load(1); 25 | const promise2 = identityLoader.load(2); 26 | 27 | const [value1, value2] = await Promise.all([promise1, promise2]); 28 | expect(value1).toBe(1); 29 | expect(value2).toBe(2); 30 | 31 | expect(loadCalls).toEqual([[1, 2]]); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) GraphQL Contributors 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 | -------------------------------------------------------------------------------- /examples/GoogleDatastore.md: -------------------------------------------------------------------------------- 1 | # Using DataLoader with Google Datastore 2 | 3 | Google Datastore is a "NoSQL" document database which supports [batch operations](https://cloud.google.com/datastore/docs/concepts/entities#batch_operations), 4 | making it well suited for use with DataLoader. 5 | 6 | Here we build an example Google Datastore DataLoader using [@google-cloud/datastore](https://cloud.google.com/nodejs/docs/reference/datastore/1.3.x/Datastore). 7 | 8 | ```js 9 | const Datastore = require('@google-cloud/datastore'); 10 | 11 | const datastore = new Datastore(); 12 | 13 | const datastoreLoader = new DataLoader( 14 | async keys => { 15 | const results = await datastore.get(keys); 16 | // Sort resulting entities by the keys they were requested with. 17 | const entities = results[0]; 18 | const entitiesByKey = {}; 19 | entities.forEach(entity => { 20 | entitiesByKey[JSON.stringify(entity[datastore.KEY])] = entity; 21 | }); 22 | return keys.map(key => entitiesByKey[JSON.stringify(key)] || null); 23 | }, 24 | { 25 | // Datastore complex keys need to be converted to a string for use as cache keys 26 | cacheKeyFn: key => JSON.stringify(key), 27 | }, 28 | ); 29 | ``` 30 | -------------------------------------------------------------------------------- /examples/CouchDB.md: -------------------------------------------------------------------------------- 1 | # Using DataLoader with CouchDB 2 | 3 | CouchDB is a "NoSQL" document database which supports batch loading via the 4 | [HTTP Bulk Document API](http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API), 5 | making it well suited for use with DataLoader. 6 | 7 | This example uses the [nano][] CouchDB client which offers a `fetch` method 8 | supporting the bulk document API. 9 | 10 | ```js 11 | const DataLoader = require('dataloader'); 12 | const nano = require('nano'); 13 | 14 | const couch = nano('http://localhost:5984'); 15 | 16 | const userDB = couch.use('users'); 17 | const userLoader = new DataLoader( 18 | keys => 19 | new Promise((resolve, reject) => { 20 | userDB.fetch({ keys: keys }, (error, docs) => { 21 | if (error) { 22 | return reject(error); 23 | } 24 | resolve( 25 | docs.rows.map(row => (row.error ? new Error(row.error) : row.doc)), 26 | ); 27 | }); 28 | }), 29 | ); 30 | 31 | // Usage 32 | 33 | const promise1 = userLoader.load('8fce1902834ac6458e9886fa7f89c0ef'); 34 | const promise2 = userLoader.load('00a271787f89c0ef2e10e88a0c00048b'); 35 | const [user1, user2] = await Promise.all([promise1, promise2]); 36 | console.log(user1, user2); 37 | ``` 38 | 39 | [nano]: https://github.com/dscape/nano 40 | -------------------------------------------------------------------------------- /examples/SQL.md: -------------------------------------------------------------------------------- 1 | # Using DataLoader with SQLite 2 | 3 | While not a key-value store, SQL offers a natural batch mechanism with 4 | `SELECT * WHERE IN` statements. While `DataLoader` is best suited for key-value 5 | stores, it is still suited for SQL when queries remain simple. This example 6 | requests the entire row at a given `id`, however your usage may differ. 7 | 8 | ```js 9 | const DataLoader = require('dataloader'); 10 | const sqlite3 = require('sqlite3'); 11 | 12 | const db = new sqlite3.Database('./to/your/db.sql'); 13 | 14 | // Dispatch a WHERE-IN query, ensuring response has rows in correct order. 15 | const userLoader = new DataLoader( 16 | ids => 17 | new Promise((resolve, reject) => { 18 | db.all( 19 | 'SELECT * FROM users WHERE id IN $ids', 20 | { $ids: ids }, 21 | (error, rows) => { 22 | if (error) { 23 | reject(error); 24 | } else { 25 | resolve( 26 | ids.map( 27 | id => 28 | rows.find(row => row.id === id) || 29 | new Error(`Row not found: ${id}`), 30 | ), 31 | ); 32 | } 33 | }, 34 | ); 35 | }), 36 | ); 37 | 38 | // Usage 39 | 40 | const promise1 = userLoader.load('1234'); 41 | const promise2 = userLoader.load('5678'); 42 | const [user1, user2] = await Promise.all([promise1, promise2]); 43 | console.log(user1, user2); 44 | ``` 45 | 46 | [sqlite3]: https://github.com/mapbox/node-sqlite3 47 | -------------------------------------------------------------------------------- /examples/Knex.md: -------------------------------------------------------------------------------- 1 | # Using DataLoader with Knex.js 2 | 3 | This example demonstrates how to use **DataLoader** with SQL databases via 4 | [Knex.js][knex], which is a SQL query builder and a client for popular 5 | databases such as **PostgreSQL**, **MySQL**, **MariaDB** etc. 6 | 7 | Similarly to the [SQL](./SQL.md) example, you can use "where in" clause to 8 | fetch multiple records by the list of IDs with the only difference that you 9 | don't have to write any SQL code by hand. 10 | 11 | ```js 12 | const DataLoader = require('dataloader'); 13 | const db = require('./db'); // an instance of Knex client 14 | 15 | // The list of data loaders 16 | 17 | const loaders = { 18 | user: new DataLoader(ids => 19 | db 20 | .table('users') 21 | .whereIn('id', ids) 22 | .select() 23 | .then(rows => ids.map(id => rows.find(x => x.id === id))), 24 | ), 25 | 26 | story: new DataLoader(ids => 27 | db 28 | .table('stories') 29 | .whereIn('id', ids) 30 | .select() 31 | .then(rows => ids.map(id => rows.find(x => x.id === id))), 32 | ), 33 | 34 | storiesByUserId: new DataLoader(ids => 35 | db 36 | .table('stories') 37 | .whereIn('author_id', ids) 38 | .select() 39 | .then(rows => ids.map(id => rows.filter(x => x.author_id === id))), 40 | ), 41 | }; 42 | 43 | // Usage 44 | 45 | const [user, stories] = await Promise.all([ 46 | loaders.user.load('1234'), 47 | loaders.storiesByUserId.load('1234'), 48 | ]); 49 | ``` 50 | 51 | For a complete example visit [kriasoft/nodejs-api-starter][nsk]. 52 | 53 | [knex]: http://knexjs.org/ 54 | [nsk]: https://github.com/kriasoft/nodejs-api-starter#readme 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to DataLoader 2 | 3 | We want to make contributing to this project as easy and transparent as 4 | possible. 5 | 6 | ## Code of Conduct 7 | 8 | This project's code of conduct is described in the GraphQL Foundation's [`CODE_OF_CONDUCT.md`](https://github.com/graphql/foundation/blob/master/CODE-OF-CONDUCT.md) 9 | 10 | ## Pull Requests 11 | 12 | We actively welcome your pull requests for documentation and code. 13 | 14 | 1. Fork the repo and create your branch from `master`. 15 | 2. If you've added code that should be tested, add tests with 100% coverage. 16 | 3. If you've changed APIs, update the documentation. 17 | 4. Ensure the test suite passes. 18 | 5. Make sure your code lints. 19 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 20 | 7. Run `yarn changeset` and describe the change you're proposing. Commit the file it creates in `.changeset` to the repo. [You can read more about changeset here.](https://github.com/changesets/changesets) 21 | 8. Open a Pull Request so we can review and incorporate your change. 22 | 23 | ## Releases 24 | 25 | To release a new version: 26 | 1. Run `yarn changeset version` to bump the version of the package. 27 | 2. Run `yarn release` this will create a new release on GitHub and publish the package to NPM. 28 | 29 | ## Issues 30 | 31 | We use GitHub issues to track public bugs. Please ensure your description is 32 | clear and has sufficient instructions to be able to reproduce the issue. 33 | 34 | ## Coding Style 35 | 36 | - 2 spaces for indentation rather than tabs 37 | - 80 character line length 38 | - See .eslintrc for the gory details. 39 | 40 | ## License 41 | 42 | By contributing to DataLoader, you agree that your contributions will be 43 | licensed under its MIT license. 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dataloader", 3 | "version": "2.2.3", 4 | "description": "A data loading utility to reduce requests to a backend via batching and caching.", 5 | "contributors": [ 6 | "Lee Byron (http://leebyron.com/)", 7 | "Daniel Schafer ", 8 | "Nicholas Schrock " 9 | ], 10 | "license": "MIT", 11 | "homepage": "https://github.com/graphql/dataloader", 12 | "bugs": { 13 | "url": "https://github.com/graphql/dataloader/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "http://github.com/graphql/dataloader.git" 18 | }, 19 | "main": "index.js", 20 | "typings": "index.d.ts", 21 | "scripts": { 22 | "test": "npm run lint && npm run check && npm run testonly", 23 | "test:ci": "npm run lint && npm run check && npm run testonly -- --coverage", 24 | "lint": "eslint .", 25 | "check": "flow check --max-warnings 0", 26 | "build": "babel src --ignore src/__tests__ --out-dir dist/ ; cp src/index.js dist/index.js.flow ; cp src/index.d.ts dist/", 27 | "watch": "babel resources/watch.js | node", 28 | "testonly": "jest src", 29 | "prerelease": ". ./resources/prepublish.sh", 30 | "release": "changeset publish" 31 | }, 32 | "files": [ 33 | "index.js", 34 | "index.js.flow", 35 | "index.d.ts", 36 | "README.md", 37 | "LICENSE", 38 | "PATENTS" 39 | ], 40 | "devDependencies": { 41 | "@babel/cli": "7.7.0", 42 | "@babel/core": "7.7.2", 43 | "@babel/node": "7.7.0", 44 | "@babel/preset-env": "7.7.1", 45 | "@babel/preset-flow": "7.0.0", 46 | "@changesets/changelog-github": "0.4.6", 47 | "@changesets/cli": "2.24.3", 48 | "babel-eslint": "10.0.3", 49 | "eslint": "6.6.0", 50 | "eslint-plugin-prettier": "^3.4.1", 51 | "flow-bin": "0.112.0", 52 | "jest": "24.9.0", 53 | "prettier": "^2.8.3", 54 | "sane": "4.1.0" 55 | }, 56 | "publishConfig": { 57 | "access": "public" 58 | }, 59 | "prettier": { 60 | "arrowParens": "avoid", 61 | "singleQuote": true, 62 | "trailingComma": "all", 63 | "overrides": [ 64 | { 65 | "files": "src/**/*.js", 66 | "options": { 67 | "parser": "babel-flow" 68 | } 69 | } 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/RethinkDB.md: -------------------------------------------------------------------------------- 1 | # RethinkDb 2 | 3 | RethinkDb offers a batching method called `getAll` but there are a few caveats : 4 | 5 | - Order of results is not guaranteed ([rethinkdb/rethinkdb#5187](https://github.com/rethinkdb/rethinkdb/issues/5187)) 6 | - Non-existent keys will not return an empty record 7 | 8 | For example, against a table `example_table` with these records: 9 | 10 | ```js 11 | [ 12 | { id: 1, name: 'Document 1' }, 13 | { id: 2, name: 'Document 2' }, 14 | ]; 15 | ``` 16 | 17 | A query `r.getAll(1, 2, 3)` could return: 18 | 19 | ```js 20 | [ 21 | { id: 2, name: 'Document 2' }, 22 | { id: 1, name: 'Document 1' }, 23 | ]; 24 | ``` 25 | 26 | Because query keys and values are associated by position in the dataloader 27 | cache, this naive implementation won't work (with the same table as above): 28 | 29 | ```js 30 | const r = require('rethinkdb'); 31 | const db = await r.connect(); 32 | 33 | const exampleLoader = new DataLoader(async keys => { 34 | const result = await db.table('example_table').getAll(...keys); 35 | return result.toArray(); 36 | }); 37 | 38 | await exampleLoader.loadMany([1, 2, 3]); // Throws (values length !== keys length) 39 | 40 | await exampleLoader.loadMany([1, 2]); 41 | await exampleLoader.load(1); // {"id": 2, "name": "Document 2"} 42 | ``` 43 | 44 | A solution is to normalize results returned by `getAll` to match the structure 45 | of supplied `keys`. 46 | 47 | To achieve this efficiently, we first write an indexing function. This function 48 | will return a `Map` indexing results. 49 | 50 | Parameters: 51 | 52 | - `results`: Array of RethinkDb results 53 | - `indexField`: String indicating which field was used as index for this batch query 54 | - `cacheKeyFn`: Optional function used to serialize non-scalar index field values 55 | 56 | ```js 57 | function indexResults(results, indexField, cacheKeyFn = key => key) { 58 | const indexedResults = new Map(); 59 | results.forEach(res => { 60 | indexedResults.set(cacheKeyFn(res[indexField]), res); 61 | }); 62 | return indexedResults; 63 | } 64 | ``` 65 | 66 | Then, we can leverage our Map to normalize RethinkDb results with another 67 | utility function which will produce a normalizing function. 68 | 69 | ```js 70 | function normalizeRethinkDbResults(keys, indexField, cacheKeyFn = key => key) { 71 | return results => { 72 | const indexedResults = indexResults(results, indexField, cacheKeyFn); 73 | return keys.map( 74 | val => 75 | indexedResults.get(cacheKeyFn(val)) || 76 | new Error(`Key not found : ${val}`), 77 | ); 78 | }; 79 | } 80 | ``` 81 | 82 | Full dataloader implementation: 83 | 84 | ```js 85 | const r = require('rethinkdb'); 86 | const db = await r.connect(); 87 | 88 | const exampleLoader = new DataLoader(async keys => { 89 | const results = await db.table('example_table').getAll(...keys); 90 | return normalizeRethinkDbResults(res.toArray(), 'id'); 91 | }); 92 | 93 | // [{"id": 1, "name": "Document 1"}, {"id": 2, "name": "Document 2"}, Error]; 94 | await exampleLoader.loadMany([1, 2, 3]); 95 | 96 | // {"id": 1, "name": "Document 1"} 97 | await exampleLoader.load(1); 98 | ``` 99 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # dataloader 2 | 3 | ## 2.2.3 4 | 5 | ### Patch Changes 6 | 7 | - [#342](https://github.com/graphql/dataloader/pull/342) [`38fedd4`](https://github.com/graphql/dataloader/commit/38fedd4106e9e3e7eb77bd68e42abc088110bd43) Thanks [@abendi](https://github.com/abendi)! - Ensure `cacheKeyFn` is not called when caching is disabled, since the key is not utilized in that case. 8 | 9 | ## 2.2.2 10 | 11 | ### Patch Changes 12 | 13 | - [#334](https://github.com/graphql/dataloader/pull/334) [`e286f66`](https://github.com/graphql/dataloader/commit/e286f662657675fa790f33abcd6aa87b5aac2be3) Thanks [@henrinormak](https://github.com/henrinormak)! - Added missing type definition for Dataloader.name 14 | 15 | ## 2.2.1 16 | 17 | ### Patch Changes 18 | 19 | - [#331](https://github.com/graphql/dataloader/pull/331) [`6d2efb7`](https://github.com/graphql/dataloader/commit/6d2efb7dd0363062de255e723c29a781d0ea9937) Thanks [@saihaj](https://github.com/saihaj)! - `name` is an optional property 20 | 21 | ## 2.2.0 22 | 23 | ### Minor Changes 24 | 25 | - [#326](https://github.com/graphql/dataloader/pull/326) [`6c758d0`](https://github.com/graphql/dataloader/commit/6c758d03bef628a69b238f053da3b263cd5e3321) Thanks [@SimenB](https://github.com/SimenB)! - Add `name` property to `DataLoader`. Useful in APM tools. 26 | 27 | ### Patch Changes 28 | 29 | - [#318](https://github.com/graphql/dataloader/pull/318) [`588a8b6`](https://github.com/graphql/dataloader/commit/588a8b6c6391aad042b369f10dc440c7e0458312) Thanks [@boopathi](https://github.com/boopathi)! - Fix the propagation of sync throws in the batch function to the loader function instead of crashing the process wtih an uncaught exception. 30 | 31 | * [#252](https://github.com/graphql/dataloader/pull/252) [`fae38f1`](https://github.com/graphql/dataloader/commit/fae38f14702e925d1e59051d7e5cb3a9a78bfde8) Thanks [@LinusU](https://github.com/LinusU)! - Fix types for priming cache with promise 32 | 33 | - [#321](https://github.com/graphql/dataloader/pull/321) [`3cd3a43`](https://github.com/graphql/dataloader/commit/3cd3a430bdb4f9ef2f7f265a29e93e0255277885) Thanks [@thekevinbrown](https://github.com/thekevinbrown)! - Resolves an issue where the maxBatchSize parameter wouldn't be fully used on each batch sent to the backend loader. 34 | 35 | ## 2.1.0 36 | 37 | ### Minor Changes 38 | 39 | - 28cf959: - Do not return void results from arrow functions https://github.com/graphql/dataloader/commit/3b0bae94e91453d9a432c02628745252abc5e011 40 | - Fix typo in `loader.load()` error message https://github.com/graphql/dataloader/commit/249b2b966a8807c50e07746ff04acb8c48fa4357 41 | - Fix typo in SQL example https://github.com/graphql/dataloader/commit/cae1a3d9bfa48e181a49fd443f43813b335dc120 42 | - Fix typo in TypeScript declaration https://github.com/graphql/dataloader/commit/ef6d32f97cde16aba84d96dc806c4439eaf8efae 43 | - Most of the browsers don't have `setImmediate`. `setImmediate || setTimeout` doesn't work and it throws `setImmediate` is not defined in this case, so we should check setImmediate with typeof. And some environments like Cloudflare Workers don't allow you to set setTimeout directly to another variable. https://github.com/graphql/dataloader/commit/3e62fbe7d42b7ab1ec54818a1491cb0107dd828a 44 | 45 | ### Patch Changes 46 | 47 | - 3135e9a: Fix typo in jsdoc comment; flip "objects are keys" to "keys are objects" 48 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present, GraphQL Foundation 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | /** 9 | * DataLoader creates a public API for loading data from a particular 10 | * data back-end with unique keys such as the id column of a SQL table 11 | * or document name in a MongoDB database, given a batch loading function. 12 | * 13 | * Each DataLoader instance contains a unique memoized cache. Use caution 14 | * when used in long-lived applications or those which serve many users 15 | * with different access permissions and consider creating a new instance 16 | * per web request. 17 | */ 18 | declare class DataLoader { 19 | constructor( 20 | batchLoadFn: DataLoader.BatchLoadFn, 21 | options?: DataLoader.Options, 22 | ); 23 | 24 | /** 25 | * Loads a key, returning a `Promise` for the value represented by that key. 26 | */ 27 | load(key: K): Promise; 28 | 29 | /** 30 | * Loads multiple keys, promising an array of values: 31 | * 32 | * var [ a, b ] = await myLoader.loadMany([ 'a', 'b' ]); 33 | * 34 | * This is equivalent to the more verbose: 35 | * 36 | * var [ a, b ] = await Promise.all([ 37 | * myLoader.load('a'), 38 | * myLoader.load('b') 39 | * ]); 40 | * 41 | */ 42 | loadMany(keys: ArrayLike): Promise>; 43 | 44 | /** 45 | * Clears the value at `key` from the cache, if it exists. Returns itself for 46 | * method chaining. 47 | */ 48 | clear(key: K): this; 49 | 50 | /** 51 | * Clears the entire cache. To be used when some event results in unknown 52 | * invalidations across this particular `DataLoader`. Returns itself for 53 | * method chaining. 54 | */ 55 | clearAll(): this; 56 | 57 | /** 58 | * Adds the provided key and value to the cache. If the key already exists, no 59 | * change is made. Returns itself for method chaining. 60 | */ 61 | prime(key: K, value: V | PromiseLike | Error): this; 62 | 63 | 64 | /** 65 | * The name given to this `DataLoader` instance. Useful for APM tools. 66 | * 67 | * Is `null` if not set in the constructor. 68 | */ 69 | name: string | null; 70 | } 71 | 72 | declare namespace DataLoader { 73 | // If a custom cache is provided, it must be of this type (a subset of ES6 Map). 74 | export type CacheMap = { 75 | get(key: K): V | void; 76 | set(key: K, value: V): any; 77 | delete(key: K): any; 78 | clear(): any; 79 | }; 80 | 81 | // A Function, which when given an Array of keys, returns a Promise of an Array 82 | // of values or Errors. 83 | export type BatchLoadFn = ( 84 | keys: ReadonlyArray, 85 | ) => PromiseLike>; 86 | 87 | // Optionally turn off batching or caching or provide a cache key function or a 88 | // custom cache instance. 89 | export type Options = { 90 | /** 91 | * Default `true`. Set to `false` to disable batching, invoking 92 | * `batchLoadFn` with a single load key. This is equivalent to setting 93 | * `maxBatchSize` to `1`. 94 | */ 95 | batch?: boolean; 96 | 97 | /** 98 | * Default `Infinity`. Limits the number of items that get passed in to the 99 | * `batchLoadFn`. May be set to `1` to disable batching. 100 | */ 101 | maxBatchSize?: number; 102 | 103 | /** 104 | * Default see https://github.com/graphql/dataloader#batch-scheduling. 105 | * A function to schedule the later execution of a batch. The function is 106 | * expected to call the provided callback in the immediate future. 107 | */ 108 | batchScheduleFn?: (callback: () => void) => void; 109 | 110 | /** 111 | * Default `true`. Set to `false` to disable memoization caching, creating a 112 | * new Promise and new key in the `batchLoadFn` for every load of the same 113 | * key. This is equivalent to setting `cacheMap` to `null`. 114 | */ 115 | cache?: boolean; 116 | 117 | /** 118 | * Default `key => key`. Produces cache key for a given load key. Useful 119 | * when keys are objects and two objects should be considered equivalent. 120 | */ 121 | cacheKeyFn?: (key: K) => C; 122 | 123 | /** 124 | * Default `new Map()`. Instance of `Map` (or an object with a similar API) 125 | * to be used as cache. May be set to `null` to disable caching. 126 | */ 127 | cacheMap?: CacheMap> | null; 128 | 129 | /** 130 | * The name given to this `DataLoader` instance. Useful for APM tools. 131 | * 132 | * Is `null` if not set in the constructor. 133 | */ 134 | name?: string | null; 135 | }; 136 | } 137 | 138 | export = DataLoader; 139 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "$ReadOnlyArray": true 4 | }, 5 | 6 | "parser": "babel-eslint", 7 | 8 | "plugins": ["prettier"], 9 | 10 | "env": { 11 | "es6": true, 12 | "node": true, 13 | "jest": true 14 | }, 15 | 16 | "rules": { 17 | "block-scoped-var": 0, 18 | "callback-return": 2, 19 | "camelcase": [2, {"properties": "always"}], 20 | "comma-dangle": 0, 21 | "comma-spacing": 0, 22 | "complexity": 0, 23 | "computed-property-spacing": [2, "never"], 24 | "consistent-return": 0, 25 | "consistent-this": 0, 26 | "default-case": 0, 27 | "dot-location": [2, "property"], 28 | "dot-notation": 0, 29 | "eol-last": 2, 30 | "eqeqeq": 2, 31 | "func-names": 0, 32 | "func-style": 0, 33 | "generator-star-spacing": [0, {"before": true, "after": false}], 34 | "guard-for-in": 2, 35 | "handle-callback-err": [2, "error"], 36 | "id-length": 0, 37 | "id-match": [2, "^(?:_?[a-zA-Z0-9]*)|[_A-Z0-9]+$"], 38 | "init-declarations": 0, 39 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 40 | "keyword-spacing": 2, 41 | "linebreak-style": 2, 42 | "lines-around-comment": 0, 43 | "max-depth": 0, 44 | "max-nested-callbacks": 0, 45 | "max-params": 0, 46 | "max-statements": 0, 47 | "new-cap": 0, 48 | "new-parens": 2, 49 | "newline-after-var": 0, 50 | "no-alert": 2, 51 | "no-array-constructor": 2, 52 | "no-bitwise": 0, 53 | "no-caller": 2, 54 | "no-catch-shadow": 0, 55 | "no-class-assign": 2, 56 | "no-cond-assign": 2, 57 | "no-console": 1, 58 | "no-const-assign": 2, 59 | "no-constant-condition": 2, 60 | "no-continue": 0, 61 | "no-control-regex": 0, 62 | "no-debugger": 1, 63 | "no-delete-var": 2, 64 | "no-div-regex": 2, 65 | "no-dupe-args": 2, 66 | "no-dupe-keys": 2, 67 | "no-duplicate-case": 2, 68 | "no-else-return": 2, 69 | "no-empty": 2, 70 | "no-empty-character-class": 2, 71 | "no-eq-null": 0, 72 | "no-eval": 2, 73 | "no-ex-assign": 2, 74 | "no-extend-native": 2, 75 | "no-extra-bind": 2, 76 | "no-extra-boolean-cast": 2, 77 | "no-extra-parens": 0, 78 | "no-extra-semi": 2, 79 | "no-fallthrough": 2, 80 | "no-floating-decimal": 2, 81 | "no-func-assign": 2, 82 | "no-implicit-coercion": 2, 83 | "no-implied-eval": 2, 84 | "no-inline-comments": 0, 85 | "no-inner-declarations": [2, "functions"], 86 | "no-invalid-regexp": 2, 87 | "no-invalid-this": 0, 88 | "no-irregular-whitespace": 2, 89 | "no-iterator": 2, 90 | "no-label-var": 2, 91 | "no-labels": 0, 92 | "no-lone-blocks": 2, 93 | "no-lonely-if": 2, 94 | "no-loop-func": 0, 95 | "no-mixed-requires": [2, true], 96 | "no-mixed-spaces-and-tabs": 2, 97 | "no-multi-spaces": 2, 98 | "no-multi-str": 2, 99 | "no-multiple-empty-lines": 0, 100 | "no-native-reassign": 0, 101 | "no-negated-in-lhs": 2, 102 | "no-nested-ternary": 0, 103 | "no-new": 2, 104 | "no-new-func": 0, 105 | "no-new-object": 2, 106 | "no-new-require": 2, 107 | "no-new-wrappers": 2, 108 | "no-obj-calls": 2, 109 | "no-octal": 2, 110 | "no-octal-escape": 2, 111 | "no-param-reassign": 2, 112 | "no-path-concat": 2, 113 | "no-plusplus": 0, 114 | "no-process-env": 0, 115 | "no-process-exit": 0, 116 | "no-proto": 2, 117 | "no-redeclare": 2, 118 | "no-regex-spaces": 2, 119 | "no-restricted-modules": 0, 120 | "no-return-assign": 2, 121 | "no-script-url": 2, 122 | "no-self-compare": 0, 123 | "no-sequences": 2, 124 | "no-shadow": 2, 125 | "no-shadow-restricted-names": 2, 126 | "no-spaced-func": 2, 127 | "no-sparse-arrays": 2, 128 | "no-sync": 2, 129 | "no-ternary": 0, 130 | "no-this-before-super": 2, 131 | "no-throw-literal": 2, 132 | "no-trailing-spaces": 2, 133 | "no-undef": 2, 134 | "no-undef-init": 2, 135 | "no-undefined": 0, 136 | "no-underscore-dangle": 0, 137 | "no-unexpected-multiline": 2, 138 | "no-unneeded-ternary": 2, 139 | "no-unreachable": 2, 140 | "no-unused-expressions": 2, 141 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 142 | "no-use-before-define": 0, 143 | "no-useless-call": 2, 144 | "no-var": 0, 145 | "no-void": 2, 146 | "no-warning-comments": 0, 147 | "no-with": 2, 148 | "object-curly-spacing": [0, "always"], 149 | "object-shorthand": [2, "always"], 150 | "one-var": [2, "never"], 151 | "operator-assignment": [2, "always"], 152 | "prefer-const": 0, 153 | "prefer-reflect": 0, 154 | "prefer-spread": 0, 155 | "quote-props": [2, "as-needed"], 156 | "radix": 2, 157 | "require-yield": 2, 158 | "semi-spacing": [2, {"before": false, "after": true}], 159 | "sort-vars": 0, 160 | "space-before-blocks": [2, "always"], 161 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 162 | "space-in-parens": 0, 163 | "space-infix-ops": [2, {"int32Hint": false}], 164 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 165 | "spaced-comment": [2, "always"], 166 | "strict": 0, 167 | "use-isnan": 2, 168 | "valid-jsdoc": 0, 169 | "valid-typeof": 2, 170 | "vars-on-top": 0, 171 | "wrap-iife": 2, 172 | "wrap-regex": 0, 173 | "yoda": [2, "never", {"exceptRange": true}], 174 | 175 | "prettier/prettier": 2 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /resources/watch.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import sane from 'sane'; 3 | import { resolve as resolvePath } from 'path'; 4 | import { spawn } from 'child_process'; 5 | import flowBinPath from 'flow-bin'; 6 | 7 | process.env.PATH += ':./node_modules/.bin'; 8 | 9 | var cmd = resolvePath(__dirname); 10 | var srcDir = resolvePath(cmd, './src'); 11 | 12 | function exec(command, options) { 13 | return new Promise((resolve, reject) => { 14 | var child = spawn(command, options, { 15 | cmd, 16 | env: process.env, 17 | stdio: 'inherit', 18 | }); 19 | child.on('exit', code => { 20 | if (code === 0) { 21 | resolve(true); 22 | } else { 23 | reject(new Error('Error code: ' + code)); 24 | } 25 | }); 26 | }); 27 | } 28 | 29 | var flowServer = spawn(flowBinPath, ['server'], { 30 | cmd, 31 | env: process.env, 32 | }); 33 | 34 | var watcher = sane(srcDir, { glob: ['**/*.js'] }) 35 | .on('ready', startWatch) 36 | .on('add', changeFile) 37 | .on('delete', deleteFile) 38 | .on('change', changeFile); 39 | 40 | process.on('SIGINT', () => { 41 | watcher.close(); 42 | flowServer.kill(); 43 | console.log(CLEARLINE + yellow(invert('stopped watching'))); 44 | process.exit(); 45 | }); 46 | 47 | var isChecking; 48 | var needsCheck; 49 | var toCheck = {}; 50 | var timeout; 51 | 52 | function startWatch() { 53 | process.stdout.write(CLEARSCREEN + green(invert('watching...'))); 54 | } 55 | 56 | function changeFile(filepath, root, stat) { 57 | if (!stat.isDirectory()) { 58 | toCheck[filepath] = true; 59 | debouncedCheck(); 60 | } 61 | } 62 | 63 | function deleteFile(filepath) { 64 | delete toCheck[filepath]; 65 | debouncedCheck(); 66 | } 67 | 68 | function debouncedCheck() { 69 | needsCheck = true; 70 | clearTimeout(timeout); 71 | timeout = setTimeout(guardedCheck, 250); 72 | } 73 | 74 | function guardedCheck() { 75 | if (isChecking || !needsCheck) { 76 | return; 77 | } 78 | isChecking = true; 79 | var filepaths = Object.keys(toCheck); 80 | toCheck = {}; 81 | needsCheck = false; 82 | checkFiles(filepaths).then(() => { 83 | isChecking = false; 84 | process.nextTick(guardedCheck); 85 | }); 86 | } 87 | 88 | function checkFiles(filepaths) { 89 | console.log('\u001b[2J'); 90 | 91 | return parseFiles(filepaths) 92 | .then(() => runTests(filepaths)) 93 | .then(testSuccess => 94 | lintFiles(filepaths).then(lintSuccess => 95 | typecheckStatus().then( 96 | typecheckSuccess => testSuccess && lintSuccess && typecheckSuccess, 97 | ), 98 | ), 99 | ) 100 | .catch(() => false) 101 | .then(success => { 102 | process.stdout.write( 103 | '\n' + (success ? '' : '\x07') + green(invert('watching...')), 104 | ); 105 | }); 106 | } 107 | 108 | // Checking steps 109 | 110 | function parseFiles(filepaths) { 111 | console.log('Checking Syntax'); 112 | 113 | return Promise.all( 114 | filepaths.map(filepath => { 115 | if (isJS(filepath) && !isTest(filepath)) { 116 | return exec('babel', [ 117 | '--optional', 118 | 'runtime', 119 | '--out-file', 120 | '/dev/null', 121 | srcPath(filepath), 122 | ]); 123 | } 124 | }), 125 | ); 126 | } 127 | 128 | function runTests(filepaths) { 129 | console.log('\nRunning Tests'); 130 | 131 | return exec( 132 | 'jest', 133 | allTests(filepaths) ? filepaths.map(srcPath) : ['src'], 134 | ).catch(() => false); 135 | } 136 | 137 | function lintFiles(filepaths) { 138 | console.log('Linting Code\n'); 139 | 140 | return filepaths.reduce( 141 | (prev, filepath) => 142 | prev.then(prevSuccess => { 143 | if (isJS(filepath)) { 144 | process.stdout.write(' ' + filepath + ' ...'); 145 | return exec('eslint', [srcPath(filepath)]) 146 | .catch(() => false) 147 | .then(success => { 148 | console.log( 149 | CLEARLINE + ' ' + (success ? CHECK : X) + ' ' + filepath, 150 | ); 151 | return prevSuccess && success; 152 | }); 153 | } 154 | return prevSuccess; 155 | }), 156 | Promise.resolve(true), 157 | ); 158 | } 159 | 160 | function typecheckStatus() { 161 | console.log('\nType Checking\n'); 162 | return exec(flowBinPath, ['status']).catch(() => false); 163 | } 164 | 165 | // Filepath 166 | 167 | function srcPath(filepath) { 168 | return resolvePath(srcDir, filepath); 169 | } 170 | 171 | // Predicates 172 | 173 | function isJS(filepath) { 174 | return filepath.indexOf('.js') === filepath.length - 3; 175 | } 176 | 177 | function allTests(filepaths) { 178 | return filepaths.length > 0 && filepaths.every(isTest); 179 | } 180 | 181 | function isTest(filepath) { 182 | return isJS(filepath) && filepath.indexOf('__tests__/') !== -1; 183 | } 184 | 185 | // Print helpers 186 | 187 | var CLEARSCREEN = '\u001b[2J'; 188 | var CLEARLINE = '\r\x1B[K'; 189 | var CHECK = green('\u2713'); 190 | var X = red('\u2718'); 191 | 192 | function invert(str) { 193 | return `\u001b[7m ${str} \u001b[27m`; 194 | } 195 | 196 | function red(str) { 197 | return `\x1B[K\u001b[1m\u001b[31m${str}\u001b[39m\u001b[22m`; 198 | } 199 | 200 | function green(str) { 201 | return `\x1B[K\u001b[1m\u001b[32m${str}\u001b[39m\u001b[22m`; 202 | } 203 | 204 | function yellow(str) { 205 | return `\x1B[K\u001b[1m\u001b[33m${str}\u001b[39m\u001b[22m`; 206 | } 207 | -------------------------------------------------------------------------------- /src/__tests__/abuse.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present, GraphQL Foundation 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | const DataLoader = require('..'); 11 | 12 | describe('Provides descriptive error messages for API abuse', () => { 13 | it('Loader creation requires a function', () => { 14 | expect(() => { 15 | // $FlowExpectError 16 | new DataLoader(); // eslint-disable-line no-new 17 | }).toThrow( 18 | 'DataLoader must be constructed with a function which accepts ' + 19 | 'Array and returns Promise>, but got: undefined.', 20 | ); 21 | 22 | expect(() => { 23 | // $FlowExpectError 24 | new DataLoader({}); // eslint-disable-line no-new 25 | }).toThrow( 26 | 'DataLoader must be constructed with a function which accepts ' + 27 | 'Array and returns Promise>, but got: [object Object].', 28 | ); 29 | }); 30 | 31 | it('Load function requires an key', () => { 32 | const idLoader = new DataLoader(async keys => keys); 33 | 34 | expect(() => { 35 | // $FlowExpectError 36 | idLoader.load(); 37 | }).toThrow( 38 | 'The loader.load() function must be called with a value, ' + 39 | 'but got: undefined.', 40 | ); 41 | 42 | expect(() => { 43 | // $FlowExpectError 44 | idLoader.load(null); 45 | }).toThrow( 46 | 'The loader.load() function must be called with a value, ' + 47 | 'but got: null.', 48 | ); 49 | 50 | // Falsey values like the number 0 is acceptable 51 | expect(() => { 52 | idLoader.load(0); 53 | }).not.toThrow(); 54 | }); 55 | 56 | it('LoadMany function requires a list of key', () => { 57 | const idLoader = new DataLoader(async keys => keys); 58 | 59 | expect(() => { 60 | // $FlowExpectError 61 | idLoader.loadMany(); 62 | }).toThrow( 63 | 'The loader.loadMany() function must be called with Array ' + 64 | 'but got: undefined.', 65 | ); 66 | 67 | expect(() => { 68 | // $FlowExpectError 69 | idLoader.loadMany(1, 2, 3); 70 | }).toThrow( 71 | 'The loader.loadMany() function must be called with Array ' + 72 | 'but got: 1.', 73 | ); 74 | 75 | // Empty array is acceptable 76 | expect(() => { 77 | idLoader.loadMany([]); 78 | }).not.toThrow(); 79 | }); 80 | 81 | it('Batch function must return a Promise, not null', async () => { 82 | // $FlowExpectError 83 | const badLoader = new DataLoader(() => null); 84 | 85 | let caughtError; 86 | try { 87 | await badLoader.load(1); 88 | } catch (error) { 89 | caughtError = error; 90 | } 91 | expect(caughtError).toBeInstanceOf(Error); 92 | expect((caughtError: any).message).toBe( 93 | 'DataLoader must be constructed with a function which accepts ' + 94 | 'Array and returns Promise>, but the function did ' + 95 | 'not return a Promise: null.', 96 | ); 97 | }); 98 | 99 | it('Batch function must return a Promise, not error synchronously', async () => { 100 | const badLoader = new DataLoader(() => { 101 | throw new Error('Mock Synchronous Error'); 102 | }); 103 | 104 | let caughtError; 105 | try { 106 | await badLoader.load(1); 107 | } catch (error) { 108 | caughtError = error; 109 | } 110 | expect(caughtError).toBeInstanceOf(Error); 111 | expect((caughtError: any).message).toBe( 112 | 'DataLoader must be constructed with a function which accepts ' + 113 | 'Array and returns Promise>, but the function ' + 114 | 'errored synchronously: Error: Mock Synchronous Error.', 115 | ); 116 | }); 117 | 118 | it('Batch function must return a Promise, not a value', async () => { 119 | // Note: this is returning the keys directly, rather than a promise to keys. 120 | // $FlowExpectError 121 | const badLoader = new DataLoader(keys => keys); 122 | 123 | let caughtError; 124 | try { 125 | await badLoader.load(1); 126 | } catch (error) { 127 | caughtError = error; 128 | } 129 | expect(caughtError).toBeInstanceOf(Error); 130 | expect((caughtError: any).message).toBe( 131 | 'DataLoader must be constructed with a function which accepts ' + 132 | 'Array and returns Promise>, but the function did ' + 133 | 'not return a Promise: 1.', 134 | ); 135 | }); 136 | 137 | it('Batch function must return a Promise of an Array, not null', async () => { 138 | // Note: this resolves to undefined 139 | // $FlowExpectError 140 | const badLoader = new DataLoader(async () => null); 141 | 142 | let caughtError; 143 | try { 144 | await badLoader.load(1); 145 | } catch (error) { 146 | caughtError = error; 147 | } 148 | expect(caughtError).toBeInstanceOf(Error); 149 | expect((caughtError: any).message).toBe( 150 | 'DataLoader must be constructed with a function which accepts ' + 151 | 'Array and returns Promise>, but the function did ' + 152 | 'not return a Promise of an Array: null.', 153 | ); 154 | }); 155 | 156 | it('Batch function must promise an Array of correct length', async () => { 157 | // Note: this resolves to empty array 158 | const badLoader = new DataLoader(async () => []); 159 | 160 | let caughtError; 161 | try { 162 | await badLoader.load(1); 163 | } catch (error) { 164 | caughtError = error; 165 | } 166 | expect(caughtError).toBeInstanceOf(Error); 167 | expect((caughtError: any).message).toBe( 168 | 'DataLoader must be constructed with a function which accepts ' + 169 | 'Array and returns Promise>, but the function did ' + 170 | 'not return a Promise of an Array of the same length as the Array ' + 171 | 'of keys.' + 172 | '\n\nKeys:\n1' + 173 | '\n\nValues:\n', 174 | ); 175 | }); 176 | 177 | it('Cache should have get, set, delete, and clear methods', async () => { 178 | class IncompleteMap { 179 | get() {} 180 | } 181 | 182 | expect(() => { 183 | // $FlowExpectError 184 | const incompleteMap = new IncompleteMap(); 185 | const options = { cacheMap: incompleteMap }; 186 | new DataLoader(async keys => keys, options); // eslint-disable-line no-new 187 | }).toThrow('Custom cacheMap missing methods: set, delete, clear'); 188 | }); 189 | 190 | it('Requires a number for maxBatchSize', () => { 191 | expect( 192 | () => 193 | // $FlowExpectError 194 | new DataLoader(async keys => keys, { maxBatchSize: null }), 195 | ).toThrow('maxBatchSize must be a positive number: null'); 196 | }); 197 | 198 | it('Requires a positive number for maxBatchSize', () => { 199 | expect( 200 | () => new DataLoader(async keys => keys, { maxBatchSize: 0 }), 201 | ).toThrow('maxBatchSize must be a positive number: 0'); 202 | }); 203 | 204 | it('Requires a function for cacheKeyFn', () => { 205 | expect( 206 | () => 207 | // $FlowExpectError 208 | new DataLoader(async keys => keys, { cacheKeyFn: null }), 209 | ).toThrow('cacheKeyFn must be a function: null'); 210 | }); 211 | 212 | it('Requires a function for batchScheduleFn', () => { 213 | expect( 214 | () => 215 | // $FlowExpectError 216 | new DataLoader(async keys => keys, { batchScheduleFn: null }), 217 | ).toThrow('batchScheduleFn must be a function: null'); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present, GraphQL Foundation 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow strict 8 | */ 9 | 10 | // A Function, which when given an Array of keys, returns a Promise of an Array 11 | // of values or Errors. 12 | export type BatchLoadFn = ( 13 | keys: $ReadOnlyArray, 14 | ) => Promise<$ReadOnlyArray>; 15 | 16 | // Optionally turn off batching or caching or provide a cache key function or a 17 | // custom cache instance. 18 | export type Options = { 19 | batch?: boolean, 20 | maxBatchSize?: number, 21 | batchScheduleFn?: (callback: () => void) => void, 22 | cache?: boolean, 23 | cacheKeyFn?: (key: K) => C, 24 | cacheMap?: CacheMap> | null, 25 | name?: string, 26 | }; 27 | 28 | // If a custom cache is provided, it must be of this type (a subset of ES6 Map). 29 | export type CacheMap = { 30 | get(key: K): V | void, 31 | set(key: K, value: V): any, 32 | delete(key: K): any, 33 | clear(): any, 34 | }; 35 | 36 | /** 37 | * A `DataLoader` creates a public API for loading data from a particular 38 | * data back-end with unique keys such as the `id` column of a SQL table or 39 | * document name in a MongoDB database, given a batch loading function. 40 | * 41 | * Each `DataLoader` instance contains a unique memoized cache. Use caution when 42 | * used in long-lived applications or those which serve many users with 43 | * different access permissions and consider creating a new instance per 44 | * web request. 45 | */ 46 | class DataLoader { 47 | constructor(batchLoadFn: BatchLoadFn, options?: Options) { 48 | if (typeof batchLoadFn !== 'function') { 49 | throw new TypeError( 50 | 'DataLoader must be constructed with a function which accepts ' + 51 | `Array and returns Promise>, but got: ${batchLoadFn}.`, 52 | ); 53 | } 54 | this._batchLoadFn = batchLoadFn; 55 | this._maxBatchSize = getValidMaxBatchSize(options); 56 | this._batchScheduleFn = getValidBatchScheduleFn(options); 57 | this._cacheKeyFn = getValidCacheKeyFn(options); 58 | this._cacheMap = getValidCacheMap(options); 59 | this._batch = null; 60 | this.name = getValidName(options); 61 | } 62 | 63 | // Private 64 | _batchLoadFn: BatchLoadFn; 65 | _maxBatchSize: number; 66 | _batchScheduleFn: (() => void) => void; 67 | _cacheKeyFn: K => C; 68 | _cacheMap: CacheMap> | null; 69 | _batch: Batch | null; 70 | 71 | /** 72 | * Loads a key, returning a `Promise` for the value represented by that key. 73 | */ 74 | load(key: K): Promise { 75 | if (key === null || key === undefined) { 76 | throw new TypeError( 77 | 'The loader.load() function must be called with a value, ' + 78 | `but got: ${String(key)}.`, 79 | ); 80 | } 81 | 82 | const batch = getCurrentBatch(this); 83 | const cacheMap = this._cacheMap; 84 | let cacheKey: ?C; 85 | 86 | // If caching and there is a cache-hit, return cached Promise. 87 | if (cacheMap) { 88 | cacheKey = this._cacheKeyFn(key); 89 | const cachedPromise = cacheMap.get(cacheKey); 90 | if (cachedPromise) { 91 | const cacheHits = batch.cacheHits || (batch.cacheHits = []); 92 | return new Promise(resolve => { 93 | cacheHits.push(() => { 94 | resolve(cachedPromise); 95 | }); 96 | }); 97 | } 98 | } 99 | 100 | // Otherwise, produce a new Promise for this key, and enqueue it to be 101 | // dispatched along with the current batch. 102 | batch.keys.push(key); 103 | const promise = new Promise((resolve, reject) => { 104 | batch.callbacks.push({ resolve, reject }); 105 | }); 106 | 107 | // If caching, cache this promise. 108 | if (cacheMap) { 109 | cacheMap.set((cacheKey: any), promise); 110 | } 111 | 112 | return promise; 113 | } 114 | 115 | /** 116 | * Loads multiple keys, promising an array of values: 117 | * 118 | * var [ a, b ] = await myLoader.loadMany([ 'a', 'b' ]); 119 | * 120 | * This is similar to the more verbose: 121 | * 122 | * var [ a, b ] = await Promise.all([ 123 | * myLoader.load('a'), 124 | * myLoader.load('b') 125 | * ]); 126 | * 127 | * However it is different in the case where any load fails. Where 128 | * Promise.all() would reject, loadMany() always resolves, however each result 129 | * is either a value or an Error instance. 130 | * 131 | * var [ a, b, c ] = await myLoader.loadMany([ 'a', 'b', 'badkey' ]); 132 | * // c instanceof Error 133 | * 134 | */ 135 | loadMany(keys: $ReadOnlyArray): Promise> { 136 | if (!isArrayLike(keys)) { 137 | throw new TypeError( 138 | 'The loader.loadMany() function must be called with Array ' + 139 | `but got: ${(keys: any)}.`, 140 | ); 141 | } 142 | // Support ArrayLike by using only minimal property access 143 | const loadPromises = []; 144 | for (let i = 0; i < keys.length; i++) { 145 | loadPromises.push(this.load(keys[i]).catch(error => error)); 146 | } 147 | return Promise.all(loadPromises); 148 | } 149 | 150 | /** 151 | * Clears the value at `key` from the cache, if it exists. Returns itself for 152 | * method chaining. 153 | */ 154 | clear(key: K): this { 155 | const cacheMap = this._cacheMap; 156 | if (cacheMap) { 157 | const cacheKey = this._cacheKeyFn(key); 158 | cacheMap.delete(cacheKey); 159 | } 160 | return this; 161 | } 162 | 163 | /** 164 | * Clears the entire cache. To be used when some event results in unknown 165 | * invalidations across this particular `DataLoader`. Returns itself for 166 | * method chaining. 167 | */ 168 | clearAll(): this { 169 | const cacheMap = this._cacheMap; 170 | if (cacheMap) { 171 | cacheMap.clear(); 172 | } 173 | return this; 174 | } 175 | 176 | /** 177 | * Adds the provided key and value to the cache. If the key already 178 | * exists, no change is made. Returns itself for method chaining. 179 | * 180 | * To prime the cache with an error at a key, provide an Error instance. 181 | */ 182 | prime(key: K, value: V | Promise | Error): this { 183 | const cacheMap = this._cacheMap; 184 | if (cacheMap) { 185 | const cacheKey = this._cacheKeyFn(key); 186 | 187 | // Only add the key if it does not already exist. 188 | if (cacheMap.get(cacheKey) === undefined) { 189 | // Cache a rejected promise if the value is an Error, in order to match 190 | // the behavior of load(key). 191 | let promise; 192 | if (value instanceof Error) { 193 | promise = Promise.reject(value); 194 | // Since this is a case where an Error is intentionally being primed 195 | // for a given key, we want to disable unhandled promise rejection. 196 | promise.catch(() => {}); 197 | } else { 198 | promise = Promise.resolve(value); 199 | } 200 | cacheMap.set(cacheKey, promise); 201 | } 202 | } 203 | return this; 204 | } 205 | 206 | /** 207 | * The name given to this `DataLoader` instance. Useful for APM tools. 208 | * 209 | * Is `null` if not set in the constructor. 210 | */ 211 | name: string | null; 212 | } 213 | 214 | // Private: Enqueue a Job to be executed after all "PromiseJobs" Jobs. 215 | // 216 | // ES6 JavaScript uses the concepts Job and JobQueue to schedule work to occur 217 | // after the current execution context has completed: 218 | // http://www.ecma-international.org/ecma-262/6.0/#sec-jobs-and-job-queues 219 | // 220 | // Node.js uses the `process.nextTick` mechanism to implement the concept of a 221 | // Job, maintaining a global FIFO JobQueue for all Jobs, which is flushed after 222 | // the current call stack ends. 223 | // 224 | // When calling `then` on a Promise, it enqueues a Job on a specific 225 | // "PromiseJobs" JobQueue which is flushed in Node as a single Job on the 226 | // global JobQueue. 227 | // 228 | // DataLoader batches all loads which occur in a single frame of execution, but 229 | // should include in the batch all loads which occur during the flushing of the 230 | // "PromiseJobs" JobQueue after that same execution frame. 231 | // 232 | // In order to avoid the DataLoader dispatch Job occuring before "PromiseJobs", 233 | // A Promise Job is created with the sole purpose of enqueuing a global Job, 234 | // ensuring that it always occurs after "PromiseJobs" ends. 235 | // 236 | // Node.js's job queue is unique. Browsers do not have an equivalent mechanism 237 | // for enqueuing a job to be performed after promise microtasks and before the 238 | // next macrotask. For browser environments, a macrotask is used (via 239 | // setImmediate or setTimeout) at a potential performance penalty. 240 | const enqueuePostPromiseJob = 241 | typeof process === 'object' && typeof process.nextTick === 'function' 242 | ? function (fn) { 243 | if (!resolvedPromise) { 244 | resolvedPromise = Promise.resolve(); 245 | } 246 | resolvedPromise.then(() => { 247 | process.nextTick(fn); 248 | }); 249 | } 250 | : typeof setImmediate === 'function' 251 | ? function (fn) { 252 | setImmediate(fn); 253 | } 254 | : function (fn) { 255 | setTimeout(fn); 256 | }; 257 | 258 | // Private: cached resolved Promise instance 259 | let resolvedPromise; 260 | 261 | // Private: Describes a batch of requests 262 | type Batch = { 263 | hasDispatched: boolean, 264 | keys: Array, 265 | callbacks: Array<{ 266 | resolve: (value: V) => void, 267 | reject: (error: Error) => void, 268 | }>, 269 | cacheHits?: Array<() => void>, 270 | }; 271 | 272 | // Private: Either returns the current batch, or creates and schedules a 273 | // dispatch of a new batch for the given loader. 274 | function getCurrentBatch(loader: DataLoader): Batch { 275 | // If there is an existing batch which has not yet dispatched and is within 276 | // the limit of the batch size, then return it. 277 | const existingBatch = loader._batch; 278 | if ( 279 | existingBatch !== null && 280 | !existingBatch.hasDispatched && 281 | existingBatch.keys.length < loader._maxBatchSize 282 | ) { 283 | return existingBatch; 284 | } 285 | 286 | // Otherwise, create a new batch for this loader. 287 | const newBatch = { hasDispatched: false, keys: [], callbacks: [] }; 288 | 289 | // Store it on the loader so it may be reused. 290 | loader._batch = newBatch; 291 | 292 | // Then schedule a task to dispatch this batch of requests. 293 | loader._batchScheduleFn(() => { 294 | dispatchBatch(loader, newBatch); 295 | }); 296 | 297 | return newBatch; 298 | } 299 | 300 | function dispatchBatch( 301 | loader: DataLoader, 302 | batch: Batch, 303 | ) { 304 | // Mark this batch as having been dispatched. 305 | batch.hasDispatched = true; 306 | 307 | // If there's nothing to load, resolve any cache hits and return early. 308 | if (batch.keys.length === 0) { 309 | resolveCacheHits(batch); 310 | return; 311 | } 312 | 313 | // Call the provided batchLoadFn for this loader with the batch's keys and 314 | // with the loader as the `this` context. 315 | let batchPromise; 316 | try { 317 | batchPromise = loader._batchLoadFn(batch.keys); 318 | } catch (e) { 319 | return failedDispatch( 320 | loader, 321 | batch, 322 | new TypeError( 323 | 'DataLoader must be constructed with a function which accepts ' + 324 | 'Array and returns Promise>, but the function ' + 325 | `errored synchronously: ${String(e)}.`, 326 | ), 327 | ); 328 | } 329 | 330 | // Assert the expected response from batchLoadFn 331 | if (!batchPromise || typeof batchPromise.then !== 'function') { 332 | return failedDispatch( 333 | loader, 334 | batch, 335 | new TypeError( 336 | 'DataLoader must be constructed with a function which accepts ' + 337 | 'Array and returns Promise>, but the function did ' + 338 | `not return a Promise: ${String(batchPromise)}.`, 339 | ), 340 | ); 341 | } 342 | 343 | // Await the resolution of the call to batchLoadFn. 344 | batchPromise 345 | .then(values => { 346 | // Assert the expected resolution from batchLoadFn. 347 | if (!isArrayLike(values)) { 348 | throw new TypeError( 349 | 'DataLoader must be constructed with a function which accepts ' + 350 | 'Array and returns Promise>, but the function did ' + 351 | `not return a Promise of an Array: ${String(values)}.`, 352 | ); 353 | } 354 | if (values.length !== batch.keys.length) { 355 | throw new TypeError( 356 | 'DataLoader must be constructed with a function which accepts ' + 357 | 'Array and returns Promise>, but the function did ' + 358 | 'not return a Promise of an Array of the same length as the Array ' + 359 | 'of keys.' + 360 | `\n\nKeys:\n${String(batch.keys)}` + 361 | `\n\nValues:\n${String(values)}`, 362 | ); 363 | } 364 | 365 | // Resolve all cache hits in the same micro-task as freshly loaded values. 366 | resolveCacheHits(batch); 367 | 368 | // Step through values, resolving or rejecting each Promise in the batch. 369 | for (let i = 0; i < batch.callbacks.length; i++) { 370 | const value = values[i]; 371 | if (value instanceof Error) { 372 | batch.callbacks[i].reject(value); 373 | } else { 374 | batch.callbacks[i].resolve(value); 375 | } 376 | } 377 | }) 378 | .catch(error => { 379 | failedDispatch(loader, batch, error); 380 | }); 381 | } 382 | 383 | // Private: do not cache individual loads if the entire batch dispatch fails, 384 | // but still reject each request so they do not hang. 385 | function failedDispatch( 386 | loader: DataLoader, 387 | batch: Batch, 388 | error: Error, 389 | ) { 390 | // Cache hits are resolved, even though the batch failed. 391 | resolveCacheHits(batch); 392 | for (let i = 0; i < batch.keys.length; i++) { 393 | loader.clear(batch.keys[i]); 394 | batch.callbacks[i].reject(error); 395 | } 396 | } 397 | 398 | // Private: Resolves the Promises for any cache hits in this batch. 399 | function resolveCacheHits(batch: Batch) { 400 | if (batch.cacheHits) { 401 | for (let i = 0; i < batch.cacheHits.length; i++) { 402 | batch.cacheHits[i](); 403 | } 404 | } 405 | } 406 | 407 | // Private: given the DataLoader's options, produce a valid max batch size. 408 | function getValidMaxBatchSize(options: ?Options): number { 409 | const shouldBatch = !options || options.batch !== false; 410 | if (!shouldBatch) { 411 | return 1; 412 | } 413 | const maxBatchSize = options && options.maxBatchSize; 414 | if (maxBatchSize === undefined) { 415 | return Infinity; 416 | } 417 | if (typeof maxBatchSize !== 'number' || maxBatchSize < 1) { 418 | throw new TypeError( 419 | `maxBatchSize must be a positive number: ${(maxBatchSize: any)}`, 420 | ); 421 | } 422 | return maxBatchSize; 423 | } 424 | 425 | // Private 426 | function getValidBatchScheduleFn( 427 | options: ?Options, 428 | ): (() => void) => void { 429 | const batchScheduleFn = options && options.batchScheduleFn; 430 | if (batchScheduleFn === undefined) { 431 | return enqueuePostPromiseJob; 432 | } 433 | if (typeof batchScheduleFn !== 'function') { 434 | throw new TypeError( 435 | `batchScheduleFn must be a function: ${(batchScheduleFn: any)}`, 436 | ); 437 | } 438 | return batchScheduleFn; 439 | } 440 | 441 | // Private: given the DataLoader's options, produce a cache key function. 442 | function getValidCacheKeyFn(options: ?Options): K => C { 443 | const cacheKeyFn = options && options.cacheKeyFn; 444 | if (cacheKeyFn === undefined) { 445 | return (key => key: any); 446 | } 447 | if (typeof cacheKeyFn !== 'function') { 448 | throw new TypeError(`cacheKeyFn must be a function: ${(cacheKeyFn: any)}`); 449 | } 450 | return cacheKeyFn; 451 | } 452 | 453 | // Private: given the DataLoader's options, produce a CacheMap to be used. 454 | function getValidCacheMap( 455 | options: ?Options, 456 | ): CacheMap> | null { 457 | const shouldCache = !options || options.cache !== false; 458 | if (!shouldCache) { 459 | return null; 460 | } 461 | const cacheMap = options && options.cacheMap; 462 | if (cacheMap === undefined) { 463 | return new Map(); 464 | } 465 | if (cacheMap !== null) { 466 | const cacheFunctions = ['get', 'set', 'delete', 'clear']; 467 | const missingFunctions = cacheFunctions.filter( 468 | fnName => cacheMap && typeof cacheMap[fnName] !== 'function', 469 | ); 470 | if (missingFunctions.length !== 0) { 471 | throw new TypeError( 472 | 'Custom cacheMap missing methods: ' + missingFunctions.join(', '), 473 | ); 474 | } 475 | } 476 | return cacheMap; 477 | } 478 | 479 | function getValidName(options: ?Options): string | null { 480 | if (options && options.name) { 481 | return options.name; 482 | } 483 | 484 | return null; 485 | } 486 | 487 | // Private 488 | function isArrayLike(x: mixed): boolean { 489 | return ( 490 | typeof x === 'object' && 491 | x !== null && 492 | typeof x.length === 'number' && 493 | (x.length === 0 || 494 | (x.length > 0 && Object.prototype.hasOwnProperty.call(x, x.length - 1))) 495 | ); 496 | } 497 | 498 | module.exports = DataLoader; 499 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DataLoader 2 | 3 | DataLoader is a generic utility to be used as part of your application's data 4 | fetching layer to provide a simplified and consistent API over various remote 5 | data sources such as databases or web services via batching and caching. 6 | 7 | [![Build Status](https://github.com/graphql/dataloader/actions/workflows/validation.yml/badge.svg)](https://github.com/graphql/dataloader/actions/workflows/validation.yml) 8 | [![Coverage Status](https://coveralls.io/repos/graphql/dataloader/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql/dataloader?branch=main) 9 | 10 | A port of the "Loader" API originally developed by [@schrockn][] at Facebook in 11 | 2010 as a simplifying force to coalesce the sundry key-value store back-end 12 | APIs which existed at the time. At Facebook, "Loader" became one of the 13 | implementation details of the "Ent" framework, a privacy-aware data entity 14 | loading and caching layer within web server product code. This ultimately became 15 | the underpinning for Facebook's GraphQL server implementation and type 16 | definitions. 17 | 18 | DataLoader is a simplified version of this original idea implemented in 19 | JavaScript for Node.js services. DataLoader is often used when implementing a 20 | [graphql-js][] service, though it is also broadly useful in other situations. 21 | 22 | This mechanism of batching and caching data requests is certainly not unique to 23 | Node.js or JavaScript, it is also the primary motivation for 24 | [Haxl](https://github.com/facebook/Haxl), Facebook's data loading library 25 | for Haskell. More about how Haxl works can be read in this [blog post](https://code.facebook.com/posts/302060973291128/open-sourcing-haxl-a-library-for-haskell/). 26 | 27 | DataLoader is provided so that it may be useful not just to build GraphQL 28 | services for Node.js but also as a publicly available reference implementation 29 | of this concept in the hopes that it can be ported to other languages. If you 30 | port DataLoader to another language, please open an issue to include a link from 31 | this repository. 32 | 33 | ## Getting Started 34 | 35 | First, install DataLoader using npm. 36 | 37 | ```sh 38 | npm install --save dataloader 39 | ``` 40 | 41 | To get started, create a `DataLoader`. Each `DataLoader` instance represents a 42 | unique cache. Typically instances are created per request when used within a 43 | web-server like [express][] if different users can see different things. 44 | 45 | > Note: DataLoader assumes a JavaScript environment with global ES6 `Promise` 46 | > and `Map` classes, available in all supported versions of Node.js. 47 | 48 | ## Batching 49 | 50 | Batching is not an advanced feature, it's DataLoader's primary feature. 51 | Create loaders by providing a batch loading function. 52 | 53 | ```js 54 | const DataLoader = require('dataloader'); 55 | 56 | const userLoader = new DataLoader(keys => myBatchGetUsers(keys)); 57 | ``` 58 | 59 | A batch loading function accepts an Array of keys, and returns a Promise which 60 | resolves to an Array of values[\*](#batch-function). 61 | 62 | Then load individual values from the loader. DataLoader will coalesce all 63 | individual loads which occur within a single frame of execution (a single tick 64 | of the event loop) and then call your batch function with all requested keys. 65 | 66 | ```js 67 | const user = await userLoader.load(1); 68 | const invitedBy = await userLoader.load(user.invitedByID); 69 | console.log(`User 1 was invited by ${invitedBy}`); 70 | 71 | // Elsewhere in your application 72 | const user = await userLoader.load(2); 73 | const lastInvited = await userLoader.load(user.lastInvitedID); 74 | console.log(`User 2 last invited ${lastInvited}`); 75 | ``` 76 | 77 | A naive application may have issued four round-trips to a backend for the 78 | required information, but with DataLoader this application will make at most 79 | two. 80 | 81 | DataLoader allows you to decouple unrelated parts of your application without 82 | sacrificing the performance of batch data-loading. While the loader presents an 83 | API that loads individual values, all concurrent requests will be coalesced and 84 | presented to your batch loading function. This allows your application to safely 85 | distribute data fetching requirements throughout your application and maintain 86 | minimal outgoing data requests. 87 | 88 | #### Batch Function 89 | 90 | A batch loading function accepts an Array of keys, and returns a Promise which 91 | resolves to an Array of values or Error instances. The loader itself is provided 92 | as the `this` context. 93 | 94 | ```js 95 | async function batchFunction(keys) { 96 | const results = await db.fetchAllKeys(keys); 97 | return keys.map(key => results[key] || new Error(`No result for ${key}`)); 98 | } 99 | 100 | const loader = new DataLoader(batchFunction); 101 | ``` 102 | 103 | There are a few constraints this function must uphold: 104 | 105 | - The Array of values must be the same length as the Array of keys. 106 | - Each index in the Array of values must correspond to the same index in the Array of keys. 107 | 108 | For example, if your batch function was provided the Array of keys: `[ 2, 9, 6, 1 ]`, 109 | and loading from a back-end service returned the values: 110 | 111 | ```js 112 | { id: 9, name: 'Chicago' } 113 | { id: 1, name: 'New York' } 114 | { id: 2, name: 'San Francisco' } 115 | ``` 116 | 117 | Our back-end service returned results in a different order than we requested, likely 118 | because it was more efficient for it to do so. Also, it omitted a result for key `6`, 119 | which we can interpret as no value existing for that key. 120 | 121 | To uphold the constraints of the batch function, it must return an Array of values 122 | the same length as the Array of keys, and re-order them to ensure each index aligns 123 | with the original keys `[ 2, 9, 6, 1 ]`: 124 | 125 | ```js 126 | [ 127 | { id: 2, name: 'San Francisco' }, 128 | { id: 9, name: 'Chicago' }, 129 | null, // or perhaps `new Error()` 130 | { id: 1, name: 'New York' }, 131 | ]; 132 | ``` 133 | 134 | #### Batch Scheduling 135 | 136 | By default DataLoader will coalesce all individual loads which occur within a 137 | single frame of execution before calling your batch function with all requested 138 | keys. This ensures no additional latency while capturing many related requests 139 | into a single batch. In fact, this is the same behavior used in Facebook's 140 | original PHP implementation in 2010. See `enqueuePostPromiseJob` in the 141 | [source code][] for more details about how this works. 142 | 143 | However sometimes this behavior is not desirable or optimal. Perhaps you expect 144 | requests to be spread out over a few subsequent ticks because of an existing use 145 | of `setTimeout`, or you just want manual control over dispatching regardless of 146 | the run loop. DataLoader allows providing a custom batch scheduler to provide 147 | these or any other behaviors. 148 | 149 | A custom scheduler is provided as `batchScheduleFn` in options. It must be a 150 | function which is passed a callback and is expected to call that callback in the 151 | immediate future to execute the batch request. 152 | 153 | As an example, here is a batch scheduler which collects all requests over a 154 | 100ms window of time (and as a consequence, adds 100ms of latency): 155 | 156 | ```js 157 | const myLoader = new DataLoader(myBatchFn, { 158 | batchScheduleFn: callback => setTimeout(callback, 100), 159 | }); 160 | ``` 161 | 162 | As another example, here is a manually dispatched batch scheduler: 163 | 164 | ```js 165 | function createScheduler() { 166 | let callbacks = []; 167 | return { 168 | schedule(callback) { 169 | callbacks.push(callback); 170 | }, 171 | dispatch() { 172 | callbacks.forEach(callback => callback()); 173 | callbacks = []; 174 | }, 175 | }; 176 | } 177 | 178 | const { schedule, dispatch } = createScheduler(); 179 | const myLoader = new DataLoader(myBatchFn, { batchScheduleFn: schedule }); 180 | 181 | myLoader.load(1); 182 | myLoader.load(2); 183 | dispatch(); 184 | ``` 185 | 186 | ## Caching 187 | 188 | DataLoader provides a memoization cache for all loads which occur in a single 189 | request to your application. After `.load()` is called once with a given key, 190 | the resulting value is cached to eliminate redundant loads. 191 | 192 | #### Caching Per-Request 193 | 194 | DataLoader caching _does not_ replace Redis, Memcache, or any other shared 195 | application-level cache. DataLoader is first and foremost a data loading mechanism, 196 | and its cache only serves the purpose of not repeatedly loading the same data in 197 | the context of a single request to your Application. To do this, it maintains a 198 | simple in-memory memoization cache (more accurately: `.load()` is a memoized function). 199 | 200 | Avoid multiple requests from different users using the DataLoader instance, which 201 | could result in cached data incorrectly appearing in each request. Typically, 202 | DataLoader instances are created when a Request begins, and are not used once the 203 | Request ends. 204 | 205 | For example, when using with [express][]: 206 | 207 | ```js 208 | function createLoaders(authToken) { 209 | return { 210 | users: new DataLoader(ids => genUsers(authToken, ids)), 211 | }; 212 | } 213 | 214 | const app = express(); 215 | 216 | app.get('/', function (req, res) { 217 | const authToken = authenticateUser(req); 218 | const loaders = createLoaders(authToken); 219 | res.send(renderPage(req, loaders)); 220 | }); 221 | 222 | app.listen(); 223 | ``` 224 | 225 | #### Caching and Batching 226 | 227 | Subsequent calls to `.load()` with the same key will result in that key not 228 | appearing in the keys provided to your batch function. _However_, the resulting 229 | Promise will still wait on the current batch to complete. This way both cached 230 | and uncached requests will resolve at the same time, allowing DataLoader 231 | optimizations for subsequent dependent loads. 232 | 233 | In the example below, User `1` happens to be cached. However, because User `1` 234 | and `2` are loaded in the same tick, they will resolve at the same time. This 235 | means both `user.bestFriendID` loads will also happen in the same tick which 236 | results in two total requests (the same as if User `1` had not been cached). 237 | 238 | ```js 239 | userLoader.prime(1, { bestFriend: 3 }); 240 | 241 | async function getBestFriend(userID) { 242 | const user = await userLoader.load(userID); 243 | return await userLoader.load(user.bestFriendID); 244 | } 245 | 246 | // In one part of your application 247 | getBestFriend(1); 248 | 249 | // Elsewhere 250 | getBestFriend(2); 251 | ``` 252 | 253 | Without this optimization, if the cached User `1` resolved immediately, this 254 | could result in three total requests since each `user.bestFriendID` load would 255 | happen at different times. 256 | 257 | #### Clearing Cache 258 | 259 | In certain uncommon cases, clearing the request cache may be necessary. 260 | 261 | The most common example when clearing the loader's cache is necessary is after 262 | a mutation or update within the same request, when a cached value could be out of 263 | date and future loads should not use any possibly cached value. 264 | 265 | Here's a simple example using SQL UPDATE to illustrate. 266 | 267 | ```js 268 | // Request begins... 269 | const userLoader = new DataLoader(...); 270 | 271 | // And a value happens to be loaded (and cached). 272 | const user = await userLoader.load(4); 273 | 274 | // A mutation occurs, invalidating what might be in cache. 275 | await sqlRun('UPDATE users WHERE id=4 SET username="zuck"'); 276 | userLoader.clear(4); 277 | 278 | // Later the value load is loaded again so the mutated data appears. 279 | const user = await userLoader.load(4); 280 | 281 | // Request completes. 282 | ``` 283 | 284 | #### Caching Errors 285 | 286 | If a batch load fails (that is, a batch function throws or returns a rejected 287 | Promise), then the requested values will not be cached. However if a batch 288 | function returns an `Error` instance for an individual value, that `Error` will 289 | be cached to avoid frequently loading the same `Error`. 290 | 291 | In some circumstances you may wish to clear the cache for these individual Errors: 292 | 293 | ```js 294 | try { 295 | const user = await userLoader.load(1); 296 | } catch (error) { 297 | if (/* determine if the error should not be cached */) { 298 | userLoader.clear(1); 299 | } 300 | throw error 301 | } 302 | ``` 303 | 304 | #### Disabling Cache 305 | 306 | In certain uncommon cases, a DataLoader which _does not_ cache may be desirable. 307 | Calling `new DataLoader(myBatchFn, { cache: false })` will ensure that every 308 | call to `.load()` will produce a _new_ Promise, and requested keys will not be 309 | saved in memory. 310 | 311 | However, when the memoization cache is disabled, your batch function will 312 | receive an array of keys which may contain duplicates! Each key will be 313 | associated with each call to `.load()`. Your batch loader should provide a value 314 | for each instance of the requested key. 315 | 316 | For example: 317 | 318 | ```js 319 | const myLoader = new DataLoader( 320 | keys => { 321 | console.log(keys); 322 | return someBatchLoadFn(keys); 323 | }, 324 | { cache: false }, 325 | ); 326 | 327 | myLoader.load('A'); 328 | myLoader.load('B'); 329 | myLoader.load('A'); 330 | 331 | // > [ 'A', 'B', 'A' ] 332 | ``` 333 | 334 | More complex cache behavior can be achieved by calling `.clear()` or `.clearAll()` 335 | rather than disabling the cache completely. For example, this DataLoader will 336 | provide unique keys to a batch function due to the memoization cache being 337 | enabled, but will immediately clear its cache when the batch function is called 338 | so later requests will load new values. 339 | 340 | ```js 341 | const myLoader = new DataLoader(keys => { 342 | myLoader.clearAll(); 343 | return someBatchLoadFn(keys); 344 | }); 345 | ``` 346 | 347 | #### Custom Cache 348 | 349 | As mentioned above, DataLoader is intended to be used as a per-request cache. 350 | Since requests are short-lived, DataLoader uses an infinitely growing [Map][] as 351 | a memoization cache. This should not pose a problem as most requests are 352 | short-lived and the entire cache can be discarded after the request completes. 353 | 354 | However this memoization caching strategy isn't safe when using a long-lived 355 | DataLoader, since it could consume too much memory. If using DataLoader in this 356 | way, you can provide a custom Cache instance with whatever behavior you prefer, 357 | as long as it follows the same API as [Map][]. 358 | 359 | The example below uses an LRU (least recently used) cache to limit total memory 360 | to hold at most 100 cached values via the [lru_map][] npm package. 361 | 362 | ```js 363 | import { LRUMap } from 'lru_map'; 364 | 365 | const myLoader = new DataLoader(someBatchLoadFn, { 366 | cacheMap: new LRUMap(100), 367 | }); 368 | ``` 369 | 370 | More specifically, any object that implements the methods `get()`, `set()`, 371 | `delete()` and `clear()` methods can be provided. This allows for custom Maps 372 | which implement various [cache algorithms][] to be provided. 373 | 374 | ## API 375 | 376 | #### class DataLoader 377 | 378 | DataLoader creates a public API for loading data from a particular 379 | data back-end with unique keys such as the `id` column of a SQL table or 380 | document name in a MongoDB database, given a batch loading function. 381 | 382 | Each `DataLoader` instance contains a unique memoized cache. Use caution when 383 | used in long-lived applications or those which serve many users with different 384 | access permissions and consider creating a new instance per web request. 385 | 386 | ##### `new DataLoader(batchLoadFn [, options])` 387 | 388 | Create a new `DataLoader` given a batch loading function and options. 389 | 390 | - _batchLoadFn_: A function which accepts an Array of keys, and returns a 391 | Promise which resolves to an Array of values. 392 | 393 | - _options_: An optional object of options: 394 | 395 | | Option Key | Type | Default | Description | 396 | | ----------------- | -------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 397 | | `batch` | Boolean | `true` | Set to `false` to disable batching, invoking `batchLoadFn` with a single load key. This is equivalent to setting `maxBatchSize` to `1`. | 398 | | `maxBatchSize` | Number | `Infinity` | Limits the number of items that get passed in to the `batchLoadFn`. May be set to `1` to disable batching. | 399 | | `batchScheduleFn` | Function | See [Batch scheduling](#batch-scheduling) | A function to schedule the later execution of a batch. The function is expected to call the provided callback in the immediate future. | 400 | | `cache` | Boolean | `true` | Set to `false` to disable memoization caching, creating a new Promise and new key in the `batchLoadFn` for every load of the same key. This is equivalent to setting `cacheMap` to `null`. | 401 | | `cacheKeyFn` | Function | `key => key` | Produces cache key for a given load key. Useful when objects are keys and two objects should be considered equivalent. | 402 | | `cacheMap` | Object | `new Map()` | Instance of [Map][] (or an object with a similar API) to be used as cache. May be set to `null` to disable caching. | 403 | | `name` | String | `null` | The name given to this `DataLoader` instance. Useful for APM tools. | 404 | 405 | ##### `load(key)` 406 | 407 | Loads a key, returning a `Promise` for the value represented by that key. 408 | 409 | - _key_: A key value to load. 410 | 411 | ##### `loadMany(keys)` 412 | 413 | Loads multiple keys, promising an array of values: 414 | 415 | ```js 416 | const [a, b] = await myLoader.loadMany(['a', 'b']); 417 | ``` 418 | 419 | This is similar to the more verbose: 420 | 421 | ```js 422 | const [a, b] = await Promise.all([myLoader.load('a'), myLoader.load('b')]); 423 | ``` 424 | 425 | However it is different in the case where any load fails. Where 426 | Promise.all() would reject, loadMany() always resolves, however each result 427 | is either a value or an Error instance. 428 | 429 | ```js 430 | var [a, b, c] = await myLoader.loadMany(['a', 'b', 'badkey']); 431 | // c instanceof Error 432 | ``` 433 | 434 | - _keys_: An array of key values to load. 435 | 436 | ##### `clear(key)` 437 | 438 | Clears the value at `key` from the cache, if it exists. Returns itself for 439 | method chaining. 440 | 441 | - _key_: A key value to clear. 442 | 443 | ##### `clearAll()` 444 | 445 | Clears the entire cache. To be used when some event results in unknown 446 | invalidations across this particular `DataLoader`. Returns itself for 447 | method chaining. 448 | 449 | ##### `prime(key, value)` 450 | 451 | Primes the cache with the provided key and value. If the key already exists, no 452 | change is made. (To forcefully prime the cache, clear the key first with 453 | `loader.clear(key).prime(key, value)`.) Returns itself for method chaining. 454 | 455 | To prime the cache with an error at a key, provide an Error instance. 456 | 457 | ## Using with GraphQL 458 | 459 | DataLoader pairs nicely well with [GraphQL][graphql-js]. GraphQL fields are 460 | designed to be stand-alone functions. Without a caching or batching mechanism, 461 | it's easy for a naive GraphQL server to issue new database requests each time a 462 | field is resolved. 463 | 464 | Consider the following GraphQL request: 465 | 466 | ``` 467 | { 468 | me { 469 | name 470 | bestFriend { 471 | name 472 | } 473 | friends(first: 5) { 474 | name 475 | bestFriend { 476 | name 477 | } 478 | } 479 | } 480 | } 481 | ``` 482 | 483 | Naively, if `me`, `bestFriend` and `friends` each need to request the backend, 484 | there could be at most 13 database requests! 485 | 486 | When using DataLoader, we could define the `User` type using the 487 | [SQLite](examples/SQL.md) example with clearer code and at most 4 database requests, 488 | and possibly fewer if there are cache hits. 489 | 490 | ```js 491 | const UserType = new GraphQLObjectType({ 492 | name: 'User', 493 | fields: () => ({ 494 | name: { type: GraphQLString }, 495 | bestFriend: { 496 | type: UserType, 497 | resolve: user => userLoader.load(user.bestFriendID), 498 | }, 499 | friends: { 500 | args: { 501 | first: { type: GraphQLInt }, 502 | }, 503 | type: new GraphQLList(UserType), 504 | resolve: async (user, { first }) => { 505 | const rows = await queryLoader.load([ 506 | 'SELECT toID FROM friends WHERE fromID=? LIMIT ?', 507 | user.id, 508 | first, 509 | ]); 510 | return rows.map(row => userLoader.load(row.toID)); 511 | }, 512 | }, 513 | }), 514 | }); 515 | ``` 516 | 517 | ## Common Patterns 518 | 519 | ### Creating a new DataLoader per request. 520 | 521 | In many applications, a web server using DataLoader serves requests to many 522 | different users with different access permissions. It may be dangerous to use 523 | one cache across many users, and is encouraged to create a new DataLoader 524 | per request: 525 | 526 | ```js 527 | function createLoaders(authToken) { 528 | return { 529 | users: new DataLoader(ids => genUsers(authToken, ids)), 530 | cdnUrls: new DataLoader(rawUrls => genCdnUrls(authToken, rawUrls)), 531 | stories: new DataLoader(keys => genStories(authToken, keys)), 532 | }; 533 | } 534 | 535 | // When handling an incoming web request: 536 | const loaders = createLoaders(request.query.authToken); 537 | 538 | // Then, within application logic: 539 | const user = await loaders.users.load(4); 540 | const pic = await loaders.cdnUrls.load(user.rawPicUrl); 541 | ``` 542 | 543 | Creating an object where each key is a `DataLoader` is one common pattern which 544 | provides a single value to pass around to code which needs to perform 545 | data loading, such as part of the `rootValue` in a [graphql-js][] request. 546 | 547 | ### Loading by alternative keys. 548 | 549 | Occasionally, some kind of value can be accessed in multiple ways. For example, 550 | perhaps a "User" type can be loaded not only by an "id" but also by a "username" 551 | value. If the same user is loaded by both keys, then it may be useful to fill 552 | both caches when a user is loaded from either source: 553 | 554 | ```js 555 | const userByIDLoader = new DataLoader(async ids => { 556 | const users = await genUsersByID(ids); 557 | for (let user of users) { 558 | usernameLoader.prime(user.username, user); 559 | } 560 | return users; 561 | }); 562 | 563 | const usernameLoader = new DataLoader(async names => { 564 | const users = await genUsernames(names); 565 | for (let user of users) { 566 | userByIDLoader.prime(user.id, user); 567 | } 568 | return users; 569 | }); 570 | ``` 571 | 572 | ### Freezing results to enforce immutability 573 | 574 | Since DataLoader caches values, it's typically assumed these values will be 575 | treated as if they were immutable. While DataLoader itself doesn't enforce 576 | this, you can create a higher-order function to enforce immutability 577 | with Object.freeze(): 578 | 579 | ```js 580 | function freezeResults(batchLoader) { 581 | return keys => batchLoader(keys).then(values => values.map(Object.freeze)); 582 | } 583 | 584 | const myLoader = new DataLoader(freezeResults(myBatchLoader)); 585 | ``` 586 | 587 | ### Batch functions which return Objects instead of Arrays 588 | 589 | DataLoader expects batch functions which return an Array of the same length as 590 | the provided keys. However this is not always a common return format from other 591 | libraries. A DataLoader higher-order function can convert from one format to another. The example below converts a `{ key: value }` result to the format 592 | DataLoader expects. 593 | 594 | ```js 595 | function objResults(batchLoader) { 596 | return keys => 597 | batchLoader(keys).then(objValues => 598 | keys.map(key => objValues[key] || new Error(`No value for ${key}`)), 599 | ); 600 | } 601 | 602 | const myLoader = new DataLoader(objResults(myBatchLoader)); 603 | ``` 604 | 605 | ## Common Back-ends 606 | 607 | Looking to get started with a specific back-end? Try the [loaders in the examples directory](/examples). 608 | 609 | ## Other Implementations 610 | 611 | Listed in alphabetical order 612 | 613 | - Elixir 614 | - [dataloader](https://github.com/absinthe-graphql/dataloader) 615 | - Golang 616 | - [Dataloader](https://github.com/nicksrandall/dataloader) 617 | - Java 618 | - [java-dataloader](https://github.com/graphql-java/java-dataloader) 619 | - .Net 620 | - [GraphQL .NET DataLoader](https://graphql-dotnet.github.io/docs/guides/dataloader/) 621 | - [Green Donut](https://github.com/ChilliCream/graphql-platform?tab=readme-ov-file#green-donut) 622 | - Perl 623 | - [perl-DataLoader](https://github.com/richardjharris/perl-DataLoader) 624 | - PHP 625 | - [DataLoaderPHP](https://github.com/overblog/dataloader-php) 626 | - Python 627 | - [aiodataloader](https://github.com/syrusakbary/aiodataloader) 628 | - ReasonML 629 | - [bs-dataloader](https://github.com/ulrikstrid/bs-dataloader) 630 | - Ruby 631 | - [BatchLoader](https://github.com/exaspark/batch-loader) 632 | - [Dataloader](https://github.com/sheerun/dataloader) 633 | - [GraphQL Batch](https://github.com/Shopify/graphql-batch) 634 | - Rust 635 | - [Dataloader](https://github.com/cksac/dataloader-rs) 636 | - Swift 637 | - [SwiftDataLoader](https://github.com/kimdv/SwiftDataLoader) 638 | - C++ 639 | - [cppdataloader](https://github.com/jafarlihi/cppdataloader) 640 | 641 | ## Video Source Code Walkthrough 642 | 643 | **DataLoader Source Code Walkthrough (YouTube):** 644 | 645 | A walkthrough of the DataLoader v1 source code. While the source has changed 646 | since this video was made, it is still a good overview of the rationale of 647 | DataLoader and how it works. 648 | 649 | 650 | 651 | [@schrockn]: https://github.com/schrockn 652 | [Map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map 653 | [graphql-js]: https://github.com/graphql/graphql-js 654 | [cache algorithms]: https://en.wikipedia.org/wiki/Cache_algorithms 655 | [express]: http://expressjs.com/ 656 | [babel/polyfill]: https://babeljs.io/docs/usage/polyfill/ 657 | [lru_map]: https://github.com/rsms/js-lru 658 | [source code]: https://github.com/graphql/dataloader/blob/main/src/index.js 659 | 660 | # Contributing to this repo 661 | 662 | This repository is managed by EasyCLA. Project participants must sign the free ([GraphQL Specification Membership agreement](https://preview-spec-membership.graphql.org) before making a contribution. You only need to do this one time, and it can be signed by [individual contributors](http://individual-spec-membership.graphql.org/) or their [employers](http://corporate-spec-membership.graphql.org/). 663 | 664 | To initiate the signature process please open a PR against this repo. The EasyCLA bot will block the merge if we still need a membership agreement from you. 665 | 666 | You can find [detailed information here](https://github.com/graphql/graphql-wg/tree/main/membership). If you have issues, please email [operations@graphql.org](mailto:operations@graphql.org). 667 | 668 | If your company benefits from GraphQL and you would like to provide essential financial support for the systems and people that power our community, please also consider membership in the [GraphQL Foundation](https://foundation.graphql.org/join). 669 | -------------------------------------------------------------------------------- /src/__tests__/dataloader.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019-present, GraphQL Foundation 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | import type { Options } from '..'; 11 | const DataLoader = require('..'); 12 | 13 | function idLoader( 14 | options?: Options, 15 | ): [DataLoader, Array<$ReadOnlyArray>] { 16 | const loadCalls = []; 17 | const identityLoader = new DataLoader(keys => { 18 | loadCalls.push(keys); 19 | return Promise.resolve(keys); 20 | }, options); 21 | return [identityLoader, loadCalls]; 22 | } 23 | 24 | describe('Primary API', () => { 25 | it('builds a really really simple data loader', async () => { 26 | const identityLoader = new DataLoader(async keys => keys); 27 | 28 | const promise1 = identityLoader.load(1); 29 | expect(promise1).toBeInstanceOf(Promise); 30 | 31 | const value1 = await promise1; 32 | expect(value1).toBe(1); 33 | }); 34 | 35 | it('references the loader as "this" in the batch function', async () => { 36 | let that; 37 | const loader = new DataLoader(async function (keys) { 38 | that = this; 39 | return keys; 40 | }); 41 | 42 | // Trigger the batch function 43 | await loader.load(1); 44 | 45 | expect(that).toBe(loader); 46 | }); 47 | 48 | it('references the loader as "this" in the cache key function', async () => { 49 | let that; 50 | const loader = new DataLoader(async keys => keys, { 51 | cacheKeyFn(key) { 52 | that = this; 53 | return key; 54 | }, 55 | }); 56 | 57 | // Trigger the cache key function 58 | await loader.load(1); 59 | 60 | expect(that).toBe(loader); 61 | }); 62 | 63 | it('supports loading multiple keys in one call', async () => { 64 | const identityLoader = new DataLoader(async keys => keys); 65 | 66 | const promiseAll = identityLoader.loadMany([1, 2]); 67 | expect(promiseAll).toBeInstanceOf(Promise); 68 | 69 | const values = await promiseAll; 70 | expect(values).toEqual([1, 2]); 71 | 72 | const promiseEmpty = identityLoader.loadMany([]); 73 | expect(promiseEmpty).toBeInstanceOf(Promise); 74 | 75 | const empty = await promiseEmpty; 76 | expect(empty).toEqual([]); 77 | }); 78 | 79 | it('supports loading multiple keys in one call with errors', async () => { 80 | const identityLoader = new DataLoader(keys => 81 | Promise.resolve( 82 | keys.map(key => (key === 'bad' ? new Error('Bad Key') : key)), 83 | ), 84 | ); 85 | 86 | const promiseAll = identityLoader.loadMany(['a', 'b', 'bad']); 87 | expect(promiseAll).toBeInstanceOf(Promise); 88 | 89 | const values = await promiseAll; 90 | expect(values).toEqual(['a', 'b', new Error('Bad Key')]); 91 | }); 92 | 93 | it('batches multiple requests', async () => { 94 | const [identityLoader, loadCalls] = idLoader(); 95 | 96 | const promise1 = identityLoader.load(1); 97 | const promise2 = identityLoader.load(2); 98 | 99 | const [value1, value2] = await Promise.all([promise1, promise2]); 100 | expect(value1).toBe(1); 101 | expect(value2).toBe(2); 102 | 103 | expect(loadCalls).toEqual([[1, 2]]); 104 | }); 105 | 106 | it('batches multiple requests with max batch sizes', async () => { 107 | const [identityLoader, loadCalls] = idLoader({ maxBatchSize: 2 }); 108 | 109 | const promise1 = identityLoader.load(1); 110 | const promise2 = identityLoader.load(2); 111 | const promise3 = identityLoader.load(3); 112 | 113 | const [value1, value2, value3] = await Promise.all([ 114 | promise1, 115 | promise2, 116 | promise3, 117 | ]); 118 | expect(value1).toBe(1); 119 | expect(value2).toBe(2); 120 | expect(value3).toBe(3); 121 | 122 | expect(loadCalls).toEqual([[1, 2], [3]]); 123 | }); 124 | 125 | it('applies maxBatchSize correctly with duplicate keys', async () => { 126 | const [identityLoader, loadCalls] = idLoader({ 127 | maxBatchSize: 3, 128 | batchScheduleFn: callback => { 129 | setTimeout(callback, 100); 130 | }, 131 | }); 132 | 133 | const values = ['a', 'b', 'a', 'a', 'a', 'b', 'c']; 134 | const results = await Promise.all( 135 | values.map(value => identityLoader.load(value)), 136 | ); 137 | 138 | expect(results).toEqual(values); 139 | expect(loadCalls).toEqual([['a', 'b', 'c']]); 140 | }); 141 | 142 | it('batches cached requests', async () => { 143 | const loadCalls = []; 144 | let resolveBatch = () => {}; 145 | const identityLoader = new DataLoader(keys => { 146 | loadCalls.push(keys); 147 | return new Promise(resolve => { 148 | resolveBatch = () => resolve(keys); 149 | }); 150 | }); 151 | 152 | identityLoader.prime(1, 1); 153 | 154 | const promise1 = identityLoader.load(1); 155 | const promise2 = identityLoader.load(2); 156 | 157 | // Track when each resolves. 158 | let promise1Resolved = false; 159 | let promise2Resolved = false; 160 | promise1.then(() => { 161 | promise1Resolved = true; 162 | }); 163 | promise2.then(() => { 164 | promise2Resolved = true; 165 | }); 166 | 167 | // Move to next macro-task (tick) 168 | await new Promise(setImmediate); 169 | 170 | expect(promise1Resolved).toBe(false); 171 | expect(promise2Resolved).toBe(false); 172 | 173 | resolveBatch(); 174 | // Move to next macro-task (tick) 175 | await new Promise(setImmediate); 176 | 177 | expect(promise1Resolved).toBe(true); 178 | expect(promise2Resolved).toBe(true); 179 | 180 | const [value1, value2] = await Promise.all([promise1, promise2]); 181 | expect(value1).toBe(1); 182 | expect(value2).toBe(2); 183 | 184 | expect(loadCalls).toEqual([[2]]); 185 | }); 186 | 187 | it('max batch size respects cached results', async () => { 188 | const loadCalls = []; 189 | let resolveBatch = () => {}; 190 | const identityLoader = new DataLoader( 191 | keys => { 192 | loadCalls.push(keys); 193 | return new Promise(resolve => { 194 | resolveBatch = () => resolve(keys); 195 | }); 196 | }, 197 | { maxBatchSize: 1 }, 198 | ); 199 | 200 | identityLoader.prime(1, 1); 201 | 202 | const promise1 = identityLoader.load(1); 203 | const promise2 = identityLoader.load(2); 204 | 205 | // Track when each resolves. 206 | let promise1Resolved = false; 207 | let promise2Resolved = false; 208 | promise1.then(() => { 209 | promise1Resolved = true; 210 | }); 211 | promise2.then(() => { 212 | promise2Resolved = true; 213 | }); 214 | 215 | // Move to next macro-task (tick) 216 | await new Promise(setImmediate); 217 | 218 | // Promise 1 resolves first since max batch size is 1, 219 | // but it still hasn't resolved yet. 220 | expect(promise1Resolved).toBe(false); 221 | expect(promise2Resolved).toBe(false); 222 | 223 | resolveBatch(); 224 | // Move to next macro-task (tick) 225 | await new Promise(setImmediate); 226 | 227 | expect(promise1Resolved).toBe(true); 228 | expect(promise2Resolved).toBe(true); 229 | 230 | const [value1, value2] = await Promise.all([promise1, promise2]); 231 | expect(value1).toBe(1); 232 | expect(value2).toBe(2); 233 | 234 | expect(loadCalls).toEqual([[2]]); 235 | }); 236 | 237 | it('coalesces identical requests', async () => { 238 | const [identityLoader, loadCalls] = idLoader(); 239 | 240 | const promise1a = identityLoader.load(1); 241 | const promise1b = identityLoader.load(1); 242 | 243 | const [value1a, value1b] = await Promise.all([promise1a, promise1b]); 244 | expect(value1a).toBe(1); 245 | expect(value1b).toBe(1); 246 | 247 | expect(loadCalls).toEqual([[1]]); 248 | }); 249 | 250 | it('coalesces identical requests across sized batches', async () => { 251 | const [identityLoader, loadCalls] = idLoader({ maxBatchSize: 2 }); 252 | 253 | const promise1a = identityLoader.load(1); 254 | const promise2 = identityLoader.load(2); 255 | const promise1b = identityLoader.load(1); 256 | const promise3 = identityLoader.load(3); 257 | 258 | const [value1a, value2, value1b, value3] = await Promise.all([ 259 | promise1a, 260 | promise2, 261 | promise1b, 262 | promise3, 263 | ]); 264 | expect(value1a).toBe(1); 265 | expect(value2).toBe(2); 266 | expect(value1b).toBe(1); 267 | expect(value3).toBe(3); 268 | 269 | expect(loadCalls).toEqual([[1, 2], [3]]); 270 | }); 271 | 272 | it('caches repeated requests', async () => { 273 | const [identityLoader, loadCalls] = idLoader(); 274 | 275 | const [a, b] = await Promise.all([ 276 | identityLoader.load('A'), 277 | identityLoader.load('B'), 278 | ]); 279 | 280 | expect(a).toBe('A'); 281 | expect(b).toBe('B'); 282 | 283 | expect(loadCalls).toEqual([['A', 'B']]); 284 | 285 | const [a2, c] = await Promise.all([ 286 | identityLoader.load('A'), 287 | identityLoader.load('C'), 288 | ]); 289 | 290 | expect(a2).toBe('A'); 291 | expect(c).toBe('C'); 292 | 293 | expect(loadCalls).toEqual([['A', 'B'], ['C']]); 294 | 295 | const [a3, b2, c2] = await Promise.all([ 296 | identityLoader.load('A'), 297 | identityLoader.load('B'), 298 | identityLoader.load('C'), 299 | ]); 300 | 301 | expect(a3).toBe('A'); 302 | expect(b2).toBe('B'); 303 | expect(c2).toBe('C'); 304 | 305 | expect(loadCalls).toEqual([['A', 'B'], ['C']]); 306 | }); 307 | 308 | it('clears single value in loader', async () => { 309 | const [identityLoader, loadCalls] = idLoader(); 310 | 311 | const [a, b] = await Promise.all([ 312 | identityLoader.load('A'), 313 | identityLoader.load('B'), 314 | ]); 315 | 316 | expect(a).toBe('A'); 317 | expect(b).toBe('B'); 318 | 319 | expect(loadCalls).toEqual([['A', 'B']]); 320 | 321 | identityLoader.clear('A'); 322 | 323 | const [a2, b2] = await Promise.all([ 324 | identityLoader.load('A'), 325 | identityLoader.load('B'), 326 | ]); 327 | 328 | expect(a2).toBe('A'); 329 | expect(b2).toBe('B'); 330 | 331 | expect(loadCalls).toEqual([['A', 'B'], ['A']]); 332 | }); 333 | 334 | it('clears all values in loader', async () => { 335 | const [identityLoader, loadCalls] = idLoader(); 336 | 337 | const [a, b] = await Promise.all([ 338 | identityLoader.load('A'), 339 | identityLoader.load('B'), 340 | ]); 341 | 342 | expect(a).toBe('A'); 343 | expect(b).toBe('B'); 344 | 345 | expect(loadCalls).toEqual([['A', 'B']]); 346 | 347 | identityLoader.clearAll(); 348 | 349 | const [a2, b2] = await Promise.all([ 350 | identityLoader.load('A'), 351 | identityLoader.load('B'), 352 | ]); 353 | 354 | expect(a2).toBe('A'); 355 | expect(b2).toBe('B'); 356 | 357 | expect(loadCalls).toEqual([ 358 | ['A', 'B'], 359 | ['A', 'B'], 360 | ]); 361 | }); 362 | 363 | it('allows priming the cache', async () => { 364 | const [identityLoader, loadCalls] = idLoader(); 365 | 366 | identityLoader.prime('A', 'A'); 367 | 368 | const [a, b] = await Promise.all([ 369 | identityLoader.load('A'), 370 | identityLoader.load('B'), 371 | ]); 372 | 373 | expect(a).toBe('A'); 374 | expect(b).toBe('B'); 375 | 376 | expect(loadCalls).toEqual([['B']]); 377 | }); 378 | 379 | it('does not prime keys that already exist', async () => { 380 | const [identityLoader, loadCalls] = idLoader(); 381 | 382 | identityLoader.prime('A', 'X'); 383 | 384 | const a1 = await identityLoader.load('A'); 385 | const b1 = await identityLoader.load('B'); 386 | expect(a1).toBe('X'); 387 | expect(b1).toBe('B'); 388 | 389 | identityLoader.prime('A', 'Y'); 390 | identityLoader.prime('B', 'Y'); 391 | 392 | const a2 = await identityLoader.load('A'); 393 | const b2 = await identityLoader.load('B'); 394 | expect(a2).toBe('X'); 395 | expect(b2).toBe('B'); 396 | 397 | expect(loadCalls).toEqual([['B']]); 398 | }); 399 | 400 | it('allows forcefully priming the cache', async () => { 401 | const [identityLoader, loadCalls] = idLoader(); 402 | 403 | identityLoader.prime('A', 'X'); 404 | 405 | const a1 = await identityLoader.load('A'); 406 | const b1 = await identityLoader.load('B'); 407 | expect(a1).toBe('X'); 408 | expect(b1).toBe('B'); 409 | 410 | identityLoader.clear('A').prime('A', 'Y'); 411 | identityLoader.clear('B').prime('B', 'Y'); 412 | 413 | const a2 = await identityLoader.load('A'); 414 | const b2 = await identityLoader.load('B'); 415 | expect(a2).toBe('Y'); 416 | expect(b2).toBe('Y'); 417 | 418 | expect(loadCalls).toEqual([['B']]); 419 | }); 420 | 421 | it('allows priming the cache with a promise', async () => { 422 | const [identityLoader, loadCalls] = idLoader(); 423 | 424 | identityLoader.prime('A', Promise.resolve('A')); 425 | 426 | const [a, b] = await Promise.all([ 427 | identityLoader.load('A'), 428 | identityLoader.load('B'), 429 | ]); 430 | 431 | expect(a).toBe('A'); 432 | expect(b).toBe('B'); 433 | 434 | expect(loadCalls).toEqual([['B']]); 435 | }); 436 | 437 | it('allows giving the loader a name', () => { 438 | expect(new DataLoader(() => Promise.resolve([])).name).toBeNull(); 439 | expect(new DataLoader(() => Promise.resolve([]), {}).name).toBeNull(); 440 | 441 | expect( 442 | new DataLoader(() => Promise.resolve([]), { name: 'Some name' }).name, 443 | ).toBe('Some name'); 444 | }); 445 | }); 446 | 447 | describe('Represents Errors', () => { 448 | it('Resolves to error to indicate failure', async () => { 449 | const loadCalls = []; 450 | const evenLoader = new DataLoader(keys => { 451 | loadCalls.push(keys); 452 | return Promise.resolve( 453 | keys.map(key => (key % 2 === 0 ? key : new Error(`Odd: ${key}`))), 454 | ); 455 | }); 456 | 457 | let caughtError; 458 | try { 459 | await evenLoader.load(1); 460 | } catch (error) { 461 | caughtError = error; 462 | } 463 | expect(caughtError).toBeInstanceOf(Error); 464 | expect((caughtError: any).message).toBe('Odd: 1'); 465 | 466 | const value2 = await evenLoader.load(2); 467 | expect(value2).toBe(2); 468 | 469 | expect(loadCalls).toEqual([[1], [2]]); 470 | }); 471 | 472 | it('Can represent failures and successes simultaneously', async () => { 473 | const loadCalls = []; 474 | const evenLoader = new DataLoader(keys => { 475 | loadCalls.push(keys); 476 | return Promise.resolve( 477 | keys.map(key => (key % 2 === 0 ? key : new Error(`Odd: ${key}`))), 478 | ); 479 | }); 480 | 481 | const promise1 = evenLoader.load(1); 482 | const promise2 = evenLoader.load(2); 483 | 484 | let caughtError; 485 | try { 486 | await promise1; 487 | } catch (error) { 488 | caughtError = error; 489 | } 490 | expect(caughtError).toBeInstanceOf(Error); 491 | expect((caughtError: any).message).toBe('Odd: 1'); 492 | 493 | expect(await promise2).toBe(2); 494 | 495 | expect(loadCalls).toEqual([[1, 2]]); 496 | }); 497 | 498 | it('Caches failed fetches', async () => { 499 | const loadCalls = []; 500 | const errorLoader = new DataLoader(keys => { 501 | loadCalls.push(keys); 502 | return Promise.resolve(keys.map(key => new Error(`Error: ${key}`))); 503 | }); 504 | 505 | let caughtErrorA; 506 | try { 507 | await errorLoader.load(1); 508 | } catch (error) { 509 | caughtErrorA = error; 510 | } 511 | expect(caughtErrorA).toBeInstanceOf(Error); 512 | expect((caughtErrorA: any).message).toBe('Error: 1'); 513 | 514 | let caughtErrorB; 515 | try { 516 | await errorLoader.load(1); 517 | } catch (error) { 518 | caughtErrorB = error; 519 | } 520 | expect(caughtErrorB).toBeInstanceOf(Error); 521 | expect((caughtErrorB: any).message).toBe('Error: 1'); 522 | 523 | expect(loadCalls).toEqual([[1]]); 524 | }); 525 | 526 | it('Handles priming the cache with an error', async () => { 527 | const [identityLoader, loadCalls] = idLoader(); 528 | 529 | identityLoader.prime(1, new Error('Error: 1')); 530 | 531 | // Wait a bit. 532 | await new Promise(setImmediate); 533 | 534 | let caughtErrorA; 535 | try { 536 | await identityLoader.load(1); 537 | } catch (error) { 538 | caughtErrorA = error; 539 | } 540 | expect(caughtErrorA).toBeInstanceOf(Error); 541 | expect((caughtErrorA: any).message).toBe('Error: 1'); 542 | 543 | expect(loadCalls).toEqual([]); 544 | }); 545 | 546 | it('Can clear values from cache after errors', async () => { 547 | const loadCalls = []; 548 | const errorLoader = new DataLoader(keys => { 549 | loadCalls.push(keys); 550 | return Promise.resolve(keys.map(key => new Error(`Error: ${key}`))); 551 | }); 552 | 553 | let caughtErrorA; 554 | try { 555 | await errorLoader.load(1).catch(error => { 556 | // Presumably determine if this error is transient, and only clear the 557 | // cache in that case. 558 | errorLoader.clear(1); 559 | throw error; 560 | }); 561 | } catch (error) { 562 | caughtErrorA = error; 563 | } 564 | expect(caughtErrorA).toBeInstanceOf(Error); 565 | expect((caughtErrorA: any).message).toBe('Error: 1'); 566 | 567 | let caughtErrorB; 568 | try { 569 | await errorLoader.load(1).catch(error => { 570 | // Again, only do this if you can determine the error is transient. 571 | errorLoader.clear(1); 572 | throw error; 573 | }); 574 | } catch (error) { 575 | caughtErrorB = error; 576 | } 577 | expect(caughtErrorB).toBeInstanceOf(Error); 578 | expect((caughtErrorB: any).message).toBe('Error: 1'); 579 | 580 | expect(loadCalls).toEqual([[1], [1]]); 581 | }); 582 | 583 | it('Propagates error to all loads', async () => { 584 | const loadCalls = []; 585 | const failLoader = new DataLoader(keys => { 586 | loadCalls.push(keys); 587 | return Promise.reject(new Error('I am a terrible loader')); 588 | }); 589 | 590 | const promise1 = failLoader.load(1); 591 | const promise2 = failLoader.load(2); 592 | 593 | let caughtErrorA; 594 | try { 595 | await promise1; 596 | } catch (error) { 597 | caughtErrorA = error; 598 | } 599 | expect(caughtErrorA).toBeInstanceOf(Error); 600 | expect((caughtErrorA: any).message).toBe('I am a terrible loader'); 601 | 602 | let caughtErrorB; 603 | try { 604 | await promise2; 605 | } catch (error) { 606 | caughtErrorB = error; 607 | } 608 | expect(caughtErrorB).toBe(caughtErrorA); 609 | 610 | expect(loadCalls).toEqual([[1, 2]]); 611 | }); 612 | }); 613 | 614 | describe('Accepts any kind of key', () => { 615 | it('Accepts objects as keys', async () => { 616 | const [identityLoader, loadCalls] = idLoader<{}>(); 617 | 618 | const keyA = {}; 619 | const keyB = {}; 620 | 621 | // Fetches as expected 622 | 623 | const [valueA, valueB] = await Promise.all([ 624 | identityLoader.load(keyA), 625 | identityLoader.load(keyB), 626 | ]); 627 | 628 | expect(valueA).toBe(keyA); 629 | expect(valueB).toBe(keyB); 630 | 631 | expect(loadCalls).toHaveLength(1); 632 | expect(loadCalls[0]).toHaveLength(2); 633 | expect(loadCalls[0][0]).toBe(keyA); 634 | expect(loadCalls[0][1]).toBe(keyB); 635 | 636 | // Caching 637 | 638 | identityLoader.clear(keyA); 639 | 640 | const [valueA2, valueB2] = await Promise.all([ 641 | identityLoader.load(keyA), 642 | identityLoader.load(keyB), 643 | ]); 644 | 645 | expect(valueA2).toBe(keyA); 646 | expect(valueB2).toBe(keyB); 647 | 648 | expect(loadCalls).toHaveLength(2); 649 | expect(loadCalls[1]).toHaveLength(1); 650 | expect(loadCalls[1][0]).toBe(keyA); 651 | }); 652 | }); 653 | 654 | describe('Accepts options', () => { 655 | // Note: mirrors 'batches multiple requests' above. 656 | it('May disable batching', async () => { 657 | const [identityLoader, loadCalls] = idLoader({ batch: false }); 658 | 659 | const promise1 = identityLoader.load(1); 660 | const promise2 = identityLoader.load(2); 661 | 662 | const [value1, value2] = await Promise.all([promise1, promise2]); 663 | expect(value1).toBe(1); 664 | expect(value2).toBe(2); 665 | 666 | expect(loadCalls).toEqual([[1], [2]]); 667 | }); 668 | 669 | // Note: mirror's 'caches repeated requests' above. 670 | it('May disable caching', async () => { 671 | const [identityLoader, loadCalls] = idLoader({ cache: false }); 672 | 673 | const [a, b] = await Promise.all([ 674 | identityLoader.load('A'), 675 | identityLoader.load('B'), 676 | ]); 677 | 678 | expect(a).toBe('A'); 679 | expect(b).toBe('B'); 680 | 681 | expect(loadCalls).toEqual([['A', 'B']]); 682 | 683 | const [a2, c] = await Promise.all([ 684 | identityLoader.load('A'), 685 | identityLoader.load('C'), 686 | ]); 687 | 688 | expect(a2).toBe('A'); 689 | expect(c).toBe('C'); 690 | 691 | expect(loadCalls).toEqual([ 692 | ['A', 'B'], 693 | ['A', 'C'], 694 | ]); 695 | 696 | const [a3, b2, c2] = await Promise.all([ 697 | identityLoader.load('A'), 698 | identityLoader.load('B'), 699 | identityLoader.load('C'), 700 | ]); 701 | 702 | expect(a3).toBe('A'); 703 | expect(b2).toBe('B'); 704 | expect(c2).toBe('C'); 705 | 706 | expect(loadCalls).toEqual([ 707 | ['A', 'B'], 708 | ['A', 'C'], 709 | ['A', 'B', 'C'], 710 | ]); 711 | }); 712 | 713 | it('Keys are repeated in batch when cache disabled', async () => { 714 | const [identityLoader, loadCalls] = idLoader({ cache: false }); 715 | 716 | const [values1, values2, values3, values4] = await Promise.all([ 717 | identityLoader.load('A'), 718 | identityLoader.load('C'), 719 | identityLoader.load('D'), 720 | identityLoader.loadMany(['C', 'D', 'A', 'A', 'B']), 721 | ]); 722 | 723 | expect(values1).toBe('A'); 724 | expect(values2).toBe('C'); 725 | expect(values3).toBe('D'); 726 | expect(values4).toEqual(['C', 'D', 'A', 'A', 'B']); 727 | 728 | expect(loadCalls).toEqual([['A', 'C', 'D', 'C', 'D', 'A', 'A', 'B']]); 729 | }); 730 | 731 | it('cacheMap may be set to null to disable cache', async () => { 732 | const [identityLoader, loadCalls] = idLoader({ cacheMap: null }); 733 | 734 | await identityLoader.load('A'); 735 | await identityLoader.load('A'); 736 | 737 | expect(loadCalls).toEqual([['A'], ['A']]); 738 | }); 739 | 740 | it('Does not interact with a cache when cache is disabled', () => { 741 | const promiseX = Promise.resolve('X'); 742 | const cacheMap = new Map([['X', promiseX]]); 743 | const [identityLoader] = idLoader({ cache: false, cacheMap }); 744 | 745 | identityLoader.prime('A', 'A'); 746 | expect(cacheMap.get('A')).toBe(undefined); 747 | identityLoader.clear('X'); 748 | expect(cacheMap.get('X')).toBe(promiseX); 749 | identityLoader.clearAll(); 750 | expect(cacheMap.get('X')).toBe(promiseX); 751 | }); 752 | 753 | it('Does not call cacheKeyFn when cache is disabled', async () => { 754 | const cacheKeyFnCalls = []; 755 | const [identityLoader] = idLoader({ 756 | cache: false, 757 | cacheKeyFn: key => { 758 | cacheKeyFnCalls.push(key); 759 | return key; 760 | }, 761 | }); 762 | 763 | await identityLoader.load('A'); 764 | expect(cacheKeyFnCalls).toEqual([]); 765 | }); 766 | 767 | it('Complex cache behavior via clearAll()', async () => { 768 | // This loader clears its cache as soon as a batch function is dispatched. 769 | const loadCalls = []; 770 | const identityLoader = new DataLoader(keys => { 771 | identityLoader.clearAll(); 772 | loadCalls.push(keys); 773 | return Promise.resolve(keys); 774 | }); 775 | 776 | const values1 = await Promise.all([ 777 | identityLoader.load('A'), 778 | identityLoader.load('B'), 779 | identityLoader.load('A'), 780 | ]); 781 | 782 | expect(values1).toEqual(['A', 'B', 'A']); 783 | 784 | const values2 = await Promise.all([ 785 | identityLoader.load('A'), 786 | identityLoader.load('B'), 787 | identityLoader.load('A'), 788 | ]); 789 | 790 | expect(values2).toEqual(['A', 'B', 'A']); 791 | 792 | expect(loadCalls).toEqual([ 793 | ['A', 'B'], 794 | ['A', 'B'], 795 | ]); 796 | }); 797 | 798 | describe('Accepts object key in custom cacheKey function', () => { 799 | function cacheKey(key: { [string]: any }): string { 800 | return Object.keys(key) 801 | .sort() 802 | .map(k => k + ':' + key[k]) 803 | .join(); 804 | } 805 | 806 | type Obj = { [string]: number }; 807 | 808 | it('Accepts objects with a complex key', async () => { 809 | const identityLoadCalls = []; 810 | const identityLoader = new DataLoader( 811 | keys => { 812 | identityLoadCalls.push(keys); 813 | return Promise.resolve(keys); 814 | }, 815 | { cacheKeyFn: cacheKey }, 816 | ); 817 | 818 | const key1 = { id: 123 }; 819 | const key2 = { id: 123 }; 820 | 821 | const value1 = await identityLoader.load(key1); 822 | const value2 = await identityLoader.load(key2); 823 | 824 | expect(identityLoadCalls).toEqual([[key1]]); 825 | expect(value1).toBe(key1); 826 | expect(value2).toBe(key1); 827 | }); 828 | 829 | it('Clears objects with complex key', async () => { 830 | const identityLoadCalls = []; 831 | const identityLoader = new DataLoader( 832 | keys => { 833 | identityLoadCalls.push(keys); 834 | return Promise.resolve(keys); 835 | }, 836 | { cacheKeyFn: cacheKey }, 837 | ); 838 | 839 | const key1 = { id: 123 }; 840 | const key2 = { id: 123 }; 841 | 842 | const value1 = await identityLoader.load(key1); 843 | identityLoader.clear(key2); // clear equivalent object key 844 | const value2 = await identityLoader.load(key1); 845 | 846 | expect(identityLoadCalls).toEqual([[key1], [key1]]); 847 | expect(value1).toBe(key1); 848 | expect(value2).toBe(key1); 849 | }); 850 | 851 | it('Accepts objects with different order of keys', async () => { 852 | const identityLoadCalls = []; 853 | const identityLoader = new DataLoader( 854 | keys => { 855 | identityLoadCalls.push(keys); 856 | return Promise.resolve(keys); 857 | }, 858 | { cacheKeyFn: cacheKey }, 859 | ); 860 | 861 | // Fetches as expected 862 | 863 | const keyA = { a: 123, b: 321 }; 864 | const keyB = { b: 321, a: 123 }; 865 | 866 | const [valueA, valueB] = await Promise.all([ 867 | identityLoader.load(keyA), 868 | identityLoader.load(keyB), 869 | ]); 870 | 871 | expect(valueA).toBe(keyA); 872 | expect(valueB).toBe(valueA); 873 | 874 | expect(identityLoadCalls).toHaveLength(1); 875 | expect(identityLoadCalls[0]).toHaveLength(1); 876 | expect(identityLoadCalls[0][0]).toBe(keyA); 877 | }); 878 | 879 | it('Allows priming the cache with an object key', async () => { 880 | const [identityLoader, loadCalls] = idLoader({ 881 | cacheKeyFn: cacheKey, 882 | }); 883 | 884 | const key1 = { id: 123 }; 885 | const key2 = { id: 123 }; 886 | 887 | identityLoader.prime(key1, key1); 888 | 889 | const value1 = await identityLoader.load(key1); 890 | const value2 = await identityLoader.load(key2); 891 | 892 | expect(loadCalls).toEqual([]); 893 | expect(value1).toBe(key1); 894 | expect(value2).toBe(key1); 895 | }); 896 | }); 897 | 898 | describe('Accepts custom cacheMap instance', () => { 899 | class SimpleMap { 900 | stash: Object; 901 | 902 | constructor() { 903 | this.stash = {}; 904 | } 905 | get(key) { 906 | return this.stash[key]; 907 | } 908 | set(key, value) { 909 | this.stash[key] = value; 910 | } 911 | delete(key) { 912 | delete this.stash[key]; 913 | } 914 | clear() { 915 | this.stash = {}; 916 | } 917 | } 918 | 919 | it('Accepts a custom cache map implementation', async () => { 920 | const aCustomMap = new SimpleMap(); 921 | const identityLoadCalls = []; 922 | const identityLoader = new DataLoader( 923 | keys => { 924 | identityLoadCalls.push(keys); 925 | return Promise.resolve(keys); 926 | }, 927 | { cacheMap: aCustomMap }, 928 | ); 929 | 930 | // Fetches as expected 931 | 932 | const [valueA, valueB1] = await Promise.all([ 933 | identityLoader.load('a'), 934 | identityLoader.load('b'), 935 | ]); 936 | 937 | expect(valueA).toBe('a'); 938 | expect(valueB1).toBe('b'); 939 | 940 | expect(identityLoadCalls).toEqual([['a', 'b']]); 941 | expect(Object.keys(aCustomMap.stash)).toEqual(['a', 'b']); 942 | 943 | const [valueC, valueB2] = await Promise.all([ 944 | identityLoader.load('c'), 945 | identityLoader.load('b'), 946 | ]); 947 | 948 | expect(valueC).toBe('c'); 949 | expect(valueB2).toBe('b'); 950 | 951 | expect(identityLoadCalls).toEqual([['a', 'b'], ['c']]); 952 | expect(Object.keys(aCustomMap.stash)).toEqual(['a', 'b', 'c']); 953 | 954 | // Supports clear 955 | 956 | identityLoader.clear('b'); 957 | const valueB3 = await identityLoader.load('b'); 958 | 959 | expect(valueB3).toBe('b'); 960 | expect(identityLoadCalls).toEqual([['a', 'b'], ['c'], ['b']]); 961 | expect(Object.keys(aCustomMap.stash)).toEqual(['a', 'c', 'b']); 962 | 963 | // Supports clear all 964 | 965 | identityLoader.clearAll(); 966 | 967 | expect(Object.keys(aCustomMap.stash)).toEqual([]); 968 | }); 969 | }); 970 | }); 971 | 972 | describe('It allows custom schedulers', () => { 973 | it('Supports manual dispatch', () => { 974 | function createScheduler() { 975 | let callbacks = []; 976 | return { 977 | schedule(callback) { 978 | callbacks.push(callback); 979 | }, 980 | dispatch() { 981 | callbacks.forEach(callback => callback()); 982 | callbacks = []; 983 | }, 984 | }; 985 | } 986 | 987 | const { schedule, dispatch } = createScheduler(); 988 | const [identityLoader, loadCalls] = idLoader({ 989 | batchScheduleFn: schedule, 990 | }); 991 | 992 | identityLoader.load('A'); 993 | identityLoader.load('B'); 994 | dispatch(); 995 | identityLoader.load('A'); 996 | identityLoader.load('C'); 997 | dispatch(); 998 | // Note: never dispatched! 999 | identityLoader.load('D'); 1000 | 1001 | expect(loadCalls).toEqual([['A', 'B'], ['C']]); 1002 | }); 1003 | 1004 | it('Custom batch scheduler is provided loader as this context', () => { 1005 | let that; 1006 | function batchScheduleFn(callback) { 1007 | that = this; 1008 | callback(); 1009 | } 1010 | 1011 | const [identityLoader] = idLoader({ batchScheduleFn }); 1012 | 1013 | identityLoader.load('A'); 1014 | expect(that).toBe(identityLoader); 1015 | }); 1016 | }); 1017 | 1018 | describe('It is resilient to job queue ordering', () => { 1019 | it('batches loads occuring within promises', async () => { 1020 | const [identityLoader, loadCalls] = idLoader(); 1021 | 1022 | await Promise.all([ 1023 | identityLoader.load('A'), 1024 | Promise.resolve() 1025 | .then(() => Promise.resolve()) 1026 | .then(() => { 1027 | identityLoader.load('B'); 1028 | Promise.resolve() 1029 | .then(() => Promise.resolve()) 1030 | .then(() => { 1031 | identityLoader.load('C'); 1032 | Promise.resolve() 1033 | .then(() => Promise.resolve()) 1034 | .then(() => { 1035 | identityLoader.load('D'); 1036 | }); 1037 | }); 1038 | }), 1039 | ]); 1040 | 1041 | expect(loadCalls).toEqual([['A', 'B', 'C', 'D']]); 1042 | }); 1043 | 1044 | it('can call a loader from a loader', async () => { 1045 | const deepLoadCalls = []; 1046 | const deepLoader = new DataLoader< 1047 | $ReadOnlyArray, 1048 | $ReadOnlyArray, 1049 | >(keys => { 1050 | deepLoadCalls.push(keys); 1051 | return Promise.resolve(keys); 1052 | }); 1053 | 1054 | const aLoadCalls = []; 1055 | const aLoader = new DataLoader(keys => { 1056 | aLoadCalls.push(keys); 1057 | return deepLoader.load(keys); 1058 | }); 1059 | 1060 | const bLoadCalls = []; 1061 | const bLoader = new DataLoader(keys => { 1062 | bLoadCalls.push(keys); 1063 | return deepLoader.load(keys); 1064 | }); 1065 | 1066 | const [a1, b1, a2, b2] = await Promise.all([ 1067 | aLoader.load('A1'), 1068 | bLoader.load('B1'), 1069 | aLoader.load('A2'), 1070 | bLoader.load('B2'), 1071 | ]); 1072 | 1073 | expect(a1).toBe('A1'); 1074 | expect(b1).toBe('B1'); 1075 | expect(a2).toBe('A2'); 1076 | expect(b2).toBe('B2'); 1077 | 1078 | expect(aLoadCalls).toEqual([['A1', 'A2']]); 1079 | expect(bLoadCalls).toEqual([['B1', 'B2']]); 1080 | expect(deepLoadCalls).toEqual([ 1081 | [ 1082 | ['A1', 'A2'], 1083 | ['B1', 'B2'], 1084 | ], 1085 | ]); 1086 | }); 1087 | }); 1088 | -------------------------------------------------------------------------------- /flow-typed/npm/jest_v24.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 27f8467378a99b6130bd20f54f31a644 2 | // flow-typed version: 6cb9e99836/jest_v24.x.x/flow_>=v0.104.x 3 | 4 | type JestMockFn, TReturn> = { 5 | (...args: TArguments): TReturn, 6 | /** 7 | * An object for introspecting mock calls 8 | */ 9 | mock: { 10 | /** 11 | * An array that represents all calls that have been made into this mock 12 | * function. Each call is represented by an array of arguments that were 13 | * passed during the call. 14 | */ 15 | calls: Array, 16 | /** 17 | * An array that contains all the object instances that have been 18 | * instantiated from this mock function. 19 | */ 20 | instances: Array, 21 | /** 22 | * An array that contains all the object results that have been 23 | * returned by this mock function call 24 | */ 25 | results: Array<{ 26 | isThrow: boolean, 27 | value: TReturn, 28 | ... 29 | }>, 30 | ... 31 | }, 32 | /** 33 | * Resets all information stored in the mockFn.mock.calls and 34 | * mockFn.mock.instances arrays. Often this is useful when you want to clean 35 | * up a mock's usage data between two assertions. 36 | */ 37 | mockClear(): void, 38 | /** 39 | * Resets all information stored in the mock. This is useful when you want to 40 | * completely restore a mock back to its initial state. 41 | */ 42 | mockReset(): void, 43 | /** 44 | * Removes the mock and restores the initial implementation. This is useful 45 | * when you want to mock functions in certain test cases and restore the 46 | * original implementation in others. Beware that mockFn.mockRestore only 47 | * works when mock was created with jest.spyOn. Thus you have to take care of 48 | * restoration yourself when manually assigning jest.fn(). 49 | */ 50 | mockRestore(): void, 51 | /** 52 | * Accepts a function that should be used as the implementation of the mock. 53 | * The mock itself will still record all calls that go into and instances 54 | * that come from itself -- the only difference is that the implementation 55 | * will also be executed when the mock is called. 56 | */ 57 | mockImplementation( 58 | fn: (...args: TArguments) => TReturn 59 | ): JestMockFn, 60 | /** 61 | * Accepts a function that will be used as an implementation of the mock for 62 | * one call to the mocked function. Can be chained so that multiple function 63 | * calls produce different results. 64 | */ 65 | mockImplementationOnce( 66 | fn: (...args: TArguments) => TReturn 67 | ): JestMockFn, 68 | /** 69 | * Accepts a string to use in test result output in place of "jest.fn()" to 70 | * indicate which mock function is being referenced. 71 | */ 72 | mockName(name: string): JestMockFn, 73 | /** 74 | * Just a simple sugar function for returning `this` 75 | */ 76 | mockReturnThis(): void, 77 | /** 78 | * Accepts a value that will be returned whenever the mock function is called. 79 | */ 80 | mockReturnValue(value: TReturn): JestMockFn, 81 | /** 82 | * Sugar for only returning a value once inside your mock 83 | */ 84 | mockReturnValueOnce(value: TReturn): JestMockFn, 85 | /** 86 | * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value)) 87 | */ 88 | mockResolvedValue(value: TReturn): JestMockFn>, 89 | /** 90 | * Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value)) 91 | */ 92 | mockResolvedValueOnce( 93 | value: TReturn 94 | ): JestMockFn>, 95 | /** 96 | * Sugar for jest.fn().mockImplementation(() => Promise.reject(value)) 97 | */ 98 | mockRejectedValue(value: TReturn): JestMockFn>, 99 | /** 100 | * Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value)) 101 | */ 102 | mockRejectedValueOnce(value: TReturn): JestMockFn>, 103 | ... 104 | }; 105 | 106 | type JestAsymmetricEqualityType = { /** 107 | * A custom Jasmine equality tester 108 | */ 109 | asymmetricMatch(value: mixed): boolean, ... }; 110 | 111 | type JestCallsType = { 112 | allArgs(): mixed, 113 | all(): mixed, 114 | any(): boolean, 115 | count(): number, 116 | first(): mixed, 117 | mostRecent(): mixed, 118 | reset(): void, 119 | ... 120 | }; 121 | 122 | type JestClockType = { 123 | install(): void, 124 | mockDate(date: Date): void, 125 | tick(milliseconds?: number): void, 126 | uninstall(): void, 127 | ... 128 | }; 129 | 130 | type JestMatcherResult = { 131 | message?: string | (() => string), 132 | pass: boolean, 133 | ... 134 | }; 135 | 136 | type JestMatcher = ( 137 | received: any, 138 | ...actual: Array 139 | ) => JestMatcherResult | Promise; 140 | 141 | type JestPromiseType = { 142 | /** 143 | * Use rejects to unwrap the reason of a rejected promise so any other 144 | * matcher can be chained. If the promise is fulfilled the assertion fails. 145 | */ 146 | rejects: JestExpectType, 147 | /** 148 | * Use resolves to unwrap the value of a fulfilled promise so any other 149 | * matcher can be chained. If the promise is rejected the assertion fails. 150 | */ 151 | resolves: JestExpectType, 152 | ... 153 | }; 154 | 155 | /** 156 | * Jest allows functions and classes to be used as test names in test() and 157 | * describe() 158 | */ 159 | type JestTestName = string | Function; 160 | 161 | /** 162 | * Plugin: jest-styled-components 163 | */ 164 | 165 | type JestStyledComponentsMatcherValue = 166 | | string 167 | | JestAsymmetricEqualityType 168 | | RegExp 169 | | typeof undefined; 170 | 171 | type JestStyledComponentsMatcherOptions = { 172 | media?: string, 173 | modifier?: string, 174 | supports?: string, 175 | ... 176 | }; 177 | 178 | type JestStyledComponentsMatchersType = { toHaveStyleRule( 179 | property: string, 180 | value: JestStyledComponentsMatcherValue, 181 | options?: JestStyledComponentsMatcherOptions 182 | ): void, ... }; 183 | 184 | /** 185 | * Plugin: jest-enzyme 186 | */ 187 | type EnzymeMatchersType = { 188 | // 5.x 189 | toBeEmpty(): void, 190 | toBePresent(): void, 191 | // 6.x 192 | toBeChecked(): void, 193 | toBeDisabled(): void, 194 | toBeEmptyRender(): void, 195 | toContainMatchingElement(selector: string): void, 196 | toContainMatchingElements(n: number, selector: string): void, 197 | toContainExactlyOneMatchingElement(selector: string): void, 198 | toContainReact(element: React$Element): void, 199 | toExist(): void, 200 | toHaveClassName(className: string): void, 201 | toHaveHTML(html: string): void, 202 | toHaveProp: ((propKey: string, propValue?: any) => void) & 203 | ((props: {...}) => void), 204 | toHaveRef(refName: string): void, 205 | toHaveState: ((stateKey: string, stateValue?: any) => void) & 206 | ((state: {...}) => void), 207 | toHaveStyle: ((styleKey: string, styleValue?: any) => void) & 208 | ((style: {...}) => void), 209 | toHaveTagName(tagName: string): void, 210 | toHaveText(text: string): void, 211 | toHaveValue(value: any): void, 212 | toIncludeText(text: string): void, 213 | toMatchElement( 214 | element: React$Element, 215 | options?: {| ignoreProps?: boolean, verbose?: boolean |} 216 | ): void, 217 | toMatchSelector(selector: string): void, 218 | // 7.x 219 | toHaveDisplayName(name: string): void, 220 | ... 221 | }; 222 | 223 | // DOM testing library extensions (jest-dom) 224 | // https://github.com/testing-library/jest-dom 225 | type DomTestingLibraryType = { 226 | /** 227 | * @deprecated 228 | */ 229 | toBeInTheDOM(container?: HTMLElement): void, 230 | toBeInTheDocument(): void, 231 | toBeVisible(): void, 232 | toBeEmpty(): void, 233 | toBeDisabled(): void, 234 | toBeEnabled(): void, 235 | toBeInvalid(): void, 236 | toBeRequired(): void, 237 | toBeValid(): void, 238 | toContainElement(element: HTMLElement | null): void, 239 | toContainHTML(htmlText: string): void, 240 | toHaveAttribute(attr: string, value?: any): void, 241 | toHaveClass(...classNames: string[]): void, 242 | toHaveFocus(): void, 243 | toHaveFormValues(expectedValues: { [name: string]: any, ... }): void, 244 | toHaveStyle(css: string): void, 245 | toHaveTextContent( 246 | text: string | RegExp, 247 | options?: { normalizeWhitespace: boolean, ... } 248 | ): void, 249 | toHaveValue(value?: string | string[] | number): void, 250 | ... 251 | }; 252 | 253 | // Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers 254 | type JestJQueryMatchersType = { 255 | toExist(): void, 256 | toHaveLength(len: number): void, 257 | toHaveId(id: string): void, 258 | toHaveClass(className: string): void, 259 | toHaveTag(tag: string): void, 260 | toHaveAttr(key: string, val?: any): void, 261 | toHaveProp(key: string, val?: any): void, 262 | toHaveText(text: string | RegExp): void, 263 | toHaveData(key: string, val?: any): void, 264 | toHaveValue(val: any): void, 265 | toHaveCss(css: { [key: string]: any, ... }): void, 266 | toBeChecked(): void, 267 | toBeDisabled(): void, 268 | toBeEmpty(): void, 269 | toBeHidden(): void, 270 | toBeSelected(): void, 271 | toBeVisible(): void, 272 | toBeFocused(): void, 273 | toBeInDom(): void, 274 | toBeMatchedBy(sel: string): void, 275 | toHaveDescendant(sel: string): void, 276 | toHaveDescendantWithText(sel: string, text: string | RegExp): void, 277 | ... 278 | }; 279 | 280 | // Jest Extended Matchers: https://github.com/jest-community/jest-extended 281 | type JestExtendedMatchersType = { 282 | /** 283 | * Note: Currently unimplemented 284 | * Passing assertion 285 | * 286 | * @param {String} message 287 | */ 288 | // pass(message: string): void; 289 | 290 | /** 291 | * Note: Currently unimplemented 292 | * Failing assertion 293 | * 294 | * @param {String} message 295 | */ 296 | // fail(message: string): void; 297 | 298 | /** 299 | * Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty. 300 | */ 301 | toBeEmpty(): void, 302 | /** 303 | * Use .toBeOneOf when checking if a value is a member of a given Array. 304 | * @param {Array.<*>} members 305 | */ 306 | toBeOneOf(members: any[]): void, 307 | /** 308 | * Use `.toBeNil` when checking a value is `null` or `undefined`. 309 | */ 310 | toBeNil(): void, 311 | /** 312 | * Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`. 313 | * @param {Function} predicate 314 | */ 315 | toSatisfy(predicate: (n: any) => boolean): void, 316 | /** 317 | * Use `.toBeArray` when checking if a value is an `Array`. 318 | */ 319 | toBeArray(): void, 320 | /** 321 | * Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x. 322 | * @param {Number} x 323 | */ 324 | toBeArrayOfSize(x: number): void, 325 | /** 326 | * Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set. 327 | * @param {Array.<*>} members 328 | */ 329 | toIncludeAllMembers(members: any[]): void, 330 | /** 331 | * Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set. 332 | * @param {Array.<*>} members 333 | */ 334 | toIncludeAnyMembers(members: any[]): void, 335 | /** 336 | * Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array. 337 | * @param {Function} predicate 338 | */ 339 | toSatisfyAll(predicate: (n: any) => boolean): void, 340 | /** 341 | * Use `.toBeBoolean` when checking if a value is a `Boolean`. 342 | */ 343 | toBeBoolean(): void, 344 | /** 345 | * Use `.toBeTrue` when checking a value is equal (===) to `true`. 346 | */ 347 | toBeTrue(): void, 348 | /** 349 | * Use `.toBeFalse` when checking a value is equal (===) to `false`. 350 | */ 351 | toBeFalse(): void, 352 | /** 353 | * Use .toBeDate when checking if a value is a Date. 354 | */ 355 | toBeDate(): void, 356 | /** 357 | * Use `.toBeFunction` when checking if a value is a `Function`. 358 | */ 359 | toBeFunction(): void, 360 | /** 361 | * Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`. 362 | * 363 | * Note: Required Jest version >22 364 | * Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same 365 | * 366 | * @param {Mock} mock 367 | */ 368 | toHaveBeenCalledBefore(mock: JestMockFn): void, 369 | /** 370 | * Use `.toBeNumber` when checking if a value is a `Number`. 371 | */ 372 | toBeNumber(): void, 373 | /** 374 | * Use `.toBeNaN` when checking a value is `NaN`. 375 | */ 376 | toBeNaN(): void, 377 | /** 378 | * Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`. 379 | */ 380 | toBeFinite(): void, 381 | /** 382 | * Use `.toBePositive` when checking if a value is a positive `Number`. 383 | */ 384 | toBePositive(): void, 385 | /** 386 | * Use `.toBeNegative` when checking if a value is a negative `Number`. 387 | */ 388 | toBeNegative(): void, 389 | /** 390 | * Use `.toBeEven` when checking if a value is an even `Number`. 391 | */ 392 | toBeEven(): void, 393 | /** 394 | * Use `.toBeOdd` when checking if a value is an odd `Number`. 395 | */ 396 | toBeOdd(): void, 397 | /** 398 | * Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive). 399 | * 400 | * @param {Number} start 401 | * @param {Number} end 402 | */ 403 | toBeWithin(start: number, end: number): void, 404 | /** 405 | * Use `.toBeObject` when checking if a value is an `Object`. 406 | */ 407 | toBeObject(): void, 408 | /** 409 | * Use `.toContainKey` when checking if an object contains the provided key. 410 | * 411 | * @param {String} key 412 | */ 413 | toContainKey(key: string): void, 414 | /** 415 | * Use `.toContainKeys` when checking if an object has all of the provided keys. 416 | * 417 | * @param {Array.} keys 418 | */ 419 | toContainKeys(keys: string[]): void, 420 | /** 421 | * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys. 422 | * 423 | * @param {Array.} keys 424 | */ 425 | toContainAllKeys(keys: string[]): void, 426 | /** 427 | * Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys. 428 | * 429 | * @param {Array.} keys 430 | */ 431 | toContainAnyKeys(keys: string[]): void, 432 | /** 433 | * Use `.toContainValue` when checking if an object contains the provided value. 434 | * 435 | * @param {*} value 436 | */ 437 | toContainValue(value: any): void, 438 | /** 439 | * Use `.toContainValues` when checking if an object contains all of the provided values. 440 | * 441 | * @param {Array.<*>} values 442 | */ 443 | toContainValues(values: any[]): void, 444 | /** 445 | * Use `.toContainAllValues` when checking if an object only contains all of the provided values. 446 | * 447 | * @param {Array.<*>} values 448 | */ 449 | toContainAllValues(values: any[]): void, 450 | /** 451 | * Use `.toContainAnyValues` when checking if an object contains at least one of the provided values. 452 | * 453 | * @param {Array.<*>} values 454 | */ 455 | toContainAnyValues(values: any[]): void, 456 | /** 457 | * Use `.toContainEntry` when checking if an object contains the provided entry. 458 | * 459 | * @param {Array.} entry 460 | */ 461 | toContainEntry(entry: [string, string]): void, 462 | /** 463 | * Use `.toContainEntries` when checking if an object contains all of the provided entries. 464 | * 465 | * @param {Array.>} entries 466 | */ 467 | toContainEntries(entries: [string, string][]): void, 468 | /** 469 | * Use `.toContainAllEntries` when checking if an object only contains all of the provided entries. 470 | * 471 | * @param {Array.>} entries 472 | */ 473 | toContainAllEntries(entries: [string, string][]): void, 474 | /** 475 | * Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries. 476 | * 477 | * @param {Array.>} entries 478 | */ 479 | toContainAnyEntries(entries: [string, string][]): void, 480 | /** 481 | * Use `.toBeExtensible` when checking if an object is extensible. 482 | */ 483 | toBeExtensible(): void, 484 | /** 485 | * Use `.toBeFrozen` when checking if an object is frozen. 486 | */ 487 | toBeFrozen(): void, 488 | /** 489 | * Use `.toBeSealed` when checking if an object is sealed. 490 | */ 491 | toBeSealed(): void, 492 | /** 493 | * Use `.toBeString` when checking if a value is a `String`. 494 | */ 495 | toBeString(): void, 496 | /** 497 | * Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings. 498 | * 499 | * @param {String} string 500 | */ 501 | toEqualCaseInsensitive(string: string): void, 502 | /** 503 | * Use `.toStartWith` when checking if a `String` starts with a given `String` prefix. 504 | * 505 | * @param {String} prefix 506 | */ 507 | toStartWith(prefix: string): void, 508 | /** 509 | * Use `.toEndWith` when checking if a `String` ends with a given `String` suffix. 510 | * 511 | * @param {String} suffix 512 | */ 513 | toEndWith(suffix: string): void, 514 | /** 515 | * Use `.toInclude` when checking if a `String` includes the given `String` substring. 516 | * 517 | * @param {String} substring 518 | */ 519 | toInclude(substring: string): void, 520 | /** 521 | * Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times. 522 | * 523 | * @param {String} substring 524 | * @param {Number} times 525 | */ 526 | toIncludeRepeated(substring: string, times: number): void, 527 | /** 528 | * Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings. 529 | * 530 | * @param {Array.} substring 531 | */ 532 | toIncludeMultiple(substring: string[]): void, 533 | ... 534 | }; 535 | 536 | interface JestExpectType { 537 | not: JestExpectType & 538 | EnzymeMatchersType & 539 | DomTestingLibraryType & 540 | JestJQueryMatchersType & 541 | JestStyledComponentsMatchersType & 542 | JestExtendedMatchersType; 543 | /** 544 | * If you have a mock function, you can use .lastCalledWith to test what 545 | * arguments it was last called with. 546 | */ 547 | lastCalledWith(...args: Array): void; 548 | /** 549 | * toBe just checks that a value is what you expect. It uses === to check 550 | * strict equality. 551 | */ 552 | toBe(value: any): void; 553 | /** 554 | * Use .toBeCalledWith to ensure that a mock function was called with 555 | * specific arguments. 556 | */ 557 | toBeCalledWith(...args: Array): void; 558 | /** 559 | * Using exact equality with floating point numbers is a bad idea. Rounding 560 | * means that intuitive things fail. 561 | */ 562 | toBeCloseTo(num: number, delta: any): void; 563 | /** 564 | * Use .toBeDefined to check that a variable is not undefined. 565 | */ 566 | toBeDefined(): void; 567 | /** 568 | * Use .toBeFalsy when you don't care what a value is, you just want to 569 | * ensure a value is false in a boolean context. 570 | */ 571 | toBeFalsy(): void; 572 | /** 573 | * To compare floating point numbers, you can use toBeGreaterThan. 574 | */ 575 | toBeGreaterThan(number: number): void; 576 | /** 577 | * To compare floating point numbers, you can use toBeGreaterThanOrEqual. 578 | */ 579 | toBeGreaterThanOrEqual(number: number): void; 580 | /** 581 | * To compare floating point numbers, you can use toBeLessThan. 582 | */ 583 | toBeLessThan(number: number): void; 584 | /** 585 | * To compare floating point numbers, you can use toBeLessThanOrEqual. 586 | */ 587 | toBeLessThanOrEqual(number: number): void; 588 | /** 589 | * Use .toBeInstanceOf(Class) to check that an object is an instance of a 590 | * class. 591 | */ 592 | toBeInstanceOf(cls: Class<*>): void; 593 | /** 594 | * .toBeNull() is the same as .toBe(null) but the error messages are a bit 595 | * nicer. 596 | */ 597 | toBeNull(): void; 598 | /** 599 | * Use .toBeTruthy when you don't care what a value is, you just want to 600 | * ensure a value is true in a boolean context. 601 | */ 602 | toBeTruthy(): void; 603 | /** 604 | * Use .toBeUndefined to check that a variable is undefined. 605 | */ 606 | toBeUndefined(): void; 607 | /** 608 | * Use .toContain when you want to check that an item is in a list. For 609 | * testing the items in the list, this uses ===, a strict equality check. 610 | */ 611 | toContain(item: any): void; 612 | /** 613 | * Use .toContainEqual when you want to check that an item is in a list. For 614 | * testing the items in the list, this matcher recursively checks the 615 | * equality of all fields, rather than checking for object identity. 616 | */ 617 | toContainEqual(item: any): void; 618 | /** 619 | * Use .toEqual when you want to check that two objects have the same value. 620 | * This matcher recursively checks the equality of all fields, rather than 621 | * checking for object identity. 622 | */ 623 | toEqual(value: any): void; 624 | /** 625 | * Use .toHaveBeenCalled to ensure that a mock function got called. 626 | */ 627 | toHaveBeenCalled(): void; 628 | toBeCalled(): void; 629 | /** 630 | * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact 631 | * number of times. 632 | */ 633 | toHaveBeenCalledTimes(number: number): void; 634 | toBeCalledTimes(number: number): void; 635 | /** 636 | * 637 | */ 638 | toHaveBeenNthCalledWith(nthCall: number, ...args: Array): void; 639 | nthCalledWith(nthCall: number, ...args: Array): void; 640 | /** 641 | * 642 | */ 643 | toHaveReturned(): void; 644 | toReturn(): void; 645 | /** 646 | * 647 | */ 648 | toHaveReturnedTimes(number: number): void; 649 | toReturnTimes(number: number): void; 650 | /** 651 | * 652 | */ 653 | toHaveReturnedWith(value: any): void; 654 | toReturnWith(value: any): void; 655 | /** 656 | * 657 | */ 658 | toHaveLastReturnedWith(value: any): void; 659 | lastReturnedWith(value: any): void; 660 | /** 661 | * 662 | */ 663 | toHaveNthReturnedWith(nthCall: number, value: any): void; 664 | nthReturnedWith(nthCall: number, value: any): void; 665 | /** 666 | * Use .toHaveBeenCalledWith to ensure that a mock function was called with 667 | * specific arguments. 668 | */ 669 | toHaveBeenCalledWith(...args: Array): void; 670 | toBeCalledWith(...args: Array): void; 671 | /** 672 | * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called 673 | * with specific arguments. 674 | */ 675 | toHaveBeenLastCalledWith(...args: Array): void; 676 | lastCalledWith(...args: Array): void; 677 | /** 678 | * Check that an object has a .length property and it is set to a certain 679 | * numeric value. 680 | */ 681 | toHaveLength(number: number): void; 682 | /** 683 | * 684 | */ 685 | toHaveProperty(propPath: string | $ReadOnlyArray, value?: any): void; 686 | /** 687 | * Use .toMatch to check that a string matches a regular expression or string. 688 | */ 689 | toMatch(regexpOrString: RegExp | string): void; 690 | /** 691 | * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. 692 | */ 693 | toMatchObject(object: Object | Array): void; 694 | /** 695 | * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object. 696 | */ 697 | toStrictEqual(value: any): void; 698 | /** 699 | * This ensures that an Object matches the most recent snapshot. 700 | */ 701 | toMatchSnapshot(propertyMatchers?: any, name?: string): void; 702 | /** 703 | * This ensures that an Object matches the most recent snapshot. 704 | */ 705 | toMatchSnapshot(name: string): void; 706 | 707 | toMatchInlineSnapshot(snapshot?: string): void; 708 | toMatchInlineSnapshot(propertyMatchers?: any, snapshot?: string): void; 709 | /** 710 | * Use .toThrow to test that a function throws when it is called. 711 | * If you want to test that a specific error gets thrown, you can provide an 712 | * argument to toThrow. The argument can be a string for the error message, 713 | * a class for the error, or a regex that should match the error. 714 | * 715 | * Alias: .toThrowError 716 | */ 717 | toThrow(message?: string | Error | Class | RegExp): void; 718 | toThrowError(message?: string | Error | Class | RegExp): void; 719 | /** 720 | * Use .toThrowErrorMatchingSnapshot to test that a function throws a error 721 | * matching the most recent snapshot when it is called. 722 | */ 723 | toThrowErrorMatchingSnapshot(): void; 724 | toThrowErrorMatchingInlineSnapshot(snapshot?: string): void; 725 | } 726 | 727 | type JestObjectType = { 728 | /** 729 | * Disables automatic mocking in the module loader. 730 | * 731 | * After this method is called, all `require()`s will return the real 732 | * versions of each module (rather than a mocked version). 733 | */ 734 | disableAutomock(): JestObjectType, 735 | /** 736 | * An un-hoisted version of disableAutomock 737 | */ 738 | autoMockOff(): JestObjectType, 739 | /** 740 | * Enables automatic mocking in the module loader. 741 | */ 742 | enableAutomock(): JestObjectType, 743 | /** 744 | * An un-hoisted version of enableAutomock 745 | */ 746 | autoMockOn(): JestObjectType, 747 | /** 748 | * Clears the mock.calls and mock.instances properties of all mocks. 749 | * Equivalent to calling .mockClear() on every mocked function. 750 | */ 751 | clearAllMocks(): JestObjectType, 752 | /** 753 | * Resets the state of all mocks. Equivalent to calling .mockReset() on every 754 | * mocked function. 755 | */ 756 | resetAllMocks(): JestObjectType, 757 | /** 758 | * Restores all mocks back to their original value. 759 | */ 760 | restoreAllMocks(): JestObjectType, 761 | /** 762 | * Removes any pending timers from the timer system. 763 | */ 764 | clearAllTimers(): void, 765 | /** 766 | * Returns the number of fake timers still left to run. 767 | */ 768 | getTimerCount(): number, 769 | /** 770 | * The same as `mock` but not moved to the top of the expectation by 771 | * babel-jest. 772 | */ 773 | doMock(moduleName: string, moduleFactory?: any): JestObjectType, 774 | /** 775 | * The same as `unmock` but not moved to the top of the expectation by 776 | * babel-jest. 777 | */ 778 | dontMock(moduleName: string): JestObjectType, 779 | /** 780 | * Returns a new, unused mock function. Optionally takes a mock 781 | * implementation. 782 | */ 783 | fn, TReturn>( 784 | implementation?: (...args: TArguments) => TReturn 785 | ): JestMockFn, 786 | /** 787 | * Determines if the given function is a mocked function. 788 | */ 789 | isMockFunction(fn: Function): boolean, 790 | /** 791 | * Given the name of a module, use the automatic mocking system to generate a 792 | * mocked version of the module for you. 793 | */ 794 | genMockFromModule(moduleName: string): any, 795 | /** 796 | * Mocks a module with an auto-mocked version when it is being required. 797 | * 798 | * The second argument can be used to specify an explicit module factory that 799 | * is being run instead of using Jest's automocking feature. 800 | * 801 | * The third argument can be used to create virtual mocks -- mocks of modules 802 | * that don't exist anywhere in the system. 803 | */ 804 | mock( 805 | moduleName: string, 806 | moduleFactory?: any, 807 | options?: Object 808 | ): JestObjectType, 809 | /** 810 | * Returns the actual module instead of a mock, bypassing all checks on 811 | * whether the module should receive a mock implementation or not. 812 | */ 813 | requireActual(moduleName: string): any, 814 | /** 815 | * Returns a mock module instead of the actual module, bypassing all checks 816 | * on whether the module should be required normally or not. 817 | */ 818 | requireMock(moduleName: string): any, 819 | /** 820 | * Resets the module registry - the cache of all required modules. This is 821 | * useful to isolate modules where local state might conflict between tests. 822 | */ 823 | resetModules(): JestObjectType, 824 | /** 825 | * Creates a sandbox registry for the modules that are loaded inside the 826 | * callback function. This is useful to isolate specific modules for every 827 | * test so that local module state doesn't conflict between tests. 828 | */ 829 | isolateModules(fn: () => void): JestObjectType, 830 | /** 831 | * Exhausts the micro-task queue (usually interfaced in node via 832 | * process.nextTick). 833 | */ 834 | runAllTicks(): void, 835 | /** 836 | * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), 837 | * setInterval(), and setImmediate()). 838 | */ 839 | runAllTimers(): void, 840 | /** 841 | * Exhausts all tasks queued by setImmediate(). 842 | */ 843 | runAllImmediates(): void, 844 | /** 845 | * Executes only the macro task queue (i.e. all tasks queued by setTimeout() 846 | * or setInterval() and setImmediate()). 847 | */ 848 | advanceTimersByTime(msToRun: number): void, 849 | /** 850 | * Executes only the macro task queue (i.e. all tasks queued by setTimeout() 851 | * or setInterval() and setImmediate()). 852 | * 853 | * Renamed to `advanceTimersByTime`. 854 | */ 855 | runTimersToTime(msToRun: number): void, 856 | /** 857 | * Executes only the macro-tasks that are currently pending (i.e., only the 858 | * tasks that have been queued by setTimeout() or setInterval() up to this 859 | * point) 860 | */ 861 | runOnlyPendingTimers(): void, 862 | /** 863 | * Explicitly supplies the mock object that the module system should return 864 | * for the specified module. Note: It is recommended to use jest.mock() 865 | * instead. 866 | */ 867 | setMock(moduleName: string, moduleExports: any): JestObjectType, 868 | /** 869 | * Indicates that the module system should never return a mocked version of 870 | * the specified module from require() (e.g. that it should always return the 871 | * real module). 872 | */ 873 | unmock(moduleName: string): JestObjectType, 874 | /** 875 | * Instructs Jest to use fake versions of the standard timer functions 876 | * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, 877 | * setImmediate and clearImmediate). 878 | */ 879 | useFakeTimers(): JestObjectType, 880 | /** 881 | * Instructs Jest to use the real versions of the standard timer functions. 882 | */ 883 | useRealTimers(): JestObjectType, 884 | /** 885 | * Creates a mock function similar to jest.fn but also tracks calls to 886 | * object[methodName]. 887 | */ 888 | spyOn( 889 | object: Object, 890 | methodName: string, 891 | accessType?: 'get' | 'set' 892 | ): JestMockFn, 893 | /** 894 | * Set the default timeout interval for tests and before/after hooks in milliseconds. 895 | * Note: The default timeout interval is 5 seconds if this method is not called. 896 | */ 897 | setTimeout(timeout: number): JestObjectType, 898 | ... 899 | }; 900 | 901 | type JestSpyType = { calls: JestCallsType, ... }; 902 | 903 | type JestDoneFn = {| 904 | (): void, 905 | fail: (error: Error) => void, 906 | |}; 907 | 908 | /** Runs this function after every test inside this context */ 909 | declare function afterEach( 910 | fn: (done: JestDoneFn) => ?Promise, 911 | timeout?: number 912 | ): void; 913 | /** Runs this function before every test inside this context */ 914 | declare function beforeEach( 915 | fn: (done: JestDoneFn) => ?Promise, 916 | timeout?: number 917 | ): void; 918 | /** Runs this function after all tests have finished inside this context */ 919 | declare function afterAll( 920 | fn: (done: JestDoneFn) => ?Promise, 921 | timeout?: number 922 | ): void; 923 | /** Runs this function before any tests have started inside this context */ 924 | declare function beforeAll( 925 | fn: (done: JestDoneFn) => ?Promise, 926 | timeout?: number 927 | ): void; 928 | 929 | /** A context for grouping tests together */ 930 | declare var describe: { 931 | /** 932 | * Creates a block that groups together several related tests in one "test suite" 933 | */ 934 | (name: JestTestName, fn: () => void): void, 935 | /** 936 | * Only run this describe block 937 | */ 938 | only(name: JestTestName, fn: () => void): void, 939 | /** 940 | * Skip running this describe block 941 | */ 942 | skip(name: JestTestName, fn: () => void): void, 943 | /** 944 | * each runs this test against array of argument arrays per each run 945 | * 946 | * @param {table} table of Test 947 | */ 948 | each( 949 | ...table: Array | mixed> | [Array, string] 950 | ): ( 951 | name: JestTestName, 952 | fn?: (...args: Array) => ?Promise, 953 | timeout?: number 954 | ) => void, 955 | ... 956 | }; 957 | 958 | /** An individual test unit */ 959 | declare var it: { 960 | /** 961 | * An individual test unit 962 | * 963 | * @param {JestTestName} Name of Test 964 | * @param {Function} Test 965 | * @param {number} Timeout for the test, in milliseconds. 966 | */ 967 | ( 968 | name: JestTestName, 969 | fn?: (done: JestDoneFn) => ?Promise, 970 | timeout?: number 971 | ): void, 972 | /** 973 | * Only run this test 974 | * 975 | * @param {JestTestName} Name of Test 976 | * @param {Function} Test 977 | * @param {number} Timeout for the test, in milliseconds. 978 | */ 979 | only: {| 980 | ( 981 | name: JestTestName, 982 | fn?: (done: JestDoneFn) => ?Promise, 983 | timeout?: number 984 | ): void, 985 | each( 986 | ...table: Array | mixed> | [Array, string] 987 | ): ( 988 | name: JestTestName, 989 | fn?: (...args: Array) => ?Promise, 990 | timeout?: number 991 | ) => void 992 | |}, 993 | /** 994 | * Skip running this test 995 | * 996 | * @param {JestTestName} Name of Test 997 | * @param {Function} Test 998 | * @param {number} Timeout for the test, in milliseconds. 999 | */ 1000 | skip( 1001 | name: JestTestName, 1002 | fn?: (done: JestDoneFn) => ?Promise, 1003 | timeout?: number 1004 | ): void, 1005 | /** 1006 | * Highlight planned tests in the summary output 1007 | * 1008 | * @param {String} Name of Test to do 1009 | */ 1010 | todo(name: string): void, 1011 | /** 1012 | * Run the test concurrently 1013 | * 1014 | * @param {JestTestName} Name of Test 1015 | * @param {Function} Test 1016 | * @param {number} Timeout for the test, in milliseconds. 1017 | */ 1018 | concurrent( 1019 | name: JestTestName, 1020 | fn?: (done: JestDoneFn) => ?Promise, 1021 | timeout?: number 1022 | ): void, 1023 | /** 1024 | * each runs this test against array of argument arrays per each run 1025 | * 1026 | * @param {table} table of Test 1027 | */ 1028 | each( 1029 | ...table: Array | mixed> | [Array, string] 1030 | ): ( 1031 | name: JestTestName, 1032 | fn?: (...args: Array) => ?Promise, 1033 | timeout?: number 1034 | ) => void, 1035 | ... 1036 | }; 1037 | 1038 | declare function fit( 1039 | name: JestTestName, 1040 | fn: (done: JestDoneFn) => ?Promise, 1041 | timeout?: number 1042 | ): void; 1043 | /** An individual test unit */ 1044 | declare var test: typeof it; 1045 | /** A disabled group of tests */ 1046 | declare var xdescribe: typeof describe; 1047 | /** A focused group of tests */ 1048 | declare var fdescribe: typeof describe; 1049 | /** A disabled individual test */ 1050 | declare var xit: typeof it; 1051 | /** A disabled individual test */ 1052 | declare var xtest: typeof it; 1053 | 1054 | type JestPrettyFormatColors = { 1055 | comment: { 1056 | close: string, 1057 | open: string, 1058 | ... 1059 | }, 1060 | content: { 1061 | close: string, 1062 | open: string, 1063 | ... 1064 | }, 1065 | prop: { 1066 | close: string, 1067 | open: string, 1068 | ... 1069 | }, 1070 | tag: { 1071 | close: string, 1072 | open: string, 1073 | ... 1074 | }, 1075 | value: { 1076 | close: string, 1077 | open: string, 1078 | ... 1079 | }, 1080 | ... 1081 | }; 1082 | 1083 | type JestPrettyFormatIndent = string => string; 1084 | type JestPrettyFormatRefs = Array; 1085 | type JestPrettyFormatPrint = any => string; 1086 | type JestPrettyFormatStringOrNull = string | null; 1087 | 1088 | type JestPrettyFormatOptions = {| 1089 | callToJSON: boolean, 1090 | edgeSpacing: string, 1091 | escapeRegex: boolean, 1092 | highlight: boolean, 1093 | indent: number, 1094 | maxDepth: number, 1095 | min: boolean, 1096 | plugins: JestPrettyFormatPlugins, 1097 | printFunctionName: boolean, 1098 | spacing: string, 1099 | theme: {| 1100 | comment: string, 1101 | content: string, 1102 | prop: string, 1103 | tag: string, 1104 | value: string, 1105 | |}, 1106 | |}; 1107 | 1108 | type JestPrettyFormatPlugin = { 1109 | print: ( 1110 | val: any, 1111 | serialize: JestPrettyFormatPrint, 1112 | indent: JestPrettyFormatIndent, 1113 | opts: JestPrettyFormatOptions, 1114 | colors: JestPrettyFormatColors 1115 | ) => string, 1116 | test: any => boolean, 1117 | ... 1118 | }; 1119 | 1120 | type JestPrettyFormatPlugins = Array; 1121 | 1122 | /** The expect function is used every time you want to test a value */ 1123 | declare var expect: { 1124 | /** The object that you want to make assertions against */ 1125 | ( 1126 | value: any 1127 | ): JestExpectType & 1128 | JestPromiseType & 1129 | EnzymeMatchersType & 1130 | DomTestingLibraryType & 1131 | JestJQueryMatchersType & 1132 | JestStyledComponentsMatchersType & 1133 | JestExtendedMatchersType, 1134 | /** Add additional Jasmine matchers to Jest's roster */ 1135 | extend(matchers: { [name: string]: JestMatcher, ... }): void, 1136 | /** Add a module that formats application-specific data structures. */ 1137 | addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void, 1138 | assertions(expectedAssertions: number): void, 1139 | hasAssertions(): void, 1140 | any(value: mixed): JestAsymmetricEqualityType, 1141 | anything(): any, 1142 | arrayContaining(value: Array): Array, 1143 | objectContaining(value: Object): Object, 1144 | /** Matches any received string that contains the exact expected string. */ 1145 | stringContaining(value: string): string, 1146 | stringMatching(value: string | RegExp): string, 1147 | not: { 1148 | arrayContaining: (value: $ReadOnlyArray) => Array, 1149 | objectContaining: (value: {...}) => Object, 1150 | stringContaining: (value: string) => string, 1151 | stringMatching: (value: string | RegExp) => string, 1152 | ... 1153 | }, 1154 | ... 1155 | }; 1156 | 1157 | // TODO handle return type 1158 | // http://jasmine.github.io/2.4/introduction.html#section-Spies 1159 | declare function spyOn(value: mixed, method: string): Object; 1160 | 1161 | /** Holds all functions related to manipulating test runner */ 1162 | declare var jest: JestObjectType; 1163 | 1164 | /** 1165 | * The global Jasmine object, this is generally not exposed as the public API, 1166 | * using features inside here could break in later versions of Jest. 1167 | */ 1168 | declare var jasmine: { 1169 | DEFAULT_TIMEOUT_INTERVAL: number, 1170 | any(value: mixed): JestAsymmetricEqualityType, 1171 | anything(): any, 1172 | arrayContaining(value: Array): Array, 1173 | clock(): JestClockType, 1174 | createSpy(name: string): JestSpyType, 1175 | createSpyObj( 1176 | baseName: string, 1177 | methodNames: Array 1178 | ): { [methodName: string]: JestSpyType, ... }, 1179 | objectContaining(value: Object): Object, 1180 | stringMatching(value: string): string, 1181 | ... 1182 | }; 1183 | --------------------------------------------------------------------------------