From ce08b09eb0bba8044e2f818a8ab28ae179fafeef Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Tue, 23 Dec 2025 07:39:16 -0500 Subject: [PATCH 1/2] feat: update package version to 0.4.0-alpha.1 and add SNS client utilities - Updated version in package.json to 0.4.0-alpha.1 - Upgraded @typescript-eslint packages to 8.50.1 - Added aws-sdk-client-mock as a dev dependency - Updated AWS SDK dependencies to version 3.957.0 - Introduced sns-client.ts with functions to initialize, get, reset SNS client and publish messages - Added tests for SNS client functionalities in sns-client.test.ts - Exported SNS client utilities from index.ts --- package-lock.json | 646 +++++++++++++++++++++------------ package.json | 12 +- src/clients/sns-client.test.ts | 292 +++++++++++++++ src/clients/sns-client.ts | 110 ++++++ src/index.ts | 7 + 5 files changed, 829 insertions(+), 238 deletions(-) create mode 100644 src/clients/sns-client.test.ts create mode 100644 src/clients/sns-client.ts diff --git a/package-lock.json b/package-lock.json index cc38453..d2a1f23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "@leanstacks/lambda-utils", - "version": "0.3.0", + "version": "0.4.0-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@leanstacks/lambda-utils", - "version": "0.3.0", + "version": "0.4.0-alpha.1", "license": "MIT", "dependencies": { - "@aws-sdk/client-dynamodb": "3.956.0", - "@aws-sdk/lib-dynamodb": "3.956.0", + "@aws-sdk/client-dynamodb": "3.957.0", + "@aws-sdk/client-sns": "3.957.0", + "@aws-sdk/lib-dynamodb": "3.957.0", "pino": "10.1.0", "pino-lambda": "4.4.1", "zod": "4.2.1" @@ -23,8 +24,9 @@ "@types/aws-lambda": "8.10.159", "@types/jest": "30.0.0", "@types/node": "25.0.3", - "@typescript-eslint/eslint-plugin": "8.50.0", - "@typescript-eslint/parser": "8.50.0", + "@typescript-eslint/eslint-plugin": "8.50.1", + "@typescript-eslint/parser": "8.50.1", + "aws-sdk-client-mock": "4.1.0", "eslint": "9.39.2", "husky": "9.1.7", "jest": "30.2.0", @@ -164,27 +166,27 @@ } }, "node_modules/@aws-sdk/client-dynamodb": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.956.0.tgz", - "integrity": "sha512-mM99L5f8Kg5IhdiZbp+WzJ1IYoxHQ/t9eEe29oh9xp7L7VafOQytJyKMVNOIsoiIRJT6XYtst5493G4JhcU4CQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.957.0.tgz", + "integrity": "sha512-H/uHYgZTmFUq2qb4b/GTxT2F8Yyqyb3pMpI3mldGINZkPYQYN9pP246pqnf+OYOClPMxSMRchrbjZgZhADMi8Q==", "license": "Apache-2.0", "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.956.0", - "@aws-sdk/credential-provider-node": "3.956.0", - "@aws-sdk/dynamodb-codec": "3.956.0", - "@aws-sdk/middleware-endpoint-discovery": "3.956.0", - "@aws-sdk/middleware-host-header": "3.956.0", - "@aws-sdk/middleware-logger": "3.956.0", - "@aws-sdk/middleware-recursion-detection": "3.956.0", - "@aws-sdk/middleware-user-agent": "3.956.0", - "@aws-sdk/region-config-resolver": "3.956.0", - "@aws-sdk/types": "3.956.0", - "@aws-sdk/util-endpoints": "3.956.0", - "@aws-sdk/util-user-agent-browser": "3.956.0", - "@aws-sdk/util-user-agent-node": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/credential-provider-node": "3.957.0", + "@aws-sdk/dynamodb-codec": "3.957.0", + "@aws-sdk/middleware-endpoint-discovery": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", @@ -217,24 +219,74 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/client-sns": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sns/-/client-sns-3.957.0.tgz", + "integrity": "sha512-edRPKM7u8rB4LBe+bv5xWcOlUMvlCH9kNT+fIa7YcSBzLYGHedE6pbgAwACwTUnBOoaKKPGZZ9Fm9o4ZD8/JXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/credential-provider-node": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-retry": "^4.4.17", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.16", + "@smithy/util-defaults-mode-node": "^4.2.19", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-sso": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.956.0.tgz", - "integrity": "sha512-TCxCa9B1IMILvk/7sig0fRQzff+M2zBQVZGWOJL8SAZq/gfElIMAf/nYjQwMhXjyq8PFDRGm4GN8ZhNKPeNleQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.957.0.tgz", + "integrity": "sha512-iRdRjd+IpOogqRPt8iNRcg30J53z4rRfMviGwpKgsEa/fx3inCUPOuca3Ap7ZDES0atnEg3KGSJ3V/NQiEJ4BA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.956.0", - "@aws-sdk/middleware-host-header": "3.956.0", - "@aws-sdk/middleware-logger": "3.956.0", - "@aws-sdk/middleware-recursion-detection": "3.956.0", - "@aws-sdk/middleware-user-agent": "3.956.0", - "@aws-sdk/region-config-resolver": "3.956.0", - "@aws-sdk/types": "3.956.0", - "@aws-sdk/util-endpoints": "3.956.0", - "@aws-sdk/util-user-agent-browser": "3.956.0", - "@aws-sdk/util-user-agent-node": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", @@ -267,13 +319,13 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.956.0.tgz", - "integrity": "sha512-BMOCXZNz5z4cR3/SaNHUfeoZQUG/y39bLscdLUgg3RL6mDOhuINIqMc0qc6G3kpwDTLVdXikF4nmx2UrRK9y5A==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.957.0.tgz", + "integrity": "sha512-DrZgDnF1lQZv75a52nFWs6MExihJF2GZB6ETZRqr6jMwhrk2kbJPUtvgbifwcL7AYmVqHQDJBrR/MqkwwFCpiw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", - "@aws-sdk/xml-builder": "3.956.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/xml-builder": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", @@ -291,13 +343,13 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.956.0.tgz", - "integrity": "sha512-aLJavJMPVTvhmggJ0pcdCKEWJk3sL9QkJkUIEoTzOou7HnxWS66N4sC5e8y27AF2nlnYfIxq3hkEiZlGi/vlfA==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.957.0.tgz", + "integrity": "sha512-475mkhGaWCr+Z52fOOVb/q2VHuNvqEDixlYIkeaO6xJ6t9qR0wpLt4hOQaR6zR1wfZV0SlE7d8RErdYq/PByog==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" @@ -307,13 +359,13 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.956.0.tgz", - "integrity": "sha512-VsKzBNhwT6XJdW3HQX6o4KOHj1MAzSwA8/zCsT9mOGecozw1yeCcQPtlWDSlfsfygKVCXz7fiJzU03yl11NKMA==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.957.0.tgz", + "integrity": "sha512-8dS55QHRxXgJlHkEYaCGZIhieCs9NU1HU1BcqQ4RfUdSsfRdxxktqUKgCnBnOOn0oD3PPA8cQOCAVgIyRb3Rfw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/property-provider": "^4.2.7", @@ -328,20 +380,20 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.956.0.tgz", - "integrity": "sha512-TlDy+IGr0JIRBwnPdV31J1kWXEcfsR3OzcNVWQrguQdHeTw2lU5eft16kdizo6OruqcZRF/LvHBDwAWx4u51ww==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.957.0.tgz", + "integrity": "sha512-YuoZmIeE91YIeUfihh8SiSu546KtTvU+4rG5SaL30U9+nGq6P11GRRgqF0ANUyRseLC9ONHt+utar4gbO3++og==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/credential-provider-env": "3.956.0", - "@aws-sdk/credential-provider-http": "3.956.0", - "@aws-sdk/credential-provider-login": "3.956.0", - "@aws-sdk/credential-provider-process": "3.956.0", - "@aws-sdk/credential-provider-sso": "3.956.0", - "@aws-sdk/credential-provider-web-identity": "3.956.0", - "@aws-sdk/nested-clients": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/credential-provider-env": "3.957.0", + "@aws-sdk/credential-provider-http": "3.957.0", + "@aws-sdk/credential-provider-login": "3.957.0", + "@aws-sdk/credential-provider-process": "3.957.0", + "@aws-sdk/credential-provider-sso": "3.957.0", + "@aws-sdk/credential-provider-web-identity": "3.957.0", + "@aws-sdk/nested-clients": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", @@ -353,14 +405,14 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.956.0.tgz", - "integrity": "sha512-p2Y62mdIlUpiyi5tvn8cKTja5kq1e3Rm5gm4wpNQ9caTayfkIEXyKrbP07iepTv60Coaylq9Fx6b5En/siAeGA==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.957.0.tgz", + "integrity": "sha512-XcD5NEQDWYk8B4gs89bkwf2d+DNF8oS2NR5RoHJEbX4l8KErVATUjpEYVn6/rAFEktungxlYTnQ5wh0cIQvP5w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/nested-clients": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/nested-clients": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", @@ -372,18 +424,18 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.956.0.tgz", - "integrity": "sha512-ITjp7uAQh17ljUsCWkPRmLjyFfupGlJVUfTLHnZJ+c7G0P0PDRquaM+fBSh0y33tauHsBa5fGnCCLRo5hy9sGQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.957.0.tgz", + "integrity": "sha512-b9FT/7BQcJ001w+3JbTiJXfxHrWvPb7zDvvC1i1FKcNOvyCt3BGu04n4nO/b71a3iBnbfBXI89hCIZQsuLcEgw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.956.0", - "@aws-sdk/credential-provider-http": "3.956.0", - "@aws-sdk/credential-provider-ini": "3.956.0", - "@aws-sdk/credential-provider-process": "3.956.0", - "@aws-sdk/credential-provider-sso": "3.956.0", - "@aws-sdk/credential-provider-web-identity": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/credential-provider-env": "3.957.0", + "@aws-sdk/credential-provider-http": "3.957.0", + "@aws-sdk/credential-provider-ini": "3.957.0", + "@aws-sdk/credential-provider-process": "3.957.0", + "@aws-sdk/credential-provider-sso": "3.957.0", + "@aws-sdk/credential-provider-web-identity": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", @@ -395,13 +447,13 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.956.0.tgz", - "integrity": "sha512-wpAex+/LGVWkHPchsn9FWy1ahFualIeSYq3ADFc262ljJjrltOWGh3+cu3OK3gTMkX6VEsl+lFvy1P7Bk7cgXA==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.957.0.tgz", + "integrity": "sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", @@ -412,15 +464,15 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.956.0.tgz", - "integrity": "sha512-IRFSDF32x8TpOEYSGMcGQVJUiYuJaFkek0aCjW0klNIZHBF1YpflVpUarK9DJe4v4ryfVq3c0bqR/JFui8QFmw==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.957.0.tgz", + "integrity": "sha512-gTLPJFOkGtn3tVGglRhCar2oOobK1YctZRAT8nfJr17uaSRoAP46zIIHNYBZZUMqImb0qAHD9Ugm+Zd9sIqxyA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.956.0", - "@aws-sdk/core": "3.956.0", - "@aws-sdk/token-providers": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/client-sso": "3.957.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/token-providers": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", @@ -431,14 +483,14 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.956.0.tgz", - "integrity": "sha512-4YkmjwZC+qoUKlVOY9xNx7BTKRdJ1R1/Zjk2QSW5aWtwkk2e07ZUQvUpbW4vGpAxGm1K4EgRcowuSpOsDTh44Q==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.957.0.tgz", + "integrity": "sha512-x17xMeD7c+rKEsWachGIMifACqkugskrETWz18QDWismFcrmUuOcZu5rUa8s9y1pnITLKUQ1xU/qDLPH52jLlA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/nested-clients": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/nested-clients": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", @@ -449,12 +501,12 @@ } }, "node_modules/@aws-sdk/dynamodb-codec": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.956.0.tgz", - "integrity": "sha512-18+va/liQYkUVn5Wdz7h4rQQHq5QplFmQBAnkIOpZoN4ir36lrS9IeyoRbcrj8AxZRTHzl+vGc69Bq3sq9s5xg==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.957.0.tgz", + "integrity": "sha512-xds1mkwEGzXrNy/gT6/ehaJ+cbYn/QM7AkdwNrO1NBlwJVLo3imO6hOnOQ/0KWG2ck1dbKv9H9f2hka67bAzEA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", + "@aws-sdk/core": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", @@ -465,13 +517,13 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@aws-sdk/client-dynamodb": "^3.956.0" + "@aws-sdk/client-dynamodb": "^3.957.0" } }, "node_modules/@aws-sdk/endpoint-cache": { - "version": "3.953.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.953.0.tgz", - "integrity": "sha512-pz67DoHk5WNmvMuyNDiomUS2xo0mq6Z3TdfLJZlWVbSKi3h8hYxVQchJ2kzgTr6wu6zt3UBbtKV9yY1IBhKMVA==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.957.0.tgz", + "integrity": "sha512-QxvFejXYYBZp/GBfT7B15gvmvuq+0f2U8RPHqArf5IqBi51ZyBqUD805tQ8TlsVrlLoi+Z4fEFw4HEM5pGvPUg==", "license": "Apache-2.0", "dependencies": { "mnemonist": "0.38.3", @@ -482,13 +534,13 @@ } }, "node_modules/@aws-sdk/lib-dynamodb": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.956.0.tgz", - "integrity": "sha512-RA21BJsFS3tqellTGURz0oqf/xGZcK2xUow9sXl17SaC8ukYvIsUyPf8q+RcCVsXVezKG9n6yJhEsHemnNjH5Q==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.957.0.tgz", + "integrity": "sha512-v/GnmDIFfqDQub4tEp/QtsWOMMvSilMbbhCv0l5rPV9sd1NUmCtrH8iBHqVKeEmwSsiCzEKNTakmxSUJszrEww==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/util-dynamodb": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/util-dynamodb": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", @@ -498,17 +550,17 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@aws-sdk/client-dynamodb": "^3.956.0" + "@aws-sdk/client-dynamodb": "^3.957.0" } }, "node_modules/@aws-sdk/middleware-endpoint-discovery": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.956.0.tgz", - "integrity": "sha512-p8oOhksZg3qxluKBM6J5F9Tv4cqzkjYYQO3iU3BENT3NXGvoHuaBbj6G/hXsKl1/jfGNUyIOpO+59r1L4zNqOg==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.957.0.tgz", + "integrity": "sha512-MJjlw4mVJNTyR5dW6wpzKLRzFPIYAMA8qUWqgG4hGscmm4GFHvWVJ9mhhdpDu7Ie4Uaikmzfy0C4xzZ+lkf1+w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/endpoint-cache": "3.953.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/endpoint-cache": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", @@ -519,12 +571,12 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.956.0.tgz", - "integrity": "sha512-JujNJDp/dj1DbsI0ntzhrz2uJ4jpumcKtr743eMpEhdboYjuu/UzY8/7n1h5JbgU9TNXgqE9lgQNa5QPG0Tvsg==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.957.0.tgz", + "integrity": "sha512-BBgKawVyfQZglEkNTuBBdC3azlyqNXsvvN4jPkWAiNYcY0x1BasaJFl+7u/HisfULstryweJq/dAvIZIxzlZaA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" @@ -534,12 +586,12 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.956.0.tgz", - "integrity": "sha512-Qff39yEOPYgRsm4SrkHOvS0nSoxXILYnC8Akp0uMRi2lOcZVyXL3WCWqIOtI830qVI4GPa796sleKguxx50RHg==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.957.0.tgz", + "integrity": "sha512-w1qfKrSKHf9b5a8O76yQ1t69u6NWuBjr5kBX+jRWFx/5mu6RLpqERXRpVJxfosbep7k3B+DSB5tZMZ82GKcJtQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, @@ -548,12 +600,12 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.956.0.tgz", - "integrity": "sha512-/f4JxL2kSCYhy63wovqts6SJkpalSLvuFe78ozt3ClrGoHGyr69o7tPRYx5U7azLgvrIGjsWUyTayeAk3YHIVQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.957.0.tgz", + "integrity": "sha512-D2H/WoxhAZNYX+IjkKTdOhOkWQaK0jjJrDBj56hKjU5c9ltQiaX/1PqJ4dfjHntEshJfu0w+E6XJ+/6A6ILBBA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", @@ -564,14 +616,14 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.956.0.tgz", - "integrity": "sha512-azH8OJ0AIe3NafaTNvJorG/ALaLNTYwVKtyaSeQKOvaL8TNuBVuDnM5iHCiWryIaRgZotomqycwyfNKLw2D3JQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.957.0.tgz", + "integrity": "sha512-50vcHu96XakQnIvlKJ1UoltrFODjsq2KvtTgHiPFteUS884lQnK5VC/8xd1Msz/1ONpLMzdCVproCQqhDTtMPQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/types": "3.956.0", - "@aws-sdk/util-endpoints": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", "@smithy/core": "^3.20.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", @@ -582,23 +634,23 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.956.0.tgz", - "integrity": "sha512-GHDQMkxoWpi3eTrhWGmghw0gsZJ5rM1ERHfBFhlhduCdtV3TyhKVmDgFG84KhU8v18dcVpSp3Pu3KwH7j1tgIg==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.957.0.tgz", + "integrity": "sha512-PZUFtaUTSZWO+mbgQGWSiwz3EqedsuKNb7Xoxjzh5rfJE352DD4/jScQEhVPxvdLw62IK9b5UDu5kZlxzBs9Ow==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.956.0", - "@aws-sdk/middleware-host-header": "3.956.0", - "@aws-sdk/middleware-logger": "3.956.0", - "@aws-sdk/middleware-recursion-detection": "3.956.0", - "@aws-sdk/middleware-user-agent": "3.956.0", - "@aws-sdk/region-config-resolver": "3.956.0", - "@aws-sdk/types": "3.956.0", - "@aws-sdk/util-endpoints": "3.956.0", - "@aws-sdk/util-user-agent-browser": "3.956.0", - "@aws-sdk/util-user-agent-node": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", @@ -631,12 +683,12 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.956.0.tgz", - "integrity": "sha512-byU5XYekW7+rZ3e067y038wlrpnPkdI4fMxcHCHrv+TAfzl8CCk5xLyzerQtXZR8cVPVOXuaYWe1zKW0uCnXUA==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.957.0.tgz", + "integrity": "sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@smithy/config-resolver": "^4.4.5", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", @@ -647,14 +699,14 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.956.0.tgz", - "integrity": "sha512-I01Q9yDeG9oXge14u/bubtSdBpok/rTsPp2AQwy5xj/5PatRTHPbUTP6tef3AH/lFCAqkI0nncIcgx6zikDdUQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.957.0.tgz", + "integrity": "sha512-oSwo3BZ6gcvhjTg036V0UQmtENUeNwfCU35iDckX961CdI1alQ3TKRWLzKrwvXCbrOx+bZsuA1PHsTbNhI/+Fw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.956.0", - "@aws-sdk/nested-clients": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/nested-clients": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", @@ -665,9 +717,9 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.956.0.tgz", - "integrity": "sha512-DMRU/p9wAlAJxEjegnLwduCA8YP2pcT/sIJ+17KSF38c5cC6CbBhykwbZLECTo+zYzoFrOqeLbqE6paH8Gx3ug==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.957.0.tgz", + "integrity": "sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.11.0", @@ -678,9 +730,9 @@ } }, "node_modules/@aws-sdk/util-dynamodb": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.956.0.tgz", - "integrity": "sha512-RZrYUZ9zacb7t2+CeD+/UkVriIQ3N4MyQi+f/KPEbqZ1mYg1yXo/kFvUL4Qdi28qve/pG3J3R1tx9eb6nMW1uQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.957.0.tgz", + "integrity": "sha512-EqFfOkNZt4oJdyxFoP+PxO4JoEnAnTNOiwLKr57F0Hi+Qp5WYPJHdMFtHrzw6j2+atXQIfQUvtk1q6eQbcvMTw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -689,16 +741,16 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@aws-sdk/client-dynamodb": "^3.956.0" + "@aws-sdk/client-dynamodb": "^3.957.0" } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.956.0.tgz", - "integrity": "sha512-xZ5CBoubS4rs9JkFniKNShDtfqxaMUnwaebYMoybZm070q9+omFkQkJYXl7kopTViEgZgQl1sAsAkrawBM8qEQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.957.0.tgz", + "integrity": "sha512-xwF9K24mZSxcxKS3UKQFeX/dPYkEps9wF1b+MGON7EvnbcucrJGyQyK1v1xFPn1aqXkBTFi+SZaMRx5E5YCVFw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-endpoints": "^3.2.7", @@ -721,25 +773,25 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.956.0.tgz", - "integrity": "sha512-s8KwYR3HqiGNni7a1DN2P3RUog64QoBQ6VCSzJkHBWb6++8KSOpqeeDkfmEz+22y1LOne+bRrpDGKa0aqOc3rQ==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.957.0.tgz", + "integrity": "sha512-exueuwxef0lUJRnGaVkNSC674eAiWU07ORhxBnevFFZEKisln+09Qrtw823iyv5I1N8T+wKfh95xvtWQrNKNQw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.956.0", + "@aws-sdk/types": "3.957.0", "@smithy/types": "^4.11.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.956.0.tgz", - "integrity": "sha512-H0r6ol3Rr63/3xvrUsLqHps+cA7VkM7uCU5NtuTHnMbv3uYYTKf9M2XFHAdVewmmRgssTzvqemrARc8Ji3SNvg==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.957.0.tgz", + "integrity": "sha512-ycbYCwqXk4gJGp0Oxkzf2KBeeGBdTxz559D41NJP8FlzSej1Gh7Rk40Zo6AyTfsNWkrl/kVi1t937OIzC5t+9Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.956.0", - "@aws-sdk/types": "3.956.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/types": "3.957.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" @@ -757,9 +809,9 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.956.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.956.0.tgz", - "integrity": "sha512-x/IvXUeQYNUEQojpRIQpFt4X7XGxqzjUlXFRdwaTCtTz3q1droXVJvYOhnX3KiMgzeHGlBJfY4Nmq3oZNEUGFw==", + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.957.0.tgz", + "integrity": "sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.11.0", @@ -2713,6 +2765,34 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, "node_modules/@smithy/abort-controller": { "version": "4.2.7", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.7.tgz", @@ -3468,6 +3548,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/sinon": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", + "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3493,17 +3590,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", - "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz", + "integrity": "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/type-utils": "8.50.0", - "@typescript-eslint/utils": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/type-utils": "8.50.1", + "@typescript-eslint/utils": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" @@ -3516,23 +3613,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.50.0", + "@typescript-eslint/parser": "^8.50.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", - "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz", + "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4" }, "engines": { @@ -3548,14 +3645,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", - "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.1.tgz", + "integrity": "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.50.0", - "@typescript-eslint/types": "^8.50.0", + "@typescript-eslint/tsconfig-utils": "^8.50.1", + "@typescript-eslint/types": "^8.50.1", "debug": "^4.3.4" }, "engines": { @@ -3570,14 +3667,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", - "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz", + "integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0" + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3588,9 +3685,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", - "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz", + "integrity": "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==", "dev": true, "license": "MIT", "engines": { @@ -3605,15 +3702,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", - "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz", + "integrity": "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/utils": "8.50.0", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/utils": "8.50.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -3630,9 +3727,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", - "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.1.tgz", + "integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==", "dev": true, "license": "MIT", "engines": { @@ -3644,16 +3741,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", - "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz", + "integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.50.0", - "@typescript-eslint/tsconfig-utils": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", + "@typescript-eslint/project-service": "8.50.1", + "@typescript-eslint/tsconfig-utils": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -3672,16 +3769,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", - "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.1.tgz", + "integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0" + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3696,13 +3793,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", - "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz", + "integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/types": "8.50.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -4138,6 +4235,18 @@ "node": ">=8.0.0" } }, + "node_modules/aws-sdk-client-mock": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/aws-sdk-client-mock/-/aws-sdk-client-mock-4.1.0.tgz", + "integrity": "sha512-h/tOYTkXEsAcV3//6C1/7U4ifSpKyJvb6auveAepqqNJl6TdZaPFEtKjBQNf8UxQdDP850knB2i/whq4zlsxJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinon": "^17.0.3", + "sinon": "^18.0.1", + "tslib": "^2.1.0" + } + }, "node_modules/babel-jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", @@ -6390,6 +6499,13 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true, + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6627,6 +6743,20 @@ "dev": true, "license": "MIT" }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6865,6 +6995,17 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7445,6 +7586,45 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sinon": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.1.tgz", + "integrity": "sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index d888970..4b7a92b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@leanstacks/lambda-utils", - "version": "0.3.0", + "version": "0.4.0-alpha.1", "description": "A collection of utilities and helper functions designed to streamline the development of AWS Lambda functions using TypeScript.", "main": "dist/index.js", "module": "dist/index.esm.js", @@ -48,8 +48,9 @@ "@types/aws-lambda": "8.10.159", "@types/jest": "30.0.0", "@types/node": "25.0.3", - "@typescript-eslint/eslint-plugin": "8.50.0", - "@typescript-eslint/parser": "8.50.0", + "@typescript-eslint/eslint-plugin": "8.50.1", + "@typescript-eslint/parser": "8.50.1", + "aws-sdk-client-mock": "4.1.0", "eslint": "9.39.2", "husky": "9.1.7", "jest": "30.2.0", @@ -63,8 +64,9 @@ "typescript": "5.9.3" }, "dependencies": { - "@aws-sdk/client-dynamodb": "3.956.0", - "@aws-sdk/lib-dynamodb": "3.956.0", + "@aws-sdk/client-dynamodb": "3.957.0", + "@aws-sdk/client-sns": "3.957.0", + "@aws-sdk/lib-dynamodb": "3.957.0", "pino": "10.1.0", "pino-lambda": "4.4.1", "zod": "4.2.1" diff --git a/src/clients/sns-client.test.ts b/src/clients/sns-client.test.ts new file mode 100644 index 0000000..ed7887a --- /dev/null +++ b/src/clients/sns-client.test.ts @@ -0,0 +1,292 @@ +import { PublishCommand, SNSClient } from '@aws-sdk/client-sns'; +import { mockClient } from 'aws-sdk-client-mock'; + +import { getSNSClient, initializeSNSClient, SNSMessageAttributes, publishToTopic, resetSNSClient } from './sns-client'; + +// Create a mock for SNSClient +const snsMock = mockClient(SNSClient); + +describe('sns-client', () => { + beforeEach(() => { + // Reset the client and mock before each test + resetSNSClient(); + snsMock.reset(); + }); + + afterEach(() => { + // Clean up after each test + resetSNSClient(); + snsMock.reset(); + }); + + describe('initializeSNSClient', () => { + it('should create client with default config', () => { + // Arrange + // Act + const result = initializeSNSClient(); + + // Assert + expect(result).toBeInstanceOf(SNSClient); + }); + + it('should create client with custom config', () => { + // Arrange + const config = { region: 'us-west-2' }; + + // Act + const result = initializeSNSClient(config); + + // Assert + expect(result).toBeInstanceOf(SNSClient); + }); + + it('should replace existing client when called multiple times', () => { + // Arrange + const result1 = initializeSNSClient({ region: 'us-east-1' }); + + // Act + const result2 = initializeSNSClient({ region: 'us-west-2' }); + + // Assert + expect(result1).toBeInstanceOf(SNSClient); + expect(result2).toBeInstanceOf(SNSClient); + expect(result2).not.toBe(result1); + }); + }); + + describe('getSNSClient', () => { + it('should return the initialized client', () => { + // Arrange + const result = initializeSNSClient(); + + // Act + const client = getSNSClient(); + + // Assert + expect(client).toBe(result); + }); + + it('should create client with default config if not initialized', () => { + // Arrange + // Act + const client = getSNSClient(); + + // Assert + expect(client).toBeInstanceOf(SNSClient); + }); + + it('should return same instance on multiple calls', () => { + // Arrange + initializeSNSClient(); + + // Act + const client1 = getSNSClient(); + const client2 = getSNSClient(); + + // Assert + expect(client1).toBe(client2); + }); + + it('should return same instance when auto-initialized', () => { + // Arrange + // Act + const client1 = getSNSClient(); + const client2 = getSNSClient(); + + // Assert + expect(client1).toBe(client2); + }); + }); + + describe('resetSNSClient', () => { + it('should reset the client', () => { + // Arrange + const client1 = initializeSNSClient(); + + // Act + resetSNSClient(); + const client2 = getSNSClient(); + + // Assert + expect(client2).not.toBe(client1); + }); + + it('should allow reinitialization after reset', () => { + // Arrange + initializeSNSClient({ region: 'us-east-1' }); + resetSNSClient(); + + // Act + const result = initializeSNSClient({ region: 'us-west-2' }); + + // Assert + expect(result).toBeInstanceOf(SNSClient); + expect(getSNSClient()).toBe(result); + }); + }); + + describe('publishToTopic', () => { + const topicArn = 'arn:aws:sns:us-east-1:123456789012:MyTopic'; + const message = { orderId: '12345', status: 'completed' }; + const messageId = 'test-message-id-123'; + + beforeEach(() => { + // Mock successful publish + snsMock.on(PublishCommand).resolves({ + MessageId: messageId, + }); + }); + + it('should publish message to topic', async () => { + // Arrange + initializeSNSClient(); + + // Act + const result = await publishToTopic(topicArn, message); + + // Assert + expect(result).toBe(messageId); + expect(snsMock.calls()).toHaveLength(1); + }); + + it('should publish message with attributes', async () => { + // Arrange + initializeSNSClient(); + const attributes: SNSMessageAttributes = { + priority: { + DataType: 'String', + StringValue: 'high', + }, + count: { + DataType: 'Number', + StringValue: '5', + }, + }; + + // Act + const result = await publishToTopic(topicArn, message, attributes); + + // Assert + expect(result).toBe(messageId); + expect(snsMock.calls()).toHaveLength(1); + const call = snsMock.call(0); + expect(call.args[0].input).toEqual({ + TopicArn: topicArn, + Message: JSON.stringify(message), + MessageAttributes: attributes, + }); + }); + + it('should publish message with array data type attributes', async () => { + // Arrange + initializeSNSClient(); + const attributes: SNSMessageAttributes = { + categories: { + DataType: 'String.Array', + StringValue: JSON.stringify(['urgent', 'vip', 'high-priority']), + }, + department: { + DataType: 'String', + StringValue: 'sales', + }, + }; + + // Act + const result = await publishToTopic(topicArn, message, attributes); + + // Assert + expect(result).toBe(messageId); + expect(snsMock.calls()).toHaveLength(1); + const call = snsMock.call(0); + expect(call.args[0].input).toEqual({ + TopicArn: topicArn, + Message: JSON.stringify(message), + MessageAttributes: attributes, + }); + }); + + it('should initialize client if not already initialized', async () => { + // Arrange + // Do not initialize the client + + // Act + const result = await publishToTopic(topicArn, message); + + // Assert + expect(result).toBe(messageId); + expect(snsMock.calls()).toHaveLength(1); + }); + + it('should use singleton client instance', async () => { + // Arrange + initializeSNSClient(); + + // Act + await publishToTopic(topicArn, message); + await publishToTopic(topicArn, { orderId: '67890' }); + + // Assert + expect(snsMock.calls()).toHaveLength(2); + }); + + it('should convert message to JSON string', async () => { + // Arrange + initializeSNSClient(); + const complexMessage = { + orderId: '12345', + items: [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + ], + metadata: { + source: 'web', + timestamp: '2025-12-23T12:00:00Z', + }, + }; + + // Act + await publishToTopic(topicArn, complexMessage); + + // Assert + const call = snsMock.call(0); + const input = call.args[0].input as { Message?: string }; + expect(input.Message).toBe(JSON.stringify(complexMessage)); + }); + + it('should return empty string if MessageId is undefined', async () => { + // Arrange + initializeSNSClient(); + snsMock.reset(); + snsMock.on(PublishCommand).resolves({ + MessageId: undefined, + }); + + // Act + const result = await publishToTopic(topicArn, message); + + // Assert + expect(result).toBe(''); + }); + + it('should throw error if publish fails', async () => { + // Arrange + initializeSNSClient(); + const error = new Error('Failed to publish message'); + snsMock.reset(); + snsMock.on(PublishCommand).rejects(error); + + // Act & Assert + await expect(publishToTopic(topicArn, message)).rejects.toThrow('Failed to publish message'); + }); + + it('should throw error if SNS service is unavailable', async () => { + // Arrange + initializeSNSClient(); + snsMock.reset(); + snsMock.on(PublishCommand).rejects(new Error('Service Unavailable')); + + // Act & Assert + await expect(publishToTopic(topicArn, message)).rejects.toThrow('Service Unavailable'); + }); + }); +}); diff --git a/src/clients/sns-client.ts b/src/clients/sns-client.ts new file mode 100644 index 0000000..2796fa6 --- /dev/null +++ b/src/clients/sns-client.ts @@ -0,0 +1,110 @@ +import { PublishCommand, SNSClient, SNSClientConfig } from '@aws-sdk/client-sns'; + +/** + * Interface for SNS message attributes. + * Supports the AWS SNS data types: String, String.Array, Number, and Binary. + * Note: String.Array is the only array type supported by AWS SNS. + */ +export interface SNSMessageAttributes { + [key: string]: { + DataType: 'String' | 'String.Array' | 'Number' | 'Binary'; + StringValue?: string; + BinaryValue?: Uint8Array; + }; +} + +/** + * Singleton instance of SNS client + */ +let snsClient: SNSClient | null = null; + +/** + * Initializes the SNS client with the provided configuration. + * If the client is already initialized, this will replace it with a new instance. + * + * @param config - SNS client configuration + * @returns The SNS client instance + * + * @example + * ```typescript + * // Initialize with default configuration + * initializeSNSClient(); + * + * // Initialize with custom configuration + * initializeSNSClient({ region: 'us-east-1' }); + * ``` + */ +export const initializeSNSClient = (config?: SNSClientConfig): SNSClient => { + snsClient = new SNSClient(config || {}); + return snsClient; +}; + +/** + * Returns the singleton SNS client instance. + * If the client has not been initialized, creates one with default configuration. + * + * @returns The SNS client instance + * + * @example + * ```typescript + * const client = getSNSClient(); + * ``` + */ +export const getSNSClient = (): SNSClient => { + if (!snsClient) { + snsClient = new SNSClient({}); + } + return snsClient; +}; + +/** + * Resets the SNS client instance. + * Useful for testing or when you need to reinitialize the client with a different configuration. + */ +export const resetSNSClient = (): void => { + snsClient = null; +}; + +/** + * Publishes a message to an SNS topic. + * + * @param topicArn - The ARN of the SNS topic to publish to + * @param message - The message content (will be converted to JSON string) + * @param attributes - Optional message attributes for filtering + * @returns Promise that resolves to the message ID + * @throws Error if the SNS publish operation fails + * + * @example + * ```typescript + * const messageId = await publishToTopic( + * 'arn:aws:sns:us-east-1:123456789012:MyTopic', + * { orderId: '12345', status: 'completed' }, + * { + * priority: { + * DataType: 'String', + * StringValue: 'high' + * }, + * categories: { + * DataType: 'String.Array', + * StringValue: JSON.stringify(['urgent', 'vip', 'customer-request']) + * } + * } + * ); + * ``` + */ +export const publishToTopic = async ( + topicArn: string, + message: Record, + attributes?: SNSMessageAttributes, +): Promise => { + const client = getSNSClient(); + + const command = new PublishCommand({ + TopicArn: topicArn, + Message: JSON.stringify(message), + MessageAttributes: attributes, + }); + + const response = await client.send(command); + return response.MessageId ?? ''; +}; diff --git a/src/index.ts b/src/index.ts index a527bcd..fe6b14a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,4 +16,11 @@ export { initializeDynamoDBClients, resetDynamoDBClients, } from './clients/dynamodb-client'; +export { + getSNSClient, + initializeSNSClient, + SNSMessageAttributes, + publishToTopic, + resetSNSClient, +} from './clients/sns-client'; export { createConfigManager, ConfigManager } from './validation/config'; From 8afd0d21f4a3003c8e7146e61e16f258428d2de4 Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Tue, 23 Dec 2025 07:57:34 -0500 Subject: [PATCH 2/2] feat: add SNS client utilities and update documentation --- README.md | 32 +++- docs/README.md | 3 +- docs/SNS_CLIENT.md | 363 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 395 insertions(+), 3 deletions(-) create mode 100644 docs/SNS_CLIENT.md diff --git a/README.md b/README.md index 814e8f2..b44444d 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ export const handler = async (event: APIGatewayProxyEvent) => { - **📝 Structured Logging** – Pino logger pre-configured for Lambda with automatic AWS request context enrichment - **📤 API Response Helpers** – Standard response formatting for API Gateway with proper HTTP status codes - **⚙️ Configuration Validation** – Environment variable validation with Zod schema support -- **🔌 AWS SDK Clients** – Pre-configured AWS SDK v3 clients including DynamoDB with document client support +- **🔌 AWS SDK Clients** – Pre-configured AWS SDK v3 clients including DynamoDB and SNS with singleton patterns - **🔒 Full TypeScript Support** – Complete type definitions and IDE autocomplete - **⚡ Lambda Optimized** – Designed for performance in serverless environments @@ -107,7 +107,8 @@ Comprehensive guides and examples are available in the `docs` directory: | **[Configuration Guide](./docs/CONFIGURATION.md)** | Validate environment variables with Zod schemas and type safety | | **[Logging Guide](./docs/LOGGING.md)** | Configure and use structured logging with automatic AWS Lambda context | | **[API Gateway Responses](./docs/API_GATEWAY_RESPONSES.md)** | Format responses for API Gateway with standard HTTP patterns | -| **[DynamoDB Client](./docs/DYNAMODB_CLIENT.md)** | Use pre-configured AWS SDK v3 clients in your handlers | +| **[DynamoDB Client](./docs/DYNAMODB_CLIENT.md)** | Use pre-configured DynamoDB clients with singleton pattern | +| **[SNS Client](./docs/SNS_CLIENT.md)** | Publish messages to SNS topics with message attributes | ## Usage @@ -195,6 +196,33 @@ export const handler = async (event: any, context: any) => { **→ See [DynamoDB Client Guide](./docs/DYNAMODB_CLIENT.md) for detailed configuration and examples** +#### SNS Client + +Publish messages to SNS topics with optional message attributes: + +```typescript +import { publishToTopic, SNSMessageAttributes } from '@leanstacks/lambda-utils'; + +export const handler = async (event: any) => { + const attributes: SNSMessageAttributes = { + priority: { + DataType: 'String', + StringValue: 'high', + }, + }; + + const messageId = await publishToTopic( + 'arn:aws:sns:us-east-1:123456789012:MyTopic', + { orderId: '12345', status: 'completed' }, + attributes, + ); + + return { statusCode: 200, body: JSON.stringify({ messageId }) }; +}; +``` + +**→ See [SNS Client Guide](./docs/SNS_CLIENT.md) for detailed configuration and examples** + Additional AWS Clients are coming soon. ## Examples diff --git a/docs/README.md b/docs/README.md index 50b2567..1869e3b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,13 +12,14 @@ Lambda Utilities is a collection of pre-configured tools and helpers designed to - **[Logging Guide](./LOGGING.md)** – Implement structured logging in your Lambda functions with Pino and automatic AWS context enrichment - **[API Gateway Responses](./API_GATEWAY_RESPONSES.md)** – Format Lambda responses for API Gateway with standard HTTP status codes and headers - **[DynamoDB Client](./DYNAMODB_CLIENT.md)** – Reusable singleton DynamoDB client instances with custom configuration +- **[SNS Client](./SNS_CLIENT.md)** – Reusable singleton SNS client for publishing messages to topics with message attributes ## Features - 📝 **Structured Logging** – Pino logger pre-configured for Lambda with automatic request context - 📤 **API Response Helpers** – Standard response formatting for API Gateway integration - ⚙️ **Configuration Validation** – Environment variable validation with Zod schema support -- 🔌 **AWS Clients** – Pre-configured AWS SDK v3 clients for common services +- 🔌 **AWS Clients** – Pre-configured AWS SDK v3 clients for DynamoDB and SNS - 🔒 **Type Safe** – Full TypeScript support with comprehensive type definitions ## Support diff --git a/docs/SNS_CLIENT.md b/docs/SNS_CLIENT.md new file mode 100644 index 0000000..b08444a --- /dev/null +++ b/docs/SNS_CLIENT.md @@ -0,0 +1,363 @@ +# SNS Client Utilities + +The SNS client utilities provide a reusable singleton instance of `SNSClient` for use in AWS Lambda functions. These utilities enable you to configure the client once and reuse it across invocations, following AWS best practices for Lambda performance optimization. + +## Overview + +The utility exports the following functions: + +- `initializeSNSClient()` - Initialize the SNS client with optional configuration +- `getSNSClient()` - Get the singleton SNS client instance +- `publishToTopic()` - Publish a message to an SNS topic +- `resetSNSClient()` - Reset the client instance (useful for testing) + +## Usage + +### Basic Usage + +```typescript +import { publishToTopic } from '@leanstacks/lambda-utils'; + +export const handler = async (event: any) => { + // Publish a message to an SNS topic + const messageId = await publishToTopic('arn:aws:sns:us-east-1:123456789012:MyTopic', { + orderId: '12345', + status: 'completed', + }); + + return { + statusCode: 200, + body: JSON.stringify({ messageId }), + }; +}; +``` + +### Publishing with Message Attributes + +Message attributes enable SNS topic subscribers to filter messages based on metadata: + +```typescript +import { publishToTopic, SNSMessageAttributes } from '@leanstacks/lambda-utils'; + +export const handler = async (event: any) => { + const attributes: SNSMessageAttributes = { + priority: { + DataType: 'String', + StringValue: 'high', + }, + category: { + DataType: 'String', + StringValue: 'order', + }, + }; + + const messageId = await publishToTopic( + 'arn:aws:sns:us-east-1:123456789012:MyTopic', + { orderId: '12345', status: 'completed' }, + attributes, + ); + + return { statusCode: 200, body: JSON.stringify({ messageId }) }; +}; +``` + +### Using String Arrays in Message Attributes + +AWS SNS supports `String.Array` as the only array data type: + +```typescript +import { publishToTopic, SNSMessageAttributes } from '@leanstacks/lambda-utils'; + +export const handler = async (event: any) => { + const attributes: SNSMessageAttributes = { + tags: { + DataType: 'String.Array', + StringValue: JSON.stringify(['urgent', 'vip', 'customer-request']), + }, + }; + + const messageId = await publishToTopic( + 'arn:aws:sns:us-east-1:123456789012:MyTopic', + { orderId: '12345' }, + attributes, + ); + + return { statusCode: 200, body: JSON.stringify({ messageId }) }; +}; +``` + +### Using the SNS Client Directly + +For advanced use cases, you can access the underlying SNS client: + +```typescript +import { getSNSClient } from '@leanstacks/lambda-utils'; +import { ListTopicsCommand } from '@aws-sdk/client-sns'; + +export const handler = async (event: any) => { + const client = getSNSClient(); + + const response = await client.send(new ListTopicsCommand({})); + + return { + statusCode: 200, + body: JSON.stringify(response.Topics), + }; +}; +``` + +### Advanced Configuration + +#### Custom SNS Client Configuration + +```typescript +import { initializeSNSClient } from '@leanstacks/lambda-utils'; + +// Initialize client with custom configuration (typically done once outside the handler) +initializeSNSClient({ + region: 'us-west-2', + endpoint: 'http://localhost:4566', // For local development with LocalStack +}); + +export const handler = async (event: any) => { + // Client is now initialized and ready to use + // Use publishToTopic or getSNSClient as needed +}; +``` + +### Lambda Handler Pattern + +```typescript +import { initializeSNSClient, publishToTopic } from '@leanstacks/lambda-utils'; + +// Initialize client outside the handler (runs once per cold start) +initializeSNSClient({ region: process.env.AWS_REGION }); + +export const handler = async (event: any) => { + const messageId = await publishToTopic(process.env.TOPIC_ARN!, { + timestamp: new Date().toISOString(), + data: event, + }); + + return { + statusCode: 200, + body: JSON.stringify({ messageId }), + }; +}; +``` + +## API Reference + +### `initializeSNSClient(config?): SNSClient` + +Initializes the SNS client with the provided configuration. + +**Parameters:** + +- `config` (optional) - SNS client configuration object + +**Returns:** + +- The `SNSClient` instance + +**Notes:** + +- If called multiple times, it will replace the existing client instance +- If no config is provided, uses default AWS SDK configuration + +### `getSNSClient(): SNSClient` + +Returns the singleton SNS client instance. + +**Returns:** + +- The `SNSClient` instance + +**Notes:** + +- If the client has not been initialized, creates one with default configuration +- Automatically initializes the client on first use if not already initialized + +### `publishToTopic(topicArn, message, attributes?): Promise` + +Publishes a message to an SNS topic. + +**Parameters:** + +- `topicArn` (string) - The ARN of the SNS topic to publish to +- `message` (Record) - The message content (will be converted to JSON string) +- `attributes` (optional) - Message attributes for filtering + +**Returns:** + +- Promise that resolves to the message ID (string) + +**Throws:** + +- Error if the SNS publish operation fails + +**Notes:** + +- Automatically initializes the SNS client if not already initialized +- Message is automatically serialized to JSON +- Returns empty string if MessageId is not provided in the response + +### `resetSNSClient(): void` + +Resets the SNS client instance to null. + +**Notes:** + +- Primarily useful for testing scenarios where you need to reinitialize the client with different configurations +- After calling this, the client will be automatically re-initialized on next use + +### `SNSMessageAttributes` + +Type definition for SNS message attributes. + +**Interface:** + +```typescript +interface SNSMessageAttributes { + [key: string]: { + DataType: 'String' | 'String.Array' | 'Number' | 'Binary'; + StringValue?: string; + BinaryValue?: Uint8Array; + }; +} +``` + +**Supported Data Types:** + +- `String` - UTF-8 encoded string values +- `String.Array` - Array of strings (must be JSON-stringified) +- `Number` - Numeric values (stored as strings) +- `Binary` - Binary data + +**Note:** `String.Array` is the only array type supported by AWS SNS. There are no `Number.Array` or `Binary.Array` types. + +## Best Practices + +1. **Initialize Outside the Handler**: Always initialize the client outside your Lambda handler function to reuse the instance across invocations. + +2. **Use Environment Variables**: Configure the client using environment variables for flexibility across environments. + +3. **Error Handling**: Always wrap publish operations in try-catch blocks to handle errors gracefully. + +4. **Message Attributes**: Use message attributes for subscriber filtering rather than including filter criteria in the message body. + +5. **Testing**: Use `resetSNSClient()` in test setup/teardown to ensure clean test isolation. + +## Message Attribute Examples + +### String Attribute + +```typescript +const attributes: SNSMessageAttributes = { + priority: { + DataType: 'String', + StringValue: 'high', + }, +}; +``` + +### Number Attribute + +```typescript +const attributes: SNSMessageAttributes = { + temperature: { + DataType: 'Number', + StringValue: '72.5', + }, +}; +``` + +### String Array Attribute + +```typescript +const attributes: SNSMessageAttributes = { + tags: { + DataType: 'String.Array', + StringValue: JSON.stringify(['urgent', 'vip', 'customer-request']), + }, +}; +``` + +### Binary Attribute + +```typescript +const attributes: SNSMessageAttributes = { + thumbnail: { + DataType: 'Binary', + BinaryValue: new Uint8Array([1, 2, 3, 4]), + }, +}; +``` + +## Testing Example + +```typescript +import { initializeSNSClient, publishToTopic, resetSNSClient } from '@leanstacks/lambda-utils'; + +describe('MyLambdaHandler', () => { + beforeEach(() => { + resetSNSClient(); + initializeSNSClient({ + region: 'us-east-1', + endpoint: 'http://localhost:4566', // LocalStack + }); + }); + + afterEach(() => { + resetSNSClient(); + }); + + it('should publish message to SNS topic', async () => { + const messageId = await publishToTopic('arn:aws:sns:us-east-1:123456789012:TestTopic', { + test: 'data', + }); + + expect(messageId).toBeTruthy(); + }); +}); +``` + +## Error Handling Example + +```typescript +import { publishToTopic } from '@leanstacks/lambda-utils'; + +export const handler = async (event: any) => { + try { + const messageId = await publishToTopic( + process.env.TOPIC_ARN!, + { orderId: event.orderId, status: 'completed' }, + { + priority: { + DataType: 'String', + StringValue: 'high', + }, + }, + ); + + return { + statusCode: 200, + body: JSON.stringify({ success: true, messageId }), + }; + } catch (error) { + console.error('Failed to publish message to SNS', error); + + return { + statusCode: 500, + body: JSON.stringify({ success: false, error: 'Failed to publish message' }), + }; + } +}; +``` + +## Related Resources + +- **[AWS SDK for JavaScript v3 - SNS Client](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-sns/)** +- **[Amazon SNS Message Attributes](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html)** +- **[AWS Lambda Best Practices](https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html)** +- **[Back to the project documentation](README.md)**