diff --git a/.circleci/config.yml b/.circleci/config.yml index 315a2d6..dc9e962 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,6 +9,7 @@ jobs: steps: - checkout - run: yarn + - run: yarn build - run: yarn test test_with_db: @@ -20,6 +21,7 @@ jobs: steps: - checkout - run: yarn + - run: yarn build - run: yarn test workflows: diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..9a9e7d1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +lib/ +renovate.json +tsconfig.json diff --git a/.gitignore b/.gitignore index 93a5202..8069a86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .idea/ coverage/ node_modules/ +lib/ temp yarn.lock *.log diff --git a/jest-dynamodb-config.js b/jest-dynamodb-config.js index 7320829..bde9d31 100644 --- a/jest-dynamodb-config.js +++ b/jest-dynamodb-config.js @@ -1,4 +1,7 @@ -module.exports = { +/** + * @type {import('./lib/types').Config} + */ +const config = { tables: [ { TableName: `files`, @@ -16,3 +19,5 @@ module.exports = { port: 8000, options: ['-sharedDb'], }; + +module.exports = config; diff --git a/jest-preset.js b/jest-preset.js index 6e86078..5102ee9 100644 --- a/jest-preset.js +++ b/jest-preset.js @@ -1,7 +1,3 @@ -const {resolve} = require('path'); +const preset = require('./lib'); -module.exports = { - globalSetup: resolve(__dirname, './setup.js'), - globalTeardown: resolve(__dirname, './teardown.js'), - testEnvironment: resolve(__dirname, './environment.js'), -}; +module.exports = preset; diff --git a/package.json b/package.json index 6b2acba..10982ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shelf/jest-dynamodb", - "version": "3.2.0", + "version": "3.3.0", "description": "Run your tests using Jest & DynamoDB local", "keywords": [ "dynamodb", @@ -17,23 +17,25 @@ "url": "shelf.io" }, "files": [ - "environment.js", "jest-preset.js", - "setup.js", - "teardown.js", - "wait-for-localhost.js" + "lib/" ], "scripts": { + "build": "rm -rf lib/ && yarn build:types && babel src --out-dir lib --ignore '**/*.test.ts' --extensions '.ts'", + "build:types": "tsc --emitDeclarationOnly --declaration --isolatedModules false --declarationDir lib", "coverage": "jest --coverage", "lint": "eslint . --ext .js,.ts,.json --fix", "lint:ci": "eslint . --ext .js,.ts,.json", - "test": "export ENVIRONMENT=local && jest ." + "prepack": "yarn build", + "test": "export ENVIRONMENT=local && jest tests", + "type-check": "tsc --noEmit", + "type-check:watch": "npm run type-check -- --watch" }, "lint-staged": { "*.{html,md,yml}": [ "prettier --write" ], - "*.{js,json}": [ + "*.{ts,js,json}": [ "eslint --fix" ] }, @@ -53,10 +55,17 @@ "dynamodb-local": "0.0.31" }, "devDependencies": { - "@shelf/babel-config": "1.0.2", - "@shelf/eslint-config": "2.18.0", + "@babel/cli": "7.18.9", + "@babel/core": "7.18.9", + "@shelf/babel-config": "1.2.0", + "@shelf/eslint-config": "2.22.0", "@shelf/prettier-config": "1.0.0", - "eslint": "8.20.0", + "@shelf/tsconfig": "0.0.8", + "@types/aws-sdk": "2.7.0", + "@types/cwd": "^0.10.0", + "@types/jest": "28.1.3", + "@types/node": "16", + "eslint": "8.21.0", "husky": "8.0.1", "jest": "28.1.3", "lint-staged": "13.0.3", diff --git a/readme.md b/readme.md index 124e282..18cadc2 100644 --- a/readme.md +++ b/readme.md @@ -40,6 +40,13 @@ Array of createTable params. Port number. The default port number is `8000`. +##### hostname + +- Type: `string` +- Required: `false` + +Port number. The default hostname number is `localhost`. + ##### options - Type: `string[]` @@ -72,9 +79,12 @@ The default value is defined at https://github.com/rynop/dynamodb-local/blob/2e6 #### 2.2 Examples You can set up tables as an object: - + > Whole list of config properties can be found [here](https://github.com/shelfio/jest-dynamodb/blob/6c64dbd4ee5a68230469ea14cbfb814470521197/src/types.ts#L80-L87) ```js -module.exports = { +/** + * @type {import('@shelf/jest-dynamodb/lib').Config}')} + */ +const config = { tables: [ { TableName: `files`, @@ -86,6 +96,7 @@ module.exports = { ], port: 8000, }; +module.exports = config; ``` Or as an async function (particularly useful when resolving DynamoDB setup dynamically from `serverless.yml`): diff --git a/setup.js b/setup.js deleted file mode 100644 index 5afe34e..0000000 --- a/setup.js +++ /dev/null @@ -1,73 +0,0 @@ -const {resolve} = require('path'); -const cwd = require('cwd'); -const {DynamoDB} = require('@aws-sdk/client-dynamodb'); -const DynamoDbLocal = require('dynamodb-local'); -const debug = require('debug')('jest-dynamodb'); -const waitForLocalhost = require('./wait-for-localhost'); - -const DEFAULT_PORT = 8000; -const DEFAULT_OPTIONS = ['-sharedDb']; - -module.exports = async function () { - const config = require(process.env.JEST_DYNAMODB_CONFIG || - resolve(cwd(), 'jest-dynamodb-config.js')); - debug('config:', config); - const { - tables: newTables, - clientConfig, - installerConfig, - port: port = DEFAULT_PORT, - options: options = DEFAULT_OPTIONS, - } = typeof config === 'function' ? await config() : config; - - const dynamoDB = new DynamoDB({ - endpoint: `http://localhost:${port}`, - tls: false, - region: 'local-env', - credentials: { - accessKeyId: 'fakeMyKeyId', - secretAccessKey: 'fakeSecretAccessKey', - }, - ...clientConfig, - }); - - global.__DYNAMODB_CLIENT__ = dynamoDB; - - try { - const promises = [dynamoDB.listTables({})]; - - if (!global.__DYNAMODB__) { - promises.push(waitForLocalhost(port)); - } - - const [{TableNames: tableNames}] = await Promise.all(promises); - await deleteTables(dynamoDB, tableNames); // cleanup leftovers - } catch (err) { - // eslint-disable-next-line no-console - debug(`fallback to launch DB due to ${err}`); - - if (installerConfig) { - DynamoDbLocal.configureInstaller(installerConfig); - } - - if (!global.__DYNAMODB__) { - debug('spinning up a local ddb instance'); - - global.__DYNAMODB__ = await DynamoDbLocal.launch(port, null, options); - debug(`dynamodb-local started on port ${port}`); - - await waitForLocalhost(port); - } - } - debug(`dynamodb-local is ready on port ${port}`); - - await createTables(dynamoDB, newTables); -}; - -function createTables(dynamoDB, tables) { - return Promise.all(tables.map(table => dynamoDB.createTable(table))); -} - -function deleteTables(dynamoDB, tableNames) { - return Promise.all(tableNames.map(tableName => dynamoDB.deleteTable({TableName: tableName}))); -} diff --git a/environment.js b/src/environment.ts similarity index 57% rename from environment.js rename to src/environment.ts index 1cd181e..508cca7 100644 --- a/environment.js +++ b/src/environment.ts @@ -1,10 +1,13 @@ /* eslint-disable no-console */ -const {TestEnvironment} = require('jest-environment-node'); +import type {EnvironmentContext} from '@jest/environment'; +import type {JestEnvironmentConfig} from '@jest/environment'; +import {TestEnvironment} from 'jest-environment-node'; + const debug = require('debug')('jest-dynamodb'); module.exports = class DynamoDBEnvironment extends TestEnvironment { - constructor(config) { - super(config); + constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { + super(config, context); } async setup() { @@ -19,7 +22,9 @@ module.exports = class DynamoDBEnvironment extends TestEnvironment { await super.teardown(); } + // @ts-ignore runScript(script) { + // @ts-ignore return super.runScript(script); } }; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..a66f672 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,9 @@ +import {resolve} from 'path'; + +export * from './types'; + +export default { + globalSetup: resolve(__dirname, './setup.js'), + globalTeardown: resolve(__dirname, './teardown.js'), + testEnvironment: resolve(__dirname, './environment.js'), +}; diff --git a/src/setup.ts b/src/setup.ts new file mode 100644 index 0000000..dd140c2 --- /dev/null +++ b/src/setup.ts @@ -0,0 +1,91 @@ +import type {ListTablesCommandOutput} from '@aws-sdk/client-dynamodb/dist-types/commands/ListTablesCommand'; +import type {argValues} from 'dynamodb-local'; +import DynamoDbLocal from 'dynamodb-local'; +import {resolve} from 'path'; +import cwd from 'cwd'; +import {DynamoDB} from '@aws-sdk/client-dynamodb'; +import type {CreateTableCommandInput} from '@aws-sdk/client-dynamodb'; +import type {Config} from './types'; +import waitForLocalhost from './utils/wait-for-localhost'; + +const debug = require('debug')('jest-dynamodb'); + +const DEFAULT_PORT = 8000; +const DEFAULT_HOST = 'localhost'; +const DEFAULT_OPTIONS: argValues[] = ['-sharedDb']; + +export default async function () { + const { + tables: newTables, + clientConfig, + installerConfig, + port: port = DEFAULT_PORT, + hostname: hostname = DEFAULT_HOST, + options: options = DEFAULT_OPTIONS, + } = await getConfig(); + + const dynamoDB = new DynamoDB({ + endpoint: `http://${hostname}:${port}`, + tls: false, + region: 'local-env', + credentials: { + accessKeyId: 'fakeMyKeyId', + secretAccessKey: 'fakeSecretAccessKey', + }, + ...clientConfig, + }); + + global.__DYNAMODB_CLIENT__ = dynamoDB; + + try { + const promises: (Promise | Promise)[] = [ + dynamoDB.listTables({}), + ]; + + if (!global.__DYNAMODB__) { + promises.push(waitForLocalhost(port, hostname)); + } + + const [TablesList] = await Promise.all(promises); + const tableNames = TablesList?.TableNames; + + if (tableNames) { + await deleteTables(dynamoDB, tableNames); + } + } catch (err) { + // eslint-disable-next-line no-console + debug(`fallback to launch DB due to ${err}`); + + if (installerConfig) { + DynamoDbLocal.configureInstaller(installerConfig); + } + + if (!global.__DYNAMODB__) { + debug('spinning up a local ddb instance'); + + global.__DYNAMODB__ = await DynamoDbLocal.launch(port, null, options); + debug(`dynamodb-local started on port ${port}`); + + await waitForLocalhost(port, hostname); + } + } + debug(`dynamodb-local is ready on port ${port}`); + + await createTables(dynamoDB, newTables); +} + +function createTables(dynamoDB: DynamoDB, tables: CreateTableCommandInput[]) { + return Promise.all(tables.map(table => dynamoDB.createTable(table))); +} + +function deleteTables(dynamoDB: DynamoDB, tableNames: string[]) { + return Promise.all(tableNames.map(tableName => dynamoDB.deleteTable({TableName: tableName}))); +} + +async function getConfig(): Promise { + const path = process.env.JEST_DYNAMODB_CONFIG || resolve(cwd(), 'jest-dynamodb-config.js'); + const config = require(path); + debug('config:', config); + + return typeof config === 'function' ? await config() : config; +} diff --git a/teardown.js b/src/teardown.ts similarity index 61% rename from teardown.js rename to src/teardown.ts index fa07657..717c8f0 100644 --- a/teardown.js +++ b/src/teardown.ts @@ -1,7 +1,9 @@ -const DynamoDbLocal = require('dynamodb-local'); +import DynamoDbLocal from 'dynamodb-local'; +import type {JestArgs} from './types'; + const debug = require('debug')('jest-dynamodb'); -module.exports = async function (jestArgs) { +export default async function (jestArgs: JestArgs) { // eslint-disable-next-line no-console debug('Teardown DynamoDB'); @@ -14,6 +16,9 @@ module.exports = async function (jestArgs) { } else { const dynamoDB = global.__DYNAMODB_CLIENT__; const {TableNames: tableNames} = await dynamoDB.listTables({}); - await Promise.all(tableNames.map(tableName => dynamoDB.deleteTable({TableName: tableName}))); + + if (tableNames?.length) { + await Promise.all(tableNames.map(tableName => dynamoDB.deleteTable({TableName: tableName}))); + } } }; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..5193737 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,87 @@ +/* eslint-disable */ +import type {DynamoDB} from '@aws-sdk/client-dynamodb'; +import type {CreateTableCommandInput} from '@aws-sdk/client-dynamodb'; +import type {DynamoDBClientConfig} from '@aws-sdk/client-dynamodb'; +import type {ChildProcess} from 'child_process'; +import type {InstallerConfig} from 'dynamodb-local'; +import type {argValues} from 'dynamodb-local'; + +declare global { + var __DYNAMODB_CLIENT__: DynamoDB; + var __DYNAMODB__: ChildProcess; +} + +export type JestArgs = { + bail: number; + changedSince?: string; + changedFilesWithAncestor: boolean; + ci: boolean; + collectCoverage: boolean; + collectCoverageFrom: Array; + collectCoverageOnlyFrom?: { + [key: string]: boolean; + }; + coverageDirectory: string; + coveragePathIgnorePatterns?: Array; + coverageProvider: object; + coverageReporters: object; + coverageThreshold?: object; + detectLeaks: boolean; + detectOpenHandles: boolean; + expand: boolean; + filter?: string; + findRelatedTests: boolean; + forceExit: boolean; + json: boolean; + globalSetup?: string; + globalTeardown?: string; + lastCommit: boolean; + logHeapUsage: boolean; + listTests: boolean; + maxConcurrency: number; + maxWorkers: number; + noStackTrace: boolean; + nonFlagArgs: Array; + noSCM?: boolean; + notify: boolean; + notifyMode: object; + outputFile?: string; + onlyChanged: boolean; + onlyFailures: boolean; + passWithNoTests: boolean; + projects: Array; + replname?: string; + reporters?: Array; + runTestsByPath: boolean; + rootDir: string; + shard?: object; + silent?: boolean; + skipFilter: boolean; + snapshotFormat: object; + errorOnDeprecated: boolean; + testFailureExitCode: number; + testNamePattern?: string; + testPathPattern: string; + testResultsProcessor?: string; + testSequencer: string; + testTimeout?: number; + updateSnapshot: object; + useStderr: boolean; + verbose?: boolean; + watch: boolean; + watchAll: boolean; + watchman: boolean; + watchPlugins?: Array<{ + path: string; + config: Record; + }> | null; +}; + +export type Config = { + tables: CreateTableCommandInput[]; + clientConfig?: DynamoDBClientConfig; + installerConfig?: InstallerConfig; + port?: number; + hostname?: string; + options?: argValues[]; +}; diff --git a/wait-for-localhost.js b/src/utils/wait-for-localhost.ts similarity index 51% rename from wait-for-localhost.js rename to src/utils/wait-for-localhost.ts index 47a754a..b17400d 100644 --- a/wait-for-localhost.js +++ b/src/utils/wait-for-localhost.ts @@ -6,24 +6,24 @@ const http = require('http'); -const waitForLocalhost = port => - new Promise(resolve => { +export default function waitForLocalhost(port: number, host: string): Promise { + return new Promise(resolve => { const retry = () => setTimeout(main, 200); - const main = () => { - const request = http.request({method: 'GET', port, path: '/'}, response => { - if (response.statusCode === 400) { - return resolve(); - } + const request = http.request( + {method: 'GET', port, host, path: '/'}, + (response: {statusCode: number}) => { + if (response.statusCode === 400) { + return resolve(); + } - retry(); - }); + retry(); + } + ); request.on('error', retry); request.end(); }; - main(); }); - -module.exports = waitForLocalhost; +} diff --git a/index-another-concurrent-test.test.js b/tests/jest-preset-concurrent.test.ts similarity index 83% rename from index-another-concurrent-test.test.js rename to tests/jest-preset-concurrent.test.ts index 40e8d71..2e18cf8 100644 --- a/index-another-concurrent-test.test.js +++ b/tests/jest-preset-concurrent.test.ts @@ -1,5 +1,5 @@ -const {DynamoDB} = require('@aws-sdk/client-dynamodb'); -const {DynamoDBDocument} = require('@aws-sdk/lib-dynamodb'); +import {DynamoDB} from '@aws-sdk/client-dynamodb'; +import {DynamoDBDocument} from '@aws-sdk/lib-dynamodb'; const ddb = DynamoDBDocument.from( new DynamoDB({ diff --git a/index.test.js b/tests/jest-preset.test.ts similarity index 83% rename from index.test.js rename to tests/jest-preset.test.ts index 2be41b0..2858d5c 100644 --- a/index.test.js +++ b/tests/jest-preset.test.ts @@ -1,5 +1,5 @@ -const {DynamoDB} = require('@aws-sdk/client-dynamodb'); -const {DynamoDBDocument} = require('@aws-sdk/lib-dynamodb'); +import {DynamoDB} from '@aws-sdk/client-dynamodb'; +import {DynamoDBDocument} from '@aws-sdk/lib-dynamodb'; const ddb = DynamoDBDocument.from( new DynamoDB({ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5bbaf96 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@shelf/tsconfig/backend", + "compilerOptions": { + "strict": true + }, + "exclude": ["node_modules"], + "include": ["src"] +}