From 00e192ef5796af212b44dffb9f24a47b8ef69905 Mon Sep 17 00:00:00 2001 From: nicholas-yong <40938341+nicholas-yong@users.noreply.github.com> Date: Mon, 8 Jan 2024 10:10:59 +0800 Subject: [PATCH 1/2] Add delay --- src/setup.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/setup.ts b/src/setup.ts index 942a22c..65f60f0 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -14,6 +14,8 @@ const DEFAULT_PORT = 8000; const DEFAULT_HOST = 'localhost'; const DEFAULT_OPTIONS: argValues[] = ['-sharedDb']; +const sleep = (time: number) => new Promise(res => setTimeout(res, time)) + export default async function () { const { tables: newTables, @@ -72,6 +74,7 @@ export default async function () { debug(`dynamodb-local is ready on port ${port}`); await createTables(dynamoDB, newTables); + await sleep(2000); } function createTables(dynamoDB: DynamoDB, tables: CreateTableCommandInput[]) { From 99050f23b8ed5127d75e8900763ae19171e21df0 Mon Sep 17 00:00:00 2001 From: nicholas-yong <40938341+nicholas-yong@users.noreply.github.com> Date: Mon, 8 Jan 2024 10:17:20 +0800 Subject: [PATCH 2/2] Add lib file to build --- .gitignore | 1 - lib/environment.d.ts | 1 + lib/environment.js | 25 +++++++++ lib/index.d.ts | 7 +++ lib/index.js | 25 +++++++++ lib/setup.d.ts | 1 + lib/setup.js | 68 ++++++++++++++++++++++++ lib/teardown.d.ts | 2 + lib/teardown.js | 33 ++++++++++++ lib/types.d.ts | 84 ++++++++++++++++++++++++++++++ lib/types.js | 1 + lib/utils/delete-tables.d.ts | 2 + lib/utils/delete-tables.js | 11 ++++ lib/utils/get-config.d.ts | 2 + lib/utils/get-config.js | 15 ++++++ lib/utils/get-relevant-tables.d.ts | 2 + lib/utils/get-relevant-tables.js | 10 ++++ lib/utils/wait-for-localhost.d.ts | 1 + lib/utils/wait-for-localhost.js | 34 ++++++++++++ 19 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 lib/environment.d.ts create mode 100644 lib/environment.js create mode 100644 lib/index.d.ts create mode 100644 lib/index.js create mode 100644 lib/setup.d.ts create mode 100644 lib/setup.js create mode 100644 lib/teardown.d.ts create mode 100644 lib/teardown.js create mode 100644 lib/types.d.ts create mode 100644 lib/types.js create mode 100644 lib/utils/delete-tables.d.ts create mode 100644 lib/utils/delete-tables.js create mode 100644 lib/utils/get-config.d.ts create mode 100644 lib/utils/get-config.js create mode 100644 lib/utils/get-relevant-tables.d.ts create mode 100644 lib/utils/get-relevant-tables.js create mode 100644 lib/utils/wait-for-localhost.d.ts create mode 100644 lib/utils/wait-for-localhost.js diff --git a/.gitignore b/.gitignore index 8069a86..93a5202 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ .idea/ coverage/ node_modules/ -lib/ temp yarn.lock *.log diff --git a/lib/environment.d.ts b/lib/environment.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/lib/environment.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/lib/environment.js b/lib/environment.js new file mode 100644 index 0000000..4f16bfd --- /dev/null +++ b/lib/environment.js @@ -0,0 +1,25 @@ +"use strict"; + +var _jestEnvironmentNode = require("jest-environment-node"); +/* eslint-disable no-console */ + +const debug = require('debug')('jest-dynamodb'); +module.exports = class DynamoDBEnvironment extends _jestEnvironmentNode.TestEnvironment { + constructor(config, context) { + super(config, context); + } + async setup() { + debug('Setup DynamoDB Test Environment'); + await super.setup(); + } + async teardown() { + debug('Teardown DynamoDB Test Environment'); + await super.teardown(); + } + + // @ts-ignore + runScript(script) { + // @ts-ignore + return super.runScript(script); + } +}; \ No newline at end of file diff --git a/lib/index.d.ts b/lib/index.d.ts new file mode 100644 index 0000000..16c4649 --- /dev/null +++ b/lib/index.d.ts @@ -0,0 +1,7 @@ +export * from './types'; +declare const _default: { + globalSetup: string; + globalTeardown: string; + testEnvironment: string; +}; +export default _default; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..7894e7d --- /dev/null +++ b/lib/index.js @@ -0,0 +1,25 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _exportNames = {}; +exports.default = void 0; +var _path = require("path"); +var _types = require("./types"); +Object.keys(_types).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _types[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _types[key]; + } + }); +}); +var _default = exports.default = { + globalSetup: (0, _path.resolve)(__dirname, './setup.js'), + globalTeardown: (0, _path.resolve)(__dirname, './teardown.js'), + testEnvironment: (0, _path.resolve)(__dirname, './environment.js') +}; \ No newline at end of file diff --git a/lib/setup.d.ts b/lib/setup.d.ts new file mode 100644 index 0000000..83a22b1 --- /dev/null +++ b/lib/setup.d.ts @@ -0,0 +1 @@ +export default function (): Promise; diff --git a/lib/setup.js b/lib/setup.js new file mode 100644 index 0000000..efbc84c --- /dev/null +++ b/lib/setup.js @@ -0,0 +1,68 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = _default; +var _dynamodbLocal = _interopRequireDefault(require("dynamodb-local")); +var _clientDynamodb = require("@aws-sdk/client-dynamodb"); +var _getConfig = _interopRequireDefault(require("./utils/get-config")); +var _deleteTables = _interopRequireDefault(require("./utils/delete-tables")); +var _waitForLocalhost = _interopRequireDefault(require("./utils/wait-for-localhost")); +var _getRelevantTables = _interopRequireDefault(require("./utils/get-relevant-tables")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +const debug = require('debug')('jest-dynamodb'); +const DEFAULT_PORT = 8000; +const DEFAULT_HOST = 'localhost'; +const DEFAULT_OPTIONS = ['-sharedDb']; +const sleep = time => new Promise(res => setTimeout(res, time)); +async function _default() { + const { + tables: newTables, + clientConfig, + installerConfig, + port = DEFAULT_PORT, + hostname = DEFAULT_HOST, + options = DEFAULT_OPTIONS + } = await (0, _getConfig.default)(debug); + const dynamoDB = new _clientDynamodb.DynamoDB({ + endpoint: `http://${hostname}:${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((0, _waitForLocalhost.default)(port, hostname)); + } + const [TablesList] = await Promise.all(promises); + const tableNames = TablesList === null || TablesList === void 0 ? void 0 : TablesList.TableNames; + if (tableNames) { + await (0, _deleteTables.default)(dynamoDB, (0, _getRelevantTables.default)(tableNames, newTables)); + } + } catch (err) { + // eslint-disable-next-line no-console + debug(`fallback to launch DB due to ${err}`); + if (installerConfig) { + _dynamodbLocal.default.configureInstaller(installerConfig); + } + if (!global.__DYNAMODB__) { + debug('spinning up a local ddb instance'); + global.__DYNAMODB__ = await _dynamodbLocal.default.launch(port, null, options); + debug(`dynamodb-local started on port ${port}`); + await (0, _waitForLocalhost.default)(port, hostname); + } + } + debug(`dynamodb-local is ready on port ${port}`); + await createTables(dynamoDB, newTables); + await sleep(2000); +} +function createTables(dynamoDB, tables) { + return Promise.all(tables.map(table => dynamoDB.createTable(table))); +} \ No newline at end of file diff --git a/lib/teardown.d.ts b/lib/teardown.d.ts new file mode 100644 index 0000000..538bd91 --- /dev/null +++ b/lib/teardown.d.ts @@ -0,0 +1,2 @@ +import type { JestArgs } from './types'; +export default function (jestArgs: JestArgs): Promise; diff --git a/lib/teardown.js b/lib/teardown.js new file mode 100644 index 0000000..d040163 --- /dev/null +++ b/lib/teardown.js @@ -0,0 +1,33 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = _default; +var _dynamodbLocal = _interopRequireDefault(require("dynamodb-local")); +var _deleteTables = _interopRequireDefault(require("./utils/delete-tables")); +var _getConfig = _interopRequireDefault(require("./utils/get-config")); +var _getRelevantTables = _interopRequireDefault(require("./utils/get-relevant-tables")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +const debug = require('debug')('jest-dynamodb'); +async function _default(jestArgs) { + // eslint-disable-next-line no-console + debug('Teardown DynamoDB'); + if (global.__DYNAMODB__) { + const watching = jestArgs.watch || jestArgs.watchAll; + if (!watching) { + await _dynamodbLocal.default.stopChild(global.__DYNAMODB__); + } + } else { + const dynamoDB = global.__DYNAMODB_CLIENT__; + const { + tables: targetTables + } = await (0, _getConfig.default)(debug); + const { + TableNames: tableNames + } = await dynamoDB.listTables({}); + if (tableNames !== null && tableNames !== void 0 && tableNames.length) { + await (0, _deleteTables.default)(dynamoDB, (0, _getRelevantTables.default)(tableNames, targetTables)); + } + } +} \ No newline at end of file diff --git a/lib/types.d.ts b/lib/types.d.ts new file mode 100644 index 0000000..75d4eb2 --- /dev/null +++ b/lib/types.d.ts @@ -0,0 +1,84 @@ +/// +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/lib/types.js b/lib/types.js new file mode 100644 index 0000000..9a390c3 --- /dev/null +++ b/lib/types.js @@ -0,0 +1 @@ +"use strict"; \ No newline at end of file diff --git a/lib/utils/delete-tables.d.ts b/lib/utils/delete-tables.d.ts new file mode 100644 index 0000000..6298f4d --- /dev/null +++ b/lib/utils/delete-tables.d.ts @@ -0,0 +1,2 @@ +import type { DynamoDB } from '@aws-sdk/client-dynamodb'; +export default function deleteTables(dynamoDB: DynamoDB, tableNames: string[]): Promise; diff --git a/lib/utils/delete-tables.js b/lib/utils/delete-tables.js new file mode 100644 index 0000000..1c0c975 --- /dev/null +++ b/lib/utils/delete-tables.js @@ -0,0 +1,11 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = deleteTables; +function deleteTables(dynamoDB, tableNames) { + return Promise.all(tableNames.map(tableName => dynamoDB.deleteTable({ + TableName: tableName + }))); +} \ No newline at end of file diff --git a/lib/utils/get-config.d.ts b/lib/utils/get-config.d.ts new file mode 100644 index 0000000..680a113 --- /dev/null +++ b/lib/utils/get-config.d.ts @@ -0,0 +1,2 @@ +import type { Config } from '../types'; +export default function getConfig(debug: any): Promise; diff --git a/lib/utils/get-config.js b/lib/utils/get-config.js new file mode 100644 index 0000000..af1872a --- /dev/null +++ b/lib/utils/get-config.js @@ -0,0 +1,15 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = getConfig; +var _path = require("path"); +var _cwd = _interopRequireDefault(require("cwd")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +async function getConfig(debug) { + const path = process.env.JEST_DYNAMODB_CONFIG || (0, _path.resolve)((0, _cwd.default)(), 'jest-dynamodb-config.js'); + const config = require(path); + debug('config:', config); + return typeof config === 'function' ? await config() : config; +} \ No newline at end of file diff --git a/lib/utils/get-relevant-tables.d.ts b/lib/utils/get-relevant-tables.d.ts new file mode 100644 index 0000000..9f60b05 --- /dev/null +++ b/lib/utils/get-relevant-tables.d.ts @@ -0,0 +1,2 @@ +import type { CreateTableCommandInput } from '@aws-sdk/client-dynamodb'; +export default function getRelevantTables(dbTables: string[], configTables: CreateTableCommandInput[]): string[]; diff --git a/lib/utils/get-relevant-tables.js b/lib/utils/get-relevant-tables.js new file mode 100644 index 0000000..c79497a --- /dev/null +++ b/lib/utils/get-relevant-tables.js @@ -0,0 +1,10 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = getRelevantTables; +function getRelevantTables(dbTables, configTables) { + const configTableNames = configTables.map(configTable => configTable.TableName); + return dbTables.filter(dbTable => configTableNames.includes(dbTable)); +} \ No newline at end of file diff --git a/lib/utils/wait-for-localhost.d.ts b/lib/utils/wait-for-localhost.d.ts new file mode 100644 index 0000000..5b9e40f --- /dev/null +++ b/lib/utils/wait-for-localhost.d.ts @@ -0,0 +1 @@ +export default function waitForLocalhost(port: number, host: string): Promise; diff --git a/lib/utils/wait-for-localhost.js b/lib/utils/wait-for-localhost.js new file mode 100644 index 0000000..28e8f92 --- /dev/null +++ b/lib/utils/wait-for-localhost.js @@ -0,0 +1,34 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = waitForLocalhost; +// +// This is copied from https://github.com/sindresorhus/wait-for-localhost/blob/v3.3.0/index.js +// With 1 change +// We rely on status code 400 instead of 200 to ensure local DDB is up and running +// + +const http = require('http'); +function waitForLocalhost(port, host) { + return new Promise(resolve => { + const retry = () => setTimeout(main, 200); + const main = () => { + const request = http.request({ + method: 'GET', + port, + host, + path: '/' + }, response => { + if (response.statusCode === 400) { + return resolve(); + } + retry(); + }); + request.on('error', retry); + request.end(); + }; + main(); + }); +} \ No newline at end of file