@tomassabol/aws-services 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ dist
package/.eslintrc.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "root": true,
3
+ "env": {
4
+ "node": true
5
+ },
6
+ "overrides": [
7
+ {
8
+ "files": ["**/*.ts", "**/*.tsx"],
9
+ "parser": "@typescript-eslint/parser",
10
+ "parserOptions": {
11
+ "project": "./tsconfig.json"
12
+ },
13
+ "plugins": ["@typescript-eslint"],
14
+ "extends": [
15
+ "eslint:recommended",
16
+ "plugin:import/recommended",
17
+ "plugin:import/typescript",
18
+ "plugin:@typescript-eslint/eslint-recommended",
19
+ "plugin:@typescript-eslint/recommended",
20
+ "prettier"
21
+ ],
22
+ "rules": {
23
+ "@typescript-eslint/no-empty-interface": "warn",
24
+ "@typescript-eslint/no-unused-vars": [
25
+ "warn",
26
+ { "argsIgnorePattern": "^_" }
27
+ ],
28
+ "@typescript-eslint/no-floating-promises": "error",
29
+ "require-await": "warn",
30
+ "no-useless-catch": "warn",
31
+ "no-console": "warn",
32
+ "no-new-object": "warn",
33
+ "object-shorthand": ["warn", "always"],
34
+ "quote-props": ["warn", "as-needed"],
35
+ "prefer-object-spread": "warn",
36
+ "prefer-destructuring": [
37
+ "warn",
38
+ {
39
+ "VariableDeclarator": {
40
+ "array": false,
41
+ "object": true
42
+ },
43
+ "AssignmentExpression": {
44
+ "array": false,
45
+ "object": false
46
+ }
47
+ }
48
+ ],
49
+ "default-param-last": "warn",
50
+ "no-param-reassign": [
51
+ "warn",
52
+ { "props": true, "ignorePropertyModificationsFor": ["acc"] }
53
+ ],
54
+ "prefer-arrow-callback": "warn",
55
+ "no-duplicate-imports": "warn",
56
+ "import/no-mutable-exports": "warn",
57
+ "import/first": "warn",
58
+ "no-iterator": "warn",
59
+ "dot-notation": "warn",
60
+ "one-var": ["warn", { "initialized": "never" }],
61
+ "no-multi-assign": "warn",
62
+ "no-plusplus": ["warn", { "allowForLoopAfterthoughts": true }],
63
+ "eqeqeq": "warn"
64
+ }
65
+ },
66
+ {
67
+ "files": ["**/*.md"],
68
+ "extends": "plugin:markdown/recommended"
69
+ },
70
+ {
71
+ "files": ["**/*.json"],
72
+ "extends": "plugin:json/recommended"
73
+ }
74
+ ]
75
+ }
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ . "$(dirname "$0")/_/husky.sh"
3
+
4
+ npx lint-staged
@@ -0,0 +1,9 @@
1
+ {
2
+ "*.ts": [
3
+ "tsc --noEmit --esModuleInterop --skipLibCheck --resolveJsonModule --target es2022 --module commonjs"
4
+ ],
5
+ "*.{js,ts,json,md}": [
6
+ "eslint --no-ignore --max-warnings 0",
7
+ "prettier --write"
8
+ ]
9
+ }
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ v18.18.2
@@ -0,0 +1,2 @@
1
+ .coverage
2
+ dist
@@ -0,0 +1,3 @@
1
+ {
2
+ "semi": false
3
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "sonarlint.connectedMode.project": {
3
+ "connectionId": "io.tomassabol.sonarqube",
4
+ "projectKey": "tomassaboldev_aws-services_bb5f4f68-64f8-4726-bdd4-579ceadf172d"
5
+ }
6
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,96 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.8.0] - 2024-06-24
9
+
10
+ ### Changed
11
+
12
+ - WMS-488 Improve sendEvent error handling
13
+
14
+ ## [1.7.1] - 2024-06-22
15
+
16
+ ### Changed
17
+
18
+ - Bump @aws-lambda-powertools/logger
19
+
20
+ ### Security
21
+
22
+ - Remove NPM token
23
+
24
+ ## [1.7.0] - 2023-11-18
25
+
26
+ ### Added
27
+
28
+ - Add Jest mock for SSM
29
+
30
+ ## [1.6.1] - 2023-11-04
31
+
32
+ ### Added
33
+
34
+ - Add getSecret method to Secrets Manager mock
35
+
36
+ ### Security
37
+
38
+ - Fix vulnerabilities
39
+
40
+ ## [1.6.0] - 2023-08-18
41
+
42
+ ### Added
43
+
44
+ - Add Jest mocks for DynamoDB, EventBridge, Secrets Manager and SQS
45
+
46
+ ## [1.5.0] - 2023-08-14
47
+
48
+ ### Changed
49
+
50
+ - Improve S3 client
51
+
52
+ ## [1.4.0] - 2023-08-12
53
+
54
+ ### Changed
55
+
56
+ - Improve EventBridge client
57
+
58
+ ## [1.3.0] - 2023-08-11
59
+
60
+ ### Added
61
+
62
+ - Add format check step to Bitbucket pipeline
63
+ - Add nvm configuration file
64
+
65
+ ### Changed
66
+
67
+ - Improve SSM client
68
+ - Improve SQS client
69
+ - Improve Secrets Manager client
70
+ - Improve DynamoDB client
71
+ - Upgrade AWS CDK libs
72
+ - Refactor NPM scripts
73
+ - Update nvm configuration file
74
+ - Improve Bitbucket pipeline
75
+
76
+ ### Removed
77
+
78
+ - Remove npm-run-all package
79
+
80
+ ### Security
81
+
82
+ - Fix vulnerabilities
83
+
84
+ ## [1.2.1] - 2023-06-08
85
+
86
+ ### Fixed
87
+
88
+ - Fix importing without dist folder
89
+
90
+ ### Security
91
+
92
+ - Fix vulnerabilities
93
+
94
+ ## [1.1.1]
95
+
96
+ - Initial release
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # AWS Services
2
+
3
+ ## How to publish
4
+
5
+ To be able to import from path `@tomassabol/aws-services/s3` and not `@tomassabol/aws-services/dist/s3`, you have to publish the package with following command:
6
+
7
+ ```sh
8
+ npm run dist
9
+ ```
10
+
11
+ ## How to use it
12
+
13
+ ```sh
14
+ npm install @tomassabol/aws-services
15
+ ```
16
+
17
+ ```typescript
18
+ import { getFromS3 } from "@tomassabol/aws-services/s3"
19
+
20
+ async function getMyObject() {
21
+ const params = {
22
+ bucket: "my-bucket-name",
23
+ key: "my-object-key",
24
+ }
25
+
26
+ const data = await getFromS3(params)
27
+ }
28
+ ```
@@ -0,0 +1,65 @@
1
+ image: node:18
2
+
3
+ definitions:
4
+ steps:
5
+ - step: &build-step
6
+ name: Build
7
+ caches:
8
+ - node
9
+ script:
10
+ - npm ci
11
+ - step: &format-step
12
+ name: Format
13
+ caches:
14
+ - node
15
+ script:
16
+ - npm run format
17
+ - step: &lint-step
18
+ name: Lint
19
+ caches:
20
+ - node
21
+ script:
22
+ - npm run lint
23
+ - step: &test-step
24
+ name: Test
25
+ caches:
26
+ - node
27
+ script:
28
+ - npm test
29
+ artifacts:
30
+ - .coverage/lcov.info
31
+ - .sonar/test-report.xml
32
+ - step: &code-analysis-step
33
+ name: Code Analysis
34
+ caches:
35
+ - sonar
36
+ script:
37
+ - pipe: sonarsource/sonarqube-scan:2.0.1
38
+ variables:
39
+ SONAR_HOST_URL: ${SONAR_HOST_URL}
40
+ SONAR_TOKEN: ${SONAR_TOKEN}
41
+
42
+ caches:
43
+ sonar: /opt/sonar-scanner/.sonar
44
+
45
+ clone:
46
+ depth: full
47
+
48
+ pipelines:
49
+ branches:
50
+ "{main,test,prod}":
51
+ - step: *build-step
52
+ - parallel:
53
+ - step: *format-step
54
+ - step: *lint-step
55
+ - step: *test-step
56
+ - step: *code-analysis-step
57
+
58
+ pull-requests:
59
+ "**":
60
+ - step: *build-step
61
+ - parallel:
62
+ - step: *format-step
63
+ - step: *lint-step
64
+ - step: *test-step
65
+ - step: *code-analysis-step
package/jest.config.js ADDED
@@ -0,0 +1,62 @@
1
+ /*
2
+ * For a detailed explanation regarding each configuration property and type check, visit:
3
+ * https://jestjs.io/docs/en/configuration.html
4
+ */
5
+
6
+ module.exports = {
7
+ /**
8
+ * Paths
9
+ */
10
+
11
+ // A list of paths to directories that Jest should use to search for files in
12
+ roots: ["./test"],
13
+
14
+ // The glob patterns Jest uses to detect test files
15
+ testMatch: ["<rootDir>/test/**/*.test.ts"],
16
+
17
+ // Transformers for files
18
+ transform: {
19
+ "^.+\\.tsx?$": [
20
+ "esbuild-jest",
21
+ {
22
+ sourcemap: true,
23
+ },
24
+ ],
25
+ },
26
+
27
+ /*
28
+ * Results processing
29
+ */
30
+
31
+ // Indicates whether the coverage information should be collected while executing the test
32
+ collectCoverage: true,
33
+
34
+ // An array of glob patterns indicating a set of files for which coverage information should be collected
35
+ collectCoverageFrom: ["src/**/*.ts"],
36
+
37
+ // An array of regexp pattern strings used to skip coverage for certain files
38
+ coveragePathIgnorePatterns: ["src/logger.ts"],
39
+
40
+ // The directory where Jest should output its coverage files
41
+ coverageDirectory: ".coverage",
42
+
43
+ // Export test results fo SonarQube scanner to process test results
44
+ testResultsProcessor: "jest-sonar-reporter",
45
+
46
+ /*
47
+ * Test environment
48
+ */
49
+
50
+ // The test environment that will be used for testing
51
+ testEnvironment: "node",
52
+
53
+ // Run for all tests
54
+ // setupFiles: ["<rootDir>/test/env.ts"],
55
+
56
+ /*
57
+ * Mocks
58
+ */
59
+
60
+ // Automatically clear mock calls and instances between every test
61
+ clearMocks: true,
62
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@tomassabol/aws-services",
3
+ "version": "1.8.0",
4
+ "license": "MIT",
5
+ "description": "AWS Services",
6
+ "scripts": {
7
+ "build": "tsc -b tsconfig.build.json",
8
+ "clean": "rimraf dist .coverage .sonar",
9
+ "coverage": "open .coverage/lcov-report/index.html",
10
+ "dist": "tsc -b tsconfig.build.json && cp package.json dist && cp README.md dist && cd dist && npm publish",
11
+ "format": "prettier --check .",
12
+ "husky-install": "husky install",
13
+ "lint": "eslint . --max-warnings 0",
14
+ "test": "jest"
15
+ },
16
+ "peerDependencies": {
17
+ "@aws-lambda-powertools/logger": "^2.2.0",
18
+ "@aws-sdk/client-appconfig": "^3.388.0",
19
+ "@aws-sdk/client-dynamodb": "^3.388.0",
20
+ "@aws-sdk/client-eventbridge": "^3.388.0",
21
+ "@aws-sdk/client-s3": "^3.388.0",
22
+ "@aws-sdk/client-secrets-manager": "^3.388.0",
23
+ "@aws-sdk/client-sns": "^3.388.0",
24
+ "@aws-sdk/client-sqs": "^3.388.0",
25
+ "@aws-sdk/client-ssm": "^3.388.0",
26
+ "@aws-sdk/lib-dynamodb": "^3.388.0"
27
+ },
28
+ "devDependencies": {
29
+ "@aws-sdk/util-stream-node": "^3.374.0",
30
+ "@tsconfig/node18": "^1.0.1",
31
+ "@types/aws-sdk": "^2.7.0",
32
+ "@types/jest": "^29.4.0",
33
+ "@types/node": "^18.14.2",
34
+ "@typescript-eslint/eslint-plugin": "^5.25.0",
35
+ "@typescript-eslint/parser": "^5.25.0",
36
+ "aws-sdk-client-mock": "^3.0.0",
37
+ "esbuild-jest": "^0.4.0",
38
+ "eslint": "^8.11.0",
39
+ "eslint-config-prettier": "^8.3.0",
40
+ "eslint-plugin-import": "^2.25.4",
41
+ "eslint-plugin-json": "^3.1.0",
42
+ "eslint-plugin-markdown": "^3.0.0",
43
+ "husky": "^8.0.1",
44
+ "jest": "^29.4.3",
45
+ "jest-cli": "^29.4.3",
46
+ "jest-sonar-reporter": "^2.0.0",
47
+ "lint-staged": "^13.1.2",
48
+ "prettier": "^2.6.2",
49
+ "prettier-plugin-sh": "^0.12.8",
50
+ "rimraf": "^5.0.0",
51
+ "typedoc": "^0.24.4",
52
+ "typedoc-plugin-markdown": "^3.14.0",
53
+ "typescript": "^5.0.4"
54
+ },
55
+ "jestSonar": {
56
+ "reportPath": ".sonar",
57
+ "reportFile": "test-report.xml"
58
+ }
59
+
60
+ ,"publishConfig": {
61
+ "access": "public"
62
+ }
63
+ }
@@ -0,0 +1,8 @@
1
+ sonar.projectKey=tomassaboldev_aws-services_bb5f4f68-64f8-4726-bdd4-579ceadf172d
2
+ sonar.sources=src
3
+ sonar.tests=test
4
+ sonar.exclusions=src/logger.ts
5
+ sonar.scm.provider=git
6
+ sonar.qualitygate.wait=true
7
+ sonar.javascript.lcov.reportPaths=.coverage/lcov.info
8
+ sonar.testExecutionReportPaths=.sonar/test-report.xml
@@ -0,0 +1,20 @@
1
+ import {
2
+ AppConfigClient,
3
+ GetApplicationCommand,
4
+ } from "@aws-sdk/client-appconfig"
5
+ import { logger } from "./logger"
6
+
7
+ export const appConfigClient = new AppConfigClient({})
8
+
9
+ export const getApplication = async (applicationId: string) => {
10
+ try {
11
+ return await appConfigClient.send(
12
+ new GetApplicationCommand({
13
+ ApplicationId: applicationId,
14
+ })
15
+ )
16
+ } catch (error) {
17
+ logger.error("getApplication", { error, applicationId })
18
+ throw error
19
+ }
20
+ }
@@ -0,0 +1,10 @@
1
+ export const mockDynamodbService = {
2
+ client: {
3
+ send: jest.fn(),
4
+ },
5
+ documentClient: {
6
+ send: jest.fn(),
7
+ },
8
+ }
9
+
10
+ jest.mock("@tomassabol/aws-services/dynamodb", () => mockDynamodbService)
@@ -0,0 +1,7 @@
1
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb"
2
+ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"
3
+
4
+ export const client: DynamoDBClient = new DynamoDBClient({})
5
+
6
+ export const documentClient: DynamoDBDocumentClient =
7
+ DynamoDBDocumentClient.from(client)
@@ -0,0 +1,5 @@
1
+ export const mockEventbridgeService = {
2
+ sendEvent: jest.fn(),
3
+ }
4
+
5
+ jest.mock("@tomassabol/aws-services/eventbridge", () => mockEventbridgeService)
@@ -0,0 +1,47 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import {
3
+ EventBridgeClient,
4
+ PutEventsCommand,
5
+ } from "@aws-sdk/client-eventbridge"
6
+ import { logger } from "./logger"
7
+
8
+ export const client = new EventBridgeClient({})
9
+
10
+ export async function sendEvent(params: {
11
+ source: string
12
+ eventBusName: string
13
+ eventType: string
14
+ event: any
15
+ }) {
16
+ const { eventBusName, eventType, event, source } = params
17
+
18
+ const input = {
19
+ Entries: [
20
+ {
21
+ EventBusName: eventBusName,
22
+ Source: source,
23
+ DetailType: eventType,
24
+ Detail: JSON.stringify(event),
25
+ },
26
+ ],
27
+ }
28
+
29
+ const command = new PutEventsCommand(input)
30
+
31
+ try {
32
+ const result = await client.send(command)
33
+
34
+ /* Check if there are any failed events */
35
+ if (result.FailedEntryCount) {
36
+ const errorDetail = result.Entries?.length
37
+ ? `${result.Entries[0].ErrorCode}: ${result.Entries[0].ErrorMessage}}`
38
+ : "unknown"
39
+ throw new Error(`PutEventsCommand error ${errorDetail}`)
40
+ }
41
+
42
+ logger.debug("sendEvent success", { input, result })
43
+ } catch (error) {
44
+ logger.error("sendEvent error", { input, error })
45
+ throw error
46
+ }
47
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { Logger } from "@aws-lambda-powertools/logger"
2
+
3
+ const noLogger = {
4
+ log: () => undefined,
5
+ debug: () => undefined,
6
+ info: () => undefined,
7
+ error: () => undefined,
8
+ warn: () => undefined,
9
+ } as typeof console
10
+
11
+ export const logger =
12
+ process.env.JEST_WORKER_ID === undefined || process.env.TEST_LOGGER
13
+ ? new Logger({ logLevel: "DEBUG" })
14
+ : noLogger
package/src/s3.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { S3, PutObjectCommandOutput } from "@aws-sdk/client-s3"
2
+ import { logger } from "./logger"
3
+
4
+ export const client = new S3({})
5
+
6
+ export async function putObjectToS3(params: {
7
+ bucket: string
8
+ key: string
9
+ body: string | Uint8Array | Buffer
10
+ contentEncoding: string
11
+ contentType: string
12
+ }): Promise<PutObjectCommandOutput> {
13
+ const { bucket, key, body, contentEncoding, contentType } = params
14
+
15
+ try {
16
+ return await client.putObject({
17
+ Bucket: bucket,
18
+ Key: key,
19
+ Body: body,
20
+ ContentEncoding: contentEncoding,
21
+ ContentType: contentType,
22
+ })
23
+ } catch (error) {
24
+ logger.error("putObjectToS3", {
25
+ error,
26
+ params: { bucket, key, contentEncoding, contentType },
27
+ })
28
+ throw error
29
+ }
30
+ }
31
+
32
+ export async function getObjectFromS3(params: {
33
+ bucket: string
34
+ key: string
35
+ }): Promise<Uint8Array> {
36
+ try {
37
+ const { bucket, key } = params
38
+
39
+ const result = await client.getObject({
40
+ Bucket: bucket,
41
+ Key: key,
42
+ })
43
+
44
+ const body = result.Body
45
+
46
+ if (body === undefined) {
47
+ logger.error("Body not found", { params })
48
+ throw new Error("Body not found")
49
+ }
50
+
51
+ return await body.transformToByteArray()
52
+ } catch (error) {
53
+ logger.error("getObjectFromS3", { error, params })
54
+ throw error
55
+ }
56
+ }
@@ -0,0 +1,9 @@
1
+ export const mockSecretsManagerService = {
2
+ getSecret: jest.fn(),
3
+ getSecretAsObject: jest.fn(),
4
+ }
5
+
6
+ jest.mock(
7
+ "@tomassabol/aws-services/secrets-manager",
8
+ () => mockSecretsManagerService
9
+ )
@@ -0,0 +1,37 @@
1
+ import {
2
+ SecretsManagerClient,
3
+ GetSecretValueCommand,
4
+ } from "@aws-sdk/client-secrets-manager"
5
+ import assert from "assert"
6
+ import { logger } from "./logger"
7
+
8
+ export const client = new SecretsManagerClient({})
9
+
10
+ export async function getSecret(secretName: string): Promise<string> {
11
+ try {
12
+ const response = await client.send(
13
+ new GetSecretValueCommand({
14
+ SecretId: secretName,
15
+ })
16
+ )
17
+ const value = response.SecretString
18
+ assert(value, "SecretString value not found")
19
+ return value
20
+ } catch (error) {
21
+ logger.error("getSecret", { error, secretName })
22
+ // For a list of exceptions thrown, see
23
+ // https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
24
+ throw error
25
+ }
26
+ }
27
+
28
+ export async function getSecretAsObject<T = unknown>(
29
+ secretName: string
30
+ ): Promise<T> {
31
+ const value = await getSecret(secretName)
32
+ try {
33
+ return JSON.parse(value)
34
+ } catch (error) {
35
+ throw Error(`Cannot parse secret ${secretName}. Expected JSON.`)
36
+ }
37
+ }
package/src/sns.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { SNSClient, PublishCommand } from "@aws-sdk/client-sns"
2
+ import { logger } from "./logger"
3
+
4
+ export const snsClient = new SNSClient({})
5
+
6
+ export const publishMessageToSNS = async (params: {
7
+ topicArn: string
8
+ message: string
9
+ options?: object
10
+ }) => {
11
+ try {
12
+ const { topicArn, message, options } = params
13
+ return await snsClient.send(
14
+ new PublishCommand({
15
+ TopicArn: topicArn,
16
+ Message: message,
17
+ ...options,
18
+ })
19
+ )
20
+ } catch (error) {
21
+ logger.error("publishMessageToSNS", { error, params })
22
+ throw error
23
+ }
24
+ }
@@ -0,0 +1,5 @@
1
+ export const mockSqsService = {
2
+ sendSqsMessage: jest.fn(),
3
+ }
4
+
5
+ jest.mock("@tomassabol/aws-services/sqs", () => mockSqsService)
package/src/sqs.ts ADDED
@@ -0,0 +1,32 @@
1
+ import {
2
+ SQSClient,
3
+ SendMessageCommand,
4
+ SendMessageCommandInput,
5
+ } from "@aws-sdk/client-sqs"
6
+ import { logger } from "./logger"
7
+
8
+ export const client = new SQSClient({})
9
+
10
+ export async function sendSqsMessage(params: {
11
+ queueUrl: string
12
+ message: string | object
13
+ }): Promise<string | undefined> {
14
+ const { queueUrl, message } = params
15
+
16
+ try {
17
+ const body = typeof message === "object" ? JSON.stringify(message) : message
18
+
19
+ const input: SendMessageCommandInput = {
20
+ QueueUrl: queueUrl,
21
+ MessageBody: body,
22
+ }
23
+
24
+ const command = new SendMessageCommand(input)
25
+
26
+ const response = await client.send(command)
27
+ return response.MessageId
28
+ } catch (error) {
29
+ logger.error("sendSqsMessage", { error, params })
30
+ throw error
31
+ }
32
+ }
@@ -0,0 +1,5 @@
1
+ export const mockSsmService = {
2
+ getSsmParameters: jest.fn(),
3
+ }
4
+
5
+ jest.mock("@tomassabol/aws-services/ssm", () => mockSsmService)
package/src/ssm.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { SSMClient, GetParametersCommand } from "@aws-sdk/client-ssm"
2
+ import assert = require("assert")
3
+
4
+ export const client = new SSMClient({})
5
+
6
+ export type SSMParameters = {
7
+ [name: string]: string
8
+ }
9
+
10
+ /**
11
+ * Get a list of parameters from SSM Parameter Store
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const params = await getSsmParameters({
16
+ * first: "/my-params/first",
17
+ * second: "/my-params/second",
18
+ * })
19
+ *
20
+ * // params = { first: "value1", second: "value2" }
21
+ * ```
22
+ */
23
+
24
+ export async function getSsmParameters<T extends SSMParameters>(
25
+ parameters: T
26
+ ): Promise<T> {
27
+ /*
28
+ * Fetch parameters from SSM
29
+ */
30
+
31
+ const command = new GetParametersCommand({
32
+ Names: Object.values(parameters),
33
+ })
34
+
35
+ const output = await client.send(command)
36
+
37
+ /*
38
+ * Map returned parameters to result object
39
+ */
40
+
41
+ const result: Record<string, string> = {}
42
+ output.Parameters?.forEach((outputParam) => {
43
+ assert(
44
+ typeof outputParam.Value === "string",
45
+ `Received invalid value of SSM parameter ${outputParam.Name}`
46
+ )
47
+ const entry = Object.entries(parameters).find(
48
+ ([_key, value]) => value === outputParam.Name
49
+ )
50
+ assert(entry, `Received invalid SSM parameter ${outputParam.Name}`)
51
+ const [key] = entry
52
+ result[key] = outputParam.Value
53
+ })
54
+
55
+ /*
56
+ * Check that all parameters were received
57
+ */
58
+
59
+ const notFoundParams = Object.keys(parameters).filter(
60
+ (key) => false === Object.hasOwn(result, key)
61
+ )
62
+
63
+ if (notFoundParams.length > 0) {
64
+ throw new Error(
65
+ `Cannot obtain SSM parameters: ${notFoundParams.join(", ")}`
66
+ )
67
+ }
68
+
69
+ return result as T
70
+ }
@@ -0,0 +1,45 @@
1
+ import { GetApplicationCommand } from "@aws-sdk/client-appconfig"
2
+ import { getApplication, appConfigClient } from "../src/appconfig"
3
+
4
+ jest.mock("@aws-sdk/client-appconfig", () => {
5
+ return {
6
+ AppConfigClient: jest.fn().mockImplementation(() => {
7
+ return { send: jest.fn() }
8
+ }),
9
+ GetApplicationCommand: jest.fn(),
10
+ }
11
+ })
12
+
13
+ describe("AppConfig", () => {
14
+ let getApplicationCommandMock: jest.Mock
15
+
16
+ beforeEach(() => {
17
+ getApplicationCommandMock = GetApplicationCommand as unknown as jest.Mock
18
+ })
19
+
20
+ afterEach(() => {
21
+ jest.clearAllMocks()
22
+ })
23
+
24
+ it("should call AppConfig.getApplication with correct parameters", async () => {
25
+ const applicationId = "test-app"
26
+
27
+ await getApplication(applicationId)
28
+
29
+ expect(appConfigClient.send).toHaveBeenCalledWith(
30
+ expect.any(getApplicationCommandMock)
31
+ )
32
+ expect(getApplicationCommandMock).toHaveBeenCalledWith({
33
+ ApplicationId: applicationId,
34
+ })
35
+ })
36
+
37
+ it("should throw an error if getting application from AppConfig fails", async () => {
38
+ const applicationId = "test-app"
39
+ const error = new Error("Get application failed")
40
+
41
+ ;(appConfigClient.send as jest.Mock).mockRejectedValueOnce(error)
42
+
43
+ await expect(getApplication(applicationId)).rejects.toThrow(error)
44
+ })
45
+ })
@@ -0,0 +1,13 @@
1
+ import { client, documentClient } from "../src/dynamodb"
2
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb"
3
+ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"
4
+
5
+ describe("dynamodb", () => {
6
+ it("should be instance of DynamoDBClient", () => {
7
+ expect(client).toBeInstanceOf(DynamoDBClient)
8
+ })
9
+
10
+ it("should be instance of DynamoDBDocumentClient", () => {
11
+ expect(documentClient).toBeInstanceOf(DynamoDBDocumentClient)
12
+ })
13
+ })
@@ -0,0 +1,35 @@
1
+ import { mockClient } from "aws-sdk-client-mock"
2
+ import { PutEventsCommand } from "@aws-sdk/client-eventbridge"
3
+ import { client, sendEvent } from "../src/eventbridge"
4
+
5
+ const eventBridgeMock = mockClient(client)
6
+
7
+ describe("eventbridge", () => {
8
+ describe("sendEvent", () => {
9
+ test("should send event to event bus", async () => {
10
+ eventBridgeMock.on(PutEventsCommand).resolves({})
11
+
12
+ const params = {
13
+ source: "source",
14
+ eventBusName: "event-bus-name",
15
+ eventType: "event-type",
16
+ event: { foo: "bar" },
17
+ }
18
+
19
+ await expect(sendEvent(params)).resolves.toBe(undefined)
20
+ })
21
+
22
+ test("should fail", async () => {
23
+ eventBridgeMock.on(PutEventsCommand).rejects(new Error("fail"))
24
+
25
+ const params = {
26
+ source: "source",
27
+ eventBusName: "event-bus-name",
28
+ eventType: "event-type",
29
+ event: { foo: "bar" },
30
+ }
31
+
32
+ await expect(sendEvent(params)).rejects.toThrowError("fail")
33
+ })
34
+ })
35
+ })
@@ -0,0 +1,78 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { mockClient } from "aws-sdk-client-mock"
3
+ import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"
4
+ import { Readable } from "stream"
5
+ import { sdkStreamMixin } from "@aws-sdk/util-stream-node"
6
+ import { client, getObjectFromS3, putObjectToS3 } from "../src/s3"
7
+
8
+ const s3Mock = mockClient(client)
9
+
10
+ describe("S3", () => {
11
+ describe("putObjectToS3()", () => {
12
+ test("should put object to S3", async () => {
13
+ s3Mock.on(PutObjectCommand).resolves({
14
+ ETag: "etag",
15
+ })
16
+
17
+ const params = {
18
+ bucket: "bucket",
19
+ key: "key",
20
+ body: "body",
21
+ contentEncoding: "content-encoding",
22
+ contentType: "content-type",
23
+ }
24
+
25
+ await expect(putObjectToS3(params)).resolves.toEqual({ ETag: "etag" })
26
+ })
27
+
28
+ test("should fail when put object to S3 fails", async () => {
29
+ s3Mock.on(PutObjectCommand).rejects(new Error("fail"))
30
+
31
+ await expect(putObjectToS3({} as any)).rejects.toThrowError("fail")
32
+ })
33
+ })
34
+
35
+ describe("getObjectFromS3()", () => {
36
+ test("should get object from S3", async () => {
37
+ s3Mock.on(GetObjectCommand).resolves({
38
+ ETag: "etag",
39
+ Body: sdkStreamMixin(Readable.from("object")),
40
+ })
41
+
42
+ const params = {
43
+ bucket: "bucket",
44
+ key: "key",
45
+ }
46
+
47
+ const data = (await getObjectFromS3(params)) as any
48
+
49
+ expect(data).toMatchInlineSnapshot(`
50
+ Uint8Array [
51
+ 111,
52
+ 98,
53
+ 106,
54
+ 101,
55
+ 99,
56
+ 116,
57
+ ]
58
+ `)
59
+ })
60
+
61
+ test("should fail when get object body is undefined", async () => {
62
+ s3Mock.on(GetObjectCommand).resolves({
63
+ ETag: "etag",
64
+ Body: undefined,
65
+ })
66
+
67
+ await expect(getObjectFromS3({} as any)).rejects.toThrowError(
68
+ "Body not found"
69
+ )
70
+ })
71
+
72
+ test("should fail when get object from S3 fails", async () => {
73
+ s3Mock.on(GetObjectCommand).rejects(new Error("fail"))
74
+
75
+ await expect(getObjectFromS3({} as any)).rejects.toThrowError("fail")
76
+ })
77
+ })
78
+ })
@@ -0,0 +1,55 @@
1
+ import { mockClient } from "aws-sdk-client-mock"
2
+ import { GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"
3
+ import { client, getSecret, getSecretAsObject } from "../src/secrets-manager"
4
+
5
+ const secretsManagerMock = mockClient(client)
6
+
7
+ describe("secrets-manager", () => {
8
+ describe("getSecret()", () => {
9
+ test("should retrieve secret string", async () => {
10
+ secretsManagerMock.on(GetSecretValueCommand).resolves({
11
+ SecretString: "secret-string",
12
+ })
13
+
14
+ await expect(getSecret("secret-name")).resolves.toBe("secret-string")
15
+ })
16
+
17
+ test("should fail when string empty", async () => {
18
+ secretsManagerMock.on(GetSecretValueCommand).resolves({
19
+ SecretString: "",
20
+ })
21
+
22
+ await expect(getSecret("secret-name")).rejects.toThrow(
23
+ "SecretString value not found"
24
+ )
25
+ })
26
+
27
+ test("should fail", async () => {
28
+ secretsManagerMock.on(GetSecretValueCommand).rejects(new Error("fail"))
29
+
30
+ await expect(getSecret("secret-name")).rejects.toThrowError("fail")
31
+ })
32
+ })
33
+
34
+ describe("getSecretAsObject()", () => {
35
+ test("should return secret object", async () => {
36
+ secretsManagerMock.on(GetSecretValueCommand).resolves({
37
+ SecretString: JSON.stringify({ foo: "bar" }),
38
+ })
39
+
40
+ await expect(getSecretAsObject("secret-name")).resolves.toEqual({
41
+ foo: "bar",
42
+ })
43
+ })
44
+
45
+ test("should fail when invalid object", async () => {
46
+ secretsManagerMock.on(GetSecretValueCommand).resolves({
47
+ SecretString: "invalid-object",
48
+ })
49
+
50
+ await expect(getSecretAsObject("secret-name")).rejects.toThrowError(
51
+ "Cannot parse secret secret-name. Expected JSON."
52
+ )
53
+ })
54
+ })
55
+ })
@@ -0,0 +1,59 @@
1
+ import { SNSClient, PublishCommand } from "@aws-sdk/client-sns"
2
+ import { publishMessageToSNS, snsClient } from "../src/sns"
3
+
4
+ jest.mock("@aws-sdk/client-sns", () => {
5
+ return {
6
+ SNSClient: jest.fn(() => {
7
+ return {
8
+ send: jest.fn(),
9
+ }
10
+ }),
11
+ PublishCommand: jest.fn(),
12
+ }
13
+ })
14
+
15
+ describe("SNS", () => {
16
+ beforeEach(() => {
17
+ ;(SNSClient as jest.Mock).mockClear()
18
+ ;(PublishCommand as unknown as jest.Mock).mockClear()
19
+ ;(snsClient.send as jest.Mock).mockClear()
20
+ })
21
+
22
+ it("should create instance of SNSClient", () => {
23
+ expect(snsClient).toBeDefined()
24
+ })
25
+
26
+ it("should call publishSNSMessage successfully", async () => {
27
+ const sendMock = jest.fn()
28
+ ;(snsClient.send as jest.Mock) = sendMock
29
+
30
+ const params = {
31
+ topicArn: "arn:aws:sns:eu-central-1:123456789012:MyTopic",
32
+ message: "Test message",
33
+ }
34
+
35
+ await publishMessageToSNS(params)
36
+ expect(sendMock).toBeCalledTimes(1)
37
+ expect(PublishCommand).toHaveBeenCalledWith({
38
+ TopicArn: params.topicArn,
39
+ Message: params.message,
40
+ })
41
+ })
42
+
43
+ it("should throw an error if publishSNSMessage fails", async () => {
44
+ const sendMock = jest
45
+ .fn()
46
+ .mockRejectedValue(new Error("Failed to send message"))
47
+ ;(snsClient.send as jest.Mock) = sendMock
48
+
49
+ const params = {
50
+ topicArn: "arn:aws:sns:euj-central-1:123456789012:MyTopic",
51
+ message: "Test message",
52
+ }
53
+
54
+ await expect(publishMessageToSNS(params)).rejects.toThrow(
55
+ "Failed to send message"
56
+ )
57
+ expect(sendMock).toBeCalledTimes(1)
58
+ })
59
+ })
@@ -0,0 +1,46 @@
1
+ import { mockClient } from "aws-sdk-client-mock"
2
+ import { SendMessageCommand } from "@aws-sdk/client-sqs"
3
+ import { client, sendSqsMessage } from "../src/sqs"
4
+
5
+ const sqsMock = mockClient(client)
6
+
7
+ describe("sqs", () => {
8
+ describe("sendSqsMessage()", () => {
9
+ test("should send string as message to SQS", async () => {
10
+ sqsMock.on(SendMessageCommand).resolves({
11
+ MessageId: "message-id",
12
+ })
13
+
14
+ const params = {
15
+ queueUrl: "queue-url",
16
+ message: "message",
17
+ }
18
+
19
+ await expect(sendSqsMessage(params)).resolves.toBe("message-id")
20
+ })
21
+
22
+ test("should send stringified object as message to SQS", async () => {
23
+ sqsMock.on(SendMessageCommand).resolves({
24
+ MessageId: "message-id",
25
+ })
26
+
27
+ const params = {
28
+ queueUrl: "queue-url",
29
+ message: { foo: "bar" },
30
+ }
31
+
32
+ await expect(sendSqsMessage(params)).resolves.toBe("message-id")
33
+ })
34
+
35
+ test("should fail", async () => {
36
+ sqsMock.on(SendMessageCommand).rejects(new Error("fail"))
37
+
38
+ const params = {
39
+ queueUrl: "queue-url",
40
+ message: "message",
41
+ }
42
+
43
+ await expect(sendSqsMessage(params)).rejects.toThrowError("fail")
44
+ })
45
+ })
46
+ })
@@ -0,0 +1,85 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { mockClient } from "aws-sdk-client-mock"
3
+ import { GetParametersCommand } from "@aws-sdk/client-ssm"
4
+ import { client, getSsmParameters } from "../src/ssm"
5
+
6
+ const ssmMock = mockClient(client)
7
+
8
+ describe("ssm", () => {
9
+ describe("getSsmParameters()", () => {
10
+ test("should return paramaters", async () => {
11
+ ssmMock.on(GetParametersCommand).resolves({
12
+ Parameters: [
13
+ { Name: "/my-params/foo", Value: "bar" },
14
+ { Name: "/my-params/baz", Value: "qux" },
15
+ ],
16
+ })
17
+
18
+ const params = {
19
+ foo: "/my-params/foo",
20
+ baz: "/my-params/baz",
21
+ }
22
+
23
+ await expect(getSsmParameters(params)).resolves.toEqual({
24
+ foo: "bar",
25
+ baz: "qux",
26
+ })
27
+ })
28
+
29
+ test("should fail when SSM parameter name is not string", async () => {
30
+ ssmMock.on(GetParametersCommand).resolves({
31
+ Parameters: [{ Name: 42 as any, Value: "bar" }],
32
+ })
33
+
34
+ const params = {
35
+ foo: "/my-params/foo",
36
+ }
37
+
38
+ await expect(getSsmParameters(params)).rejects.toThrow(
39
+ "Received invalid SSM parameter 42"
40
+ )
41
+ })
42
+
43
+ test("should fail when SSM parameter does not contain Parameters", async () => {
44
+ ssmMock.on(GetParametersCommand).resolves({})
45
+
46
+ const params = {
47
+ foo: "/my-params/foo",
48
+ }
49
+
50
+ await expect(getSsmParameters(params)).rejects.toThrow(
51
+ "Cannot obtain SSM parameters: foo"
52
+ )
53
+ })
54
+
55
+ test("should fail when SSM parameter value is not string", async () => {
56
+ ssmMock.on(GetParametersCommand).resolves({
57
+ Parameters: [{ Name: "/my-params/foo", Value: 42 as any }],
58
+ })
59
+
60
+ const params = {
61
+ foo: "/my-params/foo",
62
+ }
63
+
64
+ await expect(getSsmParameters(params)).rejects.toThrow(
65
+ "Received invalid value of SSM parameter /my-params/foo"
66
+ )
67
+ })
68
+
69
+ test("should fail when not all parameters are found", async () => {
70
+ ssmMock.on(GetParametersCommand).resolves({
71
+ Parameters: [{ Name: "/my-params/foo", Value: "bar" }],
72
+ })
73
+
74
+ const params = {
75
+ foo: "/my-params/foo",
76
+ baz: "/my-params/baz",
77
+ quux: "/my-params/quux",
78
+ }
79
+
80
+ await expect(getSsmParameters(params)).rejects.toThrow(
81
+ "Cannot obtain SSM parameters: baz, quux"
82
+ )
83
+ })
84
+ })
85
+ })
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig",
3
+ "include": ["src"]
4
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@tsconfig/node18",
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "declaration": true,
6
+ "outDir": "dist"
7
+ },
8
+ "include": ["src", "test"]
9
+ }
package/typedoc.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "entryPoints": ["src/index.ts"],
3
+ "readme": "none"
4
+ }