diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 880e13ea2a..d93cb69d0e 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # See here for image contents: https://github.com/microsoft/vscode-dev-containers/blob/v0.212.0/containers/javascript-node/.devcontainer/base.Dockerfile -FROM mcr.microsoft.com/vscode/devcontainers/javascript-node@sha256:896bfba10582c9239d1c36bab53b80af06253019f62b846fa440ee643ca63eb1 +FROM mcr.microsoft.com/vscode/devcontainers/javascript-node@sha256:78fda8c284dd3247d7385d55974e278314233f1acc130ba89757703137dbda45 # Install fnm to manage Node.js versions RUN curl -fsSL https://fnm.vercel.app/install -o /tmp/install \ diff --git a/.github/scripts/get_pr_info.js b/.github/scripts/get_pr_info.js deleted file mode 100644 index ece2f97f44..0000000000 --- a/.github/scripts/get_pr_info.js +++ /dev/null @@ -1,30 +0,0 @@ -module.exports = async ({ github, context, core }) => { - const prNumber = process.env.PR_NUMBER; - - if (prNumber === '') { - core.setFailed(`No PR number was passed. Aborting`); - } - - // Remove the `#` prefix from the PR number if it exists - const prNumberWithoutPrefix = prNumber.replace('#', ''); - - try { - const { - data: { head, base }, - } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: prNumberWithoutPrefix, - }); - - core.setOutput('headRef', head.ref); - core.setOutput('headSHA', head.sha); - core.setOutput('baseRef', base.ref); - core.setOutput('baseSHA', base.sha); - } catch (error) { - core.setFailed( - `Unable to retrieve info from PR number ${prNumber}.\n\n Error details: ${error}` - ); - throw error; - } -}; diff --git a/.github/workflows/bootstrap_region.yml b/.github/workflows/bootstrap_region.yml index 1bac0f82fa..b22fa47028 100644 --- a/.github/workflows/bootstrap_region.yml +++ b/.github/workflows/bootstrap_region.yml @@ -47,7 +47,7 @@ jobs: with: ref: ${{ github.sha }} - name: Setup Node.js - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: "22" - name: Setup dependencies @@ -88,7 +88,7 @@ jobs: mask-aws-account-id: true - id: go-setup name: Setup Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version: '>=1.23.0' - id: go-env diff --git a/.github/workflows/layer_balance.yml b/.github/workflows/layer_balance.yml index a1df2a8f44..bf02146db5 100644 --- a/.github/workflows/layer_balance.yml +++ b/.github/workflows/layer_balance.yml @@ -50,7 +50,7 @@ jobs: mask-aws-account-id: true - id: go-setup name: Setup Go - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version: '>=1.23.0' - id: go-env diff --git a/.github/workflows/layer_govcloud_verify.yml b/.github/workflows/layer_govcloud_verify.yml new file mode 100644 index 0000000000..8d5899b05e --- /dev/null +++ b/.github/workflows/layer_govcloud_verify.yml @@ -0,0 +1,108 @@ +# GovCloud Layer Verification +# --- +# This workflow queries the GovCloud layer info in production only + +on: + workflow_dispatch: + inputs: + environment: + description: Deployment environment + type: choice + options: + - Gamma + - Prod + required: true + version: + description: Layer version to verify + type: string + required: true + govcloud_version: + description: GovCloud Layer version to verify, this is mostly used in Gamma where a version mismatch might exist + type: string + required: false + + workflow_call: + inputs: + environment: + description: Deployment environment + type: string + required: true + version: + description: Layer version to verify + type: string + required: true + govcloud_version: + description: GovCloud Layer version to verify, this is mostly used in Gamma where a version mismatch might exist + type: string + required: false + +name: Layer Verification (GovCloud) +run-name: Layer Verification (GovCloud) / Version ${{ inputs.version }} + +permissions: {} + +jobs: + commercial: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + environment: Prod (Readonly) + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-east-1 + mask-aws-account-id: true + - name: Output AWSLambdaPowertoolsTypeScriptV2 + # fetch the specific layer version information from the us-east-1 commercial region + run: | + aws --region us-east-1 lambda get-layer-version-by-arn --arn 'arn:aws:lambda:us-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:${{ inputs.version }}' > AWSLambdaPowertoolsTypeScriptV2.json + - name: Store Metadata + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: AWSLambdaPowertoolsTypeScriptV2.json + path: AWSLambdaPowertoolsTypeScriptV2.json + retention-days: 1 + if-no-files-found: error + + verify: + name: Verify + needs: commercial + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + environment: GovCloud ${{ inputs.environment }} + strategy: + matrix: + region: + - us-gov-east-1 + - us-gov-west-1 + steps: + - name: Download Metadata + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + with: + name: AWSLambdaPowertoolsTypeScriptV2.json + - id: transform + run: | + echo 'CONVERTED_REGION=${{ matrix.region }}' | tr 'a-z\-' 'A-Z_' >> "$GITHUB_OUTPUT" + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 + with: + role-to-assume: ${{ secrets[format('IAM_ROLE_{0}', steps.transform.outputs.CONVERTED_REGION)] }} + aws-region: ${{ matrix.region}} + mask-aws-account-id: true + - id: govcloud_version + name: GovCloud Layer Version + run: | + echo 'govcloud_version=$([[ -n "${{ inputs.govcloud_version}}" ]] && echo ${{ inputs.govcloud_version}} || echo ${{ inputs.version }} )' >> "$GITHUB_OUTPUT" + - name: Verify Layer + run: | + export layer_output='AWSLambdaPowertoolsTypeScriptV2-${{matrix.region}}.json' + aws --region ${{ matrix.region}} lambda get-layer-version-by-arn --arn "arn:aws-us-gov:lambda:${{ matrix.region}}:${{ secrets[format('AWS_ACCOUNT_{0}', steps.transform.outputs.CONVERTED_REGION)] }}:layer:AWSLambdaPowertoolsTypeScriptV2:${{ steps.govcloud_version.outputs.govcloud_version }}" > $layer_output + REMOTE_SHA=$(jq -r '.Content.CodeSha256' $layer_output) + LOCAL_SHA=$(jq -r '.Content.CodeSha256' AWSLambdaPowertoolsTypeScriptV2.json) + test "$REMOTE_SHA" == "$LOCAL_SHA" && echo "SHA OK: ${LOCAL_SHA}" || exit 1 + jq -s -r '["Layer Arn", "Runtimes", "Version", "Description", "SHA256"], ([.[0], .[1]] | .[] | [.LayerArn, (.CompatibleRuntimes | join("/")), .Version, .Description, .Content.CodeSha256]) |@tsv' AWSLambdaPowertoolsTypeScriptV2.json $layer_output | column -t -s $'\t' \ No newline at end of file diff --git a/.github/workflows/layers_govcloud.yml b/.github/workflows/layers_govcloud.yml new file mode 100644 index 0000000000..51d3d29db7 --- /dev/null +++ b/.github/workflows/layers_govcloud.yml @@ -0,0 +1,145 @@ +name: Layer Deployment (GovCloud) + +# GovCloud Layer Publish +# --- +# This workflow publishes a specific layer version in an AWS account based on the environment input. +# +# We pull each the version of the layer and store them as artifacts, the we upload them to each of the GovCloud AWS accounts. +# +# A number of safety checks are performed to ensure safety. + +on: + workflow_dispatch: + inputs: + environment: + description: Deployment environment + type: choice + options: + - Gamma + - Prod + required: true + version: + description: Layer version to duplicate + type: string + required: true + workflow_call: + inputs: + environment: + description: Deployment environment + type: string + required: true + version: + description: Layer version to duplicate + type: string + required: true + +run-name: Layer Deployment (GovCloud) - ${{ inputs.environment }} / Version - ${{ inputs.version }} + +permissions: + contents: read + +jobs: + download: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + environment: Prod (Readonly) + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 + with: + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + aws-region: us-east-1 + mask-aws-account-id: true + - name: Grab Zip + run: | + aws --region us-east-1 lambda get-layer-version-by-arn --arn arn:aws:lambda:us-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:${{ inputs.version }} --query 'Content.Location' | xargs curl -L -o AWSLambdaPowertoolsTypeScriptV2.zip + aws --region us-east-1 lambda get-layer-version-by-arn --arn arn:aws:lambda:us-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:${{ inputs.version }} > AWSLambdaPowertoolsTypeScriptV2.json + - name: Store Zip + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: AWSLambdaPowertoolsTypeScriptV2.zip + path: AWSLambdaPowertoolsTypeScriptV2.zip + retention-days: 1 + if-no-files-found: error + - name: Store Metadata + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: AWSLambdaPowertoolsTypeScriptV2.json + path: AWSLambdaPowertoolsTypeScriptV2.json + retention-days: 1 + if-no-files-found: error + + copy: + name: Copy + needs: download + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + environment: GovCloud ${{ inputs.environment }} + strategy: + matrix: + region: + - us-gov-east-1 + - us-gov-west-1 + steps: + - name: Download Zip + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + with: + name: AWSLambdaPowertoolsTypeScriptV2.zip + - name: Download Metadata + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + with: + name: AWSLambdaPowertoolsTypeScriptV2.json + - name: Verify Layer Signature + run: | + SHA=$(jq -r '.Content.CodeSha256' 'AWSLambdaPowertoolsTypeScriptV2.json') + test "$(openssl dgst -sha256 -binary AWSLambdaPowertoolsTypeScriptV2.zip | openssl enc -base64)" == "$SHA" && echo "SHA OK: ${SHA}" || exit 1 + - id: transform + run: | + echo 'CONVERTED_REGION=${{ matrix.region }}' | tr 'a-z\-' 'A-Z_' >> "$GITHUB_OUTPUT" + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 + with: + role-to-assume: ${{ secrets[format('IAM_ROLE_{0}', steps.transform.outputs.CONVERTED_REGION)] }} + aws-region: ${{ matrix.region}} + mask-aws-account-id: true + - name: Create Layer + id: create-layer + run: | + cat AWSLambdaPowertoolsTypeScriptV2.json | jq '{"LayerName": "AWSLambdaPowertoolsTypeScriptV2", "Description": .Description, "CompatibleRuntimes": .CompatibleRuntimes, "LicenseInfo": .LicenseInfo}' > input.json + + LAYER_VERSION=$(aws --region ${{ matrix.region}} lambda publish-layer-version \ + --zip-file fileb://./AWSLambdaPowertoolsTypeScriptV2.zip \ + --cli-input-json file://./input.json \ + --query 'Version' \ + --output text) + + echo "LAYER_VERSION=$LAYER_VERSION" >> "$GITHUB_OUTPUT" + + aws --region ${{ matrix.region}} lambda add-layer-version-permission \ + --layer-name 'AWSLambdaPowertoolsTypeScriptV2' \ + --statement-id 'PublicLayer' \ + --action lambda:GetLayerVersion \ + --principal '*' \ + --version-number "$LAYER_VERSION" + - name: Verify Layer + env: + LAYER_VERSION: ${{ steps.create-layer.outputs.LAYER_VERSION }} + run: | + export layer_output='AWSLambdaPowertoolsTypeScriptV2-${{matrix.region}}.json' + aws --region ${{ matrix.region}} lambda get-layer-version-by-arn --arn 'arn:aws-us-gov:lambda:${{ matrix.region}}:${{ secrets[format('AWS_ACCOUNT_{0}', steps.transform.outputs.CONVERTED_REGION)] }}:layer:AWSLambdaPowertoolsTypeScriptV2:${{ env.LAYER_VERSION }}' > $layer_output + REMOTE_SHA=$(jq -r '.Content.CodeSha256' $layer_output) + LOCAL_SHA=$(jq -r '.Content.CodeSha256' AWSLambdaPowertoolsTypeScriptV2.json) + test "$REMOTE_SHA" == "$LOCAL_SHA" && echo "SHA OK: ${LOCAL_SHA}" || exit 1 + jq -s -r '["Layer Arn", "Runtimes", "Version", "Description", "SHA256"], ([.[0], .[1]] | .[] | [.LayerArn, (.CompatibleRuntimes | join("/")), .Version, .Description, .Content.CodeSha256]) |@tsv' AWSLambdaPowertoolsTypeScriptV2.json $layer_output | column -t -s $'\t' + + - name: Store Metadata - ${{ matrix.region }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: AWSLambdaPowertoolsTypeScriptV2-${{ matrix.region }}.json + path: AWSLambdaPowertoolsTypeScriptV2-${{ matrix.region }}.json + retention-days: 1 + if-no-files-found: error \ No newline at end of file diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml index 8cfe629f9e..54231b0411 100644 --- a/.github/workflows/make-release.yml +++ b/.github/workflows/make-release.yml @@ -49,7 +49,7 @@ jobs: with: ref: ${{ github.sha }} - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: "22" cache: "npm" diff --git a/.github/workflows/make-version.yml b/.github/workflows/make-version.yml index 051b20f0c4..bf578ce4d7 100644 --- a/.github/workflows/make-version.yml +++ b/.github/workflows/make-version.yml @@ -23,7 +23,7 @@ jobs: ref: ${{ github.ref }} fetch-depth: 0 # fetch all history, commits and tags, lerna scans it to the last tag and looks at commits, we need all of it to determine the next version - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: "22" cache: "npm" diff --git a/.github/workflows/ossf_scorecard.yml b/.github/workflows/ossf_scorecard.yml index 2fc25f122f..2871ecabba 100644 --- a/.github/workflows/ossf_scorecard.yml +++ b/.github/workflows/ossf_scorecard.yml @@ -35,7 +35,7 @@ jobs: # repo_token: ${{ secrets.SCORECARD_TOKEN }} # read-only fine-grained token to read branch protection settings - name: "Upload results" - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: SARIF file path: results.sarif @@ -43,6 +43,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 with: sarif_file: results.sarif diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml index d71bbab195..d43c776124 100644 --- a/.github/workflows/publish-package.yml +++ b/.github/workflows/publish-package.yml @@ -39,7 +39,7 @@ jobs: with: ref: ${{ github.sha }} - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: "22" cache: "npm" diff --git a/.github/workflows/publish_layer.yml b/.github/workflows/publish_layer.yml index 9c311df723..cfa014959b 100644 --- a/.github/workflows/publish_layer.yml +++ b/.github/workflows/publish_layer.yml @@ -35,7 +35,7 @@ jobs: with: ref: ${{ github.sha }} - name: Setup Node.js - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: "22" - name: Setup dependencies @@ -45,7 +45,7 @@ jobs: - name: Zip output run: zip -r cdk.out.zip layers/cdk.out - name: Archive CDK artifacts - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: cdk-layer-artifact path: cdk.out.zip @@ -97,7 +97,7 @@ jobs: with: ref: ${{ github.sha }} - name: Download CDK layer artifacts - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: path: cdk-layer-stack pattern: cdk-layer-stack-* # merge all Layer artifacts created per region earlier (reusable_deploy_layer_stack.yml; step "Save Layer ARN artifact") diff --git a/.github/workflows/quality_check.yml b/.github/workflows/quality_check.yml index 8debce5930..b3de8d873c 100644 --- a/.github/workflows/quality_check.yml +++ b/.github/workflows/quality_check.yml @@ -37,7 +37,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: ${{ matrix.version }} cache: "npm" @@ -66,7 +66,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: 22 cache: "npm" @@ -84,7 +84,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: 22 cache: "npm" @@ -102,7 +102,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: 22 cache: "npm" @@ -118,7 +118,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: 22 cache: "npm" diff --git a/.github/workflows/record_pr.yml b/.github/workflows/record_pr.yml index a1fa413003..f0eacda57a 100644 --- a/.github/workflows/record_pr.yml +++ b/.github/workflows/record_pr.yml @@ -53,7 +53,7 @@ jobs: script: | const script = require('.github/scripts/save_pr_details.js') await script({github, context, core}) - - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: pr path: pr.txt diff --git a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml index 01e7c8fef8..c3d2c4a8e8 100644 --- a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml +++ b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml @@ -58,7 +58,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: ${{ matrix.version }} cache: "npm" @@ -87,7 +87,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: 22 cache: "npm" @@ -105,7 +105,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: 22 cache: "npm" @@ -123,7 +123,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: 22 cache: "npm" @@ -139,7 +139,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: 22 cache: "npm" diff --git a/.github/workflows/reusable_deploy_layer_stack.yml b/.github/workflows/reusable_deploy_layer_stack.yml index 7b56062034..77ef81c3bd 100644 --- a/.github/workflows/reusable_deploy_layer_stack.yml +++ b/.github/workflows/reusable_deploy_layer_stack.yml @@ -75,13 +75,13 @@ jobs: role-to-assume: ${{ secrets.target-account-role }} mask-aws-account-id: true - name: Setup Node.js - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: "22" - name: Setup dependencies uses: aws-powertools/actions/.github/actions/cached-node-modules@29979bc5339bf54f76a11ac36ff67701986bb0f0 - name: Download artifact - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: ${{ inputs.artifact-name }} - name: Unzip artifact @@ -96,7 +96,7 @@ jobs: cat cdk-layer-stack/${{ matrix.region }}-layer-version.txt - name: Save Layer ARN artifact if: ${{ inputs.stage == 'PROD' }} - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: cdk-layer-stack-${{ matrix.region }} path: ./cdk-layer-stack/* # NOTE: upload-artifact does not inherit working-directory setting. diff --git a/.github/workflows/reusable_publish_docs.yml b/.github/workflows/reusable_publish_docs.yml index 15d53aae7c..e485dce686 100644 --- a/.github/workflows/reusable_publish_docs.yml +++ b/.github/workflows/reusable_publish_docs.yml @@ -52,7 +52,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: "22" cache: "npm" @@ -82,21 +82,14 @@ jobs: run: | rm -rf site mkdocs build - - name: Build API docs - run: | - rm -rf api - npm run docs-generateApiDoc - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 with: aws-region: us-east-1 role-to-assume: ${{ secrets.AWS_DOCS_ROLE_ARN }} mask-aws-account-id: true - - name: Copy API Docs - run: | - cp -r api site/ - name: Create Artifact (Site) - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: site path: site diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index c5447a7aaf..0601b33d7f 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -17,20 +17,21 @@ jobs: env: NODE_ENV: dev PR_NUMBER: ${{ inputs.prNumber }} + GH_TOKEN: ${{ github.token }} permissions: id-token: write # needed to interact with GitHub's OIDC Token endpoint. contents: read strategy: - max-parallel: 30 + max-parallel: 25 matrix: package: [ - layers, + packages/idempotency, packages/logger, packages/metrics, - packages/tracer, packages/parameters, - packages/idempotency, + packages/tracer, + layers, ] version: [18, 20, 22] arch: [x86_64, arm64] @@ -42,11 +43,14 @@ jobs: - name: Extract PR details id: extract_PR_details if: ${{ inputs.prNumber != '' }} - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - const script = require('.github/scripts/get_pr_info.js'); - await script({github, context, core}); + run: | + # Get the PR number from the input + pr_number=${{ inputs.prNumber }} + # Get the headSHA of the PR + head_sha=$(gh pr view $pr_number --json headRefOid -q '.headRefOid') + # Set the headSHA as an output variable + echo "headSHA=$head_sha" >> $GITHUB_OUTPUT + echo "headSHA=$head_sha" # Only if a PR Number was passed and the headSHA of the PR extracted, # we checkout the PR at that point in time - name: Checkout PR code @@ -55,7 +59,7 @@ jobs: with: ref: ${{ steps.extract_PR_details.outputs.headSHA }} - name: Setup NodeJS - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: '22' - name: Setup dependencies @@ -75,4 +79,4 @@ jobs: ARCH: ${{ matrix.arch }} JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: true RUNNER_DEBUG: ${{ env.RUNNER_DEBUG }} - run: npm run test:e2e -w ${{ matrix.package }} + run: npm run test:e2e -w ${{ matrix.package }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e46a4810ca..24e38b6cdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,27 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + + +### Bug Fixes + +* **ci:** Remove --compatible-architectures from workflow ([#3752](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3752)) ([dafa496](https://github.com/aws-powertools/powertools-lambda-typescript/commit/dafa49602ea45227384b63bff4d3f39d69e982d8)) +* **idempotency:** include sk in error msgs when using composite key ([#3709](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3709)) ([661f5ff](https://github.com/aws-powertools/powertools-lambda-typescript/commit/661f5ff7f3f3805e24f515892e98430dccebf979)) +* **logger:** correctly refresh sample rate ([#3722](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3722)) ([2692ca4](https://github.com/aws-powertools/powertools-lambda-typescript/commit/2692ca4d1b15763936659b05e1830d998a4d2020)) +* **parser:** ddb base schema + other exports ([#3741](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3741)) ([51a3410](https://github.com/aws-powertools/powertools-lambda-typescript/commit/51a3410be8502496362d5ed13a64fe55691604ba)) + + +### Features + +* **commons:** make utilities aware of provisioned concurrency ([#3724](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3724)) ([c28e45e](https://github.com/aws-powertools/powertools-lambda-typescript/commit/c28e45ecba315bac8fbc7744dbe21a3461747d44)) +* **logger:** set correlation ID in logs ([#3726](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3726)) ([aa74fc8](https://github.com/aws-powertools/powertools-lambda-typescript/commit/aa74fc8548ccb8cb313ffd1742184c66e8d6c22c)) +* **metrics:** allow setting functionName via constructor parameter and environment variable ([#3696](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3696)) ([3176fa0](https://github.com/aws-powertools/powertools-lambda-typescript/commit/3176fa08e1886d5c86e7b327134cc988b82cf8d8)) + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c548ea5beb..1fb13dda87 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,9 +115,35 @@ GitHub provides additional document on [forking a repository](https://help.githu You might find useful to run both the documentation website and the API reference locally while contributing: -- **Docs website**: `npm run docs-runLocalDocker` - - If this is your first time running the docs, you need to build the image: `npm run docs-buildDockerImage` -- **API reference**: `npm run docs-api-build-run` +#### Using Docker (recommended) + +1. Build the Docker image (only needed the first time): + + ```bash + npm run docs:docker:build + ``` + +2. Run the documentation website: + + ```bash + npm run docs:docker:run + ``` + +#### Using Python directly + +If you have Python 3.x installed, you can run the documentation website and API reference locally without Docker: + +1. Create a virtual environment and install dependencies: + + ```bash + npm run docs:local:setup + ``` + +2. Run the documentation website: + + ```bash + npm run docs:local:run + ``` ## Conventions diff --git a/README.md b/README.md index a69ad12ec5..be3a9e3bcb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# Powertools for AWS Lambda (TypeScript) +# Powertools for AWS Lambda (TypeScript) -![NodeSupport](https://img.shields.io/static/v1?label=node&message=%2016|%2018|%2020&color=green?style=flat-square&logo=node) +![NodeSupport](https://img.shields.io/static/v1?label=node&message=%2018|%2020|%2022&color=green?style=flat-square&logo=node) ![GitHub Release](https://img.shields.io/github/v/release/aws-powertools/powertools-lambda-typescript?style=flat-square) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=aws-powertools_powertools-lambda-typescript&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=aws-powertools_powertools-lambda-typescript) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=aws-powertools_powertools-lambda-typescript&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=aws-powertools_powertools-lambda-typescript) @@ -28,6 +28,7 @@ Find the complete project's [documentation here](https://docs.powertools.aws.dev - **[Batch Processing](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/batch/)** - Utility to handle partial failures when processing batches from Amazon SQS, Amazon Kinesis Data Streams, and Amazon DynamoDB Streams. - **[JMESPath Functions](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/jmespath/)** - Built-in JMESPath functions to easily deserialize common encoded JSON payloads in Lambda functions. - **[Parser (Zod)](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/parser/)** - Utility that provides data validation and parsing using Zod, a TypeScript-first schema declaration and validation library. +- **[Validation](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/validation/)** - JSON Schema validation for events and responses, including JMESPath support to unwrap events before validation. ## Install @@ -41,6 +42,7 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa - **Batch**: `npm install @aws-lambda-powertools/batch` - **JMESPath Functions**: `npm install @aws-lambda-powertools/jmespath` - **Parser**: `npm install @aws-lambda-powertools/parser zod@~3` +- **Validation**: `npm install @aws-lambda-powertools/validation` ### Examples @@ -68,6 +70,7 @@ The following companies, among others, use Powertools: - [Elva](https://elva-group.com) - [Flyweight](https://flyweight.io/) - [globaldatanet](https://globaldatanet.com/) +- [Guild](https://guild.com) - [Hashnode](https://hashnode.com/) - [LocalStack](https://localstack.cloud/) - [Perfect Post](https://www.perfectpost.fr) diff --git a/docs/Dockerfile b/docs/Dockerfile index 309fa6a2c6..8ae37f1976 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -1,5 +1,8 @@ # version 9.5.35 -FROM squidfunk/mkdocs-material@sha256:047452c6641137c9caa3647d050ddb7fa67b59ed48cc67ec3a4995f3d360ab32 +FROM squidfunk/mkdocs-material@sha256:f226a2d2d5983643cab401491fc40e8a5711a50e90c21433f80e91c014cff1f5 + +# Install Node.js +RUN apk add --no-cache nodejs=20.15.1-r0 npm COPY requirements.txt /tmp/ RUN pip install --require-hashes -r /tmp/requirements.txt diff --git a/docs/contributing/setup.md b/docs/contributing/setup.md index 68bcf2cba6..7f10903d87 100644 --- a/docs/contributing/setup.md +++ b/docs/contributing/setup.md @@ -65,10 +65,36 @@ You can use `npm run setup-local` to install all dependencies locally and setup !!! note "Curious about what `setup-local` does under the hood?" We use npm scripts to [automate common tasks](https://github.com/aws-powertools/powertools-lambda-typescript/blob/main/package.json#L24){target="_blank" rel="nofollow"} locally and in Continuous Integration environments. -## Local documentation +### Local documentation You might find useful to run both the documentation website and the API reference locally while contributing: -* **Docs website**: `npm run docs-runLocalDocker` - * If this is your first time running the docs, you need to build the image: `npm run docs-buildDockerImage` -* **API reference**: `npm run docs-api-build-run` +#### Using Docker (recommended) + +1. Build the Docker image (only needed the first time): + + ```bash + npm run docs:docker:build + ``` + +2. Run the documentation website: + + ```bash + npm run docs:docker:run + ``` + +#### Using Python directly + +If you have Python installed, you can run the documentation website and API reference locally without Docker: + +1. Create a virtual environment and install dependencies: + + ```bash + npm run docs:local:setup + ``` + +2. Run the documentation website: + + ```bash + npm run docs:local:run + ``` diff --git a/docs/core/logger.md b/docs/core/logger.md index 523214a147..6a72664bb3 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -207,6 +207,76 @@ When debugging in non-production environments, you can log the incoming event us Use `POWERTOOLS_LOGGER_LOG_EVENT` environment variable to enable or disable (`true`/`false`) this feature. When using Middy.js middleware or class method decorator, the `logEvent` option will take precedence over the environment variable. +### Setting a Correlation ID + +To get started, install the `@aws-lambda-powertools/jmespath` package, and pass the search function using the `correlationIdSearchFn` constructor parameter: + +=== "Setup the Logger to use JMESPath search" + + ```typescript hl_lines="5" + --8<-- "examples/snippets/logger/correlationIdLogger.ts" + ``` + +???+ tip + You can retrieve correlation IDs via `getCorrelationId` method. + +You can set a correlation ID using `correlationIdPath` parameter by passing a JMESPath expression, including our custom JMESPath functions or set it manually by calling `setCorrelationId` function. + +=== "Setting correlation ID manually" + + ```typescript hl_lines="7" + --8<-- "examples/snippets/logger/correlationIdManual.ts" + ``` + + 1. Alternatively, if the payload is more complex you can use a JMESPath expression as second parameter when prividing a search function in the constructor. + +=== "Middy.js" + + ```typescript hl_lines="13" + --8<-- "examples/snippets/logger/correlationIdMiddy.ts" + ``` + +=== "Decorator" + + ```typescript hl_lines="11" + --8<-- "examples/snippets/logger/correlationIdDecorator.ts" + ``` + +=== "payload.json" + + ```typescript + --8<-- "examples/snippets/logger/samples/correlationIdPayload.json" + ``` + +=== "log-output.json" + + ```json hl_lines="6" + --8<-- "examples/snippets/logger/samples/correlationIdOutput.json" + ``` + +To ease routine tasks like extracting correlation ID from popular event sources, we provide built-in JMESPath expressions. + +=== "Decorator" + + ```typescript hl_lines="4 14" + --8<-- "examples/snippets/logger/correlationIdPaths.ts" + ``` + +???+ note "Note: Any object key named with `-` must be escaped" + For example, **`request.headers."x-amzn-trace-id"`**. + +| Name | Expression | Description | +| ----------------------------- | ------------------------------------- | ------------------------------- | +| **API_GATEWAY_REST** | `'requestContext.requestId'` | API Gateway REST API request ID | +| **API_GATEWAY_HTTP** | `'requestContext.requestId'` | API Gateway HTTP API request ID | +| **APPSYNC_AUTHORIZER** | `'requestContext.requestId'` | AppSync resolver request ID | +| **APPSYNC_RESOLVER** | `'request.headers."x-amzn-trace-id"'` | AppSync X-Ray Trace ID | +| **APPLICATION_LOAD_BALANCER** | `'headers."x-amzn-trace-id"'` | ALB X-Ray Trace ID | +| **EVENT_BRIDGE** | `'id'` | EventBridge Event ID | +| **LAMBDA_FUNCTION_URL** | `'requestContext.requestId'` | Lambda Function URL request ID | +| **S3_OBJECT_LAMBDA** | `'xAmzRequestId'` | S3 Object trigger request ID | +| **VPC_LATTICE** | `'headers."x-amzn-trace-id'` | VPC Lattice X-Ray Trace ID | + ### Appending additional keys You can append additional keys using either mechanism: @@ -697,16 +767,19 @@ sequenceDiagram 4. **What happens if the log buffer reaches its maximum size?** Older logs are removed from the buffer to make room for new logs. This means that if the buffer is full, you may lose some logs if they are not flushed before the buffer reaches its maximum size. When this happens, we emit a warning when flushing the buffer to indicate that some logs have been dropped. -5. **What timestamp is used when I flush the logs?** +5. **How is the log size of a log line calculated?** + The log size is calculated based on the size of the stringified log line in bytes. This includes the size of the log message, the size of any additional keys, and the size of the timestamp. + +6. **What timestamp is used when I flush the logs?** The timestamp preserves the original time when the log record was created. If you create a log record at 11:00:10 and flush it at 11:00:25, the log line will retain its original timestamp of 11:00:10. -6. **What happens if I try to add a log line that is bigger than max buffer size?** +7. **What happens if I try to add a log line that is bigger than max buffer size?** The log will be emitted directly to standard output and not buffered. When this happens, we emit a warning to indicate that the log line was too big to be buffered. -7. **What happens if Lambda times out without flushing the buffer?** +8. **What happens if Lambda times out without flushing the buffer?** Logs that are still in the buffer will be lost. If you are using the log buffer to log asynchronously, you should ensure that the buffer is flushed before the Lambda function times out. You can do this by calling the `logger.flushBuffer()` method at the end of your Lambda function. -8. **Do child loggers inherit the buffer?** +9. **Do child loggers inherit the buffer?** No, child loggers do not inherit the buffer from their parent logger but only the buffer configuration. This means that if you create a child logger, it will have its own buffer and will not share the buffer with the parent logger. ### Reordering log keys position @@ -805,114 +878,41 @@ You can use values ranging from `0` to `1` (100%) when setting the `sampleRateVa This feature takes into account transient issues where additional debugging information can be useful. -Sampling decision happens at the Logger initialization. When using the `injectLambdaContext` method either as a decorator or middleware, the sampling decision is refreshed at the beginning of each Lambda invocation for you, except for cold starts. +Sampling decision happens at the Logger initialization. When using the `injectLambdaContext` method either as a decorator or Middy.js middleware, the sampling decision is refreshed at the beginning of each Lambda invocation for you, except for cold starts. If you're not using either of these, you'll need to manually call the `refreshSamplingRate()` function at the start of your handler to refresh the sampling decision for each invocation. === "handler.ts" - ```typescript hl_lines="6" + ```typescript hl_lines="5 9" --8<-- "examples/snippets/logger/logSampling.ts" ``` -=== "Example CloudWatch Logs excerpt - Invocation #1" + 1. The log level must be set to a more verbose level than `DEBUG` for log sampling to kick in. + 2. You need to call `logger.refreshSamplingRate()` at the start of your handler **only** if you're not using the `injectLambdaContext()` class method decorator or Middy.js middleware. + +=== "Example Logs Request #1 (not sampled)" ```json - { - "level": "ERROR", - "message": "This is an ERROR log", - "sampling_rate": "0.5", - "service": "serverlessAirline", - "timestamp": "2021-12-12T22:59:06.334Z", - "xray_trace_id": "abcdef123456abcdef123456abcdef123456" - } - { - "level": "DEBUG", - "message": "This is a DEBUG log that has 50% chance of being printed", - "sampling_rate": "0.5", - "service": "serverlessAirline", - "timestamp": "2021-12-12T22:59:06.337Z", - "xray_trace_id": "abcdef123456abcdef123456abcdef123456" - } - { - "level": "INFO", - "message": "This is an INFO log that has 50% chance of being printed", - "sampling_rate": "0.5", - "service": "serverlessAirline", - "timestamp": "2021-12-12T22:59:06.338Z", - "xray_trace_id": "abcdef123456abcdef123456abcdef123456" - } - { - "level": "WARN", - "message": "This is a WARN log that has 50% chance of being printed", - "sampling_rate": "0.5", - "service": "serverlessAirline", - "timestamp": "2021-12-12T22:59:06.338Z", - "xray_trace_id": "abcdef123456abcdef123456abcdef123456" - } + --8<-- "examples/snippets/logger/samples/debugLogSamplingNotSampled.json" ``` -=== "Example CloudWatch Logs excerpt - Invocation #2" +=== "Example Logs Request #2 (sampled)" ```json - { - "level": "ERROR", - "message": "This is an ERROR log", - "sampling_rate": "0.5", - "service": "serverlessAirline", - "timestamp": "2021-12-12T22:59:06.334Z", - "xray_trace_id": "abcdef123456abcdef123456abcdef123456" - } + --8<-- "examples/snippets/logger/samples/debugLogSamplingSampled.json" ``` -=== "Example CloudWatch Logs excerpt - Invocation #3" +=== "Example Logs Request #3 (sampled)" ```json - { - "level": "ERROR", - "message": "This is an ERROR log", - "sampling_rate": "0.5", - "service": "serverlessAirline", - "timestamp": "2021-12-12T22:59:06.334Z", - "xray_trace_id": "abcdef123456abcdef123456abcdef123456" - } - { - "level": "DEBUG", - "message": "This is a DEBUG log that has 50% chance of being printed", - "sampling_rate": "0.5", - "service": "serverlessAirline", - "timestamp": "2021-12-12T22:59:06.337Z", - "xray_trace_id": "abcdef123456abcdef123456abcdef123456" - } - { - "level": "INFO", - "message": "This is an INFO log that has 50% chance of being printed", - "sampling_rate": "0.5", - "service": "serverlessAirline", - "timestamp": "2021-12-12T22:59:06.338Z", - "xray_trace_id": "abcdef123456abcdef123456abcdef123456" - } - { - "level": "WARN", - "message": "This is a WARN log that has 50% chance of being printed", - "sampling_rate": "0.5", - "service": "serverlessAirline", - "timestamp": "2021-12-12T22:59:06.338Z", - "xray_trace_id": "abcdef123456abcdef123456abcdef123456" - } + --8<-- "examples/snippets/logger/samples/debugLogSamplingSampled.json" ``` -=== "Example CloudWatch Logs excerpt - Invocation #4" +=== "Example Logs Request #4 (not sampled)" ```json - { - "level": "ERROR", - "message": "This is an ERROR log", - "sampling_rate": "0.5", - "service": "serverlessAirline", - "timestamp": "2021-12-12T22:59:06.334Z", - "xray_trace_id": "abcdef123456abcdef123456abcdef123456" - } + --8<-- "examples/snippets/logger/samples/debugLogSamplingNotSampled.json" ``` ### Custom Log formatter diff --git a/docs/core/metrics.md b/docs/core/metrics.md index 42bc929ebe..f2d61eabf9 100644 --- a/docs/core/metrics.md +++ b/docs/core/metrics.md @@ -66,11 +66,12 @@ The library requires two settings. You can set them as environment variables, or These settings will be used across all metrics emitted: -| Setting | Description | Environment variable | Default | Allowed Values | Example | Constructor parameter | -| -------------------- | ---------------------------------------------------------------- | ------------------------------ | ------------------- | -------------- | ------------------- | --------------------- | -| **Service** | Optionally, sets **service** metric dimension across all metrics | `POWERTOOLS_SERVICE_NAME` | `service_undefined` | Any string | `serverlessAirline` | `serviceName` | -| **Metric namespace** | Logical container where all metrics will be placed | `POWERTOOLS_METRICS_NAMESPACE` | `default_namespace` | Any string | `serverlessAirline` | `default_namespace` | -| **Enabled** | Whether to emit metrics to standard output or not | `POWERTOOLS_METRICS_ENABLED` | `true` | Boolean | `false` | | +| Setting | Description | Environment variable | Default | Allowed Values | Example | Constructor parameter | +|----------------------|------------------------------------------------------------------|------------------------------------|----------------------------------------------------------|----------------|---------------------|-----------------------| +| **Service** | Optionally, sets **service** metric dimension across all metrics | `POWERTOOLS_SERVICE_NAME` | `service_undefined` | Any string | `serverlessAirline` | `serviceName` | +| **Metric namespace** | Logical container where all metrics will be placed | `POWERTOOLS_METRICS_NAMESPACE` | `default_namespace` | Any string | `serverlessAirline` | `default_namespace` | +| **Function Name** | Function name used as dimension for the `ColdStart` metric | `POWERTOOLS_METRICS_FUNCTION_NAME` | [See docs](#capturing-a-cold-start-invocation-as-metric) | Any string | `my-function-name` | `functionName` | +| **Enabled** | Whether to emit metrics to standard output or not | `POWERTOOLS_METRICS_ENABLED` | `true` | Boolean | `false` | | !!! tip Use your application name or main service as the metric namespace to easily group all metrics @@ -87,7 +88,7 @@ The `Metrics` utility is instantiated outside of the Lambda handler. In doing th === "template.yml" - ```yaml hl_lines="9 10" + ```yaml hl_lines="8-10" Resources: HelloWorldFunction: Type: AWS::Serverless::Function @@ -97,6 +98,7 @@ The `Metrics` utility is instantiated outside of the Lambda handler. In doing th Variables: POWERTOOLS_SERVICE_NAME: orders POWERTOOLS_METRICS_NAMESPACE: serverlessAirline + POWERTOOLS_METRICS_FUNCTION_NAME: my-function-name ``` You can initialize Metrics anywhere in your code - It'll keep track of your aggregate metrics in memory. @@ -184,7 +186,7 @@ You can call `addMetric()` with the same name multiple times. The values will be ### Adding default dimensions -You can add default dimensions to your metrics by passing them as parameters in 4 ways: +You can add default dimensions to your metrics by passing them as parameters in 4 ways: * in the constructor * in the [Middy-compatible](https://github.com/middyjs/middy){target=_blank} middleware @@ -405,6 +407,28 @@ This has the advantage of keeping cold start metric separate from your applicati !!! info "We do not emit 0 as a value for the ColdStart metric for cost-efficiency reasons. [Let us know](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new?assignees=&labels=feature-request%2C+triage&template=feature_request.md&title=) if you'd prefer a flag to override it." +#### Setting function name + +When emitting cold start metrics, the `function_name` dimension defaults to `context.functionName`. If you want to change the value you can set the `functionName` parameter in the metrics constructor, define the environment variable `POWERTOOLS_METRICS_FUNCTION_NAME`, or pass a value to `captureColdStartMetric`. + +The priority of the `function_name` dimension value is defined as: + +1. `functionName` constructor option +2. `POWERTOOLS_METRICS_FUNCTION_NAME` environment variable +3. The value passed in the `captureColdStartMetric` call, or `context.functionName` if using logMetrics decorator or Middy middleware + +=== "constructor" + + ```typescript hl_lines="6" + --8<-- "examples/snippets/metrics/functionName.ts" + ``` + +=== "captureColdStartMetric method" + + ```typescript hl_lines="12" + --8<-- "examples/snippets/metrics/captureColdStartMetric.ts" + ``` + ## Advanced ### Adding metadata diff --git a/docs/index.md b/docs/index.md index 592fc46a73..081ec1849d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -69,7 +69,9 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa For the latter, make sure to replace `{region}` with your AWS region, e.g., `eu-west-1`. - __arn:aws:lambda:{region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21__{: .copyMe}:clipboard: + !!! abstract "" + + __arn:aws:lambda:{region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22__{: .copyMe}:clipboard: ???+ note "Code snippets for popular infrastructure as code frameworks" @@ -89,7 +91,7 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa const powertoolsLayer = LayerVersion.fromLayerVersionArn( this, 'PowertoolsLayer', - `arn:aws:lambda:${Stack.of(this).region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21` + `arn:aws:lambda:${Stack.of(this).region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22` ); new Function(this, 'Function', { @@ -126,7 +128,7 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa Type: AWS::Serverless::Function Properties: Layers: - - !Sub arn:aws:lambda:${AWS::Region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21 + - !Sub arn:aws:lambda:${AWS::Region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22 ``` You can also use AWS SSM Parameter Store to dynamically add Powertools for AWS Lambda. The `{version}` placeholder is the semantic version number (e,g. 2.1.0) for a release or `_latest_`. @@ -165,7 +167,7 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa hello: handler: lambda_function.lambda_handler layers: - - arn:aws:lambda:${aws:region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21 + - arn:aws:lambda:${aws:region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22 ``` If you use `esbuild` to bundle your code, make sure to exclude `@aws-lambda-powertools/*` and `@aws-sdk/*` from being bundled since the packages are already present the layer: @@ -200,7 +202,7 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa role = ... handler = "index.handler" runtime = "nodejs22.x" - layers = ["arn:aws:lambda:{aws::region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21"] + layers = ["arn:aws:lambda:{aws::region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22"] source_code_hash = filebase64sha256("lambda_function_payload.zip") } ``` @@ -235,7 +237,7 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa const lambdaFunction = new aws.lambda.Function('function', { layers: [ - pulumi.interpolate`arn:aws:lambda:${aws.getRegionOutput().name}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21` + pulumi.interpolate`arn:aws:lambda:${aws.getRegionOutput().name}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22` ], code: new pulumi.asset.FileArchive('lambda_function_payload.zip'), tracingConfig: { @@ -259,11 +261,20 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa name: "my-function", layers: { "@aws-lambda-powertools/*": - "arn:aws:lambda:${AWS::Region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21", + "arn:aws:lambda:${AWS::Region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22", }, }); ``` +=== "Layer in GovCloud" + + We also provide layers in two GovCloud regions: + + !!! abstract "" + + * __arn:aws-us-gov:lambda:us-gov-east-1:165087284144:layer:AWSLambdaPowertoolsTypeScriptV2:22__{: .copyMe}:clipboard: + * __arn:aws-us-gov:lambda:us-gov-west-1:165093116878:layer:AWSLambdaPowertoolsTypeScriptV2:22__{: .copyMe}:clipboard: + ### Lambda Layer [Lambda Layer](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html){target="_blank"} is a `.zip` file archive that can contain additional code, pre-packaged dependencies, data, or configuration files. We compile and optimize [all dependencies](#install) to achieve an optimal build. @@ -273,38 +284,38 @@ You can use the Lambda Layer both with CommonJS and ESM (ECMAScript modules) for ??? note "Click to expand and copy any regional Lambda Layer ARN" | Region | Layer ARN | | ---------------- | ------------------------------------------------------------------------------------------------------------- | - | `us-east-1` | [arn:aws:lambda:us-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `us-east-2` | [arn:aws:lambda:us-east-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `us-west-1` | [arn:aws:lambda:us-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `us-west-2` | [arn:aws:lambda:us-west-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ap-south-1` | [arn:aws:lambda:ap-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ap-south-2` | [arn:aws:lambda:ap-south-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ap-east-1` | [arn:aws:lambda:ap-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ap-southeast-4` | [arn:aws:lambda:ap-southeast-4:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ap-southeast-5` | [arn:aws:lambda:ap-southeast-5:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ap-southeast-7` | [arn:aws:lambda:ap-southeast-7:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `eu-central-1` | [arn:aws:lambda:eu-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `eu-central-2` | [arn:aws:lambda:eu-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `eu-west-1` | [arn:aws:lambda:eu-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `eu-west-2` | [arn:aws:lambda:eu-west-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `eu-west-3` | [arn:aws:lambda:eu-west-3:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `eu-north-1` | [arn:aws:lambda:eu-north-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `eu-south-1` | [arn:aws:lambda:eu-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `eu-south-2` | [arn:aws:lambda:eu-south-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ca-central-1` | [arn:aws:lambda:ca-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `ca-west-1` | [arn:aws:lambda:ca-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `sa-east-1` | [arn:aws:lambda:sa-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `af-south-1` | [arn:aws:lambda:af-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `me-south-1` | [arn:aws:lambda:me-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `me-central-1` | [arn:aws:lambda:me-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `il-central-1` | [arn:aws:lambda:il-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | - | `mx-central-1` | [arn:aws:lambda:mx-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21](#){: .copyMe}:clipboard: | + | `us-east-1` | [arn:aws:lambda:us-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `us-east-2` | [arn:aws:lambda:us-east-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `us-west-1` | [arn:aws:lambda:us-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `us-west-2` | [arn:aws:lambda:us-west-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ap-south-1` | [arn:aws:lambda:ap-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ap-south-2` | [arn:aws:lambda:ap-south-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ap-east-1` | [arn:aws:lambda:ap-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ap-southeast-4` | [arn:aws:lambda:ap-southeast-4:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ap-southeast-5` | [arn:aws:lambda:ap-southeast-5:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ap-southeast-7` | [arn:aws:lambda:ap-southeast-7:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `eu-central-1` | [arn:aws:lambda:eu-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `eu-central-2` | [arn:aws:lambda:eu-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `eu-west-1` | [arn:aws:lambda:eu-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `eu-west-2` | [arn:aws:lambda:eu-west-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `eu-west-3` | [arn:aws:lambda:eu-west-3:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `eu-north-1` | [arn:aws:lambda:eu-north-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `eu-south-1` | [arn:aws:lambda:eu-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `eu-south-2` | [arn:aws:lambda:eu-south-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ca-central-1` | [arn:aws:lambda:ca-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `ca-west-1` | [arn:aws:lambda:ca-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `sa-east-1` | [arn:aws:lambda:sa-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `af-south-1` | [arn:aws:lambda:af-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `me-south-1` | [arn:aws:lambda:me-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `me-central-1` | [arn:aws:lambda:me-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `il-central-1` | [arn:aws:lambda:il-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | + | `mx-central-1` | [arn:aws:lambda:mx-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22](#){: .copyMe}:clipboard: | **Want to inspect the contents of the Layer?** @@ -313,7 +324,7 @@ The pre-signed URL to download this Lambda Layer will be within `Location` key i Change `{aws::region}` to your AWS region, e.g. `eu-west-1`, and run the following command: ```bash title="AWS CLI command to download Lambda Layer content" -aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{aws::region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:21 --region {aws::region} +aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{aws::region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:22 --region {aws::region} ``` ## Instrumentation @@ -351,22 +362,23 @@ Core utilities such as Tracing, Logging, and Metrics will be available across al ???+ info Explicit parameters take precedence over environment variables -| Environment variable | Description | Utility | Default | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ------------------- | -| **POWERTOOLS_SERVICE_NAME** | Set service name used for tracing namespace, metrics dimension and structured logging | All | `service_undefined` | -| **POWERTOOLS_METRICS_NAMESPACE** | Set namespace used for metrics | [Metrics](core/metrics.md) | `default_namespace` | -| **POWERTOOLS_METRICS_ENABLED** | Explicitly disables emitting metrics to stdout | [Metrics](core/metrics.md) | `true` | -| **POWERTOOLS_TRACE_ENABLED** | Explicitly disables tracing | [Tracer](core/tracer.md) | `true` | -| **POWERTOOLS_TRACER_CAPTURE_RESPONSE** | Capture Lambda or method return as metadata. | [Tracer](core/tracer.md) | `true` | -| **POWERTOOLS_TRACER_CAPTURE_ERROR** | Capture Lambda or method exception as metadata. | [Tracer](core/tracer.md) | `true` | -| **POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS** | Capture HTTP(s) requests as segments. | [Tracer](core/tracer.md) | `true` | -| **POWERTOOLS_LOGGER_LOG_EVENT** | Log incoming event | [Logger](core/logger.md) | `false` | -| **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logger](core/logger.md) | `0` | -| **POWERTOOLS_DEV** | Increase JSON indentation to ease debugging when running functions locally or in a non-production environment | [Logger](core/logger.md) | `false` | -| **POWERTOOLS_LOG_LEVEL** | Sets how verbose Logger should be, from the most verbose to the least verbose (no logs) | [Logger](core/logger.md) | `INFO` | -| **POWERTOOLS_PARAMETERS_MAX_AGE** | Adjust how long values are kept in cache (in seconds) | [Parameters](utilities/parameters.md) | `5` | -| **POWERTOOLS_PARAMETERS_SSM_DECRYPT** | Set whether to decrypt or not values retrieved from AWS Systems Manager Parameters Store | [Parameters](utilities/parameters.md) | `false` | -| **POWERTOOLS_IDEMPOTENCY_DISABLED** | Disable the Idempotency logic without changing your code, useful for testing | [Idempotency](utilities/idempotency.md) | `false` | +| Environment variable | Description | Utility | Default | +| -------------------------------------------- |------------------------------------------------------------------------------------------| --------------------------------------- |-------------------------------------------------| +| **POWERTOOLS_SERVICE_NAME** | Set service name used for tracing namespace, metrics dimension and structured logging | All | `service_undefined` | +| **POWERTOOLS_METRICS_NAMESPACE** | Set namespace used for metrics | [Metrics](core/metrics.md) | `default_namespace` | +| **POWERTOOLS_METRICS_FUNCTION_NAME** | Function name used as dimension for the `ColdStart` metric | [Metrics](core/metrics.md) | [See docs](core/metrics/#setting-function-name) | +| **POWERTOOLS_METRICS_ENABLED** | Explicitly disables emitting metrics to stdout | [Metrics](core/metrics.md) | `true` | +| **POWERTOOLS_TRACE_ENABLED** | Explicitly disables tracing | [Tracer](core/tracer.md) | `true` | +| **POWERTOOLS_TRACER_CAPTURE_RESPONSE** | Capture Lambda or method return as metadata. | [Tracer](core/tracer.md) | `true` | +| **POWERTOOLS_TRACER_CAPTURE_ERROR** | Capture Lambda or method exception as metadata. | [Tracer](core/tracer.md) | `true` | +| **POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS** | Capture HTTP(s) requests as segments. | [Tracer](core/tracer.md) | `true` | +| **POWERTOOLS_LOGGER_LOG_EVENT** | Log incoming event | [Logger](core/logger.md) | `false` | +| **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logger](core/logger.md) | `0` | +| **POWERTOOLS_DEV** | Pretty-print logs, disable metrics flushing, and disable traces - use for dev only | See section below | `false` | +| **POWERTOOLS_LOG_LEVEL** | Sets how verbose Logger should be, from the most verbose to the least verbose (no logs) | [Logger](core/logger.md) | `INFO` | +| **POWERTOOLS_PARAMETERS_MAX_AGE** | Adjust how long values are kept in cache (in seconds) | [Parameters](utilities/parameters.md) | `5` | +| **POWERTOOLS_PARAMETERS_SSM_DECRYPT** | Set whether to decrypt or not values retrieved from AWS Systems Manager Parameters Store | [Parameters](utilities/parameters.md) | `false` | +| **POWERTOOLS_IDEMPOTENCY_DISABLED** | Disable the Idempotency logic without changing your code, useful for testing | [Idempotency](utilities/idempotency.md) | `false` | Each Utility page provides information on example values and allowed values. @@ -447,6 +459,9 @@ Knowing which companies are using this library is important to help prioritize t [**globaldatanet**](https://globaldatanet.com/){target="_blank" rel="nofollow"} { .card } +[**Guild**](https://guild.com){target="_blank" rel="nofollow"} +{ .card } + [**Hashnode**](https://hashnode.com/){target="_blank" rel="nofollow"} { .card } diff --git a/docs/media/logos/guild.png b/docs/media/logos/guild.png new file mode 100644 index 0000000000..e0aecf3a4d Binary files /dev/null and b/docs/media/logos/guild.png differ diff --git a/docs/requirements.in b/docs/requirements.in index d124923b00..b5b21ca673 100644 --- a/docs/requirements.in +++ b/docs/requirements.in @@ -1,4 +1,5 @@ mike==1.1.2 -mkdocs-material==9.6.7 +mkdocs-material==9.6.9 mkdocs-git-revision-date-plugin==0.3.2 -mkdocs-exclude==1.0.2 \ No newline at end of file +mkdocs-exclude==1.0.2 +mkdocs-typedoc==1.0.4 \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 650a901075..8d6a794769 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,8 +1,8 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # -# pip-compile --generate-hashes --output-file=requirements.txt requirements.in +# pip-compile --generate-hashes --output-file=docs/requirements.txt docs/requirements.in # babel==2.16.0 \ --hash=sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b \ @@ -233,6 +233,7 @@ mkdocs==1.6.1 \ # mkdocs-exclude # mkdocs-git-revision-date-plugin # mkdocs-material + # mkdocs-typedoc mkdocs-exclude==1.0.2 \ --hash=sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51 # via -r requirements.in @@ -243,14 +244,18 @@ mkdocs-get-deps==0.2.0 \ mkdocs-git-revision-date-plugin==0.3.2 \ --hash=sha256:2e67956cb01823dd2418e2833f3623dee8604cdf223bddd005fe36226a56f6ef # via -r requirements.in -mkdocs-material==9.6.7 \ - --hash=sha256:3e2c1fceb9410056c2d91f334a00cdea3215c28750e00c691c1e46b2a33309b4 \ - --hash=sha256:8a159e45e80fcaadd9fbeef62cbf928569b93df954d4dc5ba76d46820caf7b47 +mkdocs-material==9.6.9 \ + --hash=sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1 \ + --hash=sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c # via -r requirements.in mkdocs-material-extensions==1.3.1 \ --hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \ --hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31 # via mkdocs-material +mkdocs-typedoc==1.0.4 \ + --hash=sha256:839b54c51a64bbb77c1c253eb81baad12462ea5cf38d361e262f5cfa8a45567a \ + --hash=sha256:a9601d6b04ed4fd5658c7170c58a3a52f584357be2a3c1e39995c6eed3712b1e + # via -r requirements.in packaging==24.1 \ --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index d35188c9ea..dbbe813d5c 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -298,7 +298,7 @@ All caching logic is handled by the `BaseProvider`, and provided that the return Here's an example of implementing a custom parameter store using an external service like HashiCorp Vault, a widely popular key-value secret storage. === "Provider usage" - ```typescript hl_lines="5-8 12-16" + ```typescript hl_lines="12" --8<-- "examples/snippets/parameters/customProviderVaultUsage.ts" ``` diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index e4dcd5dfdd..da315c47c8 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -44,7 +44,7 @@ Both are also able to parse either an object or JSON string as an input. Be cautious when using multiple decorators that expect event to have a specific structure, the order of evaluation for decorators is from bottom to top. === "Middy middleware" - ```typescript hl_lines="34" + ```typescript hl_lines="22" --8<-- "examples/snippets/parser/middy.ts" ``` @@ -157,7 +157,7 @@ If you want to extend a schema and transform a JSON stringified payload to an ob If you want to parse a DynamoDB stream event with unmarshalling, you can use the helper function `DynamoDBMarshalled`: === "DynamoDBStreamSchema with DynamoDBMarshalled" - ```typescript hl_lines="17" + ```typescript hl_lines="18" --8<-- "examples/snippets/parser/extendDynamoDBStreamSchema.ts" ``` @@ -180,12 +180,12 @@ Envelopes can be used via envelope parameter available in middy and decorator. Here's an example of parsing a custom schema in an event coming from EventBridge, where all you want is what's inside the detail key. === "Middy middleware" - ```typescript hl_lines="5 36" + ```typescript hl_lines="23" --8<-- "examples/snippets/parser/envelopeMiddy.ts" ``` === "Decorator" - ```typescript hl_lines="5 26 30" + ```typescript hl_lines="26" --8<-- "examples/snippets/parser/envelopeDecorator.ts" ``` @@ -230,26 +230,24 @@ The `ParsedResult` object will have `success`, `data`, or `error` and `original If the parsing is successful, the `data` field will contain the parsed event, otherwise you can access the `error` field and the `originalEvent` to handle the error and recover the original event. === "Middy middleware" - ```typescript hl_lines="32 35 38 39 44" + ```typescript hl_lines="23 28 32-33" --8<-- "examples/snippets/parser/safeParseMiddy.ts" ``` 1. Use `safeParse` option to parse the event without throwing an error - 2. Check if the result is successful or not and handle the error accordingly - 3. Use `data` to access the parsed event - 4. Use `error` to handle the error message - 5. Use `originalEvent` to get the original event and recover + 2. Use `data` to access the parsed event when successful + 3. Use `error` to handle the error message + 4. Use `originalEvent` to get the original event and recover === "Decorator" - ```typescript hl_lines="29 35 37 40 41" + ```typescript hl_lines="33 41 45-46" --8<-- "examples/snippets/parser/safeParseDecorator.ts" ``` 1. Use `safeParse` option to parse the event without throwing an error - 2. Check if the result is successful or not and handle the error accordingly - 3. Use `data` to access the parsed event - 4. Use `error` to handle the error message - 5. Use `originalEvent` to get the original event and recover + 2. Use `data` to access the parsed event when successful + 3. Use `error` to handle the error message + 4. Use `originalEvent` to get the original event and recover ## Manual parsing @@ -316,7 +314,7 @@ If you are you use middy middleware, you don't need to do this. === "handlerDecorator.test.ts" - ```typescript hl_lines="26" + ```typescript hl_lines="27" --8<-- "examples/snippets/parser/unitTestDecorator.ts" ``` @@ -338,7 +336,7 @@ This also works when using `safeParse` option. === "handlerSafeParse.test.ts" - ```typescript hl_lines="21-29 35 45" + ```typescript hl_lines="21-30 36 46" --8<-- "examples/snippets/parser/unitTestSafeParse.ts" ``` diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md index bfbaa2aac3..87038ee15a 100644 --- a/docs/utilities/validation.md +++ b/docs/utilities/validation.md @@ -1,15 +1,13 @@ --- -title: Validation (JSON Schema) +title: Validation descrition: Utility +status: new --- This utility provides [JSON Schema](https://json-schema.org) validation for events and responses, including JMESPath support to unwrap events before validation. -!!! warning - This feature is currently under development. As such it's considered not stable and we might make significant breaking changes before going before its release. You are welcome to [provide feedback](https://github.com/aws-powertools/powertools-lambda-typescript/discussions/3519) and [contribute to its implementation](https://github.com/aws-powertools/powertools-lambda-typescript/milestone/18). - ## Key features - Validate incoming event and response payloads @@ -20,7 +18,7 @@ This utility provides [JSON Schema](https://json-schema.org) validation for even ## Getting started ```bash -npm install @aws-lambda-powertools/validation ajv +npm install @aws-lambda-powertools/validation ``` You can validate inbound and outbound payloads using the validator class method decorator or Middy.js middleware. @@ -41,75 +39,16 @@ If the validation fails, we will throw a `SchemaValidationError`. All our decorators assume that the method they are decorating is an async method. This means that even when decorating a synchronous method, it will return a promise. If this is not the desired behavior, you can use one of the other patterns to validate your payloads. -=== "getting_started_decorator.ts" +=== "gettingStartedDecorator.ts" - ```typescript - import { validator } from '@aws-lambda-powertools/validation'; - import type { Context } from 'aws-lambda'; - import { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - } from './getting_started_schemas.js'; - - class Lambda { - @validator({ - inboundSchema, - outboundSchema, - }) - async handler(event: InboundSchema, context: Context): Promise { - return { - statusCode: 200, - body: `Hello from ${event.userId}`, - } - } - } - - export const handler = new Lambda().handler + ```typescript hl_lines="1 11-14" + --8<-- "examples/snippets/validation/gettingStartedDecorator.ts" ``` -=== "getting_started_schemas.ts" +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` It's not mandatory to validate both the inbound and outbound payloads. You can either use one, the other, or both. @@ -125,73 +64,16 @@ If you are using Middy.js, you can use the `validator` middleware to validate th Like the class method decorator, if the validation fails, we will throw a `SchemaValidationError`, and you don't need to use both the inbound and outbound schemas if you don't need to. -=== "getting_started_middy.ts" +=== "gettingStartedMiddy.ts" - ```typescript - import { validator } from '@aws-lambda-powertools/validation/middleware'; - import middy from '@middy/core'; - import { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - } from './getting_started_schemas.js'; - - export const handler = middy() - .use(validator({ - inboundSchema, - outboundSchema, - })) - .handler( - async (event: InboundSchema, context: Context): Promise => { - return { - statusCode: 200, - body: `Hello from ${event.userId}`, - } - }); + ```typescript hl_lines="1 12-15" + --8<-- "examples/snippets/validation/gettingStartedMiddy.ts" ``` -=== "getting_started_schemas.ts" +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` ### Standalone validation @@ -200,80 +82,18 @@ The `validate` function gives you more control over the validation process, and You can also gracefully handle schema validation errors by catching `SchemaValidationError` errors. -=== "getting_started_standalone.ts" +=== "gettingStartedStandalone.ts" - ```typescript - import { validate, SchemaValidationError } from '@aws-lambda-powertools/validation'; - import { Logger } from '@aws-lambda-powertools/logger'; - import { - inboundSchema, - type InboundSchema, - } from './getting_started_schemas.js'; - - const logger = new Logger(); - - export const handler = async (event: InboundSchema, context: Context) => { - try { - await validate({ - payload: event, - schema: inboundSchema, - }) - - return { // since we are not validating the output, we can return anything - message: 'ok' - } - } catch (error) { - if (error instanceof SchemaValidationError) { - logger.error('Schema validation failed', error) - throw new Error('Invalid event payload') - } - - throw error - } - } + ```typescript hl_lines="2 3 10-13 19" + --8<-- "examples/snippets/validation/gettingStartedStandalone.ts" ``` -=== "getting_started_schemas.ts" + 1. Since we are not validating the output, we can return anything + +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` ### Unwrapping events prior to validation @@ -284,92 +104,22 @@ Envelopes are [JMESPath expressions](https://jmespath.org/tutorial.html) to extr Here is a sample custom EventBridge event, where we only want to validate the `detail` part of the event: -=== "getting_started_envelope.ts" +=== "gettingStartedEnvelope.ts" - ```typescript - import { validator } from '@aws-lambda-powertools/validation'; - import type { Context } from 'aws-lambda'; - import { - inboundSchema, - type InboundSchema, - type OutboundSchema - } from './getting_started_schemas.js'; - - class Lambda { - @validator({ - inboundSchema, - envelope: 'detail', - }) - async handler(event: InboundSchema, context: Context) { - return { - message: `processed ${event.userId}`, - success: true, - } - } - } - - export const handler = new Lambda().handler + ```typescript hl_lines="8" + --8<-- "examples/snippets/validation/gettingStartedEnvelope.ts" ``` -=== "getting_started_schemas.ts" +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` -=== "getting_started_envelope_event.json" +=== "gettingStartedEnvelopeEvent.json" ```json - { - "version": "0", - "id": "12345678-1234-1234-1234-123456789012", - "detail-type": "myDetailType", - "source": "myEventSource", - "account": "123456789012", - "time": "2017-12-22T18:43:48Z", - "region": "us-west-2", - "resources": [], - "detail": { - "userId": "123" - } - } + --8<-- "examples/snippets/validation/samples/gettingStartedEnvelopeEvent.json" ``` This is quite powerful as it allows you to validate only the part of the event that you are interested in, and thanks to JMESPath, you can extract records from [arrays](https://jmespath.org/tutorial.html#list-and-slice-projections), combine [pipe](https://jmespath.org/tutorial.html#pipe-expressions) and filter expressions, and more. @@ -382,115 +132,22 @@ We provide built-in envelopes to easily extract payloads from popular AWS event Here is an example of how you can use the built-in envelope for SQS events: -=== "getting_started_envelope_builtin.ts" +=== "gettingStartedEnvelopeBuiltin.ts" - ```typescript - import { validator } from '@aws-lambda-powertools/validation'; - import { SQS } from '@aws-lambda-powertools/validation/envelopes/sqs'; - import type { Context } from 'aws-lambda'; - import { - inboundSchema, - type InboundSchema, - } from './getting_started_schemas.js'; - - const logger = new Logger(); - - export const handler = middy() - .use(validator({ - inboundSchema, - envelope: SQS, - })) - .handler( - async (event: Array, context: Context) => { - for (const record of event) { - logger.info(`Processing message ${record.userId}`); - } - } - ) + ```typescript hl_lines="1 13" + --8<-- "examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts" ``` -=== "getting_started_schemas.ts" +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` -=== "getting_started_envelope_event.json" +=== "gettingStartedSQSEnvelopeEvent.json" ```json - { - "Records": [ - { - "messageId": "c80e8021-a70a-42c7-a470-796e1186f753", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "{\"userId\":\"123\"}", - "attributes": { - "ApproximateReceiveCount": "3", - "SentTimestamp": "1529104986221", - "SenderId": "AIDAIC6K7FJUZ7Q", - "ApproximateFirstReceiveTimestamp": "1529104986230" - }, - "messageAttributes": {}, - "md5OfBody": "098f6bcd4621d373cade4e832627b4f6", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", - "awsRegion": "us-west-2" - }, - { - "messageId": "c80e8021-a70a-42c7-a470-796e1186f753", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "{\"userId\":\"456\"}", - "attributes": { - "ApproximateReceiveCount": "3", - "SentTimestamp": "1529104986221", - "SenderId": "AIDAIC6K7FJUZ7Q", - "ApproximateFirstReceiveTimestamp": "1529104986230" - }, - "messageAttributes": {}, - "md5OfBody": "098f6bcd4621d373cade4e832627b4f6", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", - "awsRegion": "us-west-2" - } - ] - } + --8<-- "examples/snippets/validation/samples/gettingStartedSQSEnvelopeEvent.json" ``` For a complete list of built-in envelopes, check the built-in envelopes section [here](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/jmespath/#built-in-envelopes). @@ -505,70 +162,20 @@ This is useful when you have a specific format that is not covered by the built- JSON Schemas with custom formats like `awsaccountid` will fail validation if the format is not defined. You can define custom formats using the `formats` option to any of the validation methods. -=== "schema_with_custom_format.json" +=== "schemaWithCustomFormat.json" ```json - { - "type": "object", - "properties": { - "accountId": { - "type": "string", - "format": "awsaccountid" - }, - "creditCard": { - "type": "string", - "format": "creditcard" - } - }, - "required": ["accountId"] - } + --8<-- "examples/snippets/validation/samples/schemaWithCustomFormat.json" ``` For each one of these custom formats, you need to tell us how to validate them. To do so, you can either pass a `RegExp` object or a function that receives the value and returns a boolean. For example, to validate using the schema above, you can define a custom format for `awsaccountid` like this: -=== "advanced_custom_format.ts" +=== "advancedCustomFormats.ts" - ```typescript - import { validate, SchemaValidationError } from '@aws-lambda-powertools/validation'; - import { Logger } from '@aws-lambda-powertools/logger'; - - const logger = new Logger(); - - const customFormats = { - awsaccountid: new RegExp('^[0-9]{12}$'), - creditcard: (value: string) => { - // Luhn algorithm (for demonstration purposes only - do not use in production) - const sum = value.split('').reverse().reduce((acc, digit, index) => { - const num = parseInt(digit, 10); - return acc + (index % 2 === 0 ? num : num < 5 ? num * 2 : num * 2 - 9); - }, 0); - - return sum % 10 === 0; - } - }; - - export const handler = async (event: any, context: Context) => { - try { - await validate({ - payload: event, - schema: schemaWithCustomFormat, - formats: customFormats, - }) - - return { // since we are not validating the output, we can return anything - message: 'ok' - } - } catch (error) { - if (error instanceof SchemaValidationError) { - logger.error('Schema validation failed', error) - throw new Error('Invalid event payload') - } - - throw error - } - } + ```typescript hl_lines="29" + --8<-- "examples/snippets/validation/advancedCustomFormats.ts" ``` ### Built-in JMESpath functions @@ -589,78 +196,16 @@ JSON Schema allows schemas to reference other schemas using the `$ref` keyword. You can use the `externalRefs` option to pass a list of schemas that you want to reference in your inbound and outbound schemas. -=== "advanced_custom_format.ts" +=== "advancedExternalRefs.ts" - ```typescript - import { validate } from '@aws-lambda-powertools/validation'; - import { - inboundSchema, - outboundSchema, - defsSchema, - type InboundSchema, - } from './schemas_with_external_ref.ts'; - - class Lambda { - @validator({ - inboundSchema, - outboundSchema, - externalRefs: [defsSchema], - }) - async handler(event: InboundSchema, context: Context) { - return { - message: `processed ${event.userId}`, - success: true, - } - } - } + ```typescript hl_lines="14" + --8<-- "examples/snippets/validation/advancedExternalRefs.ts" ``` -=== "schemas_with_external_ref.ts" - - ```ts - const defsSchema = { - $id: 'http://example.com/schemas/defs.json', - definitions: { - int: { type: 'integer' }, - str: { type: 'string' }, - }, - } as const; - - const inboundSchema = { - $id: 'http://example.com/schemas/inbound.json', - type: 'object', - properties: { - userId: { $ref: 'defs.json#/definitions/str' } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - $id: 'http://example.com/schemas/outbound.json', - type: 'object', - properties: { - body: { $ref: 'defs.json#/definitions/str' }, - statusCode: { $ref: 'defs.json#/definitions/int' } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - defsSchema, - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; +=== "schemasWithExternalRefs.ts" + + ```typescript + --8<-- "examples/snippets/validation/schemasWithExternalRefs.ts" ``` ### Bringing your own `ajv` instance @@ -669,39 +214,14 @@ By default, we use JSON Schema draft-07. If you want to use a different draft, y This is also useful if you want to configure `ajv` with custom options like keywords and more. -=== "advanced_custom_format.ts" +=== "advancedBringAjvInstance.ts" - ```typescript - import { validate } from '@aws-lambda-powertools/validation'; - import Ajv2019 from "ajv/dist/2019" - import { Logger } from '@aws-lambda-powertools/logger'; - - const logger = new Logger(); - - const ajv = new Ajv2019(); - - export const handler = async (event: any, context: Context) => { - try { - await validate({ - payload: event, - schema: schemaWithCustomFormat, - ajv, - }) - - return { // since we are not validating the output, we can return anything - message: 'ok' - } - } catch (error) { - if (error instanceof SchemaValidationError) { - logger.error('Schema validation failed', error) - throw new Error('Invalid event payload') - } - - throw error - } - } + ```typescript hl_lines="9 16" + --8<-- "examples/snippets/validation/advancedBringAjvInstance.ts" ``` + 1. You can pass your own `ajv` instance to any of the validation methods. This is useful if you want to configure `ajv` with custom options like keywords and more. + ## Should I use this or Parser? One of Powertools for AWS Lambda [tenets](../index.md#tenets) is to be progressive. This means that our utilities are designed to be incrementally adopted by customers at any stage of their serverless journey. diff --git a/examples/app/CHANGELOG.md b/examples/app/CHANGELOG.md index 98ef99ebeb..621ae9dd3a 100644 --- a/examples/app/CHANGELOG.md +++ b/examples/app/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + +**Note:** Version bump only for package powertools-sample-app + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) **Note:** Version bump only for package powertools-sample-app diff --git a/examples/app/package.json b/examples/app/package.json index e6e590a119..cc0b7ed762 100644 --- a/examples/app/package.json +++ b/examples/app/package.json @@ -1,6 +1,6 @@ { "name": "powertools-sample-app", - "version": "2.16.0", + "version": "2.17.0", "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com" @@ -28,30 +28,30 @@ "#errors": "./functions/commons/errors.js" }, "devDependencies": { - "@types/aws-lambda": "^8.10.147", - "@types/node": "22.13.9", - "aws-cdk-lib": "^2.181.1", + "@types/aws-lambda": "^8.10.148", + "@types/node": "22.13.13", + "aws-cdk-lib": "^2.185.0", "constructs": "^10.4.2", "source-map-support": "^0.5.21", "tsx": "^4.19.3", - "typescript": "^5.7.3", + "typescript": "^5.8.2", "vitest": "^3.0.5" }, "dependencies": { - "@aws-lambda-powertools/batch": "^2.16.0", - "@aws-lambda-powertools/idempotency": "^2.16.0", - "@aws-lambda-powertools/logger": "^2.16.0", - "@aws-lambda-powertools/metrics": "^2.16.0", - "@aws-lambda-powertools/parameters": "^2.16.0", - "@aws-lambda-powertools/tracer": "^2.16.0", - "@aws-sdk/client-ssm": "^3.759.0", - "@aws-sdk/lib-dynamodb": "^3.758.0", + "@aws-lambda-powertools/batch": "^2.17.0", + "@aws-lambda-powertools/idempotency": "^2.17.0", + "@aws-lambda-powertools/logger": "^2.17.0", + "@aws-lambda-powertools/metrics": "^2.17.0", + "@aws-lambda-powertools/parameters": "^2.17.0", + "@aws-lambda-powertools/tracer": "^2.17.0", + "@aws-sdk/client-ssm": "^3.772.0", + "@aws-sdk/lib-dynamodb": "^3.772.0", "@middy/core": "^4.7.0", - "@types/aws-lambda": "^8.10.147", - "@types/node": "22.13.9", - "aws-cdk": "^2.1002.0", + "@types/aws-lambda": "^8.10.148", + "@types/node": "22.13.13", + "aws-cdk": "^2.1005.0", "constructs": "^10.4.2", - "esbuild": "^0.25.0", - "typescript": "^5.7.3" + "esbuild": "^0.25.1", + "typescript": "^5.8.2" } } diff --git a/examples/snippets/CHANGELOG.md b/examples/snippets/CHANGELOG.md index 66abea5d81..1a62f8e50f 100644 --- a/examples/snippets/CHANGELOG.md +++ b/examples/snippets/CHANGELOG.md @@ -3,6 +3,24 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + + +### Bug Fixes + +* **logger:** correctly refresh sample rate ([#3722](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3722)) ([2692ca4](https://github.com/aws-powertools/powertools-lambda-typescript/commit/2692ca4d1b15763936659b05e1830d998a4d2020)) +* **parser:** ddb base schema + other exports ([#3741](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3741)) ([51a3410](https://github.com/aws-powertools/powertools-lambda-typescript/commit/51a3410be8502496362d5ed13a64fe55691604ba)) + + +### Features + +* **logger:** set correlation ID in logs ([#3726](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3726)) ([aa74fc8](https://github.com/aws-powertools/powertools-lambda-typescript/commit/aa74fc8548ccb8cb313ffd1742184c66e8d6c22c)) +* **metrics:** allow setting functionName via constructor parameter and environment variable ([#3696](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3696)) ([3176fa0](https://github.com/aws-powertools/powertools-lambda-typescript/commit/3176fa08e1886d5c86e7b327134cc988b82cf8d8)) + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) diff --git a/examples/snippets/event-handler/.gitignore b/examples/snippets/event-handler/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/snippets/logger/correlationIdDecorator.ts b/examples/snippets/logger/correlationIdDecorator.ts new file mode 100644 index 0000000000..f4b93fb7f4 --- /dev/null +++ b/examples/snippets/logger/correlationIdDecorator.ts @@ -0,0 +1,19 @@ +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { search } from '@aws-lambda-powertools/logger/correlationId'; + +const logger = new Logger({ + correlationIdSearchFn: search, +}); + +class Lambda implements LambdaInterface { + @logger.injectLambdaContext({ + correlationIdPath: 'headers.my_request_id_header', + }) + public async handler(_event: unknown, _context: unknown): Promise { + logger.info('This is an INFO log with some context'); + } +} + +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); diff --git a/examples/snippets/logger/correlationIdLogger.ts b/examples/snippets/logger/correlationIdLogger.ts new file mode 100644 index 0000000000..3c003fb0a5 --- /dev/null +++ b/examples/snippets/logger/correlationIdLogger.ts @@ -0,0 +1,6 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { search } from '@aws-lambda-powertools/logger/correlationId'; + +const logger = new Logger({ + correlationIdSearchFn: search, +}); diff --git a/examples/snippets/logger/correlationIdManual.ts b/examples/snippets/logger/correlationIdManual.ts new file mode 100644 index 0000000000..1037bdd86f --- /dev/null +++ b/examples/snippets/logger/correlationIdManual.ts @@ -0,0 +1,10 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import type { APIGatewayProxyEvent } from 'aws-lambda'; + +const logger = new Logger(); + +export const handler = async (event: APIGatewayProxyEvent) => { + logger.setCorrelationId(event.requestContext.requestId); // (1)! + + logger.info('log with correlation_id'); +}; diff --git a/examples/snippets/logger/correlationIdMiddy.ts b/examples/snippets/logger/correlationIdMiddy.ts new file mode 100644 index 0000000000..c0a4ff10d6 --- /dev/null +++ b/examples/snippets/logger/correlationIdMiddy.ts @@ -0,0 +1,18 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { search } from '@aws-lambda-powertools/logger/correlationId'; +import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware'; +import middy from '@middy/core'; + +const logger = new Logger({ + correlationIdSearchFn: search, +}); + +export const handler = middy() + .use( + injectLambdaContext(logger, { + correlationIdPath: 'headers.my_request_id_header', + }) + ) + .handler(async () => { + logger.info('log with correlation_id'); + }); diff --git a/examples/snippets/logger/correlationIdPaths.ts b/examples/snippets/logger/correlationIdPaths.ts new file mode 100644 index 0000000000..b6cb3f500e --- /dev/null +++ b/examples/snippets/logger/correlationIdPaths.ts @@ -0,0 +1,22 @@ +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { + correlationPaths, + search, +} from '@aws-lambda-powertools/logger/correlationId'; + +const logger = new Logger({ + correlationIdSearchFn: search, +}); + +class Lambda implements LambdaInterface { + @logger.injectLambdaContext({ + correlationIdPath: correlationPaths.API_GATEWAY_REST, + }) + public async handler(_event: unknown, _context: unknown): Promise { + logger.info('This is an INFO log with some context'); + } +} + +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); diff --git a/examples/snippets/logger/logSampling.ts b/examples/snippets/logger/logSampling.ts index 0067afb73f..94318e4454 100644 --- a/examples/snippets/logger/logSampling.ts +++ b/examples/snippets/logger/logSampling.ts @@ -1,27 +1,16 @@ import { Logger } from '@aws-lambda-powertools/logger'; -// Notice the log level set to 'ERROR' const logger = new Logger({ - logLevel: 'ERROR', + logLevel: 'ERROR', // (1)! sampleRateValue: 0.5, }); -export const handler = async ( - _event: unknown, - _context: unknown -): Promise => { - // Refresh sample rate calculation on runtime, only when not using injectLambdaContext - logger.refreshSampleRateCalculation(); - // This log item (equal to log level 'ERROR') will be printed to standard output - // in all Lambda invocations - logger.error('This is an ERROR log'); +export const handler = async () => { + logger.refreshSampleRateCalculation(); // (2)! - // These log items (below the log level 'ERROR') have ~50% chance - // of being printed in a Lambda invocation - logger.debug('This is a DEBUG log that has 50% chance of being printed'); - logger.info('This is an INFO log that has 50% chance of being printed'); - logger.warn('This is a WARN log that has 50% chance of being printed'); + logger.error('This log is always emitted'); - // Optional: refresh sample rate calculation on runtime - // logger.refreshSampleRateCalculation(); + logger.debug('This log has ~50% chance of being emitted'); + logger.info('This log has ~50% chance of being emitted'); + logger.warn('This log has ~50% chance of being emitted'); }; diff --git a/examples/snippets/logger/samples/correlationIdOutput.json b/examples/snippets/logger/samples/correlationIdOutput.json new file mode 100644 index 0000000000..b71e1984dc --- /dev/null +++ b/examples/snippets/logger/samples/correlationIdOutput.json @@ -0,0 +1,7 @@ +{ + "level": "INFO", + "message": "This is an INFO log with some context", + "timestamp": "2021-05-03 11:47:12,494+0000", + "service": "payment", + "correlation_id": "correlation_id_value" +} diff --git a/examples/snippets/logger/samples/correlationIdPayload.json b/examples/snippets/logger/samples/correlationIdPayload.json new file mode 100644 index 0000000000..ce2e8d8014 --- /dev/null +++ b/examples/snippets/logger/samples/correlationIdPayload.json @@ -0,0 +1,5 @@ +{ + "headers": { + "my_request_id_header": "correlation_id_value" + } +} diff --git a/examples/snippets/logger/samples/debugLogSamplingNotSampled.json b/examples/snippets/logger/samples/debugLogSamplingNotSampled.json new file mode 100644 index 0000000000..f876f36306 --- /dev/null +++ b/examples/snippets/logger/samples/debugLogSamplingNotSampled.json @@ -0,0 +1,8 @@ +{ + "level": "ERROR", + "message": "This log is always emitted", + "sampling_rate": "0.5", + "service": "serverlessAirline", + "timestamp": "2021-12-12T22:59:06.334Z", + "xray_trace_id": "abcdef123456abcdef123456abcdef123456" +} \ No newline at end of file diff --git a/examples/snippets/logger/samples/debugLogSamplingSampled.json b/examples/snippets/logger/samples/debugLogSamplingSampled.json new file mode 100644 index 0000000000..ae95e28aa1 --- /dev/null +++ b/examples/snippets/logger/samples/debugLogSamplingSampled.json @@ -0,0 +1,34 @@ +[ + { + "level": "ERROR", + "message": "This log is always emitted", + "sampling_rate": "0.5", + "service": "serverlessAirline", + "timestamp": "2021-12-12T22:59:06.334Z", + "xray_trace_id": "abcdef123456abcdef123456abcdef123456" + }, + { + "level": "DEBUG", + "message": "This log has ~50% chance of being emitted", + "sampling_rate": "0.5", + "service": "serverlessAirline", + "timestamp": "2021-12-12T22:59:06.337Z", + "xray_trace_id": "abcdef123456abcdef123456abcdef123456" + }, + { + "level": "INFO", + "message": "This log has ~50% chance of being emitted", + "sampling_rate": "0.5", + "service": "serverlessAirline", + "timestamp": "2021-12-12T22:59:06.338Z", + "xray_trace_id": "abcdef123456abcdef123456abcdef123456" + }, + { + "level": "WARN", + "message": "This log has ~50% chance of being emitted", + "sampling_rate": "0.5", + "service": "serverlessAirline", + "timestamp": "2021-12-12T22:59:06.338Z", + "xray_trace_id": "abcdef123456abcdef123456abcdef123456" + } +] \ No newline at end of file diff --git a/examples/snippets/metrics/captureColdStartMetric.ts b/examples/snippets/metrics/captureColdStartMetric.ts new file mode 100644 index 0000000000..289934f9a7 --- /dev/null +++ b/examples/snippets/metrics/captureColdStartMetric.ts @@ -0,0 +1,17 @@ +import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; + +const metrics = new Metrics({ + namespace: 'serverlessAirline', + serviceName: 'orders', +}); + +export const handler = async ( + _event: unknown, + _context: unknown +): Promise => { + metrics.captureColdStartMetric('my-function-name'); + + metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + + metrics.publishStoredMetrics(); +}; diff --git a/examples/snippets/metrics/functionName.ts b/examples/snippets/metrics/functionName.ts new file mode 100644 index 0000000000..2811a45af1 --- /dev/null +++ b/examples/snippets/metrics/functionName.ts @@ -0,0 +1,18 @@ +import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics'; + +const metrics = new Metrics({ + namespace: 'serverlessAirline', + serviceName: 'orders', + functionName: 'my-function-name', +}); + +export const handler = async ( + _event: unknown, + _context: unknown +): Promise => { + metrics.captureColdStartMetric(); + + metrics.addMetric('successfulBooking', MetricUnit.Count, 1); + + metrics.publishStoredMetrics(); +}; diff --git a/examples/snippets/package.json b/examples/snippets/package.json index 403c574f4b..7e21533935 100644 --- a/examples/snippets/package.json +++ b/examples/snippets/package.json @@ -1,6 +1,6 @@ { "name": "code-snippets", - "version": "2.16.0", + "version": "2.17.0", "description": "A collection code snippets for the Powertools for AWS Lambda (TypeScript) docs", "author": { "name": "Amazon Web Services", @@ -24,23 +24,22 @@ }, "homepage": "https://github.com/aws-powertools/powertools-lambda-typescript#readme", "devDependencies": { - "@aws-lambda-powertools/batch": "^2.16.0", - "@aws-lambda-powertools/idempotency": "^2.16.0", - "@aws-lambda-powertools/jmespath": "^2.16.0", - "@aws-lambda-powertools/logger": "^2.16.0", - "@aws-lambda-powertools/metrics": "^2.16.0", - "@aws-lambda-powertools/parameters": "^2.16.0", - "@aws-lambda-powertools/parser": "^2.16.0", - "@aws-lambda-powertools/tracer": "^2.16.0", - "@aws-sdk/client-appconfigdata": "^3.758.0", - "@aws-sdk/client-dynamodb": "^3.758.0", - "@aws-sdk/client-secrets-manager": "^3.758.0", - "@aws-sdk/client-ssm": "^3.759.0", - "@aws-sdk/util-dynamodb": "^3.758.0", + "@aws-lambda-powertools/batch": "^2.17.0", + "@aws-lambda-powertools/idempotency": "^2.17.0", + "@aws-lambda-powertools/jmespath": "^2.17.0", + "@aws-lambda-powertools/logger": "^2.17.0", + "@aws-lambda-powertools/metrics": "^2.17.0", + "@aws-lambda-powertools/parameters": "^2.17.0", + "@aws-lambda-powertools/parser": "^2.17.0", + "@aws-lambda-powertools/tracer": "^2.17.0", + "@aws-sdk/client-appconfigdata": "^3.772.0", + "@aws-sdk/client-dynamodb": "^3.772.0", + "@aws-sdk/client-secrets-manager": "^3.772.0", + "@aws-sdk/client-ssm": "^3.772.0", + "@aws-sdk/util-dynamodb": "^3.772.0", "@middy/core": "^4.7.0", "aws-sdk": "^2.1692.0", "aws-sdk-client-mock": "^4.1.0", - "hashi-vault-js": "^0.4.16", "zod": "^3.24.2" } } diff --git a/examples/snippets/parameters/customProviderVault.ts b/examples/snippets/parameters/customProviderVault.ts index 838f1adc9d..0bbae8252b 100644 --- a/examples/snippets/parameters/customProviderVault.ts +++ b/examples/snippets/parameters/customProviderVault.ts @@ -1,41 +1,31 @@ import { BaseProvider } from '@aws-lambda-powertools/parameters/base'; import { GetParameterError } from '@aws-lambda-powertools/parameters/errors'; -import Vault from 'hashi-vault-js'; import type { HashiCorpVaultGetOptions, HashiCorpVaultProviderOptions, } from './customProviderVaultTypes.js'; class HashiCorpVaultProvider extends BaseProvider { - public client: Vault; + readonly #baseUrl: string; readonly #token: string; + readonly #rootPath?: string; + readonly #timeout: number; + readonly #abortController: AbortController; /** * It initializes the HashiCorpVaultProvider class. * - * @param {HashiCorpVaultProviderOptions} config - The configuration object. + * @param config - The configuration object. */ public constructor(config: HashiCorpVaultProviderOptions) { super({}); - const { url, token, clientConfig, vaultClient } = config; - if (vaultClient) { - if (vaultClient instanceof Vault) { - this.client = vaultClient; - } else { - throw Error('Not a valid Vault client provided'); - } - } else { - const config = { - baseUrl: url, - ...(clientConfig ?? { - timeout: 10000, - rootPath: '', - }), - }; - this.client = new Vault(config); - } + const { url, token, rootPath, timeout } = config; + this.#baseUrl = url; + this.#rootPath = rootPath ?? 'secret'; + this.#timeout = timeout ?? 5000; this.#token = token; + this.#abortController = new AbortController(); } /** @@ -46,8 +36,8 @@ class HashiCorpVaultProvider extends BaseProvider { * * `forceFetch` - Whether to always fetch a new value from the store regardless if already available in cache * * `sdkOptions` - Extra options to pass to the HashiCorp Vault SDK, e.g. `mount` or `version` * - * @param {string} name - The name of the secret - * @param {HashiCorpVaultGetOptions} options - Options to customize the retrieval of the secret + * @param name - The name of the secret + * @param options - Options to customize the retrieval of the secret */ public async get>( name: string, @@ -68,27 +58,36 @@ class HashiCorpVaultProvider extends BaseProvider { /** * Retrieve a secret from HashiCorp Vault. * - * @param {string} name - The name of the secret - * @param {HashiCorpVaultGetOptions} options - Options to customize the retrieval of the secret + * @param name - The name of the secret + * @param options - Options to customize the retrieval of the secret */ protected async _get( name: string, options?: HashiCorpVaultGetOptions ): Promise> { - const mount = options?.sdkOptions?.mount ?? 'secret'; - const version = options?.sdkOptions?.version; + const { sdkOptions } = options ?? {}; + const mount = sdkOptions?.mount ?? this.#rootPath; + const version = sdkOptions?.version + ? `?version=${sdkOptions?.version}` + : ''; - const response = await this.client.readKVSecret( - this.#token, - name, - version, - mount - ); + setTimeout(() => { + this.#abortController.abort(); + }, this.#timeout); - if (response.isVaultError) { - throw response; + const res = await fetch( + `${this.#baseUrl}/${mount}/data/${name}${version}`, + { + headers: { 'X-Vault-Token': this.#token }, + method: 'GET', + signal: this.#abortController.signal, + } + ); + if (!res.ok) { + throw new GetParameterError(`Failed to fetch secret ${res.statusText}`); } - return response.data; + const response = await res.json(); + return response.data.data; } /** diff --git a/examples/snippets/parameters/customProviderVaultTypes.ts b/examples/snippets/parameters/customProviderVaultTypes.ts index 4542865e49..d9436134b5 100644 --- a/examples/snippets/parameters/customProviderVaultTypes.ts +++ b/examples/snippets/parameters/customProviderVaultTypes.ts @@ -1,11 +1,13 @@ import type { GetOptionsInterface } from '@aws-lambda-powertools/parameters/base/types'; -import type Vault from 'hashi-vault-js'; /** - * Base interface for HashiCorpVaultProviderOptions. - * @interface + * Options for the HashiCorpVaultProvider class constructor. + * + * @param {string} url - Indicate the server name/IP, port and API version for the Vault instance, all paths are relative to this one. + * @param {string} token - The Vault token to use for authentication. + * */ -interface HashiCorpVaultProviderOptionsBase { +interface HashiCorpVaultProviderOptions { /** * Indicate the server name/IP, port and API version for the Vault instance, all paths are relative to this one. * @example 'https://vault.example.com:8200/v1' @@ -15,53 +17,18 @@ interface HashiCorpVaultProviderOptionsBase { * The Vault token to use for authentication. */ token: string; -} - -/** - * Interface for HashiCorpVaultProviderOptions with clientConfig property. - * @interface - */ -interface HashiCorpVaultProviderOptionsWithClientConfig - extends HashiCorpVaultProviderOptionsBase { /** - * Optional configuration to pass during client initialization to customize the `hashi-vault-js` client. + * The root path to use for the secret engine. Defaults to `secret`. */ - clientConfig?: unknown; + rootPath?: string; /** - * This property should never be passed. + * The timeout in milliseconds for the HTTP requests. Defaults to `5000`. + * @example 10000 + * @default 5000 */ - vaultClient?: never; + timeout?: number; } -/** - * Interface for HashiCorpVaultProviderOptions with vaultClient property. - * - * @interface - */ -interface HashiCorpVaultProviderOptionsWithClientInstance - extends HashiCorpVaultProviderOptionsBase { - /** - * Optional `hashi-vault-js` client to pass during HashiCorpVaultProvider class instantiation. If not provided, a new client will be created. - */ - vaultClient?: Vault; - /** - * This property should never be passed. - */ - clientConfig: never; -} - -/** - * Options for the HashiCorpVaultProvider class constructor. - * - * @param {string} url - Indicate the server name/IP, port and API version for the Vault instance, all paths are relative to this one. - * @param {string} token - The Vault token to use for authentication. - * @param {Vault.VaultConfig} [clientConfig] - Optional configuration to pass during client initialization, e.g. timeout. Mutually exclusive with vaultClient. - * @param {Vault} [vaultClient] - Optional `hashi-vault-js` client to pass during HashiCorpVaultProvider class instantiation. Mutually exclusive with clientConfig. - */ -type HashiCorpVaultProviderOptions = - | HashiCorpVaultProviderOptionsWithClientConfig - | HashiCorpVaultProviderOptionsWithClientInstance; - type HashiCorpVaultReadKVSecretOptions = { /** * The mount point of the secret engine to use. Defaults to `secret`. diff --git a/examples/snippets/parameters/customProviderVaultUsage.ts b/examples/snippets/parameters/customProviderVaultUsage.ts index 5ead5b0a46..014f3b3a2f 100644 --- a/examples/snippets/parameters/customProviderVaultUsage.ts +++ b/examples/snippets/parameters/customProviderVaultUsage.ts @@ -1,25 +1,22 @@ import { Logger } from '@aws-lambda-powertools/logger'; import { HashiCorpVaultProvider } from './customProviderVault.js'; -const logger = new Logger({ logLevel: 'DEBUG' }); +const logger = new Logger({ serviceName: 'serverless-airline' }); const secretsProvider = new HashiCorpVaultProvider({ url: 'https://vault.example.com:8200/v1', token: process.env.ROOT_TOKEN ?? '', + rootPath: 'kv', }); -try { - // Retrieve a secret from HashiCorp Vault - const secret = await secretsProvider.get<{ foo: 'string' }>('my-secret', { - sdkOptions: { - mount: 'secrets', - }, - }); - if (!secret) { - throw new Error('Secret not found'); - } - logger.debug('Secret retrieved!'); -} catch (error) { - if (error instanceof Error) { - logger.error(error.message, error); - } -} +// Retrieve a secret from HashiCorp Vault +const secret = await secretsProvider.get<{ foo: 'string' }>('my-secret'); + +const res = await fetch('https://example.com/api', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${secret?.foo}`, + }, + body: JSON.stringify({ data: 'example' }), +}); +logger.debug('res status', { status: res.status }); diff --git a/examples/snippets/parser/envelopeDecorator.ts b/examples/snippets/parser/envelopeDecorator.ts index 5b04d80488..5fd09d23e1 100644 --- a/examples/snippets/parser/envelopeDecorator.ts +++ b/examples/snippets/parser/envelopeDecorator.ts @@ -1,7 +1,7 @@ import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; import { Logger } from '@aws-lambda-powertools/logger'; import { parser } from '@aws-lambda-powertools/parser'; -import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes/eventbridge'; import type { Context } from 'aws-lambda'; import { z } from 'zod'; diff --git a/examples/snippets/parser/envelopeMiddy.ts b/examples/snippets/parser/envelopeMiddy.ts index 0a6034970f..0a20ad1132 100644 --- a/examples/snippets/parser/envelopeMiddy.ts +++ b/examples/snippets/parser/envelopeMiddy.ts @@ -1,8 +1,7 @@ import { Logger } from '@aws-lambda-powertools/logger'; -import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes/eventbridge'; import { parser } from '@aws-lambda-powertools/parser/middleware'; import middy from '@middy/core'; -import type { Context } from 'aws-lambda'; import { z } from 'zod'; const logger = new Logger(); @@ -20,18 +19,11 @@ const orderSchema = z.object({ optionalField: z.string().optional(), }); -type Order = z.infer; - -const lambdaHandler = async ( - event: Order, - _context: Context -): Promise => { - for (const item of event.items) { - // item is parsed as OrderItem - logger.info('Processing item', { item }); - } -}; - -export const handler = middy(lambdaHandler).use( - parser({ schema: orderSchema, envelope: EventBridgeEnvelope }) -); +export const handler = middy() + .use(parser({ schema: orderSchema, envelope: EventBridgeEnvelope })) + .handler(async (event): Promise => { + for (const item of event.items) { + // item is parsed as OrderItem + logger.info('Processing item', { item }); + } + }); diff --git a/examples/snippets/parser/extend.ts b/examples/snippets/parser/extend.ts index 8dca184aa4..d49cf406dd 100644 --- a/examples/snippets/parser/extend.ts +++ b/examples/snippets/parser/extend.ts @@ -1,7 +1,7 @@ import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; import { Logger } from '@aws-lambda-powertools/logger'; import { parser } from '@aws-lambda-powertools/parser'; -import { EventBridgeSchema } from '@aws-lambda-powertools/parser/schemas'; +import { EventBridgeSchema } from '@aws-lambda-powertools/parser/schemas/eventbridge'; import type { Context } from 'aws-lambda'; import { z } from 'zod'; diff --git a/examples/snippets/parser/extendDynamoDBStreamSchema.ts b/examples/snippets/parser/extendDynamoDBStreamSchema.ts index 86639f5cfb..30fc8abe2e 100644 --- a/examples/snippets/parser/extendDynamoDBStreamSchema.ts +++ b/examples/snippets/parser/extendDynamoDBStreamSchema.ts @@ -1,5 +1,6 @@ import { DynamoDBMarshalled } from '@aws-lambda-powertools/parser/helpers/dynamodb'; import { + DynamoDBStreamChangeRecordBase, DynamoDBStreamRecord, DynamoDBStreamSchema, } from '@aws-lambda-powertools/parser/schemas/dynamodb'; @@ -13,7 +14,7 @@ const customSchema = z.object({ const extendedSchema = DynamoDBStreamSchema.extend({ Records: z.array( DynamoDBStreamRecord.extend({ - dynamodb: z.object({ + dynamodb: DynamoDBStreamChangeRecordBase.extend({ NewImage: DynamoDBMarshalled(customSchema).optional(), }), }) diff --git a/examples/snippets/parser/handlerSafeParseDecorator.ts b/examples/snippets/parser/handlerSafeParseDecorator.ts index ac081c5e66..2c6afbe2ce 100644 --- a/examples/snippets/parser/handlerSafeParseDecorator.ts +++ b/examples/snippets/parser/handlerSafeParseDecorator.ts @@ -1,7 +1,7 @@ import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; import { Logger } from '@aws-lambda-powertools/logger'; import { parser } from '@aws-lambda-powertools/parser'; -import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes/eventbridge'; import type { EventBridgeEvent, ParsedResult, diff --git a/examples/snippets/parser/manual.ts b/examples/snippets/parser/manual.ts index 839f62bafd..c0b33295d0 100644 --- a/examples/snippets/parser/manual.ts +++ b/examples/snippets/parser/manual.ts @@ -1,6 +1,6 @@ import { Logger } from '@aws-lambda-powertools/logger'; -import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; -import { EventBridgeSchema } from '@aws-lambda-powertools/parser/schemas'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes/eventbridge'; +import { EventBridgeSchema } from '@aws-lambda-powertools/parser/schemas/eventbridge'; import type { EventBridgeEvent } from '@aws-lambda-powertools/parser/types'; import type { Context } from 'aws-lambda'; import { z } from 'zod'; diff --git a/examples/snippets/parser/manualSafeParse.ts b/examples/snippets/parser/manualSafeParse.ts index 63dc0ac204..5af37162da 100644 --- a/examples/snippets/parser/manualSafeParse.ts +++ b/examples/snippets/parser/manualSafeParse.ts @@ -1,6 +1,6 @@ import { Logger } from '@aws-lambda-powertools/logger'; -import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; -import { EventBridgeSchema } from '@aws-lambda-powertools/parser/schemas'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes/eventbridge'; +import { EventBridgeSchema } from '@aws-lambda-powertools/parser/schemas/eventbridge'; import type { EventBridgeEvent } from '@aws-lambda-powertools/parser/types'; import type { Context } from 'aws-lambda'; import { z } from 'zod'; diff --git a/examples/snippets/parser/middy.ts b/examples/snippets/parser/middy.ts index 763fa8246a..e8c19fb3cf 100644 --- a/examples/snippets/parser/middy.ts +++ b/examples/snippets/parser/middy.ts @@ -1,7 +1,6 @@ import { Logger } from '@aws-lambda-powertools/logger'; import { parser } from '@aws-lambda-powertools/parser/middleware'; import middy from '@middy/core'; -import type { Context } from 'aws-lambda'; import { z } from 'zod'; const logger = new Logger(); @@ -19,18 +18,11 @@ const orderSchema = z.object({ optionalField: z.string().optional(), }); -type Order = z.infer; - -const lambdaHandler = async ( - event: Order, - _context: Context -): Promise => { - for (const item of event.items) { - // item is parsed as OrderItem - logger.info('Processing item', { item }); - } -}; - -export const handler = middy(lambdaHandler).use( - parser({ schema: orderSchema }) -); +export const handler = middy() + .use(parser({ schema: orderSchema })) + .handler(async (event): Promise => { + for (const item of event.items) { + // item is parsed as OrderItem + logger.info('Processing item', { item }); + } + }); diff --git a/examples/snippets/parser/safeParseDecorator.ts b/examples/snippets/parser/safeParseDecorator.ts index a9e466be92..67caf35c6e 100644 --- a/examples/snippets/parser/safeParseDecorator.ts +++ b/examples/snippets/parser/safeParseDecorator.ts @@ -1,7 +1,7 @@ import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; import { Logger } from '@aws-lambda-powertools/logger'; import { parser } from '@aws-lambda-powertools/parser'; -import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes/eventbridge'; import type { EventBridgeEvent, ParsedResult, @@ -30,20 +30,19 @@ class Lambda implements LambdaInterface { @parser({ schema: orderSchema, envelope: EventBridgeEnvelope, - safeParse: true, - }) // (1)! + safeParse: true, // (1)! + }) public async handler( event: ParsedResult, _context: Context ): Promise { if (event.success) { - // (2)! for (const item of event.data.items) { - logger.info('Processing item', { item }); // (3)! + logger.info('Processing item', { item }); // (2)! } } else { - logger.error('Failed to parse event', event.error); // (4)! - logger.error('Original event is: ', event.originalEvent); // (5)! + logger.error('Failed to parse event', { error: event.error }); // (3)! + logger.error('Original event is ', { original: event.originalEvent }); // (4)! } } } diff --git a/examples/snippets/parser/safeParseMiddy.ts b/examples/snippets/parser/safeParseMiddy.ts index d73bc49096..e7efb5cb2b 100644 --- a/examples/snippets/parser/safeParseMiddy.ts +++ b/examples/snippets/parser/safeParseMiddy.ts @@ -1,11 +1,6 @@ import { Logger } from '@aws-lambda-powertools/logger'; import { parser } from '@aws-lambda-powertools/parser/middleware'; -import type { - EventBridgeEvent, - ParsedResult, -} from '@aws-lambda-powertools/parser/types'; import middy from '@middy/core'; -import type { Context } from 'aws-lambda'; import { z } from 'zod'; const logger = new Logger(); @@ -23,23 +18,17 @@ const orderSchema = z.object({ optionalField: z.string().optional(), }); -type Order = z.infer; - -const lambdaHandler = async ( - event: ParsedResult, - _context: Context -): Promise => { - if (event.success) { - // (2)! - for (const item of event.data.items) { - logger.info('Processing item', { item }); // (3)! +export const handler = middy() + .use( + parser({ schema: orderSchema, safeParse: true }) // (1)! + ) + .handler(async (event): Promise => { + if (event.success) { + for (const item of event.data.items) { + logger.info('Processing item', { item }); // (2)! + } + } else { + logger.error('Error parsing event', { event: event.error }); // (3)! + logger.error('Original event', { event: event.originalEvent }); // (4)! } - } else { - logger.error('Error parsing event', { event: event.error }); // (4)! - logger.error('Original event', { event: event.originalEvent }); // (5)! - } -}; - -export const handler = middy(lambdaHandler).use( - parser({ schema: orderSchema, safeParse: true }) // (1)! -); + }); diff --git a/examples/snippets/parser/unitTestDecorator.ts b/examples/snippets/parser/unitTestDecorator.ts index 3e327a6224..5653cabd3c 100644 --- a/examples/snippets/parser/unitTestDecorator.ts +++ b/examples/snippets/parser/unitTestDecorator.ts @@ -1,4 +1,5 @@ import type { Context } from 'aws-lambda'; +import { describe, expect, it } from 'vitest'; import { handler } from './decorator.js'; import type { Order } from './schema.js'; diff --git a/examples/snippets/parser/unitTestSafeParse.ts b/examples/snippets/parser/unitTestSafeParse.ts index bc2e80a220..cad6f08006 100644 --- a/examples/snippets/parser/unitTestSafeParse.ts +++ b/examples/snippets/parser/unitTestSafeParse.ts @@ -3,6 +3,7 @@ import type { ParsedResult, } from '@aws-lambda-powertools/parser/types'; import type { Context } from 'aws-lambda'; +import { describe, expect, it } from 'vitest'; import { handler } from './safeParseDecorator.js'; import type { Order } from './schema.js'; diff --git a/examples/snippets/validation/advancedBringAjvInstance.ts b/examples/snippets/validation/advancedBringAjvInstance.ts new file mode 100644 index 0000000000..8d0187e09b --- /dev/null +++ b/examples/snippets/validation/advancedBringAjvInstance.ts @@ -0,0 +1,30 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { validate } from '@aws-lambda-powertools/validation'; +import { SchemaValidationError } from '@aws-lambda-powertools/validation/errors'; +import Ajv2019 from 'ajv/dist/2019'; +import { inboundSchema } from './schemas.js'; + +const logger = new Logger(); + +const ajv = new Ajv2019(); + +export const handler = async (event: unknown) => { + try { + await validate({ + payload: event, + schema: inboundSchema, + ajv, // (1)! + }); + + return { + message: 'ok', + }; + } catch (error) { + if (error instanceof SchemaValidationError) { + logger.error('Schema validation failed', error); + throw new Error('Invalid event payload'); + } + + throw error; + } +}; diff --git a/examples/snippets/validation/advancedCustomFormats.ts b/examples/snippets/validation/advancedCustomFormats.ts new file mode 100644 index 0000000000..ccc3f8610b --- /dev/null +++ b/examples/snippets/validation/advancedCustomFormats.ts @@ -0,0 +1,43 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { validate } from '@aws-lambda-powertools/validation'; +import { SchemaValidationError } from '@aws-lambda-powertools/validation/errors'; +import schemaWithCustomFormat from './samples/schemaWithCustomFormat.json'; + +const logger = new Logger(); + +const customFormats = { + awsaccountid: /^\d{12}$/, + creditcard: (value: string) => { + // Luhn algorithm (for demonstration purposes only - do not use in production) + const sum = value + .split('') + .reverse() + .reduce((acc, digit, index) => { + const num = Number.parseInt(digit, 10); + return acc + (index % 2 === 0 ? num : num < 5 ? num * 2 : num * 2 - 9); + }, 0); + + return sum % 10 === 0; + }, +}; + +export const handler = async (event: unknown) => { + try { + await validate({ + payload: event, + schema: schemaWithCustomFormat, + formats: customFormats, + }); + + return { + message: 'ok', + }; + } catch (error) { + if (error instanceof SchemaValidationError) { + logger.error('Schema validation failed', error); + throw new Error('Invalid event payload'); + } + + throw error; + } +}; diff --git a/examples/snippets/validation/advancedExternalRefs.ts b/examples/snippets/validation/advancedExternalRefs.ts new file mode 100644 index 0000000000..87ab0824ac --- /dev/null +++ b/examples/snippets/validation/advancedExternalRefs.ts @@ -0,0 +1,25 @@ +import { validator } from '@aws-lambda-powertools/validation/decorator'; +import type { Context } from 'aws-lambda'; +import { + type InboundSchema, + defsSchema, + inboundSchema, + outboundSchema, +} from './schemasWithExternalRefs.js'; + +class Lambda { + @validator({ + inboundSchema, + outboundSchema, + externalRefs: [defsSchema], + }) + async handler(event: InboundSchema, _context: Context) { + return { + message: `processed ${event.userId}`, + success: true, + }; + } +} + +const lambda = new Lambda(); +export const handler = lambda.handler.bind(lambda); diff --git a/examples/snippets/validation/gettingStartedDecorator.ts b/examples/snippets/validation/gettingStartedDecorator.ts new file mode 100644 index 0000000000..50cf98fa14 --- /dev/null +++ b/examples/snippets/validation/gettingStartedDecorator.ts @@ -0,0 +1,27 @@ +import { validator } from '@aws-lambda-powertools/validation/decorator'; +import type { Context } from 'aws-lambda'; +import { + type InboundSchema, + type OutboundSchema, + inboundSchema, + outboundSchema, +} from './schemas.js'; + +class Lambda { + @validator({ + inboundSchema, + outboundSchema, + }) + async handler( + event: InboundSchema, + _context: Context + ): Promise { + return { + statusCode: 200, + body: `Hello from ${event.userId}`, + }; + } +} + +const lambda = new Lambda(); +export const handler = lambda.handler.bind(lambda); diff --git a/examples/snippets/validation/gettingStartedEnvelope.ts b/examples/snippets/validation/gettingStartedEnvelope.ts new file mode 100644 index 0000000000..520e006bed --- /dev/null +++ b/examples/snippets/validation/gettingStartedEnvelope.ts @@ -0,0 +1,19 @@ +import { validator } from '@aws-lambda-powertools/validation/decorator'; +import type { Context } from 'aws-lambda'; +import { type InboundSchema, inboundSchema } from './schemas.js'; + +class Lambda { + @validator({ + inboundSchema, + envelope: 'detail', + }) + async handler(event: InboundSchema, context: Context) { + return { + message: `processed ${event.userId}`, + success: true, + }; + } +} + +const lambda = new Lambda(); +export const handler = lambda.handler.bind(lambda); diff --git a/examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts b/examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts new file mode 100644 index 0000000000..0cd1f788fa --- /dev/null +++ b/examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts @@ -0,0 +1,20 @@ +import { SQS } from '@aws-lambda-powertools/jmespath/envelopes'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { validator } from '@aws-lambda-powertools/validation/middleware'; +import middy from '@middy/core'; +import { type InboundSchema, inboundSchema } from './schemas.js'; + +const logger = new Logger(); + +export const handler = middy() + .use( + validator({ + inboundSchema, + envelope: SQS, + }) + ) + .handler(async (event: Array) => { + for (const record of event) { + logger.info(`Processing message ${record.userId}`); + } + }); diff --git a/examples/snippets/validation/gettingStartedMiddy.ts b/examples/snippets/validation/gettingStartedMiddy.ts new file mode 100644 index 0000000000..9b700ab9cf --- /dev/null +++ b/examples/snippets/validation/gettingStartedMiddy.ts @@ -0,0 +1,22 @@ +import { validator } from '@aws-lambda-powertools/validation/middleware'; +import middy from '@middy/core'; +import { + type InboundSchema, + type OutboundSchema, + inboundSchema, + outboundSchema, +} from './schemas.js'; + +export const handler = middy() + .use( + validator({ + inboundSchema, + outboundSchema, + }) + ) + .handler( + async (event: InboundSchema): Promise => ({ + statusCode: 200, + body: `Hello from ${event.userId}`, + }) + ); diff --git a/examples/snippets/validation/gettingStartedStandalone.ts b/examples/snippets/validation/gettingStartedStandalone.ts new file mode 100644 index 0000000000..3b8ae7aebd --- /dev/null +++ b/examples/snippets/validation/gettingStartedStandalone.ts @@ -0,0 +1,26 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { validate } from '@aws-lambda-powertools/validation'; +import { SchemaValidationError } from '@aws-lambda-powertools/validation/errors'; +import { type InboundSchema, inboundSchema } from './schemas.js'; + +const logger = new Logger(); + +export const handler = async (event: InboundSchema) => { + try { + validate({ + payload: event, + schema: inboundSchema, + }); + + return { + message: 'ok', // (1)! + }; + } catch (error) { + if (error instanceof SchemaValidationError) { + logger.error('Schema validation failed', error); + throw new Error('Invalid event payload'); + } + + throw error; + } +}; diff --git a/examples/snippets/validation/samples/gettingStartedEnvelopeEvent.json b/examples/snippets/validation/samples/gettingStartedEnvelopeEvent.json new file mode 100644 index 0000000000..17c1c5672e --- /dev/null +++ b/examples/snippets/validation/samples/gettingStartedEnvelopeEvent.json @@ -0,0 +1,13 @@ +{ + "version": "0", + "id": "12345678-1234-1234-1234-123456789012", + "detail-type": "myDetailType", + "source": "myEventSource", + "account": "123456789012", + "time": "2017-12-22T18:43:48Z", + "region": "us-west-2", + "resources": [], + "detail": { + "userId": "123" + } +} \ No newline at end of file diff --git a/examples/snippets/validation/samples/gettingStartedSQSEnvelopeEvent.json b/examples/snippets/validation/samples/gettingStartedSQSEnvelopeEvent.json new file mode 100644 index 0000000000..a8df4bf107 --- /dev/null +++ b/examples/snippets/validation/samples/gettingStartedSQSEnvelopeEvent.json @@ -0,0 +1,36 @@ +{ + "Records": [ + { + "messageId": "c80e8021-a70a-42c7-a470-796e1186f753", + "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", + "body": "{\"userId\":\"123\"}", + "attributes": { + "ApproximateReceiveCount": "3", + "SentTimestamp": "1529104986221", + "SenderId": "AIDAIC6K7FJUZ7Q", + "ApproximateFirstReceiveTimestamp": "1529104986230" + }, + "messageAttributes": {}, + "md5OfBody": "098f6bcd4621d373cade4e832627b4f6", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", + "awsRegion": "us-west-2" + }, + { + "messageId": "c80e8021-a70a-42c7-a470-796e1186f753", + "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", + "body": "{\"userId\":\"456\"}", + "attributes": { + "ApproximateReceiveCount": "3", + "SentTimestamp": "1529104986221", + "SenderId": "AIDAIC6K7FJUZ7Q", + "ApproximateFirstReceiveTimestamp": "1529104986230" + }, + "messageAttributes": {}, + "md5OfBody": "098f6bcd4621d373cade4e832627b4f6", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", + "awsRegion": "us-west-2" + } + ] +} \ No newline at end of file diff --git a/examples/snippets/validation/samples/schemaWithCustomFormat.json b/examples/snippets/validation/samples/schemaWithCustomFormat.json new file mode 100644 index 0000000000..954fe05ddd --- /dev/null +++ b/examples/snippets/validation/samples/schemaWithCustomFormat.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "properties": { + "accountId": { + "type": "string", + "format": "awsaccountid" + }, + "creditCard": { + "type": "string", + "format": "creditcard" + } + }, + "required": [ + "accountId" + ] +} \ No newline at end of file diff --git a/examples/snippets/validation/schemas.ts b/examples/snippets/validation/schemas.ts new file mode 100644 index 0000000000..60f6a199e5 --- /dev/null +++ b/examples/snippets/validation/schemas.ts @@ -0,0 +1,38 @@ +const inboundSchema = { + type: 'object', + properties: { + userId: { + type: 'string', + }, + }, + required: ['userId'], +} as const; + +type InboundSchema = { + userId: string; +}; + +const outboundSchema = { + type: 'object', + properties: { + body: { + type: 'string', + }, + statusCode: { + type: 'number', + }, + }, + required: ['body', 'statusCode'], +} as const; + +type OutboundSchema = { + body: string; + statusCode: number; +}; + +export { + inboundSchema, + outboundSchema, + type InboundSchema, + type OutboundSchema, +}; diff --git a/examples/snippets/validation/schemasWithExternalRefs.ts b/examples/snippets/validation/schemasWithExternalRefs.ts new file mode 100644 index 0000000000..d8a8a293ce --- /dev/null +++ b/examples/snippets/validation/schemasWithExternalRefs.ts @@ -0,0 +1,43 @@ +const defsSchema = { + $id: 'http://example.com/schemas/defs.json', + definitions: { + int: { type: 'integer' }, + str: { type: 'string' }, + }, +} as const; + +const inboundSchema = { + $id: 'http://example.com/schemas/inbound.json', + type: 'object', + properties: { + userId: { $ref: 'defs.json#/definitions/str' }, + }, + required: ['userId'], +} as const; + +type InboundSchema = { + userId: string; +}; + +const outboundSchema = { + $id: 'http://example.com/schemas/outbound.json', + type: 'object', + properties: { + body: { $ref: 'defs.json#/definitions/str' }, + statusCode: { $ref: 'defs.json#/definitions/int' }, + }, + required: ['body', 'statusCode'], +} as const; + +type OutboundSchema = { + body: string; + statusCode: number; +}; + +export { + defsSchema, + inboundSchema, + outboundSchema, + type InboundSchema, + type OutboundSchema, +}; diff --git a/layers/CHANGELOG.md b/layers/CHANGELOG.md index 1f71416fdb..38b84ee2d9 100644 --- a/layers/CHANGELOG.md +++ b/layers/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + +**Note:** Version bump only for package layers + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) **Note:** Version bump only for package layers diff --git a/layers/package.json b/layers/package.json index 319f82100d..4e390633d2 100644 --- a/layers/package.json +++ b/layers/package.json @@ -1,6 +1,6 @@ { "name": "layers", - "version": "2.16.0", + "version": "2.17.0", "bin": { "layer": "bin/layers.js" }, @@ -40,9 +40,9 @@ "source-map-support": "^0.5.21" }, "dependencies": { - "aws-cdk": "^2.1002.0", - "aws-cdk-lib": "^2.181.1", - "esbuild": "^0.25.0", + "aws-cdk": "^2.1005.0", + "aws-cdk-lib": "^2.185.0", + "esbuild": "^0.25.1", "tsx": "^4.19.3" } } diff --git a/layers/tests/e2e/constants.ts b/layers/tests/e2e/constants.ts index b6b477031b..e8c5d84251 100644 --- a/layers/tests/e2e/constants.ts +++ b/layers/tests/e2e/constants.ts @@ -1,5 +1 @@ export const RESOURCE_NAME_PREFIX = 'Layers-E2E'; -export const ONE_MINUTE = 60 * 1000; -export const TEST_CASE_TIMEOUT = 3 * ONE_MINUTE; -export const SETUP_TIMEOUT = 7 * ONE_MINUTE; -export const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; diff --git a/layers/tests/e2e/layerPublisher.test.ts b/layers/tests/e2e/layerPublisher.test.ts index cbffc8abfa..1f932ca98a 100644 --- a/layers/tests/e2e/layerPublisher.test.ts +++ b/layers/tests/e2e/layerPublisher.test.ts @@ -11,11 +11,7 @@ import { LayerVersion } from 'aws-cdk-lib/aws-lambda'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import packageJson from '../../package.json'; import { LayerPublisherStack } from '../../src/layer-publisher-stack.js'; -import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, -} from './constants.js'; +import { RESOURCE_NAME_PREFIX } from './constants.js'; /** * This test has two stacks: @@ -121,7 +117,7 @@ describe('Layers E2E tests', () => { }) ); } - }, SETUP_TIMEOUT); + }); it.each(cases)( 'imports and instantiates all utilities (%s)', @@ -198,5 +194,5 @@ describe('Layers E2E tests', () => { await testLayerStack.destroy(); await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/layers/vitest.config.ts b/layers/vitest.config.ts index d5aa737c68..1469ff8a66 100644 --- a/layers/vitest.config.ts +++ b/layers/vitest.config.ts @@ -3,5 +3,7 @@ import { defineProject } from 'vitest/config'; export default defineProject({ test: { environment: 'node', + hookTimeout: 1_000 * 60 * 10, // 10 minutes + testTimeout: 1_000 * 60 * 3, // 3 minutes }, }); diff --git a/lerna.json b/lerna.json index 982a374800..221add1dfa 100644 --- a/lerna.json +++ b/lerna.json @@ -16,7 +16,7 @@ "layers", "examples/snippets" ], - "version": "2.16.0", + "version": "2.17.0", "npmClient": "npm", "message": "chore(release): %s [skip ci]" } diff --git a/mkdocs.yml b/mkdocs.yml index 090841d535..5bfc63b52c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,12 +4,35 @@ site_author: Amazon Web Services repo_url: https://github.com/aws-powertools/powertools-lambda-typescript edit_uri: edit/main/docs site_url: https://docs.powertools.aws.dev/lambda/typescript +watch: [ + docs, + packages/batch/src, + examples/snippets/batch, + packages/commons/src, + packages/event-handler/src, + examples/snippets/event-handler, + packages/idempotency/src, + examples/snippets/idempotency, + packages/jmespath/src, + examples/snippets/jmespath, + packages/logger/src, + examples/snippets/logger, + packages/metrics/src, + examples/snippets/metrics, + packages/parameters/src, + examples/snippets/parameters, + packages/parser/src, + examples/snippets/parser, + packages/tracer/src, + examples/snippets/tracer, + packages/validation/src, + examples/snippets/validation, +] nav: - Homepage: - index.md - Changelog: changelog.md - - API reference: api/" target="_blank - Upgrade guide: upgrade.md - We Made This (Community): we_made_this.md - Workshop: https://s12d.com/powertools-for-aws-lambda-workshop" target="_blank @@ -23,6 +46,8 @@ nav: - utilities/batch.md - utilities/jmespath.md - utilities/parser.md + - utilities/validation.md + - API reference: api" - Processes: - Roadmap: roadmap.md - Versioning policy: versioning.md @@ -101,7 +126,7 @@ markdown_extensions: - pymdownx.tasklist: custom_checkbox: true -copyright: Copyright © 2023 Amazon Web Services +copyright: Copyright © 2025 Amazon Web Services plugins: - privacy @@ -112,6 +137,12 @@ plugins: - snippets/node_modules/* - snippets/package.json - snippets/CHANGELOG.md + - typedoc: + source: '.' + output_dir: 'api' + tsconfig: 'tsconfig.json' + options: 'typedoc.json' + name: 'API Reference' extra_css: - stylesheets/extra.css diff --git a/package-lock.json b/package-lock.json index a8a5b2abfa..81864037e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,21 +27,21 @@ ], "devDependencies": { "@biomejs/biome": "^1.9.4", - "@types/aws-lambda": "^8.10.147", - "@types/node": "^22.13.9", - "@vitest/coverage-v8": "^3.0.7", + "@types/aws-lambda": "^8.10.148", + "@types/node": "^22.13.13", + "@vitest/coverage-v8": "^3.0.9", "husky": "^9.1.7", "lerna": "8.1.2", - "lint-staged": "^15.4.3", + "lint-staged": "^15.5.0", "markdownlint-cli2": "^0.17.2", "middy4": "npm:@middy/core@^4.7.0", "middy5": "npm:@middy/core@^5.4.3", "middy6": "npm:@middy/core@^6.0.0", - "typedoc": "^0.27.9", - "typedoc-plugin-missing-exports": "^3.1.0", - "typedoc-plugin-zod": "^1.3.1", - "typescript": "^5.7.3", - "vitest": "^3.0.5" + "typedoc": "^0.28.1", + "typedoc-plugin-missing-exports": "^4.0.0", + "typedoc-plugin-zod": "^1.4.0", + "typescript": "^5.8.2", + "vitest": "^3.0.9" }, "engines": { "node": ">=18" @@ -49,68 +49,67 @@ }, "examples/app": { "name": "powertools-sample-app", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/batch": "^2.16.0", - "@aws-lambda-powertools/idempotency": "^2.16.0", - "@aws-lambda-powertools/logger": "^2.16.0", - "@aws-lambda-powertools/metrics": "^2.16.0", - "@aws-lambda-powertools/parameters": "^2.16.0", - "@aws-lambda-powertools/tracer": "^2.16.0", - "@aws-sdk/client-ssm": "^3.759.0", - "@aws-sdk/lib-dynamodb": "^3.758.0", + "@aws-lambda-powertools/batch": "^2.17.0", + "@aws-lambda-powertools/idempotency": "^2.17.0", + "@aws-lambda-powertools/logger": "^2.17.0", + "@aws-lambda-powertools/metrics": "^2.17.0", + "@aws-lambda-powertools/parameters": "^2.17.0", + "@aws-lambda-powertools/tracer": "^2.17.0", + "@aws-sdk/client-ssm": "^3.772.0", + "@aws-sdk/lib-dynamodb": "^3.772.0", "@middy/core": "^4.7.0", - "@types/aws-lambda": "^8.10.147", - "@types/node": "22.13.9", - "aws-cdk": "^2.1002.0", + "@types/aws-lambda": "^8.10.148", + "@types/node": "22.13.13", + "aws-cdk": "^2.1005.0", "constructs": "^10.4.2", - "esbuild": "^0.25.0", - "typescript": "^5.7.3" + "esbuild": "^0.25.1", + "typescript": "^5.8.2" }, "devDependencies": { - "@types/aws-lambda": "^8.10.147", - "@types/node": "22.13.9", - "aws-cdk-lib": "^2.181.1", + "@types/aws-lambda": "^8.10.148", + "@types/node": "22.13.13", + "aws-cdk-lib": "^2.185.0", "constructs": "^10.4.2", "source-map-support": "^0.5.21", "tsx": "^4.19.3", - "typescript": "^5.7.3", + "typescript": "^5.8.2", "vitest": "^3.0.5" } }, "examples/snippets": { "name": "code-snippets", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "devDependencies": { - "@aws-lambda-powertools/batch": "^2.16.0", - "@aws-lambda-powertools/idempotency": "^2.16.0", - "@aws-lambda-powertools/jmespath": "^2.16.0", - "@aws-lambda-powertools/logger": "^2.16.0", - "@aws-lambda-powertools/metrics": "^2.16.0", - "@aws-lambda-powertools/parameters": "^2.16.0", - "@aws-lambda-powertools/parser": "^2.16.0", - "@aws-lambda-powertools/tracer": "^2.16.0", - "@aws-sdk/client-appconfigdata": "^3.758.0", - "@aws-sdk/client-dynamodb": "^3.758.0", - "@aws-sdk/client-secrets-manager": "^3.758.0", - "@aws-sdk/client-ssm": "^3.759.0", - "@aws-sdk/util-dynamodb": "^3.758.0", + "@aws-lambda-powertools/batch": "^2.17.0", + "@aws-lambda-powertools/idempotency": "^2.17.0", + "@aws-lambda-powertools/jmespath": "^2.17.0", + "@aws-lambda-powertools/logger": "^2.17.0", + "@aws-lambda-powertools/metrics": "^2.17.0", + "@aws-lambda-powertools/parameters": "^2.17.0", + "@aws-lambda-powertools/parser": "^2.17.0", + "@aws-lambda-powertools/tracer": "^2.17.0", + "@aws-sdk/client-appconfigdata": "^3.772.0", + "@aws-sdk/client-dynamodb": "^3.772.0", + "@aws-sdk/client-secrets-manager": "^3.772.0", + "@aws-sdk/client-ssm": "^3.772.0", + "@aws-sdk/util-dynamodb": "^3.772.0", "@middy/core": "^4.7.0", "aws-sdk": "^2.1692.0", "aws-sdk-client-mock": "^4.1.0", - "hashi-vault-js": "^0.4.16", "zod": "^3.24.2" } }, "layers": { - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "dependencies": { - "aws-cdk": "^2.1002.0", - "aws-cdk-lib": "^2.181.1", - "esbuild": "^0.25.0", + "aws-cdk": "^2.1005.0", + "aws-cdk-lib": "^2.185.0", + "esbuild": "^0.25.1", "tsx": "^4.19.3" }, "bin": { @@ -135,9 +134,10 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.209", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.209.tgz", - "integrity": "sha512-tL7aBDzx/QBuZoQso9OST2BMCoev89v01iQZicOKlR0J6vWQLPiqZfn4vd9nissFbM4X+xIwi3UKasPBTQL0WQ==" + "version": "2.2.227", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.227.tgz", + "integrity": "sha512-BAZwZGtX166K9KRYnRBbRj/fU0FY00LOnki11OsDQMfZ9H6tno+LIhv/ZY5U4LGIaI8yP881pAhbrVxpKQnYLg==", + "license": "Apache-2.0" }, "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { "version": "2.1.0", @@ -164,9 +164,9 @@ } }, "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "39.2.7", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-39.2.7.tgz", - "integrity": "sha512-aZWc9dxnar5S9PheEp6f7F5Q3GGkft+cm8wypbTuIkqUjU6yn21oQzOWTikhBnNKf1vdaoYbjAqhBOlfoqaH+w==", + "version": "40.7.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-40.7.0.tgz", + "integrity": "sha512-00wVKn9pOOGXbeNwA4E8FUFt0zIB4PmSO7PvIiDWgpaFX3G/sWyy0A3s6bg/n2Yvkghu8r4a8ckm+mAzkAYmfA==", "bundleDependencies": [ "jsonschema", "semver" @@ -174,7 +174,10 @@ "license": "Apache-2.0", "dependencies": { "jsonschema": "~1.4.1", - "semver": "^7.6.3" + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 14.15.0" } }, "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { @@ -186,7 +189,7 @@ } }, "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { - "version": "7.6.3", + "version": "7.7.1", "inBundle": true, "license": "ISC", "bin": { @@ -215,25 +218,25 @@ } }, "node_modules/@aws-cdk/cx-api": { - "version": "2.181.1", - "resolved": "https://registry.npmjs.org/@aws-cdk/cx-api/-/cx-api-2.181.1.tgz", - "integrity": "sha512-1uFbmLxP04K5W8pqbIqwrx8V87JtSPeO1xe57NRddUcJCPdGQrnXByD7jq+amiFcJ2NyKH6C5gV3gp3pMdJs6w==", + "version": "2.184.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/cx-api/-/cx-api-2.184.1.tgz", + "integrity": "sha512-D2JbqzU5uEkol0LEglTb3cjdbS3o9jbUrEZfBSk7zNb6Kjzu06TgYajcL/ZwAxYKs7U1HenwRaXBofPYENReGg==", "bundleDependencies": [ "semver" ], "license": "Apache-2.0", "dependencies": { - "semver": "^7.6.3" + "semver": "^7.7.1" }, "engines": { "node": ">= 14.15.0" }, "peerDependencies": { - "@aws-cdk/cloud-assembly-schema": "^39.2.0" + "@aws-cdk/cloud-assembly-schema": "^40.6.0" } }, "node_modules/@aws-cdk/cx-api/node_modules/semver": { - "version": "7.6.3", + "version": "7.7.1", "inBundle": true, "license": "ISC", "bin": { @@ -244,9 +247,9 @@ } }, "node_modules/@aws-cdk/region-info": { - "version": "2.181.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/region-info/-/region-info-2.181.0.tgz", - "integrity": "sha512-3hzW9cy/7PG+DTg7OUB31m5w17cZI/vUmvWz+CfLL9o85eMDJbsnxc+YckcxKRfxINRLPC2ISVyqCdQPWyPHhA==", + "version": "2.183.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/region-info/-/region-info-2.183.0.tgz", + "integrity": "sha512-D79pFpPO3YBLd8a4N7XvYa5CYwLegycZGNr26uJc7Sr7fH1N/4cISuA/8df9LcqD1M+nTbP3sgDuLWbv6kap6g==", "license": "Apache-2.0", "engines": { "node": ">= 14.15.0" @@ -262,15 +265,15 @@ } }, "node_modules/@aws-cdk/toolkit-lib": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@aws-cdk/toolkit-lib/-/toolkit-lib-0.1.3.tgz", - "integrity": "sha512-EBl+czCQXJW9YWOH4LhnDbPQAhda2X/1Z+uVoW3KNmKLFidBrP2y1uCRXMBxEMCCzjXfun8hnoTtEr0p6MdvVA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@aws-cdk/toolkit-lib/-/toolkit-lib-0.1.6.tgz", + "integrity": "sha512-65ojo2cIpsXnA8C9e3k9vmIQ5UziZHkzPQGhCWvjf9TUNV/U8Phge5YeTKzMTOm/t8hvPfs1n5qCIBINwVPMAg==", "license": "Apache-2.0", "dependencies": { - "@aws-cdk/cloud-assembly-schema": "^40.7.0", + "@aws-cdk/cloud-assembly-schema": "^41.2.0", "@aws-cdk/cloudformation-diff": "^2.179.0", - "@aws-cdk/cx-api": "^2.180.0", - "@aws-cdk/region-info": "^2.180.0", + "@aws-cdk/cx-api": "^2.181.1", + "@aws-cdk/region-info": "^2.181.1", "@aws-sdk/client-appsync": "^3", "@aws-sdk/client-cloudcontrol": "^3", "@aws-sdk/client-cloudformation": "^3", @@ -292,7 +295,6 @@ "@aws-sdk/credential-providers": "^3", "@aws-sdk/ec2-metadata-service": "^3", "@aws-sdk/lib-storage": "^3", - "@jsii/check-node": "^1.108.0", "@smithy/middleware-endpoint": "^4.0.6", "@smithy/node-http-handler": "^4.0.3", "@smithy/property-provider": "^4.0.1", @@ -302,8 +304,8 @@ "@smithy/util-waiter": "^4.0.2", "archiver": "^7.0.1", "camelcase": "^6", - "cdk-assets": "^3.0.0", - "cdk-from-cfn": "^0.191.0", + "cdk-assets": "^3.1.1", + "cdk-from-cfn": "^0.193.0", "chalk": "^4", "chokidar": "^3", "decamelize": "^5", @@ -328,9 +330,9 @@ } }, "node_modules/@aws-cdk/toolkit-lib/node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "40.7.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-40.7.0.tgz", - "integrity": "sha512-00wVKn9pOOGXbeNwA4E8FUFt0zIB4PmSO7PvIiDWgpaFX3G/sWyy0A3s6bg/n2Yvkghu8r4a8ckm+mAzkAYmfA==", + "version": "41.2.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-41.2.0.tgz", + "integrity": "sha512-JaulVS6z9y5+u4jNmoWbHZRs9uGOnmn/ktXygNWKNu1k6lF3ad4so3s18eRu15XCbUIomxN9WPYT6Ehh7hzONw==", "bundleDependencies": [ "jsonschema", "semver" @@ -877,19 +879,19 @@ "link": true }, "node_modules/@aws-sdk/client-appconfigdata": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-appconfigdata/-/client-appconfigdata-3.758.0.tgz", - "integrity": "sha512-/c3i4p29ry3RCQXEhGOrYLW0ropDqgrMZ0/OS4bhbl6VaPQwSkhzgO1jiA7RNKmxjblcUwa0HDPwgVOGWxkH+Q==", + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-appconfigdata/-/client-appconfigdata-3.772.0.tgz", + "integrity": "sha512-Iv/AganfpN/LfnkxLf9bZC8YL5PCgbxFRR9qdKcur/i6JkMB/sI1xj+4tf5bctj3rRdVEXIyYdparV+cvpDv2g==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", - "@aws-sdk/credential-provider-node": "3.758.0", + "@aws-sdk/credential-provider-node": "3.772.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", @@ -928,6 +930,227 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/client-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.772.0.tgz", + "integrity": "sha512-sDdxepi74+cL6gXJJ2yw3UNSI7GBvoGTwZqFyPoNAzcURvaYwo8dBr7G4jS9GDanjTlO3CGVAf2VMcpqEvmoEw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.772.0.tgz", + "integrity": "sha512-T1Ec9Q25zl5c/eZUPHZsiq8vgBeWBjHM7WM5xtZszZRPqqhQGnmFlomz1r9rwhW8RFB5k8HRaD/SLKo6jtYl/A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.772.0.tgz", + "integrity": "sha512-0IdVfjBO88Mtekq/KaScYSIEPIeR+ABRvBOWyj/c/qQ2KJyI0GRlSAzpANfxDLHVPn3yEHuZd9nRL6sOmOMI0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-ini": "3.772.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.772.0.tgz", + "integrity": "sha512-yR3Y5RAVPa4ogojcBOpZUx6XyRVAkynIJCjd0avdlxW1hhnzSr5/pzoiJ6u21UCbkxlJJTDZE3jfFe7tt+HA4w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.772.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/token-providers": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.772.0.tgz", + "integrity": "sha512-yHAT5Y2y0fnecSuWRUn8NMunKfDqFYhnOpGq8UyCEcwz9aXzibU0hqRIEm51qpR81hqo0GMFDH0EOmegZ/iW5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.772.0.tgz", + "integrity": "sha512-zg0LjJa4v7fcLzn5QzZvtVS+qyvmsnu7oQnb86l6ckduZpWDCDC9+A0ZzcXTrxblPCJd3JqkoG1+Gzi4S4Ny/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/nested-clients": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.772.0.tgz", + "integrity": "sha512-gNJbBxR5YlEumsCS9EWWEASXEnysL0aDnr9MNPX1ip/g1xOqRHmytgV/+t8RFZFTKg0OprbWTq5Ich3MqsEuCQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/token-providers": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.772.0.tgz", + "integrity": "sha512-d1Waa1vyebuokcAWYlkZdtFlciIgob7B39vPRmtxMObbGumJKiOy/qCe2/FB/72h1Ej9Ih32lwvbxUjORQWN4g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-appsync": { "version": "3.741.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-appsync/-/client-appsync-3.741.0.tgz", @@ -2079,19 +2302,19 @@ } }, "node_modules/@aws-sdk/client-cloudwatch": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.758.0.tgz", - "integrity": "sha512-A4l/gMNqsyCpzgobPcBAEc6WAGWlv5B8WaBJ6YpCKbDwUFLBSbGYFHDIlDrUHfQWq8A3O1rz/oUjjnLeHf07EA==", + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.772.0.tgz", + "integrity": "sha512-eVEsD7f5RQLs9DTAniGBDcooXMIdmDqs4hPNU5ReM7xppNi/P1vExTpfIM2wkDh9OI1bGG3WSt37UdtstPs14g==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", - "@aws-sdk/credential-provider-node": "3.758.0", + "@aws-sdk/credential-provider-node": "3.772.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", @@ -2517,46 +2740,46 @@ } } }, - "node_modules/@aws-sdk/client-codebuild": { - "version": "3.741.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-codebuild/-/client-codebuild-3.741.0.tgz", - "integrity": "sha512-oHKptRuG6MNKj75EEsOzndRqLrZBoi+LYSkNXKdSlDN8Pes+bua1Zu5Ex3iM4rAYqN390pD7P4avOz/SDcMLHg==", + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/client-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.772.0.tgz", + "integrity": "sha512-sDdxepi74+cL6gXJJ2yw3UNSI7GBvoGTwZqFyPoNAzcURvaYwo8dBr7G4jS9GDanjTlO3CGVAf2VMcpqEvmoEw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/credential-provider-node": "3.741.0", + "@aws-sdk/core": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", + "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", + "@smithy/core": "^3.1.5", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/middleware-retry": "^4.0.3", - "@smithy/middleware-serde": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.2", + "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.3", - "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", @@ -2567,27 +2790,248 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codebuild/node_modules/@aws-sdk/client-sso": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.734.0.tgz", - "integrity": "sha512-oerepp0mut9VlgTwnG5Ds/lb0C0b2/rQ+hL/rF6q+HGKPfGsCuPvFx1GtwGKCXd49ase88/jVgrhcA9OQbz3kg==", + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.772.0.tgz", + "integrity": "sha512-T1Ec9Q25zl5c/eZUPHZsiq8vgBeWBjHM7WM5xtZszZRPqqhQGnmFlomz1r9rwhW8RFB5k8HRaD/SLKo6jtYl/A==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", - "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/nested-clients": "3.772.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", - "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.772.0.tgz", + "integrity": "sha512-0IdVfjBO88Mtekq/KaScYSIEPIeR+ABRvBOWyj/c/qQ2KJyI0GRlSAzpANfxDLHVPn3yEHuZd9nRL6sOmOMI0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-ini": "3.772.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.772.0.tgz", + "integrity": "sha512-yR3Y5RAVPa4ogojcBOpZUx6XyRVAkynIJCjd0avdlxW1hhnzSr5/pzoiJ6u21UCbkxlJJTDZE3jfFe7tt+HA4w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.772.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/token-providers": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.772.0.tgz", + "integrity": "sha512-yHAT5Y2y0fnecSuWRUn8NMunKfDqFYhnOpGq8UyCEcwz9aXzibU0hqRIEm51qpR81hqo0GMFDH0EOmegZ/iW5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.772.0.tgz", + "integrity": "sha512-zg0LjJa4v7fcLzn5QzZvtVS+qyvmsnu7oQnb86l6ckduZpWDCDC9+A0ZzcXTrxblPCJd3JqkoG1+Gzi4S4Ny/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/nested-clients": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.772.0.tgz", + "integrity": "sha512-gNJbBxR5YlEumsCS9EWWEASXEnysL0aDnr9MNPX1ip/g1xOqRHmytgV/+t8RFZFTKg0OprbWTq5Ich3MqsEuCQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/token-providers": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.772.0.tgz", + "integrity": "sha512-d1Waa1vyebuokcAWYlkZdtFlciIgob7B39vPRmtxMObbGumJKiOy/qCe2/FB/72h1Ej9Ih32lwvbxUjORQWN4g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codebuild": { + "version": "3.741.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-codebuild/-/client-codebuild-3.741.0.tgz", + "integrity": "sha512-oHKptRuG6MNKj75EEsOzndRqLrZBoi+LYSkNXKdSlDN8Pes+bua1Zu5Ex3iM4rAYqN390pD7P4avOz/SDcMLHg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.734.0", + "@aws-sdk/credential-provider-node": "3.741.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.734.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.1", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.2", + "@smithy/middleware-retry": "^4.0.3", + "@smithy/middleware-serde": "^4.0.1", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.2", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.3", + "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-codebuild/node_modules/@aws-sdk/client-sso": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.734.0.tgz", + "integrity": "sha512-oerepp0mut9VlgTwnG5Ds/lb0C0b2/rQ+hL/rF6q+HGKPfGsCuPvFx1GtwGKCXd49ase88/jVgrhcA9OQbz3kg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.734.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.734.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.1", + "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", @@ -2949,19 +3393,19 @@ } }, "node_modules/@aws-sdk/client-dynamodb": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.758.0.tgz", - "integrity": "sha512-ZdVVCvmQ4wlV22HgYZKndIYNKkFfTLi8PIOF5rOkqthgYRTfVzKajrVbYebCs5jMDTk73LPLl2Ze/EYBEHKlBA==", + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.772.0.tgz", + "integrity": "sha512-MxUqb6vmWkZSR5UMuL7t5Bni22gwSZAweWdOEA9eXC/W4D7NIa8rMbsNl1lPvgF8OzIBvZBjkMzIHPuW/w4MrQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", - "@aws-sdk/credential-provider-node": "3.758.0", + "@aws-sdk/credential-provider-node": "3.772.0", "@aws-sdk/middleware-endpoint-discovery": "3.734.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", @@ -3002,81 +3446,294 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2": { - "version": "3.741.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ec2/-/client-ec2-3.741.0.tgz", - "integrity": "sha512-PQiqFeVVykHmB3Vk/RlK6JPuzoOLZ+clxcu2d7RcfuwbGaiXhjaHSIVefjF+NOFekO5PHornG6r05H4RRf3M7w==", + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/client-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.772.0.tgz", + "integrity": "sha512-sDdxepi74+cL6gXJJ2yw3UNSI7GBvoGTwZqFyPoNAzcURvaYwo8dBr7G4jS9GDanjTlO3CGVAf2VMcpqEvmoEw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/credential-provider-node": "3.741.0", + "@aws-sdk/core": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-sdk-ec2": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", + "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", + "@smithy/core": "^3.1.5", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/middleware-retry": "^4.0.3", - "@smithy/middleware-serde": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.2", + "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.3", - "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-utf8": "^4.0.0", - "@smithy/util-waiter": "^4.0.2", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/client-sso": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.734.0.tgz", - "integrity": "sha512-oerepp0mut9VlgTwnG5Ds/lb0C0b2/rQ+hL/rF6q+HGKPfGsCuPvFx1GtwGKCXd49ase88/jVgrhcA9OQbz3kg==", + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.772.0.tgz", + "integrity": "sha512-T1Ec9Q25zl5c/eZUPHZsiq8vgBeWBjHM7WM5xtZszZRPqqhQGnmFlomz1r9rwhW8RFB5k8HRaD/SLKo6jtYl/A==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", - "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/nested-clients": "3.772.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", - "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.772.0.tgz", + "integrity": "sha512-0IdVfjBO88Mtekq/KaScYSIEPIeR+ABRvBOWyj/c/qQ2KJyI0GRlSAzpANfxDLHVPn3yEHuZd9nRL6sOmOMI0A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-ini": "3.772.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.772.0.tgz", + "integrity": "sha512-yR3Y5RAVPa4ogojcBOpZUx6XyRVAkynIJCjd0avdlxW1hhnzSr5/pzoiJ6u21UCbkxlJJTDZE3jfFe7tt+HA4w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.772.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/token-providers": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.772.0.tgz", + "integrity": "sha512-yHAT5Y2y0fnecSuWRUn8NMunKfDqFYhnOpGq8UyCEcwz9aXzibU0hqRIEm51qpR81hqo0GMFDH0EOmegZ/iW5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.772.0.tgz", + "integrity": "sha512-zg0LjJa4v7fcLzn5QzZvtVS+qyvmsnu7oQnb86l6ckduZpWDCDC9+A0ZzcXTrxblPCJd3JqkoG1+Gzi4S4Ny/Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/nested-clients": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.772.0.tgz", + "integrity": "sha512-gNJbBxR5YlEumsCS9EWWEASXEnysL0aDnr9MNPX1ip/g1xOqRHmytgV/+t8RFZFTKg0OprbWTq5Ich3MqsEuCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/token-providers": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.772.0.tgz", + "integrity": "sha512-d1Waa1vyebuokcAWYlkZdtFlciIgob7B39vPRmtxMObbGumJKiOy/qCe2/FB/72h1Ej9Ih32lwvbxUjORQWN4g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ec2": { + "version": "3.741.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ec2/-/client-ec2-3.741.0.tgz", + "integrity": "sha512-PQiqFeVVykHmB3Vk/RlK6JPuzoOLZ+clxcu2d7RcfuwbGaiXhjaHSIVefjF+NOFekO5PHornG6r05H4RRf3M7w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.734.0", + "@aws-sdk/credential-provider-node": "3.741.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-sdk-ec2": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.734.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.1", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.2", + "@smithy/middleware-retry": "^4.0.3", + "@smithy/middleware-serde": "^4.0.1", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.2", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.3", + "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.2", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/client-sso": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.734.0.tgz", + "integrity": "sha512-oerepp0mut9VlgTwnG5Ds/lb0C0b2/rQ+hL/rF6q+HGKPfGsCuPvFx1GtwGKCXd49ase88/jVgrhcA9OQbz3kg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.734.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.734.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.1", + "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", @@ -3388,9 +4045,9 @@ } }, "node_modules/@aws-sdk/client-ecr": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ecr/-/client-ecr-3.758.0.tgz", - "integrity": "sha512-9nTE01CuK5Vq0kPkmw3xdE6lrPDZSXOlX4wRc194GhnhxdppRla5dG6+gvZWTmpkC9a2mGqjXYsbvoSg6kZPHg==", + "version": "3.766.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ecr/-/client-ecr-3.766.0.tgz", + "integrity": "sha512-hW4CtlS22Wu6hs2BlkqtZdpzMtwCyb5k4VetSrchLLmiq+jPopPh0NGqp42OgqUYDJGqPJZF2LJaL/k/UtacPg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -4968,18 +5625,18 @@ } }, "node_modules/@aws-sdk/client-lambda": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.758.0.tgz", - "integrity": "sha512-k7L9fe0NN1v2Vhg4ofA1pb26gTdGVFdkA6XUQyElLEdcKzJzoYiQ60faNLuMPfH0zsKNvy/xKfNOD6DFZWjgEg==", + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.772.0.tgz", + "integrity": "sha512-oe+EfCzLwSHY5d8J0eh0/DNhupMUyeLCLAQ+4RwlEbMkWESa2oyV9hNZ2n7+v0xE+qG9svQXLp3jW/hVM63ncA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", - "@aws-sdk/credential-provider-node": "3.758.0", + "@aws-sdk/credential-provider-node": "3.772.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", @@ -5022,84 +5679,297 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-route-53": { - "version": "3.741.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-route-53/-/client-route-53-3.741.0.tgz", - "integrity": "sha512-TV2qOzJeKKiRZtFP88DHZvUpdXIKfdDsyhbJkkVLVJ4Rd2QNpekPZz3ce6eaokqzilHJu5LFn4D1vdgv4YZLKg==", + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/client-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.772.0.tgz", + "integrity": "sha512-sDdxepi74+cL6gXJJ2yw3UNSI7GBvoGTwZqFyPoNAzcURvaYwo8dBr7G4jS9GDanjTlO3CGVAf2VMcpqEvmoEw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/credential-provider-node": "3.741.0", + "@aws-sdk/core": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-sdk-route53": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", + "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", - "@aws-sdk/xml-builder": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", + "@smithy/core": "^3.1.5", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/middleware-retry": "^4.0.3", - "@smithy/middleware-serde": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.2", + "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.3", - "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-utf8": "^4.0.0", - "@smithy/util-waiter": "^4.0.2", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/client-sso": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.734.0.tgz", - "integrity": "sha512-oerepp0mut9VlgTwnG5Ds/lb0C0b2/rQ+hL/rF6q+HGKPfGsCuPvFx1GtwGKCXd49ase88/jVgrhcA9OQbz3kg==", + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.772.0.tgz", + "integrity": "sha512-T1Ec9Q25zl5c/eZUPHZsiq8vgBeWBjHM7WM5xtZszZRPqqhQGnmFlomz1r9rwhW8RFB5k8HRaD/SLKo6jtYl/A==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", - "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/nested-clients": "3.772.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.772.0.tgz", + "integrity": "sha512-0IdVfjBO88Mtekq/KaScYSIEPIeR+ABRvBOWyj/c/qQ2KJyI0GRlSAzpANfxDLHVPn3yEHuZd9nRL6sOmOMI0A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-ini": "3.772.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.772.0.tgz", + "integrity": "sha512-yR3Y5RAVPa4ogojcBOpZUx6XyRVAkynIJCjd0avdlxW1hhnzSr5/pzoiJ6u21UCbkxlJJTDZE3jfFe7tt+HA4w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.772.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/token-providers": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.772.0.tgz", + "integrity": "sha512-yHAT5Y2y0fnecSuWRUn8NMunKfDqFYhnOpGq8UyCEcwz9aXzibU0hqRIEm51qpR81hqo0GMFDH0EOmegZ/iW5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.772.0.tgz", + "integrity": "sha512-zg0LjJa4v7fcLzn5QzZvtVS+qyvmsnu7oQnb86l6ckduZpWDCDC9+A0ZzcXTrxblPCJd3JqkoG1+Gzi4S4Ny/Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/nested-clients": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.772.0.tgz", + "integrity": "sha512-gNJbBxR5YlEumsCS9EWWEASXEnysL0aDnr9MNPX1ip/g1xOqRHmytgV/+t8RFZFTKg0OprbWTq5Ich3MqsEuCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/token-providers": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.772.0.tgz", + "integrity": "sha512-d1Waa1vyebuokcAWYlkZdtFlciIgob7B39vPRmtxMObbGumJKiOy/qCe2/FB/72h1Ej9Ih32lwvbxUjORQWN4g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53": { + "version": "3.741.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-route-53/-/client-route-53-3.741.0.tgz", + "integrity": "sha512-TV2qOzJeKKiRZtFP88DHZvUpdXIKfdDsyhbJkkVLVJ4Rd2QNpekPZz3ce6eaokqzilHJu5LFn4D1vdgv4YZLKg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.734.0", + "@aws-sdk/credential-provider-node": "3.741.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-sdk-route53": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.734.0", + "@aws-sdk/xml-builder": "3.734.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.1", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.2", + "@smithy/middleware-retry": "^4.0.3", + "@smithy/middleware-serde": "^4.0.1", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.2", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.3", + "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53/node_modules/@aws-sdk/client-sso": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.734.0.tgz", + "integrity": "sha512-oerepp0mut9VlgTwnG5Ds/lb0C0b2/rQ+hL/rF6q+HGKPfGsCuPvFx1GtwGKCXd49ase88/jVgrhcA9OQbz3kg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.734.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.734.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.1", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.2", "@smithy/middleware-retry": "^4.0.3", "@smithy/middleware-serde": "^4.0.1", "@smithy/middleware-stack": "^4.0.1", @@ -5474,18 +6344,18 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.758.0.tgz", - "integrity": "sha512-Vi4cdCim0jQx3rrU5R1W4v3czoWL0ajBtoI15oSSt7cwLjzNA0xq4nXSa6rahjTgtZWlLeBprbquvxNzY3qg5Q==", + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.772.0.tgz", + "integrity": "sha512-hY/MWUbjeHW9fQpRQbm6AXHDwL7++/cZYu+b7vxT4gH/6g4ivLiO/kgMI0F6Xeu3v6/fMumTUuNuLDfWfE7vpQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", - "@aws-sdk/credential-provider-node": "3.758.0", + "@aws-sdk/credential-provider-node": "3.772.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", @@ -5525,80 +6395,293 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sfn": { - "version": "3.741.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sfn/-/client-sfn-3.741.0.tgz", - "integrity": "sha512-VW5q2tgTc49Xa8OfNR3jCiQvsMd2y/GYmpMWES/HlWMnWdwb/T/7QfJbJ51SDIoZ9C0x4qKZpbjPwgqbdi82nA==", + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.772.0.tgz", + "integrity": "sha512-sDdxepi74+cL6gXJJ2yw3UNSI7GBvoGTwZqFyPoNAzcURvaYwo8dBr7G4jS9GDanjTlO3CGVAf2VMcpqEvmoEw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/credential-provider-node": "3.741.0", + "@aws-sdk/core": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", + "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", + "@smithy/core": "^3.1.5", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/middleware-retry": "^4.0.3", - "@smithy/middleware-serde": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.2", + "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.3", - "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-utf8": "^4.0.0", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/client-sso": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.734.0.tgz", - "integrity": "sha512-oerepp0mut9VlgTwnG5Ds/lb0C0b2/rQ+hL/rF6q+HGKPfGsCuPvFx1GtwGKCXd49ase88/jVgrhcA9OQbz3kg==", + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.772.0.tgz", + "integrity": "sha512-T1Ec9Q25zl5c/eZUPHZsiq8vgBeWBjHM7WM5xtZszZRPqqhQGnmFlomz1r9rwhW8RFB5k8HRaD/SLKo6jtYl/A==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", - "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/nested-clients": "3.772.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-node": "^4.0.1", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.772.0.tgz", + "integrity": "sha512-0IdVfjBO88Mtekq/KaScYSIEPIeR+ABRvBOWyj/c/qQ2KJyI0GRlSAzpANfxDLHVPn3yEHuZd9nRL6sOmOMI0A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-ini": "3.772.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.772.0.tgz", + "integrity": "sha512-yR3Y5RAVPa4ogojcBOpZUx6XyRVAkynIJCjd0avdlxW1hhnzSr5/pzoiJ6u21UCbkxlJJTDZE3jfFe7tt+HA4w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.772.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/token-providers": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.772.0.tgz", + "integrity": "sha512-yHAT5Y2y0fnecSuWRUn8NMunKfDqFYhnOpGq8UyCEcwz9aXzibU0hqRIEm51qpR81hqo0GMFDH0EOmegZ/iW5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.772.0.tgz", + "integrity": "sha512-zg0LjJa4v7fcLzn5QzZvtVS+qyvmsnu7oQnb86l6ckduZpWDCDC9+A0ZzcXTrxblPCJd3JqkoG1+Gzi4S4Ny/Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/nested-clients": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.772.0.tgz", + "integrity": "sha512-gNJbBxR5YlEumsCS9EWWEASXEnysL0aDnr9MNPX1ip/g1xOqRHmytgV/+t8RFZFTKg0OprbWTq5Ich3MqsEuCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/token-providers": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.772.0.tgz", + "integrity": "sha512-d1Waa1vyebuokcAWYlkZdtFlciIgob7B39vPRmtxMObbGumJKiOy/qCe2/FB/72h1Ej9Ih32lwvbxUjORQWN4g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn": { + "version": "3.741.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sfn/-/client-sfn-3.741.0.tgz", + "integrity": "sha512-VW5q2tgTc49Xa8OfNR3jCiQvsMd2y/GYmpMWES/HlWMnWdwb/T/7QfJbJ51SDIoZ9C0x4qKZpbjPwgqbdi82nA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.734.0", + "@aws-sdk/credential-provider-node": "3.741.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.734.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.1", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.2", + "@smithy/middleware-retry": "^4.0.3", + "@smithy/middleware-serde": "^4.0.1", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.2", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.3", + "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/client-sso": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.734.0.tgz", + "integrity": "sha512-oerepp0mut9VlgTwnG5Ds/lb0C0b2/rQ+hL/rF6q+HGKPfGsCuPvFx1GtwGKCXd49ase88/jVgrhcA9OQbz3kg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.734.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.734.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.1", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", "@smithy/middleware-endpoint": "^4.0.2", @@ -5832,16 +6915,321 @@ "@smithy/middleware-serde": "^4.0.1", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.2", + "@smithy/node-http-handler": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.2", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.3", + "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/token-providers": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.734.0.tgz", + "integrity": "sha512-2U6yWKrjWjZO8Y5SHQxkFvMVWHQWbS0ufqfAIBROqmIZNubOL7jXCiVdEFekz6MZ9LF2tvYGnOW4jX8OKDGfIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/util-endpoints": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.734.0.tgz", + "integrity": "sha512-w2+/E88NUbqql6uCVAsmMxDQKu7vsKV0KqhlQb0lL+RCq4zy07yXYptVNs13qrnuTfyX7uPXkXrlugvK9R1Ucg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "@smithy/util-endpoints": "^3.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.734.0.tgz", + "integrity": "sha512-c6Iinh+RVQKs6jYUFQ64htOU2HUXFQ3TVx+8Tu3EDF19+9vzWi9UukhIMH9rqyyEXIAkk9XL7avt8y2Uyw2dGA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-ssm": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.772.0.tgz", + "integrity": "sha512-NPXybSztwGakQNonIHbMlwzTUaiHTSR+RsJnNrZkXpfp57Zny2zxcea8P/9OGQsCg3neUzRtoZxgPWb4jA9hLA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-node": "3.772.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.2", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/client-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.772.0.tgz", + "integrity": "sha512-sDdxepi74+cL6gXJJ2yw3UNSI7GBvoGTwZqFyPoNAzcURvaYwo8dBr7G4jS9GDanjTlO3CGVAf2VMcpqEvmoEw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.772.0.tgz", + "integrity": "sha512-T1Ec9Q25zl5c/eZUPHZsiq8vgBeWBjHM7WM5xtZszZRPqqhQGnmFlomz1r9rwhW8RFB5k8HRaD/SLKo6jtYl/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.772.0.tgz", + "integrity": "sha512-0IdVfjBO88Mtekq/KaScYSIEPIeR+ABRvBOWyj/c/qQ2KJyI0GRlSAzpANfxDLHVPn3yEHuZd9nRL6sOmOMI0A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-ini": "3.772.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.772.0.tgz", + "integrity": "sha512-yR3Y5RAVPa4ogojcBOpZUx6XyRVAkynIJCjd0avdlxW1hhnzSr5/pzoiJ6u21UCbkxlJJTDZE3jfFe7tt+HA4w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.772.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/token-providers": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.772.0.tgz", + "integrity": "sha512-yHAT5Y2y0fnecSuWRUn8NMunKfDqFYhnOpGq8UyCEcwz9aXzibU0hqRIEm51qpR81hqo0GMFDH0EOmegZ/iW5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.772.0.tgz", + "integrity": "sha512-zg0LjJa4v7fcLzn5QzZvtVS+qyvmsnu7oQnb86l6ckduZpWDCDC9+A0ZzcXTrxblPCJd3JqkoG1+Gzi4S4Ny/Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/nested-clients": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.772.0.tgz", + "integrity": "sha512-gNJbBxR5YlEumsCS9EWWEASXEnysL0aDnr9MNPX1ip/g1xOqRHmytgV/+t8RFZFTKg0OprbWTq5Ich3MqsEuCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.3", - "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", @@ -5852,13 +7240,13 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/token-providers": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.734.0.tgz", - "integrity": "sha512-2U6yWKrjWjZO8Y5SHQxkFvMVWHQWbS0ufqfAIBROqmIZNubOL7jXCiVdEFekz6MZ9LF2tvYGnOW4jX8OKDGfIw==", + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/token-providers": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.772.0.tgz", + "integrity": "sha512-d1Waa1vyebuokcAWYlkZdtFlciIgob7B39vPRmtxMObbGumJKiOy/qCe2/FB/72h1Ej9Ih32lwvbxUjORQWN4g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/nested-clients": "3.734.0", + "@aws-sdk/nested-clients": "3.772.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -5869,49 +7257,59 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/util-endpoints": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.734.0.tgz", - "integrity": "sha512-w2+/E88NUbqql6uCVAsmMxDQKu7vsKV0KqhlQb0lL+RCq4zy07yXYptVNs13qrnuTfyX7uPXkXrlugvK9R1Ucg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sfn/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.734.0.tgz", - "integrity": "sha512-c6Iinh+RVQKs6jYUFQ64htOU2HUXFQ3TVx+8Tu3EDF19+9vzWi9UukhIMH9rqyyEXIAkk9XL7avt8y2Uyw2dGA==", + "node_modules/@aws-sdk/client-sso": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.758.0.tgz", + "integrity": "sha512-BoGO6IIWrLyLxQG6txJw6RT2urmbtlwfggapNCrNPyYjlXpzTSJhBYjndg7TpDATFd0SXL0zm8y/tXsUXNkdYQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } } }, - "node_modules/@aws-sdk/client-ssm": { - "version": "3.759.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.759.0.tgz", - "integrity": "sha512-+h1D1jBi6p2fG+ePxwIn2N4TOZcx7ExzhZJnpPT2actC9bV6vkRbeulGr/2fqNu11/S59DZJLfOHjVDH9X1nWA==", + "node_modules/@aws-sdk/client-sts": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.758.0.tgz", + "integrity": "sha512-ue9hbzjWNQmmyoSeWDRPwnYddsD3BVao5mSFA1kXFNVqWPEenjpkZ1xAlBVzHMMNoEz7LvGI+onXIHntNyiOLQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -5952,27 +7350,26 @@ "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", "@smithy/util-utf8": "^4.0.0", - "@smithy/util-waiter": "^4.0.2", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.758.0.tgz", - "integrity": "sha512-BoGO6IIWrLyLxQG6txJw6RT2urmbtlwfggapNCrNPyYjlXpzTSJhBYjndg7TpDATFd0SXL0zm8y/tXsUXNkdYQ==", + "node_modules/@aws-sdk/client-xray": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-xray/-/client-xray-3.772.0.tgz", + "integrity": "sha512-75BLs9JROxsuL8qbPqSLszu4iCCZgJuq7s2E/hnO3B/AaJIjxGDpYSJotQcgB9F0zioJ3vmfDejtQyb+/+j3mw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-node": "3.772.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", @@ -6010,19 +7407,19 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sts": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.758.0.tgz", - "integrity": "sha512-ue9hbzjWNQmmyoSeWDRPwnYddsD3BVao5mSFA1kXFNVqWPEenjpkZ1xAlBVzHMMNoEz7LvGI+onXIHntNyiOLQ==", + "node_modules/@aws-sdk/client-xray/node_modules/@aws-sdk/client-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.772.0.tgz", + "integrity": "sha512-sDdxepi74+cL6gXJJ2yw3UNSI7GBvoGTwZqFyPoNAzcURvaYwo8dBr7G4jS9GDanjTlO3CGVAf2VMcpqEvmoEw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", - "@aws-sdk/credential-provider-node": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", @@ -6060,20 +7457,122 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-xray": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-xray/-/client-xray-3.758.0.tgz", - "integrity": "sha512-poHaIK6+G9tDqEpyWsn4IuKovMfP16XQsK0uQEJ2qdK80JbXB5RP+XJ9/6R156hCNoGhZ1DAy7ze0lVaw/9rBQ==", + "node_modules/@aws-sdk/client-xray/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.772.0.tgz", + "integrity": "sha512-T1Ec9Q25zl5c/eZUPHZsiq8vgBeWBjHM7WM5xtZszZRPqqhQGnmFlomz1r9rwhW8RFB5k8HRaD/SLKo6jtYl/A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-xray/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.772.0.tgz", + "integrity": "sha512-0IdVfjBO88Mtekq/KaScYSIEPIeR+ABRvBOWyj/c/qQ2KJyI0GRlSAzpANfxDLHVPn3yEHuZd9nRL6sOmOMI0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-ini": "3.772.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.772.0", + "@aws-sdk/credential-provider-web-identity": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-xray/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.772.0.tgz", + "integrity": "sha512-yR3Y5RAVPa4ogojcBOpZUx6XyRVAkynIJCjd0avdlxW1hhnzSr5/pzoiJ6u21UCbkxlJJTDZE3jfFe7tt+HA4w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.772.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/token-providers": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-xray/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.772.0.tgz", + "integrity": "sha512-yHAT5Y2y0fnecSuWRUn8NMunKfDqFYhnOpGq8UyCEcwz9aXzibU0hqRIEm51qpR81hqo0GMFDH0EOmegZ/iW5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-xray/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.772.0.tgz", + "integrity": "sha512-zg0LjJa4v7fcLzn5QzZvtVS+qyvmsnu7oQnb86l6ckduZpWDCDC9+A0ZzcXTrxblPCJd3JqkoG1+Gzi4S4Ny/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-xray/node_modules/@aws-sdk/nested-clients": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.772.0.tgz", + "integrity": "sha512-gNJbBxR5YlEumsCS9EWWEASXEnysL0aDnr9MNPX1ip/g1xOqRHmytgV/+t8RFZFTKg0OprbWTq5Ich3MqsEuCQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.758.0", - "@aws-sdk/credential-provider-node": "3.758.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.772.0", "@aws-sdk/middleware-user-agent": "3.758.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", @@ -6111,6 +7610,24 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/client-xray/node_modules/@aws-sdk/token-providers": { + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.772.0.tgz", + "integrity": "sha512-d1Waa1vyebuokcAWYlkZdtFlciIgob7B39vPRmtxMObbGumJKiOy/qCe2/FB/72h1Ej9Ih32lwvbxUjORQWN4g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "3.772.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/core": { "version": "3.758.0", "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.758.0.tgz", @@ -6346,13 +7863,13 @@ } }, "node_modules/@aws-sdk/lib-dynamodb": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.758.0.tgz", - "integrity": "sha512-lkxh7nkFMHY2zbPxhGQz7hVA43yRPu+ERrSiRu7I11arAOz/MJlt7MjHmt0B8x7x6isF1utNixkU28HKh9hgWQ==", + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.772.0.tgz", + "integrity": "sha512-+ir8eClWxfkwrgYrgWCGh41EZ/07JPXJwvEmmhETzNDqvT/FaWLJC5rSKJW7o8nFxljW73lrwLreIO0oyBOsZw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.758.0", - "@aws-sdk/util-dynamodb": "3.758.0", + "@aws-sdk/util-dynamodb": "3.772.0", "@smithy/core": "^3.1.5", "@smithy/smithy-client": "^4.1.6", "@smithy/types": "^4.1.0", @@ -6362,7 +7879,7 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@aws-sdk/client-dynamodb": "^3.758.0" + "@aws-sdk/client-dynamodb": "^3.772.0" } }, "node_modules/@aws-sdk/lib-storage": { @@ -6753,9 +8270,9 @@ } }, "node_modules/@aws-sdk/util-dynamodb": { - "version": "3.758.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.758.0.tgz", - "integrity": "sha512-JjBbhJLajilyMWJ/z82bYgIMj9XGISZ/QMYSpNBdzGFRmL1AL9s6NwLB6FuquRvpY9Lo3Y5vwEbedqdZPIrRFg==", + "version": "3.772.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.772.0.tgz", + "integrity": "sha512-joFi/d2BJir7jCWKYe26CqBSbC5B0FZ33UmF9K+ft5tGPvpPkdDpfkqAXD/t+NN/119TbxfSpkLehnI8VowXZg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -6764,7 +8281,7 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@aws-sdk/client-dynamodb": "^3.758.0" + "@aws-sdk/client-dynamodb": "^3.772.0" } }, "node_modules/@aws-sdk/util-endpoints": { @@ -7177,9 +8694,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", "cpu": [ "ppc64" ], @@ -7193,9 +8710,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", "cpu": [ "arm" ], @@ -7209,9 +8726,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", "cpu": [ "arm64" ], @@ -7225,9 +8742,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", "cpu": [ "x64" ], @@ -7241,9 +8758,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", "cpu": [ "arm64" ], @@ -7257,9 +8774,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", "cpu": [ "x64" ], @@ -7273,9 +8790,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", "cpu": [ "arm64" ], @@ -7289,9 +8806,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", "cpu": [ "x64" ], @@ -7305,9 +8822,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", "cpu": [ "arm" ], @@ -7321,9 +8838,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", "cpu": [ "arm64" ], @@ -7337,9 +8854,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", "cpu": [ "ia32" ], @@ -7353,9 +8870,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", "cpu": [ "loong64" ], @@ -7369,9 +8886,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", "cpu": [ "mips64el" ], @@ -7385,9 +8902,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", "cpu": [ "ppc64" ], @@ -7401,9 +8918,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", "cpu": [ "riscv64" ], @@ -7417,9 +8934,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", "cpu": [ "s390x" ], @@ -7433,9 +8950,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", "cpu": [ "x64" ], @@ -7449,9 +8966,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", "cpu": [ "arm64" ], @@ -7465,9 +8982,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", "cpu": [ "x64" ], @@ -7481,9 +8998,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", "cpu": [ "arm64" ], @@ -7497,9 +9014,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", "cpu": [ "x64" ], @@ -7513,9 +9030,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", "cpu": [ "x64" ], @@ -7529,9 +9046,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", "cpu": [ "arm64" ], @@ -7545,9 +9062,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", "cpu": [ "ia32" ], @@ -7561,9 +9078,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", "cpu": [ "x64" ], @@ -7585,14 +9102,15 @@ } }, "node_modules/@gerrit0/mini-shiki": { - "version": "1.26.1", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-1.26.1.tgz", - "integrity": "sha512-gHFUvv9f1fU2Piou/5Y7Sx5moYxcERbC7CXc6rkDLQTUBg5Dgg9L4u29/nHqfoQ3Y9R0h0BcOhd14uOEZIBP7Q==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.2.1.tgz", + "integrity": "sha512-HbzRC6MKB6U8kQhczz0APKPIzFHTrcqhaC7es2EXInq1SpjPVnpVSIsBe6hNoLWqqCx1n5VKiPXq6PfXnHZKOQ==", "dev": true, + "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^1.26.1", - "@shikijs/types": "^1.26.1", - "@shikijs/vscode-textmate": "^10.0.1" + "@shikijs/engine-oniguruma": "^3.2.1", + "@shikijs/types": "^3.2.1", + "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@hutson/parse-repository-url": { @@ -7763,19 +9281,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsii/check-node": { - "version": "1.108.0", - "resolved": "https://registry.npmjs.org/@jsii/check-node/-/check-node-1.108.0.tgz", - "integrity": "sha512-wa8AGH31Cl0x1jU/KtM6JB32IurBmK1YiX5ZnCndifRCehLnS8DmJCPYrzJbKD4xqmHigaq6696fAnM/L7qIsw==", - "license": "Apache-2.0", - "dependencies": { - "chalk": "^4.1.2", - "semver": "^7.6.3" - }, - "engines": { - "node": ">= 14.17.0" - } - }, "node_modules/@lerna/create": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/@lerna/create/-/create-8.1.2.tgz", @@ -8544,9 +10049,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", - "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.36.0.tgz", + "integrity": "sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w==", "cpu": [ "arm" ], @@ -8558,9 +10063,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", - "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.36.0.tgz", + "integrity": "sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg==", "cpu": [ "arm64" ], @@ -8572,9 +10077,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", - "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.36.0.tgz", + "integrity": "sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw==", "cpu": [ "arm64" ], @@ -8586,9 +10091,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", - "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.36.0.tgz", + "integrity": "sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA==", "cpu": [ "x64" ], @@ -8600,9 +10105,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", - "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.36.0.tgz", + "integrity": "sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg==", "cpu": [ "arm64" ], @@ -8614,9 +10119,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", - "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.36.0.tgz", + "integrity": "sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ==", "cpu": [ "x64" ], @@ -8628,9 +10133,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", - "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.36.0.tgz", + "integrity": "sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg==", "cpu": [ "arm" ], @@ -8642,9 +10147,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", - "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.36.0.tgz", + "integrity": "sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg==", "cpu": [ "arm" ], @@ -8656,9 +10161,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", - "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.36.0.tgz", + "integrity": "sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A==", "cpu": [ "arm64" ], @@ -8670,9 +10175,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", - "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.36.0.tgz", + "integrity": "sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw==", "cpu": [ "arm64" ], @@ -8684,9 +10189,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", - "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.36.0.tgz", + "integrity": "sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg==", "cpu": [ "loong64" ], @@ -8698,9 +10203,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", - "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.36.0.tgz", + "integrity": "sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg==", "cpu": [ "ppc64" ], @@ -8712,9 +10217,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", - "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.36.0.tgz", + "integrity": "sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA==", "cpu": [ "riscv64" ], @@ -8726,9 +10231,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", - "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.36.0.tgz", + "integrity": "sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag==", "cpu": [ "s390x" ], @@ -8740,9 +10245,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", - "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.36.0.tgz", + "integrity": "sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ==", "cpu": [ "x64" ], @@ -8754,9 +10259,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", - "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.36.0.tgz", + "integrity": "sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ==", "cpu": [ "x64" ], @@ -8768,9 +10273,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", - "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.36.0.tgz", + "integrity": "sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A==", "cpu": [ "arm64" ], @@ -8782,9 +10287,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", - "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.36.0.tgz", + "integrity": "sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ==", "cpu": [ "ia32" ], @@ -8796,9 +10301,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", - "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.36.0.tgz", + "integrity": "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw==", "cpu": [ "x64" ], @@ -8810,30 +10315,33 @@ ] }, "node_modules/@shikijs/engine-oniguruma": { - "version": "1.26.1", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.26.1.tgz", - "integrity": "sha512-F5XuxN1HljLuvfXv7d+mlTkV7XukC1cawdtOo+7pKgPD83CAB1Sf8uHqP3PK0u7njFH0ZhoXE1r+0JzEgAQ+kg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.2.1.tgz", + "integrity": "sha512-wZZAkayEn6qu2+YjenEoFqj0OyQI64EWsNR6/71d1EkG4sxEOFooowKivsWPpaWNBu3sxAG+zPz5kzBL/SsreQ==", "dev": true, + "license": "MIT", "dependencies": { - "@shikijs/types": "1.26.1", - "@shikijs/vscode-textmate": "^10.0.1" + "@shikijs/types": "3.2.1", + "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@shikijs/types": { - "version": "1.26.1", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.26.1.tgz", - "integrity": "sha512-d4B00TKKAMaHuFYgRf3L0gwtvqpW4hVdVwKcZYbBfAAQXspgkbWqnFfuFl3MDH6gLbsubOcr+prcnsqah3ny7Q==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.2.1.tgz", + "integrity": "sha512-/NTWAk4KE2M8uac0RhOsIhYQf4pdU0OywQuYDGIGAJ6Mjunxl2cGiuLkvu4HLCMn+OTTLRWkjZITp+aYJv60yA==", "dev": true, + "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^10.0.1", + "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz", - "integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==", - "dev": true + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" }, "node_modules/@sigstore/bundle": { "version": "1.1.0", @@ -9953,10 +11461,11 @@ } }, "node_modules/@types/aws-lambda": { - "version": "8.10.147", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.147.tgz", - "integrity": "sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==", - "dev": true + "version": "8.10.148", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.148.tgz", + "integrity": "sha512-JL+2cfkY9ODQeE06hOxSFNkafjNk4JRBgY837kpoq1GHDttq2U3BA9IzKOWxS4DLjKoymGB4i9uBrlCkjUl1yg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/cls-hooked": { "version": "4.3.8", @@ -9988,6 +11497,7 @@ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/unist": "*" } @@ -10034,9 +11544,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", - "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "version": "22.13.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", + "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -10091,9 +11601,9 @@ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==" }, "node_modules/@vitest/coverage-v8": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.7.tgz", - "integrity": "sha512-Av8WgBJLTrfLOer0uy3CxjlVuWK4CzcLBndW1Nm2vI+3hZ2ozHututkfc7Blu1u6waeQ7J8gzPK/AsBRnWA5mQ==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.9.tgz", + "integrity": "sha512-15OACZcBtQ34keIEn19JYTVuMFTlFrClclwWjHo/IRPg/8ELpkgNTl0o7WLP9WO9XGH6+tip9CPYtEOrIDJvBA==", "dev": true, "license": "MIT", "dependencies": { @@ -10114,8 +11624,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.0.7", - "vitest": "3.0.7" + "@vitest/browser": "3.0.9", + "vitest": "3.0.9" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -10167,14 +11677,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz", - "integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", + "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.7", - "@vitest/utils": "3.0.7", + "@vitest/spy": "3.0.9", + "@vitest/utils": "3.0.9", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -10183,13 +11693,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz", - "integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", + "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.7", + "@vitest/spy": "3.0.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -10210,9 +11720,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz", - "integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", + "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", "dev": true, "license": "MIT", "dependencies": { @@ -10223,13 +11733,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz", - "integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", + "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.7", + "@vitest/utils": "3.0.9", "pathe": "^2.0.3" }, "funding": { @@ -10237,13 +11747,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz", - "integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", + "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.7", + "@vitest/pretty-format": "3.0.9", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -10252,9 +11762,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz", - "integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", + "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10265,13 +11775,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz", - "integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", + "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.7", + "@vitest/pretty-format": "3.0.9", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -10772,9 +12282,9 @@ } }, "node_modules/aws-cdk": { - "version": "2.1002.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1002.0.tgz", - "integrity": "sha512-2lq1ho1Rq/sDMTieA6zna9aogk3qHM3Oq/mF7QCx2Jj0+e8/ZJOJW+5xU9oUBclRpUIxfUevC93H3eCSr1VW6g==", + "version": "2.1005.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1005.0.tgz", + "integrity": "sha512-4ejfGGrGCEl0pg1xcqkxK0lpBEZqNI48wtrXhk6dYOFYPYMZtqn1kdla29ONN+eO2unewkNF4nLP1lPYhlf9Pg==", "license": "Apache-2.0", "bin": { "cdk": "bin/cdk" @@ -10787,9 +12297,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.181.1", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.181.1.tgz", - "integrity": "sha512-PDxYiqzet17tigJ8icjzoZIzmcdusQfKNnwpRzcGu5//n3YqlKf/vGEkQuU0xcgt4lBMX4Yjuqfsl8wYidCESw==", + "version": "2.185.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.185.0.tgz", + "integrity": "sha512-RNcQeNnInumDF1hq3gAf+/A6jhvYDof5a7418gEs/y6359gTYZpTCQkgItC50iV3MmkgerrBAdOE7CDEtQNDWw==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -10805,19 +12315,19 @@ ], "license": "Apache-2.0", "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.208", + "@aws-cdk/asset-awscli-v1": "^2.2.227", "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", - "@aws-cdk/cloud-assembly-schema": "^39.2.0", + "@aws-cdk/cloud-assembly-schema": "^40.7.0", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.2.0", + "fs-extra": "^11.3.0", "ignore": "^5.3.2", - "jsonschema": "^1.4.1", + "jsonschema": "^1.5.0", "mime-types": "^2.1.35", "minimatch": "^3.1.2", "punycode": "^2.3.1", - "semver": "^7.6.3", - "table": "^6.8.2", + "semver": "^7.7.1", + "table": "^6.9.0", "yaml": "1.10.2" }, "engines": { @@ -11055,7 +12565,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.6.3", + "version": "7.7.1", "inBundle": true, "license": "ISC", "bin": { @@ -11250,10 +12760,11 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", + "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", "dev": true, + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -11517,13 +13028,13 @@ } }, "node_modules/cdk-assets": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cdk-assets/-/cdk-assets-3.0.0.tgz", - "integrity": "sha512-bYcIwAFwkkjB+DR/aFTC3HhkvurLZFokbzdLbbs8w/hmtAl0PUzQpD9bckDTbwzUvHo6QZYA4jn/gVkL6Yvf8Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cdk-assets/-/cdk-assets-3.1.1.tgz", + "integrity": "sha512-EGu1vbmfkfNEf3cHJek522UHUoKlL+na1gCqeei3c2amhoRRPSn0n96SOU7wFz5RvSh/Ruizi4pEVROddN76AA==", "license": "Apache-2.0", "dependencies": { - "@aws-cdk/cloud-assembly-schema": "^40.7.0", - "@aws-cdk/cx-api": "^2.180.0", + "@aws-cdk/cloud-assembly-schema": "^41.1.0", + "@aws-cdk/cx-api": "^2.181.1", "@aws-sdk/client-ecr": "^3", "@aws-sdk/client-s3": "^3", "@aws-sdk/client-secrets-manager": "^3", @@ -11546,9 +13057,9 @@ } }, "node_modules/cdk-assets/node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "40.7.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-40.7.0.tgz", - "integrity": "sha512-00wVKn9pOOGXbeNwA4E8FUFt0zIB4PmSO7PvIiDWgpaFX3G/sWyy0A3s6bg/n2Yvkghu8r4a8ckm+mAzkAYmfA==", + "version": "41.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-41.1.0.tgz", + "integrity": "sha512-V+twOy4asXv6IIH1jxanugBOhBjRdC0Wsr56fSmN8fECp5aJQeqZ5XAiuFSCfwkNoJa72P0cMDm5eFsLK3PzhQ==", "bundleDependencies": [ "jsonschema", "semver" @@ -11660,9 +13171,9 @@ } }, "node_modules/cdk-from-cfn": { - "version": "0.191.0", - "resolved": "https://registry.npmjs.org/cdk-from-cfn/-/cdk-from-cfn-0.191.0.tgz", - "integrity": "sha512-j+TKUSmje5iSiOQzWstH/BkrsL8L9WV57zXll+BHlGT8w5wTrou4dZvNvFQWELW9aaS+UBX0ivsJRZwuO7GiIw==", + "version": "0.193.0", + "resolved": "https://registry.npmjs.org/cdk-from-cfn/-/cdk-from-cfn-0.193.0.tgz", + "integrity": "sha512-LBKqAnsg12RRhyz+zyByI3H6REiDVNm1vofhdnEXSAIGIBuO0H/cw4mbCpz0Qr9huZYssF9ozGsbwa1K3RF2Tg==", "license": "MIT OR Apache-2.0" }, "node_modules/chai": { @@ -12946,9 +14457,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -12958,31 +14469,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" } }, "node_modules/escalade": { @@ -13876,18 +15387,6 @@ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "dev": true }, - "node_modules/hashi-vault-js": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/hashi-vault-js/-/hashi-vault-js-0.4.16.tgz", - "integrity": "sha512-5pEQEYGOUP7USJc9m1O0HRG4tX/Pdvx8V4U7FozpceiU0/ECSUhtR8NVTirnSYixLusiQ5HSuvYc+8u4IOFF9w==", - "dev": true, - "dependencies": { - "axios": "^1.7.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -15182,9 +16681,9 @@ } }, "node_modules/lint-staged": { - "version": "15.4.3", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.4.3.tgz", - "integrity": "sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==", + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.0.tgz", + "integrity": "sha512-WyCzSbfYGhK7cU+UuDDkzUiytbfbi0ZdPy2orwtM75P3WTtQBzmG40cCxIa8Ii2+XjfxzLH6Be46tUfWS85Xfg==", "dev": true, "license": "MIT", "dependencies": { @@ -17052,9 +18551,9 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.10.tgz", + "integrity": "sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==", "dev": true, "funding": [ { @@ -19349,9 +20848,9 @@ } }, "node_modules/rollup": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", - "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.36.0.tgz", + "integrity": "sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==", "dev": true, "license": "MIT", "dependencies": { @@ -19365,25 +20864,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.8", - "@rollup/rollup-android-arm64": "4.34.8", - "@rollup/rollup-darwin-arm64": "4.34.8", - "@rollup/rollup-darwin-x64": "4.34.8", - "@rollup/rollup-freebsd-arm64": "4.34.8", - "@rollup/rollup-freebsd-x64": "4.34.8", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", - "@rollup/rollup-linux-arm-musleabihf": "4.34.8", - "@rollup/rollup-linux-arm64-gnu": "4.34.8", - "@rollup/rollup-linux-arm64-musl": "4.34.8", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", - "@rollup/rollup-linux-riscv64-gnu": "4.34.8", - "@rollup/rollup-linux-s390x-gnu": "4.34.8", - "@rollup/rollup-linux-x64-gnu": "4.34.8", - "@rollup/rollup-linux-x64-musl": "4.34.8", - "@rollup/rollup-win32-arm64-msvc": "4.34.8", - "@rollup/rollup-win32-ia32-msvc": "4.34.8", - "@rollup/rollup-win32-x64-msvc": "4.34.8", + "@rollup/rollup-android-arm-eabi": "4.36.0", + "@rollup/rollup-android-arm64": "4.36.0", + "@rollup/rollup-darwin-arm64": "4.36.0", + "@rollup/rollup-darwin-x64": "4.36.0", + "@rollup/rollup-freebsd-arm64": "4.36.0", + "@rollup/rollup-freebsd-x64": "4.36.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.36.0", + "@rollup/rollup-linux-arm-musleabihf": "4.36.0", + "@rollup/rollup-linux-arm64-gnu": "4.36.0", + "@rollup/rollup-linux-arm64-musl": "4.36.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.36.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.36.0", + "@rollup/rollup-linux-riscv64-gnu": "4.36.0", + "@rollup/rollup-linux-s390x-gnu": "4.36.0", + "@rollup/rollup-linux-x64-gnu": "4.36.0", + "@rollup/rollup-linux-x64-musl": "4.36.0", + "@rollup/rollup-win32-arm64-msvc": "4.36.0", + "@rollup/rollup-win32-ia32-msvc": "4.36.0", + "@rollup/rollup-win32-x64-msvc": "4.36.0", "fsevents": "~2.3.2" } }, @@ -20631,44 +22130,47 @@ "dev": true }, "node_modules/typedoc": { - "version": "0.27.9", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.9.tgz", - "integrity": "sha512-/z585740YHURLl9DN2jCWe6OW7zKYm6VoQ93H0sxZ1cwHQEQrUn5BJrEnkWhfzUdyO+BLGjnKUZ9iz9hKloFDw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.1.tgz", + "integrity": "sha512-Mn2VPNMaxoe/hlBiLriG4U55oyAa3Xo+8HbtEwV7F5WEOPXqtxzGuMZhJYHaqFJpajeQ6ZDUC2c990NAtTbdgw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^1.24.0", + "@gerrit0/mini-shiki": "^3.2.1", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", - "yaml": "^2.6.1" + "yaml": "^2.7.0 " }, "bin": { "typedoc": "bin/typedoc" }, "engines": { - "node": ">= 18" + "node": ">= 18", + "pnpm": ">= 10" }, "peerDependencies": { "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" } }, "node_modules/typedoc-plugin-missing-exports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-3.1.0.tgz", - "integrity": "sha512-Sogbaj+qDa21NjB3SlIw4JXSwmcl/WOjwiPNaVEcPhpNG/MiRTtpwV81cT7h1cbu9StpONFPbddYWR0KV/fTWA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-4.0.0.tgz", + "integrity": "sha512-Z4ei+853xppDEhcqzyeyRs4+R0kUuKQWnMK1EtSTEd5LFkgkdW5Bdn8vfo/rsCGbYVJxOWU99fxgM1mROw5Fug==", "dev": true, + "license": "MIT", "peerDependencies": { - "typedoc": "0.26.x || 0.27.x" + "typedoc": "^0.28.1" } }, "node_modules/typedoc-plugin-zod": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/typedoc-plugin-zod/-/typedoc-plugin-zod-1.3.1.tgz", - "integrity": "sha512-u4NH1Ez168gRNnhUd0x4pZhp85maJ9y050IxSok9XwdzTpUA9NN0ee3ho8ssrzmxsvO2UDbDEiks7xtI0p6UXA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-zod/-/typedoc-plugin-zod-1.4.0.tgz", + "integrity": "sha512-Mr4hoEfjIR1xjp0RqtEyfba03NDUE4jLvgErg0VQz9L60eCVLcUrV0x+71+B7rykDMexrTx0I9Yk6SS0PuSbKw==", "dev": true, + "license": "MIT", "peerDependencies": { - "typedoc": "0.23.x || 0.24.x || 0.25.x || 0.26.x || 0.27.x" + "typedoc": "0.23.x || 0.24.x || 0.25.x || 0.26.x || 0.27.x || 0.28.x" } }, "node_modules/typedoc/node_modules/minimatch": { @@ -20687,9 +22189,9 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -20848,9 +22350,9 @@ } }, "node_modules/vite": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", - "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz", + "integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -20920,9 +22422,9 @@ } }, "node_modules/vite-node": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", - "integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", + "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", "dev": true, "license": "MIT", "dependencies": { @@ -20958,19 +22460,19 @@ } }, "node_modules/vitest": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz", - "integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", + "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.7", - "@vitest/mocker": "3.0.7", - "@vitest/pretty-format": "^3.0.7", - "@vitest/runner": "3.0.7", - "@vitest/snapshot": "3.0.7", - "@vitest/spy": "3.0.7", - "@vitest/utils": "3.0.7", + "@vitest/expect": "3.0.9", + "@vitest/mocker": "3.0.9", + "@vitest/pretty-format": "^3.0.9", + "@vitest/runner": "3.0.9", + "@vitest/snapshot": "3.0.9", + "@vitest/spy": "3.0.9", + "@vitest/utils": "3.0.9", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", @@ -20982,7 +22484,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.7", + "vite-node": "3.0.9", "why-is-node-running": "^2.3.0" }, "bin": { @@ -20998,8 +22500,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.7", - "@vitest/ui": "3.0.7", + "@vitest/browser": "3.0.9", + "@vitest/ui": "3.0.9", "happy-dom": "*", "jsdom": "*" }, @@ -21438,7 +22940,7 @@ }, "packages/batch": { "name": "@aws-lambda-powertools/batch", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing" @@ -21446,7 +22948,7 @@ }, "packages/commons": { "name": "@aws-lambda-powertools/commons", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing" @@ -21454,24 +22956,24 @@ }, "packages/event-handler": { "name": "@aws-lambda-powertools/event-handler", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0" + "@aws-lambda-powertools/commons": "^2.17.0" } }, "packages/idempotency": { "name": "@aws-lambda-powertools/idempotency", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0", - "@aws-lambda-powertools/jmespath": "^2.16.0" + "@aws-lambda-powertools/commons": "^2.17.0", + "@aws-lambda-powertools/jmespath": "^2.17.0" }, "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-dynamodb": "^3.758.0", - "@aws-sdk/lib-dynamodb": "^3.758.0", + "@aws-sdk/client-dynamodb": "^3.772.0", + "@aws-sdk/lib-dynamodb": "^3.772.0", "aws-sdk-client-mock": "^4.1.0" }, "peerDependencies": { @@ -21493,18 +22995,18 @@ }, "packages/jmespath": { "name": "@aws-lambda-powertools/jmespath", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0" + "@aws-lambda-powertools/commons": "^2.17.0" } }, "packages/logger": { "name": "@aws-lambda-powertools/logger", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0", + "@aws-lambda-powertools/commons": "^2.17.0", "lodash.merge": "^4.6.2" }, "devDependencies": { @@ -21512,9 +23014,13 @@ "@types/lodash.merge": "^4.6.9" }, "peerDependencies": { + "@aws-lambda-powertools/jmespath": "2.x", "@middy/core": "4.x || 5.x || 6.x" }, "peerDependenciesMeta": { + "@aws-lambda-powertools/jmespath": { + "optional": true + }, "@middy/core": { "optional": true } @@ -21522,14 +23028,14 @@ }, "packages/metrics": { "name": "@aws-lambda-powertools/metrics", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0" + "@aws-lambda-powertools/commons": "^2.17.0" }, "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-cloudwatch": "^3.758.0", + "@aws-sdk/client-cloudwatch": "^3.772.0", "@types/promise-retry": "^1.1.3", "promise-retry": "^2.0.1" }, @@ -21544,18 +23050,18 @@ }, "packages/parameters": { "name": "@aws-lambda-powertools/parameters", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0" + "@aws-lambda-powertools/commons": "^2.17.0" }, "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-appconfigdata": "^3.758.0", - "@aws-sdk/client-dynamodb": "^3.758.0", - "@aws-sdk/client-secrets-manager": "^3.758.0", - "@aws-sdk/client-ssm": "^3.759.0", - "@aws-sdk/util-dynamodb": "^3.758.0", + "@aws-sdk/client-appconfigdata": "^3.772.0", + "@aws-sdk/client-dynamodb": "^3.772.0", + "@aws-sdk/client-secrets-manager": "^3.772.0", + "@aws-sdk/client-ssm": "^3.772.0", + "@aws-sdk/util-dynamodb": "^3.772.0", "@smithy/util-base64": "^4.0.0", "aws-sdk-client-mock": "^4.1.0" }, @@ -21590,10 +23096,10 @@ }, "packages/parser": { "name": "@aws-lambda-powertools/parser", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0" + "@aws-lambda-powertools/commons": "^2.17.0" }, "peerDependencies": { "@middy/core": "4.x || 5.x || 6.x", @@ -21610,14 +23116,14 @@ }, "packages/testing": { "name": "@aws-lambda-powertools/testing-utils", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "dependencies": { - "@aws-cdk/toolkit-lib": "^0.1.3", - "@aws-sdk/client-lambda": "^3.758.0", + "@aws-cdk/toolkit-lib": "^0.1.6", + "@aws-sdk/client-lambda": "^3.772.0", "@smithy/util-utf8": "^4.0.0", - "aws-cdk-lib": "^2.181.1", - "esbuild": "^0.25.0", + "aws-cdk-lib": "^2.185.0", + "esbuild": "^0.25.1", "promise-retry": "^2.0.1" }, "devDependencies": { @@ -21627,16 +23133,16 @@ }, "packages/tracer": { "name": "@aws-lambda-powertools/tracer", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0", + "@aws-lambda-powertools/commons": "^2.17.0", "aws-xray-sdk-core": "^3.10.3" }, "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-dynamodb": "^3.758.0", - "@aws-sdk/client-xray": "^3.758.0" + "@aws-sdk/client-dynamodb": "^3.772.0", + "@aws-sdk/client-xray": "^3.772.0" }, "peerDependencies": { "@middy/core": "4.x || 5.x || 6.x" @@ -21649,11 +23155,11 @@ }, "packages/validation": { "name": "@aws-lambda-powertools/validation", - "version": "2.16.0", + "version": "2.17.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0", - "@aws-lambda-powertools/jmespath": "^2.16.0", + "@aws-lambda-powertools/commons": "^2.17.0", + "@aws-lambda-powertools/jmespath": "^2.17.0", "ajv": "^8.17.1" } } diff --git a/package.json b/package.json index be0ee6e7ff..d7f40a347e 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,11 @@ "commit": "commit", "setup-local": "npm ci && npm run build && husky", "build": "npm run build -ws", - "docs-website-build-run": "npm run docs-buildDockerImage && npm run docs-runLocalDocker", - "docs-buildDockerImage": "docker build -t powertools-typescript/docs ./docs/", - "docs-runLocalDocker": "docker run --rm -it -p 8000:8000 -v ${PWD}:/docs powertools-typescript/docs", - "docs-api-build-run": "npm run docs-generateApiDoc && npx live-server api", - "docs-generateApiDoc": "typedoc .", - "docs-runLocalApiDoc": "npx live-server api", + "docs:docker:build": "docker build -t powertools-typescript/docs ./docs/", + "docs:docker:run": "docker run --rm -it -p 8000:8000 -v ${PWD}:/docs powertools-typescript/docs", + "docs:local:setup": "python3 -m venv .venv && .venv/bin/pip install -r docs/requirements.txt", + "docs:local:run": ".venv/bin/mkdocs serve", + "docs:local:api": "typedoc .", "postpublish": "git restore .", "lint:markdown": "markdownlint-cli2 '**/*.md' '#node_modules' '#**/*/node_modules' '#docs/changelog.md' '#LICENSE.md' '#.github' '#**/*/CHANGELOG.md' '#examples/app/README.md'" }, @@ -52,21 +51,21 @@ "homepage": "https://github.com/aws-powertools/powertools-lambda-typescript#readme", "devDependencies": { "@biomejs/biome": "^1.9.4", - "@types/aws-lambda": "^8.10.147", - "@types/node": "^22.13.9", - "@vitest/coverage-v8": "^3.0.7", + "@types/aws-lambda": "^8.10.148", + "@types/node": "^22.13.13", + "@vitest/coverage-v8": "^3.0.9", "husky": "^9.1.7", "lerna": "8.1.2", - "lint-staged": "^15.4.3", + "lint-staged": "^15.5.0", "markdownlint-cli2": "^0.17.2", "middy4": "npm:@middy/core@^4.7.0", "middy5": "npm:@middy/core@^5.4.3", "middy6": "npm:@middy/core@^6.0.0", - "typedoc": "^0.27.9", - "typedoc-plugin-missing-exports": "^3.1.0", - "typedoc-plugin-zod": "^1.3.1", - "typescript": "^5.7.3", - "vitest": "^3.0.5" + "typedoc": "^0.28.1", + "typedoc-plugin-missing-exports": "^4.0.0", + "typedoc-plugin-zod": "^1.4.0", + "typescript": "^5.8.2", + "vitest": "^3.0.9" }, "lint-staged": { "*.{js,ts}": "biome check --write", diff --git a/packages/batch/CHANGELOG.md b/packages/batch/CHANGELOG.md index d026d98866..5f7dad62e6 100644 --- a/packages/batch/CHANGELOG.md +++ b/packages/batch/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + +**Note:** Version bump only for package @aws-lambda-powertools/batch + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) diff --git a/packages/batch/README.md b/packages/batch/README.md index fb6d8ade44..5ca98dd511 100644 --- a/packages/batch/README.md +++ b/packages/batch/README.md @@ -176,6 +176,7 @@ The following companies, among others, use Powertools: - [Elva](https://elva-group.com) - [Flyweight](https://flyweight.io/) - [globaldatanet](https://globaldatanet.com/) +- [Guild](https://guild.com) - [Hashnode](https://hashnode.com/) - [LocalStack](https://localstack.cloud/) - [Perfect Post](https://www.perfectpost.fr) diff --git a/packages/batch/package.json b/packages/batch/package.json index 5b6bfa9cd1..b9adc82d4f 100644 --- a/packages/batch/package.json +++ b/packages/batch/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/batch", - "version": "2.16.0", + "version": "2.17.0", "description": "The batch processing package for the Powertools for AWS Lambda (TypeScript) library.", "author": { "name": "Amazon Web Services", diff --git a/packages/commons/CHANGELOG.md b/packages/commons/CHANGELOG.md index 299a20b945..44e4d70d62 100644 --- a/packages/commons/CHANGELOG.md +++ b/packages/commons/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + + +### Features + +* **commons:** make utilities aware of provisioned concurrency ([#3724](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3724)) ([c28e45e](https://github.com/aws-powertools/powertools-lambda-typescript/commit/c28e45ecba315bac8fbc7744dbe21a3461747d44)) + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) diff --git a/packages/commons/README.md b/packages/commons/README.md index 8aa47fd986..d05fceffe5 100644 --- a/packages/commons/README.md +++ b/packages/commons/README.md @@ -126,6 +126,7 @@ The following companies, among others, use Powertools: - [Elva](https://elva-group.com) - [Flyweight](https://flyweight.io/) - [globaldatanet](https://globaldatanet.com/) +- [Guild](https://guild.com) - [Hashnode](https://hashnode.com/) - [LocalStack](https://localstack.cloud/) - [Perfect Post](https://www.perfectpost.fr) diff --git a/packages/commons/package.json b/packages/commons/package.json index d089c99f0f..9ce3f3a630 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/commons", - "version": "2.16.0", + "version": "2.17.0", "description": "A shared utility package for Powertools for AWS Lambda (TypeScript) libraries", "author": { "name": "Amazon Web Services", diff --git a/packages/commons/src/Utility.ts b/packages/commons/src/Utility.ts index 2932785e0d..89f1dbd03d 100644 --- a/packages/commons/src/Utility.ts +++ b/packages/commons/src/Utility.ts @@ -51,15 +51,46 @@ * ``` */ export class Utility { + readonly #initializationType: + | 'unknown' + | 'on-demand' + | 'provisioned-concurrency'; protected coldStart = true; protected readonly defaultServiceName: string = 'service_undefined'; + public constructor() { + this.#initializationType = this.getInitializationType(); + if (this.#initializationType !== 'on-demand') { + this.coldStart = false; + } + } + + /** + * Get the value of the `AWS_LAMBDA_INITIALIZATION_TYPE` environment variable. + */ + protected getInitializationType(): + | 'unknown' + | 'on-demand' + | 'provisioned-concurrency' { + const envVarValue = process.env.AWS_LAMBDA_INITIALIZATION_TYPE?.trim(); + if (envVarValue === 'on-demand') { + return 'on-demand'; + } + if (envVarValue === 'provisioned-concurrency') { + return 'provisioned-concurrency'; + } + return 'unknown'; + } + /** * Get the cold start status of the current execution environment. * * The method also flips the cold start status to `false` after the first invocation. */ protected getColdStart(): boolean { + if (this.#initializationType !== 'on-demand') { + return false; + } if (this.coldStart) { this.coldStart = false; diff --git a/packages/commons/src/version.ts b/packages/commons/src/version.ts index c6e60572eb..9c78bb2091 100644 --- a/packages/commons/src/version.ts +++ b/packages/commons/src/version.ts @@ -1,2 +1,2 @@ // this file is auto generated, do not modify -export const PT_VERSION = '2.16.0'; +export const PT_VERSION = '2.17.0'; diff --git a/packages/commons/tests/unit/Utility.test.ts b/packages/commons/tests/unit/Utility.test.ts index 16f88c3f38..15b3309538 100644 --- a/packages/commons/tests/unit/Utility.test.ts +++ b/packages/commons/tests/unit/Utility.test.ts @@ -15,6 +15,12 @@ describe('Class: Utility', () => { public validateServiceName(serviceName: string): boolean { return this.isValidServiceName(serviceName); } + public getInitializationType(): + | 'unknown' + | 'on-demand' + | 'provisioned-concurrency' { + return super.getInitializationType(); + } } it('returns the correct cold start value', () => { @@ -27,6 +33,15 @@ describe('Class: Utility', () => { expect(utility.dummyMethod()).toBe(false); }); + it('returns the correct cold start value when provisioned concurrency is used', () => { + // Prepare + process.env.AWS_LAMBDA_INITIALIZATION_TYPE = 'provisioned-concurrency'; + const utility = new TestUtility(); + + // Act & Assess + expect(utility.dummyMethod()).toBe(false); + }); + it('flips the cold start value', () => { // Prepare const utility = new TestUtility(); @@ -54,4 +69,23 @@ describe('Class: Utility', () => { expect(utility.validateServiceName('serverlessAirline')).toBe(true); expect(utility.validateServiceName('')).toBe(false); }); + + it.each([ + { value: 'on-demand', expected: 'on-demand' }, + { value: 'provisioned-concurrency', expected: 'provisioned-concurrency' }, + { value: '', expected: 'unknown' }, + ])( + 'returns the correct initialization type ($value)', + ({ value, expected }) => { + // Prepare + process.env.AWS_LAMBDA_INITIALIZATION_TYPE = value; + const utility = new TestUtility(); + + // Act + const initializationType = utility.getInitializationType(); + + // Assess + expect(initializationType).toBe(expected); + } + ); }); diff --git a/packages/commons/tests/unit/awsSdkUtils.test.ts b/packages/commons/tests/unit/awsSdkUtils.test.ts index c8f233e355..7af7af9d3a 100644 --- a/packages/commons/tests/unit/awsSdkUtils.test.ts +++ b/packages/commons/tests/unit/awsSdkUtils.test.ts @@ -6,6 +6,10 @@ import { PT_VERSION as version, } from '../../src/index.js'; +vi.hoisted(() => { + process.env.AWS_EXECUTION_ENV = ''; +}); + describe('Helpers: awsSdk', () => { describe('Function: userAgentMiddleware', () => { beforeAll(() => { diff --git a/packages/commons/vitest.config.ts b/packages/commons/vitest.config.ts index d5aa737c68..9f1196ef1f 100644 --- a/packages/commons/vitest.config.ts +++ b/packages/commons/vitest.config.ts @@ -3,5 +3,6 @@ import { defineProject } from 'vitest/config'; export default defineProject({ test: { environment: 'node', + setupFiles: ['../testing/src/setupEnv.ts'], }, }); diff --git a/packages/event-handler/CHANGELOG.md b/packages/event-handler/CHANGELOG.md index 7947464911..5afd0c1f6a 100644 --- a/packages/event-handler/CHANGELOG.md +++ b/packages/event-handler/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + +**Note:** Version bump only for package @aws-lambda-powertools/event-handler + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) **Note:** Version bump only for package @aws-lambda-powertools/event-handler diff --git a/packages/event-handler/README.md b/packages/event-handler/README.md index 094ced856e..4689f15070 100644 --- a/packages/event-handler/README.md +++ b/packages/event-handler/README.md @@ -53,6 +53,7 @@ The following companies, among others, use Powertools: - [Elva](https://elva-group.com) - [Flyweight](https://flyweight.io/) - [globaldatanet](https://globaldatanet.com/) +- [Guild](https://guild.com) - [Hashnode](https://hashnode.com/) - [LocalStack](https://localstack.cloud/) - [Perfect Post](https://www.perfectpost.fr) diff --git a/packages/event-handler/package.json b/packages/event-handler/package.json index b2a55759f8..296922d7ee 100644 --- a/packages/event-handler/package.json +++ b/packages/event-handler/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/event-handler", - "version": "2.16.0", + "version": "2.17.0", "description": "Lightweight routing to reduce boilerplate for API Gateway REST/HTTP API, ALB and Lambda Function URLs", "author": { "name": "Amazon Web Services", @@ -51,7 +51,7 @@ "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" }, "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0" + "@aws-lambda-powertools/commons": "^2.17.0" }, "keywords": [ "aws", diff --git a/packages/idempotency/CHANGELOG.md b/packages/idempotency/CHANGELOG.md index 0edae7f140..35e708fb2b 100644 --- a/packages/idempotency/CHANGELOG.md +++ b/packages/idempotency/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + + +### Bug Fixes + +* **idempotency:** include sk in error msgs when using composite key ([#3709](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3709)) ([661f5ff](https://github.com/aws-powertools/powertools-lambda-typescript/commit/661f5ff7f3f3805e24f515892e98430dccebf979)) + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) **Note:** Version bump only for package @aws-lambda-powertools/idempotency diff --git a/packages/idempotency/README.md b/packages/idempotency/README.md index bd86eaec58..c4902e7370 100644 --- a/packages/idempotency/README.md +++ b/packages/idempotency/README.md @@ -347,6 +347,7 @@ The following companies, among others, use Powertools: - [Elva](https://elva-group.com) - [Flyweight](https://flyweight.io/) - [globaldatanet](https://globaldatanet.com/) +- [Guild](https://guild.com) - [Hashnode](https://hashnode.com/) - [LocalStack](https://localstack.cloud/) - [Perfect Post](https://www.perfectpost.fr) diff --git a/packages/idempotency/package.json b/packages/idempotency/package.json index f788caf2b3..48b1f06d94 100644 --- a/packages/idempotency/package.json +++ b/packages/idempotency/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/idempotency", - "version": "2.16.0", + "version": "2.17.0", "description": "The idempotency package for the Powertools for AWS Lambda (TypeScript) library. It provides options to make your Lambda functions idempotent and safe to retry.", "author": { "name": "Amazon Web Services", @@ -98,8 +98,8 @@ "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" }, "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0", - "@aws-lambda-powertools/jmespath": "^2.16.0" + "@aws-lambda-powertools/commons": "^2.17.0", + "@aws-lambda-powertools/jmespath": "^2.17.0" }, "peerDependencies": { "@aws-sdk/client-dynamodb": ">=3.x", @@ -127,8 +127,8 @@ ], "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-dynamodb": "^3.758.0", - "@aws-sdk/lib-dynamodb": "^3.758.0", + "@aws-sdk/client-dynamodb": "^3.772.0", + "@aws-sdk/lib-dynamodb": "^3.772.0", "aws-sdk-client-mock": "^4.1.0" } } diff --git a/packages/idempotency/src/IdempotencyHandler.ts b/packages/idempotency/src/IdempotencyHandler.ts index 90b7372317..dcf27d3264 100644 --- a/packages/idempotency/src/IdempotencyHandler.ts +++ b/packages/idempotency/src/IdempotencyHandler.ts @@ -120,7 +120,11 @@ export class IdempotencyHandler { ); } throw new IdempotencyAlreadyInProgressError( - `There is already an execution in progress with idempotency key: ${idempotencyRecord.idempotencyKey}` + `There is already an execution in progress with idempotency key: ${idempotencyRecord.idempotencyKey}${ + idempotencyRecord.sortKey + ? ` and sort key: ${idempotencyRecord.sortKey}` + : '' + }` ); } diff --git a/packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts b/packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts index 3c84f1850f..e7e50fb744 100644 --- a/packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts +++ b/packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts @@ -122,6 +122,7 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer { return new IdempotencyRecord({ idempotencyKey: item[this.keyAttr], + sortKey: this.sortKeyAttr && item[this.sortKeyAttr], status: item[this.statusAttr], expiryTimestamp: item[this.expiryAttr], inProgressExpiryTimestamp: item[this.inProgressExpiryAttr], @@ -207,6 +208,7 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer { item && new IdempotencyRecord({ idempotencyKey: item[this.keyAttr], + sortKey: this.sortKeyAttr && item[this.sortKeyAttr], status: item[this.statusAttr], expiryTimestamp: item[this.expiryAttr], inProgressExpiryTimestamp: item[this.inProgressExpiryAttr], @@ -214,7 +216,9 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer { payloadHash: item[this.validationKeyAttr], }); throw new IdempotencyItemAlreadyExistsError( - `Failed to put record for already existing idempotency key: ${record.idempotencyKey}`, + `Failed to put record for already existing idempotency key: ${record.idempotencyKey}${ + this.sortKeyAttr ? ` and sort key: ${record.sortKey}` : '' + }`, idempotencyRecord ); } diff --git a/packages/idempotency/src/persistence/IdempotencyRecord.ts b/packages/idempotency/src/persistence/IdempotencyRecord.ts index 2b99cadc4f..b0aa037a63 100644 --- a/packages/idempotency/src/persistence/IdempotencyRecord.ts +++ b/packages/idempotency/src/persistence/IdempotencyRecord.ts @@ -5,6 +5,7 @@ import type { IdempotencyRecordOptions, IdempotencyRecordStatusValue, } from '../types/IdempotencyRecord.js'; +import type { DynamoDBPersistenceLayer } from './DynamoDBPersistenceLayer.js'; /** * Class representing an idempotency record. @@ -19,6 +20,10 @@ class IdempotencyRecord { * The idempotency key of the record that is used to identify the record. */ public idempotencyKey: string; + /** + * An optional sort key that can be used with the {@link DynamoDBPersistenceLayer | `DynamoDBPersistenceLayer`}. + */ + public sortKey?: string; /** * The expiry timestamp of the in progress record in milliseconds UTC. */ @@ -46,6 +51,7 @@ class IdempotencyRecord { this.responseData = config.responseData; this.payloadHash = config.payloadHash; this.status = config.status; + this.sortKey = config.sortKey; } /** diff --git a/packages/idempotency/src/types/IdempotencyRecord.ts b/packages/idempotency/src/types/IdempotencyRecord.ts index cfd01a44bf..9c941b9787 100644 --- a/packages/idempotency/src/types/IdempotencyRecord.ts +++ b/packages/idempotency/src/types/IdempotencyRecord.ts @@ -11,11 +11,35 @@ type IdempotencyRecordStatusValue = * Options for creating a new IdempotencyRecord */ type IdempotencyRecordOptions = { + /** + * The idempotency key of the record that is used to identify the record. + */ idempotencyKey: string; + /** + * An optional sort key that can be used with the {@link DynamoDBPersistenceLayer | `DynamoDBPersistenceLayer`}. + */ + sortKey?: string; + /** + * The idempotency record status can be COMPLETED, IN_PROGRESS or EXPIRED. + * We check the status during idempotency processing to make sure we don't process an expired record and handle concurrent requests. + * {@link constants.IdempotencyRecordStatusValue | IdempotencyRecordStatusValue} + */ status: IdempotencyRecordStatusValue; + /** + * The expiry timestamp of the record in milliseconds UTC. + */ expiryTimestamp?: number; + /** + * The expiry timestamp of the in progress record in milliseconds UTC. + */ inProgressExpiryTimestamp?: number; + /** + * The response data of the request, this will be returned if the payload hash matches. + */ responseData?: JSONValue; + /** + * The hash of the payload of the request, used for comparing requests. + */ payloadHash?: string; }; diff --git a/packages/idempotency/tests/e2e/constants.ts b/packages/idempotency/tests/e2e/constants.ts index 38ae9b6c83..e9cec8eadc 100644 --- a/packages/idempotency/tests/e2e/constants.ts +++ b/packages/idempotency/tests/e2e/constants.ts @@ -1,6 +1 @@ export const RESOURCE_NAME_PREFIX = 'Idempotency'; - -export const ONE_MINUTE = 60 * 1_000; -export const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; -export const SETUP_TIMEOUT = 7 * ONE_MINUTE; -export const TEST_CASE_TIMEOUT = 5 * ONE_MINUTE; diff --git a/packages/idempotency/tests/e2e/idempotentDecorator.test.ts b/packages/idempotency/tests/e2e/idempotentDecorator.test.ts index 00f0aa236d..9c83ab5417 100644 --- a/packages/idempotency/tests/e2e/idempotentDecorator.test.ts +++ b/packages/idempotency/tests/e2e/idempotentDecorator.test.ts @@ -11,12 +11,7 @@ import { Duration } from 'aws-cdk-lib'; import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { IdempotencyTestNodejsFunctionAndDynamoTable } from '../helpers/resources.js'; -import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, -} from './constants.js'; +import { RESOURCE_NAME_PREFIX } from './constants.js'; const dynamoDBClient = new DynamoDBClient({}); @@ -160,315 +155,289 @@ describe('Idempotency e2e test decorator, default settings', () => { functionNameDataIndex = testStack.findAndGetStackOutputValue('dataIndexFn'); tableNameDataIndex = testStack.findAndGetStackOutputValue('dataIndexTable'); - }, SETUP_TIMEOUT); + }); - it( - 'returns the same result and runs the handler once when called multiple times', - async () => { - const payload = { foo: 'bar' }; + it('returns the same result and runs the handler once when called multiple times', async () => { + const payload = { foo: 'bar' }; - const payloadHash = createHash('md5') - .update(JSON.stringify(payload)) - .digest('base64'); + const payloadHash = createHash('md5') + .update(JSON.stringify(payload)) + .digest('base64'); - const logs = await invokeFunction({ - functionName: functionNameDefault, - times: 2, - invocationMode: 'SEQUENTIAL', - payload: payload, - }); + const logs = await invokeFunction({ + functionName: functionNameDefault, + times: 2, + invocationMode: 'SEQUENTIAL', + payload: payload, + }); - const functionLogs = logs.map((log) => log.getFunctionLogs()); + const functionLogs = logs.map((log) => log.getFunctionLogs()); - const idempotencyRecord = await dynamoDBClient.send( - new ScanCommand({ - TableName: tableNameDefault, - }) - ); - expect(idempotencyRecord.Items).toHaveLength(1); - expect(idempotencyRecord.Items?.[0].id).toEqual( - `${functionNameDefault}#${payloadHash}` - ); - expect(idempotencyRecord.Items?.[0].data).toBeUndefined(); - expect(idempotencyRecord.Items?.[0].status).toEqual('COMPLETED'); - // During the first invocation the handler should be called, so the logs should contain 1 log - expect(functionLogs[0]).toHaveLength(1); - // We test the content of the log as well as the presence of fields from the context, this - // ensures that the all the arguments are passed to the handler when made idempotent - expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( - expect.objectContaining({ - message: 'Got test event: {"foo":"bar"}', - }) - ); - }, - TEST_CASE_TIMEOUT - ); + const idempotencyRecord = await dynamoDBClient.send( + new ScanCommand({ + TableName: tableNameDefault, + }) + ); + expect(idempotencyRecord.Items).toHaveLength(1); + expect(idempotencyRecord.Items?.[0].id).toEqual( + `${functionNameDefault}#${payloadHash}` + ); + expect(idempotencyRecord.Items?.[0].data).toBeUndefined(); + expect(idempotencyRecord.Items?.[0].status).toEqual('COMPLETED'); + // During the first invocation the handler should be called, so the logs should contain 1 log + expect(functionLogs[0]).toHaveLength(1); + // We test the content of the log as well as the presence of fields from the context, this + // ensures that the all the arguments are passed to the handler when made idempotent + expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( + expect.objectContaining({ + message: 'Got test event: {"foo":"bar"}', + }) + ); + }); - it( - 'handles parallel invocations correctly', - async () => { - const payload = { foo: 'bar' }; - const payloadHash = createHash('md5') - .update(JSON.stringify(payload)) - .digest('base64'); - const logs = await invokeFunction({ - functionName: functionNameDefaultParallel, - times: 2, - invocationMode: 'PARALLEL', - payload: payload, - }); - - const functionLogs = logs.map((log) => log.getFunctionLogs()); - - const idempotencyRecords = await dynamoDBClient.send( - new ScanCommand({ - TableName: tableNameDefaultParallel, - }) - ); - expect(idempotencyRecords.Items).toHaveLength(1); - expect(idempotencyRecords.Items?.[0].id).toEqual( - `${functionNameDefaultParallel}#${payloadHash}` - ); - expect(idempotencyRecords.Items?.[0].data).toEqual('bar'); - expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); - expect(idempotencyRecords?.Items?.[0].expiration).toBeGreaterThan( - Date.now() / 1000 - ); - const successfulInvocationLogs = functionLogs.find( - (functionLog) => - functionLog.toString().includes('Processed event') !== undefined - ); - - const failedInvocationLogs = functionLogs.find( - (functionLog) => - functionLog - .toString() - .includes('There is already an execution in progres') !== undefined - ); - - expect(successfulInvocationLogs).toBeDefined(); - expect(failedInvocationLogs).toBeDefined(); - }, - TEST_CASE_TIMEOUT - ); + it('handles parallel invocations correctly', async () => { + const payload = { foo: 'bar' }; + const payloadHash = createHash('md5') + .update(JSON.stringify(payload)) + .digest('base64'); + const logs = await invokeFunction({ + functionName: functionNameDefaultParallel, + times: 2, + invocationMode: 'PARALLEL', + payload: payload, + }); + + const functionLogs = logs.map((log) => log.getFunctionLogs()); + + const idempotencyRecords = await dynamoDBClient.send( + new ScanCommand({ + TableName: tableNameDefaultParallel, + }) + ); + expect(idempotencyRecords.Items).toHaveLength(1); + expect(idempotencyRecords.Items?.[0].id).toEqual( + `${functionNameDefaultParallel}#${payloadHash}` + ); + expect(idempotencyRecords.Items?.[0].data).toEqual('bar'); + expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); + expect(idempotencyRecords?.Items?.[0].expiration).toBeGreaterThan( + Date.now() / 1000 + ); + const successfulInvocationLogs = functionLogs.find( + (functionLog) => + functionLog.toString().includes('Processed event') !== undefined + ); - it( - 'recovers from a timed out request and processes the next one', - async () => { - const payload = { foo: 'bar' }; - const payloadHash = createHash('md5') - .update(JSON.stringify(payload.foo)) - .digest('base64'); - - const logs = await invokeFunction({ - functionName: functionNameTimeout, - times: 2, - invocationMode: 'SEQUENTIAL', - payload: Array.from({ length: 2 }, (_, index) => ({ - ...payload, - invocation: index, - })), - }); - const functionLogs = logs.map((log) => log.getFunctionLogs()); - const idempotencyRecord = await dynamoDBClient.send( - new ScanCommand({ - TableName: tableNameTimeout, - }) - ); - expect(idempotencyRecord.Items).toHaveLength(1); - expect(idempotencyRecord.Items?.[0].id).toEqual( - `${functionNameTimeout}#${payloadHash}` - ); - expect(idempotencyRecord.Items?.[0].data).toEqual({ - ...payload, - invocation: 1, - }); - expect(idempotencyRecord.Items?.[0].status).toEqual('COMPLETED'); - - try { - // During the first invocation the handler should be called, so the logs should contain 1 log - expect(functionLogs[0]).toHaveLength(2); - expect(functionLogs[0][0]).toContain('Task timed out after'); - } catch { - // During the first invocation the function should timeout so the logs should not contain any log and the report log should contain a timeout message - expect(functionLogs[0]).toHaveLength(0); - expect(logs[0].getReportLog()).toMatch(/Status: timeout$/); - } - - expect(functionLogs[1]).toHaveLength(1); - expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( - expect.objectContaining({ - message: 'Processed event', - details: 'bar', - function_name: functionNameTimeout, - }) - ); - }, - TEST_CASE_TIMEOUT - ); + const failedInvocationLogs = functionLogs.find( + (functionLog) => + functionLog + .toString() + .includes('There is already an execution in progres') !== undefined + ); - it( - 'recovers from an expired idempotency record and processes the next request', - async () => { - const payload = { - foo: 'baz', - }; - const payloadHash = createHash('md5') - .update(JSON.stringify(payload.foo)) - .digest('base64'); - - // Act - const logs = [ - ( - await invokeFunction({ - functionName: functionNameExpired, - times: 1, - invocationMode: 'SEQUENTIAL', - payload: { ...payload, invocation: 0 }, - }) - )[0], - ]; - // Wait for the idempotency record to expire - await new Promise((resolve) => setTimeout(resolve, 2000)); - logs.push( - ( - await invokeFunction({ - functionName: functionNameExpired, - times: 1, - invocationMode: 'SEQUENTIAL', - payload: { ...payload, invocation: 1 }, - }) - )[0] - ); - const functionLogs = logs.map((log) => log.getFunctionLogs()); - - // Assess - const idempotencyRecords = await dynamoDBClient.send( - new ScanCommand({ - TableName: tableNameExpired, - }) - ); - expect(idempotencyRecords.Items).toHaveLength(1); - expect(idempotencyRecords.Items?.[0].id).toEqual( - `${functionNameExpired}#${payloadHash}` - ); - expect(idempotencyRecords.Items?.[0].data).toEqual({ + expect(successfulInvocationLogs).toBeDefined(); + expect(failedInvocationLogs).toBeDefined(); + }); + + it('recovers from a timed out request and processes the next one', async () => { + const payload = { foo: 'bar' }; + const payloadHash = createHash('md5') + .update(JSON.stringify(payload.foo)) + .digest('base64'); + + const logs = await invokeFunction({ + functionName: functionNameTimeout, + times: 2, + invocationMode: 'SEQUENTIAL', + payload: Array.from({ length: 2 }, (_, index) => ({ ...payload, - invocation: 1, - }); - expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); - - // Both invocations should be successful and the logs should contain 1 log each - expect(functionLogs[0]).toHaveLength(1); - expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( - expect.objectContaining({ - message: 'Processed event', - details: 'baz', - function_name: functionNameExpired, - }) - ); - // During the second invocation the handler should be called and complete, so the logs should - // contain 1 log - expect(functionLogs[1]).toHaveLength(1); - expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( - expect.objectContaining({ - message: 'Processed event', - details: 'baz', - function_name: functionNameExpired, - }) - ); - }, - TEST_CASE_TIMEOUT - ); + invocation: index, + })), + }); + const functionLogs = logs.map((log) => log.getFunctionLogs()); + const idempotencyRecord = await dynamoDBClient.send( + new ScanCommand({ + TableName: tableNameTimeout, + }) + ); + expect(idempotencyRecord.Items).toHaveLength(1); + expect(idempotencyRecord.Items?.[0].id).toEqual( + `${functionNameTimeout}#${payloadHash}` + ); + expect(idempotencyRecord.Items?.[0].data).toEqual({ + ...payload, + invocation: 1, + }); + expect(idempotencyRecord.Items?.[0].status).toEqual('COMPLETED'); - it( - 'uses the provided custom idempotency record attributes', - async () => { - const payload = { foo: 'bar' }; - const payloadHash = createHash('md5') - .update(JSON.stringify(payload)) - .digest('base64'); - const logs = await invokeFunction({ - functionName: functionCustomConfig, - times: 1, - invocationMode: 'SEQUENTIAL', - payload: payload, - }); - - const functionLogs = logs.map((log) => log.getFunctionLogs()); - - const idempotencyRecord = await dynamoDBClient.send( - new ScanCommand({ - TableName: tableNameCustomConfig, - }) - ); - expect(idempotencyRecord.Items?.[0]).toStrictEqual({ - customId: `${functionCustomConfig}#${payloadHash}`, - dataAttr: 'bar', - statusAttr: 'COMPLETED', - expiryAttr: expect.any(Number), - inProgressExpiryAttr: expect.any(Number), - }); - - expect(functionLogs[0]).toHaveLength(1); - expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( - expect.objectContaining({ - message: 'Processed event', - details: 'bar', - }) - ); - }, - TEST_CASE_TIMEOUT - ); + try { + // During the first invocation the handler should be called, so the logs should contain 1 log + expect(functionLogs[0]).toHaveLength(2); + expect(functionLogs[0][0]).toContain('Task timed out after'); + } catch { + // During the first invocation the function should timeout so the logs should not contain any log and the report log should contain a timeout message + expect(functionLogs[0]).toHaveLength(0); + expect(logs[0].getReportLog()).toMatch(/Status: timeout$/); + } - it( - 'takes the data index argument into account when making the function idempotent', - async () => { - const payload = [{ id: '1234' }, { id: '5678' }]; - const payloadHash = createHash('md5') - .update(JSON.stringify('bar')) - .digest('base64'); - - const logs = await invokeFunction({ - functionName: functionNameDataIndex, - times: 2, - invocationMode: 'SEQUENTIAL', - payload: payload, - }); - - const functionLogs = logs.map((log) => log.getFunctionLogs()); - - const idempotencyRecord = await dynamoDBClient.send( - new ScanCommand({ - TableName: tableNameDataIndex, + expect(functionLogs[1]).toHaveLength(1); + expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( + expect.objectContaining({ + message: 'Processed event', + details: 'bar', + function_name: functionNameTimeout, + }) + ); + }); + + it('recovers from an expired idempotency record and processes the next request', async () => { + const payload = { + foo: 'baz', + }; + const payloadHash = createHash('md5') + .update(JSON.stringify(payload.foo)) + .digest('base64'); + + // Act + const logs = [ + ( + await invokeFunction({ + functionName: functionNameExpired, + times: 1, + invocationMode: 'SEQUENTIAL', + payload: { ...payload, invocation: 0 }, }) - ); - expect(idempotencyRecord.Items).toHaveLength(1); - expect(idempotencyRecord.Items?.[0].id).toEqual( - `${functionNameDataIndex}#${payloadHash}` - ); - expect(idempotencyRecord.Items?.[0].data).toEqual( - 'idempotent result: bar' - ); - expect(idempotencyRecord.Items?.[0].status).toEqual('COMPLETED'); - // During the first invocation the handler should be called, so the logs should contain 1 log - expect(functionLogs[0]).toHaveLength(1); - // We test the content of the log as well as the presence of fields from the context, this - // ensures that the all the arguments are passed to the handler when made idempotent - expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( - expect.objectContaining({ - message: 'Got test event', - id: '1234', - foo: 'bar', + )[0], + ]; + // Wait for the idempotency record to expire + await new Promise((resolve) => setTimeout(resolve, 2000)); + logs.push( + ( + await invokeFunction({ + functionName: functionNameExpired, + times: 1, + invocationMode: 'SEQUENTIAL', + payload: { ...payload, invocation: 1 }, }) - ); - }, - TEST_CASE_TIMEOUT - ); + )[0] + ); + const functionLogs = logs.map((log) => log.getFunctionLogs()); + + // Assess + const idempotencyRecords = await dynamoDBClient.send( + new ScanCommand({ + TableName: tableNameExpired, + }) + ); + expect(idempotencyRecords.Items).toHaveLength(1); + expect(idempotencyRecords.Items?.[0].id).toEqual( + `${functionNameExpired}#${payloadHash}` + ); + expect(idempotencyRecords.Items?.[0].data).toEqual({ + ...payload, + invocation: 1, + }); + expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); + + // Both invocations should be successful and the logs should contain 1 log each + expect(functionLogs[0]).toHaveLength(1); + expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( + expect.objectContaining({ + message: 'Processed event', + details: 'baz', + function_name: functionNameExpired, + }) + ); + // During the second invocation the handler should be called and complete, so the logs should + // contain 1 log + expect(functionLogs[1]).toHaveLength(1); + expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( + expect.objectContaining({ + message: 'Processed event', + details: 'baz', + function_name: functionNameExpired, + }) + ); + }); + + it('uses the provided custom idempotency record attributes', async () => { + const payload = { foo: 'bar' }; + const payloadHash = createHash('md5') + .update(JSON.stringify(payload)) + .digest('base64'); + const logs = await invokeFunction({ + functionName: functionCustomConfig, + times: 1, + invocationMode: 'SEQUENTIAL', + payload: payload, + }); + + const functionLogs = logs.map((log) => log.getFunctionLogs()); + + const idempotencyRecord = await dynamoDBClient.send( + new ScanCommand({ + TableName: tableNameCustomConfig, + }) + ); + expect(idempotencyRecord.Items?.[0]).toStrictEqual({ + customId: `${functionCustomConfig}#${payloadHash}`, + dataAttr: 'bar', + statusAttr: 'COMPLETED', + expiryAttr: expect.any(Number), + inProgressExpiryAttr: expect.any(Number), + }); + + expect(functionLogs[0]).toHaveLength(1); + expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( + expect.objectContaining({ + message: 'Processed event', + details: 'bar', + }) + ); + }); + + it('takes the data index argument into account when making the function idempotent', async () => { + const payload = [{ id: '1234' }, { id: '5678' }]; + const payloadHash = createHash('md5') + .update(JSON.stringify('bar')) + .digest('base64'); + + const logs = await invokeFunction({ + functionName: functionNameDataIndex, + times: 2, + invocationMode: 'SEQUENTIAL', + payload: payload, + }); + + const functionLogs = logs.map((log) => log.getFunctionLogs()); + + const idempotencyRecord = await dynamoDBClient.send( + new ScanCommand({ + TableName: tableNameDataIndex, + }) + ); + expect(idempotencyRecord.Items).toHaveLength(1); + expect(idempotencyRecord.Items?.[0].id).toEqual( + `${functionNameDataIndex}#${payloadHash}` + ); + expect(idempotencyRecord.Items?.[0].data).toEqual('idempotent result: bar'); + expect(idempotencyRecord.Items?.[0].status).toEqual('COMPLETED'); + // During the first invocation the handler should be called, so the logs should contain 1 log + expect(functionLogs[0]).toHaveLength(1); + // We test the content of the log as well as the presence of fields from the context, this + // ensures that the all the arguments are passed to the handler when made idempotent + expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( + expect.objectContaining({ + message: 'Got test event', + id: '1234', + foo: 'bar', + }) + ); + }); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts b/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts index f6a2cfbf63..9b92a56af8 100644 --- a/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts @@ -10,12 +10,7 @@ import { ScanCommand } from '@aws-sdk/lib-dynamodb'; import { Duration } from 'aws-cdk-lib'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { IdempotencyTestNodejsFunctionAndDynamoTable } from '../helpers/resources.js'; -import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, -} from './constants.js'; +import { RESOURCE_NAME_PREFIX } from './constants.js'; const ddb = new DynamoDBClient({}); @@ -110,255 +105,238 @@ describe('Idempotency E2E tests, middy middleware usage', () => { tableNameTimeout = testStack.findAndGetStackOutputValue('timeoutTable'); functionNameExpired = testStack.findAndGetStackOutputValue('expiredFn'); tableNameExpired = testStack.findAndGetStackOutputValue('expiredTable'); - }, SETUP_TIMEOUT); - - it( - 'returns the same result and runs the handler once when called multiple times', - async () => { - // Prepare - const payload = { - foo: 'bar', - }; - const payloadHash = createHash('md5') - .update(JSON.stringify(payload)) - .digest('base64'); + }); - // Act - const logs = await invokeFunction({ - functionName: functionNameDefault, - times: 2, - invocationMode: 'SEQUENTIAL', - payload, - }); - const functionLogs = logs.map((log) => log.getFunctionLogs()); + it('returns the same result and runs the handler once when called multiple times', async () => { + // Prepare + const payload = { + foo: 'bar', + }; + const payloadHash = createHash('md5') + .update(JSON.stringify(payload)) + .digest('base64'); - // Assess - const idempotencyRecords = await ddb.send( - new ScanCommand({ - TableName: tableNameDefault, - }) - ); - expect(idempotencyRecords.Items?.length).toEqual(1); - expect(idempotencyRecords.Items?.[0].id).toEqual( - `${functionNameDefault}#${payloadHash}` - ); - expect(idempotencyRecords.Items?.[0].data).toEqual('bar'); - expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); + // Act + const logs = await invokeFunction({ + functionName: functionNameDefault, + times: 2, + invocationMode: 'SEQUENTIAL', + payload, + }); + const functionLogs = logs.map((log) => log.getFunctionLogs()); - // During the first invocation the handler should be called, so the logs should contain 1 log - expect(functionLogs[0]).toHaveLength(1); - // We test the content of the log as well as the presence of fields from the context, this - // ensures that the all the arguments are passed to the handler when made idempotent - expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( - expect.objectContaining({ - message: 'foo', - details: 'bar', - function_name: functionNameDefault, - }) - ); - // During the second invocation the handler should not be called, so the logs should be empty - expect(functionLogs[1]).toHaveLength(0); - }, - TEST_CASE_TIMEOUT - ); + // Assess + const idempotencyRecords = await ddb.send( + new ScanCommand({ + TableName: tableNameDefault, + }) + ); + expect(idempotencyRecords.Items?.length).toEqual(1); + expect(idempotencyRecords.Items?.[0].id).toEqual( + `${functionNameDefault}#${payloadHash}` + ); + expect(idempotencyRecords.Items?.[0].data).toEqual('bar'); + expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); - it( - 'handles parallel invocations correctly', - async () => { - // Prepare - const payload = { - foo: 'bar', - }; - const payloadHash = createHash('md5') - .update(JSON.stringify(payload)) - .digest('base64'); + // During the first invocation the handler should be called, so the logs should contain 1 log + expect(functionLogs[0]).toHaveLength(1); + // We test the content of the log as well as the presence of fields from the context, this + // ensures that the all the arguments are passed to the handler when made idempotent + expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( + expect.objectContaining({ + message: 'foo', + details: 'bar', + function_name: functionNameDefault, + }) + ); + // During the second invocation the handler should not be called, so the logs should be empty + expect(functionLogs[1]).toHaveLength(0); + }); - // Act - const logs = await invokeFunction({ - functionName: functionNameDefaultParallel, - times: 2, - invocationMode: 'PARALLEL', - payload, - }); - const functionLogs = logs.map((log) => log.getFunctionLogs()); + it('handles parallel invocations correctly', async () => { + // Prepare + const payload = { + foo: 'bar', + }; + const payloadHash = createHash('md5') + .update(JSON.stringify(payload)) + .digest('base64'); - // Assess - const idempotencyRecords = await ddb.send( - new ScanCommand({ - TableName: tableNameDefaultParallel, - }) - ); - expect(idempotencyRecords.Items?.length).toEqual(1); - expect(idempotencyRecords.Items?.[0].id).toEqual( - `${functionNameDefaultParallel}#${payloadHash}` - ); - expect(idempotencyRecords.Items?.[0].data).toEqual('bar'); - expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); + // Act + const logs = await invokeFunction({ + functionName: functionNameDefaultParallel, + times: 2, + invocationMode: 'PARALLEL', + payload, + }); + const functionLogs = logs.map((log) => log.getFunctionLogs()); - /** - * Since the requests are sent in parallel we don't know which one will be processed first, - * however we expect that only on of them will be processed by the handler, while the other - * one will be rejected with IdempotencyAlreadyInProgressError. - * - * We filter the logs to find which one was successful and which one failed, then we check - * that they contain the expected logs. - */ - const successfulInvocationLogs = functionLogs.find( - (functionLog) => - functionLog.find((log) => log.includes('Processed event')) !== - undefined - ); - const failedInvocationLogs = functionLogs.find( - (functionLog) => - functionLog.find((log) => - log.includes('There is already an execution in progress') - ) !== undefined - ); - expect(successfulInvocationLogs).toHaveLength(1); - expect(failedInvocationLogs).toHaveLength(1); - }, - TEST_CASE_TIMEOUT - ); + // Assess + const idempotencyRecords = await ddb.send( + new ScanCommand({ + TableName: tableNameDefaultParallel, + }) + ); + expect(idempotencyRecords.Items?.length).toEqual(1); + expect(idempotencyRecords.Items?.[0].id).toEqual( + `${functionNameDefaultParallel}#${payloadHash}` + ); + expect(idempotencyRecords.Items?.[0].data).toEqual('bar'); + expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); - it( - 'recovers from a timed out request and processes the next one', - async () => { - // Prepare - const payload = { - foo: 'bar', - }; - const payloadHash = createHash('md5') - .update(JSON.stringify(payload.foo)) - .digest('base64'); + /** + * Since the requests are sent in parallel we don't know which one will be processed first, + * however we expect that only on of them will be processed by the handler, while the other + * one will be rejected with IdempotencyAlreadyInProgressError. + * + * We filter the logs to find which one was successful and which one failed, then we check + * that they contain the expected logs. + */ + const successfulInvocationLogs = functionLogs.find( + (functionLog) => + functionLog.find((log) => log.includes('Processed event')) !== undefined + ); + const failedInvocationLogs = functionLogs.find( + (functionLog) => + functionLog.find((log) => + log.includes('There is already an execution in progress') + ) !== undefined + ); + expect(successfulInvocationLogs).toHaveLength(1); + expect(failedInvocationLogs).toHaveLength(1); + }); - // Act - const logs = await invokeFunction({ - functionName: functionNameTimeout, - times: 2, - invocationMode: 'SEQUENTIAL', - payload: Array.from({ length: 2 }, (_, index) => ({ - ...payload, - invocation: index, - })), - }); - const functionLogs = logs.map((log) => log.getFunctionLogs()); + it('recovers from a timed out request and processes the next one', async () => { + // Prepare + const payload = { + foo: 'bar', + }; + const payloadHash = createHash('md5') + .update(JSON.stringify(payload.foo)) + .digest('base64'); - // Assess - const idempotencyRecords = await ddb.send( - new ScanCommand({ - TableName: tableNameTimeout, - }) - ); - expect(idempotencyRecords.Items?.length).toEqual(1); - expect(idempotencyRecords.Items?.[0].id).toEqual( - `${functionNameTimeout}#${payloadHash}` - ); - expect(idempotencyRecords.Items?.[0].data).toEqual({ + // Act + const logs = await invokeFunction({ + functionName: functionNameTimeout, + times: 2, + invocationMode: 'SEQUENTIAL', + payload: Array.from({ length: 2 }, (_, index) => ({ ...payload, - invocation: 1, - }); - expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); - - try { - // During the first invocation the handler should be called, so the logs should contain 1 log - expect(functionLogs[0]).toHaveLength(2); - expect(functionLogs[0][0]).toContain('Task timed out after'); - } catch { - // During the first invocation the function should timeout so the logs should not contain any log and the report log should contain a timeout message - expect(functionLogs[0]).toHaveLength(0); - expect(logs[0].getReportLog()).toMatch(/Status: timeout$/); - } + invocation: index, + })), + }); + const functionLogs = logs.map((log) => log.getFunctionLogs()); - // During the second invocation the handler should be called and complete, so the logs should - // contain 1 log - expect(functionLogs[1]).toHaveLength(1); - expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( - expect.objectContaining({ - message: 'Processed event', - details: 'bar', - function_name: functionNameTimeout, - }) - ); - }, - TEST_CASE_TIMEOUT - ); + // Assess + const idempotencyRecords = await ddb.send( + new ScanCommand({ + TableName: tableNameTimeout, + }) + ); + expect(idempotencyRecords.Items?.length).toEqual(1); + expect(idempotencyRecords.Items?.[0].id).toEqual( + `${functionNameTimeout}#${payloadHash}` + ); + expect(idempotencyRecords.Items?.[0].data).toEqual({ + ...payload, + invocation: 1, + }); + expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); - it( - 'recovers from an expired idempotency record and processes the next request', - async () => { - // Prepare - const payload = { - foo: 'bar', - }; - const payloadHash = createHash('md5') - .update(JSON.stringify(payload.foo)) - .digest('base64'); + try { + // During the first invocation the handler should be called, so the logs should contain 1 log + expect(functionLogs[0]).toHaveLength(2); + expect(functionLogs[0][0]).toContain('Task timed out after'); + } catch { + // During the first invocation the function should timeout so the logs should not contain any log and the report log should contain a timeout message + expect(functionLogs[0]).toHaveLength(0); + expect(logs[0].getReportLog()).toMatch(/Status: timeout$/); + } - // Act - const logs = [ - ( - await invokeFunction({ - functionName: functionNameExpired, - times: 1, - invocationMode: 'SEQUENTIAL', - payload: { ...payload, invocation: 0 }, - }) - )[0], - ]; - // Wait for the idempotency record to expire - await new Promise((resolve) => setTimeout(resolve, 2000)); - logs.push( - ( - await invokeFunction({ - functionName: functionNameExpired, - times: 1, - invocationMode: 'SEQUENTIAL', - payload: { ...payload, invocation: 1 }, - }) - )[0] - ); - const functionLogs = logs.map((log) => log.getFunctionLogs()); + // During the second invocation the handler should be called and complete, so the logs should + // contain 1 log + expect(functionLogs[1]).toHaveLength(1); + expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( + expect.objectContaining({ + message: 'Processed event', + details: 'bar', + function_name: functionNameTimeout, + }) + ); + }); - // Assess - const idempotencyRecords = await ddb.send( - new ScanCommand({ - TableName: tableNameExpired, - }) - ); - expect(idempotencyRecords.Items?.length).toEqual(1); - expect(idempotencyRecords.Items?.[0].id).toEqual( - `${functionNameExpired}#${payloadHash}` - ); - expect(idempotencyRecords.Items?.[0].data).toEqual({ - ...payload, - invocation: 1, - }); - expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); + it('recovers from an expired idempotency record and processes the next request', async () => { + // Prepare + const payload = { + foo: 'bar', + }; + const payloadHash = createHash('md5') + .update(JSON.stringify(payload.foo)) + .digest('base64'); - // Both invocations should be successful and the logs should contain 1 log each - expect(functionLogs[0]).toHaveLength(1); - expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( - expect.objectContaining({ - message: 'Processed event', - details: 'bar', - function_name: functionNameExpired, + // Act + const logs = [ + ( + await invokeFunction({ + functionName: functionNameExpired, + times: 1, + invocationMode: 'SEQUENTIAL', + payload: { ...payload, invocation: 0 }, }) - ); - // During the second invocation the handler should be called and complete, so the logs should - // contain 1 log - expect(functionLogs[1]).toHaveLength(1); - expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( - expect.objectContaining({ - message: 'Processed event', - details: 'bar', - function_name: functionNameExpired, + )[0], + ]; + // Wait for the idempotency record to expire + await new Promise((resolve) => setTimeout(resolve, 2000)); + logs.push( + ( + await invokeFunction({ + functionName: functionNameExpired, + times: 1, + invocationMode: 'SEQUENTIAL', + payload: { ...payload, invocation: 1 }, }) - ); - }, - TEST_CASE_TIMEOUT - ); + )[0] + ); + const functionLogs = logs.map((log) => log.getFunctionLogs()); + + // Assess + const idempotencyRecords = await ddb.send( + new ScanCommand({ + TableName: tableNameExpired, + }) + ); + expect(idempotencyRecords.Items?.length).toEqual(1); + expect(idempotencyRecords.Items?.[0].id).toEqual( + `${functionNameExpired}#${payloadHash}` + ); + expect(idempotencyRecords.Items?.[0].data).toEqual({ + ...payload, + invocation: 1, + }); + expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); + + // Both invocations should be successful and the logs should contain 1 log each + expect(functionLogs[0]).toHaveLength(1); + expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( + expect.objectContaining({ + message: 'Processed event', + details: 'bar', + function_name: functionNameExpired, + }) + ); + // During the second invocation the handler should be called and complete, so the logs should + // contain 1 log + expect(functionLogs[1]).toHaveLength(1); + expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( + expect.objectContaining({ + message: 'Processed event', + details: 'bar', + function_name: functionNameExpired, + }) + ); + }); afterAll(async () => { await testStack.destroy(); - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/packages/idempotency/tests/e2e/makeIdempotent.test.ts b/packages/idempotency/tests/e2e/makeIdempotent.test.ts index d510a0c893..c87bc9f6b7 100644 --- a/packages/idempotency/tests/e2e/makeIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeIdempotent.test.ts @@ -10,12 +10,7 @@ import { ScanCommand } from '@aws-sdk/lib-dynamodb'; import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { IdempotencyTestNodejsFunctionAndDynamoTable } from '../helpers/resources.js'; -import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, -} from './constants'; +import { RESOURCE_NAME_PREFIX } from './constants'; describe('Idempotency E2E tests, wrapper function usage', () => { const testStack = new TestStack({ @@ -99,239 +94,224 @@ describe('Idempotency E2E tests, wrapper function usage', () => { testStack.findAndGetStackOutputValue('handlerFn'); tableNameLambdaHandler = testStack.findAndGetStackOutputValue('handlerTable'); - }, SETUP_TIMEOUT); + }); - it( - 'when called twice with the same payload, it returns the same result', - async () => { - // Prepare - const payload = { - records: [ - { foo: 'bar', id: 1 }, - { foo: 'baz', id: 2 }, - { foo: 'bar', id: 1 }, - ], - }; - const payloadHashes = payload.records.map((record) => - createHash('md5').update(JSON.stringify(record)).digest('base64') - ); + it('when called twice with the same payload, it returns the same result', async () => { + // Prepare + const payload = { + records: [ + { foo: 'bar', id: 1 }, + { foo: 'baz', id: 2 }, + { foo: 'bar', id: 1 }, + ], + }; + const payloadHashes = payload.records.map((record) => + createHash('md5').update(JSON.stringify(record)).digest('base64') + ); - // Act - const logs = await invokeFunction({ - functionName: functionNameDefault, - times: 2, - invocationMode: 'SEQUENTIAL', - payload, - }); - const functionLogs = logs.map((log) => log.getFunctionLogs()); + // Act + const logs = await invokeFunction({ + functionName: functionNameDefault, + times: 2, + invocationMode: 'SEQUENTIAL', + payload, + }); + const functionLogs = logs.map((log) => log.getFunctionLogs()); - // Assess - const idempotencyRecords = await ddb.send( - new ScanCommand({ - TableName: tableNameDefault, - }) - ); - // Since records 1 and 3 have the same payload, only 2 records should be created - expect(idempotencyRecords?.Items?.length).toEqual(2); - const idempotencyRecordsItems = [ - idempotencyRecords.Items?.find( - (record) => record.id === `${functionNameDefault}#${payloadHashes[0]}` - ), - idempotencyRecords.Items?.find( - (record) => record.id === `${functionNameDefault}#${payloadHashes[1]}` - ), - ]; + // Assess + const idempotencyRecords = await ddb.send( + new ScanCommand({ + TableName: tableNameDefault, + }) + ); + // Since records 1 and 3 have the same payload, only 2 records should be created + expect(idempotencyRecords?.Items?.length).toEqual(2); + const idempotencyRecordsItems = [ + idempotencyRecords.Items?.find( + (record) => record.id === `${functionNameDefault}#${payloadHashes[0]}` + ), + idempotencyRecords.Items?.find( + (record) => record.id === `${functionNameDefault}#${payloadHashes[1]}` + ), + ]; - expect(idempotencyRecordsItems?.[0]).toStrictEqual({ - id: `${functionNameDefault}#${payloadHashes[0]}`, - data: 'Processing done: bar', - status: 'COMPLETED', - expiration: expect.any(Number), - in_progress_expiration: expect.any(Number), - }); + expect(idempotencyRecordsItems?.[0]).toStrictEqual({ + id: `${functionNameDefault}#${payloadHashes[0]}`, + data: 'Processing done: bar', + status: 'COMPLETED', + expiration: expect.any(Number), + in_progress_expiration: expect.any(Number), + }); - expect(idempotencyRecordsItems?.[1]).toStrictEqual({ - id: `${functionNameDefault}#${payloadHashes[1]}`, - data: 'Processing done: baz', - status: 'COMPLETED', - expiration: expect.any(Number), - in_progress_expiration: expect.any(Number), - }); + expect(idempotencyRecordsItems?.[1]).toStrictEqual({ + id: `${functionNameDefault}#${payloadHashes[1]}`, + data: 'Processing done: baz', + status: 'COMPLETED', + expiration: expect.any(Number), + in_progress_expiration: expect.any(Number), + }); - expect(functionLogs[0]).toHaveLength(2); - }, - TEST_CASE_TIMEOUT - ); + expect(functionLogs[0]).toHaveLength(2); + }); - it( - 'creates a DynamoDB item with the correct attributes', - async () => { - // Prepare - const payload = { - records: [ - { foo: 'bar', id: 1 }, - { foo: 'baq', id: 2 }, - { foo: 'bar', id: 3 }, - ], - }; - const payloadHashes = payload.records.map((record) => - createHash('md5').update(JSON.stringify(record)).digest('base64') - ); - const validationHashes = payload.records.map((record) => - createHash('md5').update(JSON.stringify(record.foo)).digest('base64') - ); + it('creates a DynamoDB item with the correct attributes', async () => { + // Prepare + const payload = { + records: [ + { foo: 'bar', id: 1 }, + { foo: 'baq', id: 2 }, + { foo: 'bar', id: 3 }, + ], + }; + const payloadHashes = payload.records.map((record) => + createHash('md5').update(JSON.stringify(record)).digest('base64') + ); + const validationHashes = payload.records.map((record) => + createHash('md5').update(JSON.stringify(record.foo)).digest('base64') + ); - // Act - const logs = await invokeFunction({ - functionName: functionNameCustomConfig, - times: 2, - invocationMode: 'SEQUENTIAL', - payload, - }); - const functionLogs = logs.map((log) => log.getFunctionLogs()); + // Act + const logs = await invokeFunction({ + functionName: functionNameCustomConfig, + times: 2, + invocationMode: 'SEQUENTIAL', + payload, + }); + const functionLogs = logs.map((log) => log.getFunctionLogs()); - // Assess - const idempotencyRecords = await ddb.send( - new ScanCommand({ - TableName: tableNameCustomConfig, - }) - ); - /** - * Each record should have a corresponding entry in the persistence store, - * if so then we retrieve the records based on their custom IDs - * The records are retrieved in the same order as the payload records. - */ - expect(idempotencyRecords.Items?.length).toEqual(3); - const idempotencyRecordsItems = [ - idempotencyRecords.Items?.find( - (record) => - record.customId === - `${functionNameCustomConfig}#${payloadHashes[0]}` - ), - idempotencyRecords.Items?.find( - (record) => - record.customId === - `${functionNameCustomConfig}#${payloadHashes[1]}` - ), - idempotencyRecords.Items?.find( - (record) => - record.customId === - `${functionNameCustomConfig}#${payloadHashes[2]}` - ), - ]; + // Assess + const idempotencyRecords = await ddb.send( + new ScanCommand({ + TableName: tableNameCustomConfig, + }) + ); + /** + * Each record should have a corresponding entry in the persistence store, + * if so then we retrieve the records based on their custom IDs + * The records are retrieved in the same order as the payload records. + */ + expect(idempotencyRecords.Items?.length).toEqual(3); + const idempotencyRecordsItems = [ + idempotencyRecords.Items?.find( + (record) => + record.customId === `${functionNameCustomConfig}#${payloadHashes[0]}` + ), + idempotencyRecords.Items?.find( + (record) => + record.customId === `${functionNameCustomConfig}#${payloadHashes[1]}` + ), + idempotencyRecords.Items?.find( + (record) => + record.customId === `${functionNameCustomConfig}#${payloadHashes[2]}` + ), + ]; - expect(idempotencyRecordsItems?.[0]).toStrictEqual({ - customId: `${functionNameCustomConfig}#${payloadHashes[0]}`, - dataAttr: payload.records[0], - statusAttr: 'COMPLETED', - expiryAttr: expect.any(Number), - inProgressExpiryAttr: expect.any(Number), - validationKeyAttr: validationHashes[0], - }); + expect(idempotencyRecordsItems?.[0]).toStrictEqual({ + customId: `${functionNameCustomConfig}#${payloadHashes[0]}`, + dataAttr: payload.records[0], + statusAttr: 'COMPLETED', + expiryAttr: expect.any(Number), + inProgressExpiryAttr: expect.any(Number), + validationKeyAttr: validationHashes[0], + }); - expect(idempotencyRecordsItems?.[1]).toStrictEqual({ - customId: `${functionNameCustomConfig}#${payloadHashes[1]}`, - dataAttr: payload.records[1], - statusAttr: 'COMPLETED', - expiryAttr: expect.any(Number), - inProgressExpiryAttr: expect.any(Number), - validationKeyAttr: validationHashes[1], - }); + expect(idempotencyRecordsItems?.[1]).toStrictEqual({ + customId: `${functionNameCustomConfig}#${payloadHashes[1]}`, + dataAttr: payload.records[1], + statusAttr: 'COMPLETED', + expiryAttr: expect.any(Number), + inProgressExpiryAttr: expect.any(Number), + validationKeyAttr: validationHashes[1], + }); - expect(idempotencyRecordsItems?.[2]).toStrictEqual({ - customId: `${functionNameCustomConfig}#${payloadHashes[2]}`, - dataAttr: payload.records[2], - statusAttr: 'COMPLETED', - expiryAttr: expect.any(Number), - inProgressExpiryAttr: expect.any(Number), - validationKeyAttr: validationHashes[2], - }); + expect(idempotencyRecordsItems?.[2]).toStrictEqual({ + customId: `${functionNameCustomConfig}#${payloadHashes[2]}`, + dataAttr: payload.records[2], + statusAttr: 'COMPLETED', + expiryAttr: expect.any(Number), + inProgressExpiryAttr: expect.any(Number), + validationKeyAttr: validationHashes[2], + }); - // During the first invocation, the processing function should have been called 3 times (once for each record) - expect(functionLogs[0]).toHaveLength(3); - expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( - expect.objectContaining({ - baz: 0, // index of recursion in handler, assess that all function arguments are preserved - record: payload.records[0], - message: 'Got test event', - }) - ); - expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][1])).toEqual( - expect.objectContaining({ - baz: 1, - record: payload.records[1], - message: 'Got test event', - }) - ); - expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][2])).toEqual( - expect.objectContaining({ - baz: 2, - record: payload.records[2], - message: 'Got test event', - }) - ); + // During the first invocation, the processing function should have been called 3 times (once for each record) + expect(functionLogs[0]).toHaveLength(3); + expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( + expect.objectContaining({ + baz: 0, // index of recursion in handler, assess that all function arguments are preserved + record: payload.records[0], + message: 'Got test event', + }) + ); + expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][1])).toEqual( + expect.objectContaining({ + baz: 1, + record: payload.records[1], + message: 'Got test event', + }) + ); + expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][2])).toEqual( + expect.objectContaining({ + baz: 2, + record: payload.records[2], + message: 'Got test event', + }) + ); - // During the second invocation, the processing function should have been called 0 times (all records are idempotent) - expect(functionLogs[1]).toHaveLength(0); - }, - TEST_CASE_TIMEOUT - ); + // During the second invocation, the processing function should have been called 0 times (all records are idempotent) + expect(functionLogs[1]).toHaveLength(0); + }); - it( - 'calls the wrapped function once and always returns the same result when called multiple times', - async () => { - // Prepare - const payload = { - body: JSON.stringify({ - foo: 'bar', - }), - }; - const payloadHash = createHash('md5') - .update(JSON.stringify('bar')) - .digest('base64'); + it('calls the wrapped function once and always returns the same result when called multiple times', async () => { + // Prepare + const payload = { + body: JSON.stringify({ + foo: 'bar', + }), + }; + const payloadHash = createHash('md5') + .update(JSON.stringify('bar')) + .digest('base64'); - // Act - const logs = await invokeFunction({ - functionName: functionNameLambdaHandler, - times: 2, - invocationMode: 'SEQUENTIAL', - payload, - }); - const functionLogs = logs.map((log) => log.getFunctionLogs()); + // Act + const logs = await invokeFunction({ + functionName: functionNameLambdaHandler, + times: 2, + invocationMode: 'SEQUENTIAL', + payload, + }); + const functionLogs = logs.map((log) => log.getFunctionLogs()); - // Assess - const idempotencyRecords = await ddb.send( - new ScanCommand({ - TableName: tableNameLambdaHandler, - }) - ); - expect(idempotencyRecords.Items?.length).toEqual(1); - expect(idempotencyRecords.Items?.[0].id).toEqual( - `${functionNameLambdaHandler}#${payloadHash}` - ); - expect(idempotencyRecords.Items?.[0].data).toEqual('bar'); - expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); + // Assess + const idempotencyRecords = await ddb.send( + new ScanCommand({ + TableName: tableNameLambdaHandler, + }) + ); + expect(idempotencyRecords.Items?.length).toEqual(1); + expect(idempotencyRecords.Items?.[0].id).toEqual( + `${functionNameLambdaHandler}#${payloadHash}` + ); + expect(idempotencyRecords.Items?.[0].data).toEqual('bar'); + expect(idempotencyRecords.Items?.[0].status).toEqual('COMPLETED'); - // During the first invocation the handler should be called, so the logs should contain 1 log - expect(functionLogs[0]).toHaveLength(1); - // We test the content of the log as well as the presence of fields from the context, this - // ensures that the all the arguments are passed to the handler when made idempotent - expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( - expect.objectContaining({ - message: 'foo', - details: 'bar', - function_name: functionNameLambdaHandler, - }) - ); - // During the second invocation the handler should not be called, so the logs should be empty - expect(functionLogs[1]).toHaveLength(0); - }, - TEST_CASE_TIMEOUT - ); + // During the first invocation the handler should be called, so the logs should contain 1 log + expect(functionLogs[0]).toHaveLength(1); + // We test the content of the log as well as the presence of fields from the context, this + // ensures that the all the arguments are passed to the handler when made idempotent + expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( + expect.objectContaining({ + message: 'foo', + details: 'bar', + function_name: functionNameLambdaHandler, + }) + ); + // During the second invocation the handler should not be called, so the logs should be empty + expect(functionLogs[1]).toHaveLength(0); + }); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts index c64e3c10f7..22f574ac53 100644 --- a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts @@ -49,25 +49,46 @@ describe('Class IdempotencyHandler', () => { }); describe('Method: determineResultFromIdempotencyRecord', () => { - it('throws when the record is in progress and within expiry window', async () => { - // Prepare - const stubRecord = new IdempotencyRecord({ - idempotencyKey: 'idempotencyKey', - expiryTimestamp: Date.now() + 1000, // should be in the future - inProgressExpiryTimestamp: 0, // less than current time in milliseconds - responseData: { responseData: 'responseData' }, - payloadHash: 'payloadHash', - status: IdempotencyRecordStatus.INPROGRESS, - }); - - // Act & Assess - expect(stubRecord.isExpired()).toBe(false); - expect(stubRecord.getStatus()).toBe(IdempotencyRecordStatus.INPROGRESS); - expect(() => - idempotentHandler.determineResultFromIdempotencyRecord(stubRecord) - ).toThrow(IdempotencyAlreadyInProgressError); - expect(mockResponseHook).not.toHaveBeenCalled(); - }); + it.each([ + { + keys: { + idempotencyKey: 'idempotencyKey', + sortKey: 'sk', + }, + expectedErrorMsg: + 'There is already an execution in progress with idempotency key: idempotencyKey and sort key: sk', + case: 'pk & sk', + }, + { + keys: { + idempotencyKey: 'idempotencyKey', + }, + expectedErrorMsg: + 'There is already an execution in progress with idempotency key: idempotencyKey', + case: 'pk only', + }, + ])( + 'throws when the record is in progress and within expiry window ($case)', + async ({ keys, expectedErrorMsg }) => { + // Prepare + const stubRecord = new IdempotencyRecord({ + ...keys, + expiryTimestamp: Date.now() + 1000, // should be in the future + inProgressExpiryTimestamp: 0, // less than current time in milliseconds + responseData: { responseData: 'responseData' }, + payloadHash: 'payloadHash', + status: IdempotencyRecordStatus.INPROGRESS, + }); + + // Act & Assess + expect(stubRecord.isExpired()).toBe(false); + expect(stubRecord.getStatus()).toBe(IdempotencyRecordStatus.INPROGRESS); + expect(() => + idempotentHandler.determineResultFromIdempotencyRecord(stubRecord) + ).toThrow(new IdempotencyAlreadyInProgressError(expectedErrorMsg)); + expect(mockResponseHook).not.toHaveBeenCalled(); + } + ); it('throws when the record is in progress and outside expiry window', async () => { // Prepare diff --git a/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts b/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts index 51b893caab..bb72789986 100644 --- a/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts +++ b/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts @@ -346,41 +346,75 @@ describe('Class: DynamoDBPersistenceLayer', () => { persistenceLayerSpy.mockRestore(); }); - it('throws when called with a record that fails any condition', async () => { - // Prepare - const record = new IdempotencyRecord({ - idempotencyKey: dummyKey, - status: IdempotencyRecordStatus.EXPIRED, - expiryTimestamp: 0, - }); - const expiration = Date.now(); - client.on(PutItemCommand).rejects( - new ConditionalCheckFailedException({ - $metadata: { - httpStatusCode: 400, - requestId: 'someRequestId', - }, - message: 'Conditional check failed', - Item: { - id: { S: 'test-key' }, - status: { S: 'INPROGRESS' }, - expiration: { N: expiration.toString() }, - }, - }) - ); - - // Act & Assess - await expect(persistenceLayer._putRecord(record)).rejects.toThrowError( - new IdempotencyItemAlreadyExistsError( - `Failed to put record for already existing idempotency key: ${record.idempotencyKey}`, - new IdempotencyRecord({ - idempotencyKey: 'test-key', - status: IdempotencyRecordStatus.INPROGRESS, - expiryTimestamp: expiration, + it.each([ + { + keys: { + id: 'idempotency#my-lambda-function', + sortKey: dummyKey, + }, + case: 'composite key', + }, + { + keys: { + id: dummyKey, + }, + case: 'single key', + }, + ])( + 'throws when called with a record that fails any condition ($case)', + async ({ keys }) => { + // Prepare + const { id, sortKey } = keys; + + const record = new IdempotencyRecord({ + idempotencyKey: id, + sortKey, + status: IdempotencyRecordStatus.EXPIRED, + expiryTimestamp: 0, + }); + const expiration = Date.now(); + client.on(PutItemCommand).rejects( + new ConditionalCheckFailedException({ + $metadata: { + httpStatusCode: 400, + requestId: 'someRequestId', + }, + message: 'Conditional check failed', + Item: { + id: { S: 'test-key' }, + ...(sortKey ? { sortKey: { S: sortKey } } : {}), + status: { S: 'INPROGRESS' }, + expiration: { N: expiration.toString() }, + }, }) - ) - ); - }); + ); + const testPersistenceLayer = sortKey + ? new DynamoDBPersistenceLayerTestClass({ + tableName: dummyTableName, + sortKeyAttr: 'sortKey', + }) + : persistenceLayer; + + // Act & Assess + await expect( + testPersistenceLayer._putRecord(record) + ).rejects.toThrowError( + new IdempotencyItemAlreadyExistsError( + `Failed to put record for already existing idempotency key: ${ + sortKey + ? `${record.idempotencyKey} and sort key: ${sortKey}` + : record.idempotencyKey + }`, + new IdempotencyRecord({ + idempotencyKey: 'test-key', + sortKey, + status: IdempotencyRecordStatus.INPROGRESS, + expiryTimestamp: expiration, + }) + ) + ); + } + ); it('throws when encountering an unknown error', async () => { // Prepare @@ -445,7 +479,7 @@ describe('Class: DynamoDBPersistenceLayer', () => { ); }); - it('it builds the request correctly when using composite keys', async () => { + it('builds the request correctly when using composite keys', async () => { // Prepare const persistenceLayer = new DynamoDBPersistenceLayerTestClass({ tableName: dummyTableName, diff --git a/packages/idempotency/vitest.config.ts b/packages/idempotency/vitest.config.ts index 9f1196ef1f..baa5cf7463 100644 --- a/packages/idempotency/vitest.config.ts +++ b/packages/idempotency/vitest.config.ts @@ -4,5 +4,7 @@ export default defineProject({ test: { environment: 'node', setupFiles: ['../testing/src/setupEnv.ts'], + hookTimeout: 1_000 * 60 * 10, // 10 minutes + testTimeout: 1_000 * 60 * 3, // 3 minutes }, }); diff --git a/packages/jmespath/CHANGELOG.md b/packages/jmespath/CHANGELOG.md index 8852bd79c3..ad70cf1c3a 100644 --- a/packages/jmespath/CHANGELOG.md +++ b/packages/jmespath/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + +**Note:** Version bump only for package @aws-lambda-powertools/jmespath + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) **Note:** Version bump only for package @aws-lambda-powertools/jmespath diff --git a/packages/jmespath/README.md b/packages/jmespath/README.md index f7dcad568f..490fcb40aa 100644 --- a/packages/jmespath/README.md +++ b/packages/jmespath/README.md @@ -202,6 +202,7 @@ The following companies, among others, use Powertools: - [Elva](https://elva-group.com) - [Flyweight](https://flyweight.io/) - [globaldatanet](https://globaldatanet.com/) +- [Guild](https://guild.com) - [Hashnode](https://hashnode.com/) - [LocalStack](https://localstack.cloud/) - [Perfect Post](https://www.perfectpost.fr) diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json index fb318b2552..11270fd688 100644 --- a/packages/jmespath/package.json +++ b/packages/jmespath/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/jmespath", - "version": "2.16.0", + "version": "2.17.0", "description": "A type safe and modern jmespath module to parse and extract data from JSON documents using JMESPath", "author": { "name": "Amazon Web Services", @@ -71,7 +71,7 @@ "lib" ], "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0" + "@aws-lambda-powertools/commons": "^2.17.0" }, "repository": { "type": "git", diff --git a/packages/logger/CHANGELOG.md b/packages/logger/CHANGELOG.md index 14e17740cd..5632170d87 100644 --- a/packages/logger/CHANGELOG.md +++ b/packages/logger/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + + +### Bug Fixes + +* **logger:** correctly refresh sample rate ([#3722](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3722)) ([2692ca4](https://github.com/aws-powertools/powertools-lambda-typescript/commit/2692ca4d1b15763936659b05e1830d998a4d2020)) + + +### Features + +* **commons:** make utilities aware of provisioned concurrency ([#3724](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3724)) ([c28e45e](https://github.com/aws-powertools/powertools-lambda-typescript/commit/c28e45ecba315bac8fbc7744dbe21a3461747d44)) +* **logger:** set correlation ID in logs ([#3726](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3726)) ([aa74fc8](https://github.com/aws-powertools/powertools-lambda-typescript/commit/aa74fc8548ccb8cb313ffd1742184c66e8d6c22c)) + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) diff --git a/packages/logger/README.md b/packages/logger/README.md index 9c563c9547..a654fa63c4 100644 --- a/packages/logger/README.md +++ b/packages/logger/README.md @@ -243,6 +243,7 @@ The following companies, among others, use Powertools: - [Elva](https://elva-group.com) - [Flyweight](https://flyweight.io/) - [globaldatanet](https://globaldatanet.com/) +- [Guild](https://guild.com) - [Hashnode](https://hashnode.com/) - [LocalStack](https://localstack.cloud/) - [Perfect Post](https://www.perfectpost.fr) diff --git a/packages/logger/package.json b/packages/logger/package.json index 544314ce05..3175cbe001 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/logger", - "version": "2.16.0", + "version": "2.17.0", "description": "The logging package for the Powertools for AWS Lambda (TypeScript) library", "author": { "name": "Amazon Web Services", @@ -47,6 +47,10 @@ "./types": { "import": "./lib/esm/types/index.js", "require": "./lib/cjs/types/index.js" + }, + "./correlationId": { + "import": "./lib/esm/correlationId.js", + "require": "./lib/cjs/correlationId.js" } }, "typesVersions": { @@ -58,6 +62,10 @@ "types": [ "lib/cjs/types/index.d.ts", "lib/esm/types/index.d.ts" + ], + "correlationId": [ + "lib/cjs/correlationId.d.ts", + "lib/esm/correlationId.d.ts" ] } }, @@ -68,11 +76,15 @@ "@types/lodash.merge": "^4.6.9" }, "peerDependencies": { + "@aws-lambda-powertools/jmespath": "2.x", "@middy/core": "4.x || 5.x || 6.x" }, "peerDependenciesMeta": { "@middy/core": { "optional": true + }, + "@aws-lambda-powertools/jmespath": { + "optional": true } }, "files": [ @@ -86,7 +98,7 @@ "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" }, "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0", + "@aws-lambda-powertools/commons": "^2.17.0", "lodash.merge": "^4.6.2" }, "keywords": [ diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index f39bb8ef0e..f1217a1e6f 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -140,7 +140,9 @@ class Logger extends Utility implements LoggerInterface { /** * Standard attributes managed by Powertools that will be logged in all log items. */ - private powertoolsLogData: PowertoolsLogData = {}; + private powertoolsLogData: PowertoolsLogData = { + sampleRateValue: 0, + }; /** * Temporary log attributes that can be appended with `appendKeys()` method. */ @@ -213,6 +215,27 @@ class Logger extends Utility implements LoggerInterface { */ #buffer?: CircularMap; + /** + * Search function for the correlation ID. + */ + #correlationIdSearchFn?: (expression: string, data: unknown) => unknown; + + /** + * The debug sampling rate configuration. + */ + readonly #debugLogSampling = { + /** + * The sampling rate value used to determine if the log level should be set to DEBUG. + */ + sampleRateValue: 0, + /** + * The number of times the debug sampling rate has been refreshed. + * + * We use this to determine if we should refresh it again. + */ + refreshedTimes: 0, + }; + /** * Log level used by the current instance of Logger. * @@ -302,12 +325,13 @@ class Logger extends Utility implements LoggerInterface { { logLevel: this.getLevelName(), serviceName: this.powertoolsLogData.serviceName, - sampleRateValue: this.powertoolsLogData.sampleRateValue, + sampleRateValue: this.#debugLogSampling.sampleRateValue, logFormatter: this.getLogFormatter(), customConfigService: this.getCustomConfigService(), environment: this.powertoolsLogData.environment, persistentLogAttributes: this.persistentLogAttributes, jsonReplacerFn: this.#jsonReplacerFn, + correlationIdSearchFn: this.#correlationIdSearchFn, ...(this.#bufferConfig.enabled && { logBufferOptions: { maxBytes: this.#bufferConfig.maxBytes, @@ -462,6 +486,9 @@ class Logger extends Utility implements LoggerInterface { loggerRef.refreshSampleRateCalculation(); loggerRef.addContext(context); loggerRef.logEventIfEnabled(event, options?.logEvent); + if (options?.correlationIdPath) { + loggerRef.setCorrelationId(event, options?.correlationIdPath); + } try { return await originalMethod.apply(this, [event, context, callback]); @@ -547,8 +574,18 @@ class Logger extends Utility implements LoggerInterface { * This only works for warm starts, because we don't to avoid double sampling. */ public refreshSampleRateCalculation(): void { - if (!this.coldStart) { - this.setInitialSampleRate(this.powertoolsLogData.sampleRateValue); + if (this.#debugLogSampling.refreshedTimes === 0) { + this.#debugLogSampling.refreshedTimes++; + return; + } + if ( + this.#shouldEnableDebugSampling() && + this.logLevel > LogLevelThreshold.TRACE + ) { + this.setLogLevel('DEBUG'); + this.debug('Setting log level to DEBUG due to sampling rate'); + } else { + this.setLogLevel(this.getLogLevelNameFromNumber(this.#initialLogLevel)); } } @@ -891,6 +928,16 @@ class Logger extends Utility implements LoggerInterface { } } + /** + * Make a new debug log sampling decision based on the sample rate value. + */ + #shouldEnableDebugSampling() { + return ( + this.#debugLogSampling.sampleRateValue && + randomInt(0, 100) / 100 <= this.#debugLogSampling.sampleRateValue + ); + } + /** * Check if a given key is reserved and warn the user if it is. * @@ -1142,30 +1189,24 @@ class Logger extends Utility implements LoggerInterface { * @param sampleRateValue - The sample rate value */ private setInitialSampleRate(sampleRateValue?: number): void { - this.powertoolsLogData.sampleRateValue = 0; const constructorValue = sampleRateValue; const customConfigValue = this.getCustomConfigService()?.getSampleRateValue(); const envVarsValue = this.getEnvVarsService().getSampleRateValue(); for (const value of [constructorValue, customConfigValue, envVarsValue]) { if (this.isValidSampleRate(value)) { + this.#debugLogSampling.sampleRateValue = value; this.powertoolsLogData.sampleRateValue = value; if ( - this.logLevel > LogLevelThreshold.DEBUG && - value && - randomInt(0, 100) / 100 <= value + this.#shouldEnableDebugSampling() && + this.logLevel > LogLevelThreshold.TRACE ) { - // only change logLevel if higher than debug, i.e. don't change from e.g. tracing to debug this.setLogLevel('DEBUG'); this.debug('Setting log level to DEBUG due to sampling rate'); - } else { - this.setLogLevel( - this.getLogLevelNameFromNumber(this.#initialLogLevel) - ); } - return; + break; } } } @@ -1229,6 +1270,7 @@ class Logger extends Utility implements LoggerInterface { jsonReplacerFn, logRecordOrder, logBufferOptions, + correlationIdSearchFn, } = options; if (persistentLogAttributes && persistentKeys) { @@ -1255,6 +1297,7 @@ class Logger extends Utility implements LoggerInterface { this.setLogIndentation(); this.#jsonReplacerFn = jsonReplacerFn; this.#setLogBuffering(logBufferOptions); + this.#correlationIdSearchFn = correlationIdSearchFn; return this; } @@ -1331,6 +1374,12 @@ class Logger extends Utility implements LoggerInterface { logLevel: number ): void { log.prepareForPrint(); + // This is the first time we see this traceId, so we need to clear the buffer + // from previous requests. This is ok because in AWS Lambda, the same sandbox + // environment can only ever be used by one request at a time. + if (this.#buffer?.has(xrayTraceId) === false) { + this.#buffer?.clear(); + } this.#buffer?.setItem( xrayTraceId, JSON.stringify( @@ -1381,7 +1430,6 @@ class Logger extends Utility implements LoggerInterface { /** * Empties the buffer for the current request - * */ public clearBuffer(): void { const traceId = this.envVarsService.getXrayTraceId(); @@ -1407,6 +1455,57 @@ class Logger extends Utility implements LoggerInterface { logLevel <= this.#bufferConfig.bufferAtVerbosity ); } + + /** + * Set the correlation ID for the log item. + * + * This method can be used to set the correlation ID for the log item or to search for the correlation ID in the event. + * + * @example + * ```typescript + * import { Logger } from '@aws-lambda-powertools/logger'; + * + * const logger = new Logger(); + * logger.setCorrelationId('my-correlation-id'); // sets the correlation ID directly with the first argument as value + * ``` + * + * ```typescript + * import { Logger } from '@aws-lambda-powertools/logger'; + * import { search } from '@aws-lambda-powertools/logger/correlationId'; + * + * const logger = new Logger({ correlationIdSearchFn: search }); + * logger.setCorrelationId(event, 'requestContext.requestId'); // sets the correlation ID from the event using JMSPath expression + * ``` + * + * @param value - The value to set as the correlation ID or the event to search for the correlation ID + * @param correlationIdPath - Optional JMESPath expression to extract the correlation ID for the payload + */ + public setCorrelationId(value: unknown, correlationIdPath?: string): void { + if (typeof correlationIdPath === 'string') { + if (!this.#correlationIdSearchFn) { + this.warn( + 'correlationIdPath is set but no search function was provided. The correlation ID will not be added to the log attributes.' + ); + return; + } + const correlationId = this.#correlationIdSearchFn( + correlationIdPath, + value + ); + if (correlationId) this.appendKeys({ correlation_id: correlationId }); + return; + } + + // If no correlationIdPath is provided, set the correlation ID directly + this.appendKeys({ correlation_id: value }); + } + + /** + * Get the correlation ID from the log attributes. + */ + public getCorrelationId(): unknown { + return this.temporaryLogAttributes.correlation_id; + } } export { Logger }; diff --git a/packages/logger/src/correlationId.ts b/packages/logger/src/correlationId.ts new file mode 100644 index 0000000000..1e9ab271ce --- /dev/null +++ b/packages/logger/src/correlationId.ts @@ -0,0 +1,60 @@ +import type { JSONObject } from '@aws-lambda-powertools/commons/types'; +import { search as JMESPathSearch } from '@aws-lambda-powertools/jmespath'; +import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions'; + +/** + * This function is used to search for a correlation ID in the event data and is a wrapper + * around the JMESPath search function. It allows you to specify a JMESPath expression + * to extract the correlation ID from the event data. + * @param expression - The JMESPath expression to use for searching the correlation ID. + * @param data - The event data to search in. + */ +const search = (expression: string, data: unknown) => { + return JMESPathSearch(expression, data as JSONObject, { + customFunctions: new PowertoolsFunctions(), + }); +}; + +/** + * The correlationPaths object contains the JMESPath expressions for extracting the correlation ID for various AWS services. + */ +const correlationPaths = { + /** + * API Gateway REST API request ID + */ + API_GATEWAY_REST: 'requestContext.requestId', + /** + * API Gateway HTTP API request ID + */ + API_GATEWAY_HTTP: 'requestContext.requestId', + /** + * AppSync API request ID + */ + APPSYNC_AUTHORIZER: 'requestContext.requestId', + /** + * AppSync resolver X-Ray trace ID + */ + APPSYNC_RESOLVER: 'request.headers."x-amzn-trace-id"', + /** + * ALB X-Ray trace ID + */ + APPLICATION_LOAD_BALANCER: 'headers."x-amzn-trace-id"', + /** + * EventBridge event ID + */ + EVENT_BRIDGE: 'id', + /** + * Lambda Function URL request ID + */ + LAMBDA_FUNCTION_URL: 'requestContext.requestId', + /** + * S3 Object trigger request ID + */ + S3_OBJECT_LAMBDA: 'xAmzRequestId', + /** + * VPC Lattice X-Ray trace ID + */ + VPC_LATTICE: 'headers."x-amzn-trace-id"', +} as const; + +export { correlationPaths, search }; diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index ae100eb930..f736cbff35 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -1,4 +1,4 @@ -export { Logger } from './Logger.js'; +export { LogLevel, LogLevelThreshold } from './constants.js'; export { LogFormatter } from './formatter/LogFormatter.js'; export { LogItem } from './formatter/LogItem.js'; -export { LogLevel, LogLevelThreshold } from './constants.js'; +export { Logger } from './Logger.js'; diff --git a/packages/logger/src/middleware/middy.ts b/packages/logger/src/middleware/middy.ts index f85b1af9f8..eeaef4fd49 100644 --- a/packages/logger/src/middleware/middy.ts +++ b/packages/logger/src/middleware/middy.ts @@ -95,6 +95,10 @@ const injectLambdaContext = ( request.context, options ); + + if (options?.correlationIdPath) { + logger.setCorrelationId(request.event, options.correlationIdPath); + } } }; diff --git a/packages/logger/src/types/Logger.ts b/packages/logger/src/types/Logger.ts index 01b7fbad20..e127f9441d 100644 --- a/packages/logger/src/types/Logger.ts +++ b/packages/logger/src/types/Logger.ts @@ -54,6 +54,11 @@ type InjectLambdaContextOptions = { * @default `false` */ flushBufferOnUncaughtError?: boolean; + + /** + * The path to the correlation ID in the event object, used to extract the correlation ID from the event object and add it to the log attributes. + */ + correlationIdPath?: string; }; /** @@ -217,13 +222,21 @@ type LogBufferOption = { }; }; +type CorrelationIdOption = { + /** + * The search function for the correlation ID. + */ + correlationIdSearchFn?: (expression: string, data: unknown) => unknown; +}; + /** * Options to configure the Logger. */ type ConstructorOptions = BaseConstructorOptions & (PersistentKeysOption | DeprecatedPersistentKeysOption) & (LogFormatterOption | LogRecordOrderOption) & - LogBufferOption; + LogBufferOption & + CorrelationIdOption; type LogItemMessage = string | LogAttributesWithMessage; type LogItemExtraInput = [Error | string] | LogAttributes[]; @@ -252,6 +265,7 @@ type LoggerInterface = { removeKeys(keys?: string[]): void; removePersistentLogAttributes(keys?: string[]): void; resetKeys(): void; + setCorrelationId(value: unknown, correlationIdPath?: string): void; setLogLevel(logLevel: LogLevel): void; setPersistentLogAttributes(attributes?: LogAttributes): void; shouldLogEvent(overwriteValue?: boolean): boolean; @@ -260,14 +274,14 @@ type LoggerInterface = { }; export type { + ConstructorOptions, + CustomJsonReplacerFn, Environment, + InjectLambdaContextOptions, LogAttributes, - LogLevel, LogFunction, LoggerInterface, - LogItemMessage, LogItemExtraInput, - ConstructorOptions, - InjectLambdaContextOptions, - CustomJsonReplacerFn, + LogItemMessage, + LogLevel, }; diff --git a/packages/logger/src/types/index.ts b/packages/logger/src/types/index.ts index 9544419a66..30a0e6a1d7 100644 --- a/packages/logger/src/types/index.ts +++ b/packages/logger/src/types/index.ts @@ -1,12 +1,12 @@ export type { - Environment, - LogAttributes, - LogLevel, - LogItemMessage, - LogItemExtraInput, ConstructorOptions, - InjectLambdaContextOptions, CustomJsonReplacerFn, + Environment, + InjectLambdaContextOptions, + LogAttributes, LoggerInterface, + LogItemExtraInput, + LogItemMessage, + LogLevel, } from './Logger.js'; export type { UnformattedAttributes } from './logKeys.js'; diff --git a/packages/logger/tests/e2e/advancedUses.test.FunctionCode.ts b/packages/logger/tests/e2e/advancedUses.test.FunctionCode.ts new file mode 100644 index 0000000000..1e601c6097 --- /dev/null +++ b/packages/logger/tests/e2e/advancedUses.test.FunctionCode.ts @@ -0,0 +1,63 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { + correlationPaths, + search, +} from '@aws-lambda-powertools/logger/correlationId'; +import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware'; +import type { Context } from 'aws-lambda'; +import middy from 'middy5'; + +const logger = new Logger({ + logLevel: 'DEBUG', + logBufferOptions: { + enabled: true, + flushOnErrorLog: true, + }, + correlationIdSearchFn: search, +}); + +logger.debug('a never buffered debug log'); + +export const handlerManual = async (event: unknown) => { + logger.addContext({} as Context); // we want only the cold start value + logger.setCorrelationId(event, correlationPaths.EVENT_BRIDGE); + + logger.debug('a buffered debug log'); + logger.info('an info log'); + try { + throw new Error('ops'); + } catch (error) { + logger.error('Uncaught error detected, flushing log buffer before exit', { + error, + }); + } finally { + logger.clearBuffer(); + } +}; + +export const handlerMiddy = middy() + .use( + injectLambdaContext(logger, { + correlationIdPath: correlationPaths.EVENT_BRIDGE, + flushBufferOnUncaughtError: true, + }) + ) + .handler(async () => { + logger.debug('a buffered debug log'); + logger.info('an info log'); + throw new Error('ops'); + }); + +class Lambda { + @logger.injectLambdaContext({ + correlationIdPath: correlationPaths.EVENT_BRIDGE, + flushBufferOnUncaughtError: true, + }) + public async handler(_event: unknown, _context: Context) { + logger.debug('a buffered debug log'); + logger.info('an info log'); + throw new Error('ops'); + } +} +const lambda = new Lambda(); +export const handlerDecorator = lambda.handler.bind(lambda); diff --git a/packages/logger/tests/e2e/advancedUses.test.ts b/packages/logger/tests/e2e/advancedUses.test.ts new file mode 100644 index 0000000000..6682042226 --- /dev/null +++ b/packages/logger/tests/e2e/advancedUses.test.ts @@ -0,0 +1,167 @@ +import { join } from 'node:path'; +import { + TestInvocationLogs, + TestStack, + invokeFunction, +} from '@aws-lambda-powertools/testing-utils'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { LoggerTestNodejsFunction } from '../helpers/resources.js'; +import { RESOURCE_NAME_PREFIX, STACK_OUTPUT_LOG_GROUP } from './constants.js'; + +/** + * In this e2e test for Logger, we test a number of advanced use cases: + * - Log buffering enabled with flush on error (both manually on logger.error and automatically on uncaught error) + * - Correlation ID injection (both manually and automatically) + * - Cold start detection for provisioned concurrency (always false) + * + * The test is split into three cases: + * - Manual instrumentation + * - Middy middleware + * - Decorator + */ +describe('Logger E2E - Advanced uses', () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'Advanced', + }, + }); + + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'advancedUses.test.FunctionCode.ts' + ); + + const invocationCount = 2; + const invocationLogs = new Map(); + const manualCase = 'Manual'; + const middyCase = 'Middy'; + const decoratorCase = 'Decorator'; + + beforeAll(async () => { + invocationLogs.set(manualCase, []); + invocationLogs.set(middyCase, []); + invocationLogs.set(decoratorCase, []); + for (const caseKey of invocationLogs.keys()) { + new LoggerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + handler: `handler${caseKey}`, + }, + { + logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, + nameSuffix: caseKey, + createAlias: true, + } + ); + } + + await testStack.deploy(); + + for (const caseKey of invocationLogs.keys()) { + const functionArn = testStack.findAndGetStackOutputValue(caseKey); + const logs = await invokeFunction({ + functionName: functionArn, + times: invocationCount, + invocationMode: 'SEQUENTIAL', + payload: [ + { + id: 1, + }, + { + id: 2, + }, + ], + }); + invocationLogs.set(caseKey, logs); + } + }); + + it.each([ + { + caseKey: manualCase, + }, + { + caseKey: middyCase, + }, + { + caseKey: decoratorCase, + }, + ])('$caseKey instrumentation', ({ caseKey }) => { + for (let i = 0; i < invocationCount; i++) { + const isFirstInvocation = i === 0; + // Get log messages of the i-th invocation + const fnLogs = invocationLogs.get(caseKey)?.at(i)?.getFunctionLogs(); + if (!fnLogs || fnLogs.length === 0) { + throw new Error(`Failed to get logs for ${caseKey} invocation ${i}`); + } + // When using decorator & middleware, we are actually throwing an error + // which is logged by the runtime, so we need to filter out the logs that are + // not JSON formatted + const logs = fnLogs.filter((log) => { + try { + JSON.parse(log); + return true; + } catch (error) { + return false; + } + }); + + if (isFirstInvocation) { + // Logs outside of the function handler are only present on the first invocation + expect(TestInvocationLogs.parseFunctionLog(logs[0])).toEqual( + expect.objectContaining({ + level: 'DEBUG', + message: 'a never buffered debug log', + }) + ); + } + // Since we have an extra log (above) on the first invocation, we need to + // adjust the index of the logs we are checking + const logIndexOffset = isFirstInvocation ? 1 : 0; + const correlationId = i + 1; + expect( + TestInvocationLogs.parseFunctionLog(logs[0 + logIndexOffset]) + ).toEqual( + expect.objectContaining({ + level: 'INFO', + message: 'an info log', + cold_start: false, + correlation_id: correlationId, + }) + ); + expect( + TestInvocationLogs.parseFunctionLog(logs[1 + logIndexOffset]) + ).toEqual( + expect.objectContaining({ + level: 'DEBUG', + message: 'a buffered debug log', + cold_start: false, + correlation_id: correlationId, + }) + ); + expect( + TestInvocationLogs.parseFunctionLog(logs.at(-1) as string) + ).toEqual( + expect.objectContaining({ + level: 'ERROR', + message: 'Uncaught error detected, flushing log buffer before exit', + cold_start: false, + correlation_id: correlationId, + error: expect.objectContaining({ + name: 'Error', + message: 'ops', + }), + }) + ); + } + }); + + afterAll(async () => { + if (!process.env.DISABLE_TEARDOWN) { + await testStack.destroy(); + } + }); +}); diff --git a/packages/logger/tests/e2e/basicFeatures.middy.test.ts b/packages/logger/tests/e2e/basicFeatures.middy.test.ts index 9f373c718d..04a6eb5200 100644 --- a/packages/logger/tests/e2e/basicFeatures.middy.test.ts +++ b/packages/logger/tests/e2e/basicFeatures.middy.test.ts @@ -9,10 +9,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { LoggerTestNodejsFunction } from '../helpers/resources.js'; import { RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, STACK_OUTPUT_LOG_GROUP, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, XRAY_TRACE_ID_REGEX, commonEnvironmentVars, } from './constants.js'; @@ -65,297 +62,227 @@ describe('Logger E2E tests, basic functionalities middy usage', () => { }); console.log('logGroupName', logGroupName); - }, SETUP_TIMEOUT); + }); describe('Log level filtering', () => { - it( - 'should filter log based on POWERTOOLS_LOG_LEVEL (INFO) environment variable in Lambda', - async () => { - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation and filter by level - const debugLogs = invocationLogs[i].getFunctionLogs('DEBUG'); - // Check that no log message below INFO level is logged - expect(debugLogs.length).toBe(0); - } - }, - TEST_CASE_TIMEOUT - ); + it('should filter log based on POWERTOOLS_LOG_LEVEL (INFO) environment variable in Lambda', async () => { + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation and filter by level + const debugLogs = invocationLogs[i].getFunctionLogs('DEBUG'); + // Check that no log message below INFO level is logged + expect(debugLogs.length).toBe(0); + } + }); }); describe('Context data', () => { - it( - 'should inject context info in each log', - async () => { - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); - // Check that the context is logged on every log - for (const message of logMessages) { - const log = TestInvocationLogs.parseFunctionLog(message); - expect(log).toHaveProperty('function_arn'); - expect(log).toHaveProperty('function_memory_size'); - expect(log).toHaveProperty('function_name'); - expect(log).toHaveProperty('function_request_id'); - expect(log).toHaveProperty('timestamp'); - } + it('should inject context info in each log', async () => { + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs(); + // Check that the context is logged on every log + for (const message of logMessages) { + const log = TestInvocationLogs.parseFunctionLog(message); + expect(log).toHaveProperty('function_arn'); + expect(log).toHaveProperty('function_memory_size'); + expect(log).toHaveProperty('function_name'); + expect(log).toHaveProperty('function_request_id'); + expect(log).toHaveProperty('timestamp'); } - }, - TEST_CASE_TIMEOUT - ); + } + }); - it( - 'should include coldStart equal to TRUE only on the first invocation, FALSE otherwise', - async () => { - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); - // Check that cold start is logged correctly on every log - for (const message of logMessages) { - const log = TestInvocationLogs.parseFunctionLog(message); - if (i === 0) { - expect(log.cold_start).toBe(true); - } else { - expect(log.cold_start).toBe(false); - } + it('should include coldStart equal to TRUE only on the first invocation, FALSE otherwise', async () => { + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs(); + // Check that cold start is logged correctly on every log + for (const message of logMessages) { + const log = TestInvocationLogs.parseFunctionLog(message); + if (i === 0) { + expect(log.cold_start).toBe(true); + } else { + expect(log.cold_start).toBe(false); } } - }, - TEST_CASE_TIMEOUT - ); + } + }); }); - describe('Log event', () => { - it( - 'should log the event as the first log of each invocation only', - async () => { - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); - - for (const [index, message] of logMessages.entries()) { - const log = TestInvocationLogs.parseFunctionLog(message); - // Check that the event is logged on the first log - if (index === 0) { - expect(log).toHaveProperty('event'); - expect(log.event).toStrictEqual( - expect.objectContaining({ foo: 'bar' }) - ); - // Check that the event is not logged again on the rest of the logs - } else { - expect(log).not.toHaveProperty('event'); - } - } - } - }, - TEST_CASE_TIMEOUT - ); + it('logs the event for every invocation, only once, and without keys from previous invocations', async () => { + const { RUNTIME_ADDED_KEY: runtimeAddedKey } = commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs(); + + const eventLog = logMessages.filter((log) => + log.includes('Lambda invocation event') + ); + + // Check that the event log is logged only once + expect(eventLog).toHaveLength(1); + const log = TestInvocationLogs.parseFunctionLog(eventLog[0]); + // Check that the event log is logged correctly + expect(log).toHaveProperty('event'); + expect(log.event).toStrictEqual(expect.objectContaining({ foo: 'bar' })); + // Check that the event log does not contain keys from previous invocations + expect(log).not.toHaveProperty(runtimeAddedKey); + } }); describe('Persistent additional log keys and values', () => { - it( - 'should contain persistent value in every log', - async () => { - const { - PERSISTENT_KEY: persistentKey, - PERSISTENT_VALUE: persistentValue, - } = commonEnvironmentVars; - - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); - - for (const message of logMessages) { - const log = TestInvocationLogs.parseFunctionLog(message); - // Check that the persistent key is present in every log - expect(log).toHaveProperty(persistentKey); - expect(log[persistentKey]).toBe(persistentValue); - } + it('should contain persistent value in every log', async () => { + const { + PERSISTENT_KEY: persistentKey, + PERSISTENT_VALUE: persistentValue, + } = commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs(); + + for (const message of logMessages) { + const log = TestInvocationLogs.parseFunctionLog(message); + // Check that the persistent key is present in every log + expect(log).toHaveProperty(persistentKey); + expect(log[persistentKey]).toBe(persistentValue); } - }, - TEST_CASE_TIMEOUT - ); - - it( - 'should not contain persistent keys that were removed on runtime', - async () => { - const { REMOVABLE_KEY: removableKey, REMOVABLE_VALUE: removableValue } = - commonEnvironmentVars; - - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); - - for (const [index, message] of logMessages.entries()) { - const log = TestInvocationLogs.parseFunctionLog(message); - // Check that at the time of logging the event, which happens before the handler, - // the key was still present - if (index === 0) { - expect(log).toHaveProperty(removableKey); - expect(log[removableKey]).toBe(removableValue); - // Check that all other logs that happen at runtime do not contain the key - } else { - expect(log).not.toHaveProperty(removableValue); - } - } - } - }, - TEST_CASE_TIMEOUT - ); - - it( - 'should not leak any persistent keys added runtime since clearState is enabled', - async () => { - const { RUNTIME_ADDED_KEY: runtimeAddedKey } = commonEnvironmentVars; - - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); + } + }); - for (const [index, message] of logMessages.entries()) { - const log = TestInvocationLogs.parseFunctionLog(message); - // Check that at the time of logging the event, which happens before the handler, - // the key is NOT present - if (index === 0) { - expect(log).not.toHaveProperty(runtimeAddedKey); - } else { - // Check that all other logs that happen at runtime do contain the key - expect(log).toHaveProperty(runtimeAddedKey); - // Check that the value is the same for all logs - expect(log[runtimeAddedKey]).toEqual('bar'); - } + it('should not contain persistent keys that were removed on runtime', async () => { + const { REMOVABLE_KEY: removableKey, REMOVABLE_VALUE: removableValue } = + commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs(); + + for (const [index, message] of logMessages.entries()) { + const log = TestInvocationLogs.parseFunctionLog(message); + // Check that at the time of logging the event, which happens before the handler, + // the key was still present + if (index === 0) { + expect(log).toHaveProperty(removableKey); + expect(log[removableKey]).toBe(removableValue); + // Check that all other logs that happen at runtime do not contain the key + } else { + expect(log).not.toHaveProperty(removableValue); } } - }, - TEST_CASE_TIMEOUT - ); + } + }); }); describe('One-time additional log keys and values', () => { - it( - 'should log additional keys and value only once', - async () => { - const { - SINGLE_LOG_ITEM_KEY: singleLogItemKey, - SINGLE_LOG_ITEM_VALUE: singleLogItemValue, - } = commonEnvironmentVars; - - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); - // Check that the additional log is logged only once - const logMessagesWithAdditionalLog = logMessages.filter((log) => - log.includes(singleLogItemKey) - ); - expect(logMessagesWithAdditionalLog).toHaveLength(1); - // Check that the additional log is logged correctly - const parsedLog = TestInvocationLogs.parseFunctionLog( - logMessagesWithAdditionalLog[0] - ); - expect(parsedLog[singleLogItemKey]).toBe(singleLogItemValue); - } - }, - TEST_CASE_TIMEOUT - ); + it('should log additional keys and value only once', async () => { + const { + SINGLE_LOG_ITEM_KEY: singleLogItemKey, + SINGLE_LOG_ITEM_VALUE: singleLogItemValue, + } = commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs(); + // Check that the additional log is logged only once + const logMessagesWithAdditionalLog = logMessages.filter((log) => + log.includes(singleLogItemKey) + ); + expect(logMessagesWithAdditionalLog).toHaveLength(1); + // Check that the additional log is logged correctly + const parsedLog = TestInvocationLogs.parseFunctionLog( + logMessagesWithAdditionalLog[0] + ); + expect(parsedLog[singleLogItemKey]).toBe(singleLogItemValue); + } + }); }); describe('Error logging', () => { - it( - 'should log error only once', - async () => { - const { ERROR_MSG: errorMsg } = commonEnvironmentVars; - - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation filtered by error level - const logMessages = invocationLogs[i].getFunctionLogs('ERROR'); - - // Check that the error is logged only once - expect(logMessages).toHaveLength(1); - - // Check that the error is logged correctly - const errorLog = TestInvocationLogs.parseFunctionLog(logMessages[0]); - expect(errorLog).toHaveProperty('error'); - expect(errorLog.error).toStrictEqual( - expect.objectContaining({ - location: expect.any(String), - name: 'Error', - message: errorMsg, - stack: expect.anything(), - }) - ); - } - }, - TEST_CASE_TIMEOUT - ); + it('should log error only once', async () => { + const { ERROR_MSG: errorMsg } = commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation filtered by error level + const logMessages = invocationLogs[i].getFunctionLogs('ERROR'); + + // Check that the error is logged only once + expect(logMessages).toHaveLength(1); + + // Check that the error is logged correctly + const errorLog = TestInvocationLogs.parseFunctionLog(logMessages[0]); + expect(errorLog).toHaveProperty('error'); + expect(errorLog.error).toStrictEqual( + expect.objectContaining({ + location: expect.any(String), + name: 'Error', + message: errorMsg, + stack: expect.anything(), + }) + ); + } + }); }); describe('Arbitrary object logging', () => { - it( - 'should log additional arbitrary object only once', - async () => { - const { - ARBITRARY_OBJECT_KEY: objectKey, - ARBITRARY_OBJECT_DATA: objectData, - } = commonEnvironmentVars; - - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); - // Get the log messages that contains the arbitrary object - const filteredLogs = logMessages.filter((log) => - log.includes(objectData) - ); - // Check that the arbitrary object is logged only once - expect(filteredLogs).toHaveLength(1); - const logObject = TestInvocationLogs.parseFunctionLog( - filteredLogs[0] - ); - // Check that the arbitrary object is logged correctly - expect(logObject).toHaveProperty(objectKey); - const arbitrary = logObject[objectKey] as APIGatewayAuthorizerResult; - expect(arbitrary.principalId).toBe(objectData); - expect(arbitrary.policyDocument).toEqual( - expect.objectContaining({ - Version: 'Version 1', - Statement: [ - { - Effect: 'Allow', - Action: 'geo:*', - Resource: '*', - }, - ], - }) - ); - } - }, - TEST_CASE_TIMEOUT - ); + it('should log additional arbitrary object only once', async () => { + const { + ARBITRARY_OBJECT_KEY: objectKey, + ARBITRARY_OBJECT_DATA: objectData, + } = commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs(); + // Get the log messages that contains the arbitrary object + const filteredLogs = logMessages.filter((log) => + log.includes(objectData) + ); + // Check that the arbitrary object is logged only once + expect(filteredLogs).toHaveLength(1); + const logObject = TestInvocationLogs.parseFunctionLog(filteredLogs[0]); + // Check that the arbitrary object is logged correctly + expect(logObject).toHaveProperty(objectKey); + const arbitrary = logObject[objectKey] as APIGatewayAuthorizerResult; + expect(arbitrary.principalId).toBe(objectData); + expect(arbitrary.policyDocument).toEqual( + expect.objectContaining({ + Version: 'Version 1', + Statement: [ + { + Effect: 'Allow', + Action: 'geo:*', + Resource: '*', + }, + ], + }) + ); + } + }); }); describe('X-Ray Trace ID injection', () => { - it( - 'should inject & parse the X-Ray Trace ID of the current invocation into every log', - async () => { - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); - - // Check that the X-Ray Trace ID is logged on every log - const traceIds: string[] = []; - for (const message of logMessages) { - const log = TestInvocationLogs.parseFunctionLog(message); - expect(log).toHaveProperty('xray_trace_id'); - expect(log.xray_trace_id).toMatch(XRAY_TRACE_ID_REGEX); - traceIds.push(log.xray_trace_id as string); - } + it('should inject & parse the X-Ray Trace ID of the current invocation into every log', async () => { + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs(); + + // Check that the X-Ray Trace ID is logged on every log + const traceIds: string[] = []; + for (const message of logMessages) { + const log = TestInvocationLogs.parseFunctionLog(message); + expect(log).toHaveProperty('xray_trace_id'); + expect(log.xray_trace_id).toMatch(XRAY_TRACE_ID_REGEX); + traceIds.push(log.xray_trace_id as string); } - }, - TEST_CASE_TIMEOUT - ); + } + }); }); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/packages/logger/tests/e2e/childLogger.manual.test.ts b/packages/logger/tests/e2e/childLogger.manual.test.ts index a52584ab25..57dd63c926 100644 --- a/packages/logger/tests/e2e/childLogger.manual.test.ts +++ b/packages/logger/tests/e2e/childLogger.manual.test.ts @@ -8,10 +8,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { LoggerTestNodejsFunction } from '../helpers/resources.js'; import { RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, STACK_OUTPUT_LOG_GROUP, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, commonEnvironmentVars, } from './constants.js'; @@ -57,105 +54,89 @@ describe('Logger E2E tests, child logger', () => { }); console.log('logGroupName', logGroupName); - }, SETUP_TIMEOUT); + }); describe('Child logger', () => { - it( - 'should not log at same level of parent because of its own logLevel', - async () => { - const { PARENT_LOG_MSG: parentLogMsg, CHILD_LOG_MSG: childLogMsg } = - commonEnvironmentVars; - - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation and filter by level - const infoLogs = invocationLogs[i].getFunctionLogs('INFO'); - - const parentInfoLogs = infoLogs.filter((message) => - message.includes(parentLogMsg) - ); - const childInfoLogs = infoLogs.filter((message) => - message.includes(childLogMsg) - ); - - expect(parentInfoLogs).toHaveLength(infoLogs.length); - expect(childInfoLogs).toHaveLength(0); - } - }, - TEST_CASE_TIMEOUT - ); + it('should not log at same level of parent because of its own logLevel', async () => { + const { PARENT_LOG_MSG: parentLogMsg, CHILD_LOG_MSG: childLogMsg } = + commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation and filter by level + const infoLogs = invocationLogs[i].getFunctionLogs('INFO'); + + const parentInfoLogs = infoLogs.filter((message) => + message.includes(parentLogMsg) + ); + const childInfoLogs = infoLogs.filter((message) => + message.includes(childLogMsg) + ); + + expect(parentInfoLogs).toHaveLength(infoLogs.length); + expect(childInfoLogs).toHaveLength(0); + } + }); - it( - 'should log only level passed to a child', - async () => { - const { CHILD_LOG_MSG: childLogMsg } = commonEnvironmentVars; - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); - - // Filter child logs by level - const errorChildLogs = logMessages.filter( - (message) => - message.includes('ERROR') && message.includes(childLogMsg) - ); - - // Check that the child logger only logged once (the other) - // log was filtered out by the child logger because of its logLevel - expect(errorChildLogs).toHaveLength(1); - } - }, - TEST_CASE_TIMEOUT - ); + it('should log only level passed to a child', async () => { + const { CHILD_LOG_MSG: childLogMsg } = commonEnvironmentVars; + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs(); + + // Filter child logs by level + const errorChildLogs = logMessages.filter( + (message) => + message.includes('ERROR') && message.includes(childLogMsg) + ); + + // Check that the child logger only logged once (the other) + // log was filtered out by the child logger because of its logLevel + expect(errorChildLogs).toHaveLength(1); + } + }); - it( - 'should NOT inject context into the child logger', - async () => { - const { CHILD_LOG_MSG: childLogMsg } = commonEnvironmentVars; - - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); - - // Filter child logs by level - const childLogMessages = logMessages.filter((message) => - message.includes(childLogMsg) - ); - - // Check that the context is not present in any of the child logs - for (const message of childLogMessages) { - const log = TestInvocationLogs.parseFunctionLog(message); - expect(log).not.toHaveProperty('function_arn'); - expect(log).not.toHaveProperty('function_memory_size'); - expect(log).not.toHaveProperty('function_name'); - expect(log).not.toHaveProperty('function_request_id'); - } + it('should NOT inject context into the child logger', async () => { + const { CHILD_LOG_MSG: childLogMsg } = commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs(); + + // Filter child logs by level + const childLogMessages = logMessages.filter((message) => + message.includes(childLogMsg) + ); + + // Check that the context is not present in any of the child logs + for (const message of childLogMessages) { + const log = TestInvocationLogs.parseFunctionLog(message); + expect(log).not.toHaveProperty('function_arn'); + expect(log).not.toHaveProperty('function_memory_size'); + expect(log).not.toHaveProperty('function_name'); + expect(log).not.toHaveProperty('function_request_id'); } - }, - TEST_CASE_TIMEOUT - ); + } + }); - it( - 'both logger instances should have the same persistent key/value', - async () => { - const { PERSISTENT_KEY: persistentKey } = commonEnvironmentVars; + it('both logger instances should have the same persistent key/value', async () => { + const { PERSISTENT_KEY: persistentKey } = commonEnvironmentVars; - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs(); - // Check that all logs have the persistent key/value - for (const message of logMessages) { - const log = TestInvocationLogs.parseFunctionLog(message); - expect(log).toHaveProperty(persistentKey); - } + // Check that all logs have the persistent key/value + for (const message of logMessages) { + const log = TestInvocationLogs.parseFunctionLog(message); + expect(log).toHaveProperty(persistentKey); } - }, - TEST_CASE_TIMEOUT - ); + } + }); }); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/packages/logger/tests/e2e/constants.ts b/packages/logger/tests/e2e/constants.ts index a719a35b83..6770b144ad 100644 --- a/packages/logger/tests/e2e/constants.ts +++ b/packages/logger/tests/e2e/constants.ts @@ -1,10 +1,6 @@ import { randomUUID } from 'node:crypto'; const RESOURCE_NAME_PREFIX = 'Logger'; -const ONE_MINUTE = 60 * 1000; -const TEST_CASE_TIMEOUT = ONE_MINUTE; -const SETUP_TIMEOUT = 7 * ONE_MINUTE; -const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; const STACK_OUTPUT_LOG_GROUP = 'LogGroupName'; const XRAY_TRACE_ID_REGEX = /^1-[0-9a-f]{8}-[0-9a-f]{24}$/; @@ -28,10 +24,6 @@ const commonEnvironmentVars = { export { RESOURCE_NAME_PREFIX, - ONE_MINUTE, - TEST_CASE_TIMEOUT, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, STACK_OUTPUT_LOG_GROUP, XRAY_TRACE_ID_REGEX, commonEnvironmentVars, diff --git a/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts b/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts index 21c2dad621..3a3efe2609 100644 --- a/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts +++ b/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts @@ -6,13 +6,7 @@ import { } from '@aws-lambda-powertools/testing-utils'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { LoggerTestNodejsFunction } from '../helpers/resources.js'; -import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - STACK_OUTPUT_LOG_GROUP, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, -} from './constants.js'; +import { RESOURCE_NAME_PREFIX, STACK_OUTPUT_LOG_GROUP } from './constants.js'; describe('Logger E2E tests, log event via env var setting with middy', () => { const testStack = new TestStack({ @@ -63,38 +57,34 @@ describe('Logger E2E tests, log event via env var setting with middy', () => { }); console.log('logGroupName', logGroupName); - }, SETUP_TIMEOUT); + }); describe('Log event', () => { - it( - 'should log the event as the first log of each invocation only', - async () => { - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); + it('should log the event as the first log of each invocation only', async () => { + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs(); - for (const [index, message] of logMessages.entries()) { - const log = TestInvocationLogs.parseFunctionLog(message); - // Check that the event is logged on the first log - if (index === 0) { - expect(log).toHaveProperty('event'); - expect(log.event).toStrictEqual( - expect.objectContaining({ foo: 'bar' }) - ); - // Check that the event is not logged again on the rest of the logs - } else { - expect(log).not.toHaveProperty('event'); - } + for (const [index, message] of logMessages.entries()) { + const log = TestInvocationLogs.parseFunctionLog(message); + // Check that the event is logged on the first log + if (index === 0) { + expect(log).toHaveProperty('event'); + expect(log.event).toStrictEqual( + expect.objectContaining({ foo: 'bar' }) + ); + // Check that the event is not logged again on the rest of the logs + } else { + expect(log).not.toHaveProperty('event'); } } - }, - TEST_CASE_TIMEOUT - ); + } + }); }); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/packages/logger/tests/e2e/sampleRate.decorator.test.FunctionCode.ts b/packages/logger/tests/e2e/sampleRate.decorator.test.FunctionCode.ts index bc770fe560..b0cd540189 100644 --- a/packages/logger/tests/e2e/sampleRate.decorator.test.FunctionCode.ts +++ b/packages/logger/tests/e2e/sampleRate.decorator.test.FunctionCode.ts @@ -9,23 +9,12 @@ const LOG_MSG = process.env.LOG_MSG || 'Hello World'; const logger = new Logger({ sampleRateValue: SAMPLE_RATE, }); -let firstInvocation = true; class Lambda implements LambdaInterface { - private readonly logMsg: string; + private readonly logMsg = LOG_MSG; - public constructor() { - this.logMsg = LOG_MSG; - } - - // Decorate your handler class method @logger.injectLambdaContext() public async handler(_event: TestEvent, context: Context): TestOutput { - if (firstInvocation) { - firstInvocation = false; - } else { - logger.refreshSampleRateCalculation(); - } this.printLogInAllLevels(); return { diff --git a/packages/logger/tests/e2e/sampleRate.decorator.test.ts b/packages/logger/tests/e2e/sampleRate.decorator.test.ts index 011a72d02d..d6e848f06e 100644 --- a/packages/logger/tests/e2e/sampleRate.decorator.test.ts +++ b/packages/logger/tests/e2e/sampleRate.decorator.test.ts @@ -7,13 +7,7 @@ import { } from '@aws-lambda-powertools/testing-utils'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { LoggerTestNodejsFunction } from '../helpers/resources.js'; -import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - STACK_OUTPUT_LOG_GROUP, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, -} from './constants.js'; +import { RESOURCE_NAME_PREFIX, STACK_OUTPUT_LOG_GROUP } from './constants.js'; describe('Logger E2E tests, sample rate and injectLambdaContext()', () => { const testStack = new TestStack({ @@ -62,73 +56,65 @@ describe('Logger E2E tests, sample rate and injectLambdaContext()', () => { }); console.log('logGroupName', logGroupName); - }, SETUP_TIMEOUT); + }); describe('Enabling sample rate', () => { - it( - 'should log all levels based on given sample rate, not just ERROR', - async () => { - // Fetch log streams from all invocations - let countSampled = 0; - let countNotSampled = 0; + it('should log all levels based on given sample rate, not just ERROR', async () => { + // Fetch log streams from all invocations + let countSampled = 0; + let countNotSampled = 0; - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(); + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs(); - if (logMessages.length === 1 && logMessages[0].includes('ERROR')) { - countNotSampled++; - } else if ( - (logMessages.length === 5 && - logMessages[0].includes( - 'Setting log level to DEBUG due to sampling rate' - )) || - logMessages.length === 4 - ) { - countSampled++; - } else { - console.error(`Log group ${logGroupName} contains missing log`); - throw new Error( - 'Sampled log should have either 1 error log or 5 logs of all levels' - ); - } + if (logMessages.length === 1 && logMessages[0].includes('ERROR')) { + countNotSampled++; + } else if ( + (logMessages.length === 5 && + logMessages[0].includes( + 'Setting log level to DEBUG due to sampling rate' + )) || + logMessages.length === 4 + ) { + countSampled++; + } else { + console.error(`Log group ${logGroupName} contains missing log`); + throw new Error( + 'Sampled log should have either 1 error log or 5 logs of all levels' + ); } + } - // Given that we set rate to 0.5. The chance that we get all invocationCount sampled - // (or not sampled) is less than 0.5^20 - expect(countSampled).toBeGreaterThan(0); - expect(countNotSampled).toBeGreaterThan(0); - }, - TEST_CASE_TIMEOUT - ); + // Given that we set rate to 0.5. The chance that we get all invocationCount sampled + // (or not sampled) is less than 0.5^20 + expect(countSampled).toBeGreaterThan(0); + expect(countNotSampled).toBeGreaterThan(0); + }); }); describe('Decorator injectLambdaContext()', () => { - it( - 'should inject Lambda context into every log emitted', - async () => { - for (let i = 0; i < invocationCount; i++) { - // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs('ERROR'); + it('should inject Lambda context into every log emitted', async () => { + for (let i = 0; i < invocationCount; i++) { + // Get log messages of the invocation + const logMessages = invocationLogs[i].getFunctionLogs('ERROR'); - // Check that the context is logged on every log - for (const message of logMessages) { - const log = TestInvocationLogs.parseFunctionLog(message); - expect(log).toHaveProperty('function_arn'); - expect(log).toHaveProperty('function_memory_size'); - expect(log).toHaveProperty('function_name'); - expect(log).toHaveProperty('function_request_id'); - expect(log).toHaveProperty('timestamp'); - } + // Check that the context is logged on every log + for (const message of logMessages) { + const log = TestInvocationLogs.parseFunctionLog(message); + expect(log).toHaveProperty('function_arn'); + expect(log).toHaveProperty('function_memory_size'); + expect(log).toHaveProperty('function_name'); + expect(log).toHaveProperty('function_request_id'); + expect(log).toHaveProperty('timestamp'); } - }, - TEST_CASE_TIMEOUT - ); + } + }); }); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/packages/logger/tests/unit/injectLambdaContext.test.ts b/packages/logger/tests/unit/injectLambdaContext.test.ts index 36c6d7c6fa..949969e967 100644 --- a/packages/logger/tests/unit/injectLambdaContext.test.ts +++ b/packages/logger/tests/unit/injectLambdaContext.test.ts @@ -3,6 +3,7 @@ import middy from '@middy/core'; import type { Context } from 'aws-lambda'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Logger } from '../../src/Logger.js'; +import { search } from '../../src/correlationId.js'; import { injectLambdaContext } from '../../src/middleware/middy.js'; const event = { @@ -185,38 +186,231 @@ describe('Inject Lambda Context', () => { ); }); - it('refreshes sample rate calculation before each invocation using decorator for warm start only', async () => { + it.each([ + { + case: 'middleware', + getHandler: (logger: Logger) => + middy(async () => { + logger.info('Hello, world!'); + }).use(injectLambdaContext(logger)), + }, + { + case: 'decorator', + getHandler: (logger: Logger) => { + class Lambda { + @logger.injectLambdaContext() + public async handler( + _event: unknown, + _context: Context + ): Promise { + logger.info('test'); + } + } + const lambda = new Lambda(); + return lambda.handler.bind(lambda); + }, + }, + ])( + 'refreshes sample rate calculation before only during warm starts ($case)', + async ({ getHandler }) => { + // Prepare + const logger = new Logger({ sampleRateValue: 1 }); + const setLogLevelSpy = vi.spyOn(logger, 'setLogLevel'); + + const handler = getHandler(logger); + + // Act + await handler(event, context); // cold start + await handler(event, context); // warm start + + // Assess + expect(setLogLevelSpy).toHaveBeenCalledTimes(1); + expect(console.debug).toHaveBeenCalledTimes(2); + expect(console.debug).toHaveLoggedNth( + 1, + expect.objectContaining({ + message: 'Setting log level to DEBUG due to sampling rate', + }) + ); + expect(console.debug).toHaveLoggedNth( + 2, + expect.objectContaining({ + message: 'Setting log level to DEBUG due to sampling rate', + }) + ); + } + ); + + it.each([ + { + case: 'middleware', + getHandler: (logger: Logger) => + middy(async () => { + logger.info('Hello, world!'); + }).use( + injectLambdaContext(logger, { + correlationIdPath: 'headers."x-correlation-id"', + }) + ), + }, + { + case: 'decorator', + getHandler: (logger: Logger) => { + class Lambda { + @logger.injectLambdaContext({ + correlationIdPath: 'headers."x-correlation-id"', + }) + public async handler( + _event: unknown, + _context: Context + ): Promise { + logger.info('Hello, world!'); + } + } + const lambda = new Lambda(); + return lambda.handler.bind(lambda); + }, + }, + ])('sets correlation ID through $case', async ({ getHandler }) => { // Prepare - const logger = new Logger({ sampleRateValue: 0.5 }); - const refreshSpy = vi.spyOn(logger, 'refreshSampleRateCalculation'); + const logger = new Logger({ correlationIdSearchFn: search }); + const handler = getHandler(logger); + const testEvent = { + headers: { + 'x-correlation-id': '12345-test-id', + }, + }; - class Lambda { - @logger.injectLambdaContext() - public async handler(_event: unknown, _context: Context): Promise { - logger.info('test'); - } - } - const lambda = new Lambda(); // Act - await lambda.handler({}, {} as Context); + await handler(testEvent, context); + + // Assess + expect(console.info).toHaveBeenCalledTimes(1); + expect(console.info).toHaveLoggedNth( + 1, + expect.objectContaining({ + message: 'Hello, world!', + correlation_id: '12345-test-id', + ...getContextLogEntries(), + }) + ); + expect(logger.getCorrelationId()).toBe('12345-test-id'); + }); + + it('warns when correlationIdPath is provided but no search function is available', async () => { + // Prepare + const logger = new Logger(); // No search function provided + const warnSpy = vi.spyOn(logger, 'warn'); + const testEvent = { + headers: { + 'x-correlation-id': '12345-test-id', + }, + }; + // Act - Use middleware which will internally call setCorrelationIdFromPath + const handler = middy(async () => { + logger.info('Hello, world!'); + }).use( + injectLambdaContext(logger, { + correlationIdPath: 'headers.x-correlation-id', + }) + ); + + await handler(testEvent, context); // Assess - expect(refreshSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith( + 'correlationIdPath is set but no search function was provided. The correlation ID will not be added to the log attributes.' + ); }); - it('refreshes sample rate calculation before each invocation using middleware for warm start only', async () => { + it('does not set correlation ID when search function returns falsy value', async () => { // Prepare - const logger = new Logger({ sampleRateValue: 0.5 }); - const refreshSpy = vi.spyOn(logger, 'refreshSampleRateCalculation'); + const logger = new Logger({ correlationIdSearchFn: search }); + // Act - Use middleware which will internally call setCorrelationIdFromPath const handler = middy(async () => { logger.info('Hello, world!'); - }).use(injectLambdaContext(logger)); + }).use( + injectLambdaContext(logger, { + correlationIdPath: 'headers."non-existent"', + }) + ); + + await handler({ foo: 'bar' }, context); + + // Assess + expect(logger.getCorrelationId()).toBeUndefined(); + expect(console.info).toHaveBeenCalledTimes(1); + expect(console.info).toHaveLoggedNth( + 1, + expect.not.objectContaining({ + correlation_id: expect.anything(), + }) + ); + }); + + it('propagates search function to child loggers', () => { + // Prepare + const mockSearch = vi.fn().mockReturnValue('found-id'); + const logger = new Logger({ correlationIdSearchFn: mockSearch }); // Act - await handler(event, context); + const childLogger = logger.createChild(); + childLogger.setCorrelationId({ some: 'event' }, 'some.path'); + + // Assess + expect(mockSearch).toHaveBeenCalledWith('some.path', { some: 'event' }); + expect(childLogger.getCorrelationId()).toBe('found-id'); + }); + + it('allows using different types as correlation ID', () => { + // Prepare + const logger = new Logger(); + const numericId = 12345; + + // Act + logger.setCorrelationId(numericId); + logger.info('Using numeric ID'); // Assess - expect(refreshSpy).toHaveBeenCalledTimes(1); + expect(console.info).toHaveBeenCalledTimes(1); + expect(console.info).toHaveLoggedNth( + 1, + expect.objectContaining({ + message: 'Using numeric ID', + correlation_id: numericId, + }) + ); + expect(logger.getCorrelationId()).toBe(numericId); + }); + + it('uses the API_GATEWAY_REST predefined path to extract correlation ID', async () => { + // Prepare + const logger = new Logger({ correlationIdSearchFn: search }); + const handler = middy(async () => { + logger.info('Using API Gateway request ID'); + }).use( + injectLambdaContext(logger, { + correlationIdPath: 'requestContext.requestId', + }) + ); + const testEvent = { + requestContext: { + requestId: 'api-gateway-request-id', + }, + }; + + // Act + await handler(testEvent, context); + + // Assess + expect(console.info).toHaveBeenCalledTimes(1); + expect(console.info).toHaveLoggedNth( + 1, + expect.objectContaining({ + message: 'Using API Gateway request ID', + correlation_id: 'api-gateway-request-id', + }) + ); }); }); diff --git a/packages/logger/tests/unit/sampling.test.ts b/packages/logger/tests/unit/sampling.test.ts index 2453afc7f3..2179e4d041 100644 --- a/packages/logger/tests/unit/sampling.test.ts +++ b/packages/logger/tests/unit/sampling.test.ts @@ -129,15 +129,13 @@ describe('Log sampling', () => { expect(logger.getLevelName()).toBe(LogLevel.INFO); }); - it('when sample rate calculation is refreshed, it respects probability sampling and change log level to DEBUG when not in coldStart ', () => { + it('refreshes and applies log sampling', () => { // Prepare const logger = new Logger({ logLevel: LogLevel.ERROR, sampleRateValue: 0.1, // 10% probability }); - logger.getColdStart(); // Set coldStart to false - let logLevelChangedToDebug = 0; const numOfIterations = 1000; const minExpected = numOfIterations * 0.05; // Min expected based on 5% probability diff --git a/packages/logger/vitest.config.ts b/packages/logger/vitest.config.ts index 9f1196ef1f..baa5cf7463 100644 --- a/packages/logger/vitest.config.ts +++ b/packages/logger/vitest.config.ts @@ -4,5 +4,7 @@ export default defineProject({ test: { environment: 'node', setupFiles: ['../testing/src/setupEnv.ts'], + hookTimeout: 1_000 * 60 * 10, // 10 minutes + testTimeout: 1_000 * 60 * 3, // 3 minutes }, }); diff --git a/packages/metrics/CHANGELOG.md b/packages/metrics/CHANGELOG.md index 020b4ebb7e..6f07ec4795 100644 --- a/packages/metrics/CHANGELOG.md +++ b/packages/metrics/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + + +### Features + +* **metrics:** allow setting functionName via constructor parameter and environment variable ([#3696](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3696)) ([3176fa0](https://github.com/aws-powertools/powertools-lambda-typescript/commit/3176fa08e1886d5c86e7b327134cc988b82cf8d8)) + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) **Note:** Version bump only for package @aws-lambda-powertools/metrics diff --git a/packages/metrics/README.md b/packages/metrics/README.md index c74a07a113..3187959a8e 100644 --- a/packages/metrics/README.md +++ b/packages/metrics/README.md @@ -164,6 +164,7 @@ The following companies, among others, use Powertools: - [Elva](https://elva-group.com) - [Flyweight](https://flyweight.io/) - [globaldatanet](https://globaldatanet.com/) +- [Guild](https://guild.com) - [Hashnode](https://hashnode.com/) - [LocalStack](https://localstack.cloud/) - [Perfect Post](https://www.perfectpost.fr) diff --git a/packages/metrics/package.json b/packages/metrics/package.json index f189e9f857..c6e053de4d 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/metrics", - "version": "2.16.0", + "version": "2.17.0", "description": "The metrics package for the Powertools for AWS Lambda (TypeScript) library", "author": { "name": "Amazon Web Services", @@ -65,7 +65,7 @@ "main": "./lib/cjs/index.js", "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-cloudwatch": "^3.758.0", + "@aws-sdk/client-cloudwatch": "^3.772.0", "@types/promise-retry": "^1.1.3", "promise-retry": "^2.0.1" }, @@ -88,7 +88,7 @@ "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" }, "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0" + "@aws-lambda-powertools/commons": "^2.17.0" }, "keywords": [ "aws", diff --git a/packages/metrics/src/Metrics.ts b/packages/metrics/src/Metrics.ts index 93b2e35787..a3a743e96d 100644 --- a/packages/metrics/src/Metrics.ts +++ b/packages/metrics/src/Metrics.ts @@ -371,8 +371,10 @@ class Metrics extends Utility implements MetricsInterface { * metrics.captureColdStartMetric(); * }; * ``` + * + * @param functionName - Optional function name to use as `function_name` dimension in the metric. It's used only if the `functionName` constructor parameter or environment variable are not set. */ - public captureColdStartMetric(): void { + public captureColdStartMetric(functionName?: string): void { if (!this.getColdStart()) return; const singleMetric = this.singleMetric(); @@ -381,8 +383,9 @@ class Metrics extends Utility implements MetricsInterface { service: this.defaultDimensions.service, }); } - if (this.functionName != null) { - singleMetric.addDimension('function_name', this.functionName); + const value = this.functionName?.trim() ?? functionName?.trim(); + if (value && value.length > 0) { + singleMetric.addDimension('function_name', value); } singleMetric.addMetric(COLD_START_METRIC, MetricUnits.Count, 1); } @@ -543,8 +546,9 @@ class Metrics extends Utility implements MetricsInterface { context: Context, callback: Callback ): Promise { - metricsRef.functionName = context.functionName; - if (captureColdStartMetric) metricsRef.captureColdStartMetric(); + if (captureColdStartMetric) { + metricsRef.captureColdStartMetric(context.functionName); + } let result: unknown; try { @@ -749,28 +753,14 @@ class Metrics extends Utility implements MetricsInterface { } /** - * Set the function name to be added to each metric as a dimension. - * - * When using the {@link Metrics.logMetrics | `logMetrics()`} decorator, or the Middy.js middleware, the function - * name is automatically inferred from the Lambda context. - * - * @example - * ```typescript - * import { Metrics } from '@aws-lambda-powertools/metrics'; - * - * const metrics = new Metrics({ - * namespace: 'serverlessAirline', - * serviceName: 'orders' - * }); - * - * metrics.setFunctionName('my-function-name'); - * ``` - * - * @param name - The function name + * @deprecated Override the function name for `ColdStart` metrics inferred from the context either via: + * - `functionName` constructor parameter + * - `POWERTOOLS_FUNCTION_NAME` environment variable + * - {@link Metrics.captureColdStartMetric | `captureColdStartMetric('myFunctionName')`} method */ - public setFunctionName(name: string): void { + /* v8 ignore start */ public setFunctionName(name: string): void { this.functionName = name; - } + } /* v8 ignore end */ /** * Set the flag to throw an error if no metrics are emitted. @@ -917,6 +907,19 @@ class Metrics extends Utility implements MetricsInterface { this.envVarsService = new EnvironmentVariablesService(); } + /** + * Set the function name for the cold start metric. + * + * @param functionName - The function name to be used for the cold start metric set in the constructor + */ + protected setFunctionNameForColdStartMetric(functionName?: string): void { + const value = + functionName?.trim() ?? this.getEnvVarsService().getFunctionName().trim(); + if (value && value.length > 0) { + this.functionName = value; + } + } + /** * Set the namespace to be used. * @@ -951,6 +954,7 @@ class Metrics extends Utility implements MetricsInterface { serviceName, singleMetric, defaultDimensions, + functionName, } = options; this.setEnvVarsService(); @@ -960,6 +964,7 @@ class Metrics extends Utility implements MetricsInterface { this.setNamespace(namespace); this.setService(serviceName); this.setDefaultDimensions(defaultDimensions); + this.setFunctionNameForColdStartMetric(functionName); this.isSingleMetric = singleMetric || false; return this; diff --git a/packages/metrics/src/config/EnvironmentVariablesService.ts b/packages/metrics/src/config/EnvironmentVariablesService.ts index a611308649..6fb6c6719c 100644 --- a/packages/metrics/src/config/EnvironmentVariablesService.ts +++ b/packages/metrics/src/config/EnvironmentVariablesService.ts @@ -11,8 +11,8 @@ class EnvironmentVariablesService extends CommonEnvironmentVariablesService implements ConfigServiceInterface { - private namespaceVariable = 'POWERTOOLS_METRICS_NAMESPACE'; - + private readonly namespaceVariable = 'POWERTOOLS_METRICS_NAMESPACE'; + private readonly functionNameVariable = 'POWERTOOLS_METRICS_FUNCTION_NAME'; private readonly disabledVariable = 'POWERTOOLS_METRICS_DISABLED'; /** @@ -22,6 +22,13 @@ class EnvironmentVariablesService return this.get(this.namespaceVariable); } + /** + * Get the value of the `POWERTOOLS_METRICS_FUNCTION_NAME` environment variable. + */ + public getFunctionName(): string { + return this.get(this.functionNameVariable); + } + /** * Get the value of the `POWERTOOLS_METRICS_DISABLED` or `POWERTOOLS_DEV` environment variables. * diff --git a/packages/metrics/src/middleware/middy.ts b/packages/metrics/src/middleware/middy.ts index e02a39d79a..d06a0977e4 100644 --- a/packages/metrics/src/middleware/middy.ts +++ b/packages/metrics/src/middleware/middy.ts @@ -62,7 +62,6 @@ const logMetrics = ( const logMetricsBefore = async (request: MiddyLikeRequest): Promise => { for (const metrics of metricsInstances) { - metrics.setFunctionName(request.context.functionName); const { throwOnEmptyMetrics, defaultDimensions, captureColdStartMetric } = options; if (throwOnEmptyMetrics) { @@ -72,7 +71,7 @@ const logMetrics = ( metrics.setDefaultDimensions(defaultDimensions); } if (captureColdStartMetric) { - metrics.captureColdStartMetric(); + metrics.captureColdStartMetric(request.context.functionName); } } diff --git a/packages/metrics/src/types/ConfigServiceInterface.ts b/packages/metrics/src/types/ConfigServiceInterface.ts index 81caa2d53b..68a86e1571 100644 --- a/packages/metrics/src/types/ConfigServiceInterface.ts +++ b/packages/metrics/src/types/ConfigServiceInterface.ts @@ -12,6 +12,10 @@ interface ConfigServiceInterface extends ConfigServiceBaseInterface { * Get the value of the `POWERTOOLS_METRICS_NAMESPACE` environment variable. */ getNamespace(): string; + /** + * Get the value of the `POWERTOOLS_METRICS_FUNCTION_NAME` environment variable. + */ + getFunctionName(): string; } export type { ConfigServiceInterface }; diff --git a/packages/metrics/src/types/Metrics.ts b/packages/metrics/src/types/Metrics.ts index da71711501..ccd914351e 100644 --- a/packages/metrics/src/types/Metrics.ts +++ b/packages/metrics/src/types/Metrics.ts @@ -60,6 +60,19 @@ type MetricsOptions = { * @see {@link MetricsInterface.setDefaultDimensions | `setDefaultDimensions()`} */ defaultDimensions?: Dimensions; + /** + * Function name to use as dimension for the `ColdStart` metric. + * + * When not provided, the function name is inferred either via: + * - `POWERTOOLS_FUNCTION_NAME` environment variable + * - AWS Lambda function context, **only** when using the {@link MetricsInterface.logMetrics | `logMetrics()`} decorator or the Middy.js middleware + * - `functionName` parameter in the {@link MetricsInterface.captureColdStartMetric | `captureColdStartMetric()`} method + * + * If none of the above are available, the `ColdStart` metric will not include a function name dimension. + * + * @see {@link MetricsInterface.setFunctionName | `setFunctionName()`} + */ + functionName?: string; /** * Logger object to be used for emitting debug, warning, and error messages. * @@ -272,8 +285,10 @@ interface MetricsInterface { * metrics.captureColdStartMetric(); * }; * ``` + * + * @param functionName - Optional function name to use as `function_name` dimension in the metric. It's used only if the `functionName` constructor parameter or environment variable are not set. */ - captureColdStartMetric(): void; + captureColdStartMetric(functionName?: string): void; /** * Clear all previously set default dimensions. * @@ -445,24 +460,10 @@ interface MetricsInterface { */ setDefaultDimensions(dimensions: Dimensions | undefined): void; /** - * Set the function name to be added to each metric as a dimension. - * - * When using the {@link MetricsInterface.logMetrics | `logMetrics()`} decorator, or the Middy.js middleware, the function - * name is automatically inferred from the Lambda context. - * - * @example - * ```typescript - * import { Metrics } from '@aws-lambda-powertools/metrics'; - * - * const metrics = new Metrics({ - * namespace: 'serverlessAirline', - * serviceName: 'orders' - * }); - * - * metrics.setFunctionName('my-function-name'); - * ``` - * - * @param name - The function name + * @deprecated Override the function name for `ColdStart` metrics inferred from the context either via: + * - `functionName` constructor parameter + * - `POWERTOOLS_FUNCTION_NAME` environment variable + * - {@link MetricsInterface.captureColdStartMetric | `captureColdStartMetric()`} method */ setFunctionName(name: string): void; /** diff --git a/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts b/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts index 58398e562f..39ce36248a 100644 --- a/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts +++ b/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts @@ -10,14 +10,7 @@ import { import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { getMetrics, sortDimensions } from '../helpers/metricsUtils.js'; import { MetricsTestNodejsFunction } from '../helpers/resources.js'; -import { - ONE_MINUTE, - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, - commonEnvironmentVars, -} from './constants.js'; +import { RESOURCE_NAME_PREFIX, commonEnvironmentVars } from './constants.js'; describe('Metrics E2E tests, basic features decorator usage', () => { const testStack = new TestStack({ @@ -66,133 +59,123 @@ describe('Metrics E2E tests, basic features decorator usage', () => { times: invocations, invocationMode: 'SEQUENTIAL', }); - }, SETUP_TIMEOUT); + }); describe('ColdStart metrics', () => { - it( - 'captures the ColdStart Metric', - async () => { - const { - EXPECTED_NAMESPACE: expectedNamespace, - EXPECTED_DEFAULT_DIMENSIONS: expectedDefaultDimensions, - } = commonEnvironmentVars; - - const expectedDimensions = [ - { Name: 'service', Value: expectedServiceName }, - { Name: 'function_name', Value: fnNameBasicFeatures }, - { - Name: Object.keys(expectedDefaultDimensions)[0], - Value: expectedDefaultDimensions.MyDimension, - }, - ]; - // Check coldstart metric dimensions - const coldStartMetrics = await getMetrics( - cloudwatchClient, - expectedNamespace, - 'ColdStart', - 1 - ); - - expect(coldStartMetrics.Metrics?.length).toBe(1); - const coldStartMetric = coldStartMetrics.Metrics?.[0]; - expect(sortDimensions(coldStartMetric?.Dimensions)).toStrictEqual( - sortDimensions(expectedDimensions) - ); - - // Check coldstart metric value - const adjustedStartTime = new Date(startTime.getTime() - ONE_MINUTE); - const endTime = new Date(new Date().getTime() + ONE_MINUTE); - const coldStartMetricStat = await cloudwatchClient.send( - new GetMetricStatisticsCommand({ - Namespace: expectedNamespace, - StartTime: adjustedStartTime, - Dimensions: expectedDimensions, - EndTime: endTime, - Period: 60, - MetricName: 'ColdStart', - Statistics: ['Sum'], - }) - ); - - // Despite lambda has been called twice, coldstart metric sum should only be 1 - const singleDataPoint = coldStartMetricStat.Datapoints - ? coldStartMetricStat.Datapoints[0] - : {}; - expect(singleDataPoint?.Sum).toBe(1); - }, - TEST_CASE_TIMEOUT - ); + it('captures the ColdStart Metric', async () => { + const { + EXPECTED_NAMESPACE: expectedNamespace, + EXPECTED_DEFAULT_DIMENSIONS: expectedDefaultDimensions, + } = commonEnvironmentVars; + + const expectedDimensions = [ + { Name: 'service', Value: expectedServiceName }, + { Name: 'function_name', Value: fnNameBasicFeatures }, + { + Name: Object.keys(expectedDefaultDimensions)[0], + Value: expectedDefaultDimensions.MyDimension, + }, + ]; + // Check coldstart metric dimensions + const coldStartMetrics = await getMetrics( + cloudwatchClient, + expectedNamespace, + 'ColdStart', + 1 + ); + + expect(coldStartMetrics.Metrics?.length).toBe(1); + const coldStartMetric = coldStartMetrics.Metrics?.[0]; + expect(sortDimensions(coldStartMetric?.Dimensions)).toStrictEqual( + sortDimensions(expectedDimensions) + ); + + // Check coldstart metric value + const adjustedStartTime = new Date(startTime.getTime() - 60 * 1000); + const endTime = new Date(new Date().getTime() + 60 * 1000); + const coldStartMetricStat = await cloudwatchClient.send( + new GetMetricStatisticsCommand({ + Namespace: expectedNamespace, + StartTime: adjustedStartTime, + Dimensions: expectedDimensions, + EndTime: endTime, + Period: 60, + MetricName: 'ColdStart', + Statistics: ['Sum'], + }) + ); + + // Despite lambda has been called twice, coldstart metric sum should only be 1 + const singleDataPoint = coldStartMetricStat.Datapoints + ? coldStartMetricStat.Datapoints[0] + : {}; + expect(singleDataPoint?.Sum).toBe(1); + }); }); describe('Default and extra dimensions', () => { - it( - 'produces a Metric with the default and extra one dimensions', - async () => { - const { - EXPECTED_NAMESPACE: expectedNamespace, - EXPECTED_METRIC_NAME: expectedMetricName, - EXPECTED_METRIC_VALUE: expectedMetricValue, - EXPECTED_DEFAULT_DIMENSIONS: expectedDefaultDimensions, - EXPECTED_EXTRA_DIMENSION: expectedExtraDimension, - } = commonEnvironmentVars; - - // Check metric dimensions - const metrics = await getMetrics( - cloudwatchClient, - expectedNamespace, - expectedMetricName, - 1 - ); - - expect(metrics.Metrics?.length).toBe(1); - const metric = metrics.Metrics?.[0]; - const expectedDimensions = [ - { Name: 'service', Value: expectedServiceName }, - { - Name: Object.keys(expectedDefaultDimensions)[0], - Value: expectedDefaultDimensions.MyDimension, - }, - { - Name: Object.keys(expectedExtraDimension)[0], - Value: expectedExtraDimension.MyExtraDimension, - }, - ]; - expect(sortDimensions(metric?.Dimensions)).toStrictEqual( - sortDimensions(expectedDimensions) - ); - - // Check coldstart metric value - const adjustedStartTime = new Date( - startTime.getTime() - 3 * ONE_MINUTE - ); - const endTime = new Date(new Date().getTime() + ONE_MINUTE); - const metricStat = await cloudwatchClient.send( - new GetMetricStatisticsCommand({ - Namespace: expectedNamespace, - StartTime: adjustedStartTime, - Dimensions: expectedDimensions, - EndTime: endTime, - Period: 60, - MetricName: expectedMetricName, - Statistics: ['Sum'], - }) - ); - - // Since lambda has been called twice in this test and potentially more in others, metric sum should be at least of expectedMetricValue * invocationCount - const singleDataPoint = metricStat.Datapoints - ? metricStat.Datapoints[0] - : {}; - expect(singleDataPoint?.Sum).toBeGreaterThanOrEqual( - Number.parseInt(expectedMetricValue) * invocations - ); - }, - TEST_CASE_TIMEOUT - ); + it('produces a Metric with the default and extra one dimensions', async () => { + const { + EXPECTED_NAMESPACE: expectedNamespace, + EXPECTED_METRIC_NAME: expectedMetricName, + EXPECTED_METRIC_VALUE: expectedMetricValue, + EXPECTED_DEFAULT_DIMENSIONS: expectedDefaultDimensions, + EXPECTED_EXTRA_DIMENSION: expectedExtraDimension, + } = commonEnvironmentVars; + + // Check metric dimensions + const metrics = await getMetrics( + cloudwatchClient, + expectedNamespace, + expectedMetricName, + 1 + ); + + expect(metrics.Metrics?.length).toBe(1); + const metric = metrics.Metrics?.[0]; + const expectedDimensions = [ + { Name: 'service', Value: expectedServiceName }, + { + Name: Object.keys(expectedDefaultDimensions)[0], + Value: expectedDefaultDimensions.MyDimension, + }, + { + Name: Object.keys(expectedExtraDimension)[0], + Value: expectedExtraDimension.MyExtraDimension, + }, + ]; + expect(sortDimensions(metric?.Dimensions)).toStrictEqual( + sortDimensions(expectedDimensions) + ); + + // Check coldstart metric value + const adjustedStartTime = new Date(startTime.getTime() - 3 * 60 * 1000); + const endTime = new Date(new Date().getTime() + 60 * 1000); + const metricStat = await cloudwatchClient.send( + new GetMetricStatisticsCommand({ + Namespace: expectedNamespace, + StartTime: adjustedStartTime, + Dimensions: expectedDimensions, + EndTime: endTime, + Period: 60, + MetricName: expectedMetricName, + Statistics: ['Sum'], + }) + ); + + // Since lambda has been called twice in this test and potentially more in others, metric sum should be at least of expectedMetricValue * invocationCount + const singleDataPoint = metricStat.Datapoints + ? metricStat.Datapoints[0] + : {}; + expect(singleDataPoint?.Sum).toBeGreaterThanOrEqual( + Number.parseInt(expectedMetricValue) * invocations + ); + }); }); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/packages/metrics/tests/e2e/basicFeatures.manual.test.ts b/packages/metrics/tests/e2e/basicFeatures.manual.test.ts index 5272ee8578..694d444847 100644 --- a/packages/metrics/tests/e2e/basicFeatures.manual.test.ts +++ b/packages/metrics/tests/e2e/basicFeatures.manual.test.ts @@ -10,14 +10,7 @@ import { import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { getMetrics, sortDimensions } from '../helpers/metricsUtils.js'; import { MetricsTestNodejsFunction } from '../helpers/resources.js'; -import { - ONE_MINUTE, - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, - commonEnvironmentVars, -} from './constants.js'; +import { RESOURCE_NAME_PREFIX, commonEnvironmentVars } from './constants.js'; describe('Metrics E2E tests, manual usage', () => { const testStack = new TestStack({ @@ -64,121 +57,111 @@ describe('Metrics E2E tests, manual usage', () => { times: invocations, invocationMode: 'SEQUENTIAL', }); - }, SETUP_TIMEOUT); + }); describe('ColdStart metrics', () => { - it( - 'captures the ColdStart Metric', - async () => { - const { EXPECTED_NAMESPACE: expectedNamespace } = commonEnvironmentVars; - - // Check coldstart metric dimensions - const coldStartMetrics = await getMetrics( - cloudwatchClient, - expectedNamespace, - 'ColdStart', - 1 - ); - expect(coldStartMetrics.Metrics?.length).toBe(1); - const coldStartMetric = coldStartMetrics.Metrics?.[0]; - expect(coldStartMetric?.Dimensions).toStrictEqual([ - { Name: 'service', Value: expectedServiceName }, - ]); - - // Check coldstart metric value - const adjustedStartTime = new Date(startTime.getTime() - 60 * 1000); - const endTime = new Date(new Date().getTime() + 60 * 1000); - const coldStartMetricStat = await cloudwatchClient.send( - new GetMetricStatisticsCommand({ - Namespace: expectedNamespace, - StartTime: adjustedStartTime, - Dimensions: [{ Name: 'service', Value: expectedServiceName }], - EndTime: endTime, - Period: 60, - MetricName: 'ColdStart', - Statistics: ['Sum'], - }) - ); - - // Despite lambda has been called twice, coldstart metric sum should only be 1 - const singleDataPoint = coldStartMetricStat.Datapoints - ? coldStartMetricStat.Datapoints[0] - : {}; - expect(singleDataPoint?.Sum).toBe(1); - }, - TEST_CASE_TIMEOUT - ); + it('captures the ColdStart Metric', async () => { + const { EXPECTED_NAMESPACE: expectedNamespace } = commonEnvironmentVars; + + // Check coldstart metric dimensions + const coldStartMetrics = await getMetrics( + cloudwatchClient, + expectedNamespace, + 'ColdStart', + 1 + ); + expect(coldStartMetrics.Metrics?.length).toBe(1); + const coldStartMetric = coldStartMetrics.Metrics?.[0]; + expect(coldStartMetric?.Dimensions).toStrictEqual([ + { Name: 'service', Value: expectedServiceName }, + ]); + + // Check coldstart metric value + const adjustedStartTime = new Date(startTime.getTime() - 60 * 1000); + const endTime = new Date(new Date().getTime() + 60 * 1000); + const coldStartMetricStat = await cloudwatchClient.send( + new GetMetricStatisticsCommand({ + Namespace: expectedNamespace, + StartTime: adjustedStartTime, + Dimensions: [{ Name: 'service', Value: expectedServiceName }], + EndTime: endTime, + Period: 60, + MetricName: 'ColdStart', + Statistics: ['Sum'], + }) + ); + + // Despite lambda has been called twice, coldstart metric sum should only be 1 + const singleDataPoint = coldStartMetricStat.Datapoints + ? coldStartMetricStat.Datapoints[0] + : {}; + expect(singleDataPoint?.Sum).toBe(1); + }); }); describe('Default and extra dimensions', () => { - it( - 'produces a Metric with the default and extra one dimensions', - async () => { - const { - EXPECTED_NAMESPACE: expectedNamespace, - EXPECTED_METRIC_NAME: expectedMetricName, - EXPECTED_METRIC_VALUE: expectedMetricValue, - EXPECTED_DEFAULT_DIMENSIONS: expectedDefaultDimensions, - EXPECTED_EXTRA_DIMENSION: expectedExtraDimension, - } = commonEnvironmentVars; - - // Check metric dimensions - const metrics = await getMetrics( - cloudwatchClient, - expectedNamespace, - expectedMetricName, - 1 - ); - - expect(metrics.Metrics?.length).toBe(1); - const metric = metrics.Metrics?.[0]; - const expectedDimensions = [ - { Name: 'service', Value: expectedServiceName }, - { - Name: Object.keys(expectedDefaultDimensions)[0], - Value: expectedDefaultDimensions.MyDimension, - }, - { - Name: Object.keys(expectedExtraDimension)[0], - Value: expectedExtraDimension.MyExtraDimension, - }, - ]; - expect(sortDimensions(metric?.Dimensions)).toStrictEqual( - sortDimensions(expectedDimensions) - ); - - // Check coldstart metric value - const adjustedStartTime = new Date( - startTime.getTime() - 3 * ONE_MINUTE - ); - const endTime = new Date(new Date().getTime() + ONE_MINUTE); - const metricStat = await cloudwatchClient.send( - new GetMetricStatisticsCommand({ - Namespace: expectedNamespace, - StartTime: adjustedStartTime, - Dimensions: expectedDimensions, - EndTime: endTime, - Period: 60, - MetricName: expectedMetricName, - Statistics: ['Sum'], - }) - ); - - // Since lambda has been called twice in this test and potentially more in others, metric sum should be at least of expectedMetricValue * invocationCount - const singleDataPoint = metricStat.Datapoints - ? metricStat.Datapoints[0] - : {}; - expect(singleDataPoint.Sum).toBeGreaterThanOrEqual( - Number.parseInt(expectedMetricValue) * invocations - ); - }, - TEST_CASE_TIMEOUT - ); + it('produces a Metric with the default and extra one dimensions', async () => { + const { + EXPECTED_NAMESPACE: expectedNamespace, + EXPECTED_METRIC_NAME: expectedMetricName, + EXPECTED_METRIC_VALUE: expectedMetricValue, + EXPECTED_DEFAULT_DIMENSIONS: expectedDefaultDimensions, + EXPECTED_EXTRA_DIMENSION: expectedExtraDimension, + } = commonEnvironmentVars; + + // Check metric dimensions + const metrics = await getMetrics( + cloudwatchClient, + expectedNamespace, + expectedMetricName, + 1 + ); + + expect(metrics.Metrics?.length).toBe(1); + const metric = metrics.Metrics?.[0]; + const expectedDimensions = [ + { Name: 'service', Value: expectedServiceName }, + { + Name: Object.keys(expectedDefaultDimensions)[0], + Value: expectedDefaultDimensions.MyDimension, + }, + { + Name: Object.keys(expectedExtraDimension)[0], + Value: expectedExtraDimension.MyExtraDimension, + }, + ]; + expect(sortDimensions(metric?.Dimensions)).toStrictEqual( + sortDimensions(expectedDimensions) + ); + + // Check coldstart metric value + const adjustedStartTime = new Date(startTime.getTime() - 3 * 60 * 1000); + const endTime = new Date(new Date().getTime() + 60 * 1000); + const metricStat = await cloudwatchClient.send( + new GetMetricStatisticsCommand({ + Namespace: expectedNamespace, + StartTime: adjustedStartTime, + Dimensions: expectedDimensions, + EndTime: endTime, + Period: 60, + MetricName: expectedMetricName, + Statistics: ['Sum'], + }) + ); + + // Since lambda has been called twice in this test and potentially more in others, metric sum should be at least of expectedMetricValue * invocationCount + const singleDataPoint = metricStat.Datapoints + ? metricStat.Datapoints[0] + : {}; + expect(singleDataPoint.Sum).toBeGreaterThanOrEqual( + Number.parseInt(expectedMetricValue) * invocations + ); + }); }); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/packages/metrics/tests/e2e/constants.ts b/packages/metrics/tests/e2e/constants.ts index dbffabf33a..7837b7e551 100644 --- a/packages/metrics/tests/e2e/constants.ts +++ b/packages/metrics/tests/e2e/constants.ts @@ -2,10 +2,6 @@ import { randomUUID } from 'node:crypto'; import { MetricUnit } from '../../src/index.js'; const RESOURCE_NAME_PREFIX = 'Metrics'; -const ONE_MINUTE = 60 * 1000; -const TEST_CASE_TIMEOUT = 3 * ONE_MINUTE; -const SETUP_TIMEOUT = 7 * ONE_MINUTE; -const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; const commonEnvironmentVars = { EXPECTED_METRIC_NAME: 'MyMetric', @@ -21,11 +17,4 @@ const commonEnvironmentVars = { POWERTOOLS_SERVICE_NAME: 'metrics-e2e-testing', }; -export { - RESOURCE_NAME_PREFIX, - ONE_MINUTE, - TEST_CASE_TIMEOUT, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - commonEnvironmentVars, -}; +export { RESOURCE_NAME_PREFIX, commonEnvironmentVars }; diff --git a/packages/metrics/tests/unit/coldStartMetric.test.ts b/packages/metrics/tests/unit/coldStartMetric.test.ts index 9a9cba37c9..e63ee9eb72 100644 --- a/packages/metrics/tests/unit/coldStartMetric.test.ts +++ b/packages/metrics/tests/unit/coldStartMetric.test.ts @@ -65,27 +65,62 @@ describe('ColdStart metric', () => { ); }); - it('includes the function name in the cold start metric', () => { + it('does not override the function name from constructor in the cold start metric', () => { // Prepare const functionName = 'my-function'; const metrics = new Metrics({ namespace: DEFAULT_NAMESPACE, + functionName: 'another-function', }); - metrics.setFunctionName(functionName); // Act - metrics.captureColdStartMetric(); + metrics.captureColdStartMetric(functionName); // Assess expect(console.log).toHaveEmittedEMFWith( expect.objectContaining({ service: 'hello-world', [COLD_START_METRIC]: 1, - function_name: 'my-function', + function_name: 'another-function', }) ); }); + it.each([ + { + case: 'empty string', + functionName: '', + }, + { + case: 'undefined', + functionName: undefined, + }, + ])( + 'does not include the function name if not set or invalid ($case)', + ({ functionName }) => { + // Prepare + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + }); + + // Act + metrics.captureColdStartMetric(functionName); + + // Assess + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + [COLD_START_METRIC]: 1, + }) + ); + expect(console.log).toHaveEmittedEMFWith( + expect.not.objectContaining({ + function_name: 'my-function', + }) + ); + } + ); + it('emits the metric only once', () => { // Prepare const metrics = new Metrics({ diff --git a/packages/metrics/tests/unit/initializeMetrics.test.ts b/packages/metrics/tests/unit/initializeMetrics.test.ts index 663328a294..a6b4a62a9d 100644 --- a/packages/metrics/tests/unit/initializeMetrics.test.ts +++ b/packages/metrics/tests/unit/initializeMetrics.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_NAMESPACE } from '../../src/constants.js'; +import { COLD_START_METRIC, DEFAULT_NAMESPACE } from '../../src/constants.js'; import { MetricUnit, Metrics } from '../../src/index.js'; import type { ConfigServiceInterface } from '../../src/types/index.js'; @@ -113,6 +113,123 @@ describe('Initialize Metrics', () => { ); }); + it('prioritizes the function name provided in the constructor', () => { + // Prepare + process.env.POWERTOOLS_METRICS_FUNCTION_NAME = 'another-function'; + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + functionName: 'my-function-name', + }); + + // Act + metrics.captureColdStartMetric(); + + // Assess + expect(console.log).toHaveBeenCalledTimes(1); + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + [COLD_START_METRIC]: 1, + function_name: 'my-function-name', + }) + ); + }); + + it('uses the function name provided in the environment variables', () => { + // Prepare + process.env.POWERTOOLS_METRICS_FUNCTION_NAME = 'another-function'; + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + }); + + // Act + metrics.captureColdStartMetric(); + + // Assess + expect(console.log).toHaveBeenCalledTimes(1); + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + [COLD_START_METRIC]: 1, + function_name: 'another-function', + }) + ); + }); + + it.each([ + { + case: 'an empty string', + functionName: '', + }, + { + case: 'undefined', + functionName: undefined, + }, + ])( + 'does not set the function name from env when is $case', + ({ functionName }) => { + // Prepare + process.env.POWERTOOLS_METRICS_FUNCTION_NAME = functionName; + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + }); + + // Act + metrics.captureColdStartMetric(); + + // Assess + expect(console.log).toHaveBeenCalledTimes(1); + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + [COLD_START_METRIC]: 1, + }) + ); + expect(console.log).toHaveEmittedEMFWith( + expect.not.objectContaining({ + function_name: expect.anything(), + }) + ); + } + ); + + it.each([ + { + case: 'an empty string', + functionName: '', + }, + { + case: 'undefined', + functionName: undefined, + }, + ])( + 'does not set the function name from constructor when is $case', + ({ functionName }) => { + // Prepare + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + functionName, + }); + + // Act + metrics.captureColdStartMetric(); + + // Assess + expect(console.log).toHaveBeenCalledTimes(1); + expect(console.log).toHaveEmittedEMFWith( + expect.objectContaining({ + service: 'hello-world', + [COLD_START_METRIC]: 1, + }) + ); + expect(console.log).toHaveEmittedEMFWith( + expect.not.objectContaining({ + function_name: expect.anything(), + }) + ); + } + ); + it('uses the custom config service provided', () => { // Prepare const configService = { @@ -128,6 +245,9 @@ describe('Initialize Metrics', () => { isValueTrue(value: string): boolean { return value === 'true'; }, + getFunctionName(): string { + return 'custom-function-name'; + }, }; const metrics = new Metrics({ singleMetric: true, diff --git a/packages/metrics/tests/unit/logMetrics.test.ts b/packages/metrics/tests/unit/logMetrics.test.ts index 90234c1e60..a0e2a33447 100644 --- a/packages/metrics/tests/unit/logMetrics.test.ts +++ b/packages/metrics/tests/unit/logMetrics.test.ts @@ -6,6 +6,11 @@ import { COLD_START_METRIC, DEFAULT_NAMESPACE } from '../../src/constants.js'; import { MetricUnit, Metrics } from '../../src/index.js'; import { logMetrics } from '../../src/middleware/middy.js'; +const contextFunctionName = 'context-function-name'; +const contextWithFunctionName = { + functionName: contextFunctionName, +} as Context; + describe('LogMetrics decorator & Middy.js middleware', () => { const ENVIRONMENT_VARIABLES = process.env; @@ -45,8 +50,8 @@ describe('LogMetrics decorator & Middy.js middleware', () => { const handler = lambda.handler.bind(lambda); // Act - await handler({}, {} as Context); - await handler({}, {} as Context); + await handler({}, contextWithFunctionName); + await handler({}, contextWithFunctionName); // Assess expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(2); @@ -56,6 +61,7 @@ describe('LogMetrics decorator & Middy.js middleware', () => { expect.objectContaining({ [COLD_START_METRIC]: 1, service: 'hello-world', + function_name: contextFunctionName, }) ); expect(console.log).toHaveEmittedNthMetricWith( @@ -82,6 +88,94 @@ describe('LogMetrics decorator & Middy.js middleware', () => { ); }); + it('default function name in the cold start metric to context.functionName when using decorator', async () => { + // Prepare + const metrics = new Metrics({ + singleMetric: false, + namespace: DEFAULT_NAMESPACE, + }); + + vi.spyOn(metrics, 'publishStoredMetrics'); + class Test { + readonly #metricName: string; + + public constructor(name: string) { + this.#metricName = name; + } + + @metrics.logMetrics({ captureColdStartMetric: true }) + async handler(_event: unknown, _context: Context) { + this.addGreetingMetric(); + } + + addGreetingMetric() { + metrics.addMetric(this.#metricName, MetricUnit.Count, 1); + } + } + const lambda = new Test('greetings'); + const handler = lambda.handler.bind(lambda); + + // Act + await handler({}, contextWithFunctionName); + + // Assess + expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(1); + expect(console.log).toHaveBeenCalledTimes(2); + expect(console.log).toHaveEmittedNthEMFWith( + 1, + expect.objectContaining({ + [COLD_START_METRIC]: 1, + service: 'hello-world', + function_name: contextFunctionName, + }) + ); + }); + + it('does not override existing function name in the cold start metric when using decorator', async () => { + // Prepare + const functionName = 'function-name'; + const metrics = new Metrics({ + singleMetric: false, + namespace: DEFAULT_NAMESPACE, + functionName, + }); + + vi.spyOn(metrics, 'publishStoredMetrics'); + class Test { + readonly #metricName: string; + + public constructor(name: string) { + this.#metricName = name; + } + + @metrics.logMetrics({ captureColdStartMetric: true }) + async handler(_event: unknown, _context: Context) { + this.addGreetingMetric(); + } + + addGreetingMetric() { + metrics.addMetric(this.#metricName, MetricUnit.Count, 1); + } + } + const lambda = new Test('greetings'); + const handler = lambda.handler.bind(lambda); + + // Act + await handler({}, contextWithFunctionName); + + // Assess + expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(1); + expect(console.log).toHaveBeenCalledTimes(2); + expect(console.log).toHaveEmittedNthEMFWith( + 1, + expect.objectContaining({ + [COLD_START_METRIC]: 1, + service: 'hello-world', + function_name: functionName, + }) + ); + }); + it('captures the cold start metric on the first invocation when using the Middy.js middleware', async () => { // Prepare const metrics = new Metrics({ @@ -94,8 +188,8 @@ describe('LogMetrics decorator & Middy.js middleware', () => { }).use(logMetrics(metrics, { captureColdStartMetric: true })); // Act - await handler({}, {} as Context); - await handler({}, {} as Context); + await handler({}, contextWithFunctionName); + await handler({}, contextWithFunctionName); // Assess expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(2); @@ -109,6 +203,60 @@ describe('LogMetrics decorator & Middy.js middleware', () => { ); }); + it('default function name in the cold start metric to context.functionName when using the Middy.js middleware', async () => { + // Prepare + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + }); + + vi.spyOn(metrics, 'publishStoredMetrics'); + const handler = middy(async () => { + metrics.addMetric('greetings', MetricUnit.Count, 1); + }).use(logMetrics(metrics, { captureColdStartMetric: true })); + + // Act + await handler({}, contextWithFunctionName); + + // Assess + expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(1); + expect(console.log).toHaveEmittedNthEMFWith( + 1, + expect.objectContaining({ + [COLD_START_METRIC]: 1, + service: 'hello-world', + function_name: contextFunctionName, + }) + ); + }); + + it('does not override existing function name in the cold start metric when using the Middy.js middleware', async () => { + // Prepare + const functionName = 'my-function'; + const metrics = new Metrics({ + namespace: DEFAULT_NAMESPACE, + functionName, + }); + + vi.spyOn(metrics, 'publishStoredMetrics'); + const handler = middy(async () => { + metrics.addMetric('greetings', MetricUnit.Count, 1); + }).use(logMetrics(metrics, { captureColdStartMetric: true })); + + // Act + await handler({}, contextWithFunctionName); + + // Assess + expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(1); + expect(console.log).toHaveEmittedNthEMFWith( + 1, + expect.objectContaining({ + [COLD_START_METRIC]: 1, + service: 'hello-world', + function_name: functionName, + }) + ); + }); + it('includes default dimensions passed in the decorator', async () => { // Prepare const metrics = new Metrics({ @@ -125,7 +273,7 @@ describe('LogMetrics decorator & Middy.js middleware', () => { const handler = lambda.handler.bind(lambda); // Act - await handler({}, {} as Context); + await handler({}, contextWithFunctionName); // Assess expect(console.log).toHaveBeenCalledTimes(1); @@ -157,7 +305,7 @@ describe('LogMetrics decorator & Middy.js middleware', () => { ); // Act - await handler({}, {} as Context); + await handler({}, contextWithFunctionName); // Assess expect(console.log).toHaveBeenCalledTimes(1); @@ -261,8 +409,8 @@ describe('LogMetrics decorator & Middy.js middleware', () => { .use(myCustomMiddleware()); // Act - await handler({ idx: 0 }, {} as Context); - await handler({ idx: 1 }, {} as Context); + await handler({ idx: 0 }, contextWithFunctionName); + await handler({ idx: 1 }, contextWithFunctionName); // Assess expect(metrics.publishStoredMetrics).toHaveBeenCalledTimes(2); diff --git a/packages/metrics/vitest.config.ts b/packages/metrics/vitest.config.ts index 9f1196ef1f..baa5cf7463 100644 --- a/packages/metrics/vitest.config.ts +++ b/packages/metrics/vitest.config.ts @@ -4,5 +4,7 @@ export default defineProject({ test: { environment: 'node', setupFiles: ['../testing/src/setupEnv.ts'], + hookTimeout: 1_000 * 60 * 10, // 10 minutes + testTimeout: 1_000 * 60 * 3, // 3 minutes }, }); diff --git a/packages/parameters/CHANGELOG.md b/packages/parameters/CHANGELOG.md index 4bbe48afe4..7cf5a9e4c2 100644 --- a/packages/parameters/CHANGELOG.md +++ b/packages/parameters/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + +**Note:** Version bump only for package @aws-lambda-powertools/parameters + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) **Note:** Version bump only for package @aws-lambda-powertools/parameters diff --git a/packages/parameters/README.md b/packages/parameters/README.md index d3544d96b7..e6a32c9d4b 100644 --- a/packages/parameters/README.md +++ b/packages/parameters/README.md @@ -229,6 +229,7 @@ The following companies, among others, use Powertools: - [Elva](https://elva-group.com) - [Flyweight](https://flyweight.io/) - [globaldatanet](https://globaldatanet.com/) +- [Guild](https://guild.com) - [Hashnode](https://hashnode.com/) - [LocalStack](https://localstack.cloud/) - [Perfect Post](https://www.perfectpost.fr) diff --git a/packages/parameters/package.json b/packages/parameters/package.json index 57260ad26b..5e0969bdf5 100644 --- a/packages/parameters/package.json +++ b/packages/parameters/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/parameters", - "version": "2.16.0", + "version": "2.17.0", "description": "The parameters package for the Powertools for AWS Lambda (TypeScript) library", "author": { "name": "Amazon Web Services", @@ -156,16 +156,16 @@ ], "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-appconfigdata": "^3.758.0", - "@aws-sdk/client-dynamodb": "^3.758.0", - "@aws-sdk/client-secrets-manager": "^3.758.0", - "@aws-sdk/client-ssm": "^3.759.0", - "@aws-sdk/util-dynamodb": "^3.758.0", + "@aws-sdk/client-appconfigdata": "^3.772.0", + "@aws-sdk/client-dynamodb": "^3.772.0", + "@aws-sdk/client-secrets-manager": "^3.772.0", + "@aws-sdk/client-ssm": "^3.772.0", + "@aws-sdk/util-dynamodb": "^3.772.0", "@smithy/util-base64": "^4.0.0", "aws-sdk-client-mock": "^4.1.0" }, "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0" + "@aws-lambda-powertools/commons": "^2.17.0" }, "peerDependencies": { "@aws-sdk/client-appconfigdata": ">=3.x", diff --git a/packages/parameters/src/types/BaseProvider.ts b/packages/parameters/src/types/BaseProvider.ts index 44c3ee417e..3154bb0893 100644 --- a/packages/parameters/src/types/BaseProvider.ts +++ b/packages/parameters/src/types/BaseProvider.ts @@ -17,7 +17,7 @@ type BaseProviderConstructorOptions = { * * If the `awsSdkV3Client` is not provided, this will be used to create a new client. */ - awsSdkV3ClientPrototype: new ( + awsSdkV3ClientPrototype?: new ( config?: unknown ) => unknown; }; diff --git a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts index 3bd7e3a9b7..5159600f79 100644 --- a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts +++ b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts @@ -8,12 +8,7 @@ import { TestNodejsFunction } from '@aws-lambda-powertools/testing-utils/resourc import { toBase64 } from '@smithy/util-base64'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { TestAppConfigWithProfiles } from '../helpers/resources.js'; -import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, -} from './constants.js'; +import { RESOURCE_NAME_PREFIX } from './constants.js'; /** * This test suite deploys a CDK stack with a Lambda function and a number of AppConfig parameters. @@ -172,7 +167,7 @@ describe('Parameters E2E tests, AppConfig provider', () => { invocationLogs = await invokeFunctionOnce({ functionName, }); - }, SETUP_TIMEOUT); + }); describe('AppConfigProvider usage', () => { // Test 1 - get a single parameter as-is (no transformation - should return an Uint8Array) @@ -222,62 +217,50 @@ describe('Parameters E2E tests, AppConfig provider', () => { // Test 5 - get parameter twice with middleware, which counts the number // of requests, we check later if we only called AppConfig API once - it( - 'should retrieve single parameter cached', - () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[4]); + it('should retrieve single parameter cached', () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[4]); - expect(testLog).toStrictEqual({ - test: 'get-cached', - value: 1, - }); - }, - TEST_CASE_TIMEOUT - ); + expect(testLog).toStrictEqual({ + test: 'get-cached', + value: 1, + }); + }); // Test 6 - get parameter twice, but force fetch 2nd time, // we count number of SDK requests and check that we made two API calls - it( - 'should retrieve single parameter twice without caching', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[5]); + it('should retrieve single parameter twice without caching', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[5]); - expect(testLog).toStrictEqual({ - test: 'get-forced', - value: 2, - }); - }, - TEST_CASE_TIMEOUT - ); + expect(testLog).toStrictEqual({ + test: 'get-forced', + value: 2, + }); + }); // Test 7 - get parameter twice, using maxAge to avoid primary cache // we count number of SDK requests and check that we made two API calls // and check that the values match - it( - 'should retrieve single parameter twice, with expiration between and matching values', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[6]); - const result = freeFormPlainTextValue; + it('should retrieve single parameter twice, with expiration between and matching values', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[6]); + const result = freeFormPlainTextValue; - expect(testLog).toStrictEqual({ - test: 'get-expired', - value: { - counter: 2, - result1: result, - result2: result, - }, - }); - }, - TEST_CASE_TIMEOUT - ); + expect(testLog).toStrictEqual({ + test: 'get-expired', + value: { + counter: 2, + result1: result, + result2: result, + }, + }); + }); }); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/packages/parameters/tests/e2e/constants.ts b/packages/parameters/tests/e2e/constants.ts index b9b9e6ef30..42e1837795 100644 --- a/packages/parameters/tests/e2e/constants.ts +++ b/packages/parameters/tests/e2e/constants.ts @@ -1,5 +1 @@ export const RESOURCE_NAME_PREFIX = 'Parameters'; -export const ONE_MINUTE = 60 * 1000; -export const TEST_CASE_TIMEOUT = 3 * ONE_MINUTE; -export const SETUP_TIMEOUT = 7 * ONE_MINUTE; -export const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; diff --git a/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts b/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts index 17c56600d2..333bbb7fce 100644 --- a/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts +++ b/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts @@ -8,12 +8,7 @@ import { TestNodejsFunction } from '@aws-lambda-powertools/testing-utils/resourc import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { TestDynamodbTableWithItems } from '../helpers/resources.js'; -import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, -} from './constants.js'; +import { RESOURCE_NAME_PREFIX } from './constants.js'; /** * This test suite deploys a CDK stack with a Lambda function and a number of DynamoDB tables. @@ -264,68 +259,52 @@ describe('Parameters E2E tests, dynamoDB provider', () => { invocationLogs = await invokeFunctionOnce({ functionName, }); - }, SETUP_TIMEOUT); + }); describe('DynamoDBProvider usage', () => { // Test 1 - get a single parameter with default options (keyAttr: 'id', valueAttr: 'value') - it( - 'should retrieve a single parameter', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[0]); + it('should retrieve a single parameter', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[0]); - expect(testLog).toStrictEqual({ - test: 'get', - value: 'foo', - }); - }, - TEST_CASE_TIMEOUT - ); + expect(testLog).toStrictEqual({ + test: 'get', + value: 'foo', + }); + }); // Test 2 - get multiple parameters with default options (keyAttr: 'id', sortAttr: 'sk', valueAttr: 'value') - it( - 'should retrieve multiple parameters', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[1]); + it('should retrieve multiple parameters', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[1]); - expect(testLog).toStrictEqual({ - test: 'get-multiple', - value: { config: 'bar', key: 'baz' }, - }); - }, - TEST_CASE_TIMEOUT - ); + expect(testLog).toStrictEqual({ + test: 'get-multiple', + value: { config: 'bar', key: 'baz' }, + }); + }); // Test 3 - get a single parameter with custom options (keyAttr: 'key', valueAttr: 'val') - it( - 'should retrieve a single parameter', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[2]); + it('should retrieve a single parameter', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[2]); - expect(testLog).toStrictEqual({ - test: 'get-custom', - value: 'foo', - }); - }, - TEST_CASE_TIMEOUT - ); + expect(testLog).toStrictEqual({ + test: 'get-custom', + value: 'foo', + }); + }); // Test 4 - get multiple parameters with custom options (keyAttr: 'key', sortAttr: 'sort', valueAttr: 'val') - it( - 'should retrieve multiple parameters', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[3]); + it('should retrieve multiple parameters', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[3]); - expect(testLog).toStrictEqual({ - test: 'get-multiple-custom', - value: { config: 'bar', key: 'baz' }, - }); - }, - TEST_CASE_TIMEOUT - ); + expect(testLog).toStrictEqual({ + test: 'get-multiple-custom', + value: { config: 'bar', key: 'baz' }, + }); + }); // Test 5 - get a single parameter with json transform it('should retrieve a single parameter with json transform', async () => { @@ -390,5 +369,5 @@ describe('Parameters E2E tests, dynamoDB provider', () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/packages/parameters/tests/e2e/secretsProvider.class.test.ts b/packages/parameters/tests/e2e/secretsProvider.class.test.ts index 4acbd65eba..87467a9b06 100644 --- a/packages/parameters/tests/e2e/secretsProvider.class.test.ts +++ b/packages/parameters/tests/e2e/secretsProvider.class.test.ts @@ -8,12 +8,7 @@ import { TestNodejsFunction } from '@aws-lambda-powertools/testing-utils/resourc import { SecretValue } from 'aws-cdk-lib'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { TestSecret } from '../helpers/resources.js'; -import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, -} from './constants.js'; +import { RESOURCE_NAME_PREFIX } from './constants.js'; /** * Collection of e2e tests for SecretsProvider utility. @@ -138,50 +133,38 @@ describe('Parameters E2E tests, Secrets Manager provider', () => { invocationLogs = await invokeFunctionOnce({ functionName, }); - }, SETUP_TIMEOUT); + }); describe('SecretsProvider usage', () => { - it( - 'should retrieve a secret as plain string', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[0]); - - expect(testLog).toStrictEqual({ - test: 'get-plain', - value: 'foo', - }); - }, - TEST_CASE_TIMEOUT - ); + it('should retrieve a secret as plain string', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[0]); + + expect(testLog).toStrictEqual({ + test: 'get-plain', + value: 'foo', + }); + }); - it( - 'should retrieve a secret using transform json option', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[1]); + it('should retrieve a secret using transform json option', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[1]); - expect(testLog).toStrictEqual({ - test: 'get-transform-json', - value: { foo: 'bar' }, - }); - }, - TEST_CASE_TIMEOUT - ); + expect(testLog).toStrictEqual({ + test: 'get-transform-json', + value: { foo: 'bar' }, + }); + }); - it( - 'should retrieve a secret using transform binary option', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[2]); + it('should retrieve a secret using transform binary option', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[2]); - expect(testLog).toStrictEqual({ - test: 'get-transform-binary', - value: 'foo', - }); - }, - TEST_CASE_TIMEOUT - ); + expect(testLog).toStrictEqual({ + test: 'get-transform-binary', + value: 'foo', + }); + }); }); it('should retrieve a secret twice with cached value', async () => { @@ -210,5 +193,5 @@ describe('Parameters E2E tests, Secrets Manager provider', () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/packages/parameters/tests/e2e/ssmProvider.class.test.ts b/packages/parameters/tests/e2e/ssmProvider.class.test.ts index 71d7f75a1d..fa5bf8931b 100644 --- a/packages/parameters/tests/e2e/ssmProvider.class.test.ts +++ b/packages/parameters/tests/e2e/ssmProvider.class.test.ts @@ -10,12 +10,7 @@ import { TestSecureStringParameter, TestStringParameter, } from '../helpers/resources.js'; -import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, -} from './constants.js'; +import { RESOURCE_NAME_PREFIX } from './constants.js'; /** * This test suite deploys a CDK stack with a Lambda function and a number of SSM parameters. @@ -189,200 +184,160 @@ describe('Parameters E2E tests, SSM provider', () => { invocationLogs = await invokeFunctionOnce({ functionName, }); - }, SETUP_TIMEOUT); + }); describe('SSMProvider usage', () => { // Test 1 - get a single parameter by name with default options - it( - 'should retrieve a single parameter', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[0]); - - expect(testLog).toStrictEqual({ - test: 'get', - value: paramAValue, - }); - }, - TEST_CASE_TIMEOUT - ); + it('should retrieve a single parameter', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[0]); + + expect(testLog).toStrictEqual({ + test: 'get', + value: paramAValue, + }); + }); // Test 2 - get a single parameter by name with decrypt - it( - 'should retrieve a single parameter with decryption', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[1]); - - expect(testLog).toStrictEqual({ - test: 'get-decrypt', - value: paramEncryptedAValue, - }); - }, - TEST_CASE_TIMEOUT - ); + it('should retrieve a single parameter with decryption', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[1]); + + expect(testLog).toStrictEqual({ + test: 'get-decrypt', + value: paramEncryptedAValue, + }); + }); // Test 3 - get multiple parameters by path with default options - it( - 'should retrieve multiple parameters', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[2]); - const expectedParameterNameA = paramA.substring( - paramA.lastIndexOf('/') + 1 - ); - const expectedParameterNameB = paramB.substring( - paramB.lastIndexOf('/') + 1 - ); - - expect(testLog).toStrictEqual({ - test: 'get-multiple', - value: { - [expectedParameterNameA]: paramAValue, - [expectedParameterNameB]: paramBValue, - }, - }); - }, - TEST_CASE_TIMEOUT - ); + it('should retrieve multiple parameters', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[2]); + const expectedParameterNameA = paramA.substring( + paramA.lastIndexOf('/') + 1 + ); + const expectedParameterNameB = paramB.substring( + paramB.lastIndexOf('/') + 1 + ); + + expect(testLog).toStrictEqual({ + test: 'get-multiple', + value: { + [expectedParameterNameA]: paramAValue, + [expectedParameterNameB]: paramBValue, + }, + }); + }); // Test 4 - get multiple parameters by path recursively // (aka. get all parameters under a path recursively) i.e. // given /param, retrieve /param/get/a and /param/get/b (note path depth) - it( - 'should retrieve multiple parameters recursively', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[3]); - const expectedParameterNameA = paramA.substring( - paramA.lastIndexOf('/') + 1 - ); - const expectedParameterNameB = paramB.substring( - paramB.lastIndexOf('/') + 1 - ); - - expect(testLog).toStrictEqual({ - test: 'get-multiple-recursive', - value: { - [expectedParameterNameA]: paramAValue, - [expectedParameterNameB]: paramBValue, - }, - }); - }, - TEST_CASE_TIMEOUT - ); + it('should retrieve multiple parameters recursively', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[3]); + const expectedParameterNameA = paramA.substring( + paramA.lastIndexOf('/') + 1 + ); + const expectedParameterNameB = paramB.substring( + paramB.lastIndexOf('/') + 1 + ); + + expect(testLog).toStrictEqual({ + test: 'get-multiple-recursive', + value: { + [expectedParameterNameA]: paramAValue, + [expectedParameterNameB]: paramBValue, + }, + }); + }); - it( - 'should retrieve multiple parameters with decryption', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[4]); - const expectedParameterNameA = paramEncryptedA.substring( - paramEncryptedA.lastIndexOf('/') + 1 - ); - const expectedParameterNameB = paramEncryptedB.substring( - paramEncryptedB.lastIndexOf('/') + 1 - ); - - expect(testLog).toStrictEqual({ - test: 'get-multiple-decrypt', - value: { - [expectedParameterNameA]: paramEncryptedAValue, - [expectedParameterNameB]: paramEncryptedBValue, - }, - }); - }, - TEST_CASE_TIMEOUT - ); + it('should retrieve multiple parameters with decryption', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[4]); + const expectedParameterNameA = paramEncryptedA.substring( + paramEncryptedA.lastIndexOf('/') + 1 + ); + const expectedParameterNameB = paramEncryptedB.substring( + paramEncryptedB.lastIndexOf('/') + 1 + ); + + expect(testLog).toStrictEqual({ + test: 'get-multiple-decrypt', + value: { + [expectedParameterNameA]: paramEncryptedAValue, + [expectedParameterNameB]: paramEncryptedBValue, + }, + }); + }); // Test 6 - get multiple parameters by name with default options - it( - 'should retrieve multiple parameters by name', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[5]); - - expect(testLog).toStrictEqual({ - test: 'get-multiple-by-name', - value: { - [paramA]: paramAValue, - [paramB]: paramBValue, - }, - }); - }, - TEST_CASE_TIMEOUT - ); + it('should retrieve multiple parameters by name', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[5]); + + expect(testLog).toStrictEqual({ + test: 'get-multiple-by-name', + value: { + [paramA]: paramAValue, + [paramB]: paramBValue, + }, + }); + }); // Test 7 - get multiple parameters by name, some of them encrypted and some not - it( - 'should retrieve multiple parameters by name with mixed decryption', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[6]); - - expect(testLog).toStrictEqual({ - test: 'get-multiple-by-name-mixed-decrypt', - value: { - [paramEncryptedA]: paramEncryptedAValue, - [paramEncryptedB]: paramEncryptedBValue, - [paramA]: paramAValue, - }, - }); - }, - TEST_CASE_TIMEOUT - ); + it('should retrieve multiple parameters by name with mixed decryption', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[6]); + + expect(testLog).toStrictEqual({ + test: 'get-multiple-by-name-mixed-decrypt', + value: { + [paramEncryptedA]: paramEncryptedAValue, + [paramEncryptedB]: paramEncryptedBValue, + [paramA]: paramAValue, + }, + }); + }); // Test 8 - get parameter twice with middleware, which counts the number // of requests, we check later if we only called SSM API once - it( - 'should retrieve single parameter cached', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[7]); - - expect(testLog).toStrictEqual({ - test: 'get-cached', - value: 1, - }); - }, - TEST_CASE_TIMEOUT - ); + it('should retrieve single parameter cached', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[7]); + + expect(testLog).toStrictEqual({ + test: 'get-cached', + value: 1, + }); + }); // Test 9 - get parameter twice, but force fetch 2nd time, // we count number of SDK requests and check that we made two API calls - it( - 'should retrieve single parameter twice without caching', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[8]); - - expect(testLog).toStrictEqual({ - test: 'get-forced', - value: 2, - }); - }, - TEST_CASE_TIMEOUT - ); + it('should retrieve single parameter twice without caching', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[8]); + + expect(testLog).toStrictEqual({ + test: 'get-forced', + value: 2, + }); + }); // Test 10 - store and overwrite single parameter - it( - 'should store and overwrite single parameter', - async () => { - const logs = invocationLogs.getFunctionLogs(); - const testLog = TestInvocationLogs.parseFunctionLog(logs[9]); - - expect(testLog).toStrictEqual({ - test: 'set', - value: 'overwritten', - }); - }, - TEST_CASE_TIMEOUT - ); + it('should store and overwrite single parameter', async () => { + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[9]); + + expect(testLog).toStrictEqual({ + test: 'set', + value: 'overwritten', + }); + }); }); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); }); diff --git a/packages/parameters/vitest.config.ts b/packages/parameters/vitest.config.ts index 9f1196ef1f..baa5cf7463 100644 --- a/packages/parameters/vitest.config.ts +++ b/packages/parameters/vitest.config.ts @@ -4,5 +4,7 @@ export default defineProject({ test: { environment: 'node', setupFiles: ['../testing/src/setupEnv.ts'], + hookTimeout: 1_000 * 60 * 10, // 10 minutes + testTimeout: 1_000 * 60 * 3, // 3 minutes }, }); diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md index e0267efed4..cd69942fca 100644 --- a/packages/parser/CHANGELOG.md +++ b/packages/parser/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + + +### Bug Fixes + +* **parser:** ddb base schema + other exports ([#3741](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3741)) ([51a3410](https://github.com/aws-powertools/powertools-lambda-typescript/commit/51a3410be8502496362d5ed13a64fe55691604ba)) + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) diff --git a/packages/parser/README.md b/packages/parser/README.md index d18ab9b107..c5176766df 100644 --- a/packages/parser/README.md +++ b/packages/parser/README.md @@ -316,6 +316,7 @@ The following companies, among others, use Powertools: - [Elva](https://elva-group.com) - [Flyweight](https://flyweight.io/) - [globaldatanet](https://globaldatanet.com/) +- [Guild](https://guild.com) - [Hashnode](https://hashnode.com/) - [LocalStack](https://localstack.cloud/) - [Perfect Post](https://www.perfectpost.fr) diff --git a/packages/parser/package.json b/packages/parser/package.json index 9619e754a3..03ba6ebe3b 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/parser", - "version": "2.16.0", + "version": "2.17.0", "description": "The parser package for the Powertools for AWS Lambda (TypeScript) library.", "author": { "name": "Amazon Web Services", @@ -41,208 +41,94 @@ } }, "./middleware": { - "require": "./lib/cjs/middleware/parser.js", - "import": "./lib/esm/middleware/parser.js" + "require": { + "types": "./lib/cjs/middleware/index.d.ts", + "default": "./lib/cjs/middleware/index.js" + }, + "import": { + "types": "./lib/esm/middleware/index.d.ts", + "default": "./lib/esm/middleware/index.js" + } }, "./schemas": { - "require": "./lib/cjs/schemas/index.js", - "import": "./lib/esm/schemas/index.js" - }, - "./schemas/alb": { - "require": "./lib/cjs/schemas/alb.js", - "import": "./lib/esm/schemas/alb.js" - }, - "./schemas/api-gateway": { - "require": "./lib/cjs/schemas/apigw.js", - "import": "./lib/esm/schemas/apigw.js" - }, - "./schemas/api-gatewayv2": { - "require": "./lib/cjs/schemas/apigwv2.js", - "import": "./lib/esm/schemas/apigwv2.js" - }, - "./schemas/appsync": { - "require": "./lib/cjs/schemas/appsync.js", - "import": "./lib/esm/schemas/appsync.js" - }, - "./schemas/cloudformation-custom-resources": { - "require": "./lib/cjs/schemas/cloudformation-custom-resources.js", - "import": "./lib/esm/schemas/cloudformation-custom-resources.js" - }, - "./schemas/cloudwatch": { - "require": "./lib/cjs/schemas/cloudwatch.js", - "import": "./lib/esm/schemas/cloudwatch.js" - }, - "./schemas/dynamodb": { - "require": "./lib/cjs/schemas/dynamodb.js", - "import": "./lib/esm/schemas/dynamodb.js" - }, - "./schemas/eventbridge": { - "require": "./lib/cjs/schemas/eventbridge.js", - "import": "./lib/esm/schemas/eventbridge.js" - }, - "./schemas/kafka": { - "require": "./lib/cjs/schemas/kafka.js", - "import": "./lib/esm/schemas/kafka.js" - }, - "./schemas/kinesis": { - "require": "./lib/cjs/schemas/kinesis.js", - "import": "./lib/esm/schemas/kinesis.js" - }, - "./schemas/kinesis-firehose": { - "require": "./lib/cjs/schemas/kinesis-firehose.js", - "import": "./lib/esm/schemas/kinesis-firehose.js" - }, - "./schemas/lambda": { - "require": "./lib/cjs/schemas/lambda.js", - "import": "./lib/esm/schemas/lambda.js" - }, - "./schemas/s3": { - "require": "./lib/cjs/schemas/s3.js", - "import": "./lib/esm/schemas/s3.js" - }, - "./schemas/ses": { - "require": "./lib/cjs/schemas/ses.js", - "import": "./lib/esm/schemas/ses.js" - }, - "./schemas/sns": { - "require": "./lib/cjs/schemas/sns.js", - "import": "./lib/esm/schemas/sns.js" - }, - "./schemas/sqs": { - "require": "./lib/cjs/schemas/sqs.js", - "import": "./lib/esm/schemas/sqs.js" - }, - "./schemas/transfer-family": { - "require": "./lib/cjs/schemas/transfer-family.js", - "import": "./lib/esm/schemas/transfer-family.js" - }, - "./schemas/vpc-lattice": { - "require": "./lib/cjs/schemas/vpc-lattice.js", - "import": "./lib/esm/schemas/vpc-lattice.js" + "require": { + "types": "./lib/cjs/schemas/index.d.ts", + "default": "./lib/cjs/schemas/index.js" + }, + "import": { + "types": "./lib/esm/schemas/index.d.ts", + "default": "./lib/esm/schemas/index.js" + } }, - "./schemas/vpc-latticev2": { - "require": "./lib/cjs/schemas/vpc-latticev2.js", - "import": "./lib/esm/schemas/vpc-latticev2.js" + "./schemas/*": { + "require": { + "types": "./lib/cjs/schemas/*.d.ts", + "default": "./lib/cjs/schemas/*.js" + }, + "import": { + "types": "./lib/esm/schemas/*.d.ts", + "default": "./lib/esm/schemas/*.js" + } }, "./envelopes": { - "require": "./lib/cjs/envelopes/index.js", - "import": "./lib/esm/envelopes/index.js" + "require": { + "types": "./lib/cjs/envelopes/index.d.ts", + "default": "./lib/cjs/envelopes/index.js" + }, + "import": { + "types": "./lib/esm/envelopes/index.d.ts", + "default": "./lib/esm/envelopes/index.js" + } }, "./envelopes/*": { - "require": "./lib/cjs/envelopes/*.js", - "import": "./lib/esm/envelopes/*.js" + "require": { + "types": "./lib/cjs/envelopes/*.d.ts", + "default": "./lib/cjs/envelopes/*.js" + }, + "import": { + "types": "./lib/esm/envelopes/*.d.ts", + "default": "./lib/esm/envelopes/*.js" + } }, "./helpers": { - "require": "./lib/cjs/helpers.js", - "import": "./lib/esm/helpers.js" + "require": { + "types": "./lib/cjs/helpers/index.d.ts", + "default": "./lib/cjs/helpers/index.js" + }, + "import": { + "types": "./lib/esm/helpers/index.d.ts", + "default": "./lib/esm/helpers/index.js" + } }, "./helpers/dynamodb": { - "require": "./lib/cjs/helpers/dynamodb.js", - "import": "./lib/esm/helpers/dynamodb.js" + "require": { + "types": "./lib/cjs/helpers/dynamodb.d.ts", + "default": "./lib/cjs/helpers/dynamodb.js" + }, + "import": { + "types": "./lib/esm/helpers/dynamodb.d.ts", + "default": "./lib/esm/helpers/dynamodb.js" + } }, "./types": { - "require": "./lib/cjs/types/index.js", - "import": "./lib/esm/types/index.js" - } - }, - "typesVersions": { - "*": { - "types": [ - "./lib/cjs/types/index.d.ts", - "./lib/esm/types/index.d.ts" - ], - "middleware": [ - "./lib/cjs/middleware/parser.d.ts", - "./lib/esm/middleware/parser.d.ts" - ], - "schemas": [ - "./lib/cjs/schemas/index.d.ts", - "./lib/esm/schemas/index.d.ts" - ], - "schemas/alb": [ - "./lib/cjs/schemas/alb.d.ts", - "./lib/esm/schemas/alb.d.ts" - ], - "schemas/api-gateway": [ - "./lib/cjs/schemas/apigw.d.ts", - "./lib/esm/schemas/apigw.d.ts" - ], - "schemas/api-gatewayv2": [ - "./lib/cjs/schemas/apigwv2.d.ts", - "./lib/esm/schemas/apigwv2.d.ts" - ], - "schemas/appsync": [ - "./lib/cjs/schemas/appsync.d.ts", - "./lib/esm/schemas/appsync.d.ts" - ], - "schemas/cloudformation-custom-resources": [ - "./lib/cjs/schemas/cloudformation-custom-resources.d.ts", - "./lib/esm/schemas/cloudformation-custom-resources.d.ts" - ], - "schemas/cloudwatch": [ - "./lib/cjs/schemas/cloudwatch.d.ts", - "./lib/esm/schemas/cloudwatch.d.ts" - ], - "schemas/dynamodb": [ - "./lib/cjs/schemas/dynamodb.d.ts", - "./lib/esm/schemas/dynamodb.d.ts" - ], - "schemas/eventbridge": [ - "./lib/cjs/schemas/eventbridge.d.ts", - "./lib/esm/schemas/eventbridge.d.ts" - ], - "schemas/kafka": [ - "./lib/cjs/schemas/kafka.d.ts", - "./lib/esm/schemas/kafka.d.ts" - ], - "schemas/kinesis": [ - "./lib/cjs/schemas/kinesis.d.ts", - "./lib/esm/schemas/kinesis.d.ts" - ], - "schemas/kinesis-firehose": [ - "./lib/cjs/schemas/kinesis-firehose.d.ts", - "./lib/esm/schemas/kinesis-firehose.d.ts" - ], - "schemas/lambda": [ - "./lib/cjs/schemas/lambda.d.ts", - "./lib/esm/schemas/lambda.d.ts" - ], - "schemas/s3": [ - "./lib/cjs/schemas/s3.d.ts", - "./lib/esm/schemas/s3.d.ts" - ], - "schemas/ses": [ - "./lib/cjs/schemas/ses.d.ts", - "./lib/esm/schemas/ses.d.ts" - ], - "schemas/sns": [ - "./lib/cjs/schemas/sns.d.ts", - "./lib/esm/schemas/sns.d.ts" - ], - "schemas/sqs": [ - "./lib/cjs/schemas/sqs.d.ts", - "./lib/esm/schemas/sqs.d.ts" - ], - "schemas/vpc-lattice": [ - "./lib/cjs/schemas/vpc-lattice.d.ts", - "./lib/esm/schemas/vpc-lattice.d.ts" - ], - "schemas/vpc-latticev2": [ - "./lib/cjs/schemas/vpc-latticev2.d.ts", - "./lib/esm/schemas/vpc-latticev2.d.ts" - ], - "envelopes": [ - "./lib/cjs/envelopes/index.d.ts", - "./lib/esm/envelopes/index.d.ts" - ], - "envelopes/*": [ - "./lib/cjs/envelopes/*.d.ts", - "./lib/esm/envelopes/*.d.ts" - ], - "helpers": [ - "./lib/cjs/helpers.d.ts", - "./lib/esm/helpers.d.ts" - ] + "require": { + "types": "./lib/cjs/types/index.d.ts", + "default": "./lib/cjs/types/index.js" + }, + "import": { + "types": "./lib/esm/types/index.d.ts", + "default": "./lib/esm/types/index.js" + } + }, + "./errors": { + "require": { + "types": "./lib/cjs/errors.d.ts", + "default": "./lib/cjs/errors.js" + }, + "import": { + "types": "./lib/esm/errors.d.ts", + "default": "./lib/esm/errors.js" + } } }, "main": "./lib/cjs/index.js", @@ -274,7 +160,7 @@ "nodejs" ], "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0" + "@aws-lambda-powertools/commons": "^2.17.0" }, "peerDependencies": { "@middy/core": "4.x || 5.x || 6.x", diff --git a/packages/parser/src/envelopes/api-gateway.ts b/packages/parser/src/envelopes/api-gateway.ts index 2ff9d3b8ab..0abaecc6d8 100644 --- a/packages/parser/src/envelopes/api-gateway.ts +++ b/packages/parser/src/envelopes/api-gateway.ts @@ -1,6 +1,6 @@ import type { ZodSchema, z } from 'zod'; import { ParseError } from '../errors.js'; -import { APIGatewayProxyEventSchema } from '../schemas/apigw.js'; +import { APIGatewayProxyEventSchema } from '../schemas/api-gateway.js'; import type { ParsedResult } from '../types/parser.js'; import { envelopeDiscriminator } from './envelope.js'; diff --git a/packages/parser/src/envelopes/api-gatewayv2.ts b/packages/parser/src/envelopes/api-gatewayv2.ts index 486fb74593..408bcde42c 100644 --- a/packages/parser/src/envelopes/api-gatewayv2.ts +++ b/packages/parser/src/envelopes/api-gatewayv2.ts @@ -1,6 +1,6 @@ import type { ZodSchema, z } from 'zod'; import { ParseError } from '../errors.js'; -import { APIGatewayProxyEventV2Schema } from '../schemas/apigwv2.js'; +import { APIGatewayProxyEventV2Schema } from '../schemas/api-gatewayv2.js'; import type { ParsedResult } from '../types/index.js'; import { envelopeDiscriminator } from './envelope.js'; diff --git a/packages/parser/src/helpers.ts b/packages/parser/src/helpers/index.ts similarity index 95% rename from packages/parser/src/helpers.ts rename to packages/parser/src/helpers/index.ts index 10915620d7..f24424d17b 100644 --- a/packages/parser/src/helpers.ts +++ b/packages/parser/src/helpers/index.ts @@ -1,7 +1,4 @@ import { type ZodTypeAny, z } from 'zod'; -/** - * @typedef {import('../schemas/alb').AlbSchema} AlbSchema - */ /** * A helper function to parse a JSON string and validate it against a schema. diff --git a/packages/parser/src/middleware/parser.ts b/packages/parser/src/middleware/index.ts similarity index 100% rename from packages/parser/src/middleware/parser.ts rename to packages/parser/src/middleware/index.ts diff --git a/packages/parser/src/schemas/apigw.ts b/packages/parser/src/schemas/api-gateway.ts similarity index 100% rename from packages/parser/src/schemas/apigw.ts rename to packages/parser/src/schemas/api-gateway.ts diff --git a/packages/parser/src/schemas/apigwv2.ts b/packages/parser/src/schemas/api-gatewayv2.ts similarity index 100% rename from packages/parser/src/schemas/apigwv2.ts rename to packages/parser/src/schemas/api-gatewayv2.ts diff --git a/packages/parser/src/schemas/dynamodb.ts b/packages/parser/src/schemas/dynamodb.ts index ee361b783b..00170ac672 100644 --- a/packages/parser/src/schemas/dynamodb.ts +++ b/packages/parser/src/schemas/dynamodb.ts @@ -230,5 +230,6 @@ export { DynamoDBStreamSchema, DynamoDBStreamRecord, DynamoDBStreamChangeRecord, + DynamoDBStreamChangeRecordBase, UserIdentity, }; diff --git a/packages/parser/src/schemas/index.ts b/packages/parser/src/schemas/index.ts index 18f3a35424..eedaaeb800 100644 --- a/packages/parser/src/schemas/index.ts +++ b/packages/parser/src/schemas/index.ts @@ -4,7 +4,7 @@ export { APIGatewayRequestAuthorizerEventSchema, APIGatewayTokenAuthorizerEventSchema, APIGatewayEventRequestContextSchema, -} from './apigw.js'; +} from './api-gateway.js'; export { AppSyncResolverSchema, AppSyncBatchResolverSchema, @@ -14,7 +14,7 @@ export { APIGatewayRequestAuthorizerEventV2Schema, APIGatewayRequestAuthorizerV2Schema, APIGatewayRequestContextV2Schema, -} from './apigwv2.js'; +} from './api-gatewayv2.js'; export { CloudFormationCustomResourceCreateSchema, CloudFormationCustomResourceDeleteSchema, diff --git a/packages/parser/src/schemas/lambda.ts b/packages/parser/src/schemas/lambda.ts index 0530f56d67..e5fe679329 100644 --- a/packages/parser/src/schemas/lambda.ts +++ b/packages/parser/src/schemas/lambda.ts @@ -1,4 +1,4 @@ -import { APIGatewayProxyEventV2Schema } from './apigwv2.js'; +import { APIGatewayProxyEventV2Schema } from './api-gatewayv2.js'; /** * Zod schema for Lambda Function URL follows the API Gateway HTTP APIs Payload Format Version 2.0. diff --git a/packages/parser/src/schemas/s3.ts b/packages/parser/src/schemas/s3.ts index c7d0b78541..650712a78a 100644 --- a/packages/parser/src/schemas/s3.ts +++ b/packages/parser/src/schemas/s3.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { JSONStringified } from '../helpers.js'; +import { JSONStringified } from '../helpers/index.js'; import { EventBridgeSchema } from './eventbridge.js'; import { SqsRecordSchema } from './sqs.js'; diff --git a/packages/parser/tests/unit/envelopes/api-gateway.test.ts b/packages/parser/tests/unit/envelopes/api-gateway.test.ts index 275d9d7c1b..83c6b3b455 100644 --- a/packages/parser/tests/unit/envelopes/api-gateway.test.ts +++ b/packages/parser/tests/unit/envelopes/api-gateway.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { ZodError, z } from 'zod'; import { ApiGatewayEnvelope } from '../../../src/envelopes/index.js'; import { ParseError } from '../../../src/errors.js'; -import { JSONStringified } from '../../../src/helpers.js'; +import { JSONStringified } from '../../../src/helpers/index.js'; import type { APIGatewayProxyEvent } from '../../../src/types/schema.js'; import { getTestEvent, omit } from '../helpers/utils.js'; diff --git a/packages/parser/tests/unit/envelopes/api-gatewayv2.test.ts b/packages/parser/tests/unit/envelopes/api-gatewayv2.test.ts index 99f90373a0..4d03b6f2f1 100644 --- a/packages/parser/tests/unit/envelopes/api-gatewayv2.test.ts +++ b/packages/parser/tests/unit/envelopes/api-gatewayv2.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { ZodError, z } from 'zod'; import { ApiGatewayV2Envelope } from '../../../src/envelopes/api-gatewayv2.js'; import { ParseError } from '../../../src/errors.js'; -import { JSONStringified } from '../../../src/helpers.js'; +import { JSONStringified } from '../../../src/helpers/index.js'; import type { APIGatewayProxyEventV2 } from '../../../src/types/schema.js'; import { getTestEvent, omit } from '../helpers/utils.js'; diff --git a/packages/parser/tests/unit/envelopes/cloudwatch.test.ts b/packages/parser/tests/unit/envelopes/cloudwatch.test.ts index 4e617e088d..139666b57d 100644 --- a/packages/parser/tests/unit/envelopes/cloudwatch.test.ts +++ b/packages/parser/tests/unit/envelopes/cloudwatch.test.ts @@ -4,7 +4,7 @@ import { ZodError, z } from 'zod'; import { ParseError } from '../../../src'; import { CloudWatchEnvelope } from '../../../src/envelopes/cloudwatch.js'; import { DecompressError } from '../../../src/errors.js'; -import { JSONStringified } from '../../../src/helpers.js'; +import { JSONStringified } from '../../../src/helpers/index'; import { getTestEvent } from '../helpers/utils.js'; const decompressRecordToJSON = ( diff --git a/packages/parser/tests/unit/envelopes/kafka.test.ts b/packages/parser/tests/unit/envelopes/kafka.test.ts index 70df419978..284bd89ea9 100644 --- a/packages/parser/tests/unit/envelopes/kafka.test.ts +++ b/packages/parser/tests/unit/envelopes/kafka.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { ZodError, z } from 'zod'; import { KafkaEnvelope } from '../../../src/envelopes/kafka.js'; import { ParseError } from '../../../src/errors.js'; -import { JSONStringified } from '../../../src/helpers.js'; +import { JSONStringified } from '../../../src/helpers/index.js'; import { getTestEvent } from '../helpers/utils.js'; describe('Envelope: Kafka', () => { diff --git a/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts b/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts index bf5476c412..77e788d87c 100644 --- a/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts +++ b/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { ZodError, z } from 'zod'; import { KinesisFirehoseEnvelope } from '../../../src/envelopes/kinesis-firehose.js'; import { ParseError } from '../../../src/errors.js'; -import { JSONStringified } from '../../../src/helpers.js'; +import { JSONStringified } from '../../../src/helpers/index.js'; import type { KinesisFireHoseEvent, KinesisFireHoseSqsEvent, diff --git a/packages/parser/tests/unit/envelopes/sns-sqs.test.ts b/packages/parser/tests/unit/envelopes/sns-sqs.test.ts index 93c193a227..8d8c0d7e07 100644 --- a/packages/parser/tests/unit/envelopes/sns-sqs.test.ts +++ b/packages/parser/tests/unit/envelopes/sns-sqs.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { ZodError, z } from 'zod'; import { SnsSqsEnvelope } from '../../../src/envelopes/sns-sqs.js'; import { ParseError } from '../../../src/errors.js'; -import { JSONStringified } from '../../../src/helpers.js'; +import { JSONStringified } from '../../../src/helpers/index.js'; import type { SqsEvent } from '../../../src/types/schema.js'; import { getTestEvent } from '../helpers/utils.js'; diff --git a/packages/parser/tests/unit/envelopes/sns.test.ts b/packages/parser/tests/unit/envelopes/sns.test.ts index ee0793fd0c..7e4eee6552 100644 --- a/packages/parser/tests/unit/envelopes/sns.test.ts +++ b/packages/parser/tests/unit/envelopes/sns.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { ZodError, z } from 'zod'; import { SnsEnvelope } from '../../../src/envelopes/sns.js'; import { ParseError } from '../../../src/errors.js'; -import { JSONStringified } from '../../../src/helpers.js'; +import { JSONStringified } from '../../../src/helpers/index.js'; import type { SnsEvent } from '../../../src/types/schema.js'; import { getTestEvent } from '../helpers/utils.js'; diff --git a/packages/parser/tests/unit/envelopes/sqs.test.ts b/packages/parser/tests/unit/envelopes/sqs.test.ts index 97a0539709..99fd3cb35f 100644 --- a/packages/parser/tests/unit/envelopes/sqs.test.ts +++ b/packages/parser/tests/unit/envelopes/sqs.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { ZodError, z } from 'zod'; import { SqsEnvelope } from '../../../src/envelopes/sqs.js'; import { ParseError } from '../../../src/errors.js'; -import { JSONStringified } from '../../../src/helpers.js'; +import { JSONStringified } from '../../../src/helpers/index.js'; import type { SqsEvent } from '../../../src/types/schema.js'; import { getTestEvent } from '../helpers/utils.js'; diff --git a/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts b/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts index 9a77317d81..1980db49e9 100644 --- a/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts +++ b/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { ZodError, z } from 'zod'; import { VpcLatticeEnvelope } from '../../../src/envelopes/vpc-lattice.js'; import { ParseError } from '../../../src/errors.js'; -import { JSONStringified } from '../../../src/helpers.js'; +import { JSONStringified } from '../../../src/helpers/index.js'; import type { VpcLatticeEvent } from '../../../src/types/index.js'; import { getTestEvent, omit } from '../helpers/utils.js'; diff --git a/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts b/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts index a3097b741e..49f5ec81bf 100644 --- a/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts +++ b/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { ZodError, z } from 'zod'; import { VpcLatticeV2Envelope } from '../../../src/envelopes/vpc-latticev2.js'; import { ParseError } from '../../../src/errors.js'; -import { JSONStringified } from '../../../src/helpers.js'; +import { JSONStringified } from '../../../src/helpers/index.js'; import type { VpcLatticeEventV2 } from '../../../src/types/index.js'; import { getTestEvent, omit } from '../helpers/utils.js'; diff --git a/packages/parser/tests/unit/helpers.test.ts b/packages/parser/tests/unit/helpers.test.ts index fd4b73eb14..9811234c2d 100644 --- a/packages/parser/tests/unit/helpers.test.ts +++ b/packages/parser/tests/unit/helpers.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { z } from 'zod'; -import { JSONStringified } from '../../src/helpers.js'; import { DynamoDBMarshalled } from '../../src/helpers/dynamodb.js'; +import { JSONStringified } from '../../src/helpers/index.js'; import { AlbSchema } from '../../src/schemas/alb.js'; import { DynamoDBStreamRecord, diff --git a/packages/parser/tests/unit/parser.middy.test.ts b/packages/parser/tests/unit/parser.middy.test.ts index c6bfdb34cc..34a3f1caf8 100644 --- a/packages/parser/tests/unit/parser.middy.test.ts +++ b/packages/parser/tests/unit/parser.middy.test.ts @@ -5,7 +5,7 @@ import { z } from 'zod'; import { EventBridgeEnvelope } from '../../src/envelopes/eventbridge.js'; import { SqsEnvelope } from '../../src/envelopes/sqs.js'; import { ParseError } from '../../src/errors.js'; -import { parser } from '../../src/middleware/parser.js'; +import { parser } from '../../src/middleware/index.js'; import type { EventBridgeEvent, ParsedResult, diff --git a/packages/parser/typedoc.json b/packages/parser/typedoc.json index 0650b79fad..cc06598557 100644 --- a/packages/parser/typedoc.json +++ b/packages/parser/typedoc.json @@ -2,11 +2,11 @@ "extends": ["../../typedoc.base.json"], "entryPoints": [ "./src/index.ts", - "./src/middleware/parser.ts", + "./src/middleware/index.ts", "./src/types/index.ts", "./src/envelopes/index.ts", "./src/schemas/index.ts", - "./src/helpers.ts", + "./src/helpers/index.ts", "./src/helpers/dynamodb.ts" ], "readme": "README.md" diff --git a/packages/testing/CHANGELOG.md b/packages/testing/CHANGELOG.md index 8fe1002b88..05d203fc7c 100644 --- a/packages/testing/CHANGELOG.md +++ b/packages/testing/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + +**Note:** Version bump only for package @aws-lambda-powertools/testing-utils + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) **Note:** Version bump only for package @aws-lambda-powertools/testing-utils diff --git a/packages/testing/package.json b/packages/testing/package.json index 640cf4ae71..6dc479ef4f 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/testing-utils", - "version": "2.16.0", + "version": "2.17.0", "description": "A package containing utilities to test your serverless workloads", "author": { "name": "Amazon Web Services", @@ -97,11 +97,11 @@ }, "homepage": "https://github.com/aws-powertools/powertools-lambda-typescript/tree/main/packages/testing#readme", "dependencies": { - "@aws-cdk/toolkit-lib": "^0.1.3", - "@aws-sdk/client-lambda": "^3.758.0", + "@aws-cdk/toolkit-lib": "^0.1.6", + "@aws-sdk/client-lambda": "^3.772.0", "@smithy/util-utf8": "^4.0.0", - "aws-cdk-lib": "^2.181.1", - "esbuild": "^0.25.0", + "aws-cdk-lib": "^2.185.0", + "esbuild": "^0.25.1", "promise-retry": "^2.0.1" }, "devDependencies": { diff --git a/packages/testing/src/TestInvocationLogs.ts b/packages/testing/src/TestInvocationLogs.ts index 2de21918ae..45c189aa5f 100644 --- a/packages/testing/src/TestInvocationLogs.ts +++ b/packages/testing/src/TestInvocationLogs.ts @@ -4,6 +4,7 @@ import type { FunctionLog } from './types.js'; const CloudWatchLogKeywords = { END: 'END RequestId', INIT_START: 'INIT_START', + INIT_REPORT: 'INIT_REPORT', REPORT: 'REPORT RequestId', START: 'START RequestId', XRAY: 'XRAY TraceId', @@ -99,13 +100,15 @@ class TestInvocationLogs { } /** - * Return the index of the log that contains `INIT_START` - * @param logs - * @returns {number} index of the log that contains `INIT_START` + * Return the index of the log that contains `INIT_START` or `INIT_REPORT` + * + * @param logs - Array of logs */ public static getInitLogIndex(logs: string[]): number { - return logs.findIndex((log) => - log.startsWith(CloudWatchLogKeywords.INIT_START) + return logs.findIndex( + (log) => + log.startsWith(CloudWatchLogKeywords.INIT_START) || + log.startsWith(CloudWatchLogKeywords.INIT_REPORT) ); } diff --git a/packages/testing/src/TestStack.ts b/packages/testing/src/TestStack.ts index 63b5a193c3..5ed69d1448 100644 --- a/packages/testing/src/TestStack.ts +++ b/packages/testing/src/TestStack.ts @@ -66,19 +66,63 @@ class TestStack { Service: 'Powertools-for-AWS-e2e-tests', }, }); + let lastCreateLog = 0; + let lastDestroyLog = 0; + const creationDeleteLogFrequency = 10000; // 10 seconds + const that = this; this.#cli = new Toolkit({ color: false, ioHost: { + /** + * Log messages to the console depending on the log level. + * + * If the `RUNNER_DEBUG` environment variable is set to `1`, all messages are logged. + * + * Otherwise, we log messages that are either warnings or errors as well as periodic + * updates on the stack creation and destruction process. + * + * @param msg - Message to log sent by the CDK CLI + */ async notify(msg) { + if (process.env.RUNNER_DEBUG === '1') { + testConsole.log(msg); + return; + } + if (msg.message.includes('destroyed') && msg.message.includes('✅')) { + testConsole.log(msg.message); + return; + } + if (msg.message.includes('✅') && !msg.message.includes('deployed')) { + testConsole.log(`${that.testName} deployed successfully`); + return; + } + if (msg.message.includes('CREATE_IN_PROGRESS')) { + if (Date.now() - lastCreateLog < creationDeleteLogFrequency) { + return; + } + lastCreateLog = Date.now(); + testConsole.log(`${that.testName} stack is being created...`); + return; + } + if (msg.message.includes('DELETE_IN_PROGRESS')) { + if (Date.now() - lastDestroyLog < creationDeleteLogFrequency) { + return; + } + lastDestroyLog = Date.now(); + testConsole.log(`${that.testName} stack is being destroyed...`); + return; + } + if (['warning', 'error'].includes(msg.level)) { + testConsole.log(msg); + } + }, + async requestResponse(msg) { if ( process.env.RUNNER_DEBUG === '1' || ['warning', 'error'].includes(msg.level) ) { testConsole.log(msg); } - }, - async requestResponse(msg) { - testConsole.log(msg); return msg.defaultResponse; }, }, diff --git a/packages/testing/src/resources/TestNodejsFunction.ts b/packages/testing/src/resources/TestNodejsFunction.ts index 01a383153f..e20ee86183 100644 --- a/packages/testing/src/resources/TestNodejsFunction.ts +++ b/packages/testing/src/resources/TestNodejsFunction.ts @@ -1,6 +1,6 @@ import { randomUUID } from 'node:crypto'; -import { CfnOutput, type CfnResource, Duration } from 'aws-cdk-lib'; -import { Tracing } from 'aws-cdk-lib/aws-lambda'; +import { CfnOutput, Duration } from 'aws-cdk-lib'; +import { Alias, Tracing } from 'aws-cdk-lib/aws-lambda'; import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs'; import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; import type { TestStack } from '../TestStack.js'; @@ -56,8 +56,18 @@ class TestNodejsFunction extends NodejsFunction { logGroup, }); + let outputValue = this.functionName; + if (extraProps.createAlias) { + const dev = new Alias(this, 'dev', { + aliasName: 'dev', + version: this.currentVersion, + provisionedConcurrentExecutions: 1, + }); + outputValue = dev.functionArn; + } + new CfnOutput(this, extraProps.nameSuffix, { - value: this.functionName, + value: outputValue, }); } } diff --git a/packages/testing/src/setupEnv.ts b/packages/testing/src/setupEnv.ts index 0a0c83b84a..9caf9e724f 100644 --- a/packages/testing/src/setupEnv.ts +++ b/packages/testing/src/setupEnv.ts @@ -377,3 +377,4 @@ if ( process.env._HANDLER = 'index.handler'; process.env.POWERTOOLS_SERVICE_NAME = 'hello-world'; process.env.AWS_XRAY_LOGGING_LEVEL = 'silent'; +process.env.AWS_LAMBDA_INITIALIZATION_TYPE = 'on-demand'; diff --git a/packages/testing/src/types.ts b/packages/testing/src/types.ts index 454c2d87b4..432f01e42f 100644 --- a/packages/testing/src/types.ts +++ b/packages/testing/src/types.ts @@ -19,6 +19,12 @@ interface ExtraTestProps { * @default 'CJS' */ outputFormat?: 'CJS' | 'ESM'; + /** + * Whether to create an alias for the function. + * + * @default false + */ + createAlias?: boolean; } type TestDynamodbTableProps = Omit< diff --git a/packages/tracer/CHANGELOG.md b/packages/tracer/CHANGELOG.md index 15293fee91..74302884d9 100644 --- a/packages/tracer/CHANGELOG.md +++ b/packages/tracer/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + +**Note:** Version bump only for package @aws-lambda-powertools/tracer + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) **Note:** Version bump only for package @aws-lambda-powertools/tracer diff --git a/packages/tracer/README.md b/packages/tracer/README.md index 421418f139..e9411c0e56 100644 --- a/packages/tracer/README.md +++ b/packages/tracer/README.md @@ -154,6 +154,7 @@ The following companies, among others, use Powertools: - [Elva](https://elva-group.com) - [Flyweight](https://flyweight.io/) - [globaldatanet](https://globaldatanet.com/) +- [Guild](https://guild.com) - [Hashnode](https://hashnode.com/) - [LocalStack](https://localstack.cloud/) - [Perfect Post](https://www.perfectpost.fr) diff --git a/packages/tracer/package.json b/packages/tracer/package.json index b6fa0a6feb..e0174d740e 100644 --- a/packages/tracer/package.json +++ b/packages/tracer/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/tracer", - "version": "2.16.0", + "version": "2.17.0", "description": "The tracer package for the Powertools for AWS Lambda (TypeScript) library", "author": { "name": "Amazon Web Services", @@ -30,8 +30,8 @@ "license": "MIT-0", "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-dynamodb": "^3.758.0", - "@aws-sdk/client-xray": "^3.758.0" + "@aws-sdk/client-dynamodb": "^3.772.0", + "@aws-sdk/client-xray": "^3.772.0" }, "peerDependencies": { "@middy/core": "4.x || 5.x || 6.x" @@ -87,7 +87,7 @@ "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" }, "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0", + "@aws-lambda-powertools/commons": "^2.17.0", "aws-xray-sdk-core": "^3.10.3" }, "keywords": [ diff --git a/packages/tracer/tests/e2e/constants.ts b/packages/tracer/tests/e2e/constants.ts index 77ea5c9ed2..e4db5268cf 100644 --- a/packages/tracer/tests/e2e/constants.ts +++ b/packages/tracer/tests/e2e/constants.ts @@ -1,10 +1,5 @@ // Prefix for all resources created by the E2E tests const RESOURCE_NAME_PREFIX = 'Tracer'; -// Constants relating time to be used in the tests -const ONE_MINUTE = 60 * 1_000; -const TEST_CASE_TIMEOUT = 5 * ONE_MINUTE; -const SETUP_TIMEOUT = 7 * ONE_MINUTE; -const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; // Expected values for custom annotations, metadata, and response const EXPECTED_ANNOTATION_KEY = 'myAnnotation'; @@ -17,10 +12,6 @@ const EXPECTED_SUBSEGMENT_NAME = '### mySubsegment'; export { RESOURCE_NAME_PREFIX, - ONE_MINUTE, - TEST_CASE_TIMEOUT, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, EXPECTED_ANNOTATION_KEY, EXPECTED_ANNOTATION_VALUE, EXPECTED_METADATA_KEY, diff --git a/packages/tracer/tests/e2e/decorator.test.ts b/packages/tracer/tests/e2e/decorator.test.ts index 8a87282431..260dfae659 100644 --- a/packages/tracer/tests/e2e/decorator.test.ts +++ b/packages/tracer/tests/e2e/decorator.test.ts @@ -8,8 +8,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { invokeAllTestCases } from '../helpers/invokeAllTests.js'; import { RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, EXPECTED_ANNOTATION_KEY as expectedCustomAnnotationKey, EXPECTED_ANNOTATION_VALUE as expectedCustomAnnotationValue, EXPECTED_ERROR_MESSAGE as expectedCustomErrorMessage, @@ -82,13 +80,13 @@ describe('Tracer E2E tests, decorator instrumentation', () => { */ expectedSegmentsCount: 4, }); - }, SETUP_TIMEOUT); + }); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); it('should generate all trace data correctly', async () => { // Assess diff --git a/packages/tracer/tests/e2e/manual.test.ts b/packages/tracer/tests/e2e/manual.test.ts index 80fa766be1..52dbab375e 100644 --- a/packages/tracer/tests/e2e/manual.test.ts +++ b/packages/tracer/tests/e2e/manual.test.ts @@ -8,8 +8,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { invokeAllTestCases } from '../helpers/invokeAllTests.js'; import { RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, EXPECTED_ANNOTATION_KEY as expectedCustomAnnotationKey, EXPECTED_ANNOTATION_VALUE as expectedCustomAnnotationValue, EXPECTED_ERROR_MESSAGE as expectedCustomErrorMessage, @@ -79,13 +77,13 @@ describe('Tracer E2E tests, manual instantiation', () => { */ expectedSegmentsCount: 2, }); - }, SETUP_TIMEOUT); + }); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); it('should generate all trace data correctly', async () => { // Assess diff --git a/packages/tracer/tests/e2e/middy.test.ts b/packages/tracer/tests/e2e/middy.test.ts index 0a33d4377e..361c350166 100644 --- a/packages/tracer/tests/e2e/middy.test.ts +++ b/packages/tracer/tests/e2e/middy.test.ts @@ -8,8 +8,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { invokeAllTestCases } from '../helpers/invokeAllTests.js'; import { RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, EXPECTED_ANNOTATION_KEY as expectedCustomAnnotationKey, EXPECTED_ANNOTATION_VALUE as expectedCustomAnnotationValue, EXPECTED_ERROR_MESSAGE as expectedCustomErrorMessage, @@ -81,13 +79,13 @@ describe('Tracer E2E tests, middy instrumentation', () => { */ expectedSegmentsCount: 4, }); - }, SETUP_TIMEOUT); + }); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); } - }, TEARDOWN_TIMEOUT); + }); it('should generate all trace data correctly', () => { // Assess diff --git a/packages/tracer/vitest.config.ts b/packages/tracer/vitest.config.ts index 9f1196ef1f..baa5cf7463 100644 --- a/packages/tracer/vitest.config.ts +++ b/packages/tracer/vitest.config.ts @@ -4,5 +4,7 @@ export default defineProject({ test: { environment: 'node', setupFiles: ['../testing/src/setupEnv.ts'], + hookTimeout: 1_000 * 60 * 10, // 10 minutes + testTimeout: 1_000 * 60 * 3, // 3 minutes }, }); diff --git a/packages/validation/CHANGELOG.md b/packages/validation/CHANGELOG.md index 88ae494c5f..887a97fb5e 100644 --- a/packages/validation/CHANGELOG.md +++ b/packages/validation/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.17.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.16.0...v2.17.0) (2025-03-25) + +**Note:** Version bump only for package @aws-lambda-powertools/validation + + + + + # [2.16.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.15.0...v2.16.0) (2025-03-07) diff --git a/packages/validation/README.md b/packages/validation/README.md index b843d1823b..74911812e6 100644 --- a/packages/validation/README.md +++ b/packages/validation/README.md @@ -2,9 +2,6 @@ This utility provides JSON Schema validation for events and responses, including JMESPath support to unwrap events before validation. -> [!Warning] -> This feature is currently under development. As such it's considered not stable and we might make significant breaking changes before going before its release. You are welcome to [provide feedback](https://github.com/aws-powertools/powertools-lambda-typescript/discussions/3519) and [contribute to its implementation](https://github.com/aws-powertools/powertools-lambda-typescript/milestone/18). - Powertools for AWS Lambda (TypeScript) is a developer toolkit to implement Serverless [best practices and increase developer velocity](https://docs.powertools.aws.dev/lambda/typescript/latest/#features). You can use the library in both TypeScript and JavaScript code bases. To get started, install the package by running: @@ -13,8 +10,222 @@ To get started, install the package by running: npm i @aws-lambda-powertools/validation ``` -> [!Note] -> This readme is a work in progress. +## Features + +You can validate inbound and outbound payloads using the `@validator` class method decorator or `validator` Middy.js middleware. + +You can also use the standalone `validate` function, if you want more control over the validation process such as handling a validation error. + +### Validator decorator + +The `@validator` decorator is a TypeScript class method decorator that you can use to validate both the incoming event and the response payload. + +If the validation fails, we will throw a `SchemaValidationError`. + +```typescript +import { validator } from '@aws-lambda-powertools/validation/decorator'; +import type { Context } from 'aws-lambda'; + +const inboundSchema = { + type: 'object', + properties: { + value: { type: 'number' }, + }, + required: ['value'], + additionalProperties: false, +}; + +const outboundSchema = { + type: 'object', + properties: { + result: { type: 'number' }, + }, + required: ['result'], + additionalProperties: false, +}; + +class Lambda { + @validator({ + inboundSchema, + outboundSchema, + }) + async handler(event: { value: number }, _context: Context) { + // Your handler logic here + return { result: event.value * 2 }; + } +} + +const lambda = new Lambda(); +export const handler = lambda.handler.bind(lambda); +``` + +It's not mandatory to validate both the inbound and outbound payloads. You can either use one, the other, or both. + +### Validator middleware + +If you are using Middy.js, you can instead use the `validator` middleware to validate the incoming event and response payload. + +```typescript +import { validator } from '@aws-lambda-powertools/validation/middleware'; +import middy from '@middy/core'; + +const inboundSchema = { + type: 'object', + properties: { + foo: { type: 'string' }, + }, + required: ['foo'], + additionalProperties: false, +}; + +const outboundSchema = { + type: 'object', + properties: { + bar: { type: 'number' }, + }, + required: ['bar'], + additionalProperties: false, +}; + +export const handler = middy() + .use(validation({ inboundSchema, outboundSchema })) + .handler(async (event) => { + // Your handler logic here + return { bar: 42 }; + }); +``` + +Like the `@validator` decorator, you can choose to validate only the inbound or outbound payload. + +### Standalone validate function + +The `validate` function gives you more control over the validation process, and is typically used within the Lambda handler, or any other function that needs to validate data. + +When using the standalone function, you can gracefully handle schema validation errors by catching `SchemaValidationError` errors. + +```typescript +import { validate } from '@aws-lambda-powertools/validation'; +import { SchemaValidationError } from '@aws-lambda-powertools/validation/errors'; + +const schema = { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'number' }, + }, + required: ['name', 'age'], + additionalProperties: false, +} as const; + +const payload = { name: 'John', age: 30 }; + +export const handler = async (event: unknown) => { + try { + const validatedData = validate({ + payload, + schema, + }); + + // Your handler logic here + } catch (error) { + if (error instanceof SchemaValidationError) { + // Handle the validation error + return { + statusCode: 400, + body: JSON.stringify({ message: error.message }), + }; + } + // Handle other errors + throw error; + } +} +``` + +### JMESPath support + +In some cases you might want to validate only a portion of the event payload - this is what the `envelope` option is for. + +You can use JMESPath expressions to specify the path to the property you want to validate. The validator will unwrap the event before validating it. + +```typescript +import { validate } from '@aws-lambda-powertools/validation'; + +const schema = { + type: 'object', + properties: { + user: { type: 'string' }, + }, + required: ['user'], + additionalProperties: false, +} as const; + +const payload = { + data: { + user: 'Alice', + }, +}; + +const validatedData = validate({ + payload, + schema, + envelope: 'data', +}); +``` + +### Extending the validator + +Since the validator is built on top of [Ajv](https://ajv.js.org/), you can extend it with custom formats and external schemas, as well as bringing your own `ajv` instance. + +The example below shows how to pass additional options to the `validate` function, but you can also pass them to the `@validator` decorator and `validator` middleware. + +```typescript +import { validate } from '@aws-lambda-powertools/validation'; + +const formats = { + ageRange: (value: number) => return value >= 0 && value <= 120, +}; + +const definitionSchema = { + $id: 'https://example.com/schemas/definitions.json', + definitions: { + user: { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'number', format: 'ageRange' }, + }, + required: ['name', 'age'], + additionalProperties: false, + } + } +} as const; + +const schema = { + $id: 'https://example.com/schemas/user.json', + type: 'object', + properties: { + user: { $ref: 'definitions.json#/definitions/user' }, + }, + required: ['user'], + additionalProperties: false, +} as const; + +const payload = { + user: { + name: 'Alice', + age: 25, + }, +}; + +const validatedData = validate({ + payload, + schema, + externalRefs: [definitionSchema], + formats, +}); +``` + +For more information on how to use the `validate` function, please refer to the [documentation](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/validation). ## Contribute @@ -47,6 +258,7 @@ The following companies, among others, use Powertools: - [Elva](https://elva-group.com) - [Flyweight](https://flyweight.io/) - [globaldatanet](https://globaldatanet.com/) +- [Guild](https://guild.com) - [Hashnode](https://hashnode.com/) - [LocalStack](https://localstack.cloud/) - [Perfect Post](https://www.perfectpost.fr) diff --git a/packages/validation/package.json b/packages/validation/package.json index d842f95518..24d1589bcb 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,12 +1,11 @@ { "name": "@aws-lambda-powertools/validation", - "version": "2.16.0", + "version": "2.17.0", "description": "An utility to validate events and responses using JSON Schemas", "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com" }, - "private": true, "scripts": { "test": "vitest --run", "test:unit": "vitest --run", @@ -18,7 +17,7 @@ "test:e2e": "echo \"Not implemented\"", "build:cjs": "tsc --build tsconfig.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", "build:esm": "tsc --build tsconfig.esm.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json", - "build": "echo \"Not implemented\"", + "build": "npm run build:esm & npm run build:cjs", "lint": "biome lint .", "lint:fix": "biome check --write .", "prepack": "node ../../.github/scripts/release_patch_package_json.js ." @@ -29,12 +28,42 @@ "exports": { ".": { "require": { - "types": "./lib/cjs/index.d.ts", - "default": "./lib/cjs/index.js" + "types": "./lib/cjs/validate.d.ts", + "default": "./lib/cjs/validate.js" }, "import": { - "types": "./lib/esm/index.d.ts", - "default": "./lib/esm/index.js" + "types": "./lib/esm/validate.d.ts", + "default": "./lib/esm/validate.js" + } + }, + "./middleware": { + "require": { + "types": "./lib/cjs/middleware.d.ts", + "default": "./lib/cjs/middleware.js" + }, + "import": { + "types": "./lib/esm/middleware.d.ts", + "default": "./lib/esm/middleware.js" + } + }, + "./errors": { + "require": { + "types": "./lib/cjs/errors.d.ts", + "default": "./lib/cjs/errors.js" + }, + "import": { + "types": "./lib/esm/errors.d.ts", + "default": "./lib/esm/errors.js" + } + }, + "./decorator": { + "require": { + "types": "./lib/cjs/decorator.d.ts", + "default": "./lib/cjs/decorator.js" + }, + "import": { + "types": "./lib/esm/decorator.d.ts", + "default": "./lib/esm/decorator.js" } } }, @@ -51,8 +80,8 @@ "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" }, "dependencies": { - "@aws-lambda-powertools/commons": "^2.16.0", - "@aws-lambda-powertools/jmespath": "^2.16.0", + "@aws-lambda-powertools/commons": "^2.17.0", + "@aws-lambda-powertools/jmespath": "^2.17.0", "ajv": "^8.17.1" }, "keywords": [ diff --git a/packages/validation/src/decorator.ts b/packages/validation/src/decorator.ts index 45a9edfb3d..a9020e3469 100644 --- a/packages/validation/src/decorator.ts +++ b/packages/validation/src/decorator.ts @@ -1,15 +1,148 @@ import { SchemaValidationError } from './errors.js'; import type { ValidatorOptions } from './types.js'; +import { getErrorCause } from './utils.js'; import { validate } from './validate.js'; -export function validator(options: ValidatorOptions) { + +/** + * Class method decorator to validate the input and output of a method using JSON Schema. + * + * @example + * ```typescript + * import { validator } from '@aws-lambda-powertools/validation/decorator'; + * import type { Context } from 'aws-lambda'; + * + * const inboundSchema = { + * type: 'object', + * properties: { + * value: { type: 'number' }, + * }, + * required: ['value'], + * additionalProperties: false, + * }; + * + * const outboundSchema = { + * type: 'object', + * properties: { + * result: { type: 'number' }, + * }, + * required: ['result'], + * additionalProperties: false, + * }; + * + * class Lambda { + * ⁣@validator({ + * inboundSchema, + * outboundSchema, + * }) + * async handler(event: { value: number }, _context: Context) { + * // Your handler logic here + * return { result: event.value * 2 }; + * } + * } + * + * const lambda = new Lambda(); + * export const handler = lambda.handler.bind(lambda); + * ``` + * + * When validating nested payloads, you can also provide an optional JMESPath expression to extract a specific part of the payload + * before validation using the `envelope` parameter. This is useful when the payload is nested or when you want to validate only a specific part of it. + * + * @example + * ```typescript + * import { validator } from '@aws-lambda-powertools/validation/decorator'; + * + * class Lambda { + * ⁣@validator({ + * inboundSchema: { + * type: 'number', + * }, + * envelope: 'nested', + * }) + * async handler(event: number, _context: Context) { + * return { result: event * 2 }; + * } + * } + * + * const lambda = new Lambda(); + * export const handler = lambda.handler.bind(lambda); + * ``` + * + * Since the Validation utility is built on top of Ajv, you can also provide custom formats and external references + * to the validation process. This allows you to extend the validation capabilities of Ajv to suit your specific needs. + * + * @example + * ```typescript + * import { validate } from '@aws-lambda-powertools/validation'; + * + * const formats = { + * ageRange: (value: number) => return value >= 0 && value <= 120, + * }; + * + * const definitionSchema = { + * $id: 'https://example.com/schemas/definitions.json', + * definitions: { + * user: { + * type: 'object', + * properties: { + * name: { type: 'string' }, + * age: { type: 'number', format: 'ageRange' }, + * }, + * required: ['name', 'age'], + * additionalProperties: false, + * } + * } + * } as const; + * + * const schema = { + * $id: 'https://example.com/schemas/user.json', + * type: 'object', + * properties: { + * user: { $ref: 'definitions.json#/definitions/user' }, + * }, + * required: ['user'], + * additionalProperties: false, + * } as const; + * + * const payload = { + * user: { + * name: 'Alice', + * age: 25, + * }, + * }; + * + * class Lambda { + * ⁣@validator({ + * inboundSchema: schema, + * externalRefs: [definitionSchema], + * formats, + * }) + * async handler(event: { value: number }, _context: Context) { + * // Your handler logic here + * return { result: event.value * 2 }; + * } + * } + * + * const lambda = new Lambda(); + * export const handler = lambda.handler.bind(lambda); + * ``` + * + * Additionally, you can provide an existing Ajv instance to reuse the same instance across multiple validations. If + * you don't provide an Ajv instance, a new one will be created for each validation. + * + * @param options - The validation options + * @param options.inboundSchema - The JSON schema for inbound validation. + * @param options.outboundSchema - The JSON schema for outbound validation. + * @param options.envelope - Optional JMESPath expression to use as envelope for the payload. + * @param options.formats - Optional formats for validation. + * @param options.externalRefs - Optional external references for validation. + * @param options.ajv - Optional Ajv instance to use for validation, if not provided a new instance will be created. + */ +function validator(options: ValidatorOptions) { return ( _target: unknown, _propertyKey: string | symbol, descriptor: PropertyDescriptor ) => { - if (!descriptor.value) { - return descriptor; - } const { inboundSchema, outboundSchema, @@ -18,7 +151,7 @@ export function validator(options: ValidatorOptions) { externalRefs, ajv, } = options; - if (!inboundSchema && !outboundSchema) { + if (!options.inboundSchema && !outboundSchema) { return descriptor; } const originalMethod = descriptor.value; @@ -35,7 +168,9 @@ export function validator(options: ValidatorOptions) { ajv: ajv, }); } catch (error) { - throw new SchemaValidationError('Inbound validation failed', error); + throw new SchemaValidationError('Inbound schema validation failed', { + cause: getErrorCause(error), + }); } } const result = await originalMethod.apply(this, [ @@ -52,7 +187,9 @@ export function validator(options: ValidatorOptions) { ajv: ajv, }); } catch (error) { - throw new SchemaValidationError('Outbound Validation failed', error); + throw new SchemaValidationError('Outbound schema validation failed', { + cause: getErrorCause(error), + }); } } return result; @@ -60,3 +197,5 @@ export function validator(options: ValidatorOptions) { return descriptor; }; } + +export { validator }; diff --git a/packages/validation/src/errors.ts b/packages/validation/src/errors.ts index db41e7f7e5..ce925b5465 100644 --- a/packages/validation/src/errors.ts +++ b/packages/validation/src/errors.ts @@ -1,9 +1,85 @@ -export class SchemaValidationError extends Error { - public errors: unknown; +/** + * Base error class for all validation errors. + * + * This error is usually not thrown directly, but it's used as a base class for + * other errors thrown by the Validation utility. You can use it to catch all + * validation errors in a single catch block. + */ +class ValidationError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = 'ValidationError'; + } +} - constructor(message: string, errors?: unknown) { - super(message); +/** + * Error thrown when a schema validation fails. + * + * This error is thrown when the validation of a payload against a schema fails, + * the `cause` property contains the original Ajv issues. + * + * @example + * ```typescript + * import { validate } from '@aws-lambda-powertools/validation'; + * import { ValidationError } from '@aws-lambda-powertools/validation/errors'; + * + * const schema = { + * type: 'number', + * minimum: 0, + * maximum: 100, + * }; + * + * const payload = -1; + * + * try { + * validate({ payload, schema }); + * } catch (error) { + * if (error instanceof ValidationError) { + * // cause includes the original Ajv issues + * const { message, cause } = error; + * // ... handle the error + * } + * + * // handle other errors + * } + * ``` + */ +class SchemaValidationError extends ValidationError { + constructor(message: string, options?: ErrorOptions) { + super(message, options); this.name = 'SchemaValidationError'; - this.errors = errors; } } + +/** + * Error thrown when a schema compilation fails. + * + * This error is thrown when you pass an invalid schema to the validator. + * + * @example + * ```typescript + * import { validate } from '@aws-lambda-powertools/validation'; + * + * const schema = { + * invalid: 'schema', + * }; + * + * try { + * validate({ payload: {}, schema }); + * } catch (error) { + * if (error instanceof SchemaCompilationError) { + * // handle the error + * } + * + * // handle other errors + * } + * ``` + */ +class SchemaCompilationError extends ValidationError { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = 'SchemaCompilationError'; + } +} + +export { ValidationError, SchemaValidationError, SchemaCompilationError }; diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts deleted file mode 100644 index 039a9236fa..0000000000 --- a/packages/validation/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { validate } from './validate.js'; -export { SchemaValidationError } from './errors.js'; -export { validator } from './decorator.js'; diff --git a/packages/validation/src/middleware.ts b/packages/validation/src/middleware.ts index e706d6c846..af6d953033 100644 --- a/packages/validation/src/middleware.ts +++ b/packages/validation/src/middleware.ts @@ -1,39 +1,152 @@ +import type { + MiddlewareFn, + MiddyLikeRequest, +} from '@aws-lambda-powertools/commons/types'; import { SchemaValidationError } from './errors.js'; import type { ValidatorOptions } from './types.js'; +import { getErrorCause } from './utils.js'; import { validate } from './validate.js'; -export function validation(options: ValidatorOptions) { - return { - before: async (handler: { event: unknown }) => { - if (options.inboundSchema) { - try { - handler.event = validate({ - payload: handler.event, - schema: options.inboundSchema, - envelope: options.envelope, - formats: options.formats, - externalRefs: options.externalRefs, - ajv: options.ajv, - }); - } catch (error) { - throw new SchemaValidationError('Inbound validation failed', error); - } +/** + * Middy.js middleware to validate your event and response payloads using JSON schema. + * + * Both inbound and outbound schemas are optional. If only one is provided, only that one will be validated. + * + * @example + * ```typescript + * import { validation } from '@aws-lambda-powertools/validation/middleware'; + * import middy from '@middy/core'; + * + * const inboundSchema = { + * type: 'object', + * properties: { + * foo: { type: 'string' }, + * }, + * required: ['foo'], + * additionalProperties: false, + * }; + * + * const outboundSchema = { + * type: 'object', + * properties: { + * bar: { type: 'number' }, + * }, + * required: ['bar'], + * additionalProperties: false, + * }; + * + * export const handler = middy() + * .use(validation({ inboundSchema, outboundSchema })) + * .handler(async (event) => { + * // Your handler logic here + * return { bar: 42 }; + * }); + * ``` + * + * Since the Validation utility is built on top of Ajv, you can also provide custom formats and external references + * to the validation process. This allows you to extend the validation capabilities of Ajv to suit your specific needs. + * + * @example + * ```typescript + * import { validator } from '@aws-lambda-powertools/validation/middleware'; + * import middy from '@middy/core'; + * + * const formats = { + * ageRange: (value: number) => return value >= 0 && value <= 120, + * }; + * + * const definitionSchema = { + * $id: 'https://example.com/schemas/definitions.json', + * definitions: { + * user: { + * type: 'object', + * properties: { + * name: { type: 'string' }, + * age: { type: 'number', format: 'ageRange' }, + * }, + * required: ['name', 'age'], + * additionalProperties: false, + * } + * } + * } as const; + * + * const schema = { + * $id: 'https://example.com/schemas/user.json', + * type: 'object', + * properties: { + * user: { $ref: 'definitions.json#/definitions/user' }, + * }, + * required: ['user'], + * additionalProperties: false, + * } as const; + * + * export const handler = middy() + * .use(validation({ + * inboundSchema, + * outboundSchema, + * externalRefs: [definitionSchema], + * formats, + * })) + * .handler(async (event) => { + * // Your handler logic here + * return { bar: 42 }; + * }); + * ``` + * + * Additionally, you can provide an existing Ajv instance to reuse the same instance across multiple validations. If + * you don't provide an Ajv instance, a new one will be created for each validation. + * + * @param options - The validation options + * @param options.inboundSchema - The JSON schema for inbound validation. + * @param options.outboundSchema - The JSON schema for outbound validation. + * @param options.envelope - Optional JMESPath expression to use as envelope for the payload. + * @param options.formats - Optional formats for validation. + * @param options.externalRefs - Optional external references for validation. + * @param options.ajv - Optional Ajv instance to use for validation, if not provided a new instance will be created. + */ +const validator = (options: ValidatorOptions) => { + const before: MiddlewareFn = async (request) => { + if (options.inboundSchema) { + const originalEvent = structuredClone(request.event); + try { + request.event = validate({ + payload: originalEvent, + schema: options.inboundSchema, + envelope: options.envelope, + formats: options.formats, + externalRefs: options.externalRefs, + ajv: options.ajv, + }); + } catch (error) { + throw new SchemaValidationError('Inbound schema validation failed', { + cause: getErrorCause(error), + }); } - }, - after: async (handler: { response: unknown }) => { - if (options.outboundSchema) { - try { - handler.response = validate({ - payload: handler.response, - schema: options.outboundSchema, - formats: options.formats, - externalRefs: options.externalRefs, - ajv: options.ajv, - }); - } catch (error) { - throw new SchemaValidationError('Outbound validation failed', error); - } + } + }; + + const after = async (handler: MiddyLikeRequest) => { + if (options.outboundSchema) { + try { + handler.response = validate({ + payload: handler.response, + schema: options.outboundSchema, + formats: options.formats, + externalRefs: options.externalRefs, + ajv: options.ajv, + }); + } catch (error) { + throw new SchemaValidationError('Outbound schema validation failed', { + cause: getErrorCause(error), + }); } - }, + } }; -} + + return { + before, + after, + }; +}; + +export { validator }; diff --git a/packages/validation/src/types.ts b/packages/validation/src/types.ts index 4543e6ffe9..c5d63d89cc 100644 --- a/packages/validation/src/types.ts +++ b/packages/validation/src/types.ts @@ -1,36 +1,65 @@ -import type { - Ajv, - AnySchema, - AsyncFormatDefinition, - FormatDefinition, -} from 'ajv'; +import type { Ajv, AnySchema, Format } from 'ajv'; type Prettify = { [K in keyof T]: T[K]; } & {}; +/** + * Options to customize the JSON Schema validation. + * + * @param payload - The data to validate. + * @param schema - The JSON schema for validation. + * @param envelope - Optional JMESPATH expression to use as envelope for the payload. + * @param formats - Optional formats for validation. + * @param externalRefs - Optional external references for validation. + * @param ajv - Optional Ajv instance to use for validation, if not provided a new instance will be created. + */ type ValidateParams = { + /** + * The data to validate. + */ payload: unknown; + /** + * The JSON schema for validation. + */ schema: AnySchema; + /** + * Optional JMESPATH expression to use as envelope for the payload. + */ envelope?: string; - formats?: Record< - string, - | string - | RegExp - | FormatDefinition - | FormatDefinition - | AsyncFormatDefinition - | AsyncFormatDefinition - >; - externalRefs?: object[]; + /** + * Optional formats for validation. + */ + formats?: Record; + /** + * Optional external references for validation. + */ + externalRefs?: AnySchema | AnySchema[]; + /** + * Optional Ajv instance to use for validation, if not provided a new instance will be created. + */ ajv?: Ajv; }; -type ValidatorOptions = Prettify< - Omit & { - inboundSchema?: AnySchema; - outboundSchema?: AnySchema; - } ->; +/** + * Options to customize the JSON Schema validation. + * + * @param inboundSchema - The JSON schema for inbound validation. + * @param outboundSchema - The JSON schema for outbound validation. + * @param envelope - Optional JMESPATH expression to use as envelope for the payload. + * @param formats - Optional formats for validation. + * @param externalRefs - Optional external references for validation. + * @param ajv - Optional Ajv instance to use for validation, if not provided a new instance will be created. + */ +interface ValidatorOptions extends Omit { + /** + * The JSON schema for inbound validation. + */ + inboundSchema?: AnySchema; + /** + * The JSON schema for outbound validation. + */ + outboundSchema?: AnySchema; +} export type { ValidateParams, ValidatorOptions }; diff --git a/packages/validation/src/utils.ts b/packages/validation/src/utils.ts new file mode 100644 index 0000000000..dbace4fd00 --- /dev/null +++ b/packages/validation/src/utils.ts @@ -0,0 +1,16 @@ +/** + * Get the original cause of the error if it is a `SchemaValidationError`. + * + * This is useful so that we don't rethrow the same error type. + * + * @param error - The error to extract the cause from. + */ +const getErrorCause = (error: unknown): unknown => { + let cause = error; + if (error instanceof Error && error.name === 'SchemaValidationError') { + cause = error.cause; + } + return cause; +}; + +export { getErrorCause }; diff --git a/packages/validation/src/validate.ts b/packages/validation/src/validate.ts index 36d0fccb0a..b618a32d39 100644 --- a/packages/validation/src/validate.ts +++ b/packages/validation/src/validate.ts @@ -1,9 +1,124 @@ import { search } from '@aws-lambda-powertools/jmespath'; import { Ajv, type ValidateFunction } from 'ajv'; -import { SchemaValidationError } from './errors.js'; +import { SchemaCompilationError, SchemaValidationError } from './errors.js'; import type { ValidateParams } from './types.js'; -export function validate(params: ValidateParams): T { +/** + * Validates a payload against a JSON schema using Ajv. + * + * @example + * ```typescript + * import { validate } from '@aws-lambda-powertools/validation'; + * + * const schema = { + * type: 'object', + * properties: { + * name: { type: 'string' }, + * age: { type: 'number' }, + * }, + * required: ['name', 'age'], + * additionalProperties: false, + * } as const; + * + * const payload = { name: 'John', age: 30 }; + * + * const validatedData = validate({ + * payload, + * schema, + * }); + * ``` + * + * When validating, you can also provide an optional JMESPath expression to extract a specific part of the payload + * before validation using the `envelope` parameter. This is useful when the payload is nested or when you want to + * validate only a specific part of it. + * + * ```typescript + * import { validate } from '@aws-lambda-powertools/validation'; + * + * const schema = { + * type: 'object', + * properties: { + * user: { type: 'string' }, + * }, + * required: ['user'], + * additionalProperties: false, + * } as const; + * + * const payload = { + * data: { + * user: 'Alice', + * }, + * }; + * + * const validatedData = validate({ + * payload, + * schema, + * envelope: 'data', + * }); + * ``` + * + * Since the Validation utility is built on top of Ajv, you can also provide custom formats and external references + * to the validation process. This allows you to extend the validation capabilities of Ajv to suit your specific needs. + * + * @example + * ```typescript + * import { validate } from '@aws-lambda-powertools/validation'; + * + * const formats = { + * ageRange: (value: number) => return value >= 0 && value <= 120, + * }; + * + * const definitionSchema = { + * $id: 'https://example.com/schemas/definitions.json', + * definitions: { + * user: { + * type: 'object', + * properties: { + * name: { type: 'string' }, + * age: { type: 'number', format: 'ageRange' }, + * }, + * required: ['name', 'age'], + * additionalProperties: false, + * } + * } + * } as const; + * + * const schema = { + * $id: 'https://example.com/schemas/user.json', + * type: 'object', + * properties: { + * user: { $ref: 'definitions.json#/definitions/user' }, + * }, + * required: ['user'], + * additionalProperties: false, + * } as const; + * + * const payload = { + * user: { + * name: 'Alice', + * age: 25, + * }, + * }; + * + * const validatedData = validate({ + * payload, + * schema, + * externalRefs: [definitionSchema], + * formats, + * }); + * ``` + * + * Additionally, you can provide an existing Ajv instance to reuse the same instance across multiple validations. If + * you don't provide an Ajv instance, a new one will be created for each validation. + * + * @param params.payload - The payload to validate. + * @param params.schema - The JSON schema to validate against. + * @param params.envelope - Optional JMESPath expression to use as envelope for the payload. + * @param params.formats - Optional formats for validation. + * @param params.externalRefs - Optional external references for validation. + * @param params.ajv - Optional Ajv instance to use for validation, if not provided a new instance will be created. + */ +const validate = (params: ValidateParams): T => { const { payload, schema, envelope, formats, externalRefs, ajv } = params; const ajvInstance = ajv || new Ajv({ allErrors: true }); @@ -14,16 +129,16 @@ export function validate(params: ValidateParams): T { } if (externalRefs) { - for (const refSchema of externalRefs) { - ajvInstance.addSchema(refSchema); - } + ajvInstance.addSchema(externalRefs); } let validateFn: ValidateFunction; try { validateFn = ajvInstance.compile(schema); } catch (error) { - throw new SchemaValidationError('Failed to compile schema', error); + throw new SchemaCompilationError('Failed to compile schema', { + cause: error, + }); } const trimmedEnvelope = envelope?.trim(); @@ -33,11 +148,12 @@ export function validate(params: ValidateParams): T { const valid = validateFn(dataToValidate); if (!valid) { - throw new SchemaValidationError( - 'Schema validation failed', - validateFn.errors - ); + throw new SchemaValidationError('Schema validation failed', { + cause: validateFn.errors, + }); } return dataToValidate as T; -} +}; + +export { validate }; diff --git a/packages/validation/tests/unit/decorator.test.ts b/packages/validation/tests/unit/decorator.test.ts index 94ba2f0c40..b2b6df0562 100644 --- a/packages/validation/tests/unit/decorator.test.ts +++ b/packages/validation/tests/unit/decorator.test.ts @@ -20,7 +20,7 @@ const outboundSchema = { additionalProperties: false, }; -describe('validator decorator', () => { +describe('Decorator: validator', () => { it('should validate inbound and outbound successfully', async () => { // Prepare class TestClass { @@ -31,8 +31,10 @@ describe('validator decorator', () => { } const instance = new TestClass(); const input = { value: 5 }; + // Act const output = await instance.multiply(input); + // Assess expect(output).toEqual({ result: 10 }); }); @@ -49,6 +51,7 @@ describe('validator decorator', () => { const invalidInput = { value: 'not a number' } as unknown as { value: number; }; + // Act & Assess await expect(instance.multiply(invalidInput)).rejects.toThrow( SchemaValidationError @@ -59,14 +62,14 @@ describe('validator decorator', () => { // Prepare class TestClassInvalid { @validator({ inboundSchema, outboundSchema }) - async multiply(input: { value: number }): Promise<{ result: number }> { - return { result: 'invalid' } as unknown as { result: number }; + async multiply(_input: { value: number }) { + return { result: 'invalid' }; } } const instance = new TestClassInvalid(); - const input = { value: 5 }; + // Act & Assess - await expect(instance.multiply(input)).rejects.toThrow( + await expect(instance.multiply({ value: 5 })).rejects.toThrow( SchemaValidationError ); }); @@ -81,23 +84,12 @@ describe('validator decorator', () => { } const instance = new TestClassNoOp(); const data = { foo: 'bar' }; + // Act const result = await instance.echo(data); - // Assess - expect(result).toEqual(data); - }); - it('should return descriptor unmodified if descriptor.value is undefined', () => { - // Prepare - const descriptor: PropertyDescriptor = {}; - // Act - const result = validator({ inboundSchema })( - null as unknown as object, - 'testMethod', - descriptor - ); // Assess - expect(result).toEqual(descriptor); + expect(result).toEqual(data); }); it('should validate inbound only', async () => { @@ -110,8 +102,10 @@ describe('validator decorator', () => { } const instance = new TestClassInbound(); const input = { value: 10 }; + // Act const output = await instance.process(input); + // Assess expect(output).toEqual({ data: JSON.stringify(input) }); }); @@ -126,8 +120,10 @@ describe('validator decorator', () => { } const instance = new TestClassOutbound(); const input = { text: 'hello' }; + // Act const output = await instance.process(input); + // Assess expect(output).toEqual({ result: 42 }); }); diff --git a/packages/validation/tests/unit/index.test.ts b/packages/validation/tests/unit/index.test.ts deleted file mode 100644 index e2af63bd92..0000000000 --- a/packages/validation/tests/unit/index.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { SchemaValidationError, validate } from '../../src/index.js'; - -describe('Index exports', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should export validate as a function', () => { - // Act & Assess - expect(typeof validate).toBe('function'); - }); - - it('should export SchemaValidationError as a function', () => { - // Act & Assess - expect(typeof SchemaValidationError).toBe('function'); - }); -}); diff --git a/packages/validation/tests/unit/middleware.test.ts b/packages/validation/tests/unit/middleware.test.ts index 204c61aadb..1d2796638a 100644 --- a/packages/validation/tests/unit/middleware.test.ts +++ b/packages/validation/tests/unit/middleware.test.ts @@ -1,7 +1,8 @@ import middy from '@middy/core'; +import type { Context } from 'aws-lambda'; import { describe, expect, it } from 'vitest'; import { SchemaValidationError } from '../../src/errors.js'; -import { validation } from '../../src/middleware.js'; +import { validator } from '../../src/middleware.js'; const inboundSchema = { type: 'object', @@ -21,53 +22,71 @@ const outboundSchema = { additionalProperties: false, }; -const response = { outputValue: 20 }; -const baseHandler = async (event: unknown) => { - return response; +const baseHandler = async (event: { inputValue: unknown }) => { + return { + outputValue: event.inputValue, + }; }; -describe('validation middleware with Middy', () => { - it('should validate inbound and outbound successfully', async () => { +describe('Middleware: validator', () => { + it('validates both inbound and outbound successfully', async () => { // Prepare - const middleware = validation({ inboundSchema, outboundSchema }); - const wrappedHandler = middy(baseHandler).use(middleware); - const event = { inputValue: 10 }; + const handler = middy(baseHandler).use( + validator({ inboundSchema, outboundSchema }) + ); + // Act - const result = await wrappedHandler(event); + const result = await handler({ inputValue: 10 }, {} as Context); + // Assess - expect(result).toEqual(response); + expect(result).toEqual({ outputValue: 10 }); }); - it('should throw error on inbound validation failure', async () => { + it('throws an error on inbound validation failure', async () => { // Prepare - const middleware = validation({ inboundSchema }); - const wrappedHandler = middy(baseHandler).use(middleware); - const invalidEvent = { inputValue: 'invalid' }; + const handler = middy(baseHandler).use(validator({ inboundSchema })); + // Act & Assess - await expect(wrappedHandler(invalidEvent)).rejects.toThrow( - SchemaValidationError + await expect( + handler({ inputValue: 'invalid' }, {} as Context) + ).rejects.toThrow( + new SchemaValidationError('Inbound schema validation failed', { + cause: [ + expect.objectContaining({ + keyword: 'type', + message: 'must be number', + }), + ], + }) ); }); - it('should throw error on outbound validation failure', async () => { - const invalidHandler = async (_event: unknown) => { - return { outputValue: 'invalid' }; - }; - const middleware = validation({ outboundSchema }); - const wrappedHandler = middy(invalidHandler).use(middleware); - const event = { any: 'value' }; + it('throws an error on outbound validation failure', async () => { + const handler = middy(() => { + return 'invalid output'; + }).use(validator({ outboundSchema })); + // Act & Assess - await expect(wrappedHandler(event)).rejects.toThrow(SchemaValidationError); + await expect(handler({ inputValue: 10 }, {} as Context)).rejects.toThrow( + new SchemaValidationError('Outbound schema validation failed', { + cause: [ + expect.objectContaining({ + keyword: 'type', + message: 'must be object', + }), + ], + }) + ); }); - it('should no-op when no schemas are provided', async () => { + it('skips validation when no schemas are provided', async () => { // Prepare - const middleware = validation({}); - const wrappedHandler = middy(baseHandler).use(middleware); - const event = { anyKey: 'anyValue' }; + const handler = middy(baseHandler).use(validator({})); + // Act - const result = await wrappedHandler(event); + const result = await handler({ inputValue: 'bar' }, {} as Context); + // Assess - expect(result).toEqual(response); + expect(result).toEqual({ outputValue: 'bar' }); }); }); diff --git a/packages/validation/tests/unit/validate.test.ts b/packages/validation/tests/unit/validate.test.ts index b4480f580e..c1e46cd1ca 100644 --- a/packages/validation/tests/unit/validate.test.ts +++ b/packages/validation/tests/unit/validate.test.ts @@ -1,6 +1,9 @@ import Ajv from 'ajv'; import { describe, expect, it } from 'vitest'; -import { SchemaValidationError } from '../../src/errors.js'; +import { + SchemaCompilationError, + SchemaValidationError, +} from '../../src/errors.js'; import type { ValidateParams } from '../../src/types.js'; import { validate } from '../../src/validate.js'; @@ -17,8 +20,7 @@ describe('validate function', () => { required: ['name', 'age'], additionalProperties: false, }; - - const params: ValidateParams = { payload, schema }; + const params: ValidateParams = { payload, schema }; // Act const result = validate(params); @@ -75,22 +77,23 @@ describe('validate function', () => { it('uses provided ajv instance and custom formats', () => { // Prepare - const payload = { email: 'test@example.com' }; + const payload = { email: 'test@example.com', region: 'us-east-1' }; const schema = { type: 'object', properties: { email: { type: 'string', format: 'custom-email' }, + region: { type: 'string', format: 'allowedRegions' }, }, - required: ['email'], + required: ['email', 'region'], additionalProperties: false, }; const ajvInstance = new Ajv({ allErrors: true }); const formats = { 'custom-email': { - type: 'string', validate: (email: string) => email.includes('@'), }, + allowedRegions: /^(us-east-1|us-west-1)$/, }; const params: ValidateParams = { @@ -149,7 +152,7 @@ describe('validate function', () => { expect(result).toEqual(payload); }); - it('throws SchemaValidationError when schema compilation fails', () => { + it('throws the correct error when schema compilation fails', () => { // Prepare const payload = { name: 'John' }; const schema = { @@ -162,6 +165,6 @@ describe('validate function', () => { const params: ValidateParams = { payload, schema }; // Act & Assess - expect(() => validate(params)).toThrow(SchemaValidationError); + expect(() => validate(params)).toThrow(SchemaCompilationError); }); }); diff --git a/packages/validation/typedoc.json b/packages/validation/typedoc.json new file mode 100644 index 0000000000..3032e9d15e --- /dev/null +++ b/packages/validation/typedoc.json @@ -0,0 +1,13 @@ +{ + "extends": [ + "../../typedoc.base.json" + ], + "entryPoints": [ + "./src/types.ts", + "./src/validate.ts", + "./src/middleware.ts", + "./src/decorator.ts", + "./src/errors.ts", + ], + "readme": "README.md" +} \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts index e8056e9a46..78c6de7f3e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,20 +13,14 @@ export default defineConfig({ include: ['packages/*/src/**'], exclude: [ ...coverageConfigDefaults.exclude, - 'packages/batch/src/types.ts', - 'packages/commons/src/types/**', - 'packages/event-handler/src/types/**', - 'packages/idempotency/src/types/**', - 'packages/jmespath/src/types.ts', - 'packages/logger/src/types/**', - 'packages/metrics/src/types/**', - 'packages/parameters/src/types/**', - 'packages/parser/src/types/**', 'layers/**', + 'packages/*/src/types/**', + 'packages/*/src/types.ts', 'packages/testing/**', - 'packages/tracer/src/types/**', ], }, setupFiles: ['./packages/testing/src/setupEnv.ts'], + hookTimeout: 1_000 * 60 * 10, // 10 minutes + testTimeout: 1_000 * 60 * 3, // 3 minutes }, });