diff --git a/docs/modules/dynamodb.md b/docs/modules/dynamodb.md new file mode 100644 index 000000000..c1ff070b7 --- /dev/null +++ b/docs/modules/dynamodb.md @@ -0,0 +1,47 @@ +# DynamoDB + +## Install + +```bash +npm install @testcontainers/dynamodb --save-dev +``` + +## Examples + +These examples use the following libraries: + +- [@aws-sdk/client-dynamodb](https://www.npmjs.com/package/@aws-sdk/client-dynamodb) + + npm install @aws-sdk/client-dynamodb + +Choose an image from the [container registry](https://hub.docker.com/r/amazon/dynamodb-local) and substitute `IMAGE`. + +### Connect to local endpoint + +In order to use a DynamoDB Client we have to specify +at least `region`, `credentials` and `endpoint` eventhough `region` and `credentials` +will not be used: + + +[](../../packages/modules/dynamodb/src/dynamodb-container.test.ts) inside_block:connectEndpoint + + +### Connect via client config + +The above can also be written as + + +[](../../packages/modules/dynamodb/src/dynamodb-container.test.ts) inside_block:connectClientConfig + + +### Use a shared database + + +[](../../packages/modules/dynamodb/src/dynamodb-container.test.ts) inside_block:sharedDB + + +### Disable telemetry + + +[](../../packages/modules/dynamodb/src/dynamodb-container.test.ts) inside_block:disabledTelemetry + diff --git a/mkdocs.yml b/mkdocs.yml index 7e70bc96e..4a2dd3027 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,6 +64,7 @@ nav: - CosmosDB: modules/cosmosdb.md - Couchbase: modules/couchbase.md - CouchDB: modules/couchdb.md + - DynamoDB: modules/dynamodb.md - Elasticsearch: modules/elasticsearch.md - Etcd: modules/etcd.md - GCloud: modules/gcloud.md diff --git a/package-lock.json b/package-lock.json index 1a3b130a0..4dfb12b63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,28 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.1151.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.1151.0.tgz", + "integrity": "sha512-kWsWKCbL/53ABtNTUSJLLicEG4xE2CAXllPIp8iq8LDafxjAp/dZa82/z0s2eHpDGnbImjXBCYjhOZBGlFf/qQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/credential-provider-node": "^3.972.86", + "@aws-sdk/dynamodb-codec": "^3.973.48", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.32", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-s3": { "version": "3.1146.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1146.0.tgz", @@ -85,17 +107,17 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.978.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.1.tgz", - "integrity": "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==", + "version": "3.978.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.3.tgz", + "integrity": "sha512-nslpzUCTI9b2IQMWSd6mSAXldBexPc8rr0d3dXUutXYFLFaTdsHa2GD7sJdsn3PhyjK0O5K8ypd6zi+RXXPVlg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.974.6", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", - "@smithy/core": "^3.35.0", - "@smithy/signature-v4": "^5.7.3", + "@smithy/core": "^3.35.2", + "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" @@ -105,15 +127,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.72", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.72.tgz", - "integrity": "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==", + "version": "3.972.74", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.74.tgz", + "integrity": "sha512-rsRJcW2iFUhCgclyD6OSbMEaPl8Eu0qSeRZb2uuCK1ViZ66hl5wOe/p6Vr6MO3b+ydyKgFeqza2p6tZThvILlw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", + "@aws-sdk/core": "^3.978.3", "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -122,15 +144,15 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.74", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.74.tgz", - "integrity": "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==", + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.76.tgz", + "integrity": "sha512-pnKdh0Br7OJPj+7qKjzgTfDTFW0RhEMKXNDR2jv9CAM4oHuG/4z3hRh8KtutLfpmItuRtDbKBmwWkDqHwwAWXw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", + "@aws-sdk/core": "^3.978.3", "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@smithy/core": "^3.35.2", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", @@ -141,22 +163,22 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.973.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.17.tgz", - "integrity": "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==", + "version": "3.973.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.19.tgz", + "integrity": "sha512-kcjvAxqQEV8yuwkX58RelNn3ne65o7JgRWf3Z/gVvbi7Xuw1qSDLJlatrLD76+40cmRxmNkLxBoyp/vAQChQ5g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/credential-provider-env": "^3.972.72", - "@aws-sdk/credential-provider-http": "^3.972.74", - "@aws-sdk/credential-provider-login": "^3.972.79", - "@aws-sdk/credential-provider-process": "^3.972.72", - "@aws-sdk/credential-provider-sso": "^3.973.16", - "@aws-sdk/credential-provider-web-identity": "^3.972.78", - "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/credential-provider-env": "^3.972.74", + "@aws-sdk/credential-provider-http": "^3.972.76", + "@aws-sdk/credential-provider-login": "^3.972.81", + "@aws-sdk/credential-provider-process": "^3.972.74", + "@aws-sdk/credential-provider-sso": "^3.973.18", + "@aws-sdk/credential-provider-web-identity": "^3.972.80", + "@aws-sdk/nested-clients": "^3.997.48", "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@smithy/core": "^3.35.2", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" @@ -166,16 +188,16 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.79", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.79.tgz", - "integrity": "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==", + "version": "3.972.81", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.81.tgz", + "integrity": "sha512-CeQHSbE2CnVdaHIJ/NayRaCecIER+Pm9faUPkmRATO54rCoMTEPSbVieGHzEwvm1dZM2HIs6qQZCznhayNTWJQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -184,20 +206,20 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.84", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.84.tgz", - "integrity": "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==", + "version": "3.972.86", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.86.tgz", + "integrity": "sha512-WNttSyr6e8innPtBSty//CdnqJcgZN6eAbtw3v/CR2IObz6MYZEo8eusdtuCHJ2ujwFqC2bSPHVyhXGHx8HruQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.72", - "@aws-sdk/credential-provider-http": "^3.972.74", - "@aws-sdk/credential-provider-ini": "^3.973.17", - "@aws-sdk/credential-provider-process": "^3.972.72", - "@aws-sdk/credential-provider-sso": "^3.973.16", - "@aws-sdk/credential-provider-web-identity": "^3.972.78", + "@aws-sdk/credential-provider-env": "^3.972.74", + "@aws-sdk/credential-provider-http": "^3.972.76", + "@aws-sdk/credential-provider-ini": "^3.973.19", + "@aws-sdk/credential-provider-process": "^3.972.74", + "@aws-sdk/credential-provider-sso": "^3.973.18", + "@aws-sdk/credential-provider-web-identity": "^3.972.80", "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@smithy/core": "^3.35.2", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" @@ -207,15 +229,15 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.72", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.72.tgz", - "integrity": "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==", + "version": "3.972.74", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.74.tgz", + "integrity": "sha512-Yj6kZFJQdbR+PygTkqdVLPHHpdDpOS9GCYVcDCuweCRRig9LF24ELQBMfyUw1vspvrZJUfnOGVyMjhaL094ahw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", + "@aws-sdk/core": "^3.978.3", "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -224,17 +246,17 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.973.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.16.tgz", - "integrity": "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==", + "version": "3.973.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.18.tgz", + "integrity": "sha512-FnpPhdY4qUGtX/8X6eKxh+uD+9Y19i9FYibgk4SBe0M935s9vocHkHzaxaMNk+YkHmvzgdu8cHA5SCoeU+kkew==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/nested-clients": "^3.997.46", - "@aws-sdk/token-providers": "3.1138.0", + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", + "@aws-sdk/token-providers": "3.1150.0", "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -243,16 +265,63 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.78", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.78.tgz", - "integrity": "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==", + "version": "3.972.80", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.80.tgz", + "integrity": "sha512-TJF25Ndwy2HxcVj6oxMgp8qVlJAawlExHYBXqhuC6Zt2m+A7WpdkjBD6QglNbiArlteYljZHEY3R7KKmno6ISA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/dynamodb-codec": { + "version": "3.973.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.973.48.tgz", + "integrity": "sha512-LlTEc0u7gs2Qhr09vnRbdI8pY8O6/hP7wcsnlwrWQGEEheLEm3SB5Aqv9z7LiTXXEb2elBdSUyQ5OPuprELD6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.3", + "@smithy/core": "^3.35.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.11.tgz", + "integrity": "sha512-8q1ICxcDjHId3bBryuu/j+1L9y5/3uQnwzLDt5j2ElcjZSoWmFtymdJy7OjLrluSMe0Z4mq5bcH4fxBXvlEHfw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.972.32", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.32.tgz", + "integrity": "sha512-SshKt+B6n8Nn7EHs8aRs1Ye+GJmgeX0lq1lIIV7R5tRZdTGBMzO+nXqvWMTxfN5UmNYYfSBRJbbwbl0xevusFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/endpoint-cache": "^3.972.11", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -279,16 +348,16 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.46", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.46.tgz", - "integrity": "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==", + "version": "3.997.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.48.tgz", + "integrity": "sha512-B01jdXJtAW/XOiUzGdI2mz67WyEHbGEc9X6Euzw3bjLAa+txTqdZDER4X2FsIEcXzV2ABqudVZ5thali8tCCzQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/signature-v4-multi-region": "^3.996.47", + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/signature-v4-multi-region": "^3.996.48", "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@smithy/core": "^3.35.2", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", @@ -299,14 +368,14 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.47", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.47.tgz", - "integrity": "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==", + "version": "3.996.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.48.tgz", + "integrity": "sha512-cFDXfEbXaGwbnpvR/DyqfxfjxpjXVRVthQLgkwvcfDS5u3+8G9WSj1X1Ag/pTRg/QOeOno1Tq3ycEdrKsCUcOA==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.974.6", - "@smithy/signature-v4": "^5.7.3", + "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -315,16 +384,16 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1138.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1138.0.tgz", - "integrity": "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==", + "version": "3.1150.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1150.0.tgz", + "integrity": "sha512-wDU36ofhabZYu+d2KSriyDbwzxNoaOp+4dG0p3qkabk7HeALR5nIWyQbjoyJZLn3iqWkv/xNdyUfbfevDVDzzg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/core": "^3.978.3", + "@aws-sdk/nested-clients": "^3.997.48", "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -5704,9 +5773,9 @@ "peer": true }, "node_modules/@smithy/core": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.0.tgz", - "integrity": "sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.36.0.tgz", + "integrity": "sha512-3hJiwG1F5t1SoXqp/yOCmYXziyx7ZVqF63S5W6YU201e/qwVaHPex+E/3vvyY9a3ORq/b+BydeNZpP2saQ9X2g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5844,6 +5913,10 @@ "resolved": "packages/modules/couchdb", "link": true }, + "node_modules/@testcontainers/dynamodb": { + "resolved": "packages/modules/dynamodb", + "link": true + }, "node_modules/@testcontainers/elasticsearch": { "resolved": "packages/modules/elasticsearch", "link": true @@ -11375,6 +11448,16 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "obliterator": "^1.6.1" + } + }, "node_modules/mockserver-client": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/mockserver-client/-/mockserver-client-8.0.0.tgz", @@ -12039,6 +12122,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", + "dev": true, + "license": "MIT" + }, "node_modules/obug": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", @@ -15374,6 +15464,17 @@ "nano": "^11.0.7" } }, + "packages/modules/dynamodb": { + "name": "@testcontainers/dynamodb", + "version": "12.2.1", + "license": "MIT", + "dependencies": { + "testcontainers": "^12.2.1" + }, + "devDependencies": { + "@aws-sdk/client-dynamodb": "^3.1146.0" + } + }, "packages/modules/elasticsearch": { "name": "@testcontainers/elasticsearch", "version": "12.2.1", diff --git a/packages/modules/dynamodb/Dockerfile b/packages/modules/dynamodb/Dockerfile new file mode 100644 index 000000000..4fc11a57d --- /dev/null +++ b/packages/modules/dynamodb/Dockerfile @@ -0,0 +1 @@ +FROM amazon/dynamodb-local:3.3.2 diff --git a/packages/modules/dynamodb/package.json b/packages/modules/dynamodb/package.json new file mode 100644 index 000000000..d9d020893 --- /dev/null +++ b/packages/modules/dynamodb/package.json @@ -0,0 +1,37 @@ +{ + "name": "@testcontainers/dynamodb", + "version": "12.2.1", + "license": "MIT", + "keywords": [ + "dynamodb", + "testing", + "docker", + "testcontainers" + ], + "description": "DynamoDB module for Testcontainers", + "homepage": "https://github.com/testcontainers/testcontainers-node#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/testcontainers/testcontainers-node.git" + }, + "bugs": { + "url": "https://github.com/testcontainers/testcontainers-node/issues" + }, + "main": "build/index.js", + "files": [ + "build" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "prepack": "shx cp ../../../README.md . && shx cp ../../../LICENSE .", + "build": "tsc --project tsconfig.build.json" + }, + "dependencies": { + "testcontainers": "^12.2.1" + }, + "devDependencies": { + "@aws-sdk/client-dynamodb": "^3.1146.0" + } +} diff --git a/packages/modules/dynamodb/src/dynamodb-container.test.ts b/packages/modules/dynamodb/src/dynamodb-container.test.ts new file mode 100644 index 000000000..0af0e79c2 --- /dev/null +++ b/packages/modules/dynamodb/src/dynamodb-container.test.ts @@ -0,0 +1,268 @@ +import { + CreateTableCommand, + DeleteTableCommand, + DynamoDBClient, + GetItemCommand, + ListTablesCommand, + PutItemCommand, + waitUntilTableExists, + waitUntilTableNotExists, +} from "@aws-sdk/client-dynamodb"; +import { describe, it } from "vitest"; +import { getImage } from "../../../testcontainers/src/utils/test-helper"; +import { DynamoDBContainer } from "./dynamodb-container"; + +const IMAGE = getImage(__dirname); + +/** + * Create an empty dummy user table + * + * This function can be used in test cases + * to easily add a new table. + * + * @param client - DynamoDBClient pointing to the container + * @param tableName - The desired table name + * @returns - Three functions: teardown, addUser, getUser + */ +async function createDummyTable( + client: DynamoDBClient, + tableName: string +): Promise<{ + teardown: () => Promise; + addUser: (userId: number, name: string) => Promise; + getUser: (userId: number) => Promise; +}> { + const command = new CreateTableCommand({ + TableName: tableName, + BillingMode: "PAY_PER_REQUEST", + AttributeDefinitions: [ + { + AttributeName: "userId", + AttributeType: "N", + }, + ], + KeySchema: [ + { + AttributeName: "userId", + KeyType: "HASH", + }, + ], + }); + + await client.send(command); + await waitUntilTableExists({ client, maxWaitTime: 30 }, { TableName: tableName }); + + return { + teardown: async (): Promise => { + const deleteCommand = new DeleteTableCommand({ TableName: tableName }); + await client.send(deleteCommand); + await waitUntilTableNotExists({ client, maxWaitTime: 30 }, { TableName: tableName }); + }, + addUser: async (userId, name) => { + const putCommand = new PutItemCommand({ + TableName: tableName, + Item: { + userId: { N: userId.toString() }, + name: { S: name }, + }, + }); + await client.send(putCommand); + }, + getUser: async (userId) => { + const getCommand = new GetItemCommand({ + TableName: tableName, + Key: { + userId: { N: userId.toString() }, + }, + ConsistentRead: true, + }); + const response = await client.send(getCommand); + if (response.Item?.name?.S) { + return response.Item.name.S; + } + return null; + }, + }; +} + +describe("DynamoDBContainer", { timeout: 180_000 }, () => { + it("starts a container", async () => { + // connectClientConfig { + await using container = await new DynamoDBContainer(IMAGE).start(); + const client = new DynamoDBClient(container.clientConfig()); + + const command = new ListTablesCommand({}); + const result = await client.send(command); + + expect(result.TableNames).toStrictEqual([]); + // } + }); + + it("connects with endpoint", async () => { + // connectEndpoint { + await using container = await new DynamoDBContainer(IMAGE).start(); + const client = new DynamoDBClient({ + region: "us-east-1", + credentials: { + accessKeyId: "AKIAIOSFODNN7EXAMPLE", + secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + }, + endpoint: container.getEndpoint(), + }); + + const command = new ListTablesCommand({}); + const result = await client.send(command); + + expect(result.TableNames).toStrictEqual([]); + // } + }); + + it("stores data", async () => { + await using container = await new DynamoDBContainer(IMAGE).start(); + const client = new DynamoDBClient(container.clientConfig()); + + const { teardown, addUser, getUser } = await createDummyTable(client, "users"); + + await addUser(1, "Bob"); + await addUser(2, "Alice"); + + const userA = await getUser(1); + const userB = await getUser(2); + expect(userA).toBe("Bob"); + expect(userB).toBe("Alice"); + + await teardown(); + }); + + it("separates data by client by default", async () => { + await using container = await new DynamoDBContainer(IMAGE).start(); + + // Uses dummy credentials from the documentation + const clientA = new DynamoDBClient({ + region: "eu-central-1", + endpoint: container.getEndpoint(), + credentials: { + accessKeyId: "AKIAIOSFODNN7EXAMPLE", + secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + }, + }); + + // Uses slightly different credentials to simulate another + // AWS account + const clientB = new DynamoDBClient({ + region: "eu-central-1", + endpoint: container.getEndpoint(), + credentials: { + accessKeyId: "AKIAIOSFODNN7EXAMPL2", + secretAccessKey: "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", + }, + }); + + await createDummyTable(clientA, "users"); + + const resultA = await clientA.send(new ListTablesCommand()); + const resultB = await clientB.send(new ListTablesCommand()); + + // Since clientA created the table, only clientA should see it + expect(resultA.TableNames).toStrictEqual(["users"]); + expect(resultB.TableNames).toStrictEqual([]); + }); + + it("uses a shared table when requested", async () => { + // sharedDB { + await using container = await new DynamoDBContainer(IMAGE).withSharedDB().start(); + + // Both clients will see the same data eventhough the + // ACCESS_KEY_ID differ + const clientA = new DynamoDBClient({ + region: "us-east-1", + endpoint: container.getEndpoint(), + credentials: { + accessKeyId: "AKIAIOSFODNN7EXAMPLE", + secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + }, + }); + + const clientB = new DynamoDBClient({ + region: "us-east-1", + endpoint: container.getEndpoint(), + credentials: { + accessKeyId: "AKIAIOSFODNN7EXAMPL2", + secretAccessKey: "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", + }, + }); + // } + + await createDummyTable(clientA, "users"); + + const resultA = await clientA.send(new ListTablesCommand()); + const resultB = await clientB.send(new ListTablesCommand()); + + // Eventhough clientA created the table, both should see it due to + // the shared table design + expect(resultA.TableNames).toStrictEqual(["users"]); + expect(resultB.TableNames).toStrictEqual(["users"]); + }); + + it("persists data when dbPath is set", async () => { + // persistWithDbPath { + await using container = await new DynamoDBContainer(IMAGE).withDbPath(".").start(); + const client = new DynamoDBClient(container.clientConfig()); + // } + + const { addUser } = await createDummyTable(client, "users"); + await addUser(1, "Bob"); + + await container.restart(); + + // restarting will change the mapped port so we need a new client + const clientAfterRestart = new DynamoDBClient(container.clientConfig()); + + // The table should still be there including its content + const command = new GetItemCommand({ + TableName: "users", + Key: { userId: { N: "1" } }, + ConsistentRead: true, + }); + const response = await clientAfterRestart.send(command); + expect(response.Item).toStrictEqual({ + userId: { N: "1" }, + name: { S: "Bob" }, + }); + }); + + it("stores data only in memory when requested", async () => { + // withInMemory { + await using container = await new DynamoDBContainer(IMAGE).withInMemory().start(); + const client = new DynamoDBClient(container.clientConfig()); + // } + + const { addUser } = await createDummyTable(client, "users"); + await addUser(1, "Bob"); + + await container.restart(); + + // restarting will change the mapped port so we need a new client + const clientAfterRestart = new DynamoDBClient(container.clientConfig()); + + // We should not be able to see the table anymore + const result = await clientAfterRestart.send(new ListTablesCommand()); + expect(result.TableNames).toStrictEqual([]); + }); + + it("allows to disable telemetry", async () => { + /* Unfortunately there is no way to validate this right now, but at + * least we know it does not pass something invalid to the container + */ + + // disabledTelemetry { + await using container = await new DynamoDBContainer(IMAGE).withDisableTelemetry().start(); + const client = new DynamoDBClient(container.clientConfig()); + // } + + const command = new ListTablesCommand({}); + const result = await client.send(command); + + expect(result.TableNames).toStrictEqual([]); + }); +}); diff --git a/packages/modules/dynamodb/src/dynamodb-container.ts b/packages/modules/dynamodb/src/dynamodb-container.ts new file mode 100644 index 000000000..556608f35 --- /dev/null +++ b/packages/modules/dynamodb/src/dynamodb-container.ts @@ -0,0 +1,182 @@ +import { AbstractStartedContainer, GenericContainer } from "testcontainers"; + +// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.UsageNotes.html#DynamoDBLocal.Endpoint +const DYNAMODB_PORT = 8000; + +/* When running in AWS we do not have to pass any client configuration: + * + * * The default endpoint will be used + * * The region will be inferred from the lambda region + * * The credentials are taken from the lambda IAM role + * + * When connecting locally, we have to provide all three eventhough region + * and credentials are not really relevant. We want to help the user with + * a minimal configuration that works + */ +type DynamoDBLocalClientConfig = { + endpoint: string; + region: string; + credentials: { + readonly accessKeyId: string; + readonly secretAccessKey: string; + }; +}; + +export class DynamoDBContainer extends GenericContainer { + private sharedDB: boolean = false; + private inMemory: boolean = false; + private disableTelemetry: boolean = false; + private dbPath?: string; + + constructor(image: string) { + super(image); + this.withExposedPorts(DYNAMODB_PORT).withStartupTimeout(120_000); + } + + /** + * Share table between clients + * + * By default the DynamoDB container will present different tables + * depending on the current AWS access key. + * + * Use this option to use a shared database instead. This will + * pass ยด-sharedDb` to the container. For more information please read + * https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.UsageNotes.html#DynamoDBLocal.CommandLineOptions + */ + public withSharedDB(): this { + this.sharedDB = true; + return this; + } + + /** + * Specify a database path + * + * Specify a directory (for example `.`) to persist the + * database. This can be used to preserve tables between container + * restarts but in general is not required. + * + * This option will set `-dbPath` when invoking the container. + * Fore more information please read + * https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.UsageNotes.html#DynamoDBLocal.CommandLineOptions + * + * @param dbPath - The path to the database file inside the container + */ + public withDbPath(dbPath: string): this { + this.dbPath = dbPath; + return this; + } + + /** + * Ensure the database is kept in memory + * + * Use this option to keep the complete database in memory. This will + * pass `-inMemory` to the container. + * Fore more information please read + * https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.UsageNotes.html#DynamoDBLocal.CommandLineOptions + */ + public withInMemory(): this { + this.inMemory = true; + return this; + } + + /** + * Do not sent any telemetry + * + * By default the AWS DynamoDB container will sent telemetry information. + * Use this option to pass `-disableTelemetry` to the container + * Fore more information please read + * https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.UsageNotes.html#DynamoDBLocal.CommandLineOptions + */ + public withDisableTelemetry(): this { + this.disableTelemetry = true; + return this; + } + + private createCommand(): string[] | null { + const command: string[] = []; + if (this.sharedDB) { + command.push("-sharedDb"); + } + if (this.inMemory) { + command.push("-inMemory"); + } + if (this.disableTelemetry) { + command.push("-disableTelemetry"); + } + if (this.dbPath) { + command.push("-dbPath", this.dbPath); + } + + if (command.length === 0) { + return null; + } + + // If we pass arguments we also have to pass + // -jar DynamoDBLocal.jar + return ["-jar", "DynamoDBLocal.jar", ...command]; + } + + /** + * Start the DynamoDB container + */ + public override async start(): Promise { + const command = this.createCommand(); + if (command) { + this.withCommand(command); + } + + const started = await super.start(); + return new StartedDynamoDBContainer(started); + } +} + +export class StartedDynamoDBContainer extends AbstractStartedContainer { + public getPort(): number { + return this.startedTestContainer.getMappedPort(DYNAMODB_PORT); + } + + /*+ + * Returns the endpoint to reach the DynamoDB service + * + * The returned endpoint can be used to pass to your DynamoDB client. You + * can also use `clientConfig` instead which provides you a complete + * client configuration suitable for most usecases + */ + public getEndpoint(): string { + const hostname = this.getHost(); + const url = new URL(`http://${hostname}`); + url.port = this.getPort().toString(); + return url.toString(); + } + + /** + * Returns a partial DynamoDBConfig + * + * When intializing a DynamoDB client inside AWS itself (e.g. inside a + * lambda) we do not have to pass any configuration settings. When + * connecting locally, we have to set at least `endpoint`, + * `region` (can be a dummy) and `credentials` (can also be a dummy). + * + * `clientConfig` will return minimal configuration with `endpoint`, a dummy + * `region` of `us-east-1` and dummy credentials. + * + * This allows you to intialize a client through + * + * ```js + * const client = new DynamoDBClient(container.clientConfig()); + * ``` + */ + public clientConfig(): DynamoDBLocalClientConfig { + // The AWS credentials you see here are fake credentials and taken from the + // following example: + // https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html + return { + endpoint: this.getEndpoint(), + region: "us-east-1", + credentials: { + accessKeyId: "AKIAIOSFODNN7EXAMPLE", + secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + }, + }; + } +} diff --git a/packages/modules/dynamodb/src/index.ts b/packages/modules/dynamodb/src/index.ts new file mode 100644 index 000000000..9c6beccd5 --- /dev/null +++ b/packages/modules/dynamodb/src/index.ts @@ -0,0 +1 @@ +export { DynamoDBContainer, StartedDynamoDBContainer } from "./dynamodb-container"; diff --git a/packages/modules/dynamodb/tsconfig.build.json b/packages/modules/dynamodb/tsconfig.build.json new file mode 100644 index 000000000..91dc68846 --- /dev/null +++ b/packages/modules/dynamodb/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["build", "src/**/*.test.ts"], + "references": [ + { + "path": "../../testcontainers" + } + ] +} diff --git a/packages/modules/dynamodb/tsconfig.json b/packages/modules/dynamodb/tsconfig.json new file mode 100644 index 000000000..a06097b42 --- /dev/null +++ b/packages/modules/dynamodb/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "paths": { + "testcontainers": ["../../testcontainers/src"] + } + }, + "exclude": ["build"], + "references": [ + { + "path": "../../testcontainers" + } + ] +}