├── .circleci └── config.yml ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .husky ├── _ │ └── husky.sh ├── post-commit └── pre-commit ├── .npmrc ├── .nvmrc ├── .prettierignore ├── CHANGELOG.md ├── eslint.config.mjs ├── jest-dynamodb-config.js ├── jest-preset.js ├── license ├── package.json ├── readme.md ├── renovate.json ├── src ├── environment.ts ├── index.ts ├── setup.ts ├── teardown.ts ├── types.ts └── utils │ ├── delete-tables.ts │ ├── get-config.ts │ ├── get-relevant-tables.ts │ └── wait-for-localhost.ts ├── tests ├── jest-preset-concurrent.test.ts └── jest-preset.test.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | parameters: 4 | node_docker_image: 5 | type: string 6 | default: cimg/node:22.14.0-browsers 7 | 8 | jobs: 9 | test_without_db: 10 | working_directory: ~/repo 11 | docker: 12 | - image: << parameters.node_docker_image >> 13 | steps: 14 | - checkout 15 | - run: yarn 16 | - run: yarn build 17 | - run: yarn test 18 | 19 | test_with_db: 20 | working_directory: ~/repo 21 | docker: 22 | - image: << parameters.node_docker_image >> 23 | - image: circleci/dynamodb 24 | steps: 25 | - checkout 26 | - run: yarn 27 | - run: yarn build 28 | - run: yarn test 29 | 30 | workflows: 31 | version: 2 32 | build_and_test: 33 | jobs: 34 | - test_with_db 35 | - test_without_db 36 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | coverage/ 3 | node_modules/ 4 | lib/ 5 | temp 6 | yarn.lock 7 | *.log 8 | .DS_Store 9 | dist/ 10 | !.husky/_/husky.sh 11 | -------------------------------------------------------------------------------- /.husky/_/husky.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -z "$husky_skip_init" ]; then 3 | debug () { 4 | if [ "$HUSKY_DEBUG" = "1" ]; then 5 | echo "husky (debug) - $1" 6 | fi 7 | } 8 | 9 | readonly hook_name="$(basename "$0")" 10 | debug "starting $hook_name..." 11 | 12 | if [ "$HUSKY" = "0" ]; then 13 | debug "HUSKY env variable is set to 0, skipping hook" 14 | exit 0 15 | fi 16 | 17 | if [ -f ~/.huskyrc ]; then 18 | debug "sourcing ~/.huskyrc" 19 | . ~/.huskyrc 20 | fi 21 | 22 | export readonly husky_skip_init=1 23 | sh -e "$0" "$@" 24 | exitCode="$?" 25 | 26 | if [ $exitCode != 0 ]; then 27 | echo "husky - $hook_name hook exited with code $exitCode (error)" 28 | fi 29 | 30 | exit $exitCode 31 | fi 32 | -------------------------------------------------------------------------------- /.husky/post-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | git update-index --again 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | yarn lint-staged 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.13.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Breaking Changes 2 | 3 | ## 4.0.0 4 | 5 | - Switched `node` version `18`->`22` 6 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import rules from '@shelf/eslint-config/typescript.js'; 2 | 3 | export default [ 4 | ...rules, 5 | {files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx', '**/*.json']}, 6 | { 7 | ignores: [ 8 | '.idea/', 9 | 'coverage/', 10 | 'draft.js', 11 | 'lib/', 12 | 'dist/', 13 | 'node_modules/', 14 | 'packages/**/tsconfig.types.json', 15 | 'packages/**/node_modules/**', 16 | 'packages/**/lib/**', 17 | 'renovate.json', 18 | ], 19 | }, 20 | ]; 21 | -------------------------------------------------------------------------------- /jest-dynamodb-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('./lib/types').Config} 3 | */ 4 | const config = { 5 | tables: [ 6 | { 7 | TableName: `files`, 8 | KeySchema: [{AttributeName: 'id', KeyType: 'HASH'}], 9 | AttributeDefinitions: [{AttributeName: 'id', AttributeType: 'S'}], 10 | ProvisionedThroughput: {ReadCapacityUnits: 1, WriteCapacityUnits: 1}, 11 | }, 12 | { 13 | TableName: `users`, 14 | KeySchema: [{AttributeName: 'id', KeyType: 'HASH'}], 15 | AttributeDefinitions: [{AttributeName: 'id', AttributeType: 'S'}], 16 | ProvisionedThroughput: {ReadCapacityUnits: 1, WriteCapacityUnits: 1}, 17 | }, 18 | ], 19 | port: 8000, 20 | options: ['-sharedDb'], 21 | }; 22 | 23 | module.exports = config; 24 | -------------------------------------------------------------------------------- /jest-preset.js: -------------------------------------------------------------------------------- 1 | const preset = require('./lib').default; 2 | 3 | module.exports = preset; 4 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Gemshelf Inc. (shelf.io) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shelf/jest-dynamodb", 3 | "version": "4.0.0", 4 | "description": "Run your tests using Jest & DynamoDB local", 5 | "keywords": [ 6 | "dynamodb", 7 | "dynamodb local", 8 | "jest", 9 | "jest environment", 10 | "jest preset" 11 | ], 12 | "repository": "shelfio/jest-dynamodb", 13 | "license": "MIT", 14 | "author": { 15 | "name": "Vlad Holubiev", 16 | "email": "vlad@shelf.io", 17 | "url": "shelf.io" 18 | }, 19 | "files": [ 20 | "jest-preset.js", 21 | "lib/" 22 | ], 23 | "scripts": { 24 | "build": "rm -rf lib/ && yarn build:types && babel src --out-dir lib --ignore '**/*.test.ts' --extensions '.ts'", 25 | "build:types": "tsc --emitDeclarationOnly --declaration --isolatedModules false --declarationDir lib", 26 | "coverage": "jest --coverage", 27 | "lint": "yarn lint:ci --fix", 28 | "lint:ci": "eslint . --quiet", 29 | "prepack": "yarn build", 30 | "test": "export ENVIRONMENT=local && jest tests", 31 | "type-check": "tsc --noEmit", 32 | "type-check:watch": "npm run type-check -- --watch" 33 | }, 34 | "lint-staged": { 35 | "*.{html,md,yml}": [ 36 | "prettier --write" 37 | ], 38 | "*.{ts,js,json}": [ 39 | "eslint --fix" 40 | ] 41 | }, 42 | "babel": { 43 | "extends": "@shelf/babel-config/backend" 44 | }, 45 | "prettier": "@shelf/prettier-config", 46 | "jest": { 47 | "preset": "./jest-preset.js" 48 | }, 49 | "dependencies": { 50 | "@aws-sdk/client-dynamodb": "3.624.0", 51 | "@aws-sdk/lib-dynamodb": "3.624.0", 52 | "@aws-sdk/util-dynamodb": "3.624.0", 53 | "cwd": "0.10.0", 54 | "debug": "4.3.4", 55 | "dynamodb-local": "0.0.34" 56 | }, 57 | "devDependencies": { 58 | "@babel/cli": "7.27.0", 59 | "@babel/core": "7.26.10", 60 | "@shelf/babel-config": "1.2.0", 61 | "@shelf/eslint-config": "4.2.1", 62 | "@shelf/prettier-config": "1.0.0", 63 | "@shelf/tsconfig": "0.1.0", 64 | "@types/aws-sdk": "2.7.4", 65 | "@types/cwd": "^0.10.2", 66 | "@types/jest": "29.5.14", 67 | "@types/node": "22", 68 | "eslint": "9.25.1", 69 | "husky": "8.0.3", 70 | "jest": "29.7.0", 71 | "lint-staged": "13.3.0", 72 | "prettier": "3.5.3", 73 | "typescript": "5.8.3" 74 | }, 75 | "engines": { 76 | "node": ">=22" 77 | }, 78 | "publishConfig": { 79 | "access": "public" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # jest-dynamodb [![CircleCI](https://circleci.com/gh/shelfio/jest-dynamodb/tree/master.svg?style=svg)](https://circleci.com/gh/shelfio/jest-dynamodb/tree/master) ![](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) [![npm (scoped)](https://img.shields.io/npm/v/@shelf/jest-dynamodb.svg)](https://www.npmjs.com/package/@shelf/jest-dynamodb) 2 | 3 | > Jest preset to run DynamoDB Local 4 | 5 | ## Usage 6 | 7 | ### 0. Install 8 | 9 | ``` 10 | $ yarn add @shelf/jest-dynamodb --dev 11 | ``` 12 | 13 | Make sure `java` runtime available for running [DynamoDBLocal.jar](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html) 14 | 15 | ### 1. Create `jest.config.js` 16 | 17 | ```js 18 | module.exports = { 19 | preset: '@shelf/jest-dynamodb', 20 | }; 21 | ``` 22 | 23 | ### 2. Create `jest-dynamodb-config.js` 24 | 25 | #### 2.1 Properties 26 | 27 | ##### tables 28 | 29 | - Type: `object[]` 30 | - Required: `true` 31 | 32 | Array of createTable params. 33 | 34 | - [Create Table API](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#createTable-property). 35 | 36 | ##### port 37 | 38 | - Type: `number` 39 | - Required: `false` 40 | 41 | Port number. The default port number is `8000`. 42 | 43 | ##### hostname 44 | 45 | - Type: `string` 46 | - Required: `false` 47 | 48 | Hostname. The default hostname is `localhost`. 49 | 50 | ##### options 51 | 52 | - Type: `string[]` 53 | - Required: `false` 54 | 55 | Additional arguments for dynamodb-local. The default value is `['-sharedDb']`. 56 | 57 | - [dynamodb-local](https://github.com/rynop/dynamodb-local) 58 | - [DynamoDB Local Usage Notes](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.UsageNotes.html) 59 | 60 | ##### clientConfig 61 | 62 | - Type: `object` 63 | - Required: `false` 64 | 65 | Constructor params of DynamoDB client. 66 | 67 | - [Constructor Property](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#constructor-property) 68 | 69 | ##### installerConfig 70 | 71 | - Type: `{installPath?: string, downloadUrl?: string}` 72 | - Required: `false` 73 | 74 | - `installPath` defines the location where dynamodb-local is installed or will be installed. 75 | - `downloadUrl` defines the url of dynamodb-local package. 76 | 77 | The default value is defined at https://github.com/rynop/dynamodb-local/blob/2e6c1cb2edde4de0dc51a71c193c510b939d4352/index.js#L16-L19 78 | 79 | #### 2.2 Examples 80 | 81 | You can set up tables as an object: 82 | > Whole list of config properties can be found [here](https://github.com/shelfio/jest-dynamodb/blob/6c64dbd4ee5a68230469ea14cbfb814470521197/src/types.ts#L80-L87) 83 | ```js 84 | /** 85 | * @type {import('@shelf/jest-dynamodb/lib').Config}')} 86 | */ 87 | const config = { 88 | tables: [ 89 | { 90 | TableName: `files`, 91 | KeySchema: [{AttributeName: 'id', KeyType: 'HASH'}], 92 | AttributeDefinitions: [{AttributeName: 'id', AttributeType: 'S'}], 93 | ProvisionedThroughput: {ReadCapacityUnits: 1, WriteCapacityUnits: 1}, 94 | }, 95 | // etc 96 | ], 97 | port: 8000, 98 | }; 99 | module.exports = config; 100 | ``` 101 | 102 | Or as an async function (particularly useful when resolving DynamoDB setup dynamically from `serverless.yml`): 103 | 104 | ```js 105 | module.exports = async () => { 106 | const serverless = new (require('serverless'))(); 107 | // If using monorepo where DynamoDB serverless.yml is in another directory 108 | // const serverless = new (require('serverless'))({ servicePath: '../../../core/data' }); 109 | 110 | await serverless.init(); 111 | const service = await serverless.variables.populateService(); 112 | const resources = service.resources.filter(r => Object.keys(r).includes('Resources'))[0]; 113 | 114 | const tables = Object.keys(resources) 115 | .map(name => resources[name]) 116 | .filter(r => r.Type === 'AWS::DynamoDB::Table') 117 | .map(r => r.Properties); 118 | 119 | return { 120 | tables, 121 | port: 8000, 122 | }; 123 | }; 124 | ``` 125 | 126 | Or read table definitions from a CloudFormation template (example handles a !Sub on TableName, i.e. TableName: !Sub "\${env}-users" ): 127 | 128 | ```js 129 | const yaml = require('js-yaml'); 130 | const fs = require('fs'); 131 | const {CLOUDFORMATION_SCHEMA} = require('cloudformation-js-yaml-schema'); 132 | 133 | module.exports = async () => { 134 | const cf = yaml.load(fs.readFileSync('../cf-templates/example-stack.yaml', 'utf8'), { 135 | schema: CLOUDFORMATION_SCHEMA, 136 | }); 137 | var tables = []; 138 | Object.keys(cf.Resources).forEach(item => { 139 | tables.push(cf.Resources[item]); 140 | }); 141 | 142 | tables = tables 143 | .filter(r => r.Type === 'AWS::DynamoDB::Table') 144 | .map(r => { 145 | let table = r.Properties; 146 | if (typeof r.TableName === 'object') { 147 | table.TableName = table.TableName.data.replace('${env}', 'test'); 148 | } 149 | delete table.TimeToLiveSpecification; //errors on dynamo-local 150 | return table; 151 | }); 152 | 153 | return { 154 | tables, 155 | port: 8000, 156 | }; 157 | }; 158 | ``` 159 | 160 | ### 3.1 Configure DynamoDB client (from aws-sdk v2) 161 | 162 | ```js 163 | const {DocumentClient} = require('aws-sdk/clients/dynamodb'); 164 | 165 | const isTest = process.env.JEST_WORKER_ID; 166 | const config = { 167 | convertEmptyValues: true, 168 | ...(isTest && { 169 | endpoint: 'localhost:8000', 170 | sslEnabled: false, 171 | region: 'local-env', 172 | credentials: { 173 | accessKeyId: 'fakeMyKeyId', 174 | secretAccessKey: 'fakeSecretAccessKey', 175 | }, 176 | }), 177 | }; 178 | 179 | const ddb = new DocumentClient(config); 180 | ``` 181 | 182 | ### 3.2 Configure DynamoDB client (from aws-sdk v3) 183 | 184 | ```js 185 | const {DynamoDB} = require('@aws-sdk/client-dynamodb'); 186 | const {DynamoDBDocument} = require('@aws-sdk/lib-dynamodb'); 187 | 188 | const isTest = process.env.JEST_WORKER_ID; 189 | 190 | const ddb = DynamoDBDocument.from( 191 | new DynamoDB({ 192 | ...(isTest && { 193 | endpoint: 'http://localhost:8000', 194 | region: 'local-env', 195 | credentials: { 196 | accessKeyId: 'fakeMyKeyId', 197 | secretAccessKey: 'fakeSecretAccessKey', 198 | }, 199 | }), 200 | }), 201 | { 202 | marshallOptions: { 203 | convertEmptyValues: true, 204 | }, 205 | } 206 | ); 207 | ``` 208 | 209 | ### 4. PROFIT! Write tests 210 | 211 | ```js 212 | it('should insert item into table', async () => { 213 | await ddb.put({TableName: 'files', Item: {id: '1', hello: 'world'}}).promise(); 214 | 215 | const {Item} = await ddb.get({TableName: 'files', Key: {id: '1'}}).promise(); 216 | 217 | expect(Item).toEqual({ 218 | id: '1', 219 | hello: 'world', 220 | }); 221 | }); 222 | ``` 223 | 224 | ## Monorepo Support 225 | 226 | By default the `jest-dynamodb-config.js` is read from `cwd` directory, but this might not be suitable for monorepos with nested [jest projects](https://jestjs.io/docs/configuration#projects-arraystring--projectconfig) with nested `jest.config.*` files nested in subdirectories. 227 | 228 | If your `jest-dynamodb-config.js` file is not located at `{cwd}/jest-dynamodb-config.js` or you are using nested `jest projects`, you can define the environment variable `JEST_DYNAMODB_CONFIG` with the absolute path of the respective `jest-dynamodb-config.js` file. 229 | 230 | ### Example Using `JEST_DYNAMODB_CONFIG` in nested project 231 | 232 | ``` 233 | // src/nested/project/jest.config.js 234 | const path = require('path'); 235 | 236 | // Define path of project level config - extension not required as file will be imported via `require(process.env.JEST_DYNAMODB_CONFIG)` 237 | process.env.JEST_DYNAMODB_CONFIG = path.resolve(__dirname, './jest-dynamodb-config'); 238 | 239 | module.exports = { 240 | preset: '@shelf/jest-dynamodb' 241 | displayName: 'nested-project', 242 | }; 243 | ``` 244 | 245 | ## Troubleshooting 246 | 247 |
248 | UnknownError: Not Found 249 | 250 | Perhaps something is using your port specified in `jest-dynamodb-config.js`. 251 | 252 |
253 | 254 |
255 | com.almworks.sqlite4java.Internal log WARNING: [sqlite] cannot open DB[1]: 256 | 257 | See https://www.josephso.dev/using-jest-dynamodb-in-apple-silicon-platform-workaround/#community-build 258 | 259 |
260 | 261 | ## Alternatives 262 | 263 | - [jest-dynalite](https://github.com/freshollie/jest-dynalite) - a much lighter version which spins up an instance for each runner & doesn't depend on Java 264 | 265 | ## Read 266 | 267 | - [dynamodb-local](https://github.com/rynop/dynamodb-local) 268 | - [DynamoDB Local Usage Notes](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.UsageNotes.html) 269 | - [Create Table API](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#createTable-property) 270 | 271 | ## Used by 272 | 273 | - [dynamodb-parallel-scan](https://github.com/shelfio/dynamodb-parallel-scan) 274 | - [@nasa-gcn/dynamodb-autoincrement](https://github.com/nasa-gcn/dynamodb-autoincrement) 275 | 276 | ## See Also 277 | 278 | - [jest-mongodb](https://github.com/shelfio/jest-mongodb) 279 | 280 | ## Publish 281 | 282 | ```sh 283 | $ git checkout master 284 | $ yarn version 285 | $ yarn publish 286 | $ git push origin master --tags 287 | ``` 288 | 289 | ## License 290 | 291 | MIT © [Shelf](https://shelf.io) 292 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>shelfio/renovate-config-public" 4 | ], 5 | "labels": [ 6 | "backend" 7 | ], 8 | "ignoreDeps": [ 9 | "cimg/node" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/environment.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import {TestEnvironment} from 'jest-environment-node'; 3 | import type {EnvironmentContext} from '@jest/environment'; 4 | import type {JestEnvironmentConfig} from '@jest/environment'; 5 | 6 | const debug = require('debug')('jest-dynamodb'); 7 | 8 | module.exports = class DynamoDBEnvironment extends TestEnvironment { 9 | constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { 10 | super(config, context); 11 | } 12 | 13 | async setup() { 14 | debug('Setup DynamoDB Test Environment'); 15 | 16 | await super.setup(); 17 | } 18 | 19 | async teardown() { 20 | debug('Teardown DynamoDB Test Environment'); 21 | 22 | await super.teardown(); 23 | } 24 | 25 | // @ts-ignore 26 | runScript(script) { 27 | // @ts-ignore 28 | return super.runScript(script); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path'; 2 | 3 | export * from './types'; 4 | 5 | export default { 6 | globalSetup: resolve(__dirname, './setup.js'), 7 | globalTeardown: resolve(__dirname, './teardown.js'), 8 | testEnvironment: resolve(__dirname, './environment.js'), 9 | }; 10 | -------------------------------------------------------------------------------- /src/setup.ts: -------------------------------------------------------------------------------- 1 | import DynamoDbLocal from 'dynamodb-local'; 2 | import {DynamoDB} from '@aws-sdk/client-dynamodb'; 3 | import type {ListTablesCommandOutput} from '@aws-sdk/client-dynamodb/dist-types/commands/ListTablesCommand'; 4 | import type {argValues} from 'dynamodb-local'; 5 | import type {CreateTableCommandInput} from '@aws-sdk/client-dynamodb'; 6 | import getConfig from './utils/get-config'; 7 | import deleteTables from './utils/delete-tables'; 8 | import waitForLocalhost from './utils/wait-for-localhost'; 9 | import getRelevantTables from './utils/get-relevant-tables'; 10 | 11 | const debug = require('debug')('jest-dynamodb'); 12 | 13 | const DEFAULT_PORT = 8000; 14 | const DEFAULT_HOST = 'localhost'; 15 | const DEFAULT_OPTIONS: argValues[] = ['-sharedDb']; 16 | 17 | export default async function () { 18 | const { 19 | tables: newTables, 20 | clientConfig, 21 | installerConfig, 22 | port: port = DEFAULT_PORT, 23 | hostname: hostname = DEFAULT_HOST, 24 | options: options = DEFAULT_OPTIONS, 25 | } = await getConfig(debug); 26 | 27 | const dynamoDB = new DynamoDB({ 28 | endpoint: `http://${hostname}:${port}`, 29 | tls: false, 30 | region: 'local-env', 31 | credentials: { 32 | accessKeyId: 'fakeMyKeyId', 33 | secretAccessKey: 'fakeSecretAccessKey', 34 | }, 35 | ...clientConfig, 36 | }); 37 | 38 | global.__DYNAMODB_CLIENT__ = dynamoDB; 39 | 40 | try { 41 | const promises: (Promise | Promise)[] = [ 42 | dynamoDB.listTables({}), 43 | ]; 44 | 45 | if (!global.__DYNAMODB__) { 46 | promises.push(waitForLocalhost(port, hostname)); 47 | } 48 | 49 | const [TablesList] = await Promise.all(promises); 50 | const tableNames = TablesList?.TableNames; 51 | 52 | if (tableNames) { 53 | await deleteTables(dynamoDB, getRelevantTables(tableNames, newTables)); 54 | } 55 | } catch (err) { 56 | // eslint-disable-next-line no-console 57 | debug(`fallback to launch DB due to ${err}`); 58 | 59 | if (installerConfig) { 60 | DynamoDbLocal.configureInstaller(installerConfig); 61 | } 62 | 63 | if (!global.__DYNAMODB__) { 64 | debug('spinning up a local ddb instance'); 65 | 66 | global.__DYNAMODB__ = await DynamoDbLocal.launch(port, null, options); 67 | debug(`dynamodb-local started on port ${port}`); 68 | 69 | await waitForLocalhost(port, hostname); 70 | } 71 | } 72 | debug(`dynamodb-local is ready on port ${port}`); 73 | 74 | await createTables(dynamoDB, newTables); 75 | } 76 | 77 | function createTables(dynamoDB: DynamoDB, tables: CreateTableCommandInput[]) { 78 | return Promise.all(tables.map(table => dynamoDB.createTable(table))); 79 | } 80 | -------------------------------------------------------------------------------- /src/teardown.ts: -------------------------------------------------------------------------------- 1 | import DynamoDbLocal from 'dynamodb-local'; 2 | import type {JestArgs} from './types'; 3 | import deleteTables from './utils/delete-tables'; 4 | import getConfig from './utils/get-config'; 5 | import getRelevantTables from './utils/get-relevant-tables'; 6 | 7 | const debug = require('debug')('jest-dynamodb'); 8 | 9 | export default async function (jestArgs: JestArgs) { 10 | // eslint-disable-next-line no-console 11 | debug('Teardown DynamoDB'); 12 | 13 | if (global.__DYNAMODB__) { 14 | const watching = jestArgs.watch || jestArgs.watchAll; 15 | 16 | if (!watching) { 17 | await DynamoDbLocal.stopChild(global.__DYNAMODB__); 18 | } 19 | } else { 20 | const dynamoDB = global.__DYNAMODB_CLIENT__; 21 | const {tables: targetTables} = await getConfig(debug); 22 | 23 | const {TableNames: tableNames} = await dynamoDB.listTables({}); 24 | 25 | if (tableNames?.length) { 26 | await deleteTables(dynamoDB, getRelevantTables(tableNames, targetTables)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type {DynamoDB} from '@aws-sdk/client-dynamodb'; 3 | import type {CreateTableCommandInput} from '@aws-sdk/client-dynamodb'; 4 | import type {DynamoDBClientConfig} from '@aws-sdk/client-dynamodb'; 5 | import type {ChildProcess} from 'child_process'; 6 | import type {InstallerConfig} from 'dynamodb-local'; 7 | import type {argValues} from 'dynamodb-local'; 8 | 9 | declare global { 10 | var __DYNAMODB_CLIENT__: DynamoDB; 11 | var __DYNAMODB__: ChildProcess; 12 | } 13 | 14 | export type JestArgs = { 15 | bail: number; 16 | changedSince?: string; 17 | changedFilesWithAncestor: boolean; 18 | ci: boolean; 19 | collectCoverage: boolean; 20 | collectCoverageFrom: Array; 21 | collectCoverageOnlyFrom?: { 22 | [key: string]: boolean; 23 | }; 24 | coverageDirectory: string; 25 | coveragePathIgnorePatterns?: Array; 26 | coverageProvider: object; 27 | coverageReporters: object; 28 | coverageThreshold?: object; 29 | detectLeaks: boolean; 30 | detectOpenHandles: boolean; 31 | expand: boolean; 32 | filter?: string; 33 | findRelatedTests: boolean; 34 | forceExit: boolean; 35 | json: boolean; 36 | globalSetup?: string; 37 | globalTeardown?: string; 38 | lastCommit: boolean; 39 | logHeapUsage: boolean; 40 | listTests: boolean; 41 | maxConcurrency: number; 42 | maxWorkers: number; 43 | noStackTrace: boolean; 44 | nonFlagArgs: Array; 45 | noSCM?: boolean; 46 | notify: boolean; 47 | notifyMode: object; 48 | outputFile?: string; 49 | onlyChanged: boolean; 50 | onlyFailures: boolean; 51 | passWithNoTests: boolean; 52 | projects: Array; 53 | replname?: string; 54 | reporters?: Array; 55 | runTestsByPath: boolean; 56 | rootDir: string; 57 | shard?: object; 58 | silent?: boolean; 59 | skipFilter: boolean; 60 | snapshotFormat: object; 61 | errorOnDeprecated: boolean; 62 | testFailureExitCode: number; 63 | testNamePattern?: string; 64 | testPathPattern: string; 65 | testResultsProcessor?: string; 66 | testSequencer: string; 67 | testTimeout?: number; 68 | updateSnapshot: object; 69 | useStderr: boolean; 70 | verbose?: boolean; 71 | watch: boolean; 72 | watchAll: boolean; 73 | watchman: boolean; 74 | watchPlugins?: Array<{ 75 | path: string; 76 | config: Record; 77 | }> | null; 78 | }; 79 | 80 | export type Config = { 81 | tables: CreateTableCommandInput[]; 82 | clientConfig?: DynamoDBClientConfig; 83 | installerConfig?: InstallerConfig; 84 | port?: number; 85 | hostname?: string; 86 | options?: argValues[]; 87 | }; 88 | -------------------------------------------------------------------------------- /src/utils/delete-tables.ts: -------------------------------------------------------------------------------- 1 | import type {DynamoDB} from '@aws-sdk/client-dynamodb'; 2 | 3 | export default function deleteTables(dynamoDB: DynamoDB, tableNames: string[]) { 4 | return Promise.all(tableNames.map(tableName => dynamoDB.deleteTable({TableName: tableName}))); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/get-config.ts: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path'; 2 | import cwd from 'cwd'; 3 | import type {Config} from '../types'; 4 | 5 | export default async function getConfig(debug: any): Promise { 6 | const path = process.env.JEST_DYNAMODB_CONFIG || resolve(cwd(), 'jest-dynamodb-config.js'); 7 | const config = require(path); 8 | debug('config:', config); 9 | 10 | return typeof config === 'function' ? await config() : config; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/get-relevant-tables.ts: -------------------------------------------------------------------------------- 1 | import type {CreateTableCommandInput} from '@aws-sdk/client-dynamodb'; 2 | 3 | export default function getRelevantTables( 4 | dbTables: string[], 5 | configTables: CreateTableCommandInput[] 6 | ) { 7 | const configTableNames = configTables.map(configTable => configTable.TableName); 8 | 9 | return dbTables.filter(dbTable => configTableNames.includes(dbTable)); 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/wait-for-localhost.ts: -------------------------------------------------------------------------------- 1 | // 2 | // This is copied from https://github.com/sindresorhus/wait-for-localhost/blob/v3.3.0/index.js 3 | // With 1 change 4 | // We rely on status code 400 instead of 200 to ensure local DDB is up and running 5 | // 6 | 7 | const http = require('http'); 8 | 9 | export default function waitForLocalhost(port: number, host: string): Promise { 10 | return new Promise(resolve => { 11 | const retry = () => setTimeout(main, 200); 12 | const main = () => { 13 | const request = http.request( 14 | {method: 'GET', port, host, path: '/'}, 15 | (response: {statusCode: number}) => { 16 | if (response.statusCode === 400) { 17 | return resolve(); 18 | } 19 | 20 | retry(); 21 | } 22 | ); 23 | 24 | request.on('error', retry); 25 | request.end(); 26 | }; 27 | main(); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /tests/jest-preset-concurrent.test.ts: -------------------------------------------------------------------------------- 1 | import {DynamoDB} from '@aws-sdk/client-dynamodb'; 2 | import {DynamoDBDocument} from '@aws-sdk/lib-dynamodb'; 3 | 4 | const ddb = DynamoDBDocument.from( 5 | new DynamoDB({ 6 | endpoint: 'http://localhost:8000', 7 | tls: false, 8 | region: 'local-env', 9 | credentials: { 10 | accessKeyId: 'fakeMyKeyId', 11 | secretAccessKey: 'fakeSecretAccessKey', 12 | }, 13 | }), 14 | { 15 | marshallOptions: { 16 | convertEmptyValues: true, 17 | }, 18 | } 19 | ); 20 | 21 | it('should insert item into another table concurrently', async () => { 22 | await ddb.put({TableName: 'users', Item: {id: '1', hello: 'world'}}); 23 | 24 | const {Item} = await ddb.get({TableName: 'users', Key: {id: '1'}}); 25 | 26 | expect(Item).toEqual({ 27 | id: '1', 28 | hello: 'world', 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/jest-preset.test.ts: -------------------------------------------------------------------------------- 1 | import {DynamoDB} from '@aws-sdk/client-dynamodb'; 2 | import {DynamoDBDocument} from '@aws-sdk/lib-dynamodb'; 3 | 4 | const ddb = DynamoDBDocument.from( 5 | new DynamoDB({ 6 | endpoint: 'http://localhost:8000', 7 | tls: false, 8 | region: 'local-env', 9 | credentials: { 10 | accessKeyId: 'fakeMyKeyId', 11 | secretAccessKey: 'fakeSecretAccessKey', 12 | }, 13 | }), 14 | { 15 | marshallOptions: { 16 | convertEmptyValues: true, 17 | }, 18 | } 19 | ); 20 | 21 | it('should insert item into table', async () => { 22 | await ddb.put({TableName: 'files', Item: {id: '1', hello: 'world'}}); 23 | 24 | const {Item} = await ddb.get({TableName: 'files', Key: {id: '1'}}); 25 | 26 | expect(Item).toEqual({ 27 | id: '1', 28 | hello: 'world', 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@shelf/tsconfig/backend", 3 | "compilerOptions": { 4 | "strict": true 5 | }, 6 | "exclude": ["node_modules"], 7 | "include": ["src"] 8 | } 9 | --------------------------------------------------------------------------------